diff --git a/CMakeLists.txt b/CMakeLists.txt index 93e47b5..e97717b 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.9 LANGUAGES CXX Swift) +project(xone_macos VERSION 0.1.10 LANGUAGES CXX Swift) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) @@ -96,6 +96,7 @@ add_executable(xone_app ) target_include_directories(xone_app PRIVATE "${CMAKE_SOURCE_DIR}/include") target_compile_definitions(xone_app PRIVATE ${BASE_DEFINITIONS}) +target_compile_features(xone_app PRIVATE cxx_std_23) target_link_libraries(xone_app PRIVATE xone_api ${BASE_LIBRARIES}) # Swift sees the C ABI via a bridging header (no module map required) @@ -111,6 +112,7 @@ add_executable(xone_cli ) target_include_directories(xone_cli PRIVATE "${CMAKE_SOURCE_DIR}/include") target_compile_definitions(xone_cli PRIVATE ${BASE_DEFINITIONS}) +target_compile_features(xone_cli PRIVATE cxx_std_23) target_link_libraries(xone_cli PRIVATE xone_mt76) # ============================================================================== diff --git a/PLAN.md b/PLAN.md index d1ac31d..3483d85 100644 --- a/PLAN.md +++ b/PLAN.md @@ -35,6 +35,24 @@ User-space macOS app that speaks to the Xbox Wireless Dongle (MT76xx chip) and e └──────────────────────────────────────────────────────┘ ``` +## Current Status + +- Phase 1 (GIP + auth): ported and unit-tested. +- Phase 2 (USB transport): probe/open, async read pump, vendor requests, + bulk write. Verified on hardware. +- Phase 3 (MT76 chip): register/EFUSE access, firmware load (per-PID images), + radio init (registers, crystal, MAC/BSSID, channel eval, beacon). Verified + end-to-end: `radio-init` completes, beacon TX enabled, FCE shows firmware + running. +- C API + Swift app: async session (fast probe; firmware + radio on a worker + thread), state display (idle/starting/ready/error). + +Remaining for Phase 3: controller association and the data path (design below). +Then Phase 4 (HID) and Phase 5 (app polish). + +**Immediate goal:** pair a controller and show it in the GUI. Exposing it as +a macOS HID device is deferred. + ## Directory Layout ``` @@ -156,6 +174,82 @@ Extract the protocol logic from Linux kernel code into standalone C. - Menubar icon for status - Firmware download helper (script or in-app) +## Controller Association Design + +Ported from `transport/dongle.c` and `transport/mt76.c`. Goal: pair a +controller and show it in the GUI. Exposing it as a macOS HID device is +deferred. + +### RX Path + +Both EP IN endpoints (0x04 WLAN, 0x05 CMD) feed one handler: + +1. `process_buffer`: take the raw IN buffer. +2. `process_message`: parse the u32 info header; read D_PORT. Ignore command + responses (CMD_SEQ == 0x01). Strip header + 4-byte trailer. + - D_PORT == WLAN: go to step 3. + - D_PORT == CPU_RX: dispatch by EVT_TYPE: + - BUTTON (0x04): enter pairing mode. + - PACKET_RX (0x0c): go to step 3 (payload is a WLAN frame). + - CLIENT_LOST (0x0e): payload[0] = wcid; remove that client. +3. `process_wlan`: parse rxwi (16 bytes); if RXINFO_L2PAD set, skip the + 2-byte pad after the 802.11 header; trim to MPDU_LEN from rxwi.ctl. +4. `process_frame`: dispatch by frame_control: + - DATA|QOS_DATA: feed the client's GIP adapter (`gip_process_buffer`). + - MGMT|ASSOC_REQ: add a client (addr2 = controller MAC). + - MGMT|DISASSOC: remove client (wcid from rxwi.ctl). + - MGMT|0x70 (WLAN_RESERVED): client command; payload[1] is PAIR_REQ (0x01) + or ENABLE_ENCRYPTION (0x10). + +### Client Lifecycle + +- create_client: find a free WCID (1..16); create a GIP adapter for it. +- associate_client(wcid, mac): `write_burst(WCID_ADDR, mac)`; ms_command( + ADD_CLIENT, {wcid-1,0,0,0,0x40,0x1f,0,0}); send an ASSOC_RESP mgmt frame via + `send_wlan` (fc = MGMT|ASSOC_RESP, da/sa/bssid, status_code = 0x0110, + aid = 0x0f00). +- pair_client(mac): send a PAIR_RESP mgmt frame via `send_wlan` + (fc = MGMT|0x70, reserved + PAIR_RESP byte + 9-byte payload). +- remove_client(wcid): ms_command(REMOVE_CLIENT, {wcid-1,0,0,0}); zero the + WCID ADDR/IV/ATTR regions. +- LED: on when a client is added outside pairing mode; off when the last + client leaves. + +### Architecture + +One GIP adapter per controller, keyed by the chip's WCID (matches upstream). +The rxwi.wcid identifies which controller a frame belongs to; within each +adapter the GIP client ID is 0. Our ported `get_client` auto-creates a client +on first packet, so a fresh adapter yields its single client on demand. +(Verify the GIP header client ID on hardware.) + +### Constants To Add (mt76_defs.hpp) + +- WCID regions: ADDR base 0x1800 (+n*8), KEY base 0x8000 (+n*32, len 16), + IV base 0xa000 (+n*8), ATTR base 0xa800 (+n*4); ATTR pairwise bit 0, + pkey mode genmask(3,1) = AES_CCMP (4). +- TXD info: DPORT genmask(29,27), QSEL genmask(26,25) EDCA=2, WIV bit 24, + 80211 bit 19. +- RX FCE info: CMD_SEQ genmask(19,16), EVT_TYPE genmask(23,20), D_PORT + genmask(29,27). +- rxwi: RXINFO_L2PAD bit 14; CTL_WCID genmask(7,0); CTL_MPDU_LEN genmask(29,16). +- Events: BUTTON 0x04, PACKET_RX 0x0c, CLIENT_LOST 0x0e. +- Client commands: WLAN_RESERVED fc 0x70; PAIR_REQ 0x01, PAIR_RESP 0x02, + ENABLE_ENCRYPTION 0x10. +- 802.11 FCTL: MGMT 0x00, DATA 0x08; ASSOC_REQ 0x00, ASSOC_RESP 0x10, + DISASSOC 0xa0, QOS_DATA 0x70. + +### chip Functions To Port (mt76.c) + +`send_wlan`, `associate_client`, `pair_client`, `send_client_command`, +`set_client_key`, `remove_client`. + +### GUI + +- C API: expose connected controllers (count + per-client info: MAC, product + name from GIP identify, battery). +- Swift app: list controllers in the Controllers section of the Debug view. + ## Dongle Initialization Sequence ``` diff --git a/include/app/xone_api.h b/include/app/xone_api.h index dd3bdc2..7ad3dd9 100644 --- a/include/app/xone_api.h +++ b/include/app/xone_api.h @@ -65,6 +65,13 @@ const char *xone_mac_address(const xone_dongle *d); // Empty string if the firmware was not loaded. Valid until xone_close(). const char *xone_firmware_build(const xone_dongle *d); +// Number of connected controllers. +int xone_controller_count(xone_dongle const *d); + +// Copy the controller at index into buf as "xx:xx:xx:xx:xx:xx". Returns 0 on +// success, -1 if index is out of range. +int xone_controller_mac(xone_dongle const *d, int index, char *buf, int len); + #ifdef __cplusplus } #endif diff --git a/include/mt76/mt76.hpp b/include/mt76/mt76.hpp index dd666b6..10e6726 100644 --- a/include/mt76/mt76.hpp +++ b/include/mt76/mt76.hpp @@ -12,6 +12,7 @@ #include #include #include +#include #include "mt76/mt76_defs.hpp" @@ -55,7 +56,7 @@ public: auto select_function(std::uint32_t func, std::uint32_t val) -> int; auto set_power_mode(power_mode mode) -> int; auto load_cr(cr_mode mode) -> int; - auto write_burst(std::uint32_t idx, void *data, std::size_t len) -> int; + auto write_burst(std::uint32_t idx, void const *data, std::size_t len) -> int; auto send_ms_command(ms_command cmd, void *data, std::size_t len) -> int; auto calibrate(calibration calib, std::uint32_t val) -> int; @@ -69,6 +70,37 @@ public: // Enter or leave pairing mode (port of xone_mt76_set_pairing). auto set_pairing(bool enable) -> int; + // Controller association and data path (ports of the xone_mt76_* client + // functions). `addr` is a 6-byte controller MAC. + // + // send_wlan prepends a TXWI and frames the payload as an MCU message, + // then bulk-writes it to EP OUT. + auto send_wlan(std::span frame) -> int; + + // Associate a controller: program its WCID MAC, ADD_CLIENT ms_command, + // and transmit an ASSOC_RESP mgmt frame. + auto associate_client(std::uint8_t wcid, std::span addr) + -> int; + + // Reply to a pairing request with a PAIR_RESP mgmt frame. + auto pair_client(std::span addr) -> int; + + // Send a reserved (0x70) management command to a client, waiting for an + // acknowledgment (port of xone_mt76_send_client_command). + auto send_client_command(std::uint8_t wcid, + std::span addr, + client_cmd cmd, + std::span data) -> int; + + // Install the per-client AES-CCMP key (16 bytes) in the chip WCID + // registers (port of xone_mt76_set_client_key). + auto set_client_key(std::uint8_t wcid, std::span key) + -> int; + + // Remove a client: REMOVE_CLIENT ms_command and zero its WCID regions + // (port of xone_mt76_remove_client). + auto remove_client(std::uint8_t wcid) -> int; + 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. diff --git a/include/mt76/mt76_defs.hpp b/include/mt76/mt76_defs.hpp index 36dbbfe..1d5d69b 100644 --- a/include/mt76/mt76_defs.hpp +++ b/include/mt76/mt76_defs.hpp @@ -30,6 +30,13 @@ inline constexpr auto field_prep(std::uint32_t mask, std::uint32_t val) -> std:: return (val << __builtin_ctz(mask)) & mask; } +// Extract a bitfield (port of the kernel FIELD_GET macro). +inline constexpr auto field_get(std::uint32_t mask, std::uint32_t val) + -> std::uint32_t +{ + return (val & mask) >> __builtin_ctz(mask); +} + // Register address flag: use the config-space vendor request codes. constexpr std::uint32_t mt_vend_type_cfg = bit(30); @@ -382,4 +389,102 @@ constexpr std::uint8_t mt_txwi_ack_ctl_nseq = bit(1); // 802.11 frame control for a beacon (MGMT type, BEACON subtype). constexpr std::uint16_t ieee80211_fc_beacon = 0x0080; +// WCID regions (port of MT_WCID_*). n is the zero-based WCID index. +constexpr std::uint32_t mt_wcid_addr_base = 0x1800; +inline constexpr auto mt_wcid_addr(std::size_t n) -> std::uint32_t +{ + return mt_wcid_addr_base + (n << 3); +} +constexpr std::uint32_t mt_wcid_key_base = 0x8000; +inline constexpr auto mt_wcid_key(std::size_t n) -> std::uint32_t +{ + return mt_wcid_key_base + (n << 5); +} +constexpr std::uint32_t mt_wcid_iv_base = 0xa000; +inline constexpr auto mt_wcid_iv(std::size_t n) -> std::uint32_t +{ + return mt_wcid_iv_base + (n << 3); +} +constexpr std::uint32_t mt_wcid_attr_base = 0xa800; +inline constexpr auto mt_wcid_attr(std::size_t n) -> std::uint32_t +{ + return mt_wcid_attr_base + (n << 2); +} +constexpr std::uint32_t mt_wcid_attr_pairwise = bit(0); +constexpr std::uint32_t mt_wcid_attr_pkey_mode = genmask(3, 1); +constexpr std::size_t xone_mt_wcid_key_len = 16; + +// TX descriptor info fields (port of MT_TXD_INFO_*), used by send_wlan. +constexpr std::uint32_t mt_txd_info_80211 = bit(19); +constexpr std::uint32_t mt_txd_info_wiv = bit(24); +constexpr std::uint32_t mt_txd_info_qsel = genmask(26, 25); +constexpr std::uint32_t mt_txd_info_dport = genmask(29, 27); + +// RX FCE info fields (port of MT_RX_FCE_INFO_*), used by the RX path. +constexpr std::uint32_t mt_rx_fce_info_len = genmask(13, 0); +constexpr std::uint32_t mt_rx_fce_info_evt_type = genmask(23, 20); + +// Receive descriptor (port of struct mt76_rxwi). Little-endian wire order, +// 32 bytes total. Only rxinfo and ctl are consumed by the RX path. +struct mt76_rxwi { + std::uint32_t rxinfo; // MT_RXINFO_L2PAD etc. + std::uint32_t ctl; // wcid (bits 7:0), mpdu_len (bits 29:16) + std::uint16_t tid_sn; + std::uint16_t rate; + std::uint8_t rssi[4]; + std::uint32_t bbp_rxinfo[4]; +}; +constexpr std::uint32_t mt_rxinfo_l2pad = bit(14); +constexpr std::uint32_t mt_rxwi_ctl_wcid = genmask(7, 0); +constexpr std::uint32_t mt_rxwi_ctl_mpdu_len = genmask(29, 16); + +// CPU RX event types (port of XONE_MT_EVT_*). +enum cpu_evt : std::uint32_t { + evt_button = 0x04, + evt_packet_rx = 0x0c, + evt_client_lost = 0x0e, +}; + +// Client command codes (port of XONE_MT_CLIENT_*). +enum client_cmd : std::uint32_t { + client_pair_req = 0x01, + client_pair_resp = 0x02, + client_enable_encryption = 0x10, +}; + +// 802.11 frame control: type (bits 3:2) and subtype (bits 7:4). +// FCTL_FTYPE/FCTL_STYPE are the masks used to match a frame's type/subtype. +constexpr std::uint16_t ieee80211_fctl_ftype = 0x0c; +constexpr std::uint16_t ieee80211_fctl_stype = 0xf0; +constexpr std::uint16_t ieee80211_ftype_mgmt = 0x00; +constexpr std::uint16_t ieee80211_ftype_data = 0x08; +constexpr std::uint16_t ieee80211_stype_assoc_req = 0x00; +constexpr std::uint16_t ieee80211_stype_assoc_resp = 0x10; +constexpr std::uint16_t ieee80211_stype_disassoc = 0xa0; +constexpr std::uint16_t ieee80211_stype_qos_data = 0x70; +// MGMT subtype for reserved (pairing) frames. +constexpr std::uint16_t ieee80211_stype_wlan_reserved = 0x70; + +// TX descriptor fields (port of MT_TXWI_*). +constexpr std::uint32_t mt_txwi_flags_mpdu_density = genmask(7, 5); +constexpr std::uint8_t mt_txwi_ack_ctl_req = bit(0); +constexpr std::uint8_t ieee80211_ht_mpdu_density_4 = 4; + +// TX queue selection (port of enum mt76_qsel). +enum qsel : std::uint32_t { + qsel_mgmt = 0, + qsel_hcca = 1, + qsel_edca = 2, + qsel_edca_2 = 3, +}; + +// Cipher types (port of enum mt76_cipher_type). +enum cipher_type : std::uint32_t { + cipher_none = 0, + cipher_wep40 = 1, + cipher_wep104 = 2, + cipher_tkip = 3, + cipher_aes_ccmp = 4, +}; + } // namespace xone::mt76 diff --git a/src/app/api.cpp b/src/app/api.cpp index dcc1a88..8e88466 100644 --- a/src/app/api.cpp +++ b/src/app/api.cpp @@ -2,12 +2,15 @@ #include "app/xone_api.h" +#include #include #include #include #include +#include #include #include +#include #include "common/log.hpp" #include "mt76/mt76.hpp" @@ -17,11 +20,19 @@ #define XONE_VERSION "0.1.0" #endif +// One connected controller (tracked for the GUI). Guarded by xone_dongle's +// lock; written by the transport reader thread, read by the UI thread. +struct controller { + std::uint8_t wcid; + std::array mac; +}; + // Definition of the opaque struct xone_dongle from app/xone_api.h. struct xone_dongle { std::unique_ptr transport; std::unique_ptr chip; - // Guards state, firmware_build, and error (written by the start worker). + // Guards state, firmware_build, error, and controllers. Written by the + // start worker and the transport reader thread; read by the UI thread. mutable std::mutex lock; std::uint16_t pid = 0; std::uint16_t chip_id = 0; @@ -32,13 +43,29 @@ struct xone_dongle { // Firmware load + radio init runs here so the UI thread never blocks. std::thread worker; + // Connected controllers (wcid 1..16). Guarded by lock. + std::vector controllers; + ~xone_dongle() { + // Stop the reader thread before the chip is torn down so a late IN + // completion cannot touch freed state. + transport.reset(); // 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(); } + + // RX dispatch, driven from the transport reader thread. Port of the + // xone_dongle_process_* functions in transport/dongle.c. + auto process_message(void const *data, std::size_t len) -> void; + auto process_wlan(std::span buf) -> void; + auto handle_association(std::span addr) -> void; + auto handle_disassociation(std::uint8_t wcid) -> void; + auto handle_client_command(std::span payload, + std::uint8_t wcid, + std::span addr) -> void; }; namespace { @@ -47,6 +74,9 @@ namespace { std::unique_ptr current_session; std::mutex session_mutex; // guards current_session +// 802.11 management frame header length (three addresses, no extensions). +constexpr std::size_t k_mgmt_hdr_len = 24; + // Select the firmware image for a product ID: firmware/xone_dongle_.bin. auto firmware_path_for(std::uint16_t pid) -> std::string { @@ -57,6 +87,161 @@ auto firmware_path_for(std::uint16_t pid) -> std::string } // namespace +// --- RX dispatch (port of xone_dongle_process_*) --------------------------- + +auto xone_dongle::process_message(void const *data, std::size_t len) -> void +{ + using namespace xone::mt76; + + if (!chip || len < 2 * cmd_hdr_len) + return; + + auto buf = std::span( + static_cast(data), len); + + auto info = xone::load_le32(buf.data()); + auto port = field_get(mt_mcu_msg_port, info); // bits 29:27 + + // Ignore command responses (CMD_SEQ == 1). + if (field_get(mt_mcu_msg_cmd_seq, info) == 0x01) + return; + + // Strip the header + trailer. + buf = buf.subspan(cmd_hdr_len, len - 2 * cmd_hdr_len); + + if (port == dma_msg_port::wlan_port) { + process_wlan(buf); + return; + } + + if (port != dma_msg_port::cpu_rx_port) + return; + + switch (field_get(mt_rx_fce_info_evt_type, info)) { + case cpu_evt::evt_button: + // Pairing mode is entered via the CLI for now. + break; + case cpu_evt::evt_packet_rx: + process_wlan(buf); + break; + case cpu_evt::evt_client_lost: + if (!buf.empty()) + handle_disassociation(buf[0]); + break; + default: + break; + } +} + +auto xone_dongle::process_wlan(std::span buf) -> void +{ + using namespace xone::mt76; + + if (buf.size() < sizeof(mt76_rxwi)) + return; + + auto rxinfo = xone::load_le32(buf.data()); + auto ctl = xone::load_le32(buf.data() + 4); + auto wcid = static_cast(field_get(mt_rxwi_ctl_wcid, ctl)); + + // The frame starts after the 32-byte rxwi. + std::span frame = buf.subspan(sizeof(mt76_rxwi)); + if (frame.size() < k_mgmt_hdr_len) + return; + + auto fc = xone::load_le16(frame.data()); + auto match = fc & (ieee80211_fctl_ftype | ieee80211_fctl_stype); + + // L2PAD: 2 bytes of padding after the 802.11 header. + std::size_t pad = (rxinfo & mt_rxinfo_l2pad) ? 2 : 0; + + if (match == (ieee80211_ftype_mgmt | ieee80211_stype_assoc_req)) { + handle_association(frame.subspan(10, 6)); // addr2 + } else if (match == (ieee80211_ftype_mgmt | ieee80211_stype_disassoc)) { + handle_disassociation(wcid); + } else if (match == (ieee80211_ftype_mgmt | ieee80211_stype_wlan_reserved)) { + handle_client_command(frame.subspan(k_mgmt_hdr_len + pad), wcid, + frame.subspan(10, 6)); // addr2 + } + // DATA|QOS_DATA (GIP) is handled in a later increment. +} + +auto xone_dongle::handle_association(std::span addr) -> void +{ + // Find a free WCID slot (1..16). + std::uint8_t wcid = 0; + { + std::lock_guard guard(lock); + for (std::uint8_t i = 1; i <= 16; ++i) { + bool used = false; + for (auto const& c : controllers) + if (c.wcid == i) { used = true; break; } + if (!used) { wcid = i; break; } + } + } + if (wcid == 0) + return; // no free slot + + std::array mac{}; + std::memcpy(mac.data(), addr.data(), mac.size()); + + // Program the chip outside the lock (synchronous USB I/O). + if (chip->associate_client(wcid, mac) != 0) { + xone::log_msg(xone::log_level::warn, + "api: associate wcid=%d failed", wcid); + return; + } + + std::lock_guard guard(lock); + controllers.push_back(controller{wcid, mac}); + xone::log_msg(xone::log_level::info, + "api: controller associated (wcid=%d)", wcid); +} + +auto xone_dongle::handle_disassociation(std::uint8_t wcid) -> void +{ + if (wcid == 0 || wcid > 16) + return; + + // Remove from the chip outside the lock. + chip->remove_client(wcid); + + std::lock_guard guard(lock); + for (auto it = controllers.begin(); it != controllers.end(); ++it) { + if (it->wcid == wcid) { + controllers.erase(it); + break; + } + } + xone::log_msg(xone::log_level::info, + "api: controller removed (wcid=%d)", wcid); +} + +auto xone_dongle::handle_client_command(std::span payload, + std::uint8_t wcid, + std::span addr) -> void +{ + using namespace xone::mt76; + if (payload.size() < 2 || payload[0] != ieee80211_stype_wlan_reserved) + return; + + switch (payload[1]) { + case client_cmd::client_pair_req: { + std::array mac{}; + std::memcpy(mac.data(), addr.data(), mac.size()); + chip->pair_client(mac); + xone::log_msg(xone::log_level::info, + "api: controller paired (wcid=%d)", wcid); + break; + } + case client_cmd::client_enable_encryption: + // Encryption is enabled in a later increment (needs the GIP key). + break; + default: + break; + } +} + extern "C" const char *xone_version(void) { return XONE_VERSION; @@ -74,7 +259,17 @@ extern "C" xone_dongle *xone_open(void) return nullptr; auto s = std::make_unique(); - s->transport = xone::usb::transport::probe(nullptr, nullptr); + // Wire the RX dispatch into the transport's read pump. The callback + // fires on the reader thread; process_message is null-safe until the + // chip is created below. + auto *session = s.get(); + xone::usb::frame_callback frames = [session](std::uint8_t ep, + void const *data, + std::size_t len) { + (void)ep; + session->process_message(data, len); + }; + s->transport = xone::usb::transport::probe(frames, nullptr); if (!s->transport) return nullptr; @@ -184,3 +379,24 @@ extern "C" const char *xone_firmware_build(const xone_dongle *d) std::lock_guard guard(d->lock); return d->firmware_build; } + +// Number of connected controllers. +extern "C" int xone_controller_count(xone_dongle const *d) +{ + std::lock_guard guard(d->lock); + return static_cast(d->controllers.size()); +} + +// Copy the controller's MAC into buf as "xx:xx:xx:xx:xx:xx". Returns 0 on +// success, -1 if index is out of range. +extern "C" int xone_controller_mac(xone_dongle const *d, int index, char *buf, + int len) +{ + std::lock_guard guard(d->lock); + if (index < 0 || index >= static_cast(d->controllers.size())) + return -1; + auto const& c = d->controllers[index]; + std::snprintf(buf, len, "%02x:%02x:%02x:%02x:%02x:%02x", + c.mac[0], c.mac[1], c.mac[2], c.mac[3], c.mac[4], c.mac[5]); + return 0; +} diff --git a/src/app/main.swift b/src/app/main.swift index a3f5e21..94579b4 100644 --- a/src/app/main.swift +++ b/src/app/main.swift @@ -30,6 +30,8 @@ struct ContentView: View { // 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 + // Connected controllers (MAC strings), refreshed each tick. + @State private var controllers: [String] = [] private let timer = Timer.publish(every: 1.0, on: .main, in: .common) .autoconnect() @@ -84,6 +86,7 @@ struct ContentView: View { session = nil } else if let s = session { radioState = Int(xone_state(s)) + refreshControllers(s) } donglePresent = present } @@ -130,6 +133,19 @@ struct ContentView: View { return build.isEmpty ? "not loaded" : build } + // Query the C API for connected controllers and store their MACs. + private func refreshControllers(_ session: OpaquePointer) { + let count = Int(xone_controller_count(session)) + var list: [String] = [] + for i in 0.. int return send_command(mcu_cmd::cmd_load_cr, payload, sizeof(payload)); } -auto chip::write_burst(std::uint32_t idx, void *data, std::size_t len) -> int +auto chip::write_burst(std::uint32_t idx, void const *data, std::size_t len) -> int { std::vector buf(4 + len); xone::store_le32(buf.data(), idx + mt_mcu_memmap_wlan); // Register offset in memory. @@ -761,4 +761,172 @@ auto chip::resume_radio() -> int return 0; } +// Build a TXWI (struct mt76_txwi, 20 bytes, packed) for a WLAN frame. +auto build_txwi(std::uint8_t wcid, std::size_t len_ctl) + -> std::array +{ + std::array txwi{}; + xone::store_le16(txwi.data() + 0, field_prep(mt_txwi_flags_mpdu_density, + ieee80211_ht_mpdu_density_4)); + xone::store_le16(txwi.data() + 2, + field_prep(mt_rxwi_rate_phy, phy_type::phy_ofdm)); + txwi[4] = mt_txwi_ack_ctl_req; + txwi[5] = wcid; // 0xff for broadcast (assoc/pair), wcid-1 for a client. + xone::store_le16(txwi.data() + 6, static_cast(len_ctl)); + return txwi; +} + +auto chip::send_wlan(std::span frame) -> int +{ + auto txwi = build_txwi(0xff, frame.size()); + + std::vector payload(txwi.size() + frame.size()); + std::memcpy(payload.data(), txwi.data(), txwi.size()); + std::memcpy(payload.data() + txwi.size(), frame.data(), frame.size()); + + // Enhanced distributed channel access (EDCA), wireless info valid (WIV). + auto info = field_prep(mt_txd_info_dport, dma_msg_port::wlan_port) + | field_prep(mt_txd_info_qsel, qsel::qsel_edca) + | mt_txd_info_wiv + | mt_txd_info_80211; + + auto buf = build_message(info, payload.data(), payload.size()); + auto ret = transport_.bulk_write(buf.data(), buf.size()); + return ret < 0 ? ret : 0; +} + +auto chip::associate_client(std::uint8_t wcid, + std::span addr) -> int +{ + auto address = mac_address(); + + // struct ieee80211_mgmt (assoc_resp), 26 bytes: + // [frame_control][duration][da][sa][bssid][status_code][aid]. + std::uint8_t mgmt[26] = {}; + xone::store_le16(mgmt + 0, ieee80211_ftype_mgmt + | ieee80211_stype_assoc_resp); + std::memcpy(mgmt + 4, addr.data(), 6); // da + std::memcpy(mgmt + 10, address.data(), 6); // sa + std::memcpy(mgmt + 16, address.data(), 6); // bssid + xone::store_le16(mgmt + 22, 0x0110); // status_code (original) + xone::store_le16(mgmt + 24, 0x0f00); // aid (original) + + // Payload: mgmt frame plus 8 bytes of zero padding. + std::vector payload(sizeof(mgmt) + 8, 0); + std::memcpy(payload.data(), mgmt, sizeof(mgmt)); + + if (auto err = write_burst(mt_wcid_addr(wcid), addr.data(), 6); err != 0) + return err; + + // ADD_CLIENT ms_command: {wcid-1, 0, 0, 0, 0x40, 0x1f, 0, 0}. + auto idx = static_cast(wcid - 1); + std::uint8_t data[8] = { idx, 0x00, 0x00, 0x00, 0x40, 0x1f, 0x00, 0x00 }; + if (auto err = send_ms_command(ms_command::ms_add_client, + data, sizeof(data)); err != 0) + return err; + + return send_wlan(payload); +} + +auto chip::pair_client(std::span addr) -> int +{ + auto address = mac_address(); + + // struct ieee80211_hdr_3addr (22 bytes) plus the reserved command and a + // 9-byte payload: [fc][duration][addr1][addr2][addr3][0x70][PAIR_RESP] + // [data]. + std::uint8_t frame[33] = {}; + xone::store_le16(frame + 0, ieee80211_ftype_mgmt + | ieee80211_stype_wlan_reserved); + std::memcpy(frame + 4, addr.data(), 6); // addr1 + std::memcpy(frame + 10, address.data(), 6); // addr2 + std::memcpy(frame + 16, address.data(), 6); // addr3 + frame[22] = ieee80211_stype_wlan_reserved; // reserved (0x70) + frame[23] = client_cmd::client_pair_resp; + std::uint8_t data[9] = { 0x00, 0x45, 0x55, 0x01, 0x0f, 0x8f, 0xff, 0x87, + 0x1f }; + std::memcpy(frame + 24, data, sizeof(data)); + + return send_wlan({ frame, frame + sizeof(frame) }); +} + +auto chip::send_client_command(std::uint8_t wcid, + std::span addr, + client_cmd cmd, + std::span data) -> int +{ + auto address = mac_address(); + + // Payload: [info(8)][txwi(20)][hdr(22)][0x70][cmd][data]. + auto txwi = build_txwi(wcid - 1, 22 + 2 + data.size()); + std::vector payload(8 + txwi.size() + 22 + 2 + data.size()); + + // info: {0, 0, 0, wcid-1, 0, 0, 0, 0}. + payload[3] = wcid - 1; + std::memcpy(payload.data() + 8, txwi.data(), txwi.size()); + + auto hdr = payload.data() + 28; + xone::store_le16(hdr + 0, ieee80211_ftype_mgmt + | ieee80211_stype_wlan_reserved); + std::memcpy(hdr + 4, addr.data(), 6); // addr1 + std::memcpy(hdr + 10, address.data(), 6); // addr2 + std::memcpy(hdr + 16, address.data(), 6); // addr3 + hdr[22] = ieee80211_stype_wlan_reserved; // reserved (0x70) + hdr[23] = static_cast(cmd); + if (!data.empty()) + std::memcpy(hdr + 24, data.data(), data.size()); + + return send_command(0, payload.data(), payload.size()); +} + +auto chip::set_client_key(std::uint8_t wcid, + std::span key) -> int +{ + if (key.size() != xone_mt_wcid_key_len) + return -22; // -EINVAL + + // IV: {0x01, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00}. + std::uint8_t iv[8] = { 0x01, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00 }; + auto attr = field_prep(mt_wcid_attr_pkey_mode, cipher_type::cipher_aes_ccmp) + | mt_wcid_attr_pairwise; + + if (auto err = write_burst(mt_wcid_key(wcid), key.data(), key.size()); + err != 0) + return err; + + if (auto err = write_burst(mt_wcid_iv(wcid), iv, sizeof(iv)); err != 0) + return err; + + return write_burst(mt_wcid_attr(wcid), &attr, sizeof(attr)); +} + +auto chip::remove_client(std::uint8_t wcid) -> int +{ + // REMOVE_CLIENT ms_command: {wcid-1, 0, 0, 0}. + std::uint8_t data[4] = { static_cast(wcid - 1), 0x00, 0x00, + 0x00 }; + if (auto err = send_ms_command(ms_command::ms_remove_client, + data, sizeof(data)); err != 0) + return err; + + // Zero the WCID regions. + std::uint8_t addr[6] = {}; + std::uint8_t iv[8] = {}; + std::uint32_t attr = 0; + std::uint8_t key[xone_mt_wcid_key_len] = {}; + + if (auto err = write_burst(mt_wcid_addr(wcid), addr, sizeof(addr)); + err != 0) + return err; + + if (auto err = write_burst(mt_wcid_iv(wcid), iv, sizeof(iv)); err != 0) + return err; + + if (auto err = write_burst(mt_wcid_attr(wcid), &attr, sizeof(attr)); + err != 0) + return err; + + return write_burst(mt_wcid_key(wcid), key, sizeof(key)); +} + } // namespace xone::mt76 diff --git a/tests/test_mt76.cpp b/tests/test_mt76.cpp index 34499d5..0953ceb 100644 --- a/tests/test_mt76.cpp +++ b/tests/test_mt76.cpp @@ -117,6 +117,29 @@ void test_beacon_layout(void) TEST_ASSERT_EQUAL_UINT32(sizeof(xone::mt76::broadcast_address), 6); } +// Pin the WCID regions and RX descriptor to transport/mt76_defs.h values. +void test_client_association_layout(void) +{ + // WCID register regions (1-based index; slot 0 is reserved). + TEST_ASSERT_EQUAL_UINT32(xone::mt76::mt_wcid_addr(0), 0x1800); + TEST_ASSERT_EQUAL_UINT32(xone::mt76::mt_wcid_addr(1), 0x1808); + TEST_ASSERT_EQUAL_UINT32(xone::mt76::mt_wcid_key(1), 0x8020); + TEST_ASSERT_EQUAL_UINT32(xone::mt76::mt_wcid_iv(1), 0xa008); + TEST_ASSERT_EQUAL_UINT32(xone::mt76::mt_wcid_attr(1), 0xa804); + TEST_ASSERT_EQUAL_UINT32( + static_cast(xone::mt76::xone_mt_wcid_key_len), 16); + + // RX descriptor is 32 bytes on the wire. + TEST_ASSERT_EQUAL_UINT32(sizeof(xone::mt76::mt76_rxwi), 32); + + // Client command and 802.11 frame control values. + TEST_ASSERT_EQUAL_UINT16(xone::mt76::ieee80211_stype_assoc_resp, 0x0010); + TEST_ASSERT_EQUAL_UINT16(xone::mt76::ieee80211_stype_disassoc, 0x00a0); + TEST_ASSERT_EQUAL_UINT16(xone::mt76::ieee80211_stype_qos_data, 0x0070); + TEST_ASSERT_EQUAL_UINT16( + xone::mt76::ieee80211_stype_wlan_reserved, 0x0070); +} + int main(void) { UNITY_BEGIN(); @@ -127,5 +150,6 @@ int main(void) RUN_TEST(test_radio_registers); RUN_TEST(test_channel_table); RUN_TEST(test_beacon_layout); + RUN_TEST(test_client_association_layout); return UNITY_END(); }