feat: expose dongle debug info through the C API

Add an opaque xone_dongle session handle to the C ABI bridge:
xone_open() probes, resets, and loads the firmware (path optional),
and xone_pid/xone_chip_id/xone_mac_address/xone_firmware_build return
debug info for the open session. chip captures the firmware build
string from the image header during load.

The Swift app opens a session when the dongle appears and shows PID,
chip ID, MAC address, and firmware build string in a debug section.

Co-Authored-By: qwen3.8-27b@q2_k_xl: added C API session handle and debug UI
This commit is contained in:
portersky
2026-08-17 16:44:02 +02:00
parent 21dc0e09b3
commit 4f5f16e43f
6 changed files with 153 additions and 5 deletions
+1 -1
View File
@@ -6,7 +6,7 @@ if(NOT CMAKE_GENERATOR MATCHES "^(Ninja|Xcode)$")
endif()
cmake_minimum_required(VERSION 3.21)
project(xone_macos VERSION 0.1.6 LANGUAGES CXX Swift)
project(xone_macos VERSION 0.1.7 LANGUAGES CXX Swift)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
+27
View File
@@ -8,6 +8,7 @@
// ==============================================================================
#include <stdbool.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
@@ -19,6 +20,32 @@ const char *xone_version(void);
// TRUE if an Xbox Wireless Dongle is connected, FALSE otherwise.
bool xone_dongle_present(void);
// Opaque handle to an open dongle session (probe + firmware load).
// Create with xone_open(); call the getters from the thread that opened
// it, and before xone_close(). Only one session may be open at a time.
typedef struct xone_dongle xone_dongle;
// Open a dongle session: probe, USB reset, and (if firmware_path is not
// NULL) load the firmware image from that file. Returns NULL if no dongle
// is present or opening fails.
xone_dongle *xone_open(const char *firmware_path);
// Close the session and release the dongle.
void xone_close(xone_dongle *d);
// Product ID of the connected dongle (e.g. 0x02E6).
uint16_t xone_pid(const xone_dongle *d);
// Chip ID from EFUSE (e.g. 0x7612 for MT7612). 0 if unknown.
uint16_t xone_chip_id(const xone_dongle *d);
// MAC address from EFUSE as "xx:xx:xx:xx:xx:xx". Valid until xone_close().
const char *xone_mac_address(const xone_dongle *d);
// Build string of the loaded firmware image (e.g. "201703281033____").
// Empty string if the firmware was not loaded. Valid until xone_close().
const char *xone_firmware_build(const xone_dongle *d);
#ifdef __cplusplus
}
#endif
+4
View File
@@ -45,6 +45,9 @@ public:
// Resets the MCU if firmware is already loaded. Returns 0 or -errno.
auto load_firmware(char const *path) -> int;
// Build string of the loaded firmware image (empty until load_firmware).
auto firmware_build() const -> char const *;
private:
// Send an MCU command (port of xone_mt76_send_command). `cmd` is the
// MT_MCU_MSG_CMD_TYPE field. Returns bytes written, or a negative errno.
@@ -59,6 +62,7 @@ private:
auto reset_firmware() -> int;
usb::transport &transport_;
char build_time_[17] = {};
};
} // namespace xone::mt76
+75
View File
@@ -2,12 +2,32 @@
#include "app/xone_api.h"
#include <cstdio>
#include <memory>
#include "common/log.hpp"
#include "mt76/mt76.hpp"
#include "usb/usb_transport.hpp"
#ifndef XONE_VERSION
#define XONE_VERSION "0.1.0"
#endif
// Definition of the opaque struct xone_dongle from app/xone_api.h.
struct xone_dongle {
std::unique_ptr<xone::usb::transport> transport;
std::unique_ptr<xone::mt76::chip> chip;
std::uint16_t chip_id = 0;
char mac_address[18] = {};
};
namespace {
// Single dongle session (the app only ever has one).
std::unique_ptr<xone_dongle> current_session;
} // namespace
extern "C" const char *xone_version(void)
{
return XONE_VERSION;
@@ -17,3 +37,58 @@ extern "C" bool xone_dongle_present(void)
{
return xone::usb::dongle_present();
}
extern "C" xone_dongle *xone_open(const char *firmware_path)
{
if (current_session)
return nullptr;
auto s = std::make_unique<xone_dongle>();
s->transport = xone::usb::transport::probe(nullptr, nullptr);
if (!s->transport)
return nullptr;
s->chip = std::make_unique<xone::mt76::chip>(*s->transport);
// EFUSE reads work before and after the firmware load.
s->chip_id = s->chip->chip_id();
auto mac = s->chip->mac_address();
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]);
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);
}
current_session = std::move(s);
return current_session.get();
}
extern "C" void xone_close(xone_dongle *d)
{
if (d != current_session.get())
return;
current_session.reset();
}
extern "C" std::uint16_t xone_pid(const xone_dongle *d)
{
return d->transport->pid();
}
extern "C" std::uint16_t xone_chip_id(const xone_dongle *d)
{
return d->chip_id;
}
extern "C" const char *xone_mac_address(const xone_dongle *d)
{
return d->mac_address;
}
extern "C" const char *xone_firmware_build(const xone_dongle *d)
{
return d->chip->firmware_build();
}
+39 -1
View File
@@ -26,6 +26,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
struct ContentView: View {
@State private var donglePresent = false
@State private var session: OpaquePointer? = nil
private let timer = Timer.publish(every: 1.0, on: .main, in: .common)
.autoconnect()
@@ -42,6 +43,10 @@ struct ContentView: View {
statusView
if let session {
debugView(session)
}
Divider()
controllersView
@@ -62,8 +67,41 @@ struct ContentView: View {
.frame(width: 420)
.frame(minHeight: 320)
.onReceive(timer) { _ in
donglePresent = xone_dongle_present()
let present = xone_dongle_present()
if present && session == nil {
session = xone_open("firmware/xow_dongle.bin")
} else if !present, let s = session {
xone_close(s)
session = nil
}
donglePresent = present
}
}
private func debugView(_ session: OpaquePointer) -> some View {
VStack(alignment: .leading, spacing: 4) {
Text("Debug")
.font(.headline)
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))
}
}
private func debugRow(_ label: String, _ value: String) -> some View {
HStack {
Text(label)
.foregroundStyle(.secondary)
Spacer()
Text(value)
.font(.system(.body, design: .monospaced))
}
}
private func firmwareBuildText(_ session: OpaquePointer) -> String {
let build = String(cString: xone_firmware_build(session))
return build.isEmpty ? "not loaded" : build
}
private var statusView: some View {
+7 -3
View File
@@ -212,9 +212,8 @@ auto chip::send_firmware(void const *fw_data, std::size_t fw_size) -> int
if (fw_size != sizeof(fw_header) + ilm_len + dlm_len)
return -EINVAL;
char build_time[17] = {};
std::memcpy(build_time, header->build_time, sizeof(header->build_time));
xone::log_msg(log_level::info, "mt76: firmware build %s", build_time);
std::memcpy(build_time_, header->build_time, sizeof(header->build_time));
xone::log_msg(log_level::info, "mt76: firmware build %s", build_time_);
// Configure the DMA, enable FCE and packet DMA.
write_register(mt_usb_u3dma_cfg | mt_vend_type_cfg,
@@ -249,6 +248,11 @@ auto chip::reset_firmware() -> int
return 0;
}
auto chip::firmware_build() const -> char const *
{
return build_time_;
}
auto chip::load_firmware(char const *path) -> int
{
// If firmware is already loaded, reset the MCU.