Claude
Skills
Sign in
Back

whatcable-macos-usb-inspector

Included with Lifetime
$97 forever

macOS menu bar app that identifies USB-C cable capabilities and charging diagnostics using IOKit

General

What this skill does


# WhatCable macOS USB-C Inspector

> Skill by [ara.so](https://ara.so) — Daily 2026 Skills collection.

WhatCable is a macOS 14+ menu bar app (Swift/SwiftUI) that reads IOKit services to surface what each USB-C cable plugged into your Mac can actually do — speed, power rating, e-marker data, PDO profiles, and connected device identity — in plain English.

## Project Structure

```
Sources/WhatCable/
├── WhatCableApp.swift          # App entry point, menu bar setup
├── ContentView.swift           # Main popover UI
├── PortSummary.swift           # Plain-English logic per port
├── PDVDO.swift                 # PD VDO bit-twiddling / spec decoding
├── IOKitReader.swift           # IOKit service queries
└── ...
scripts/
└── build-app.sh                # Universal binary + notarisation
```

## Install / Build

```bash
# Run locally (development)
swift run WhatCable

# Build distributable universal app (arm64 + x86_64)
./scripts/build-app.sh
# → dist/WhatCable.app
# → dist/WhatCable.zip
```

Requires Swift 5.9 / Xcode 15+, macOS 14 (Sonoma) or later.

### Signed + Notarised Build

```bash
cp .env.example .env
# Edit .env:
# DEVELOPER_ID="Developer ID Application: Your Name (TEAMID)"
# NOTARY_PROFILE="WhatCable-notary"

# Store notarytool credentials once
xcrun notarytool store-credentials "WhatCable-notary" \
    --apple-id "$APPLE_ID" \
    --team-id "$TEAM_ID" \
    --password "$APP_SPECIFIC_PASSWORD"

./scripts/build-app.sh
```

## Key Concepts

### IOKit Service Families

| Service | Purpose |
|---|---|
| `AppleHPMInterfaceType10/11/12` (M3+) | Per-port state, transports, plug orientation, e-marker presence |
| `AppleTCControllerType10` (M1/M2) | Same, older HPM interface |
| `IOPortFeaturePowerSource` | Full PDO list + live negotiated PDO |
| `IOPortTransportComponentCCUSBPDSOP` | PD Discover Identity VDOs (SOP = partner, SOP' = cable e-marker) |

### USB PD VDO Decoding (PDVDO.swift)

The core bit-twiddling follows USB Power Delivery 3.x spec. Cable e-marker VDOs encode speed and current in specific bit fields.

```swift
// Example: decode cable speed from Passive Cable VDO
struct PassiveCableVDO {
    let raw: UInt32

    // Bits [5:3] — USB SuperSpeed signalling support
    var usbSSSignalling: UInt8 {
        UInt8((raw >> 3) & 0b111)
    }

    var dataRate: String {
        switch usbSSSignalling {
        case 0b000: return "USB 2.0 only"
        case 0b001: return "USB 3.2 Gen 1 (5 Gbps)"
        case 0b010: return "USB 3.2 Gen 2 (10 Gbps)"
        case 0b011: return "USB 3.2 Gen 2x2 / USB4 Gen 2 (20 Gbps)"
        case 0b100: return "USB4 Gen 3 (40 Gbps)"
        default:    return "Unknown"
        }
    }

    // Bits [6:5] — VBUS current handling
    var currentCapability: String {
        switch (raw >> 5) & 0b11 {
        case 0b00: return "USB Type-C Default (≤3A)"
        case 0b01: return "3A"
        case 0b10: return "5A"
        default:   return "Reserved"
        }
    }
}
```

### Reading IOKit Properties

```swift
import IOKit

func readPortProperties(serviceName: String) -> [String: Any]? {
    var iterator: io_iterator_t = 0
    let matchDict = IOServiceMatching(serviceName)
    guard IOServiceGetMatchingServices(kIOMainPortDefault,
                                       matchDict, &iterator) == KERN_SUCCESS else {
        return nil
    }
    defer { IOObjectRelease(iterator) }

    var service = IOIteratorNext(iterator)
    var results: [String: Any] = [:]
    while service != 0 {
        defer {
            IOObjectRelease(service)
            service = IOIteratorNext(iterator)
        }
        if let props = copyProperties(service) {
            results.merge(props) { _, new in new }
        }
    }
    return results.isEmpty ? nil : results
}

private func copyProperties(_ service: io_service_t) -> [String: Any]? {
    var propsRef: Unmanaged<CFMutableDictionary>?
    guard IORegistryEntryCreateCFProperties(service, &propsRef,
                                             kCFAllocatorDefault, 0) == KERN_SUCCESS,
          let props = propsRef?.takeRetainedValue() as? [String: Any] else {
        return nil
    }
    return props
}
```

### Port Summary Plain-English Logic (PortSummary.swift)

```swift
enum PortHeadline: String {
    case thunderbolt    = "Thunderbolt / USB4"
    case usbDevice      = "USB device connected"
    case chargingOnly   = "Charging only"
    case slowCable      = "Slow USB / charge-only cable"
    case nothing        = "Nothing connected"
}

struct PortSummary {
    let headline: PortHeadline
    let chargingDiagnostic: String?
    let dataRate: String?
    let currentRating: String?
    let negotiatedPDO: PDO?
    let allPDOs: [PDO]

    static func from(ioKitProps: [String: Any]) -> PortSummary {
        let hasThunderbolt = ioKitProps["Thunderbolt"] as? Bool ?? false
        let hasUSB3       = ioKitProps["USB3"] as? Bool ?? false
        let isConnected   = ioKitProps["Connected"] as? Bool ?? false
        let emarkerPresent = ioKitProps["CableEMarker"] as? Bool ?? false

        let headline: PortHeadline
        if !isConnected {
            headline = .nothing
        } else if hasThunderbolt {
            headline = .thunderbolt
        } else if hasUSB3 {
            headline = .usbDevice
        } else if emarkerPresent {
            headline = .chargingOnly
        } else {
            headline = .slowCable
        }

        // Parse PDOs for charging diagnostic
        let pdos = parsePDOs(from: ioKitProps)
        let negotiated = pdos.first(where: { $0.isActive })
        let diagnostic = buildChargingDiagnostic(pdos: pdos, negotiated: negotiated)

        return PortSummary(
            headline: headline,
            chargingDiagnostic: diagnostic,
            dataRate: emarkerPresent ? decodeDataRate(ioKitProps) : nil,
            currentRating: emarkerPresent ? decodeCurrentRating(ioKitProps) : nil,
            negotiatedPDO: negotiated,
            allPDOs: pdos
        )
    }
}
```

### PDO Parsing (Power Data Objects)

```swift
struct PDO {
    let voltage: Double    // Volts
    let maxCurrent: Double // Amps
    let maxWatts: Double   // voltage * maxCurrent
    let isActive: Bool

    static func parse(raw: UInt32, isActive: Bool) -> PDO? {
        // Fixed supply PDO: bits [19:10] = max current (10mA units),
        //                   bits [29:20] = voltage (50mV units)
        let currentRaw = (raw >> 10) & 0x3FF
        let voltageRaw = (raw >> 20) & 0x3FF
        guard voltageRaw > 0 else { return nil }

        let voltage = Double(voltageRaw) * 0.05
        let current = Double(currentRaw) * 0.01
        return PDO(
            voltage: voltage,
            maxCurrent: current,
            maxWatts: voltage * current,
            isActive: isActive
        )
    }
}

func parsePDOs(from props: [String: Any]) -> [PDO] {
    guard let pdoArray = props["PDOs"] as? [UInt32],
          let activePDOIndex = props["ActivePDOIndex"] as? Int else {
        return []
    }
    return pdoArray.enumerated().compactMap { idx, raw in
        PDO.parse(raw: raw, isActive: idx == activePDOIndex)
    }
}
```

### SwiftUI Popover Pattern (ContentView.swift)

```swift
import SwiftUI

struct ContentView: View {
    @StateObject private var model = CableModel()

    var body: some View {
        VStack(alignment: .leading, spacing: 0) {
            HeaderView()
            Divider()
            ScrollView {
                VStack(alignment: .leading, spacing: 12) {
                    ForEach(model.ports) { port in
                        PortRowView(port: port)
                    }
                }
                .padding()
            }
        }
        .frame(width: 360)
        .onAppear { model.refresh() }
    }
}

struct PortRowView: View {
    let port: PortSummary

    var body: some View {
        VStack(alignment: .leading, spacing: 4) {
            Label(port.headline.rawValue, systemImage: iconName(for: port.headline))
                .font(.headline)

          

Related in General