Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ControlBooth/ControlBoothApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ struct ControlBoothApp: App {
// receiver competing for port 5000.
guard !AppDelegate.isHostingUnitTests else { return }
store.seedDsdNeoScannerPipelineIfNeeded()
usbHubPower.refresh()
scheduler.reschedule(events: eventStore.events, pipelineStore: store, runner: runner)
airPlayReceiverService.setSettingsStore(airPlaySettingsStore)
airPlayReceiverService.applySettings(airPlaySettingsStore.settings)
Expand Down
4 changes: 3 additions & 1 deletion ControlBooth/Services/ScriptingCommands.swift
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,9 @@ private func usbHubPower(reportingTo command: NSScriptCommand) -> USBHubPowerCon

@MainActor
private func usbHubPowerJSON(_ controller: USBHubPowerController, refreshing: Bool) -> String {
if refreshing { controller.refresh() }
// A status without the tree still names the chosen port and the RTL-SDRs
// on it, which come from the tree.
if refreshing { controller.refresh() } else { controller.refreshIfStale() }
let encoder = JSONEncoder()
encoder.outputFormatting = [.sortedKeys]
let data = (try? encoder.encode(controller.status(includingTree: refreshing))) ?? Data("{}".utf8)
Expand Down
15 changes: 15 additions & 0 deletions ControlBooth/Services/USBHubPowerController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,23 @@ final class USBHubPowerController {

var canCycle: Bool { settings.isConfigured && cycler.isInstalled && !isCycling }

/// When the tree was last read; nil before the first read.
@ObservationIgnored private var lastRefresh: Date?

/// How long a read of the tree is good for when a status is asked for.
static let statusMaxAge: TimeInterval = 10

/// Reads the tree unless it was read within `maxAge` seconds, so a status
/// poll names the chosen port and its RTL-SDRs without launching ioreg and
/// uhubctl on every request.
func refreshIfStale(maxAge: TimeInterval = USBHubPowerController.statusMaxAge) {
if let lastRefresh, Date().timeIntervalSince(lastRefresh) < maxAge { return }
refresh()
}

/// Reads the tree and uhubctl's hub list again.
func refresh() {
lastRefresh = Date()
let cycler = self.cycler
do {
let hubs = cycler.isInstalled ? try cycler.listHubs() : []
Expand Down
16 changes: 16 additions & 0 deletions ControlBoothTests/USBTreeTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,20 @@ struct USBTreeTests {
// A path that merely starts with the same digits isn't below it.
#expect(!USBTree.devices(affectedBy: mtt, in: rows).contains { $0.path == "2-2.1.3" })
}

/// A status asked for before anything read the tree used to name the port by
/// its USB ID and list no RTL-SDRs.
@MainActor @Test func statusReadsTheTreeWhenItHasNone() {
let controller = USBHubPowerController(
settings: USBHubPowerSettings(enabled: true, hubLocation: "2-2.1", hubID: "045b:0209",
port: 1, portDeviceID: "1a40:0101"),
cycler: USBHubPowerCycler(executable: nil))
#expect(controller.status(includingTree: false).affectedRTLSDRs.isEmpty)
controller.refreshIfStale()
// The tree was read (whatever this machine has); a second ask within the
// age limit doesn't read it again.
let first = controller.rows
controller.refreshIfStale()
#expect(controller.rows == first)
}
}
Loading