diff --git a/ControlBooth/Services/USBTree.swift b/ControlBooth/Services/USBTree.swift index 36f4c9f..74b6ea9 100644 --- a/ControlBooth/Services/USBTree.swift +++ b/ControlBooth/Services/USBTree.swift @@ -22,6 +22,9 @@ nonisolated struct USBTreeRow: Identifiable, Equatable, Codable { /// True when the hub it is plugged into can switch this port's power on /// its own (uhubctl's `ppps`), so this is something that can be power cycled. var selectable: Bool + /// True for a hub uhubctl lists as `ppps`: it can switch each of its own + /// ports' power. False for other hubs and for devices that aren't hubs. + var switchesPorts: Bool = false } /// Builds the USB tree from the IORegistry (`ioreg -a -p IOUSB -l`), marking @@ -86,7 +89,8 @@ nonisolated enum USBTree { isHub: isHub, isRTLSDR: isRTL, parentPath: parent?.path, parentUSBID: parent?.usbID, port: parent == nil ? nil : Int(path.split(separator: ".").last ?? ""), - selectable: parent?.switchesPorts ?? false)) + selectable: parent?.switchesPorts ?? false, + switchesPorts: isHub && hubs.contains { $0.location == path && $0.usbID == usbID && $0.switchesPerPort })) let hub = hubs.first { $0.location == path && $0.usbID == usbID } let me = Parent(path: path, usbID: usbID, switchesPorts: hub?.switchesPerPort ?? false) for child in children { walk(child, depth: depth + 1, parent: me, hubs: hubs, into: &rows) } diff --git a/ControlBooth/Views/USBTreeView.swift b/ControlBooth/Views/USBTreeView.swift index f2066b7..76f3adf 100644 --- a/ControlBooth/Views/USBTreeView.swift +++ b/ControlBooth/Views/USBTreeView.swift @@ -80,6 +80,15 @@ struct USBTreeView: View { if !row.serial.isEmpty { Text(row.serial).font(.caption.monospaced()).foregroundStyle(.secondary) } + if row.isHub { + Label(row.switchesPorts ? "per-port power" : "no per-port power", + systemImage: row.switchesPorts ? "bolt.fill" : "bolt.slash") + .font(.caption2) + .foregroundStyle(row.switchesPorts ? Color.green : .secondary) + .help(row.switchesPorts + ? "uhubctl can switch each port of this hub on its own (ppps)." + : "uhubctl can't switch this hub's ports individually (ganged, or not listed).") + } Spacer() if let port = row.port, let parent = row.parentPath { Text("\(parent) port \(port)").font(.caption).foregroundStyle(.secondary) diff --git a/ControlBoothTests/USBTreeTests.swift b/ControlBoothTests/USBTreeTests.swift index 51f465b..6bd224d 100644 --- a/ControlBoothTests/USBTreeTests.swift +++ b/ControlBoothTests/USBTreeTests.swift @@ -70,6 +70,13 @@ struct USBTreeTests { #expect(USBTree.rows(fromIORegPlist: plist, hubs: []).allSatisfy { !$0.selectable }) } + @Test func onlyPerPortSwitchableHubsAreFlagged() { + let rows = USBTree.rows(fromIORegPlist: plist, hubs: hubs) + // 2-2.1 is the only hub uhubctl lists as ppps; the others are ganged or devices. + #expect(rows.filter(\.switchesPorts).map(\.path) == ["2-2.1"]) + #expect(USBTree.rows(fromIORegPlist: plist, hubs: []).allSatisfy { !$0.switchesPorts }) + } + @Test func rtlsdrsBelowAPortLosePowerWithIt() throws { let rows = USBTree.rows(fromIORegPlist: plist, hubs: hubs) let mtt = try #require(rows.first { $0.path == "2-2.1.1" })