Files
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

204 lines
9.9 KiB
Markdown

# Xbox Wireless Dongle: macOS Port
User-space macOS app for the Xbox Wireless Dongle (MediaTek MT76xx).
Ports the Linux kernel driver [`xone/`](../xone/) to macOS.
See [PLAN.md](PLAN.md) for full architecture and implementation phases.
---
## Building
Requires CMake ≥ 3.21, Xcode (or Command Line Tools), and Ninja.
Swift is only supported by the **Ninja** and **Xcode** generators in CMake,
so configure with one of them explicitly:
```sh
cmake -S . -B build -GNinja -DBUILD_TESTING=ON
ninja -C build
./build/xone_app
ninja -C build check # build and run the test suite
```
Layout (mirrors `../refix/`):
- `deps/`: CMake modules: `Platform.cmake`, `Flags.cmake`, `Sanitizers.cmake`,
`FindUnity.cmake`
- `src/{usb,mt76,gip,auth,hid}`: C++ protocol stack (ported from
`medusalix/xone`)
- `src/app/`: Swift app entry point + C ABI bridge (`include/app/xone_api.h`)
- `src/cli/`: C++ CLI (`xone_cli`) for manual protocol sequences
- `scripts/`: `download-firmware.sh` (port of the upstream firmware fetcher)
- `tests/`: Unity test suite (`BUILD_TESTING=ON`)
---
## Investigation Checklist
These items need research on macOS before implementation can begin.
Strike through or check off as each is resolved.
### 1. IOKit USB Access
- [x] **IOUSBLib vs IOUSBFamily**: Resolved: the SDK exposes only the
struct-based `IOUSBDeviceInterface` / `IOUSBInterfaceInterface` (v197/v190)
via `IOCreatePlugInInterfaceForService()` + `QueryInterface`. Implemented in
`src/usb/usb_transport.cpp`.
- [x] **USB device matching**: Class match on `IOUSBDevice` plus a user-space
VID/PID filter on the `idVendor`/`idProduct` properties (numeric registry
matching proved unreliable). Verified with a physical dongle (PID 0x02E6).
- [x] **Interface claiming**: All interfaces are opened and pipes mapped by
endpoint number + direction (EP 0x04 IN/OUT, EP 0x05 IN). Verified: opens
cleanly with no driver conflict.
- [x] **Async transfer latency**: Resolved: the `ReadPipeAsync` pump (4
outstanding reads per IN pipe, resubmission in the completion handler)
streams live controller input with no drops at game-report rates.
- [x] **Device reconnect handling**: Resolved: `kIOTerminatedNotification`
with a PID filter fires on unplug and chip re-enumeration. Verified
across firmware-load resets and repeated unplug/replug cycles.
### 2. Firmware Loading
- [x] **Firmware binary availability**: Resolved: `scripts/download-firmware.sh`
(port of `../xone/install/firmware.sh`) fetches the Windows Update CAB
images and the hashes match the Linux driver. Images land in `firmware/`.
- [x] **Firmware load sequence**: Resolved: ported to `src/mt76/mt76.cpp`
(control request to firmware mode, bulk transfer in 0x3800-byte chunks,
MCU completion poll, chip reset). Verified on hardware: FCE shows the
firmware running after load.
- [x] **Post-firmware reconnect**: Resolved: the chip re-enumerates with the
same VID/PID; the app and CLI close, wait, and reopen the device. The
full sequence runs end-to-end.
### 3. MT76 Register Access
- [x] **Vendor request format**: Resolved: the Linux values work unchanged via
`IOUSBDeviceInterface->DeviceRequest()` (bRequest 0x84/0x86 for register
R/W). All radio init traffic uses this path.
- [x] **Register timing**: Resolved: `usleep()` / `clock_nanosleep()` in
user-space are sufficient; radio init and channel evaluation complete
reliably.
- [x] **EFUSE read**: Resolved: the EFUSE sequence returns valid MAC address,
chip ID, and TX power calibration data on macOS (`xone_cli info`).
### 4. 802.11 Frame Handling
- [x] **Beacon construction**: Resolved: beacons with the Microsoft OUI
(00:50:f2) information element transmit correctly, including the chip
header before the 802.11 frame. Controllers hear them and associate.
- [x] **Frame encapsulation**: Resolved: TX/RX chip headers handled per the
Linux driver (`send_wlan` / `process_wlan`). Association, pairing, and
encryption-enable management frames verified against hardware.
- [x] **QoS data frames**: RX path verified: controller input QoS frames are
decrypted, fed to the GIP layer, and shown live in the app. Host to
controller TX (rumble) verified on hardware via the app's Rumble
button.
### 5. AES-CCMP Encryption
- [x] **Crypto backend choice**: CommonCrypto (SHA-256/HMAC), Security.framework
(RSA PKCS#1), and a self-contained P-256 ECDH (no public macOS C API for EC
key agreement). Implemented in `src/auth/crypto.cpp`.
- [x] **CCMP mode implementation**: Not needed on the host: AES-CCMP runs on the
MT76 chip; the host only installs keys via WCID registers
(`xone_mt76_set_client_key`).
- [x] **ECDH key exchange**: P-256 implemented and validated against
OpenSSL-derived test vectors (`tests/test_crypto.cpp`).
### 6. Virtual HID Gamepad
- [ ] **HID Proxy Driver feasibility**: Research Apple's [HID Proxy
Driver](https://developer.apple.com/documentation/coreaudio/hid_proxy_driver)
(DriverKit). Can we create a virtual Xbox controller that games recognize
natively?
- [ ] **IOHIDDevice user-space alternative**: Can we create a virtual HID
device entirely in user-space? Test with `IOHIDManager` and see if games
(Steam, Game Center) recognize it.
- [ ] **HID report descriptor**: Write an Xbox 360/One-compatible HID report
descriptor. Test with existing Xbox controller (via Bluetooth) to capture the
exact report format macOS expects.
- [ ] **Force feedback**: Can the virtual HID device receive rumble commands
from games and relay them to the controller via GIP?
### 7. Core Audio (Headset Support)
- [ ] **Audio Unit setup**: Test creating an `AURenderCallback` /
`AUOutputUnit` for headset playback and `AURecordingCallback` / `AUInputUnit`
for mic input.
- [ ] **Latency requirements**: The GIP protocol sends audio in 8ms intervals.
Can Core Audio maintain this latency without glitches?
- [ ] **Format negotiation**: The headset negotiates audio format (sample rate,
channels) via GIP. Map GIP audio formats to Core Audio
`AudioStreamBasicDescription`.
### 8. Build System
- [x] **CMake vs Xcode**: Resolved: CMake with the Ninja generator compiles
both the C++ stack and the Swift app (see Building). An Xcode project is
only needed later for a DriverKit extension, if Phase 4 goes that route.
- [ ] **Minimum macOS version**: Target 12.0 (Monterey) for modern
IOKit/DriverKit. Verify all APIs are available.
- [ ] **Code signing**: Development builds run unsigned on the local machine
with no entitlements needed for IOKit USB access. DriverKit
notarization and distribution signing deferred to the HID phase.
### 9. Regulatory / Legal
- [ ] **5GHz channel restrictions**: Channel evaluation picks a working
channel and pairing is verified on hardware; no regulatory failures seen
so far. Regional edge cases (blocked channels) remain untested.
- [ ] **Firmware license**: The firmware binaries are from Microsoft Windows
Update. Confirm they can be redistributed with the macOS port (the Linux
driver includes them with a disclaimer).
### 10. Testing Hardware
- [x] **Dongle**: Xbox Wireless Dongle (PID 0x02FE preferred, 0x02E6 also
works). In hand: PID 0x02E6 verified end-to-end.
- [x] **Controller**: Xbox One or Series X|S controller (for pairing and input
testing). In hand: pairing and live input verified.
- [ ] **Headset**: Xbox Wireless Headset (optional, for audio testing)
- [ ] **macOS machine**: Intel or Apple Silicon (test both if possible, IOKit
may differ)
---
## Quick Reference
| Linux API | macOS Replacement | Status |
| ------------------------- | ---------------------------------------- | ------------------------------------- |
| `usb_control_msg()` | `IOUSBDeviceInterface->DeviceRequest()` | ✅ Done (`send_vendor_request`) |
| `usb_bulk_msg()` | `WritePipe()` / `ReadPipeAsync()` | ✅ Done (`bulk_write`, reader thread) |
| `usb_submit_urb()` | `ReadPipeAsync()` + `CFRunLoopSource` | ✅ Done (reader thread) |
| `kzalloc` / `kfree` | `malloc` / `free` | ✅ Straightforward |
| `spin_lock_irqsave` | `std::mutex` / `std::condition_variable` | ✅ Done (`usb_transport.cpp`) |
| `msleep` / `mdelay` | `usleep()` / `clock_nanosleep()` | ✅ Done (radio init, channel eval) |
| `crypto_shash_*` | CommonCrypto / Security.framework | ✅ Done (`auth/crypto.cpp`) |
| `input_register_device()` | HID Proxy Driver / IOHIDSystem | ☐ Investigate |
| `snd_pcm_*` | Core Audio (Audio Units) | ☐ Investigate |
| `request_firmware()` | File I/O (`fopen`/`fread`) | ✅ Straightforward |
| `cfg80211_*` | Nothing (no regulatory reporting) | ✅ Remove |
| `bus_register()` | Custom client management | ✅ Redesign |
| `device_create()` / sysfs | Nothing (no sysfs) | ✅ Remove |
---
## Current Status
Working end-to-end on hardware:
- Dongle probe/open, async read pump, vendor register R/W
- Firmware download (`scripts/download-firmware.sh`) and load
- EFUSE read, radio init, channel evaluation, beacon TX
- Pairing mode, controller association, GIP handshake + auth
- Live controller input (buttons, sticks, triggers) in the Swift app
- Rumble test button (host to controller TX over the GIP data path)
Not done yet: virtual HID gamepad (Phase 4), headset audio (Phase 5).
`xone_cli` exercises the stack manually in a single dongle session:
```sh
./build/xone_cli info firmware firmware/xow_dongle.bin radio-init pair
```