feat: pair controllers and show them in the app

Port the MT76 client functions (send_wlan, associate_client,
pair_client, set_client_key, remove_client) and wire the RX dispatch
into the session: EP IN frames are parsed, ASSOC_REQ associates a
controller (WCID plus chip programming), PAIR_REQ replies with
PAIR_RESP, and DISASSOC or client-lost removes it. The C API exposes
connected controllers and the app lists them by MAC. Bumps
xone_cli/xone_app to C++23: they were falling back to the default
standard once mt76.hpp started using std::span. Adds a unit test pinning
the WCID regions, the 32-byte rxwi, and the new frame control values.

Co-Authored-By: qwen3.8-27b@q2_k_xl: client functions, RX dispatch, GUI list
This commit is contained in:
portersky
2026-08-17 20:40:47 +02:00
parent b3e747b900
commit dae51f085b
9 changed files with 682 additions and 7 deletions
+3 -1
View File
@@ -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.9 LANGUAGES CXX Swift) project(xone_macos VERSION 0.1.10 LANGUAGES CXX Swift)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON) 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_include_directories(xone_app PRIVATE "${CMAKE_SOURCE_DIR}/include")
target_compile_definitions(xone_app PRIVATE ${BASE_DEFINITIONS}) 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}) target_link_libraries(xone_app PRIVATE xone_api ${BASE_LIBRARIES})
# Swift sees the C ABI via a bridging header (no module map required) # 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_include_directories(xone_cli PRIVATE "${CMAKE_SOURCE_DIR}/include")
target_compile_definitions(xone_cli PRIVATE ${BASE_DEFINITIONS}) 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) target_link_libraries(xone_cli PRIVATE xone_mt76)
# ============================================================================== # ==============================================================================
+94
View File
@@ -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 ## Directory Layout
``` ```
@@ -156,6 +174,82 @@ Extract the protocol logic from Linux kernel code into standalone C.
- Menubar icon for status - Menubar icon for status
- Firmware download helper (script or in-app) - 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 ## Dongle Initialization Sequence
``` ```
+7
View File
@@ -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(). // Empty string if the firmware was not loaded. Valid until xone_close().
const char *xone_firmware_build(const xone_dongle *d); 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 #ifdef __cplusplus
} }
#endif #endif
+33 -1
View File
@@ -12,6 +12,7 @@
#include <array> #include <array>
#include <cstddef> #include <cstddef>
#include <cstdint> #include <cstdint>
#include <span>
#include "mt76/mt76_defs.hpp" #include "mt76/mt76_defs.hpp"
@@ -55,7 +56,7 @@ public:
auto select_function(std::uint32_t func, std::uint32_t val) -> int; auto select_function(std::uint32_t func, std::uint32_t val) -> int;
auto set_power_mode(power_mode mode) -> int; auto set_power_mode(power_mode mode) -> int;
auto load_cr(cr_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 send_ms_command(ms_command cmd, void *data, std::size_t len) -> int;
auto calibrate(calibration calib, std::uint32_t val) -> 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). // Enter or leave pairing mode (port of xone_mt76_set_pairing).
auto set_pairing(bool enable) -> int; 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<std::uint8_t const> 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<std::uint8_t const> addr)
-> int;
// Reply to a pairing request with a PAIR_RESP mgmt frame.
auto pair_client(std::span<std::uint8_t const> 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<std::uint8_t const> addr,
client_cmd cmd,
std::span<std::uint8_t const> 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<std::uint8_t const> 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: private:
// Send an MCU command (port of xone_mt76_send_command). `cmd` is the // 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. // MT_MCU_MSG_CMD_TYPE field. Returns bytes written, or a negative errno.
+105
View File
@@ -30,6 +30,13 @@ inline constexpr auto field_prep(std::uint32_t mask, std::uint32_t val) -> std::
return (val << __builtin_ctz(mask)) & mask; 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. // Register address flag: use the config-space vendor request codes.
constexpr std::uint32_t mt_vend_type_cfg = bit(30); 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). // 802.11 frame control for a beacon (MGMT type, BEACON subtype).
constexpr std::uint16_t ieee80211_fc_beacon = 0x0080; 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 } // namespace xone::mt76
+218 -2
View File
@@ -2,12 +2,15 @@
#include "app/xone_api.h" #include "app/xone_api.h"
#include <array>
#include <cstdio> #include <cstdio>
#include <cstring> #include <cstring>
#include <memory> #include <memory>
#include <mutex> #include <mutex>
#include <span>
#include <string> #include <string>
#include <thread> #include <thread>
#include <vector>
#include "common/log.hpp" #include "common/log.hpp"
#include "mt76/mt76.hpp" #include "mt76/mt76.hpp"
@@ -17,11 +20,19 @@
#define XONE_VERSION "0.1.0" #define XONE_VERSION "0.1.0"
#endif #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<std::uint8_t, 6> mac;
};
// Definition of the opaque struct xone_dongle from app/xone_api.h. // Definition of the opaque struct xone_dongle from app/xone_api.h.
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). // 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; mutable std::mutex lock;
std::uint16_t pid = 0; std::uint16_t pid = 0;
std::uint16_t chip_id = 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. // Firmware load + radio init runs here so the UI thread never blocks.
std::thread worker; std::thread worker;
// Connected controllers (wcid 1..16). Guarded by lock.
std::vector<controller> controllers;
~xone_dongle() ~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 // The session is destroyed at process exit even without an explicit
// xone_close(); joining here avoids terminating on a joinable thread. // xone_close(); joining here avoids terminating on a joinable thread.
if (worker.joinable()) if (worker.joinable())
worker.join(); 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<std::uint8_t const> buf) -> void;
auto handle_association(std::span<std::uint8_t const> addr) -> void;
auto handle_disassociation(std::uint8_t wcid) -> void;
auto handle_client_command(std::span<std::uint8_t const> payload,
std::uint8_t wcid,
std::span<std::uint8_t const> addr) -> void;
}; };
namespace { namespace {
@@ -47,6 +74,9 @@ namespace {
std::unique_ptr<xone_dongle> current_session; std::unique_ptr<xone_dongle> current_session;
std::mutex session_mutex; // guards 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_<pid>.bin. // Select the firmware image for a product ID: firmware/xone_dongle_<pid>.bin.
auto firmware_path_for(std::uint16_t pid) -> std::string 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 } // 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<std::uint8_t const>(
static_cast<std::uint8_t const *>(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<std::uint8_t const> 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<std::uint8_t>(field_get(mt_rxwi_ctl_wcid, ctl));
// The frame starts after the 32-byte rxwi.
std::span<std::uint8_t const> 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<std::uint8_t const> addr) -> void
{
// Find a free WCID slot (1..16).
std::uint8_t wcid = 0;
{
std::lock_guard<std::mutex> 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<std::uint8_t, 6> 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<std::mutex> 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<std::mutex> 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<std::uint8_t const> payload,
std::uint8_t wcid,
std::span<std::uint8_t const> 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<std::uint8_t, 6> 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) extern "C" const char *xone_version(void)
{ {
return XONE_VERSION; return XONE_VERSION;
@@ -74,7 +259,17 @@ extern "C" xone_dongle *xone_open(void)
return nullptr; return nullptr;
auto s = std::make_unique<xone_dongle>(); auto s = std::make_unique<xone_dongle>();
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) if (!s->transport)
return nullptr; return nullptr;
@@ -184,3 +379,24 @@ extern "C" const char *xone_firmware_build(const xone_dongle *d)
std::lock_guard<std::mutex> guard(d->lock); std::lock_guard<std::mutex> guard(d->lock);
return d->firmware_build; return d->firmware_build;
} }
// Number of connected controllers.
extern "C" int xone_controller_count(xone_dongle const *d)
{
std::lock_guard<std::mutex> guard(d->lock);
return static_cast<int>(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<std::mutex> guard(d->lock);
if (index < 0 || index >= static_cast<int>(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;
}
+29 -2
View File
@@ -30,6 +30,8 @@ struct ContentView: View {
// Radio state polled from the background worker each tick; a change here // Radio state polled from the background worker each tick; a change here
// is what re-renders the debug section. // is what re-renders the debug section.
@State private var radioState = XONE_STATE_IDLE @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) private let timer = Timer.publish(every: 1.0, on: .main, in: .common)
.autoconnect() .autoconnect()
@@ -84,6 +86,7 @@ struct ContentView: View {
session = nil session = nil
} else if let s = session { } else if let s = session {
radioState = Int(xone_state(s)) radioState = Int(xone_state(s))
refreshControllers(s)
} }
donglePresent = present donglePresent = present
} }
@@ -130,6 +133,19 @@ struct ContentView: View {
return build.isEmpty ? "not loaded" : build 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..<count {
var buf = [CChar](repeating: 0, count: 18)
if xone_controller_mac(session, Int32(i), &buf, 18) == 0 {
list.append(String(cString: buf))
}
}
controllers = list
}
private var statusView: some View { private var statusView: some View {
HStack(spacing: 8) { HStack(spacing: 8) {
Circle() Circle()
@@ -144,8 +160,19 @@ struct ContentView: View {
VStack(alignment: .leading, spacing: 8) { VStack(alignment: .leading, spacing: 8) {
Text("Controllers") Text("Controllers")
.font(.headline) .font(.headline)
Text("No controllers connected.") if controllers.isEmpty {
.foregroundStyle(.secondary) Text("No controllers connected.")
.foregroundStyle(.secondary)
} else {
ForEach(controllers, id: \.self) { mac in
HStack {
Image(systemName: "gamecontroller")
.foregroundStyle(.secondary)
Text(mac)
.font(.system(.body, design: .monospaced))
}
}
}
} }
} }
} }
+169 -1
View File
@@ -337,7 +337,7 @@ auto chip::load_cr(cr_mode mode) -> int
return send_command(mcu_cmd::cmd_load_cr, payload, sizeof(payload)); 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<std::uint8_t> buf(4 + len); std::vector<std::uint8_t> buf(4 + len);
xone::store_le32(buf.data(), idx + mt_mcu_memmap_wlan); // Register offset in memory. 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; 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::uint8_t, 20>
{
std::array<std::uint8_t, 20> 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<std::uint16_t>(len_ctl));
return txwi;
}
auto chip::send_wlan(std::span<std::uint8_t const> frame) -> int
{
auto txwi = build_txwi(0xff, frame.size());
std::vector<std::uint8_t> 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<std::uint8_t const> 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<std::uint8_t> 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<std::uint8_t>(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<std::uint8_t const> 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<std::uint8_t const> addr,
client_cmd cmd,
std::span<std::uint8_t const> 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<std::uint8_t> 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<std::uint8_t>(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<std::uint8_t const> 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<std::uint8_t>(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 } // namespace xone::mt76
+24
View File
@@ -117,6 +117,29 @@ void test_beacon_layout(void)
TEST_ASSERT_EQUAL_UINT32(sizeof(xone::mt76::broadcast_address), 6); 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<std::uint32_t>(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) int main(void)
{ {
UNITY_BEGIN(); UNITY_BEGIN();
@@ -127,5 +150,6 @@ int main(void)
RUN_TEST(test_radio_registers); RUN_TEST(test_radio_registers);
RUN_TEST(test_channel_table); RUN_TEST(test_channel_table);
RUN_TEST(test_beacon_layout); RUN_TEST(test_beacon_layout);
RUN_TEST(test_client_association_layout);
return UNITY_END(); return UNITY_END();
} }