490b8af585
Update PLAN.md and README.md current status: host to controller TX (rumble) is implemented and pending hardware confirmation; the dongle LED is driven at chip level, so no data path LED work remains. Co-Authored-By: qwen3.8-27b@q3_k_xl: updated status docs
347 lines
15 KiB
Markdown
347 lines
15 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).
|
|
- 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.
|
|
|
|
Host to controller TX over the data path (rumble) is implemented; the dongle
|
|
LED is driven at chip level for pairing and client lifecycle. Phase 3 is
|
|
complete pending hardware confirmation of rumble. Next: Phase 4 (HID) 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 (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 (+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
|