feat: async C API and Swift radio state display

Split the dongle session into a fast probe (xone_open) and a background
start (xone_start) that loads the firmware and initializes the radio on
a worker thread, so the app UI never blocks. Add xone_state/xone_error
for progress display, and select the firmware image per product ID when
no path is given. The Swift app polls the state each second and shows
idle/starting/ready/error plus the firmware build or error string.

The session destructor joins the worker so quitting the app does not
terminate on a joinable thread. Note in the CLI and transport that the
recover/re-enumerate path should not be used yet: a crashed SIE drops
off the bus, while the MCU watchdog recovers it after ~90s of quiet.

Co-Authored-By: qwen3.8-27b@q2_k_xl: async C API, Swift state display, and exit-time worker join
This commit is contained in:
portersky
2026-08-17 19:57:40 +02:00
parent c9ea1a6919
commit b3e747b900
7 changed files with 174 additions and 30 deletions
+28 -2
View File
@@ -27,6 +27,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
struct ContentView: View {
@State private var donglePresent = false
@State private var session: OpaquePointer? = nil
// Radio state polled from the background worker each tick; a change here
// is what re-renders the debug section.
@State private var radioState = XONE_STATE_IDLE
private let timer = Timer.publish(every: 1.0, on: .main, in: .common)
.autoconnect()
@@ -69,10 +72,18 @@ struct ContentView: View {
.onReceive(timer) { _ in
let present = xone_dongle_present()
if present && session == nil {
session = xone_open("firmware/xow_dongle.bin")
// Probe is fast; firmware load + radio init run on a
// background thread so the UI never blocks.
if let s = xone_open() {
session = s
xone_start(s, nil)
radioState = Int(xone_state(s))
}
} else if !present, let s = session {
xone_close(s)
session = nil
} else if let s = session {
radioState = Int(xone_state(s))
}
donglePresent = present
}
@@ -85,7 +96,12 @@ struct ContentView: View {
debugRow("PID", String(format: "0x%04X", xone_pid(session)))
debugRow("Chip ID", String(format: "0x%04X", xone_chip_id(session)))
debugRow("MAC", String(cString: xone_mac_address(session)))
debugRow("Firmware", firmwareBuildText(session))
debugRow("State", stateText(radioState))
if radioState == XONE_STATE_READY {
debugRow("Firmware", firmwareBuildText(session))
} else if radioState == XONE_STATE_ERROR {
debugRow("Error", String(cString: xone_error(session)))
}
}
}
@@ -99,6 +115,16 @@ struct ContentView: View {
}
}
private func stateText(_ state: Int) -> String {
switch state {
case XONE_STATE_IDLE: return "idle"
case XONE_STATE_STARTING: return "starting..."
case XONE_STATE_READY: return "ready"
case XONE_STATE_ERROR: return "error"
default: return "unknown"
}
}
private func firmwareBuildText(_ session: OpaquePointer) -> String {
let build = String(cString: xone_firmware_build(session))
return build.isEmpty ? "not loaded" : build