import Foundation /// Plain-English interpretation of a USBCPort's raw IOKit data. public struct PortSummary { public enum Status { case empty case charging case dataDevice case thunderboltCable case displayCable case unknown } public let status: Status public let headline: String public let subtitle: String public let bullets: [String] public init(status: Status, headline: String, subtitle: String, bullets: [String]) { self.status = status self.headline = headline self.subtitle = subtitle self.bullets = bullets } } extension PortSummary { /// - Parameter isConnectedOverride: Pass `true`/`false` to bypass the /// `port.connectionActive` flag. The menu-bar UI sets this from a live /// union of the device/power/PD watchers because some Apple-silicon /// controllers (notably AppleHPMInterfaceType11 / MagSafe) hold /// ConnectionActive=true for several seconds after unplug, which left /// the UI showing a phantom "Connected" card. Pass `nil` (the default) /// to fall back to `port.connectionActive` for callers that don't /// track the live signals (CLI / JSON snapshots). public init( port: USBCPort, sources: [PowerSource] = [], identities: [PDIdentity] = [], devices: [USBDevice] = [], thunderboltSwitches: [ThunderboltSwitch] = [], isConnectedOverride: Bool? = nil ) { let connected = isConnectedOverride ?? (port.connectionActive == true) let active = port.transportsActive let supported = port.transportsSupported let hasUSB3 = active.contains("USB3") || port.superSpeedActive == true let hasUSB2 = active.contains("USB2") let hasTB = active.contains("CIO") // Thunderbolt = Converged I/O let hasDP = active.contains("DisplayPort") // Configuration Channel: required for USB-PD. Without CC the OS cannot // run Discover Identity, so we can't infer anything about the cable's // e-marker. M4 Mac Mini front USB-C ports are an example: they hang // off a plain xHCI controller (no PD), so reporting "basic cable" on // them wrongly blames the cable. See issue #50. let pdCapable = supported.contains("CC") // E-marker presence is "did the cable respond to Discover Identity?", // which means we have an SOP'/SOP'' PDIdentity for this port. The // port's `ActiveCable` IOKit flag means "this cable contains active // signal-conditioning electronics", which is unrelated: passive // cables (including high-end USB4 / 240W EPR cables) carry e-markers // too. let hasEmarker = identities.contains { $0.endpoint == .sopPrime || $0.endpoint == .sopDoublePrime } let portLabel = port.portDescription ?? port.serviceName if !connected { self.status = .empty self.headline = String(localized: "Nothing connected", bundle: .module) self.subtitle = String(localized: "Plug a cable into \(portLabel) to see what it can do.", bundle: .module) self.bullets = [] return } var bullets: [String] = [] // Bullets are grouped by the question the user is mentally asking, // so related facts sit next to each other: // // A. What's happening on this port and what's plugged in? // - link speed / Thunderbolt link // - DisplayPort note // - connected device // B. What does the cable advertise? // - e-marker presence // - cable speed and power rating // - active-cable details (medium, element, isolation) // - port-level optical flag // - cable maker // C. What does the power negotiation look like? // - charger max // - currently negotiated PDO // ------------------------------------------------------------ // A. Live link / what's plugged in // ------------------------------------------------------------ if hasTB { // If we have a matching Thunderbolt switch graph for this port, // emit specific link-state bullets (negotiated speed, lane // count, daisy-chain info). Otherwise fall back to the generic // "active" line so older paths still work. let tbBullets = thunderboltBullets(for: port, switches: thunderboltSwitches) if tbBullets.isEmpty { bullets.append(String(localized: "Thunderbolt / USB4 link active", bundle: .module)) } else { bullets.append(contentsOf: tbBullets) } } else if hasUSB3 { bullets.append(String(localized: "SuperSpeed USB (5 Gbps or faster)", bundle: .module)) } else if hasUSB2 { bullets.append(String(localized: "USB 2.0 only (480 Mbps), no high-speed data", bundle: .module)) } if hasDP { bullets.append(String(localized: "Carrying DisplayPort video", bundle: .module)) } // Partner identity (SOP): what's connected. if let partner = identities.first(where: { $0.endpoint == .sop }), let header = partner.idHeader { let kind = header.ufpProductType != .undefined ? header.ufpProductType.label : header.dfpProductType.label let vendor = VendorDB.label(for: partner.vendorID) bullets.append(String(localized: "Connected device: \(kind), \(vendor)", bundle: .module)) } // ------------------------------------------------------------ // B. The cable // ------------------------------------------------------------ // E-marker presence. The whole cable-details bullet only makes // sense on USB-C, where the user can swap cables and might wonder // why details are missing. On MagSafe the cable is part of the // brick (and MagSafe absolutely does negotiate Power Delivery, // just over its own pins, not the CC line we test for // `pdCapable`), so don't emit any "no e-marker" wording there. let isMagSafe = port.portTypeDescription?.hasPrefix("MagSafe") == true if hasEmarker { bullets.append(String(localized: "Cable has an e-marker chip (advertises its capabilities)", bundle: .module)) } else if !active.isEmpty && !isMagSafe { if pdCapable { bullets.append(String(localized: "Cable does not advertise an e-marker (basic cable)", bundle: .module)) } else { bullets.append(String(localized: "This port can't read cable details (USB-only port, no Power Delivery)", bundle: .module)) } } // Cable e-marker (SOP'): the cable's own capabilities. let cableEmarker = identities.first(where: { $0.endpoint == .sopPrime || $0.endpoint == .sopDoublePrime }) if let cable = cableEmarker, let cv = cable.cableVDO { let speedLabel = cv.speed.label bullets.append(String(localized: "Cable speed: \(speedLabel)", bundle: .module)) let currentLabel = cv.current.label let maxVolts = cv.maxVolts let maxWatts = cv.maxWatts bullets.append(String(localized: "Cable rated for \(currentLabel) at up to \(maxVolts)V (~\(maxWatts)W)", bundle: .module)) if cv.cableType == .active { if let v2 = cable.activeCableVDO2 { let medium = v2.physicalConnection.label.lowercased() let element = v2.activeElement.label.lowercased() bullets.append(String(localized: "Active \(medium) cable, \(element)", bundle: .module)) if v2.physicalConnection == .optical { if v2.opticallyIsolated { bullets.append(String(localized: "Optical fibres are electrically isolated end-to-end", bundle: .module)) } else { bullets.append(String(localized: "Optical cable, not electrically isolated (carries copper alongside the fibres)", bundle: .module)) } } } else { bullets.append(String(localized: "Active cable (contains signal-conditioning electronics)", bundle: .module)) } } } // Port-level optical flag. Independent of the e-marker's claim; // kept on its own line for now so users can see both signals. if port.opticalCable == true { bullets.append(String(localized: "Optical cable", bundle: .module)) } // Cable e-marker vendor (SOP'): who made the cable. if let cable = cableEmarker, cable.vendorID != 0 { let vendor = VendorDB.label(for: cable.vendorID) bullets.append(String(localized: "Cable made by \(vendor)", bundle: .module)) } // ------------------------------------------------------------ // C. Charging numbers // ------------------------------------------------------------ // Power summary from PD or MagSafe power sources. let chargingSource = PowerSource.preferredChargingSource(in: sources) if let chargingSource { let maxW = Int((Double(chargingSource.maxPowerMW) / 1000).rounded()) let hasOptions = !chargingSource.options.isEmpty if hasOptions && maxW > 0 { bullets.append(String(localized: "Charger advertises up to \(maxW)W", bundle: .module)) } if let win = chargingSource.winning { let volts = win.voltsLabel let amps = win.ampsLabel let watts = win.wattsLabel bullets.append(String(localized: "Currently negotiated: \(volts) @ \(amps) (\(watts))", bundle: .module)) } } // Headline + status // Only show a wattage suffix if we have a real number (>0 and we have // options, not just the winning PDO). let chargerW: Int? = { guard let chargingSource, !chargingSource.options.isEmpty else { return nil } let w = Int((Double(chargingSource.maxPowerMW) / 1000).rounded()) return w > 0 ? w : nil }() // Cable limit suffix: only emitted when the cable's e-marker // reports a maxWatts that is strictly less than what the charger // advertises. The diagnostic banner already explains this in // detail when a cable is plugged in; the headline suffix is the // at-a-glance equivalent so the user can spot a cable mismatch // without reading further. let cableLimitSuffix: String = { guard let chargerW, let cableW = cableEmarker?.cableVDO?.maxWatts, cableW > 0, cableW < chargerW else { return "" } return String(localized: " · \(cableW)W cable", bundle: .module) }() if hasTB { self.status = .thunderboltCable if let w = chargerW { self.headline = String(localized: "Thunderbolt / USB4 · \(w)W charger", bundle: .module) + cableLimitSuffix } else { self.headline = String(localized: "Thunderbolt / USB4", bundle: .module) + cableLimitSuffix } self.subtitle = subtitleForCapabilities(usb3: true, dp: hasDP, emarker: hasEmarker) } else if hasUSB3 && hasDP { self.status = .displayCable if let w = chargerW { self.headline = String(localized: "USB-C with video · \(w)W charger", bundle: .module) + cableLimitSuffix } else { self.headline = String(localized: "USB-C with video", bundle: .module) + cableLimitSuffix } self.subtitle = String(localized: "Carrying both data and DisplayPort video.", bundle: .module) } else if hasDP { self.status = .displayCable if let w = chargerW { self.headline = String(localized: "Display connected · \(w)W charger", bundle: .module) + cableLimitSuffix } else { self.headline = String(localized: "Display connected", bundle: .module) + cableLimitSuffix } self.subtitle = String(localized: "DisplayPort video over USB-C alt mode.", bundle: .module) } else if hasUSB3 { self.status = .dataDevice if let w = chargerW { self.headline = String(localized: "USB device · \(w)W charger", bundle: .module) + cableLimitSuffix } else { self.headline = String(localized: "USB device", bundle: .module) + cableLimitSuffix } self.subtitle = String(localized: "SuperSpeed data link is active.", bundle: .module) } else if hasUSB2 && !hasUSB3 { self.status = .dataDevice if let w = chargerW { self.headline = String(localized: "Slow USB device or charge-only cable · \(w)W charger", bundle: .module) + cableLimitSuffix } else { self.headline = String(localized: "Slow USB device or charge-only cable", bundle: .module) + cableLimitSuffix } self.subtitle = String(localized: "Only USB 2.0 is active. If you expected high speed, the cable may not support it.", bundle: .module) } else if chargingSource != nil { self.status = .charging if let w = chargerW { self.headline = String(localized: "Charging · \(w)W charger", bundle: .module) + cableLimitSuffix } else { self.headline = String(localized: "Charging", bundle: .module) + cableLimitSuffix } self.subtitle = String(localized: "Power is flowing. No data connection.", bundle: .module) } else if active.isEmpty && supported.contains("USB2") { self.status = .charging self.headline = String(localized: "Charging only", bundle: .module) self.subtitle = String(localized: "Power is flowing but no data link is established.", bundle: .module) } else { self.status = .unknown self.headline = String(localized: "Connected", bundle: .module) self.subtitle = String(localized: "Couldn't determine cable type from this port.", bundle: .module) } self.bullets = bullets } } /// Build the TB-specific bullets for a port whose `transportsActive` /// includes `"CIO"`. Returns an empty array if we can't find a matching /// switch (e.g. the port doesn't have an `@N` suffix, or the Thunderbolt /// watcher hasn't populated yet). Caller falls back to a generic bullet /// in that case. private func thunderboltBullets( for port: USBCPort, switches: [ThunderboltSwitch] ) -> [String] { guard !switches.isEmpty, let socketID = ThunderboltTopology.socketID(fromServiceName: port.serviceName), let root = ThunderboltTopology.hostRoot(forSocketID: socketID, in: switches) else { return [] } let chain = ThunderboltTopology.chain(from: root, in: switches) var bullets: [String] = [] // First-hop link state: the host root's downstream lane port describes // the cable's negotiated speed. if let hostPort = ThunderboltTopology.activeDownstreamLanePort(root), let label = ThunderboltLabels.linkLabel(for: hostPort) { // label is e.g. "Up to 20 Gb/s × 2" — replace the leading "Up" // with "up" for the bullet phrasing without lowercasing units. let linkSpeed = label.replacingOccurrences(of: "Up to", with: "up to") bullets.append(String(localized: "Linked at \(linkSpeed)", bundle: .module)) } // Connected-device line. Only meaningful when there's at least one // downstream switch. let downstream = chain.dropFirst() if !downstream.isEmpty { let names = downstream.map { ThunderboltLabels.deviceName(for: $0) } let hops = downstream.count let path = names.joined(separator: " → ") if hops == 1 { bullets.append(String(localized: "Connected to \(path)", bundle: .module)) } else { bullets.append(String(localized: "Connected via \(hops) hops: \(path)", bundle: .module)) } } // Step-down detection: only meaningful on real daisy-chains // (two or more downstream switches). On a single-hop link, the // host's downstream port and the device's upstream port describe // the SAME physical cable from opposite ends; the two readings can // disagree on lane count (the controller-side view aggregates lanes // that the device-side view doesn't), and that disagreement is not // a real step-down. With two or more hops, comparing the first link // (host -> device 1) to the last link (device N-1 -> device N) // genuinely contrasts two distinct cables. if downstream.count >= 2, let hostPort = ThunderboltTopology.activeDownstreamLanePort(root), let last = downstream.last, let lastLeg = ThunderboltTopology.activeDownstreamLanePort(last) ?? last.ports.first(where: { $0.adapterType.isLane && $0.hasActiveLink }), let stepLabel = stepDownLabel(host: hostPort, lastLeg: lastLeg) { bullets.append(stepLabel) } return bullets } /// If the last-leg link is slower than the host link (per-lane Gbps drop /// or lane count drop), describe the change. Returns nil for symmetric /// chains where every leg matches. private func stepDownLabel(host: ThunderboltPort, lastLeg: ThunderboltPort) -> String? { guard let hostLabel = ThunderboltLabels.linkLabel(for: host), let lastLabel = ThunderboltLabels.linkLabel(for: lastLeg) else { return nil } if hostLabel == lastLabel { return nil } let h = hostLabel.replacingOccurrences(of: "Up to", with: "up to") let l = lastLabel.replacingOccurrences(of: "Up to", with: "up to") return String(localized: "Last leg drops from \(h) to \(l)", bundle: .module) } private func subtitleForCapabilities(usb3: Bool, dp: Bool, emarker: Bool) -> String { var parts: [String] = [] if usb3 { parts.append(String(localized: "high-speed data", bundle: .module)) } if dp { parts.append(String(localized: "video", bundle: .module)) } if emarker { parts.append(String(localized: "smart cable", bundle: .module)) } if parts.isEmpty { return String(localized: "Connected.", bundle: .module) } let capabilities = parts.joined(separator: ", ") return String(localized: "Supports \(capabilities).", bundle: .module) }