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
+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,10 +67,43 @@ 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 {
HStack(spacing: 8) {
Circle()
+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.