Files
xone_macos/PLAN.md
T
portersky 176a563400 docs: phase 4 uses userspace IOHIDUserDevice, not DriverKit
macOS has a user-space virtual HID device API (IOHIDUserDevice,
available since 10.15). No DriverKit extension is needed: the app
creates one virtual device per controller with our report descriptor.

Development works with SIP disabled and no Apple account (verified on
macOS 15.7 that creation fails with SIP enabled; dji-fpv3 and
ControlLab document the SIP-disabled dev workflow). Distribution with
SIP enabled requires the com.apple.developer.hid.virtual.device
entitlement from a paid Developer Program membership.

Co-Authored-By: qwen3.8-27b@q3_k_xl: researched virtual HID options
2026-08-29 15:49:30 +02:00

366 lines
16 KiB
Markdown

# 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 via the user-space
`IOHIDUserDevice` API (no DriverKit needed; development requires SIP
disabled, distribution requires the `com.apple.developer.hid.virtual.device`
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_t` for 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.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_put``malloc`/buffer management
- `msleep`/`mdelay``usleep`/`nanosleep`
- `ieee80211_*` → custom 802.11 frame builders
- `cfg80211_*` → 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
- Presentation mechanism: `IOHIDUserDevice` (IOKit, user-space virtual HID
device API, available since macOS 10.15). No DriverKit needed: the app
creates one virtual device per controller with our report descriptor,
pushes input reports from GIP state, and receives output reports for
rumble relay.
- Signing/entitlements:
- Development (SIP disabled): `IOHIDUserDeviceCreateWithProperties`
works without any Apple account. Verified on macOS 15.7: with SIP on,
creation fails; open-source projects (dji-fpv3, ControlLab) document
the SIP-disabled dev workflow.
- Distribution (SIP enabled): requires the
`com.apple.developer.hid.virtual.device` entitlement in a
provisioning profile (paid Apple Developer Program membership).
- Map Xbox controller buttons/sticks/triggers to standard Xbox One HID
report descriptor: done. `hid/hid_report.hpp` defines the 64-byte
input/output report layout and the HID report descriptor; GIP state maps
to input reports via `make_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: implement the `IOHIDUserDevice` wrapper in `src/hid/`, wire it
into the app, verify on hardware with SIP disabled.
### 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 (+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
```
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