diff --git a/CMakeLists.txt b/CMakeLists.txt index 0467d2b..f56705d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) diff --git a/include/app/xone_api.h b/include/app/xone_api.h index 4c6417d..6381664 100644 --- a/include/app/xone_api.h +++ b/include/app/xone_api.h @@ -8,6 +8,7 @@ // ============================================================================== #include +#include #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 diff --git a/include/mt76/mt76.hpp b/include/mt76/mt76.hpp index 1b16a15..d620ad2 100644 --- a/include/mt76/mt76.hpp +++ b/include/mt76/mt76.hpp @@ -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 diff --git a/src/app/api.cpp b/src/app/api.cpp index bb436fa..ac0ba94 100644 --- a/src/app/api.cpp +++ b/src/app/api.cpp @@ -2,12 +2,32 @@ #include "app/xone_api.h" +#include +#include + +#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 transport; + std::unique_ptr chip; + std::uint16_t chip_id = 0; + char mac_address[18] = {}; +}; + +namespace { + +// Single dongle session (the app only ever has one). +std::unique_ptr 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(); + s->transport = xone::usb::transport::probe(nullptr, nullptr); + if (!s->transport) + return nullptr; + + s->chip = std::make_unique(*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(); +} diff --git a/src/app/main.swift b/src/app/main.swift index ad270d8..a3f9c9c 100644 --- a/src/app/main.swift +++ b/src/app/main.swift @@ -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() diff --git a/src/mt76/mt76.cpp b/src/mt76/mt76.cpp index a618649..6ccee37 100644 --- a/src/mt76/mt76.cpp +++ b/src/mt76/mt76.cpp @@ -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.