Phase 4 foundation, independent of the presentation mechanism: - hid/hid_report.hpp: standard Xbox One report layout (64-byte input: buttons/guide/triggers/sticks; 64-byte output: motors), the HID report descriptor, and GIP state to report mapping. - make_input_report(): remaps GIP button bits to the standard Xbox bitmask, scales 10-bit triggers to 8-bit, sticks pass through. - parse_output_report(): extracts motor intensities from output reports for the rumble relay; malformed reports stop the motors. - test_hid: unit tests for the mapping, scaling, and descriptor. Report packing verified with a descriptor walker: input and output are exactly 64 bytes each. Next step is presentation via DriverKit, which needs a Developer ID with the DriverKit entitlement. Co-Authored-By: qwen3.8-27b@q3_k_xl: implemented HID report layer
16 KiB
Xbox Wireless Dongle: macOS Port
Goal
User-space macOS app that speaks to the Xbox Wireless Dongle (MT76xx chip) and exposes connected controllers as HID gamepads.
Architecture
┌──────────────────────────────────────────────────────┐
│ macOS App (Swift/C++) │
│ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │
│ │ App/UI │ │ HID Proxy│ │ Core Audio (opt) │ │
│ └────┬─────┘ └────▲─────┘ └──────────────────┘ │
│ │ │ │
│ ┌────▼──────────────▼──────────────────────┐ │
│ │ GIP Protocol Layer │ │
│ │ (bus/protocol.c: ported, pure C) │ │
│ └──────────────────┬───────────────────────┘ │
│ │ │
│ ┌──────────────────▼───────────────────────┐ │
│ │ MT76 Chip Protocol Layer │ │
│ │ (transport/mt76.c: ported, USB calls) │ │
│ └──────────────────┬───────────────────────┘ │
│ │ │
│ ┌──────────────────▼───────────────────────┐ │
│ │ USB Transport Layer (new) │ │
│ │ (IOKit / IOUSBInterfaceInterface) │ │
│ └──────────────────────────────────────────┘ │
│ │ │
│ ┌──────────────────▼───────────────────────┐ │
│ │ Xbox Wireless Dongle │ │
│ │ VID:045E PID:02FE (02E6, 02F9, 091E) │ │
│ └──────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────┘
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, radio init (registers, crystal, MAC/BSSID, channel eval, beacon), pairing mode, controller association, GIP handshake + auth. Verified end-to-end on hardware: live controller input is shown in the app (RX data path) and rumble TX works from the app's Rumble button.
- C API + Swift app: async session (fast probe; firmware + radio on a worker thread), state display (idle/starting/ready/error), controller list with live input monitor, battery level, and a rumble test button.
- Phase 4 (HID) started: standard Xbox One report layout, HID report
descriptor, and GIP-to-report mapping implemented in
src/hid/and unit tested. Remaining: present the reports as a virtual device via a DriverKit extension (needs a Developer ID with the DriverKit entitlement).
The dongle LED is driven at chip level for pairing and client lifecycle. Phase 3 is complete. Next: Phase 4 (virtual HID gamepad) and Phase 5 (app polish); exposing the controller as a macOS HID gamepad is the main user-visible gap.
Directory Layout
macos/
├── PLAN.md ← this file
├── src/
│ ├── usb/ ← IOKit USB transport (new)
│ ├── mt76/ ← MT76 chip protocol (port from transport/mt76.c)
│ ├── gip/ ← GIP protocol (port from bus/protocol.c)
│ ├── auth/ ← Auth + crypto (port from auth/)
│ ├── hid/ ← Virtual HID gamepad (new)
│ ├── cli/ ← C++ CLI for manual protocol sequences
│ └── app/ ← macOS app entry point + UI (new)
├── include/
│ ├── common/ ← Shared types, platform abstraction
│ ├── usb/
│ ├── mt76/
│ ├── gip/
│ ├── auth/
│ └── hid/
├── scripts/
│ └── download-firmware.sh ← Port of install/firmware.sh
├── firmware/ ← Downloaded firmware binaries (gitignored)
└── build/ ← Build output (gitignored)
Implementation Phases
Phase 1: Foundation (Linux → C library)
Extract the protocol logic from Linux kernel code into standalone C.
src/gip/
- Port
bus/protocol.c→ pure C, no kernel deps - Redefine
__le16,__packed,guid_tfor user-space - Replace
kzalloc/kfree→malloc/free - Replace
spin_lock_irqsave→ pthread mutex or lock-free ring buffer - Replace
dev_dbg/gip_err→ NSLog or custom logger - Replace
EXPORT_SYMBOL_GPL→ nothing (static library)
src/auth/
- Port
auth/auth.c+auth/crypto.c→ AES-CCMP, authentication handshake - Should be mostly copy-paste, these are pure crypto
include/common/
- Platform types (
stdint.hbased) - Endianness helpers
- Memory allocator abstraction (so we can swap malloc if needed)
Phase 2: USB Transport (new)
src/usb/
- IOKit device matching on
USB\VID_045E&PID_02FE(and other PIDs) - Open device, claim interface (bInterfaceNumber = 1)
IOUSBDevInterfacefor control transfers (vendor requests)IOUSBInterfaceInterfacefor bulk endpoints:- EP 0x04 IN: WLAN data (802.11 frames from controllers)
- EP 0x04 OUT: Bulk out (commands to chip)
- EP 0x05 IN: MCU commands (firmware load responses)
- Async completion callbacks → callback/dispatch queue
- Device disconnect handling (chip reconnects during firmware load)
Key IOKit APIs:
IOServiceMatching("IOUSBDevice"): find dongleIOUSBDeviceOpen/IOUSBInterfaceOpen: claimDeviceRequest: control transfers (register R/W)WritePipe/ReadPipe: bulk transfersCreateInterruptEndpoint: for EP 0x05 (MCU)
Phase 3: MT76 Chip Protocol (port)
src/mt76/
- Port
transport/mt76.c→ replace USB calls with Phase 2 transport - Firmware loading: send binary in 0x3800-byte chunks, poll for completion
- EFUSE read: MAC address, chip ID, crystal trim, TX power calibration
- Radio init: ~80 hardcoded register writes (AGC, EDCA, TX power, protection)
- Channel evaluation: cycle through 12 channels, pick highest power
- Beacon transmission: 802.11 beacons with Microsoft OUI IE
- Pairing mode: rotate channels every 2s when pairing enabled
- Controller association: 802.11 assoc, WCID assignment, AES-CCMP encryption
- Data path: QoS data frames to/from controllers
Key replacement map:
usb_control_msg()→IOUSBDevInterface->DeviceRequest()usb_bulk_msg()→IOUSBInterfaceInterface->WritePipe()/ReadPipe()kzalloc/skb_put→malloc/buffer managementmsleep/mdelay→usleep/nanosleepieee80211_*→ custom 802.11 frame builderscfg80211_*→ nothing (no regulatory domain reporting needed)
Phase 4: Virtual HID Gamepad (in progress)
src/hid/
- Expose controller as macOS HID device so games work natively
- Options:
- HID Proxy Driver (DriverKit): Maps USB device to virtual HID. Minimal kernel code. Preferred approach.
- IOHIDSystem user-space: Create virtual HID device entirely in user-space. May not work for all games.
- Gamepad wrapper: Lower-level, translate input events to HID reports.
- Map Xbox controller buttons/sticks/triggers to standard Xbox One HID
report descriptor: done.
hid/hid_report.hppdefines the 64-byte input/output report layout and the HID report descriptor; GIP state maps to input reports viamake_input_report()(unit tested). - Handle force feedback (rumble): send back to dongle via GIP. TX is done
(Phase 3); output-report parsing for the relay is implemented in
parse_output_report(). - Battery status reporting
- Remaining: present the reports as a virtual device (DriverKit extension). Needs a Developer ID with the DriverKit entitlement to build and load.
Phase 5: macOS App (new)
src/app/
- Swift or C++ main app
- Device discovery (IOKit notification when dongle plugged in)
- Pairing UI (LED blink, controller button press)
- Status display (connected controllers, battery)
- Preferences (channel selection, LED mode)
- 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:
process_buffer: take the raw IN buffer.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.
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.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).
- DATA|QOS_DATA: feed the client's GIP adapter (
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 viasend_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 (+n8), KEY base 0x8000 (+n32, len 16), IV base 0xa000 (+n8), ATTR base 0xa800 (+n4); 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
1. Detect dongle via IOKit
2. Download/verify firmware (firmware.sh)
3. Load firmware into chip (bulk transfer, 0x3800 byte chunks)
4. Chip disconnects + reconnects (handle this!)
5. Read EFUSE (MAC, chip ID, TX power cal)
6. Initialize radio (~80 register writes)
7. Evaluate channels (find best channel)
8. Start beacon transmission
9. Enter pairing mode (channel rotation)
10. When controller associates:
a. Parse 802.11 assoc request
b. Assign WCID (client ID)
c. Set up AES-CCMP encryption
d. Send assoc response
11. GIP handshake:
a. Controller ANNOUNCE (vendor/product/fw version)
b. Host sends IDENTIFY request
c. Controller sends IDENTIFY (capabilities, interfaces, HID descriptor)
d. Auth handshake (AES-CCMP key exchange)
e. Status reports begin (battery, connected)
12. Game loop:
a. Controller → HID reports (input)
b. Host → rumble, LED, audio control
Firmware
Firmware binaries are downloaded from Microsoft Windows Update driver catalog:
| PID | Dongle Type | Firmware File |
|---|---|---|
| all | Supported dongles | xow_dongle.bin |
Downloaded via scripts/download-firmware.sh (port of install/firmware.sh).
Known Challenges
-
USB timing: MT76 is timing-sensitive. User-space USB on macOS may have different latency than Linux kernel URBs. May need careful tuning of
usleepvalues. -
Chip reconnect: During firmware load, the dongle disconnects and reconnects. IOKit needs to handle this gracefully (close, wait, reopen, re-claim).
-
Virtual HID: Games expect a real HID device. HID Proxy Driver (DriverKit) is the cleanest path but requires a minimal kernel extension.
-
5GHz regulatory: The dongle uses 5GHz channels. macOS may have regulatory restrictions. May need to limit to 2.4GHz only.
-
Audio: Headset audio is complex (real-time PCM streaming). Lower priority, tackle after gamepad works.
Source Files to Port
| Linux source | Target | Notes |
|---|---|---|
transport/mt76.c |
src/mt76/mt76.c |
Replace USB calls, remove cfg80211 |
transport/mt76.h |
include/mt76/mt76.hpp |
Clean up kernel types |
transport/mt76_defs.h |
include/mt76/mt76_defs.hpp |
Mostly copy (register defs) |
bus/protocol.c |
src/gip/protocol.c |
Replace kernel alloc/lock/debug |
bus/protocol.h |
include/gip/protocol.hpp |
Clean up kernel types |
bus/bus.c |
src/gip/bus.c |
Client lifecycle management |
auth/auth.c |
src/auth/auth.c |
Pure crypto, mostly copy |
auth/auth.h |
include/auth/auth.hpp |
Copy |
auth/crypto.c |
src/auth/crypto.cpp |
AES-CCMP, mostly copy |
auth/crypto.h |
include/auth/crypto.hpp |
Copy |
driver/gamepad.c |
N/A | Replaced by HID layer |
Build System
- CMake or Xcode project: either works
- Static library for protocol/auth layers
- macOS app bundle for the final product
- Minimum macOS: 12.0 (Monterey): for modern IOKit/DriverKit support
Dependencies
- IOKit (system)
- CoreFoundation (system)
- HID Proxy Driver framework (optional, for virtual gamepad)
- Core Audio (optional, for headset support)
- No external dependencies for protocol/auth layers