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:
+1
-1
@@ -6,7 +6,7 @@ if(NOT CMAKE_GENERATOR MATCHES "^(Ninja|Xcode)$")
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
cmake_minimum_required(VERSION 3.21)
|
cmake_minimum_required(VERSION 3.21)
|
||||||
project(xone_macos VERSION 0.1.8 LANGUAGES CXX Swift)
|
project(xone_macos VERSION 0.1.9 LANGUAGES CXX Swift)
|
||||||
|
|
||||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||||
|
|
||||||
|
|||||||
+27
-8
@@ -20,19 +20,38 @@ const char *xone_version(void);
|
|||||||
// TRUE if an Xbox Wireless Dongle is connected, FALSE otherwise.
|
// TRUE if an Xbox Wireless Dongle is connected, FALSE otherwise.
|
||||||
bool xone_dongle_present(void);
|
bool xone_dongle_present(void);
|
||||||
|
|
||||||
// Opaque handle to an open dongle session (probe + firmware load).
|
// Opaque handle to an open dongle session. Create with xone_open(); only one
|
||||||
// Create with xone_open(); call the getters from the thread that opened
|
// session may be open at a time. The getters are safe to call from any thread.
|
||||||
// it, and before xone_close(). Only one session may be open at a time.
|
|
||||||
typedef struct xone_dongle xone_dongle;
|
typedef struct xone_dongle xone_dongle;
|
||||||
|
|
||||||
// Open a dongle session: probe, USB reset, and (if firmware_path is not
|
// Session states reported by xone_state().
|
||||||
// NULL) load the firmware image from that file. Returns NULL if no dongle
|
enum {
|
||||||
// is present or opening fails.
|
XONE_STATE_IDLE = 0, // Probed; no firmware loaded yet
|
||||||
xone_dongle *xone_open(const char *firmware_path);
|
XONE_STATE_STARTING, // Firmware load + radio init in progress
|
||||||
|
XONE_STATE_READY, // Radio initialized
|
||||||
|
XONE_STATE_ERROR // Last operation failed (see xone_error)
|
||||||
|
};
|
||||||
|
|
||||||
// Close the session and release the dongle.
|
// Open a dongle session: probe and USB reset. Fast; returns NULL if no dongle
|
||||||
|
// is present or opening fails. The EFUSE getters (xone_pid/xone_chip_id/
|
||||||
|
// xone_mac_address) are valid immediately after this.
|
||||||
|
xone_dongle *xone_open(void);
|
||||||
|
|
||||||
|
// Kick off firmware load + radio init on a background thread. Non-blocking:
|
||||||
|
// returns 0 once started, -1 if the session is busy or already ready. If
|
||||||
|
// firmware_path is NULL, the image is selected per product ID
|
||||||
|
// (firmware/xone_dongle_<pid>.bin).
|
||||||
|
int xone_start(xone_dongle *d, const char *firmware_path);
|
||||||
|
|
||||||
|
// Close the session and release the dongle. Waits for any in-progress start.
|
||||||
void xone_close(xone_dongle *d);
|
void xone_close(xone_dongle *d);
|
||||||
|
|
||||||
|
// Current session state (XONE_STATE_*).
|
||||||
|
int xone_state(const xone_dongle *d);
|
||||||
|
|
||||||
|
// Human-readable error from the last failed operation. Empty string if none.
|
||||||
|
const char *xone_error(const xone_dongle *d);
|
||||||
|
|
||||||
// Product ID of the connected dongle (e.g. 0x02E6).
|
// Product ID of the connected dongle (e.g. 0x02E6).
|
||||||
uint16_t xone_pid(const xone_dongle *d);
|
uint16_t xone_pid(const xone_dongle *d);
|
||||||
|
|
||||||
|
|||||||
@@ -78,6 +78,11 @@ public:
|
|||||||
// rescan). Host-side operation that works even when EP0 is wedged. Any
|
// rescan). Host-side operation that works even when EP0 is wedged. Any
|
||||||
// existing transport handles are invalidated; probe() again for a fresh
|
// existing transport handles are invalidated; probe() again for a fresh
|
||||||
// session. Returns 0 on success, -errno on failure.
|
// session. Returns 0 on success, -errno on failure.
|
||||||
|
//
|
||||||
|
// Do not use yet: when the chip's SIE is crashed it cannot finish the
|
||||||
|
// host-side port reset handshake and the device drops off the bus
|
||||||
|
// entirely. A crashed chip recovers on its own via the MCU watchdog after
|
||||||
|
// ~90s of a quiet bus, so prefer waiting over re-enumeration.
|
||||||
auto re_enumerate() -> int;
|
auto re_enumerate() -> int;
|
||||||
|
|
||||||
~transport();
|
~transport();
|
||||||
|
|||||||
+104
-15
@@ -3,7 +3,11 @@
|
|||||||
#include "app/xone_api.h"
|
#include "app/xone_api.h"
|
||||||
|
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
|
#include <cstring>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <mutex>
|
||||||
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
#include "common/log.hpp"
|
#include "common/log.hpp"
|
||||||
#include "mt76/mt76.hpp"
|
#include "mt76/mt76.hpp"
|
||||||
@@ -17,14 +21,39 @@
|
|||||||
struct xone_dongle {
|
struct xone_dongle {
|
||||||
std::unique_ptr<xone::usb::transport> transport;
|
std::unique_ptr<xone::usb::transport> transport;
|
||||||
std::unique_ptr<xone::mt76::chip> chip;
|
std::unique_ptr<xone::mt76::chip> chip;
|
||||||
|
// Guards state, firmware_build, and error (written by the start worker).
|
||||||
|
mutable std::mutex lock;
|
||||||
|
std::uint16_t pid = 0;
|
||||||
std::uint16_t chip_id = 0;
|
std::uint16_t chip_id = 0;
|
||||||
char mac_address[18] = {};
|
char mac_address[18] = {};
|
||||||
|
int state = XONE_STATE_IDLE;
|
||||||
|
char firmware_build[64] = {};
|
||||||
|
char error[256] = {};
|
||||||
|
// Firmware load + radio init runs here so the UI thread never blocks.
|
||||||
|
std::thread worker;
|
||||||
|
|
||||||
|
~xone_dongle()
|
||||||
|
{
|
||||||
|
// The session is destroyed at process exit even without an explicit
|
||||||
|
// xone_close(); joining here avoids terminating on a joinable thread.
|
||||||
|
if (worker.joinable())
|
||||||
|
worker.join();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
// Single dongle session (the app only ever has one).
|
// Single dongle session (the app only ever has one).
|
||||||
std::unique_ptr<xone_dongle> current_session;
|
std::unique_ptr<xone_dongle> current_session;
|
||||||
|
std::mutex session_mutex; // guards current_session
|
||||||
|
|
||||||
|
// Select the firmware image for a product ID: firmware/xone_dongle_<pid>.bin.
|
||||||
|
auto firmware_path_for(std::uint16_t pid) -> std::string
|
||||||
|
{
|
||||||
|
char path[64];
|
||||||
|
std::snprintf(path, sizeof(path), "firmware/xone_dongle_%04x.bin", pid);
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
@@ -38,8 +67,9 @@ extern "C" bool xone_dongle_present(void)
|
|||||||
return xone::usb::dongle_present();
|
return xone::usb::dongle_present();
|
||||||
}
|
}
|
||||||
|
|
||||||
extern "C" xone_dongle *xone_open(const char *firmware_path)
|
extern "C" xone_dongle *xone_open(void)
|
||||||
{
|
{
|
||||||
|
std::lock_guard<std::mutex> guard(session_mutex);
|
||||||
if (current_session)
|
if (current_session)
|
||||||
return nullptr;
|
return nullptr;
|
||||||
|
|
||||||
@@ -51,34 +81,92 @@ extern "C" xone_dongle *xone_open(const char *firmware_path)
|
|||||||
s->chip = std::make_unique<xone::mt76::chip>(*s->transport);
|
s->chip = std::make_unique<xone::mt76::chip>(*s->transport);
|
||||||
|
|
||||||
// EFUSE reads work before and after the firmware load.
|
// EFUSE reads work before and after the firmware load.
|
||||||
|
s->pid = s->transport->pid();
|
||||||
s->chip_id = s->chip->chip_id();
|
s->chip_id = s->chip->chip_id();
|
||||||
auto mac = s->chip->mac_address();
|
auto mac = s->chip->mac_address();
|
||||||
std::snprintf(s->mac_address, sizeof(s->mac_address), "%02x:%02x:%02x:%02x:%02x:%02x",
|
std::snprintf(s->mac_address, sizeof(s->mac_address), "%02x:%02x:%02x:%02x:%02x:%02x",
|
||||||
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
|
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
|
||||||
|
|
||||||
if (firmware_path) {
|
|
||||||
int ret = s->chip->load_firmware(firmware_path);
|
|
||||||
if (ret != 0)
|
|
||||||
xone::log_msg(xone::log_level::warn, "api: firmware load failed (%d)", ret);
|
|
||||||
else if (auto err = s->chip->init_radio(); err != 0)
|
|
||||||
xone::log_msg(xone::log_level::warn,
|
|
||||||
"api: radio init failed (%d)", err);
|
|
||||||
}
|
|
||||||
|
|
||||||
current_session = std::move(s);
|
current_session = std::move(s);
|
||||||
return current_session.get();
|
return current_session.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extern "C" int xone_start(xone_dongle *d, const char *firmware_path)
|
||||||
|
{
|
||||||
|
if (!d)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
// Select the image per product ID when no explicit path is given.
|
||||||
|
std::string path = firmware_path ? std::string(firmware_path) : firmware_path_for(d->pid);
|
||||||
|
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> guard(d->lock);
|
||||||
|
if (d->state != XONE_STATE_IDLE && d->state != XONE_STATE_ERROR)
|
||||||
|
return -1; // busy or already ready
|
||||||
|
d->state = XONE_STATE_STARTING;
|
||||||
|
d->error[0] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
// A previous failed attempt may have left a finished worker behind (retry
|
||||||
|
// from error); join it so the thread slot is reusable.
|
||||||
|
if (d->worker.joinable())
|
||||||
|
d->worker.join();
|
||||||
|
|
||||||
|
d->worker = std::thread([d, path] {
|
||||||
|
xone::log_msg(xone::log_level::info, "api: start: loading firmware %s", path.c_str());
|
||||||
|
int err = d->chip->load_firmware(path.c_str());
|
||||||
|
bool loaded = (err == 0);
|
||||||
|
if (loaded) {
|
||||||
|
xone::log_msg(xone::log_level::info, "api: start: firmware ok, init radio");
|
||||||
|
err = d->chip->init_radio();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> guard(d->lock);
|
||||||
|
if (!loaded) {
|
||||||
|
d->state = XONE_STATE_ERROR;
|
||||||
|
std::snprintf(d->error, sizeof(d->error), "firmware load failed (%d)", err);
|
||||||
|
xone::log_msg(xone::log_level::warn, "api: start: firmware load failed (%d)", err);
|
||||||
|
} else if (err != 0) {
|
||||||
|
d->state = XONE_STATE_ERROR;
|
||||||
|
std::snprintf(d->error, sizeof(d->error), "radio init failed (%d)", err);
|
||||||
|
xone::log_msg(xone::log_level::warn, "api: start: radio init failed (%d)", err);
|
||||||
|
} else {
|
||||||
|
std::strncpy(d->firmware_build, d->chip->firmware_build(),
|
||||||
|
sizeof(d->firmware_build) - 1);
|
||||||
|
d->state = XONE_STATE_READY;
|
||||||
|
xone::log_msg(xone::log_level::info, "api: start: radio ready");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
extern "C" void xone_close(xone_dongle *d)
|
extern "C" void xone_close(xone_dongle *d)
|
||||||
{
|
{
|
||||||
if (d != current_session.get())
|
// Wait for the background work to finish before releasing the session.
|
||||||
return;
|
if (d->worker.joinable())
|
||||||
current_session.reset();
|
d->worker.join();
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> guard(session_mutex);
|
||||||
|
if (d == current_session.get())
|
||||||
|
current_session.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" int xone_state(const xone_dongle *d)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> guard(d->lock);
|
||||||
|
return d->state;
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" const char *xone_error(const xone_dongle *d)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> guard(d->lock);
|
||||||
|
return d->error;
|
||||||
}
|
}
|
||||||
|
|
||||||
extern "C" std::uint16_t xone_pid(const xone_dongle *d)
|
extern "C" std::uint16_t xone_pid(const xone_dongle *d)
|
||||||
{
|
{
|
||||||
return d->transport->pid();
|
return d->pid; // set once at open, read-only afterwards
|
||||||
}
|
}
|
||||||
|
|
||||||
extern "C" std::uint16_t xone_chip_id(const xone_dongle *d)
|
extern "C" std::uint16_t xone_chip_id(const xone_dongle *d)
|
||||||
@@ -93,5 +181,6 @@ extern "C" const char *xone_mac_address(const xone_dongle *d)
|
|||||||
|
|
||||||
extern "C" const char *xone_firmware_build(const xone_dongle *d)
|
extern "C" const char *xone_firmware_build(const xone_dongle *d)
|
||||||
{
|
{
|
||||||
return d->chip->firmware_build();
|
std::lock_guard<std::mutex> guard(d->lock);
|
||||||
|
return d->firmware_build;
|
||||||
}
|
}
|
||||||
|
|||||||
+28
-2
@@ -27,6 +27,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
|||||||
struct ContentView: View {
|
struct ContentView: View {
|
||||||
@State private var donglePresent = false
|
@State private var donglePresent = false
|
||||||
@State private var session: OpaquePointer? = nil
|
@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)
|
private let timer = Timer.publish(every: 1.0, on: .main, in: .common)
|
||||||
.autoconnect()
|
.autoconnect()
|
||||||
@@ -69,10 +72,18 @@ struct ContentView: View {
|
|||||||
.onReceive(timer) { _ in
|
.onReceive(timer) { _ in
|
||||||
let present = xone_dongle_present()
|
let present = xone_dongle_present()
|
||||||
if present && session == nil {
|
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 {
|
} else if !present, let s = session {
|
||||||
xone_close(s)
|
xone_close(s)
|
||||||
session = nil
|
session = nil
|
||||||
|
} else if let s = session {
|
||||||
|
radioState = Int(xone_state(s))
|
||||||
}
|
}
|
||||||
donglePresent = present
|
donglePresent = present
|
||||||
}
|
}
|
||||||
@@ -85,7 +96,12 @@ struct ContentView: View {
|
|||||||
debugRow("PID", String(format: "0x%04X", xone_pid(session)))
|
debugRow("PID", String(format: "0x%04X", xone_pid(session)))
|
||||||
debugRow("Chip ID", String(format: "0x%04X", xone_chip_id(session)))
|
debugRow("Chip ID", String(format: "0x%04X", xone_chip_id(session)))
|
||||||
debugRow("MAC", String(cString: xone_mac_address(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 {
|
private func firmwareBuildText(_ session: OpaquePointer) -> String {
|
||||||
let build = String(cString: xone_firmware_build(session))
|
let build = String(cString: xone_firmware_build(session))
|
||||||
return build.isEmpty ? "not loaded" : build
|
return build.isEmpty ? "not loaded" : build
|
||||||
|
|||||||
+6
-4
@@ -21,7 +21,7 @@ auto usage() -> int
|
|||||||
"Usage: xone_cli <command>...\n"
|
"Usage: xone_cli <command>...\n"
|
||||||
"\n"
|
"\n"
|
||||||
"Commands (run in a single dongle session):\n"
|
"Commands (run in a single dongle session):\n"
|
||||||
" recover Port reset + re-enumerate (first command)\n"
|
" recover Port reset + re-enumerate (first command; do not use yet)\n"
|
||||||
" info Show dongle and chip state\n"
|
" info Show dongle and chip state\n"
|
||||||
" firmware <path> Load the firmware image\n"
|
" firmware <path> Load the firmware image\n"
|
||||||
" radio-init Initialize the radio\n"
|
" radio-init Initialize the radio\n"
|
||||||
@@ -139,9 +139,11 @@ auto main(int argc, char **argv) -> int
|
|||||||
std::fprintf(stderr, "\n");
|
std::fprintf(stderr, "\n");
|
||||||
};
|
};
|
||||||
|
|
||||||
// `recover` forces a hub port reset and re-enumeration. The transport is
|
// `recover` forces a hub port reset and re-enumeration. Do not use it yet:
|
||||||
// dead afterwards, so probe again for the fresh device. It must be the
|
// when the chip's SIE is crashed, it cannot complete the host-side port
|
||||||
// first command.
|
// reset handshake and the device drops off the bus entirely. A crashed
|
||||||
|
// chip recovers on its own via the MCU watchdog after ~90s of a quiet
|
||||||
|
// bus, so just wait instead. It must be the first command.
|
||||||
int arg = 1;
|
int arg = 1;
|
||||||
if (!std::strcmp(argv[1], "recover")) {
|
if (!std::strcmp(argv[1], "recover")) {
|
||||||
auto current = xone::usb::transport::probe(on_frame, nullptr);
|
auto current = xone::usb::transport::probe(on_frame, nullptr);
|
||||||
|
|||||||
@@ -170,6 +170,9 @@ void transport::stop_pump()
|
|||||||
|
|
||||||
auto transport::re_enumerate() -> int
|
auto transport::re_enumerate() -> int
|
||||||
{
|
{
|
||||||
|
// Do not use yet: a crashed SIE cannot finish the host-side port reset
|
||||||
|
// handshake and drops off the bus. A crashed chip recovers on its own via
|
||||||
|
// the MCU watchdog after ~90s of a quiet bus, so prefer waiting.
|
||||||
// Stop the pump first so no completion callback touches the interface
|
// Stop the pump first so no completion callback touches the interface
|
||||||
// references while the kernel tears them down.
|
// references while the kernel tears them down.
|
||||||
stop_pump();
|
stop_pump();
|
||||||
|
|||||||
Reference in New Issue
Block a user