Files
xone_macos/PLAN.md
T
portersky 73c026218d docs: mark rumble TX verified on hardware
Rumble works from the Rumble button in the app; Phase 3 is complete.
Next up is Phase 4 (virtual HID gamepad).

Co-Authored-By: qwen3.8-27b@q3_k_xl: marked rumble verified
2026-08-29 15:22:16 +02:00

15 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.

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_t for user-space
  • Replace kzalloc/kfreemalloc/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.h based)
  • 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)
  • IOUSBDevInterface for control transfers (vendor requests)
  • IOUSBInterfaceInterface for 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 dongle
  • IOUSBDeviceOpen / IOUSBInterfaceOpen: claim
  • DeviceRequest: control transfers (register R/W)
  • WritePipe / ReadPipe: bulk transfers
  • CreateInterruptEndpoint: 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_putmalloc/buffer management
  • msleep/mdelayusleep/nanosleep
  • ieee80211_* → custom 802.11 frame builders
  • cfg80211_* → nothing (no regulatory domain reporting needed)

Phase 4: Virtual HID Gamepad (new)

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 360/One HID report descriptor
  • Handle force feedback (rumble): send back to dongle via GIP
  • Battery status reporting

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:

  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 (+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

  1. USB timing: MT76 is timing-sensitive. User-space USB on macOS may have different latency than Linux kernel URBs. May need careful tuning of usleep values.

  2. Chip reconnect: During firmware load, the dongle disconnects and reconnects. IOKit needs to handle this gracefully (close, wait, reopen, re-claim).

  3. Virtual HID: Games expect a real HID device. HID Proxy Driver (DriverKit) is the cleanest path but requires a minimal kernel extension.

  4. 5GHz regulatory: The dongle uses 5GHz channels. macOS may have regulatory restrictions. May need to limit to 2.4GHz only.

  5. 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