diff --git a/.gitignore b/.gitignore index 5eb13e18..cb502ec9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,11 @@ Firmware/.vscode Firmware/RP2040/build +Firmware/RP2040/build_* + +# Build dirs created by scripts/build.sh and scripts/build_all_boards.sh +scripts/build +scripts/build_* +Firmware/RP2040/scripts Firmware/RP2040/.ignore Firmware/RP2040/src/USBDevice/DeviceDriver/XboxOG/tud_xid/tud_xid_xremote_rom.h Firmware/RP2040/src/BLEServer/att_delayed_response.h @@ -10,4 +16,15 @@ Firmware/ESP32/components/btstack Firmware/ESP32/sdkconfig.old Firmware/external/pico-sdk Firmware/external/picotool -Tools/dvd-dongle-rom.bin \ No newline at end of file +Tools/dvd-dongle-rom.bin + +# Python +**/__pycache__/ +*.py[cod] + +# Local docs (not for GitHub) +Firmware/RP2040/docs/XInput_joypad-os_comparison.md +Firmware/RP2040/docs/BLUEPAD32_PS4_PS5_PARSING.md +docs/Input_Controllers_Research.md +docs/Other_Projects_Output_Modes.md +docs/Wired_Retro_Controllers.md \ No newline at end of file diff --git a/.gitmodules b/.gitmodules index 55128a87..ac54d661 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,9 +1,9 @@ [submodule "Firmware/external/bluepad32"] path = Firmware/external/bluepad32 - url = https://github.com/ricardoquesada/bluepad32.git + url = https://github.com/MegaCadeDev/bluepad32.git [submodule "Firmware/external/tinyusb"] path = Firmware/external/tinyusb - url = https://github.com/hathach/tinyusb.git + url = https://github.com/MegaCadeDev/tinyusb.git [submodule "Tools/dump-dvd-kit"] path = Tools/dump-dvd-kit url = https://github.com/XboxDev/dump-dvd-kit.git diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..0075052e --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,135 @@ +# Changelog / Version history + +Features and fixes added in this fork. For the latest firmware improvements (PS3, XInput, latency, **SteamOS / Bazzite (STEAM mode — DualSense emulation + touchpad mouse)**, **Pico W Bluetooth / DS4 Classic BT**, **Xbox Series BLE disconnect reboot**, **Switch 2 Pro + Joy-Con 2 BLE**, **Steam Controller 2026 (Triton) BLE**, **DualShock 3 USB→BT auto-pair (`0xF5`)**, **Waveshare RP2350-USB-A wired host fixes (Switch Pro, PS3, Xbox 360 receiver, Razer Atrox XBO)**, **PS3 / PS4 motion passthrough**, etc.), see [Firmware/RP2040/docs/IMPROVEMENTS.md](Firmware/RP2040/docs/IMPROVEMENTS.md). + +--- + +### Version 1.0.0.13a + +- **Steam Controller 2026 (Triton) BLE — pair/input restored** — After DIS, Triton was stuck in HOGP (`hids_host_connect`) and never reached input. Match SDL/Android: skip standard HID-over-GATT and use Valve’s proprietary GATT service (`100F6C32-…`), enable notifications on input `0x45`/`0x47`, and resolve the connection handle from GATT packets (not the unused `channel` arg). Also quiet BR/LE scan during BLE connect and raise HID descriptor / HIDS report limits. **Files:** `uni_hid_parser_steam_triton.c`, `uni_bt_le.c`, `Bluepad32.cpp`, `uni_hid_device.h`, `btstack_hids_num_reports.diff`, `patch_libs.cmake`. + +--- + +### Version 1.0.0.12a + +- **DualShock 3 — USB → Bluetooth auto-pair restored** — Wiring a **DS3** into the adapter USB host again programs the pad with the Pico’s **BD_ADDR** (**HID feature `0xF5`**, same as sixaxispairer) so you can unplug and press **PS**. The PIO USB sync init path had marked init **DONE** before the old async **GET 0xF2** pair chain ran, so **`0xF5` never sent**. Fix: send **`0xF5` synchronously** right after **0xF4** + LED OUT (still deferred if the BT address is not ready yet). **Files:** `PS3.cpp` / `PS3.h` (USB host). See [IMPROVEMENTS.md](Firmware/RP2040/docs/IMPROVEMENTS.md) (*DualShock 3 — automatic USB programming for Bluetooth pairing*). +- **Steam Controller 2026 (Triton) — Bluetooth LE** — New Bluepad32 parser so the **Steam Controller 2026** (`28de:1303` BLE, also registers `1302`/`1304`/`1305`) can pair with Pico W / Pico 2 W / **RP2354** BT adapters. Parses HID state report **`0x45`**, disables lizard mode, maps sticks/triggers/buttons, dual rumble (`0x80`). **Pairing:** hold **RB + B + Steam**; remove/disconnect the pad from PC/phone Bluetooth first (one host only). **LE Secure Connections** required (legacy SMP failed with `AUTH_REQUIREMENTS_MISMATCH`) — enabled in `btstack_config.h`; BLE pads (except Xbox/Switch 2) use **SC + Bonding**. **Latency:** request BLE connection interval **~7.5–11 ms** on connect. **Buttons:** Triton **View → Start**, **Menu → Back/Select** (SDL-style; Valve labels are not Xbox-style), **Steam → Guide/SYS**. **Files:** `uni_hid_parser_steam_triton.*`, `btstack_config.h`, `uni_bt_le.c`, `uni_controller_list.h`, `uni_hid_device.c`, `Bluepad32.cpp`. See [IMPROVEMENTS.md](Firmware/RP2040/docs/IMPROVEMENTS.md) (*Steam Controller 2026 — Bluetooth*). +- **Switch 2 Pro — anti-deadzone drift and L3/R3** ([#64](https://github.com/MegaCadeDev/OGX-Mini-2026/issues/64)) — WebApp **Anti-Deadzone Circular/Square** amplified imperfect Switch 2 Pro stick centers into constant **bottom-left drift**. Idle readings are now gated with a small noise floor when anti-dz is active (without weakening the anti-dz curve). Circular anti-dz resize formula matched the WebApp (was incorrectly using `anti_dz_circle` instead of `anti_dz_square`). **L3/R3** stick clicks mapped to the correct physical sticks on wired Switch 2 Pro. **Follow-up:** inner-deadzone gate now uses **raw** stick magnitude (matches WebApp); adaptive idle gate scales with anti-dz strength; profile joystick settings stay enabled whenever the saved profile customizes sticks/triggers. **Switch 2 wired disconnect:** bulk **keepalive** every **2 s** plus **USB disable-timeout** after bulk bring-up (Switch 2 skipped Switch 1 init that sent this). **PIO USB** host unmount reboot is **debounced 1.5 s** so transient umount on Feather/Pico does not tear down the USB device stack. **Files:** `Gamepad.h`, `JoystickSettings.*`, `SwitchPro.cpp`, `Standard.cpp`, `Switch2ProHost.cpp`. +- **RP2354 — Bluetooth (Pi Radio Module 2)** — **`OGXM_BOARD=RP2354`** now uses the **Pico 2 W** board profile (`pico2_w` / CYW43439): **wireless gamepads** (Bluepad32 — DS4, DualSense, Switch Pro/2, Xbox Series, etc.) plus **PIO USB host** on **GP0/GP1** for wired pads or Wii mode. Same firmware path as **Pico W / Pico 2 W**. **Files:** `CMakeLists.txt`, `Config.h`, `OGXMini.cpp`, `PicoW.cpp`, `TaskQueue.*`. +- **STEAM mode — touchpad mouse only** — Removed the **right-stick mouse** fallback (**Start + Select/Back** / **LT/RT** clicks). The HID mouse interface moves only when the input controller has a **touchpad** (DualSense). Other pads still emulate DualSense as gamepad-only. **Files:** `Steam.cpp`; removed `SteamStickMouse.*`. +- **Switch mode — rumble passthrough** — Switch / Switch 2 console **HD rumble** (output reports **0x01** / **0x10** / **0x11**) was acknowledged but never written to **`PadOut`**, so Bluetooth pads (**DualSense**, **Xbox One S**, etc.) never vibrated. Decode HD rumble amplitude and forward to the wireless pad. **Files:** `Switch.cpp`, `Switch.h`. +- **XInput / Xbox 360 — stock stick feel** ([#38](https://github.com/MegaCadeDev/OGX-Mini-2026/issues/38)) — Modern pads (especially **Xbox Series over Bluetooth**) felt too twitchy vs a real 360 stick. In **XInput mode**, when the WebApp has **not** customized sticks, apply a **360-like** default: **~8–9% remapped inner deadzone**, response **curve 0.35** (quieter center / steeper outer), and **circular clamp** (`uncap_radius` off). WebApp custom stick settings still override. **Files:** `JoystickSettings.*`, `Gamepad.h`, board `initialize()` paths. +- **PS3 / PS4 output — motion passthrough** — **PS3** and **PS4 (DualShock 4 USB)** output modes forward **accelerometer** data (and **gyro** where the input pad provides it) into the emulated report. Supported **input** pads include **DualShock 4** and **DualSense** (**Bluetooth** on Pico W / Pico 2 W and **wired USB** host), **Switch Pro** / **Switch 2 Pro** (**Bluetooth** and **wired USB**), and **Wii Remote** (**Bluetooth** — accel report **0x31** / **0x35** requested automatically when motion output is active; Nunchuk stick maps to left stick). Motion is remapped into the DS4/Sixaxis playing frame; Wii Remote orientation is tuned for **IR end toward the TV**. **Switch** output does **not** pass through motion for now. **Console use:** OGX-Mini does **not** plug directly into a **PlayStation 3** or **PlayStation 4** for motion titles — you need an **additional USB adapter** between the console and OGX-Mini. **Tested with [Brook Wingman XE 2 Converter](https://www.brookaccessories.com/products/wingman-xe2).** The same setup works on **PS4** (select **PS4 output mode** on OGX-Mini; the Brook presents it to the console). **Files:** `MotionImu.h`, `Gamepad.h`, `PS3.cpp`, `PS4.cpp` (USB device), `Bluepad32.cpp`, `SwitchPro.cpp`, `Switch2ProHost.cpp`, `PS4.cpp` / `PS5.cpp` (USB host), `MotionOutputActive.h`, `DeviceManager.cpp`. See [IMPROVEMENTS.md](Firmware/RP2040/docs/IMPROVEMENTS.md) (*PS3 / PS4 output — motion passthrough*), [Controller_Mappings.md](Firmware/RP2040/docs/Controller_Mappings.md), and [README — Motion controls](README.md#ps3--ps4-motion-controls). +- **Pico W / Pico 2 W — Bluetooth reconnect after disconnect** — After a wireless pad fully connected (**device ready**) and then disconnected (**Home**, **Start+Select**, or link drop), **in-place BLE reconnect** (especially **Xbox Series**) often failed: LED solid or off, no rumble, no inputs, until the Pico was unplugged. Bonded re-encryption and leftover HIDS state do not match the working first-connect path. **Fix:** when the **last** ready BT controller disconnects, restore **pairing mode** immediately (LED blink + BT scan), then **watchdog reboot** after **~500 ms** so the next pair is identical to a fresh plug-in. Failed pair attempts do **not** reboot (avoids reboot loops while scanning). **Files:** `Bluepad32.cpp` (`device_disconnected_cb`, `device_ready_cb`), `board_api.cpp` (`watchdog_reboot`). See [IMPROVEMENTS.md](Firmware/RP2040/docs/IMPROVEMENTS.md) (*Pico W / Pico 2 W — Bluetooth disconnect reboot for clean reconnect*). +- **Pico W / Pico 2 W / RP2354 BT — USB resume restores pairing** — When the adapter stays plugged into a host that **USB-suspends** in standby (e.g. **Xbox 360** soft shutdown), Bluetooth **BR/LE scan** could stop even though no controller was connected, leaving the LED solid and no pairing. **`tud_resume_cb`** now restores pairing scans on wake (no full reboot). A **45 s idle watchdog** on the BT core restarts scans if they die during long suspend. **Files:** `Bluepad32.cpp`, `tud_callbacks.cpp`. + +--- + +### Version 1.0.0.11a + +- **PIO USB host — SOF servicing and teardown (all PIO host boards)** — PIO USB has no hardware SOF; wired IN/OUT stalls without periodic **`pio_usb_host_frame()`**. **Standard** path (RP2350-USB-A, Pico, Feather, RP2350-Zero, etc.): Core1 runs a **~1 kHz repeating hardware timer** plus **`pio_usb_host_frame()`** in the main host loop; **`skip_alarm_pool`** on PIO config avoids timer pool conflicts. **`HostManager::send_feedback()`** calls **`pio_usb_host_frame()`** after OUT on **every** `CONFIG_EN_USB_HOST` build (previously Pico W only). **`board_api_usbh::stop_pio_usb_host()`** stops the SOF timer, resets Core1, and **`tuh_deinit`** before USB device disconnect/reboot. **Button-combo output mode change** sets **`s_driver_reboot_pending`** so **`host_mounted(false)`** does not queue a spurious second disconnect reboot during an intentional driver switch. **Files:** `Standard.cpp`, `HostManager.h`, `board_api.cpp`, `board_api_usbh.cpp`. See [IMPROVEMENTS.md](Firmware/RP2040/docs/IMPROVEMENTS.md) (*PIO USB host — wired connection fixes*, shared HostManager section). +- **Waveshare RP2350-USB-A — wired Switch Pro (PIO USB host)** — Primary validation on **Waveshare RP2350-USB-A** (`OGXM_BOARD=RP2350_USB_A`); driver code is **shared** with all **PIO USB host** builds (`CONFIG_EN_USB_HOST`). **Switch 1 Pro** (PID **0x2009**): replaced Bluetooth-style **0x11** init with **Chromium-style wired USB** sequence — report **0x80** (MAC → handshake → baud → handshake → disable timeout) then **0x12** + subcommand **0x33** probe, LED / full report / IMU subcommands; non-blocking HID OUT with **`pio_usb_host_frame()`** in the feedback path; init advances per IN report with **timeout fallback**; **D-pad** (UP↔DOWN, LEFT↔RIGHT) and **L3↔R3** mapping fixes. **Switch 2 Pro** (PID **0x2069**): **17-packet bulk OUT** bring-up on **USB config 1 / interface 1** (~**12 ms** between packets, same sequence as `Tools/controller_capture/switch2_usb_init.py` / HandHeldLegend ProCon 2 enabler); after bulk completes the pad goes straight to **DONE** — **no** Switch 1 **0x80** HID init; **Switch2ProHost** decodes report **0x09**. **Hot-swap / unplug:** **`disconnect_cb`** cancels bulk bring-up and aborts endpoints; async bulk callbacks resolve the driver by **`(address, instance)`** via **`HostManager::get_switch_pro_host()`** so swapping Switch 1 ↔ Switch 2 Pro does not corrupt state. **Bluetooth** (Pico W / Pico 2 W) unchanged. Protocol references: [Chromium gamepad](https://chromium.googlesource.com/chromium/src/+/main/device/gamepad/) (Switch wired USB init), [HandHeldLegend procon2tool](https://github.com/HandHeldLegend/procon2tool), local **`Tools/controller_capture/switch2_usb_init.py`**. See [IMPROVEMENTS.md](Firmware/RP2040/docs/IMPROVEMENTS.md) (*PIO USB host — wired connection fixes*) and [Wired_Controllers.md](Firmware/RP2040/docs/Wired_Controllers.md). +- **DualShock 3 / PS3 controller — wired USB host (PIO USB)** — **DualShock 3** over **USB cable** now connects reliably on PIO USB host boards (validated on **RP2350-USB-A**). Replaced gadget-style default OUT report with **USB Host Shield** host layout; **synchronous init** sends **SET feature 0xF4** `{0x42,0x0c,0,0}` (enable USB reports) then a **player LED OUT** report, with **`pio_usb_host_frame()`** polled during control transfers so init completes under PIO USB. **1 Hz keepalive OUT** on PIO USB (`KEEPALIVE_MS`) so the pad stays connected when idle; **`HostManager`** calls **`send_feedback()`** for PS3 on the feedback timer. **Bluetooth auto-pair** (feature **0xF5**) on Pico W / Pico 2 W / RP2354 BT builds runs as a **sync SET** after that wired init (see **1.0.0.12a** restore note). **File:** `PS3Host.cpp`. See [IMPROVEMENTS.md](Firmware/RP2040/docs/IMPROVEMENTS.md) (*DualShock 3 — wired USB host*). +- **Xbox 360 wireless USB receiver — wired host (PIO USB)** — **Microsoft PC wireless receiver** (`045e:0719`) + **Xbox 360 wireless pad** sync and input fixed on PIO USB host when **`MAX_GAMEPADS=1`**. **`OGXM_TUH_XINPUT_INSTANCES=4`** when `MAX_GAMEPADS < 4` so all **four RF ports** are polled; **`HostManager`** shares one **`Xbox360WHost`** driver across receiver interfaces and routes **`process_report` / connect / disconnect** to the live driver. **`tuh_xinput`:** **`prime_port_for_pairing()`** sends **RUMBLE_ENABLE + LED** on idle ports; controller-present detection uses **byte1 bit 7 (`0x80`)** (not headset-only **0x40**); accepts report size **0x13** or **0x14**; removed **1 s sleep** on connect; **`service_wireless_ports()`** from feedback timer. **`Xbox360WHost`:** byte-offset decode for wireless input reports; tracks **active instance** for rumble. **Files:** `Config.h`, `HostManager.h`, `tuh_xinput.cpp`, `Xbox360W.cpp`. See [IMPROVEMENTS.md](Firmware/RP2040/docs/IMPROVEMENTS.md) (*Xbox 360 wireless receiver — wired USB host*). +- **Pico W / Pico 2 W — wired unplug watchdog (360 receiver)** — **No-input** unplug timeout raised from **~3 s** to **~10 s** so a **360 wireless PC receiver** plugged into the adapter’s PIO USB host port (which keeps sending status IN even with no pad paired) is not mistaken for unplug and torn down while Bluetooth should stay available. **File:** `PicoW.cpp`. +- **Four-channel / ESP32 I2C — pad wire format** — **`I2CWirePadIn`** fixed **23-byte** payload for master/slave I2C **`PacketIn`** links; excludes **`Gamepad::PadIn` IMU fields** added for DS4/DS5/Switch Pro that broke the **32-byte** wire layout on **Four_Channel_I2C** and **ESP32_Bluepad32_I2C** boards. **Files:** `I2CWirePad.h`, `Four_Channel_I2C.cpp`, `ESP32_Bluepad32_I2C.cpp`. +- **Razer Atrox Xbox One — wired USB host (PIO USB)** — **Razer Atrox Arcade Stick** for **Xbox One** (`1532:0a00`) now connects and reports inputs on PIO USB host boards (validated on **RP2350-USB-A**). Vendor-class **GIP** interface opened via **`tuh_xinput`**; init follows **[XBOFS.win](https://github.com/OOPMan/XBOFS.win)** (`05 20 00 01 00` **POWER_ON**, wait for OUT, then **30-byte** IN read loop). Input uses the XBOFS byte layout (dpad byte 5, face/LT/RT on byte 22). **`start_xboxone()`** deferred **50 ms** after mount; **`service_gip()`** re-arms stalled IN only (no repeated **POWER_ON**, which caused power cycling). Standard **Xbox One** wired pads keep the **64-byte** Linux xpad GIP path. **Xbox 360 Atrox** (`24c6:5000`) unchanged — standard XInput with digital trigger bytes. **Files:** `XboxArcadeStick.h`, `tuh_xinput.cpp`, `XboxOne.cpp`, `HostManager.h`. See [IMPROVEMENTS.md](Firmware/RP2040/docs/IMPROVEMENTS.md) (*Razer Atrox Xbox One — GIP arcade stick*) and [Wired_Controllers.md](Firmware/RP2040/docs/Wired_Controllers.md). +- **SteamOS / Bazzite (STEAM output mode)** — New USB output mode for **Linux desktop** / **Steam Deck desktop**: enumerates as **Sony DualSense** (`054c:0ce6`) with a second **HID mouse** interface. **Select:** **Start + Left Bumper + D-pad Up** (~3 s), web app **SteamOS / Bazzite**, or `-DOGXM_FIXED_DRIVER=STEAM` (build-script fixed mode). **DualSense emulation:** passthrough of real PS5 reports when input is DualSense (BT or wired USB host); other controllers get a synthesized DualSense report (face/shoulder/trigger/stick/D-pad/Share/Options/PS mapping). **Touchpad → mouse:** DualSense touchpad drag moves the cursor; tap = left click. Controllers without a touchpad are gamepad-only (no stick-mouse fallback). **Files:** `Steam.cpp`, `SteamPassthrough.h`, `SteamBtReport.h`, `SteamTouchpad.cpp`, `Descriptors/PS5Usb.h`, `Descriptors/Steam.h`, `Bluepad32.cpp`, `PS5.cpp` (wired host), `userSettings.js`. See [README — SteamOS / Bazzite](README.md#steamos--bazzite-output-mode), [IMPROVEMENTS.md](Firmware/RP2040/docs/IMPROVEMENTS.md#steam-mode--steamos--bazzite-linux-desktop), and [Controller_Mappings.md — STEAM](Firmware/RP2040/docs/Controller_Mappings.md#steamos--bazzite-steam-mode). + +--- + +### Version 1.0.0.10a + +- **Nintendo Switch 2 — wireless on Pico W / Pico 2 W** — New **Bluepad32 BLE GATT client** (`uni_hid_parser_switch2`) connects **Switch 2 Pro** (PID **0x2069**), **Joy-Con 2 Left** (**0x2067**), and **Joy-Con 2 Right** (**0x2066**) over **Bluetooth LE** (non-standard pairing; not Windows Bluetooth settings). **Joy-Con 2 L+R** merge into **one player** (pair **Left** first, then **Right** while left stays connected). Parses **63-byte** input notifications (composed button dword at offset **4**), maps buttons, sticks, motion, rumble, and **Home → PS / Guide (SYS)** with a short **Home latch** so single-frame BLE pulses are not lost before the USB output driver runs. **Rumble keepalive** on the vibration characteristic keeps the link up. **NSO GameCube** (PID **0x2073**) is registered but not yet confirmed. Protocol references: [Nadeflore/switch2-controllers](https://github.com/Nadeflore/switch2-controllers), [TommyWabg/switch2-controllers-windows10-gyro](https://github.com/TommyWabg/switch2-controllers-windows10-gyro), and community notes in [BlueRetro #1249](https://github.com/darthcloud/BlueRetro/issues/1249). See [IMPROVEMENTS.md](Firmware/RP2040/docs/IMPROVEMENTS.md) (*Nintendo Switch 2 — Bluetooth*). +- **Switch 1 Joy-Con (Classic Bluetooth) — L+R pair merge** — Original **Joy-Con (L/R)** (PIDs **0x2006** / **0x2007**) use the same **Left first, then Right** flow as Joy-Con 2. Merged input maps to one gamepad slot; solo Joy-Con keeps **BR/EDR inquiry** for the partner. Requires **`CONFIG_BLUEPAD32_MAX_DEVICES=2`** on single-player builds. See [IMPROVEMENTS.md](Firmware/RP2040/docs/IMPROVEMENTS.md) (*Nintendo Switch 2 — Bluetooth*, item 8). +- **Joy-Con pair latency (Switch 1 + Switch 2)** — Fixed lag and missed edges when **both** merged halves have buttons pressed at once. **Half-cache merge** (always merge from latest L and R state, emit once through primary), **2-slot** Bluetooth pad staging on Core1→Core0, **Switch 1:** IMU disabled when paired; **Switch 2:** deferred merge emit, reduced/alternating keepalive, Joy-Con–only stick parse, no hot-path raw UART logging. See [IMPROVEMENTS.md](Firmware/RP2040/docs/IMPROVEMENTS.md) (*Joy-Con pair merge — latency when both halves are active*). + +--- + +### Version 1.0.0.9a + +- **Nintendo Switch 2 Pro (USB PID 0x2069), wired** — **Switch2ProHost** now maps **face buttons, d‑pad, LB/RB, L3, R3, Start, Guide/SYS, Minus (Back), ZL/ZR as LT/RT** from the stable **10‑byte** input payload after report **0x09** (extended / IMU bytes unused to avoid flicker). **GL**, **GR**, and **Chat** are documented in code but intentionally unmapped. **Wired USB to the adapter only** (see [Wired_Controllers.md](Firmware/RP2040/docs/Wired_Controllers.md)). Debug: optional **`OGXM_SWITCH2_HID_RAW_LOG`** logs when the **three digital button bytes** change (UART). +- **Bluetooth (Pico W / Pico 2 W)** — **~1 second connection rumble** when a wireless controller reaches **device ready**, so you get haptic feedback that pairing completed. **DualShock 4** uses a **delayed start** before rumble (same spirit as the existing PS4 FF grace period). See [IMPROVEMENTS.md](Firmware/RP2040/docs/IMPROVEMENTS.md) (*Next version* and *§6. Connection rumble*). +- **PS3 mode on Windows PC** — **Host rumble** forwarded to the Bluetooth pad no longer **sticks on at idle**: deadzone on the **large motor** byte and **small motor on only when the host sends `1`** (DS3 semantics). See [IMPROVEMENTS.md](Firmware/RP2040/docs/IMPROVEMENTS.md) (*PS3 mode* item 7). +- **Pico W / Pico 2 W — PIO USB wired unplug** — Unplugging the gamepad from the adapter’s **USB host** port is detected reliably: **PIO-owned D+/D−** often never looks “disconnected” to GPIO/HCD alone. Firmware now uses **debounced** **HCD port status**, **no `tuh_mounted` device** (all TinyUSB address slots), and a **no-input** watchdog (**~3 s** without `process_report` / setup activity) before **`tuh_deinit`** and restoring **GPIO line IRQ** monitoring so **Bluetooth** can resume without power-cycling. See [IMPROVEMENTS.md](Firmware/RP2040/docs/IMPROVEMENTS.md) (*Pico W / Pico 2 W — PIO USB wired controller unplug detection*). +- **DualShock 3 — USB Bluetooth pairing on adapters with BT** — Plugging a **DS3** into the **USB host** port automatically sends **HID feature report `0xF5`** with this device’s **Bluetooth address** (same idea as [Bluepad32 sixaxispairer](https://bluepad32.readthedocs.io/en/latest/pair_ds3/)), so you can **unplug** and press **PS** to use the pad wirelessly. If the stack has not published the local BD_ADDR yet, pairing runs on a later report. **Note:** PIO USB wired-init in **1.0.0.11a** briefly broke this path; **1.0.0.12a** restores it with a **sync `0xF5`** after **0xF4** + LED. See [IMPROVEMENTS.md](Firmware/RP2040/docs/IMPROVEMENTS.md) (*DualShock 3 — automatic USB programming for Bluetooth pairing*). + +--- + +### Version 1.0.0.8a + +- **PS4 (DualShock 4) over Bluetooth (Pico W)** — **OGX-Mini BLE advertising** (phone setup app) runs continuously on the same **CYW43439** radio as **Classic BR/EDR** (DS4). That mix commonly **drops the DS4 link** right after the blue LED. **Fix:** **`gap_advertisements_enable(0)`** before DS4 HID setup and whenever any **ACL** gamepad is ready; **re-enable** when the last Classic pad disconnects (BLE-only pads e.g. DualSense unchanged). Still: **BR inquiry off** during ACL, **no DS4 virtual mouse**, **6 s** PS4 rumble grace. **Docs:** [README](README.md) (Wireless Bluetooth), [IMPROVEMENTS.md](Firmware/RP2040/docs/IMPROVEMENTS.md) (*Pico W / Pico 2 W — DualShock 4 and Classic Bluetooth*). +- **New controller support** — **Flydigi Vader 4 Pro** (DInput mode) added: VID `0xEED1`, PID `0x04B4` in `DINPUT_IDS` in `HardwareIDs.h`. A full list of supported wired controllers by driver type is in [Firmware/RP2040/docs/Wired_Controllers.md](Firmware/RP2040/docs/Wired_Controllers.md); the main [README](README.md) “Adding supported controllers” section explains how to get VID/PID to request more. +- **Bluetooth random disconnect fix (Pico W)** — The main loop on Pico W / Pico 2 W now uses **`sleep_ms(1)`** each iteration (matching [Team-Resurgent/OGX-Mini](https://github.com/Team-Resurgent/OGX-Mini)), so Core1 (BT stack) gets enough CPU time. This reduces random Bluetooth disconnects from Core0 starving the radio stack. (Separate from the **stall watchdog** below, which only disconnects when HID input **stops arriving** while the link still looks connected.) +- **Reconnect after disconnect (OG Xbox / BT)** — After the controller disconnects (e.g. idle), the dongle would show pairing mode but the controller could not reconnect without unplugging the dongle. When the last device disconnects, the platform now calls **`uni_bt_enable_new_connections_unsafe(true)`** so scanning is explicitly re-enabled and the controller can reconnect without power-cycling the dongle. +- **Xbox Series controller (1914) over BLE (Pico 2 W)** — Series pads often send **HID only when inputs change**, unlike 1708 (classic BT) which polls constantly. The **input-stall watchdog** (no HID for N seconds → force disconnect) was **firing while the pad was simply idle**, so the link dropped within ~1 minute and reconnect looked broken. **Fix:** **No stall disconnect for BLE Xbox** (`hids_cid` path). **Keepalive** (zero rumble write every **12 s**) remains so idle sessions stay alive. **Reconnect:** if the same Xbox BLE pad advertises again while a **disconnected-but-ready** slot still exists (before HCI teardown), Bluepad32 used to **ignore** the ad — **now** that stale Xbox BLE slot is **deleted** so pairing can complete and inputs work. +- **BT input path — avoid stalling the stack (root cause mitigation)** — HID callbacks run on **Core1**; they previously called **`set_pad_in()`**, which used a **blocking mutex** also taken by Core0. If Core0 held the lock (e.g. during `get_pad_in()`), Core1 could block **inside the Bluetooth stack** for a long time, starving HCI/L2CAP and contributing to “connected but no inputs” / zombie-link behaviour. **Fix:** Bluetooth now uses **`set_pad_in_from_bluetooth()`** — lock-free staging merged into the queue on Core0 inside **`get_pad_in()`**. Wired/USB hosts still use **`set_pad_in()`**. Pad-in queue depth increased **2 → 8** for extra headroom. +- **BT “zombie link” (inputs die, stick stuck, can’t reconnect)** — If the link still dies with no HID delivery (controller firmware / radio / CYW43), OG Xbox mode keeps the **last** USB report until disconnect. **Safety net:** if **no gamepad HID callback** for **8 seconds**, we **`uni_hid_device_disconnect()`** so you can reconnect without power-cycling the dongle. +- **OG Xbox random disconnect** — Already addressed in 1.0.0.7a: reports are built and sent only when `new_pad_in()` is true (Team-Resurgent timing). Sending every poll caused disconnects on some OG Xbox setups. +- **PS2 (GPIO) loads as controller faster** — On Pico W, the main loop runs **PS2 poll first** each tick: drain all pending PS2 transactions, then update the report and drain again so the adapter responds within byte time. This reduces OPL pad-init hang and helps the console see the controller quickly at boot. Together with the first response byte being the mode byte (no leading 0xFF), PS2/OPL compatibility and “load as controller” speed are improved. +- **IGR and shutdown combos (OG Xbox and PS2)** + - **PS2 (GPIO):** **Home only** = IGR → sends **L1+L2+R1+R2+Start+Select** (triggers full). **Home+Start** = shutdown → sends **L1+L2+R1+R2+L3+R3** (triggers full). + - **OG Xbox:** **Guide only** = IGR → **LT+RT+Start+Back**. **Shutdown** = **LT+RT+Back+White** (triggers full): hold **Guide + Menu (Start)** *or* **Guide + View (Back)**. Xbox BT often does not report Menu while Guide is held, so **Guide+View** is the reliable chord on Xbox pads. Physical Start is cleared on shutdown so the console does not see an extra Start (that prevented some softmods from accepting the combo). +- **Build scripts for new users** — Interactive build scripts let you build firmware without memorizing CMake options. From the project root: **Linux/macOS:** `./scripts/build.sh`; **Windows (PowerShell):** `.\scripts\build.ps1`. The script checks prerequisites (git, python3, cmake, ninja, arm-none-eabi-gcc), prompts for board, default vs fixed output mode, and Release/Debug, then builds into `scripts/build/`. See the main [README](README.md) Build section for details. + +--- + +### Version 1.0.0.7a +- **OG Xbox mode stability** — Report sending matches Team-Resurgent: the adapter only builds and sends the HID report when new gamepad input is available (`new_pad_in()`). Sending every poll cycle caused random disconnects on the Original Xbox. Guide combos kept: **Guide only** = IGR, **Guide+Start** = shutdown. Rumble handling is unchanged. +- **PS2 (GPIO) / OPL stability** — The PS2 controller protocol response was reverted so the first response byte is the mode byte (no leading 0xFF). This restores compatibility with Open PS2 Loader and games; OPL no longer hangs at startup with the adapter connected. Core layout: Core1 runs Bluetooth when used; Core0 runs the main loop and PS2 device poll. +- **PS2 and OG Xbox — Home/Guide IGR and shutdown** — Same interaction on both: **Home only** = **restart (IGR)**; **Home+Start** = **shutdown**. PS2: Home only sends L1+L2+R1+R2+Start+Select (OPL IGR), Home+Start sends L1+L2+R1+R2+L3+R3. OG Xbox: Guide only sends LT+RT+Start+Back (IGR), Guide+Start sends LT+RT+Back+White. See [IMPROVEMENTS.md](Firmware/RP2040/docs/IMPROVEMENTS.md#ps2-gpio-and-og-xbox--homeguide-igr-and-shutdown). +- **Xbox 360 (XInput) wake from standby** — When the 360 was turned off via **Guide → Turn off console** (soft shutdown), you can power it back on with the controller: **press Guide (Home)** or **hold Start for 3 seconds** (Start is preferred on Xbox One/PS5 pads so the controller doesn't turn off). **Disclaimer:** The console must have been started at least once with the adapter connected; wake does **not** work from a cold start (e.g. console just plugged in or after a full power loss). In that case turn the console on with the front power button first. +- **PS3 wake from standby** — Same idea as 360: when the PS3 is in standby (turned off via **PS button → Turn off system**), you can wake it with **PS (Home)** or **hold Start for 3 seconds**—**only if the console keeps USB power when off**. Many PS3s cut power to the USB ports when shut down, so the adapter and controller disconnect; in that case wake is not possible. If your controller stays powered (e.g. charging light on) when the PS3 is off, wake may work. Same disclaimer as 360: no wake from cold start. + +### Version 1.0.0.6a +- **PS1/PS2 (GPIO) output mode** — The adapter can output to a PS1 or PS2 console as a controller over GPIO (no USB device in this mode). Select via button combo **Start + B** or set a fixed driver build. **Wiring (standard Pico):** connect the board to the console's controller port using DATA=GP19, COMMAND=GP20, ATTENTION/SEL=GP21, CLOCK=GP22, ACKNOWLEDGE=GP26. Plug your USB gamepad into the adapter's USB port; the console sees a DualShock-style controller. _Protocol and PIO code ported from [PicoGamepadConverter](https://github.com/Loc15/PicoGamepadConverter)._ +- **GameCube (GPIO) output mode** — The adapter can output to a GameCube or Wii (GameCube ports) as a controller over a single DATA line. Select via button combo **Start + X** or set a fixed driver build. **Wiring (standard Pico):** DATA=GP19, GND=GND; connect to the console's controller port (single wire + ground). Plug your USB gamepad into the adapter's USB port. _JoyBus protocol and PIO code ported from [PicoGamepadConverter](https://github.com/Loc15/PicoGamepadConverter)._ +- **N64 (GPIO) output mode** — The adapter can output to a Nintendo 64 console as a controller over a single DATA line. Select via button combo **Start + Right Bumper** or set a fixed driver build. **Wiring (standard Pico):** DATA=GP19, GND=GND; connect to the N64 controller port (same pin as GameCube). Use any **wired (USB)** or **Bluetooth** (Pico W / Pico 2 W) gamepad as input. _Protocol and PIO send code ported from [pdaxrom/usb2n64-adapter](https://github.com/pdaxrom/usb2n64-adapter). Memory pak and rumble pak are not emulated._ +- **Dreamcast (Maple Bus)** — **Input:** Read a real Dreamcast controller over GPIO (Pin A=GP10, B=GP11). **Output:** Use a USB or **Bluetooth** gamepad to control a Dreamcast: select **Start + Y** for Dreamcast mode, connect your BT or USB pad, wire the adapter's Maple Bus (GP10/GP11) to the console's controller port. Core1 runs the Maple device (responds to device info and GET_CONDITION); Core0 feeds gamepad state from USB/BT. When input source is **Dreamcast GPIO**, the bus is used as host only (read a DC pad). See [Dreamcast Port guide](Firmware/RP2040/docs/Dreamcast_Port.md). Maple Bus from [DreamPicoPort](https://github.com/OrangeFox86/DreamPicoPort). +- **Pin-outs and default mappings** — See [GPIO Output Pin-Out and Mappings](Firmware/RP2040/docs/GPIO_Output_Pinout_and_Mappings.md) for pin numbers (PS1/PS2, Dreamcast, GameCube, N64) and default button/stick mappings for each mode. +- **Bluetooth input for PS1/PS2, GameCube, Dreamcast, and N64** — On **Pico W** (or Pico 2 W), you can use a **Bluetooth controller** as input while outputting to PS1, PS2, GameCube, Dreamcast, or N64. Select the mode (**Start + B**, **Start + X**, **Start + Y**, or **Start + Right Bumper**) as usual; pair your BT gamepad; the console sees the adapter as a controller. Wiring is the same for each mode (PS1/PS2: DATA/CMD/ATT/CLK/ACK; GameCube/N64: DATA=GP19 + GND; Dreamcast: GP10/GP11). +- **GPIO input from real PS1/PS2, GameCube, N64, or Dreamcast controllers** — You can use a **real wired PS1/PS2, GameCube, N64, or Dreamcast controller** as the adapter's input in two ways: (1) **Output to other consoles (Switch, Xbox, DInput, etc.):** set output mode (e.g. **Start + D-pad Down** for Switch), then hold **Start + Select** for ~2.5 s until the input source cycles to **PSX GPIO**, **GameCube GPIO**, **Dreamcast GPIO**, or **N64 GPIO**; plug the real controller into the GPIO pins and connect the adapter's USB to the target console. (2) **Output to PS1/PS2, GameCube, or Dreamcast:** in that output mode, hold **Start + Select** to set input to the matching GPIO and plug in the real controller (same pins). **Wiring:** PS1/PS2: DATA=GP19, CMD=GP20, ATT=GP21, CLK=GP22; GameCube/N64: DATA=GP19 + GND; Dreamcast: GP10, GP11. _PSX host, JoyBus host, N64 host ported from [PicoGamepadConverter](https://github.com/Loc15/PicoGamepadConverter)._ + +**PS2 output mode improvements (v1.0.0.6a):** +- **Button polarity (active-low)** — The PS2 controller protocol uses active-low button bytes: **0xFF = released**, **0 = pressed**. The driver was sending 0 when no button was pressed, so the console read all buttons as pressed (e.g. constant D-pad Up and menu scrolling). Fixed by initialising the report with `buttons1 = buttons2 = 0xFF` and clearing bits when a button is pressed, so neutral/no-controller state is correct. +- **Left stick Y axis** — Left analog stick up/down was inverted on output. The mapping for left stick Y was corrected so physical stick up corresponds to the value the PS2 expects for up. +- **Protocol flow** — PS2 device (controller) protocol and escape-mode response lengths follow [PicoGamepadConverter](https://github.com/Loc15/PicoGamepadConverter) and [DS4toPS2](https://github.com/TonyMacDonald1995/DS4toPS2) (single response byte then command, 7-byte escape responses with leading 0x5A) for broad console compatibility. +- **OPL (Open PS2 Loader)** — On Pico W, the main loop now drains all pending PS2 transactions each tick so rapid pad init (e.g. OPL at startup) does not desync or hang. **Keep the controller connected at boot** when using OPL; the PS2 pad driver typically does not re-detect controllers plugged in after an app has started, so unplugging then replugging will not give input in OPL. If OPL hangs (black screen) or inputs stop being registered, **power-cycle the Pico** (unplug and replug the adapter) so the PS2 link resets; the adapter also primes 0xFF after a desync drain so the next transaction is valid. **DualSense (PS5) over Bluetooth:** the Options button (Start) may not be reported as Start by the BT stack in some cases; if Options doesn't work in OPL or in-game, try an Xbox or other controller, or use the DualSense over USB if your board supports it. + +### Version 1.0.0.5a +- **Switch Pro emulation** — Switch output mode now emulates a **Nintendo Switch Pro Controller** over USB (report format, subcommands, init handshake). Face-button and report layout follow the standard Pro Controller protocol (bit layout aligned with hardware/reference). _Switch Pro protocol and button report layout reference: [retro-pico-switch](https://github.com/DavidPagels/retro-pico-switch) (N64/GameCube → Pico → Switch); Bluepad32 Switch parser used for validation._ +- **Waveshare RP2350-USB-A board support** — Build with `-DOGXM_BOARD=RP2350_USB_A` for the Waveshare RP2350-USB-A (wired USB host + RGB LED). _Board support from upstream [OGX-Mini](https://github.com/wiredopposite/OGX-Mini/commit/68bdec451077a33b0b1247ce01ed30e470163dea)._ +- **Additional board support** — **RP2350_ZERO** (Waveshare RP2350-Zero), **RP2040_XIAO** (Seeed Studio XIAO RP2040), and **RP2354**. All use the same Standard (PIO-USB host) firmware path; build with `-DOGXM_BOARD=RP2350_ZERO`, `RP2040_XIAO`, or `RP2354`. _Board support from [Sakura-Research-Lab/OGX-Mini-2026-Testing](https://github.com/Sakura-Research-Lab/OGX-Mini-2026-Testing)._ +- **8BitDo XInput (Xbox 360) fix** — Wired 8BitDo controllers (VID 0x2DC8, PID 0x3016 / 0x3106) are detected and sent a repeating LED-off keepalive so they stay connected and stable. _From [Sakura-Research-Lab/OGX-Mini-2026-Testing](https://github.com/Sakura-Research-Lab/OGX-Mini-2026-Testing)._ +- **Switch analog stick sensitivity** — Slight sensitivity increase (1.2× gain) for analog sticks in Switch Pro emulation mode; configurable via `STICK_GAIN_NUM` / `STICK_GAIN_DEN` in `Switch.cpp`. +- **Original Xbox (OG) Guide button behavior** — In OG Xbox mode, the Guide (SYS) button is mapped to Start; **tap** = Start, **hold 1 s** = soft IGR (LT+RT+Start+Back), **hold 3 s** = system shutdown (LT+RT+D-pad Up+Back). _Implementation based on [Sakura-Research-Lab/OGX-Mini-2026-Testing](https://github.com/Sakura-Research-Lab/OGX-Mini-2026-Testing)._ + +### Version 1.0.0.4a +- **Wii (Wiimote) output mode** — On Pico W and Pico 2 W, build with `-DOGXM_FIXED_DRIVER=WII` for a Wii-only firmware. The adapter appears as a Wiimote over Bluetooth; use a USB gamepad on the external PIO USB port. Supports No Extension, Nunchuk, and Classic Controller report modes (cycle with Home + D-pad Down). See [Wii Mode Guide](Firmware/RP2040/docs/Wii_Mode_Guide.md). _Approach and mappings based on [PicoGamepadConverter](https://github.com/wiredopposite/PicoGamepadConverter)._ +- Xbox 360 no longer requires the USB patch to authenticate, uses the same authentication as Joypad-OS to do official handshake with Retail consoles. Homebrew/ Jailbreaking is no longer required to use the XINPUT mode on 360 but it will no longer output to PC. +- **Rumble fix for Xbox 360 controllers** — Rumble on 360 pads is now handled correctly when using the adapter (including wireless RUMBLE_ENABLE sequence). _Xbox 360 vibration/rumble handling attributed to [faithvoid](https://github.com/faithvoid) on GitHub._ +- **Pico 2 / Pico 2 W build fixes** — Firmware builds and runs correctly on RP2350 (Pico 2 and Pico 2 W). +- Improved latency on Xbox 360 and PS3 controllers. +- **PS3 mode:** Home (PS) button works with DS4/DS5 over Bluetooth (8-frame latch so short taps register). Analog sticks now match DualShock 3 spec: full 0–255 range, center 0x80 (128), ~1.5% deadzone for accurate emulation. See [IMPROVEMENTS.md](Firmware/RP2040/docs/IMPROVEMENTS.md#ps3-mode--input-delays-stuck-inputs-home-button-and-analog-stick-emulation). +- Added build flags so you can make it only output in a specific mode and disable mode switching. +- Using a PS5 controller allows you to tap the touchpad to enable or disable the adaptive triggers. + +### Version 1.0.0.3a patched +- Ability to emulate the GameCube controller adapter for Wii U. (Also tested on Switch 1/ 2) +- Wii U & Switch Pro controllers are supported fully with working LT and RT +- Wii Remotes are supported along with the following controllers connected: Nunchuck/ GameCube/ Wii Gamepad. +- Disconnection Combo has been added: Start+Select for most controllers, L3+R3 for OUYA controllers. + +## Features new to v1.0.0 +- Bluetooth functionality for the Pico W, Pico 2 W, and Pico+ESP32. +- Web application (connectable via USB or Bluetooth) for configuring deadzones and buttons mappings, supports up to 8 saved profiles. +- Pi Pico 2 and Pico 2 W (RP2350) support. +- Reduced latency by about 3-4 ms, graphs showing comparisons are coming. +- 4 channel functionality, connect 4 Picos and use one Xbox 360 wireless adapter to control all 4. +- Delayed USB mount until a controller is plugged in, useful for internal installation (non-Bluetooth boards only). +- Generic HID controller support. +- Dualshock 3 emulation (minus gyros), rumble now works. +- Steel Battalion controller emulation with a wireless Xbox 360 chatpad. +- Xbox DVD dongle emulation. You must provide or dump your own dongle firmware, see the Tools directory. +- Analog button support on OG Xbox and PS3. +- RGB LED support for RP2040-Zero and Adafruit Feather boards. diff --git a/Firmware/FWDefines.cmake b/Firmware/FWDefines.cmake index dfc6cda9..c8531f36 100644 --- a/Firmware/FWDefines.cmake +++ b/Firmware/FWDefines.cmake @@ -1,2 +1,2 @@ set(FW_NAME "OGX-Mini") -set(FW_VERSION "v1.0.0a3") \ No newline at end of file +set(FW_VERSION "v1.0.0.13a") diff --git a/Firmware/RP2040/CMakeLists.txt b/Firmware/RP2040/CMakeLists.txt index 291dbf50..fe2e4e96 100644 --- a/Firmware/RP2040/CMakeLists.txt +++ b/Firmware/RP2040/CMakeLists.txt @@ -13,6 +13,11 @@ set(SRC ${CMAKE_CURRENT_LIST_DIR}/src) set(EXTERNAL_DIR ${CMAKE_CURRENT_LIST_DIR}/../external) set(PICOSDK_VERSION_TAG "2.1.0") +# Default Pico SDK path when not set (e.g. by CMake Tools or command line) +if(NOT PICO_SDK_PATH) + set(PICO_SDK_PATH ${EXTERNAL_DIR}/pico-sdk CACHE PATH "Path to Pico SDK") +endif() + include(${CMAKE_CURRENT_LIST_DIR}/../cmake/init_submodules.cmake) include(${CMAKE_CURRENT_LIST_DIR}/../cmake/patch_libs.cmake) include(${CMAKE_CURRENT_LIST_DIR}/../cmake/generate_gatt_header.cmake) @@ -62,7 +67,19 @@ set(SOURCES_BOARD ${SRC}/USBDevice/DeviceDriver/DeviceDriver.cpp ${SRC}/USBDevice/DeviceDriver/PSClassic/PSClassic.cpp ${SRC}/USBDevice/DeviceDriver/PS3/PS3.cpp + ${SRC}/USBDevice/DeviceDriver/PS4/PS4.cpp + ${SRC}/USBDevice/DeviceDriver/Steam/Steam.cpp + ${SRC}/USBDevice/DeviceDriver/Steam/SteamTouchpad.cpp ${SRC}/USBDevice/DeviceDriver/Switch/Switch.cpp + ${SRC}/USBDevice/DeviceDriver/WiiU/WiiU.cpp + ${SRC}/USBDevice/DeviceDriver/Wii/Wii.cpp + ${SRC}/USBDevice/DeviceDriver/PS1PS2/PS1PS2.cpp + ${SRC}/USBDevice/DeviceDriver/PS1PS2/controller_simulator.c + ${SRC}/USBDevice/DeviceDriver/GameCube/GameCube.cpp + ${SRC}/USBDevice/DeviceDriver/GameCube/gamecube_device.c + ${SRC}/USBDevice/DeviceDriver/Dreamcast/Dreamcast.cpp + ${SRC}/USBDevice/DeviceDriver/N64/N64.cpp + ${SRC}/USBDevice/DeviceDriver/N64/n64_device.c ${SRC}/USBDevice/DeviceDriver/XInput/XInput.cpp ${SRC}/USBDevice/DeviceDriver/XboxOG/XboxOG_GP.cpp ${SRC}/USBDevice/DeviceDriver/XboxOG/XboxOG_SB.cpp @@ -71,6 +88,18 @@ set(SOURCES_BOARD ${SRC}/USBDevice/DeviceDriver/WebApp/WebApp.cpp ${SRC}/USBDevice/DeviceDriver/XInput/tud_xinput/tud_xinput.cpp ${SRC}/USBDevice/DeviceDriver/XboxOG/tud_xid/tud_xid.cpp + + ${SRC}/USBHost/GPIOHost/GPIOHost.cpp + ${SRC}/USBHost/GPIOHost/psx_host/psx_host.c + ${SRC}/USBHost/GPIOHost/joybus_host/joybus_controller.c + ${SRC}/USBHost/GPIOHost/joybus_host/n64_host.c + ${SRC}/USBHost/GPIOHost/dreamcast_host/MapleBus.cpp + # libxsm3 for Xbox 360 (XSM3) authentication in XInput mode + ${EXTERNAL_DIR}/libxsm3/xsm3.c + ${EXTERNAL_DIR}/libxsm3/usbdsec.c + ${EXTERNAL_DIR}/libxsm3/excrypt_des.c + ${EXTERNAL_DIR}/libxsm3/excrypt_parve.c + ${EXTERNAL_DIR}/libxsm3/excrypt_sha.c ) set(LIBS_BOARD # Base @@ -93,6 +122,8 @@ set(INC_DIRS_BOARD # Config options +option(OGXM_SWITCH2_HID_RAW_LOG "UART: log Switch 2 Pro (0x2069) 64B HID when the 3 digital button bytes change only (Debug)" OFF) + set(MAX_GAMEPADS 1 CACHE STRING "Set number of gamepads, 1 to 4") if (MAX_GAMEPADS GREATER 4 OR MAX_GAMEPADS LESS 1) message(FATAL_ERROR "MAX_GAMEPADS must be between 1 and 4") @@ -110,17 +141,20 @@ if (OGXM_BOARD STREQUAL "PI_PICO") elseif (OGXM_BOARD STREQUAL "PI_PICO2") add_compile_definitions(CONFIG_OGXM_BOARD_PI_PICO2=1) set(EN_USB_HOST TRUE) + set(PICO_BOARD pico2) set(PICO_PLATFORM rp2350) set(FLASH_SIZE_MB 4) elseif(OGXM_BOARD STREQUAL "PI_PICOW") add_compile_definitions(CONFIG_OGXM_BOARD_PI_PICOW=1) set(EN_BLUETOOTH TRUE) + set(EN_USB_HOST TRUE) # PIO USB host for Wii mode (controller on external USB) set(PICO_BOARD pico_w) elseif(OGXM_BOARD STREQUAL "PI_PICO2W") add_compile_definitions(CONFIG_OGXM_BOARD_PI_PICO2W=1) set(EN_BLUETOOTH TRUE) + set(EN_USB_HOST TRUE) # PIO USB host for Wii mode (controller on external USB) set(PICO_BOARD pico2_w) set(PICO_PLATFORM rp2350) set(FLASH_SIZE_MB 4) @@ -167,12 +201,102 @@ elseif(OGXM_BOARD STREQUAL "ESP32_BLUERETRO_I2C") add_compile_definitions(OGXM_RETAIL=1) endif() +elseif(OGXM_BOARD STREQUAL "RP2350_USB_A") + add_compile_definitions(CONFIG_OGXM_BOARD_RP2350_USB_A=1) + set(EN_USB_HOST TRUE) + set(PICO_PLATFORM rp2350) + set(EN_RGB TRUE) + set(FLASH_SIZE_MB 2) + +elseif(OGXM_BOARD STREQUAL "RP2350_ZERO") + add_compile_definitions(CONFIG_OGXM_BOARD_RP2350_ZERO=1) + set(EN_USB_HOST TRUE) + set(PICO_PLATFORM rp2350) + set(EN_RGB TRUE) + set(FLASH_SIZE_MB 2) + +elseif(OGXM_BOARD STREQUAL "RP2040_XIAO") + add_compile_definitions(CONFIG_OGXM_BOARD_RP2040_XIAO=1) + set(EN_USB_HOST TRUE) + set(EN_RGB TRUE) + +elseif(OGXM_BOARD STREQUAL "RP2354") + add_compile_definitions(CONFIG_OGXM_BOARD_RP2354=1) + set(EN_BLUETOOTH TRUE) + set(EN_USB_HOST TRUE) + set(PICO_BOARD pico2_w) + set(PICO_PLATFORM rp2350) + set(FLASH_SIZE_MB 4) + else() - message(FATAL_ERROR "Invalid OGXM_BOARD value. See options in src/board_config.h") + message(FATAL_ERROR "Invalid OGXM_BOARD value. See options in src/Board/Config.h") endif() -add_definitions(-DOGXM_BOARD=${OGXM_BOARD}) +# Optional: default or lock USB output mode. +# OGXM_FIXED_DRIVER = one of: XINPUT, DINPUT, XBOXOG, XBOXOG_SB, XBOXOG_XR, SWITCH, WIIU, WII, PS3, PSCLASSIC, WEBAPP, PS1PS2, GAMECUBE, DREAMCAST, N64, PS4, STEAM. +# Empty = use first valid driver as default, combos enabled. +# Set = use that mode as default (first boot / NVS init). If OGXM_FIXED_DRIVER_ALLOW_COMBOS=OFF (default): +# combos disabled, mode locked. If OGXM_FIXED_DRIVER_ALLOW_COMBOS=ON: default is this mode, combos still work. +set(OGXM_FIXED_DRIVER "" CACHE STRING "Default/lock output mode. One of: XINPUT, DINPUT, XBOXOG, XBOXOG_SB, XBOXOG_XR, SWITCH, WIIU, WII, PS3, PSCLASSIC, WEBAPP, PS1PS2, GAMECUBE, DREAMCAST, N64, PS4, STEAM. Empty = combos enabled.") +set(OGXM_FIXED_DRIVER_ALLOW_COMBOS OFF CACHE BOOL "If OGXM_FIXED_DRIVER is set: ON = use it as default but allow combos to change mode; OFF = lock to that mode, combos disabled.") +if(OGXM_FIXED_DRIVER) + add_compile_definitions(CONFIG_OGXM_FIXED_DRIVER=1) + # WII is build-only: no combos to switch into or out of Wii. + if(OGXM_FIXED_DRIVER_ALLOW_COMBOS AND NOT (OGXM_FIXED_DRIVER STREQUAL "WII")) + add_compile_definitions(CONFIG_OGXM_FIXED_DRIVER_ALLOW_COMBOS=1) + endif() + if(OGXM_FIXED_DRIVER STREQUAL "XBOXOG") + add_compile_definitions(OGXM_FIXED_DRIVER_TYPE=1) + elseif(OGXM_FIXED_DRIVER STREQUAL "XBOXOG_SB") + add_compile_definitions(OGXM_FIXED_DRIVER_TYPE=2) + elseif(OGXM_FIXED_DRIVER STREQUAL "XBOXOG_XR") + add_compile_definitions(OGXM_FIXED_DRIVER_TYPE=3) + elseif(OGXM_FIXED_DRIVER STREQUAL "XINPUT") + add_compile_definitions(OGXM_FIXED_DRIVER_TYPE=4) + elseif(OGXM_FIXED_DRIVER STREQUAL "PS3") + add_compile_definitions(OGXM_FIXED_DRIVER_TYPE=5) + elseif(OGXM_FIXED_DRIVER STREQUAL "DINPUT") + add_compile_definitions(OGXM_FIXED_DRIVER_TYPE=6) + elseif(OGXM_FIXED_DRIVER STREQUAL "PSCLASSIC") + add_compile_definitions(OGXM_FIXED_DRIVER_TYPE=7) + elseif(OGXM_FIXED_DRIVER STREQUAL "SWITCH") + add_compile_definitions(OGXM_FIXED_DRIVER_TYPE=8) + elseif(OGXM_FIXED_DRIVER STREQUAL "WIIU") + add_compile_definitions(OGXM_FIXED_DRIVER_TYPE=9) + elseif(OGXM_FIXED_DRIVER STREQUAL "WII") + add_compile_definitions(OGXM_FIXED_DRIVER_TYPE=10) + elseif(OGXM_FIXED_DRIVER STREQUAL "PS1PS2") + add_compile_definitions(OGXM_FIXED_DRIVER_TYPE=11) + elseif(OGXM_FIXED_DRIVER STREQUAL "GAMECUBE") + add_compile_definitions(OGXM_FIXED_DRIVER_TYPE=12) + elseif(OGXM_FIXED_DRIVER STREQUAL "DREAMCAST") + add_compile_definitions(OGXM_FIXED_DRIVER_TYPE=13) + elseif(OGXM_FIXED_DRIVER STREQUAL "N64") + add_compile_definitions(OGXM_FIXED_DRIVER_TYPE=14) + elseif(OGXM_FIXED_DRIVER STREQUAL "PS4") + add_compile_definitions(OGXM_FIXED_DRIVER_TYPE=15) + elseif(OGXM_FIXED_DRIVER STREQUAL "STEAM") + add_compile_definitions(OGXM_FIXED_DRIVER_TYPE=16) + elseif(OGXM_FIXED_DRIVER STREQUAL "WEBAPP") + add_compile_definitions(OGXM_FIXED_DRIVER_TYPE=100) + else() + message(FATAL_ERROR "OGXM_FIXED_DRIVER must be one of: XINPUT, DINPUT, XBOXOG, XBOXOG_SB, XBOXOG_XR, SWITCH, WIIU, WII, PS3, PSCLASSIC, WEBAPP, PS1PS2, GAMECUBE, DREAMCAST, N64, PS4, STEAM") + endif() + if(OGXM_FIXED_DRIVER STREQUAL "WII") + message(STATUS "Wii-only build: ${OGXM_FIXED_DRIVER} (combos disabled, build option only)") + elseif(OGXM_FIXED_DRIVER_ALLOW_COMBOS) + message(STATUS "Default output mode: ${OGXM_FIXED_DRIVER} (combos enabled)") + else() + message(STATUS "Fixed output mode: ${OGXM_FIXED_DRIVER} (combos disabled)") + endif() +endif() + +# Do not add -DOGXM_BOARD here; Config.h defines OGXM_BOARD from CONFIG_OGXM_BOARD_* to avoid redefinition warnings + +# Main loop delay (microseconds). 0 = lowest latency (default). Set to 250+ for lower CPU. +set(MAIN_LOOP_DELAY_US 0 CACHE STRING "Main loop delay in microseconds; 0 = low latency (default)") +add_compile_definitions(MAIN_LOOP_DELAY_US=${MAIN_LOOP_DELAY_US}) if(EN_USB_HOST) message(STATUS "USB host enabled.") @@ -184,6 +308,7 @@ if(EN_USB_HOST) ${SRC}/USBHost/HostDriver/PSClassic/PSClassic.cpp ${SRC}/USBHost/HostDriver/SwitchWired/SwitchWired.cpp ${SRC}/USBHost/HostDriver/SwitchPro/SwitchPro.cpp + ${SRC}/USBHost/HostDriver/SwitchPro/Switch2ProHost.cpp ${SRC}/USBHost/HostDriver/PS5/PS5.cpp ${SRC}/USBHost/HostDriver/PS4/PS4.cpp ${SRC}/USBHost/HostDriver/PS3/PS3.cpp @@ -230,6 +355,25 @@ if(EN_BLUETOOTH) pico_btstack_cyw43 bluepad32 ) + + # Wii mode: Wiimote BT emulator (Pico W/2W with USB host) + if(EN_USB_HOST AND (OGXM_BOARD STREQUAL "PI_PICOW" OR OGXM_BOARD STREQUAL "PI_PICO2W" OR OGXM_BOARD STREQUAL "RP2354")) + message(STATUS "Wii Wiimote lib enabled.") + list(APPEND SOURCES_BOARD + ${SRC}/Wii/utils.c + ${SRC}/Wii/WiiReportConverter.cpp + ${SRC}/Wii/wiimote-lib/wiimote_btstack.c + ${SRC}/Wii/wiimote-lib/wiimote.c + ${SRC}/Wii/wiimote-lib/wm_reports.c + ${SRC}/Wii/wiimote-lib/motion.c + ${SRC}/Wii/wiimote-lib/wm_crypto.c + ${SRC}/Wii/wiimote-lib/inet.c + ) + list(APPEND INC_DIRS_BOARD + ${SRC}/Wii + ${SRC}/Wii/wiimote-lib + ) + endif() endif() if(EN_RGB) @@ -268,6 +412,8 @@ if(EN_ESP32) endif() list(APPEND LIBS_BOARD hardware_i2c + hardware_pio + hardware_dma ) endif() @@ -293,11 +439,26 @@ if(EXISTS ${SRC}/USBDevice/DeviceDriver/XboxOG/tud_xid/tud_xid_xremote_rom.h) add_definitions(-DXREMOTE_ROM_AVAILABLE) endif() -if(NOT EN_BLUETOOTH) +# RP2040-only: force generic boot2 when no Bluetooth (RP2350 boards use platform boot2) +if(NOT EN_BLUETOOTH + AND NOT (OGXM_BOARD STREQUAL "PI_PICO2") + AND NOT (OGXM_BOARD STREQUAL "PI_PICO2W") + AND NOT (OGXM_BOARD STREQUAL "RP2350_USB_A") + AND NOT (OGXM_BOARD STREQUAL "RP2350_ZERO") + AND NOT (OGXM_BOARD STREQUAL "RP2354")) add_compile_definitions(PICO_BOOT_STAGE2_CHOOSE_GENERIC_03H=1) add_compile_definitions(PICO_XOSC_STARTUP_DELAY_MULTIPLIER=64) endif() +# Cross-compile: skip linker in compiler check (ARM linker does not accept Windows flags) +set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY) +set(CMAKE_SYSTEM_NAME PICO) +set(CMAKE_CXX_STANDARD_LIBRARIES "") +set(CMAKE_C_STANDARD_LIBRARIES "") + +# Use our Platform/PICO.cmake so CMake does not add Windows linker flags (--out-implib, etc.) +list(PREPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_LIST_DIR}/cmake) + include(${PICO_SDK_PATH}/pico_sdk_init.cmake) project(${FW_NAME} C CXX ASM) @@ -309,15 +470,18 @@ add_executable(${FW_NAME} ${SOURCES_BOARD}) set(BUILD_STR "") if(CMAKE_BUILD_TYPE STREQUAL "Debug") - if(OGXM_BOARD STREQUAL "PI_PICOW") - message(FATAL_ERROR "Debug build will not work with the Pico W currently") - endif() - - message(STATUS "Debug build enabled.") + message(STATUS "Debug build enabled (UART logging at 115200; TinyUSB host prevents USB CDC stdio — use UART TX pin, see CMakeLists UART message).") set(BUILD_STR "-Debug") - set(TX_PIN 0) - set(RX_PIN 1) + # Pico W/2W: use UART1 on GP4 (TX) / GP5 (RX) - valid UART pins, avoids PIO USB (GP0/1) in Wii mode + # Note: GP10/11 are not valid UART pins on RP2040/RP2350 + if(OGXM_BOARD STREQUAL "PI_PICOW" OR OGXM_BOARD STREQUAL "PI_PICO2W" OR OGXM_BOARD STREQUAL "RP2354") + set(TX_PIN 4) + set(RX_PIN 5) + else() + set(TX_PIN 0) + set(RX_PIN 1) + endif() set(UART_PORT) include(${CMAKE_CURRENT_LIST_DIR}/cmake/get_pico_uart_port.cmake) @@ -325,6 +489,8 @@ if(CMAKE_BUILD_TYPE STREQUAL "Debug") message(STATUS "UART port: ${UART_PORT}, TX: ${TX_PIN}, RX: ${RX_PIN}") + # USB CDC stdio is disabled by Pico SDK when TinyUSB host is linked; OGXM_LOG uses printf → UART only. + pico_enable_stdio_usb(${FW_NAME} 0) pico_enable_stdio_uart(${FW_NAME} 1) target_compile_definitions(${FW_NAME} PRIVATE PICO_DEFAULT_UART=${UART_PORT} @@ -334,6 +500,12 @@ if(CMAKE_BUILD_TYPE STREQUAL "Debug") add_compile_definitions(CFG_TUSB_DEBUG=1) add_compile_definitions(OGXM_DEBUG=1) + add_compile_definitions(CONFIG_OGXM_DEBUG=1) + + if(OGXM_SWITCH2_HID_RAW_LOG) + target_compile_definitions(${FW_NAME} PRIVATE OGXM_SWITCH2_HID_RAW_LOG=1) + message(STATUS "OGXM_SWITCH2_HID_RAW_LOG: UART hex when Switch 2 Pro 3 digital button bytes change") + endif() target_compile_options(${FW_NAME} PRIVATE -Wall # Enable most warnings @@ -368,11 +540,22 @@ pico_set_program_version(${FW_NAME} ${FW_VERSION}) target_include_directories(${FW_NAME} PRIVATE ${SRC} + ${EXTERNAL_DIR}/libxsm3 ) +target_compile_definitions(${FW_NAME} PRIVATE XSM3_NO_DEBUGGING=1 XSM3_EMBEDDED=1) if(EN_RGB) pico_generate_pio_header(${FW_NAME} ${SRC}/Board/Pico_WS2812/WS2812.pio) endif() +pico_generate_pio_header(${FW_NAME} ${SRC}/USBDevice/DeviceDriver/PS1PS2/psxSPI.pio) +pico_generate_pio_header(${FW_NAME} ${SRC}/USBDevice/DeviceDriver/GameCube/joybus.pio) +pico_generate_pio_header(${FW_NAME} ${SRC}/USBDevice/DeviceDriver/N64/n64send.pio) +pico_generate_pio_header(${FW_NAME} ${SRC}/USBHost/GPIOHost/psx_host/clock.pio) +pico_generate_pio_header(${FW_NAME} ${SRC}/USBHost/GPIOHost/joybus_host/controller.pio) +pico_generate_pio_header(${FW_NAME} ${SRC}/USBHost/GPIOHost/dreamcast_host/maple_in.pio) +pico_generate_pio_header(${FW_NAME} ${SRC}/USBHost/GPIOHost/dreamcast_host/maple_out.pio) + +target_include_directories(${FW_NAME} PRIVATE ${SRC}/USBHost/GPIOHost/dreamcast_host) include_directories(${INC_DIRS_BOARD}) @@ -382,6 +565,12 @@ endif() if(EN_BLUETOOTH) add_subdirectory(${BLUEPAD32_ROOT}/src/components/bluepad32 libbluepad32) + # Propagate to bluepad32 sources (e.g. DS4 parser): CYW43-specific link + setup path + target_compile_definitions(bluepad32 PRIVATE OGXM_BLUEPAD32_PICO_W=1) + target_include_directories(bluepad32 PRIVATE ${SRC}) + if(CMAKE_BUILD_TYPE STREQUAL "Debug") + target_compile_definitions(bluepad32 PRIVATE CONFIG_OGXM_DEBUG=1) + endif() endif() add_subdirectory(${LIBFIXMATH_PATH} libfixmath) @@ -404,6 +593,16 @@ else() set(EXE_FILENAME "${FW_NAME}-${FW_VERSION}-${OGXM_BOARD}${BUILD_STR}") endif() +# Output filename always reflects the mode: fixed driver name (XINPUT, PS3, ...) or "Multi" for combo mode +if(OGXM_FIXED_DRIVER) + set(EXE_FILENAME "${EXE_FILENAME}-${OGXM_FIXED_DRIVER}") + if(OGXM_FIXED_DRIVER_ALLOW_COMBOS) + set(EXE_FILENAME "${EXE_FILENAME}-Combos") + endif() +else() + set(EXE_FILENAME "${EXE_FILENAME}-Multi") +endif() + set_target_properties(${FW_NAME} PROPERTIES OUTPUT_NAME ${EXE_FILENAME}) pico_add_extra_outputs(${FW_NAME}) \ No newline at end of file diff --git a/Firmware/RP2040/cmake/Platform/PICO.cmake b/Firmware/RP2040/cmake/Platform/PICO.cmake new file mode 100644 index 00000000..4ec1b156 --- /dev/null +++ b/Firmware/RP2040/cmake/Platform/PICO.cmake @@ -0,0 +1,17 @@ +# Platform file for PICO (bare-metal ARM). Overrides Windows-GNU link rules +# so the ARM linker is not given Windows-only flags (--out-implib, --major-image-version). + +set_property(GLOBAL PROPERTY TARGET_SUPPORTS_SHARED_LIBS FALSE) +set(CMAKE_EXECUTABLE_SUFFIX .elf) + +if(CMAKE_HOST_WIN32) + include(Platform/WindowsPaths) +else() + include(Platform/UnixPaths) +endif() + +# Bare-metal executable link: no Windows implib or image version flags +# (C, CXX, and ASM - boot_stage2 is ASM-only) +set(CMAKE_C_LINK_EXECUTABLE " -o ") +set(CMAKE_CXX_LINK_EXECUTABLE " -o ") +set(CMAKE_ASM_LINK_EXECUTABLE " -o ") diff --git a/Firmware/RP2040/docs/Controller_Mappings.md b/Firmware/RP2040/docs/Controller_Mappings.md new file mode 100644 index 00000000..db15c91b --- /dev/null +++ b/Firmware/RP2040/docs/Controller_Mappings.md @@ -0,0 +1,386 @@ +# Controller button and stick mappings + +This document describes how OGX-Mini maps **input controllers** (USB host / Bluetooth) into the internal **`PadIn`** layout, and how each **output mode** maps **`PadIn`** to what the console or host sees. + +**Related docs** + +| Topic | Document | +|-------|----------| +| GPIO output (PS1/PS2, Dreamcast, GameCube, N64) pin-outs + mappings | [GPIO_Output_Pinout_and_Mappings.md](GPIO_Output_Pinout_and_Mappings.md) | +| Wii **output** mode (Wiimote over BT) | [Wii_Mode_Guide.md](Wii_Mode_Guide.md) | +| Supported wired USB pads (VID/PID lists) | [Wired_Controllers.md](Wired_Controllers.md) | +| PS3/PS4 motion (accel/gyro) | [IMPROVEMENTS.md — motion passthrough](IMPROVEMENTS.md#ps3--ps4-output--motion-passthrough) | +| Bluetooth pad list (Bluepad32) | [Bluepad32 supported gamepads](https://bluepad32.readthedocs.io/en/latest/supported_gamepads/) | + +--- + +## How mappings work + +```text +[Input controller] → PadIn → [Output driver] → Console / PC / GPIO + USB host ↑ USB gadget or GPIO protocol + Bluetooth (Bluepad32) │ + └── User profiles may remap MAP_* fields (web app) +``` + +**`PadIn`** is the firmware’s canonical gamepad state (`Gamepad::PadIn` in `Gamepad.h`). Names follow **Xbox-style** layout: + +| PadIn field | Meaning | +|-------------|---------| +| **A / B / X / Y** | Face buttons (A = south / Cross on PlayStation) | +| **LB / RB** | Bumpers | +| **trigger_l / trigger_r** | Analog triggers (0–255) | +| **Start / Back** | Menu buttons | +| **SYS** | Guide / Home / PS | +| **MISC** | Share / Capture / touchpad click (pad-dependent) | +| **L3 / R3** | Stick clicks | +| **dpad** | Up / Down / Left / Right (diagonals supported) | +| **joystick_lx/ly, joystick_rx/ry** | Sticks (−32768 … +32767, center 0) | +| **accel / gyro** | Motion (when input pad provides IMU) | + +**User remapping:** For most **USB output** modes, button and stick assignments can be changed in the [web app](https://megacadedev.github.io/OGX-Mini-2026-WebApp/) (saved profiles alter `Gamepad::MAP_*` fields). **GPIO output** modes and some specialty mappings use fixed defaults only (see [GPIO doc](GPIO_Output_Pinout_and_Mappings.md#changing-mappings)). + +**Source code index:** `Gamepad.h` (PadIn), `Bluepad32/Bluepad32.cpp` (BT → PadIn), `USBHost/HostDriver/*` (wired USB → PadIn), `USBDevice/DeviceDriver/*` and `Gamepad/*` GPIO drivers (PadIn → output). + +--- + +## Input → PadIn + +### Standard layout (most Bluetooth and USB pads) + +Bluepad32 (Pico W / Pico 2 W) and most USB host drivers (XInput, DualShock 3/4/5, etc.) map the physical controller into **`PadIn` using the Xbox-style table above** — face buttons, bumpers, triggers, sticks, and menu buttons align with their Xbox equivalents. PlayStation and Switch pads are normalized to this layout in the host/BT layer. + +| Physical (examples) | PadIn | +|---------------------|-------| +| Xbox A / DS Cross / Switch B | **A** | +| Xbox B / DS Circle / Switch A | **B** | +| Xbox X / DS Square / Switch Y | **X** | +| Xbox Y / DS Triangle / Switch X | **Y** | +| View / Select / Minus | **Back** | +| Menu / Start / Plus | **Start** | +| Guide / PS / Home | **SYS** | +| Share / Capture | **MISC** (when present) | + +**Disconnect combo (Bluetooth):** **Start + Back** (~0.5 s). **OUYA:** **L3 + R3** (no Start/Select). + +### Steam Controller 2026 / Triton (Bluetooth LE) + +Wireless only on **Pico W / Pico 2 W / RP2354** (`28de:1303`). Pairing and protocol: [IMPROVEMENTS — Steam Controller 2026](IMPROVEMENTS.md#steam-controller-2026-triton--bluetooth). + +Valve’s printed **View / Menu** labels do **not** match Xbox View/Menu. Firmware follows SDL: + +| Triton control | PadIn | +|----------------|-------| +| **View** | **Start** | +| **Menu** | **Back** | +| **Steam** (or QAM) | **SYS** | +| A / B / X / Y, LB / RB, L3 / R3, d-pad, LT / RT, sticks | Standard layout | +| L4 / L5 / R4 / R5 grips | Unmapped | + +--- + +### Wii Remote (Bluetooth input) + +Handheld Wiimotes (not Wii U Pro, Classic Controller, or Balance Board) are forced to **vertical (TV-style)** orientation on connect: `uni_hid_parser_wii_set_mode(WII_MODE_VERTICAL)` in `Bluepad32.cpp`. Hold the remote with the **IR end toward the TV**. Plug the **Nunchuk in before pairing** (or reconnect after attaching it) so extension init completes. + +| Wiimote (vertical) | PadIn | +|--------------------|-------| +| D-pad | D-pad | +| **B** (trigger under body) | **A** | +| **A** (large face button) | **B** | +| **1** | **X** (no Nunchuk) / **LB** (with Nunchuk) | +| **2** | **Y** (no Nunchuk) / **RB** (with Nunchuk) | +| **+** | **Start** | +| **−** | **Back** | +| **Home** | **SYS** | + +| Nunchuk | PadIn | +|---------|-------| +| **Stick** | **Left stick** (`joystick_lx` / `joystick_ly`) | +| **C** | **X** | +| **Z** | **Y** | + +**Motion:** In **PS3 / PS4 output mode**, continuous accel reports are requested: **`0x31`** (Wiimote only) or **`0x35`** (Wiimote + Nunchuk). **Switch** output does not request or pass through motion for now. See [motion passthrough](IMPROVEMENTS.md#ps3--ps4-output--motion-passthrough). + +**Triggers:** Wiimote **B** maps to **A**, not analog triggers. Shoulder/Z does not fill `trigger_l` / `trigger_r` when mapped as LB/RB. + +--- + +### Switch Pro Controller (wired USB host) + +Switch 1 Pro (`0x2009`) uses `SwitchProHost` with deliberate swaps vs a 1:1 Pro layout (`SwitchPro.cpp`): + +| Switch Pro physical | PadIn | +|---------------------|-------| +| **Y** | **X** | +| **B** | **A** | +| **A** | **B** | +| **X** | **Y** | +| **L / R** | **LB / RB** | +| **ZL / ZR** | **trigger_l / trigger_r** (digital full) | +| **− / +** | **Back / Start** | +| **Home** | **SYS** | +| **Capture** | **MISC** | +| **L3 / R3** | **R3 / L3** (swapped) | +| D-pad Up/Down/Left/Right | D-pad **Down/Up/Right/Left** (swapped) | + +Sticks are scaled to `PadIn` int16 range. IMU is available for **PS3/PS4 output** motion passthrough. + +--- + +### Switch 2 Pro (wired USB host) + +Switch 2 Pro (`0x2069`) uses `Switch2ProHost` with **Switch-2-specific** bit positions (not the classic `Buttons0/1/2` layout). See `Switch2ProHost.cpp` for bit definitions. Summary: + +| Switch 2 Pro | PadIn | +|--------------|-------| +| Face **B / A / Y / X** (bits 0–3) | **A / B / X / Y** | +| **RB** (bit 4) | **RB** | +| **LB** (Home bit on misc byte) | **LB** | +| **Minus** | **Back** | +| Classic **R** bit | **Start** | +| Stick **L3** (classic ZR bit) | **L3** | +| Stick **R3** (misc bit 7) | **R3** | +| **Home** (left byte) | **SYS** | +| **ZL / ZR** digital bits | **trigger_l / trigger_r** (digital) | +| D-pad (misc − / L3 / + / R3 bit positions) | D-pad | + +Sticks use the same 12-bit packing as Switch 1 Pro, with **Y inverted** into PadIn. **Anti-deadzone** profiles apply a small noise floor (~8%) when inner deadzone is unset so imperfect stick centers do not drift. + +**GL**, **GR**, and **Chat** are documented in source but intentionally unmapped. + +--- + +### Generic / DInput USB host + +Unknown HID pads use the generic host path; mapping depends on the report descriptor. Use the [controller capture tool](../../../Tools/controller_capture/README.md) when adding support. DInput **output** mode uses PlayStation names on the wire (see below). + +--- + +## PadIn → USB output modes + +Unless noted, **D-pad** and **sticks** map directly. **Triggers** map to analog axes where the protocol supports them. + +### XInput (Xbox 360) + +| PadIn | Xbox 360 | +|-------|----------| +| A / B / X / Y | A / B / X / Y | +| LB / RB | LB / RB | +| trigger_l / trigger_r | LT / RT | +| Back / Start | Back / Start | +| L3 / R3 | L3 / R3 | +| SYS | Guide | +| D-pad | D-pad | +| Sticks | Left / right stick | + +**Source:** `USBDevice/DeviceDriver/XInput/XInput.cpp` + +--- + +### PlayStation 3 (DualShock 3 / Sixaxis) + +| PadIn | DS3 | +|-------|-----| +| A | **Cross** | +| B | **Circle** | +| X | **Square** | +| Y | **Triangle** | +| LB / RB | L1 / R1 | +| trigger_l / trigger_r | L2 / R2 (+ analog axes) | +| Back / Start | Select / Start | +| L3 / R3 | L3 / R3 | +| SYS | **PS** (8-frame latch for BT taps) | +| MISC | Touchpad (byte bit; rarely used on DS3) | +| D-pad | D-pad | +| Sticks | Left / right (0–255, center 0x80) | +| accel / gyro | **Sixaxis** (when input provides motion) | + +**Source:** `USBDevice/DeviceDriver/PS3/PS3.cpp` + +--- + +### PlayStation 4 (DualShock 4 USB) + +| PadIn | DS4 | +|-------|-----| +| A / B / X / Y | Cross / Circle / Square / Triangle | +| LB / RB | L1 / R1 | +| trigger_l / trigger_r | L2 / R2 | +| Back / Start | Share / Options | +| L3 / R3 | L3 / R3 | +| SYS | PS | +| MISC | Touchpad click | +| D-pad / sticks | D-pad / sticks | +| accel / gyro | DS4 motion fields (when input provides motion) | + +**Source:** `USBDevice/DeviceDriver/PS4/PS4.cpp` + +--- + +### Nintendo Switch (Pro Controller emulation) + +Face buttons use **Nintendo names on the wire**; mapping from PadIn is **crossed** so PlayStation/Xbox inputs feel correct on Switch: + +| PadIn | Switch Pro output | +|-------|-------------------| +| **A** | **B** | +| **B** | **A** | +| **X** | **Y** | +| **Y** | **X** | +| LB / RB | **L / R** | +| trigger_l / trigger_r | **ZL / ZR** | +| Back / Start | **− / +** | +| L3 / R3 | L3 / R3 | +| SYS | **Home** | +| MISC | **Capture** | +| D-pad | D-pad | +| Sticks | Left / right (12-bit; Y inverted vs PadIn) | + +**Rumble (v1.0.0.12a):** Console **HD rumble** in output reports **`0x01` / `0x10` / `0x11`** is decoded into **`PadOut`** and forwarded to the input pad (Bluetooth DualSense / Xbox, etc.). See [IMPROVEMENTS — Switch HD rumble](IMPROVEMENTS.md#switch-mode--hd-rumble-passthrough). + +**Source:** `USBDevice/DeviceDriver/Switch/Switch.cpp` + +--- + +### DInput (DirectInput) + +| PadIn | DInput report | +|-------|----------------| +| A / B / X / Y | Cross / Circle / Square / Triangle | +| LB / RB | L1 / R1 | +| trigger_l / trigger_r | L2 / R2 axes | +| Back / Start | Select / Start | +| SYS / MISC | Mode / TP | +| L3 / R3 | L3 / R3 | +| D-pad / sticks | D-pad / sticks | + +**Source:** `USBDevice/DeviceDriver/DInput/DInput.cpp` + +--- + +### SteamOS / Bazzite (STEAM mode) + +**Select mode:** **Start + Left Bumper + D-pad Up** (~3 s). + +Enumerates as **DualSense** (`054c:0ce6`) plus a **HID mouse** interface. **DualSense input** (BT or wired USB): gamepad report is **passthrough**; touchpad → separate **mouse** interface. **Other input:** report is **synthesized** from PadIn using the mapping below; **no stick-mouse fallback**. + +| PadIn | DualSense USB | +|-------|----------------| +| A / B / X / Y | Cross / Circle / Square / Triangle | +| LB / RB | L1 / R1 | +| trigger_l / trigger_r | L2 / R2 | +| Back / Start | Share / Options | +| L3 / R3 | L3 / R3 | +| SYS | PS | +| MISC | Touchpad click | +| **DualSense touchpad** (input) | USB mouse movement + click (IF1) | + +**Source:** `USBDevice/DeviceDriver/Steam/*`, `SteamBtReport.h`, `SteamTouchpad.cpp` + +--- + +### Original Xbox (Gamepad) + +| PadIn | OG Xbox | +|-------|---------| +| A / B / X / Y | A / B / X / Y (digital or analog face per build) | +| LB / RB | White / Black | +| trigger_l / trigger_r | LT / RT | +| Back / Start | Back / Start | +| L3 / R3 | L3 / R3 | +| SYS | **Guide** (tap = Start; hold chords for IGR/shutdown — see README) | +| Sticks | Left / right (**Y inverted** on wire) | + +**Source:** `USBDevice/DeviceDriver/XboxOG/XboxOG_GP.cpp` + +--- + +### Original Xbox — Steel Battalion + +Uses a dedicated chatpad + gamepad map table (`XboxOG_SB.cpp`). See source `GP_MAP` / `CHATPAD_MAP` for the full SB control surface. + +--- + +### Original Xbox — DVD Remote (XRemote) + +| PadIn | Remote button | +|-------|----------------| +| SYS | Display | +| Start | Play | +| Back | Stop | +| A | Select | +| Y | Pause | +| X | Display | +| B | Back | +| LB / RB (alone or together) | Skip − / Skip + / Display | +| L3 / R3 (alone or together) | Title / Menu / Info | + +**Source:** `USBDevice/DeviceDriver/XboxOG/XboxOG_XR.cpp` + +--- + +### PlayStation Classic + +| PadIn | PS Classic | +|-------|------------| +| A / B / X / Y | Cross / Circle / Square / Triangle | +| LB / RB | L1 / R1 | +| Back / Start | Select / Start | +| D-pad / sticks | D-pad / sticks | + +**Source:** `USBDevice/DeviceDriver/PSClassic/PSClassic.cpp` + +--- + +### Wii U (GameCube adapter emulation) + +Emulates a **GameCube controller** on the Wii U adapter port. Face buttons are **swapped** to match GC layout: + +| PadIn | GameCube (Wii U report) | +|-------|-------------------------| +| **B** | **A** | +| **A** | **B** | +| **Y** | **X** | +| **X** | **Y** | +| Start | Start | +| RB | **Z** | +| trigger_l / trigger_r (full) | **L / R** digital | +| D-pad | D-pad | +| Left stick | Main stick (Y axis may invert for Xbox vs Nintendo hosts) | +| Right stick | C-stick | + +**LB** is not mapped to a GC button. **Source:** `USBDevice/DeviceDriver/WiiU/WiiU.cpp` + +--- + +## PadIn → GPIO output modes + +PS1/PS2, Dreamcast, GameCube, and N64 use fixed **PadIn → console** tables and pin-outs documented in: + +**[GPIO_Output_Pinout_and_Mappings.md](GPIO_Output_Pinout_and_Mappings.md)** + +--- + +## USB gamepad → Wii Remote (Wii output mode) + +When the firmware is built with **`-DOGXM_FIXED_DRIVER=WII`**, a USB/BT gamepad is converted into Wiimote (+ extension) reports over Bluetooth. Mappings for **No Extension**, **Nunchuk**, and **Classic Controller** are in: + +**[Wii_Mode_Guide.md — §5 Button and stick mapping](Wii_Mode_Guide.md#5-button-and-stick-mapping-by-mode)** + +--- + +## Quick reference: PlayStation ↔ PadIn ↔ Xbox + +| PlayStation | PadIn | Xbox | +|-------------|-------|------| +| Cross | **A** | A | +| Circle | **B** | B | +| Square | **X** | X | +| Triangle | **Y** | Y | +| L1 / R1 | LB / RB | LB / RB | +| L2 / R2 | triggers | LT / RT | +| Share / Options | Back / Start | View / Menu | +| PS | SYS | Guide | + +Use this table when reading any **PadIn → output** section above. diff --git a/Firmware/RP2040/docs/Dreamcast_Port.md b/Firmware/RP2040/docs/Dreamcast_Port.md new file mode 100644 index 00000000..d79a5445 --- /dev/null +++ b/Firmware/RP2040/docs/Dreamcast_Port.md @@ -0,0 +1,75 @@ +# Dreamcast (Maple Bus) Port from DreamPicoPort + +OGX-Mini has **stub** support for Dreamcast: you can select **Start + Y** for Dreamcast mode and **Start + Select** to cycle input source to **Dreamcast GPIO**, but the actual Maple Bus protocol is not yet implemented. This document describes how to complete the port from [DreamPicoPort](https://github.com/OrangeFox86/DreamPicoPort) (Dreamcast to USB gamepad converter for Raspberry Pi Pico). + +## What DreamPicoPort Provides + +- **Maple Bus**: Sega’s serial protocol for Dreamcast controllers and peripherals. Two wires (A and B), bidirectional, frame-based with CRC. +- **Host mode**: Read a real Dreamcast controller → USB (or in our case, fill `Gamepad::PadIn`). +- **Client mode**: USB gamepad → emulate a Dreamcast controller (we feed from `Gamepad::PadIn` and respond to the console). + +DreamPicoPort uses: + +- Two PIO programs: `maple_in.pio` (read) and `maple_out.pio` (write), on **different PIO blocks** (each block has a 32-instruction limit). +- DMA for TX/RX, IRQ on completion. +- **Pin A** and **Pin B** = two consecutive GPIOs (e.g. 10 and 11). Optional **direction pin** for a transceiver. +- Packet format: `MaplePacket` (frame word, payload, 8-bit CRC). Commands: device info, GET_CONDITION (controller state), etc. +- Controller condition: 8-byte `controller_condition_t` (buttons, analog L/R, sticks). See `inc/dreamcast_structures.h`. + +## Files to Port from DreamPicoPort + +1. **Maple Bus HAL** + - `inc/hal/MapleBus/MapleBusInterface.hpp` + - `inc/hal/MapleBus/MaplePacket.hpp` + - `src/hal/MapleBus/MapleBus.hpp` + - `src/hal/MapleBus/MapleBus.cpp` + - `src/hal/MapleBus/PioProgram.hpp` + - `src/hal/MapleBus/maple_in.pio` + - `src/hal/MapleBus/maple_out.pio` + +2. **Dreamcast types** + - `inc/dreamcast_constants.h` (commands, device functions) + - `inc/dreamcast_structures.h` (`controller_condition_t`) + +3. **Configuration** + - Provide a small `configuration.h` (or equivalent) with: + - `MAPLE_OPEN_LINE_CHECK_TIME_US`, `MAPLE_NS_PER_BIT`, `MAPLE_RESPONSE_TIMEOUT_US`, etc. + - Pins: e.g. `P1_BUS_START_PIN` (pin A), `P1_DIR_PIN` (-1 if no direction pin). + - DreamPicoPort uses a **forked pico-sdk**; we use the stock SDK, so any SDK-specific code may need minor adjustments. + +4. **Client (output to Dreamcast)** + - Either port `DreamcastController` + `DreamcastMainPeripheral` / `DreamcastPeripheral` (full state machine), or implement a **minimal controller** that: + - Responds to **device info** and **GET_CONDITION**. + - Builds `controller_condition_t` from `Gamepad::PadIn` (see `Gamepad::PadIn` ↔ `controller_condition_t` mapping in DreamPicoPort’s `DreamcastController` / host code). + - Run this loop on **core1** (like PS1/PS2 and GameCube), replacing the stub in `Dreamcast.cpp` / `dreamcast_core1_entry()`. + +5. **Host (read Dreamcast controller)** + - Port the host path that sends GET_CONDITION and reads the response (e.g. hostLib code that uses `MapleBus::write(..., autostartRead=true)` and parses the reply). + - In `GPIOHost::dreamcast_host_init()` create and configure the Maple Bus (pin A, optional dir pin; use one PIO for in and one for out). + - In `GPIOHost::dreamcast_host_poll()` send GET_CONDITION, read response, fill `controller_condition_t`, convert to `Gamepad::PadIn`, call `gamepad.set_pad_in()`. + +## OGX-Mini Integration Points + +- **Device (output)** + - `USBDevice/DeviceDriver/Dreamcast/Dreamcast.cpp` + - `DreamcastDevice::initialize()`: one-time Maple Bus + minimal controller setup. + - `DreamcastDevice::process(idx, gamepad)`: not used for USB; core1 loop reads a shared report (e.g. a `controller_condition_t` or `PadIn` updated from the main loop). + - `dreamcast_core1_entry()`: run the Maple Bus **device** loop (wait for console commands, respond with device info / condition from the shared report). + +- **Host (input)** + - `USBHost/GPIOHost/GPIOHost.cpp` + - `dreamcast_host_init(pio_in_index, pio_out_index, pin_a, dir_pin)`: create MapleBus instance(s), configure pins. + - `dreamcast_host_poll(gamepad)`: send GET_CONDITION, parse response into `controller_condition_t`, map to `PadIn`, `gamepad.set_pad_in()`. + +- **Board** + - Standard and PicoW already treat `DeviceDriverType::DREAMCAST` as GPIO device mode (no USB device, core1 runs `dreamcast_core1_entry`). + - When input source is `DREAMCAST_GPIO`, they call `dreamcast_host_init()` and `dreamcast_host_poll()`. + +- **Pins** + - DreamPicoPort defaults: e.g. P1 bus = GPIO 10 (A), 11 (B); direction pin 6. We use `dreamcast_host_init(1, 0, 10, -1)` in the stub (PIO1 for in, PIO0 for out, pin A 10, no dir pin). Adjust in your config to match hardware. + +## References + +- [DreamPicoPort](https://github.com/OrangeFox86/DreamPicoPort) – source for Maple Bus and Dreamcast controller logic. +- [Maple Bus (Dreamcast wiki)](https://dreamcast.wiki/Maple_bus) – protocol overview. +- DreamPicoPort README: Maple Bus implementation, PIO data handoff, timeouts. diff --git a/Firmware/RP2040/docs/GPIO_Output_Pinout_and_Mappings.md b/Firmware/RP2040/docs/GPIO_Output_Pinout_and_Mappings.md new file mode 100644 index 00000000..7fae28e4 --- /dev/null +++ b/Firmware/RP2040/docs/GPIO_Output_Pinout_and_Mappings.md @@ -0,0 +1,193 @@ +# GPIO Output Modes: Pin-Outs and Default Controller Mappings + +This document lists the **pin connections** and **default button/stick mappings** for the GPIO-based output modes (PS1/PS2, Dreamcast, GameCube, N64) on OGX-Mini. + +**Why two kinds of pin numbers?** +- **GPIO numbers** (e.g. 19, 20, 21) are **RP2040 GPIO pins** used for data. The firmware uses these; you wire each signal to the matching GPIO on the Pico. +- **“Connector” numbers** (e.g. connector pin 4, pin 5) are the **pin positions on the console or controller plug** (the physical port). VCC and GND are **not** GPIOs — they’re power rails. On the **adapter (Pico) side** you use the board’s **3V3** and **GND** pins (the Pico’s power pins, not GPIO). On the **console/controller side** you use the plug’s VCC and GND positions (connector pin X, Y) so your cable is wired correctly. So: data = GPIO numbers on the Pico; power = Pico 3V3/GND on the adapter side, and connector pin numbers on the plug side. + +**Conventions:** +- **Output mode** = adapter emulates a controller and connects to the console’s controller port. +- **Input (host) mode** = adapter reads a real console controller over GPIO and uses it as input (e.g. to send to Switch/Xbox over USB). +- PadIn = internal gamepad state (from USB/BT or from a GPIO host). Mappings below are **PadIn → console**. +- **Power:** Adapter side = Pico **3V3** and **GND** (board power pins). Console/controller side = **connector pin** numbers for VCC and GND on the plug. When **outputting** to the console, only connect adapter GND to the port’s GND. When **reading** a real controller, connect Pico **3V3** to the plug’s VCC pin and Pico **GND** to the plug’s GND pin. + +--- + +## 1. PlayStation 1 / PlayStation 2 (PS1/PS2) + +### Pin-out (output to console and input from real controller) + +PS1/PS2 use a **9-pin** controller connector. Below: **GPIO** = RP2040 pin (adapter); **connector** = plug pin # (console/controller cable). + +| Signal | Adapter (Pico) | Console/controller plug | Notes | +|---------------|----------------|--------------------------|---------------------------------| +| DATA | **GPIO 19** | Connector 1 | Bidirectional data line | +| COMMAND (CMD) | **GPIO 20** | Connector 2 | | +| GND | **GND** | Connector 4 | Not a GPIO — Pico GND pin | +| VCC | **3V3** | Connector 5 | Not a GPIO — Pico 3V3 pin; only when powering a controller | +| ATTENTION/SEL | **GPIO 21** | Connector 6 | Select / Attention | +| CLOCK (CLK) | **GPIO 22** | Connector 7 | | +| ACKNOWLEDGE | **GPIO 26** | Connector 9 | ACK from controller to console | + +**Output mode:** Connect each GPIO to the same-numbered connector pin and Pico **GND** to connector **4** (do not use console VCC). **Input mode:** Same GPIO↔connector data mapping; connect Pico **3V3** → plug pin **5**, Pico **GND** → plug pin **4** to power the controller. + +**Can the Pico be powered by the PS2 controller port?** +The PS1/PS2 controller port provides **3.3 V** on connector pin **5** (VCC) and **GND** on pin **4**. In theory you could power the Pico from the port by connecting pin 5 to the Pico’s **VSYS** (e.g. pin 39 on the Pico), since VSYS accepts about 1.8–5.5 V. **This is not recommended:** the port is designed to power a small controller, not an MCU; current may be limited and the adapter’s draw (especially with USB or Bluetooth) can exceed what the port safely supplies. The supported setup is to power the Pico via USB or an external 3.3 V / 5 V supply and only connect **GND** and the data lines to the console. + +### Default controller mapping (PadIn → PS1/PS2) + +| Console (PS1/PS2) | Adapter input (PadIn) | +|-------------------|------------------------| +| D-pad Up/Down/Left/Right | D-pad Up/Down/Left/Right | +| Select | Back (Select) | +| Start | Start | +| L3 | L3 (left stick click) | +| R3 | R3 (right stick click) | +| L1 | LB (left bumper) | +| R1 | RB (right bumper) | +| L2 | Left trigger (digital + analog) | +| R2 | Right trigger (digital + analog) | +| Triangle | Y | +| Circle | B | +| Cross | A | +| Square | X | +| Right stick X/Y | joystick_rx / joystick_ry | +| Left stick X/Y | joystick_lx / joystick_ly | + +Sticks use 0x80 (128) as center; full range 0–255. + +--- + +## 2. Dreamcast (Maple Bus) + +### Pin-out (output to console and input from real controller) + +Dreamcast uses a **5-pin** controller connector. **GPIO** = RP2040 (adapter); **connector** = plug pin #. + +| Signal | Adapter (Pico) | Console/controller plug | Notes | +|--------|----------------|--------------------------|-------------------------------| +| D0 | **GPIO 10** | Connector 1 | Maple Bus data line A | +| VCC | — | Connector 2 | +5 V from console (not 3V3) | +| GND | **GND** | Connector 3 | Not a GPIO — Pico GND pin | +| Sense | — | Connector 4 | Grounded by controller | +| D1 | **GPIO 11** | Connector 5 | Maple Bus data line B | + +**Output mode:** Connect **GPIO 10** and **11** to connector 1 and 5, and Pico **GND** to connector **3**. Do not connect console 5V to the Pico. **Input mode:** Same data + GND. To power a Dreamcast controller you need +5V (Pico is 3.3V — use console port or external 5V). + +No direction pin is used in the default configuration (`dir_pin = -1`). + +### Default controller mapping (PadIn → Dreamcast) + +| Console (Dreamcast) | Adapter input (PadIn) | +|---------------------|------------------------| +| D-pad Up/Down/Left/Right | D-pad Up/Down/Left/Right | +| A | A | +| B | B | +| X | X | +| Y | Y | +| Start | Start | +| C | RB | +| Z | LB | +| D (extra) | MISC | +| L trigger (analog) | trigger_l (0–255) | +| R trigger (analog) | trigger_r (0–255) | +| Left stick | joystick_lx / joystick_ly | +| Right stick | joystick_rx / joystick_ry | + +Dreamcast uses inverted logic for buttons (0 = pressed). Sticks are 0–255, 128 center. + +--- + +## 3. GameCube / Wii (GameCube ports) + +### Pin-out (output to console and input from real controller) + +GameCube controller port is a **7-pin** connector. **GPIO** = RP2040 (adapter); **connector** = plug pin #. + +| Signal | Adapter (Pico) | Console/controller plug | Notes | +|--------|----------------|--------------------------|------------------------------------| +| — | — | Connector 1 | 5 V rumble (not used for logic) | +| DATA | **GPIO 19** | Connector 2 | Single-wire JoyBus data | +| GND | **GND** | Connector 3, 4 | Not a GPIO — Pico GND pin | +| 3.3 V | **3V3** | Connector 6 | Not a GPIO — Pico 3V3; only when powering a controller | + +**Output mode:** Connect **GPIO 19** to connector **2** and Pico **GND** to connector **3** (and/or **4**). Do not connect console 5V or 3.3V to the Pico. **Input mode:** Same DATA and GND; connect Pico **3V3** → plug **6** to power the controller. + +### Default controller mapping (PadIn → GameCube) + +| Console (GameCube) | Adapter input (PadIn) | +|--------------------|------------------------| +| D-pad Up/Down/Left/Right | D-pad Up/Down/Left/Right | +| A | A | +| B | B | +| X | X | +| Y | Y | +| Start | Start | +| Z | MISC | +| L | LB | +| R | RB | +| L trigger (analog) | trigger_l (0–255) | +| R trigger (analog) | trigger_r (0–255) | +| Left stick | joystick_lx / joystick_ly | +| C-stick (right) | joystick_rx / joystick_ry | + +Sticks use 0x80 (128) as center; full range 0–255. + +--- + +## 4. Nintendo 64 (N64) + +### Pin-out (output to console and input from real controller) + +N64 controller port is a **3-pin** connector. **GPIO** = RP2040 (adapter); **connector** = plug pin #. + +| Signal | Adapter (Pico) | Console/controller plug | Notes | +|--------|----------------|--------------------------|------------------------------------| +| VCC | **3V3** | Connector 1 | Not a GPIO — Pico 3V3; only when powering a controller | +| DATA | **GPIO 19** | Connector 2 | Single-wire data (3.3 V logic) | +| GND | **GND** | Connector 3 | Not a GPIO — Pico GND pin | + +**Output mode:** Connect **GPIO 19** to connector **2** and Pico **GND** to connector **3**. Do not connect console 3.3V to the Pico. **Input mode:** Same DATA and GND; connect Pico **3V3** → plug **1** to power the controller. + +Same physical pin (GP19) as GameCube; only one of N64 or GameCube is active at a time. + +### Default controller mapping (PadIn → N64) + +| Console (N64) | Adapter input (PadIn) | +|---------------|------------------------| +| D-pad Up/Down/Left/Right | D-pad Up/Down/Left/Right | +| A | A | +| B | B | +| Z | MISC | +| Start | Start | +| L | LB | +| R | RB | +| C-Up | Right stick Y negative (threshold 4000) | +| C-Down | Right stick Y positive (threshold 4000) | +| C-Left | Right stick X negative (threshold 4000) | +| C-Right | Right stick X positive (threshold 4000) | +| Joystick X/Y | joystick_lx / joystick_ly | + +N64 stick is signed 8-bit, 0x80 center. C-buttons are derived from the right analog stick with a ±4000 threshold; diagonals combine. + +--- + +## Summary table (output mode: Pico GPIO + power) + +| Mode | Pico: data (GPIO) | Pico: power | Plug: GND | Plug: VCC (input mode only) | +|----------|---------------------|--------------|------------|------------------------------| +| PS1/PS2 | 19, 20, 21, 22, 26 | GND | Connector 4 | Connector 5 (Pico 3V3) | +| Dreamcast| 10, 11 | GND | Connector 3 | Connector 2 (5V from console) | +| GameCube | 19 | GND | Connector 3 or 4 | Connector 6 (Pico 3V3) | +| N64 | 19 | GND | Connector 3 | Connector 1 (Pico 3V3) | + +Data lines use **GPIO** numbers on the Pico. Power uses the Pico’s **GND** and **3V3** pins (not GPIO). Connector columns are the **plug pin numbers** on the console/controller cable. When **reading** a controller, also connect Pico 3V3 to the plug’s VCC pin (except Dreamcast, which needs 5V from the console or another source). + +**Note:** GameCube and N64 share GP19. Do not use both at the same time on the same pin; switch output mode (and input source if using GPIO input) as needed. + +--- + +## Changing mappings + +Default mappings are fixed in the device drivers. User-configurable button remapping is available for **USB device** modes (e.g. Switch, XInput) via the web app; GPIO output modes (PS1/PS2, Dreamcast, GameCube, N64) currently use the defaults above only. See **[Controller_Mappings.md](Controller_Mappings.md)** for the full mapping reference across all modes. diff --git a/Firmware/RP2040/docs/IMPROVEMENTS.md b/Firmware/RP2040/docs/IMPROVEMENTS.md new file mode 100644 index 00000000..dd6eed8d --- /dev/null +++ b/Firmware/RP2040/docs/IMPROVEMENTS.md @@ -0,0 +1,874 @@ +# Firmware Improvements + +Improvements and fixes applied to the OGX-Mini RP2040 firmware in this project. + +**Version:** From **v1.0.0a3** the version was bumped to **v1.0.0a4** to reflect Wii U controller fixes, Gamecube USB mode, PS3 driver fixes, latency improvements, and Xbox 360 (XInput) support (see below). **v1.0.0.8a+** documents **Pico W / Pico 2 W** work on **DualShock 4 (Classic Bluetooth)** vs **BLE advertising**, **BR inquiry**, and related BT stability (see *Pico W / Pico 2 W — DualShock 4 and Classic Bluetooth* below). **v1.0.0.10a** adds **Nintendo Switch 2** wireless support (**Pro 2** and **Joy-Con 2**) over **BLE**, **Switch 1 Joy-Con L+R merge**, and **Joy-Con dual-half latency fixes** (see *Nintendo Switch 2 — Bluetooth* and *Joy-Con pair merge — latency* below). **v1.0.0.11a** adds **PIO USB host wired connection fixes** — **Switch 1/2 Pro**, **DualShock 3**, **Xbox 360 wireless receiver**, and **Razer Atrox Xbox One** (`1532:0a00`) — validated on **Waveshare RP2350-USB-A**, plus **STEAM output mode** for **SteamOS / Bazzite** (see *STEAM mode* below). **v1.0.0.12a** adds **PS3 / PS4 motion passthrough**, **Bluetooth disconnect reboot**, **USB resume → restore BT pairing**, **XInput stock stick feel (#38)**, **Switch HD rumble passthrough**, **Switch 2 anti-deadzone / L3–R3 (#64)**, **RP2354 Bluetooth**, **STEAM touchpad-mouse-only**, **Steam Controller 2026 (Triton) BLE**, and **DualShock 3 USB → Bluetooth auto-pair restore** (sync feature **0xF5** after PIO USB wired init; see sections below). + +**Version 1.0.0.12a — documented here for release notes:** + +- **DualShock 3 — USB → Bluetooth auto-pair restored** — Sync **feature `0xF5`** after **0xF4** + LED (PIO USB sync init had skipped the old async F2→F5 path). See [§ DualShock 3 — automatic USB programming for Bluetooth pairing](#dualshock-3--automatic-usb-programming-for-bluetooth-pairing) and [§ DualShock 3 — wired USB host](#dualshock-3--wired-usb-host). +- **Steam Controller 2026 (Triton) — Bluetooth** — See [§ Steam Controller 2026 (Triton) — Bluetooth](#steam-controller-2026-triton--bluetooth) below. +- **PS3 / PS4 output — motion passthrough** — See [§ PS3 / PS4 output — motion passthrough](#ps3--ps4-output--motion-passthrough) below. +- **Pico W / Pico 2 W — Bluetooth disconnect reboot for clean reconnect** — See [§ Pico W / Pico 2 W — Bluetooth disconnect reboot for clean reconnect](#pico-w--pico-2-w--bluetooth-disconnect-reboot-for-clean-reconnect) below. +- **USB resume restores pairing scans** — See [§ Pico W / Pico 2 W / RP2354 BT — USB resume restores pairing scans](#pico-w--pico-2-w--rp2354-bt--usb-resume-restores-pairing-scans) below. +- **XInput / Xbox 360 — stock stick feel (#38)** — See [§ XInput / Xbox 360 — stock stick feel](#xinput--xbox-360--stock-stick-feel) below. +- **Switch mode — HD rumble passthrough** — See [§ Switch mode — HD rumble passthrough](#switch-mode--hd-rumble-passthrough) below. +- **Switch 2 Pro — anti-deadzone drift and L3/R3 (#64)** — See [§ Switch 2 Pro — anti-deadzone and L3/R3](#switch-2-pro--anti-deadzone-and-l3r3) below. +- **RP2354 Bluetooth** — Board profile uses Pico 2 W BT path; see [§ Additional board support](#additional-board-support-rp2350_zero-rp2040_xiao-rp2354). +- **STEAM mode** — Touchpad → mouse only (no stick-mouse); see [§ STEAM mode](#steam-mode--steamos--bazzite-linux-desktop). + +**Version 1.0.0.9a — documented here for release notes:** + +- **Bluetooth (Pico W / Pico 2 W):** ~**1 second** haptic **connection rumble** when a wireless controller reaches **device ready** (so you can feel that pairing completed). **DualShock 4** uses a **longer start delay** before rumble (same idea as the existing PS4 FF grace window) so early force-feedback does not destabilize the link. **File:** `src/Bluepad32/Bluepad32.cpp` — `ogxm_play_connection_rumble()` from `device_ready_cb`. +- **PS3 mode with the adapter plugged into a Windows PC:** **Host output rumble** forwarded to the Bluetooth pad no longer stays on at idle. Windows DInput often sends a **small non-zero** large-motor byte or a **small-motor byte other than 0/1**; the driver now applies a **deadzone** on the large motor and treats the small motor as on **only when the byte is `1`** (DS3 output semantics). **File:** `src/USBDevice/DeviceDriver/PS3/PS3.cpp` — `new_report_out_` path. +- **Pico W / Pico 2 W — PIO USB wired unplug:** **Reliable disconnect** when the gamepad cable is removed from the adapter’s PIO USB host port (see [§ Pico W / Pico 2 W — PIO USB wired controller unplug detection](#pico-w--pico-2-w--pio-usb-wired-controller-unplug-detection)). **Files:** `src/OGXMini/Board/PicoW.cpp`, `src/USBHost/HostManager.h`. +- **DualShock 3 — automatic Bluetooth pairing over USB (boards with Bluetooth):** When a **PS3 / DualShock 3** controller is used **wired** on the USB host, right after **0xF4** + LED init the firmware sync-sends **HID feature report `0xF5`** with the adapter’s **local BD_ADDR** (same as [Bluepad32’s sixaxispairer](https://bluepad32.readthedocs.io/en/latest/pair_ds3/)), so the user can **unplug USB** and connect with **PS**. If `uni_local_bd_addr` is not ready yet, pairing is **deferred** until a later report. **(Behavior as of v1.0.0.12a** — sync SET after PIO USB wired init; originally added in 1.0.0.9a.) **File:** `src/USBHost/HostDriver/PS3/PS3.cpp` (guarded by `CONFIG_EN_BLUETOOTH`). + +--- + +## Pico W / Pico 2 W — Bluetooth disconnect reboot for clean reconnect + +**Goal:** After a wireless controller fully connects and then disconnects, the next pair must work like a **fresh plug-in** — rumble, inputs, and pairing LED — without unplugging the Pico. + +**Problem:** In-place BLE reconnect (especially **Xbox Series / One over BLE**) does not reliably re-run the same path as first connect. Bonded **re-encryption**, leftover **HIDS** client / descriptor state, and scan flags left the adapter in a bad state: LED solid or off, no connection rumble, no gamepad input, until a power cycle. + +**Approach:** Treat a successful pad’s disconnect as a full reset of the radio stack, the same way unplugging the Pico does. + +| Item | Detail | +|------|--------| +| **When** | Last Bluetooth pad that reached **device ready** disconnects (**Home**, **Start+Select**, or link drop) | +| **Immediate** | Restore **pairing mode**: LED blink + BT scan / new connections enabled | +| **After ~500 ms** | **Watchdog reboot** (`watchdog_reboot`) — reliable from the BT core (Core1) | +| **Not when** | Failed pair attempts while already scanning (avoids reboot loops) | +| **Multi-pad** | No reboot while another BT pad is still connected | + +**User flow:** Disconnect pad → LED flashes (pairing) → brief reboot (USB may re-enumerate on the PC) → LED flashes again → press the controller’s connect button → normal first-connect rumble and inputs. + +**Files:** `src/Bluepad32/Bluepad32.cpp` (`device_ready_cb` sets `s_bt_slot_was_ready`; `device_disconnected_cb` restores pairing mode and schedules reboot), `src/Board/board_api.cpp` (`board_api::reboot()` via `watchdog_reboot`). + +--- + +## Pico W / Pico 2 W / RP2354 BT — USB resume restores pairing scans + +**Goal:** When the adapter stays plugged into a console that **USB-suspends** in standby (e.g. **Xbox 360** soft shutdown via Guide → Turn off console), Bluetooth **BR/EDR inquiry** and **BLE scan** must resume after the host wakes so the user can pair without unplugging the dongle. + +**Problem:** With no wireless pad connected, long USB suspend could leave Bluepad32’s “new connections enabled” flag **true** while the radio had **stopped scanning** — LED solid or off, no pairing blink. + +**Approach:** + +| Item | Detail | +|------|--------| +| **USB resume** | `tud_resume_cb` → `bluepad32::on_usb_device_resume()` (marshaled to BT core) → `restore_bt_pairing_mode(-1)` when no BT pad and no PIO wired host pad is active | +| **Idle watchdog** | Every **45 s** on the BT run loop, restart BR/LE scans if idle and pairing should be active | +| **No reboot on resume** | Unlike pad disconnect, console wake does **not** trigger watchdog reboot (avoids XSM3 re-auth every standby cycle) | +| **Wired host release** | `wired_usb_release_enable_bt_pairing()` uses the same scan-restore path (not only `enable_new_connections`, which is a no-op when the flag is already true) | + +**Files:** `src/Bluepad32/Bluepad32.cpp`, `src/USBDevice/tud_callbacks.cpp`. + +--- + +## XInput / Xbox 360 — stock stick feel + +**Goal:** Controllers used in **Xbox 360 (XInput) mode** — especially modern pads like **Xbox Series / One over Bluetooth** on Pico 2 W — should feel closer to a **stock 360** stick than a near-linear high-precision Series stick ([#38](https://github.com/MegaCadeDev/OGX-Mini-2026/issues/38)). + +**Problem:** Default profiles left sticks **uncurved** (`curve=1`, `dz_inner=0`, `uncap_radius=true`). Series pads report tiny physical deflections as strong USB values, so 360 games feel twitchy. A real 360 stick is quieter near center and firmer toward the rim. + +**Approach (XInput mode only):** + +| Item | Detail | +|------|--------| +| **When** | Current output driver is **XInput** and WebApp stick settings are still at defaults (not customized) | +| **Inner deadzone** | Remapped **~8%** left / **~9%** right (mild — not Microsoft’s ~24% software constant, so games that also deadzone are not double-dulled) | +| **Curve** | **0.35** (`mag^(1/curve)`): quieter center, steeper outer third | +| **Uncap** | **Off** — clamp to circular radius like a stock pad | +| **Override** | Any WebApp stick customization replaces these defaults | + +**Files:** `src/UserSettings/JoystickSettings.*`, `src/Gamepad/Gamepad.h`, board `initialize()` (`PicoW.cpp`, `Standard.cpp`, `Four_Channel_I2C.cpp`). + +--- + +## Switch mode — HD rumble passthrough + +**Goal:** When OGX-Mini enumerates as a **Switch Pro** controller, console **HD rumble** must reach the **input** pad (especially Bluetooth DualSense / Xbox). + +**Problem:** Output reports **`0x01`** / **`0x10`** / **`0x11`** (and related rumble commands) were acknowledged for Switch protocol bookkeeping but never decoded into **`PadOut`**, so wireless pads never vibrated in Switch mode. + +**Approach:** Parse left/right HD rumble motor blocks (dekuNukem-style amplitude decode), write **`rumble_l` / `rumble_r`** into `PadOut`, and let Bluepad32 / USB host feedback forward motors to the input controller. Applies to all boards that support Switch output (including Pico 2 W / RP2354 with BT pads). + +**Files:** `src/USBDevice/DeviceDriver/Switch/Switch.cpp`, `Switch.h`. + +--- + +## Switch 2 Pro — anti-deadzone and L3/R3 + +**Goal:** Fix WebApp **anti-deadzone** drift and **L3/R3** mapping on wired **Switch 2 Pro** ([#64](https://github.com/MegaCadeDev/OGX-Mini-2026/issues/64)). + +**Problem:** Anti-Deadzone Circular/Square amplified imperfect Switch 2 Pro stick centers into constant bottom-left drift. Circular resize used the wrong WebApp field (`anti_dz_circle` vs `anti_dz_square`). Wired **L3/R3** were mapped to the wrong physical sticks. + +**Approach:** + +| Item | Detail | +|------|--------| +| **Idle gate** | Small noise floor when anti-dz is active (raw magnitude; adaptive with anti-dz strength) | +| **Circular formula** | Match WebApp (`anti_dz_square` where intended) | +| **L3/R3** | Correct physical stick clicks on wired Switch 2 Pro | +| **Profiles** | Joystick settings stay enabled when the saved profile customizes sticks/triggers | +| **Wired disconnect** | Bulk **keepalive** every **2 s** + USB disable-timeout after Switch 2 bulk bring-up; PIO unmount reboot **debounced 1.5 s** | + +**Files:** `Gamepad.h`, `JoystickSettings.*`, `SwitchPro.cpp`, `Standard.cpp`, `Switch2ProHost.cpp`. + +--- + +## STEAM mode — SteamOS / Bazzite (Linux desktop) + +**Goal:** On **SteamOS**, **Bazzite**, or other **Linux desktops** (including **Steam Deck** in desktop mode), present a **PS5 DualSense** USB gamepad **and** route the **touchpad** to a separate **HID mouse** so the user can move the desktop cursor without Steam Input remapping. + +### Select mode (button combos and build options) + +| Method | Detail | +|--------|--------| +| **Button combo** | **Start + Left Bumper + D-pad Up** — hold ~3 s; saved to flash; RP2040 resets | +| **Web app** | Output mode **SteamOS / Bazzite** (value `16`) | +| **Fixed build** | **`-DOGXM_FIXED_DRIVER=STEAM`** — also option **13** in `scripts/build.sh` / `build.ps1` fixed-mode menu | + +**User docs:** [README — SteamOS / Bazzite output mode](../../../README.md#steamos--bazzite-output-mode). **Mappings:** [Controller_Mappings.md — STEAM mode](Controller_Mappings.md#steamos--bazzite-steam-mode). + +### DualSense USB emulation + +| Item | Detail | +|------|--------| +| **USB VID:PID** | `054c:0ce6` (Sony DualSense) | +| **Interfaces** | IF0: DualSense gamepad (64-byte HID report, ID `0x01`); IF1: relative HID mouse | +| **DualSense input (BT or wired USB host)** | **`SteamPassthrough`** — full **64-byte** report copied each frame (ControllerMedic-style PS5 passthrough); wired host sticks/triggers taken from raw PS5 IN when available | +| **Other input (Xbox, DS4, Switch Pro, etc.)** | **`SteamBtReport`** — synthesizes a DualSense **`PS5::InReport`** from PadIn / `uni_gamepad_t` (face → Cross/Circle/Square/Triangle, LB/RB → L1/R1, triggers, sticks, D-pad, Share/Options, PS, touchpad-click bit) | +| **Descriptors** | `Descriptors/PS5Usb.h` (gamepad), `Descriptors/Steam.h` (composite + mouse interface) | + +### Touchpad → mouse + +| Item | Detail | +|------|--------| +| **Input** | **DualSense (PS5)** — Bluetooth on Pico W / Pico 2 W (`uni_hid_parser_ds5_get_touchpad()`), or wired USB host (`PS5Host::process_report()` → `SteamPassthrough::store()`) | +| **Output** | **`SteamTouchpad`** — touch finger deltas → **relative mouse** on IF1; touchpad click (`TP` / `MISC`) → **left button** | +| **Report merge (BT)** | Raw touch bytes merged into USB report offset **33** (`SteamTouchpad::apply_to_passthrough()`) for Linux `hid-playstation` compatibility | +| **No touchpad** | Mouse interface stays idle — no right-stick mouse fallback | + +### How it works (code path) + +1. **`SteamDevice::process()`** — Builds the outgoing DualSense USB report (passthrough or synthesized), then sends mouse HID **only** when the input pad has a touchpad. +2. **Bluetooth** — `SteamBtReport::update_from_uni_gamepad()` + touchpad parser; `Bluepad32.cpp` sets `SteamPassthrough::input_has_touchpad` for DS5. +3. **Wired USB host** — `PS5Host::process_report()` stores the raw report when output driver is **STEAM**. +4. **Non-PS5 input** — `SteamBtReport::update_from_gamepad()` / `fill_report_from_pad()` maps any supported PadIn into DualSense layout. + +After changing to STEAM mode, **replug** the adapter USB cable on the PC so the composite device (gamepad + mouse) enumerates cleanly. + +**Verify on Linux:** + +```bash +lsusb -v -d 054c:0ce6 2>/dev/null | grep -E "iProduct|bNumInterfaces|bEndpointAddress" +sudo evtest # select the mouse interface; drag touchpad for REL_X / REL_Y +``` + +**Files:** `src/USBDevice/DeviceDriver/Steam/` (`Steam.cpp`, `SteamPassthrough.h`, `SteamBtReport.h`, `SteamTouchpad.cpp`), `src/Descriptors/PS5Usb.h`, `src/Descriptors/Steam.h`, `src/Bluepad32/Bluepad32.cpp`, `src/USBHost/HostDriver/PS5/PS5.cpp`, `WebApp/modules/userSettings.js` (mode label **SteamOS / Bazzite**). + +--- + +## PS3 / PS4 output — motion passthrough + +**Goal:** In **PS3** and **PS4 (DualShock 4 USB)** output modes, pass **accelerometer** (and **gyro** when available) from modern input controllers into the emulated report so titles that use tilt / gyro respond correctly. **Switch** output mode does **not** pass through motion for now (Pro report IMU bytes stay zero). + +**Important — console wiring (PS3/PS4):** OGX-Mini is a **USB gadget** (it emulates a controller to a **host**). It does **not** replace a Brook-style dongle on its own. To play **motion games on a real PlayStation 3 or PlayStation 4**, chain: + +```text +[Your input pad] → (BT or USB host on OGX-Mini) → OGX-Mini → USB → [USB adapter] → PS3 or PS4 +``` + +**Tested adapter:** **[Brook Wingman XE 2 Converter](https://www.brookaccessories.com/products/wingman-xe2)** — validated for **PS3** Sixaxis titles with OGX-Mini in **PS3 output mode**. The same Brook adapter also works on **PS4**: set OGX-Mini to **PS4 output mode** (**Start + Left Bumper + D-pad Left**), plug OGX-Mini into the Brook, and connect the Brook to the **PS4** USB port. + +| Item | Detail | +|------|--------| +| **Output modes** | **PS3** (Start + D-pad Left) — Sixaxis in the DS3 report; **PS4** (Start + LB + D-pad Left) — Brook-style accel/gyro in the DS4 report | +| **Input — Bluetooth (Pico W / Pico 2 W)** | **DualShock 4**, **DualSense**, **Switch Pro**, **Wii Remote** (accel only; **0x31** report requested when motion output is active) | +| **Input — wired USB host** | **DualShock 4**, **DualSense**, **Switch 1 Pro**, **Switch 2 Pro** (IMU parsed from full HID payload) | +| **Wii Remote hold** | Point **IR end toward the TV**; roll/pitch tuned for that orientation | +| **Wii Motion Plus gyro** | Not yet parsed from extension reports — accel only from Wii Remote | +| **PC / emulator testing** | Motion is also present when OGX-Mini is in PS3/PS4 mode on a **PC** (no Brook required) | + +### How it works + +1. **`Gamepad::PadIn`** — `accel[3]`, `gyro[3]`, and `motion_source` tag the active IMU (`MOTION_SRC_DS4`, `DS5`, `SWITCH_PRO`, `SWITCH_USB`, `DS4_USB`, `DS5_USB`, `WII_BT`, etc.). +2. **`MotionImu.h`** — Unit conversion for USB Switch/DS4/DS5 payloads; **`remap_to_ds4_playing_frame()`** rotates Switch Pro and Wii BT samples into the DS4/Sixaxis playing frame (+Z ≈ 1 G when level). +3. **`PS3.cpp`** — `apply_pad_imu_to_ps3_sixaxis()` encodes accel into Linux **hid-sony** Sixaxis layout (X @ 41–42, Y/Z swapped per kernel); **Wii** uses a dedicated pitch/gravity wire assignment. +4. **`PS4.cpp`** — Brook-style accel/gyro scaling into the 64-byte DS4 gadget report when `has_motion()`. +5. **`MotionOutputActive`** — PS3/PS4 drivers set a flag so Bluepad32 can request Wii accel reports without `WII_MODE_ACCEL` (that mode breaks button layouts). +6. **`Bluepad32.cpp`** — Maps DS4/DS5/Switch/Wii motion from Bluepad reports; on connect requests **continuous** Wii **0x31** (DRM_KA) or **0x35** (DRM_KAE with Nunchuk) when motion output is active. Nunchuk stick → **left stick**. `uni_hid_parser_wii.c` implements **DRM_KAE** parsing. + +**Files:** `src/Gamepad/MotionImu.h`, `src/Gamepad/Gamepad.h`, `src/USBDevice/DeviceDriver/PS3/PS3.cpp`, `src/USBDevice/DeviceDriver/PS4/PS4.cpp`, `src/USBDevice/DeviceDriver/MotionOutputActive.h`, `src/USBDevice/DeviceManager.cpp`, `src/Bluepad32/Bluepad32.cpp`, `src/USBHost/HostDriver/SwitchPro/SwitchPro.cpp`, `src/USBHost/HostDriver/SwitchPro/Switch2ProHost.cpp`, `src/USBHost/HostDriver/PS4/PS4.cpp`, `src/USBHost/HostDriver/PS5/PS5.cpp`. + +**User docs:** [README — Motion controls](../../../README.md#ps3--ps4-motion-controls). + +**Button mappings (all modes):** [Controller_Mappings.md](Controller_Mappings.md) + +--- + +## Steam Controller 2026 (Triton) — Bluetooth + +**Goal:** Pair the **Steam Controller 2026** (Valve “Triton”) over **Bluetooth LE** to **Pico W**, **Pico 2 W**, and **RP2354** (Pi Radio Module 2) adapters and use it as a normal gamepad input in any USB or GPIO output mode. + +| Variant | VID:PID | Role | +|---------|---------|------| +| Body USB | `28de:1302` | Wired USB (host stack — not this feature) | +| Body BLE | `28de:1303` | **Wireless input target** | +| Puck dongle | `28de:1304` | Valve RF puck (PID registered) | +| Nereid | `28de:1305` | Related PID (registered) | + +**Advertisement / services (validated on Linux):** Appearance **`0x03c4`** (gamepad), HID UUID **`00001812`**, Valve vendor service **`100f6c32-…`**, Modalias **`usb:v28DEp1303`**. Address type is typically **random**. + +Unlike the original Steam Controller (custom GATT only), Triton also exposes **standard HID-over-GATT**. Main input report ID **`0x45`** (`TritonMTUNoQuat_t`: buttons, hall triggers, dual sticks, trackpads). Feature report **ID 1** + message **`0x87`** disables **lizard mode** (desktop mouse/keyboard), refreshed about every **3 s**. Rumble uses output report **`0x80`** (`MsgHapticRumble`) with a **~40 ms** resend while active. + +### Pairing (user) + +1. **Remove** the controller from any PC/phone Bluetooth list and make sure it is **disconnected** (BLE is one host at a time). +2. Put OGX-Mini in pairing mode (LED blinking). +3. On the controller, hold **RB + B + Steam** until it advertises. +4. Wait for connection rumble / solid LED. **Do not** pair the pad in Windows/macOS/phone Bluetooth settings for OGX use. + +### Changes + +1. **Bluepad32 parser** — `uni_hid_parser_steam_triton.c` / `.h`: decode **`0x45`** / **`0x42`** / **`0x47`** state reports, battery **`0x43`**, lizard-off + rumble, teardown of timers on disconnect. +2. **Controller type** — `k_eControllerType_SteamControllerTriton` (**61**); PIDs **`0x1302`–`0x1305`** in `uni_controller_list.h`. +3. **LE Secure Connections** — Triton rejects legacy SMP with **`AUTH_REQUIREMENTS_MISMATCH`**. Enabled **`ENABLE_LE_SECURE_CONNECTIONS`** in `Firmware/RP2040/src/btstack_config.h`. Non–Xbox / non–Switch-2 BLE pads pair with **`SM_AUTHREQ_SECURE_CONNECTION | SM_AUTHREQ_BONDING`** (`uni_bt_le.c`); one Just-Works retry if AuthReq still mismatches. +4. **Connection interval** — On setup, request **6–9 × 1.25 ms (~7.5–11 ms)** like Xbox BLE (default intervals feel very laggy). +5. **Button map** — Face / LB/RB / L3/R3 / d-pad / triggers as normal. Triton **View → Start**, **Menu → Back/Select** (matches SDL; printed names are not Xbox View/Menu). **Steam** (and QAM) → **Guide / SYS**. Grip paddles L4/L5/R4/R5 left unmapped. +6. **OGX path** — Rumble routed in `Bluepad32.cpp`; stall-watchdog exemption so idle HID does not force-disconnect. + +**References:** [SDL3 `SDL_hidapi_steam_triton.c`](https://github.com/libsdl-org/SDL/blob/main/src/joystick/hidapi/SDL_hidapi_steam_triton.c), [`controller_structs.h`](https://github.com/libsdl-org/SDL/blob/main/src/joystick/hidapi/steam/controller_structs.h). + +**Files:** `uni_hid_parser_steam_triton.c` / `.h`, `btstack_config.h`, `uni_bt_le.c`, `uni_controller_type.h`, `uni_controller_list.h`, `uni_hid_device.c`, `uni_gamepad.c`, `Bluepad32.cpp`. + +--- + +## Nintendo Switch 2 — Bluetooth (Pico W / Pico 2 W) + +**Goal:** Use **Nintendo Switch 2** controllers as **wireless input** on **Pico W** and **Pico 2 W**, in any USB or GPIO output mode (e.g. PS3, XInput, OG Xbox, Switch Pro emulation). + +**Confirmed wireless (BLE):** + +| Controller | Product ID | Notes | +|------------|------------|--------| +| **Switch 2 Pro** | **0x2069** | Full gamepad; same 63-byte composed report path as Joy-Con 2. | +| **Joy-Con 2 (L)** | **0x2067** | Solo or **merged pair** with Joy-Con R → one player. | +| **Joy-Con 2 (R)** | **0x2066** | Solo or merged with Joy-Con L (pair L first, then SYNC R). | + +**Not yet confirmed:** **NSO GameCube** (**0x2073**) — PID registered; GC-style report path exists in the parser. + +Switch 2 controllers use a **proprietary BLE GATT protocol**, not standard HID-over-GATT. They advertise as **“Nintendo Switch”** with manufacturer data and require a **custom pairing / SYNC** command sequence before encrypted input notifications arrive. + +**References (protocol reverse engineering — thank you to these projects):** + +| Project | Contribution | +|---------|----------------| +| **[Nadeflore/switch2-controllers](https://github.com/Nadeflore/switch2-controllers)** | Foundational Switch 2 **BLE discovery, pairing, and GATT** flow for Joy-Con 2 / Pro 2 on PC. | +| **[TommyWabg/switch2-controllers-windows10-gyro](https://github.com/TommyWabg/switch2-controllers-windows10-gyro)** | Extended fork with **63-byte composed input report** layout (button dword @ offset 4, sticks @ 10/13, IMU @ 48), **init / feature / rumble** command formats, and **Pro Controller** rumble packet structure. | +| **[BlueRetro #1249](https://github.com/darthcloud/BlueRetro/issues/1249)** (darthcloud) | Early community notes on **63-byte reports**, **notify handle 0x000A**, and Switch 2 **button bitfield** layout. | +| **`Switch2ProHost.cpp`** (this repo) | **Wired USB** bit layout for PID **0x2069** — cross-check for face / d-pad / **Home → SYS** mapping on PS3-oriented builds. | + +### Changes + +1. **New Bluepad32 parser** — `Firmware/external/bluepad32/.../parser/uni_hid_parser_switch2.c` (+ `.h`): GATT service discovery, **SYNC / pair** subcommands, calibration read, **input notify** on handle **0x0A**, **command** write/notify, **Pro** and **Joy-Con** vibration characteristics (keepalive rumble), connection parameter update (~7.5 ms), and teardown on disconnect. Supports PIDs **0x2069** (Pro), **0x2066** (Joy-Con R), **0x2067** (Joy-Con L). + +2. **LE advertisement hook** — `uni_bt_le.c` detects Switch 2 manufacturer data and routes connect to the Switch 2 parser instead of generic BLE HID. + +3. **Input decode** — **63-byte** notify: little-endian **button dword** at offset **4** (masks verified on hardware). Face, d-pad, ±, L3/R3, shoulders, ZL/ZR digital, **Home** (`0x100000` / `0x1000`), and **Capture** mapped into Bluepad32 `uni_gamepad_t`. Sticks calibrated from factory data; gyro/accel forwarded when present. + +4. **Home → PS / Guide** — Home sets `MISC_BUTTON_SYSTEM` → `MAP_BUTTON_SYS` in `Bluepad32.cpp` → PS3 / XInput / etc. **Home latch** (~24 frames) in the parser so Nintendo’s often **single-frame** Home pulse is not dropped by the lock-free Core1→Core0 pad staging buffer before the PS3 **8-frame SYS latch** runs. + +5. **Link stability** — Periodic **rumble keepalive** (zero-amplitude vibration write every ~5–8 ms) prevents **HCI disconnect (0x08)** when the pad would otherwise go idle without output reports. + +6. **OGX integration** — `Bluepad32.cpp`: rumble routing, motion source `MOTION_SRC_SWITCH_PRO`, stall-watchdog exemption, keepalive timer for Switch 2 BLE devices. + +7. **Joy-Con 2 pair merge** — When **Left** and **Right** Joy-Con 2 are both connected, input is **merged into one gamepad** (player slot = lower BT index, typically player 1). Left half: left stick, L/ZL, d-pad; right half: face buttons, right stick, R/ZR. Solo Joy-Con keeps BLE scan on for the partner; disconnecting one half unpairs and the other continues solo. + +8. **Switch 1 Joy-Con pair merge** — Original **Joy-Con (L/R)** over **Classic Bluetooth** (PIDs **0x2006** / **0x2007**) use the same **L first, then R** flow in `uni_hid_parser_switch.c`. When paired, each half uses **Pro-style** stick/button layout before merge; solo Joy-Con keeps **BR/EDR inquiry** running for the partner. Requires **two** Bluepad32 device slots (`CONFIG_BLUEPAD32_MAX_DEVICES=2`) even on single-player USB builds. + +9. **Joy-Con pair latency (Switch 1 + Switch 2)** — When **both** halves send input at once, merged pairs no longer feel laggy or drop button edges. See [§ Joy-Con pair merge — latency when both halves are active](#joy-con-pair-merge--latency-when-both-halves-are-active) below. + +**Pairing (user):** Put the **Pro 2** or **Joy-Con 2** in pairing mode (SYNC / hold the pairing button per Nintendo instructions). The adapter scans and connects; **do not** pair the pad in Windows/macOS Bluetooth settings first. + +**Joy-Con 2 as one controller (like Switch):** Pair the **Left** Joy-Con first, then put the **Right** Joy-Con in SYNC mode while the left is still connected. The firmware **merges** both halves into **one player** (gamepad slot **1** / the lower BT slot index). Left stick, d-pad, L/ZL map from the left Joy-Con; face buttons, right stick, R/ZR from the right. If only one Joy-Con is connected, it works solo and BLE scan stays on so you can add the partner. + +**Switch 1 Joy-Con (same user flow):** Pair **Left** first (hold **SYNC** on the rail), then **Right** while left stays connected. Uses Classic BT inquiry instead of BLE scan. Solo horizontal Joy-Con layout applies until the pair is formed; merged input uses a full gamepad layout. **Wired USB** for Switch 2–family pads remains in **Switch2ProHost** / **SwitchProHost** — see [Wired_Controllers.md](Wired_Controllers.md). + +**Files:** `uni_hid_parser_switch2.c`, `uni_hid_parser_switch2.h`, `uni_hid_parser_switch.c`, `uni_hid_parser_switch.h`, `uni_bt_le.c`, `uni_bt_bredr.c`, `uni_hid_device.c`, `uni_controller_list.h`, `src/Bluepad32/Bluepad32.cpp`. + +--- + +## Joy-Con pair merge — latency when both halves are active + +**Problem:** With **Left + Right** Joy-Cons merged into one player (Switch 1 Classic BT or Switch 2 BLE), pressing buttons on **both** halves at the same time could feel **laggy** or miss rapid edges. Switch 1 Pro / solo Joy-Con and Switch 2 Pro were unaffected. + +**Root causes:** + +1. **Stale merge** — Each half’s report was merged using the partner’s last parsed `controller.gamepad`, which could be **one frame behind** when L and R updated in the same interval. +2. **Dropped Core1→Core0 reports** — Bluetooth HID runs on **Core1**; a **single** lock-free staging slot could be **overwritten** before Core0 drained it when L and R fired back-to-back. +3. **Switch 2 radio / BT-thread load** — Dual BLE links plus **keepalive vibration writes** and **verbose raw-input logging** on button changes competed with inbound notifications on the shared CYW43439 radio. + +### Changes + +#### Shared (Switch 1 + Switch 2 parsers + Gamepad) + +1. **Per-half cached state** — Each Joy-Con slot stores its latest parsed `uni_gamepad_t` in a half cache (`sw1_joycon_half_gp` / `sw2_joycon_half_gp`). On any report from either half, merge reads **both caches** and emits **once** through the **primary** (left) device slot only. +2. **2-slot Bluetooth pad staging** — `Gamepad::set_pad_in_from_bluetooth()` uses a **two-entry ring** instead of one overwrite buffer, so rapid L-then-R reports are less likely to lose a frame before Core0 merges them into the main pad-in queue. **File:** `src/Gamepad/Gamepad.h`. + +#### Switch 1 (Classic Bluetooth) + +3. **IMU off when paired** — After L+R pair is established, both halves receive **SUBCMD_ENABLE_IMU (0)** so 0x30 reports carry less motion traffic and parsing skips IMU while paired. **File:** `uni_hid_parser_switch.c` — `switch_set_imu_enabled()`, `switch_establish_joycon_pair()`. + +#### Switch 2 (BLE) + +4. **Deferred merge emit** — When paired, L/R input notifications **update the half cache immediately** but schedule a **0 ms** run-loop timer to merge and call `uni_hid_device_process_controller()` **once** per BT tick if both halves updated in the same loop iteration. +5. **Keepalive traffic reduced** — Removed **keepalive on every input notification** (timer already maintains the link). **Secondary** Joy-Con **stops** its 5 ms keepalive timer when paired; **primary** alternates idle rumble between L and R every **10 ms** (one GATT write per tick). Keepalive is **deferred briefly** while either half is actively reporting. **Bluepad32** skips redundant feedback-path keepalive when the parser timer is already running. +6. **Joy-Con–specific parse** — Joy-Con 2 L/R parse **only their physical stick** (not both sticks + IMU on every 63-byte packet). Raw hex input logging on button change is **disabled** by default (`SW2_DEBUG_RAW_INPUT=0`) so UART work does not stall the BT thread during gameplay. +7. **Connection interval** — **7.5 ms** connection parameter update is requested on **both** halves when the pair is formed. + +**Files:** `uni_hid_parser_switch.c`, `uni_hid_parser_switch2.c`, `uni_hid_parser_switch2.h`, `src/Gamepad/Gamepad.h`, `src/Bluepad32/Bluepad32.cpp`. + +**User impact:** Paired Joy-Cons (Switch 1 or Switch 2) should feel as responsive when using **both** halves simultaneously as when using one half alone. Pairing flow unchanged: **Left first**, then **Right** (SYNC on the partner while left stays connected). + +--- + +## PIO USB host — wired connection fixes (Waveshare RP2350-USB-A) + +**Primary test platform:** **Waveshare RP2350-USB-A** (`OGXM_BOARD=RP2350_USB_A`, build option **7** in `scripts/build.sh`). Firmware uses the **Standard** path: **Core1** runs **`tuh_task()`** plus a **1 ms SOF timer** for PIO USB host. + +**Scope:** Changes live in **shared USB host drivers** and **`HostManager`**, not board-specific `#ifdef`s. Any build with **`CONFIG_EN_USB_HOST`** (Pico, Feather, RP2350-Zero, RP2350-USB-A, etc.) gets the same wired-host behavior. **Pico W / Pico 2 W Bluetooth** (Bluepad32) is **unchanged**. + +**Goal:** Reliable **wired USB host** input on PIO USB adapters for **Switch Pro 1/2**, **DualShock 3**, the **Xbox 360 wireless PC receiver** + pad, and **Xbox One GIP arcade sticks** (e.g. **Razer Atrox XBO**) — without starving the PIO stack during init or OUT transfers. + +### Shared — HostManager and PIO USB servicing + +1. **`pio_usb_host_frame()` on all PIO USB host boards** — Previously gated to **Pico W only**; now runs after **`send_feedback()`** / nested **`tuh_task()`** on **every** `CONFIG_EN_USB_HOST` build so wired IN reports do not die after ~1–2 s when the host sends OUT. +2. **Periodic feedback for init-heavy HID drivers** — **`HostManager::send_feedback()`** now always invokes **`send_feedback()`** for **PS3** and **Switch Pro / Switch 2 Pro** (not only on rumble / `new_pad_out()`), so wired init steps and keepalives run on a timer. +3. **`disconnect_cb()` on unmount** — Drivers get a clean teardown hook when a device is removed (bulk cancel for Switch 2, state reset for 360 wireless, etc.). +4. **XInput instance storage** — **`MAX_INTERFACES = MAX_GAMEPADS + OGXM_TUH_XINPUT_INSTANCES`**; **`OGXM_TUH_XINPUT_INSTANCES = 4`** when `MAX_GAMEPADS < 4` so the **360 wireless receiver’s four ports** each have a TinyUSB slot even on single-player builds. + +**Files:** `HostManager.h`, `Board/Config.h`. + +### Nintendo Switch Pro — wired USB + +**References:** + +| Project | Contribution | +|---------|----------------| +| **[Chromium `device/gamepad`](https://chromium.googlesource.com/chromium/src/+/main/device/gamepad/)** | **Switch 1 Pro wired USB** init: report **0x80** subcommands (MAC, handshake, baud, disable timeout), **0x81** ack handling, **0x12** output + subcommands for LED / mode / IMU. | +| **`Tools/controller_capture/switch2_usb_init.py`** (this repo) | Captured **Switch 2–family bulk OUT** packet sequence used on PC before HID reports work. | +| **[HandHeldLegend/procon2tool](https://github.com/HandHeldLegend/procon2tool)** | **ProCon 2 enabler** — same bulk bring-up pattern as the capture tool; cross-check for **17-packet** interface **1** sequence. | +| **Bluepad32 `uni_hid_parser_switch.c`** | **Switch 1** button bit layout (`Buttons0/1/2`) for wired report decode validation. | +| **`Switch2ProHost.cpp`** (v1.0.0.9a) | **Switch 2 Pro wired** digitals from report **0x09** — extended here with bulk-only bring-up (no post-bulk Switch 1 init). | + +### Switch 1 Pro (PID 0x2009) — wired USB init + +1. **Wired USB state machine** — `InitState` phases: **USB_MAC** → **USB_HANDSHAKE1** → **USB_BAUD** → **USB_HANDSHAKE2** → **USB_NO_TIMEOUT** → **LED** → **LED_HOME** → **FULL_REPORT** → **IMU** → **DONE**. +2. **0x80 path** — OUT on report ID **`SwitchPro::REPORT_ID_USB_OUT` (0x80)** with subcommands **`USB_SUB_MAC`**, **`USB_SUB_HANDSHAKE`**, **`USB_SUB_BAUD`**, **`USB_SUB_DISABLE_TIMEOUT` (0x04)**; advance on matching **0x81** IN ack bytes. +3. **0x12 + 0x33 probe** — After disable-timeout, send **`CMD::AND_RUMBLE` (0x12)** with subcommand **`USB_PROBE` (0x33)**; on **0x21** subcommand ack, continue to player LED and **full report mode** subcommands. +4. **PIO-friendly OUT** — **`try_hid_out_id()`** checks **`tuh_hid_send_ready()`**; **`send_feedback()`** calls **`pio_usb_host_frame()`** + **`tuh_task()`** during init so Core1 keeps the PIO host alive. +5. **Per-IN advancement** — Init steps also run from **`process_report()`** on each IN packet, not only from the feedback timer. +6. **Timeout fallback** — If **USB_NO_TIMEOUT** ack is slow, **8 retries** in **`send_feedback()`** force advance to **LED** so init does not hang indefinitely. +7. **Input mapping fixes** — D-pad **UP↔DOWN** and **LEFT↔RIGHT** swapped; **L3↔R3** swapped in **`SwitchProHost::process_report()`**. + +### Switch 2 Pro (PID 0x2069) and Switch 2–family USB — bulk bring-up + +Applies to Nintendo VID **0x057E** PIDs **0x2066**, **0x2067**, **0x2069**, **0x2073** (`is_switch2_usb_family()`). + +1. **Bulk sequence** — Read configuration descriptor, open **interface 1** bulk IN/OUT, send **17 packets** from **`Switch2UsbInitPackets.h`** (~**12 ms** spacing between rounds), optional bulk IN read per round when an IN endpoint exists. +2. **No post-bulk Switch 1 init** — On successful bulk completion, **`init_state_ = DONE`** immediately and **`tuh_hid_receive_report()`** starts — **Switch 1 `0x80` HID init is skipped** for Switch 2–family devices. +3. **Switch2ProHost** — Parses **64-byte** HID report ID **0x09** (1-byte counter + **10-byte** payload) with **Switch 2–specific** button bits (see v1.0.0.9a / [Wired_Controllers.md](Wired_Controllers.md)). +4. **Async safety** — Bulk callbacks use **`switch2_make_cb_token(daddr, instance)`** and **`HostManager::get_switch_pro_host()`** to look up the live driver; safe after hot-swap or unplug. +5. **Teardown** — **`disconnect_cb()`** / destructor call **`switch2_cancel_bringup()`**: abort in-flight bulk transfers, clear pending retry state. + +### DualShock 3 — wired USB host + +**Problem:** **DualShock 3** over **USB cable** failed to connect or dropped shortly after plug-in on **PIO USB host** boards. Init used the **USB gadget** default OUT template (leading **`0x01`**) and **async-only** control transfers without servicing **`pio_usb_host_frame()`**, so enable/LED commands never completed reliably. + +**Reference:** [USB Host Shield PS3](https://github.com/felis/USB_Host_Shield_2.0) host report buffer / feature **0xF4** enable sequence. + +#### Changes + +1. **Host OUT report layout** — **`init_host_out_report()`** builds the **USB Host Shield `PS3_REPORT_BUFFER`** layout (player LED bitmap **`0x02 << player`**, per-LED timing fields) instead of copying gadget **`DEFAULT_OUT_REPORT`**. +2. **Synchronous wired init** — On plug-in: **SET feature 0xF4** `{0x42, 0x0c, 0x00, 0x00}` then **synchronous OUT** (rumble/LED report) via **`send_control_xfer_wait()`**, polling **`pio_usb_host_frame()` + `tuh_task()`** up to **150 ms** per step. +3. **Immediate input** — Wired path sets **`reports_enabled`** after enable + LED OUT (and BT **0xF5** when applicable); **`tuh_hid_receive_report()`** starts without the old multi-stage **GET feature 0xF2** chain. +4. **Bluetooth auto-pair (BT boards)** — After **0xF4** + LED, **`program_ds3_bt_host()`** sync-sends **feature 0xF5** with the adapter BD_ADDR (see section below). +5. **PIO keepalive** — **`KEEPALIVE_MS = 1000`** on USB host sends periodic neutral OUT so the DS3 stays connected when idle; **`HostManager`** includes PS3 in periodic **`send_feedback()`**. +6. **Async OUT for rumble** — Runtime rumble/keepalive uses non-blocking **`send_control_output_async()`** so feedback does not stall IN. + +**File:** `src/USBHost/HostDriver/PS3/PS3.cpp`, `PS3.h`. + +**User testing (Waveshare RP2350-USB-A):** **DualShock 3 wired** connects and holds input after flash. + +### Xbox 360 wireless receiver — wired USB host + +**Problem:** **Microsoft Xbox 360 wireless PC receiver** (`045e:0719`) plugged into the adapter’s **PIO USB host** port often failed to **sync** a wireless pad or delivered **no input** when **`MAX_GAMEPADS=1`**. Only one of four receiver interfaces was stored; **controller-present** was mis-detected (**headset `0x40`** treated as paired); connect path **slept 1 s** and blocked PIO servicing; **`Xbox360WHost`** assumed a fixed struct layout for variable-length wireless reports. + +#### Changes + +1. **Four XInput instances on single-player builds** — **`OGXM_TUH_XINPUT_INSTANCES = 4`** when `MAX_GAMEPADS < 4`; **`MAX_INTERFACES = MAX_GAMEPADS + OGXM_TUH_XINPUT_INSTANCES`**; **`host_storage_index()`** maps XInput instances to **`MAX_GAMEPADS + instance`**. +2. **One driver, four ports** — **`HostManager`** treats sibling **`XBOX360W`** interfaces as sharing one gamepad slot when `MAX_GAMEPADS=1`; **`process_report` / `connect_cb` / `disconnect_cb`** fall back to the mounted **`Xbox360WHost`** when the per-instance slot has no driver pointer. +3. **`tuh_xinput` pairing** — **`prime_port_for_pairing()`** on each idle port: **RUMBLE_ENABLE + player LED** (quadrant from interface number **0/2/4/6**); called at **set_config** and periodically via **`service_wireless_ports()`** from **`HostManager::send_feedback()`**. +4. **Connect detection** — Controller present when **IN byte1 & 0x80** (not merely non-zero / headset **0x40**); on connect: **RUMBLE_ENABLE**, **`wait_for_tx_complete()`** (200 ms cap), **LED** — **no 1 s sleep**. +5. **Input reports** — Accept **report size 0x13 or 0x14**; **`Xbox360WHost::process_report()`** decodes **button word, triggers, sticks** from raw byte offsets; tracks **`active_instance_`** for rumble on the port that last connected. +6. **Rumble** — **`set_rumble()`** skips **RUMBLE_ENABLE** when already connected; returns false if port not connected (avoids spurious OUT on idle RF slots). + +**Files:** `Board/Config.h`, `HostManager.h`, `HostDriver/XInput/tuh_xinput/tuh_xinput.cpp`, `HostDriver/XInput/Xbox360W.cpp`. + +**User testing (Waveshare RP2350-USB-A):** **360 wireless receiver + pad** sync and input confirmed over PIO USB host. + +### Razer Atrox Xbox One — GIP arcade stick (wired USB host) + +**Problem:** **Razer Atrox Arcade Stick** for **Xbox One** (`1532:0a00`) enumerates as a **vendor-class GIP** device (not standard Microsoft `0x47/0xD0` XInput). On PIO USB host it would **mount** (adapter LED on) but the stick **lit briefly then powered off** with **no inputs** — or **flashed on/off** when the host retried **POWER_ON** too often. + +**References:** + +| Project | Contribution | +|---------|----------------| +| **[OOPMan/XBOFS.win](https://github.com/OOPMan/XBOFS.win)** | Atrox-specific **GIP init** (`05 20 00 01 00`), **30-byte** IN reads, input layout (face/LT/RT on **byte 22**). | +| **Linux `xpad`** | Generic Xbox One **POWER_ON** packet and **64-byte** IN for standard gamepads; **MAP_TRIGGERS_TO_BUTTONS** quirk for `1532:0a00` in the device table. | + +#### Changes + +1. **Vendor GIP detection** — **`tuh_xinput::open()`** claims interface **0** for known XBO arcade sticks (`1532:0a00`, Mad Catz TE2, PDP, Hori, etc.) via **`XboxArcadeStick.h`** VID/PID table. +2. **Arcade init path (XBOFS)** — **`start_xboxone()`** (deferred **50 ms** after mount from **`XboxOneHost::initialize()`**): send **POWER_ON** once, **`wait_for_tx_complete()`** on OUT with **`pio_usb_host_frame()`**, then arm **30-byte** IN polling. Standard Xbox One pads keep **IN-first** + full **`xboxone_init()`** with **64-byte** reads. +3. **Input decode** — **`XboxOneHost`** uses **XBOFS byte layout** for arcade VID/PIDs; standard pads use **`GipWireButtons`** / **`InReport`** layout. +4. **PIO keepalive** — **`service_gip()`** from **`HostManager::send_feedback()`** re-arms stalled IN every **250 ms** for arcade sticks only. **Does not** repeat **POWER_ON** (repeated power commands caused visible **on/off flashing**). +5. **Xbox 360 Atrox** (`24c6:5000`) — unchanged: standard **XInput** with **digital trigger** bytes in **`Xbox360Host`**. + +**Files:** `src/USBHost/HostDriver/XInput/XboxArcadeStick.h`, `tuh_xinput/tuh_xinput.cpp`, `tuh_xinput/tuh_xinput.h`, `XboxOne.cpp`, `XboxOne.h`, `HostManager.h`. + +**User testing (Waveshare RP2350-USB-A):** **Razer Atrox Xbox One** (`1532:0a00`) — connect, stay powered, and full button input confirmed. + +### Hot-swap (Switch 1 ↔ Switch 2 Pro) + +Unplug during bulk or HID init runs **`HostManager::disconnect_cb()`** → driver **`disconnect_cb()`** → bulk cancel. Plugging the other model starts a fresh init path (Switch 1 **0x80** vs Switch 2 bulk) without requiring an adapter power cycle. + +### Files (all wired-host fixes) + +| File | Role | +|------|------| +| `src/USBHost/HostManager.h` | PIO **`pio_usb_host_frame()`** on all host boards; PS3/Switch periodic feedback; 360W driver routing; **`disconnect_cb`** on unmount; **`get_switch_pro_host()`** | +| `src/Board/Config.h` | **`OGXM_TUH_XINPUT_INSTANCES`** (4 when `MAX_GAMEPADS < 4`) | +| `src/USBHost/HostDriver/SwitchPro/SwitchPro.cpp` | Switch 1 wired init, Switch 2 bulk bring-up, Switch 1 input mapping, PIO USB service loop | +| `src/USBHost/HostDriver/SwitchPro/SwitchPro.h` | Init state machine, bulk bring-up members, `disconnect_cb` | +| `src/USBHost/HostDriver/SwitchPro/Switch2ProHost.cpp` | Switch 2 Pro wired button decode | +| `src/USBHost/HostDriver/SwitchPro/Switch2UsbInitPackets.h` | 17 bulk OUT packets (capture tool / HHL) | +| `src/Descriptors/SwitchPro.h` | Wired USB constants (`REPORT_ID_USB_OUT`, `USB_SUB_*`, `USB_PROBE`) | +| `src/USBHost/HostDriver/PS3/PS3.cpp` | DualShock 3 wired init, sync control xfer, 1 Hz keepalive | +| `src/USBHost/HostDriver/XInput/tuh_xinput/tuh_xinput.cpp` | 360 wireless receiver port priming, connect detect, **`service_wireless_ports()`**, **`start_xboxone()`**, **`service_gip()`** | +| `src/USBHost/HostDriver/XInput/tuh_xinput/tuh_xinput.h` | GIP arcade interface state (`gip_power_sent`, IN stall tracking) | +| `src/USBHost/HostDriver/XInput/XboxArcadeStick.h` | XBO arcade VID/PIDs, **30-byte** xfer size, XBOFS report parser | +| `src/USBHost/HostDriver/XInput/XboxOne.cpp` | Arcade vs standard GIP decode; deferred **`start_xboxone()`** | +| `src/USBHost/HostDriver/XInput/Xbox360W.cpp` | Wireless input decode from byte offsets, active instance rumble | + +**Build:** `./scripts/build.sh` → board **RP2350_USB_A**; typical **`-DMAX_GAMEPADS=1`**. Optional **`-DOGXM_SWITCH2_HID_RAW_LOG=ON`** for UART hex when Switch 2 Pro digital button bytes change. + +**User testing (Waveshare RP2350-USB-A):** **Switch 2 Pro wired**, **DualShock 3 wired**, **Xbox 360 wireless receiver + pad**, and **Razer Atrox Xbox One** (`1532:0a00`) confirmed on PIO USB host. **Switch 1 Pro wired** — init and mapping updated; retest after flash if a prior S2 regression was seen. + +--- + +## Xbox 360 (XInput) support + +**Goal:** Use the adapter in XInput mode on Xbox 360 with Bluetooth controllers (e.g. PS5, Xbox One). The 360 requires XSM3 authentication and specific USB descriptors. + +**References:** [joypad-os](https://github.com/joypad-ai/joypad-os) XInput implementation; [libxsm3](https://github.com/InvoxiPlayGames/libxsm3). + +### Changes + +1. **Descriptors** + - Device and configuration descriptors aligned with joypad-os (153-byte config, 4 interfaces, bConfigurationValue 1, bMaxPower 0xFA). + - Configuration callback returns `nullptr` for `index != 0` (single config). + - String descriptor index 4 (XSM3 security) uses a 96-character buffer and the full string (no 31-char truncation). + +2. **XSM3 authentication** + - XSM3 state is initialized at driver init (not in the 0x81 callback). + - Challenge init (0x82) and verify (0x87) data are stored when received; crypto (`xsm3_do_challenge_init` / `xsm3_do_challenge_verify`) runs in the main loop (`process()`), not in the USB callback. + - 0x83 responses: 46 bytes for init, 22 bytes for verify. + - 0x86 state: 1 = processing, 2 = response ready. + +3. **USB on Pico 2 W** + - USB is initialized before Core1 (Bluetooth) so the 360 can enumerate and run XSM3 even if BT firmware is still loading. + +4. **Control handling** + - Vendor and class control requests are forwarded to the active driver so XSM3 traffic reaches the XInput handler. + +5. **Wake console from standby (remote wakeup)** + - When the Xbox 360 has been turned off via **Guide → Turn off console** (not the front power button), the console may keep USB power and put the bus in suspend. The adapter can then wake the console by signaling USB remote wakeup when you: + - **Press Guide (Home)** on the controller, or + - **Hold Start for 3 seconds** (avoids holding Guide on Xbox One/PS5 pads, which can turn the controller off). + - Remote wakeup is advertised in the configuration descriptor; the stack defaults it enabled when supported so wake works even if the host did not send SET_FEATURE before standby. + - **Disclaimer:** Wake only works when the console was previously powered on with the adapter connected and then turned off via the controller (soft shutdown). It **cannot** power on the console from a cold start—e.g. if the console was just plugged in, lost power completely, or was turned off with the front power button. In those cases use the console’s power button to turn it on first. + +Descriptors and XSM3 flow are aligned with [joypad-os](https://github.com/joypad-ai/joypad-os); a local comparison doc may be kept out of the repo for reference. + +--- + +## PS3 mode — input delays, stuck inputs, Home button, and analog stick emulation + +**Source:** Fixes from [OGX-Mini-Plus](https://github.com/guimaraf/OGX-Mini-Plus) (v1.1.1) — *“PS3 Driver Fixes - Fixed input delays and stuck inputs.”* Plus subsequent improvements for Home (PS) button and DS3-accurate analog sticks. + +**Files:** `src/USBDevice/DeviceDriver/PS3/PS3.cpp`, `src/Descriptors/PS3.h` + +### Changes + +1. **L2/R2 axis values** + - `report_in_.l2_axis` and `report_in_.r2_axis` are now set from `gp_in.trigger_l` and `gp_in.trigger_r`. + - Previously left at zero, which could cause stuck or incorrect trigger behaviour on PS3. Filling these is required for many PS3 games. + +2. **DualShock 3–accurate analog sticks** + - Sticks now match the real DS3/Sixaxis HID spec: + - **Range:** 0–255 (full 8-bit; was 0–254). + - **Center:** **0x80 (128)** at rest and in deadzone (was 0x7F). Matches Linux gamepad spec and HID logical max 255. + - **Deadzone:** ~1.5% (512 on ±32768) so small movements register without drift; in deadzone the report sends 0x80. + - **Scaling:** Linear map from signed 16-bit input to 0–255 with correct rounding so center (0) → 128. + - Reduces stick drift and matches console expectations for DS3-compatible games. + +3. **D-pad in analog mode** + - When `gamepad.analog_enabled()` is true, D-pad axes (`up_axis`, `down_axis`, `left_axis`, `right_axis`) are now derived from the **digital** D-pad bits instead of `gp_in.analog[ANALOG_OFF_*]`. + - Prevents noisy analog D-pad values from causing stuck or wrong D-pad input on PS3. + +4. **Face button axes (digital / non-analog branch)** + - Corrected mapping so that: + - `circle_axis` = BUTTON_B (was BUTTON_X) + - `cross_axis` = BUTTON_A (was BUTTON_B) + - `square_axis` = BUTTON_X (was BUTTON_A) + - Circle / Cross / Square now match the intended face buttons. + +5. **Home (PS) button** + - Report is built only when `tud_hid_ready()` (right before send), using `gamepad.get_pad_in()` so the console gets the latest state (helps with DS4/DS5 over Bluetooth). + - **PS button latch:** When `BUTTON_SYS` is set, the driver latches the PS bit for 8 consecutive report frames so short taps are not missed by timing. `buttons[2]` bit 0 (PS) and bit 1 (Touchpad) are set from `BUTTON_SYS` and `BUTTON_MISC`. + - If Home does not work over Bluetooth, try a wired controller (some consoles only react to the first controller's Home). + +6. **Wake console from standby (remote wakeup)** + - The configuration descriptor now advertises **remote wakeup** (`bmAttributes` 0xA0) so the PS3 can suspend the USB bus in standby and the adapter can signal wake. When the console is in standby (turned off via **PS button → Turn off system** or similar, not full power loss), you can wake it by **pressing PS (Home)** or **holding Start for 3 seconds**—**only if the console keeps USB power when off**. + - **Many PS3s cut power to the USB ports** when shut down, so the adapter and controller disconnect and wake is not possible. If your controller stays powered (e.g. charging LED) when the PS3 is off, that model may keep USB in standby and wake may work. Same disclaimer as 360: no wake from cold start; use the console power button first if needed. + - **Recovery Mode:** Even when wake from standby is not possible (e.g. console cuts USB when off), the adapter is **confirmed working in PS3 Recovery Mode** — you can use it to navigate and select options in Recovery. + +7. **Host rumble → Bluetooth pad (PS3 mode on PC)** *(v1.0.0.9a)* + When the adapter is in **PS3 output mode** and connected to a **Windows** host, the OS still sends **HID output** (rumble) to the device. That output is forwarded to the **Bluetooth** gamepad as `PadOut` rumble. Some Windows stacks leave **noise** in the report (small large-motor values, or non‑`0`/`1` small-motor bytes). Forwarding that blindly caused **constant vibration** on the wireless controller at idle. + **Fix:** Ignore large-motor values **below a small threshold** (deadzone), and treat the **small motor as on only when the host byte is `1`** (not “any non-zero”). + **File:** `PS3.cpp` — block that runs when `new_report_out_` is set after `set_report_cb` parses the DS3 output report. + +8. **Sixaxis / motion passthrough** *(v1.0.0.12a)* + When the input gamepad provides IMU data (`has_motion()`), accel/gyro are encoded into the DS3 Sixaxis fields using Linux **hid-sony** wire layout. See [§ PS3 / PS4 output — motion passthrough](#ps3--ps4-output--motion-passthrough) for supported input pads, Wii Remote notes, and **Brook Wingman XE 2** console wiring. + +--- + +## Latency reduction + +**Goal:** Reduce input-to-output latency in the device (Core0) main loop, especially for XInput with Bluetooth controllers (PS5, Xbox One). + +### Main loop delay + +- **Before:** The device loop used `sleep_ms(1)` every iteration, then a configurable `MAIN_LOOP_DELAY_US` (default 250 µs). +- **After:** + - **Default: 0 µs** — no added delay; loop runs as fast as possible for minimum latency (low-latency default). + - **250+ µs** — set via CMake (e.g. `-DMAIN_LOOP_DELAY_US=250`) to reduce CPU use if desired. + +**Files changed:** + +- `src/Board/Config.h` — `MAIN_LOOP_DELAY_US` default **0**; override via CMake. +- `src/OGXMini/Board/Standard.cpp`, `PicoW.cpp`, `Four_Channel_I2C.cpp` — use `sleep_us(MAIN_LOOP_DELAY_US)` when `> 0`. +- `CMakeLists.txt` — `MAIN_LOOP_DELAY_US` cache variable (default 0). + +### XInput (360): report always fresh; send when ready (minimal latency) + +Same goal as Switch Pro and PS3: the only added latency when using a wireless controller is the Bluetooth radio. + +- **Every** `process()` call: read `get_pad_in()` and build `in_report_` (buttons, triggers, sticks). Then, if suspended, wake; call `tud_xinput::send_report(&in_report_)`. `send_report()` only actually transmits when the IN endpoint is free (`send_report_ready()`); otherwise we keep the latest `in_report_` so that (1) the host’s `get_report_cb` returns current state if it polls, and (2) the next time the endpoint is free we send that report. No `new_pad_in()` gate. +- **File:** `src/USBDevice/DeviceDriver/XInput/XInput.cpp` — `process()` always builds `in_report_` every loop; `tud_xinput::send_report()` sends only when `send_report_ready()` (see `tud_xinput.cpp`). + +### Switch Pro and PS3: report always fresh; send when ready (minimal latency) + +**Goal:** For Switch Pro and PS3 output modes, the only added latency should be wireless Bluetooth (radio) when using a BT controller. The adapter does not batch, throttle, or delay reports. + +- **Switch Pro:** Every `process()` call reads `get_pad_in()`, builds `switch_report_`, and builds the standard or subcommand report into `report_` **before** any USB decisions. So (1) the host’s `get_report` (poll) always gets the latest `report_`, and (2) when `tud_hid_n_ready(0)` we push that same report. Init reply (0x81) is sent first when pending, then the standard report. No “only build when ready” — report is always current. +- **PS3:** Every `process()` call reads `get_pad_in()` and builds `report_in_` (full DS3 report). Then, when `tud_hid_ready()`, we send it. So (1) the host’s `get_report_cb` always returns the latest `report_in_`, and (2) we push that report whenever the IN endpoint is free. No “only build when ready” — report is always current. + +Both modes: no `new_pad_in()` gate; main loop runs with `MAIN_LOOP_DELAY_US=0` by default; `tud_task()` runs before `process()` so the endpoint is ready when we try to send. + +### Xbox OG (Duke) gamepad: send only on new input (match Team-Resurgent) + +Report build/send timing matches [Team-Resurgent/OGX-Mini](https://github.com/Team-Resurgent/OGX-Mini): the HID report is built and sent **only** when `gamepad.new_pad_in()` is true. Sending every poll caused random disconnects on some OG Xbox setups; the “send when new input” rule avoids that. Guide (SYS) combos are kept: **Guide only** = IGR (LT+RT+Start+Back), **Guide+Start** = shutdown (LT+RT+Back+White). Rumble handling is unchanged. + +- **File:** `src/USBDevice/DeviceDriver/XboxOG/XboxOG_GP.cpp` — `process()` builds `in_report_` and calls `tud_xid::send_report()` only when `new_pad_in()` and `send_report_ready(0)`. + +**DualSense (PS5) input when outputting to OG Xbox:** The PS5 USB host no longer skips reports that are byte-identical to the previous one. Previously, “unchanged” reports did not call `set_pad_in()`, so with a polled output (OG Xbox) some transitions or sustained input could be dropped when the host loop was slower than the DualSense report rate. Every DualSense report is now pushed into the gamepad queue so the OG Xbox device always has the latest state. **File:** `src/USBHost/HostDriver/PS5/PS5.cpp` — removed the unchanged-report early return; every report is parsed and passed to `gamepad.set_pad_in()`. + +### Main loop order: tud_task() before process() + +The main loop now calls **`tud_task()` before** `device_driver->process()`. That way the USB stack updates completion status of the previous IN transfer first; then `process()` sees the endpoint as ready and can send the next report immediately with the latest gamepad state. Reduces latency by up to one main-loop iteration (avoids sending only every other loop when the host polls frequently). + +**Files:** `src/OGXMini/Board/PicoW.cpp`, `Standard.cpp`, `Four_Channel_I2C.cpp` — order is `process_tasks()` → `tud_task()` → `process()` for each gamepad. + +--- + +## PS2 (GPIO) / Open PS2 Loader stability + +**Issue:** With a “primed” first response byte (0xFF) before the mode byte, Open PS2 Loader could hang at startup (black screen) when the adapter was connected. + +**Fix:** The PS2 controller (device) response was reverted so the **first response byte is the mode byte** (no leading 0xFF). Protocol and escape-mode response lengths otherwise follow PicoGamepadConverter and DS4toPS2. The main loop drains all pending PS2 transactions each tick so rapid pad init (e.g. OPL at boot) does not desync. Core1 runs Bluetooth when used; Core0 runs the main loop and `psx_device_poll()` so the console sees input correctly. + +**File:** `src/USBDevice/DeviceDriver/PS1PS2/controller_simulator.c` — first response byte is the mode byte; no `prime_first_byte()` / leading 0xFF. + +--- + +## PS2 (GPIO) and OG Xbox — Home/Guide IGR and shutdown + +**Goal:** Use the Home (PS2) or Guide (OG Xbox) button for in-game reset (IGR) and console shutdown with the same interaction pattern on both platforms: **Home only** = restart (IGR), **Home+Start** = shutdown. + +### PS2 (GPIO) mode + +**Files:** `src/USBDevice/DeviceDriver/PS1PS2/PS1PS2.cpp`, `PS1PS2.h` + +- **Home only** — Sends the OPL in-game reset combo: **L1+L2+R1+R2+Start+Select** (triggers full). The console restarts the game / returns to OPL. +- **Home+Start** — Sends shutdown combo: **L1+L2+R1+R2+L3+R3** (triggers full). The console shuts down. + +### OG Xbox mode + +**Files:** `src/USBDevice/DeviceDriver/XboxOG/XboxOG_GP.cpp`, `XboxOG_GP.h` + +- **Guide only** — Sends IGR (restart) combo: **LT+RT+Start+Back** (triggers full). The console performs a soft reset / returns to dashboard (or IGR handler). +- **Guide+Start** — Sends shutdown combo: **LT+RT+Back+White** (triggers full). The console shuts down. + +### How to use + +| Goal | Setting | +|------|--------| +| Low latency (default) | Use default `MAIN_LOOP_DELAY_US=0`. | +| Lower CPU use | Configure with e.g. `-DMAIN_LOOP_DELAY_US=250`. | + +### Notes + +- Core1 (Bluetooth / gamepad) runs with no sleep in its loop. +- USB full-speed poll interval (e.g. 4 ms for XInput) still applies; the improvement is that each report carries the **latest** input and the main loop adds no extra delay by default. +- Bluetooth adds latency; the changes above minimize the adapter’s contribution. + +--- + +## Additional board support (RP2350_ZERO, RP2040_XIAO, RP2354) + +**Source:** [Sakura-Research-Lab/OGX-Mini-2026-Testing](https://github.com/Sakura-Research-Lab/OGX-Mini-2026-Testing). + +Three additional boards use the same Standard (PIO-USB host) code path as PI_PICO, RP2040_ZERO, ADAFRUIT_FEATHER, and RP2350_USB_A: + +| Board | CMake option | Notes | +|-------|--------------|--------| +| Waveshare RP2350-Zero | `OGXM_BOARD=RP2350_ZERO` | PIO USB D+ = GP10, RGB = GP16; RP2350. | +| Seeed Studio XIAO RP2040 | `OGXM_BOARD=RP2040_XIAO` | PIO USB D+ = GP0, RGB = GP12, LED = GP17. | +| RP2354 | `OGXM_BOARD=RP2354` | **RP2350 + Pi Radio Module 2** (CYW43439, same class as **Pico 2 W**): **Bluetooth** wireless pads + PIO USB host on **GP0/GP1**, LED **GP25**. | + +**Files:** `src/Board/Config.h` (board IDs and pin defines), `src/OGXMini/OGXMini.cpp` (init/run/host_mounted tables), `src/OGXMini/Board/Standard.cpp` (extended `#if`), `CMakeLists.txt` (board branches). + +--- + +## 8BitDo XInput (Xbox 360) host fix + +**Source:** [Sakura-Research-Lab/OGX-Mini-2026-Testing](https://github.com/Sakura-Research-Lab/OGX-Mini-2026-Testing). + +Some 8BitDo wired XInput controllers (VID 0x2DC8, PID 0x3016 or 0x3106) can disconnect or behave oddly if the host leaves the controller LED on. The Xbox 360 host driver now detects these devices by VID/PID and schedules a repeating delayed task (every 1 s) that sends “LED off” to the controller. Other controllers are unchanged (LED stays on as before). + +**File:** `src/USBHost/HostDriver/XInput/Xbox360.cpp` — `initialize()` calls `tuh_vid_pid_get()`, and if 8BitDo, queues `TaskQueue::Core1::queue_delayed_task(..., 1000, true, set_led(..., false))`. + +--- + +## PS5 (DualSense) over Bluetooth — reducing perceived delay vs Xbox One + +**Why PS5 can feel slower than Xbox One over BT:** DualSense sends larger reports (78 bytes, with gyro/accel), and Bluepad32’s DS5 parser does more work per report (calibration, etc.). Xbox One reports are smaller and the parser is lighter. Bluetooth poll/report rate and radio latency dominate; the adapter’s job is to not add extra delay. + +**Change in this firmware:** The PS5 adaptive-trigger toggle (touchpad/mute button) now runs **after** `set_pad_in(gp_in)`. Previously it ran at the start of the callback; when you pressed the touchpad it could send two output reports (left/right trigger effect) before updating gamepad state, which could delay the next main-loop read. Now the gamepad state is always written first, then the trigger effect is sent, so input is not held up by the trigger command. + +**If you need minimum latency with a DualSense:** Use the controller **wired** on the PIO USB host port when possible; wired PS5 uses the same low-latency path as other USB host controllers and avoids BT report size and rate limits. + +--- + +## Pico W / Pico 2 W — PIO USB wired controller unplug detection + +**Problem:** On **Pico W / Pico 2 W**, the **USB gamepad** plugs into a **PIO USB** host. While PIO owns **D+ / D−**, reading line state with **`gpio_get()`** (as in **`pio_usb_bus_get_line_state()`** / **`hcd_port_connect_status()`**) often **does not** show a clean **SE0** after you pull the cable — lines can **float** or sit in a state that still looks like full-speed idle. The firmware could keep thinking the port was **connected**, so **TinyUSB** stayed up, **HostManager** still had a slot, and **Bluetooth** stayed blocked (wired takeover) until a power-cycle or “shorting” the port. + +**Approach:** In **`pico_w_pio_usb_bt_mux_tick()`** (`src/OGXMini/Board/PicoW.cpp`), treat **unplug** when **`HostManager::any_mounted()`** is still true **and** **any** of these **hints** fires (all share one **debounce**, ~**60 ms** wall time): + +1. **`!hcd_port_connect_status(BOARD_TUH_RHPORT)`** — line-based disconnect when the HCD stack *does* see disconnect. +2. **No configured TinyUSB device** — loop device addresses **`1 … CFG_TUH_DEVICE_MAX + CFG_TUH_HUB`** (matches TinyUSB’s internal **`TOTAL_DEVICES`**) and require **`tuh_mounted(d)`** for at least one address. If the stack has dropped configuration, tear down even if the line hint lied. +3. **No USB host input for several seconds** — **`HostManager::usb_host_input_idle_ms()`** (default threshold **3000 ms** in `PicoW.cpp`) — after unplug, **IN transfers** and **`process_report()`** usually stop even if (1) and (2) lag. **`record_usb_host_input_activity()`** is called from **`process_report()`** and after successful **`setup_driver()`** (`src/USBHost/HostManager.h`). **`HostManager::initialize()`** seeds **`last_usb_host_input_ms_`** from **`board_api::ms_since_boot()`** so idle time is not huge before the first device. + +After debounced confirmation, **`pico_w_usb_host_full_stop()`** runs **`tuh_deinit`**, stops the SOF timer, clears unplug debounce state, and **`board_api_usbh::enable_host_line_irq_monitoring()`** so normal **GPIO unplug/plug** IRQs work again; **Bluetooth** release paths run as before. + +**Tuning:** If unplug feels slow, lower **`USB_UNPLUG_NO_INPUT_MS`** in `PicoW.cpp`. If a controller that **rarely sends reports** when idle ever mis-triggers unplug, raise that constant slightly. + +--- + +## DualShock 3 — automatic USB programming for Bluetooth pairing + +**Background:** The **DualShock 3** does not implement standard Bluetooth pairing. The host’s **BD_ADDR** must be written to the pad over **USB** using a **HID feature report** (report ID **`0xF5`**, 8 bytes: id + padding + 6-byte MAC). This is what the [Bluepad32 sixaxispairer](https://bluepad32.readthedocs.io/en/latest/pair_ds3/) utility does from a PC. + +**Behavior in this firmware:** On builds with **`CONFIG_EN_BLUETOOTH`** (e.g. **Pico W / Pico 2 W / RP2354**), after sync wired init (**feature 0xF4** + LED OUT), **`PS3Host::program_ds3_bt_host()`** sends **`SET_REPORT`** **feature 0xF5** with **`uni_bt_get_local_bd_addr_safe()`** using the same **synchronous control path** as **0xF4** (so PIO USB host frames complete the transfer). If the local address is still **all zeros**, a **deferred** flag is set and **`try_deferred_ds3_bt_pair()`** retries from **`process_report()`** until the address is valid, then sends **0xF5** once. + +**Regression note:** An earlier PIO USB wired-init change marked init complete before the old async **GET 0xF2 → SET 0xF5** chain ran, so **`0xF5` was never sent**. Pairing no longer depends on that F2 chain. + +**User steps:** Plug the **DS3 into the adapter’s USB host port** (wait for it to work wired if you like), then **unplug** and press **PS** to use it wirelessly — same as the manual pairing flow documented for Bluepad32. + +**Files:** `src/USBHost/HostDriver/PS3/PS3.cpp`, `PS3.h`. + +--- + +## Pico W / Pico 2 W — DualShock 4 and Classic Bluetooth (BR/EDR) stability + +**Context:** On **CYW43439** (Pico W / Pico 2 W), **BLE** and **Classic Bluetooth (BR/EDR)** share one radio. **DualShock 4** uses **Classic ACL** only. **DualSense**, **Xbox Series (BLE)**, and **Switch Pro** (typical pairing) use **LE** — so DS4 was uniquely sensitive to how the stack used the radio at the same time as other activity. + +### 1. BLE advertising vs Classic ACL (main DS4 drop fix) + +**OGX-Mini** starts **BLE advertising** via **BLEServer** so the **phone / web app** can discover the adapter. If that advertising stays on while a **Classic** gamepad (DS4, DualShock 3 BT, Xbox 360 wireless) holds an **ACL** link, the connection often **dies within seconds** (e.g. blue LED then disconnect). + +- **`gap_advertisements_enable(0)`** at the **start of DS4 HID setup** (before lightbar / calibration traffic), and again whenever **any** ready gamepad uses **`GAP_CONNECTION_ACL`**. +- **`gap_advertisements_enable(1)`** when the **last** Classic pad disconnects (BLE-only controllers, e.g. DualSense still connected, are unaffected by this rule). + +**Files:** `Firmware/external/bluepad32/.../parser/uni_hid_parser_ds4.c` (OGXM Pico path), `Firmware/RP2040/src/Bluepad32/Bluepad32.cpp`. + +**User impact:** While a **Classic Bluetooth** controller is connected, the adapter **does not advertise** for the BLE web app. Disconnect that controller (or use **USB** for setup) to use **Bluetooth** in the web app again. + +### 2. BR/EDR inquiry while Classic is connected + +**Periodic inquiry** (scanning for new gamepads) **plus** an active **Classic ACL** also contends on the same radio and contributed to **short-lived DS4** links. + +- **`uni_bt_bredr_scan_stop()`** at the beginning of DS4 setup and when any **ACL** pad becomes **ready**. +- **`uni_bt_bredr_scan_start()`** when the **last** Classic pad disconnects (if scanning is still enabled globally). + +**File:** `Bluepad32.cpp` (and DS4 setup in `uni_hid_parser_ds4.c`). + +### 3. DS4 virtual “touchpad mouse” disabled on Pico + +Bluepad32 can register a **second virtual HID device** (mouse) on the same DS4 link. On Pico W that path was linked to **unstable links**; the OGX build (**`OGXM_BLUEPAD32_PICO_W`**) **does not create** that virtual device. **DualSense** still uses its normal setup (virtual child rejected in `device_ready` where applicable). + +**File:** `uni_hid_parser_ds4.c` (CMake defines `OGXM_BLUEPAD32_PICO_W=1` for the Bluepad32 library in `Firmware/RP2040/CMakeLists.txt`). + +### 4. PS4 rumble / FF grace period + +For **`CONTROLLER_TYPE_PS4Controller`**, **rumble output** is **delayed ~6 seconds** after connect so the host cannot push force-feedback during the fragile init window. + +**File:** `Bluepad32.cpp` — `send_feedback_cb` / `device_ready_cb`. + +### 5. Related Pico W Bluetooth improvements (see also Summary table) + +- **Core0 `sleep_ms(1)`** in the main loop so Core1 (BT stack) gets CPU time ([Team-Resurgent/OGX-Mini](https://github.com/Team-Resurgent/OGX-Mini) pattern). +- **Lock-free Bluetooth input path** (`set_pad_in_from_bluetooth`) so HID callbacks never block on Core0’s mutex. **Joy-Con pairs:** **2-slot** staging ring so back-to-back L/R reports are not overwritten before Core0 drains (see [§ Joy-Con pair merge — latency](#joy-con-pair-merge--latency-when-both-halves-are-active)). +- **Reconnect:** last device disconnect calls **`uni_bt_enable_new_connections_unsafe(true)`** so pairing can resume without power-cycling. +- **8 s input-stall disconnect** (non-virtual, non-BLE-Xbox) to clear zombie links; **BLE Xbox** uses keepalive instead of stall disconnect. + +### 6. Connection rumble when the wireless controller is ready *(v1.0.0.9a)* + +When Bluepad32 reports **`device_ready`** for a non-virtual gamepad, the firmware plays about **one second** of **dual-motor rumble** (via `play_dual_rumble` when the controller supports it) so you can **feel** that the Bluetooth link is up. **DualShock 4** uses a **~1.2 s delay** before starting that rumble so it does not overlap the fragile post-connect window (aligned with the existing PS4 rumble grace logic). Other controller types use a shorter default delay. + +**File:** `src/Bluepad32/Bluepad32.cpp` — `ogxm_play_connection_rumble()`, called from `device_ready_cb`. + +--- + +## Switch Pro — analog stick sensitivity + +A configurable sensitivity gain is applied to the analog sticks in Switch Pro emulation. Raw stick values (outside the deadzone) are scaled by **STICK_GAIN_NUM / STICK_GAIN_DEN** (default 120/100 = 1.2×) before mapping to the 12-bit Switch report. The same physical deflection produces slightly larger output for a more responsive feel. + +**File:** `src/USBDevice/DeviceDriver/Switch/Switch.cpp` — `gamepad_to_switch_report()`; tune via `STICK_GAIN_NUM` and `STICK_GAIN_DEN`. + +--- + +## Build scripts (new users) + +To build firmware without memorizing CMake options, use the interactive build scripts from the **project root**: + +| Platform | Command | +|----------|---------| +| **Linux / macOS** | `./scripts/build.sh` | +| **Windows (PowerShell)** | `.\scripts\build.ps1` | + +The script checks for required tools (git, python3, cmake, ninja, arm-none-eabi-gcc) and prints install hints if something is missing. It then prompts for: (1) board (Pi Pico, Pico W, Pico 2 W, RP2040-Zero, XIAO, Feather, 4CH I2C, ESP32 hybrid, etc.); (2) default (all modes via combos) or fixed output mode (e.g. Wii, GameCube, N64); (3) Release or Debug. Build output (`.uf2`, `.elf`) is written to **`scripts/build/`**; on failure you can save a log to `scripts/build_log.txt`. See the main [README](../../../README.md) Build section for the full description. + +--- + +## Future planned + +Work that is **not implemented** in current firmware but has been researched. These items are **not** a matter of adding a VID/PID to a table — they need new host drivers and/or radio stacks. + +### Xbox Wireless Adapter for Windows (`045e:02e6`, `045e:02fe`) + +**Devices:** + +| VID:PID | Model | USB product string | Status | +|---------|-------|-------------------|--------| +| **`045e:02e6`** | 1713 | Xbox Wireless Adapter for Windows (older) | **Not supported** | +| **`045e:02fe`** | 1790 | **XBOX ACC** (newer) | **Not supported** | + +These are **USB dongles** that receive **Xbox Wireless** (proprietary 2.4 GHz) from Xbox One and Xbox Series controllers. They are **not** gamepads and do **not** enumerate as XInput devices. + +**Do not confuse with the Xbox 360 PC receiver** — that **is supported** today: + +| VID:PID | Device | Why it works | +|---------|--------|--------------| +| **`045e:0719`** (typical) | Xbox 360 wireless **PC receiver** | Presents a standard XInput-style USB interface (`bInterfaceSubClass=0x5D`, `bInterfaceProtocol=0x81`). Handled by `tuh_xinput` / `Xbox360WHost`. | + +The One/Series dongles use a **completely different USB shape** (vendor bulk, not XInput interrupt). + +#### What the dongle looks like on USB + +From hardware dumps (e.g. [ControllersInfo adapter descriptor](https://github.com/DJm00n/ControllersInfo/blob/master/xboxone/DescriptorDump_Adapter%20(Xbox%20Wireless%20Adapter%20for%20Windows).txt)): + +- **Device class:** vendor-specific (`0xFF/0xFF/0xFF`), product name **"XBOX ACC"** +- **Endpoints:** **bulk** IN/OUT (512-byte packets on USB 2.0), not the interrupt endpoints used by **wired** Xbox One pads +- **Inside the stick:** a **Mediatek MT76** Wi‑Fi chipset that must run **dongle firmware** before it can talk to controllers + +OGX-Mini’s existing **wired Xbox One GIP** path (`tuh_xinput`, subclass `0x47` / protocol `0xD0`, `XboxOneHost`) parses GIP **after** the controller is already connected over USB. The wireless adapter never exposes that interface — the host must drive the **dongle radio** first. + +#### Why this is not a simple port + +1. **Firmware upload** — Linux [xone](https://github.com/medusalix/xone) / [xow](https://github.com/medusalix/xow) load **`xow_dongle.bin`** / variant blobs (e.g. **`xone_dongle_02fe.bin`**) into the MT76 chip over USB before the device is useful. That blob must be shipped in flash and the load sequence reimplemented on RP2040. + +2. **Wireless stack, not HID** — The driver brings up **MT76** (channels, pairing scan, client join/leave, optional encryption), wraps **GIP** payloads in **802.11-style data frames**, and sends them on bulk OUT queues. Inbound bulk IN carries WLAN frames that must be parsed to extract GIP input. This is the bulk of [xone `transport/dongle.c`](https://github.com/medusalix/xone/blob/master/transport/dongle.c) + [`transport/mt76.c`](https://github.com/medusalix/xone/blob/master/transport/mt76.c) — thousands of lines, not a report-descriptor tweak. + +3. **Resource cost** — xone uses many bulk URBs, WLAN buffers up to tens of KB per packet, and ongoing radio work. RP2040 flash/RAM and Core1 USB host timing are tight compared to a PC kernel driver. + +4. **Pairing model** — Controllers must be **paired to the dongle** (Sync on the pad while the dongle is in pairing mode). Pads previously used over **USB** or **Bluetooth** will not auto-attach until re-paired to the dongle — same as on Windows with xone. + +5. **Reuse is partial only** — Once a wireless client is connected and GIP input arrives, decoding could **reuse** existing GIP constants and mapping from `Descriptors/XboxOne.h` / `XboxOneHost.cpp`. Everything **before** that (USB dongle + radio + framing) is new work — comparable in scope to adding Switch 2 bulk bring-up, but **larger** because of firmware + Wi‑Fi. + +**Rough implementation phases (if pursued):** + +1. New TinyUSB **vendor bulk** class driver; claim `045e:02e6` / `045e:02fe`; embed firmware; port MT76 init from xone. +2. Bulk I/O loops, pairing, client add/remove (mirror `Xbox360W` connect callbacks in `HostManager`). +3. Bridge decoded GIP `0x20` / `0x07` reports into `XboxOneHost` (or a shared GIP parser). +4. Rumble/LED over wireless GIP OUT path. + +**Practical alternatives today:** + +- **Xbox 360 wireless receiver** + 360 pads — already supported (`Xbox360WHost`). +- **Xbox One / Series controller** — **Bluetooth** on Pico W / Pico 2 W (Bluepad32), or **wired USB** (`XboxOneHost`). + +**References:** [medusalix/xone](https://github.com/medusalix/xone), [medusalix/xow](https://github.com/medusalix/xow), [SDL discussion of 02fe vs XInput PID](https://github.com/libsdl-org/SDL/pull/8683), [ControllersInfo dongle descriptor dump](https://github.com/DJm00n/ControllersInfo/tree/master/xboxone). + +**Files that would be touched (when implemented):** new `tuh_xbox_dongle` (or similar) under `USBHost/HostDriver/XInput/`, `HostManager.h`, `tuh_callbacks.cpp`, `tusb_config.h`; possible shared GIP layer with `XboxOne.cpp`. + +--- + +## Summary + +| Area | Improvement | +|------|-------------| +| **XInput (360)** | XSM3 authentication and descriptors aligned with joypad-os; adapter works on Xbox 360 with BT controllers (PS5, Xbox One). **360 wireless PC receiver** supported (`Xbox360WHost`). **v1.0.0.11a:** PIO USB wired receiver — **4 XInput instances** when `MAX_GAMEPADS=1`, port **priming**, **0x80** connect detect, byte-offset decode. **Razer Atrox XBO** (`1532:0a00`) — vendor GIP arcade path via **XBOFS** init + **30-byte** IN + XBOFS input layout; **POWER_ON** once only. **Xbox One/Series wireless dongle (`045e:02e6` / `02fe`)** — see [Future planned](#future-planned). 8BitDo wired fix: LED keepalive for VID 0x2DC8 / PID 0x3016 or 0x3106. **v1.0.0.12a (#38):** default (non–WebApp-customized) sticks use **stock 360-like** feel — see [§ XInput stock stick feel](#xinput--xbox-360--stock-stick-feel). | +| **PS3** | Stuck inputs and delays addressed via L2/R2 axes; DS3-accurate sticks (0–255, center 0x80, ~1.5% deadzone); D-pad and face button mapping; Home (PS) button with 8-frame latch for BT controllers. **v1.0.0.9a:** PC host rumble deadzone + strict small-motor `0`/`1`. **v1.0.0.11a:** **DualShock 3 wired USB host** — USB Host Shield init, sync control xfer + PIO service, **1 Hz keepalive**. **v1.0.0.12a:** motion passthrough (see Motion row); **DS3 USB→BT auto-pair** sync **0xF5** restored on BT boards. | +| **PS2 (GPIO)** | Home only = IGR (L1+L2+R1+R2+Start+Select); Home+Start = shutdown (L1+L2+R1+R2+L3+R3). OPL and protocol stability (first response byte = mode byte). | +| **OG Xbox** | Guide only = IGR. Shutdown = LT+RT+Back+White via **Guide+Start** or **Guide+View (Back)**; Xbox BT often omits Start while Guide is held. Shutdown report strips Start so the chord matches BIOS/softmod expectations. | +| **Switch Pro** | Analog stick sensitivity gain (default 1.2×) for more responsive sticks; configurable in `Switch.cpp`. **v1.0.0.11a:** **Switch 1/2 Pro wired** on PIO USB host — Chromium **0x80** init, bulk bring-up, mapping fixes. **v1.0.0.12a:** **HD rumble** from console → `PadOut` for BT/USB input pads. See [§ Switch HD rumble](#switch-mode--hd-rumble-passthrough) and [§ PIO USB host — wired connection fixes](#pio-usb-host--wired-connection-fixes-waveshare-rp2350-usb-a). | +| **Switch 2** | **Pro 2** (wired **0x2069**): **Switch2ProHost** + bulk bring-up (v1.0.0.11a). **Pro 2 + Joy-Con 2 L/R** (BLE): **uni_hid_parser_switch2** — GATT pairing, 63-byte input, rumble keepalive, Home → SYS latch, **L+R pair merge**, **dual-half latency fixes**. **v1.0.0.12a (#64):** anti-deadzone idle gate + L3/R3 / wired disconnect keepalives. See [§ Nintendo Switch 2 — Bluetooth](#nintendo-switch-2--bluetooth-pico-w--pico-2-w), [§ Joy-Con pair latency](#joy-con-pair-merge--latency-when-both-halves-are-active), [§ anti-deadzone](#switch-2-pro--anti-deadzone-and-l3r3). | +| **Steam Controller 2026** | **Triton BLE** (`28de:1303`) on Pico W / Pico 2 W / RP2354 — HOGP parser, **LE Secure Connections**, lizard-off, rumble, **~7.5–11 ms** conn interval, View→Start / Menu→Back. See [§ Steam Controller 2026](#steam-controller-2026-triton--bluetooth). | +| **Boards** | RP2350_ZERO, RP2040_XIAO supported (Standard/PIO-USB host). **RP2354:** RP2350 + **Pi Radio Module 2** — **Bluetooth** + PIO USB host (Pico 2 W firmware path). **RP2350-USB-A (Waveshare):** v1.0.0.11a validation for **Switch Pro, DualShock 3, 360 wireless receiver**, and **Razer Atrox XBO** wired host. | +| **Latency** | Main loop delay default **0 µs**; `tud_task()` before `process()` so reports send every loop when ready; XInput/Switch/PS3 send latest state when USB ready (no `new_pad_in()` gate). Switch Pro and PS3 always build report every loop so host poll (`get_report`) and IN push both see current state — only remaining delay is BT radio when wireless. | +| **STEAM (SteamOS / Bazzite)** | **Start + LB + D-pad Up** (~3 s), web app, or **`-DOGXM_FIXED_DRIVER=STEAM`**. USB **DualSense** (`054c:0ce6`) + **HID mouse**. **DualSense input:** passthrough report + **touchpad → mouse** (BT or wired USB host). **Other pads:** synthesized DualSense report; **no stick-mouse fallback**. See [§ STEAM mode](#steam-mode--steamos--bazzite-linux-desktop). | +| **Motion (PS3/PS4 out)** | **v1.0.0.12a:** Accel/gyro passthrough into emulated DS3/DS4 from DS4/DS5/Switch Pro/SW2/Wii Remote. Console use needs **Brook Wingman XE 2** (or similar). See [§ motion passthrough](#ps3--ps4-output--motion-passthrough). | +| **Build** | Interactive scripts `scripts/build.sh` (Linux/macOS) and `scripts/build.ps1` (Windows) for board selection, fixed/default mode (**STEAM**, PS4, Wii, etc.), and Release/Debug; output in `scripts/build/`. See [README](../../../README.md) Build section. | +| **Bluetooth (Pico W / 2 W / RP2354)** | **DS4 / Classic ACL:** BLE advertising **paused** while Classic pad connected; **BR inquiry stopped** during ACL; **no DS4 virtual mouse**; **6 s PS4 rumble** grace. **Xbox Series (BLE):** no stall disconnect when idle; keepalive 12 s. **v1.0.0.12a:** after last **ready** pad disconnects, restore pairing mode then **watchdog reboot** (~500 ms); **USB resume** + **45 s** idle scan watchdog restore pairing without reboot; **LE Secure Connections** for pads that require it (e.g. Triton); **Steam Controller 2026** HOGP parser. **Switch 2 (BLE):** Pro 2 + Joy-Con 2 L/R — custom GATT parser, rumble keepalive, Home latch, **L+R merge**, **dual-half latency fixes**. **Switch 1 Joy-Con (Classic BT):** L+R merge, IMU off when paired. **General:** `sleep_ms(1)` main loop; lock-free BT pad-in (**2-slot** staging for Joy-Con pairs). **v1.0.0.9a:** ~**1 s connection rumble** at `device_ready` (DS4 delayed start). | +| **PIO USB host (Pico W)** | **Wired unplug:** Debounced combo of **HCD connect**, **`tuh_mounted` over all device addresses**, and **no `process_report` / setup activity** (~**3 s**) so disconnect registers when D+/D− line state is wrong under PIO; **`tuh_deinit`** + restore GPIO line IRQs + BT release. **v1.0.0.11a (all PIO host boards):** **`pio_usb_host_frame()`** after feedback OUT; wired **Switch Pro / PS3 / 360 receiver / Razer Atrox XBO** connection fixes — see [§ PIO USB host — wired connection fixes](#pio-usb-host--wired-connection-fixes-waveshare-rp2350-usb-a). | +| **DS3 + Bluetooth** | **USB auto-pair:** After sync **0xF4** + LED, **feature `0xF5`** programs the **DS3** with the adapter’s **BD_ADDR** (`CONFIG_EN_BLUETOOTH`); deferred if BT address not ready. **v1.0.0.12a:** restored after PIO USB wired-init regression (sync SET, no F2 chain). See [§ DualShock 3 — automatic USB programming for Bluetooth pairing](#dualshock-3--automatic-usb-programming-for-bluetooth-pairing). | +| **Multi-adapter (same console)** | **Known limitation:** Two OGX-Mini units plugged into the **same** console usually collide — identical **VID/PID**, shared USB serial (e.g. XInput `"1.0"`), and on **Xbox 360** the same **XSM3 ID**. Workarounds without firmware changes: **one OGX + one native/other-brand pad**, or a **single multi-port** host (e.g. 360 wireless receiver). Unique per-unit identity is not implemented yet. | diff --git a/Firmware/RP2040/docs/PICO2W_WII_USB_SETUP.md b/Firmware/RP2040/docs/PICO2W_WII_USB_SETUP.md new file mode 100644 index 00000000..f2c34a55 --- /dev/null +++ b/Firmware/RP2040/docs/PICO2W_WII_USB_SETUP.md @@ -0,0 +1,110 @@ +# Pico 2 W – Wii Mode with USB Controller + +This document describes how **Pico 2 W** (and Pico W) use a **USB gamepad** on an **external USB host port** when the adapter is in **Wii (Wiimote) mode**. The approach is the same as in [PicoGamepadConverter](https://github.com/wiredopposite/PicoGamepadConverter): the single Bluetooth radio is used for the Wiimote connection to the Wii, so the controller must be USB and must be on **PIO USB** (external port), not the built-in USB. + +--- + +## 1. Hardware constraints + +- **Pico 2 W** (and Pico W) have a single **Bluetooth radio** (CYW43439). +- In **Wii mode** that radio is used for the **Wiimote connection** (Pico → Wii). +- You **cannot** use Bluetooth for both the Wii and a wireless controller at the same time. +- **Solution:** Use a **USB controller** on an external USB host port (PIO USB on GPIO). The controller must be plugged into the **external** USB connector (D+/D− on GPIO), not the Pico’s built-in USB port. + +--- + +## 2. USB host: use PIO USB (external port) in Wii mode + +### The bug (in projects that get it wrong) + +If the firmware uses the **wrong USB port** for the controller in Wii mode: + +- **`tuh_init(0)`** → **native USB (roothub 0)** = the Pico’s **built-in USB port** (the one used for programming). +- The controller is on the **external USB** on **GP0/GP1** (**PIO USB, roothub 1**), which is then **never used**. +- Result: controller gets power (e.g. vibrate) but is **never enumerated** → no LED, no input. + +### The fix in OGX-Mini + +When the output mode is **WII** (Wiimote) on Pico W / Pico 2 W, the firmware uses **PIO USB** (roothub port **1**) for the host: + +- Configure PIO USB with the chosen D+/D− pins (and optional swap). +- Call `tuh_configure(1, TUH_CFGID_RPI_PIO_USB_CONFIGURATION, &pio_cfg)` then `tuh_init(1)`. +- Run `tuh_task()` in the host loop. + +So the controller **must** be plugged into the **external** USB port (PIO USB). The built-in USB is not used for the host in Wii mode. + +--- + +## 3. PIO USB pin configuration (Pico W / Pico 2 W) + +For OGX-Mini, Pico W and Pico 2 W use the following when USB host is used (e.g. Wii mode). Defined in `Board/Config.h` for `CONFIG_OGXM_BOARD_PI_PICOW` / `PI_PICO2W`: + +- **`PIO_USB_DP_PIN`** – Base pin for D+ (e.g. `0` → D+ = GP0, D− = GP1 with default pinout). +- **`PIO_USB_SWAP_DP_DM`** – Optional. Set to `1` if the controller gets power but does not enumerate (D+/D− swapped on the cable). + +**Wiring:** + +| Config | D+ | D− | +|---------------------|------|------| +| Swap = 0, PIN = 0 | GP0 | GP1 | +| Swap = 1 | GP1 | GP0 | + +Also connect USB **GND** and **VBUS** (5 V) to the external connector. + +--- + +## 4. Building for Pico 2 W (Wii mode) + +Wii mode is **build-option only**: you must build with **`-DOGXM_FIXED_DRIVER=WII`**. Button combos cannot switch into or out of Wii; the firmware is Wii-only and combos are disabled. + +- Set **`OGXM_BOARD`** to **`PI_PICO2W`** (e.g. `-DOGXM_BOARD=PI_PICO2W`). +- Set **`OGXM_FIXED_DRIVER`** to **`WII`** (e.g. `-DOGXM_FIXED_DRIVER=WII`). +- On Windows, use the project’s **build script** (e.g. `scripts/build-with-vs.ps1 -Board PI_PICO2W -Debug`) or a **Visual Studio Developer Command Prompt** so host tools build correctly. + +Example (from `Firmware/RP2040`): +`.\scripts\build-with-vs.ps1 -Board PI_PICO2W -FixedDriver WII -Debug` + +--- + +## 5. Usage summary (Wii mode on Pico 2 W) + +1. **Wire** the external USB connector: D+ and D− to the chosen GPIOs (e.g. GP0/GP1), plus GND and 5 V (VBUS). +2. **Build** firmware with **`-DOGXM_FIXED_DRIVER=WII`** for **PI_PICO2W** and **flash** the `.uf2`. +3. **Plug** the USB controller into the **external** port; the controller LED should turn on when it enumerates. +4. **Sync** the Pico to the Wii (Wii Sync, then Pico as needed). The Pico appears as a Wiimote; input comes from the USB gamepad. + +--- + +## 6. External USB in other output modes (Pico W / Pico 2 W) + +For firmware built for **Pico W / Pico 2 W** with **USB host** enabled but **not** Wii-only (e.g. combo builds, Xbox/Switch/PS output over Bluetooth or USB device), the **same external PIO USB port** (D+/D− on GP0/GP1 per §3) can host a **wired gamepad**. Input is merged with the normal gamepad path and appears on the chosen output. + +- **Mutual exclusion with Bluetooth:** If **any** Bluetooth gamepad is already connected, the firmware does **not** bring up PIO USB host for that tick; wired devices on the external port are **ignored** until **all** Bluetooth gamepads disconnect. Conversely, when a wired controller **enumerates** on the external port, **all** Bluetooth gamepads are disconnected and **new** Bluetooth connections are disabled until that wired device is **unplugged** (then Bluetooth pairing/connect is allowed again). +- **No external port required:** If you do **not** wire the external connector, the firmware **debounces** D+/D− activity before starting the host; builds behave like **Bluetooth-only** adapters with no dependency on the port being present. + +**Wii-only builds** (`-DOGXM_FIXED_DRIVER=WII`) still use the dedicated Wii USB host path on Core 1 as before; this section applies to **non-Wii** Pico W / Pico 2 W builds that include `CONFIG_EN_USB_HOST`. + +--- + +## 7. Debug / UART logging (crash debugging) + +To see where the firmware gets to when switching to Wii mode (or if it crashes), build in **Debug** so that **UART logging** is enabled. Logs go to **UART1** on **GP4 (TX)** and **GP5 (RX)** so they do not conflict with PIO USB on GP0/GP1. (GP10/11 are not valid UART pins on RP2040/RP2350.) + +1. **Build Debug** for Pico 2 W: `cmake -G Ninja -DOGXM_BOARD=PI_PICO2W -DCMAKE_BUILD_TYPE=Debug ` then `ninja`. Or use the build script with a Debug build directory. +2. **Connect a USB–UART adapter** (3.3 V) to **GP4 (TX)** and **GP5 (RX)** and GND. Open a terminal at **115200** baud. +3. **Flash** the Debug `.uf2`, switch to Wii mode, and watch the log. You should see: `PicoW init: start` … `driver inited`; `PicoW run: wii_mode=1` … `Core1 launched`; `WII Core1: entry` … `HostManager init done` … `tuh_init done` … `entering loop`; `PicoW run: Wii main loop tick`. The **last line printed** before a crash or hang shows how far the code got. + +--- + +## 8. Troubleshooting + +| Symptom | Likely cause | What to do | +|--------|----------------|------------| +| Controller vibrates, no LED, doesn’t sync | Host using native USB instead of PIO USB | Ensure Wii mode uses PIO USB (roothub 1) as in §2. | +| Same | D+ and D− swapped on cable | Set `PIO_USB_SWAP_DP_DM 1` in config and rebuild, or swap D+ and D− wires. | +| No response at all | Wrong port or no VBUS | Use the **external** USB (PIO USB) and ensure VBUS has 5 V. | +| Crash when switching to Wii | Debug with UART (GP4/5) | Build Debug, connect UART to GP4/5 @ 115200; see §7. | + +--- + +This setup gives **Wii (Wiimote) output** with a **USB controller** on the Pico 2 W’s external USB host port, with the single Bluetooth radio reserved for the Wii connection. diff --git a/Firmware/RP2040/docs/README.md b/Firmware/RP2040/docs/README.md new file mode 100644 index 00000000..11fb0803 --- /dev/null +++ b/Firmware/RP2040/docs/README.md @@ -0,0 +1,14 @@ +# RP2040 firmware docs + +See the [project README](../../README.md) for overview, supported platforms, build, and configuration. + +## Firmware docs (this folder) + +| Document | Description | +|----------|-------------| +| [**Controller_Mappings.md**](Controller_Mappings.md) | **Master reference:** PadIn layout, input → PadIn (BT/USB), and PadIn → every output mode (XInput, PS3, PS4, Switch, STEAM, OG Xbox, Wii U, GPIO, Wii output). | +| [Wii_Mode_Guide.md](Wii_Mode_Guide.md) | Wii mode (build-option only): No Extension / Nunchuk / Classic, USB host, sync and auto-connect, button mapping, supported controllers. | +| [PICO2W_WII_USB_SETUP.md](PICO2W_WII_USB_SETUP.md) | Pico 2 W / Pico W: USB host wiring (PIO USB), pins, build, troubleshooting for Wii mode. | +| [GPIO_Output_Pinout_and_Mappings.md](GPIO_Output_Pinout_and_Mappings.md) | GPIO pin-outs and default button/stick mappings for PS1/PS2, Dreamcast, GameCube, and N64 output (and input) modes. | +| [Dreamcast_Port.md](Dreamcast_Port.md) | Dreamcast (Maple Bus) port status and notes for completing the port from DreamPicoPort (stub only in firmware for now). | +| [IMPROVEMENTS.md](IMPROVEMENTS.md) | Firmware improvements and **Future planned** (e.g. Xbox One/Series wireless dongle `045e:02e6` / `02fe`); **STEAM mode**, **PS3/PS4 motion**, **XInput stock stick feel (#38)**, **Switch HD rumble**, **Switch 2 anti-dz (#64)**, Pico W Bluetooth, Switch 2 / Joy-Con merge, **Steam Controller 2026 (Triton) BLE**, USB resume pairing, PIO USB unplug detection. | diff --git a/Firmware/RP2040/docs/Wii_Mode_Guide.md b/Firmware/RP2040/docs/Wii_Mode_Guide.md new file mode 100644 index 00000000..3514ceee --- /dev/null +++ b/Firmware/RP2040/docs/Wii_Mode_Guide.md @@ -0,0 +1,141 @@ +# Wii Mode Guide + +This guide covers **Wii mode** on OGX-Mini when using a **Pico W** or **Pico 2 W**. In this mode the adapter appears as a **Nintendo Wiimote** over Bluetooth to a Wii or Wii U, while your **input** comes from a **USB gamepad** plugged into the board’s **external USB host port**. + +**Wii mode is build-option only:** you cannot switch into or out of Wii with button combos. Build with `-DOGXM_FIXED_DRIVER=WII` to get a Wii-only firmware; combos are disabled in that build. + +For hardware wiring and USB port setup (PIO USB, pins, build), see [PICO2W_WII_USB_SETUP.md](PICO2W_WII_USB_SETUP.md). + +--- + +## 1. How Wii mode works + +- The adapter’s **Bluetooth** is used only for the **Wiimote** link to the console (Wii or Wii U). +- A **USB controller** must be connected to the **external USB host port** (PIO USB on GPIO). The built-in USB port is for programming and is **not** used for the gamepad in Wii mode. +- The firmware converts USB gamepad input into Wiimote (and extension) reports and sends them over Bluetooth. The console sees a normal Wiimote; you control it with your USB pad. + +--- + +## 2. USB connection (controller input) + +- **Use the external USB port** (PIO USB). On Pico W / Pico 2 W this is the connector wired to **D+/D− on GP0/GP1** (or swapped per board config). See [PICO2W_WII_USB_SETUP.md](PICO2W_WII_USB_SETUP.md) for pins and wiring. +- **Do not** plug the gamepad into the Pico’s **built-in** USB port (the one used for flashing). That port is not used as host in Wii mode. +- Power the board and plug the USB controller into the external port. When it enumerates, the controller’s LED (if any) should indicate it’s active. The firmware waits for a controller before starting the Wiimote connection flow. + +--- + +## 3. Connecting to the Wii / Wii U + +### First-time sync + +1. **Build** the firmware with Wii mode: `-DOGXM_FIXED_DRIVER=WII` (see [PICO2W_WII_USB_SETUP.md](PICO2W_WII_USB_SETUP.md) for full build steps). +2. Power the adapter and plug in the USB controller on the **external** port. +3. On the **Wii or Wii U**: open **Wii Remote** (sync) from the system menu. +4. The adapter appears as **“Nintendo RVL-CNT-01”**. When the console asks to press **Sync**, trigger sync on the **adapter** (or follow on-screen instructions). The adapter may also **auto-connect** if it was previously paired (see below). +5. After pairing, the console treats the adapter as player 1’s Wiimote; input comes from the USB gamepad. + +### Auto-connect (after first sync) + +- The adapter **saves the last paired console’s Bluetooth address** in flash. +- On later power-ups it will **automatically try to connect** to that console (like a normal Wiimote). You do **not** need to press Sync again unless you want to pair a different console or re-pair after the console “forgot” the remote. +- If auto-connect fails (e.g. authentication error because the console removed the remote), the saved address is cleared and the adapter stays **discoverable** so you can sync again from the Wii/U menu. + +--- + +## 4. Wii report modes (No Extension, Nunchuk, Classic) + +The adapter can emulate three Wiimote “shapes” that many games support. You **cycle** between them with a button combo; the **LED flashes** to show the current mode after a change. + +| Mode | LED flashes | Description | +|-------------------|------------|-------------| +| **No Extension** | 1 | Wiimote only (can be used horizontal/sideways). | +| **Nunchuk** | 2 | Wiimote + Nunchuk (stick, C, Z). | +| **Classic** | 3 | Classic Controller (two sticks, triggers, full button set). | + +- **Default at boot:** **No Extension** (1 flash when you switch to it). +- **Cycle:** Hold **Home (Guide) + D-pad Down** for about **1 second** (10 frames). The mode advances in order: **No Extension → Nunchuk → Classic → No Extension** (repeat). Each time you change mode, the LED flashes 1, 2, or 3 times to indicate which mode you’re in. + +--- + +## 5. Button and stick mapping by mode + +Mappings assume an **Xbox-style** USB gamepad (A/B/X/Y, Start, Back, Guide, D-pad, bumpers, triggers, two sticks). Other USB pads are mapped to the same logical layout by the host driver. + +For the **full mapping reference** (all output modes, Bluetooth inputs including **Wii Remote vertical**, Switch Pro swaps, PS3/PS4, etc.), see **[Controller_Mappings.md](Controller_Mappings.md)**. + +### No Extension (Wiimote only) + +| Wiimote | USB gamepad | +|------------|----------------| +| A, B | A, B | +| 1, 2 | X, Y | +| +, − | Start, Back | +| Home | Guide (SYS) | +| D-pad | D-pad | +| IR pointer | Left stick | +| Sideways | Home + LB (toggle horizontal mode) | + +### Nunchuk (Wiimote + Nunchuk) + +| Wiimote | USB gamepad | +|------------|----------------| +| A, B, 1, 2, +, −, Home, D-pad | Same as above | +| IR pointer | **Right stick** | +| Nunchuk stick | **Left stick** | +| C | Left trigger > half | +| Z | Right bumper (RB) | +| Shake | Left bumper (LB) held | + +### Classic Controller + +| Classic | USB gamepad | +|------------|----------------| +| A, B, X, Y | A, B, Y, X (X↔Y) | +| +, −, Home | Start, Back, Guide | +| D-pad | D-pad | +| L/R bumpers | LB, RB | +| L/R triggers | Left/right triggers | +| Left stick | Left stick | +| Right stick | Right stick (also used for IR pointer) | + +In all modes, **IR pointer** and sticks use a small **deadzone** so the cursor or character doesn’t drift when the stick is at rest. + +--- + +## 6. Supported controllers for input (USB host) + +In Wii mode the adapter uses the **USB host** on the external port and supports the same controllers as in other OGX-Mini host modes. Typically tested/supported USB gamepads include: + +- **Xbox One** (wired USB) +- **Xbox 360** (wired USB) +- **Xbox 360 Wireless** (with USB receiver) +- **Generic Xinput / DirectInput (DInput)** pads +- **PlayStation** (PS3, PS4, PS5) over USB, when supported by the host stack +- **Nintendo Switch Pro** (wired USB), when supported +- Other HID gamepads that enumerate as a single gamepad + +Only **one** controller is used (player 1). It must enumerate on the **external** PIO USB port. If your pad is not recognized, check wiring and [PICO2W_WII_USB_SETUP.md](PICO2W_WII_USB_SETUP.md) (e.g. D+/D− swap). + +--- + +## 7. Wii is build-option only (no combos) + +Wii mode is **not** selectable by button combo. To use Wii you must **build** with: + +- **`-DOGXM_FIXED_DRIVER=WII`** + +Combos are **disabled** in Wii builds (you cannot switch to another mode at runtime). To use a different mode (Switch, XInput, etc.), reflash a firmware built without `OGXM_FIXED_DRIVER` or with a different fixed driver. + +--- + +## 8. Summary + +| Topic | Detail | +|-------|--------| +| **Input** | USB gamepad on **external** USB port (PIO USB). | +| **Output** | Wiimote over Bluetooth to Wii / Wii U. | +| **Modes** | No Extension (1 flash), Nunchuk (2), Classic (3). Cycle with **Home + D-pad Down** (~1 s). | +| **Sync** | First time: sync from console. After that, adapter auto-connects to last console. | +| **Wii build** | Build with `-DOGXM_FIXED_DRIVER=WII`; no combos to switch mode. | + +For wiring, pins, and build instructions, see [PICO2W_WII_USB_SETUP.md](PICO2W_WII_USB_SETUP.md). diff --git a/Firmware/RP2040/docs/Wired_Controllers.md b/Firmware/RP2040/docs/Wired_Controllers.md new file mode 100644 index 00000000..91a924ae --- /dev/null +++ b/Firmware/RP2040/docs/Wired_Controllers.md @@ -0,0 +1,142 @@ +# Wired controllers (USB input) + +This document lists controllers supported when connected to the OGX-Mini adapter **via a wired (USB) connection**. These work as input when the adapter is outputting to any supported platform (Original Xbox, Xbox 360, Switch, PS3, DInput, PS1/PS2 GPIO, Dreamcast, GameCube, N64, etc.). + +**Note:** Some third‑party controllers change their VID/PID depending on mode; they may need to be set to the correct mode (e.g. XInput, DInput, or Switch) to be recognized. + +--- + +## By driver type + +### XInput (Xbox 360, Xbox One, Xbox Series) + +- **Microsoft:** Xbox 360 (wired and **360 PC wireless receiver**), Xbox One / Series **wired USB**, Xbox Elite +- **Not supported (planned):** Xbox One / Series **wireless USB dongle** (`045e:02e6`, `045e:02fe`) — see [IMPROVEMENTS.md — Future planned](IMPROVEMENTS.md#future-planned) +- **Third‑party:** Controllers that identify as XInput over USB (e.g. many 8BitDo, PowerA, PDP, Afterglow when in XInput mode) +- **Razer Atrox Arcade Stick:** **Xbox One** (`1532:0a00`, vendor GIP); **Xbox 360** (`24c6:5000`, standard XInput, digital LT/RT) +- **Other Xbox One GIP arcade sticks:** Mad Catz FightStick TE 2 (`0738:4a01`), PDP Xbox One Arcade Stick (`0e6f:015c`), Hori RAP Hayabusa / V Kai / Fighting Commander ONE — see `XBOX_ONE_GIP_IDS` in `XboxArcadeStick.h` + +*Matched by USB class for standard XInput pads; arcade sticks above use explicit VID/PID + GIP init. UsbdSecPatch is not required on Xbox 360.* + +**Razer Atrox arcade stick (wired USB host):** **Xbox One** model (`1532:0a00`) uses GIP on interface 0. Init follows **XBOFS.win** (`05 20 00 01 00`, wait for OUT, then **30-byte** IN read loop); input uses XBOFS byte layout (face/LT/RT on byte 22). Standard Xbox One pads use 64-byte Linux xpad GIP. **Xbox 360** model (`24c6:5000`) uses standard XInput with digital trigger bytes. + +**Xbox 360 wireless PC receiver (`045e:0719`), wired to adapter USB host:** Plug the **receiver** into the adapter’s host port; sync a **wireless 360 pad** to any quadrant. On **PIO USB host** boards (Waveshare RP2350-USB-A, Pico, Feather, etc.), v1.0.0.11a polls **all four RF ports** even when `MAX_GAMEPADS=1` and primes idle ports for sync. See [IMPROVEMENTS.md — PIO USB host wired fixes](IMPROVEMENTS.md#pio-usb-host--wired-connection-fixes-waveshare-rp2350-usb-a). + +--- + +### PlayStation 3 (DualShock 3 and compatible) + +- Sony DualShock 3 (Dualshock 3 / Batoh) +- Sony DualShock 3 (alternate/clone VID) +- Razer Panthera (PS3) +- Afterglow PS3 (multiple models) +- Bigben, Gioteck, Hori (Fighting Commander, Horipad, Fightstick), Mad Catz (Fightstick, Fightpad), PDP, PowerA, Nyko, and other PS3‑protocol controllers + +*See `PS3_IDS` in `HardwareIDs.h` for the full VID/PID list.* + +**Button mappings (all modes):** [Controller_Mappings.md](Controller_Mappings.md) + +**DualShock 3 over USB cable (PIO USB host):** v1.0.0.11a sends **SET feature 0xF4** and a **player LED OUT** report at connect (USB Host Shield sequence) with **`pio_usb_host_frame()`** during init, plus a **1 Hz keepalive OUT** so the pad stays connected. On **Pico W / Pico 2 W / RP2354** BT builds, **v1.0.0.12a** sync-sends Bluetooth **auto-pair** (feature **0xF5** with the adapter BD_ADDR) right after that wired init (deferred if the radio address is not ready yet). See [IMPROVEMENTS.md — DualShock 3 wired USB host](IMPROVEMENTS.md#dualshock-3--wired-usb-host) and [automatic USB programming for Bluetooth pairing](IMPROVEMENTS.md#dualshock-3--automatic-usb-programming-for-bluetooth-pairing). + +--- + +### PlayStation 4 (DualShock 4 and compatible) + +- Sony DualShock 4 (all revisions) +- Sony DS4 wireless adapter (USB dongle) +- Hori Fighting Commander 4, Hori PS4 Mini +- Brook, Nacon, PDP, PowerA, Razer (Raiju), Scuf, Victrix, and other DS4‑protocol controllers + +*See `PS4_IDS` in `HardwareIDs.h` for the full VID/PID list.* + +--- + +### PlayStation 5 (DualSense and compatible) + +- Sony DualSense (PS5) +- Sony DualSense Edge (PS5) +- Sony PS5 Access Controller + +*See `PS5_IDS` in `HardwareIDs.h` for the full VID/PID list.* + +--- + +### Nintendo Switch (Pro and wired) + +- **Switch Pro / Joy‑Con / Wii U Pro:** + Nintendo Switch Pro Controller, Wii U Pro Controller, Joy‑Con (L/R), Nintendo Switch 2 Pro Controller, Joy‑Con 2 (L/R) +- **Wired Switch:** + PowerA (wired, Enhanced, Fusion, Spectra, Arcade Stick, Fusion Pro), Hori (Pokken, Horipad for Switch / Switch 2), Afterglow Deluxe, Faceoff Deluxe, and other wired Switch controllers + +*See `SWITCH_PRO_IDS` and `SWITCH_WIRED_IDS` in `HardwareIDs.h` for the full VID/PID list.* + +**Switch 1 Pro (PID 0x2009), wired USB:** Uses **Chromium-style wired USB init** (report **0x80** subcommands: MAC, handshake, baud, disable timeout; then **0x12** + subcommand **0x33** probe, player LED, full report mode, IMU). **Not** the Bluetooth **0x11** handshake path. After init, standard **SwitchProHost** decodes the **10-byte** `SwitchPro::InReport` payload (report IDs **0x30** / **0x31** or raw 10 bytes). D-pad and **L3/R3** mapping corrected for wired layout. See [IMPROVEMENTS.md](IMPROVEMENTS.md#pio-usb-host--wired-connection-fixes-waveshare-rp2350-usb-a). + +**Switch 2–family USB (Pro Controller 2, Joy‑Con 2, NSO GameCube, etc.):** +The firmware runs the same vendor **bulk OUT** bring‑up on **USB configuration 1, interface 1** as the PC capture tool (`Tools/controller_capture/switch2_usb_init.py` / [HandHeldLegend procon2tool](https://github.com/HandHeldLegend/procon2tool)). **Switch 2–family devices do not run Switch 1 `0x80` HID init after bulk** — bring-up completes at **`DONE`** and HID input reports begin immediately. Wired input reports may use report ID **0x09** (with a 1‑byte counter after the ID). + +**Switch 2 Pro (PID 0x2069), wired USB:** After the same **bulk OUT** bring‑up on **config 1 / interface 1**, **Switch2ProHost** parses the standard **64‑byte** HID report (ID **0x09**, 1‑byte counter, then **10‑byte** `SwitchPro::InReport` payload). Digitals use **Switch‑2‑specific** bit positions (not classic **Buttons0/1/2**): face **B/A/Y/X**, **RB**, **LB** (Home bit), **Minus** → Back, **Plus** / d‑pad directions, **L3**, **Start** (classic R), **R3** (classic ZR bit), **SYS** (classic `bl` d‑pad up), **ZL/ZR** → **LT/RT** (digital full press from captured bits), and **Capture** is still not mapped to **MISC**. **GL**, **GR**, and **Chat** are **documented in source** (`Switch2ProHost.cpp`) but **left unmapped**. Extended report bytes are not used for decoding (avoids IMU coupling / flicker). **Bluetooth LE** for **Pro 2** and **Joy-Con 2** (**0x2066** / **0x2067**) on **Pico W / Pico 2 W** is documented in [IMPROVEMENTS.md](IMPROVEMENTS.md#nintendo-switch-2--bluetooth-pico-w--pico-2-w) (custom GATT — not wired USB). Other Switch 2–family PIDs keep **SwitchProHost**. + +--- + +### PlayStation Classic + +- PlayStation Classic controller (official) + +*See `PSCLASSIC_IDS` in `HardwareIDs.h`.* + +--- + +### Nintendo 64 (USB) + +- Retrolink N64 USB gamepad +- Hyperkin Admiral N64, Hyperkin N64 Controller Adapter +- Mayflash N64 Adapter + +*See `N64_IDS` in `HardwareIDs.h` for the full VID/PID list.* + +--- + +### Original Xbox (USB host) + +- Original Xbox Duke and S (via USB adapter/converter) + +*Used when the adapter is in OG Xbox output mode with a compatible USB host path.* + +--- + +### DInput / Generic HID + +Controllers that use the standard HID gamepad (DInput) protocol, including: + +- **Logitech:** F310, F510, F710, RumblePad 2, Dual Action, WingMan, ChillStream, etc. +- **8BitDo:** Many models in DInput/HID mode (e.g. Pro 2, Pro 3, Ultimate, Lite, FC30 Pro, F30 Arcade, GameCube, Dogbone) +- **Hori:** Fightsticks, Fighting Commander, Horipad (various), Taiko Controller, etc. +- **Mad Catz:** Fightsticks, Fightpad, CTRLR, etc. +- **Qanba, Mayflash, Brook:** Arcade sticks and adapters +- **PowerA, PDP, Afterglow:** Various Xbox/PC controllers in HID mode +- **GameSir:** G3, G4, T3, T4, G7 Pro, etc. +- **Razer:** Kishi, Hydra, Serval, Raiju (PC/HID) +- **Steam:** Steam Controller, Steam Deck (when presenting as gamepad). **Steam Controller 2026** body USB is **`28de:1302`** (generic HID if supported by the host stack). **Wireless BLE** (`28de:1303`) on Pico W / Pico 2 W / RP2354 is documented in [IMPROVEMENTS — Steam Controller 2026](IMPROVEMENTS.md#steam-controller-2026-triton--bluetooth). +- **Sony:** DualShock 2 (via USB adapter), Steam Virtual Gamepad +- **Flydigi:** Vader 4 Pro (DInput mode) +- **Scuf:** Envision (Linux/HID) +- **Elecom:** Various gamepads and adapters +- **Other:** ThrustMaster, BigBen, Capcom Home Arcade, Cthulhu, XinMo, Zenaim, Datel, and other generic DInput/HID gamepads and arcade sticks + +*See `DINPUT_IDS` in `HardwareIDs.h` for the full VID/PID list. This list is large and includes many fightsticks and third‑party pads.* + +--- + +### Generic HID (unspecified mapping) + +- Other HID gamepad‑like devices may work; button and axis mappings might need to be adjusted in the [web app](https://megacadedev.github.io/OGX-Mini-2026-WebApp/). + +--- + +## Reference + +- **VID/PID lists:** `Firmware/RP2040/src/USBHost/HardwareIDs.h` +- **Host drivers:** `Firmware/RP2040/src/USBHost/HostDriver/` +- **Platform selection:** See the main [README](../../../README.md) for button combos to change output platform. +- **SteamOS / Bazzite (STEAM output):** USB presents **DualSense** (`054c:0ce6`) + **HID mouse**. **DualSense** input (wired or BT): passthrough gamepad + **touchpad → cursor**; other pads get synthesized DualSense mapping (gamepad only — no stick mouse). **Start + LB + D-pad Up** selects mode. See [README — SteamOS / Bazzite](../../../README.md#steamos--bazzite-output-mode), [IMPROVEMENTS — STEAM mode](IMPROVEMENTS.md#steam-mode--steamos--bazzite-linux-desktop), and [Controller_Mappings — STEAM](Controller_Mappings.md#steamos--bazzite-steam-mode). diff --git a/Firmware/RP2040/src/Bluepad32/Bluepad32.cpp b/Firmware/RP2040/src/Bluepad32/Bluepad32.cpp index 836811e5..1bf47c40 100644 --- a/Firmware/RP2040/src/Bluepad32/Bluepad32.cpp +++ b/Firmware/RP2040/src/Bluepad32/Bluepad32.cpp @@ -1,41 +1,175 @@ #include +#include #include #include #include #include +#include +#include #include "btstack_run_loop.h" +#include "gap.h" #include "uni.h" +#include "bt/uni_bt.h" +#include "bt/uni_bt_bredr.h" +#include "bt/uni_bt_le.h" +#include "uni_hid_device.h" #include "sdkconfig.h" + +#if defined(CONFIG_TARGET_PICO_W) && defined(CONFIG_EN_USB_HOST) +/** Core0 USB mux reads this while Core1 BT stack updates connections — mirror bt_devices_[].connected. */ +static std::atomic s_bt_any_connected_cached{false}; +#endif + #include "Bluepad32/Bluepad32.h" #include "Board/board_api.h" #include "Board/ogxm_log.h" +#include "parser/uni_hid_parser_ds5.h" +#include "controller/uni_controller.h" +#include "parser/uni_hid_parser_wii.h" +#include "Gamepad/MotionImu.h" +#include "USBDevice/DeviceDriver/MotionOutputActive.h" +#include "USBDevice/DeviceDriver/Steam/SteamActive.h" +#include "USBDevice/DeviceDriver/Steam/SteamBtReport.h" +#include "USBDevice/DeviceDriver/Steam/SteamPassthrough.h" +#include "USBDevice/DeviceDriver/Steam/SteamTouchpad.h" #ifndef CONFIG_BLUEPAD32_PLATFORM_CUSTOM #error "Pico W must use BLUEPAD32_PLATFORM_CUSTOM" #endif -static_assert((CONFIG_BLUEPAD32_MAX_DEVICES == MAX_GAMEPADS), "Mismatch between BP32 and Gamepad max devices"); +static_assert((CONFIG_BLUEPAD32_MAX_DEVICES >= MAX_GAMEPADS), + "Bluepad32 must allow at least as many BT devices as USB gamepad slots"); namespace bluepad32 { +static bool bp32_is_switch_joycon(const uni_hid_device_t* d) { + return d != nullptr && (d->controller_type == CONTROLLER_TYPE_SwitchJoyConLeft || + d->controller_type == CONTROLLER_TYPE_SwitchJoyConRight); +} + +static bool bp32_is_joycon_pair_secondary(const uni_hid_device_t* d) { + if (uni_hid_parser_switch2_is_ble_device(d)) + return uni_hid_parser_switch2_is_joycon_pair_secondary(d); + if (bp32_is_switch_joycon(d)) + return uni_hid_parser_switch_is_joycon_pair_secondary(d); + return false; +} + +static int bp32_get_gamepad_output_idx(uni_hid_device_t* d) { + if (uni_hid_parser_switch2_is_ble_device(d)) + return uni_hid_parser_switch2_get_gamepad_output_idx(d); + if (bp32_is_switch_joycon(d)) + return uni_hid_parser_switch_get_gamepad_output_idx(d); + return uni_hid_device_get_idx_for_instance(d); +} + +static int bp32_get_pair_partner_idx(uni_hid_device_t* d) { + if (uni_hid_parser_switch2_is_ble_device(d)) + return uni_hid_parser_switch2_get_pair_partner_idx(d); + if (bp32_is_switch_joycon(d)) + return uni_hid_parser_switch_get_pair_partner_idx(d); + return -1; +} + +static void bp32_disconnect_controller_and_joycon_partner(uni_hid_device_t* d) { + if (!d) + return; + const int partner_idx = bp32_get_pair_partner_idx(d); + if (partner_idx >= 0 && partner_idx < CONFIG_BLUEPAD32_MAX_DEVICES) { + uni_hid_device_t* partner = uni_hid_device_get_instance_for_idx(partner_idx); + if (partner && partner != d) { + printf("[BP32] Disconnect combo: also disconnecting Joy-Con partner slot %d\n", partner_idx); + uni_hid_device_disconnect(partner); + } + } + uni_hid_device_disconnect(d); +} + static constexpr uint32_t FEEDBACK_TIME_MS = 250; static constexpr uint32_t LED_CHECK_TIME_MS = 500; +/** Idle pairing health check — restarts BR/LE scan if they died during long USB suspend (e.g. 360 standby). */ +static constexpr uint32_t PAIRING_WATCHDOG_MS = 45000; +/** If no HID input report reaches us for this long while "connected", the BT link is zombie + * (L2CAP stops delivering; OG Xbox then holds last USB report). Force disconnect so user can reconnect. */ +static constexpr uint32_t BT_INPUT_STALL_DISCONNECT_MS = 8000; +/** BLE Xbox: host→pad output while idle (no rumble) or controller sleeps link ~1 min */ +static constexpr uint32_t XBOX_BLE_KEEPALIVE_MS = 12000; +/** Switch 2 Pro BLE drops link (HCI 0x08) without periodic vibration writes. */ +static constexpr uint32_t SW2_BLE_KEEPALIVE_MS = 8; + +/** One-second rumble when a pad becomes ready so the user knows it is connected. */ +static constexpr uint16_t CONNECT_RUMBLE_DURATION_MS = 1000; +static constexpr uint8_t CONNECT_RUMBLE_WEAK = 160; +static constexpr uint8_t CONNECT_RUMBLE_STRONG = 160; +/** DS4: defer FF slightly — early output reports can destabilize the link (see s_ps4_rumble_ok_ms). */ +static constexpr uint16_t CONNECT_RUMBLE_DELAY_PS4_MS = 1200; +static constexpr uint16_t CONNECT_RUMBLE_DELAY_DEFAULT_MS = 300; + +static uint32_t s_last_bt_input_ms[CONFIG_BLUEPAD32_MAX_DEVICES]{}; +static uint32_t s_xbox_ble_ka_last_ms[CONFIG_BLUEPAD32_MAX_DEVICES]{}; +static uint32_t s_sw2_ble_ka_last_ms[CONFIG_BLUEPAD32_MAX_DEVICES]{}; +/** Ignore Start+Select disconnect combo for this long after connect (DS4 can glitch both on first reports). */ +static uint32_t s_bt_disconnect_combo_grace_until_ms[MAX_GAMEPADS]{}; +/** DS4 BT: delay rumble output (host can request rumble immediately; early FF reports can drop link). */ +static uint32_t s_ps4_rumble_ok_ms[MAX_GAMEPADS]{}; struct BTDevice { bool connected{false}; Gamepad* gamepad{nullptr}; }; -BTDevice bt_devices_[MAX_GAMEPADS]; +BTDevice bt_devices_[CONFIG_BLUEPAD32_MAX_DEVICES]; btstack_timer_source_t feedback_timer_; btstack_timer_source_t led_timer_; bool led_timer_set_{false}; bool feedback_timer_set_{false}; +static constexpr uint32_t GPIO_PROCESS_INTERVAL_MS = 4; +static btstack_timer_source_t gpio_process_timer_; +static void (*gpio_process_cb_)(void*) = nullptr; +static void* gpio_process_ctx_ = nullptr; + +static void (*s_pico_w_pio_usb_mux_tick)(void) = nullptr; +static btstack_timer_source_t s_pico_w_usb_mux_timer_; +static btstack_context_callback_registration_t s_pico_w_usb_mux_main_reg; + +// Timer callbacks must not call sleep_* (Pico panics). TinyUSB host enumeration does; run mux on main thread. +static void pico_w_usb_mux_run_on_main(void* ctx) { + (void)ctx; + if (s_pico_w_pio_usb_mux_tick != nullptr) { + s_pico_w_pio_usb_mux_tick(); + } +} + +static void pico_w_usb_mux_timer_cb(btstack_timer_source_t* ts) { + s_pico_w_usb_mux_main_reg.callback = pico_w_usb_mux_run_on_main; + s_pico_w_usb_mux_main_reg.context = nullptr; + btstack_run_loop_execute_on_main_thread(&s_pico_w_usb_mux_main_reg); + btstack_run_loop_set_timer(ts, 1); + btstack_run_loop_add_timer(ts); +} + +static void gpio_process_timer_cb(btstack_timer_source_t* ts) { + if (gpio_process_cb_ != nullptr && gpio_process_ctx_ != nullptr) { + gpio_process_cb_(gpio_process_ctx_); + } + btstack_run_loop_set_timer(ts, GPIO_PROCESS_INTERVAL_MS); + btstack_run_loop_add_timer(ts); +} + +// PS5: touchpad click toggles adaptive triggers (per-controller state) +static bool adaptive_trigger_enabled_[MAX_GAMEPADS]{false}; +static bool prev_touchpad_clicked_[MAX_GAMEPADS]{false}; +// Defer sending adaptive trigger effect out of BT callback to avoid l2cap_send in callback (reduces input lag) +static bool pending_adaptive_trigger_send_[MAX_GAMEPADS]{false}; + bool any_connected() { +#if defined(CONFIG_TARGET_PICO_W) && defined(CONFIG_EN_USB_HOST) + return s_bt_any_connected_cached.load(std::memory_order_acquire); +#else for (auto& device : bt_devices_) { if (device.connected) @@ -44,6 +178,44 @@ bool any_connected() } } return false; +#endif +} + +bool is_wii_controller_connected(uint8_t idx) +{ + if (idx >= MAX_GAMEPADS) { + return false; + } + uni_hid_device_t* bp_device = uni_hid_device_get_instance_for_idx(idx); + return (bp_device != nullptr && bp_device->controller_type == CONTROLLER_TYPE_WiiController); +} + +bool any_wii_controller_connected() +{ + for (uint8_t i = 0; i < MAX_GAMEPADS; ++i) + { + if (bt_devices_[i].connected && is_wii_controller_connected(i)) + { + return true; + } + } + return false; +} + +/** TV-style Wiimote (not Wii U Pro, Classic, or Balance Board). */ +static bool ogxm_is_handheld_wiimote(const uni_hid_device_t* device) +{ + if (device == nullptr || device->controller_type != CONTROLLER_TYPE_WiiController) { + return false; + } + switch (device->controller_subtype) { + case CONTROLLER_SUBTYPE_WIIUPRO: + case CONTROLLER_SUBTYPE_WII_CLASSIC: + case CONTROLLER_SUBTYPE_WII_BALANCE_BOARD: + return false; + default: + return true; + } } //This solves a function pointer/crash issue with bluepad32 @@ -80,6 +252,14 @@ void set_rumble(uni_hid_device_t* bp_device, uint16_t length, uint8_t rumble_l, case CONTROLLER_TYPE_SwitchJoyConLeft: uni_hid_parser_switch_play_dual_rumble(bp_device, 0, length, rumble_l, rumble_r); break; + case CONTROLLER_TYPE_Switch2ProController: + case CONTROLLER_TYPE_Switch2JoyConRight: + case CONTROLLER_TYPE_Switch2JoyConLeft: + uni_hid_parser_switch2_play_dual_rumble(bp_device, 0, length, rumble_l, rumble_r); + break; + case CONTROLLER_TYPE_SteamControllerTriton: + uni_hid_parser_steam_triton_play_dual_rumble(bp_device, 0, length, rumble_l, rumble_r); + break; default: break; } @@ -88,19 +268,90 @@ void set_rumble(uni_hid_device_t* bp_device, uint16_t length, uint8_t rumble_l, static void send_feedback_cb(btstack_timer_source *ts) { uni_hid_device_t* bp_device = nullptr; + const uint32_t now_ms = to_ms_since_boot(get_absolute_time()); - for (uint8_t i = 0; i < MAX_GAMEPADS; ++i) + for (uint8_t i = 0; i < CONFIG_BLUEPAD32_MAX_DEVICES; ++i) { - if (!bt_devices_[i].connected || + if (!bt_devices_[i].connected || !(bp_device = uni_hid_device_get_instance_for_idx(i))) { continue; } + /* BLE Xbox (Series) and Switch 2 BLE may not send reports every poll interval. */ + const bool skip_stall_disconnect = + (bp_device->controller_type == CONTROLLER_TYPE_XBoxOneController && bp_device->hids_cid != 0) || + uni_hid_parser_switch2_is_ble_device(bp_device) || + uni_hid_parser_steam_triton_is_device(bp_device); + /* Virtual slot (e.g. DS4's BT "mouse"): never gets gamepad HID → would always stall-disconnect + * and drop the real controller. */ + if (uni_hid_device_is_virtual_device(bp_device)) + goto after_stall_check; + if (uni_hid_parser_switch2_is_joycon_pair_secondary(bp_device)) + goto after_stall_check; + if (bp32_is_joycon_pair_secondary(bp_device)) + goto after_stall_check; + if (!skip_stall_disconnect) + { + const int stall_idx = bp32_get_gamepad_output_idx(bp_device); + const unsigned stall_slot = + (stall_idx >= 0 && stall_idx < static_cast(MAX_GAMEPADS)) + ? static_cast(stall_idx) + : static_cast(i); + if (s_last_bt_input_ms[stall_slot] != 0 && + (now_ms - s_last_bt_input_ms[stall_slot]) > BT_INPUT_STALL_DISCONNECT_MS) + { + printf("[Bluepad32] BT input stalled (%u ms); forcing disconnect (slot %u)\n", + static_cast(now_ms - s_last_bt_input_ms[stall_slot]), static_cast(i)); + uni_hid_device_disconnect(bp_device); + continue; + } + } + after_stall_check: - Gamepad::PadOut gp_out = bt_devices_[i].gamepad->get_pad_out(); + const int out_i = bp32_get_gamepad_output_idx(bp_device); + const int gp_idx = + (out_i >= 0 && out_i < static_cast(MAX_GAMEPADS)) ? out_i : static_cast(i); + if (gp_idx < 0 || gp_idx >= static_cast(MAX_GAMEPADS) || bt_devices_[gp_idx].gamepad == nullptr) + continue; + + Gamepad::PadOut gp_out = bt_devices_[gp_idx].gamepad->get_pad_out(); + if (bp_device->controller_type == CONTROLLER_TYPE_XBoxOneController && bp_device->hids_cid != 0 && + gp_out.rumble_l == 0 && gp_out.rumble_r == 0) + { + const uint32_t last_ka = s_xbox_ble_ka_last_ms[i]; + if (last_ka == 0u || (now_ms - last_ka) >= XBOX_BLE_KEEPALIVE_MS) + { + uni_hid_parser_xboxone_ble_keepalive(bp_device); + s_xbox_ble_ka_last_ms[i] = now_ms; + } + } + if (uni_hid_parser_switch2_is_ble_device(bp_device) && uni_hid_parser_switch2_is_ready(bp_device) && + !uni_hid_parser_switch2_is_joycon_pair_secondary(bp_device) && + !uni_hid_parser_switch2_keepalive_timer_active(bp_device) && + gp_out.rumble_l == 0 && gp_out.rumble_r == 0) + { + const uint32_t last_ka = s_sw2_ble_ka_last_ms[i]; + if (last_ka == 0u || (now_ms - last_ka) >= SW2_BLE_KEEPALIVE_MS) + { + uni_hid_parser_switch2_send_keepalive(bp_device); + s_sw2_ble_ka_last_ms[i] = now_ms; + } + } if (gp_out.rumble_l > 0 || gp_out.rumble_r > 0) { - set_rumble(bp_device, static_cast(FEEDBACK_TIME_MS), gp_out.rumble_l, gp_out.rumble_r); + if (bp_device->controller_type == CONTROLLER_TYPE_PS4Controller && + now_ms < s_ps4_rumble_ok_ms[i]) + ; + else if (!bp32_is_joycon_pair_secondary(bp_device)) + { + set_rumble(bp_device, static_cast(FEEDBACK_TIME_MS), gp_out.rumble_l, gp_out.rumble_r); + const int partner = bp32_get_pair_partner_idx(bp_device); + if (partner >= 0 && partner < CONFIG_BLUEPAD32_MAX_DEVICES) { + uni_hid_device_t* pd = uni_hid_device_get_instance_for_idx(partner); + if (pd) + set_rumble(pd, static_cast(FEEDBACK_TIME_MS), gp_out.rumble_l, gp_out.rumble_r); + } + } } } @@ -114,7 +365,13 @@ static void check_led_cb(btstack_timer_source *ts) led_state = !led_state; - board_api::set_led(any_connected() ? true : led_state); +#if defined(CONFIG_TARGET_PICO_W) && defined(CONFIG_EN_USB_HOST) + const bool wired_host_pad = board_api::usb::host_any_pad_mounted(); +#else + const bool wired_host_pad = false; +#endif + /* Solid LED when a BT pad is connected or a wired USB host controller is active (Pico W mux). */ + board_api::set_led((any_connected() || wired_host_pad) ? true : led_state); btstack_run_loop_set_timer(ts, LED_CHECK_TIME_MS); btstack_run_loop_add_timer(ts); @@ -126,50 +383,392 @@ static void init(int argc, const char** arg_V) { } static void init_complete_cb(void) { + // Faster pairing: more aggressive GAP inquiry/periodic (units: 1.28s). + // Defaults are inquiry=3, max=5, min=4; slightly tighter so we discover and reconnect sooner. + uni_bt_set_gap_inquiry_length(2); + uni_bt_set_gap_max_peridic_length(4); + uni_bt_set_gap_min_peridic_length(3); + uni_bt_enable_new_connections_unsafe(true); // uni_bt_del_keys_unsafe(); uni_property_dump_all(); + OGXM_LOG("BT: stack ready — BR/EDR inquiry + BLE scan (hold Pro 2 SYNC to pair)\n"); } static uni_error_t device_discovered_cb(bd_addr_t addr, const char* name, uint16_t cod, uint8_t rssi) { - if (!((cod & UNI_BT_COD_MINOR_MASK) & UNI_BT_COD_MINOR_GAMEPAD)) { + uint8_t minor = cod & UNI_BT_COD_MINOR_MASK; + + if (!(minor & (UNI_BT_COD_MINOR_GAMEPAD | + UNI_BT_COD_MINOR_JOYSTICK | + UNI_BT_COD_MINOR_REMOTE_CONTROL))) { return UNI_ERROR_IGNORE_DEVICE; } + return UNI_ERROR_SUCCESS; } static void device_connected_cb(uni_hid_device_t* device) { + if (device == nullptr) { + return; + } + if (uni_hid_parser_switch2_is_ble_device(device)) { + OGXM_LOG("SW2: connected pid=0x%04x — waiting for encryption/GATT\n", device->product_id); + } +} + +/** CYW43: resume OGX BLE advertising when no Classic (BR/EDR) gamepad remains connected. */ +static void ogxm_resume_ble_ads_if_no_acl_pad(int disconnected_idx) { +#if defined(CONFIG_TARGET_PICO_W) + for (uint8_t i = 0; i < CONFIG_BLUEPAD32_MAX_DEVICES; ++i) { + if (i == static_cast(disconnected_idx)) + continue; + if (!bt_devices_[i].connected) + continue; + uni_hid_device_t* d = uni_hid_device_get_instance_for_idx(static_cast(i)); + if (!d || uni_bt_conn_get_state(&d->conn) != UNI_BT_CONN_STATE_DEVICE_READY) + continue; + if (gap_get_connection_type(d->conn.handle) == GAP_CONNECTION_ACL) + return; + } + gap_advertisements_enable(1); +#endif +} + +/** CYW43: any active BLE gamepad link (HOGP or Triton Valve GATT) needs scans off. */ +static bool device_is_ble_hogp(const uni_hid_device_t* d) { + if (d == nullptr) + return false; + if (d->hids_cid != 0 && d->hids_cid != 0xffff) + return true; + /* Triton skips HIDS — still needs quiet radio while LE link is up. */ + if (uni_hid_parser_steam_triton_is_device(d) && d->conn.handle != UNI_BT_CONN_HANDLE_INVALID && + gap_get_connection_type(d->conn.handle) == GAP_CONNECTION_LE) + return true; + return false; +} + +/** CYW43: periodic BR/EDR inquiry while a Classic ACL link is up can drop DS4/PS3 in ~1–2 s. */ +static void maybe_restart_bredr_inquiry_after_disconnect(int disconnected_idx) { +#if defined(CONFIG_TARGET_PICO_W) + if (!uni_bt_enable_new_connections_is_enabled()) + return; + for (uint8_t i = 0; i < CONFIG_BLUEPAD32_MAX_DEVICES; ++i) { + if (i == static_cast(disconnected_idx)) + continue; + if (!bt_devices_[i].connected) + continue; + uni_hid_device_t* d = uni_hid_device_get_instance_for_idx(static_cast(i)); + if (!d || uni_bt_conn_get_state(&d->conn) != UNI_BT_CONN_STATE_DEVICE_READY) + continue; + if (gap_get_connection_type(d->conn.handle) == GAP_CONNECTION_ACL) + return; + /* Any BLE HOGP pad: keep BR inquiry off (same radio contention as Xbox). */ + if (device_is_ble_hogp(d)) + return; + } + uni_bt_bredr_scan_start(); +#endif +} + +/** + * CYW43: Xbox Series (BLE) connect stops LE scan (and BR inquiry) to avoid supervision timeout, + * but leaves Bluepad32 "new connections" enabled. On disconnect, enable_new_connections(true) is a + * no-op, so LE scan never resumes and the pad cannot reconnect without power-cycling the Pico. + * Restart LE scan unless another pad still requires it off. + */ +static void maybe_restart_ble_scan_after_disconnect(int disconnected_idx) { +#if defined(CONFIG_TARGET_PICO_W) + if (!uni_bt_enable_new_connections_is_enabled()) + return; + for (uint8_t i = 0; i < CONFIG_BLUEPAD32_MAX_DEVICES; ++i) { + if (i == static_cast(disconnected_idx)) + continue; + if (!bt_devices_[i].connected) + continue; + uni_hid_device_t* d = uni_hid_device_get_instance_for_idx(static_cast(i)); + if (!d || uni_bt_conn_get_state(&d->conn) != UNI_BT_CONN_STATE_DEVICE_READY) + continue; + if (device_is_ble_hogp(d)) + return; + /* Solo Switch Joy-Con keeps LE scan off while waiting for partner over Classic BT. */ + if (uni_hid_parser_switch_solo_needs_partner(d)) + return; + } + uni_bt_le_scan_start(); +#endif +} + +/** Set in device_ready — only reboot after a fully working pad disconnects (not failed pair attempts). */ +static bool s_bt_slot_was_ready[CONFIG_BLUEPAD32_MAX_DEVICES]{}; + +/** Full chip reset after last ready BT pad drops — next pair is always a clean first connect. */ +static btstack_timer_source_t s_bt_disconnect_reboot_timer; +static bool s_bt_disconnect_reboot_pending = false; + +static void bt_disconnect_reboot_cb(btstack_timer_source_t* ts) { + (void)ts; + s_bt_disconnect_reboot_pending = false; + printf("[BP32] Last controller disconnected — restarting Pico for clean reconnect\n"); + board_api::reboot(); +} + +static void restore_bt_pairing_mode(int disconnected_idx); + +#if defined(CONFIG_EN_BLUETOOTH) && defined(CONFIG_TARGET_PICO_W) +/** True while a BLE pad is connected but not yet DEVICE_READY (pairing / DIS / HIDS). */ +static bool any_ble_connect_in_progress() { + for (uint8_t i = 0; i < CONFIG_BLUEPAD32_MAX_DEVICES; ++i) { + uni_hid_device_t* d = uni_hid_device_get_instance_for_idx(static_cast(i)); + if (!d || d->conn.handle == UNI_BT_CONN_HANDLE_INVALID) + continue; + if (uni_bt_conn_get_state(&d->conn) == UNI_BT_CONN_STATE_DEVICE_READY) + continue; + if (gap_get_connection_type(d->conn.handle) == GAP_CONNECTION_LE) + return true; + } + return false; +} + +static bool is_pairing_idle() { +#if defined(CONFIG_EN_USB_HOST) + if (board_api::usb::host_any_pad_mounted()) + return false; +#endif + /* Mid HOGP discovery still looks "disconnected" to bt_devices_[], but scans + * must stay off or Triton's HIDS discovery never finishes. */ + if (any_ble_connect_in_progress()) + return false; + return !any_connected(); +} + +/** Restart BR/LE scans when idle; no-op if a pad or wired host controller is active. */ +static void ensure_idle_pairing_scans(int disconnected_idx) { + if (!is_pairing_idle()) + return; + maybe_restart_bredr_inquiry_after_disconnect(disconnected_idx); + maybe_restart_ble_scan_after_disconnect(disconnected_idx); + ogxm_resume_ble_ads_if_no_acl_pad(disconnected_idx); + if (!uni_bt_enable_new_connections_is_enabled()) + uni_bt_enable_new_connections_unsafe(true); +} + +static btstack_timer_source_t s_pairing_watchdog_timer; + +static void pairing_watchdog_cb(btstack_timer_source_t* ts) { + ensure_idle_pairing_scans(-1); + btstack_run_loop_set_timer(ts, PAIRING_WATCHDOG_MS); + btstack_run_loop_add_timer(ts); +} + +static btstack_context_callback_registration_t s_usb_resume_bt_reg; + +static void usb_resume_on_bt_main(void* ctx) { + (void)ctx; + if (!is_pairing_idle()) + return; + printf("[BP32] USB resume — restoring BT pairing scans\n"); + restore_bt_pairing_mode(-1); +} +#endif /* CONFIG_EN_BLUETOOTH && CONFIG_TARGET_PICO_W */ + +static void restore_bt_pairing_mode(int disconnected_idx) { + if (led_timer_set_) + btstack_run_loop_remove_timer(&led_timer_); + led_timer_set_ = true; + led_timer_.process = check_led_cb; + led_timer_.context = nullptr; + btstack_run_loop_set_timer(&led_timer_, LED_CHECK_TIME_MS); + btstack_run_loop_add_timer(&led_timer_); + board_api::set_led(false); + +#if defined(CONFIG_EN_BLUETOOTH) && defined(CONFIG_TARGET_PICO_W) + ensure_idle_pairing_scans(disconnected_idx); +#else + maybe_restart_bredr_inquiry_after_disconnect(disconnected_idx); + maybe_restart_ble_scan_after_disconnect(disconnected_idx); + ogxm_resume_ble_ads_if_no_acl_pad(disconnected_idx); + uni_bt_enable_new_connections_unsafe(true); +#endif } static void device_disconnected_cb(uni_hid_device_t* device) { int idx = uni_hid_device_get_idx_for_instance(device); - if (idx >= MAX_GAMEPADS || idx < 0) { + if (idx >= CONFIG_BLUEPAD32_MAX_DEVICES || idx < 0) { return; } + if (uni_hid_parser_switch2_is_ble_device(device)) { + OGXM_LOG("SW2: disconnected slot %d\n", idx); + } + + const bool was_ready = s_bt_slot_was_ready[idx]; + s_bt_slot_was_ready[idx] = false; + bt_devices_[idx].connected = false; + bool any_other_connected = false; + for (uint8_t i = 0; i < CONFIG_BLUEPAD32_MAX_DEVICES; ++i) { + if (bt_devices_[i].connected) { + any_other_connected = true; + break; + } + } +#if defined(CONFIG_TARGET_PICO_W) && defined(CONFIG_EN_USB_HOST) + s_bt_any_connected_cached.store(any_other_connected, std::memory_order_release); +#endif + s_last_bt_input_ms[idx] = 0; + s_xbox_ble_ka_last_ms[idx] = 0; + s_sw2_ble_ka_last_ms[idx] = 0; + s_bt_disconnect_combo_grace_until_ms[idx] = 0; + s_ps4_rumble_ok_ms[idx] = 0; + prev_touchpad_clicked_[idx] = false; + pending_adaptive_trigger_send_[idx] = false; bt_devices_[idx].gamepad->reset_pad_in(); + SteamPassthrough::clear(); - if (!led_timer_set_ && !any_connected()) { - led_timer_set_ = true; - led_timer_.process = check_led_cb; - led_timer_.context = nullptr; - btstack_run_loop_set_timer(&led_timer_, LED_CHECK_TIME_MS); - btstack_run_loop_add_timer(&led_timer_); - } - if (feedback_timer_set_ && !any_connected()) { + if (feedback_timer_set_ && !any_other_connected) { feedback_timer_set_ = false; btstack_run_loop_remove_timer(&feedback_timer_); } + + if (any_other_connected) + return; + + /* Immediately show pairing mode (LED blink + BT scan). */ + restore_bt_pairing_mode(idx); + + /* + * Only reboot after a pad that reached device_ready. Failed pair attempts must + * not reboot or we never stay in pairing mode. Reboot clears BT/HIDS so the + * next successful pair matches a fresh plug-in. + */ + if (was_ready && !s_bt_disconnect_reboot_pending) { + s_bt_disconnect_reboot_pending = true; + s_bt_disconnect_reboot_timer.process = bt_disconnect_reboot_cb; + s_bt_disconnect_reboot_timer.context = nullptr; + btstack_run_loop_set_timer(&s_bt_disconnect_reboot_timer, 500); + btstack_run_loop_add_timer(&s_bt_disconnect_reboot_timer); + printf("[BP32] Pairing mode on — reboot in 500 ms for clean reconnect\n"); + } } -static uni_error_t device_ready_cb(uni_hid_device_t* device) { +static void ogxm_play_connection_rumble(uni_hid_device_t* device) +{ + if (device == nullptr || device->report_parser.play_dual_rumble == nullptr) { + return; + } + const uint16_t start_delay = + (device->controller_type == CONTROLLER_TYPE_PS4Controller) + ? CONNECT_RUMBLE_DELAY_PS4_MS + : CONNECT_RUMBLE_DELAY_DEFAULT_MS; + device->report_parser.play_dual_rumble( + device, + start_delay, + CONNECT_RUMBLE_DURATION_MS, + CONNECT_RUMBLE_WEAK, + CONNECT_RUMBLE_STRONG); +} + +static uni_error_t device_ready_cb(uni_hid_device_t* device) { + /* DS4/DS5 BT create a second "virtual mouse" device on the same ACL. OGX-Mini only uses + * gamepad input; accepting the virtual slot destabilized the link (disconnect ~2 s). */ + if (uni_hid_device_is_virtual_device(device)) + return UNI_ERROR_INVALID_CONTROLLER; + int idx = uni_hid_device_get_idx_for_instance(device); - if (idx >= MAX_GAMEPADS || idx < 0) { + if (idx < 0 || idx >= CONFIG_BLUEPAD32_MAX_DEVICES) { return UNI_ERROR_SUCCESS; } + const int out_idx = bp32_get_gamepad_output_idx(device); + bt_devices_[idx].connected = true; + s_bt_slot_was_ready[idx] = true; +#if defined(CONFIG_OGXM_DEBUG) + if (uni_hid_parser_switch2_is_ble_device(device)) { + OGXM_LOG("SW2: READY slot %d pid=0x%04x out=%d — input active\n", idx, device->product_id, out_idx); + } else if (bp32_is_switch_joycon(device)) { + OGXM_LOG("SW1: READY slot %d Joy-Con out=%d — input active\n", idx, out_idx); + } +#endif +#if defined(CONFIG_TARGET_PICO_W) && defined(CONFIG_EN_USB_HOST) + s_bt_any_connected_cached.store(true, std::memory_order_release); +#endif +#if defined(CONFIG_TARGET_PICO_W) + if (device_is_ble_hogp(device)) { + /* CYW43439: BLE scan + BR inquiry during an active LE HOGP link stalls GATT / drops link. */ + uni_bt_le_scan_stop(); + uni_bt_bredr_scan_stop(); + gap_advertisements_enable(0); + } else if (gap_get_connection_type(device->conn.handle) == GAP_CONNECTION_ACL) { + gap_advertisements_enable(0); + if (uni_hid_parser_switch_solo_needs_partner(device)) { + /* Solo Joy-Con (L or R): keep inquiry + page scan for the partner; pause BLE scan. */ + uni_bt_le_scan_stop(); + gap_connectable_control(1); + uni_bt_bredr_scan_start(); + } else if (!uni_hid_parser_switch_any_awaiting_partner()) { + uni_bt_bredr_scan_stop(); + } + } +#endif + const uint32_t tnow = to_ms_since_boot(get_absolute_time()); + const int pad_idx = (out_idx >= 0 && out_idx < static_cast(MAX_GAMEPADS)) + ? out_idx + : (idx < static_cast(MAX_GAMEPADS) ? idx : 0); + s_last_bt_input_ms[pad_idx] = tnow; + s_bt_disconnect_combo_grace_until_ms[pad_idx] = tnow + 3500u; + if (device->controller_type == CONTROLLER_TYPE_PS4Controller) + s_ps4_rumble_ok_ms[pad_idx] = tnow + 6000u; + /* Xbox BLE: 0 = send keepalive on next feedback tick (wakes Series/SW2 pad link immediately). */ + if (device->controller_type == CONTROLLER_TYPE_XBoxOneController && device->hids_cid != 0) + s_xbox_ble_ka_last_ms[idx] = 0; + if (uni_hid_parser_switch2_is_ble_device(device)) + s_sw2_ble_ka_last_ms[idx] = 0; + + // Set controller player LED to match slot (e.g. Wii U: LED 1 = player 1, LED 2 = player 2). + // Joy-Con pairs set LEDs in the parser when merged; skip secondary to avoid player-2 LED. + if (device->report_parser.set_player_leds != nullptr && + !bp32_is_joycon_pair_secondary(device)) { + device->report_parser.set_player_leds(device, static_cast(1u << pad_idx)); + } + + /* Wiimote: Bluepad32 defaults to sideways (horizontal) mapping; OGX uses TV-style vertical. + * set_mode() overwrites controller_subtype — save extension info first. */ + if (ogxm_is_handheld_wiimote(device)) { + const uint8_t saved_subtype = device->controller_subtype; + const bool has_nunchuk = + saved_subtype == CONTROLLER_SUBTYPE_WIIMOTE_NUNCHUK || + saved_subtype == CONTROLLER_SUBTYPE_WIIMOTE_NUNCHUK_ACCEL; + + uni_hid_parser_wii_set_mode(device, WII_MODE_VERTICAL); + + /* Motion output: request accel reports. With a nunchuk, DRM_KA (0x31) is ignored / + * has no extension bytes — use DRM_KAE (0x35) so stick + accel both work. + * Byte 0x04 = continuous reporting (wiibrew); without it the Wiimote only sends on + * large changes, so Switch gyro-pointer feels dead except when shaking hard. + * Keep mode VERTICAL (not WII_MODE_ACCEL) so button layouts stay correct. */ + if (MotionOutputActive::is_enabled()) { + if (has_nunchuk) { + device->controller_subtype = CONTROLLER_SUBTYPE_WIIMOTE_NUNCHUK_ACCEL; + static constexpr uint8_t kWiiReqAccelNunchuk[] = {0xa2, 0x12, 0x04, 0x35}; + uni_hid_device_send_intr_report(device, kWiiReqAccelNunchuk, sizeof(kWiiReqAccelNunchuk)); + } else { + device->controller_subtype = CONTROLLER_SUBTYPE_WIIMOTE_ACCEL; + static constexpr uint8_t kWiiReqAccel[] = {0xa2, 0x12, 0x04, 0x31}; + uni_hid_device_send_intr_report(device, kWiiReqAccel, sizeof(kWiiReqAccel)); + } + } else if (has_nunchuk) { + device->controller_subtype = CONTROLLER_SUBTYPE_WIIMOTE_NUNCHUK; + } + } + + // PS5: start with adaptive triggers off; touchpad click toggles them. + if (device->controller_type == CONTROLLER_TYPE_PS5Controller) { + adaptive_trigger_enabled_[pad_idx] = false; + ds5_adaptive_trigger_effect_t off = ds5_new_adaptive_trigger_effect_off(); + ds5_set_adaptive_trigger_effect(device, UNI_ADAPTIVE_TRIGGER_TYPE_LEFT, &off); + ds5_set_adaptive_trigger_effect(device, UNI_ADAPTIVE_TRIGGER_TYPE_RIGHT, &off); + } if (led_timer_set_) { led_timer_set_ = false; @@ -183,6 +782,9 @@ static uni_error_t device_ready_cb(uni_hid_device_t* device) { btstack_run_loop_set_timer(&feedback_timer_, FEEDBACK_TIME_MS); btstack_run_loop_add_timer(&feedback_timer_); } + + ogxm_play_connection_rumble(device); + return UNI_ERROR_SUCCESS; } @@ -190,6 +792,15 @@ static void oob_event_cb(uni_platform_oob_event_t event, void* data) { return; } +// Set to 1 to print all Bluepad32 controller inputs to UART (only when state changes) +#ifndef BLUEPAD32_UART_LOG_INPUT +#if defined(CONFIG_OGXM_DEBUG) +#define BLUEPAD32_UART_LOG_INPUT 1 +#else +#define BLUEPAD32_UART_LOG_INPUT 0 +#endif +#endif + static void controller_data_cb(uni_hid_device_t* device, uni_controller_t* controller) { static uni_gamepad_t prev_uni_gp[MAX_GAMEPADS] = {}; @@ -198,7 +809,31 @@ static void controller_data_cb(uni_hid_device_t* device, uni_controller_t* contr } uni_gamepad_t *uni_gp = &controller->gamepad; - int idx = uni_hid_device_get_idx_for_instance(device); + const int bt_slot = uni_hid_device_get_idx_for_instance(device); + int idx = bp32_get_gamepad_output_idx(device); + if (idx < 0) + idx = bt_slot; + if (idx < 0 || idx >= static_cast(MAX_GAMEPADS)) + return; + { + const uint32_t now_cb = to_ms_since_boot(get_absolute_time()); + s_last_bt_input_ms[static_cast(idx)] = now_cb; + if (bt_slot >= 0 && bt_slot < CONFIG_BLUEPAD32_MAX_DEVICES && bt_slot != idx) + s_last_bt_input_ms[static_cast(bt_slot)] = now_cb; + } + +#if BLUEPAD32_UART_LOG_INPUT + { + bool changed = std::memcmp(uni_gp, &prev_uni_gp[idx], sizeof(uni_gamepad_t)) != 0; + if (changed && device->controller_type != CONTROLLER_TYPE_Switch2ProController) { + printf("[BP32 idx=%d] dpad=0x%02x btns=0x%04x misc=0x%02x brake=%u throttle=%u " + "Lx=%d Ly=%d Rx=%d Ry=%d\n", + idx, (unsigned)uni_gp->dpad, (unsigned)uni_gp->buttons, (unsigned)uni_gp->misc_buttons, + (unsigned)uni_gp->brake, (unsigned)uni_gp->throttle, + (int)uni_gp->axis_x, (int)uni_gp->axis_y, (int)uni_gp->axis_rx, (int)uni_gp->axis_ry); + } + } +#endif Gamepad* gamepad = bt_devices_[idx].gamepad; Gamepad::PadIn gp_in; @@ -233,25 +868,170 @@ static void controller_data_cb(uni_hid_device_t* device, uni_controller_t* contr break; } - if (uni_gp->buttons & BUTTON_A) gp_in.buttons |= gamepad->MAP_BUTTON_A; - if (uni_gp->buttons & BUTTON_B) gp_in.buttons |= gamepad->MAP_BUTTON_B; - if (uni_gp->buttons & BUTTON_X) gp_in.buttons |= gamepad->MAP_BUTTON_X; - if (uni_gp->buttons & BUTTON_Y) gp_in.buttons |= gamepad->MAP_BUTTON_Y; - if (uni_gp->buttons & BUTTON_SHOULDER_L) gp_in.buttons |= gamepad->MAP_BUTTON_LB; - if (uni_gp->buttons & BUTTON_SHOULDER_R) gp_in.buttons |= gamepad->MAP_BUTTON_RB; - if (uni_gp->buttons & BUTTON_THUMB_L) gp_in.buttons |= gamepad->MAP_BUTTON_L3; - if (uni_gp->buttons & BUTTON_THUMB_R) gp_in.buttons |= gamepad->MAP_BUTTON_R3; - if (uni_gp->misc_buttons & MISC_BUTTON_BACK) gp_in.buttons |= gamepad->MAP_BUTTON_BACK; - if (uni_gp->misc_buttons & MISC_BUTTON_START) gp_in.buttons |= gamepad->MAP_BUTTON_START; - if (uni_gp->misc_buttons & MISC_BUTTON_SYSTEM) gp_in.buttons |= gamepad->MAP_BUTTON_SYS; - - gp_in.trigger_l = gamepad->scale_trigger_l<10>(static_cast(uni_gp->brake)); - gp_in.trigger_r = gamepad->scale_trigger_r<10>(static_cast(uni_gp->throttle)); + if (is_wii_controller_connected(idx)) { + if (uni_gp->buttons & BUTTON_A) gp_in.buttons |= gamepad->MAP_BUTTON_A; + if (uni_gp->buttons & BUTTON_B) gp_in.buttons |= gamepad->MAP_BUTTON_B; + if (uni_gp->buttons & BUTTON_X) gp_in.buttons |= gamepad->MAP_BUTTON_X; + if (uni_gp->buttons & BUTTON_Y) gp_in.buttons |= gamepad->MAP_BUTTON_Y; + if (uni_gp->buttons & BUTTON_SHOULDER_L) gp_in.buttons |= gamepad->MAP_BUTTON_LB; + if (uni_gp->buttons & BUTTON_SHOULDER_R) gp_in.buttons |= gamepad->MAP_BUTTON_RB; + //if (uni_gp->buttons & BUTTON_THUMB_L) gp_in.buttons |= gamepad->MAP_BUTTON_L3; + //if (uni_gp->buttons & BUTTON_THUMB_R) gp_in.buttons |= gamepad->MAP_BUTTON_R3; + if (uni_gp->misc_buttons & MISC_BUTTON_BACK) gp_in.buttons |= gamepad->MAP_BUTTON_BACK; + if (uni_gp->misc_buttons & MISC_BUTTON_START) gp_in.buttons |= gamepad->MAP_BUTTON_START; + if (uni_gp->misc_buttons & MISC_BUTTON_SYSTEM) gp_in.buttons |= gamepad->MAP_BUTTON_SYS; + } + else { + if (uni_gp->buttons & BUTTON_A) gp_in.buttons |= gamepad->MAP_BUTTON_A; + if (uni_gp->buttons & BUTTON_B) gp_in.buttons |= gamepad->MAP_BUTTON_B; + if (uni_gp->buttons & BUTTON_X) gp_in.buttons |= gamepad->MAP_BUTTON_X; + if (uni_gp->buttons & BUTTON_Y) gp_in.buttons |= gamepad->MAP_BUTTON_Y; + if (uni_gp->buttons & BUTTON_SHOULDER_L) gp_in.buttons |= gamepad->MAP_BUTTON_LB; + if (uni_gp->buttons & BUTTON_SHOULDER_R) gp_in.buttons |= gamepad->MAP_BUTTON_RB; + if (uni_gp->buttons & BUTTON_THUMB_L) gp_in.buttons |= gamepad->MAP_BUTTON_L3; + if (uni_gp->buttons & BUTTON_THUMB_R) gp_in.buttons |= gamepad->MAP_BUTTON_R3; + if (uni_gp->misc_buttons & MISC_BUTTON_BACK) gp_in.buttons |= gamepad->MAP_BUTTON_BACK; + if (uni_gp->misc_buttons & MISC_BUTTON_START) gp_in.buttons |= gamepad->MAP_BUTTON_START; + if (uni_gp->misc_buttons & MISC_BUTTON_SYSTEM) gp_in.buttons |= gamepad->MAP_BUTTON_SYS; + } + + // Check for disconnect combo: Start+Select for most controllers, L3+R3 for OUYA (no Start/Select) + static uint32_t disconnect_combo_hold_time[MAX_GAMEPADS] = {0}; + const uint32_t now_cb = to_ms_since_boot(get_absolute_time()); + const bool combo_grace = + (idx >= 0 && idx < MAX_GAMEPADS && now_cb < s_bt_disconnect_combo_grace_until_ms[idx]); + bool is_ouya = (device->controller_type == CONTROLLER_TYPE_OUYAController); + bool combo_pressed = is_ouya + ? ((uni_gp->buttons & BUTTON_THUMB_L) && (uni_gp->buttons & BUTTON_THUMB_R)) + : ((uni_gp->misc_buttons & MISC_BUTTON_START) && (uni_gp->misc_buttons & MISC_BUTTON_BACK)); + + if (combo_grace) { + disconnect_combo_hold_time[idx] = 0; + } else if (combo_pressed) { + disconnect_combo_hold_time[idx]++; + // Require combo to be held for ~500ms (assuming ~60Hz callback rate, ~30 frames) + if (disconnect_combo_hold_time[idx] >= 30) { + printf("[BP32] Disconnect combo detected, disconnecting controller %d\n", idx); + bp32_disconnect_controller_and_joycon_partner(device); + disconnect_combo_hold_time[idx] = 0; + return; // Don't process further input after disconnect + } + } else { + disconnect_combo_hold_time[idx] = 0; + } + + // Prefer analog triggers (brake / throttle) when present, but fall back to + // digital trigger buttons (e.g. Wii U LT / RT) when analog value is zero. + // For Wii controllers: Z button (shoulder) reports brake/throttle, but we only want it to map to LB/RB, not triggers + // So skip trigger mapping when shoulder buttons are pressed on Wii controllers + bool wii_shoulder_pressed = is_wii_controller_connected(idx) && + ((uni_gp->buttons & BUTTON_SHOULDER_L) || (uni_gp->buttons & BUTTON_SHOULDER_R)); - std::tie(gp_in.joystick_lx, gp_in.joystick_ly) = gamepad->scale_joystick_l<10>(uni_gp->axis_x, uni_gp->axis_y); - std::tie(gp_in.joystick_rx, gp_in.joystick_ry) = gamepad->scale_joystick_r<10>(uni_gp->axis_rx, uni_gp->axis_ry); + if (!wii_shoulder_pressed) { + gp_in.trigger_l = gamepad->scale_trigger_l<10>(static_cast(uni_gp->brake)); + gp_in.trigger_r = gamepad->scale_trigger_r<10>(static_cast(uni_gp->throttle)); + + if (gp_in.trigger_l == 0 && (uni_gp->buttons & BUTTON_TRIGGER_L)) { + gp_in.trigger_l = 0xFF; + } + if (gp_in.trigger_r == 0 && (uni_gp->buttons & BUTTON_TRIGGER_R)) { + gp_in.trigger_r = 0xFF; + } + } + + /* Nunchuk stick is Bluepad's right axis; map to left stick for character movement. */ + const bool wii_nunchuk = + device->controller_type == CONTROLLER_TYPE_WiiController && + (device->controller_subtype == CONTROLLER_SUBTYPE_WIIMOTE_NUNCHUK || + device->controller_subtype == CONTROLLER_SUBTYPE_WIIMOTE_NUNCHUK_ACCEL); + if (wii_nunchuk) { + std::tie(gp_in.joystick_lx, gp_in.joystick_ly) = + gamepad->scale_joystick_l<10>(uni_gp->axis_rx, uni_gp->axis_ry); + gp_in.joystick_rx = 0; + gp_in.joystick_ry = 0; + } else { + std::tie(gp_in.joystick_lx, gp_in.joystick_ly) = + gamepad->scale_joystick_l<10>(uni_gp->axis_x, uni_gp->axis_y); + std::tie(gp_in.joystick_rx, gp_in.joystick_ry) = + gamepad->scale_joystick_r<10>(uni_gp->axis_rx, uni_gp->axis_ry); + } - gamepad->set_pad_in(gp_in); + gp_in.motion_source = Gamepad::PadIn::MOTION_SRC_NONE; + switch (device->controller_type) { + case CONTROLLER_TYPE_PS4Controller: + gp_in.motion_source = Gamepad::PadIn::MOTION_SRC_DS4; + break; + case CONTROLLER_TYPE_PS5Controller: + gp_in.motion_source = Gamepad::PadIn::MOTION_SRC_DS5; + break; + case CONTROLLER_TYPE_SwitchProController: + case CONTROLLER_TYPE_Switch2ProController: + gp_in.motion_source = Gamepad::PadIn::MOTION_SRC_SWITCH_PRO; + break; + case CONTROLLER_TYPE_WiiController: + if (device->controller_subtype == CONTROLLER_SUBTYPE_WIIMOTE_ACCEL || + device->controller_subtype == CONTROLLER_SUBTYPE_WIIMOTE_NUNCHUK_ACCEL) { + gp_in.motion_source = Gamepad::PadIn::MOTION_SRC_WII_BT; + } else { + const int32_t l1 = (uni_gp->accel[0] >= 0 ? uni_gp->accel[0] : -uni_gp->accel[0]) + + (uni_gp->accel[1] >= 0 ? uni_gp->accel[1] : -uni_gp->accel[1]) + + (uni_gp->accel[2] >= 0 ? uni_gp->accel[2] : -uni_gp->accel[2]); + if (l1 > 16) { + gp_in.motion_source = Gamepad::PadIn::MOTION_SRC_WII_BT; + } + } + break; + default: + break; + } + if (gp_in.motion_source == Gamepad::PadIn::MOTION_SRC_WII_BT) { + MotionImu::fill_from_wii_bt(gp_in.accel, gp_in.gyro, uni_gp->accel); + /* Switch cursor/aim uses gyro; synthesize rate from accel (no MotionPlus parse yet). */ + if (idx >= 0 && idx < static_cast(MAX_GAMEPADS)) { + static MotionImu::WiiPseudoGyroState s_wii_gyro[MAX_GAMEPADS]{}; + MotionImu::apply_wii_pseudo_gyro(gp_in.accel, gp_in.gyro, s_wii_gyro[idx], + to_ms_since_boot(get_absolute_time())); + } + } else if (gp_in.motion_source != Gamepad::PadIn::MOTION_SRC_NONE) { + for (int i = 0; i < 3; i++) { + gp_in.accel[i] = uni_gp->accel[i]; + gp_in.gyro[i] = uni_gp->gyro[i]; + } + } + + if (SteamActive::is_enabled()) { + SteamPassthrough::input_has_touchpad = + (device->controller_type == CONTROLLER_TYPE_PS5Controller); + SteamBtReport::update_from_uni_gamepad(uni_gp); + if (device->controller_type == CONTROLLER_TYPE_PS5Controller) { + uint8_t touch_points[8]{}; + bool touchpad_click = false; + uni_hid_parser_ds5_get_touchpad(device, touch_points, &touchpad_click); + SteamTouchpad::apply_to_passthrough(touch_points, touchpad_click); + std::memcpy(gp_in.touch_raw, touch_points, sizeof(gp_in.touch_raw)); + gp_in.touchpad_click = touchpad_click ? 1 : 0; + gp_in.touchpad_valid = 1; + } + } + + gamepad->set_pad_in_from_bluetooth(gp_in); + +#if BLUEPAD32_UART_LOG_INPUT + if (idx >= 0 && idx < static_cast(MAX_GAMEPADS) && + device->controller_type != CONTROLLER_TYPE_Switch2ProController) { + std::memcpy(&prev_uni_gp[idx], uni_gp, sizeof(uni_gamepad_t)); + } +#endif + + // PS5: defer adaptive trigger send to main loop so callback never does l2cap_send (reduces input lag) + if (device->controller_type == CONTROLLER_TYPE_PS5Controller && idx >= 0 && idx < static_cast(MAX_GAMEPADS)) { + bool touchpad_clicked = (uni_gp->misc_buttons & MISC_BUTTON_CAPTURE) != 0; + if (touchpad_clicked && !prev_touchpad_clicked_[idx]) { + adaptive_trigger_enabled_[idx] = !adaptive_trigger_enabled_[idx]; + pending_adaptive_trigger_send_[idx] = true; + } + prev_touchpad_clicked_[idx] = touchpad_clicked; + } } const uni_property_t* get_property_cb(uni_property_idx_t idx) @@ -259,6 +1039,27 @@ const uni_property_t* get_property_cb(uni_property_idx_t idx) return nullptr; } +void process_pending_adaptive_triggers() +{ + for (uint8_t i = 0; i < MAX_GAMEPADS; ++i) { + if (!pending_adaptive_trigger_send_[i]) + continue; + pending_adaptive_trigger_send_[i] = false; + uni_hid_device_t* device = uni_hid_device_get_instance_for_idx(static_cast(i)); + if (!device || device->controller_type != CONTROLLER_TYPE_PS5Controller) + continue; + if (adaptive_trigger_enabled_[i]) { + ds5_adaptive_trigger_effect_t on = ds5_new_adaptive_trigger_effect_feedback(5, 4); + ds5_set_adaptive_trigger_effect(device, UNI_ADAPTIVE_TRIGGER_TYPE_LEFT, &on); + ds5_set_adaptive_trigger_effect(device, UNI_ADAPTIVE_TRIGGER_TYPE_RIGHT, &on); + } else { + ds5_adaptive_trigger_effect_t off = ds5_new_adaptive_trigger_effect_off(); + ds5_set_adaptive_trigger_effect(device, UNI_ADAPTIVE_TRIGGER_TYPE_LEFT, &off); + ds5_set_adaptive_trigger_effect(device, UNI_ADAPTIVE_TRIGGER_TYPE_RIGHT, &off); + } + } +} + uni_platform* get_driver() { static uni_platform driver = @@ -279,7 +1080,46 @@ uni_platform* get_driver() //Public API -void run_task(Gamepad(&gamepads)[MAX_GAMEPADS]) +void set_gpio_device_process_callback(void (*callback)(void* ctx), void* ctx) { + gpio_process_cb_ = callback; + gpio_process_ctx_ = ctx; +} + +void set_pico_w_pio_usb_mux_tick(void (*tick_cb)(void)) { + s_pico_w_pio_usb_mux_tick = tick_cb; +} + +void wired_usb_takeover_disconnect_bt() { +#if defined(CONFIG_TARGET_PICO_W) && defined(CONFIG_EN_USB_HOST) + /* Core0 mux uses this atomic; disconnect callbacks run async on Core1. Clear immediately so + * we do not treat BT as active and tuh_deinit() wired USB during the disconnect window. */ + s_bt_any_connected_cached.store(false, std::memory_order_release); +#endif + for (uint8_t i = 0; i < CONFIG_BLUEPAD32_MAX_DEVICES; ++i) { + uni_bt_disconnect_device_safe(i); + } + uni_bt_enable_new_connections_safe(false); +} + +void wired_usb_release_enable_bt_pairing() { +#if defined(CONFIG_EN_BLUETOOTH) && defined(CONFIG_TARGET_PICO_W) + s_usb_resume_bt_reg.callback = usb_resume_on_bt_main; + s_usb_resume_bt_reg.context = nullptr; + btstack_run_loop_execute_on_main_thread(&s_usb_resume_bt_reg); +#else + uni_bt_enable_new_connections_safe(true); +#endif +} + +void on_usb_device_resume() { +#if defined(CONFIG_EN_BLUETOOTH) && defined(CONFIG_TARGET_PICO_W) + s_usb_resume_bt_reg.callback = usb_resume_on_bt_main; + s_usb_resume_bt_reg.context = nullptr; + btstack_run_loop_execute_on_main_thread(&s_usb_resume_bt_reg); +#endif +} + +void init(Gamepad(&gamepads)[MAX_GAMEPADS]) { for (uint8_t i = 0; i < MAX_GAMEPADS; ++i) { @@ -295,6 +1135,31 @@ void run_task(Gamepad(&gamepads)[MAX_GAMEPADS]) btstack_run_loop_set_timer(&led_timer_, LED_CHECK_TIME_MS); btstack_run_loop_add_timer(&led_timer_); + if (gpio_process_cb_ != nullptr) { + gpio_process_timer_.process = gpio_process_timer_cb; + gpio_process_timer_.context = nullptr; + btstack_run_loop_set_timer(&gpio_process_timer_, GPIO_PROCESS_INTERVAL_MS); + btstack_run_loop_add_timer(&gpio_process_timer_); + } + + if (s_pico_w_pio_usb_mux_tick != nullptr) { + s_pico_w_usb_mux_timer_.process = pico_w_usb_mux_timer_cb; + s_pico_w_usb_mux_timer_.context = nullptr; + btstack_run_loop_set_timer(&s_pico_w_usb_mux_timer_, 1); + btstack_run_loop_add_timer(&s_pico_w_usb_mux_timer_); + } + +#if defined(CONFIG_EN_BLUETOOTH) && defined(CONFIG_TARGET_PICO_W) + s_pairing_watchdog_timer.process = pairing_watchdog_cb; + s_pairing_watchdog_timer.context = nullptr; + btstack_run_loop_set_timer(&s_pairing_watchdog_timer, PAIRING_WATCHDOG_MS); + btstack_run_loop_add_timer(&s_pairing_watchdog_timer); +#endif +} + +void run_task(Gamepad(&gamepads)[MAX_GAMEPADS]) +{ + init(gamepads); btstack_run_loop_execute(); } diff --git a/Firmware/RP2040/src/Bluepad32/Bluepad32.h b/Firmware/RP2040/src/Bluepad32/Bluepad32.h index 19b13a21..c3068678 100644 --- a/Firmware/RP2040/src/Bluepad32/Bluepad32.h +++ b/Firmware/RP2040/src/Bluepad32/Bluepad32.h @@ -10,5 +10,23 @@ and kept away from tinyusb due to naming conflicts */ namespace bluepad32 { + /** True if any Bluetooth gamepad is connected (Classic or LE). */ + bool any_connected(); void run_task(Gamepad(&gamepads)[MAX_GAMEPADS]); + void init(Gamepad(&gamepads)[MAX_GAMEPADS]); + /** + * Pico W / Pico 2 W only: register a 1 ms callback from the Bluetooth run loop for + * PIO USB host + BT mutual exclusion. Call once before run_task(); pass nullptr to disable. + */ + void set_pico_w_pio_usb_mux_tick(void (*tick_cb)(void)); + /** Disconnect all BT gamepads and block new connections (PIO wired USB took over). */ + void wired_usb_takeover_disconnect_bt(); + /** Re-enable BT pairing after wired USB device unplugged. */ + void wired_usb_release_enable_bt_pairing(); + /** Pico W / Pico 2 W / RP2354 BT: restore pairing scans after USB host resume (e.g. Xbox 360 standby wake). */ + void on_usb_device_resume(); + /** Call from main loop to send deferred PS5 adaptive-trigger updates (keeps BT callback fast). */ + void process_pending_adaptive_triggers(); + /** Optional: when set, a timer in the BT run loop calls this every ~4 ms (for GPIO device modes: PS1/PS2, GameCube). Call before run_task(). */ + void set_gpio_device_process_callback(void (*callback)(void* ctx), void* ctx); } \ No newline at end of file diff --git a/Firmware/RP2040/src/Board/Config.h b/Firmware/RP2040/src/Board/Config.h index 658378e2..0dea246c 100644 --- a/Firmware/RP2040/src/Board/Config.h +++ b/Firmware/RP2040/src/Board/Config.h @@ -9,7 +9,11 @@ #define ESP32_BLUERETRO_I2C 5 #define EXTERNAL_4CH_I2C 6 #define INTERNAL_4CH_I2C 7 -#define BOARDS_COUNT 8 +#define RP2350_USB_A 8 +#define RP2350_ZERO 9 +#define RP2040_XIAO 10 +#define RP2354 11 +#define BOARDS_COUNT 12 #define SYSCLOCK_KHZ 240000 @@ -17,6 +21,19 @@ #define MAX_GAMEPADS 1 #endif +/** TinyUSB XInput host instances. The 360 wireless USB receiver exposes 4 controller interfaces; + * all 4 must be polled even when MAX_GAMEPADS is 1 so sync works on any RF slot. */ +#if MAX_GAMEPADS < 4 + #define OGXM_TUH_XINPUT_INSTANCES 4 +#else + #define OGXM_TUH_XINPUT_INSTANCES MAX_GAMEPADS +#endif + +/** Main loop delay (device/core0) in microseconds. 0 = low latency (default). 250+ = lower CPU. */ +#ifndef MAIN_LOOP_DELAY_US + #define MAIN_LOOP_DELAY_US 0 +#endif + #if defined(CONFIG_OGXM_BOARD_PI_PICO) || defined(CONFIG_OGXM_BOARD_PI_PICO2) #define OGXM_BOARD PI_PICO #define PIO_USB_DP_PIN 0 // DM = 1 @@ -24,6 +41,11 @@ #elif defined(CONFIG_OGXM_BOARD_PI_PICOW) || defined(CONFIG_OGXM_BOARD_PI_PICO2W) #define OGXM_BOARD PI_PICOW + // PIO USB host for external port (e.g. Wii mode: controller on USB, BT for Wiimote) + #define PIO_USB_DP_PIN 0 // D+ = GP0, D- = GP1 + #ifndef PIO_USB_SWAP_DP_DM + #define PIO_USB_SWAP_DP_DM 0 // set 1 if controller powers but does not enumerate + #endif #elif defined(CONFIG_OGXM_BOARD_RP2040_ZERO) #define OGXM_BOARD RP2040_ZERO @@ -89,16 +111,37 @@ #define MAX_GAMEPADS 1 #endif +#elif defined(CONFIG_OGXM_BOARD_RP2350_USB_A) + #define OGXM_BOARD RP2350_USB_A + #define PIO_USB_DP_PIN 12 + #define RGB_PXL_PIN 16 + +#elif defined(CONFIG_OGXM_BOARD_RP2350_ZERO) + #define OGXM_BOARD RP2350_ZERO + #define PIO_USB_DP_PIN 10 // DM = 11 + #define RGB_PXL_PIN 16 + +#elif defined(CONFIG_OGXM_BOARD_RP2040_XIAO) + #define OGXM_BOARD RP2040_XIAO + #define RGB_PXL_PIN 12 + #define RGB_PWR_PIN 11 + #define PIO_USB_DP_PIN 0 // DM = 1 + #define LED_INDICATOR_PIN 17 + +#elif defined(CONFIG_OGXM_BOARD_RP2354) + #define OGXM_BOARD RP2354 + /* RP2350 + Pi Radio Module 2 (CYW43439): same PIO USB host pins as Pico 2 W. */ + #define PIO_USB_DP_PIN 0 // D+ = GP0, D- = GP1 + #ifndef PIO_USB_SWAP_DP_DM + #define PIO_USB_SWAP_DP_DM 0 + #endif + #define LED_INDICATOR_PIN 25 + #else #error "Invalid OGXMini board selected" #endif -#if defined(CONFIG_OGXM_DEBUG) - //Pins and port are defined in CMakeLists.txt - #define DEBUG_UART_PORT __CONCAT(uart,PICO_DEFAULT_UART) -#endif // defined(CONFIG_OGXM_DEBUG) - #if defined(I2C_SDA_PIN) #define I2C_BAUDRATE 400 * 1000 #define I2C_PORT ((I2C_SDA_PIN == 2 ) || \ @@ -126,4 +169,10 @@ } #endif // defined(PIO_USB_DP_PIN) +/* Boards that run the Pico W firmware path (CYW43439 BT + optional PIO USB host mux). */ +#if defined(CONFIG_OGXM_BOARD_PI_PICOW) || defined(CONFIG_OGXM_BOARD_PI_PICO2W) || \ + (defined(CONFIG_OGXM_BOARD_RP2354) && defined(CONFIG_EN_BLUETOOTH)) + #define OGXM_BOARD_USES_PICO_W_FIRMWARE 1 +#endif + #endif // _BOARD_CONFIG_H_ \ No newline at end of file diff --git a/Firmware/RP2040/src/Board/board_api.cpp b/Firmware/RP2040/src/Board/board_api.cpp index fe78315c..6cea5de5 100644 --- a/Firmware/RP2040/src/Board/board_api.cpp +++ b/Firmware/RP2040/src/Board/board_api.cpp @@ -1,16 +1,55 @@ #include #include #include +#include #include +#include +#include #include "tusb.h" +#if defined(CONFIG_EN_USB_HOST) +#include "pio_usb.h" +#endif + #include "Board/Config.h" #include "Board/board_api.h" #include "Board/ogxm_log.h" #include "Board/board_api_private/board_api_private.h" #include "TaskQueue/TaskQueue.h" +#if defined(CONFIG_EN_USB_HOST) +#include "USBHost/HostManager.h" +#endif + +extern "C" { + +// TUSB_OPT_TIME_CALLBACK=1: TinyUSB expects the platform to supply millis; the SDK helper may +// not be linked in this configuration. Used by our tusb_time_delay_ms_api override and TinyUSB. +uint32_t tusb_time_millis_api(void) { + return time_us_32() / 1000u; +} + +// Overrides TinyUSB's weak default (busy-wait on millis only). With PIO USB host and +// skip_alarm_pool there is no hardware 1 kHz SOF interrupt — pio_usb_host_frame() must run +// about once per millisecond. Enumeration calls tusb_time_delay_ms_api() for 50+ ms resets +// from inside tuh_task(); without servicing PIO here, SOFs stop and control transfers fail +// ("Enumeration attempt N" retries, controller never works). +void tusb_time_delay_ms_api(uint32_t ms) { + const uint32_t start = tusb_time_millis_api(); + while ((tusb_time_millis_api() - start) < ms) { +#if defined(CONFIG_EN_USB_HOST) + pio_usb_host_frame(); + const uint32_t now = tusb_time_millis_api(); + while (tusb_time_millis_api() == now) { + tight_loop_contents(); + } +#endif + } +} + +} // extern "C" + namespace board_api { mutex_t gpio_mutex_; @@ -22,12 +61,28 @@ bool usb::host_connected() { return false; } +bool usb::host_any_pad_mounted() { +#if defined(CONFIG_EN_USB_HOST) + return HostManager::get_instance().any_mounted(); +#else + return false; +#endif +} + //Only call this from core0 void usb::disconnect_all() { OGXM_LOG("Disconnecting USB and resetting Core1\n"); TaskQueue::suspend_delayed_tasks(); +#if defined(CONFIG_EN_USB_HOST) + if (board_api_usbh::stop_pio_usb_host) { + board_api_usbh::stop_pio_usb_host(); + } else { + multicore_reset_core1(); + } +#else multicore_reset_core1(); +#endif sleep_ms(500); tud_disconnect(); sleep_ms(500); @@ -51,14 +106,12 @@ void set_led(bool state) { } void reboot() { - #define AIRCR_REG (*((volatile uint32_t *)(0xE000ED0C))) - #define AIRCR_SYSRESETREQ (1 << 2) - #define AIRCR_VECTKEY (0x5FA << 16) - OGXM_LOG("Rebooting\n"); - - AIRCR_REG = AIRCR_VECTKEY | AIRCR_SYSRESETREQ; - while(1); + /* Watchdog reset is reliable from either core (BT runs on Core1). */ + watchdog_reboot(0, 0, 0); + while (1) { + tight_loop_contents(); + } } uint32_t ms_since_boot() { diff --git a/Firmware/RP2040/src/Board/board_api.h b/Firmware/RP2040/src/Board/board_api.h index e5f82322..1fb3769a 100644 --- a/Firmware/RP2040/src/Board/board_api.h +++ b/Firmware/RP2040/src/Board/board_api.h @@ -14,6 +14,8 @@ namespace board_api { namespace usb { bool host_connected(); void disconnect_all(); + /** True if TinyUSB host has at least one mounted gamepad driver (wired controller active). */ + bool host_any_pad_mounted(); } } diff --git a/Firmware/RP2040/src/Board/board_api_private/board_api_bt.cpp b/Firmware/RP2040/src/Board/board_api_private/board_api_bt.cpp index c6f5d00b..63da26fa 100644 --- a/Firmware/RP2040/src/Board/board_api_private/board_api_bt.cpp +++ b/Firmware/RP2040/src/Board/board_api_private/board_api_bt.cpp @@ -5,6 +5,7 @@ #include #include "Board/board_api_private/board_api_private.h" +#include "Board/ogxm_log.h" #if defined(CYW43_WL_GPIO_LED_PIN) && defined(LED_INDICATOR_PIN) static_assert(CYW43_WL_GPIO_LED_PIN != LED_INDICATOR_PIN, "CYW43_WL_GPIO_LED_PIN cannot be the same as LED_INDICATOR_PIN"); @@ -15,15 +16,18 @@ namespace board_api_bt { std::atomic inited{false}; void init() { - if (cyw43_arch_init() != 0) { + if (cyw43_arch_init() != 0) { + OGXM_LOG("board_api_bt: CYW43 init failed\n"); panic("CYW43 init failed"); } else { inited.store(true); + OGXM_LOG("board_api_bt: CYW43 init OK\n"); } } void set_led(bool state) { if (!inited.load()) { + OGXM_LOG("board_api_bt: set_led skipped (not inited)\n"); return; } cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, state ? 1 : 0); diff --git a/Firmware/RP2040/src/Board/board_api_private/board_api_private.h b/Firmware/RP2040/src/Board/board_api_private/board_api_private.h index f2cb9c6f..6b02aed4 100644 --- a/Firmware/RP2040/src/Board/board_api_private/board_api_private.h +++ b/Firmware/RP2040/src/Board/board_api_private/board_api_private.h @@ -24,6 +24,14 @@ namespace board_api_rgb { namespace board_api_usbh { void init() __attribute__((weak)); bool host_connected() __attribute__((weak)); + /** Stop edge IRQs on D+/D− before PIO USB reclaims pins (avoids IO_IRQ storm / BT stall). */ + void suspend_line_irq(); + /** While PIO owns D+/D−, update the same flag GPIO IRQs used (from hcd_port_connect_status). */ + void store_host_line_connected(bool connected); + /** After tuh_deinit / PIO release path, re-enable D+/D− edge IRQs for the next cable attach. */ + void enable_host_line_irq_monitoring(); + /** Stop PIO USB host (SOF timer, tuh_deinit) before core reset / reboot. Standard boards. */ + void stop_pio_usb_host() __attribute__((weak)); } #endif // BOARD_API_PRIVATE_H \ No newline at end of file diff --git a/Firmware/RP2040/src/Board/board_api_private/board_api_usbh.cpp b/Firmware/RP2040/src/Board/board_api_private/board_api_usbh.cpp index ec20f5d4..e9f04995 100644 --- a/Firmware/RP2040/src/Board/board_api_private/board_api_usbh.cpp +++ b/Firmware/RP2040/src/Board/board_api_private/board_api_usbh.cpp @@ -11,33 +11,43 @@ namespace board_api_usbh { std::atomic host_connected_ = false; void host_pin_isr(uint gpio, uint32_t events) { + (void)gpio; + (void)events; + // Do not leave IRQs disabled while a device is attached — that prevented unplug edges from + // ever being seen and could strand host_connected_ out of sync with the pins. + const uint32_t dp_state = gpio_get(PIO_USB_DP_PIN); + const uint32_t dm_state = gpio_get(PIO_USB_DP_PIN + 1); + host_connected_.store((dp_state != 0u) || (dm_state != 0u)); +} + +bool host_connected() { + return host_connected_.load(); +} + +void suspend_line_irq() { gpio_set_irq_enabled(PIO_USB_DP_PIN, GPIO_IRQ_EDGE_RISE | GPIO_IRQ_EDGE_FALL, false); gpio_set_irq_enabled(PIO_USB_DP_PIN + 1, GPIO_IRQ_EDGE_RISE | GPIO_IRQ_EDGE_FALL, false); +} - if (gpio == PIO_USB_DP_PIN || gpio == PIO_USB_DP_PIN + 1) { - uint32_t dp_state = gpio_get(PIO_USB_DP_PIN); - uint32_t dm_state = gpio_get(PIO_USB_DP_PIN + 1); - - if (dp_state || dm_state) { - host_connected_.store(true); - } else { - host_connected_.store(false); - gpio_set_irq_enabled(PIO_USB_DP_PIN, GPIO_IRQ_EDGE_RISE | GPIO_IRQ_EDGE_FALL, true); - gpio_set_irq_enabled(PIO_USB_DP_PIN + 1, GPIO_IRQ_EDGE_RISE | GPIO_IRQ_EDGE_FALL, true); - } - } +void store_host_line_connected(bool connected) { + host_connected_.store(connected); } -bool host_connected() { - return host_connected_.load(); +void enable_host_line_irq_monitoring() { + gpio_set_irq_enabled_with_callback(PIO_USB_DP_PIN, GPIO_IRQ_EDGE_RISE | GPIO_IRQ_EDGE_FALL, true, &host_pin_isr); + gpio_set_irq_enabled_with_callback(PIO_USB_DP_PIN + 1, GPIO_IRQ_EDGE_RISE | GPIO_IRQ_EDGE_FALL, true, &host_pin_isr); + host_pin_isr(0, 0); } +void stop_pio_usb_host() __attribute__((weak)); +void stop_pio_usb_host() {} + void init() { #if defined(VCC_EN_PIN) gpio_init(VCC_EN_PIN); gpio_set_dir(VCC_EN_PIN, GPIO_OUT); gpio_put(VCC_EN_PIN, 1); -#endif +#endif gpio_init(PIO_USB_DP_PIN); gpio_set_dir(PIO_USB_DP_PIN, GPIO_IN); @@ -57,4 +67,12 @@ void init() { } // namespace board_api_usbh +#else // !defined(CONFIG_EN_USB_HOST) + +namespace board_api_usbh { +void suspend_line_irq() {} +void store_host_line_connected(bool) {} +void enable_host_line_irq_monitoring() {} +} // namespace board_api_usbh + #endif // defined(CONFIG_EN_USB_HOST) \ No newline at end of file diff --git a/Firmware/RP2040/src/Board/ogxm_log.cpp b/Firmware/RP2040/src/Board/ogxm_log.cpp index 6bb55c5f..bc0a20ab 100644 --- a/Firmware/RP2040/src/Board/ogxm_log.cpp +++ b/Firmware/RP2040/src/Board/ogxm_log.cpp @@ -2,13 +2,12 @@ #if defined(CONFIG_OGXM_DEBUG) #include +#include #include #include #include #include #include -#include -#include #include "USBDevice/DeviceDriver/DeviceDriverTypes.h" #include "Board/ogxm_log.h" @@ -21,9 +20,15 @@ std::ostream& operator<<(std::ostream& os, DeviceDriverType type) { case DeviceDriverType::XBOXOG_XR: os << "XBOXOG_XR"; break; case DeviceDriverType::XINPUT: os << "XINPUT"; break; case DeviceDriverType::PS3: os << "PS3"; break; + case DeviceDriverType::PS4: os << "PS4"; break; + case DeviceDriverType::STEAM: os << "STEAM"; break; case DeviceDriverType::DINPUT: os << "DINPUT"; break; case DeviceDriverType::PSCLASSIC: os << "PSCLASSIC"; break; case DeviceDriverType::SWITCH: os << "SWITCH"; break; + case DeviceDriverType::WIIU: os << "WIIU"; break; + case DeviceDriverType::WII: os << "WII"; break; + case DeviceDriverType::PS1PS2: os << "PS1PS2"; break; + case DeviceDriverType::GAMECUBE: os << "GAMECUBE"; break; case DeviceDriverType::WEBAPP: os << "WEBAPP"; break; case DeviceDriverType::UART_BRIDGE: os << "UART_BRIDGE"; break; default: os << "UNKNOWN"; break; @@ -34,9 +39,8 @@ std::ostream& operator<<(std::ostream& os, DeviceDriverType type) { namespace ogxm_log { void init() { - uart_init(DEBUG_UART_PORT, PICO_DEFAULT_UART_BAUD_RATE); - gpio_set_function(PICO_DEFAULT_UART_TX_PIN, GPIO_FUNC_UART); - gpio_set_function(PICO_DEFAULT_UART_RX_PIN, GPIO_FUNC_UART); + // Debug: pico_stdio_uart only (host stack disables USB CDC stdio). main/init_board set PICO_DEFAULT_UART pins. + setvbuf(stdout, nullptr, _IONBF, 0); } void log(const std::string& message) { @@ -50,7 +54,8 @@ void log(const std::string& message) { std::string formatted_msg = "OGXM: " + message; - uart_puts(DEBUG_UART_PORT, formatted_msg.c_str()); + printf("%s", formatted_msg.c_str()); + fflush(stdout); mutex_exit(&log_mutex); } diff --git a/Firmware/RP2040/src/Descriptors/PS3.h b/Firmware/RP2040/src/Descriptors/PS3.h index 72516d67..9c9532ce 100644 --- a/Firmware/RP2040/src/Descriptors/PS3.h +++ b/Firmware/RP2040/src/Descriptors/PS3.h @@ -8,7 +8,8 @@ namespace PS3 { static constexpr uint8_t MAGIC_BYTES[8] = { 0x21, 0x26, 0x01, 0x07, 0x00, 0x00, 0x00, 0x00 }; - static constexpr uint8_t JOYSTICK_MID = 0x7F; + // DS3/Sixaxis analog sticks: 8-bit range 0–255, center 0x80 (128) per HID spec + static constexpr uint8_t JOYSTICK_MID = 0x80; static constexpr uint16_t SIXAXIS_MID = 0xFF01; namespace ReportID @@ -340,7 +341,7 @@ namespace PS3 0x01, // bNumInterfaces 1 0x01, // bConfigurationValue 0x00, // iConfiguration (String Index) - 0x80, // bmAttributes + 0xA0, // bmAttributes (bus powered, remote wakeup — for wake from standby via PS or Start) 0xFA, // bMaxPower 500mA 0x09, // bLength diff --git a/Firmware/RP2040/src/Descriptors/PS4Usb.h b/Firmware/RP2040/src/Descriptors/PS4Usb.h new file mode 100644 index 00000000..adb6fd35 --- /dev/null +++ b/Firmware/RP2040/src/Descriptors/PS4Usb.h @@ -0,0 +1,126 @@ +#ifndef _PS4_USB_DESCRIPTORS_H_ +#define _PS4_USB_DESCRIPTORS_H_ + +#include + +/* + * USB IDs and 467-byte HID report descriptor captured from Brook → DS4 (CUH-ZCT1x) mode + * (see Resources/Brook/brook-lsusb-v.txt, brook-usbhid-dump.txt 001:032:000). + */ + +namespace PS4Usb +{ + static const uint8_t STRING_LANGUAGE[] = { 0x09, 0x04 }; + static const uint8_t STRING_MANUFACTURER[] = "Sony Computer Entertainment"; + static const uint8_t STRING_PRODUCT[] = "Wireless Controller"; + static const uint8_t STRING_VERSION[] = "1.0"; + + static const uint8_t *STRING_DESCRIPTORS[] __attribute__((unused)) = + { + STRING_LANGUAGE, + STRING_MANUFACTURER, + STRING_PRODUCT, + STRING_VERSION + }; + + static const uint8_t DEVICE_DESCRIPTORS[] = + { + 0x12, + 0x01, + 0x00, 0x02, + 0x00, + 0x00, + 0x00, + 0x40, + 0x4C, 0x05, + 0xC4, 0x05, + 0x00, 0x01, + 0x01, + 0x02, + 0x00, + 0x01, + }; + + static const uint8_t REPORT_DESCRIPTORS[] = + { + 0x05, 0x01, 0x09, 0x05, 0xa1, 0x01, 0x85, 0x01, 0x09, 0x30, 0x09, 0x31, 0x09, 0x32, 0x09, 0x35, + 0x15, 0x00, 0x26, 0xff, 0x00, 0x75, 0x08, 0x95, 0x04, 0x81, 0x02, 0x09, 0x39, 0x15, 0x00, 0x25, + 0x07, 0x35, 0x00, 0x46, 0x3b, 0x01, 0x65, 0x14, 0x75, 0x04, 0x95, 0x01, 0x81, 0x42, 0x65, 0x00, + 0x05, 0x09, 0x19, 0x01, 0x29, 0x0e, 0x15, 0x00, 0x25, 0x01, 0x75, 0x01, 0x95, 0x0e, 0x81, 0x02, + 0x06, 0x00, 0xff, 0x09, 0x20, 0x75, 0x06, 0x95, 0x01, 0x15, 0x00, 0x25, 0x7f, 0x81, 0x02, 0x05, + 0x01, 0x09, 0x33, 0x09, 0x34, 0x15, 0x00, 0x26, 0xff, 0x00, 0x75, 0x08, 0x95, 0x02, 0x81, 0x02, + 0x06, 0x00, 0xff, 0x09, 0x21, 0x95, 0x36, 0x81, 0x02, 0x85, 0x05, 0x09, 0x22, 0x95, 0x1f, 0x91, + 0x02, 0x85, 0x04, 0x09, 0x23, 0x95, 0x24, 0xb1, 0x02, 0x85, 0x02, 0x09, 0x24, 0x95, 0x24, 0xb1, + 0x02, 0x85, 0x08, 0x09, 0x25, 0x95, 0x03, 0xb1, 0x02, 0x85, 0x10, 0x09, 0x26, 0x95, 0x04, 0xb1, + 0x02, 0x85, 0x11, 0x09, 0x27, 0x95, 0x02, 0xb1, 0x02, 0x85, 0x12, 0x06, 0x02, 0xff, 0x09, 0x21, + 0x95, 0x0f, 0xb1, 0x02, 0x85, 0x13, 0x09, 0x22, 0x95, 0x16, 0xb1, 0x02, 0x85, 0x14, 0x06, 0x05, + 0xff, 0x09, 0x20, 0x95, 0x10, 0xb1, 0x02, 0x85, 0x15, 0x09, 0x21, 0x95, 0x2c, 0xb1, 0x02, 0x06, + 0x80, 0xff, 0x85, 0x80, 0x09, 0x20, 0x95, 0x06, 0xb1, 0x02, 0x85, 0x81, 0x09, 0x21, 0x95, 0x06, + 0xb1, 0x02, 0x85, 0x82, 0x09, 0x22, 0x95, 0x05, 0xb1, 0x02, 0x85, 0x83, 0x09, 0x23, 0x95, 0x01, + 0xb1, 0x02, 0x85, 0x84, 0x09, 0x24, 0x95, 0x04, 0xb1, 0x02, 0x85, 0x85, 0x09, 0x25, 0x95, 0x06, + 0xb1, 0x02, 0x85, 0x86, 0x09, 0x26, 0x95, 0x06, 0xb1, 0x02, 0x85, 0x87, 0x09, 0x27, 0x95, 0x23, + 0xb1, 0x02, 0x85, 0x88, 0x09, 0x28, 0x95, 0x22, 0xb1, 0x02, 0x85, 0x89, 0x09, 0x29, 0x95, 0x02, + 0xb1, 0x02, 0x85, 0x90, 0x09, 0x30, 0x95, 0x05, 0xb1, 0x02, 0x85, 0x91, 0x09, 0x31, 0x95, 0x03, + 0xb1, 0x02, 0x85, 0x92, 0x09, 0x32, 0x95, 0x03, 0xb1, 0x02, 0x85, 0x93, 0x09, 0x33, 0x95, 0x0c, + 0xb1, 0x02, 0x85, 0xa0, 0x09, 0x40, 0x95, 0x06, 0xb1, 0x02, 0x85, 0xa1, 0x09, 0x41, 0x95, 0x01, + 0xb1, 0x02, 0x85, 0xa2, 0x09, 0x42, 0x95, 0x01, 0xb1, 0x02, 0x85, 0xa3, 0x09, 0x43, 0x95, 0x30, + 0xb1, 0x02, 0x85, 0xa4, 0x09, 0x44, 0x95, 0x0d, 0xb1, 0x02, 0x85, 0xa5, 0x09, 0x45, 0x95, 0x15, + 0xb1, 0x02, 0x85, 0xa6, 0x09, 0x46, 0x95, 0x15, 0xb1, 0x02, 0x85, 0xf0, 0x09, 0x47, 0x95, 0x3f, + 0xb1, 0x02, 0x85, 0xf1, 0x09, 0x48, 0x95, 0x3f, 0xb1, 0x02, 0x85, 0xf2, 0x09, 0x49, 0x95, 0x0f, + 0xb1, 0x02, 0x85, 0xa7, 0x09, 0x4a, 0x95, 0x01, 0xb1, 0x02, 0x85, 0xa8, 0x09, 0x4b, 0x95, 0x01, + 0xb1, 0x02, 0x85, 0xa9, 0x09, 0x4c, 0x95, 0x08, 0xb1, 0x02, 0x85, 0xaa, 0x09, 0x4e, 0x95, 0x01, + 0xb1, 0x02, 0x85, 0xab, 0x09, 0x4f, 0x95, 0x39, 0xb1, 0x02, 0x85, 0xac, 0x09, 0x50, 0x95, 0x39, + 0xb1, 0x02, 0x85, 0xad, 0x09, 0x51, 0x95, 0x0b, 0xb1, 0x02, 0x85, 0xae, 0x09, 0x52, 0x95, 0x01, + 0xb1, 0x02, 0x85, 0xaf, 0x09, 0x53, 0x95, 0x02, 0xb1, 0x02, 0x85, 0xb0, 0x09, 0x54, 0x95, 0x3f, + 0xb1, 0x02, 0xc0, + }; + + static_assert(sizeof(REPORT_DESCRIPTORS) == 467, "Brook DS4 report descriptor length"); + + static const uint8_t CONFIGURATION_DESCRIPTORS[] = + { + 0x09, + 0x02, + 0x29, 0x00, + 0x01, + 0x01, + 0x00, + 0xA0, + 0xFA, + + 0x09, + 0x04, + 0x00, + 0x00, + 0x02, + 0x03, + 0x00, + 0x00, + 0x00, + + 0x09, + 0x21, + 0x11, 0x01, + 0x00, + 0x01, + 0x22, + 0xD3, 0x01, + + 0x07, + 0x05, + 0x84, + 0x03, + 0x40, 0x00, + 0x05, + + 0x07, + 0x05, + 0x03, + 0x03, + 0x40, 0x00, + 0x05, + }; + +} // namespace PS4Usb + +#endif diff --git a/Firmware/RP2040/src/Descriptors/PS5Usb.h b/Firmware/RP2040/src/Descriptors/PS5Usb.h new file mode 100644 index 00000000..30231ace --- /dev/null +++ b/Firmware/RP2040/src/Descriptors/PS5Usb.h @@ -0,0 +1,70 @@ +#ifndef _PS5_USB_DESCRIPTORS_H_ +#define _PS5_USB_DESCRIPTORS_H_ + +#include + +/* + * DualSense (CFI-ZCT1) USB identity and 273-byte HID report descriptor. + * Configuration is HID-only (no audio interfaces) — sufficient for PC/Linux gamepad drivers. + */ + +namespace PS5Usb +{ + static const uint8_t STRING_LANGUAGE[] = { 0x09, 0x04 }; + static const uint8_t STRING_MANUFACTURER[] = "Sony Interactive Entertainment"; + static const uint8_t STRING_PRODUCT[] = "Wireless Controller"; + static const uint8_t STRING_VERSION[] = "1.00"; + + static const uint8_t *STRING_DESCRIPTORS[] __attribute__((unused)) = + { + STRING_LANGUAGE, + STRING_MANUFACTURER, + STRING_PRODUCT, + STRING_VERSION + }; + + static const uint8_t DEVICE_DESCRIPTORS[] = + { + 0x12, + 0x01, + 0x00, 0x02, + 0x00, + 0x00, + 0x00, + 0x40, + 0x4C, 0x05, + 0xCE, 0x0C, + 0x00, 0x01, + 0x01, + 0x02, + 0x00, + 0x01, + }; + + static const uint8_t REPORT_DESCRIPTORS[] = + { + 0x05, 0x01, 0x09, 0x05, 0xa1, 0x01, 0x85, 0x01, 0x09, 0x30, 0x09, 0x31, 0x09, 0x32, 0x09, 0x35, + 0x09, 0x33, 0x09, 0x34, 0x15, 0x00, 0x26, 0xff, 0x00, 0x75, 0x08, 0x95, 0x06, 0x81, 0x02, 0x06, + 0x00, 0xff, 0x09, 0x20, 0x95, 0x01, 0x81, 0x02, 0x05, 0x01, 0x09, 0x39, 0x15, 0x00, 0x25, 0x07, + 0x35, 0x00, 0x46, 0x3b, 0x01, 0x65, 0x14, 0x75, 0x04, 0x95, 0x01, 0x81, 0x42, 0x65, 0x00, 0x05, + 0x09, 0x19, 0x01, 0x29, 0x0f, 0x15, 0x00, 0x25, 0x01, 0x75, 0x01, 0x95, 0x0f, 0x81, 0x02, 0x06, + 0x00, 0xff, 0x09, 0x21, 0x95, 0x0d, 0x81, 0x02, 0x06, 0x00, 0xff, 0x09, 0x22, 0x15, 0x00, 0x26, + 0xff, 0x00, 0x75, 0x08, 0x95, 0x34, 0x81, 0x02, 0x85, 0x02, 0x09, 0x23, 0x95, 0x2f, 0x91, 0x02, + 0x85, 0x05, 0x09, 0x33, 0x95, 0x28, 0xb1, 0x02, 0x85, 0x08, 0x09, 0x34, 0x95, 0x2f, 0xb1, 0x02, + 0x85, 0x09, 0x09, 0x24, 0x95, 0x13, 0xb1, 0x02, 0x85, 0x0a, 0x09, 0x25, 0x95, 0x1a, 0xb1, 0x02, + 0x85, 0x20, 0x09, 0x26, 0x95, 0x3f, 0xb1, 0x02, 0x85, 0x21, 0x09, 0x27, 0x95, 0x04, 0xb1, 0x02, + 0x85, 0x22, 0x09, 0x40, 0x95, 0x3f, 0xb1, 0x02, 0x85, 0x80, 0x09, 0x28, 0x95, 0x3f, 0xb1, 0x02, + 0x85, 0x81, 0x09, 0x29, 0x95, 0x3f, 0xb1, 0x02, 0x85, 0x82, 0x09, 0x2a, 0x95, 0x09, 0xb1, 0x02, + 0x85, 0x83, 0x09, 0x2b, 0x95, 0x3f, 0xb1, 0x02, 0x85, 0x84, 0x09, 0x2c, 0x95, 0x3f, 0xb1, 0x02, + 0x85, 0x85, 0x09, 0x2d, 0x95, 0x02, 0xb1, 0x02, 0x85, 0xa0, 0x09, 0x2e, 0x95, 0x01, 0xb1, 0x02, + 0x85, 0xe0, 0x09, 0x2f, 0x95, 0x3f, 0xb1, 0x02, 0x85, 0xf0, 0x09, 0x30, 0x95, 0x3f, 0xb1, 0x02, + 0x85, 0xf1, 0x09, 0x31, 0x95, 0x3f, 0xb1, 0x02, 0x85, 0xf2, 0x09, 0x32, 0x95, 0x0f, 0xb1, 0x02, + 0x85, 0xf4, 0x09, 0x35, 0x95, 0x3f, 0xb1, 0x02, 0x85, 0xf5, 0x09, 0x36, 0x95, 0x03, 0xb1, 0x02, + 0xc0, + }; + + static_assert(sizeof(REPORT_DESCRIPTORS) == 273, "DualSense USB report descriptor length"); + +} // namespace PS5Usb + +#endif diff --git a/Firmware/RP2040/src/Descriptors/Steam.h b/Firmware/RP2040/src/Descriptors/Steam.h new file mode 100644 index 00000000..94f37656 --- /dev/null +++ b/Firmware/RP2040/src/Descriptors/Steam.h @@ -0,0 +1,47 @@ +#ifndef _STEAM_DESCRIPTORS_H_ +#define _STEAM_DESCRIPTORS_H_ + +#include + +#include "Descriptors/PS5Usb.h" + +/* + * SteamOS / Bazzite: DualSense gamepad (interface 0) + relative HID mouse (interface 1) + * for touchpad passthrough — same layout as ControllerMedic PS5MedicUsb. + */ +namespace Steam +{ + static constexpr uint8_t ITF_GAMEPAD = 0; + static constexpr uint8_t ITF_MOUSE = 1; + + static const uint8_t MOUSE_REPORT_DESCRIPTORS[] = + { + 0x05, 0x01, 0x09, 0x02, 0xA1, 0x01, 0x09, 0x01, 0xA1, 0x00, + 0x05, 0x09, 0x19, 0x01, 0x29, 0x03, 0x15, 0x00, 0x25, 0x01, + 0x95, 0x03, 0x75, 0x01, 0x81, 0x02, 0x95, 0x01, 0x75, 0x05, + 0x81, 0x03, 0x05, 0x01, 0x09, 0x30, 0x09, 0x31, 0x15, 0x81, + 0x25, 0x7F, 0x75, 0x08, 0x95, 0x02, 0x81, 0x06, 0xC0, 0xC0, + }; + + static_assert(sizeof(MOUSE_REPORT_DESCRIPTORS) == 50, "Steam mouse HID descriptor length"); + + /* DualSense gamepad (IF0, 273-byte report) + mouse (IF1, EP 0x85 IN). wTotalLength = 0x42. */ + static const uint8_t CONFIGURATION_DESCRIPTORS[] = + { + 0x09, 0x02, 0x42, 0x00, 0x02, 0x01, 0x00, 0xA0, 0xFA, + + 0x09, 0x04, 0x00, 0x00, 0x02, 0x03, 0x00, 0x00, 0x00, + 0x09, 0x21, 0x11, 0x01, 0x00, 0x01, 0x22, 0x11, 0x01, + 0x07, 0x05, 0x84, 0x03, 0x40, 0x00, 0x06, + 0x07, 0x05, 0x03, 0x03, 0x40, 0x00, 0x06, + + 0x09, 0x04, 0x01, 0x00, 0x01, 0x03, 0x00, 0x00, 0x00, + 0x09, 0x21, 0x11, 0x01, 0x00, 0x01, 0x22, 0x32, 0x00, + 0x07, 0x05, 0x85, 0x03, 0x04, 0x00, 0x0A, + }; + + static_assert(sizeof(CONFIGURATION_DESCRIPTORS) == 0x42, "Steam configuration descriptor length"); + +} // namespace Steam + +#endif diff --git a/Firmware/RP2040/src/Descriptors/SwitchPro.h b/Firmware/RP2040/src/Descriptors/SwitchPro.h index 6b209efd..2e79e04a 100644 --- a/Firmware/RP2040/src/Descriptors/SwitchPro.h +++ b/Firmware/RP2040/src/Descriptors/SwitchPro.h @@ -1,270 +1,149 @@ -#ifndef SWITCH_PRO_DESCRIPTORS_H_ -#define SWITCH_PRO_DESCRIPTORS_H_ +#ifndef _SWITCH_PRO_DESCRIPTORS_H_ +#define _SWITCH_PRO_DESCRIPTORS_H_ #include -namespace SwitchPro +/** + * Shared Switch Pro Controller types for both USB Host (parsing a real Pro) + * and USB Device (emulating a Pro). Host uses InReport, OutReport, CMD, Buttons*. + * Device uses the same InReport (as SwitchReport), Btn, HAT_*, and descriptors from SwitchProDevice.h. + */ +namespace SwitchPro { - static constexpr uint8_t INFO_CONN_MASK = 0xAB; - static constexpr uint8_t INFO_BATTERY_MASK = 0x0F; - - namespace CMD - { - static constexpr uint8_t HID = 0x80; - static constexpr uint8_t RUMBLE_ONLY = 0x10; - static constexpr uint8_t AND_RUMBLE = 0x01; - static constexpr uint8_t LED = 0x30; - static constexpr uint8_t LED_HOME = 0x38; - static constexpr uint8_t GYRO = 0x40; - static constexpr uint8_t MODE = 0x03; - static constexpr uint8_t FULL_REPORT_MODE = 0x30; - static constexpr uint8_t HANDSHAKE = 0x02; - static constexpr uint8_t DISABLE_TIMEOUT = 0x04; - } - - namespace Buttons0 - { - static constexpr uint8_t Y = 0x01; - static constexpr uint8_t X = 0x02; - static constexpr uint8_t B = 0x04; - static constexpr uint8_t A = 0x08; - static constexpr uint8_t R = 0x40; - static constexpr uint8_t ZR = 0x80; - }; - - namespace Buttons1 - { - static constexpr uint8_t MINUS = 0x01; - static constexpr uint8_t PLUS = 0x02; - static constexpr uint8_t L3 = 0x04; - static constexpr uint8_t R3 = 0x08; - static constexpr uint8_t HOME = 0x10; - static constexpr uint8_t CAPTURE = 0x20; - }; - - namespace Buttons2 - { - static constexpr uint8_t DPAD_DOWN = 0x01; - static constexpr uint8_t DPAD_UP = 0x02; - static constexpr uint8_t DPAD_RIGHT = 0x04; - static constexpr uint8_t DPAD_LEFT = 0x08; - static constexpr uint8_t L = 0x40; - static constexpr uint8_t ZL = 0x80; - }; - - #pragma pack(push, 1) - struct InReport - { - uint8_t report_id; - uint8_t timer; - - // uint8_t connInfo : 4; - // uint8_t battery : 4; - uint8_t info; - - uint8_t buttons[3]; - - // uint16_t leftX : 12; - // uint16_t leftY : 12; - // uint16_t rightX : 12; - // uint16_t rightY : 12; - uint8_t joysticks[6]; - - uint8_t vibrator; - - uint16_t accelerX; - uint16_t accelerY; - uint16_t accelerZ; - - uint16_t velocityX; - uint16_t velocityY; - uint16_t velocityZ; - }; - static_assert(sizeof(InReport) == 25, "InReport size is not correct"); - - struct OutReport - { - uint8_t command; - uint8_t sequence_counter; - uint8_t rumble_l[4]; - uint8_t rumble_r[4]; - uint8_t sub_command; - uint8_t sub_command_args[3]; - }; - static_assert(sizeof(OutReport) == 14, "OutReport size is not correct"); - #pragma pack(pop) - - -// static const uint8_t switch_pro_string_language[] = { 0x09, 0x04 }; -// static const uint8_t switch_pro_string_manufacturer[] = "Nintnedo Co., Ltd."; -// static const uint8_t switch_pro_string_product[] = "Pro Controller"; -// static const uint8_t switch_pro_string_version[] = "000000000001"; - -// static const uint8_t *switch_pro_string_descriptors[] __attribute__((unused)) = -// { -// switch_pro_string_language, -// switch_pro_string_product, // switch these? -// switch_pro_string_manufacturer, // ? -// switch_pro_string_version -// }; - -// static const uint8_t switch_pro_device_descriptor[] = -// { -// 0x12, // bLength -// 0x01, // bDescriptorType (Device) -// 0x00, 0x02, // bcdUSB 2.00 -// 0x00, // bDeviceClass (Use class information in the Interface Descriptors) -// 0x00, // bDeviceSubClass -// 0x00, // bDeviceProtocol -// 0x40, // bMaxPacketSize0 64 -// 0x7E, 0x05, // idVendor 0x057E -// 0x09, 0x20, // idProduct 0x2009 -// 0x00, 0x02, // bcdDevice 4.00 -// 0x01, // iManufacturer (String Index) -// 0x02, // iProduct (String Index) -// 0x03, // iSerialNumber (String Index) -// 0x01, // bNumConfigurations 1 -// }; - -// static const uint8_t switch_pro_configuration_descriptor[] = -// { -// 0x09, // bLength -// 0x02, // bDescriptorType (Configuration) -// 0x29, 0x00, // wTotalLength 41 -// 0x01, // bNumInterfaces 1 -// 0x01, // bConfigurationValue -// 0x00, // iConfiguration (String Index) -// 0xA0, // bmAttributes Remote Wakeup -// 0xFA, // bMaxPower 500mA - -// 0x09, // bLength -// 0x04, // bDescriptorType (Interface) -// 0x00, // bInterfaceNumber 0 -// 0x00, // bAlternateSetting -// 0x02, // bNumEndpoints 2 -// 0x03, // bInterfaceClass -// 0x00, // bInterfaceSubClass -// 0x00, // bInterfaceProtocol -// 0x00, // iInterface (String Index) - -// 0x09, // bLength -// 0x21, // bDescriptorType (HID) -// 0x11, 0x01, // bcdHID 1.11 -// 0x00, // bCountryCode -// 0x01, // bNumDescriptors -// 0x22, // bDescriptorType[0] (HID) -// 0xCB, 0x00, // wDescriptorLength[0] 203 - -// 0x07, // bLength -// 0x05, // bDescriptorType (Endpoint) -// 0x81, // bEndpointAddress (IN/D2H) -// 0x03, // bmAttributes (Interrupt) -// 0x40, 0x00, // wMaxPacketSize 64 -// 0x08, // bInterval 8 (unit depends on device speed) - -// 0x07, // bLength -// 0x05, // bDescriptorType (Endpoint) -// 0x01, // bEndpointAddress (OUT/H2D) -// 0x03, // bmAttributes (Interrupt) -// 0x40, 0x00, // wMaxPacketSize 64 -// 0x08, // bInterval 8 (unit depends on device speed) -// }; - -// static const uint8_t switch_pro_report_descriptor[] = -// { -// 0x05, 0x01, // Usage Page (Generic Desktop Ctrls) -// 0x15, 0x00, // Logical Minimum (0) -// 0x09, 0x04, // Usage (Joystick) -// 0xA1, 0x01, // Collection (Application) -// 0x85, 0x30, // Report ID (48) -// 0x05, 0x01, // Usage Page (Generic Desktop Ctrls) -// 0x05, 0x09, // Usage Page (Button) -// 0x19, 0x01, // Usage Minimum (0x01) -// 0x29, 0x0A, // Usage Maximum (0x0A) -// 0x15, 0x00, // Logical Minimum (0) -// 0x25, 0x01, // Logical Maximum (1) -// 0x75, 0x01, // Report Size (1) -// 0x95, 0x0A, // Report Count (10) -// 0x55, 0x00, // Unit Exponent (0) -// 0x65, 0x00, // Unit (None) -// 0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) -// 0x05, 0x09, // Usage Page (Button) -// 0x19, 0x0B, // Usage Minimum (0x0B) -// 0x29, 0x0E, // Usage Maximum (0x0E) -// 0x15, 0x00, // Logical Minimum (0) -// 0x25, 0x01, // Logical Maximum (1) -// 0x75, 0x01, // Report Size (1) -// 0x95, 0x04, // Report Count (4) -// 0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) -// 0x75, 0x01, // Report Size (1) -// 0x95, 0x02, // Report Count (2) -// 0x81, 0x03, // Input (Const,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) -// 0x0B, 0x01, 0x00, 0x01, 0x00, // Usage (0x010001) -// 0xA1, 0x00, // Collection (Physical) -// 0x0B, 0x30, 0x00, 0x01, 0x00, // Usage (0x010030) -// 0x0B, 0x31, 0x00, 0x01, 0x00, // Usage (0x010031) -// 0x0B, 0x32, 0x00, 0x01, 0x00, // Usage (0x010032) -// 0x0B, 0x35, 0x00, 0x01, 0x00, // Usage (0x010035) -// 0x15, 0x00, // Logical Minimum (0) -// 0x27, 0xFF, 0xFF, 0x00, 0x00, // Logical Maximum (65534) -// 0x75, 0x10, // Report Size (16) -// 0x95, 0x04, // Report Count (4) -// 0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) -// 0xC0, // End Collection -// 0x0B, 0x39, 0x00, 0x01, 0x00, // Usage (0x010039) -// 0x15, 0x00, // Logical Minimum (0) -// 0x25, 0x07, // Logical Maximum (7) -// 0x35, 0x00, // Physical Minimum (0) -// 0x46, 0x3B, 0x01, // Physical Maximum (315) -// 0x65, 0x14, // Unit (System: English Rotation, Length: Centimeter) -// 0x75, 0x04, // Report Size (4) -// 0x95, 0x01, // Report Count (1) -// 0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) -// 0x05, 0x09, // Usage Page (Button) -// 0x19, 0x0F, // Usage Minimum (0x0F) -// 0x29, 0x12, // Usage Maximum (0x12) -// 0x15, 0x00, // Logical Minimum (0) -// 0x25, 0x01, // Logical Maximum (1) -// 0x75, 0x01, // Report Size (1) -// 0x95, 0x04, // Report Count (4) -// 0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) -// 0x75, 0x08, // Report Size (8) -// 0x95, 0x34, // Report Count (52) -// 0x81, 0x03, // Input (Const,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) -// 0x06, 0x00, 0xFF, // Usage Page (Vendor Defined 0xFF00) -// 0x85, 0x21, // Report ID (33) -// 0x09, 0x01, // Usage (0x01) -// 0x75, 0x08, // Report Size (8) -// 0x95, 0x3F, // Report Count (63) -// 0x81, 0x03, // Input (Const,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) -// 0x85, 0x81, // Report ID (-127) -// 0x09, 0x02, // Usage (0x02) -// 0x75, 0x08, // Report Size (8) -// 0x95, 0x3F, // Report Count (63) -// 0x81, 0x03, // Input (Const,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) -// 0x85, 0x01, // Report ID (1) -// 0x09, 0x03, // Usage (0x03) -// 0x75, 0x08, // Report Size (8) -// 0x95, 0x3F, // Report Count (63) -// 0x91, 0x83, // Output (Const,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Volatile) -// 0x85, 0x10, // Report ID (16) -// 0x09, 0x04, // Usage (0x04) -// 0x75, 0x08, // Report Size (8) -// 0x95, 0x3F, // Report Count (63) -// 0x91, 0x83, // Output (Const,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Volatile) -// 0x85, 0x80, // Report ID (-128) -// 0x09, 0x05, // Usage (0x05) -// 0x75, 0x08, // Report Size (8) -// 0x95, 0x3F, // Report Count (63) -// 0x91, 0x83, // Output (Const,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Volatile) -// 0x85, 0x82, // Report ID (-126) -// 0x09, 0x06, // Usage (0x06) -// 0x75, 0x08, // Report Size (8) -// 0x95, 0x3F, // Report Count (63) -// 0x91, 0x83, // Output (Const,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Volatile) -// 0xC0, // End Collection -// }; - -}; // namespace SwitchPro - -#endif // SWITCH_PRO_DESCRIPTORS_H_ \ No newline at end of file +#pragma pack(push, 1) + struct InReport + { + uint8_t batteryConnection; + uint8_t buttons[3]; + union + { + uint8_t joysticks[6]; + struct { uint8_t l[3]; uint8_t r[3]; }; + }; + }; + static_assert(sizeof(InReport) == 10, "InReport must be 10 bytes"); + + struct OutReport + { + uint8_t command; + uint8_t sequence_counter; + uint8_t rumble_l[4]; + uint8_t rumble_r[4]; + uint8_t sub_command; + uint8_t sub_command_args[5]; + }; +#pragma pack(pop) + + namespace CMD + { + static constexpr uint8_t RUMBLE_ONLY = 0x10; + static constexpr uint8_t HID = 0x11; + static constexpr uint8_t HANDSHAKE = 0x00; + static constexpr uint8_t DISABLE_TIMEOUT = 0x01; + static constexpr uint8_t AND_RUMBLE = 0x12; + static constexpr uint8_t LED = 0x30; + static constexpr uint8_t LED_HOME = 0x38; + static constexpr uint8_t MODE = 0x03; + static constexpr uint8_t FULL_REPORT_MODE = 0x30; + static constexpr uint8_t GYRO = 0x40; + /** Unrecognized subcommand; sent after 0x80 0x04 so init gets a 0x21 ack. */ + static constexpr uint8_t USB_PROBE = 0x33; + /** Wired USB output report ID and subcommands (not Bluetooth 0x11 path). */ + static constexpr uint8_t USB_OUT = 0x80; + static constexpr uint8_t USB_SUB_MAC = 0x01; + static constexpr uint8_t USB_SUB_HANDSHAKE = 0x02; + static constexpr uint8_t USB_SUB_BAUD = 0x03; + static constexpr uint8_t USB_SUB_DISABLE_TIMEOUT = 0x04; + } + + // First report byte (buttons_right): bit layout per real hardware / Bluepad32 parse_report_30_pro_controller + namespace Buttons0 + { + static constexpr uint8_t Y = (1U << 0); + static constexpr uint8_t X = (1U << 1); + static constexpr uint8_t B = (1U << 2); + static constexpr uint8_t A = (1U << 3); + static constexpr uint8_t R = (1U << 6); + static constexpr uint8_t ZR = (1U << 7); + } + namespace Buttons1 + { + static constexpr uint8_t MINUS = (1U << 0); + static constexpr uint8_t PLUS = (1U << 1); + static constexpr uint8_t L3 = (1U << 2); + static constexpr uint8_t R3 = (1U << 3); + static constexpr uint8_t HOME = (1U << 4); + static constexpr uint8_t CAPTURE = (1U << 5); + } + namespace Buttons2 + { + static constexpr uint8_t DPAD_UP = (1U << 0); + static constexpr uint8_t DPAD_DOWN = (1U << 1); + static constexpr uint8_t DPAD_LEFT = (1U << 2); + static constexpr uint8_t DPAD_RIGHT= (1U << 3); + static constexpr uint8_t L = (1U << 6); + static constexpr uint8_t ZL = (1U << 7); + } + + using SwitchReport = InReport; + + namespace Btn + { + static constexpr uint8_t Y = Buttons0::Y; + static constexpr uint8_t B = Buttons0::B; + static constexpr uint8_t A = Buttons0::A; + static constexpr uint8_t X = Buttons0::X; + static constexpr uint8_t R = Buttons0::R; + static constexpr uint8_t ZR = Buttons0::ZR; + static constexpr uint8_t MINUS = Buttons1::MINUS; + static constexpr uint8_t PLUS = Buttons1::PLUS; + static constexpr uint8_t L3 = Buttons1::L3; + static constexpr uint8_t R3 = Buttons1::R3; + static constexpr uint8_t HOME = Buttons1::HOME; + static constexpr uint8_t CAPTURE = Buttons1::CAPTURE; + static constexpr uint8_t L = Buttons2::L; + static constexpr uint8_t ZL = Buttons2::ZL; + } + + static constexpr uint8_t HAT_NEUTRAL = 0x00; + static constexpr uint8_t HAT_DOWN = 0x01; + static constexpr uint8_t HAT_UP = 0x02; + static constexpr uint8_t HAT_RIGHT = 0x04; + static constexpr uint8_t HAT_LEFT = 0x08; + static constexpr uint8_t HAT_DOWN_RIGHT = 0x05; + static constexpr uint8_t HAT_DOWN_LEFT = 0x09; + static constexpr uint8_t HAT_UP_RIGHT = 0x06; + static constexpr uint8_t HAT_UP_LEFT = 0x0A; + + static constexpr uint16_t STICK_MIN = 0x000; + static constexpr uint16_t STICK_MID = 0x7FF; + static constexpr uint16_t STICK_MAX = 0xFFF; + + static constexpr uint8_t REPORT_ID_OUTPUT_SUBCMD = 0x01; + static constexpr uint8_t REPORT_ID_OUTPUT_RUMBLE = 0x10; + static constexpr uint8_t REPORT_ID_STANDARD = 0x30; + /** Switch 2 Pro (USB) full input reports use 0x09; layout matches 0x30 after id + timer. */ + static constexpr uint8_t REPORT_ID_SWITCH2_FULL = 0x09; + /** Alternate full-report id seen on some Switch-family USB stacks. */ + static constexpr uint8_t REPORT_ID_FULL_ALT = 0x31; + static constexpr uint8_t REPORT_ID_SUBCMD = 0x21; + static constexpr uint8_t REPORT_ID_USB_INIT = 0x81; + static constexpr uint8_t REPORT_ID_USB_OUT = 0x80; + static constexpr int REPORT_SIZE = 64; + static constexpr int USB_INIT_REPORT_SIZE = 64; + + static constexpr uint8_t SUBCMD_BT_PAIR = 0x01; + static constexpr uint8_t SUBCMD_DEVICE_INFO = 0x02; + static constexpr uint8_t SUBCMD_SET_MODE = 0x03; + static constexpr uint8_t SUBCMD_TRIGGER = 0x04; + static constexpr uint8_t SUBCMD_SHIPMENT = 0x08; + static constexpr uint8_t SUBCMD_SPI_READ = 0x10; + static constexpr uint8_t SUBCMD_NFC_IR_CFG = 0x21; + static constexpr uint8_t SUBCMD_NFC_IR_STATE= 0x22; + static constexpr uint8_t SUBCMD_SET_PLAYER = 0x30; + static constexpr uint8_t SUBCMD_IMU = 0x40; + static constexpr uint8_t SUBCMD_IMU_SENS = 0x41; + static constexpr uint8_t SUBCMD_VIBRATION = 0x48; +} + +#endif diff --git a/Firmware/RP2040/src/Descriptors/SwitchProDevice.h b/Firmware/RP2040/src/Descriptors/SwitchProDevice.h new file mode 100644 index 00000000..6b039fb6 --- /dev/null +++ b/Firmware/RP2040/src/Descriptors/SwitchProDevice.h @@ -0,0 +1,55 @@ +#ifndef _SWITCH_PRO_DEVICE_DESCRIPTORS_H_ +#define _SWITCH_PRO_DEVICE_DESCRIPTORS_H_ + +#include "Descriptors/SwitchPro.h" + +/** + * Nintendo Switch Pro Controller USB device descriptors (emulation). + * VID 0x057E, PID 0x2009. Report ID 0x30, 64-byte reports. + */ +namespace SwitchPro +{ + static const uint8_t STRING_LANGUAGE[] = { 0x09, 0x04 }; + static const char STRING_MANUFACTURER[] = "Nintendo Co., Ltd."; + static const char STRING_PRODUCT[] = "Pro Controller"; + static const char STRING_VERSION[] = "000000000001"; + + static const uint8_t DEVICE_DESCRIPTOR[] = + { + 0x12, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00, 0x40, + 0x7E, 0x05, 0x09, 0x20, 0x10, 0x02, 0x01, 0x02, 0x03, 0x01 + }; + + static const uint8_t CONFIGURATION_DESCRIPTOR[] = + { + 0x09, 0x02, 0x29, 0x00, 0x01, 0x01, 0x00, 0xA0, 0xFA, + 0x09, 0x04, 0x00, 0x00, 0x02, 0x03, 0x00, 0x00, 0x00, + 0x09, 0x21, 0x11, 0x01, 0x00, 0x01, 0x22, 0xCB, 0x00, + 0x07, 0x05, 0x81, 0x03, 0x40, 0x00, 0x08, + 0x07, 0x05, 0x01, 0x03, 0x40, 0x00, 0x08 + }; + + static const uint8_t REPORT_DESCRIPTOR[] = + { + 0x05, 0x01, 0x15, 0x00, 0x09, 0x04, 0xA1, 0x01, + 0x85, 0x30, 0x05, 0x01, 0x05, 0x09, 0x19, 0x01, 0x29, 0x0A, 0x15, 0x00, 0x25, 0x01, 0x75, 0x01, 0x95, 0x0A, 0x55, 0x00, 0x65, 0x00, 0x81, 0x02, + 0x05, 0x09, 0x19, 0x0B, 0x29, 0x0E, 0x15, 0x00, 0x25, 0x01, 0x75, 0x01, 0x95, 0x04, 0x81, 0x02, + 0x75, 0x01, 0x95, 0x02, 0x81, 0x03, + 0x0B, 0x01, 0x00, 0x01, 0x00, 0xA1, 0x00, + 0x0B, 0x30, 0x00, 0x01, 0x00, 0x0B, 0x31, 0x00, 0x01, 0x00, 0x0B, 0x32, 0x00, 0x01, 0x00, 0x0B, 0x35, 0x00, 0x01, 0x00, + 0x15, 0x00, 0x27, 0xFF, 0xFF, 0x00, 0x00, 0x75, 0x10, 0x95, 0x04, 0x81, 0x02, + 0xC0, + 0x0B, 0x39, 0x00, 0x01, 0x00, 0x15, 0x00, 0x25, 0x07, 0x35, 0x00, 0x46, 0x3B, 0x01, 0x65, 0x14, 0x75, 0x04, 0x95, 0x01, 0x81, 0x02, + 0x05, 0x09, 0x19, 0x0F, 0x29, 0x12, 0x15, 0x00, 0x25, 0x01, 0x75, 0x01, 0x95, 0x04, 0x81, 0x02, + 0x75, 0x08, 0x95, 0x34, 0x81, 0x03, + 0x06, 0x00, 0xFF, 0x85, 0x21, 0x09, 0x01, 0x75, 0x08, 0x95, 0x3F, 0x81, 0x03, + 0x85, 0x81, 0x09, 0x02, 0x75, 0x08, 0x95, 0x3F, 0x81, 0x03, + 0x85, 0x01, 0x09, 0x03, 0x75, 0x08, 0x95, 0x3F, 0x91, 0x83, + 0x85, 0x10, 0x09, 0x04, 0x75, 0x08, 0x95, 0x3F, 0x91, 0x83, + 0x85, 0x80, 0x09, 0x05, 0x75, 0x08, 0x95, 0x3F, 0x91, 0x83, + 0x85, 0x82, 0x09, 0x06, 0x75, 0x08, 0x95, 0x3F, 0x91, 0x83, + 0xC0 + }; +} + +#endif diff --git a/Firmware/RP2040/src/Descriptors/WiiU.h b/Firmware/RP2040/src/Descriptors/WiiU.h new file mode 100644 index 00000000..5c97fd18 --- /dev/null +++ b/Firmware/RP2040/src/Descriptors/WiiU.h @@ -0,0 +1,168 @@ +#ifndef _WIIU_DESCRIPTORS_H_ +#define _WIIU_DESCRIPTORS_H_ + +#include + +#include "tusb.h" + +namespace WiiU +{ + // Wii U GameCube Adapter protocol (same as Dolphin / gc_adapter_read.py) + static constexpr uint8_t HID_REPORT_ID = 0x21; + static constexpr uint8_t INIT_PAYLOAD = 0x13; + static constexpr uint8_t CONTROLLER_PAYLOAD_SIZE = 37; // report_id + 4 * 9 bytes per port + + // Controller type in first byte of each 9-byte port block + static constexpr uint8_t PORT_WIRED = (1 << 4); + static constexpr uint8_t PORT_WIRELESS = (1 << 5); + + // GameCube button bits (bytes 1 and 2 of each port block) + namespace GCButtons + { + // Byte 1 + static constexpr uint8_t A = (1 << 0); + static constexpr uint8_t B = (1 << 1); + static constexpr uint8_t X = (1 << 2); + static constexpr uint8_t Y = (1 << 3); + static constexpr uint8_t LEFT = (1 << 4); + static constexpr uint8_t RIGHT = (1 << 5); + static constexpr uint8_t DOWN = (1 << 6); + static constexpr uint8_t UP = (1 << 7); + // Byte 2 + static constexpr uint8_t START = (1 << 0); + static constexpr uint8_t Z = (1 << 1); + static constexpr uint8_t R = (1 << 2); + static constexpr uint8_t L = (1 << 3); + } + + static constexpr uint8_t STICK_CENTER = 128; + + #pragma pack(push, 1) + // Full adapter input report: report ID 0x21 + 4 ports × 9 bytes + struct InReport + { + uint8_t report_id{ HID_REPORT_ID }; + uint8_t port_data[4][9]; // [port][type, b1, b2, stickX, stickY, substickX, substickY, triggerL, triggerR] + }; + static_assert(sizeof(InReport) == CONTROLLER_PAYLOAD_SIZE, "WiiU::InReport must be 37 bytes"); + #pragma pack(pop) + + static const uint8_t STRING_LANGUAGE[] = { 0x09, 0x04 }; + static const uint8_t STRING_MANUFACTURER[] = "Nintendo"; + static const uint8_t STRING_PRODUCT[] = "GameCube For Switch"; + static const uint8_t STRING_SERIAL[] = "GH-GC-001 T8"; + static const uint8_t STRING_VERSION[] = "1.0"; + + static const uint8_t *STRING_DESCRIPTORS[] __attribute__((unused)) = + { + STRING_LANGUAGE, + STRING_MANUFACTURER, + STRING_PRODUCT, + STRING_SERIAL, + STRING_VERSION + }; + + // Nintendo Wii U GameCube Adapter + static const uint8_t DEVICE_DESCRIPTORS[] = + { + 0x12, // bLength + 0x01, // bDescriptorType (Device) + 0x00, 0x02, // bcdUSB 2.00 + 0x00, // bDeviceClass (Use class information in the Interface Descriptors) + 0x00, // bDeviceSubClass + 0x00, // bDeviceProtocol + 0x40, // bMaxPacketSize0 64 + 0x7E, 0x05, // idVendor 0x057E (Nintendo) + 0x37, 0x03, // idProduct 0x0337 (Wii U GameCube Adapter) + 0x00, 0x01, // bcdDevice 1.00 + 0x01, // iManufacturer (String Index) + 0x02, // iProduct (String Index) + 0x03, // iSerialNumber (String Index) + 0x01, // bNumConfigurations 1 + }; + + // HID report descriptor (exact dump from real Nintendo Wii U GameCube Adapter 057e:0337) + static const uint8_t REPORT_DESCRIPTORS[] = + { + 0x05, 0x05, 0x09, 0x00, 0xA1, 0x01, 0x85, 0x11, 0x19, 0x00, 0x2A, 0xFF, 0x00, 0x15, 0x00, 0x26, + 0xFF, 0x00, 0x75, 0x08, 0x95, 0x05, 0x91, 0x00, 0xC0, 0xA1, 0x01, 0x85, 0x21, 0x19, 0x00, 0x2A, + 0xFF, 0x00, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x25, 0x81, 0x00, 0xC0, 0xA1, 0x01, + 0x85, 0x12, 0x19, 0x00, 0x2A, 0xFF, 0x00, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x01, + 0x91, 0x00, 0xC0, 0xA1, 0x01, 0x85, 0x22, 0x19, 0x00, 0x2A, 0xFF, 0x00, 0x15, 0x00, 0x26, 0xFF, + 0x00, 0x75, 0x08, 0x95, 0x19, 0x81, 0x00, 0xC0, 0xA1, 0x01, 0x85, 0x13, 0x19, 0x00, 0x2A, 0xFF, + 0x00, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x01, 0x91, 0x00, 0xC0, 0xA1, 0x01, 0x85, + 0x23, 0x19, 0x00, 0x2A, 0xFF, 0x00, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x02, 0x81, + 0x00, 0xC0, 0xA1, 0x01, 0x85, 0x14, 0x19, 0x00, 0x2A, 0xFF, 0x00, 0x15, 0x00, 0x26, 0xFF, 0x00, + 0x75, 0x08, 0x95, 0x01, 0x91, 0x00, 0xC0, 0xA1, 0x01, 0x85, 0x24, 0x19, 0x00, 0x2A, 0xFF, 0x00, + 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x02, 0x81, 0x00, 0xC0, 0xA1, 0x01, 0x85, 0x15, + 0x19, 0x00, 0x2A, 0xFF, 0x00, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x01, 0x91, 0x00, + 0xC0, 0xA1, 0x01, 0x85, 0x25, 0x19, 0x00, 0x2A, 0xFF, 0x00, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, + 0x08, 0x95, 0x02, 0x81, 0x00, 0xC0, + }; + static_assert(sizeof(REPORT_DESCRIPTORS) == 214, "WiiU report descriptor must be 214 bytes"); + + // HID class descriptor (matches real adapter: bcdHID 1.10, report len 214) + static const uint8_t HID_DESCRIPTORS[] = + { + 0x09, // bLength + 0x21, // bDescriptorType (HID) + 0x10, 0x01, // bcdHID 1.10 + 0x00, // bCountryCode + 0x01, // bNumDescriptors + 0x22, // bDescriptorType[0] (Report) + 0xD6, 0x00, // wDescriptorLength 214 + }; + + // Raw configuration descriptor (matches real adapter: IN 37 bytes, OUT 5 bytes, bInterval 8, bmAttributes 0xe0) + static const uint8_t CONFIGURATION_DESCRIPTORS[] = + { + // Configuration descriptor + 0x09, // bLength + 0x02, // bDescriptorType (Configuration) + 0x29, 0x00, // wTotalLength 41 + 0x01, // bNumInterfaces 1 + 0x01, // bConfigurationValue 1 + 0x00, // iConfiguration (String Index) + 0xE0, // bmAttributes (Self Powered | Remote Wakeup) + 0xFA, // bMaxPower 500mA (0xFA * 2mA) + + // Interface descriptor (HID) + 0x09, // bLength + 0x04, // bDescriptorType (Interface) + 0x00, // bInterfaceNumber 0 + 0x00, // bAlternateSetting 0 + 0x02, // bNumEndpoints 2 + 0x03, // bInterfaceClass (HID) + 0x00, // bInterfaceSubClass + 0x00, // bInterfaceProtocol + 0x00, // iInterface (String Index) + + // HID descriptor + 0x09, // bLength + 0x21, // bDescriptorType (HID) + 0x10, 0x01, // bcdHID 1.10 + 0x00, // bCountryCode + 0x01, // bNumDescriptors + 0x22, // bDescriptorType[0] (Report) + 0xD6, 0x00, // wDescriptorLength 214 + + // Endpoint descriptor (OUT 0x02, 5 bytes, interval 8) + 0x07, // bLength + 0x05, // bDescriptorType (Endpoint) + 0x02, // bEndpointAddress (OUT) + 0x03, // bmAttributes (Interrupt) + 0x05, 0x00, // wMaxPacketSize 5 + 0x08, // bInterval 8 + + // Endpoint descriptor (IN 0x81, 37 bytes, interval 8) + 0x07, // bLength + 0x05, // bDescriptorType (Endpoint) + 0x81, // bEndpointAddress (IN) + 0x03, // bmAttributes (Interrupt) + 0x25, 0x00, // wMaxPacketSize 37 + 0x08, // bInterval 8 + }; + +}; // namespace WiiU + +#endif // _WIIU_DESCRIPTORS_H_ diff --git a/Firmware/RP2040/src/Descriptors/XInput.h b/Firmware/RP2040/src/Descriptors/XInput.h index 4f72b306..38334fd2 100644 --- a/Firmware/RP2040/src/Descriptors/XInput.h +++ b/Firmware/RP2040/src/Descriptors/XInput.h @@ -170,86 +170,85 @@ namespace XInput static_assert(sizeof(OutReport) == 8, "XInput::OutReport is misaligned"); #pragma pack(pop) + // String descriptors — from joypad-os xinput_descriptors.h static const uint8_t STRING_LANGUAGE[] = { 0x09, 0x04 }; - static const uint8_t STRING_MANUFACTURER[] = "Microsoft"; - static const uint8_t STRING_PRODUCT[] = "XInput STANDARD GAMEPAD"; + static const uint8_t STRING_MANUFACTURER[] = "\xa9Microsoft Corporation"; + static const uint8_t STRING_PRODUCT[] = "Xbox 360 Controller"; static const uint8_t STRING_VERSION[] = "1.0"; + // XSM3 security string (iInterface=4), joypad-os: XINPUT_SECURITY_STRING + static const uint8_t STRING_XSM3[] = "Xbox Security Method 3, Version 1.00, \xa9 2005 Microsoft Corporation. All rights reserved."; static const uint8_t *DESC_STRING[] __attribute__((unused)) = { STRING_LANGUAGE, STRING_MANUFACTURER, STRING_PRODUCT, - STRING_VERSION + STRING_VERSION, + STRING_XSM3, }; + // Device descriptor — from joypad-os (XINPUT_VID 0x045E, XINPUT_PID 0x028E, XINPUT_BCD_DEVICE 0x0114) static const uint8_t DESC_DEVICE[] = { 0x12, // bLength 0x01, // bDescriptorType (Device) 0x00, 0x02, // bcdUSB 2.00 - 0xFF, // bDeviceClass - 0xFF, // bDeviceSubClass - 0xFF, // bDeviceProtocol - 0x40, // bMaxPacketSize0 64 - 0x5E, 0x04, // idVendor 0x045E - 0x8E, 0x02, // idProduct 0x028E - 0x14, 0x01, // bcdDevice 2.14 + 0xFF, // bDeviceClass + 0xFF, // bDeviceSubClass + 0xFF, // bDeviceProtocol + 0x40, // bMaxPacketSize0 64 + 0x5E, 0x04, // idVendor 0x045E (Microsoft) + 0x8E, 0x02, // idProduct 0x028E (Xbox 360 Controller) + 0x14, 0x01, // bcdDevice 0x0114 (v1.14) 0x01, // iManufacturer (String Index) 0x02, // iProduct (String Index) 0x03, // iSerialNumber (String Index) 0x01, // bNumConfigurations 1 }; + // Configuration descriptor (153 bytes) — from joypad-os xinput_config_descriptor[] + // https://github.com/joypad-ai/joypad-os src/usb/usbd/descriptors/xinput_descriptors.h + static constexpr uint16_t CONFIG_TOTAL_LENGTH = 153; + static const uint8_t DESC_CONFIGURATION[] = { - 0x09, // bLength - 0x02, // bDescriptorType (Configuration) - 0x30, 0x00, // wTotalLength 48 - 0x01, // bNumInterfaces 1 + // Configuration descriptor (9 bytes) + 0x09, 0x02, + 0x99, 0x00, // wTotalLength 153 (LE) + 0x04, // bNumInterfaces 0x01, // bConfigurationValue - 0x00, // iConfiguration (String Index) - 0x80, // bmAttributes - 0xFA, // bMaxPower 500mA - - 0x09, // bLength - 0x04, // bDescriptorType (Interface) - 0x00, // bInterfaceNumber 0 - 0x00, // bAlternateSetting - 0x02, // bNumEndpoints 2 - 0xFF, // bInterfaceClass - 0x5D, // bInterfaceSubClass - 0x01, // bInterfaceProtocol - 0x00, // iInterface (String Index) - - 0x10, // bLength - 0x21, // bDescriptorType (HID) - // 0x10, 0x01, // bcdHID 1.10 - 0x00, 0x01, // bcdHID 1.00 - 0x01, // bCountryCode - 0x24, // bNumDescriptors - 0x81, // bDescriptorType[0] (Unknown 0x81) - 0x14, 0x03, // wDescriptorLength[0] 788 - 0x00, // bDescriptorType[1] (Unknown 0x00) - 0x03, 0x13, // wDescriptorLength[1] 4867 - 0x01, // bDescriptorType[2] (Unknown 0x02) - 0x00, 0x03, // wDescriptorLength[2] 768 - 0x00, // bDescriptorType[3] (Unknown 0x00) - - 0x07, // bLength - 0x05, // bDescriptorType (Endpoint) - 0x81, // bEndpointAddress (IN/D2H) - 0x03, // bmAttributes (Interrupt) - 0x20, 0x00, // wMaxPacketSize 32 - 0x01, // bInterval 1 (unit depends on device speed) - - 0x07, // bLength - 0x05, // bDescriptorType (Endpoint) - 0x01, // bEndpointAddress (OUT/H2D) - 0x03, // bmAttributes (Interrupt) - 0x20, 0x00, // wMaxPacketSize 32 - 0x08, // bInterval 8 (unit depends on device speed) + 0x00, // iConfiguration + 0xA0, // bmAttributes (bus powered, remote wakeup) + 0xFA, // bMaxPower (500mA) + + // ---- Interface 0: Gamepad (SubClass 0x5D, Protocol 0x01) ---- + 0x09, 0x04, 0x00, 0x00, 0x02, 0xFF, 0x5D, 0x01, 0x00, + 0x11, 0x21, 0x00, 0x01, 0x01, 0x25, 0x81, 0x14, + 0x00, 0x00, 0x00, 0x00, 0x13, 0x02, 0x08, 0x00, 0x00, + 0x07, 0x05, 0x81, 0x03, 0x20, 0x00, 0x04, + 0x07, 0x05, 0x02, 0x03, 0x20, 0x00, 0x08, + + // ---- Interface 1: Audio (SubClass 0x5D, Protocol 0x03) ---- + 0x09, 0x04, 0x01, 0x00, 0x04, 0xFF, 0x5D, 0x03, 0x00, + 0x1B, 0x21, 0x00, 0x01, 0x01, 0x01, 0x83, 0x40, + 0x01, 0x04, 0x20, 0x16, 0x85, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x16, 0x06, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x07, 0x05, 0x83, 0x03, 0x20, 0x00, 0x02, + 0x07, 0x05, 0x04, 0x03, 0x20, 0x00, 0x04, + 0x07, 0x05, 0x85, 0x03, 0x20, 0x00, 0x40, + 0x07, 0x05, 0x06, 0x03, 0x20, 0x00, 0x10, + + // ---- Interface 2: Plugin Module (SubClass 0x5D, Protocol 0x02) ---- + 0x09, 0x04, 0x02, 0x00, 0x01, 0xFF, 0x5D, 0x02, 0x00, + 0x09, 0x21, 0x00, 0x01, 0x01, 0x22, 0x86, 0x03, 0x00, + 0x07, 0x05, 0x86, 0x03, 0x20, 0x00, 0x10, + + // ---- Interface 3: Security (SubClass 0xFD, Protocol 0x13), iInterface=4 ---- + 0x09, 0x04, 0x03, 0x00, 0x00, 0xFF, 0xFD, 0x13, 0x04, + 0x06, 0x41, 0x00, 0x01, 0x01, 0x03, }; + static_assert(sizeof(DESC_CONFIGURATION) == CONFIG_TOTAL_LENGTH, "XInput config descriptor size"); }; #endif // _XINPUT_DESCRIPTORS_H_ \ No newline at end of file diff --git a/Firmware/RP2040/src/Descriptors/XboxOne.h b/Firmware/RP2040/src/Descriptors/XboxOne.h index cda84eb7..fe183d65 100644 --- a/Firmware/RP2040/src/Descriptors/XboxOne.h +++ b/Firmware/RP2040/src/Descriptors/XboxOne.h @@ -28,36 +28,321 @@ namespace XboxOne static constexpr uint8_t R3 = (1 << 7); }; + // GIP Command IDs + static constexpr uint8_t GIP_CMD_DEVICE_ARRIVAL = 0x02; + static constexpr uint8_t GIP_CMD_DEVICE_STATUS = 0x03; + static constexpr uint8_t GIP_CMD_DEVICE_DESCRIPTOR = 0x04; + static constexpr uint8_t GIP_CMD_VIRTUAL_KEY = 0x07; // Guide/Home; payload byte 4: 01=pressed, 00=released + static constexpr uint8_t GIP_CMD_SET_DEVICE_STATE = 0x09; + static constexpr uint8_t GIP_CMD_INPUT = 0x20; + static constexpr uint8_t GIP_CMD_SYSTEM_FOCUS_CHANGE = 0xE0; + + // GIP Header Flags + namespace GipFlags + { + static constexpr uint8_t NONE = 0x00; + static constexpr uint8_t INTERNAL = 0x20; // Core protocol message (request from host) + static constexpr uint8_t RESPONSE_FIRST = 0xF0; // First chunk of response (from device) + static constexpr uint8_t RESPONSE_CONTINUE = 0xA0; // Continuation chunk of response (from device) + } + + // GIP Battery Types + namespace BatteryType + { + static constexpr uint8_t WIRED = 0; + static constexpr uint8_t STANDARD = 1; + static constexpr uint8_t CHARGE_KIT = 2; + } + + // GIP Battery Levels + namespace BatteryLevel + { + static constexpr uint8_t LOW = 0; + static constexpr uint8_t MEDIUM = 1; + static constexpr uint8_t HIGH = 2; + static constexpr uint8_t FULL = 3; + } + + // GIP Interface GUIDs (for descriptor) + static constexpr uint8_t GUID_GAMEPAD[16] = { + 0x08, 0x2E, 0x40, 0x2C, 0x07, 0xDF, 0x45, 0xE1, + 0xA5, 0xAB, 0xA3, 0x12, 0x7A, 0xF1, 0x97, 0xB5 + }; + #pragma pack(push, 1) - struct InReport + + // GIP Message Header (8 bytes for USB GIP) + struct GipMessageHeader + { + uint8_t command; + uint8_t client : 4; + uint8_t needsAck : 1; + uint8_t internal : 1; + uint8_t chunkStart : 1; + uint8_t chunked : 1; + uint8_t sequence; + uint8_t length; + }; + static_assert(sizeof(GipMessageHeader) == 4, "GipMessageHeader must be 4 bytes"); + + // Device Arrival Message (GIP CMD 0x02) + struct DeviceArrivalMessage + { + GipMessageHeader header; + uint64_t deviceId; // Unique device identifier + uint16_t vendorId; // 0x045E + uint16_t productId; // 0x02D1 + struct { + uint16_t major; + uint16_t minor; + uint16_t build; + uint16_t revision; + } firmwareVersion; + uint8_t unknown[8]; // Padding to make total 32 bytes + }; + static_assert(sizeof(DeviceArrivalMessage) == 32, "DeviceArrivalMessage must be 32 bytes"); + + // Device Status Message (GIP CMD 0x03) + struct DeviceStatusMessage + { + GipMessageHeader header; + uint8_t batteryLevel : 2; + uint8_t batteryType : 2; + uint8_t unknown : 3; + uint8_t connected : 1; + uint8_t unknown2[3]; + }; + static_assert(sizeof(DeviceStatusMessage) == 8, "DeviceStatusMessage must be 8 bytes"); + + // Device Descriptor Message (GIP CMD 0x04) + // Windows requests this to identify the device type (gamepad, chatpad, etc.) + // According to GIP spec, this is a variable-size message (typically 200-500 bytes) + // It contains a descriptor header with offsets to various metadata blocks + // NOTE: Vendor/product IDs are NOT in the descriptor header - they're only in Device Arrival message + struct DeviceDescriptorHeader + { + uint16_t unknown1; // 0x0293 (real controller value) + uint16_t unknown2; // 0x0010 (real controller value) + uint16_t unknown3; // 0x0001 (real controller value) + uint8_t padding[8]; // 8 bytes of zeros + uint16_t offset_externalCommands; + uint16_t offset_firmwareVersions; + uint16_t offset_audioFormats; + uint16_t offset_inputCommands; + uint16_t offset_outputCommands; + uint16_t offset_classNames; + uint16_t offset_interfaceGuids; + uint16_t offset_hidDescriptor; + uint16_t unknown4; // 0x004C (real controller value) + }; + // Note: Size may be 32 or 36 bytes depending on compiler alignment + // We'll use sizeof() in the code rather than hardcoding + + // Minimal Device Descriptor Message structure + // We'll build this dynamically to include at least the interface GUID + struct DeviceDescriptorMessage { - struct GipHeader - { - uint8_t command; - uint8_t client : 4; - uint8_t needsAck : 1; - uint8_t internal : 1; - uint8_t chunkStart : 1; - uint8_t chunked : 1; - uint8_t sequence; - uint8_t length; - } header; + GipMessageHeader header; // 4 bytes + DeviceDescriptorHeader descHeader; // 32 bytes + // Following buffer will contain metadata blocks + // For a minimal gamepad descriptor, we need at least: + // - Interface GUID block (16 bytes + 1 byte count = 17 bytes minimum) + uint8_t buffer[64]; // Buffer for metadata (can be expanded) + }; - uint8_t buttons[2]; + // 48-byte input report (4-byte header + 44-byte payload). Header: 0x20, 0x00, sequence, 0x2C. + // We put gamepad data at payload offset 0 (report bytes 4-17) for Linux xpad/GamepadTester. + // Real controller uses payload offset 6 for buttons; both formats are 48 bytes total. + static constexpr uint16_t GIP_INPUT_REPORT_SIZE = 48; + static constexpr uint8_t GIP_INPUT_PAYLOAD_SIZE = 44; // 0x2C - uint16_t trigger_l; - uint16_t trigger_r; + // Input Report (GIP CMD 0x20) - logical gamepad state (we build 48-byte wire format from this) + struct InReport + { + GipMessageHeader header; + uint16_t buttons; // Bitfield: Sync, Menu, View, A, B, X, Y, Dpad, Shoulders, Thumbs + uint16_t trigger_l; // 0x000 to 0x3FF (0-1023) + uint16_t trigger_r; // 0x000 to 0x3FF (0-1023) int16_t joystick_lx; int16_t joystick_ly; int16_t joystick_rx; int16_t joystick_ry; + uint8_t guide_pressed; // Home/Guide: when set, report bytes 12-13 = 0xF5FE (real controller encoding) + }; + static_assert(sizeof(InReport) == 19, "XboxOne::InReport must be 19 bytes"); + + // Wire format: GIP 0x20 payload bytes 0-1 (report bytes 4-5) use Microsoft layout: + // B0=Sync, B1=reserved, B2=Start, B3=Back, B4=A, B5=B, B6=X, B7=Y, + // B8=DPAD_UP, B9=DPAD_DOWN, B10=DPAD_LEFT, B11=DPAD_RIGHT, B12=LB, B13=RB, B14=L3, B15=R3. + namespace GipWireButtons + { + static constexpr uint16_t SYNC = (1U << 0); + static constexpr uint16_t START = (1U << 2); + static constexpr uint16_t BACK = (1U << 3); + static constexpr uint16_t A = (1U << 4); + static constexpr uint16_t B = (1U << 5); + static constexpr uint16_t X = (1U << 6); + static constexpr uint16_t Y = (1U << 7); + static constexpr uint16_t DPAD_UP = (1U << 8); + static constexpr uint16_t DPAD_DOWN = (1U << 9); + static constexpr uint16_t DPAD_LEFT = (1U << 10); + static constexpr uint16_t DPAD_RIGHT = (1U << 11); + static constexpr uint16_t LEFT_SHOULDER = (1U << 12); + static constexpr uint16_t RIGHT_SHOULDER = (1U << 13); + static constexpr uint16_t LEFT_THUMB = (1U << 14); + static constexpr uint16_t RIGHT_THUMB = (1U << 15); + } - uint8_t reserved[18]; // 18-byte padding at the end + // Logical Gamepad bitfield (evdev-style); used after mapping from GipWireButtons. + namespace GamepadButtons + { + static constexpr uint16_t A = 0x0001; // B0 + static constexpr uint16_t B = 0x0002; // B1 + static constexpr uint16_t X = 0x0004; // B2 + static constexpr uint16_t Y = 0x0008; // B3 + static constexpr uint16_t LEFT_SHOULDER = 0x0010; // B4 LB + static constexpr uint16_t RIGHT_SHOULDER = 0x0020; // B5 RB + static constexpr uint16_t LEFT_TRIGGER_BTN = 0x0040; // B6 LT (from trigger value) + static constexpr uint16_t RIGHT_TRIGGER_BTN = 0x0080; // B7 RT + static constexpr uint16_t VIEW = 0x0100; // B8 Select/Back + static constexpr uint16_t MENU = 0x0200; // B9 Start + static constexpr uint16_t LEFT_THUMB = 0x0400; // B10 L3 + static constexpr uint16_t RIGHT_THUMB = 0x0800; // B11 R3 + static constexpr uint16_t DPAD_UP = 0x1000; // B12 + static constexpr uint16_t DPAD_DOWN = 0x2000; // B13 + static constexpr uint16_t DPAD_LEFT = 0x4000; // B14 + static constexpr uint16_t DPAD_RIGHT = 0x8000; // B15 + // B16 = Home/Guide (evdev often as separate key; we can't fit in 16 bits here) + static constexpr uint16_t SYNC = 0x0000; // unused in 16-bit; map to KEY_PROG1 etc. if needed + } + + // Set Device State / Force Feedback (GIP CMD 0x09) + struct OutReport + { + GipMessageHeader header; + uint8_t unknown1; + uint8_t flags; // Force feedback flags + uint8_t leftTrigger; // Left trigger vibration (0-255) + uint8_t rightTrigger; // Right trigger vibration (0-255) + uint8_t leftMotor; // Left motor (0-255) + uint8_t rightMotor; // Right motor (0-255) + uint8_t duration; // Duration in 10ms units + uint8_t delay; // Delay before starting + uint8_t repeat; // Repeat count }; - static_assert(sizeof(InReport) == 36, "XboxOne::InReport is not the correct size"); + static_assert(sizeof(OutReport) == 13, "XboxOne::OutReport must be 13 bytes"); + + // Force Feedback Flags + namespace ForceFeedbackFlags + { + static constexpr uint8_t RIGHT_MOTOR = 0x01; + static constexpr uint8_t LEFT_MOTOR = 0x02; + static constexpr uint8_t RIGHT_TRIGGER = 0x04; + static constexpr uint8_t LEFT_TRIGGER = 0x08; + } #pragma pack(pop) + // USB Descriptors + static const uint8_t STRING_LANGUAGE[] = { 0x09, 0x04 }; + static const uint8_t STRING_MANUFACTURER[] = "Microsoft"; + static const uint8_t STRING_PRODUCT[] = "Xbox One Controller"; + static const uint8_t STRING_SERIAL[] = "000000000000"; + static const uint8_t STRING_VERSION[] = "1.0"; + + static const uint8_t *STRING_DESCRIPTORS[] __attribute__((unused)) = + { + STRING_LANGUAGE, + STRING_MANUFACTURER, + STRING_PRODUCT, + STRING_SERIAL, + STRING_VERSION + }; + + // Device descriptor - Windows loads xb1usb for VID_045E&PID_02D1 (Xbox One Controller) + static const uint8_t DEVICE_DESCRIPTORS[] = + { + 0x12, // bLength + 0x01, // bDescriptorType (Device) + 0x00, 0x02, // bcdUSB 2.00 + 0xFF, // bDeviceClass (Vendor Specific) + 0x47, // bDeviceSubClass (71) + 0xD0, // bDeviceProtocol (208) + 0x40, // bMaxPacketSize0 64 + 0x5E, 0x04, // idVendor 0x045E (Microsoft) + 0xD1, 0x02, // idProduct 0x02D1 (Xbox One Controller - Windows compatible) + 0x16, 0x05, // bcdDevice 5.16 (match capture) + 0x01, // iManufacturer (String Index) + 0x02, // iProduct (String Index) + 0x03, // iSerialNumber (String Index) + 0x01, // bNumConfigurations 1 + }; + + // Configuration descriptor - match real controller: Interface 0 has BOTH GIP endpoints (0x02 OUT, 0x82 IN) + static const uint8_t CONFIGURATION_DESCRIPTORS[] = + { + // Configuration descriptor + 0x09, // bLength + 0x02, // bDescriptorType (Configuration) + 0x32, 0x00, // wTotalLength 50 bytes + 0x03, // bNumInterfaces 3 + 0x01, // bConfigurationValue 1 + 0x00, // iConfiguration (String Index) + 0xA0, // bmAttributes (Bus Powered, Remote Wakeup supported) + 0xFA, // bMaxPower 500mA + + // Interface 0 - GIP data (both IN and OUT on this interface, like real controller) + 0x09, // bLength + 0x04, // bDescriptorType (Interface) + 0x00, // bInterfaceNumber 0 + 0x00, // bAlternateSetting 0 + 0x02, // bNumEndpoints 2 + 0xFF, // bInterfaceClass (Vendor Specific) + 0x47, // bInterfaceSubClass (71) + 0xD0, // bInterfaceProtocol (208) + 0x00, // iInterface (String Index) + + // Endpoint 0x02 OUT (first EP in descriptor = 0x02) + 0x07, // bLength + 0x05, // bDescriptorType (Endpoint) + 0x02, // bEndpointAddress (OUT) + 0x03, // bmAttributes (Interrupt) + 0x40, 0x00, // wMaxPacketSize 64 + 0x04, // bInterval 4 + + // Endpoint 0x82 IN (second EP = 0x82) + 0x07, // bLength + 0x05, // bDescriptorType (Endpoint) + 0x82, // bEndpointAddress (IN) + 0x03, // bmAttributes (Interrupt) + 0x40, 0x00, // wMaxPacketSize 64 + 0x04, // bInterval 4 + + // Interface 1 - no endpoints (placeholder for 3-interface layout) + 0x09, // bLength + 0x04, // bDescriptorType (Interface) + 0x01, // bInterfaceNumber 1 + 0x00, // bAlternateSetting 0 + 0x00, // bNumEndpoints 0 + 0xFF, // bInterfaceClass (Vendor Specific) + 0x47, // bInterfaceSubClass (71) + 0xD0, // bInterfaceProtocol (208) + 0x00, // iInterface (String Index) + + // Interface 2 - no endpoints (placeholder for 3-interface layout) + 0x09, // bLength + 0x04, // bDescriptorType (Interface) + 0x02, // bInterfaceNumber 2 + 0x00, // bAlternateSetting 0 + 0x00, // bNumEndpoints 0 + 0xFF, // bInterfaceClass (Vendor Specific) + 0x47, // bInterfaceSubClass (71) + 0xD0, // bInterfaceProtocol (208) + 0x00, // iInterface (String Index) + }; + + static constexpr uint16_t DRIVER_LEN = sizeof(CONFIGURATION_DESCRIPTORS); + }; // namespace XboxOne #endif // _XBOX_ONE_DESCRIPTORS_H_ \ No newline at end of file diff --git a/Firmware/RP2040/src/Gamepad/Gamepad.h b/Firmware/RP2040/src/Gamepad/Gamepad.h index 9f8cca5e..65441b3d 100644 --- a/Firmware/RP2040/src/Gamepad/Gamepad.h +++ b/Firmware/RP2040/src/Gamepad/Gamepad.h @@ -16,13 +16,14 @@ #include "UserSettings/UserProfile.h" #include "UserSettings/JoystickSettings.h" #include "UserSettings/TriggerSettings.h" +#include "USBDevice/DeviceDriver/DeviceDriverTypes.h" #include "Board/ogxm_log.h" class Gamepad { public: + //Defaults used by device to get buttons - static constexpr uint8_t DPAD_UP = 0x01; static constexpr uint8_t DPAD_DOWN = 0x02; static constexpr uint8_t DPAD_LEFT = 0x04; @@ -96,6 +97,16 @@ class Gamepad #pragma pack(push, 1) struct PadIn { + /** IMU for PS3 Sixaxis / PS4 gadget: Sony, Switch Pro, Wii Remote, wired or BT. */ + static constexpr uint8_t MOTION_SRC_NONE = 0; + static constexpr uint8_t MOTION_SRC_DS4 = 1; + static constexpr uint8_t MOTION_SRC_DS5 = 2; + static constexpr uint8_t MOTION_SRC_SWITCH_PRO = 3; + static constexpr uint8_t MOTION_SRC_DS5_USB = 4; + static constexpr uint8_t MOTION_SRC_DS4_USB = 5; + static constexpr uint8_t MOTION_SRC_SWITCH_USB = 6; + static constexpr uint8_t MOTION_SRC_WII_BT = 7; + uint8_t dpad; uint16_t buttons; uint8_t trigger_l; @@ -105,7 +116,31 @@ class Gamepad int16_t joystick_rx; int16_t joystick_ry; uint8_t analog[10]; - + + /** Bluepad / USB DS4+DS5: int32 accel (DS4 units/g) and gyro (DS4 rate units). */ + int32_t accel[3]; + int32_t gyro[3]; + /** If MOTION_SRC_NONE, PS3 mode leaves Sixaxis at neutral; else maps to DS3 report. */ + uint8_t motion_source; + + bool has_motion() const + { + return motion_source == MOTION_SRC_DS4 || + motion_source == MOTION_SRC_DS5 || + motion_source == MOTION_SRC_SWITCH_PRO || + motion_source == MOTION_SRC_DS5_USB || + motion_source == MOTION_SRC_DS4_USB || + motion_source == MOTION_SRC_SWITCH_USB || + motion_source == MOTION_SRC_WII_BT; + } + + /** DualSense touchpad raw bytes (Steam mode mouse). Wire format: 8 bytes at DS5 touch offset. */ + uint8_t touch_raw[8]; + uint8_t touchpad_click; + /** Set when touch_raw was filled from a DS5 report this frame. */ + uint8_t touchpad_valid; + uint8_t touchpad_reserved; + PadIn() { std::memset(this, 0, sizeof(PadIn)); @@ -143,16 +178,51 @@ class Gamepad inline bool new_pad_in() const { return new_pad_in_.load(); } inline bool new_pad_out() const { return new_pad_out_.load(); } + // True if current pad_out has non-zero rumble (read-only, does not clear new_pad_out). + inline bool has_rumble() + { + mutex_enter_blocking(&pad_out_mutex_); + bool r = (pad_out_.rumble_l > 0 || pad_out_.rumble_r > 0); + mutex_exit(&pad_out_mutex_); + return r; + } + //True if both host and device have enabled analog inline bool analog_enabled() const { return analog_enabled_.load(std::memory_order_relaxed); } inline PadIn get_pad_in() { mutex_enter_blocking(&pad_in_mutex_); - PadIn pad_in = pad_in_; - new_pad_in_.store(false); + /* Bluetooth HID runs on Core1; never block there on this mutex (would stall HCI/L2CAP). + * Latest BT reports are staged lock-free (2 slots) and merged here on Core0. */ + while (bt_pad_staged_count_.load(std::memory_order_acquire) > 0) { + const unsigned ri = bt_pad_staged_read_.load(std::memory_order_relaxed) % BT_PAD_STAGING_SLOTS; + const PadIn& p = bt_pending_pads_[ri]; + if (pad_in_count_ < PAD_IN_QUEUE_SIZE) { + pad_in_queue_[pad_in_tail_] = p; + pad_in_tail_ = (pad_in_tail_ + 1) % PAD_IN_QUEUE_SIZE; + pad_in_count_++; + } else { + pad_in_head_ = (pad_in_head_ + 1) % PAD_IN_QUEUE_SIZE; + pad_in_queue_[pad_in_tail_] = p; + pad_in_tail_ = (pad_in_tail_ + 1) % PAD_IN_QUEUE_SIZE; + } + bt_pad_staged_read_.store((ri + 1) % BT_PAD_STAGING_SLOTS, std::memory_order_relaxed); + bt_pad_staged_count_.fetch_sub(1, std::memory_order_acq_rel); + new_pad_in_.store(true); + } + PadIn pad_in; + if (pad_in_count_ > 0) { + pad_in = pad_in_queue_[pad_in_head_]; + pad_in_head_ = (pad_in_head_ + 1) % PAD_IN_QUEUE_SIZE; + pad_in_count_--; + last_pad_in_ = pad_in; + if (pad_in_count_ == 0) + new_pad_in_.store(false); + } else { + pad_in = last_pad_in_; + } mutex_exit(&pad_in_mutex_); - return pad_in; } @@ -195,20 +265,44 @@ class Gamepad } } - void set_profile(const UserProfile& user_profile) - { + void set_profile(const UserProfile& user_profile, DeviceDriverType driver = DeviceDriverType::NONE) + { set_profile_mappings(user_profile); - set_profile_settings(user_profile); + set_profile_settings(user_profile, driver); } inline void set_pad_in(PadIn pad_in) { mutex_enter_blocking(&pad_in_mutex_); - pad_in_ = pad_in; + if (pad_in_count_ < PAD_IN_QUEUE_SIZE) { + pad_in_queue_[pad_in_tail_] = pad_in; + pad_in_tail_ = (pad_in_tail_ + 1) % PAD_IN_QUEUE_SIZE; + pad_in_count_++; + } else { + /* Queue full: drop oldest, keep newest so fast taps (press+release) both get sent */ + pad_in_head_ = (pad_in_head_ + 1) % PAD_IN_QUEUE_SIZE; + pad_in_queue_[pad_in_tail_] = pad_in; + pad_in_tail_ = (pad_in_tail_ + 1) % PAD_IN_QUEUE_SIZE; + } new_pad_in_.store(true); mutex_exit(&pad_in_mutex_); } + /** Bluetooth (Core1): never blocks on Core0 — avoids stalling the BT stack in HID callbacks. */ + inline void set_pad_in_from_bluetooth(const PadIn& pad_in) + { + if (bt_pad_staged_count_.load(std::memory_order_acquire) >= BT_PAD_STAGING_SLOTS) { + const unsigned drop = bt_pad_staged_read_.load(std::memory_order_relaxed) % BT_PAD_STAGING_SLOTS; + bt_pad_staged_read_.store((drop + 1) % BT_PAD_STAGING_SLOTS, std::memory_order_relaxed); + bt_pad_staged_count_.fetch_sub(1, std::memory_order_acq_rel); + } + const unsigned wi = bt_pad_staged_write_.load(std::memory_order_relaxed) % BT_PAD_STAGING_SLOTS; + bt_pending_pads_[wi] = pad_in; + bt_pad_staged_write_.store((wi + 1) % BT_PAD_STAGING_SLOTS, std::memory_order_release); + bt_pad_staged_count_.fetch_add(1, std::memory_order_release); + new_pad_in_.store(true, std::memory_order_release); + } + inline void set_pad_out(const PadOut& pad_out) { mutex_enter_blocking(&pad_out_mutex_); @@ -224,10 +318,20 @@ class Gamepad mutex_exit(&chatpad_in_mutex_); } - inline void reset_pad_in() - { + // Wii U GC adapter: set by host when controller uses positive Y for physical up (e.g. Xbox One/360) + void set_stick_y_positive_is_up(bool v) { stick_y_positive_is_up_ = v; } + bool stick_y_positive_is_up() const { return stick_y_positive_is_up_; } + + inline void reset_pad_in() + { + bt_pad_staged_write_.store(0, std::memory_order_relaxed); + bt_pad_staged_read_.store(0, std::memory_order_relaxed); + bt_pad_staged_count_.store(0, std::memory_order_relaxed); mutex_enter_blocking(&pad_in_mutex_); - pad_in_ = PadIn(); + pad_in_head_ = 0; + pad_in_tail_ = 0; + pad_in_count_ = 0; + last_pad_in_ = PadIn(); mutex_exit(&pad_in_mutex_); new_pad_in_.store(true); } @@ -341,13 +445,24 @@ class Gamepad : trigger_value; } -private: + static constexpr unsigned PAD_IN_QUEUE_SIZE = 8; + static constexpr unsigned BT_PAD_STAGING_SLOTS = 2; + +private: + PadIn bt_pending_pads_[BT_PAD_STAGING_SLOTS]{}; + std::atomic bt_pad_staged_write_{0}; + std::atomic bt_pad_staged_read_{0}; + std::atomic bt_pad_staged_count_{0}; mutex_t pad_in_mutex_; mutex_t pad_out_mutex_; mutex_t chatpad_in_mutex_; PadOut pad_out_; - PadIn pad_in_; + PadIn pad_in_queue_[PAD_IN_QUEUE_SIZE]; + unsigned pad_in_head_{0}; + unsigned pad_in_tail_{0}; + unsigned pad_in_count_{0}; + PadIn last_pad_in_{}; ChatpadIn chatpad_in_{0}; std::atomic new_pad_in_{false}; @@ -359,6 +474,8 @@ class Gamepad bool profile_analog_enabled_{false}; + bool stick_y_positive_is_up_{false}; // true for Xbox (positive Y = up), false for Wii U/Nintendo + JoystickSettings joy_settings_l_; JoystickSettings joy_settings_r_; TriggerSettings trig_settings_l_; @@ -369,27 +486,62 @@ class Gamepad bool trig_settings_l_en_{false}; bool trig_settings_r_en_{false}; - void set_profile_settings(const UserProfile& profile) + void set_profile_settings(const UserProfile& profile, DeviceDriverType driver = DeviceDriverType::NONE) { profile_analog_enabled_ = profile.analog_enabled ? true : false; +#if 0 OGXM_LOG("profile_analog_enabled_: %d\n", profile_analog_enabled_); +#endif + + const bool xinput_stock = + (driver == DeviceDriverType::XINPUT); + + if (JoystickSettings::raw_has_customization(profile.joystick_settings_l)) + { + joy_settings_l_en_ = true; + if (!joy_settings_l_.is_same(profile.joystick_settings_l)) + { + joy_settings_l_.set_from_raw(profile.joystick_settings_l); + //This needs to be addressed in the webapp, just multiply here for now + joy_settings_l_.axis_restrict *= static_cast(100); + joy_settings_l_.angle_restrict *= static_cast(100); + joy_settings_l_.anti_dz_angular *= static_cast(100); + } + } + else if (xinput_stock) + { + joy_settings_l_.set_xbox360_stock_feel(false); + joy_settings_l_en_ = true; + } + else + { + joy_settings_l_ = JoystickSettings{}; + joy_settings_l_en_ = false; + } - if ((joy_settings_l_en_ = !joy_settings_l_.is_same(profile.joystick_settings_l))) + if (JoystickSettings::raw_has_customization(profile.joystick_settings_r)) { - joy_settings_l_.set_from_raw(profile.joystick_settings_l); - //This needs to be addressed in the webapp, just multiply here for now - joy_settings_l_.axis_restrict *= static_cast(100); - joy_settings_l_.angle_restrict *= static_cast(100); - joy_settings_l_.anti_dz_angular *= static_cast(100); + joy_settings_r_en_ = true; + if (!joy_settings_r_.is_same(profile.joystick_settings_r)) + { + joy_settings_r_.set_from_raw(profile.joystick_settings_r); + //This needs to be addressed in the webapp, just multiply here for now + joy_settings_r_.axis_restrict *= static_cast(100); + joy_settings_r_.angle_restrict *= static_cast(100); + joy_settings_r_.anti_dz_angular *= static_cast(100); + } } - if ((joy_settings_r_en_ = !joy_settings_r_.is_same(profile.joystick_settings_r))) + else if (xinput_stock) { - joy_settings_r_.set_from_raw(profile.joystick_settings_r); - //This needs to be addressed in the webapp, just multiply here for now - joy_settings_r_.axis_restrict *= static_cast(100); - joy_settings_r_.angle_restrict *= static_cast(100); - joy_settings_r_.anti_dz_angular *= static_cast(100); + joy_settings_r_.set_xbox360_stock_feel(true); + joy_settings_r_en_ = true; } + else + { + joy_settings_r_ = JoystickSettings{}; + joy_settings_r_en_ = false; + } + if ((trig_settings_l_en_ = !trig_settings_l_.is_same(profile.trigger_settings_l))) { trig_settings_l_.set_from_raw(profile.trigger_settings_l); @@ -399,11 +551,13 @@ class Gamepad trig_settings_r_.set_from_raw(profile.trigger_settings_r); } +#if 0 OGXM_LOG("GamepadMapper: JoyL: %s, JoyR: %s, TrigL: %s, TrigR: %s\n", joy_settings_l_en_ ? "Enabled" : "Disabled", joy_settings_r_en_ ? "Enabled" : "Disabled", trig_settings_l_en_ ? "Enabled" : "Disabled", trig_settings_r_en_ ? "Enabled" : "Disabled"); +#endif } void set_profile_mappings(const UserProfile& profile) @@ -481,13 +635,44 @@ class Gamepad ? FIX_0 : ((abs_y - set.axis_restrict) * inv_axis_restrict); + /* WebApp uses raw stick magnitude for the inner deadzone test (before axial restrict). */ + const Fix16 raw_magnitude = fix16::sqrt(fix16::sq(abs_x) + fix16::sq(abs_y)); Fix16 in_magnitude = fix16::sqrt(fix16::sq(axial_x) + fix16::sq(axial_y)); - if (in_magnitude < set.dz_inner) + /* Anti-deadzone pushes any non-zero reading to at least anti_dz_* magnitude. + * Switch Pro / Switch 2 Pro USB sticks are rarely perfect at the nominal + * center, so with dz_inner=0 that becomes a constant drift (often bottom-left + * after Y invert). Gate idle noise when anti-dz is active, but keep the + * user's dz_inner for the magnitude warp so anti-dz still applies fully. */ + const bool anti_dz_active = + set.anti_dz_square > FIX_EPSILON || set.anti_dz_circle > FIX_EPSILON || + set.anti_dz_square_y_scale > FIX_EPSILON || set.anti_dz_circle_y_scale > FIX_EPSILON; + static const Fix16 FIX_ANTI_DZ_NOISE_MIN(0.07f); + static const Fix16 FIX_ANTI_DZ_NOISE_MAX(0.12f); + Fix16 gate = set.dz_inner; + if (anti_dz_active && gate < FIX_ANTI_DZ_NOISE_MIN) + { + const Fix16 anti_ref = + (set.anti_dz_circle > set.anti_dz_square) ? set.anti_dz_circle : set.anti_dz_square; + Fix16 adaptive_gate = anti_ref * Fix16(0.4f); + if (adaptive_gate < FIX_ANTI_DZ_NOISE_MIN) + { + adaptive_gate = FIX_ANTI_DZ_NOISE_MIN; + } + if (adaptive_gate > FIX_ANTI_DZ_NOISE_MAX) + { + adaptive_gate = FIX_ANTI_DZ_NOISE_MAX; + } + gate = adaptive_gate; + } + + if (raw_magnitude <= gate) { return { 0, 0 }; } + const Fix16 dz_inner = set.dz_inner; + Fix16 angle = fix16::abs(axial_x) < FIX_EPSILON ? FIX_90 @@ -496,10 +681,10 @@ class Gamepad Fix16 anti_r_scale = (set.anti_dz_square_y_scale == FIX_0) ? set.anti_dz_square : set.anti_dz_square_y_scale; Fix16 anti_dz_c = set.anti_dz_circle; - if (anti_r_scale > FIX_0 && anti_dz_c > FIX_0) + if (set.anti_dz_circle_y_scale > FIX_0 && anti_dz_c > FIX_0) { - Fix16 anti_ellip_scale = anti_ellip_scale / anti_dz_c; - Fix16 ellipse_angle = fix16::atan((FIX_1 / anti_ellip_scale) * fix16::tan(fix16::rad2deg(rAngle))); + Fix16 anti_ellip_scale = set.anti_dz_circle_y_scale / anti_dz_c; + Fix16 ellipse_angle = fix16::atan((FIX_1 / anti_ellip_scale) * fix16::tan(fix16::deg2rad(rAngle))); ellipse_angle = (ellipse_angle < FIX_0) ? FIX_ELLIPSE_DEF : ellipse_angle; Fix16 ellipse_x = fix16::cos(ellipse_angle); @@ -507,9 +692,11 @@ class Gamepad anti_dz_c *= fix16::sqrt(fix16::sq(ellipse_x) + fix16::sq(ellipse_y)); } + /* Match WebApp: counter square anti-dz shrinkage (must use anti_dz_square, not circle). */ if (anti_dz_c > FIX_0) { - anti_dz_c = anti_dz_c / ((anti_dz_c * (FIX_1 - set.anti_dz_circle / set.dz_outer)) / (anti_dz_c * (FIX_1 - set.anti_dz_square))); + anti_dz_c = anti_dz_c / ((anti_dz_c * (FIX_1 - set.anti_dz_square / set.dz_outer)) / + (anti_dz_c * (FIX_1 - set.anti_dz_square))); } if (abs_x > set.axis_restrict && abs_y > set.axis_restrict) @@ -554,7 +741,7 @@ class Gamepad } //Deadzone Warp - Fix16 out_magnitude = (in_magnitude - set.dz_inner) / (set.anti_dz_outer - set.dz_inner); + Fix16 out_magnitude = (in_magnitude - dz_inner) / (set.anti_dz_outer - dz_inner); out_magnitude = fix16::pow(out_magnitude, (FIX_1 / set.curve)) * (set.dz_outer - anti_dz_c) + anti_dz_c; out_magnitude = (out_magnitude > set.dz_outer && !set.uncap_radius) ? set.dz_outer : out_magnitude; diff --git a/Firmware/RP2040/src/Gamepad/I2CWirePad.h b/Firmware/RP2040/src/Gamepad/I2CWirePad.h new file mode 100644 index 00000000..260b08e8 --- /dev/null +++ b/Firmware/RP2040/src/Gamepad/I2CWirePad.h @@ -0,0 +1,55 @@ +#ifndef _I2C_WIRE_PAD_H_ +#define _I2C_WIRE_PAD_H_ + +#include + +#include "Gamepad/Gamepad.h" + +/** Fixed-size pad payload for I2C master/slave links (32-byte PacketIn). Excludes IMU + * fields added to Gamepad::PadIn for DS4/DS5/Switch Pro — not used between 4ch boards. */ +#pragma pack(push, 1) +struct I2CWirePadIn { + uint8_t dpad; + uint16_t buttons; + uint8_t trigger_l; + uint8_t trigger_r; + int16_t joystick_lx; + int16_t joystick_ly; + int16_t joystick_rx; + int16_t joystick_ry; + uint8_t analog[10]; +}; +#pragma pack(pop) +static_assert(sizeof(I2CWirePadIn) == 23, "I2CWirePadIn size mismatch"); + +inline I2CWirePadIn i2c_wire_pad_from(const Gamepad::PadIn& pad_in) +{ + I2CWirePadIn wire{}; + wire.dpad = pad_in.dpad; + wire.buttons = pad_in.buttons; + wire.trigger_l = pad_in.trigger_l; + wire.trigger_r = pad_in.trigger_r; + wire.joystick_lx = pad_in.joystick_lx; + wire.joystick_ly = pad_in.joystick_ly; + wire.joystick_rx = pad_in.joystick_rx; + wire.joystick_ry = pad_in.joystick_ry; + std::memcpy(wire.analog, pad_in.analog, sizeof(wire.analog)); + return wire; +} + +inline Gamepad::PadIn i2c_pad_from_wire(const I2CWirePadIn& wire) +{ + Gamepad::PadIn pad_in; + pad_in.dpad = wire.dpad; + pad_in.buttons = wire.buttons; + pad_in.trigger_l = wire.trigger_l; + pad_in.trigger_r = wire.trigger_r; + pad_in.joystick_lx = wire.joystick_lx; + pad_in.joystick_ly = wire.joystick_ly; + pad_in.joystick_rx = wire.joystick_rx; + pad_in.joystick_ry = wire.joystick_ry; + std::memcpy(pad_in.analog, wire.analog, sizeof(pad_in.analog)); + return pad_in; +} + +#endif diff --git a/Firmware/RP2040/src/Gamepad/MotionImu.h b/Firmware/RP2040/src/Gamepad/MotionImu.h new file mode 100644 index 00000000..f778e8c3 --- /dev/null +++ b/Firmware/RP2040/src/Gamepad/MotionImu.h @@ -0,0 +1,355 @@ +#ifndef _MOTION_IMU_H_ +#define _MOTION_IMU_H_ + +#include +#include + +#include "Gamepad/Gamepad.h" + +namespace MotionImu +{ + +constexpr int32_t kDs4AccRange = 4 * 8192; +constexpr int32_t kDs4GyroRange = 2048 * 1024; +constexpr int32_t kRawSensDenom = 32767; + +constexpr int32_t kSwitchDefaultAccelScale = 16384; +constexpr int32_t kSwitchDefaultGyroScale = 13371; +constexpr int32_t kSwitchGyroPrecScale = 1000; +constexpr int32_t kWiiAccelCountsPerG = 0x6C; +constexpr int32_t kDs4UnitsPerG = 8192; + +#pragma pack(push, 1) +struct SwitchImuSample +{ + int16_t accel[3]; + int16_t gyro[3]; +}; +#pragma pack(pop) + +static_assert(sizeof(SwitchImuSample) == 12); + +inline int32_t ds4_ds5_usb_raw_to_units(int16_t raw, int32_t range) +{ + return static_cast((static_cast(range) * static_cast(raw)) / kRawSensDenom); +} + +inline void fill_from_ds4_usb_report(int32_t accel[3], int32_t gyro[3], const uint8_t* report) +{ + int16_t raw_g[3]; + int16_t raw_a[3]; + std::memcpy(&raw_g[0], &report[13], 2); + std::memcpy(&raw_g[1], &report[15], 2); + std::memcpy(&raw_g[2], &report[17], 2); + std::memcpy(&raw_a[0], &report[19], 2); + std::memcpy(&raw_a[1], &report[21], 2); + std::memcpy(&raw_a[2], &report[23], 2); + for (int i = 0; i < 3; i++) { + gyro[i] = ds4_ds5_usb_raw_to_units(raw_g[i], kDs4GyroRange); + accel[i] = ds4_ds5_usb_raw_to_units(raw_a[i], kDs4AccRange); + } +} + +inline void fill_from_ds5_usb_raw(int32_t accel[3], int32_t gyro[3], const uint16_t raw_accel[3], const uint16_t raw_gyro[3]) +{ + for (int i = 0; i < 3; i++) { + gyro[i] = ds4_ds5_usb_raw_to_units(static_cast(raw_gyro[i]), kDs4GyroRange); + accel[i] = ds4_ds5_usb_raw_to_units(static_cast(raw_accel[i]), kDs4AccRange); + } +} + +/* Bluepad default factory cal when SPI calib is unavailable (wired USB). */ +inline void fill_from_switch_usb_payload(int32_t accel[3], int32_t gyro[3], const uint8_t* payload, uint16_t payload_len) +{ + constexpr uint16_t kInReportSize = 10; + constexpr uint16_t kImuSampleSize = sizeof(SwitchImuSample); + + if (payload_len < kInReportSize + kImuSampleSize) { + for (int i = 0; i < 3; i++) { + accel[i] = 0; + gyro[i] = 0; + } + return; + } + + uint16_t imu_off = kInReportSize; + if (payload_len >= kInReportSize + (3 * kImuSampleSize)) { + imu_off = static_cast(kInReportSize + (2 * kImuSampleSize)); + } + + SwitchImuSample raw{}; + std::memcpy(&raw, payload + imu_off, sizeof(raw)); + + const int32_t accel_div = kSwitchDefaultAccelScale; + const int32_t gyro_div = kSwitchDefaultGyroScale; + for (int i = 0; i < 3; i++) { + if (accel_div != 0) { + accel[i] = (static_cast(raw.accel[i]) * kSwitchDefaultAccelScale) / accel_div; + } else { + accel[i] = raw.accel[i]; + } + if (gyro_div != 0) { + const int64_t num = static_cast(kSwitchGyroPrecScale) * static_cast(raw.gyro[i]); + gyro[i] = static_cast((num * kSwitchDefaultGyroScale) / gyro_div); + } else { + gyro[i] = raw.gyro[i]; + } + } +} + +inline bool switch_usb_imu_unchanged(const uint8_t* prev_imu, const uint8_t* payload, uint16_t payload_len) +{ + constexpr uint16_t kInReportSize = 10; + constexpr uint16_t kImuSampleSize = sizeof(SwitchImuSample); + if (payload_len < kInReportSize + kImuSampleSize) { + return true; + } + + uint16_t imu_off = kInReportSize; + if (payload_len >= kInReportSize + (3 * kImuSampleSize)) { + imu_off = static_cast(kInReportSize + (2 * kImuSampleSize)); + } + + return std::memcmp(prev_imu, payload + imu_off, kImuSampleSize) == 0; +} + +/* Bluepad Wii DRM_KA: centered int16 accel (~108 counts per G). */ +inline void fill_from_wii_bt(int32_t accel[3], int32_t gyro[3], const int32_t wii_accel[3]) +{ + for (int i = 0; i < 3; i++) { + accel[i] = static_cast((static_cast(wii_accel[i]) * kDs4UnitsPerG) / kWiiAccelCountsPerG); + gyro[i] = 0; + } +} + +/** + * Switch motion UIs integrate gyro (Linux ABS_RX/RY/RZ), not accel (ABS_X/Y/Z). + * Wiimote has no parsed MotionPlus gyro, so approximate rate from filtered gravity. + * + * hid-nintendo scales Pro gyro int16 by ~8k× (switch_raw 3 → ABS ~25k). Emit + * instantaneous d(accel)/dt only (no accumulation), cap ±1. PadIn.gyro is + * Switch-native int16. Still pose ⇒ gyro=0. + */ +struct WiiPseudoGyroState +{ + int32_t filt_accel[3]; + int32_t prev_filt[3]; + uint32_t prev_ms; + bool valid; +}; + +inline void apply_wii_pseudo_gyro(int32_t accel[3], int32_t gyro[3], WiiPseudoGyroState& st, uint32_t now_ms) +{ + gyro[0] = gyro[1] = gyro[2] = 0; + + if (!st.valid) { + for (int i = 0; i < 3; i++) { + st.filt_accel[i] = accel[i]; + st.prev_filt[i] = accel[i]; + } + st.prev_ms = now_ms; + st.valid = true; + return; + } + + for (int i = 0; i < 3; i++) { + st.filt_accel[i] = (st.filt_accel[i] * 2 + accel[i]) / 3; + accel[i] = st.filt_accel[i]; + } + + uint32_t dt = now_ms - st.prev_ms; + if (dt < 1) { + return; + } + if (dt > 40) { + dt = 40; + } + + /* ±1 → Linux ~8k; d=80 over 10 ms → raw 1. */ + constexpr int32_t kMaxRaw = 1; + for (int i = 0; i < 3; i++) { + int32_t d = st.filt_accel[i] - st.prev_filt[i]; + st.prev_filt[i] = st.filt_accel[i]; + + if (d > -20 && d < 20) { + continue; + } + + int32_t rate = d / (static_cast(dt) * 5); + if (rate > kMaxRaw) { + rate = kMaxRaw; + } else if (rate < -kMaxRaw) { + rate = -kMaxRaw; + } + gyro[i] = rate; + } + st.prev_ms = now_ms; +} + +/* + * Rotate accel/gyro into the DS4/Sixaxis playing frame (+Z = 1 G when level, sticks up). + * Switch Pro raw: X → triggers, Y → left, Z → out of face buttons. + * Wiimote pointed at TV: gravity mostly on -Y; map to DS4 +Z neutral. + */ +inline void remap_to_ds4_playing_frame(uint8_t motion_source, int32_t accel[3], int32_t gyro[3]) +{ + const int32_t ix = accel[0]; + const int32_t iy = accel[1]; + const int32_t iz = accel[2]; + const int32_t gx = gyro[0]; + const int32_t gy = gyro[1]; + const int32_t gz = gyro[2]; + + switch (motion_source) { + case Gamepad::PadIn::MOTION_SRC_WII_BT: + /* Wiimote IR toward TV: gravity on -Y; map into DS4 playing frame. */ + accel[0] = -ix; + accel[1] = iz; + accel[2] = -iy; + gyro[0] = -gx; + gyro[1] = gz; + gyro[2] = -gy; + break; + case Gamepad::PadIn::MOTION_SRC_SWITCH_PRO: + case Gamepad::PadIn::MOTION_SRC_SWITCH_USB: + accel[0] = -iy; + accel[1] = -ix; + accel[2] = -iz; + gyro[0] = -gy; + gyro[1] = -gx; + gyro[2] = -gz; + break; + default: + break; + } +} + +inline int16_t clamp_i16(int32_t v) +{ + if (v > 32767) { + return 32767; + } + if (v < -32768) { + return -32768; + } + return static_cast(v); +} + +/** + * Encode PadIn motion into one Switch Pro IMU sample (little-endian int16 + * accel[3] + gyro[3]). PadIn units: Switch sources use Pro-native accel and + * gyro*1000; DS4/DS5 use DS4 playing-frame units (8192/G); Wii uses Wiimote-frame + * accel (DS4 units/g) and **Switch-native** gyro (see apply_wii_pseudo_gyro). + */ +inline void encode_switch_imu_sample(SwitchImuSample& out, const Gamepad::PadIn& gp_in) +{ + if (!gp_in.has_motion()) { + out.accel[0] = 0; + out.accel[1] = 0; + out.accel[2] = static_cast(kSwitchDefaultAccelScale); /* +1 G face-up */ + out.gyro[0] = out.gyro[1] = out.gyro[2] = 0; + return; + } + + int32_t ax = gp_in.accel[0]; + int32_t ay = gp_in.accel[1]; + int32_t az = gp_in.accel[2]; + int32_t gx = gp_in.gyro[0]; + int32_t gy = gp_in.gyro[1]; + int32_t gz = gp_in.gyro[2]; + + int32_t sx = 0; + int32_t sy = 0; + int32_t sz = 0; + int32_t sgx = 0; + int32_t sgy = 0; + int32_t sgz = 0; + + switch (gp_in.motion_source) { + case Gamepad::PadIn::MOTION_SRC_SWITCH_PRO: + case Gamepad::PadIn::MOTION_SRC_SWITCH_USB: + out.accel[0] = clamp_i16(ax); + out.accel[1] = clamp_i16(ay); + out.accel[2] = clamp_i16(az); + out.gyro[0] = clamp_i16(gx / kSwitchGyroPrecScale); + out.gyro[1] = clamp_i16(gy / kSwitchGyroPrecScale); + out.gyro[2] = clamp_i16(gz / kSwitchGyroPrecScale); + return; + case Gamepad::PadIn::MOTION_SRC_WII_BT: + /* Accel: Wiimote → Switch frame (DS4 units → Pro scale). */ + sx = -az; + sy = ax; + sz = ay; + out.accel[0] = clamp_i16((sx * kSwitchDefaultAccelScale) / kDs4UnitsPerG); + out.accel[1] = clamp_i16((sy * kSwitchDefaultAccelScale) / kDs4UnitsPerG); + out.accel[2] = clamp_i16((sz * kSwitchDefaultAccelScale) / kDs4UnitsPerG); + /* Gyro already Switch-native; same axis remap (no /1000). */ + out.gyro[0] = clamp_i16(-gz); + out.gyro[1] = clamp_i16(gx); + out.gyro[2] = clamp_i16(gy); + return; + default: + /* DS4/DS5 PadIn is DS4 playing frame; invert Switch→DS4 remap. */ + sx = -ay; + sy = -ax; + sz = -az; + sgx = -gy; + sgy = -gx; + sgz = -gz; + break; + } + + out.accel[0] = clamp_i16((sx * kSwitchDefaultAccelScale) / kDs4UnitsPerG); + out.accel[1] = clamp_i16((sy * kSwitchDefaultAccelScale) / kDs4UnitsPerG); + out.accel[2] = clamp_i16((sz * kSwitchDefaultAccelScale) / kDs4UnitsPerG); + out.gyro[0] = clamp_i16(sgx / kSwitchGyroPrecScale); + out.gyro[1] = clamp_i16(sgy / kSwitchGyroPrecScale); + out.gyro[2] = clamp_i16(sgz / kSwitchGyroPrecScale); +} + +/** + * Write three IMU samples (36 bytes) at dest — Pro report 0x30 layout. + * Real Pro spaces samples ~5 ms apart; keep a short history so hosts that + * differentiate slots (or average them) see motion instead of three copies. + * + * Wii pseudo-gyro is a one-shot impulse: do **not** use history (it would + * re-emit old gyro in older slots for 2–3 reports). Only the newest slot gets + * gyro; the other two carry accel only. + */ +struct SwitchImuHistory +{ + SwitchImuSample samples[3]; + bool valid; +}; + +inline void encode_switch_imu_report(uint8_t* dest, const Gamepad::PadIn& gp_in, SwitchImuHistory& hist) +{ + SwitchImuSample sample{}; + encode_switch_imu_sample(sample, gp_in); + + if (gp_in.motion_source == Gamepad::PadIn::MOTION_SRC_WII_BT) { + SwitchImuSample accel_only = sample; + accel_only.gyro[0] = accel_only.gyro[1] = accel_only.gyro[2] = 0; + std::memcpy(dest + 0 * sizeof(sample), &accel_only, sizeof(sample)); + std::memcpy(dest + 1 * sizeof(sample), &accel_only, sizeof(sample)); + std::memcpy(dest + 2 * sizeof(sample), &sample, sizeof(sample)); + hist.valid = false; + return; + } + + if (!hist.valid) { + hist.samples[0] = hist.samples[1] = hist.samples[2] = sample; + hist.valid = true; + } else { + hist.samples[0] = hist.samples[1]; + hist.samples[1] = hist.samples[2]; + hist.samples[2] = sample; + } + for (int i = 0; i < 3; i++) { + std::memcpy(dest + static_cast(i) * sizeof(sample), &hist.samples[i], sizeof(sample)); + } +} + +} // namespace MotionImu + +#endif // _MOTION_IMU_H_ diff --git a/Firmware/RP2040/src/OGXMini/Board/ESP32_Bluepad32_I2C.cpp b/Firmware/RP2040/src/OGXMini/Board/ESP32_Bluepad32_I2C.cpp index b98203df..923247af 100644 --- a/Firmware/RP2040/src/OGXMini/Board/ESP32_Bluepad32_I2C.cpp +++ b/Firmware/RP2040/src/OGXMini/Board/ESP32_Bluepad32_I2C.cpp @@ -16,6 +16,7 @@ #include "Board/board_api.h" #include "Board/esp32_api.h" #include "Gamepad/Gamepad.h" +#include "Gamepad/I2CWirePad.h" #include "TaskQueue/TaskQueue.h" enum class PacketID : uint8_t { @@ -31,7 +32,7 @@ struct PacketIn { PacketID packet_id{PacketID::SET_PAD}; uint8_t index{0}; DeviceDriverType device_type{DeviceDriverType::NONE}; - Gamepad::PadIn pad_in{Gamepad::PadIn()}; + I2CWirePadIn pad_in{}; uint8_t reserved[5]{0}; }; static_assert(sizeof(PacketIn) == 32, "i2c_driver_esp::PacketIn size mismatch"); @@ -73,7 +74,7 @@ static inline void slave_handler(i2c_inst_t *i2c, i2c_slave_event_t event) { switch (packet_in.packet_id) { case PacketID::SET_PAD: if (packet_in.index < MAX_GAMEPADS) { - _gamepads[packet_in.index].set_pad_in(packet_in.pad_in); + _gamepads[packet_in.index].set_pad_in(i2c_pad_from_wire(packet_in.pad_in)); } break; case PacketID::SET_DRIVER: diff --git a/Firmware/RP2040/src/OGXMini/Board/ESP32_Blueretro_I2C.cpp b/Firmware/RP2040/src/OGXMini/Board/ESP32_Blueretro_I2C.cpp index 1a0f3ff5..a713cf2c 100644 --- a/Firmware/RP2040/src/OGXMini/Board/ESP32_Blueretro_I2C.cpp +++ b/Firmware/RP2040/src/OGXMini/Board/ESP32_Blueretro_I2C.cpp @@ -113,9 +113,10 @@ static void core1_task() { } void set_gp_check_timer(uint32_t task_id) { +#if !defined(CONFIG_OGXM_FIXED_DRIVER) || defined(CONFIG_OGXM_FIXED_DRIVER_ALLOW_COMBOS) UserSettings& user_settings = UserSettings::get_instance(); - - TaskQueue::Core0::queue_delayed_task(task_id, UserSettings::GP_CHECK_DELAY_MS, true, + + TaskQueue::Core0::queue_delayed_task(task_id, UserSettings::GP_CHECK_DELAY_MS, true, [&user_settings] { //Check gamepad inputs for button combo to change usb device driver if (user_settings.check_for_driver_change(_gamepads[0])) { @@ -124,6 +125,9 @@ void set_gp_check_timer(uint32_t task_id) { user_settings.store_driver_type(user_settings.get_current_driver()); } }); +#else + (void)task_id; // Fixed output, combos disabled +#endif } void run_uart_bridge() { diff --git a/Firmware/RP2040/src/OGXMini/Board/Four_Channel_I2C.cpp b/Firmware/RP2040/src/OGXMini/Board/Four_Channel_I2C.cpp index 5b86f353..bb2a3b36 100644 --- a/Firmware/RP2040/src/OGXMini/Board/Four_Channel_I2C.cpp +++ b/Firmware/RP2040/src/OGXMini/Board/Four_Channel_I2C.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -19,6 +20,7 @@ #include "Board/ogxm_log.h" #include "UserSettings/UserSettings.h" #include "Gamepad/Gamepad.h" +#include "Gamepad/I2CWirePad.h" #include "TaskQueue/TaskQueue.h" constexpr uint32_t FEEDBACK_DELAY_MS = 250; @@ -53,7 +55,7 @@ namespace I2C { struct PacketIn { uint8_t packet_len{sizeof(PacketIn)}; PacketID packet_id{PacketID::PAD}; - Gamepad::PadIn pad_in{Gamepad::PadIn()}; + I2CWirePadIn pad_in{}; Gamepad::ChatpadIn chatpad_in{0}; uint8_t reserved[4]{0}; }; @@ -127,7 +129,7 @@ namespace I2C { // instance_->packet_in_ = *packet_in_p; // *packet_out_p = instance_->packet_out_; // instance_->new_pad_in_.store(true); - _gamepads[0].set_pad_in(packet_in_p->pad_in); + _gamepads[0].set_pad_in(i2c_pad_from_wire(packet_in_p->pad_in)); if (_gamepads[0].new_pad_out()) { packet_out_p->pad_out = _gamepads[0].get_pad_out(); } @@ -186,7 +188,7 @@ namespace I2C { }; static constexpr size_t NUM_SLAVES = MAX_GAMEPADS - 1; - static_assert(NUM_SLAVES > 0, "I2CMaster::NUM_SLAVES must be greater than 0 to use I2C"); + // NUM_SLAVES may be 0 when MAX_GAMEPADS==1 (slave-only or master with no I2C slaves) std::array _slaves; @@ -248,7 +250,7 @@ namespace I2C { if (packet_cmd.status == Status::READY) { Gamepad& gamepad = _gamepads[i + 1]; PacketIn packet_in; - packet_in.pad_in = gamepad.get_pad_in(); + packet_in.pad_in = i2c_wire_pad_from(gamepad.get_pad_in()); packet_in.chatpad_in = gamepad.get_chatpad_in(); if (write_blocking(slave.address, &packet_in, sizeof(PacketIn))) { @@ -263,7 +265,7 @@ namespace I2C { } static void xbox360w_connect(bool connected, uint8_t idx) { - if (idx < 1 || idx >= MAX_GAMEPADS) { + if (idx < 1 || idx > NUM_SLAVES) { return; } // This function can be called from core1 @@ -354,7 +356,7 @@ void core1_task() { tuh_init(BOARD_TUH_RHPORT); uint32_t tid_feedback = TaskQueue::Core1::get_new_task_id(); - TaskQueue::Core1::queue_delayed_task(tid_feedback, FEEDBACK_DELAY_MS, true, + TaskQueue::Core1::queue_delayed_task(tid_feedback, FEEDBACK_DELAY_MS, true, [&host_manager] { host_manager.send_feedback(); }); @@ -366,8 +368,9 @@ void core1_task() { } void set_gp_check_timer(uint32_t task_id) { +#if !defined(CONFIG_OGXM_FIXED_DRIVER) || defined(CONFIG_OGXM_FIXED_DRIVER_ALLOW_COMBOS) UserSettings& user_settings = UserSettings::get_instance(); - TaskQueue::Core0::queue_delayed_task(task_id, UserSettings::GP_CHECK_DELAY_MS, true, + TaskQueue::Core0::queue_delayed_task(task_id, UserSettings::GP_CHECK_DELAY_MS, true, [&user_settings] { //Check gamepad inputs for button combo to change usb device driver if (user_settings.check_for_driver_change(_gamepads[0])) @@ -376,6 +379,9 @@ void set_gp_check_timer(uint32_t task_id) { user_settings.store_driver_type(user_settings.get_current_driver()); } }); +#else + (void)task_id; // Fixed output, combos disabled +#endif } void four_ch_i2c::wireless_connected(bool connected, uint8_t idx) { @@ -418,7 +424,7 @@ void four_ch_i2c::initialize() { board_api::init_board(); for (uint8_t i = 0; i < MAX_GAMEPADS; ++i) { - _gamepads[i].set_profile(user_settings.get_profile_by_index(i)); + _gamepads[i].set_profile(user_settings.get_profile_by_index(i), user_settings.get_current_driver()); } DeviceManager::get_instance().initialize_driver(user_settings.get_current_driver(), _gamepads); @@ -444,17 +450,21 @@ void four_ch_i2c::run() { if (I2C::role() == I2C::Role::MASTER) { while (true) { TaskQueue::Core0::process_tasks(); + tud_task(); I2C::Master::process(); device_driver->process(0, _gamepads[0]); - tud_task(); - sleep_ms(1); +#if MAIN_LOOP_DELAY_US > 0 + sleep_us(MAIN_LOOP_DELAY_US); +#endif } } else { while (true) { TaskQueue::Core0::process_tasks(); - device_driver->process(0, _gamepads[0]); tud_task(); - sleep_ms(1); + device_driver->process(0, _gamepads[0]); +#if MAIN_LOOP_DELAY_US > 0 + sleep_us(MAIN_LOOP_DELAY_US); +#endif } } } diff --git a/Firmware/RP2040/src/OGXMini/Board/PicoW.cpp b/Firmware/RP2040/src/OGXMini/Board/PicoW.cpp index 929d2497..0d15c920 100644 --- a/Firmware/RP2040/src/OGXMini/Board/PicoW.cpp +++ b/Firmware/RP2040/src/OGXMini/Board/PicoW.cpp @@ -1,33 +1,405 @@ #include "Board/Config.h" #include "OGXMini/Board/PicoW.h" -#if (OGXM_BOARD == PI_PICOW) +#if defined(OGXM_BOARD_USES_PICO_W_FIRMWARE) +#include +#include #include +#include #include - +#include #include "tusb.h" #include "bsp/board_api.h" #include "USBDevice/DeviceManager.h" +#include "USBDevice/DeviceDriver/DeviceDriver.h" +#include "USBDevice/DeviceDriver/DeviceDriverTypes.h" +#include "USBDevice/DeviceDriver/PS1PS2/psx_simulator.h" +#include "USBDevice/DeviceDriver/GameCube/gc_simulator.h" +#include "USBDevice/DeviceDriver/Dreamcast/Dreamcast.h" +#include "USBDevice/DeviceDriver/N64/N64.h" +#include "USBHost/GPIOHost/GPIOHost.h" #include "UserSettings/UserSettings.h" +#include "Board/Config.h" #include "Board/board_api.h" +#include "Board/board_api_private/board_api_private.h" +#include "Board/ogxm_log.h" #include "Bluepad32/Bluepad32.h" #include "BLEServer/BLEServer.h" #include "Gamepad/Gamepad.h" #include "TaskQueue/TaskQueue.h" +#if defined(CONFIG_EN_USB_HOST) +#include "host/hcd.h" +#include "pio_usb.h" +#include "USBHost/HostManager.h" +#include +#include "Wii/WiiReportConverter.h" + +/** Called from flash_safe_execute on Core1; only write flash. Reboot is done by caller after return. */ +static void store_driver_type_callback(void* arg) { + auto d = *static_cast(arg); + UserSettings::get_instance().store_driver_type_only(d); +} + +extern "C" { +#include "Wii/wiimote-lib/wiimote.h" +#include "Wii/wiimote-lib/wiimote_btstack.h" +} + +static WiimoteReport s_wiimote_report = {}; // shared: Core0 reads, Core1 writes + +extern "C" void wii_led_on(void) { board_api::set_led(true); } +extern "C" void wii_led_off(void) { board_api::set_led(false); } +#endif + Gamepad _gamepads[MAX_GAMEPADS]; +#if defined(CONFIG_EN_USB_HOST) +// PIO USB host normally uses an alarm_pool SOF timer (~1 kHz). On Pico W that collides with BTstack / TaskQueue +// timer IRQs (see prior irq_set_exclusive_handler assert) and fixed alarm indices (alarm 3 "already claimed"). +// Use skip_alarm_pool + pio_usb_host_frame() every ~1 ms from Core0 main loop (or Wii Core1 loop). +// Do not run this from BTstack timers / execute_on_main_thread — TinyUSB uses sleep_ms during enumeration. + +// Debounce D+/D- in real time before tuh_init (not "N mux calls") so we can safely run multiple +// host frames per main-loop visit when catching up SOFs without shortening this delay. +constexpr int32_t PIO_LINE_STABLE_US = 40 * 1000; +/** Wall-time spacing for send_feedback — do not tie to SOF catch-up tick count (bursts would hammer the device). */ +constexpr uint32_t USB_FEEDBACK_INTERVAL_MS = 200; + +static bool s_pio_usb_tuh_inited = false; +/** When true, `pio_usb_host_frame()` runs from SDK repeating_timer at 1 kHz (not from main-loop catch-up). */ +static bool s_pico_w_usb_sof_hw_timer = false; +static repeating_timer_t s_pico_w_usb_sof_timer; +static bool s_bt_new_conn_off_for_usb = false; +/** Line high continuously since this time (valid while s_usb_line_debounce_armed). */ +static absolute_time_t s_usb_line_high_since; +static bool s_usb_line_debounce_armed = false; +static uint32_t s_last_usb_feedback_wall_ms = 0; + +static bool pico_w_usb_sof_timer_cb(repeating_timer_t* rt) { + (void)rt; + pio_usb_host_frame(); + return true; +} + +static void pico_w_usb_sof_timer_start() { + if (s_pico_w_usb_sof_hw_timer) { + return; + } + /* skip_alarm_pool: PIO-USB does not start its own SOF timer. Main-loop + sleep_ms(1) cannot hold 1 kHz + * SOF under CYW43/BT load — wired input dies in ~1–2 s. Use SDK repeating_timer (separate from + * the alarm_pool that collides on Pico W) like a dedicated Core1 loop in Wii mode. */ + if (add_repeating_timer_us(-1000, pico_w_usb_sof_timer_cb, nullptr, &s_pico_w_usb_sof_timer)) { + s_pico_w_usb_sof_hw_timer = true; + } +} + +static void pico_w_usb_sof_timer_stop() { + if (!s_pico_w_usb_sof_hw_timer) { + return; + } + cancel_repeating_timer(&s_pico_w_usb_sof_timer); + s_pico_w_usb_sof_hw_timer = false; +} + +/** Debounce any unplug hint (line / stack / idle) past USB bus reset glitches (10–50 ms). */ +constexpr int32_t USB_UNPLUG_DEBOUNCE_US = 60 * 1000; +/** If D+/D− never read as SE0 under PIO, IN reports stop when the cable is pulled — detect unplug this way. + * Keep high enough that HID pads that only report on change are not mistaken for unplug. + * 360 wireless receiver sends status even with no controller paired. */ +constexpr uint32_t USB_UNPLUG_NO_INPUT_MS = 10000; +static bool s_usb_unplug_debounce_armed = false; +static absolute_time_t s_usb_unplug_debounce_since; + +static bool pico_w_tuh_any_device_configured() { + /* usbh.c: TOTAL_DEVICES = CFG_TUH_DEVICE_MAX + CFG_TUH_HUB (hub consumes one address slot). */ + const uint8_t addr_max = static_cast(CFG_TUH_DEVICE_MAX + CFG_TUH_HUB); + for (uint8_t d = 1; d <= addr_max; ++d) { + if (tuh_mounted(d)) { + return true; + } + } + return false; +} + +static void pico_w_usb_host_full_stop() { + pico_w_usb_sof_timer_stop(); + (void)tuh_deinit(BOARD_TUH_RHPORT); + s_pio_usb_tuh_inited = false; + s_usb_line_debounce_armed = false; + s_usb_unplug_debounce_armed = false; + /* GPIO IRQs were suspended for PIO; restore so unplug/plug is visible again without "shorting" the port. */ + board_api_usbh::enable_host_line_irq_monitoring(); +} + +/** + * Runs every ~1 ms from Core0 (normal Pico W modes) or from Core1 (GPIO modes that block Core0 in run_task). + * - If any BT gamepad is connected: PIO USB host is not initialized (wired ignored until BT disconnects). + * - If a wired device enumerates on the PIO port: disconnect BT pads and block new BT connections until unplugged. + */ +static void pico_w_pio_usb_bt_mux_tick() { + HostManager& hm = HostManager::get_instance(); + const bool bt_active = bluepad32::any_connected(); + const bool wired_mounted = hm.any_mounted(); + + /* Only tear down PIO USB host for BT when no wired pad is mounted. Otherwise a slow BT + * disconnect after wired takeover (or a stray ACL/device_ready) leaves s_bt_any_connected_cached + * true for ~seconds while USB is working — we would tuh_deinit() here and kill wired input + * until replug. Wired wins while HostManager still has a device slot. */ + if (bt_active && !wired_mounted) { + if (s_pio_usb_tuh_inited) { + pico_w_usb_host_full_stop(); + } + return; + } + + /* Unplug: pio_usb_bus_get_line_state() uses gpio_get on D+/D− while PIO owns them — often never + * reads SE0 when the cable is removed (floating FS-idle), so HCD "connect" stays true. Use any of: + * HCD line, TinyUSB configured device gone, or no process_report() for USB_UNPLUG_NO_INPUT_MS. */ + if (s_pio_usb_tuh_inited) { + const bool hcd_line_connected = hcd_port_connect_status(BOARD_TUH_RHPORT); + board_api_usbh::store_host_line_connected(hcd_line_connected); + + const bool tuh_still_has_device = pico_w_tuh_any_device_configured(); + const uint32_t input_idle_ms = hm.usb_host_input_idle_ms(); + + const bool unplug_hint = wired_mounted && + (!hcd_line_connected || !tuh_still_has_device || (input_idle_ms >= USB_UNPLUG_NO_INPUT_MS)); + + if (unplug_hint) { + if (!s_usb_unplug_debounce_armed) { + s_usb_unplug_debounce_armed = true; + s_usb_unplug_debounce_since = get_absolute_time(); + } else if (absolute_time_diff_us(s_usb_unplug_debounce_since, get_absolute_time()) >= USB_UNPLUG_DEBOUNCE_US) { + pico_w_usb_host_full_stop(); + s_bt_new_conn_off_for_usb = false; + bluepad32::wired_usb_release_enable_bt_pairing(); + return; + } + } else { + s_usb_unplug_debounce_armed = false; + } + } + + if (!s_pio_usb_tuh_inited) { + // Sample D+/D− directly: host_connected() is IRQ-driven and can stay false if an edge was + // missed (Core0 load, CYW43), or the ISR left IRQs disabled while lines were high. + // Pins are still GPIO inputs here; PIO has not taken over yet. + const bool line = (gpio_get(PIO_USB_DP_PIN) != 0) || (gpio_get(PIO_USB_DP_PIN + 1) != 0); + if (!line) { + s_usb_line_debounce_armed = false; + return; + } + if (!s_usb_line_debounce_armed) { + s_usb_line_high_since = get_absolute_time(); + s_usb_line_debounce_armed = true; + } + if (absolute_time_diff_us(s_usb_line_high_since, get_absolute_time()) < PIO_LINE_STABLE_US) { + return; + } + // main() already called flash_safe_execute_core_init() on Core0; do not call from Core1 BT timer (deadlock risk). + hm.initialize(_gamepads); + // PIO USB takes over D+/D−; GPIO edge IRQs on those pins must be off or IO_IRQ floods and BT stalls. + board_api_usbh::suspend_line_irq(); + pio_usb_configuration_t pio_cfg = PIO_USB_CONFIG; + pio_cfg.alarm_pool = nullptr; + pio_cfg.skip_alarm_pool = true; + pio_cfg.tx_ch = 2; + tuh_configure(BOARD_TUH_RHPORT, TUH_CFGID_RPI_PIO_USB_CONFIGURATION, &pio_cfg); + if (!tuh_init(BOARD_TUH_RHPORT)) { + s_usb_line_debounce_armed = false; + return; + } + s_pio_usb_tuh_inited = true; + pico_w_usb_sof_timer_start(); + } + + if (!s_pico_w_usb_sof_hw_timer) { + pio_usb_host_frame(); + } + + // Host drivers (e.g. Xbox360W chatpad) queue Core1 delayed tasks; safe to drain from Core0 (spinlocks). + TaskQueue::Core1::process_tasks(); + + tuh_task(); + + { + const uint32_t now_ms = to_ms_since_boot(get_absolute_time()); + if ((now_ms - s_last_usb_feedback_wall_ms) >= USB_FEEDBACK_INTERVAL_MS) { + s_last_usb_feedback_wall_ms = now_ms; + hm.send_feedback(); + } + } + + if (wired_mounted) { + if (!s_bt_new_conn_off_for_usb) { + bluepad32::wired_usb_takeover_disconnect_bt(); + s_bt_new_conn_off_for_usb = true; + } + } else { + if (s_bt_new_conn_off_for_usb) { + bluepad32::wired_usb_release_enable_bt_pairing(); + s_bt_new_conn_off_for_usb = false; + } + } +} + +void pico_w::poll_usb_host_mux_from_core0() { + static absolute_time_t next_usb_mux; + static bool usb_mux_time_inited = false; + if (!usb_mux_time_inited) { + next_usb_mux = get_absolute_time(); + usb_mux_time_inited = true; + } + absolute_time_t now = get_absolute_time(); + + if (!s_pio_usb_tuh_inited) { + if (absolute_time_diff_us(next_usb_mux, now) < 0) { + return; + } + next_usb_mux = delayed_by_us(next_usb_mux, 1000); + pico_w_pio_usb_bt_mux_tick(); + return; + } + + if (s_pico_w_usb_sof_hw_timer) { + /* SOF is 1 kHz from repeating_timer; run TinyUSB + mux every visit (no 1 ms gate). */ + pico_w_pio_usb_bt_mux_tick(); + return; + } + + if (absolute_time_diff_us(next_usb_mux, now) < 0) { + return; + } + // Fallback if hardware SOF timer failed: manual frames + catch-up (Wii Core1–style burst). + constexpr int k_max_catchup_frames = 500; + for (int n = 0; n < k_max_catchup_frames && absolute_time_diff_us(next_usb_mux, now) >= 0; ++n) { + next_usb_mux = delayed_by_us(next_usb_mux, 1000); + pico_w_pio_usb_bt_mux_tick(); + now = get_absolute_time(); + } +} +#endif // CONFIG_EN_USB_HOST + +static DeviceDriver* s_gpio_device_driver = nullptr; +static void gpio_device_process_cb(void* ctx) { + (void)ctx; + if (s_gpio_device_driver) { + HostInputSource input_src = UserSettings::get_instance().get_input_source(); + if (input_src == HostInputSource::PSX_GPIO) { + GPIOHost::psx_host_poll(_gamepads[0]); + } else if (input_src == HostInputSource::GAMECUBE_GPIO) { + GPIOHost::joybus_host_poll(_gamepads[0]); + } else if (input_src == HostInputSource::DREAMCAST_GPIO) { + GPIOHost::dreamcast_host_poll(_gamepads[0]); + } else if (input_src == HostInputSource::N64_GPIO) { + GPIOHost::n64_host_poll(_gamepads[0]); + } + for (uint8_t i = 0; i < MAX_GAMEPADS; ++i) { + s_gpio_device_driver->process(i, _gamepads[i]); + } + bluepad32::process_pending_adaptive_triggers(); + } +} + +#if defined(CONFIG_EN_USB_HOST) +constexpr uint32_t FEEDBACK_DELAY_MS = 200; + +void core1_task_wii_usb_host() { + OGXM_LOG("WII Core1: entry\n"); + flash_safe_execute_core_init(); // so Core0 can flash_safe_execute when saving Wii address (TLV) + HostManager& host_manager = HostManager::get_instance(); + host_manager.initialize(_gamepads); + OGXM_LOG("WII Core1: HostManager init done\n"); + + OGXM_LOG("WII Core1: waiting for host_connected() (plug USB controller into external port GP0/GP1)\n"); + uint32_t wait_count = 0; + while (!board_api::usb::host_connected()) { + sleep_ms(100); + if ((++wait_count % 20) == 0) { + OGXM_LOG("WII Core1: still waiting for host_connected()...\n"); + } + } + OGXM_LOG("WII Core1: host_connected\n"); + + pio_usb_configuration_t pio_cfg = PIO_USB_CONFIG; + pio_cfg.alarm_pool = nullptr; + pio_cfg.skip_alarm_pool = true; + // CYW43 (BT on Core0) claims DMA 0 and 1; use channel 2 for PIO USB TX to avoid "DMA channel 0 is already claimed" + pio_cfg.tx_ch = 2; + tuh_configure(BOARD_TUH_RHPORT, TUH_CFGID_RPI_PIO_USB_CONFIGURATION, &pio_cfg); + OGXM_LOG("WII Core1: tuh_configure done\n"); + tuh_init(BOARD_TUH_RHPORT); + OGXM_LOG("WII Core1: tuh_init done\n"); + + uint32_t tid_feedback = TaskQueue::Core1::get_new_task_id(); + TaskQueue::Core1::queue_delayed_task(tid_feedback, FEEDBACK_DELAY_MS, true, + [&host_manager]() { host_manager.send_feedback(); }); + OGXM_LOG("WII Core1: feedback task queued, entering loop\n"); + + s_wiimote_report.mode = (uint8_t)NO_EXTENSION; // Wiimote only; home+down cycles to Nunchuk, then Classic + +#if !defined(CONFIG_OGXM_FIXED_DRIVER) || defined(CONFIG_OGXM_FIXED_DRIVER_ALLOW_COMBOS) + uint32_t last_gp_check_ms = 0; // throttle: same 600 ms / 3 s hold as other modes + uint32_t gp_check_ticks = 0; +#endif + + absolute_time_t next_pio_host_frame = get_absolute_time(); + while (true) { + absolute_time_t now = get_absolute_time(); + while (absolute_time_diff_us(next_pio_host_frame, now) >= 0) { + pio_usb_host_frame(); + next_pio_host_frame = delayed_by_us(next_pio_host_frame, 1000); + now = get_absolute_time(); + } + TaskQueue::Core1::process_tasks(); + tuh_task(); + Wii::gamepad_to_wiimote_report(_gamepads[0], &s_wiimote_report); + +#if !defined(CONFIG_OGXM_FIXED_DRIVER) || defined(CONFIG_OGXM_FIXED_DRIVER_ALLOW_COMBOS) + const uint32_t now_ms = board_api::ms_since_boot(); + if (now_ms - last_gp_check_ms >= static_cast(UserSettings::GP_CHECK_DELAY_MS)) { + last_gp_check_ms = now_ms; + UserSettings& user_settings = UserSettings::get_instance(); + bool changed = user_settings.check_for_driver_change(_gamepads[0]); +#if defined(CONFIG_OGXM_DEBUG) + if ((++gp_check_ticks % 10) == 0) + OGXM_LOG("WII Core1: gp check tick " + std::to_string(gp_check_ticks) + "\n"); +#endif + if (changed) { + static DeviceDriverType new_driver; + new_driver = user_settings.get_current_driver(); + OGXM_LOG("WII Core1: driver change detected, new_driver=" + OGXM_TO_STRING(new_driver) + ", calling flash_safe_execute\n"); + int fs_ret = flash_safe_execute(store_driver_type_callback, &new_driver, 200); + OGXM_LOG("WII Core1: flash_safe_execute returned " + std::to_string(fs_ret) + " (0=ok)\n"); + if (fs_ret == 0) { + OGXM_LOG("WII Core1: calling reboot\n"); + board_api::reboot(); + } + } + } +#endif + } +} +#endif + void core1_task() { + OGXM_LOG("PicoW Core1: entry\n"); board_api::init_bluetooth(); + OGXM_LOG("PicoW Core1: init_bluetooth done\n"); board_api::set_led(true); + OGXM_LOG("PicoW Core1: set_led(true) done\n"); BLEServer::init_server(_gamepads); + OGXM_LOG("PicoW Core1: BLEServer init done, entering run_task\n"); + /* PIO USB mux runs on Core0 main loop (poll_usb_host_mux_from_core0), not here — avoids sleep_ms panic. */ bluepad32::run_task(_gamepads); } void set_gp_check_timer(uint32_t task_id) { +#if !defined(CONFIG_OGXM_FIXED_DRIVER) || defined(CONFIG_OGXM_FIXED_DRIVER_ALLOW_COMBOS) UserSettings& user_settings = UserSettings::get_instance(); - TaskQueue::Core0::queue_delayed_task(task_id, UserSettings::GP_CHECK_DELAY_MS, true, + TaskQueue::Core0::queue_delayed_task(task_id, UserSettings::GP_CHECK_DELAY_MS, true, [&user_settings] { //Check gamepad inputs for button combo to change usb device driver if (user_settings.check_for_driver_change(_gamepads[0])) { @@ -35,47 +407,199 @@ void set_gp_check_timer(uint32_t task_id) { user_settings.store_driver_type(user_settings.get_current_driver()); } }); +#else + (void)task_id; // Fixed output, combos disabled +#endif } void pico_w::initialize() { + OGXM_LOG("PicoW init: start\n"); board_api::init_board(); + OGXM_LOG("PicoW init: board inited\n"); UserSettings& user_settings = UserSettings::get_instance(); user_settings.initialize_flash(); +#if defined(CONFIG_OGXM_DEBUG) + OGXM_LOG("PicoW init: flash inited, driver=" + OGXM_TO_STRING(user_settings.get_current_driver()) + "\n"); +#else + OGXM_LOG("PicoW init: flash inited\n"); +#endif for (uint8_t i = 0; i < MAX_GAMEPADS; ++i) { - _gamepads[i].set_profile(user_settings.get_profile_by_index(i)); + _gamepads[i].set_profile(user_settings.get_profile_by_index(i), user_settings.get_current_driver()); } DeviceManager& device_manager = DeviceManager::get_instance(); device_manager.initialize_driver(user_settings.get_current_driver(), _gamepads); +#if defined(CONFIG_EN_USB_HOST) + HostManager::get_instance().initialize(_gamepads); +#endif + OGXM_LOG("PicoW init: driver inited\n"); } void pico_w::run() { + OGXM_LOG("PicoW run: start\n"); multicore_reset_core1(); - multicore_launch_core1(core1_task); - - uint32_t tid_gp_check = TaskQueue::Core0::get_new_task_id(); - set_gp_check_timer(tid_gp_check); + UserSettings& user_settings = UserSettings::get_instance(); DeviceDriver* device_driver = DeviceManager::get_instance().get_driver(); + DeviceDriverType current_driver = user_settings.get_current_driver(); + const bool wii_mode = (current_driver == DeviceDriverType::WII); + const bool gpio_device_mode = (current_driver == DeviceDriverType::PS1PS2 || current_driver == DeviceDriverType::GAMECUBE || current_driver == DeviceDriverType::DREAMCAST); + OGXM_LOG("PicoW run: wii_mode=" + std::string(wii_mode ? "1" : "0") + " gpio_device_mode=" + std::string(gpio_device_mode ? "1" : "0") + "\n"); + +#if defined(CONFIG_EN_USB_HOST) + if (wii_mode) { + OGXM_LOG("PicoW run: launching Core1 (Wii USB host)\n"); + multicore_launch_core1(core1_task_wii_usb_host); + OGXM_LOG("PicoW run: Core1 launched, starting Wiimote BT (discoverable as Nintendo RVL-CNT-01)\n"); + board_api::init_bluetooth(); + wiimote_emulator_set_led(wii_led_on, wii_led_off); + wiimote_emulator(&s_wiimote_report); // never returns: BT run loop, LED blinks until connected + } else +#endif + if (gpio_device_mode) { + /* PS2 on Pico W: same as Switch/PS3 – Core1 = BT, Core0 = main loop. Core1 writes _gamepads, Core0 reads and calls process() + psx_device_poll(). */ + const bool ps2_poll_mode = (current_driver == DeviceDriverType::PS1PS2); + if (ps2_poll_mode) { + psx_device_set_poll_mode(true); + OGXM_LOG("PicoW run: PS2 poll mode (BT on Core1, protocol on Core0)\n"); + } + OGXM_LOG("PicoW run: GPIO device mode\n"); + HostInputSource input_src = user_settings.get_input_source(); + s_gpio_device_driver = device_driver; + if (input_src == HostInputSource::PSX_GPIO) { + GPIOHost::psx_host_init(0, 20, 19); + } else if (input_src == HostInputSource::GAMECUBE_GPIO && current_driver != DeviceDriverType::GAMECUBE) { + /* Skip joybus host when in GameCube device mode: Core1 uses GPIO 19 for console output. */ + GPIOHost::joybus_host_init(0, 19); + } else if (input_src == HostInputSource::DREAMCAST_GPIO) { + GPIOHost::dreamcast_host_init(1, 0, 10, -1); + } else if (input_src == HostInputSource::N64_GPIO) { + GPIOHost::n64_host_init(0, 19); + } + bluepad32::set_gpio_device_process_callback(gpio_device_process_cb, nullptr); + /* Present as USB device to console immediately so PS2/GC/Dreamcast see the adapter at boot. */ + tud_init(BOARD_TUD_RHPORT); + OGXM_LOG("PicoW run: tud_init done (GPIO device mode)\n"); + if (ps2_poll_mode) { + /* PS2: BT on Core1 (like Switch/PS3); Core0 main loop does process() + psx_device_poll(). */ + OGXM_LOG("PicoW run: launching Core1 (BT)\n"); + multicore_launch_core1(core1_task); + } else { + OGXM_LOG("PicoW run: Core0 calling init_bluetooth/set_led/BLEServer\n"); + board_api::init_bluetooth(); + board_api::set_led(true); + BLEServer::init_server(_gamepads); + OGXM_LOG("PicoW run: Core0 BT/LED/BLEServer done, launching Core1\n"); + if (current_driver == DeviceDriverType::DREAMCAST) { + dreamcast_set_core1_device_mode(input_src != HostInputSource::DREAMCAST_GPIO); + } + if (current_driver == DeviceDriverType::GAMECUBE) { + multicore_launch_core1(gamecube_core1_entry); + } else if (current_driver == DeviceDriverType::N64) { + multicore_launch_core1(n64_core1_entry); + } else { + multicore_launch_core1(dreamcast_core1_entry); + } + /* Let Core1 reach flash_safe_execute_core_init() before we enter run_task (BT stack may touch flash). */ + sleep_ms(100); +#if defined(CONFIG_EN_USB_HOST) + bluepad32::set_pico_w_pio_usb_mux_tick(pico_w_pio_usb_bt_mux_tick); +#endif + bluepad32::run_task(_gamepads); /* never returns */ + } + } else { + tud_init(BOARD_TUD_RHPORT); + OGXM_LOG("PicoW run: tud_init done, launching Core1 (BT)\n"); + multicore_launch_core1(core1_task); + OGXM_LOG("PicoW run: Core1 (BT) launched\n"); + + HostInputSource input_src = user_settings.get_input_source(); + if (input_src == HostInputSource::PSX_GPIO) { + GPIOHost::psx_host_init(0, 20, 19); + } else if (input_src == HostInputSource::GAMECUBE_GPIO) { + GPIOHost::joybus_host_init(0, 19); + } else if (input_src == HostInputSource::DREAMCAST_GPIO) { + GPIOHost::dreamcast_host_init(1, 0, 10, -1); + } else if (input_src == HostInputSource::N64_GPIO) { + GPIOHost::n64_host_init(0, 19); + } + } - tud_init(BOARD_TUD_RHPORT); + uint32_t tid_gp_check = TaskQueue::Core0::get_new_task_id(); + set_gp_check_timer(tid_gp_check); + OGXM_LOG("PicoW run: gp check timer set, entering main loop\n"); + static bool mounted_logged = false; + static uint32_t loop_count = 0; + const bool ps2_poll_mode = gpio_device_mode && (current_driver == DeviceDriverType::PS1PS2); while (true) { + /* PS2 first: drain before any other work so we respond within byte time (reduces OPL pad-init hang). */ + if (ps2_poll_mode) { + while (psx_device_poll() != 0) + ; + /* Short burst: update report and drain again to catch OPL's rapid init. */ + for (uint8_t i = 0; i < MAX_GAMEPADS; ++i) + device_driver->process(i, _gamepads[i]); + while (psx_device_poll() != 0) + ; + } TaskQueue::Core0::process_tasks(); - + if (!wii_mode) { + tud_task(); + HostInputSource input_src = UserSettings::get_instance().get_input_source(); + if (input_src == HostInputSource::PSX_GPIO) { + GPIOHost::psx_host_poll(_gamepads[0]); + } else if (input_src == HostInputSource::GAMECUBE_GPIO && UserSettings::get_instance().get_current_driver() != DeviceDriverType::GAMECUBE) { + GPIOHost::joybus_host_poll(_gamepads[0]); + } else if (input_src == HostInputSource::DREAMCAST_GPIO) { + GPIOHost::dreamcast_host_poll(_gamepads[0]); + } else if (input_src == HostInputSource::N64_GPIO) { + GPIOHost::n64_host_poll(_gamepads[0]); + } + } + if (ps2_poll_mode) { + while (psx_device_poll() != 0) + ; + if (loop_count != 0 && (loop_count % 10000u) == 0) + OGXM_LOG("PicoW run: main loop psx_device_poll tick " + std::to_string(loop_count) + "\n"); + } for (uint8_t i = 0; i < MAX_GAMEPADS; ++i) { device_driver->process(i, _gamepads[i]); - tud_task(); } - sleep_ms(1); + bluepad32::process_pending_adaptive_triggers(); + if (!wii_mode) { + if (tud_mounted() && !mounted_logged) { + mounted_logged = true; + OGXM_LOG("PicoW run: USB configured\n"); + } + } else if (loop_count == 0 || loop_count == 5000) { + OGXM_LOG("PicoW run: Wii main loop tick\n"); + } + loop_count++; + /* Run USB host mux after all heavy work (device_driver->process, etc.) so pio_usb_host_frame + * catch-up covers the whole iteration; otherwise SOFs can lag for tens of ms and wired input dies. */ +#if defined(CONFIG_EN_USB_HOST) + if (!wii_mode) { + pico_w::poll_usb_host_mux_from_core0(); + } +#endif + /* With hardware SOF timer, Core0 can sleep briefly; wired USB still needs tuh_task often. + * sleep_ms(1) alone starved TinyUSB vs Wii Core1's tight loop. */ + if (!ps2_poll_mode) { +#if defined(CONFIG_EN_USB_HOST) + if (!wii_mode && s_pio_usb_tuh_inited && HostManager::get_instance().any_mounted()) { + sleep_us(400); + } else +#endif + { + sleep_ms(1); + } + } } } -// #else // (OGXM_BOARD == PI_PICOW) - -// void pico_w::initialize() {} -// void pico_w::run() {} +// #else // OGXM_BOARD_USES_PICO_W_FIRMWARE -#endif // (OGXM_BOARD == PI_PICOW) +#endif // OGXM_BOARD_USES_PICO_W_FIRMWARE diff --git a/Firmware/RP2040/src/OGXMini/Board/PicoW.h b/Firmware/RP2040/src/OGXMini/Board/PicoW.h index 2a153035..3b5a86fd 100644 --- a/Firmware/RP2040/src/OGXMini/Board/PicoW.h +++ b/Firmware/RP2040/src/OGXMini/Board/PicoW.h @@ -5,4 +5,8 @@ namespace pico_w { void initialize(); void run(); +#if defined(CONFIG_EN_USB_HOST) + /** Core0: ~1 kHz PIO USB host + BT/wired mux (must not run from BT timer / execute_on_main_thread). */ + void poll_usb_host_mux_from_core0(); +#endif } // namespace pico_w \ No newline at end of file diff --git a/Firmware/RP2040/src/OGXMini/Board/Standard.cpp b/Firmware/RP2040/src/OGXMini/Board/Standard.cpp index 1db4f58b..71f84ac0 100644 --- a/Firmware/RP2040/src/OGXMini/Board/Standard.cpp +++ b/Firmware/RP2040/src/OGXMini/Board/Standard.cpp @@ -1,63 +1,159 @@ #include "Board/Config.h" #include "OGXMini/Board/Standard.h" -#if ((OGXM_BOARD == PI_PICO) || (OGXM_BOARD == RP2040_ZERO) || (OGXM_BOARD == ADAFRUIT_FEATHER)) +#if ((OGXM_BOARD == PI_PICO) || (OGXM_BOARD == RP2040_ZERO) || (OGXM_BOARD == ADAFRUIT_FEATHER) || (OGXM_BOARD == RP2350_USB_A) || (OGXM_BOARD == RP2350_ZERO) || (OGXM_BOARD == RP2040_XIAO)) #include +#include +#include #include "tusb.h" #include "bsp/board_api.h" #include "pio_usb.h" #include "USBHost/HostManager.h" +#include "USBHost/GPIOHost/GPIOHost.h" #include "USBDevice/DeviceManager.h" +#include "USBDevice/DeviceDriver/DeviceDriverTypes.h" +#include "USBDevice/DeviceDriver/PS1PS2/psx_simulator.h" +#include "USBDevice/DeviceDriver/GameCube/gc_simulator.h" +#include "USBDevice/DeviceDriver/Dreamcast/Dreamcast.h" +#include "USBDevice/DeviceDriver/N64/N64.h" #include "TaskQueue/TaskQueue.h" #include "Gamepad/Gamepad.h" +#include "UserSettings/UserSettings.h" #include "Board/board_api.h" +#include "Board/board_api_private/board_api_private.h" #include "Board/ogxm_log.h" constexpr uint32_t FEEDBACK_DELAY_MS = 200; Gamepad _gamepads[MAX_GAMEPADS]; +static repeating_timer_t s_pio_usb_sof_timer; +static bool s_pio_usb_sof_hw_timer = false; +/** Set during intentional mode-change reboot so host_mounted(false) does not queue a second teardown. */ +static std::atomic s_driver_reboot_pending{false}; +/** Debounce transient PIO USB umount before tearing down the USB device stack. */ +static std::atomic s_host_teardown_scheduled{false}; +constexpr uint32_t HOST_UNMOUNT_DEBOUNCE_MS = 1500; + +static void pio_usb_sof_timer_stop() { + if (!s_pio_usb_sof_hw_timer) { + return; + } + cancel_repeating_timer(&s_pio_usb_sof_timer); + s_pio_usb_sof_hw_timer = false; +} + +static bool pio_usb_sof_timer_cb(repeating_timer_t* rt) { + (void)rt; + pio_usb_host_frame(); + return true; +} + +static void pio_usb_sof_timer_start() { + if (s_pio_usb_sof_hw_timer) { + return; + } + if (add_repeating_timer_us(-1000, pio_usb_sof_timer_cb, nullptr, &s_pio_usb_sof_timer)) { + s_pio_usb_sof_hw_timer = true; + } +} + +/** PIO USB has no hardware SOF; run pio_usb_host_frame() at ~1 kHz or IN/OUT transfers stall. */ +static void pio_usb_sof_service_due_frames() { + if (s_pio_usb_sof_hw_timer) { + return; + } + static absolute_time_t next_pio_frame; + static bool next_inited = false; + if (!next_inited) { + next_pio_frame = get_absolute_time(); + next_inited = true; + } + absolute_time_t now = get_absolute_time(); + while (absolute_time_diff_us(next_pio_frame, now) >= 0) { + pio_usb_host_frame(); + next_pio_frame = delayed_by_us(next_pio_frame, 1000); + now = get_absolute_time(); + } +} + +static void configure_pio_usb_host() { + pio_usb_configuration_t pio_cfg = PIO_USB_CONFIG; + pio_cfg.skip_alarm_pool = true; + tuh_configure(BOARD_TUH_RHPORT, TUH_CFGID_RPI_PIO_USB_CONFIGURATION, &pio_cfg); +} + +static void pio_usb_host_loop_tick() { + pio_usb_sof_service_due_frames(); + tuh_task(); +} + +namespace board_api_usbh { + +void stop_pio_usb_host() { + pio_usb_sof_timer_stop(); + multicore_reset_core1(); + sleep_ms(50); + (void)tuh_deinit(BOARD_TUH_RHPORT); + for (int i = 0; i < 20; ++i) { + tuh_task(); + sleep_ms(1); + } + enable_host_line_irq_monitoring(); +} + +} // namespace board_api_usbh + void core1_task() { HostManager& host_manager = HostManager::get_instance(); host_manager.initialize(_gamepads); - //Pico-PIO-USB will not reliably detect a hot plug on some boards, + //Pico-PIO-USB will not reliably detect a hot plug on some boards, //monitor and init host stack after connection while(!board_api::usb::host_connected()) { sleep_ms(100); } - pio_usb_configuration_t pio_cfg = PIO_USB_CONFIG; - tuh_configure(BOARD_TUH_RHPORT, TUH_CFGID_RPI_PIO_USB_CONFIGURATION, &pio_cfg); - + configure_pio_usb_host(); + board_api_usbh::suspend_line_irq(); tuh_init(BOARD_TUH_RHPORT); + pio_usb_sof_timer_start(); uint32_t tid_feedback = TaskQueue::Core1::get_new_task_id(); - TaskQueue::Core1::queue_delayed_task(tid_feedback, FEEDBACK_DELAY_MS, true, + TaskQueue::Core1::queue_delayed_task(tid_feedback, FEEDBACK_DELAY_MS, true, [&host_manager] { host_manager.send_feedback(); }); while (true) { + pio_usb_sof_service_due_frames(); TaskQueue::Core1::process_tasks(); tuh_task(); } } void set_gp_check_timer(uint32_t task_id) { +#if !defined(CONFIG_OGXM_FIXED_DRIVER) || defined(CONFIG_OGXM_FIXED_DRIVER_ALLOW_COMBOS) UserSettings& user_settings = UserSettings::get_instance(); - - TaskQueue::Core0::queue_delayed_task(task_id, UserSettings::GP_CHECK_DELAY_MS, true, + + TaskQueue::Core0::queue_delayed_task(task_id, UserSettings::GP_CHECK_DELAY_MS, true, [&user_settings] { - //Check gamepad inputs for button combo to change usb device driver - if (user_settings.check_for_driver_change(_gamepads[0])) { + // Check for input source change (Start+Select when in PS1PS2/GAMECUBE mode) + if (user_settings.check_for_input_source_change(_gamepads[0])) { + // No reboot; input source stored + } + // Check gamepad inputs for button combo to change usb device driver + else if (user_settings.check_for_driver_change(_gamepads[0])) { OGXM_LOG("Driver change detected, storing new driver.\n"); - //This will store the new mode and reboot the pico + s_driver_reboot_pending = true; user_settings.store_driver_type(user_settings.get_current_driver()); } }); +#else + (void)task_id; // Fixed output, combos disabled +#endif } //Called by tusb host so we know to connect or disconnect usb @@ -65,17 +161,38 @@ void standard::host_mounted(bool host_mounted) { static std::atomic tud_is_inited = false; board_api::set_led(host_mounted); - if (!host_mounted && tud_is_inited.load()) { - TaskQueue::Core0::queue_task([]() { - OGXM_LOG("USB disconnected, rebooting.\n"); - board_api::usb::disconnect_all(); - board_api::reboot(); - }); - } else if (!tud_is_inited.load()) { - TaskQueue::Core0::queue_task([]() { - tud_init(BOARD_TUD_RHPORT); - tud_is_inited.store(true); - }); + DeviceDriverType dt = DeviceManager::get_instance().get_driver_type(); + if (dt == DeviceDriverType::PS1PS2 || dt == DeviceDriverType::GAMECUBE || dt == DeviceDriverType::DREAMCAST) { + return; + } + + if (!host_mounted && tud_is_inited.load() && !s_driver_reboot_pending.load()) { + if (!s_host_teardown_scheduled.exchange(true)) { + TaskQueue::Core0::queue_delayed_task( + TaskQueue::Core0::get_new_task_id(), + HOST_UNMOUNT_DEBOUNCE_MS, + false, + []() { + s_host_teardown_scheduled.store(false); + if (s_driver_reboot_pending.load()) { + return; + } + if (HostManager::get_instance().any_mounted()) { + return; + } + OGXM_LOG("USB disconnected, rebooting.\n"); + board_api::usb::disconnect_all(); + board_api::reboot(); + }); + } + } else if (host_mounted) { + s_host_teardown_scheduled.store(false); + if (!tud_is_inited.load()) { + TaskQueue::Core0::queue_task([]() { + tud_init(BOARD_TUD_RHPORT); + tud_is_inited.store(true); + }); + } } } @@ -86,27 +203,85 @@ void standard::initialize() { user_settings.initialize_flash(); for (uint8_t i = 0; i < MAX_GAMEPADS; ++i) { - _gamepads[i].set_profile(user_settings.get_profile_by_index(i)); + _gamepads[i].set_profile(user_settings.get_profile_by_index(i), user_settings.get_current_driver()); } DeviceManager::get_instance().initialize_driver(user_settings.get_current_driver(), _gamepads); } void standard::run() { - multicore_reset_core1(); - multicore_launch_core1(core1_task); - DeviceDriverType current_driver = UserSettings::get_instance().get_current_driver(); + const bool gpio_device_mode = (current_driver == DeviceDriverType::PS1PS2 || current_driver == DeviceDriverType::GAMECUBE || current_driver == DeviceDriverType::DREAMCAST); - if (current_driver != DeviceDriverType::WEBAPP) { - // Wait for something to call host_mounted() - while (!tud_inited()) { + if (gpio_device_mode) { + HostManager& host_manager = HostManager::get_instance(); + host_manager.initialize(_gamepads); + + while (!board_api::usb::host_connected()) { TaskQueue::Core0::process_tasks(); sleep_ms(100); } + + configure_pio_usb_host(); + board_api_usbh::suspend_line_irq(); + tuh_init(BOARD_TUH_RHPORT); + pio_usb_sof_timer_start(); + + uint32_t tid_feedback = TaskQueue::Core0::get_new_task_id(); + TaskQueue::Core0::queue_delayed_task(tid_feedback, FEEDBACK_DELAY_MS, true, [&host_manager]() { + host_manager.send_feedback(); + }); + + HostInputSource input_src = UserSettings::get_instance().get_input_source(); + if (current_driver == DeviceDriverType::DREAMCAST) { + dreamcast_set_core1_device_mode(input_src != HostInputSource::DREAMCAST_GPIO); + } + multicore_reset_core1(); + if (current_driver == DeviceDriverType::PS1PS2) { + multicore_launch_core1(psx_device_main); + } else if (current_driver == DeviceDriverType::GAMECUBE) { + multicore_launch_core1(gamecube_core1_entry); + } else if (current_driver == DeviceDriverType::N64) { + multicore_launch_core1(n64_core1_entry); + } else { + multicore_launch_core1(dreamcast_core1_entry); + } + if (input_src == HostInputSource::PSX_GPIO) { + GPIOHost::psx_host_init(0, 20, 19); + } else if (input_src == HostInputSource::GAMECUBE_GPIO) { + GPIOHost::joybus_host_init(0, 19); + } + if (input_src == HostInputSource::DREAMCAST_GPIO) { + GPIOHost::dreamcast_host_init(1, 0, 10, -1); + } + if (input_src == HostInputSource::N64_GPIO) { + GPIOHost::n64_host_init(0, 19); + } + /* Present as USB device to console immediately so PS2/GC/Dreamcast see the adapter at boot + * (host_mounted() returns early for these drivers and never calls tud_init). */ + tud_init(BOARD_TUD_RHPORT); } else { - //Connect immediately in WebApp mode - host_mounted(true); + multicore_reset_core1(); + multicore_launch_core1(core1_task); + + if (current_driver != DeviceDriverType::WEBAPP) { + while (!tud_inited()) { + TaskQueue::Core0::process_tasks(); + sleep_ms(100); + } + } else { + host_mounted(true); + } + + /* GPIO input when output is USB (e.g. real PS1/PS2/GC controller → Switch/Xbox/DInput) */ + HostInputSource input_src = UserSettings::get_instance().get_input_source(); + if (input_src == HostInputSource::PSX_GPIO) { + GPIOHost::psx_host_init(0, 20, 19); + } else if (input_src == HostInputSource::GAMECUBE_GPIO) { + GPIOHost::joybus_host_init(0, 19); + } else if (input_src == HostInputSource::N64_GPIO) { + GPIOHost::n64_host_init(0, 19); + } } uint32_t tid_gp_check = TaskQueue::Core0::get_new_task_id(); @@ -116,19 +291,45 @@ void standard::run() { while (true) { TaskQueue::Core0::process_tasks(); - + if (gpio_device_mode) { + pio_usb_host_loop_tick(); + tud_task(); + HostInputSource input_src = UserSettings::get_instance().get_input_source(); + if (input_src == HostInputSource::PSX_GPIO) { + GPIOHost::psx_host_poll(_gamepads[0]); + } else if (input_src == HostInputSource::GAMECUBE_GPIO) { + GPIOHost::joybus_host_poll(_gamepads[0]); + } else if (input_src == HostInputSource::DREAMCAST_GPIO) { + GPIOHost::dreamcast_host_poll(_gamepads[0]); + } else if (input_src == HostInputSource::N64_GPIO) { + GPIOHost::n64_host_poll(_gamepads[0]); + } + } else { + tud_task(); + HostInputSource input_src = UserSettings::get_instance().get_input_source(); + if (input_src == HostInputSource::PSX_GPIO) { + GPIOHost::psx_host_poll(_gamepads[0]); + } else if (input_src == HostInputSource::GAMECUBE_GPIO) { + GPIOHost::joybus_host_poll(_gamepads[0]); + } else if (input_src == HostInputSource::DREAMCAST_GPIO) { + GPIOHost::dreamcast_host_poll(_gamepads[0]); + } else if (input_src == HostInputSource::N64_GPIO) { + GPIOHost::n64_host_poll(_gamepads[0]); + } + } for (uint8_t i = 0; i < MAX_GAMEPADS; ++i) { device_driver->process(i, _gamepads[i]); } - tud_task(); - sleep_ms(1); +#if MAIN_LOOP_DELAY_US > 0 + sleep_us(MAIN_LOOP_DELAY_US); +#endif } } -// #else // OGXM_BOARD == PI_PICO || OGXM_BOARD == RP2040_ZERO || OGXM_BOARD == ADAFRUIT_FEATHER +// #else // OGXM_BOARD == PI_PICO || ... || RP2354 // void standard::host_mounted(bool host_mounted) {} // void standard::initialize() {} // void standard::run() {} -#endif // OGXM_BOARD == PI_PICO || OGXM_BOARD == RP2040_ZERO || OGXM_BOARD == ADAFRUIT_FEATHER \ No newline at end of file +#endif // PI_PICO || RP2040_ZERO || ADAFRUIT_FEATHER || RP2350_USB_A || RP2350_ZERO || RP2040_XIAO \ No newline at end of file diff --git a/Firmware/RP2040/src/OGXMini/OGXMini.cpp b/Firmware/RP2040/src/OGXMini/OGXMini.cpp index a754c630..4030fc0e 100644 --- a/Firmware/RP2040/src/OGXMini/OGXMini.cpp +++ b/Firmware/RP2040/src/OGXMini/OGXMini.cpp @@ -22,6 +22,10 @@ namespace OGXMini { esp32_br_i2c::initialize, // ESP32_BLUERETRO_I2C four_ch_i2c::initialize, // EXTERNAL_4CH_I2C four_ch_i2c::initialize, // INTERNAL_4CH_I2C + standard::initialize, // RP2350_USB_A + standard::initialize, // RP2350_ZERO + standard::initialize, // RP2040_XIAO + pico_w::initialize, // RP2354 (CYW43439 / Pi Radio Module 2) }; static constexpr RunFunc run_func[BOARDS_COUNT] = { @@ -33,6 +37,10 @@ namespace OGXMini { esp32_br_i2c::run, // ESP32_BLUERETRO_I2C four_ch_i2c::run, // EXTERNAL_4CH_I2C four_ch_i2c::run, // INTERNAL_4CH_I2C + standard::run, // RP2350_USB_A + standard::run, // RP2350_ZERO + standard::run, // RP2040_XIAO + pico_w::run, // RP2354 }; static constexpr HostMountedFunc host_mount_func[BOARDS_COUNT] = { @@ -44,6 +52,10 @@ namespace OGXMini { nullptr, // ESP32_BLUERETRO_I2C four_ch_i2c::host_mounted, // EXTERNAL_4CH_I2C four_ch_i2c::host_mounted, // INTERNAL_4CH_I2C + standard::host_mounted, // RP2350_USB_A + standard::host_mounted, // RP2350_ZERO + standard::host_mounted, // RP2040_XIAO + nullptr, // RP2354 }; static constexpr HostMountedWTypeFunc host_mount_w_type_func[BOARDS_COUNT] = { @@ -55,6 +67,10 @@ namespace OGXMini { nullptr, // ESP32_BLUERETRO_I2C four_ch_i2c::host_mounted_w_type, // EXTERNAL_4CH_I2C four_ch_i2c::host_mounted_w_type, // INTERNAL_4CH_I2C + nullptr, // RP2350_USB_A + nullptr, // RP2350_ZERO + nullptr, // RP2040_XIAO + nullptr, // RP2354 }; static constexpr WirelessConnectedFunc wl_conn_func[BOARDS_COUNT] = { @@ -66,6 +82,10 @@ namespace OGXMini { nullptr, // ESP32_BLUERETRO_I2C four_ch_i2c::wireless_connected, // EXTERNAL_4CH_I2C four_ch_i2c::wireless_connected, // INTERNAL_4CH_I2C + nullptr, // RP2350_USB_A + nullptr, // RP2350_ZERO + nullptr, // RP2040_XIAO + nullptr, // RP2354 }; void initialize() { diff --git a/Firmware/RP2040/src/TaskQueue/TaskQueue.cpp b/Firmware/RP2040/src/TaskQueue/TaskQueue.cpp index 2e8fbc18..81198cfc 100644 --- a/Firmware/RP2040/src/TaskQueue/TaskQueue.cpp +++ b/Firmware/RP2040/src/TaskQueue/TaskQueue.cpp @@ -1,9 +1,11 @@ #include "TaskQueue/TaskQueue.h" -TaskQueue::TaskQueue(CoreNum core_num) -{ +TaskQueue::TaskQueue(CoreNum core_num) +{ alarm_num_ = (core_num == CoreNum::Core0) ? 0 : 1; - alarm_num_ += (OGXM_BOARD == PI_PICOW) ? 1 : 0; //BTStack uses alarm 0 +#if defined(OGXM_BOARD_USES_PICO_W_FIRMWARE) + alarm_num_ += 1; // BTStack uses alarm 0; Pico W path offsets Core1 timer +#endif hw_set_bits(&timer_hw->inte, 1u << alarm_num_); @@ -196,7 +198,7 @@ void TaskQueue::timer_irq_handler() void TaskQueue::suspend_delayed_tasks() { get_core0().suspend_delayed(); -#if (OGXM_BOARD != PI_PICOW) && (OGXM_BOARD != PI_PICO2W) +#if !defined(OGXM_BOARD_USES_PICO_W_FIRMWARE) get_core1().suspend_delayed(); #endif } @@ -204,7 +206,7 @@ void TaskQueue::suspend_delayed_tasks() void TaskQueue::resume_delayed_tasks() { get_core0().resume_delayed(); -#if (OGXM_BOARD != PI_PICOW) && (OGXM_BOARD != PI_PICO2W) +#if !defined(OGXM_BOARD_USES_PICO_W_FIRMWARE) get_core1().resume_delayed(); #endif } diff --git a/Firmware/RP2040/src/TaskQueue/TaskQueue.h b/Firmware/RP2040/src/TaskQueue/TaskQueue.h index 7ee2e0cb..cd0c5387 100644 --- a/Firmware/RP2040/src/TaskQueue/TaskQueue.h +++ b/Firmware/RP2040/src/TaskQueue/TaskQueue.h @@ -1,6 +1,8 @@ #ifndef TASK_QUEUE_H #define TASK_QUEUE_H +#include +#include #include #include #include @@ -46,7 +48,7 @@ class TaskQueue } }; -#if (OGXM_BOARD != PI_PICOW) //BTstack uses core1 +#if !defined(OGXM_BOARD_USES_PICO_W_FIRMWARE) || defined(CONFIG_EN_USB_HOST) // Pico W path: Core1 for Wii USB host struct Core1 { static inline uint32_t get_new_task_id() @@ -78,7 +80,7 @@ class TaskQueue get_core1().resume_delayed(); } }; // Core1 -#endif // OGXM_BOARD != PI_PICOW +#endif static void suspend_delayed_tasks(); static void resume_delayed_tasks(); diff --git a/Firmware/RP2040/src/USBDevice/DeviceDriver/DeviceDriverTypes.h b/Firmware/RP2040/src/USBDevice/DeviceDriver/DeviceDriverTypes.h index 1fae44ec..160a3441 100644 --- a/Firmware/RP2040/src/USBDevice/DeviceDriver/DeviceDriverTypes.h +++ b/Firmware/RP2040/src/USBDevice/DeviceDriver/DeviceDriverTypes.h @@ -14,6 +14,14 @@ enum class DeviceDriverType : uint8_t DINPUT, PSCLASSIC, SWITCH, + WIIU, + WII, // Wiimote (Pico W/2W: controller on PIO USB, BT for Wii link) + PS1PS2, // PS1/PS2 controller over GPIO (no USB device; output to console port) + GAMECUBE, // GameCube controller over GPIO (single wire; no USB device) + DREAMCAST, // Dreamcast controller over Maple Bus GPIO (no USB device; output/input to console) + N64, // N64 controller over GPIO (single wire; no USB device; output to N64 console) + PS4, // DualShock 4 USB HID (CUH-ZCT1x-class descriptor) + STEAM, // SteamOS/Bazzite: DualSense USB gamepad + HID mouse (touchpad passthrough) WEBAPP = 100, UART_BRIDGE }; diff --git a/Firmware/RP2040/src/USBDevice/DeviceDriver/Dreamcast/Dreamcast.cpp b/Firmware/RP2040/src/USBDevice/DeviceDriver/Dreamcast/Dreamcast.cpp new file mode 100644 index 00000000..119a28f1 --- /dev/null +++ b/Firmware/RP2040/src/USBDevice/DeviceDriver/Dreamcast/Dreamcast.cpp @@ -0,0 +1,186 @@ +/* Dreamcast Maple Bus output (device). USB/BT gamepad -> adapter -> Dreamcast. */ + +#include "USBDevice/DeviceDriver/Dreamcast/Dreamcast.h" +#include "Gamepad/Gamepad.h" +#include "USBHost/GPIOHost/dreamcast_host/MapleBus.h" +#include "USBHost/GPIOHost/dreamcast_host/MaplePacket.hpp" +#include "USBHost/GPIOHost/dreamcast_host/dreamcast_constants.h" +#include "USBHost/GPIOHost/dreamcast_host/dreamcast_structures.h" +#include "USBHost/GPIOHost/dreamcast_host/maple_config.h" +#include +#include +#include +#include +#include + +static bool s_core1_device_mode = false; +static controller_condition_t s_device_condition; +static mutex_t s_condition_mutex; + +/* Standard controller device info (28 words). Layout from BlueRetro / Maple spec. */ +#define DC_ID_CTRL 0x00000001 +#define DC_DESC_CTRL 0x000F06FE +#define DC_PWR_CTRL 0xAE01F401 + +static const uint8_t s_ctrl_area_dir_name[32] = { + 0x72, 0x44, 0x00, 0xFF, 0x63, 0x6D, 0x61, 0x65, 0x20, 0x74, 0x73, 0x61, 0x74, 0x6E, 0x6F, 0x43, + 0x6C, 0x6C, 0x6F, 0x72, 0x20, 0x20, 0x72, 0x65, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, +}; +static const uint8_t s_brand[32] = { + 'O', 'G', 'X', '-', 'M', 'i', 'n', 'i', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', +}; + +static void fill_device_info_payload(uint32_t* out) { + out[0] = DC_ID_CTRL; + out[1] = DC_DESC_CTRL; + out[2] = 0u; + out[3] = 0u; + memcpy(&out[4], s_ctrl_area_dir_name, 32); + memcpy(&out[12], s_brand, 32); + for (int i = 20; i < 27; i++) out[i] = 0u; + out[27] = DC_PWR_CTRL; +} + +void dreamcast_set_core1_device_mode(bool enable) { + s_core1_device_mode = enable; +} + +static void pad_in_to_controller_condition(const Gamepad::PadIn& pad, controller_condition_t& cond) { + memset(&cond, 0, sizeof(cond)); + cond.l = pad.trigger_l; + cond.r = pad.trigger_r; + cond.a = (pad.buttons & Gamepad::BUTTON_A) ? 0 : 1; + cond.b = (pad.buttons & Gamepad::BUTTON_B) ? 0 : 1; + cond.x = (pad.buttons & Gamepad::BUTTON_X) ? 0 : 1; + cond.y = (pad.buttons & Gamepad::BUTTON_Y) ? 0 : 1; + cond.start = (pad.buttons & Gamepad::BUTTON_START) ? 0 : 1; + cond.c = (pad.buttons & Gamepad::BUTTON_RB) ? 0 : 1; + cond.z = (pad.buttons & Gamepad::BUTTON_LB) ? 0 : 1; + cond.d = (pad.buttons & Gamepad::BUTTON_MISC) ? 0 : 1; + cond.up = (pad.dpad & Gamepad::DPAD_UP) ? 0 : 1; + cond.down = (pad.dpad & Gamepad::DPAD_DOWN) ? 0 : 1; + cond.left = (pad.dpad & Gamepad::DPAD_LEFT) ? 0 : 1; + cond.right= (pad.dpad & Gamepad::DPAD_RIGHT) ? 0 : 1; + cond.upb = cond.downb = cond.leftb = cond.rightb = 1; + /* Sticks: int16 -32767..32767 -> 0..255, 128 center */ + auto stick16_to_8 = [](int16_t v) -> uint8_t { + int32_t x = (int32_t)v * 127 / 32767 + 128; + if (x < 0) x = 0; + if (x > 255) x = 255; + return (uint8_t)x; + }; + cond.lAnalogLR = stick16_to_8(pad.joystick_lx); + cond.lAnalogUD = stick16_to_8(pad.joystick_ly); + cond.rAnalogLR = stick16_to_8(pad.joystick_rx); + cond.rAnalogUD = stick16_to_8(pad.joystick_ry); +} + +void DreamcastDevice::initialize() { + mutex_init(&s_condition_mutex); + memset(&s_device_condition, 0, sizeof(s_device_condition)); + s_device_condition.up = s_device_condition.down = s_device_condition.left = s_device_condition.right = 1; + s_device_condition.a = s_device_condition.b = s_device_condition.c = s_device_condition.x = 1; + s_device_condition.y = s_device_condition.z = s_device_condition.d = s_device_condition.start = 1; + s_device_condition.upb = s_device_condition.downb = s_device_condition.leftb = s_device_condition.rightb = 1; + s_device_condition.lAnalogLR = s_device_condition.lAnalogUD = 128; + s_device_condition.rAnalogLR = s_device_condition.rAnalogUD = 128; +} + +void DreamcastDevice::process(const uint8_t idx, Gamepad& gamepad) { + if (idx != 0) return; + Gamepad::PadIn pad = gamepad.get_pad_in(); + controller_condition_t cond; + pad_in_to_controller_condition(pad, cond); + mutex_enter_blocking(&s_condition_mutex); + memcpy(&s_device_condition, &cond, sizeof(s_device_condition)); + mutex_exit(&s_condition_mutex); +} + +void dreamcast_core1_entry(void) { + flash_safe_execute_core_init(); // allow Core0 to perform flash operations (e.g. UserSettings) + if (!s_core1_device_mode) { + while (true) sleep_ms(1); + return; + } + const uint32_t pinA = 10; + MapleBus bus(pinA, -1, true); + MaplePacket reqPacket, respPacket; + respPacket.payloadByteOrder = MaplePacket::ByteOrder::HOST; + + while (true) { + if (!bus.startRead(0, MaplePacket::ByteOrder::HOST)) + continue; + uint64_t deadline = time_us_64() + MAPLE_RESPONSE_TIMEOUT_US + 2000; + MapleBus::Status st; + while (time_us_64() < deadline) { + st = bus.processEvents(time_us_64()); + if (st.phase == MapleBus::Phase::READ_COMPLETE || st.phase == MapleBus::Phase::READ_FAILED) + break; + sleep_us(20); + } + if (st.phase != MapleBus::Phase::READ_COMPLETE || !st.readBuffer || st.readBufferLen < 1) + continue; + + uint32_t frameWord = st.readBuffer[0]; + uint8_t cmd = MaplePacket::Frame::getFrameCommand(frameWord, st.rxByteOrder); + uint8_t recipient = MaplePacket::Frame::getFrameRecipientAddr(frameWord, st.rxByteOrder); + uint8_t sender = MaplePacket::Frame::getFrameSenderAddr(frameWord, st.rxByteOrder); + + if (cmd == COMMAND_DEVICE_INFO_REQUEST) { + respPacket.setFrame(COMMAND_RESPONSE_DEVICE_INFO, MAPLE_HOST_ADDR, MAPLE_CTRL_ADDR, 28); + fill_device_info_payload(respPacket.payload); + respPacket.payloadCount = 28; + } else if (cmd == COMMAND_GET_CONDITION) { + mutex_enter_blocking(&s_condition_mutex); + uint32_t condWords[2]; + memcpy(condWords, &s_device_condition, sizeof(condWords)); + mutex_exit(&s_condition_mutex); + respPacket.setFrame(COMMAND_RESPONSE_DATA_XFER, MAPLE_HOST_ADDR, MAPLE_CTRL_ADDR, 3); + respPacket.payload[0] = DEVICE_FN_CONTROLLER; + respPacket.payload[1] = condWords[0]; + respPacket.payload[2] = condWords[1]; + respPacket.payloadCount = 3; + } else { + continue; + } + + if (!bus.write(respPacket, false)) + continue; + deadline = time_us_64() + 5000; + while (time_us_64() < deadline && bus.isBusy()) { + bus.processEvents(time_us_64()); + sleep_us(50); + } + } +} + +uint16_t DreamcastDevice::get_report_cb(uint8_t itf, uint8_t report_id, hid_report_type_t report_type, uint8_t* buffer, uint16_t req_len) { + (void)itf; (void)report_id; (void)report_type; (void)buffer; (void)req_len; + return 0; +} +void DreamcastDevice::set_report_cb(uint8_t itf, uint8_t report_id, hid_report_type_t report_type, uint8_t const* buffer, uint16_t buffer_size) { + (void)itf; (void)report_id; (void)report_type; (void)buffer; (void)buffer_size; +} +bool DreamcastDevice::vendor_control_xfer_cb(uint8_t rhport, uint8_t stage, tusb_control_request_t const* request) { + (void)rhport; (void)stage; (void)request; + return false; +} +const uint16_t* DreamcastDevice::get_descriptor_string_cb(uint8_t index, uint16_t langid) { + (void)index; (void)langid; + return nullptr; +} +const uint8_t* DreamcastDevice::get_descriptor_device_cb() { + return nullptr; +} +const uint8_t* DreamcastDevice::get_hid_descriptor_report_cb(uint8_t itf) { + (void)itf; + return nullptr; +} +const uint8_t* DreamcastDevice::get_descriptor_configuration_cb(uint8_t index) { + (void)index; + return nullptr; +} +const uint8_t* DreamcastDevice::get_descriptor_device_qualifier_cb() { + return nullptr; +} diff --git a/Firmware/RP2040/src/USBDevice/DeviceDriver/Dreamcast/Dreamcast.h b/Firmware/RP2040/src/USBDevice/DeviceDriver/Dreamcast/Dreamcast.h new file mode 100644 index 00000000..76eb6df2 --- /dev/null +++ b/Firmware/RP2040/src/USBDevice/DeviceDriver/Dreamcast/Dreamcast.h @@ -0,0 +1,30 @@ +/* Dreamcast Maple Bus output (device) – stub until DreamPicoPort Maple Bus HAL is ported. + * See Firmware/RP2040/docs/Dreamcast_Port.md for port steps from https://github.com/OrangeFox86/DreamPicoPort */ + +#ifndef _D_DREAMCAST_DRIVER_H_ +#define _D_DREAMCAST_DRIVER_H_ + +#include "USBDevice/DeviceDriver/DeviceDriver.h" + +class DreamcastDevice : public DeviceDriver +{ +public: + void initialize() override; + void process(const uint8_t idx, class Gamepad& gamepad) override; + uint16_t get_report_cb(uint8_t itf, uint8_t report_id, hid_report_type_t report_type, uint8_t* buffer, uint16_t req_len) override; + void set_report_cb(uint8_t itf, uint8_t report_id, hid_report_type_t report_type, uint8_t const* buffer, uint16_t buffer_size) override; + bool vendor_control_xfer_cb(uint8_t rhport, uint8_t stage, tusb_control_request_t const* request) override; + const uint16_t* get_descriptor_string_cb(uint8_t index, uint16_t langid) override; + const uint8_t* get_descriptor_device_cb() override; + const uint8_t* get_hid_descriptor_report_cb(uint8_t itf) override; + const uint8_t* get_descriptor_configuration_cb(uint8_t index) override; + const uint8_t* get_descriptor_device_qualifier_cb() override; +}; + +/** Entry point for core1 when driver is DREAMCAST (Maple Bus device loop). */ +void dreamcast_core1_entry(void); + +/** Call before launching core1: true = run as Maple device (USB/BT -> DC); false = bus used as host only. */ +void dreamcast_set_core1_device_mode(bool enable); + +#endif diff --git a/Firmware/RP2040/src/USBDevice/DeviceDriver/GameCube/GameCube.cpp b/Firmware/RP2040/src/USBDevice/DeviceDriver/GameCube/GameCube.cpp new file mode 100644 index 00000000..8510a32d --- /dev/null +++ b/Firmware/RP2040/src/USBDevice/DeviceDriver/GameCube/GameCube.cpp @@ -0,0 +1,110 @@ +#include +#include "pico/flash.h" +#include "USBDevice/DeviceDriver/GameCube/GameCube.h" +#include "USBDevice/DeviceDriver/GameCube/gc_simulator.h" + +#ifndef GAMECUBE_DATA_PIN +#define GAMECUBE_DATA_PIN 19 +#endif + +static GCReport* s_gc_report = nullptr; + +void gamecube_core1_entry(void) { + flash_safe_execute_core_init(); // allow Core0 to perform flash operations (e.g. UserSettings) + if (s_gc_report == nullptr) + return; // driver not initialized; avoid null deref in gc_device_main + gc_device_main(0, s_gc_report, GAMECUBE_DATA_PIN); +} + +namespace { + constexpr uint8_t GC_A = 0x01; + constexpr uint8_t GC_B = 0x02; + constexpr uint8_t GC_X = 0x04; + constexpr uint8_t GC_Y = 0x08; + constexpr uint8_t GC_START = 0x10; + constexpr uint8_t GC_DPAD_LEFT = 0x01; + constexpr uint8_t GC_DPAD_RIGHT = 0x02; + constexpr uint8_t GC_DPAD_DOWN = 0x04; + constexpr uint8_t GC_DPAD_UP = 0x08; + constexpr uint8_t GC_Z = 0x10; + constexpr uint8_t GC_R = 0x20; + constexpr uint8_t GC_L = 0x40; + constexpr uint8_t GC_BYTE2_MSB = 0x80; + constexpr uint8_t STICK_CENTER = 0x80; +} + +void GameCubeDevice::initialize() { + std::memset(&gc_report_, 0, sizeof(gc_report_)); + s_gc_report = &gc_report_; + gc_report_.lx = STICK_CENTER; + gc_report_.ly = STICK_CENTER; + gc_report_.rx = STICK_CENTER; + gc_report_.ry = STICK_CENTER; + gc_report_.buttons2 = GC_BYTE2_MSB; +} + +void GameCubeDevice::process(const uint8_t idx, Gamepad& gamepad) { + (void)idx; + if (!gamepad.new_pad_in()) + return; + + Gamepad::PadIn gp = gamepad.get_pad_in(); + + gc_report_.buttons1 = 0; + if (gp.buttons & Gamepad::BUTTON_A) gc_report_.buttons1 |= GC_A; + if (gp.buttons & Gamepad::BUTTON_B) gc_report_.buttons1 |= GC_B; + if (gp.buttons & Gamepad::BUTTON_X) gc_report_.buttons1 |= GC_X; + if (gp.buttons & Gamepad::BUTTON_Y) gc_report_.buttons1 |= GC_Y; + if (gp.buttons & Gamepad::BUTTON_START) gc_report_.buttons1 |= GC_START; + + gc_report_.buttons2 = GC_BYTE2_MSB; + if (gp.dpad & Gamepad::DPAD_LEFT) gc_report_.buttons2 |= GC_DPAD_LEFT; + if (gp.dpad & Gamepad::DPAD_RIGHT) gc_report_.buttons2 |= GC_DPAD_RIGHT; + if (gp.dpad & Gamepad::DPAD_DOWN) gc_report_.buttons2 |= GC_DPAD_DOWN; + if (gp.dpad & Gamepad::DPAD_UP) gc_report_.buttons2 |= GC_DPAD_UP; + if (gp.buttons & Gamepad::BUTTON_MISC) gc_report_.buttons2 |= GC_Z; + if (gp.buttons & Gamepad::BUTTON_RB) gc_report_.buttons2 |= GC_R; + if (gp.buttons & Gamepad::BUTTON_LB) gc_report_.buttons2 |= GC_L; + + gc_report_.l = gp.trigger_l; + gc_report_.r = gp.trigger_r; + + int32_t lx = (int32_t)gp.joystick_lx * 127 / 32767 + STICK_CENTER; + int32_t ly = (int32_t)gp.joystick_ly * -127 / 32767 + STICK_CENTER; + int32_t rx = (int32_t)gp.joystick_rx * 127 / 32767 + STICK_CENTER; + int32_t ry = (int32_t)gp.joystick_ry * -127 / 32767 + STICK_CENTER; + if (lx < 0) lx = 0; else if (lx > 255) lx = 255; + if (ly < 0) ly = 0; else if (ly > 255) ly = 255; + if (rx < 0) rx = 0; else if (rx > 255) rx = 255; + if (ry < 0) ry = 0; else if (ry > 255) ry = 255; + gc_report_.lx = (uint8_t)lx; + gc_report_.ly = (uint8_t)ly; + gc_report_.rx = (uint8_t)rx; + gc_report_.ry = (uint8_t)ry; +} + +uint16_t GameCubeDevice::get_report_cb(uint8_t itf, uint8_t report_id, hid_report_type_t report_type, uint8_t* buffer, uint16_t req_len) { + (void)itf; (void)report_id; (void)report_type; (void)buffer; (void)req_len; + return 0; +} +void GameCubeDevice::set_report_cb(uint8_t itf, uint8_t report_id, hid_report_type_t report_type, uint8_t const* buffer, uint16_t buffer_size) { + (void)itf; (void)report_id; (void)report_type; (void)buffer; (void)buffer_size; +} +bool GameCubeDevice::vendor_control_xfer_cb(uint8_t rhport, uint8_t stage, tusb_control_request_t const* request) { + (void)rhport; (void)stage; (void)request; + return false; +} +const uint16_t* GameCubeDevice::get_descriptor_string_cb(uint8_t index, uint16_t langid) { + (void)index; (void)langid; + return nullptr; +} +const uint8_t* GameCubeDevice::get_descriptor_device_cb() { return nullptr; } +const uint8_t* GameCubeDevice::get_hid_descriptor_report_cb(uint8_t itf) { + (void)itf; + return nullptr; +} +const uint8_t* GameCubeDevice::get_descriptor_configuration_cb(uint8_t index) { + (void)index; + return nullptr; +} +const uint8_t* GameCubeDevice::get_descriptor_device_qualifier_cb() { return nullptr; } diff --git a/Firmware/RP2040/src/USBDevice/DeviceDriver/GameCube/GameCube.h b/Firmware/RP2040/src/USBDevice/DeviceDriver/GameCube/GameCube.h new file mode 100644 index 00000000..ebac1e7f --- /dev/null +++ b/Firmware/RP2040/src/USBDevice/DeviceDriver/GameCube/GameCube.h @@ -0,0 +1,28 @@ +#ifndef _D_GAMECUBE_DRIVER_H_ +#define _D_GAMECUBE_DRIVER_H_ + +#include +#include "USBDevice/DeviceDriver/DeviceDriver.h" +#include "USBDevice/DeviceDriver/GameCube/gc_simulator.h" + +class GameCubeDevice : public DeviceDriver +{ +public: + void initialize() override; + void process(const uint8_t idx, Gamepad& gamepad) override; + uint16_t get_report_cb(uint8_t itf, uint8_t report_id, hid_report_type_t report_type, uint8_t* buffer, uint16_t req_len) override; + void set_report_cb(uint8_t itf, uint8_t report_id, hid_report_type_t report_type, uint8_t const* buffer, uint16_t buffer_size) override; + bool vendor_control_xfer_cb(uint8_t rhport, uint8_t stage, tusb_control_request_t const* request) override; + const uint16_t* get_descriptor_string_cb(uint8_t index, uint16_t langid) override; + const uint8_t* get_descriptor_device_cb() override; + const uint8_t* get_hid_descriptor_report_cb(uint8_t itf) override; + const uint8_t* get_descriptor_configuration_cb(uint8_t index) override; + const uint8_t* get_descriptor_device_qualifier_cb() override; + + GCReport& get_gc_report() { return gc_report_; } + +private: + GCReport gc_report_{}; +}; + +#endif // _D_GAMECUBE_DRIVER_H_ diff --git a/Firmware/RP2040/src/USBDevice/DeviceDriver/GameCube/gamecube_device.c b/Firmware/RP2040/src/USBDevice/DeviceDriver/GameCube/gamecube_device.c new file mode 100644 index 00000000..4246be41 --- /dev/null +++ b/Firmware/RP2040/src/USBDevice/DeviceDriver/GameCube/gamecube_device.c @@ -0,0 +1,101 @@ +/* GameCube JoyBus device. Ported from PicoGamepadConverter (Loc15). */ + +#include "pico/stdlib.h" +#include "hardware/pio.h" +#include "hardware/clocks.h" +#include "joybus.pio.h" +#include "USBDevice/DeviceDriver/GameCube/gc_simulator.h" + +void __time_critical_func(convertToPio)(const uint8_t* command, const int len, uint32_t* result, int* resultLen) { + if (len == 0) { + *resultLen = 0; + return; + } + *resultLen = len / 2 + 1; + for (int i = 0; i < *resultLen; i++) + result[i] = 0; + for (int i = 0; i < len; i++) { + for (int j = 0; j < 8; j++) { + result[i / 2] += 1u << (2 * (8 * (i % 2) + j) + 1); + result[i / 2] += (!!(command[i] & (0x80u >> j))) << (2 * (8 * (i % 2) + j)); + } + } + result[len / 2] += 3u << (2 * (8 * (len % 2))); +} + +#define ORG 127 + +void gc_device_main(unsigned pio, GCReport* data, int data_pin) { + volatile GCReport* gcReport = data; + PIO gamecube_pio = pio ? pio1 : pio0; + + gpio_init(data_pin); + gpio_set_dir(data_pin, GPIO_IN); + gpio_pull_up(data_pin); + + sleep_us(100); + + uint offset = pio_add_program(gamecube_pio, &joybus_program); + uint sm = pio_claim_unused_sm(gamecube_pio, true); + pio_sm_config config = joybus_program_get_default_config(offset); + pio_gpio_init(gamecube_pio, data_pin); + sm_config_set_in_pins(&config, data_pin); + sm_config_set_out_pins(&config, data_pin, 1); + sm_config_set_set_pins(&config, data_pin, 1); + float div = (float)clock_get_hz(clk_sys) / (25e6f); + sm_config_set_clkdiv(&config, div); + sm_config_set_out_shift(&config, true, false, 32); + sm_config_set_in_shift(&config, false, true, 8); + + pio_sm_init(gamecube_pio, sm, offset, &config); + pio_sm_set_enabled(gamecube_pio, sm, true); + + while (true) { + uint8_t buffer[3]; + buffer[0] = (uint8_t)pio_sm_get_blocking(gamecube_pio, sm); + if (buffer[0] == 0) { + uint8_t probeResponse[3] = { 0x09, 0x00, 0x03 }; + uint32_t result[2]; + int resultLen; + convertToPio(probeResponse, 3, result, &resultLen); + sleep_us(6); + + pio_sm_set_enabled(gamecube_pio, sm, false); + pio_sm_init(gamecube_pio, sm, offset + joybus_offset_outmode, &config); + pio_sm_set_enabled(gamecube_pio, sm, true); + for (int i = 0; i < resultLen; i++) + pio_sm_put_blocking(gamecube_pio, sm, result[i]); + } else if (buffer[0] == 0x41) { + uint8_t originResponse[10] = { 0x00, 0x80, ORG, ORG, ORG, ORG, 0, 0, 0, 0 }; + uint32_t result[6]; + int resultLen; + convertToPio(originResponse, 10, result, &resultLen); + + pio_sm_set_enabled(gamecube_pio, sm, false); + pio_sm_init(gamecube_pio, sm, offset + joybus_offset_outmode, &config); + pio_sm_set_enabled(gamecube_pio, sm, true); + for (int i = 0; i < resultLen; i++) + pio_sm_put_blocking(gamecube_pio, sm, result[i]); + } else if (buffer[0] == 0x40) { + buffer[0] = (uint8_t)pio_sm_get_blocking(gamecube_pio, sm); + + uint32_t result[5]; + int resultLen; + convertToPio((const uint8_t*)gcReport, 8, result, &resultLen); + + (void)pio_sm_get_blocking(gamecube_pio, sm); + sleep_us(7); + + pio_sm_set_enabled(gamecube_pio, sm, false); + pio_sm_init(gamecube_pio, sm, offset + joybus_offset_outmode, &config); + pio_sm_set_enabled(gamecube_pio, sm, true); + for (int i = 0; i < resultLen; i++) + pio_sm_put_blocking(gamecube_pio, sm, result[i]); + } else { + pio_sm_set_enabled(gamecube_pio, sm, false); + sleep_us(400); + pio_sm_init(gamecube_pio, sm, offset + joybus_offset_inmode, &config); + pio_sm_set_enabled(gamecube_pio, sm, true); + } + } +} diff --git a/Firmware/RP2040/src/USBDevice/DeviceDriver/GameCube/gc_simulator.h b/Firmware/RP2040/src/USBDevice/DeviceDriver/GameCube/gc_simulator.h new file mode 100644 index 00000000..1e250d3d --- /dev/null +++ b/Firmware/RP2040/src/USBDevice/DeviceDriver/GameCube/gc_simulator.h @@ -0,0 +1,35 @@ +/* GameCube controller protocol (device side). Ported from PicoGamepadConverter by Loc15. + * https://github.com/Loc15/PicoGamepadConverter + * Single-wire JoyBus; output to GameCube/Wii console. */ + +#ifndef GAMECUBE_GC_SIMULATOR_H_ +#define GAMECUBE_GC_SIMULATOR_H_ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct __attribute__((packed, aligned(1))) { + uint8_t buttons1; /* A, B, X, Y, Start */ + uint8_t buttons2; /* D-pad, Z, R, L, 0x80 */ + uint8_t lx; /* left stick X, 0x80 center */ + uint8_t ly; /* left stick Y */ + uint8_t rx; /* right stick X (C-stick) */ + uint8_t ry; /* right stick Y */ + uint8_t l; /* L analog 0–255 */ + uint8_t r; /* R analog 0–255 */ +} GCReport; + +/* Run on core1; blocks forever. pio: 0 or 1, data_pin: GPIO for DATA line (e.g. 19). */ +void gc_device_main(unsigned pio, GCReport* data, int data_pin); + +/* Entry for core1 when using OGX-Mini GameCube driver (uses report set in initialize). */ +void gamecube_core1_entry(void); + +#ifdef __cplusplus +} +#endif + +#endif /* GAMECUBE_GC_SIMULATOR_H_ */ diff --git a/Firmware/RP2040/src/USBDevice/DeviceDriver/GameCube/joybus.pio b/Firmware/RP2040/src/USBDevice/DeviceDriver/GameCube/joybus.pio new file mode 100644 index 00000000..f0ff2b5b --- /dev/null +++ b/Firmware/RP2040/src/USBDevice/DeviceDriver/GameCube/joybus.pio @@ -0,0 +1,27 @@ +; GameCube JoyBus protocol (device side). From PicoGamepadConverter (Loc15). +; THIS PIO PROGRAM EXPECTS THE SYSTEM CLOCK TO BE 125MHz +; Single wire bidirectional; clock is /5 => 25MHz + +.program joybus +PUBLIC inmode: + set pindirs, 0 +inagain: + wait 0 pin 0 [31] + nop [31] + in pins, 1 + wait 1 pin 0 + jmp inagain + +PUBLIC outmode: + set pins, 1 + set pindirs, 1 +outagain: + pull ifempty block + out X, 1 + out Y, 1 + jmp !Y inmode + set pins, 0 [ 24 ] + mov pins, X [ 24 ] + mov pins, X [ 24 ] + set pins, 1 [ 19 ] + jmp outagain diff --git a/Firmware/RP2040/src/USBDevice/DeviceDriver/MotionOutputActive.h b/Firmware/RP2040/src/USBDevice/DeviceDriver/MotionOutputActive.h new file mode 100644 index 00000000..8b56bc8d --- /dev/null +++ b/Firmware/RP2040/src/USBDevice/DeviceDriver/MotionOutputActive.h @@ -0,0 +1,19 @@ +#pragma once + +#include + +/** True while USB output is PS3 or PS4 (Sixaxis / DS4 IMU gadget). Switch motion disabled for now. */ +namespace MotionOutputActive +{ + inline std::atomic enabled{false}; + + inline void set(bool on) + { + enabled.store(on, std::memory_order_release); + } + + inline bool is_enabled() + { + return enabled.load(std::memory_order_acquire); + } +} diff --git a/Firmware/RP2040/src/USBDevice/DeviceDriver/N64/N64.cpp b/Firmware/RP2040/src/USBDevice/DeviceDriver/N64/N64.cpp new file mode 100644 index 00000000..913bd01a --- /dev/null +++ b/Firmware/RP2040/src/USBDevice/DeviceDriver/N64/N64.cpp @@ -0,0 +1,100 @@ +/* N64 controller output (device). Any USB/BT gamepad -> adapter -> N64 console. Ported from pdaxrom/usb2n64-adapter. */ + +#include +#include "pico/flash.h" +#include "USBDevice/DeviceDriver/N64/N64.h" +#include "Gamepad/Gamepad.h" + +#ifndef N64_DATA_PIN +#define N64_DATA_PIN 19 +#endif + +void n64_core1_entry(void) { + flash_safe_execute_core_init(); // allow Core0 to perform flash operations (e.g. UserSettings) + n64_device_main(N64_DATA_PIN); +} + +namespace { + /* N64 report format matches host input: byte0 A,B,Z,START,D-pad; byte1 L,R,C; byte2/3 joy_x, joy_y (signed, 0 center). */ + constexpr uint8_t N64_A = 0x80; + constexpr uint8_t N64_B = 0x40; + constexpr uint8_t N64_Z = 0x20; + constexpr uint8_t N64_START = 0x10; + constexpr uint8_t N64_R = 0x10; + constexpr uint8_t N64_L = 0x20; + constexpr uint8_t N64_C_UP = 0x08; + constexpr uint8_t N64_C_DOWN = 0x04; + constexpr uint8_t N64_C_LEFT = 0x02; + constexpr uint8_t N64_C_RIGHT = 0x01; +} + +void N64Device::initialize() { + N64Report r = {}; + r.buttons1 = 0; + r.buttons2 = 0; + r.joy_x = 0; + r.joy_y = 0; + n64_device_set_report(&r); +} + +void N64Device::process(const uint8_t idx, Gamepad& gamepad) { + (void)idx; + if (!gamepad.new_pad_in()) + return; + + Gamepad::PadIn gp = gamepad.get_pad_in(); + N64Report r = {}; + uint8_t dpad = 0; + if (gp.dpad & Gamepad::DPAD_UP) dpad |= 0x08; + if (gp.dpad & Gamepad::DPAD_DOWN) dpad |= 0x04; + if (gp.dpad & Gamepad::DPAD_LEFT) dpad |= 0x02; + if (gp.dpad & Gamepad::DPAD_RIGHT) dpad |= 0x01; + r.buttons1 = dpad; + if (gp.buttons & Gamepad::BUTTON_A) r.buttons1 |= N64_A; + if (gp.buttons & Gamepad::BUTTON_B) r.buttons1 |= N64_B; + if (gp.buttons & Gamepad::BUTTON_MISC) r.buttons1 |= N64_Z; + if (gp.buttons & Gamepad::BUTTON_START) r.buttons1 |= N64_START; + + r.buttons2 = 0; + if (gp.buttons & Gamepad::BUTTON_LB) r.buttons2 |= N64_L; + if (gp.buttons & Gamepad::BUTTON_RB) r.buttons2 |= N64_R; + if (gp.joystick_ry < -4000) r.buttons2 |= N64_C_UP; + if (gp.joystick_ry > 4000) r.buttons2 |= N64_C_DOWN; + if (gp.joystick_rx < -4000) r.buttons2 |= N64_C_LEFT; + if (gp.joystick_rx > 4000) r.buttons2 |= N64_C_RIGHT; + + int32_t jx = (int32_t)gp.joystick_lx * 127 / 32767; + int32_t jy = (int32_t)gp.joystick_ly * -127 / 32767; + if (jx < -128) jx = -128; else if (jx > 127) jx = 127; + if (jy < -128) jy = -128; else if (jy > 127) jy = 127; + r.joy_x = (int8_t)jx; + r.joy_y = (int8_t)jy; + + n64_device_set_report(&r); +} + +uint16_t N64Device::get_report_cb(uint8_t itf, uint8_t report_id, hid_report_type_t report_type, uint8_t* buffer, uint16_t req_len) { + (void)itf; (void)report_id; (void)report_type; (void)buffer; (void)req_len; + return 0; +} +void N64Device::set_report_cb(uint8_t itf, uint8_t report_id, hid_report_type_t report_type, uint8_t const* buffer, uint16_t buffer_size) { + (void)itf; (void)report_id; (void)report_type; (void)buffer; (void)buffer_size; +} +bool N64Device::vendor_control_xfer_cb(uint8_t rhport, uint8_t stage, tusb_control_request_t const* request) { + (void)rhport; (void)stage; (void)request; + return false; +} +const uint16_t* N64Device::get_descriptor_string_cb(uint8_t index, uint16_t langid) { + (void)index; (void)langid; + return nullptr; +} +const uint8_t* N64Device::get_descriptor_device_cb() { return nullptr; } +const uint8_t* N64Device::get_hid_descriptor_report_cb(uint8_t itf) { + (void)itf; + return nullptr; +} +const uint8_t* N64Device::get_descriptor_configuration_cb(uint8_t index) { + (void)index; + return nullptr; +} +const uint8_t* N64Device::get_descriptor_device_qualifier_cb() { return nullptr; } diff --git a/Firmware/RP2040/src/USBDevice/DeviceDriver/N64/N64.h b/Firmware/RP2040/src/USBDevice/DeviceDriver/N64/N64.h new file mode 100644 index 00000000..a2f059e1 --- /dev/null +++ b/Firmware/RP2040/src/USBDevice/DeviceDriver/N64/N64.h @@ -0,0 +1,24 @@ +#ifndef _D_N64_DRIVER_H_ +#define _D_N64_DRIVER_H_ + +#include "USBDevice/DeviceDriver/DeviceDriver.h" +#include "USBDevice/DeviceDriver/N64/n64_device.h" + +void n64_core1_entry(void); + +class N64Device : public DeviceDriver +{ +public: + void initialize() override; + void process(const uint8_t idx, Gamepad& gamepad) override; + uint16_t get_report_cb(uint8_t itf, uint8_t report_id, hid_report_type_t report_type, uint8_t* buffer, uint16_t req_len) override; + void set_report_cb(uint8_t itf, uint8_t report_id, hid_report_type_t report_type, uint8_t const* buffer, uint16_t buffer_size) override; + bool vendor_control_xfer_cb(uint8_t rhport, uint8_t stage, tusb_control_request_t const* request) override; + const uint16_t* get_descriptor_string_cb(uint8_t index, uint16_t langid) override; + const uint8_t* get_descriptor_device_cb() override; + const uint8_t* get_hid_descriptor_report_cb(uint8_t itf) override; + const uint8_t* get_descriptor_configuration_cb(uint8_t index) override; + const uint8_t* get_descriptor_device_qualifier_cb() override; +}; + +#endif // _D_N64_DRIVER_H_ diff --git a/Firmware/RP2040/src/USBDevice/DeviceDriver/N64/n64_device.c b/Firmware/RP2040/src/USBDevice/DeviceDriver/N64/n64_device.c new file mode 100644 index 00000000..1ebd8b27 --- /dev/null +++ b/Firmware/RP2040/src/USBDevice/DeviceDriver/N64/n64_device.c @@ -0,0 +1,112 @@ +/* N64 controller device (output to N64 console). Ported from pdaxrom/usb2n64-adapter. */ + +#include "pico/stdlib.h" +#include "hardware/pio.h" +#include "hardware/dma.h" +#include "hardware/clocks.h" +#include "n64send.pio.h" +#include "n64_device.h" + +#define N64SEND_DATA(d0, d1, b) ((((b) - 1) << 16) | ((uint32_t)(d0) << 8) | (uint32_t)(d1)) + +/* Shared report from Core0; Core1 reads when handling poll. */ +static volatile N64Report s_n64_report; + +void n64_device_set_report(const N64Report* report) { + if (report) { + s_n64_report.buttons1 = report->buttons1; + s_n64_report.buttons2 = report->buttons2; + s_n64_report.joy_x = report->joy_x; + s_n64_report.joy_y = report->joy_y; + } +} + +static __not_in_flash_func(uint32_t) read_bits(int pin, int num_bits) { + uint32_t value = 0; + for (int i = 0; i < num_bits; i++) { + busy_wait_us(2); + value = (value << 1) | (gpio_get(pin) ? 1u : 0u); + } + return value; +} + +/* Wait for console to pull line low (start of command). */ +static __not_in_flash_func(void) wait_for_line_low(int pin) { + while (gpio_get(pin)) { } +} + +static PIO s_n64_pio; +static uint s_n64_sm; +static uint s_n64_offset; +static uint s_n64_dma_chan; + +static __not_in_flash_func(void) send_response(uint32_t* dma_buf, int num_words) { + pio_sm_exec(s_n64_pio, s_n64_sm, pio_encode_jmp(s_n64_offset + n64send_dma_offset_loop)); + dma_channel_transfer_from_buffer_now(s_n64_dma_chan, dma_buf, num_words); + dma_channel_wait_for_finish_blocking(s_n64_dma_chan); + while (pio_sm_get_pc(s_n64_pio, s_n64_sm) != (s_n64_offset + n64send_dma_offset_stop)) { } +} + +static uint32_t s_dma_buf[8]; + +void n64_device_main(int data_pin) { + gpio_init(data_pin); + gpio_put(data_pin, 0); + gpio_pull_up(data_pin); + gpio_set_dir(data_pin, GPIO_IN); + + PIO pio = pio0; + uint offset = pio_add_program(pio, &n64send_dma_program); + uint sm = pio_claim_unused_sm(pio, true); + s_n64_pio = pio; + s_n64_sm = sm; + s_n64_offset = offset; + + uint dma_chan = dma_claim_unused_channel(true); + s_n64_dma_chan = dma_chan; + + dma_channel_config cfg = dma_channel_get_default_config(dma_chan); + channel_config_set_transfer_data_size(&cfg, DMA_SIZE_32); + channel_config_set_read_increment(&cfg, true); + channel_config_set_write_increment(&cfg, false); + channel_config_set_dreq(&cfg, pio_get_dreq(pio, sm, true)); + dma_channel_configure(dma_chan, &cfg, &pio->txf[sm], NULL, 0, false); + + pio_sm_config c = n64send_dma_program_get_default_config(offset); + sm_config_set_out_shift(&c, false, false, 32); + sm_config_set_in_shift(&c, false, false, 32); + sm_config_set_out_pins(&c, data_pin, 1); + sm_config_set_set_pins(&c, data_pin, 1); + pio_gpio_init(pio, data_pin); + pio_sm_set_consecutive_pindirs(pio, sm, data_pin, 1, false); + sm_config_set_clkdiv(&c, 16.625f); + sm_config_set_fifo_join(&c, PIO_FIFO_JOIN_TX); + pio_sm_init(pio, sm, offset, &c); + pio_sm_set_enabled(pio, sm, true); + + for (;;) { + wait_for_line_low(data_pin); + uint32_t cmd = read_bits(data_pin, 9); + /* Command byte is bits 1-8 (bit 0 is start). */ + uint8_t cmd_byte = (uint8_t)(cmd >> 1); + + if (cmd_byte == 0x00 || cmd_byte == 0xFF) { + /* Identity: standard controller 0x05, 0x00; then 0x01 (1 byte). */ + s_dma_buf[0] = N64SEND_DATA(0x05, 0x00, 16); + s_dma_buf[1] = N64SEND_DATA(0x01, 0x00, 8); + s_dma_buf[2] = 0; + send_response(s_dma_buf, 3); + } else if (cmd_byte == 0x01) { + /* Poll: 4 bytes = buttons1, buttons2, joy_x, joy_y */ + uint8_t b0 = s_n64_report.buttons1; + uint8_t b1 = s_n64_report.buttons2; + int8_t jx = s_n64_report.joy_x; + int8_t jy = s_n64_report.joy_y; + s_dma_buf[0] = N64SEND_DATA(b0, b1, 16); + s_dma_buf[1] = N64SEND_DATA((uint8_t)jx, (uint8_t)jy, 16); + s_dma_buf[2] = 0; + send_response(s_dma_buf, 3); + } + /* Ignore 0x02/0x03 (memory pak) and 0x13 (keyboard) for now. */ + } +} diff --git a/Firmware/RP2040/src/USBDevice/DeviceDriver/N64/n64_device.h b/Firmware/RP2040/src/USBDevice/DeviceDriver/N64/n64_device.h new file mode 100644 index 00000000..29be7334 --- /dev/null +++ b/Firmware/RP2040/src/USBDevice/DeviceDriver/N64/n64_device.h @@ -0,0 +1,30 @@ +/* N64 controller device (output to N64 console). Ported from pdaxrom/usb2n64-adapter. */ + +#ifndef N64_DEVICE_H_ +#define N64_DEVICE_H_ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* 4-byte report: buttons1, buttons2, joy_x, joy_y (same format as N64 host input). */ +typedef struct __attribute__((packed)) { + uint8_t buttons1; + uint8_t buttons2; + int8_t joy_x; + int8_t joy_y; +} N64Report; + +/* Run on core1; blocks forever. data_pin = GPIO for N64 data line (e.g. 19). */ +void n64_device_main(int data_pin); + +/* Set report from Core0 (called by N64Device::process). */ +void n64_device_set_report(const N64Report* report); + +#ifdef __cplusplus +} +#endif + +#endif /* N64_DEVICE_H_ */ diff --git a/Firmware/RP2040/src/USBDevice/DeviceDriver/N64/n64send.pio b/Firmware/RP2040/src/USBDevice/DeviceDriver/N64/n64send.pio new file mode 100644 index 00000000..d42b7c46 --- /dev/null +++ b/Firmware/RP2040/src/USBDevice/DeviceDriver/N64/n64send.pio @@ -0,0 +1,37 @@ +; N64 controller response output. Ported from pdaxrom/usb2n64-adapter. +; Sends bits with N64 timing: pull 32-bit word (bit_count in high 16, bytes in low 16), send bits then stop. + +.program n64send_dma + SET PINS, 0 + SET PINDIRS, 0 + +public loop: + PULL BLOCK + OUT Y, 16 + JMP !Y, send_stop + +pull_and_send: + OUT X, 1 + JMP !X send_0 + JMP send_1 + +send_0: + SET PINDIRS, 1 [30] + NOP [4] + SET PINDIRS, 0 [7] + JMP Y-- pull_and_send + JMP loop + +send_1: + SET PINDIRS, 1 [11] + SET PINDIRS, 0 [31] + JMP Y-- pull_and_send + JMP loop + +send_stop: + SET PINDIRS, 1 [22] + SET PINDIRS, 0 +public stop: + JMP stop + +.wrap diff --git a/Firmware/RP2040/src/USBDevice/DeviceDriver/PS1PS2/PS1PS2.cpp b/Firmware/RP2040/src/USBDevice/DeviceDriver/PS1PS2/PS1PS2.cpp new file mode 100644 index 00000000..d497c3f2 --- /dev/null +++ b/Firmware/RP2040/src/USBDevice/DeviceDriver/PS1PS2/PS1PS2.cpp @@ -0,0 +1,147 @@ +#include +#include "pico/multicore.h" +#include "USBDevice/DeviceDriver/PS1PS2/PS1PS2.h" +#include "USBDevice/DeviceDriver/PS1PS2/psx_simulator.h" + +namespace { + constexpr uint8_t PSX_SELECT = 0x01; + constexpr uint8_t PSX_L3 = 0x02; + constexpr uint8_t PSX_R3 = 0x04; + constexpr uint8_t PSX_START = 0x08; + constexpr uint8_t PSX_UP = 0x10; + constexpr uint8_t PSX_RIGHT = 0x20; + constexpr uint8_t PSX_DOWN = 0x40; + constexpr uint8_t PSX_LEFT = 0x80; + constexpr uint8_t PSX_L2 = 0x01; + constexpr uint8_t PSX_R2 = 0x02; + constexpr uint8_t PSX_L1 = 0x04; + constexpr uint8_t PSX_R1 = 0x08; + constexpr uint8_t PSX_TRI = 0x10; + constexpr uint8_t PSX_CIR = 0x20; + constexpr uint8_t PSX_CROSS = 0x40; + constexpr uint8_t PSX_SQU = 0x80; + constexpr uint8_t STICK_CENTER = 0x80; +} + +void PS1PS2Device::core1_restart_cb() { + multicore_reset_core1(); + multicore_launch_core1(psx_device_main); +} + +void PS1PS2Device::initialize() { + std::memset(&psx_report_, 0, sizeof(psx_report_)); + /* PS2 protocol is active-low: 0xFF = released, 0 = pressed. Default to all released. */ + psx_report_.buttons1 = 0xFF; + psx_report_.buttons2 = 0xFF; + psx_report_.lx = STICK_CENTER; + psx_report_.ly = STICK_CENTER; + psx_report_.rx = STICK_CENTER; + psx_report_.ry = STICK_CENTER; + psx_device_init(0, &psx_report_, &PS1PS2Device::core1_restart_cb); +} + +void PS1PS2Device::process(const uint8_t idx, Gamepad& gamepad) { + (void)idx; + /* Always refresh report from gamepad so Core1 (psx_device_main) sees current/last state when it reads inputState. */ + Gamepad::PadIn gp = gamepad.get_pad_in(); + + /* Active-low: start all released (0xFF), clear bit when pressed */ + psx_report_.buttons1 = 0xFF; + if (gp.dpad & Gamepad::DPAD_UP) psx_report_.buttons1 &= ~PSX_UP; + if (gp.dpad & Gamepad::DPAD_RIGHT) psx_report_.buttons1 &= ~PSX_RIGHT; + if (gp.dpad & Gamepad::DPAD_DOWN) psx_report_.buttons1 &= ~PSX_DOWN; + if (gp.dpad & Gamepad::DPAD_LEFT) psx_report_.buttons1 &= ~PSX_LEFT; + if (gp.buttons & Gamepad::BUTTON_BACK) psx_report_.buttons1 &= ~PSX_SELECT; + if (gp.buttons & Gamepad::BUTTON_L3) psx_report_.buttons1 &= ~PSX_L3; + if (gp.buttons & Gamepad::BUTTON_R3) psx_report_.buttons1 &= ~PSX_R3; + if (gp.buttons & Gamepad::BUTTON_START) psx_report_.buttons1 &= ~PSX_START; + + psx_report_.buttons2 = 0xFF; + if (gp.buttons & Gamepad::BUTTON_LB) psx_report_.buttons2 &= ~PSX_L1; + if (gp.buttons & Gamepad::BUTTON_RB) psx_report_.buttons2 &= ~PSX_R1; + if (gp.buttons & Gamepad::BUTTON_Y) psx_report_.buttons2 &= ~PSX_TRI; + if (gp.buttons & Gamepad::BUTTON_B) psx_report_.buttons2 &= ~PSX_CIR; + if (gp.buttons & Gamepad::BUTTON_A) psx_report_.buttons2 &= ~PSX_CROSS; + if (gp.buttons & Gamepad::BUTTON_X) psx_report_.buttons2 &= ~PSX_SQU; + if (gp.trigger_l > 0) psx_report_.buttons2 &= ~PSX_L2; + if (gp.trigger_r > 0) psx_report_.buttons2 &= ~PSX_R2; + + psx_report_.l2 = gp.trigger_l; + psx_report_.r2 = gp.trigger_r; + + int32_t lx = static_cast(gp.joystick_lx) * 127 / 32767 + STICK_CENTER; + int32_t ly = static_cast(gp.joystick_ly) * 127 / 32767 + STICK_CENTER; /* PS2 left stick Y: up = lower value */ + int32_t rx = static_cast(gp.joystick_rx) * 127 / 32767 + STICK_CENTER; + int32_t ry = static_cast(gp.joystick_ry) * -127 / 32767 + STICK_CENTER; + if (lx < 0) lx = 0; else if (lx > 255) lx = 255; + if (ly < 0) ly = 0; else if (ly > 255) ly = 255; + if (rx < 0) rx = 0; else if (rx > 255) rx = 255; + if (ry < 0) ry = 0; else if (ry > 255) ry = 255; + psx_report_.lx = static_cast(lx); + psx_report_.ly = static_cast(ly); + psx_report_.rx = static_cast(rx); + psx_report_.ry = static_cast(ry); + + /* Home only = IGR (L1+L2+R1+R2+Start+Select); Home+Start = shutdown (L1+L2+R1+R2+L3+R3) */ + bool home_pressed = (gp.buttons & Gamepad::BUTTON_SYS) != 0; + bool start_pressed = (gp.buttons & Gamepad::BUTTON_START) != 0; + if (home_pressed && start_pressed) { + psx_report_.buttons1 &= ~(PSX_L3 | PSX_R3); + psx_report_.buttons2 &= ~(PSX_L1 | PSX_L2 | PSX_R1 | PSX_R2); + psx_report_.l2 = 0xFF; + psx_report_.r2 = 0xFF; + } else if (home_pressed) { + psx_report_.buttons1 &= ~(PSX_SELECT | PSX_START); + psx_report_.buttons2 &= ~(PSX_L1 | PSX_L2 | PSX_R1 | PSX_R2); + psx_report_.l2 = 0xFF; + psx_report_.r2 = 0xFF; + } +} + +uint16_t PS1PS2Device::get_report_cb(uint8_t itf, uint8_t report_id, hid_report_type_t report_type, uint8_t* buffer, uint16_t req_len) { + (void)itf; + (void)report_id; + (void)report_type; + (void)buffer; + (void)req_len; + return 0; +} + +void PS1PS2Device::set_report_cb(uint8_t itf, uint8_t report_id, hid_report_type_t report_type, uint8_t const* buffer, uint16_t buffer_size) { + (void)itf; + (void)report_id; + (void)report_type; + (void)buffer; + (void)buffer_size; +} + +bool PS1PS2Device::vendor_control_xfer_cb(uint8_t rhport, uint8_t stage, tusb_control_request_t const* request) { + (void)rhport; + (void)stage; + (void)request; + return false; +} + +const uint16_t* PS1PS2Device::get_descriptor_string_cb(uint8_t index, uint16_t langid) { + (void)index; + (void)langid; + return nullptr; +} + +const uint8_t* PS1PS2Device::get_descriptor_device_cb() { + return nullptr; +} + +const uint8_t* PS1PS2Device::get_hid_descriptor_report_cb(uint8_t itf) { + (void)itf; + return nullptr; +} + +const uint8_t* PS1PS2Device::get_descriptor_configuration_cb(uint8_t index) { + (void)index; + return nullptr; +} + +const uint8_t* PS1PS2Device::get_descriptor_device_qualifier_cb() { + return nullptr; +} diff --git a/Firmware/RP2040/src/USBDevice/DeviceDriver/PS1PS2/PS1PS2.h b/Firmware/RP2040/src/USBDevice/DeviceDriver/PS1PS2/PS1PS2.h new file mode 100644 index 00000000..79e67fdb --- /dev/null +++ b/Firmware/RP2040/src/USBDevice/DeviceDriver/PS1PS2/PS1PS2.h @@ -0,0 +1,31 @@ +#ifndef _D_PS1PS2_DRIVER_H_ +#define _D_PS1PS2_DRIVER_H_ + +#include + +#include "USBDevice/DeviceDriver/DeviceDriver.h" +#include "USBDevice/DeviceDriver/PS1PS2/psx_simulator.h" + +class PS1PS2Device : public DeviceDriver +{ +public: + void initialize() override; + void process(const uint8_t idx, Gamepad& gamepad) override; + uint16_t get_report_cb(uint8_t itf, uint8_t report_id, hid_report_type_t report_type, uint8_t* buffer, uint16_t req_len) override; + void set_report_cb(uint8_t itf, uint8_t report_id, hid_report_type_t report_type, uint8_t const* buffer, uint16_t buffer_size) override; + bool vendor_control_xfer_cb(uint8_t rhport, uint8_t stage, tusb_control_request_t const* request) override; + const uint16_t* get_descriptor_string_cb(uint8_t index, uint16_t langid) override; + const uint8_t* get_descriptor_device_cb() override; + const uint8_t* get_hid_descriptor_report_cb(uint8_t itf) override; + const uint8_t* get_descriptor_configuration_cb(uint8_t index) override; + const uint8_t* get_descriptor_device_qualifier_cb() override; + + PSXInputState& get_psx_state() { return psx_report_; } + +private: + static void core1_restart_cb(); + + PSXInputState psx_report_{}; +}; + +#endif // _D_PS1PS2_DRIVER_H_ diff --git a/Firmware/RP2040/src/USBDevice/DeviceDriver/PS1PS2/controller_simulator.c b/Firmware/RP2040/src/USBDevice/DeviceDriver/PS1PS2/controller_simulator.c new file mode 100644 index 00000000..2ce8abef --- /dev/null +++ b/Firmware/RP2040/src/USBDevice/DeviceDriver/PS1PS2/controller_simulator.c @@ -0,0 +1,479 @@ +/* PS1/PS2 controller protocol (device side). Ported from PicoGamepadConverter (Loc15). + * https://github.com/Loc15/PicoGamepadConverter */ + +#include +#include "pico/stdlib.h" +#include "hardware/pio.h" +#include "hardware/gpio.h" +#include "pico/multicore.h" +#include "psxSPI.pio.h" +#include "USBDevice/DeviceDriver/PS1PS2/psx_simulator.h" + +#if defined(CONFIG_OGXM_BOARD_PI_PICOW) || defined(CONFIG_OGXM_BOARD_PI_PICO2W) || defined(CONFIG_OGXM_BOARD_RP2354) +/* Pico W / Pico 2 W: do not use GPIO IRQ for PIN_SEL; CYW43 uses gpio_add_raw_irq_handler and conflicts. */ +#define PSX_DEVICE_POLLED_SEL 1 +#else +#include "hardware/irq.h" +#include "hardware/structs/io_bank0.h" +#define PSX_DEVICE_POLLED_SEL 0 +#endif + +#define MODE_DIGITAL 0x41 +#define MODE_ANALOG 0x73 +#define MODE_ANALOG_PRESSURE 0x79 +#define MODE_CONFIG 0xF3 + +#define CMD_PRES_CONFIG 0x40 +#define CMD_POLL_CONFIG_STATUS 0x41 +#define CMD_POLL 0x42 +#define CMD_CONFIG 0x43 +#define CMD_STATUS 0x45 +#define CMD_CONST_46 0x46 +#define CMD_CONST_47 0x47 +#define CMD_CONST_4C 0x4C +#define CMD_ENABLE_RUMBLE 0x4D +#define CMD_POLL_CONFIG 0x4F +#define CMD_ANALOG_SWITCH 0x44 + +#define UP 0x10 +#define RIGHT 0x20 +#define DOWN 0x40 +#define LEFT 0x80 +#define L2 0x01 +#define R2 0x02 +#define L1 0x04 +#define R1 0x08 +#define TRI 0x10 +#define CIR 0x20 +#define X 0x40 +#define SQU 0x80 + +static PIO psx_device_pio; +static uint smCmdReader; +static uint smDatWriter; +static uint offsetCmdReader; +static uint offsetDatWriter; + +volatile PSXInputState *inputState; +static void (*core1_function)(void); +static bool poll_mode = false; + +static uint8_t mode = MODE_ANALOG; +static bool config = false; +static bool analogLock = false; +static uint8_t motorBytes[6] = { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }; +static uint8_t pollConfig[4] = { 0x00, 0x00, 0x00, 0x00 }; + +static void cancel_ack(void) { + pio_sm_exec(psx_device_pio, smCmdReader, pio_encode_jmp(offsetCmdReader)); +} + +static void SEND(uint8_t byte) { + write_byte_blocking(psx_device_pio, smDatWriter, byte); +} + +static uint8_t RECV_CMD(void) { + return read_byte_blocking(psx_device_pio, smCmdReader); +} + +static void initController(void) { + mode = MODE_ANALOG; /* default to DualShock (analog) so sticks work from first poll */ + config = false; + analogLock = false; + memset(motorBytes, 0xFF, sizeof(motorBytes)); + pollConfig[0] = 0xFF; + pollConfig[1] = 0xFF; + pollConfig[2] = 0x03; + pollConfig[3] = 0x00; +} + +static void processRumble(uint8_t index, uint8_t value) { + (void)index; + (void)value; +} + +static void processPresConfig(void) { + if (!config) return; + uint8_t buf[7] = { 0x5A, 0x00, 0x00, 0x02, 0x00, 0x00, 0x5A }; + for (uint8_t i = 0; i < 7; i++) { + SEND(buf[i]); + RECV_CMD(); + } +} + +static void processPollConfigStatus(void) { + if (!config) return; + uint8_t buf[7] = { 0x5A, (mode == MODE_DIGITAL) ? 0x00 : 0xFF, (mode == MODE_DIGITAL) ? 0x00 : 0xFF, (mode == MODE_DIGITAL) ? 0x00 : 0x03, (mode == MODE_DIGITAL) ? 0x00 : 0x00, 0x00, (mode == MODE_DIGITAL) ? 0x00 : 0x5A }; + for (uint8_t i = 0; i < 7; i++) { + SEND(buf[i]); + RECV_CMD(); + } +} + +static void processPoll(void) { + config = false; + switch (mode) { + case MODE_DIGITAL: { + uint8_t buf[3] = { 0x5A, inputState->buttons1, inputState->buttons2 }; + for (uint8_t i = 0; i < 3; i++) { + SEND(buf[i]); + processRumble(i, RECV_CMD()); + } + break; + } + case MODE_ANALOG: { + uint8_t buf[7] = { 0x5A, inputState->buttons1, inputState->buttons2, inputState->rx, inputState->ry, inputState->lx, inputState->ly }; + for (uint8_t i = 0; i < 7; i++) { + SEND(buf[i]); + processRumble(i, RECV_CMD()); + } + break; + } + case MODE_ANALOG_PRESSURE: { + uint8_t buf[19] = { + 0x5A, inputState->buttons1, inputState->buttons2, inputState->rx, inputState->ry, inputState->lx, inputState->ly, + (inputState->buttons1 & RIGHT) ? 0x00 : 0xFF, (inputState->buttons1 & LEFT) ? 0x00 : 0xFF, + (inputState->buttons1 & UP) ? 0x00 : 0xFF, (inputState->buttons1 & DOWN) ? 0x00 : 0xFF, + (inputState->buttons2 & TRI) ? 0x00 : 0xFF, (inputState->buttons2 & CIR) ? 0x00 : 0xFF, + (inputState->buttons2 & X) ? 0x00 : 0xFF, (inputState->buttons2 & SQU) ? 0x00 : 0xFF, + (inputState->buttons2 & L1) ? 0x00 : 0xFF, (inputState->buttons2 & R1) ? 0x00 : 0xFF, + inputState->l2, inputState->r2 + }; + for (uint8_t i = 0; i < 19; i++) { + SEND(buf[i]); + processRumble(i, RECV_CMD()); + } + break; + } + default: + break; + } +} + +static void processConfig(void) { + switch (config ? MODE_CONFIG : mode) { + case MODE_CONFIG: + for (uint8_t i = 0; i < 7; i++) { + SEND((i == 0) ? 0x5A : 0x00); + if (i != 1) + RECV_CMD(); + else + config = RECV_CMD(); + } + break; + case MODE_DIGITAL: { + uint8_t buf[3] = { 0x5A, inputState->buttons1, inputState->buttons2 }; + for (uint8_t i = 0; i < 3; i++) { + SEND(buf[i]); + if (i != 1) + RECV_CMD(); + else + config = RECV_CMD(); + } + break; + } + case MODE_ANALOG: { + uint8_t buf[7] = { 0x5A, inputState->buttons1, inputState->buttons2, inputState->rx, inputState->ry, inputState->lx, inputState->ly }; + for (uint8_t i = 0; i < 7; i++) { + SEND(buf[i]); + if (i != 1) + RECV_CMD(); + else + config = RECV_CMD(); + } + break; + } + case MODE_ANALOG_PRESSURE: { + uint8_t buf[19] = { + 0x5A, inputState->buttons1, inputState->buttons2, inputState->rx, inputState->ry, inputState->lx, inputState->ly, + (inputState->buttons1 & RIGHT) ? 0x00 : 0xFF, (inputState->buttons1 & LEFT) ? 0x00 : 0xFF, + (inputState->buttons1 & UP) ? 0x00 : 0xFF, (inputState->buttons1 & DOWN) ? 0x00 : 0xFF, + (inputState->buttons2 & TRI) ? 0x00 : 0xFF, (inputState->buttons2 & CIR) ? 0x00 : 0xFF, + (inputState->buttons2 & X) ? 0x00 : 0xFF, (inputState->buttons2 & SQU) ? 0x00 : 0xFF, + (inputState->buttons2 & L1) ? 0x00 : 0xFF, (inputState->buttons2 & R1) ? 0x00 : 0xFF, + inputState->l2, inputState->r2 + }; + for (uint8_t i = 0; i < 19; i++) { + SEND(buf[i]); + if (i != 1) + RECV_CMD(); + else + config = RECV_CMD(); + } + break; + } + default: + break; + } +} + +static uint8_t detectAnalog(void) { + if ((pollConfig[0] + pollConfig[1] + pollConfig[2] + pollConfig[3]) > 0) + return MODE_ANALOG_PRESSURE; + return MODE_ANALOG; +} + +static void processAnalogSwitch(void) { + if (!config) return; + for (uint8_t i = 0; i < 7; i++) { + switch (i) { + case 0: + SEND(0x5A); + RECV_CMD(); + break; + case 1: + SEND(0x00); + mode = (RECV_CMD() == 0x01) ? detectAnalog() : MODE_DIGITAL; + break; + case 2: + SEND(0x00); + analogLock = (RECV_CMD() == 0x03) ? 1 : 0; + break; + default: + SEND(0x00); + RECV_CMD(); + break; + } + } +} + +static void processStatus(void) { + if (!config) return; + uint8_t buf[7] = { 0x5A, 0x03, 0x02, (mode == MODE_DIGITAL) ? 0x00 : 0x01, 0x02, 0x01, 0x00 }; + for (uint8_t i = 0; i < 7; i++) { + SEND(buf[i]); + RECV_CMD(); + } +} + +static void processConst46(void) { + if (!config) return; + SEND(0x5A); + RECV_CMD(); + SEND(0x00); + uint8_t offset = RECV_CMD(); + uint8_t buf[5] = { 0x00, 0x01, (offset == 0x00) ? 0x02 : 0x01, (offset == 0x00) ? 0x00 : 0x01, 0x0F }; + for (uint8_t i = 0; i < 5; i++) { + SEND(buf[i]); + RECV_CMD(); + } +} + +static void processConst47(void) { + if (!config) return; + uint8_t buf[7] = { 0x5A, 0x00, 0x00, 0x02, 0x00, 0x01, 0x00 }; + for (uint8_t i = 0; i < 7; i++) { + SEND(buf[i]); + RECV_CMD(); + } +} + +static void processConst4c(void) { + if (!config) return; + SEND(0x5A); + RECV_CMD(); + SEND(0x00); + uint8_t offset = RECV_CMD(); + uint8_t buf[5] = { 0x00, 0x00, (offset == 0x00) ? 0x04 : 0x07, 0x00, 0x00 }; + for (uint8_t i = 0; i < 5; i++) { + SEND(buf[i]); + RECV_CMD(); + } +} + +static void processEnableRumble(void) { + if (!config) return; + for (uint8_t i = 0; i < 7; i++) { + if (i == 0) { + SEND(0x5A); + RECV_CMD(); + } else { + SEND(motorBytes[i - 1]); + motorBytes[i - 1] = RECV_CMD(); + } + } +} + +static void processPollConfig(void) { + if (!config) return; + for (int i = 0; i < 7; i++) { + if (i >= 1 && i <= 4) { + SEND(0x00); + pollConfig[i - 1] = RECV_CMD(); + } else { + SEND((i == 0 || i == 6) ? 0x5A : 0x00); + RECV_CMD(); + } + } + if ((pollConfig[0] + pollConfig[1] + pollConfig[2] + pollConfig[3]) != 0) + mode = MODE_ANALOG_PRESSURE; + else + mode = MODE_ANALOG; +} + +static void process_joy_req(void) { + SEND(config ? MODE_CONFIG : mode); + uint8_t cmd = RECV_CMD(); + switch (cmd) { + case CMD_POLL: + processPoll(); + break; + case CMD_CONFIG: + processConfig(); + break; + case CMD_STATUS: + processStatus(); + break; + case CMD_CONST_46: + processConst46(); + break; + case CMD_CONST_47: + processConst47(); + break; + case CMD_CONST_4C: + processConst4c(); + break; + case CMD_POLL_CONFIG_STATUS: + processPollConfigStatus(); + break; + case CMD_ENABLE_RUMBLE: + processEnableRumble(); + break; + case CMD_POLL_CONFIG: + processPollConfig(); + break; + case CMD_PRES_CONFIG: + processPresConfig(); + break; + case CMD_ANALOG_SWITCH: + processAnalogSwitch(); + break; + default: + break; + } +} + +void psx_device_main(void) { + initController(); + while (true) { + if (RECV_CMD() == 0x01) + process_joy_req(); + } +} + +static void __time_critical_func(restart_pio_sm)(void) { + pio_set_sm_mask_enabled(psx_device_pio, (1u << smCmdReader) | (1u << smDatWriter), false); + pio_restart_sm_mask(psx_device_pio, (1u << smCmdReader) | (1u << smDatWriter)); + pio_sm_exec(psx_device_pio, smCmdReader, pio_encode_jmp(offsetCmdReader)); + pio_sm_exec(psx_device_pio, smDatWriter, pio_encode_jmp(offsetDatWriter)); + pio_sm_clear_fifos(psx_device_pio, smCmdReader); + pio_sm_drain_tx_fifo(psx_device_pio, smDatWriter); + + if (poll_mode) + goto enable_sm; + multicore_reset_core1(); + if (core1_function) + core1_function(); +enable_sm: + pio_enable_sm_mask_in_sync(psx_device_pio, (1u << smCmdReader) | (1u << smDatWriter)); +} + +#if !PSX_DEVICE_POLLED_SEL +/* Shared handler: IO_IRQ_BANK0 is used by CYW43/GPIO elsewhere; only run when our pin triggered. */ +static void __time_critical_func(sel_isr_callback)(void) { + uint32_t word = io_bank0_hw->intr[PIN_SEL / 8]; + uint32_t shift = 4u * (PIN_SEL % 8); + if (!((word >> shift) & 0xFu)) + return; + gpio_acknowledge_irq(PIN_SEL, GPIO_IRQ_EDGE_RISE); + restart_pio_sm(); +} +#endif + +static void init_pio(void) { + gpio_set_dir(PIN_DAT, false); + gpio_set_dir(PIN_CMD, false); + gpio_set_dir(PIN_SEL, false); + gpio_set_dir(PIN_CLK, false); + gpio_set_dir(PIN_ACK, false); + gpio_disable_pulls(PIN_DAT); + gpio_disable_pulls(PIN_CMD); + gpio_disable_pulls(PIN_SEL); + gpio_disable_pulls(PIN_CLK); + gpio_disable_pulls(PIN_ACK); + + smCmdReader = pio_claim_unused_sm(psx_device_pio, true); + smDatWriter = pio_claim_unused_sm(psx_device_pio, true); + + offsetCmdReader = pio_add_program(psx_device_pio, &cmd_reader_program); + offsetDatWriter = pio_add_program(psx_device_pio, &dat_writer_program); + + cmd_reader_program_init(psx_device_pio, smCmdReader, offsetCmdReader); + dat_writer_program_init(psx_device_pio, smDatWriter, offsetDatWriter); + pio_enable_sm_mask_in_sync(psx_device_pio, (1u << smCmdReader) | (1u << smDatWriter)); +} + +void psx_device_set_poll_mode(bool enable) { + poll_mode = enable; +} + +#if PSX_DEVICE_POLLED_SEL +/* When Core1 runs psx_device_main(), Core0 must not read the CMD FIFO. Call this from Core0 + * main loop to handle SEL rising edge (restart PIO) only. Does not read from FIFO. */ +void psx_device_sel_restart_check(void) { + static bool last_sel_high = false; + bool sel_high = gpio_get(PIN_SEL); + if (sel_high && !last_sel_high) + restart_pio_sm(); + last_sel_high = sel_high; +} +#else +void psx_device_sel_restart_check(void) { (void)0; } +#endif + +/* One iteration of the protocol when in poll mode (Core0-driven, e.g. Pico W with BT on Core1). + * Return 1 if a transaction was processed, 0 if no data or after desync drain. + * If the first byte is not 0x01 we are desynced (e.g. missed a transaction); drain the RX FIFO + * so the next SEL transaction starts clean. Caller should loop until 0 so OPL/rapid init don't fall behind. */ +int psx_device_poll(void) { + static bool controller_inited = false; +#if PSX_DEVICE_POLLED_SEL + static bool last_sel_high = false; + bool sel_high = gpio_get(PIN_SEL); + if (sel_high && !last_sel_high) + restart_pio_sm(); + last_sel_high = sel_high; +#endif + if (!controller_inited) { + initController(); + controller_inited = true; + } + if (pio_sm_get_rx_fifo_level(psx_device_pio, smCmdReader) == 0) + return 0; + uint8_t first = (uint8_t)(pio_sm_get(psx_device_pio, smCmdReader) >> 24); + if (first != 0x01) { + /* Desynced: drain RX FIFO so next transaction (after SEL cycle) is clean. */ + while (pio_sm_get_rx_fifo_level(psx_device_pio, smCmdReader) > 0) + (void)pio_sm_get(psx_device_pio, smCmdReader); + return 0; + } + process_joy_req(); + return 1; +} + +void psx_device_init(unsigned pio, PSXInputState *data, void (*reset_pio)(void)) { + psx_device_pio = pio ? pio1 : pio0; + inputState = data; + core1_function = reset_pio; + + init_pio(); + + gpio_set_slew_rate(PIN_DAT, GPIO_SLEW_RATE_FAST); + gpio_set_drive_strength(PIN_DAT, GPIO_DRIVE_STRENGTH_12MA); + +#if !PSX_DEVICE_POLLED_SEL + gpio_set_irq_enabled(PIN_SEL, GPIO_IRQ_EDGE_RISE, true); + irq_add_shared_handler(IO_IRQ_BANK0, sel_isr_callback, 0u); + irq_set_enabled(IO_IRQ_BANK0, true); +#endif +} diff --git a/Firmware/RP2040/src/USBDevice/DeviceDriver/PS1PS2/psxSPI.pio b/Firmware/RP2040/src/USBDevice/DeviceDriver/PS1PS2/psxSPI.pio new file mode 100644 index 00000000..a6b4f3f9 --- /dev/null +++ b/Firmware/RP2040/src/USBDevice/DeviceDriver/PS1PS2/psxSPI.pio @@ -0,0 +1,87 @@ +; PS1/PS2 controller protocol (device side). From PicoGamepadConverter (Loc15). +; Interfaces with the modified SPI protocol used by PSX to communicate with controller/memory cards. +; Pinout for OGX-Mini (standard Pico): CMD=19, CLK=20, ATT/SEL=21, DAT=22, ACK=26 + +.define PUBLIC PIN_DAT 19 +.define PUBLIC PIN_CMD 20 +.define PUBLIC PIN_SEL 21 +.define PUBLIC PIN_CLK 22 +.define PUBLIC PIN_ACK 26 + +.program cmd_reader +; Samples CMD line during rising clock edges, waits for SEL low before starting +sel_high: +wait 0 gpio PIN_SEL +set x, 7 +.wrap_target +wait 0 gpio PIN_CLK +wait 1 gpio PIN_CLK +in pins 1 +.wrap + +.program dat_reader +sel_high: +wait 0 gpio PIN_SEL +.wrap_target +wait 0 gpio PIN_CLK +wait 1 gpio PIN_CLK +in pins 1 +.wrap + +.program dat_writer +.side_set 1 pindirs +set pindirs, 0 side 0 +wait 0 gpio PIN_SEL side 0 +.wrap_target +pull side 0 +nop side 1 [5] +set x, 7 side 0 [5] +sendbit: +wait 1 gpio PIN_CLK side 0 +wait 0 gpio PIN_CLK side 0 +out pindirs 1 side 0 +jmp x-- sendbit side 0 +.wrap + +% c-sdk { +#define SLOW_CLKDIV 50 + +static inline void cmd_reader_program_init(PIO pio, uint sm, uint offset) { + pio_sm_config c = cmd_reader_program_get_default_config(offset); + sm_config_set_in_pins(&c, PIN_CMD); + pio_sm_set_consecutive_pindirs(pio, sm, PIN_CMD, 1, false); + pio_sm_set_consecutive_pindirs(pio, sm, PIN_SEL, 1, false); + pio_sm_set_consecutive_pindirs(pio, sm, PIN_CLK, 1, false); + sm_config_set_in_shift(&c, true, true, 8); + sm_config_set_fifo_join(&c, PIO_FIFO_JOIN_RX); + sm_config_set_clkdiv_int_frac(&c, SLOW_CLKDIV, 0x00); + pio_sm_init(pio, sm, offset, &c); +} + +static inline void dat_writer_program_init(PIO pio, uint sm, uint offset) { + pio_sm_config c = dat_writer_program_get_default_config(offset); + sm_config_set_out_pins(&c, PIN_DAT, 1); + sm_config_set_set_pins(&c, PIN_DAT, 1); + sm_config_set_sideset_pins(&c, PIN_ACK); + pio_sm_set_pins_with_mask(pio, sm, 0, 1 << PIN_DAT); + pio_sm_set_consecutive_pindirs(pio, sm, PIN_DAT, 1, false); + pio_gpio_init(pio, PIN_DAT); + pio_sm_set_pins_with_mask(pio, sm, 0, 1 << PIN_ACK); + pio_sm_set_consecutive_pindirs(pio, sm, PIN_ACK, 1, false); + pio_gpio_init(pio, PIN_ACK); + pio_sm_set_consecutive_pindirs(pio, sm, PIN_SEL, 1, false); + pio_sm_set_consecutive_pindirs(pio, sm, PIN_CLK, 1, false); + sm_config_set_out_shift(&c, true, true, 8); + sm_config_set_fifo_join(&c, PIO_FIFO_JOIN_TX); + sm_config_set_clkdiv_int_frac(&c, SLOW_CLKDIV, 0x00); + pio_sm_init(pio, sm, offset, &c); +} + +static inline uint8_t read_byte_blocking(PIO pio, uint sm) { + return (uint8_t)(pio_sm_get_blocking(pio, sm) >> 24); +} + +static inline void write_byte_blocking(PIO pio, uint sm, uint32_t byte) { + pio_sm_put_blocking(pio, sm, ~byte & 0xFF); +} +%} diff --git a/Firmware/RP2040/src/USBDevice/DeviceDriver/PS1PS2/psx_simulator.h b/Firmware/RP2040/src/USBDevice/DeviceDriver/PS1PS2/psx_simulator.h new file mode 100644 index 00000000..2c691c87 --- /dev/null +++ b/Firmware/RP2040/src/USBDevice/DeviceDriver/PS1PS2/psx_simulator.h @@ -0,0 +1,48 @@ +/* PS1/PS2 controller protocol (device side). Ported from PicoGamepadConverter by Loc15. + * https://github.com/Loc15/PicoGamepadConverter + * License: GPL-3.0. OGX-Mini uses this for GPIO output to PS1/PS2 console. */ + +#ifndef PS1PS2_PSX_SIMULATOR_H_ +#define PS1PS2_PSX_SIMULATOR_H_ + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct __attribute__((packed, aligned(1))) { + uint8_t buttons1; + uint8_t buttons2; + uint8_t lx; + uint8_t ly; + uint8_t rx; + uint8_t ry; + uint8_t l2; + uint8_t r2; +} PSXInputState; + +/* Initialize the PS1/PS2 controller simulator. Uses PIO and GPIO; call once before launching core1. + * pio: 0 = PIO0, 1 = PIO1. + * data: pointer to shared state (written by main loop, read by simulator on core1). + * reset_pio: callback to restart core1 (e.g. multicore_reset_core1 + multicore_launch_core1(psx_device_main)). */ +void psx_device_init(unsigned pio, PSXInputState* data, void (*reset_pio)(void)); + +/* Entry for core1: run this in an infinite loop. Blocks. */ +void psx_device_main(void); + +/* When enabled, SEL IRQ only restarts PIO (no Core1 reset). Use with psx_device_poll() on Core0 (e.g. Pico W: BT on Core1, PS2 on Core0). */ +void psx_device_set_poll_mode(bool enable); + +/* SEL rising-edge check and PIO restart only; does not read CMD FIFO. Use from Core0 when Core1 runs psx_device_main() (Pico W PS2 + OPL). */ +void psx_device_sel_restart_check(void); + +/* One poll iteration; call from Core0 main loop when in poll mode. Returns 1 if a command was processed. */ +int psx_device_poll(void); + +#ifdef __cplusplus +} +#endif + +#endif /* PS1PS2_PSX_SIMULATOR_H_ */ diff --git a/Firmware/RP2040/src/USBDevice/DeviceDriver/PS3/PS3.cpp b/Firmware/RP2040/src/USBDevice/DeviceDriver/PS3/PS3.cpp index c070b5d1..5e162ef4 100644 --- a/Firmware/RP2040/src/USBDevice/DeviceDriver/PS3/PS3.cpp +++ b/Firmware/RP2040/src/USBDevice/DeviceDriver/PS3/PS3.cpp @@ -1,8 +1,104 @@ #include #include +#include "pico/time.h" +#include "Gamepad/MotionImu.h" #include "USBDevice/DeviceDriver/PS3/PS3.h" +namespace { + +/* + * DS3 Sixaxis wire format (Linux drivers/hid/hid-sony.c): bytes 41–48 are MSB-first 16-bit values; + * accel layout X @ 41–42, Z @ 43–44, Y @ 45–46; kernel uses 10-bit–style center 511. + * PadIn accel/gyro follow Bluepad DS4 units (accel, gyro[2] for yaw rate). + */ +void apply_pad_imu_to_ps3_sixaxis(PS3::InReport& rep, const Gamepad::PadIn& gp_in) +{ + constexpr int32_t kDs4UnitsPerG = 8192; + constexpr int32_t kDs3CountsPerG = 113; + constexpr int32_t kGyroPerDps = 1024; + + int32_t accel[3] = {gp_in.accel[0], gp_in.accel[1], gp_in.accel[2]}; + int32_t gyro[3] = {gp_in.gyro[0], gp_in.gyro[1], gp_in.gyro[2]}; + MotionImu::remap_to_ds4_playing_frame(gp_in.motion_source, accel, gyro); + + int32_t ax = accel[0]; + int32_t ay = accel[1]; + int32_t az = accel[2]; + + auto iabs = [](int32_t v) -> int64_t { + const int64_t w = static_cast(v); + return w >= 0 ? w : -w; + }; + const int64_t l1 = iabs(ax) + iabs(ay) + iabs(az); + constexpr int64_t kNearZeroL1 = 512; + if (l1 < kNearZeroL1) { + ax = 0; + ay = 0; + az = kDs4UnitsPerG; + } + + auto ds4_to_ds3_delta = [](int32_t ds4_units) -> int32_t { + return static_cast((static_cast(ds4_units) * kDs3CountsPerG) / kDs4UnitsPerG); + }; + + auto encode_accel = [&ds4_to_ds3_delta](int32_t ds4_units, bool kernel_inverts) -> uint16_t { + int32_t d = ds4_to_ds3_delta(ds4_units); + if (d > 511) { + d = 511; + } + if (d < -512) { + d = -512; + } + int32_t raw = kernel_inverts ? (511 - d) : (511 + d); + if (raw < 0) { + raw = 0; + } + if (raw > 1023) { + raw = 1023; + } + return static_cast(raw); + }; + + const bool wii_imu = gp_in.motion_source == Gamepad::PadIn::MOTION_SRC_WII_BT; + + const uint16_t raw_x = encode_accel(ax, false); + /* Linux hid-sony: report Z bytes → kernel ABS_Z, report Y bytes → kernel ABS_Y. + * Real DS3 neutral gravity is on ABS_Y; Wii pitch was on ay and gravity on az in + * DS4 frame — swap wires so gravity lands on acceler_z (kernel Y). */ + uint16_t raw_z_wire; + uint16_t raw_y_wire; + if (wii_imu) { + raw_z_wire = encode_accel(ay, true); + raw_y_wire = encode_accel(az, true); + } else { + raw_z_wire = encode_accel(az, true); + raw_y_wire = encode_accel(ay, true); + } + + rep.acceler_x = __builtin_bswap16(raw_x); + rep.acceler_y = __builtin_bswap16(raw_z_wire); + rep.acceler_z = __builtin_bswap16(raw_y_wire); + + int32_t gz = static_cast((static_cast(gyro[2]) * 511) / (kGyroPerDps * 64)); + if (gz > 511) { + gz = 511; + } + if (gz < -512) { + gz = -512; + } + int32_t raw_g_i = 511 + gz; + if (raw_g_i < 0) { + raw_g_i = 0; + } + if (raw_g_i > 1023) { + raw_g_i = 1023; + } + rep.gyro_z = __builtin_bswap16(static_cast(raw_g_i)); +} + +} // namespace + void PS3Device::initialize() { class_driver_ = @@ -20,7 +116,40 @@ void PS3Device::initialize() void PS3Device::process(const uint8_t idx, Gamepad& gamepad) { - if (gamepad.new_pad_in()) + // Remote wake when host has suspended the bus (e.g. PS3 standby): PS button or Start held 3s (same as 360). + { + static bool start_wake_sent = false; + static bool start_held = false; + static absolute_time_t start_hold_begin = { 0 }; + Gamepad::PadIn gp_wake = gamepad.get_pad_in(); + bool start_pressed = (gp_wake.buttons & Gamepad::BUTTON_START) != 0; + if (start_pressed) + { + if (!start_held) + { + start_held = true; + start_hold_begin = get_absolute_time(); + } + else + { + uint64_t hold_ms = to_ms_since_boot(get_absolute_time()) - to_ms_since_boot(start_hold_begin); + if (hold_ms >= 3000 && tud_suspended() && !start_wake_sent) + { + tud_remote_wakeup(); + start_wake_sent = true; + } + } + } + else + { + start_held = false; + start_wake_sent = false; + } + if (tud_suspended() && (gp_wake.buttons & Gamepad::BUTTON_SYS)) + tud_remote_wakeup(); + } + + /* Rebuild report_in_ every frame so GET_REPORT and IMU stay current; send on HID IN when ready. */ { Gamepad::PadIn gp_in = gamepad.get_pad_in(); report_in_ = PS3::InReport(); @@ -65,31 +194,54 @@ void PS3Device::process(const uint8_t idx, Gamepad& gamepad) if (gp_in.buttons & Gamepad::BUTTON_START) report_in_.buttons[0] |= PS3::Buttons0::START; if (gp_in.buttons & Gamepad::BUTTON_L3) report_in_.buttons[0] |= PS3::Buttons0::L3; if (gp_in.buttons & Gamepad::BUTTON_R3) report_in_.buttons[0] |= PS3::Buttons0::R3; - if (gp_in.buttons & Gamepad::BUTTON_SYS) report_in_.buttons[2] |= PS3::Buttons2::SYS; + // DS3 report: buttons[2] byte = PS (bit0), Touchpad (bit1). Latch PS so DS4/DS5 over BT short taps aren't missed. + if (gp_in.buttons & Gamepad::BUTTON_SYS) + sys_button_latch_frames_ = 8; + if (sys_button_latch_frames_ > 0) { + report_in_.buttons[2] |= PS3::Buttons2::SYS; + sys_button_latch_frames_--; + } if (gp_in.buttons & Gamepad::BUTTON_MISC) report_in_.buttons[2] |= PS3::Buttons2::TP; if (gp_in.trigger_l) report_in_.buttons[1] |= PS3::Buttons1::L2; if (gp_in.trigger_r) report_in_.buttons[1] |= PS3::Buttons1::R2; - report_in_.joystick_lx = Scale::int16_to_uint8(gp_in.joystick_lx); - report_in_.joystick_ly = Scale::int16_to_uint8(gp_in.joystick_ly); - report_in_.joystick_rx = Scale::int16_to_uint8(gp_in.joystick_rx); - report_in_.joystick_ry = Scale::int16_to_uint8(gp_in.joystick_ry); + // L2/R2 axis values (critical for PS3 games - avoids stuck triggers) + report_in_.l2_axis = gp_in.trigger_l; + report_in_.r2_axis = gp_in.trigger_r; + + // Emulate DS3/Sixaxis sticks: 8-bit range 0–255, center 0x80 (128). Small deadzone (~1.5%) like real hardware. + constexpr int16_t DEADZONE = 512; // ~1.5% of 32768 + constexpr uint8_t PS3_CENTER = PS3::JOYSTICK_MID; // 0x80 + + auto joystick_to_ps3 = [](int16_t value, int16_t deadzone) -> uint8_t { + if (value > -deadzone && value < deadzone) + return PS3_CENTER; + int32_t in = static_cast(value) + 32768; + int32_t scaled = (in * 255 + 32768) / 65535; + if (scaled < 0) scaled = 0; + if (scaled > 255) scaled = 255; + return static_cast(scaled); + }; + + report_in_.joystick_lx = joystick_to_ps3(gp_in.joystick_lx, DEADZONE); + report_in_.joystick_ly = joystick_to_ps3(gp_in.joystick_ly, DEADZONE); + report_in_.joystick_rx = joystick_to_ps3(gp_in.joystick_rx, DEADZONE); + report_in_.joystick_ry = joystick_to_ps3(gp_in.joystick_ry, DEADZONE); if (gamepad.analog_enabled()) { - report_in_.up_axis = gp_in.analog[Gamepad::ANALOG_OFF_UP]; - report_in_.down_axis = gp_in.analog[Gamepad::ANALOG_OFF_DOWN]; - report_in_.right_axis = gp_in.analog[Gamepad::ANALOG_OFF_RIGHT]; - report_in_.left_axis = gp_in.analog[Gamepad::ANALOG_OFF_LEFT]; - report_in_.triangle_axis = gp_in.analog[Gamepad::ANALOG_OFF_Y]; report_in_.circle_axis = gp_in.analog[Gamepad::ANALOG_OFF_B]; report_in_.cross_axis = gp_in.analog[Gamepad::ANALOG_OFF_A]; report_in_.square_axis = gp_in.analog[Gamepad::ANALOG_OFF_X]; - report_in_.r1_axis = gp_in.analog[Gamepad::ANALOG_OFF_RB]; report_in_.l1_axis = gp_in.analog[Gamepad::ANALOG_OFF_LB]; + // D-pad: use digital conversion to avoid noisy analog causing stuck inputs + report_in_.up_axis = (gp_in.dpad & Gamepad::DPAD_UP) ? 0xFF : 0; + report_in_.down_axis = (gp_in.dpad & Gamepad::DPAD_DOWN) ? 0xFF : 0; + report_in_.right_axis = (gp_in.dpad & Gamepad::DPAD_RIGHT) ? 0xFF : 0; + report_in_.left_axis = (gp_in.dpad & Gamepad::DPAD_LEFT) ? 0xFF : 0; } else { @@ -99,31 +251,37 @@ void PS3Device::process(const uint8_t idx, Gamepad& gamepad) report_in_.left_axis = (gp_in.dpad & Gamepad::DPAD_LEFT) ? 0xFF : 0; report_in_.triangle_axis = (gp_in.buttons & Gamepad::BUTTON_Y) ? 0xFF : 0; - report_in_.circle_axis = (gp_in.buttons & Gamepad::BUTTON_X) ? 0xFF : 0; - report_in_.cross_axis = (gp_in.buttons & Gamepad::BUTTON_B) ? 0xFF : 0; - report_in_.square_axis = (gp_in.buttons & Gamepad::BUTTON_A) ? 0xFF : 0; + report_in_.circle_axis = (gp_in.buttons & Gamepad::BUTTON_B) ? 0xFF : 0; + report_in_.cross_axis = (gp_in.buttons & Gamepad::BUTTON_A) ? 0xFF : 0; + report_in_.square_axis = (gp_in.buttons & Gamepad::BUTTON_X) ? 0xFF : 0; report_in_.r1_axis = (gp_in.buttons & Gamepad::BUTTON_RB) ? 0xFF : 0; report_in_.l1_axis = (gp_in.buttons & Gamepad::BUTTON_LB) ? 0xFF : 0; } - } - if (tud_suspended()) - { - tud_remote_wakeup(); + if (gp_in.has_motion()) { + apply_pad_imu_to_ps3_sixaxis(report_in_, gp_in); + } } - if (tud_hid_ready()) - { - //PS3 seems to start using stale data if a report isn't sent every frame + if (tud_hid_ready()) { tud_hid_report(0, reinterpret_cast(&report_in_), sizeof(PS3::InReport)); } if (new_report_out_) { Gamepad::PadOut gp_out; - gp_out.rumble_l = report_out_.rumble.left_motor_force; - gp_out.rumble_r = report_out_.rumble.right_motor_on ? Range::MAX : 0; + /* Windows DInput often leaves a small non-zero large-motor byte or sets the small-motor + * field to values other than 0/1; forwarding that blindly causes constant rumble on the BT pad. */ + constexpr uint8_t HOST_LARGE_MOTOR_DEADZONE = 12; + uint8_t lf = report_out_.rumble.left_motor_force; + if (lf < HOST_LARGE_MOTOR_DEADZONE) { + lf = 0; + } + gp_out.rumble_l = lf; + /* DS3 output uses 0/1 for the small motor; only 0x01 counts as on (not "any non-zero"). */ + const uint8_t rm = report_out_.rumble.right_motor_on; + gp_out.rumble_r = (rm == 1u) ? Range::MAX : 0; gamepad.set_pad_out(gp_out); new_report_out_ = false; } diff --git a/Firmware/RP2040/src/USBDevice/DeviceDriver/PS3/PS3.h b/Firmware/RP2040/src/USBDevice/DeviceDriver/PS3/PS3.h index 97f511a0..629540f2 100644 --- a/Firmware/RP2040/src/USBDevice/DeviceDriver/PS3/PS3.h +++ b/Firmware/RP2040/src/USBDevice/DeviceDriver/PS3/PS3.h @@ -27,6 +27,8 @@ class PS3Device : public DeviceDriver PS3::BTInfo bt_info_; uint8_t ef_byte_; bool new_report_out_{false}; + // Latch PS button so short taps (DS4/DS5 over BT) aren't missed due to cross-core timing. + uint8_t sys_button_latch_frames_{0}; }; #endif // _PS3_DEVICE_H_ diff --git a/Firmware/RP2040/src/USBDevice/DeviceDriver/PS4/PS4.cpp b/Firmware/RP2040/src/USBDevice/DeviceDriver/PS4/PS4.cpp new file mode 100644 index 00000000..27d5224d --- /dev/null +++ b/Firmware/RP2040/src/USBDevice/DeviceDriver/PS4/PS4.cpp @@ -0,0 +1,315 @@ +#include +#include + +#include "pico/time.h" +#include "Gamepad/MotionImu.h" +#include "USBDevice/DeviceDriver/PS4/PS4.h" +#include "Descriptors/PS4Usb.h" + +namespace { + +constexpr uint8_t kReportIdIn = 0x01; + +uint8_t joystick_to_u8(int16_t value, int16_t deadzone) +{ + constexpr uint8_t center = PS4::JOYSTICK_MID; + if (value > -deadzone && value < deadzone) { + return center; + } + int32_t in = static_cast(value) + 32768; + int32_t scaled = (in * 255 + 32768) / 65535; + if (scaled < 0) { + scaled = 0; + } + if (scaled > 255) { + scaled = 255; + } + return static_cast(scaled); +} + +/* Brook-style scaling (gyro/8, accel/64). Truncating division zeros small Bluetooth IMU samples; + * use symmetric rounding so light motion still reaches the host. */ +int16_t scale_i32_to_i16_rounded(int32_t v, int32_t div) +{ + if (div <= 0) { + div = 1; + } + const int64_t num = static_cast(v); + const int64_t d = static_cast(div); + const int64_t q = (num >= 0) ? (num + d / 2) / d : (num - d / 2) / d; + if (q > 32767) { + return 32767; + } + if (q < -32768) { + return -32768; + } + return static_cast(q); +} + +/* Brook capture: int16 LE gyro @ 13–18, accel @ 19–24 in report id 1. Filled from DS4/DS5 BT, + * Switch Pro, or wired DS4/DualSense USB host paths. */ +void apply_pad_imu_to_ps4_report(std::array& rep, const Gamepad::PadIn& gp_in) +{ + if (!gp_in.has_motion()) { + return; + } + + int32_t accel[3] = {gp_in.accel[0], gp_in.accel[1], gp_in.accel[2]}; + int32_t gyro[3] = {gp_in.gyro[0], gp_in.gyro[1], gp_in.gyro[2]}; + MotionImu::remap_to_ds4_playing_frame(gp_in.motion_source, accel, gyro); + + const int16_t gx = scale_i32_to_i16_rounded(gyro[0], 8); + const int16_t gy = scale_i32_to_i16_rounded(gyro[1], 8); + const int16_t gz = scale_i32_to_i16_rounded(gyro[2], 8); + const int16_t ax = scale_i32_to_i16_rounded(accel[0], 64); + const int16_t ay = scale_i32_to_i16_rounded(accel[1], 64); + const int16_t az = scale_i32_to_i16_rounded(accel[2], 64); + + std::memcpy(&rep[13], &gx, 2); + std::memcpy(&rep[15], &gy, 2); + std::memcpy(&rep[17], &gz, 2); + std::memcpy(&rep[19], &ax, 2); + std::memcpy(&rep[21], &ay, 2); + std::memcpy(&rep[23], &az, 2); +} + +} // namespace + +void PS4Device::initialize() +{ + class_driver_ = { + .name = TUD_DRV_NAME("PS4"), + .init = hidd_init, + .deinit = hidd_deinit, + .reset = hidd_reset, + .open = hidd_open, + .control_xfer_cb = hidd_control_xfer_cb, + .xfer_cb = hidd_xfer_cb, + .sof = NULL + }; + std::memset(report_in_.data(), 0, report_in_.size()); + report_in_[0] = kReportIdIn; + report_in_[1] = report_in_[2] = report_in_[3] = report_in_[4] = PS4::JOYSTICK_MID; + report_out_.report_id = 0x05; +} + +void PS4Device::process(const uint8_t idx, Gamepad& gamepad) +{ + (void)idx; + { + static bool start_wake_sent = false; + static bool start_held = false; + static absolute_time_t start_hold_begin = { 0 }; + Gamepad::PadIn gp_wake = gamepad.get_pad_in(); + bool start_pressed = (gp_wake.buttons & Gamepad::BUTTON_START) != 0; + if (start_pressed) { + if (!start_held) { + start_held = true; + start_hold_begin = get_absolute_time(); + } else { + uint64_t hold_ms = to_ms_since_boot(get_absolute_time()) - to_ms_since_boot(start_hold_begin); + if (hold_ms >= 3000 && tud_suspended() && !start_wake_sent) { + tud_remote_wakeup(); + start_wake_sent = true; + } + } + } else { + start_held = false; + start_wake_sent = false; + } + if (tud_suspended() && (gp_wake.buttons & Gamepad::BUTTON_SYS)) { + tud_remote_wakeup(); + } + } + + Gamepad::PadIn gp_in = gamepad.get_pad_in(); + std::memset(report_in_.data(), 0, report_in_.size()); + report_in_[0] = kReportIdIn; + + constexpr int16_t DEADZONE = 512; + report_in_[1] = joystick_to_u8(gp_in.joystick_lx, DEADZONE); + report_in_[2] = joystick_to_u8(gp_in.joystick_ly, DEADZONE); + report_in_[3] = joystick_to_u8(gp_in.joystick_rx, DEADZONE); + report_in_[4] = joystick_to_u8(gp_in.joystick_ry, DEADZONE); + + uint8_t b0 = PS4::Buttons0::DPAD_CENTER; + switch (gp_in.dpad) + { + case Gamepad::DPAD_UP: + b0 = PS4::Buttons0::DPAD_UP; + break; + case Gamepad::DPAD_DOWN: + b0 = PS4::Buttons0::DPAD_DOWN; + break; + case Gamepad::DPAD_LEFT: + b0 = PS4::Buttons0::DPAD_LEFT; + break; + case Gamepad::DPAD_RIGHT: + b0 = PS4::Buttons0::DPAD_RIGHT; + break; + case Gamepad::DPAD_UP_RIGHT: + b0 = PS4::Buttons0::DPAD_UP_RIGHT; + break; + case Gamepad::DPAD_DOWN_RIGHT: + b0 = PS4::Buttons0::DPAD_RIGHT_DOWN; + break; + case Gamepad::DPAD_DOWN_LEFT: + b0 = PS4::Buttons0::DPAD_DOWN_LEFT; + break; + case Gamepad::DPAD_UP_LEFT: + b0 = PS4::Buttons0::DPAD_LEFT_UP; + break; + default: + break; + } + + uint8_t b1 = 0; + uint8_t b2 = 0; + + if (gp_in.buttons & Gamepad::BUTTON_X) { + b0 |= PS4::Buttons0::SQUARE; + } + if (gp_in.buttons & Gamepad::BUTTON_A) { + b0 |= PS4::Buttons0::CROSS; + } + if (gp_in.buttons & Gamepad::BUTTON_B) { + b0 |= PS4::Buttons0::CIRCLE; + } + if (gp_in.buttons & Gamepad::BUTTON_Y) { + b0 |= PS4::Buttons0::TRIANGLE; + } + if (gp_in.buttons & Gamepad::BUTTON_LB) { + b1 |= PS4::Buttons1::L1; + } + if (gp_in.buttons & Gamepad::BUTTON_RB) { + b1 |= PS4::Buttons1::R1; + } + if (gp_in.buttons & Gamepad::BUTTON_L3) { + b1 |= PS4::Buttons1::L3; + } + if (gp_in.buttons & Gamepad::BUTTON_R3) { + b1 |= PS4::Buttons1::R3; + } + if (gp_in.buttons & Gamepad::BUTTON_BACK) { + b1 |= PS4::Buttons1::SHARE; + } + if (gp_in.buttons & Gamepad::BUTTON_START) { + b1 |= PS4::Buttons1::OPTIONS; + } + if (gp_in.buttons & Gamepad::BUTTON_SYS) { + sys_button_latch_frames_ = 8; + } + if (sys_button_latch_frames_ > 0) { + b2 |= PS4::Buttons2::PS; + sys_button_latch_frames_--; + } + if (gp_in.buttons & Gamepad::BUTTON_MISC) { + b2 |= PS4::Buttons2::TP; + } + + if (gp_in.trigger_l) { + b1 |= PS4::Buttons1::L2; + } + if (gp_in.trigger_r) { + b1 |= PS4::Buttons1::R2; + } + + report_in_[5] = b0; + report_in_[6] = b1; + touch_seq_ = static_cast((touch_seq_ + 1u) & 0x3Fu); + b2 = static_cast(b2 | static_cast(static_cast(touch_seq_ << 2))); + report_in_[7] = b2; + + report_in_[8] = gp_in.trigger_l; + report_in_[9] = gp_in.trigger_r; + + frame_seq_++; + std::memcpy(&report_in_[10], &frame_seq_, sizeof(frame_seq_)); + report_in_[12] = 0; + + apply_pad_imu_to_ps4_report(report_in_, gp_in); + + if (tud_hid_ready()) { + tud_hid_report(0, report_in_.data(), static_cast(report_in_.size())); + } + + if (new_report_out_) { + Gamepad::PadOut gp_out; + gp_out.rumble_l = report_out_.motor_left; + gp_out.rumble_r = report_out_.motor_right; + gamepad.set_pad_out(gp_out); + new_report_out_ = false; + } +} + +uint16_t PS4Device::get_report_cb(uint8_t itf, uint8_t report_id, hid_report_type_t report_type, uint8_t *buffer, uint16_t reqlen) +{ + (void)itf; + if (report_type == HID_REPORT_TYPE_INPUT) { + if (report_id == 0 || report_id == kReportIdIn) { + const uint16_t n = static_cast(std::min(reqlen, report_in_.size())); + std::memcpy(buffer, report_in_.data(), n); + return n; + } + } else if (report_type == HID_REPORT_TYPE_FEATURE) { + std::memset(buffer, 0, reqlen); + return reqlen; + } + return 0; +} + +void PS4Device::set_report_cb(uint8_t itf, uint8_t report_id, hid_report_type_t report_type, uint8_t const *buffer, uint16_t bufsize) +{ + (void)itf; + if (report_type != HID_REPORT_TYPE_OUTPUT) { + return; + } + uint8_t rid = report_id; + const uint8_t *buf = buffer; + uint16_t len = bufsize; + if (rid == 0 && len > 0) { + rid = buffer[0]; + len = static_cast(len - 1u); + buf = &buffer[1]; + } + if (rid == 0x05 && len >= sizeof(PS4::OutReport)) { + std::memcpy(&report_out_, buf, sizeof(PS4::OutReport)); + new_report_out_ = true; + } +} + +bool PS4Device::vendor_control_xfer_cb(uint8_t rhport, uint8_t stage, tusb_control_request_t const *request) +{ + (void)rhport; + (void)stage; + (void)request; + return false; +} + +const uint16_t* PS4Device::get_descriptor_string_cb(uint8_t index, uint16_t langid) +{ + const char *value = reinterpret_cast(PS4Usb::STRING_DESCRIPTORS[index]); + return get_string_descriptor(value, index); +} + +const uint8_t* PS4Device::get_descriptor_device_cb() +{ + return PS4Usb::DEVICE_DESCRIPTORS; +} + +const uint8_t* PS4Device::get_hid_descriptor_report_cb(uint8_t itf) +{ + (void)itf; + return PS4Usb::REPORT_DESCRIPTORS; +} + +const uint8_t* PS4Device::get_descriptor_configuration_cb(uint8_t index) +{ + (void)index; + return PS4Usb::CONFIGURATION_DESCRIPTORS; +} + +const uint8_t* PS4Device::get_descriptor_device_qualifier_cb() +{ + return nullptr; +} diff --git a/Firmware/RP2040/src/USBDevice/DeviceDriver/PS4/PS4.h b/Firmware/RP2040/src/USBDevice/DeviceDriver/PS4/PS4.h new file mode 100644 index 00000000..e0b70b94 --- /dev/null +++ b/Firmware/RP2040/src/USBDevice/DeviceDriver/PS4/PS4.h @@ -0,0 +1,33 @@ +#ifndef _PS4_DEVICE_H_ +#define _PS4_DEVICE_H_ + +#include +#include + +#include "USBDevice/DeviceDriver/DeviceDriver.h" +#include "Descriptors/PS4.h" + +class PS4Device : public DeviceDriver +{ +public: + void initialize() override; + void process(const uint8_t idx, Gamepad& gamepad) override; + uint16_t get_report_cb(uint8_t itf, uint8_t report_id, hid_report_type_t report_type, uint8_t *buffer, uint16_t reqlen) override; + void set_report_cb(uint8_t itf, uint8_t report_id, hid_report_type_t report_type, uint8_t const *buffer, uint16_t bufsize) override; + bool vendor_control_xfer_cb(uint8_t rhport, uint8_t stage, tusb_control_request_t const *request) override; + const uint16_t* get_descriptor_string_cb(uint8_t index, uint16_t langid) override; + const uint8_t* get_descriptor_device_cb() override; + const uint8_t* get_hid_descriptor_report_cb(uint8_t itf) override; + const uint8_t* get_descriptor_configuration_cb(uint8_t index) override; + const uint8_t* get_descriptor_device_qualifier_cb() override; + +private: + std::array report_in_{}; + PS4::OutReport report_out_{}; + bool new_report_out_{false}; + uint16_t frame_seq_{0}; + uint8_t touch_seq_{0}; + uint8_t sys_button_latch_frames_{0}; +}; + +#endif diff --git a/Firmware/RP2040/src/USBDevice/DeviceDriver/Steam/Steam.cpp b/Firmware/RP2040/src/USBDevice/DeviceDriver/Steam/Steam.cpp new file mode 100644 index 00000000..af08d321 --- /dev/null +++ b/Firmware/RP2040/src/USBDevice/DeviceDriver/Steam/Steam.cpp @@ -0,0 +1,172 @@ +#include +#include + +#include "USBDevice/DeviceDriver/Steam/Steam.h" +#include "USBDevice/DeviceDriver/Steam/SteamPassthrough.h" +#include "USBDevice/DeviceDriver/Steam/SteamTouchpad.h" +#include "USBDevice/DeviceDriver/Steam/SteamBtReport.h" +#include "Descriptors/PS5Usb.h" +#include "Descriptors/Steam.h" + +namespace { + +constexpr uint8_t kReportIdIn = 0x01; + +void init_neutral_report(std::array& rep) +{ + std::memset(rep.data(), 0, rep.size()); + rep[0] = kReportIdIn; + rep[1] = rep[2] = rep[3] = rep[4] = PS5::JOYSTICK_MID; +} + +} // namespace + +void SteamDevice::initialize() +{ + class_driver_ = { + .name = TUD_DRV_NAME("STEAM"), + .init = hidd_init, + .deinit = hidd_deinit, + .reset = hidd_reset, + .open = hidd_open, + .control_xfer_cb = hidd_control_xfer_cb, + .xfer_cb = hidd_xfer_cb, + .sof = NULL + }; + + std::memcpy(device_descriptor_, PS5Usb::DEVICE_DESCRIPTORS, sizeof(device_descriptor_)); + init_neutral_report(report_in_); + report_out_.report_id = PS5::OutReportID::RUMBLE; +} + +void SteamDevice::process(const uint8_t idx, Gamepad& gamepad) +{ + (void)idx; + + const Gamepad::PadIn gp_in = gamepad.get_pad_in(); + + if (!SteamPassthrough::input_has_touchpad) { + PS5::InReport rep{}; + SteamBtReport::fill_report_from_pad(rep, gp_in, gamepad); + std::memcpy(report_in_.data(), &rep, sizeof(rep)); + if (report_in_.size() > sizeof(rep)) { + std::memset(report_in_.data() + sizeof(rep), 0, report_in_.size() - sizeof(rep)); + } + /* Core1 passthrough uses raw host axes (no profile axis-restrict) for sticks/triggers. */ + if (SteamPassthrough::has_report) { + auto* out = reinterpret_cast(report_in_.data()); + const auto* raw = reinterpret_cast(SteamPassthrough::report); + out->joystick_rx = raw->joystick_rx; + out->joystick_ry = raw->joystick_ry; + out->trigger_l = raw->trigger_l; + out->trigger_r = raw->trigger_r; + } + } else if (SteamPassthrough::has_report) { + std::memcpy(report_in_.data(), SteamPassthrough::report, SteamPassthrough::USB_REPORT_SIZE); + } else { + init_neutral_report(report_in_); + } + + /* Mouse only from DualSense (or other) touchpad — no right-stick mouse fallback. */ + if (SteamPassthrough::input_has_touchpad) { + SteamTouchpad::send_mouse_from_report(report_in_.data()); + } + + if (tud_hid_n_ready(Steam::ITF_GAMEPAD)) { + tud_hid_n_report(Steam::ITF_GAMEPAD, 0, report_in_.data(), static_cast(report_in_.size())); + } + + if (new_report_out_) { + Gamepad::PadOut gp_out; + gp_out.rumble_l = report_out_.motor_left; + gp_out.rumble_r = report_out_.motor_right; + gamepad.set_pad_out(gp_out); + new_report_out_ = false; + } +} + +uint16_t SteamDevice::get_report_cb(uint8_t itf, uint8_t report_id, hid_report_type_t report_type, uint8_t *buffer, uint16_t reqlen) +{ + if (report_type == HID_REPORT_TYPE_INPUT) { + if (itf == Steam::ITF_MOUSE) { + const uint16_t n = static_cast(std::min(reqlen, 4)); + std::memset(buffer, 0, n); + const uint16_t copy = static_cast(std::min(n, sizeof(SteamTouchpad::last_mouse_report))); + std::memcpy(buffer, SteamTouchpad::last_mouse_report, copy); + return n; + } + if (itf == Steam::ITF_GAMEPAD && (report_id == 0 || report_id == kReportIdIn)) { + const uint16_t n = static_cast(std::min(reqlen, report_in_.size())); + std::memcpy(buffer, report_in_.data(), n); + return n; + } + } else if (report_type == HID_REPORT_TYPE_FEATURE) { + std::memset(buffer, 0, reqlen); + return reqlen; + } + return 0; +} + +void SteamDevice::set_report_cb(uint8_t itf, uint8_t report_id, hid_report_type_t report_type, uint8_t const *buffer, uint16_t bufsize) +{ + if (itf != Steam::ITF_GAMEPAD || report_type != HID_REPORT_TYPE_OUTPUT) { + return; + } + + uint8_t rid = report_id; + const uint8_t *buf = buffer; + uint16_t len = bufsize; + if (rid == 0 && len > 0) { + rid = buffer[0]; + len = static_cast(len - 1u); + buf = &buffer[1]; + } + + if ((rid == PS5::OutReportID::RUMBLE || rid == PS5::OutReportID::CONTROL) && + len >= sizeof(PS5::OutReport)) { + std::memcpy(&report_out_, buf, sizeof(PS5::OutReport)); + new_report_out_ = true; + } +} + +bool SteamDevice::vendor_control_xfer_cb(uint8_t rhport, uint8_t stage, tusb_control_request_t const *request) +{ + (void)rhport; + (void)stage; + (void)request; + return false; +} + +const uint16_t* SteamDevice::get_descriptor_string_cb(uint8_t index, uint16_t langid) +{ + const char *value = reinterpret_cast(PS5Usb::STRING_DESCRIPTORS[index]); + return get_string_descriptor(value, index); +} + +const uint8_t* SteamDevice::get_descriptor_device_cb() +{ + device_descriptor_[8] = static_cast(SteamPassthrough::host_vid & 0xFF); + device_descriptor_[9] = static_cast((SteamPassthrough::host_vid >> 8) & 0xFF); + device_descriptor_[10] = static_cast(SteamPassthrough::host_pid & 0xFF); + device_descriptor_[11] = static_cast((SteamPassthrough::host_pid >> 8) & 0xFF); + return device_descriptor_; +} + +const uint8_t* SteamDevice::get_hid_descriptor_report_cb(uint8_t itf) +{ + if (itf == Steam::ITF_MOUSE) { + return Steam::MOUSE_REPORT_DESCRIPTORS; + } + return PS5Usb::REPORT_DESCRIPTORS; +} + +const uint8_t* SteamDevice::get_descriptor_configuration_cb(uint8_t index) +{ + (void)index; + return Steam::CONFIGURATION_DESCRIPTORS; +} + +const uint8_t* SteamDevice::get_descriptor_device_qualifier_cb() +{ + return nullptr; +} diff --git a/Firmware/RP2040/src/USBDevice/DeviceDriver/Steam/Steam.h b/Firmware/RP2040/src/USBDevice/DeviceDriver/Steam/Steam.h new file mode 100644 index 00000000..168955ac --- /dev/null +++ b/Firmware/RP2040/src/USBDevice/DeviceDriver/Steam/Steam.h @@ -0,0 +1,32 @@ +#ifndef _STEAM_DEVICE_H_ +#define _STEAM_DEVICE_H_ + +#include +#include + +#include "USBDevice/DeviceDriver/DeviceDriver.h" +#include "Descriptors/PS5.h" +#include "USBDevice/DeviceDriver/Steam/SteamPassthrough.h" + +class SteamDevice : public DeviceDriver +{ +public: + void initialize() override; + void process(const uint8_t idx, Gamepad& gamepad) override; + uint16_t get_report_cb(uint8_t itf, uint8_t report_id, hid_report_type_t report_type, uint8_t *buffer, uint16_t reqlen) override; + void set_report_cb(uint8_t itf, uint8_t report_id, hid_report_type_t report_type, uint8_t const *buffer, uint16_t bufsize) override; + bool vendor_control_xfer_cb(uint8_t rhport, uint8_t stage, tusb_control_request_t const *request) override; + const uint16_t* get_descriptor_string_cb(uint8_t index, uint16_t langid) override; + const uint8_t* get_descriptor_device_cb() override; + const uint8_t* get_hid_descriptor_report_cb(uint8_t itf) override; + const uint8_t* get_descriptor_configuration_cb(uint8_t index) override; + const uint8_t* get_descriptor_device_qualifier_cb() override; + +private: + std::array report_in_{}; + PS5::OutReport report_out_{}; + bool new_report_out_{false}; + uint8_t device_descriptor_[18]{}; +}; + +#endif diff --git a/Firmware/RP2040/src/USBDevice/DeviceDriver/Steam/SteamActive.h b/Firmware/RP2040/src/USBDevice/DeviceDriver/Steam/SteamActive.h new file mode 100644 index 00000000..300566ed --- /dev/null +++ b/Firmware/RP2040/src/USBDevice/DeviceDriver/Steam/SteamActive.h @@ -0,0 +1,19 @@ +#pragma once + +#include + +/** True while the USB output driver is SteamDevice (touchpad → HID mouse). */ +namespace SteamActive +{ + inline std::atomic enabled{false}; + + inline void set(bool on) + { + enabled.store(on, std::memory_order_release); + } + + inline bool is_enabled() + { + return enabled.load(std::memory_order_acquire); + } +} diff --git a/Firmware/RP2040/src/USBDevice/DeviceDriver/Steam/SteamBtReport.h b/Firmware/RP2040/src/USBDevice/DeviceDriver/Steam/SteamBtReport.h new file mode 100644 index 00000000..fa241fd0 --- /dev/null +++ b/Firmware/RP2040/src/USBDevice/DeviceDriver/Steam/SteamBtReport.h @@ -0,0 +1,234 @@ +#pragma once + +#include +#include +#include + +#include "Descriptors/PS5.h" +#include "Gamepad/Gamepad.h" +#include "USBDevice/DeviceDriver/Steam/SteamPassthrough.h" + +#if defined(CONFIG_EN_BLUETOOTH) +#include "controller/uni_gamepad.h" +#endif + +namespace SteamBtReport +{ + +#if defined(CONFIG_EN_BLUETOOTH) + +inline uint8_t ps5_stick_byte(int32_t axis) +{ + int32_t b = axis / 4 + 127; + if (b < 0) { + return 0; + } + if (b > 255) { + return 255; + } + return static_cast(b); +} + +inline uint8_t ps5_trigger_byte(int32_t value) +{ + int32_t b = value >> 2; + if (b < 0) { + return 0; + } + if (b > 255) { + return 255; + } + return static_cast(b); +} + +inline void update_from_uni_gamepad(const uni_gamepad_t* gp) +{ + PS5::InReport rep{}; + rep.report_id = 0x01; + rep.joystick_lx = ps5_stick_byte(gp->axis_x); + rep.joystick_ly = ps5_stick_byte(gp->axis_y); + rep.joystick_rx = ps5_stick_byte(gp->axis_rx); + rep.joystick_ry = ps5_stick_byte(gp->axis_ry); + rep.trigger_l = ps5_trigger_byte(gp->brake); + rep.trigger_r = ps5_trigger_byte(gp->throttle); + + uint8_t b0 = PS5::Buttons0::DPAD_CENTER; + switch (gp->dpad) { + case DPAD_UP: + b0 = PS5::Buttons0::DPAD_UP; + break; + case DPAD_DOWN: + b0 = PS5::Buttons0::DPAD_DOWN; + break; + case DPAD_LEFT: + b0 = PS5::Buttons0::DPAD_LEFT; + break; + case DPAD_RIGHT: + b0 = PS5::Buttons0::DPAD_RIGHT; + break; + case DPAD_UP | DPAD_RIGHT: + b0 = PS5::Buttons0::DPAD_UP_RIGHT; + break; + case DPAD_DOWN | DPAD_RIGHT: + b0 = PS5::Buttons0::DPAD_RIGHT_DOWN; + break; + case DPAD_DOWN | DPAD_LEFT: + b0 = PS5::Buttons0::DPAD_DOWN_LEFT; + break; + case DPAD_UP | DPAD_LEFT: + b0 = PS5::Buttons0::DPAD_LEFT_UP; + break; + default: + break; + } + if (gp->buttons & BUTTON_X) { + b0 |= PS5::Buttons0::SQUARE; + } + if (gp->buttons & BUTTON_A) { + b0 |= PS5::Buttons0::CROSS; + } + if (gp->buttons & BUTTON_B) { + b0 |= PS5::Buttons0::CIRCLE; + } + if (gp->buttons & BUTTON_Y) { + b0 |= PS5::Buttons0::TRIANGLE; + } + rep.buttons[0] = b0; + + uint8_t b1 = 0; + if (gp->buttons & BUTTON_SHOULDER_L) { + b1 |= PS5::Buttons1::L1; + } + if (gp->buttons & BUTTON_SHOULDER_R) { + b1 |= PS5::Buttons1::R1; + } + if (gp->misc_buttons & MISC_BUTTON_BACK) { + b1 |= PS5::Buttons1::SHARE; + } + if (gp->misc_buttons & MISC_BUTTON_START) { + b1 |= PS5::Buttons1::OPTIONS; + } + if (gp->buttons & BUTTON_THUMB_L) { + b1 |= PS5::Buttons1::L3; + } + if (gp->buttons & BUTTON_THUMB_R) { + b1 |= PS5::Buttons1::R3; + } + rep.buttons[1] = b1; + + uint8_t b2 = 0; + if (gp->misc_buttons & MISC_BUTTON_SYSTEM) { + b2 |= PS5::Buttons2::PS; + } + if (gp->misc_buttons & MISC_BUTTON_CAPTURE) { + b2 |= PS5::Buttons2::MUTE; + } + rep.buttons[2] = b2; + + uint8_t buf[SteamPassthrough::USB_REPORT_SIZE]{}; + std::memcpy(buf, &rep, sizeof(rep)); + SteamPassthrough::store(buf, sizeof(buf)); +} + +#endif /* CONFIG_EN_BLUETOOTH */ + +inline uint8_t pad_stick_byte(int16_t axis) +{ + int32_t scaled = (static_cast(axis) + 32768) * 255 / 65535; + if (scaled < 0) { + return 0; + } + if (scaled > 255) { + return 255; + } + return static_cast(scaled); +} + +inline void fill_report_from_pad(PS5::InReport& rep, const Gamepad::PadIn& gp_in, const Gamepad& gamepad) +{ + rep.report_id = 0x01; + rep.joystick_lx = pad_stick_byte(gp_in.joystick_lx); + rep.joystick_ly = pad_stick_byte(gp_in.joystick_ly); + rep.joystick_rx = pad_stick_byte(gp_in.joystick_rx); + rep.joystick_ry = pad_stick_byte(gp_in.joystick_ry); + rep.trigger_l = gp_in.trigger_l; + rep.trigger_r = gp_in.trigger_r; + + uint8_t b0 = PS5::Buttons0::DPAD_CENTER; + if (gp_in.dpad & gamepad.MAP_DPAD_UP) { + if (gp_in.dpad & gamepad.MAP_DPAD_LEFT) { + b0 = PS5::Buttons0::DPAD_LEFT_UP; + } else if (gp_in.dpad & gamepad.MAP_DPAD_RIGHT) { + b0 = PS5::Buttons0::DPAD_UP_RIGHT; + } else { + b0 = PS5::Buttons0::DPAD_UP; + } + } else if (gp_in.dpad & gamepad.MAP_DPAD_DOWN) { + if (gp_in.dpad & gamepad.MAP_DPAD_LEFT) { + b0 = PS5::Buttons0::DPAD_DOWN_LEFT; + } else if (gp_in.dpad & gamepad.MAP_DPAD_RIGHT) { + b0 = PS5::Buttons0::DPAD_RIGHT_DOWN; + } else { + b0 = PS5::Buttons0::DPAD_DOWN; + } + } else if (gp_in.dpad & gamepad.MAP_DPAD_LEFT) { + b0 = PS5::Buttons0::DPAD_LEFT; + } else if (gp_in.dpad & gamepad.MAP_DPAD_RIGHT) { + b0 = PS5::Buttons0::DPAD_RIGHT; + } + + if (gp_in.buttons & gamepad.MAP_BUTTON_X) { + b0 |= PS5::Buttons0::SQUARE; + } + if (gp_in.buttons & gamepad.MAP_BUTTON_A) { + b0 |= PS5::Buttons0::CROSS; + } + if (gp_in.buttons & gamepad.MAP_BUTTON_B) { + b0 |= PS5::Buttons0::CIRCLE; + } + if (gp_in.buttons & gamepad.MAP_BUTTON_Y) { + b0 |= PS5::Buttons0::TRIANGLE; + } + rep.buttons[0] = b0; + + uint8_t b1 = 0; + if (gp_in.buttons & gamepad.MAP_BUTTON_LB) { + b1 |= PS5::Buttons1::L1; + } + if (gp_in.buttons & gamepad.MAP_BUTTON_RB) { + b1 |= PS5::Buttons1::R1; + } + if (gp_in.buttons & gamepad.MAP_BUTTON_BACK) { + b1 |= PS5::Buttons1::SHARE; + } + if (gp_in.buttons & gamepad.MAP_BUTTON_START) { + b1 |= PS5::Buttons1::OPTIONS; + } + if (gp_in.buttons & gamepad.MAP_BUTTON_L3) { + b1 |= PS5::Buttons1::L3; + } + if (gp_in.buttons & gamepad.MAP_BUTTON_R3) { + b1 |= PS5::Buttons1::R3; + } + rep.buttons[1] = b1; + + uint8_t b2 = 0; + if (gp_in.buttons & gamepad.MAP_BUTTON_SYS) { + b2 |= PS5::Buttons2::PS; + } + if (gp_in.buttons & gamepad.MAP_BUTTON_MISC) { + b2 |= PS5::Buttons2::MUTE; + } + rep.buttons[2] = b2; +} + +inline void update_from_gamepad(Gamepad& gamepad) +{ + PS5::InReport rep{}; + fill_report_from_pad(rep, gamepad.get_pad_in(), gamepad); + uint8_t buf[SteamPassthrough::USB_REPORT_SIZE]{}; + std::memcpy(buf, &rep, sizeof(rep)); + SteamPassthrough::store(buf, sizeof(buf)); +} + +} // namespace SteamBtReport diff --git a/Firmware/RP2040/src/USBDevice/DeviceDriver/Steam/SteamInput.h b/Firmware/RP2040/src/USBDevice/DeviceDriver/Steam/SteamInput.h new file mode 100644 index 00000000..63f042f2 --- /dev/null +++ b/Firmware/RP2040/src/USBDevice/DeviceDriver/Steam/SteamInput.h @@ -0,0 +1,27 @@ +#pragma once + +#include +#include + +/** Latest wired DualSense input for Steam mode touchpad → USB mouse (raw 64-byte wire layout). */ +namespace SteamInput +{ + static constexpr uint8_t USB_REPORT_SIZE = 64; + + inline uint8_t report[USB_REPORT_SIZE]{}; + inline volatile bool has_wired_report = false; + + inline void store_wired(const uint8_t* data, uint16_t len) + { + std::memset(report, 0, USB_REPORT_SIZE); + const uint16_t n = (len > USB_REPORT_SIZE) ? USB_REPORT_SIZE : len; + std::memcpy(report, data, n); + has_wired_report = true; + } + + inline void clear() + { + has_wired_report = false; + std::memset(report, 0, USB_REPORT_SIZE); + } +} diff --git a/Firmware/RP2040/src/USBDevice/DeviceDriver/Steam/SteamPassthrough.h b/Firmware/RP2040/src/USBDevice/DeviceDriver/Steam/SteamPassthrough.h new file mode 100644 index 00000000..c76b5f92 --- /dev/null +++ b/Firmware/RP2040/src/USBDevice/DeviceDriver/Steam/SteamPassthrough.h @@ -0,0 +1,37 @@ +#pragma once + +#include +#include + +namespace SteamPassthrough +{ + static constexpr uint8_t USB_REPORT_SIZE = 64; + + inline uint8_t report[USB_REPORT_SIZE]{}; + inline volatile bool has_report = false; + /** Input device exposes a DualSense-style touchpad (BT or wired PS5). */ + inline volatile bool input_has_touchpad = false; + inline uint16_t host_vid = 0x054C; + inline uint16_t host_pid = 0x0CE6; + + inline void set_host_identity(uint16_t vid, uint16_t pid) + { + host_vid = vid; + host_pid = pid; + } + + inline void store(const uint8_t* data, uint16_t len) + { + std::memset(report, 0, USB_REPORT_SIZE); + const uint16_t n = (len > USB_REPORT_SIZE) ? USB_REPORT_SIZE : len; + std::memcpy(report, data, n); + has_report = true; + } + + inline void clear() + { + has_report = false; + input_has_touchpad = false; + std::memset(report, 0, USB_REPORT_SIZE); + } +} diff --git a/Firmware/RP2040/src/USBDevice/DeviceDriver/Steam/SteamTouchCache.h b/Firmware/RP2040/src/USBDevice/DeviceDriver/Steam/SteamTouchCache.h new file mode 100644 index 00000000..062db109 --- /dev/null +++ b/Firmware/RP2040/src/USBDevice/DeviceDriver/Steam/SteamTouchCache.h @@ -0,0 +1,76 @@ +#pragma once + +#include +#include +#include + +#include "USBDevice/DeviceDriver/Steam/SteamTouchpad.h" + +/** Core1 (Bluepad32) writes, Core0 (SteamDevice) reads — DualSense touchpad samples. */ +namespace SteamTouchCache +{ + inline uint8_t touch_points[8]{}; + inline uint8_t usb_report[64]{}; + inline bool touchpad_click = false; + inline std::atomic generation{0}; + + inline void store(const uint8_t points[8], bool click) + { + std::memcpy(touch_points, points, 8); + touchpad_click = click; + + std::memset(usb_report, 0, sizeof(usb_report)); + usb_report[0] = 0x01; + std::memcpy(&usb_report[SteamTouchpad::kReportTouchPointsOffset], points, 8); + if (click) { + usb_report[10] |= 0x02; /* PS5::Buttons2::TP */ + } + + generation.fetch_add(1, std::memory_order_release); + } + + /** Decode touch from a raw DualSense 0x31 Bluetooth input report. */ + inline void store_from_bt_report(const uint8_t* report, uint16_t len) + { + if (!report || len < 42 || report[0] != 0x31) { + return; + } + enum { kTouchOffset = 34 }; + if (len < kTouchOffset + 8) { + return; + } + const bool click = (len >= 12) ? ((report[11] & 0x02) != 0) : false; + store(&report[kTouchOffset], click); + } + + inline void clear() + { + touch_points[0] = 0x80; + touch_points[1] = touch_points[2] = touch_points[3] = 0; + touch_points[4] = 0x80; + touch_points[5] = touch_points[6] = touch_points[7] = 0; + touchpad_click = false; + std::memset(usb_report, 0, sizeof(usb_report)); + generation.store(0, std::memory_order_release); + } + + /** Returns false if no sample has been stored yet. */ + inline bool load(uint8_t out_points[8], bool& click) + { + for (int attempt = 0; attempt < 4; ++attempt) { + const uint32_t gen_before = generation.load(std::memory_order_acquire); + if (gen_before == 0) { + return false; + } + std::memcpy(out_points, touch_points, 8); + click = touchpad_click; + const uint32_t gen_after = generation.load(std::memory_order_acquire); + if (gen_before == gen_after) { + return true; + } + } + std::memcpy(out_points, touch_points, 8); + click = touchpad_click; + return generation.load(std::memory_order_acquire) != 0; + } +} diff --git a/Firmware/RP2040/src/USBDevice/DeviceDriver/Steam/SteamTouchpad.cpp b/Firmware/RP2040/src/USBDevice/DeviceDriver/Steam/SteamTouchpad.cpp new file mode 100644 index 00000000..c0eeb870 --- /dev/null +++ b/Firmware/RP2040/src/USBDevice/DeviceDriver/Steam/SteamTouchpad.cpp @@ -0,0 +1,205 @@ +#include "USBDevice/DeviceDriver/Steam/SteamTouchpad.h" + +#include +#include + +#include "Descriptors/PS5.h" +#include "Descriptors/Steam.h" +#include "USBDevice/DeviceDriver/Steam/SteamPassthrough.h" +#include "tusb.h" + +namespace SteamTouchpad +{ + +uint8_t last_mouse_report[3]{}; + +namespace +{ + +constexpr int kDeltaDivisor = 1; +constexpr int kMaxDelta = 127; + +int clamp_delta(int v) +{ + if (v > kMaxDelta) { + return kMaxDelta; + } + if (v < -kMaxDelta) { + return -kMaxDelta; + } + return v; +} + +const uint8_t* touch_raw(const uint8_t report[64]) +{ + return &report[kReportTouchPointsOffset]; +} + +bool send_mouse_hid(uint8_t buttons, int8_t dx, int8_t dy) +{ + if (!tud_mounted() || !tud_hid_n_ready(Steam::ITF_MOUSE)) { + return false; + } + const uint8_t rep[3] = { + buttons, + static_cast(dx), + static_cast(dy), + }; + if (tud_hid_n_report(Steam::ITF_MOUSE, 0, rep, sizeof(rep))) { + std::memcpy(last_mouse_report, rep, sizeof(rep)); + return true; + } + return false; +} + +void send_mouse_deltas(uint8_t buttons, int cx, int cy, int& last_x, int& last_y, bool touching, + bool& was_touching, uint8_t& prev_buttons) +{ + int8_t dx = 0; + int8_t dy = 0; + + if (touching && was_touching && last_x >= 0) { + dx = static_cast(clamp_delta((cx - last_x) / kDeltaDivisor)); + dy = static_cast(clamp_delta((cy - last_y) / kDeltaDivisor)); + } + + if (dx != 0 || dy != 0 || buttons != prev_buttons) { + if (send_mouse_hid(buttons, dx, dy)) { + prev_buttons = buttons; + if (touching) { + last_x = cx; + last_y = cy; + } else { + last_x = -1; + last_y = -1; + } + was_touching = touching; + return; + } + } + + if (!touching) { + last_x = -1; + last_y = -1; + } + was_touching = touching; +} + +void decode_active_finger(const uint8_t touch_points[8], int& cx, int& cy, bool& touching) +{ + int x0 = 0; + int y0 = 0; + bool touch0 = false; + int x1 = 0; + int y1 = 0; + bool touch1 = false; + touchpoint_decode(&touch_points[0], x0, y0, touch0); + touchpoint_decode(&touch_points[4], x1, y1, touch1); + + touching = touch0 || touch1; + cx = touch0 ? x0 : x1; + cy = touch0 ? y0 : y1; +} + +} // namespace + +void touchpoint_decode(const uint8_t raw[4], int& x, int& y, bool& touching) +{ + touching = (raw[0] & 0x80) == 0; + const uint8_t x_lo = raw[1]; + const uint8_t x_hi = raw[2] & 0x0F; + const uint8_t y_lo = (raw[2] >> 4) & 0x0F; + const uint8_t y_hi = raw[3]; + x = (x_hi << 8) | x_lo; + y = (y_hi << 4) | y_lo; +} + +void touchpoint_encode(uint8_t raw[4], int x, int y, bool touching, uint8_t counter) +{ + if (x < 0) { + x = 0; + } + if (x > 1919) { + x = 1919; + } + if (y < 0) { + y = 0; + } + if (y > 1079) { + y = 1079; + } + raw[0] = static_cast((counter & 0x7F) | (touching ? 0 : 0x80)); + raw[1] = static_cast(x & 0xFF); + raw[2] = static_cast((x >> 8) & 0x0F) | static_cast((y & 0x0F) << 4); + raw[3] = static_cast((y >> 4) & 0xFF); +} + +void apply_to_passthrough(const uint8_t touch_points[8], bool touchpad_click) +{ + if (!SteamPassthrough::has_report) { + return; + } + + std::memcpy(&SteamPassthrough::report[kReportTouchPointsOffset], touch_points, 8); + + auto* in = reinterpret_cast(SteamPassthrough::report); + if (touchpad_click) { + in->buttons[2] |= static_cast(PS5::Buttons2::TP); + } else { + in->buttons[2] &= static_cast(~static_cast(PS5::Buttons2::TP)); + } +} + +void send_mouse_from_touch(const uint8_t touch_points[8], bool touchpad_click) +{ + static int last_x = -1; + static int last_y = -1; + static bool was_touching = false; + static uint8_t prev_buttons = 0; + + int cx = 0; + int cy = 0; + bool touching = false; + decode_active_finger(touch_points, cx, cy, touching); + + uint8_t buttons = touchpad_click ? 0x01u : 0x00u; + send_mouse_deltas(buttons, cx, cy, last_x, last_y, touching, was_touching, prev_buttons); +} + +void send_mouse_from_report(const uint8_t report[64]) +{ + const uint8_t* tp = touch_raw(report); + + int x0 = 0; + int y0 = 0; + bool touch0 = false; + int x1 = 0; + int y1 = 0; + bool touch1 = false; + touchpoint_decode(&tp[0], x0, y0, touch0); + touchpoint_decode(&tp[4], x1, y1, touch1); + + static int last_x = -1; + static int last_y = -1; + static bool was_touching = false; + static uint8_t prev_buttons = 0; + + const bool touching = touch0 || touch1; + const int cx = touch0 ? x0 : x1; + const int cy = touch0 ? y0 : y1; + + uint8_t buttons = 0; + const auto* in = reinterpret_cast(report); + if (in->buttons[2] & static_cast(PS5::Buttons2::TP)) { + buttons |= 0x01u; + } + + send_mouse_deltas(buttons, cx, cy, last_x, last_y, touching, was_touching, prev_buttons); +} + +bool send_mouse_packet(uint8_t buttons, int8_t dx, int8_t dy) +{ + return send_mouse_hid(buttons, dx, dy); +} + +} // namespace SteamTouchpad diff --git a/Firmware/RP2040/src/USBDevice/DeviceDriver/Steam/SteamTouchpad.h b/Firmware/RP2040/src/USBDevice/DeviceDriver/Steam/SteamTouchpad.h new file mode 100644 index 00000000..4d821fb7 --- /dev/null +++ b/Firmware/RP2040/src/USBDevice/DeviceDriver/Steam/SteamTouchpad.h @@ -0,0 +1,32 @@ +#pragma once + +#include +#include + +namespace SteamTouchpad +{ + +/** First touch point in a 64-byte DualSense USB input report (report ID at [0]). */ +constexpr size_t kReportTouchPointsOffset = 33; + +void touchpoint_decode(const uint8_t raw[4], int& x, int& y, bool& touching); + +/** Encode one DualSense touch point (inverse of touchpoint_decode). */ +void touchpoint_encode(uint8_t raw[4], int x, int y, bool touching, uint8_t counter); + +/** Bluetooth: raw 8-byte touch region + physical click. */ +void send_mouse_from_touch(const uint8_t touch_points[8], bool touchpad_click); + +/** Wired USB: full 64-byte DualSense input report. */ +void send_mouse_from_report(const uint8_t report[64]); + +/** Merge touch bytes into the Steam passthrough USB input report. */ +void apply_to_passthrough(const uint8_t touch_points[8], bool touchpad_click); + +/** Last relative mouse HID packet sent (for GET_REPORT on interface 1). */ +extern uint8_t last_mouse_report[3]; + +/** Send a relative mouse HID report on the Steam mouse interface. */ +bool send_mouse_packet(uint8_t buttons, int8_t dx, int8_t dy); + +} // namespace SteamTouchpad diff --git a/Firmware/RP2040/src/USBDevice/DeviceDriver/Switch/Switch.cpp b/Firmware/RP2040/src/USBDevice/DeviceDriver/Switch/Switch.cpp index df1dddde..f240c09b 100644 --- a/Firmware/RP2040/src/USBDevice/DeviceDriver/Switch/Switch.cpp +++ b/Firmware/RP2040/src/USBDevice/DeviceDriver/Switch/Switch.cpp @@ -1,136 +1,466 @@ #include +#include "pico/time.h" + +#include "Board/ogxm_log.h" #include "USBDevice/DeviceDriver/Switch/Switch.h" +#include "Gamepad/Gamepad.h" +#include "Gamepad/Range.h" -void SwitchDevice::initialize() +namespace { - class_driver_ = - { + constexpr uint8_t VIB_OPTS[4] = { 0x0A, 0x0C, 0x0B, 0x09 }; + + /** HD rumble motor block → 0–255 intensity (dekuNukem / Switch Pro reverse engineering). */ + uint8_t hd_rumble_amplitude(const uint8_t motor[4]) + { + if (motor[0] == 0x00 && motor[1] == 0x01 && motor[2] == 0x40 && motor[3] == 0x40) + return 0; + + const uint8_t hfa = static_cast((motor[1] >> 1) & 0x7F); + const uint16_t lfa = static_cast(motor[3] | ((static_cast(motor[2] & 0x80u)) << 1)); + + const uint8_t hfa_scaled = (hfa > 127) ? 255 : static_cast(hfa * 2); + const uint8_t lfa_scaled = (lfa > 255) ? 255 : static_cast(lfa); + return (hfa_scaled > lfa_scaled) ? hfa_scaled : lfa_scaled; + } + + bool is_rumble_output_cmd(uint8_t cmd) + { + return cmd == SwitchPro::REPORT_ID_OUTPUT_SUBCMD || + cmd == SwitchPro::REPORT_ID_OUTPUT_RUMBLE || + cmd == 0x11 || + cmd == SwitchPro::CMD::AND_RUMBLE; + } + + bool is_subcommand_output_cmd(uint8_t cmd) + { + return cmd == SwitchPro::REPORT_ID_OUTPUT_SUBCMD || + cmd == 0x11 || + cmd == SwitchPro::CMD::AND_RUMBLE; + } +} + +void SwitchDevice::initialize() +{ + class_driver_ = { .name = TUD_DRV_NAME("SWITCH"), .init = hidd_init, - .deinit = hidd_deinit, + .deinit = hidd_deinit, .reset = hidd_reset, .open = hidd_open, .control_xfer_cb = hidd_control_xfer_cb, .xfer_cb = hidd_xfer_cb, .sof = NULL }; + switch_report_.batteryConnection = 0x81; + std::memset(report_.data(), 0, report_.size()); + std::memset(pending_output_.data(), 0, pending_output_.size()); + std::memset(report_81_.data(), 0, report_81_.size()); + has_pending_81_ = false; + OGXM_LOG("Switch: device init (Pro Controller emulation)\n"); +} + +void SwitchDevice::set_timer() +{ + if (timestamp_ms_ == 0) + { + timestamp_ms_ = to_ms_since_boot(get_absolute_time()); + report_[0] = 0x00; + return; + } + uint32_t now = to_ms_since_boot(get_absolute_time()); + uint32_t delta = now - timestamp_ms_; + uint32_t ticks = static_cast(delta * 4u); + timer_ = (timer_ + static_cast(ticks)) & 0xFF; + report_[0] = timer_; + timestamp_ms_ = now; +} + +void SwitchDevice::gamepad_to_switch_report(const Gamepad::PadIn& gp_in, SwitchPro::SwitchReport& out, const Gamepad& gamepad) +{ + out.batteryConnection = 0x81; + out.buttons[0] = 0; + out.buttons[1] = 0; + out.buttons[2] = 0; + + // Face: match project convention. SwitchPro host (SwitchPro.cpp) maps Pro→gamepad as: + // Pro Y→MAP_X, Pro B→MAP_A, Pro A→MAP_B, Pro X→MAP_Y. So device must output inverse: + // internal A→Pro B, B→Pro A, X→Pro Y, Y→Pro X. (PS: Cross=A→Pro B, Circle=B→Pro A, Square=X→Pro Y, Triangle=Y→Pro X.) + if (gp_in.buttons & Gamepad::BUTTON_A) out.buttons[0] |= SwitchPro::Btn::B; + if (gp_in.buttons & Gamepad::BUTTON_B) out.buttons[0] |= SwitchPro::Btn::A; + if (gp_in.buttons & Gamepad::BUTTON_X) out.buttons[0] |= SwitchPro::Btn::Y; + if (gp_in.buttons & Gamepad::BUTTON_Y) out.buttons[0] |= SwitchPro::Btn::X; + if (gp_in.buttons & gamepad.MAP_BUTTON_RB) out.buttons[0] |= SwitchPro::Btn::R; + if (gp_in.trigger_r) out.buttons[0] |= SwitchPro::Btn::ZR; + + if (gp_in.buttons & gamepad.MAP_BUTTON_BACK) out.buttons[1] |= SwitchPro::Btn::MINUS; + if (gp_in.buttons & gamepad.MAP_BUTTON_START) out.buttons[1] |= SwitchPro::Btn::PLUS; + if (gp_in.buttons & gamepad.MAP_BUTTON_L3) out.buttons[1] |= SwitchPro::Btn::L3; + if (gp_in.buttons & gamepad.MAP_BUTTON_R3) out.buttons[1] |= SwitchPro::Btn::R3; + if (gp_in.buttons & gamepad.MAP_BUTTON_SYS) out.buttons[1] |= SwitchPro::Btn::HOME; + if (gp_in.buttons & gamepad.MAP_BUTTON_MISC) out.buttons[1] |= SwitchPro::Btn::CAPTURE; + + uint8_t hat = SwitchPro::HAT_NEUTRAL; + if (gp_in.dpad & gamepad.MAP_DPAD_UP) hat |= SwitchPro::HAT_UP; + if (gp_in.dpad & gamepad.MAP_DPAD_DOWN) hat |= SwitchPro::HAT_DOWN; + if (gp_in.dpad & gamepad.MAP_DPAD_LEFT) hat |= SwitchPro::HAT_LEFT; + if (gp_in.dpad & gamepad.MAP_DPAD_RIGHT) hat |= SwitchPro::HAT_RIGHT; + out.buttons[2] = hat; + if (gp_in.buttons & gamepad.MAP_BUTTON_LB) out.buttons[2] |= SwitchPro::Btn::L; + if (gp_in.trigger_l) out.buttons[2] |= SwitchPro::Btn::ZL; + + // Sticks: int16 -> 12-bit (0x000–0xFFF), center 0x7FF. Use same sensitivity as Switch Pro + // wired host (SwitchProHost::normalize_axis): 12bit -> (value - 2047) * 22 -> int16. + // Inverse: 12bit = 2047 + int16/22, so emulated output matches wired Pro feel. + constexpr int16_t DZ = 256; + constexpr int32_t PRO_CENTER = 2047; // 0x7FF + constexpr int32_t PRO_GAIN_INV = 22; // host uses *22 so we use /22 + auto to12 = [](int16_t val) -> uint16_t { + if (val > -DZ && val < DZ) + return static_cast(SwitchPro::STICK_MID); + int32_t u = PRO_CENTER + (static_cast(val) / PRO_GAIN_INV); + if (u < 0) u = 0; + if (u > 4095) u = 4095; + return static_cast(u); + }; + uint16_t lx = to12(gp_in.joystick_lx); + uint16_t ly = to12(gp_in.joystick_ly); + uint16_t rx = to12(gp_in.joystick_rx); + uint16_t ry = to12(gp_in.joystick_ry); + // Invert Y axes so up/down match Switch expectation (both sticks were reversed) + ly = static_cast(0xFFF - ly); + ry = static_cast(0xFFF - ry); + + out.l[0] = static_cast(lx & 0xFF); + out.l[1] = static_cast(((ly & 0x0F) << 4) | (lx >> 8)); + out.l[2] = static_cast(ly >> 4); + out.r[0] = static_cast(rx & 0xFF); + out.r[1] = static_cast(((ry & 0x0F) << 4) | (rx >> 8)); + out.r[2] = static_cast(ry >> 4); +} - in_report_.fill(SwitchWired::InReport()); -} - -void SwitchDevice::process(const uint8_t idx, Gamepad& gamepad) -{ - SwitchWired::InReport& in_report = in_report_[idx]; - - if (gamepad.new_pad_in()) - { - Gamepad::PadIn gp_in = gamepad.get_pad_in(); - - switch (gp_in.dpad) - { - case Gamepad::DPAD_UP: - in_report.dpad = SwitchWired::DPad::UP; - break; - case Gamepad::DPAD_DOWN: - in_report.dpad = SwitchWired::DPad::DOWN; - break; - case Gamepad::DPAD_LEFT: - in_report.dpad = SwitchWired::DPad::LEFT; - break; - case Gamepad::DPAD_RIGHT: - in_report.dpad = SwitchWired::DPad::RIGHT; - break; - case Gamepad::DPAD_UP_LEFT: - in_report.dpad = SwitchWired::DPad::UP_LEFT; - break; - case Gamepad::DPAD_UP_RIGHT: - in_report.dpad = SwitchWired::DPad::UP_RIGHT; - break; - case Gamepad::DPAD_DOWN_LEFT: - in_report.dpad = SwitchWired::DPad::DOWN_LEFT; - break; - case Gamepad::DPAD_DOWN_RIGHT: - in_report.dpad = SwitchWired::DPad::DOWN_RIGHT; - break; - default: - in_report.dpad = SwitchWired::DPad::CENTER; - break; - } - - in_report.buttons = 0; - - if (gp_in.buttons & Gamepad::BUTTON_X) in_report.buttons |= SwitchWired::Buttons::Y; - if (gp_in.buttons & Gamepad::BUTTON_A) in_report.buttons |= SwitchWired::Buttons::B; - if (gp_in.buttons & Gamepad::BUTTON_Y) in_report.buttons |= SwitchWired::Buttons::X; - if (gp_in.buttons & Gamepad::BUTTON_B) in_report.buttons |= SwitchWired::Buttons::A; - if (gp_in.buttons & Gamepad::BUTTON_LB) in_report.buttons |= SwitchWired::Buttons::L; - if (gp_in.buttons & Gamepad::BUTTON_RB) in_report.buttons |= SwitchWired::Buttons::R; - if (gp_in.buttons & Gamepad::BUTTON_BACK) in_report.buttons |= SwitchWired::Buttons::MINUS; - if (gp_in.buttons & Gamepad::BUTTON_START) in_report.buttons |= SwitchWired::Buttons::PLUS; - if (gp_in.buttons & Gamepad::BUTTON_L3) in_report.buttons |= SwitchWired::Buttons::L3; - if (gp_in.buttons & Gamepad::BUTTON_R3) in_report.buttons |= SwitchWired::Buttons::R3; - if (gp_in.buttons & Gamepad::BUTTON_SYS) in_report.buttons |= SwitchWired::Buttons::HOME; - if (gp_in.buttons & Gamepad::BUTTON_MISC) in_report.buttons |= SwitchWired::Buttons::CAPTURE; - - if (gp_in.trigger_l) in_report.buttons |= SwitchWired::Buttons::ZL; - if (gp_in.trigger_r) in_report.buttons |= SwitchWired::Buttons::ZR; - - in_report.joystick_lx = Scale::int16_to_uint8(gp_in.joystick_lx); - in_report.joystick_ly = Scale::int16_to_uint8(gp_in.joystick_ly); - in_report.joystick_rx = Scale::int16_to_uint8(gp_in.joystick_rx); - in_report.joystick_ry = Scale::int16_to_uint8(gp_in.joystick_ry); - } +void SwitchDevice::build_standard_report(const SwitchPro::SwitchReport& sw) +{ + std::memset(report_.data(), 0, report_.size()); + set_timer(); + std::memcpy(report_.data() + 1, &sw, sizeof(SwitchPro::SwitchReport)); + if (vibration_enabled_) + { + vibration_idx_ = (vibration_idx_ + 1) % 4; + vibration_report_ = VIB_OPTS[vibration_idx_]; + } + report_[11] = vibration_report_; + /* IMU passthrough disabled for now (report bytes 12–47 stay zero). */ +} + +void SwitchDevice::build_subcommand_reply(const SwitchPro::SwitchReport& sw) +{ + std::memset(report_.data(), 0, report_.size()); + set_timer(); + std::memcpy(report_.data() + 1, &sw, sizeof(SwitchPro::SwitchReport)); + report_[11] = vibration_report_; + // Reply data starts at byte 12 in payload (timer=0, SwitchReport=1-10, vib=11, reply=12+) + uint8_t* r = report_.data() + 12; + + // Subcommand at byte 10 of host output report (retro-pico-switch layout) + uint8_t sub = pending_output_[10]; + OGXM_LOG("Switch: replying to subcmd=0x%02x\n", (unsigned)sub); + switch (sub) + { + case SwitchPro::SUBCMD_BT_PAIR: + r[0] = 0x81; r[1] = 0x01; r[2] = 0x03; + break; + case SwitchPro::SUBCMD_DEVICE_INFO: + r[0] = 0x82; r[1] = 0x02; r[2] = 0x03; r[3] = 0x48; r[4] = 0x03; r[5] = 0x02; + std::memcpy(r + 6, addr_.data(), 6); + r[12] = 0x01; r[13] = 0x01; + break; + case SwitchPro::SUBCMD_SHIPMENT: + r[0] = 0x80; r[1] = 0x08; + break; + case SwitchPro::SUBCMD_SET_MODE: + r[0] = 0x80; r[1] = 0x03; + break; + case SwitchPro::SUBCMD_TRIGGER: + r[0] = 0x83; r[1] = 0x04; + break; + case SwitchPro::SUBCMD_IMU: + r[0] = 0x80; r[1] = 0x40; + break; + case SwitchPro::SUBCMD_IMU_SENS: + r[0] = 0x80; r[1] = 0x41; + break; + case SwitchPro::SUBCMD_VIBRATION: + r[0] = 0x80; r[1] = 0x48; + vibration_enabled_ = true; + vibration_idx_ = 0; + vibration_report_ = VIB_OPTS[0]; + break; + case SwitchPro::SUBCMD_SET_PLAYER: + r[0] = 0x80; r[1] = 0x30; + break; + case SwitchPro::SUBCMD_NFC_IR_STATE: + r[0] = 0x80; r[1] = 0x22; + break; + case SwitchPro::SUBCMD_NFC_IR_CFG: + r[0] = 0xA0; r[1] = 0x21; + r[2] = 0x01; r[3] = 0x00; r[4] = 0xFF; r[5] = 0x00; r[6] = 0x08; r[7] = 0x00; r[8] = 0x1B; r[9] = 0x01; + r[37] = 0xC8; + break; + case SwitchPro::SUBCMD_SPI_READ: + { + r[0] = 0x90; r[1] = 0x10; + uint8_t addr_lo = pending_output_[11]; + uint8_t addr_hi = pending_output_[12]; + uint8_t read_len = pending_output_[15]; + r[2] = addr_lo; r[3] = addr_hi; r[6] = read_len; + if (addr_hi == 0x60 && addr_lo == 0x00) + std::memset(r + 7, 0xFF, 16); + else if (addr_hi == 0x60 && addr_lo == 0x50) + { + std::memset(r + 7, 0x32, 3); + std::memset(r + 10, 0xFF, 3); + std::memset(r + 13, 0xFF, 7); + } + else if (addr_hi == 0x60 && addr_lo == 0x80) + { + r[7] = 0x50; r[8] = 0xFD; r[9] = 0x00; r[10] = 0x00; r[11] = 0xC6; r[12] = 0x0F; + const uint8_t params[18] = { 0x0F, 0x30, 0x61, 0x96, 0x30, 0xF3, 0xD4, 0x14, 0x54, 0x41, 0x15, 0x54, 0xC7, 0x79, 0x9C, 0x33, 0x36, 0x63 }; + std::memcpy(r + 13, params, 18); + } + else if (addr_hi == 0x60 && addr_lo == 0x98) + { + const uint8_t params[18] = { 0x0F, 0x30, 0x61, 0x96, 0x30, 0xF3, 0xD4, 0x14, 0x54, 0x41, 0x15, 0x54, 0xC7, 0x79, 0x9C, 0x33, 0x36, 0x63 }; + std::memcpy(r + 7, params, 18); + } + else if (addr_hi == 0x80 && addr_lo == 0x10) + std::memset(r + 7, 0xFF, 3); + else if (addr_hi == 0x60 && addr_lo == 0x3D) + { + const uint8_t l_cal[9] = { 0xD4, 0x75, 0x61, 0xE5, 0x87, 0x7C, 0xEC, 0x55, 0x61 }; + const uint8_t r_cal[9] = { 0x5D, 0xD8, 0x7F, 0x18, 0xE6, 0x61, 0x86, 0x65, 0x5D }; + std::memcpy(r + 7, l_cal, 9); + std::memcpy(r + 16, r_cal, 9); + r[25] = 0xFF; + std::memset(r + 26, 0x32, 3); + std::memset(r + 29, 0xFF, 3); + } + else if (addr_hi == 0x60 && addr_lo == 0x20) + { + const uint8_t sa[24] = { 0xCC, 0x00, 0x40, 0x00, 0x91, 0x01, 0x00, 0x40, 0x00, 0x40, 0x00, 0x40, 0xE7, 0xFF, 0x0E, 0x00, 0xDC, 0xFF, 0x3B, 0x34, 0x3B, 0x34, 0x3B, 0x34 }; + std::memcpy(r + 7, sa, 24); + } + else + std::memset(r + 7, 0xFF, read_len); + break; + } + default: + r[0] = 0x80; r[1] = sub; + break; + } +} + +void SwitchDevice::parse_host_rumble(uint8_t report_id, uint8_t const* buffer, uint16_t bufsize) +{ + const uint8_t* motor_l = nullptr; + const uint8_t* motor_r = nullptr; + + /* OutReport layout: cmd, seq, rumble_l[4], rumble_r[4], … */ + if (bufsize >= 10 && is_rumble_output_cmd(buffer[0])) + { + motor_l = &buffer[2]; + motor_r = &buffer[6]; + } + /* TinyUSB stripped report ID: seq, rumble_l[4], rumble_r[4], … */ + else if (bufsize >= 9 && is_rumble_output_cmd(report_id)) + { + motor_l = &buffer[1]; + motor_r = &buffer[5]; + } + else + return; + + const uint8_t left = hd_rumble_amplitude(motor_l); + const uint8_t right = hd_rumble_amplitude(motor_r); + if (left == rumble_l_ && right == rumble_r_) + return; + rumble_l_ = left; + rumble_r_ = right; + rumble_dirty_ = true; +} + +void SwitchDevice::process(const uint8_t idx, Gamepad& gamepad) +{ + (void)idx; + if (rumble_dirty_) + { + Gamepad::PadOut gp_out; + gp_out.rumble_l = rumble_l_; + gp_out.rumble_r = rumble_r_; + gamepad.set_pad_out(gp_out); + rumble_dirty_ = false; + } + + // Always read latest state and build report so get_report_cb and IN pushes both see current state (minimal latency). + Gamepad::PadIn gp_in = gamepad.get_pad_in(); + gamepad_to_switch_report(gp_in, switch_report_, gamepad); + + bool is_subcommand = has_pending_output_; + if (has_pending_output_) + build_subcommand_reply(switch_report_); + else + build_standard_report(switch_report_); if (tud_suspended()) - { tud_remote_wakeup(); - } - if (tud_hid_n_ready(idx)) - { - tud_hid_n_report(idx, 0, reinterpret_cast(&in_report), sizeof(SwitchWired::InReport)); - } + + // USB init: host reads 0x81 from interrupt IN after sending 0x80 0x01/0x02/ etc. Push it first. + if (has_pending_81_ && tud_hid_n_ready(0)) + { + tud_hid_n_report(0, SwitchPro::REPORT_ID_USB_INIT, report_81_.data() + 1, 63); + has_pending_81_ = false; + return; + } + + if (tud_hid_n_ready(0)) + { + tud_hid_n_report(0, is_subcommand ? SwitchPro::REPORT_ID_SUBCMD : SwitchPro::REPORT_ID_STANDARD, report_.data(), 63); + if (is_subcommand) + { + has_pending_output_ = false; + std::memset(pending_output_.data(), 0, pending_output_.size()); + } + } + else if (is_subcommand) + OGXM_LOG("Switch: HID not ready, subcommand reply dropped\n"); } -uint16_t SwitchDevice::get_report_cb(uint8_t itf, uint8_t report_id, hid_report_type_t report_type, uint8_t *buffer, uint16_t reqlen) +uint16_t SwitchDevice::get_report_cb(uint8_t itf, uint8_t report_id, hid_report_type_t report_type, uint8_t* buffer, uint16_t reqlen) { - if (report_type != HID_REPORT_TYPE_INPUT || itf >= MAX_GAMEPADS) - { - return 0; - } - std::memcpy(buffer, &in_report_[itf], sizeof(SwitchWired::InReport)); - return sizeof(SwitchWired::InReport); + (void)itf; + // Log init and subcommand report requests (standard 0x30 polling is very frequent) + if (report_id == SwitchPro::REPORT_ID_USB_INIT) + OGXM_LOG("Switch: get_report id=0x81 (USB init) reqlen=%u\n", (unsigned)reqlen); + if (report_id == SwitchPro::REPORT_ID_SUBCMD) + OGXM_LOG("Switch: get_report id=0x21 (subcmd reply) reqlen=%u\n", (unsigned)reqlen); + if (report_type != HID_REPORT_TYPE_INPUT) + { + OGXM_LOG("Switch: get_report rejected (bad type)\n"); + return 0; + } + // Report ID 0x81: USB init handshake reply (MAC, handshake, timeout ack) + if (report_id == SwitchPro::REPORT_ID_USB_INIT) + { + if (!has_pending_81_ || reqlen < 2) + return 0; + uint16_t copy_len = reqlen < SwitchPro::USB_INIT_REPORT_SIZE ? reqlen : SwitchPro::USB_INIT_REPORT_SIZE; + std::memcpy(buffer, report_81_.data(), copy_len); + has_pending_81_ = false; + return copy_len; + } + if (report_id != SwitchPro::REPORT_ID_STANDARD && report_id != SwitchPro::REPORT_ID_SUBCMD) + { + OGXM_LOG("Switch: get_report rejected (bad id)\n"); + return 0; + } + if (reqlen < SwitchPro::REPORT_SIZE) + { + OGXM_LOG("Switch: get_report rejected (reqlen %u < %d)\n", (unsigned)reqlen, SwitchPro::REPORT_SIZE); + return 0; + } + buffer[0] = report_id; + std::memcpy(buffer + 1, report_.data(), 63); + return SwitchPro::REPORT_SIZE; } -void SwitchDevice::set_report_cb(uint8_t itf, uint8_t report_id, hid_report_type_t report_type, uint8_t const *buffer, uint16_t bufsize) {} +void SwitchDevice::set_report_cb(uint8_t itf, uint8_t report_id, hid_report_type_t report_type, uint8_t const* buffer, uint16_t bufsize) +{ + (void)itf; + (void)report_type; + OGXM_LOG("Switch: set_report id=0x%02x type=%u len=%u\n", (unsigned)report_id, (unsigned)report_type, (unsigned)bufsize); + if (bufsize > 12) + OGXM_LOG("Switch: cmd=0x%02x seq=0x%02x subcmd=0x%02x\n", + (unsigned)buffer[0], (unsigned)buffer[1], (unsigned)buffer[10]); + OGXM_LOG_HEX(buffer, bufsize > 64 ? 64 : bufsize); + if (bufsize > SwitchPro::REPORT_SIZE) + bufsize = SwitchPro::REPORT_SIZE; + std::memcpy(pending_output_.data(), buffer, bufsize); + + parse_host_rumble(report_id, buffer, bufsize); -bool SwitchDevice::vendor_control_xfer_cb(uint8_t rhport, uint8_t stage, tusb_control_request_t const *request) + // USB init: 2-byte reports [0x80, subtype] require reply on report ID 0x81 (Chromium / Switch console). + if (bufsize >= 2 && buffer[0] == 0x80) + { + uint8_t subtype = buffer[1]; + std::memset(report_81_.data(), 0, report_81_.size()); + report_81_[0] = SwitchPro::REPORT_ID_USB_INIT; + report_81_[1] = subtype; + if (subtype == 0x01) // Request MAC: subtype, padding, device_type (0x03 = Pro), mac[6] + { + report_81_[2] = 0x00; + report_81_[3] = 0x03; // kUsbDeviceTypeProController + std::memcpy(report_81_.data() + 4, addr_.data(), 6); + } + has_pending_81_ = true; + } + /* Subcommand reply only for rumble+subcmd reports — not rumble-only 0x10 (often 64-byte padded). */ + else if (bufsize >= 11) + { + const uint8_t cmd = (buffer[0] != 0) ? buffer[0] : report_id; + if (is_subcommand_output_cmd(cmd)) + has_pending_output_ = true; + } +} + +bool SwitchDevice::vendor_control_xfer_cb(uint8_t rhport, uint8_t stage, tusb_control_request_t const* request) { - return false; + (void)rhport; + OGXM_LOG("Switch: vendor_control stage=%u bmReqType=0x%02x bReq=0x%02x wVal=0x%04x wIdx=0x%04x wLen=%u\n", + (unsigned)stage, (unsigned)request->bmRequestType, (unsigned)request->bRequest, + (unsigned)request->wValue, (unsigned)request->wIndex, (unsigned)request->wLength); + return false; } -const uint16_t* SwitchDevice::get_descriptor_string_cb(uint8_t index, uint16_t langid) +const uint16_t* SwitchDevice::get_descriptor_string_cb(uint8_t index, uint16_t langid) { - const char *value = reinterpret_cast(SwitchWired::STRING_DESCRIPTORS[index]); - return get_string_descriptor(value, index); + (void)langid; + OGXM_LOG("Switch: get_descriptor_string index=%u\n", (unsigned)index); + if (index == 0) + { + static uint16_t lang[2] = { 0x0304, 0x0409 }; + return lang; + } + const char* str = nullptr; + if (index == 1) str = SwitchPro::STRING_MANUFACTURER; + else if (index == 2) str = SwitchPro::STRING_PRODUCT; + else if (index == 3) str = SwitchPro::STRING_VERSION; + if (!str) + return nullptr; + return get_string_descriptor(str, index); } -const uint8_t* SwitchDevice::get_descriptor_device_cb() +const uint8_t* SwitchDevice::get_descriptor_device_cb() { - return SwitchWired::DEVICE_DESCRIPTORS; + OGXM_LOG("Switch: get_descriptor device\n"); + return SwitchPro::DEVICE_DESCRIPTOR; } -const uint8_t* SwitchDevice::get_hid_descriptor_report_cb(uint8_t itf) +const uint8_t* SwitchDevice::get_hid_descriptor_report_cb(uint8_t itf) { - return SwitchWired::REPORT_DESCRIPTORS; + (void)itf; + OGXM_LOG("Switch: get_descriptor HID report\n"); + return SwitchPro::REPORT_DESCRIPTOR; } -const uint8_t* SwitchDevice::get_descriptor_configuration_cb(uint8_t index) +const uint8_t* SwitchDevice::get_descriptor_configuration_cb(uint8_t index) { - return SwitchWired::CONFIGURATION_DESCRIPTORS; + (void)index; + OGXM_LOG("Switch: get_descriptor configuration\n"); + return SwitchPro::CONFIGURATION_DESCRIPTOR; } -const uint8_t* SwitchDevice::get_descriptor_device_qualifier_cb() +const uint8_t* SwitchDevice::get_descriptor_device_qualifier_cb() { return nullptr; -} \ No newline at end of file +} diff --git a/Firmware/RP2040/src/USBDevice/DeviceDriver/Switch/Switch.h b/Firmware/RP2040/src/USBDevice/DeviceDriver/Switch/Switch.h index a0d016f9..343191b4 100644 --- a/Firmware/RP2040/src/USBDevice/DeviceDriver/Switch/Switch.h +++ b/Firmware/RP2040/src/USBDevice/DeviceDriver/Switch/Switch.h @@ -3,26 +3,53 @@ #include #include +#include #include "USBDevice/DeviceDriver/DeviceDriver.h" -#include "Descriptors/SwitchWired.h" +#include "Descriptors/SwitchProDevice.h" -class SwitchDevice : public DeviceDriver +class SwitchDevice : public DeviceDriver { public: void initialize() override; void process(const uint8_t idx, Gamepad& gamepad) override; - uint16_t get_report_cb(uint8_t itf, uint8_t report_id, hid_report_type_t report_type, uint8_t *buffer, uint16_t reqlen) override; - void set_report_cb(uint8_t itf, uint8_t report_id, hid_report_type_t report_type, uint8_t const *buffer, uint16_t bufsize) override; - bool vendor_control_xfer_cb(uint8_t rhport, uint8_t stage, tusb_control_request_t const *request) override; + uint16_t get_report_cb(uint8_t itf, uint8_t report_id, hid_report_type_t report_type, uint8_t* buffer, uint16_t reqlen) override; + void set_report_cb(uint8_t itf, uint8_t report_id, hid_report_type_t report_type, uint8_t const* buffer, uint16_t bufsize) override; + bool vendor_control_xfer_cb(uint8_t rhport, uint8_t stage, tusb_control_request_t const* request) override; const uint16_t* get_descriptor_string_cb(uint8_t index, uint16_t langid) override; const uint8_t* get_descriptor_device_cb() override; - const uint8_t* get_hid_descriptor_report_cb(uint8_t itf) override; + const uint8_t* get_hid_descriptor_report_cb(uint8_t itf) override; const uint8_t* get_descriptor_configuration_cb(uint8_t index) override; const uint8_t* get_descriptor_device_qualifier_cb() override; private: - std::array in_report_; + void gamepad_to_switch_report(const Gamepad::PadIn& gp_in, SwitchPro::SwitchReport& out, const Gamepad& gamepad); + void build_standard_report(const SwitchPro::SwitchReport& sw); + void build_subcommand_reply(const SwitchPro::SwitchReport& sw); + void set_timer(); + /** Decode HD rumble from a console output report into rumble_l_/rumble_r_. */ + void parse_host_rumble(uint8_t report_id, uint8_t const* buffer, uint16_t bufsize); + + std::array report_{}; + SwitchPro::SwitchReport switch_report_{}; + std::array pending_output_{}; + bool has_pending_output_ = false; + + uint8_t timer_ = 0; + uint32_t timestamp_ms_ = 0; + uint8_t vibration_report_ = 0x00; + uint8_t vibration_idx_ = 0; + bool vibration_enabled_ = false; + std::array addr_ = { 0x7C, 0xBB, 0x8A, 0x12, 0x34, 0x56 }; + + // Latest rumble decoded from host 0x01/0x10/0x11 output; applied in process(). + uint8_t rumble_l_ = 0; + uint8_t rumble_r_ = 0; + bool rumble_dirty_ = false; + + // USB init handshake: reply to 0x80 0x01 (MAC), 0x80 0x02 (handshake), 0x80 0x04/0x05 (timeout) + std::array report_81_{}; + bool has_pending_81_ = false; }; -#endif // _SWITCH_DEVICE_H_ +#endif diff --git a/Firmware/RP2040/src/USBDevice/DeviceDriver/Wii/Wii.cpp b/Firmware/RP2040/src/USBDevice/DeviceDriver/Wii/Wii.cpp new file mode 100644 index 00000000..c80d9f96 --- /dev/null +++ b/Firmware/RP2040/src/USBDevice/DeviceDriver/Wii/Wii.cpp @@ -0,0 +1,32 @@ +#include "USBDevice/DeviceDriver/Wii/Wii.h" + +void WiiDevice::initialize() +{ + // No USB device (tud) in Wii mode on Pico W; BT is used for Wiimote. + (void)class_driver_; // unused +} + +void WiiDevice::process(const uint8_t idx, Gamepad& gamepad) +{ + (void)idx; + (void)gamepad; + // Gamepad is filled by USB host on Core1. Optional: send to Wiimote BT stack here. +} + +uint16_t WiiDevice::get_report_cb(uint8_t, uint8_t, hid_report_type_t, uint8_t*, uint16_t) +{ + return 0; +} + +void WiiDevice::set_report_cb(uint8_t, uint8_t, hid_report_type_t, uint8_t const*, uint16_t) {} + +bool WiiDevice::vendor_control_xfer_cb(uint8_t, uint8_t, tusb_control_request_t const*) +{ + return false; +} + +const uint16_t* WiiDevice::get_descriptor_string_cb(uint8_t, uint16_t) { return nullptr; } +const uint8_t* WiiDevice::get_descriptor_device_cb() { return nullptr; } +const uint8_t* WiiDevice::get_hid_descriptor_report_cb(uint8_t) { return nullptr; } +const uint8_t* WiiDevice::get_descriptor_configuration_cb(uint8_t) { return nullptr; } +const uint8_t* WiiDevice::get_descriptor_device_qualifier_cb() { return nullptr; } diff --git a/Firmware/RP2040/src/USBDevice/DeviceDriver/Wii/Wii.h b/Firmware/RP2040/src/USBDevice/DeviceDriver/Wii/Wii.h new file mode 100644 index 00000000..1288fab6 --- /dev/null +++ b/Firmware/RP2040/src/USBDevice/DeviceDriver/Wii/Wii.h @@ -0,0 +1,24 @@ +#ifndef _WII_DEVICE_H_ +#define _WII_DEVICE_H_ + +#include "USBDevice/DeviceDriver/DeviceDriver.h" + +/** Stub Wiimote driver for Pico W/2W Wii mode. + * Controller is on PIO USB host; BT is used for Wiimote link to the Wii. + * process() can be extended to send gamepad state to a Wiimote BT stack. */ +class WiiDevice : public DeviceDriver +{ +public: + void initialize() override; + void process(const uint8_t idx, Gamepad& gamepad) override; + uint16_t get_report_cb(uint8_t itf, uint8_t report_id, hid_report_type_t report_type, uint8_t* buffer, uint16_t reqlen) override; + void set_report_cb(uint8_t itf, uint8_t report_id, hid_report_type_t report_type, uint8_t const* buffer, uint16_t bufsize) override; + bool vendor_control_xfer_cb(uint8_t rhport, uint8_t stage, tusb_control_request_t const* request) override; + const uint16_t* get_descriptor_string_cb(uint8_t index, uint16_t langid) override; + const uint8_t* get_descriptor_device_cb() override; + const uint8_t* get_hid_descriptor_report_cb(uint8_t itf) override; + const uint8_t* get_descriptor_configuration_cb(uint8_t index) override; + const uint8_t* get_descriptor_device_qualifier_cb() override; +}; + +#endif // _WII_DEVICE_H_ diff --git a/Firmware/RP2040/src/USBDevice/DeviceDriver/WiiU/WiiU.cpp b/Firmware/RP2040/src/USBDevice/DeviceDriver/WiiU/WiiU.cpp new file mode 100644 index 00000000..6ae32692 --- /dev/null +++ b/Firmware/RP2040/src/USBDevice/DeviceDriver/WiiU/WiiU.cpp @@ -0,0 +1,186 @@ +#include + +#include "Board/ogxm_log.h" +#include "USBDevice/DeviceDriver/WiiU/WiiU.h" + +// Set to 1 to bypass init gate: always send reports for testing (Dolphin/Linux may not send 0x13). +#ifndef WIIU_SKIP_INIT_GATE +#define WIIU_SKIP_INIT_GATE 1 +#endif + +void WiiUDevice::initialize() +{ + class_driver_ = + { + .name = TUD_DRV_NAME("WIIU"), + .init = hidd_init, + .deinit = hidd_deinit, + .reset = hidd_reset, + .open = hidd_open, + .control_xfer_cb = hidd_control_xfer_cb, + .xfer_cb = hidd_xfer_cb, + .sof = NULL + }; + + in_report_.report_id = WiiU::HID_REPORT_ID; + std::memset(in_report_.port_data, 0, sizeof(in_report_.port_data)); + init_received_ = false; +} + +// GC stick deadzone: values near center map to 128 to avoid spurious LEFT/RIGHT/UP/DOWN +static constexpr int16_t STICK_DEADZONE = 4096; // ~12.5% of -32768..32767 + +// Dolphin GameCube test reported max stick range: Left ±111, C-stick ~±110..112 (center 0). +// Our bytes 0–255 map to that via host; Y uses 1..254 to avoid full-up→down misread. + +static uint8_t stick_to_gc(int16_t value) +{ + if (value > -STICK_DEADZONE && value < STICK_DEADZONE) + return WiiU::STICK_CENTER; + return Scale::int16_to_uint8(value); +} + +// Y axis only: map to 1..254 so 0/255 are never sent (some hosts treat extremes as wrong direction). +static uint8_t stick_y_to_gc(int16_t value) +{ + if (value > -STICK_DEADZONE && value < STICK_DEADZONE) + return WiiU::STICK_CENTER; + uint8_t u = Scale::int16_to_uint8(value); + if (u <= 1) return 1u; + if (u >= 254) return 254u; + return u; +} + +static void fill_port_block(uint8_t port[9], const Gamepad::PadIn& gp_in, bool stick_y_positive_is_up) +{ + // Byte 0 (type): Bit 4 = Wired. Dolphin: (type & 0x10) != 0 → connected. + port[0] = WiiU::PORT_WIRED; + + // Byte 1 (b1): A, B, X, Y, D-pad Left, Right, Down, Up — direct mapping (A→A, B→B, etc.) + uint8_t b1 = 0; + if (gp_in.buttons & Gamepad::BUTTON_B) b1 |= WiiU::GCButtons::A; + if (gp_in.buttons & Gamepad::BUTTON_A) b1 |= WiiU::GCButtons::B; + if (gp_in.buttons & Gamepad::BUTTON_Y) b1 |= WiiU::GCButtons::X; + if (gp_in.buttons & Gamepad::BUTTON_X) b1 |= WiiU::GCButtons::Y; + if (gp_in.dpad & Gamepad::DPAD_LEFT) b1 |= WiiU::GCButtons::LEFT; + if (gp_in.dpad & Gamepad::DPAD_RIGHT) b1 |= WiiU::GCButtons::RIGHT; + if (gp_in.dpad & Gamepad::DPAD_DOWN) b1 |= WiiU::GCButtons::DOWN; + if (gp_in.dpad & Gamepad::DPAD_UP) b1 |= WiiU::GCButtons::UP; + port[1] = b1; + + // Byte 2 (b2): Start, Z, R (digital), L (digital) + // L/R come from triggers at 255; Z from RB; LB maps to nothing + uint8_t b2 = 0; + if (gp_in.buttons & Gamepad::BUTTON_START) b2 |= WiiU::GCButtons::START; + if (gp_in.buttons & Gamepad::BUTTON_RB) b2 |= WiiU::GCButtons::Z; + if (gp_in.trigger_l >= 255) b2 |= WiiU::GCButtons::L; + if (gp_in.trigger_r >= 255) b2 |= WiiU::GCButtons::R; + port[2] = b2; + + // Bytes 3–4: main stick X, Y (0–255, 128 = center). + // Y: set by host — Xbox uses positive=up (send value), Wii U/Nintendo use negative=up (send 255-value). + port[3] = stick_to_gc(gp_in.joystick_lx); + { + uint8_t ly = stick_y_to_gc(gp_in.joystick_ly); + port[4] = stick_y_positive_is_up ? ly : static_cast(255 - ly); + } + // Bytes 5–6: C-stick (substick) X, Y + port[5] = stick_to_gc(gp_in.joystick_rx); + { + uint8_t ry = stick_y_to_gc(gp_in.joystick_ry); + port[6] = stick_y_positive_is_up ? ry : static_cast(255 - ry); + } + // Bytes 7–8: L/R trigger analog (0–255) + port[7] = gp_in.trigger_l; + port[8] = gp_in.trigger_r; +} + +void WiiUDevice::process(const uint8_t idx, Gamepad& gamepad) +{ + if (idx >= 4) + return; + + // Always update from current gamepad state so late controller connections are reflected immediately. + // Dolphin may stop re-checking after seeing initial zeros; filling every frame avoids that. + // Combo check (check_for_driver_change) also uses get_pad_in(); both see the same current state. + Gamepad::PadIn gp_in = gamepad.get_pad_in(); + fill_port_block(in_report_.port_data[idx], gp_in, gamepad.stick_y_positive_is_up()); + + if (tud_suspended()) + tud_remote_wakeup(); + + // Send once per frame after the last port, only when init received (or WIIU_SKIP_INIT_GATE) and endpoint ready. + // Real adapter does not send reports until host sends Start Polling (0x13). + constexpr uint8_t last_port = (MAX_GAMEPADS >= 4) ? 3 : (MAX_GAMEPADS - 1); + if (idx == last_port && (WIIU_SKIP_INIT_GATE || init_received_) && tud_hid_n_ready(0)) + { + // TinyUSB prepends report_id; pass only port_data (36 bytes) to avoid double report ID + tud_hid_n_report(0, WiiU::HID_REPORT_ID, in_report_.port_data, sizeof(in_report_.port_data)); +#if defined(CONFIG_OGXM_DEBUG) + static uint32_t send_count = 0; + if (++send_count % 500 == 0) + OGXM_LOG("WiiU: IN report sent (count " + std::to_string(send_count) + ")\n"); +#endif + } +} + +uint16_t WiiUDevice::get_report_cb(uint8_t itf, uint8_t report_id, hid_report_type_t report_type, uint8_t *buffer, uint16_t reqlen) +{ + if (report_type != HID_REPORT_TYPE_INPUT || report_id != WiiU::HID_REPORT_ID || itf != 0) + return 0; + if (reqlen < sizeof(WiiU::InReport)) + return 0; + std::memcpy(buffer, &in_report_, sizeof(WiiU::InReport)); + return sizeof(WiiU::InReport); +} + +void WiiUDevice::set_report_cb(uint8_t itf, uint8_t report_id, hid_report_type_t report_type, uint8_t const *buffer, uint16_t bufsize) +{ + // Host sends Start Polling (0x13) via Output report. May be [0x13] or [0x21, 0x13] if report ID prepended. + bool is_init = (bufsize >= 1 && buffer[0] == WiiU::INIT_PAYLOAD) || + (bufsize >= 2 && buffer[1] == WiiU::INIT_PAYLOAD); + if (is_init) + { + init_received_ = true; +#if defined(CONFIG_OGXM_DEBUG) + OGXM_LOG("WiiU: init 0x13 received from host\n"); +#endif + } + (void)itf; + (void)report_id; + (void)report_type; +} + +bool WiiUDevice::vendor_control_xfer_cb(uint8_t rhport, uint8_t stage, tusb_control_request_t const *request) +{ + (void)rhport; + (void)stage; + (void)request; + return false; +} + +const uint16_t* WiiUDevice::get_descriptor_string_cb(uint8_t index, uint16_t langid) +{ + const char *value = reinterpret_cast(WiiU::STRING_DESCRIPTORS[index]); + return get_string_descriptor(value, index); +} + +const uint8_t* WiiUDevice::get_descriptor_device_cb() +{ + return WiiU::DEVICE_DESCRIPTORS; +} + +const uint8_t* WiiUDevice::get_hid_descriptor_report_cb(uint8_t itf) +{ + return WiiU::REPORT_DESCRIPTORS; +} + +const uint8_t* WiiUDevice::get_descriptor_configuration_cb(uint8_t index) +{ + return WiiU::CONFIGURATION_DESCRIPTORS; +} + +const uint8_t* WiiUDevice::get_descriptor_device_qualifier_cb() +{ + return nullptr; +} diff --git a/Firmware/RP2040/src/USBDevice/DeviceDriver/WiiU/WiiU.h b/Firmware/RP2040/src/USBDevice/DeviceDriver/WiiU/WiiU.h new file mode 100644 index 00000000..537a12e3 --- /dev/null +++ b/Firmware/RP2040/src/USBDevice/DeviceDriver/WiiU/WiiU.h @@ -0,0 +1,28 @@ +#ifndef _WIIU_DEVICE_H_ +#define _WIIU_DEVICE_H_ + +#include + +#include "USBDevice/DeviceDriver/DeviceDriver.h" +#include "Descriptors/WiiU.h" + +class WiiUDevice : public DeviceDriver +{ +public: + void initialize() override; + void process(const uint8_t idx, Gamepad& gamepad) override; + uint16_t get_report_cb(uint8_t itf, uint8_t report_id, hid_report_type_t report_type, uint8_t *buffer, uint16_t reqlen) override; + void set_report_cb(uint8_t itf, uint8_t report_id, hid_report_type_t report_type, uint8_t const *buffer, uint16_t bufsize) override; + bool vendor_control_xfer_cb(uint8_t rhport, uint8_t stage, tusb_control_request_t const *request) override; + const uint16_t* get_descriptor_string_cb(uint8_t index, uint16_t langid) override; + const uint8_t* get_descriptor_device_cb() override; + const uint8_t* get_hid_descriptor_report_cb(uint8_t itf) override; + const uint8_t* get_descriptor_configuration_cb(uint8_t index) override; + const uint8_t* get_descriptor_device_qualifier_cb() override; + +private: + WiiU::InReport in_report_{}; + bool init_received_{ false }; // true after host sends Start Polling (0x13) +}; + +#endif // _WIIU_DEVICE_H_ diff --git a/Firmware/RP2040/src/USBDevice/DeviceDriver/XInput/XInput.cpp b/Firmware/RP2040/src/USBDevice/DeviceDriver/XInput/XInput.cpp index 43d1465d..a6c206cc 100644 --- a/Firmware/RP2040/src/USBDevice/DeviceDriver/XInput/XInput.cpp +++ b/Firmware/RP2040/src/USBDevice/DeviceDriver/XInput/XInput.cpp @@ -1,80 +1,151 @@ +#include #include +#include "pico/time.h" +#include "tusb.h" #include "USBDevice/DeviceDriver/XInput/tud_xinput/tud_xinput.h" #include "USBDevice/DeviceDriver/XInput/XInput.h" -void XInputDevice::initialize() +extern "C" { +#include "xsm3.h" +} + +// XSM3 state and buffers — match joypad-os: init at driver init, defer crypto to process() loop +namespace { + enum class Xsm3AuthState : uint8_t { + Idle = 0, + InitReceived = 1, // 0x82 data received, pending xsm3_do_challenge_init + Responded = 2, // init response ready for 0x83 + VerifyReceived = 3, // 0x87 data received, pending xsm3_do_challenge_verify + Authenticated = 4, + }; + static Xsm3AuthState xsm3_auth_state = Xsm3AuthState::Idle; + static uint8_t xsm3_buf_82[0x22]; + static uint8_t xsm3_buf_87[0x16]; + // joypad-os: state 1 = processing, state 2 = response ready + static constexpr uint8_t XSM3_STATE_PROCESSING = 1; + static constexpr uint8_t XSM3_STATE_READY = 2; + // joypad-os: init response is 46 bytes (0x2E), not full 0x30 + static constexpr uint16_t XSM3_RESPONSE_INIT_LEN = 46u; + static constexpr uint16_t XSM3_RESPONSE_VERIFY_LEN = 0x16u; + + static void xsm3_process_pending(void) { + if (xsm3_auth_state == Xsm3AuthState::InitReceived) { + xsm3_do_challenge_init(xsm3_buf_82); + xsm3_auth_state = Xsm3AuthState::Responded; + } else if (xsm3_auth_state == Xsm3AuthState::VerifyReceived) { + xsm3_do_challenge_verify(xsm3_buf_87); + xsm3_auth_state = Xsm3AuthState::Authenticated; + } + } +} + +void XInputDevice::initialize() { - class_driver_ = *tud_xinput::class_driver(); + class_driver_ = *tud_xinput::class_driver(); + // joypad-os: init XSM3 at mode init so 0x81 can send ID without doing init in callback + xsm3_initialise_state(); + xsm3_set_identification_data(xsm3_id_data_ms_controller); + xsm3_auth_state = Xsm3AuthState::Idle; } void XInputDevice::process(const uint8_t idx, Gamepad& gamepad) { - if (gamepad.new_pad_in()) - { - in_report_.buttons[0] = 0; - in_report_.buttons[1] = 0; - - Gamepad::PadIn gp_in = gamepad.get_pad_in(); - - switch (gp_in.dpad) - { - case Gamepad::DPAD_UP: - in_report_.buttons[0] = XInput::Buttons0::DPAD_UP; - break; - case Gamepad::DPAD_DOWN: - in_report_.buttons[0] = XInput::Buttons0::DPAD_DOWN; - break; - case Gamepad::DPAD_LEFT: - in_report_.buttons[0] = XInput::Buttons0::DPAD_LEFT; - break; - case Gamepad::DPAD_RIGHT: - in_report_.buttons[0] = XInput::Buttons0::DPAD_RIGHT; - break; - case Gamepad::DPAD_UP_LEFT: - in_report_.buttons[0] = XInput::Buttons0::DPAD_UP | XInput::Buttons0::DPAD_LEFT; - break; - case Gamepad::DPAD_UP_RIGHT: - in_report_.buttons[0] = XInput::Buttons0::DPAD_UP | XInput::Buttons0::DPAD_RIGHT; - break; - case Gamepad::DPAD_DOWN_LEFT: - in_report_.buttons[0] = XInput::Buttons0::DPAD_DOWN | XInput::Buttons0::DPAD_LEFT; - break; - case Gamepad::DPAD_DOWN_RIGHT: - in_report_.buttons[0] = XInput::Buttons0::DPAD_DOWN | XInput::Buttons0::DPAD_RIGHT; - break; - default: - break; - } - - if (gp_in.buttons & Gamepad::BUTTON_BACK) in_report_.buttons[0] |= XInput::Buttons0::BACK; - if (gp_in.buttons & Gamepad::BUTTON_START) in_report_.buttons[0] |= XInput::Buttons0::START; - if (gp_in.buttons & Gamepad::BUTTON_L3) in_report_.buttons[0] |= XInput::Buttons0::L3; - if (gp_in.buttons & Gamepad::BUTTON_R3) in_report_.buttons[0] |= XInput::Buttons0::R3; - - if (gp_in.buttons & Gamepad::BUTTON_X) in_report_.buttons[1] |= XInput::Buttons1::X; - if (gp_in.buttons & Gamepad::BUTTON_A) in_report_.buttons[1] |= XInput::Buttons1::A; - if (gp_in.buttons & Gamepad::BUTTON_Y) in_report_.buttons[1] |= XInput::Buttons1::Y; - if (gp_in.buttons & Gamepad::BUTTON_B) in_report_.buttons[1] |= XInput::Buttons1::B; - if (gp_in.buttons & Gamepad::BUTTON_LB) in_report_.buttons[1] |= XInput::Buttons1::LB; - if (gp_in.buttons & Gamepad::BUTTON_RB) in_report_.buttons[1] |= XInput::Buttons1::RB; - if (gp_in.buttons & Gamepad::BUTTON_SYS) in_report_.buttons[1] |= XInput::Buttons1::HOME; - - in_report_.trigger_l = gp_in.trigger_l; - in_report_.trigger_r = gp_in.trigger_r; - - in_report_.joystick_lx = gp_in.joystick_lx; - in_report_.joystick_ly = Range::invert(gp_in.joystick_ly); - in_report_.joystick_rx = gp_in.joystick_rx; - in_report_.joystick_ry = Range::invert(gp_in.joystick_ry); - - if (tud_suspended()) - { - tud_remote_wakeup(); - } - - tud_xinput::send_report((uint8_t*)&in_report_, sizeof(XInput::InReport)); - } + // joypad-os: run XSM3 crypto in task loop, not in USB callback + xsm3_process_pending(); + + // Always read latest state and build report every loop so get_report_cb and IN send both see current state (minimal latency; only delay is BT radio when wireless). + in_report_.buttons[0] = 0; + in_report_.buttons[1] = 0; + Gamepad::PadIn gp_in = gamepad.get_pad_in(); + + switch (gp_in.dpad) + { + case Gamepad::DPAD_UP: + in_report_.buttons[0] = XInput::Buttons0::DPAD_UP; + break; + case Gamepad::DPAD_DOWN: + in_report_.buttons[0] = XInput::Buttons0::DPAD_DOWN; + break; + case Gamepad::DPAD_LEFT: + in_report_.buttons[0] = XInput::Buttons0::DPAD_LEFT; + break; + case Gamepad::DPAD_RIGHT: + in_report_.buttons[0] = XInput::Buttons0::DPAD_RIGHT; + break; + case Gamepad::DPAD_UP_LEFT: + in_report_.buttons[0] = XInput::Buttons0::DPAD_UP | XInput::Buttons0::DPAD_LEFT; + break; + case Gamepad::DPAD_UP_RIGHT: + in_report_.buttons[0] = XInput::Buttons0::DPAD_UP | XInput::Buttons0::DPAD_RIGHT; + break; + case Gamepad::DPAD_DOWN_LEFT: + in_report_.buttons[0] = XInput::Buttons0::DPAD_DOWN | XInput::Buttons0::DPAD_LEFT; + break; + case Gamepad::DPAD_DOWN_RIGHT: + in_report_.buttons[0] = XInput::Buttons0::DPAD_DOWN | XInput::Buttons0::DPAD_RIGHT; + break; + default: + break; + } + + if (gp_in.buttons & Gamepad::BUTTON_BACK) in_report_.buttons[0] |= XInput::Buttons0::BACK; + if (gp_in.buttons & Gamepad::BUTTON_START) in_report_.buttons[0] |= XInput::Buttons0::START; + if (gp_in.buttons & Gamepad::BUTTON_L3) in_report_.buttons[0] |= XInput::Buttons0::L3; + if (gp_in.buttons & Gamepad::BUTTON_R3) in_report_.buttons[0] |= XInput::Buttons0::R3; + + if (gp_in.buttons & Gamepad::BUTTON_X) in_report_.buttons[1] |= XInput::Buttons1::X; + if (gp_in.buttons & Gamepad::BUTTON_A) in_report_.buttons[1] |= XInput::Buttons1::A; + if (gp_in.buttons & Gamepad::BUTTON_Y) in_report_.buttons[1] |= XInput::Buttons1::Y; + if (gp_in.buttons & Gamepad::BUTTON_B) in_report_.buttons[1] |= XInput::Buttons1::B; + if (gp_in.buttons & Gamepad::BUTTON_LB) in_report_.buttons[1] |= XInput::Buttons1::LB; + if (gp_in.buttons & Gamepad::BUTTON_RB) in_report_.buttons[1] |= XInput::Buttons1::RB; + if (gp_in.buttons & Gamepad::BUTTON_SYS) in_report_.buttons[1] |= XInput::Buttons1::HOME; + + in_report_.trigger_l = gp_in.trigger_l; + in_report_.trigger_r = gp_in.trigger_r; + + in_report_.joystick_lx = gp_in.joystick_lx; + in_report_.joystick_ly = Range::invert(gp_in.joystick_ly); + in_report_.joystick_rx = gp_in.joystick_rx; + in_report_.joystick_ry = Range::invert(gp_in.joystick_ry); + + // Remote wake when host has suspended the bus (e.g. 360 "off" with USB power kept): + // - Guide (Home) press, or + // - Start held for 3 seconds (avoids holding Guide on Xbox One/PS5 pads, which can turn the controller off). + { + static bool start_wake_sent = false; + static bool start_held = false; + static absolute_time_t start_hold_begin = { 0 }; + bool start_pressed = (gp_in.buttons & Gamepad::BUTTON_START) != 0; + if (start_pressed) + { + if (!start_held) + { + start_held = true; + start_hold_begin = get_absolute_time(); + } + else + { + uint64_t hold_ms = to_ms_since_boot(get_absolute_time()) - to_ms_since_boot(start_hold_begin); + if (hold_ms >= 3000 && tud_suspended() && !start_wake_sent) + { + tud_remote_wakeup(); + start_wake_sent = true; + } + } + } + else + { + start_held = false; + start_wake_sent = false; + } + if (tud_suspended() && (gp_in.buttons & Gamepad::BUTTON_SYS)) + tud_remote_wakeup(); + } + + // send_report() only transmits when endpoint is free; otherwise we keep latest in_report_ for get_report_cb + tud_xinput::send_report((uint8_t*)&in_report_, sizeof(XInput::InReport)); if (tud_xinput::receive_report(reinterpret_cast(&out_report_), sizeof(XInput::OutReport)) && out_report_.report_id == XInput::OutReportID::RUMBLE) @@ -94,13 +165,111 @@ uint16_t XInputDevice::get_report_cb(uint8_t itf, uint8_t report_id, hid_report_ void XInputDevice::set_report_cb(uint8_t itf, uint8_t report_id, hid_report_type_t report_type, uint8_t const *buffer, uint16_t bufsize) {} -bool XInputDevice::vendor_control_xfer_cb(uint8_t rhport, uint8_t stage, tusb_control_request_t const *request) +bool XInputDevice::vendor_control_xfer_cb(uint8_t rhport, uint8_t stage, tusb_control_request_t const *request) { - return false; + printf("XSM3 cb bReq 0x%02x stage %u bmReqType 0x%02x wIdx 0x%04x\n", + (unsigned)request->bRequest, (unsigned)stage, (unsigned)request->bmRequestType, (unsigned)request->wIndex); + // 360 can send XSM3 as Vendor (0x40/0xC0) or Class (0x20/0xA0) to the security interface. + // Accept Vendor (bits 6-5 == 0x40) or Class (bits 6-5 == 0x20). + uint8_t type_bits = request->bmRequestType & 0x60; + if (type_bits != 0x40 && type_bits != 0x20) + return false; + + switch (request->bRequest) + { + // 0x81: Host GET — send controller identification (0x1D bytes). XSM3 already inited in initialize(). + case 0x81: + if (stage == CONTROL_STAGE_SETUP) printf("XSM3: 0x81 GET_SERIAL\n"); + if (stage == CONTROL_STAGE_SETUP && request->wLength >= 0x1D) + return tud_control_xfer(rhport, request, const_cast(xsm3_id_data_ms_controller), 0x1D); + return true; + + // 0x82: Host OUT — receive challenge init; defer xsm3_do_challenge_init to process() (joypad-os) + case 0x82: + if (stage == CONTROL_STAGE_SETUP && request->wLength >= 0x22) + return tud_control_xfer(rhport, request, xsm3_buf_82, 0x22); + if (stage == CONTROL_STAGE_DATA) + { + printf("XSM3: 0x82 challenge init received (defer process)\n"); + xsm3_auth_state = Xsm3AuthState::InitReceived; + } + return true; + + // 0x83: Host GET — send challenge response. Joypad: 46 bytes init, 22 bytes verify. + case 0x83: + if (stage == CONTROL_STAGE_SETUP) + { + if (xsm3_auth_state == Xsm3AuthState::Responded) + { + printf("XSM3: 0x83 sending init response (46 bytes)\n"); + uint16_t len = (request->wLength < XSM3_RESPONSE_INIT_LEN) ? request->wLength : XSM3_RESPONSE_INIT_LEN; + bool ok = tud_control_xfer(rhport, request, xsm3_challenge_response, len); + if (ok) + xsm3_auth_state = Xsm3AuthState::Idle; + return ok; + } + if (xsm3_auth_state == Xsm3AuthState::Authenticated) + { + printf("XSM3: 0x83 sending verify response (0x16 bytes)\n"); + uint16_t len = (request->wLength < XSM3_RESPONSE_VERIFY_LEN) ? request->wLength : XSM3_RESPONSE_VERIFY_LEN; + bool ok = tud_control_xfer(rhport, request, xsm3_challenge_response, len); + if (ok) + xsm3_auth_state = Xsm3AuthState::Idle; + return ok; + } + return false; + } + return true; + + // 0x84: Host GET — keepalive, zero-length response (joypad-os) + case 0x84: + if (stage == CONTROL_STAGE_SETUP) printf("XSM3: 0x84 KEEPALIVE\n"); + if (stage == CONTROL_STAGE_SETUP) + return tud_control_xfer(rhport, request, nullptr, 0); + return true; + + // 0x86: Host GET — state 1 = processing, 2 = response ready (joypad-os) + case 0x86: + if (stage == CONTROL_STAGE_SETUP && request->wLength >= 2) + { + uint8_t state_val = (xsm3_auth_state == Xsm3AuthState::Responded || xsm3_auth_state == Xsm3AuthState::Authenticated) + ? XSM3_STATE_READY : XSM3_STATE_PROCESSING; + printf("XSM3: 0x86 GET_STATE %u\n", (unsigned)state_val); + uint8_t state_data[] = { state_val, 0x00 }; + return tud_control_xfer(rhport, request, state_data, 2); + } + return true; + + // 0x87: Host OUT — receive verify; defer xsm3_do_challenge_verify to process() (joypad-os) + case 0x87: + if (stage == CONTROL_STAGE_SETUP && request->wLength >= 0x16) + return tud_control_xfer(rhport, request, xsm3_buf_87, 0x16); + if (stage == CONTROL_STAGE_DATA) + { + printf("XSM3: 0x87 verify received (defer process)\n"); + xsm3_auth_state = Xsm3AuthState::VerifyReceived; + } + return true; + + default: + return false; + } } -const uint16_t * XInputDevice::get_descriptor_string_cb(uint8_t index, uint16_t langid) +const uint16_t * XInputDevice::get_descriptor_string_cb(uint8_t index, uint16_t langid) { + // joypad-os: XInput string index 4 (XSM3 security) uses a 96-char buffer and full length + if (index == 4) + { + static uint16_t xsm3_str[96]; + const char *str = reinterpret_cast(XInput::STRING_XSM3); + size_t len = std::strlen(str); + if (len > 95) len = 95; + for (size_t i = 0; i < len; i++) + xsm3_str[1 + i] = static_cast(str[i]); + xsm3_str[0] = (0x03u << 8) | static_cast(2 * len + 2); + return xsm3_str; + } const char *value = reinterpret_cast(XInput::DESC_STRING[index]); return get_string_descriptor(value, index); } @@ -115,9 +284,12 @@ const uint8_t * XInputDevice::get_hid_descriptor_report_cb(uint8_t itf) return nullptr; } -const uint8_t * XInputDevice::get_descriptor_configuration_cb(uint8_t index) +const uint8_t * XInputDevice::get_descriptor_configuration_cb(uint8_t index) { - return XInput::DESC_CONFIGURATION; + // joypad-os: single config; return nullptr for index != 0 (bNumConfigurations == 1) + if (index != 0) + return nullptr; + return XInput::DESC_CONFIGURATION; } const uint8_t * XInputDevice::get_descriptor_device_qualifier_cb() diff --git a/Firmware/RP2040/src/USBDevice/DeviceDriver/XInput/tud_xinput/tud_xinput.cpp b/Firmware/RP2040/src/USBDevice/DeviceDriver/XInput/tud_xinput/tud_xinput.cpp index 26f38f1d..a5322401 100644 --- a/Firmware/RP2040/src/USBDevice/DeviceDriver/XInput/tud_xinput/tud_xinput.cpp +++ b/Firmware/RP2040/src/USBDevice/DeviceDriver/XInput/tud_xinput/tud_xinput.cpp @@ -1,6 +1,7 @@ #include "tusb_option.h" #if (TUSB_OPT_DEVICE_ENABLED && CFG_TUD_XINPUT) +#include #include #include @@ -8,19 +9,20 @@ #include "device/usbd_pvt.h" #include "Descriptors/XInput.h" +#include "USBDevice/DeviceManager.h" #include "USBDevice/DeviceDriver/XInput/tud_xinput/tud_xinput.h" namespace tud_xinput { static constexpr uint16_t ENDPOINT_SIZE = 32; +static constexpr uint8_t DESC_TYPE_VENDOR = 0x21; +static constexpr uint8_t DESC_TYPE_SEC = 0x41; uint8_t endpoint_in_ = 0xFF; uint8_t endpoint_out_ = 0xFF; uint8_t ep_in_buffer_[ENDPOINT_SIZE]; uint8_t ep_out_buffer_[ENDPOINT_SIZE]; -//Class Driver - static void init(void) { endpoint_in_ = 0xFF; @@ -37,66 +39,116 @@ static bool deinit(void) static void reset(uint8_t rhport) { + (void)rhport; init(); } +static inline uint16_t tu_desc_len(uint8_t const* desc) { return desc[0]; } + +// Match joypad-os: claim all 4 interfaces, but only OPEN endpoints for gamepad (0x01). +// Audio (0x03) and plugin (0x02) endpoints are skipped (stub). Security (0xFD/0x13) has no EPs. static uint16_t open(uint8_t rhport, tusb_desc_interface_t const *itf_descriptor, uint16_t max_length) { - uint16_t driver_length = sizeof(tusb_desc_interface_t) + (itf_descriptor->bNumEndpoints * sizeof(tusb_desc_endpoint_t)) + 16; - - TU_VERIFY(max_length >= driver_length, 0); - - uint8_t const *current_descriptor = tu_desc_next(itf_descriptor); - uint8_t found_endpoints = 0; - while ((found_endpoints < itf_descriptor->bNumEndpoints) && (driver_length <= max_length)) - { - tusb_desc_endpoint_t const *endpoint_descriptor = (tusb_desc_endpoint_t const *)current_descriptor; - if (TUSB_DESC_ENDPOINT == tu_desc_type(endpoint_descriptor)) - { - TU_ASSERT(usbd_edpt_open(rhport, endpoint_descriptor)); - - if (tu_edpt_dir(endpoint_descriptor->bEndpointAddress) == TUSB_DIR_IN) - { - endpoint_in_ = endpoint_descriptor->bEndpointAddress; - } - else - { - endpoint_out_ = endpoint_descriptor->bEndpointAddress; - } - - ++found_endpoints; - } - - current_descriptor = tu_desc_next(current_descriptor); - } - return driver_length; + uint16_t drv_len = sizeof(tusb_desc_interface_t); + uint8_t const *p_desc = tu_desc_next((uint8_t const*)itf_descriptor); + + if (itf_descriptor->bInterfaceClass != 0xFF) + return 0; + + // --- Interface 0: Gamepad (0x5D/0x01) — open endpoints --- + if (itf_descriptor->bInterfaceSubClass == 0x5D && itf_descriptor->bInterfaceProtocol == 0x01) + { + if (p_desc[1] == DESC_TYPE_VENDOR) { + drv_len += tu_desc_len(p_desc); + p_desc = tu_desc_next(p_desc); + } + for (uint8_t i = 0; i < itf_descriptor->bNumEndpoints; i++) { + tusb_desc_endpoint_t const *ep_desc = (tusb_desc_endpoint_t const *)p_desc; + if (TUSB_DESC_ENDPOINT != tu_desc_type(ep_desc)) break; + TU_VERIFY(usbd_edpt_open(rhport, ep_desc), 0); + if (tu_edpt_dir(ep_desc->bEndpointAddress) == TUSB_DIR_IN) + endpoint_in_ = ep_desc->bEndpointAddress; + else + endpoint_out_ = ep_desc->bEndpointAddress; + drv_len += sizeof(tusb_desc_endpoint_t); + p_desc = tu_desc_next(p_desc); + } + if (endpoint_out_ != 0xFF) + usbd_edpt_xfer(rhport, endpoint_out_, ep_out_buffer_, ENDPOINT_SIZE); + TU_VERIFY(max_length >= drv_len, 0); + return drv_len; + } + + // --- Interface 1: Audio (0x5D/0x03) — skip endpoints (stub) --- + if (itf_descriptor->bInterfaceSubClass == 0x5D && itf_descriptor->bInterfaceProtocol == 0x03) + { + if (p_desc[1] == DESC_TYPE_VENDOR) { + drv_len += tu_desc_len(p_desc); + p_desc = tu_desc_next(p_desc); + } + for (uint8_t i = 0; i < itf_descriptor->bNumEndpoints; i++) { + drv_len += sizeof(tusb_desc_endpoint_t); + p_desc = tu_desc_next(p_desc); + } + TU_VERIFY(max_length >= drv_len, 0); + return drv_len; + } + + // --- Interface 2: Plugin (0x5D/0x02) — skip endpoint (stub) --- + if (itf_descriptor->bInterfaceSubClass == 0x5D && itf_descriptor->bInterfaceProtocol == 0x02) + { + if (p_desc[1] == DESC_TYPE_VENDOR) { + drv_len += tu_desc_len(p_desc); + p_desc = tu_desc_next(p_desc); + } + for (uint8_t i = 0; i < itf_descriptor->bNumEndpoints; i++) { + drv_len += sizeof(tusb_desc_endpoint_t); + p_desc = tu_desc_next(p_desc); + } + TU_VERIFY(max_length >= drv_len, 0); + return drv_len; + } + + // --- Interface 3: Security (0xFD/0x13) — skip 0x41 descriptor, no EPs --- + if (itf_descriptor->bInterfaceSubClass == 0xFD && itf_descriptor->bInterfaceProtocol == 0x13) + { + if (p_desc[1] == DESC_TYPE_SEC) { + drv_len += tu_desc_len(p_desc); + } + TU_VERIFY(max_length >= drv_len, 0); + return drv_len; + } + + return 0; } +// Forward interface-directed control requests (e.g. XSM3 from 360) to the device driver. +// The 360 may send XSM3 as Class requests to the security interface; TinyUSB routes those here. static bool control_xfer_cb(uint8_t rhport, uint8_t stage, tusb_control_request_t const *request) { - return true; + printf("tud_xinput ctrl bReq 0x%02x stage %u type 0x%02x wIdx 0x%04x\n", + (unsigned)request->bRequest, (unsigned)stage, (unsigned)request->bmRequestType, (unsigned)request->wIndex); + return DeviceManager::get_instance().get_driver()->vendor_control_xfer_cb(rhport, stage, request); } static bool xfer_cb(uint8_t rhport, uint8_t ep_addr, xfer_result_t result, uint32_t xferred_bytes) { - if (ep_addr == endpoint_out_) - { - usbd_edpt_xfer(BOARD_TUD_RHPORT, endpoint_out_, ep_out_buffer_, ENDPOINT_SIZE); - } - return true; + (void)result; + (void)xferred_bytes; + if (ep_addr == endpoint_out_) + usbd_edpt_xfer(rhport, endpoint_out_, ep_out_buffer_, ENDPOINT_SIZE); + return true; } -// Public API - const usbd_class_driver_t* class_driver() { static const usbd_class_driver_t tud_class_driver_ = { - #if CFG_TUSB_DEBUG >= 2 +#if CFG_TUSB_DEBUG >= 2 .name = "XINPUT", - #else +#else .name = NULL, - #endif +#endif .init = init, .deinit = deinit, .reset = reset, @@ -110,31 +162,23 @@ const usbd_class_driver_t* class_driver() bool send_report_ready() { - if (tud_ready() && - (endpoint_in_ != 0xFF) && - (!usbd_edpt_busy(BOARD_TUD_RHPORT, endpoint_in_))) - { - return true; - } - return false; + return tud_ready() && + (endpoint_in_ != 0xFF) && + (!usbd_edpt_busy(BOARD_TUD_RHPORT, endpoint_in_)); } bool receive_report_ready() { - if (tud_ready() && - (endpoint_out_ != 0xFF) && - (!usbd_edpt_busy(BOARD_TUD_RHPORT, endpoint_out_))) - { - return true; - } - return false; + return tud_ready() && + (endpoint_out_ != 0xFF) && + (!usbd_edpt_busy(BOARD_TUD_RHPORT, endpoint_out_)); } bool send_report(const uint8_t *report, uint16_t len) { if (send_report_ready()) { - std::memcpy(ep_in_buffer_, report, std::min(len, ENDPOINT_SIZE)); + std::memcpy(ep_in_buffer_, report, std::min(len, (uint16_t)ENDPOINT_SIZE)); usbd_edpt_claim(BOARD_TUD_RHPORT, endpoint_in_); usbd_edpt_xfer(BOARD_TUD_RHPORT, endpoint_in_, ep_in_buffer_, sizeof(XInput::InReport)); usbd_edpt_release(BOARD_TUD_RHPORT, endpoint_in_); @@ -151,11 +195,10 @@ bool receive_report(uint8_t *report, uint16_t len) usbd_edpt_xfer(BOARD_TUD_RHPORT, endpoint_out_, ep_out_buffer_, ENDPOINT_SIZE); usbd_edpt_release(BOARD_TUD_RHPORT, endpoint_out_); } - - std::memcpy(report, ep_out_buffer_, std::min(len, ENDPOINT_SIZE)); + std::memcpy(report, ep_out_buffer_, std::min(len, (uint16_t)ENDPOINT_SIZE)); return true; } } // namespace tud_xinput -#endif // (TUSB_OPT_DEVICE_ENABLED && CFG_TUD_XINPUT) \ No newline at end of file +#endif // (TUSB_OPT_DEVICE_ENABLED && CFG_TUD_XINPUT) diff --git a/Firmware/RP2040/src/USBDevice/DeviceDriver/XInput/tud_xinput/tud_xinput.h b/Firmware/RP2040/src/USBDevice/DeviceDriver/XInput/tud_xinput/tud_xinput.h index 74a87473..9da42428 100644 --- a/Firmware/RP2040/src/USBDevice/DeviceDriver/XInput/tud_xinput/tud_xinput.h +++ b/Firmware/RP2040/src/USBDevice/DeviceDriver/XInput/tud_xinput/tud_xinput.h @@ -6,13 +6,12 @@ #include "tusb.h" #include "device/usbd_pvt.h" -namespace tud_xinput +namespace tud_xinput { bool send_report_ready(); bool send_report(const uint8_t *report, uint16_t len); bool receive_report(uint8_t *report, uint16_t len); const usbd_class_driver_t* class_driver(); - -} // namespace tud_xinput +} #endif // _TUD_XINPUT_H_ \ No newline at end of file diff --git a/Firmware/RP2040/src/USBDevice/DeviceDriver/XboxOG/XboxOG_GP.cpp b/Firmware/RP2040/src/USBDevice/DeviceDriver/XboxOG/XboxOG_GP.cpp index eeda9c0c..9af1802b 100644 --- a/Firmware/RP2040/src/USBDevice/DeviceDriver/XboxOG/XboxOG_GP.cpp +++ b/Firmware/RP2040/src/USBDevice/DeviceDriver/XboxOG/XboxOG_GP.cpp @@ -1,25 +1,73 @@ #include -#include #include "USBDevice/DeviceDriver/XboxOG/tud_xid/tud_xid.h" #include "USBDevice/DeviceDriver/XboxOG/XboxOG_GP.h" +#include "Gamepad/Gamepad.h" +#if defined(CONFIG_OGXM_DEBUG) +#include +#include "Board/ogxm_log.h" +#endif -void XboxOGDevice::initialize() +void XboxOGDevice::initialize() { +#if defined(CONFIG_OGXM_DEBUG) + OGXM_LOG("XboxOG: initialize\n"); +#endif tud_xid::initialize(tud_xid::Type::GAMEPAD); class_driver_ = *tud_xid::class_driver(); std::memset(&in_report_, 0, sizeof(XboxOG::GP::InReport)); in_report_.report_len = sizeof(XboxOG::GP::InReport); +#if defined(CONFIG_OGXM_DEBUG) + OGXM_LOG("XboxOG: init done\n"); +#endif } void XboxOGDevice::process(const uint8_t idx, Gamepad& gamepad) { + /* Build/send only when new input (match Team-Resurgent timing). Guide/Guide+Start combos for IGR/shutdown kept. */ +#if defined(CONFIG_OGXM_DEBUG) + static uint32_t s_process_ticks = 0; + static uint32_t s_send_count = 0; + static uint32_t s_new_pad_in_count = 0; + static uint32_t s_last_new_pad_in_tick = 0; + static uint32_t s_ready_false_count = 0; // had new_pad_in but send_report_ready(0) was false + s_process_ticks++; +#endif if (gamepad.new_pad_in()) { +#if defined(CONFIG_OGXM_DEBUG) + s_new_pad_in_count++; + s_last_new_pad_in_tick = s_process_ticks; +#endif std::memset(&in_report_.buttons, 0, 8); Gamepad::PadIn gp_in = gamepad.get_pad_in(); + // Guide only = IGR (LT+RT+Start+Back). + // Shutdown = LT+RT+Back+White only (no Start on the wire — extra Start breaks many BIOSes). + // Guide+Menu (Start): works on PS5 / some pads. Xbox BT often drops Menu while Guide is held; + // use Guide+View (Back) instead on Xbox controllers. + bool guide_pressed = (gp_in.buttons & Gamepad::BUTTON_SYS) != 0; + bool start_pressed = (gp_in.buttons & Gamepad::BUTTON_START) != 0; + bool view_pressed = (gp_in.buttons & Gamepad::BUTTON_BACK) != 0; + const bool shutdown_combo = + guide_pressed && (start_pressed || view_pressed); + if (shutdown_combo) + { + gp_in.trigger_l = gamepad.scale_trigger_l(0xFF); + gp_in.trigger_r = gamepad.scale_trigger_r(0xFF); + gp_in.buttons |= Gamepad::BUTTON_BACK; + gp_in.buttons |= Gamepad::BUTTON_LB; // White + gp_in.buttons &= ~Gamepad::BUTTON_START; // chord must not include Start + } + else if (guide_pressed) + { + gp_in.trigger_l = gamepad.scale_trigger_l(0xFF); + gp_in.trigger_r = gamepad.scale_trigger_r(0xFF); + gp_in.buttons |= Gamepad::BUTTON_START; + gp_in.buttons |= Gamepad::BUTTON_BACK; + } + switch (gp_in.dpad) { case Gamepad::DPAD_UP: @@ -49,7 +97,7 @@ void XboxOGDevice::process(const uint8_t idx, Gamepad& gamepad) default: break; } - + if (gp_in.buttons & Gamepad::BUTTON_BACK) in_report_.buttons |= XboxOG::GP::Buttons::BACK; if (gp_in.buttons & Gamepad::BUTTON_START) in_report_.buttons |= XboxOG::GP::Buttons::START; if (gp_in.buttons & Gamepad::BUTTON_L3) in_report_.buttons |= XboxOG::GP::Buttons::L3; @@ -84,16 +132,51 @@ void XboxOGDevice::process(const uint8_t idx, Gamepad& gamepad) if (tud_suspended()) { +#if defined(CONFIG_OGXM_DEBUG) + OGXM_LOG("XboxOG: tud_remote_wakeup\n"); +#endif tud_remote_wakeup(); } - if (tud_xid::send_report_ready(0)) + bool ready = tud_xid::send_report_ready(0); +#if defined(CONFIG_OGXM_DEBUG) + if (ready) + s_send_count++; + else + { + s_ready_false_count++; + if (s_ready_false_count <= 3 || s_ready_false_count % 2000 == 0) + OGXM_LOG("XboxOG: new_pad_in but send_report_ready=false (count=" + std::to_string(s_ready_false_count) + ")\n"); + } +#endif + if (ready) { tud_xid::send_report(0, reinterpret_cast(&in_report_), sizeof(XboxOG::GP::InReport)); } } +#if defined(CONFIG_OGXM_DEBUG) + // Periodic status every 10000 process ticks to see why it might stop responding + if (s_process_ticks > 0 && s_process_ticks % 10000 == 0) + { + uint32_t ticks_since_input = s_process_ticks - s_last_new_pad_in_tick; + OGXM_LOG("XboxOG: tick=" + std::to_string(s_process_ticks) + + " new_pad_in=" + std::to_string(s_new_pad_in_count) + + " sent=" + std::to_string(s_send_count) + + " ready_false=" + std::to_string(s_ready_false_count) + + " ticks_since_input=" + std::to_string(ticks_since_input) + "\n"); + if (ticks_since_input > 20000) + OGXM_LOG("XboxOG: WARNING no new input for " + std::to_string(ticks_since_input) + " ticks (controller/driver may be idle or not sending)\n"); + } +#endif + if (tud_xid::receive_report(0, reinterpret_cast(&out_report_), sizeof(XboxOG::GP::OutReport))) { +#if defined(CONFIG_OGXM_DEBUG) + static uint32_t s_rumble_recv_count = 0; + s_rumble_recv_count++; + if (s_rumble_recv_count <= 3 || s_rumble_recv_count % 2000 == 0) + OGXM_LOG("XboxOG: rumble recv #" + std::to_string(s_rumble_recv_count) + "\n"); +#endif Gamepad::PadOut gp_out; gp_out.rumble_l = Scale::uint16_to_uint8(out_report_.rumble_l); gp_out.rumble_r = Scale::uint16_to_uint8(out_report_.rumble_r); @@ -101,8 +184,19 @@ void XboxOGDevice::process(const uint8_t idx, Gamepad& gamepad) } } -uint16_t XboxOGDevice::get_report_cb(uint8_t itf, uint8_t report_id, hid_report_type_t report_type, uint8_t *buffer, uint16_t reqlen) +uint16_t XboxOGDevice::get_report_cb(uint8_t itf, uint8_t report_id, hid_report_type_t report_type, uint8_t *buffer, uint16_t reqlen) { +#if defined(CONFIG_OGXM_DEBUG) + static uint32_t s_get_report_count = 0; + static uint32_t s_last_logged_get_report = 0; + s_get_report_count++; + // Log first 5, then every 2000 so we see if Xbox stops polling (get_report_cb stops being called) + if (s_get_report_count <= 5 || s_get_report_count - s_last_logged_get_report >= 2000) + { + OGXM_LOG("XboxOG: get_report_cb #" + std::to_string(s_get_report_count) + " (Xbox polling)\n"); + s_last_logged_get_report = s_get_report_count; + } +#endif std::memcpy(buffer, &in_report_, sizeof(in_report_)); return sizeof(XboxOG::GP::InReport); } diff --git a/Firmware/RP2040/src/USBDevice/DeviceDriver/XboxOG/tud_xid/tud_xid.cpp b/Firmware/RP2040/src/USBDevice/DeviceDriver/XboxOG/tud_xid/tud_xid.cpp index be40d424..3dc41984 100644 --- a/Firmware/RP2040/src/USBDevice/DeviceDriver/XboxOG/tud_xid/tud_xid.cpp +++ b/Firmware/RP2040/src/USBDevice/DeviceDriver/XboxOG/tud_xid/tud_xid.cpp @@ -12,6 +12,9 @@ #else #define XREMOTE_ENABLED 0 #endif +#if defined(CONFIG_OGXM_DEBUG) +#include "Board/ogxm_log.h" +#endif namespace tud_xid { @@ -284,6 +287,25 @@ bool xid_control_xfer_cb(uint8_t rhport, uint8_t stage, tusb_control_request_t c uint8_t index = get_idx_by_itf(static_cast(request->wIndex)); TU_VERIFY(index != 0xFF, false); +#if defined(CONFIG_OGXM_DEBUG) + { + static uint32_t s_ctl_count = 0; + s_ctl_count++; + RequestType rt = get_request_type(request); + const char* rt_str = "UNKNOWN"; + switch (rt) { + case RequestType::GET_REPORT: rt_str = "GET_REPORT"; break; + case RequestType::SET_REPORT: rt_str = "SET_REPORT"; break; + case RequestType::GET_DESC: rt_str = "GET_DESC"; break; + case RequestType::GET_CAPABILITIES_IN: rt_str = "GET_CAP_IN"; break; + case RequestType::GET_CAPABILITIES_OUT: rt_str = "GET_CAP_OUT"; break; + default: break; + } + if (s_ctl_count <= 15 || s_ctl_count % 500 == 0) + OGXM_LOG("XboxOG XID: ctrl #%lu stage=%u %s\n", (unsigned long)s_ctl_count, (unsigned)stage, rt_str); + } +#endif + Interface& interface = interfaces_[index]; bool ret = false; @@ -338,6 +360,11 @@ bool xid_control_xfer_cb(uint8_t rhport, uint8_t stage, tusb_control_request_t c request->bmRequestType, request->bRequest, request->wValue); +#if defined(CONFIG_OGXM_DEBUG) + OGXM_LOG("XboxOG XID: STALL wIndex=%02x bmReq=%02x bReq=%02x wVal=%04x\n", + (unsigned)request->wIndex, (unsigned)request->bmRequestType, + (unsigned)request->bRequest, (unsigned)request->wValue); +#endif return false; } diff --git a/Firmware/RP2040/src/USBDevice/DeviceManager.cpp b/Firmware/RP2040/src/USBDevice/DeviceManager.cpp index ef660535..4dd2304e 100644 --- a/Firmware/RP2040/src/USBDevice/DeviceManager.cpp +++ b/Firmware/RP2040/src/USBDevice/DeviceManager.cpp @@ -4,12 +4,22 @@ #include "USBDevice/DeviceDriver/PSClassic/PSClassic.h" #include "USBDevice/DeviceDriver/XInput/XInput.h" #include "USBDevice/DeviceDriver/Switch/Switch.h" +#include "USBDevice/DeviceDriver/WiiU/WiiU.h" +#include "USBDevice/DeviceDriver/Wii/Wii.h" #include "USBDevice/DeviceDriver/DInput/DInput.h" #include "USBDevice/DeviceDriver/PS3/PS3.h" +#include "USBDevice/DeviceDriver/PS4/PS4.h" +#include "USBDevice/DeviceDriver/Steam/Steam.h" +#include "USBDevice/DeviceDriver/Steam/SteamActive.h" +#include "USBDevice/DeviceDriver/MotionOutputActive.h" #include "USBDevice/DeviceDriver/XboxOG/XboxOG_GP.h" #include "USBDevice/DeviceDriver/XboxOG/XboxOG_SB.h" #include "USBDevice/DeviceDriver/XboxOG/XboxOG_XR.h" #include "USBDevice/DeviceDriver/WebApp/WebApp.h" +#include "USBDevice/DeviceDriver/PS1PS2/PS1PS2.h" +#include "USBDevice/DeviceDriver/GameCube/GameCube.h" +#include "USBDevice/DeviceDriver/Dreamcast/Dreamcast.h" +#include "USBDevice/DeviceDriver/N64/N64.h" #include "USBDevice/DeviceManager.h" #if defined(CONFIG_EN_UART_BRIDGE) @@ -19,41 +29,91 @@ void DeviceManager::initialize_driver( DeviceDriverType driver_type, Gamepad(&gamepads)[MAX_GAMEPADS]) { //TODO: Put gamepad setup in the drivers themselves - bool has_analog = false; + bool has_analog = false; + current_driver_type_ = driver_type; + SteamActive::set(driver_type == DeviceDriverType::STEAM); + MotionOutputActive::set(driver_type == DeviceDriverType::PS3 || + driver_type == DeviceDriverType::PS4); + + printf("Attempting to allocate driver\n"); switch (driver_type) { case DeviceDriverType::DINPUT: + printf("DINPUT Loaded\n"); has_analog = true; device_driver_ = std::make_unique(); break; case DeviceDriverType::PS3: + printf("PS3 Loaded\n"); has_analog = true; device_driver_ = std::make_unique(); break; + case DeviceDriverType::PS4: + printf("PS4 Loaded\n"); + has_analog = true; + device_driver_ = std::make_unique(); + break; + case DeviceDriverType::STEAM: + printf("STEAM Loaded\n"); + has_analog = true; + device_driver_ = std::make_unique(); + break; case DeviceDriverType::PSCLASSIC: + printf("PSCLASSIC Loaded\n"); device_driver_ = std::make_unique(); break; case DeviceDriverType::SWITCH: + printf("SWITCH Loaded\n"); device_driver_ = std::make_unique(); break; + case DeviceDriverType::WIIU: + printf("WIIU Loaded\n"); + device_driver_ = std::make_unique(); + break; + case DeviceDriverType::WII: + printf("WII Loaded\n"); + device_driver_ = std::make_unique(); + break; + case DeviceDriverType::PS1PS2: + printf("PS1/PS2 Loaded (GPIO output)\n"); + device_driver_ = std::make_unique(); + break; + case DeviceDriverType::GAMECUBE: + printf("GameCube Loaded (GPIO output)\n"); + device_driver_ = std::make_unique(); + break; + case DeviceDriverType::DREAMCAST: + printf("Dreamcast Loaded (GPIO Maple Bus)\n"); + device_driver_ = std::make_unique(); + break; + case DeviceDriverType::N64: + printf("N64 Loaded (GPIO output)\n"); + device_driver_ = std::make_unique(); + break; case DeviceDriverType::XINPUT: + printf("XINPUT Loaded\n"); device_driver_ = std::make_unique(); break; case DeviceDriverType::XBOXOG: + printf("XBOXOG Loaded\n"); has_analog = true; device_driver_ = std::make_unique(); break; case DeviceDriverType::XBOXOG_SB: + printf("XBOXOG SB Loaded\n"); device_driver_ = std::make_unique(); break; case DeviceDriverType::XBOXOG_XR: + printf("XBOXOG XR Loaded\n"); device_driver_ = std::make_unique(); break; case DeviceDriverType::WEBAPP: + printf("WEBAPP Loaded\n"); device_driver_ = std::make_unique(); break; #if defined(CONFIG_EN_UART_BRIDGE) case DeviceDriverType::UART_BRIDGE: + printf("UART Loaded\n"); device_driver_ = std::make_unique(); break; #endif //defined(CONFIG_EN_UART_BRIDGE) diff --git a/Firmware/RP2040/src/USBDevice/DeviceManager.h b/Firmware/RP2040/src/USBDevice/DeviceManager.h index 8cb6b5a6..237ef50b 100644 --- a/Firmware/RP2040/src/USBDevice/DeviceManager.h +++ b/Firmware/RP2040/src/USBDevice/DeviceManager.h @@ -21,12 +21,14 @@ class DeviceManager { void initialize_driver(DeviceDriverType driver_type, Gamepad(&gamepads)[MAX_GAMEPADS]); DeviceDriver* get_driver() { return device_driver_.get(); } - + DeviceDriverType get_driver_type() const { return current_driver_type_; } + private: DeviceManager() = default; ~DeviceManager() = default; std::unique_ptr device_driver_{nullptr}; + DeviceDriverType current_driver_type_{DeviceDriverType::NONE}; }; #endif // _DEVICE_MANAGER_H_ \ No newline at end of file diff --git a/Firmware/RP2040/src/USBDevice/tud_callbacks.cpp b/Firmware/RP2040/src/USBDevice/tud_callbacks.cpp index c463ff98..e929adec 100644 --- a/Firmware/RP2040/src/USBDevice/tud_callbacks.cpp +++ b/Firmware/RP2040/src/USBDevice/tud_callbacks.cpp @@ -1,13 +1,24 @@ #include +#include #include "tusb.h" #include "class/hid/hid_device.h" #include "device/usbd_pvt.h" #include "USBDevice/DeviceManager.h" +#include "USBDevice/DeviceDriver/DeviceDriverTypes.h" + +#if defined(CONFIG_EN_BLUETOOTH) && defined(CONFIG_TARGET_PICO_W) +#include "Bluepad32/Bluepad32.h" +#endif const usbd_class_driver_t *usbd_app_driver_get_cb(uint8_t *driver_count) { + DeviceDriverType dt = DeviceManager::get_instance().get_driver_type(); + if (dt == DeviceDriverType::PS1PS2 || dt == DeviceDriverType::GAMECUBE || dt == DeviceDriverType::DREAMCAST) { + *driver_count = 0; + return nullptr; + } *driver_count = 1; return DeviceManager::get_instance().get_driver()->get_class_driver(); } @@ -20,11 +31,14 @@ uint16_t tud_hid_get_report_cb(uint8_t itf, uint8_t report_id, hid_report_type_t void tud_hid_set_report_cb(uint8_t itf, uint8_t report_id, hid_report_type_t report_type, uint8_t const *buffer, uint16_t bufsize) { DeviceManager::get_instance().get_driver()->set_report_cb(itf, report_id, report_type, buffer, bufsize); - tud_hid_report(report_id, buffer, bufsize); + // Do not echo received output back as input report (e.g. Wii U GC adapter init 0x13 must only be received; + // TinyUSB re-arms the OUT endpoint after this returns so the host can keep sending 0x13). } bool tud_vendor_control_xfer_cb(uint8_t rhport, uint8_t stage, tusb_control_request_t const *request) { + printf("tud_vendor_cb bReq 0x%02x stage %u type 0x%02x wIdx 0x%04x\n", + (unsigned)request->bRequest, (unsigned)stage, (unsigned)request->bmRequestType, (unsigned)request->wIndex); return DeviceManager::get_instance().get_driver()->vendor_control_xfer_cb(rhport, stage, request); } @@ -51,4 +65,10 @@ uint8_t const *tud_descriptor_configuration_cb(uint8_t index) uint8_t const* tud_descriptor_device_qualifier_cb() { return DeviceManager::get_instance().get_driver()->get_descriptor_device_qualifier_cb(); -} \ No newline at end of file +} + +#if defined(CONFIG_EN_BLUETOOTH) && defined(CONFIG_TARGET_PICO_W) +void tud_resume_cb(void) { + bluepad32::on_usb_device_resume(); +} +#endif \ No newline at end of file diff --git a/Firmware/RP2040/src/USBHost/GPIOHost/GPIOHost.cpp b/Firmware/RP2040/src/USBHost/GPIOHost/GPIOHost.cpp new file mode 100644 index 00000000..237a2a2e --- /dev/null +++ b/Firmware/RP2040/src/USBHost/GPIOHost/GPIOHost.cpp @@ -0,0 +1,274 @@ +#include "USBHost/GPIOHost/GPIOHost.h" +#include "Gamepad/Gamepad.h" +#include "USBHost/GPIOHost/psx_host/psx_host.h" +#include "USBHost/GPIOHost/joybus_host/joybus_controller.h" +#include "USBHost/GPIOHost/joybus_host/n64_host.h" +#include "USBHost/GPIOHost/dreamcast_host/MapleBus.h" +#include "USBHost/GPIOHost/dreamcast_host/MaplePacket.hpp" +#include "USBHost/GPIOHost/dreamcast_host/dreamcast_constants.h" +#include "USBHost/GPIOHost/dreamcast_host/dreamcast_structures.h" + +#include +#include + +namespace GPIOHost { + +/* PSX report: byte0=id, 1=buttons1 (dpad + start), 2=buttons2 (face + shoulders), 3=rx, 4=ry, 5=lx, 6=ly, 7,8 */ +static constexpr uint8_t PSX_UP = 0x10; +static constexpr uint8_t PSX_RIGHT = 0x20; +static constexpr uint8_t PSX_DOWN = 0x40; +static constexpr uint8_t PSX_LEFT = 0x80; +static constexpr uint8_t PSX_START = 0x08; +static constexpr uint8_t PSX_L2 = 0x01; +static constexpr uint8_t PSX_R2 = 0x02; +static constexpr uint8_t PSX_L1 = 0x04; +static constexpr uint8_t PSX_R1 = 0x08; +static constexpr uint8_t PSX_TRI = 0x10; +static constexpr uint8_t PSX_CIR = 0x20; +static constexpr uint8_t PSX_X = 0x40; +static constexpr uint8_t PSX_SQU = 0x80; + +static inline int16_t psx_stick_to_int16(uint8_t b) +{ + int32_t v = (int32_t)(b & 0xFFu) - 128; + if (v < -127) v = -127; + if (v > 127) v = 127; + return (int16_t)(v * 32767 / 127); +} + +void psx_host_init(unsigned pio_index, unsigned gpio_cmd, unsigned gpio_data) +{ + ::psx_host_init(pio_index, gpio_cmd, gpio_data); +} + +void psx_host_poll(Gamepad& gamepad) +{ + uint8_t buf[9]; + if (!::psx_host_take_data(buf)) + { + return; + } + Gamepad::PadIn pad_in; + std::memset(&pad_in, 0, sizeof(pad_in)); + + uint8_t b1 = buf[1]; + uint8_t b2 = buf[2]; + if (b1 & PSX_UP) pad_in.dpad |= Gamepad::DPAD_UP; + if (b1 & PSX_DOWN) pad_in.dpad |= Gamepad::DPAD_DOWN; + if (b1 & PSX_LEFT) pad_in.dpad |= Gamepad::DPAD_LEFT; + if (b1 & PSX_RIGHT) pad_in.dpad |= Gamepad::DPAD_RIGHT; + if (b1 & PSX_START) pad_in.buttons |= Gamepad::BUTTON_START; + if (b2 & PSX_X) pad_in.buttons |= Gamepad::BUTTON_A; + if (b2 & PSX_CIR) pad_in.buttons |= Gamepad::BUTTON_B; + if (b2 & PSX_TRI) pad_in.buttons |= Gamepad::BUTTON_X; + if (b2 & PSX_SQU) pad_in.buttons |= Gamepad::BUTTON_Y; + if (b2 & PSX_L1) pad_in.buttons |= Gamepad::BUTTON_LB; + if (b2 & PSX_R1) pad_in.buttons |= Gamepad::BUTTON_RB; + pad_in.trigger_l = (b2 & PSX_L2) ? 0xFF : 0; + pad_in.trigger_r = (b2 & PSX_R2) ? 0xFF : 0; + + pad_in.joystick_rx = psx_stick_to_int16(buf[3]); + pad_in.joystick_ry = psx_stick_to_int16(buf[4]); + pad_in.joystick_lx = psx_stick_to_int16(buf[5]); + pad_in.joystick_ly = psx_stick_to_int16(buf[6]); + + gamepad.set_pad_in(pad_in); +} + +/* GameCube report: buttons1, buttons2, lx, ly, rx, ry, l, r (same as GCReport). */ +static constexpr uint8_t GC_A = 0x01; +static constexpr uint8_t GC_B = 0x02; +static constexpr uint8_t GC_X = 0x04; +static constexpr uint8_t GC_Y = 0x08; +static constexpr uint8_t GC_START = 0x10; +static constexpr uint8_t GC_DPAD_LEFT = 0x01; +static constexpr uint8_t GC_DPAD_RIGHT = 0x02; +static constexpr uint8_t GC_DPAD_DOWN = 0x04; +static constexpr uint8_t GC_DPAD_UP = 0x08; +static constexpr uint8_t GC_Z = 0x10; +static constexpr uint8_t GC_R = 0x20; +static constexpr uint8_t GC_L = 0x40; +static constexpr uint8_t STICK_CENTER = 0x80; + +static int16_t gc_stick_to_int16(uint8_t b) +{ + int32_t v = (int32_t)(b & 0xFFu) - (int32_t)STICK_CENTER; + if (v < -127) v = -127; + if (v > 127) v = 127; + return (int16_t)(v * 32767 / 127); +} + +void joybus_host_init(unsigned pio_index, unsigned data_pin) +{ + ::joybus_host_init(pio_index, data_pin); +} + +void joybus_host_poll(Gamepad& gamepad) +{ + static const uint8_t poll_cmd[] = { 0x40, 0x03 }; + ::joybus_send_data(poll_cmd, 2, 8); + const uint8_t *r = ::joybus_get_response(); + + Gamepad::PadIn pad_in; + std::memset(&pad_in, 0, sizeof(pad_in)); + + uint8_t b1 = r[0]; + uint8_t b2 = r[1]; + if (b2 & GC_DPAD_UP) pad_in.dpad |= Gamepad::DPAD_UP; + if (b2 & GC_DPAD_DOWN) pad_in.dpad |= Gamepad::DPAD_DOWN; + if (b2 & GC_DPAD_LEFT) pad_in.dpad |= Gamepad::DPAD_LEFT; + if (b2 & GC_DPAD_RIGHT) pad_in.dpad |= Gamepad::DPAD_RIGHT; + if (b1 & GC_START) pad_in.buttons |= Gamepad::BUTTON_START; + if (b1 & GC_A) pad_in.buttons |= Gamepad::BUTTON_A; + if (b1 & GC_B) pad_in.buttons |= Gamepad::BUTTON_B; + if (b1 & GC_X) pad_in.buttons |= Gamepad::BUTTON_X; + if (b1 & GC_Y) pad_in.buttons |= Gamepad::BUTTON_Y; + if (b2 & GC_L) pad_in.buttons |= Gamepad::BUTTON_LB; + if (b2 & GC_R) pad_in.buttons |= Gamepad::BUTTON_RB; + if (b2 & GC_Z) pad_in.buttons |= Gamepad::BUTTON_MISC; + pad_in.trigger_l = r[6]; + pad_in.trigger_r = r[7]; + + pad_in.joystick_lx = gc_stick_to_int16(r[2]); + pad_in.joystick_ly = gc_stick_to_int16(r[3]); + pad_in.joystick_rx = gc_stick_to_int16(r[4]); + pad_in.joystick_ry = gc_stick_to_int16(r[5]); + + gamepad.set_pad_in(pad_in); +} + +/* N64 report: 4 bytes from PicoGamepadConverter layout. byte0: A=0x80, B=0x40, Z=0x20, START=0x10, D-pad low nibble. */ +/* byte1: R=0x10, L=0x20, C buttons low nibble (C_UP=8, C_DOWN=4, C_LEFT=2, C_RIGHT=1). byte2/3: joy X, Y (0x80 center). */ +static constexpr uint8_t N64_A = 0x80; +static constexpr uint8_t N64_B = 0x40; +static constexpr uint8_t N64_Z = 0x20; +static constexpr uint8_t N64_START = 0x10; +static constexpr uint8_t N64_DPAD = 0x0F; +static constexpr uint8_t N64_R = 0x10; +static constexpr uint8_t N64_L = 0x20; +static constexpr uint8_t N64_C = 0x0F; +static constexpr uint8_t N64_C_UP = 0x08; +static constexpr uint8_t N64_C_DOWN = 0x04; +static constexpr uint8_t N64_C_LEFT = 0x02; +static constexpr uint8_t N64_C_RIGHT = 0x01; +static constexpr uint8_t N64_JOY_CENTER = 0x80; + +static int16_t n64_stick_to_int16(uint8_t b) +{ + int32_t v = (int32_t)(b & 0xFFu) - (int32_t)N64_JOY_CENTER; + if (v < -127) v = -127; + if (v > 127) v = 127; + return (int16_t)(v * 32767 / 127); +} + +void n64_host_init(unsigned pio_index, unsigned data_pin) +{ + ::n64_host_init(pio_index, data_pin); +} + +void n64_host_poll(Gamepad& gamepad) +{ + ::n64_host_poll(); + const uint8_t* r = ::n64_host_get_response(); + + Gamepad::PadIn pad_in; + std::memset(&pad_in, 0, sizeof(pad_in)); + + uint8_t b0 = r[0]; + uint8_t b1 = r[1]; + uint8_t dpad_nibble = b0 & N64_DPAD; + if (dpad_nibble == 0x08 || dpad_nibble == 0x09 || dpad_nibble == 0x0A) pad_in.dpad |= Gamepad::DPAD_UP; + if (dpad_nibble == 0x04 || dpad_nibble == 0x05 || dpad_nibble == 0x06) pad_in.dpad |= Gamepad::DPAD_DOWN; + if (dpad_nibble == 0x02 || dpad_nibble == 0x06 || dpad_nibble == 0x0A) pad_in.dpad |= Gamepad::DPAD_LEFT; + if (dpad_nibble == 0x01 || dpad_nibble == 0x05 || dpad_nibble == 0x09) pad_in.dpad |= Gamepad::DPAD_RIGHT; + + if (b0 & N64_START) pad_in.buttons |= Gamepad::BUTTON_START; + if (b0 & N64_A) pad_in.buttons |= Gamepad::BUTTON_A; + if (b0 & N64_B) pad_in.buttons |= Gamepad::BUTTON_B; + if (b0 & N64_Z) pad_in.buttons |= Gamepad::BUTTON_MISC; + if (b1 & N64_L) pad_in.buttons |= Gamepad::BUTTON_LB; + if (b1 & N64_R) pad_in.buttons |= Gamepad::BUTTON_RB; + /* C buttons -> right stick (diagonals combine) */ + uint8_t c_nibble = b1 & N64_C; + pad_in.joystick_rx = ((c_nibble & N64_C_RIGHT) ? 32767 : 0) - ((c_nibble & N64_C_LEFT) ? 32767 : 0); + pad_in.joystick_ry = ((c_nibble & N64_C_DOWN) ? 32767 : 0) - ((c_nibble & N64_C_UP) ? 32767 : 0); + + pad_in.joystick_lx = n64_stick_to_int16(r[2]); + pad_in.joystick_ly = n64_stick_to_int16(r[3]); + + gamepad.set_pad_in(pad_in); +} + +static std::unique_ptr s_dreamcast_bus; + +void dreamcast_host_init(unsigned /* pio_in_index */, unsigned /* pio_out_index */, unsigned pin_a, int dir_pin) { + s_dreamcast_bus = std::make_unique(pin_a, dir_pin, true); +} + +static void controller_condition_to_pad_in(const controller_condition_t& cond, Gamepad::PadIn& pad) { + std::memset(&pad, 0, sizeof(pad)); + /* Dreamcast: 0 = pressed. PadIn: bit set = pressed */ + if (!cond.up) pad.dpad |= Gamepad::DPAD_UP; + if (!cond.down) pad.dpad |= Gamepad::DPAD_DOWN; + if (!cond.left) pad.dpad |= Gamepad::DPAD_LEFT; + if (!cond.right) pad.dpad |= Gamepad::DPAD_RIGHT; + if (!cond.a) pad.buttons |= Gamepad::BUTTON_A; + if (!cond.b) pad.buttons |= Gamepad::BUTTON_B; + if (!cond.x) pad.buttons |= Gamepad::BUTTON_X; + if (!cond.y) pad.buttons |= Gamepad::BUTTON_Y; + if (!cond.start) pad.buttons |= Gamepad::BUTTON_START; + if (!cond.c) pad.buttons |= Gamepad::BUTTON_RB; + if (!cond.z) pad.buttons |= Gamepad::BUTTON_LB; + if (!cond.d) pad.buttons |= Gamepad::BUTTON_MISC; + pad.trigger_l = cond.l; + pad.trigger_r = cond.r; + /* Sticks: 0–255, 128 center -> int16 -32767..32767 */ + auto stick8_to_16 = [](uint8_t v) -> int16_t { + int32_t x = (int32_t)(v & 0xFFu) - 128; + if (x < -127) x = -127; + if (x > 127) x = 127; + return (int16_t)(x * 32767 / 127); + }; + pad.joystick_lx = stick8_to_16(cond.lAnalogLR); + pad.joystick_ly = stick8_to_16(cond.lAnalogUD); + pad.joystick_rx = stick8_to_16(cond.rAnalogLR); + pad.joystick_ry = stick8_to_16(cond.rAnalogUD); +} + +void dreamcast_host_poll(Gamepad& gamepad) { + if (!s_dreamcast_bus) return; + MapleBus& bus = *s_dreamcast_bus; + if (bus.isBusy()) { + MapleBus::Status st = bus.processEvents(time_us_64()); + (void)st; + return; + } + MaplePacket req; + req.setFrame(COMMAND_GET_CONDITION, MAPLE_CTRL_ADDR, MAPLE_HOST_ADDR, 0); + req.payloadCount = 0; + req.payloadByteOrder = MaplePacket::ByteOrder::HOST; + if (!bus.write(req, true, MAPLE_RESPONSE_TIMEOUT_US, MaplePacket::ByteOrder::HOST)) + return; + uint64_t deadline = time_us_64() + MAPLE_RESPONSE_TIMEOUT_US + 500; + while (time_us_64() < deadline) { + MapleBus::Status st = bus.processEvents(time_us_64()); + if (st.phase == MapleBus::Phase::READ_COMPLETE && st.readBuffer && st.readBufferLen >= 3) { + uint32_t frame = st.readBuffer[0]; + uint8_t cmd = MaplePacket::Frame::getFrameCommand(frame, st.rxByteOrder); + if (cmd == COMMAND_RESPONSE_DATA_XFER) { + const uint32_t* pl = st.readBuffer + 1; + controller_condition_t cond; + memcpy(&cond, pl + 1, sizeof(cond)); /* skip function code word */ + Gamepad::PadIn pad; + controller_condition_to_pad_in(cond, pad); + gamepad.set_pad_in(pad); + } + return; + } + if (st.phase == MapleBus::Phase::READ_FAILED || st.phase == MapleBus::Phase::WRITE_FAILED) + return; + busy_wait_us(50); + } +} + +} // namespace GPIOHost diff --git a/Firmware/RP2040/src/USBHost/GPIOHost/GPIOHost.h b/Firmware/RP2040/src/USBHost/GPIOHost/GPIOHost.h new file mode 100644 index 00000000..b77e9765 --- /dev/null +++ b/Firmware/RP2040/src/USBHost/GPIOHost/GPIOHost.h @@ -0,0 +1,32 @@ +#ifndef USBHOST_GPIOHOST_GPIOHOST_H_ +#define USBHOST_GPIOHOST_GPIOHOST_H_ + +class Gamepad; + +namespace GPIOHost { + +/** Initialize PSX host. CMD pin = gpio_cmd (CLK=+1, ATT=+2), DATA=gpio_data. Call when input source is PSX_GPIO. */ +void psx_host_init(unsigned pio_index, unsigned gpio_cmd, unsigned gpio_data); + +/** Poll PSX controller and update gamepad if new data. Call from main loop when input is PSX_GPIO. */ +void psx_host_poll(Gamepad& gamepad); + +/** Initialize GameCube JoyBus host. data_pin = single wire. Call when input source is GAMECUBE_GPIO. */ +void joybus_host_init(unsigned pio_index, unsigned data_pin); + +/** Poll GameCube controller and update gamepad. Call from main loop when input is GAMECUBE_GPIO. */ +void joybus_host_poll(Gamepad& gamepad); + +/** Dreamcast Maple Bus host: init (stub until DreamPicoPort is ported). Pin A = first of two consecutive GPIOs. */ +void dreamcast_host_init(unsigned pio_in_index, unsigned pio_out_index, unsigned pin_a, int dir_pin); +/** Poll Dreamcast controller and update gamepad (stub until port). */ +void dreamcast_host_poll(Gamepad& gamepad); + +/** Initialize N64 controller host (1-wire data pin). Call when input source is N64_GPIO. */ +void n64_host_init(unsigned pio_index, unsigned data_pin); +/** Poll N64 controller and update gamepad. Call from main loop when input is N64_GPIO. */ +void n64_host_poll(Gamepad& gamepad); + +} // namespace GPIOHost + +#endif /* USBHOST_GPIOHOST_GPIOHOST_H_ */ diff --git a/Firmware/RP2040/src/USBHost/GPIOHost/dreamcast_host/MapleBus.cpp b/Firmware/RP2040/src/USBHost/GPIOHost/dreamcast_host/MapleBus.cpp new file mode 100644 index 00000000..70b0a6f2 --- /dev/null +++ b/Firmware/RP2040/src/USBHost/GPIOHost/dreamcast_host/MapleBus.cpp @@ -0,0 +1,349 @@ +/* Maple Bus host implementation. Ported from DreamPicoPort (OrangeFox86). */ + +#include "MapleBus.h" +#include "maple_config.h" +#include "maple_utils.h" +#include +#include +#include +#include +#include + +#define MAPLE_OUT_PIO pio0 +#define MAPLE_IN_PIO pio1 + +static MapleBus* s_bus_for_write_irq = nullptr; +static MapleBus* s_bus_for_read_irq = nullptr; + +static void maple_write_irq_handler(void) { + if (s_bus_for_write_irq) s_bus_for_write_irq->writeIsr(); +} +static void maple_read_irq_handler(void) { + if (s_bus_for_read_irq) s_bus_for_read_irq->readIsr(); +} + +MapleBus::MapleBus(uint32_t pinA, int32_t dirPin, bool dirOutHigh) + : mPinA(pinA) + , mPinB(pinA + 1) + , mDirPin(dirPin) + , mDirOutHigh(dirOutHigh) + , mMaskA(1u << pinA) + , mMaskB(1u << (pinA + 1)) + , mMaskAB(mMaskA | mMaskB) + , mOutOffset(pio_add_program(MAPLE_OUT_PIO, &maple_out_program)) + , mInOffset(pio_add_program(MAPLE_IN_PIO, &maple_in_program)) + , mSmOut(pio_claim_unused_sm(MAPLE_OUT_PIO, true)) + , mSmIn(pio_claim_unused_sm(MAPLE_IN_PIO, true)) + , mDmaWriteChannel(dma_claim_unused_channel(true)) + , mDmaReadChannel(dma_claim_unused_channel(true)) + , mCurrentPhase(Phase::IDLE) + , mExpectingResponse(false) + , mResponseTimeoutUs(MAPLE_RESPONSE_TIMEOUT_US) + , mProcKillTime(UINT64_MAX) + , mLastReceivedWordTimeUs(0) + , mLastReadTransferCount(0) +{ + /* Out SM: clock from MAPLE_NS_PER_BIT and maple_out DOUBLE_PHASE_TICKS */ + pio_sm_config cOut = maple_out_program_get_default_config(mOutOffset); + sm_config_set_sideset_pins(&cOut, mPinA); + sm_config_set_set_pins(&cOut, mPinA, 2); + sm_config_set_out_shift(&cOut, false, true, 32); + float div = (MAPLE_CPU_FREQ_KHZ * (MAPLE_NS_PER_BIT / 3.0f * 2.0f)) / maple_out_DOUBLE_PHASE_TICKS / 1000000.0f; + sm_config_set_clkdiv(&cOut, div); + pio_sm_init(MAPLE_OUT_PIO, mSmOut, mOutOffset, &cOut); + + /* In SM */ + pio_sm_config cIn = maple_in_program_get_default_config(mInOffset); + sm_config_set_in_pins(&cIn, mPinA); + sm_config_set_jmp_pin(&cIn, mPinA); + sm_config_set_out_shift(&cIn, true, false, 32); + sm_config_set_in_shift(&cIn, false, true, 32); + sm_config_set_clkdiv(&cIn, 1.0f); + pio_sm_init(MAPLE_IN_PIO, mSmIn, mInOffset, &cIn); + + gpio_set_dir_in_masked(mMaskAB); + gpio_set_pulls(mPinA, true, false); + gpio_set_pulls(mPinB, true, false); + + if (mDirPin >= 0) { + gpio_init((uint)mDirPin); + setDirection(false); + gpio_set_dir((uint)mDirPin, true); + } + + /* One bus per host: register for IRQ */ + if (!s_bus_for_write_irq) { + s_bus_for_write_irq = this; + irq_set_exclusive_handler(PIO0_IRQ_0, maple_write_irq_handler); + irq_set_enabled(PIO0_IRQ_0, true); + pio_set_irq0_source_enabled(MAPLE_OUT_PIO, pis_interrupt0, true); + } + if (!s_bus_for_read_irq) { + s_bus_for_read_irq = this; + irq_set_exclusive_handler(PIO1_IRQ_0, maple_read_irq_handler); + irq_set_enabled(PIO1_IRQ_0, true); + pio_set_irq0_source_enabled(MAPLE_IN_PIO, pis_interrupt0, true); + } + + /* DMA write: memory -> PIO TX FIFO, bswap */ + dma_channel_config dc = dma_channel_get_default_config(mDmaWriteChannel); + channel_config_set_read_increment(&dc, true); + channel_config_set_write_increment(&dc, false); + channel_config_set_bswap(&dc, true); + channel_config_set_dreq(&dc, pio_get_dreq(MAPLE_OUT_PIO, mSmOut, true)); + dma_channel_configure(mDmaWriteChannel, &dc, + &MAPLE_OUT_PIO->txf[mSmOut], mWriteBuffer, 1, false); + + /* DMA read: PIO RX FIFO -> memory, bswap */ + dma_channel_config dr = dma_channel_get_default_config(mDmaReadChannel); + channel_config_set_read_increment(&dr, false); + channel_config_set_write_increment(&dr, true); + channel_config_set_bswap(&dr, true); + channel_config_set_dreq(&dr, pio_get_dreq(MAPLE_IN_PIO, mSmIn, false)); + dma_channel_configure(mDmaReadChannel, &dr, + mReadBuffer, &MAPLE_IN_PIO->rxf[mSmIn], sizeof(mReadBuffer) / sizeof(mReadBuffer[0]), false); +} + +static void crc8_word(uint32_t word, uint8_t& crc) { + const uint8_t* p = (const uint8_t*)&word; + for (unsigned i = 0; i < 4; i++) crc ^= p[i]; +} +static void crc8_words(const uint32_t* src, uint32_t len, uint8_t& crc) { + for (; len; len--, src++) crc8_word(*src, crc); +} + +bool MapleBus::lineCheck() { +#if (MAPLE_OPEN_LINE_CHECK_TIME_US > 0) + uint64_t end = time_us_64() + MAPLE_OPEN_LINE_CHECK_TIME_US + 1; + while (time_us_64() < end) { + if ((gpio_get_all() & mMaskAB) != mMaskAB) return false; + } +#endif + return true; +} + +void MapleBus::setDirection(bool output) { + if (!output) { + gpio_set_dir_in_masked(mMaskAB); + gpio_set_function(mPinB, GPIO_FUNC_SIO); + gpio_set_function(mPinA, GPIO_FUNC_SIO); + } + if (mDirPin >= 0) + gpio_put((uint)mDirPin, mDirOutHigh ? output : !output); +} + +void MapleBus::writeIsr() { + pio_interrupt_clear(MAPLE_OUT_PIO, 1u); + pio_sm_set_enabled(MAPLE_OUT_PIO, mSmOut, false); + if (!mExpectingResponse) { + gpio_set_mask(mMaskAB); + gpio_set_dir_out_masked(mMaskAB); + } else { + gpio_set_pulls(mPinB, false, true); + } + gpio_set_dir_in_masked(mMaskAB); + gpio_set_function(mPinB, GPIO_FUNC_SIO); + gpio_set_function(mPinA, GPIO_FUNC_SIO); + + if (mExpectingResponse) { + pio_sm_clear_fifos(MAPLE_IN_PIO, mSmIn); + pio_sm_restart(MAPLE_IN_PIO, mSmIn); + pio_sm_clkdiv_restart(MAPLE_IN_PIO, mSmIn); + pio_sm_exec(MAPLE_IN_PIO, mSmIn, pio_encode_jmp(mInOffset)); + pio_sm_set_consecutive_pindirs(MAPLE_IN_PIO, mSmIn, mPinA, 2, false); + pio_gpio_init(MAPLE_IN_PIO, mPinA); + pio_gpio_init(MAPLE_IN_PIO, mPinB); + pio_sm_set_enabled(MAPLE_IN_PIO, mSmIn, true); + setDirection(false); + gpio_set_pulls(mPinB, true, false); + mProcKillTime = (mResponseTimeoutUs == UINT64_MAX) ? UINT64_MAX : (time_us_64() + mResponseTimeoutUs); + mCurrentPhase = Phase::WAITING_FOR_READ_START; + } else { + setDirection(false); + mCurrentPhase = Phase::WRITE_COMPLETE; + } +} + +void MapleBus::readIsr() { + pio_interrupt_clear(MAPLE_IN_PIO, 1u); + if (mCurrentPhase == Phase::WAITING_FOR_READ_START) { + mCurrentPhase = Phase::READ_IN_PROGRESS; + mLastReceivedWordTimeUs = time_us_64(); + } else if (mCurrentPhase == Phase::READ_IN_PROGRESS) { + pio_sm_set_enabled(MAPLE_IN_PIO, mSmIn, false); + gpio_set_dir_in_masked(mMaskAB); + gpio_set_function(mPinB, GPIO_FUNC_SIO); + gpio_set_function(mPinA, GPIO_FUNC_SIO); + mCurrentPhase = Phase::READ_COMPLETE; + } +} + +bool MapleBus::write(const MaplePacket& packet, bool autostartRead, uint64_t readTimeoutUs, MaplePacket::ByteOrder rxByteOrder) { + if (mCurrentPhase != Phase::IDLE) return false; + + dma_channel_abort(mDmaWriteChannel); + dma_channel_abort(mDmaReadChannel); + + uint8_t crc = 0; + uint32_t frameWord = packet.getFrameWord(); + crc8_word(frameWord, crc); + for (unsigned i = 0; i < packet.payloadCount; i++) crc8_word(packet.payload[i], crc); + + bool flipBytes = (packet.payloadByteOrder != MaplePacket::ByteOrder::NETWORK); + uint32_t totalBits = MaplePacket::getNumTotalBits((uint32_t)packet.payloadCount); + unsigned len = 0; + mWriteBuffer[len++] = flipBytes ? MaplePacket::flipWordBytes(totalBits) : totalBits; + mWriteBuffer[len++] = frameWord; + for (unsigned i = 0; i < packet.payloadCount; i++) + mWriteBuffer[len++] = packet.payload[i]; + mWriteBuffer[len++] = flipBytes ? (uint32_t)crc : ((uint32_t)crc << 24); + + if (!lineCheck()) return false; + + mExpectingResponse = autostartRead; + mResponseTimeoutUs = readTimeoutUs; + mRxByteOrder = rxByteOrder; + mCurrentPhase = Phase::WRITE_IN_PROGRESS; + + if (autostartRead) { + mLastReadTransferCount = sizeof(mReadBuffer) / sizeof(mReadBuffer[0]); + dma_channel_transfer_to_buffer_now(mDmaReadChannel, mReadBuffer, mLastReadTransferCount); + pio_sm_clear_fifos(MAPLE_IN_PIO, mSmIn); + pio_sm_restart(MAPLE_IN_PIO, mSmIn); + pio_sm_clkdiv_restart(MAPLE_IN_PIO, mSmIn); + pio_sm_exec(MAPLE_IN_PIO, mSmIn, pio_encode_jmp(mInOffset)); + pio_sm_set_consecutive_pindirs(MAPLE_IN_PIO, mSmIn, mPinA, 2, false); + } + + setDirection(true); + pio_sm_clear_fifos(MAPLE_OUT_PIO, mSmOut); + pio_sm_restart(MAPLE_OUT_PIO, mSmOut); + pio_sm_clkdiv_restart(MAPLE_OUT_PIO, mSmOut); + pio_sm_exec(MAPLE_OUT_PIO, mSmOut, pio_encode_jmp(mOutOffset)); + pio_sm_set_consecutive_pindirs(MAPLE_OUT_PIO, mSmOut, mPinA, 2, false); + pio_gpio_init(MAPLE_OUT_PIO, mPinA); + pio_gpio_init(MAPLE_OUT_PIO, mPinB); + pio_sm_set_enabled(MAPLE_OUT_PIO, mSmOut, true); + dma_channel_transfer_from_buffer_now(mDmaWriteChannel, mWriteBuffer, len); + + uint32_t txNs = MaplePacket::getTxTimeNs((uint32_t)packet.payloadCount, MAPLE_NS_PER_BIT); + txNs = txNs * (100 + MAPLE_WRITE_TIMEOUT_EXTRA_PERCENT) / 100; + mProcKillTime = time_us_64() + MAPLE_INT_DIVIDE_CEILING(txNs, 1000); + return true; +} + +bool MapleBus::startRead(uint64_t readTimeoutUs, MaplePacket::ByteOrder rxByteOrder) { + if (mCurrentPhase != Phase::IDLE) return false; + dma_channel_abort(mDmaWriteChannel); + dma_channel_abort(mDmaReadChannel); + mRxByteOrder = rxByteOrder; + bool flipBytes = (rxByteOrder != MaplePacket::ByteOrder::NETWORK); + dma_channel_config c = dma_channel_get_default_config(mDmaReadChannel); + channel_config_set_read_increment(&c, false); + channel_config_set_write_increment(&c, true); + channel_config_set_bswap(&c, flipBytes); + channel_config_set_dreq(&c, pio_get_dreq(MAPLE_IN_PIO, mSmIn, false)); + dma_channel_configure(mDmaReadChannel, &c, mReadBuffer, &MAPLE_IN_PIO->rxf[mSmIn], + sizeof(mReadBuffer) / sizeof(mReadBuffer[0]), false); + mLastReadTransferCount = sizeof(mReadBuffer) / sizeof(mReadBuffer[0]); + dma_channel_transfer_to_buffer_now(mDmaReadChannel, mReadBuffer, mLastReadTransferCount); + mProcKillTime = (readTimeoutUs == 0) ? UINT64_MAX : (time_us_64() + readTimeoutUs); + mCurrentPhase = Phase::WAITING_FOR_READ_START; + setDirection(false); + pio_sm_clear_fifos(MAPLE_IN_PIO, mSmIn); + pio_sm_restart(MAPLE_IN_PIO, mSmIn); + pio_sm_clkdiv_restart(MAPLE_IN_PIO, mSmIn); + pio_sm_exec(MAPLE_IN_PIO, mSmIn, pio_encode_jmp(mInOffset)); + pio_sm_set_consecutive_pindirs(MAPLE_IN_PIO, mSmIn, mPinA, 2, false); + pio_gpio_init(MAPLE_IN_PIO, mPinA); + pio_gpio_init(MAPLE_IN_PIO, mPinB); + pio_sm_set_enabled(MAPLE_IN_PIO, mSmIn, true); + return true; +} + +MapleBus::Status MapleBus::processEvents(uint64_t currentTimeUs) { + Status st; + st.phase = mCurrentPhase; + st.readBuffer = nullptr; + st.readBufferLen = 0; + + if (st.phase == Phase::READ_COMPLETE) { + uint64_t t0 = time_us_64(); + while (!pio_sm_is_rx_fifo_empty(MAPLE_IN_PIO, mSmIn) && (time_us_64() - t0 < 1000)) + tight_loop_contents(); + uint32_t total = sizeof(mReadBuffer) / sizeof(mReadBuffer[0]); + uint32_t left = dma_channel_hw_addr(mDmaReadChannel)->transfer_count; + uint32_t dmaWordsRead = total - left; + + if (dmaWordsRead > 1) { + uint8_t len = (mRxByteOrder != MaplePacket::ByteOrder::NETWORK) + ? (mReadBuffer[0] & 0xFF) : (mReadBuffer[0] >> 24); + uint8_t expectedCrc = (mRxByteOrder != MaplePacket::ByteOrder::NETWORK) + ? (mReadBuffer[dmaWordsRead - 1] & 0xFF) : (mReadBuffer[dmaWordsRead - 1] >> 24); + + if (len <= dmaWordsRead - 2) { + for (uint32_t i = 0; i < dmaWordsRead - 1; i++) mLastRead[i] = mReadBuffer[i]; + uint8_t crc = 0; + crc8_words(mLastRead, dmaWordsRead - 1, crc); + if (crc == expectedCrc) { + st.readBuffer = mLastRead; + st.readBufferLen = dmaWordsRead - 1; + st.rxByteOrder = mRxByteOrder; + } else { + st.phase = Phase::READ_FAILED; + st.failureReason = FailureReason::CRC_INVALID; + } + } else { + st.phase = Phase::READ_FAILED; + st.failureReason = FailureReason::MISSING_DATA; + } + } else { + st.phase = Phase::READ_FAILED; + st.failureReason = FailureReason::MISSING_DATA; + } + mCurrentPhase = Phase::IDLE; + return st; + } + + if (st.phase == Phase::WRITE_COMPLETE) { + mCurrentPhase = Phase::IDLE; + return st; + } + + if (st.phase == Phase::READ_IN_PROGRESS) { + uint32_t tc = dma_channel_hw_addr(mDmaReadChannel)->transfer_count; + if (tc == 0) { + st.phase = Phase::READ_FAILED; + st.failureReason = FailureReason::BUFFER_OVERFLOW; + mCurrentPhase = Phase::IDLE; + } else if (tc == mLastReadTransferCount) { + if (currentTimeUs > mLastReceivedWordTimeUs && + (currentTimeUs - mLastReceivedWordTimeUs) >= MAPLE_INTER_WORD_READ_TIMEOUT_US) { + pio_sm_set_enabled(MAPLE_IN_PIO, mSmIn, false); + st.phase = Phase::READ_FAILED; + st.failureReason = FailureReason::TIMEOUT; + mCurrentPhase = Phase::IDLE; + } + } else { + mLastReadTransferCount = tc; + mLastReceivedWordTimeUs = currentTimeUs; + } + return st; + } + + if (st.phase != Phase::IDLE && currentTimeUs >= mProcKillTime) { + if (st.phase == Phase::WAITING_FOR_READ_START) { + pio_sm_set_enabled(MAPLE_IN_PIO, mSmIn, false); + st.phase = Phase::READ_FAILED; + st.failureReason = FailureReason::TIMEOUT; + } else { + pio_sm_set_enabled(MAPLE_OUT_PIO, mSmOut, false); + pio_sm_set_enabled(MAPLE_IN_PIO, mSmIn, false); + setDirection(false); + st.phase = Phase::WRITE_FAILED; + st.failureReason = FailureReason::TIMEOUT; + } + mCurrentPhase = Phase::IDLE; + } + return st; +} diff --git a/Firmware/RP2040/src/USBHost/GPIOHost/dreamcast_host/MapleBus.h b/Firmware/RP2040/src/USBHost/GPIOHost/dreamcast_host/MapleBus.h new file mode 100644 index 00000000..786d0117 --- /dev/null +++ b/Firmware/RP2040/src/USBHost/GPIOHost/dreamcast_host/MapleBus.h @@ -0,0 +1,87 @@ +/* Maple Bus host. Ported from DreamPicoPort. */ + +#pragma once + +#include "MaplePacket.hpp" +#include "maple_config.h" +#include "hardware/pio.h" +#include "hardware/dma.h" +#include + +/* Generated by pico_generate_pio_header */ +#include "maple_in.pio.h" +#include "maple_out.pio.h" + +class MapleBus { +public: + enum class Phase : uint8_t { + IDLE = 0, + WRITE_IN_PROGRESS, + WRITE_FAILED, + WRITE_COMPLETE, + WAITING_FOR_READ_START, + READ_IN_PROGRESS, + READ_FAILED, + READ_COMPLETE, + }; + enum class FailureReason : uint8_t { + NONE = 0, + CRC_INVALID, + MISSING_DATA, + BUFFER_OVERFLOW, + TIMEOUT, + }; + struct Status { + Phase phase; + FailureReason failureReason; + const uint32_t* readBuffer; + uint32_t readBufferLen; + MaplePacket::ByteOrder rxByteOrder; + }; + + MapleBus(uint32_t pinA, int32_t dirPin = -1, bool dirOutHigh = true); + + bool write(const MaplePacket& packet, bool autostartRead, + uint64_t readTimeoutUs = MAPLE_RESPONSE_TIMEOUT_US, + MaplePacket::ByteOrder rxByteOrder = MaplePacket::ByteOrder::HOST); + + /** Start listening for an incoming packet (device mode). No write; use processEvents until READ_COMPLETE. */ + bool startRead(uint64_t readTimeoutUs = 0, MaplePacket::ByteOrder rxByteOrder = MaplePacket::ByteOrder::HOST); + + void writeIsr(); + void readIsr(); + + Status processEvents(uint64_t currentTimeUs); + + bool isBusy() const { return mCurrentPhase != Phase::IDLE; } + +private: + bool lineCheck(); + void setDirection(bool output); + + const uint32_t mPinA; + const uint32_t mPinB; + const int32_t mDirPin; + const bool mDirOutHigh; + const uint32_t mMaskA; + const uint32_t mMaskB; + const uint32_t mMaskAB; + const uint mOutOffset; + const uint mInOffset; + const uint mSmOut; + const uint mSmIn; + const int mDmaWriteChannel; + const int mDmaReadChannel; + + volatile uint32_t mWriteBuffer[258]; + volatile uint32_t mReadBuffer[258]; + uint32_t mLastRead[256]; + + Phase mCurrentPhase; + bool mExpectingResponse; + uint64_t mResponseTimeoutUs; + MaplePacket::ByteOrder mRxByteOrder; + uint64_t mProcKillTime; + uint64_t mLastReceivedWordTimeUs; + uint32_t mLastReadTransferCount; +}; diff --git a/Firmware/RP2040/src/USBHost/GPIOHost/dreamcast_host/MaplePacket.hpp b/Firmware/RP2040/src/USBHost/GPIOHost/dreamcast_host/MaplePacket.hpp new file mode 100644 index 00000000..cf76be72 --- /dev/null +++ b/Firmware/RP2040/src/USBHost/GPIOHost/dreamcast_host/MaplePacket.hpp @@ -0,0 +1,102 @@ +/* Minimal Maple packet for host GET_CONDITION. Ported from DreamPicoPort MaplePacket.hpp. */ + +#pragma once + +#include +#include +#include "dreamcast_constants.h" +#include "maple_config.h" + +struct MaplePacket { + enum class ByteOrder { HOST, NETWORK }; + + struct Frame { + uint8_t command; + uint8_t recipientAddr; + uint8_t senderAddr; + uint8_t length; + + static constexpr uint32_t LEN_POSITION_HOST = 0; + static constexpr uint32_t SENDER_ADDR_POSITION_HOST = 8; + static constexpr uint32_t RECIPIENT_ADDR_POSITION_HOST = 16; + static constexpr uint32_t COMMAND_POSITION_HOST = 24; + static constexpr uint32_t LEN_POSITION_NETWORK = 24; + static constexpr uint32_t SENDER_ADDR_POSITION_NETWORK = 8; + static constexpr uint32_t RECIPIENT_ADDR_POSITION_NETWORK = 16; + static constexpr uint32_t COMMAND_POSITION_NETWORK = 0; + + static uint8_t getFramePacketLength(uint32_t frameWord, ByteOrder byteOrder = ByteOrder::HOST) { + return (frameWord >> (byteOrder == ByteOrder::NETWORK ? LEN_POSITION_NETWORK : LEN_POSITION_HOST)) & 0xFF; + } + static uint8_t getFrameSenderAddr(uint32_t frameWord, ByteOrder byteOrder = ByteOrder::HOST) { + return (frameWord >> (byteOrder == ByteOrder::NETWORK ? SENDER_ADDR_POSITION_NETWORK : SENDER_ADDR_POSITION_HOST)) & 0xFF; + } + static uint8_t getFrameRecipientAddr(uint32_t frameWord, ByteOrder byteOrder = ByteOrder::HOST) { + return (frameWord >> (byteOrder == ByteOrder::NETWORK ? RECIPIENT_ADDR_POSITION_NETWORK : RECIPIENT_ADDR_POSITION_HOST)) & 0xFF; + } + static uint8_t getFrameCommand(uint32_t frameWord, ByteOrder byteOrder = ByteOrder::HOST) { + return (frameWord >> (byteOrder == ByteOrder::NETWORK ? COMMAND_POSITION_NETWORK : COMMAND_POSITION_HOST)) & 0xFF; + } + + void setFromFrameWord(uint32_t frameWord, ByteOrder byteOrder = ByteOrder::HOST) { + length = getFramePacketLength(frameWord, byteOrder); + senderAddr = getFrameSenderAddr(frameWord, byteOrder); + recipientAddr = getFrameRecipientAddr(frameWord, byteOrder); + command = getFrameCommand(frameWord, byteOrder); + } + + uint32_t toWord(ByteOrder byteOrder = ByteOrder::HOST) const { + if (byteOrder == ByteOrder::NETWORK) + return (uint32_t)length << LEN_POSITION_NETWORK | + (uint32_t)senderAddr << SENDER_ADDR_POSITION_NETWORK | + (uint32_t)recipientAddr << RECIPIENT_ADDR_POSITION_NETWORK | + (uint32_t)command << COMMAND_POSITION_NETWORK; + return (uint32_t)length << LEN_POSITION_HOST | + (uint32_t)senderAddr << SENDER_ADDR_POSITION_HOST | + (uint32_t)recipientAddr << RECIPIENT_ADDR_POSITION_HOST | + (uint32_t)command << COMMAND_POSITION_HOST; + } + + bool isValid() const { return command != COMMAND_INVALID; } + }; + + Frame frame; + static constexpr unsigned MAX_PAYLOAD = 32; + uint32_t payload[MAX_PAYLOAD]; + unsigned payloadCount; + ByteOrder payloadByteOrder; + + MaplePacket() : frame{}, payload{}, payloadCount(0), payloadByteOrder(ByteOrder::HOST) { + frame.command = COMMAND_INVALID; + } + + void setFrame(uint8_t cmd, uint8_t recipient, uint8_t sender, uint8_t len) { + frame.command = cmd; + frame.recipientAddr = recipient; + frame.senderAddr = sender; + frame.length = len; + payloadCount = len; + } + + uint32_t getFrameWord() const { + Frame f = frame; + f.length = (uint8_t)payloadCount; + return f.toWord(payloadByteOrder); + } + + static uint32_t getNumTotalBits(uint32_t numPayloadWords) { + return ((numPayloadWords + 1) * 4 + 1) * 8; + } + uint32_t getNumTotalBits() const { return getNumTotalBits((uint32_t)payloadCount); } + + static uint32_t getTxTimeNs(uint32_t numPayloadWords, uint32_t nsPerBit) { + return (getNumTotalBits(numPayloadWords) + 14) * nsPerBit; + } + uint32_t getTxTimeNs() const { return getTxTimeNs((uint32_t)payloadCount, MAPLE_NS_PER_BIT); } + + static uint32_t flipWordBytes(uint32_t word) { + return (word << 24) | ((word << 8) & 0xFF0000) | ((word >> 8) & 0xFF00) | (word >> 24); + } + + bool isValid() const { return frame.isValid() && frame.length == payloadCount; } +}; diff --git a/Firmware/RP2040/src/USBHost/GPIOHost/dreamcast_host/PioProgram.hpp b/Firmware/RP2040/src/USBHost/GPIOHost/dreamcast_host/PioProgram.hpp new file mode 100644 index 00000000..d0d208fa --- /dev/null +++ b/Firmware/RP2040/src/USBHost/GPIOHost/dreamcast_host/PioProgram.hpp @@ -0,0 +1,15 @@ +/* PIO program holder. Ported from DreamPicoPort. */ + +#pragma once + +#include "hardware/pio.h" + +class PioProgram { +public: + PioProgram(pio_hw_t* pio, const pio_program_t* program) + : mPio(pio), mProgram(program), mProgramOffset(pio_add_program(pio, program)) {} + + pio_hw_t* const mPio; + const pio_program_t* const mProgram; + const uint mProgramOffset; +}; diff --git a/Firmware/RP2040/src/USBHost/GPIOHost/dreamcast_host/dreamcast_constants.h b/Firmware/RP2040/src/USBHost/GPIOHost/dreamcast_host/dreamcast_constants.h new file mode 100644 index 00000000..2b39a99b --- /dev/null +++ b/Firmware/RP2040/src/USBHost/GPIOHost/dreamcast_host/dreamcast_constants.h @@ -0,0 +1,35 @@ +/* Dreamcast Maple Bus commands. Ported from DreamPicoPort. */ + +#pragma once + +#include + +#define DEVICE_FN_CONTROLLER 0x00000001 + +enum DreamcastCommand : uint8_t { + COMMAND_DEVICE_INFO_REQUEST = 0x01, + COMMAND_EXT_DEVICE_INFO_REQUEST = 0x02, + COMMAND_RESET = 0x03, + COMMAND_SHUTDOWN = 0x04, + COMMAND_RESPONSE_DEVICE_INFO = 0x05, + COMMAND_RESPONSE_EXT_DEVICE_INFO = 0x06, + COMMAND_RESPONSE_ACK = 0x07, + COMMAND_RESPONSE_DATA_XFER = 0x08, + COMMAND_GET_CONDITION = 0x09, + COMMAND_GET_MEMORY_INFORMATION = 0x0A, + COMMAND_BLOCK_READ = 0x0B, + COMMAND_BLOCK_WRITE = 0x0C, + COMMAND_GET_LAST_ERROR = 0x0D, + COMMAND_SET_CONDITION = 0x0E, + COMMAND_RESPONSE_AR_ERROR = 0xF9, + COMMAND_RESPONSE_LCD_ERROR = 0xFA, + COMMAND_RESPONSE_FILE_ERROR = 0xFB, + COMMAND_RESPONSE_REQUEST_RESEND = 0xFC, + COMMAND_RESPONSE_UNKNOWN_COMMAND = 0xFD, + COMMAND_RESPONSE_FUNCTION_CODE_NOT_SUPPORTED = 0xFE, + COMMAND_INVALID = 0xFF +}; + +/* Host (console) address; controller is typically 0 on port A */ +#define MAPLE_HOST_ADDR 0x20 +#define MAPLE_CTRL_ADDR 0x00 diff --git a/Firmware/RP2040/src/USBHost/GPIOHost/dreamcast_host/dreamcast_structures.h b/Firmware/RP2040/src/USBHost/GPIOHost/dreamcast_host/dreamcast_structures.h new file mode 100644 index 00000000..b9e317e4 --- /dev/null +++ b/Firmware/RP2040/src/USBHost/GPIOHost/dreamcast_host/dreamcast_structures.h @@ -0,0 +1,40 @@ +/* Dreamcast controller structures. Ported from DreamPicoPort (OrangeFox86). + * https://github.com/OrangeFox86/DreamPicoPort */ + +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* 8-byte controller condition (little endian). 0 = pressed for buttons. */ +typedef struct controller_condition_s { + uint8_t l; /* 0 released .. 255 pressed */ + uint8_t r; + unsigned z : 1; + unsigned y : 1; + unsigned x : 1; + unsigned d : 1; + unsigned upb : 1; + unsigned downb : 1; + unsigned leftb : 1; + unsigned rightb : 1; + unsigned c : 1; + unsigned b : 1; + unsigned a : 1; + unsigned start : 1; + unsigned up : 1; + unsigned down : 1; + unsigned left : 1; + unsigned right : 1; + uint8_t rAnalogUD; /* 0 up, 128 neutral, 255 down */ + uint8_t rAnalogLR; + uint8_t lAnalogUD; + uint8_t lAnalogLR; +} __attribute__((packed)) controller_condition_t; + +#ifdef __cplusplus +} +#endif diff --git a/Firmware/RP2040/src/USBHost/GPIOHost/dreamcast_host/maple_config.h b/Firmware/RP2040/src/USBHost/GPIOHost/dreamcast_host/maple_config.h new file mode 100644 index 00000000..a37ad85b --- /dev/null +++ b/Firmware/RP2040/src/USBHost/GPIOHost/dreamcast_host/maple_config.h @@ -0,0 +1,28 @@ +/* Maple Bus timing and pin config. Adapted from DreamPicoPort configuration.h */ + +#pragma once + +#ifndef MAPLE_OPEN_LINE_CHECK_TIME_US +#define MAPLE_OPEN_LINE_CHECK_TIME_US 10 +#endif + +#ifndef MAPLE_NS_PER_BIT +#define MAPLE_NS_PER_BIT 480 +#endif + +#ifndef MAPLE_WRITE_TIMEOUT_EXTRA_PERCENT +#define MAPLE_WRITE_TIMEOUT_EXTRA_PERCENT 20 +#endif + +#ifndef MAPLE_RESPONSE_TIMEOUT_US +#define MAPLE_RESPONSE_TIMEOUT_US 1000 +#endif + +#ifndef MAPLE_INTER_WORD_READ_TIMEOUT_US +#define MAPLE_INTER_WORD_READ_TIMEOUT_US 300 +#endif + +/* CPU frequency in kHz (RP2040 default 125000) */ +#ifndef MAPLE_CPU_FREQ_KHZ +#define MAPLE_CPU_FREQ_KHZ 125000 +#endif diff --git a/Firmware/RP2040/src/USBHost/GPIOHost/dreamcast_host/maple_in.pio b/Firmware/RP2040/src/USBHost/GPIOHost/dreamcast_host/maple_in.pio new file mode 100644 index 00000000..a24d51c0 --- /dev/null +++ b/Firmware/RP2040/src/USBHost/GPIOHost/dreamcast_host/maple_in.pio @@ -0,0 +1,54 @@ +; Maple Bus input (read). Ported from DreamPicoPort (OrangeFox86). +; Waits for start sequence, samples data bits (A=clock, B=data), detects end. + +.program maple_in + +wait_start: +mov y, ~NULL +b_toggle_loop: +jmp pin check_toggle_count +mov osr, pins +out x 2 +jmp x-- b_toggle_loop +wait 1 pin 1 +jmp pin wait_start +jmp y-- b_toggle_loop + +check_toggle_count: +mov y, ~y +set x, 4 +jmp x!=y wait_start + +data_start: +irq set 0 rel +wait 0 pin 0 + +.wrap_target +data_loop: +set y, 1 +end_detect_b_toggle: +mov osr, pins +out x 1 +jmp !x sample_b_from_osr +out x 1 +jmp !y end_detect_b_to_low +end_detect_b_to_high: +jmp !x end_detect_b_toggle +jmp y-- end_detect_b_toggle +end_detect_b_to_low: +jmp x-- end_detect_b_toggle + +end_detected: +in null 24 +irq wait 0 rel + +sample_b_from_osr: +in osr 1 + +sample_a: +wait 1 pin 1 +wait 0 pin 1 +in pins 1 +wait 1 pin 0 + +.wrap diff --git a/Firmware/RP2040/src/USBHost/GPIOHost/dreamcast_host/maple_out.pio b/Firmware/RP2040/src/USBHost/GPIOHost/dreamcast_host/maple_out.pio new file mode 100644 index 00000000..faea5d3f --- /dev/null +++ b/Firmware/RP2040/src/USBHost/GPIOHost/dreamcast_host/maple_out.pio @@ -0,0 +1,64 @@ +; Maple Bus output (write). Ported from DreamPicoPort (OrangeFox86). +; First word = number of bits to send; then frame + payload + CRC. + +.program maple_out +.side_set 2 opt + +.define public MASK_A 0x1 +.define public MASK_B 0x2 +.define public MASK_AB 0x3 +.define public DOUBLE_PHASE_TICKS 4 +.define public SINGLE_PHASE_TICKS 2 + +maple_out_start: +out x, 32 side MASK_AB + +start_sequence: +set pindirs MASK_AB side MASK_AB [DOUBLE_PHASE_TICKS - 1] +set y, 3 side MASK_B [DOUBLE_PHASE_TICKS - 1] +start_b_toggle_loop: +nop side 0 [DOUBLE_PHASE_TICKS - 1] +nop side MASK_B [DOUBLE_PHASE_TICKS - 2] +jmp y-- start_b_toggle_loop +jmp x--, first_clock_a_data_b side MASK_AB + +first_clock_a_data_b: +out y, 1 side MASK_AB +jmp !y, clock_a_data_b_zero side MASK_AB +jmp first_clock_a_data_b_one side MASK_AB + +clock_a_data_b: +out y, 1 side MASK_A [SINGLE_PHASE_TICKS - 2] +jmp !y clock_a_data_b_zero +clock_a_data_b_one: +nop side MASK_AB [SINGLE_PHASE_TICKS - 1] +first_clock_a_data_b_one: +jmp x-- clock_b_data_a side MASK_B [SINGLE_PHASE_TICKS - 1] + +clock_a_data_b_zero: +nop side MASK_A [SINGLE_PHASE_TICKS - 1] +jmp x-- clock_b_data_a side 0 [SINGLE_PHASE_TICKS - 1] + +clock_b_data_a: +out y, 1 side MASK_B [SINGLE_PHASE_TICKS - 2] +jmp !y clock_b_data_a_zero +clock_b_data_a_one: +nop side MASK_AB [SINGLE_PHASE_TICKS - 1] +jmp x-- clock_a_data_b side MASK_A [SINGLE_PHASE_TICKS - 1] + +.wrap + +clock_b_data_a_zero: +nop side MASK_B [SINGLE_PHASE_TICKS - 1] +jmp x-- clock_a_data_b side 0 [SINGLE_PHASE_TICKS - 1] + +.wrap_target + +end_sequence: +out y, 24 side MASK_A [SINGLE_PHASE_TICKS - 1] +nop side MASK_AB [DOUBLE_PHASE_TICKS - 1] +nop side MASK_A [DOUBLE_PHASE_TICKS - 1] +nop side 0 [DOUBLE_PHASE_TICKS - 1] +nop side MASK_A [DOUBLE_PHASE_TICKS - 1] +nop side 0 [DOUBLE_PHASE_TICKS - 1] +irq wait 0 rel side MASK_A diff --git a/Firmware/RP2040/src/USBHost/GPIOHost/dreamcast_host/maple_utils.h b/Firmware/RP2040/src/USBHost/GPIOHost/dreamcast_host/maple_utils.h new file mode 100644 index 00000000..9d8dad61 --- /dev/null +++ b/Firmware/RP2040/src/USBHost/GPIOHost/dreamcast_host/maple_utils.h @@ -0,0 +1,3 @@ +#pragma once + +#define MAPLE_INT_DIVIDE_CEILING(x, y) (((x) + (y) - 1) / (y)) diff --git a/Firmware/RP2040/src/USBHost/GPIOHost/joybus_host/controller.pio b/Firmware/RP2040/src/USBHost/GPIOHost/joybus_host/controller.pio new file mode 100644 index 00000000..23f35b49 --- /dev/null +++ b/Firmware/RP2040/src/USBHost/GPIOHost/joybus_host/controller.pio @@ -0,0 +1,59 @@ +; JoyBus host (read GameCube controller). Ported from PicoGamepadConverter (Loc15). +; Single pin: output to send, input to receive. + +.program controller +.side_set 1 opt pindirs +.define public T1 4 +.define public T2 12 + +.wrap_target + out y, 5 side 0 ; load # of bytes expected in response +sendData: + out x, 1 side 0 [T1 - 1]; send data bit by bit + jmp !x do_zero side 1 [T1 - 1] ; set 0 for 4 cycles +do_one: + jmp !OSRE sendData side 0 [T1 * 2 - 1] ; set 1 for 10 cycles + jmp sendStop [T1 - 1] +do_zero: + jmp !OSRE sendData [T1 * 2 - 1]; set 1 + jmp sendStop side 0 [T1 - 1] +sendStop: + nop side 1 [T1 - 1] ; send the stop bit (1) + nop side 0 + +receiveByte: + set x, 7 +getBit: + wait 0 pin 0 [T1 + 1]; wait until the line goes low + in pins 1 ; read + wait 1 pin 0 ; wait for a 1 if it's a 0 + jmp x-- getBit ; get the next bit + jmp y-- receiveByte ; get the next byte + +.wrap + + +% c-sdk { +#include "hardware/clocks.h" +#include "hardware/pio.h" + +static inline void controller_program_init(PIO pio, uint sm, uint offset, uint pin_controller, pio_sm_config *c) { + pio_sm_set_consecutive_pindirs(pio, sm, pin_controller, 1, false); + pio_gpio_init(pio, pin_controller); + + sm_config_set_in_pins(c, pin_controller); + sm_config_set_sideset_pins(c, pin_controller); + sm_config_set_jmp_pin(c, pin_controller); + sm_config_set_out_shift(c, false, true, 13); + sm_config_set_in_shift(c, false, true, 8); + + int cyclesPerBit = (controller_T1 + controller_T2) / 4; + float frequency = 1000000; + float clockDiv = clock_get_hz(clk_sys) / (cyclesPerBit * frequency); + sm_config_set_clkdiv(c, clockDiv); + + pio_sm_init(pio, sm, offset, c); + pio_sm_set_enabled(pio, sm, true); +} + +%} diff --git a/Firmware/RP2040/src/USBHost/GPIOHost/joybus_host/joybus_controller.c b/Firmware/RP2040/src/USBHost/GPIOHost/joybus_host/joybus_controller.c new file mode 100644 index 00000000..9508ad69 --- /dev/null +++ b/Firmware/RP2040/src/USBHost/GPIOHost/joybus_host/joybus_controller.c @@ -0,0 +1,62 @@ +/* JoyBus host (read GameCube controller). Ported from PicoGamepadConverter (Loc15). */ + +#include "pico/stdlib.h" +#include "hardware/pio.h" +#include "controller.pio.h" + +#define JOYBUS_STATE_BYTES 8 + +static PIO joybus_pio; +static uint joybus_sm; +static pio_sm_config joybus_config; +static uint joybus_offset; +static uint8_t controller_state[JOYBUS_STATE_BYTES]; + +static void update_pio_output_size(uint8_t auto_pull_length) +{ + pio_sm_set_enabled(joybus_pio, joybus_sm, false); + joybus_pio->sm[joybus_sm].shiftctrl = (joybus_pio->sm[joybus_sm].shiftctrl & 0xA00FFFFF) | + (0x8u << 20) | + (((auto_pull_length + 5) & 0x1Fu) << 25); + joybus_pio->ctrl |= 1u << (4 + joybus_sm); + pio_sm_set_enabled(joybus_pio, joybus_sm, true); +} + +void joybus_host_init(uint pio_index, uint pin) +{ + joybus_pio = pio_index ? pio1 : pio0; + joybus_offset = pio_add_program(joybus_pio, &controller_program); + joybus_sm = pio_claim_unused_sm(joybus_pio, true); + joybus_config = controller_program_get_default_config(joybus_offset); + controller_program_init(joybus_pio, joybus_sm, joybus_offset, pin, &joybus_config); + update_pio_output_size(2); /* first transfer is poll: 2 bytes request */ +} + +void joybus_send_data(const uint8_t *request, uint8_t data_length, uint8_t response_length) +{ + uint32_t data_with_response_length = ((response_length - 1) & 0x1Fu) << 27; + for (int i = 0; i < data_length; i++) + { + data_with_response_length |= (uint32_t)request[i] << (19 - i * 8); + } + pio_sm_put_blocking(joybus_pio, joybus_sm, data_with_response_length); + + int16_t remaining = (int16_t)response_length; + while (remaining > 0) + { + absolute_time_t timeout = make_timeout_time_us(600); + while (pio_sm_is_rx_fifo_empty(joybus_pio, joybus_sm) && !time_reached(timeout)) + { + } + uint32_t data = pio_sm_get(joybus_pio, joybus_sm); + controller_state[response_length - remaining] = (uint8_t)(data & 0xFFu); + remaining--; + } + /* Next transfer will send data_length bytes (e.g. 2 for poll). */ + update_pio_output_size(data_length); +} + +const uint8_t* joybus_get_response(void) +{ + return controller_state; +} diff --git a/Firmware/RP2040/src/USBHost/GPIOHost/joybus_host/joybus_controller.h b/Firmware/RP2040/src/USBHost/GPIOHost/joybus_host/joybus_controller.h new file mode 100644 index 00000000..5acf394e --- /dev/null +++ b/Firmware/RP2040/src/USBHost/GPIOHost/joybus_host/joybus_controller.h @@ -0,0 +1,24 @@ +#ifndef USBHOST_GPIOHOST_JOYBUS_JOYBUS_CONTROLLER_H_ +#define USBHOST_GPIOHOST_JOYBUS_JOYBUS_CONTROLLER_H_ + +#include "hardware/pio.h" +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** Initialize JoyBus host (GameCube controller). pio_index 0 or 1, pin = data line. */ +void joybus_host_init(uint pio_index, uint pin); + +/** Send request and read response. request/response lengths in bytes. Response written to internal buffer. */ +void joybus_send_data(const uint8_t *request, uint8_t data_length, uint8_t response_length); + +/** Get pointer to last response buffer (8 bytes for GameCube). Valid until next joybus_send_data. */ +const uint8_t* joybus_get_response(void); + +#ifdef __cplusplus +} +#endif + +#endif /* USBHOST_GPIOHOST_JOYBUS_JOYBUS_CONTROLLER_H_ */ diff --git a/Firmware/RP2040/src/USBHost/GPIOHost/joybus_host/n64_host.c b/Firmware/RP2040/src/USBHost/GPIOHost/joybus_host/n64_host.c new file mode 100644 index 00000000..201a73a7 --- /dev/null +++ b/Firmware/RP2040/src/USBHost/GPIOHost/joybus_host/n64_host.c @@ -0,0 +1,73 @@ +/* N64 controller host (1-wire). Ported from PicoGamepadConverter (Loc15) / joybus_host. */ + +#include "pico/stdlib.h" +#include "hardware/pio.h" +#include "controller.pio.h" + +#define N64_RESPONSE_BYTES 4 + +static PIO n64_pio; +static uint n64_sm; +static pio_sm_config n64_config; +static uint n64_offset; +static uint8_t n64_state[N64_RESPONSE_BYTES]; + +static void n64_update_pio_output_size(uint8_t auto_pull_length) +{ + pio_sm_set_enabled(n64_pio, n64_sm, false); + n64_pio->sm[n64_sm].shiftctrl = (n64_pio->sm[n64_sm].shiftctrl & 0xA00FFFFF) | + (0x8u << 20) | + (((auto_pull_length + 5) & 0x1Fu) << 25); + n64_pio->ctrl |= 1u << (4 + n64_sm); + pio_sm_set_enabled(n64_pio, n64_sm, true); +} + +static void n64_send_data(const uint8_t *request, uint8_t data_length, uint8_t response_length) +{ + uint32_t data_with_response_length = ((response_length - 1) & 0x1Fu) << 27; + for (int i = 0; i < data_length; i++) + { + data_with_response_length |= (uint32_t)request[i] << (19 - i * 8); + } + pio_sm_put_blocking(n64_pio, n64_sm, data_with_response_length); + + int16_t remaining = (int16_t)response_length; + while (remaining > 0) + { + absolute_time_t timeout = make_timeout_time_us(600); + while (pio_sm_is_rx_fifo_empty(n64_pio, n64_sm) && !time_reached(timeout)) + { + } + uint32_t data = pio_sm_get(n64_pio, n64_sm); + n64_state[response_length - remaining] = (uint8_t)(data & 0xFFu); + remaining--; + } + n64_update_pio_output_size(data_length); +} + +void n64_host_init(uint pio_index, uint data_pin) +{ + n64_pio = pio_index ? pio1 : pio0; + n64_offset = pio_add_program(n64_pio, &controller_program); + n64_sm = pio_claim_unused_sm(n64_pio, true); + n64_config = controller_program_get_default_config(n64_offset); + controller_program_init(n64_pio, n64_sm, n64_offset, data_pin, &n64_config); + n64_update_pio_output_size(1); + + /* N64 init: send 0x00, expect 1 byte (identify). */ + const uint8_t init_cmd[] = { 0x00 }; + n64_send_data(init_cmd, 1, 1); + sleep_us(200); +} + +void n64_host_poll(void) +{ + const uint8_t poll_cmd[] = { 0x01 }; + n64_send_data(poll_cmd, 1, N64_RESPONSE_BYTES); + busy_wait_us(500); +} + +const uint8_t* n64_host_get_response(void) +{ + return n64_state; +} diff --git a/Firmware/RP2040/src/USBHost/GPIOHost/joybus_host/n64_host.h b/Firmware/RP2040/src/USBHost/GPIOHost/joybus_host/n64_host.h new file mode 100644 index 00000000..8dd78131 --- /dev/null +++ b/Firmware/RP2040/src/USBHost/GPIOHost/joybus_host/n64_host.h @@ -0,0 +1,23 @@ +#ifndef USBHOST_GPIOHOST_JOYBUS_N64_HOST_H_ +#define USBHOST_GPIOHOST_JOYBUS_N64_HOST_H_ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** Initialize N64 controller host (1-wire, same protocol as PicoGamepadConverter). Uses same controller.pio as JoyBus. */ +void n64_host_init(uint pio_index, uint data_pin); + +/** Poll N64 controller (sends 0x01, reads 4 bytes). Call from main loop when input source is N64_GPIO. */ +void n64_host_poll(void); + +/** Get pointer to last poll response (4 bytes: buttons1, buttons2, joy_x, joy_y). Valid until next n64_host_poll. */ +const uint8_t* n64_host_get_response(void); + +#ifdef __cplusplus +} +#endif + +#endif /* USBHOST_GPIOHOST_JOYBUS_N64_HOST_H_ */ diff --git a/Firmware/RP2040/src/USBHost/GPIOHost/psx_host/clock.pio b/Firmware/RP2040/src/USBHost/GPIOHost/psx_host/clock.pio new file mode 100644 index 00000000..21fada27 --- /dev/null +++ b/Firmware/RP2040/src/USBHost/GPIOHost/psx_host/clock.pio @@ -0,0 +1,40 @@ +; PS1/PS2 controller host (read real controller). Ported from PicoGamepadConverter (Loc15). +; CMD, CLK, ATT are outputs; DATA is input. + +.program clock +.side_set 1 opt +.wrap_target + set y, 2 [7] ; Commands counter -> 3 bytes command + set pins, 0b11 [7] side 1 ; Set ATTENTION, CLK and CMD -> HIGH + pull [7] ; Pull 32 bits data + set pins, 0b01 [2] ; Set ATTENTION -> LOW + +set_counter: + set x, 7 [7] ; 8 bit counter + delay between bytes + +bitloop: + out pins, 1 ; Pull a bit + set pins, 0b00 ; Driver pin low + in pins, 1 ; Read a bit + set pins, 0b01 ; Drive pin high + jmp set_clock ; Jump to set_clock + +set_clock: + jmp x-- bitloop + jmp y-- set_counter + set y, 5 ; Data counter -> 6 bytes data + +set_counter2: + set x, 7 [7] ; 8 bit counter + delay between bytes + +bitloop2: + set pins, 0b00 [1] ; Driver pin low + in pins, 1 ; Read a bit + set pins, 0b01 ; Driver pin high + jmp set_clock2 ; Jump to set_clock + +set_clock2: + jmp x-- bitloop2 + jmp y-- set_counter2 + +.wrap diff --git a/Firmware/RP2040/src/USBHost/GPIOHost/psx_host/psx_host.c b/Firmware/RP2040/src/USBHost/GPIOHost/psx_host/psx_host.c new file mode 100644 index 00000000..96b29ab4 --- /dev/null +++ b/Firmware/RP2040/src/USBHost/GPIOHost/psx_host/psx_host.c @@ -0,0 +1,95 @@ +/* PS1/PS2 controller host (read real controller). Ported from PicoGamepadConverter (Loc15). + * https://github.com/Loc15/PicoGamepadConverter */ + +#include "pico/stdlib.h" +#include "hardware/pio.h" +#include "hardware/dma.h" +#include "hardware/clocks.h" + +#include "clock.pio.h" + +#define N_BYTES 9 + +static PIO psx_pio; +static uint psx_sm; +static int psx_dma_chan; +static uint32_t data_psx[N_BYTES]; +static const uint32_t cmd = 0x01u | (0x42u << 8); + +static volatile uint8_t psx_host_buffer[N_BYTES]; +static volatile bool psx_host_ready; + +static void __not_in_flash_func(dma_handler)(void) +{ + dma_hw->ints0 = 1u << psx_dma_chan; + dma_channel_set_read_addr(psx_dma_chan, &psx_pio->rxf[psx_sm], false); + dma_channel_set_write_addr(psx_dma_chan, (void *)&data_psx[0], false); + dma_channel_set_trans_count(psx_dma_chan, N_BYTES, false); + dma_channel_start(psx_dma_chan); + + for (int i = 0; i < N_BYTES; i++) + { + psx_host_buffer[i] = (uint8_t)(data_psx[i] & 0xFFu); + } + psx_host_ready = true; + + pio_sm_put_blocking(psx_pio, psx_sm, cmd); +} + +void psx_host_init(uint pio_index, uint gpio_output, uint gpio_input) +{ + psx_pio = pio_index ? pio1 : pio0; + psx_host_ready = false; + + uint offset = pio_add_program(psx_pio, &clock_program); + psx_sm = pio_claim_unused_sm(psx_pio, true); + pio_sm_config c = clock_program_get_default_config(offset); + sm_config_set_set_pins(&c, gpio_output + 1, 2); + sm_config_set_out_pins(&c, gpio_output, 1); + sm_config_set_sideset_pins(&c, gpio_output); + sm_config_set_in_pins(&c, gpio_input); + sm_config_set_in_shift(&c, true, true, 8); + pio_gpio_init(psx_pio, gpio_output); + pio_gpio_init(psx_pio, gpio_output + 1); + pio_gpio_init(psx_pio, gpio_output + 2); + pio_gpio_init(psx_pio, gpio_input); + gpio_pull_up(gpio_input); + gpio_pull_up(gpio_output + 1); + gpio_pull_up(gpio_output + 2); + pio_sm_set_consecutive_pindirs(psx_pio, psx_sm, gpio_output, 3, true); + pio_sm_set_consecutive_pindirs(psx_pio, psx_sm, gpio_input, 1, false); + float div = (float)clock_get_hz(clk_sys) / (6.0f * 12500.0f); + sm_config_set_clkdiv(&c, div); + pio_sm_init(psx_pio, psx_sm, offset, &c); + pio_sm_set_enabled(psx_pio, psx_sm, true); + + psx_dma_chan = dma_claim_unused_channel(true); + dma_channel_config c_dma = dma_channel_get_default_config(psx_dma_chan); + channel_config_set_transfer_data_size(&c_dma, DMA_SIZE_32); + channel_config_set_read_increment(&c_dma, false); + channel_config_set_write_increment(&c_dma, true); + channel_config_set_dreq(&c_dma, pio_get_dreq(psx_pio, psx_sm, false)); + dma_channel_configure(psx_dma_chan, &c_dma, + (void *)&data_psx[0], + &psx_pio->rxf[psx_sm], + N_BYTES, + false); + dma_channel_set_irq0_enabled(psx_dma_chan, true); + irq_set_exclusive_handler(DMA_IRQ_0, dma_handler); + irq_set_enabled(DMA_IRQ_0, true); + dma_handler(); +} + +bool psx_host_take_data(uint8_t *out) +{ + if (!psx_host_ready) + { + return false; + } + for (int i = 0; i < N_BYTES; i++) + { + out[i] = psx_host_buffer[i]; + } + psx_host_ready = false; + return true; +} diff --git a/Firmware/RP2040/src/USBHost/GPIOHost/psx_host/psx_host.h b/Firmware/RP2040/src/USBHost/GPIOHost/psx_host/psx_host.h new file mode 100644 index 00000000..524f9476 --- /dev/null +++ b/Firmware/RP2040/src/USBHost/GPIOHost/psx_host/psx_host.h @@ -0,0 +1,21 @@ +#ifndef USBHOST_GPIOHOST_PSX_HOST_PSX_HOST_H_ +#define USBHOST_GPIOHOST_PSX_HOST_PSX_HOST_H_ + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** Initialize PSX controller host. gpio_output = CMD pin (CLK=gpio_output+1, ATT=gpio_output+2); gpio_input = DATA. */ +void psx_host_init(uint pio_index, uint gpio_output, uint gpio_input); + +/** Copy latest 9-byte report if available and clear ready. Returns true if data was copied. */ +bool psx_host_take_data(uint8_t *out); + +#ifdef __cplusplus +} +#endif + +#endif /* USBHOST_GPIOHOST_PSX_HOST_PSX_HOST_H_ */ diff --git a/Firmware/RP2040/src/USBHost/HardwareIDs.h b/Firmware/RP2040/src/USBHost/HardwareIDs.h index b02b7941..c18b2db1 100644 --- a/Firmware/RP2040/src/USBHost/HardwareIDs.h +++ b/Firmware/RP2040/src/USBHost/HardwareIDs.h @@ -11,6 +11,7 @@ struct HardwareID uint16_t pid; }; +// DInput / generic HID; includes Xbox controllers when presented as HID (XInput uses class driver when possible) static const HardwareID DINPUT_IDS[] = { {0x044F, 0xB324}, // ThrustMaster Dual Trigger (PS3 mode) @@ -18,29 +19,360 @@ static const HardwareID DINPUT_IDS[] = {0x0810, 0x0003}, // Personal Communication Systems, Inc. Generic {0x146B, 0x0902}, // BigBen Interactive Wired Mini PS3 Game Controller {0x2563, 0x0575}, // SHANWAN 2In1 USB Joystick - {0x046D, 0xC218} // Logitech RumblePad 2 + {0x046D, 0xC218}, // Logitech RumblePad 2 + // Microsoft Xbox (045E) not in DINPUT: driven only by XInput. Adding them here causes + // double driver on Adafruit Feather (HID + XInput) and "vibrates but no input". See upstream OGX-Mini. + {0x2DC8, 0x3016}, // 8BitDo wired XInput (HID fallback) + {0x2DC8, 0x3106}, // 8BitDo wired XInput (HID fallback) + {0x2DC8, 0x2101}, // 8BitDo Xbox One SN30 Pro + {0x0E6F, 0x0131}, // Afterglow Xbox + {0x0E6F, 0x0132}, // Afterglow Xbox + {0x0E6F, 0x0134}, // Afterglow Xbox + {0x0E6F, 0x1314}, // Afterglow Xbox + {0x0E6F, 0x0113}, // Afterglow Xbox Controller + {0x0E6F, 0x0213}, // Afterglow Xbox Controller + {0x0E6F, 0x0413}, // Afterglow Xbox Controller + {0x0E6F, 0x0139}, // Afterglow Xbox + {0x12AB, 0x0301}, // Afterglow Xbox + {0x1BAD, 0xF900}, // Afterglow Xbox + {0x1BAD, 0x00FD}, // Xbox 360 Controller (PDP/hub) + {0x1BAD, 0x01FD}, // Xbox 360 Controller + {0x1BAD, 0x16F0}, // Xbox 360 Controller + {0x1BAD, 0x028E}, // Xbox 360 Controller + {0x0F0D, 0x0086}, // Hori Fighting Commander Xbox 360 + {0x0F0D, 0x00BA}, // Hori Fighting Commander Xbox 360 + {0x0738, 0x4718}, // Mad Catz Street Fighter 4 Xbox 360 FightStick + {0x0738, 0x4716}, // Xbox 360 Controller + {0x0738, 0x4520}, // Xbox 360 Controller + {0x0738, 0x4426}, // Xbox 360 Controller + {0x0738, 0x4726}, // Xbox 360 Controller + {0x0738, 0xB726}, // Xbox 360 Controller + {0x0738, 0x4736}, // Xbox 360 Controller + {0x0738, 0x4728}, // Xbox 360 Fightpad + {0x0738, 0x4516}, // Xbox Controller + {0x0738, 0x4526}, // Xbox Controller + {0x0738, 0x4536}, // Xbox Controller + {0x0738, 0x4586}, // Xbox Controller + // Microsoft SideWinder / legacy (VID 045E) + {0x045E, 0x0028}, // Microsoft Dual Strike + {0x045E, 0x0003}, // Microsoft SideWinder + {0x045E, 0x0007}, // Microsoft SideWinder + {0x045E, 0x000E}, // Microsoft SideWinder Freestyle Pro + {0x045E, 0x0027}, // Microsoft SideWinder Plug and Play + {0x120C, 0x8802}, // Nyko Air Flo Xbox + {0x120C, 0x880A}, // Xbox Controller + {0x120C, 0x8810}, // Xbox Controller + {0x162E, 0xBEEF}, // Xbox 360 Controller (generic) + {0x24C6, 0x5300}, // Xbox 360 Controller + {0x24C6, 0xFAFD}, // Xbox 360 Controller + {0x062A, 0x0020}, // Xbox Controller + // GameSir (multi-mode HID fallback, from SDL_GameControllerDB) + {0x05AC, 0x033D}, // GameSir G3 + {0x05AC, 0x055B}, // GameSir G3w + {0x05AC, 0x022D}, // GameSir G4 + {0x05AC, 0x044D}, // GameSir G4 (alt) + {0x05AC, 0x061A}, // GameSir T3 2.02 + {0x3537, 0x1022}, // GameSir G7 Pro + {0x3537, 0x1004}, // GameSir T4 Kaleid + {0x3537, 0x1094}, // GameSir Tegenaria Lite + // 8BitDo multi-mode (from SDL_GameControllerDB, HID fallback) + {0x2DC8, 0x3019}, // 8BitDo 64 + {0x2DC8, 0x5109}, // 8BitDo Dogbone + {0x2DC8, 0x9000}, // 8BitDo FC30 Pro + {0x2DC8, 0x5112}, // 8BitDo Lite 2 + {0x2DC8, 0x5111}, // 8BitDo Lite SE + {0x2DC8, 0x286A}, // 8BitDo GameCube + {0x2DC8, 0x6003}, // 8BitDo Pro 2 + {0x2DC8, 0x6006}, // 8BitDo Pro 2 + {0x2DC8, 0x6103}, // 8BitDo Pro 2 + {0x2DC8, 0x6009}, // 8BitDo Pro 3 + {0x2DC8, 0x3012}, // 8BitDo Ultimate + {0x2DC8, 0x3011}, // 8BitDo Ultimate Wired + {0x2DC8, 0x3013}, // 8BitDo Ultimate Wireless + {0x2DC8, 0x3100}, // 8BitDo Adapter + {0x2DC8, 0x3105}, // 8BitDo Adapter 2 + {0x2DC8, 0x3101}, // 8BitDo Receiver + {0x2DC8, 0x3102}, // 8BitDo Receiver + {0x2DC8, 0x3103}, // 8BitDo Receiver + {0x2DC8, 0x3104}, // 8BitDo Receiver + // Flydigi (multi-mode; DInput fallback when not in XInput) + {0xEED1, 0x04B4}, // Flydigi Vader 4 Pro (DInput mode) + // Fightsticks / arcade sticks (from SDL_GameControllerDB PC/Arcade + PC/PlayStation) + {0x8000, 0x1002}, // 8BitDo F30 + {0x1235, 0xAB11}, // 8BitDo F30 Arcade Joystick + {0x2DC8, 0x2810}, // 8BitDo F30 Arcade Joystick + {0x2DC8, 0xAB11}, // 8BitDo F30 Arcade Joystick + {0x1080, 0x0009}, // 8BitDo F30 Arcade Stick + {0x2DC8, 0x3810}, // 8BitDo F30 Pro + {0x1C5A, 0x0024}, // Capcom Home Arcade + {0x1C5B, 0x0024}, // Capcom Home Arcade + {0x1C5B, 0x0025}, // Capcom Home Arcade + {0x14D8, 0xCD07}, // Cthulhu + {0x14D8, 0xFACE}, // Cthulhu + {0x14D8, 0x6208}, // HitBox Edition Cthulhu + {0x0F0D, 0x0010}, // Hori Fightstick + {0x0F0D, 0x0032}, // Hori Fightstick 3W + {0x0F0D, 0x00C0}, // Hori Fightstick 4 + {0x0F0D, 0x000D}, // Hori Fightstick EX2 + {0x0F0D, 0x0137}, // Hori Fightstick Mini + {0x0F0D, 0x0040}, // Hori Fightstick Mini 3 + {0x0F0D, 0x0021}, // Hori Fightstick V3 + {0x0F0D, 0x0027}, // Hori Fightstick V3 + {0x0F0D, 0x0011}, // Hori Real Arcade Pro 3 + {0x0F0D, 0x0026}, // Hori Real Arcade Pro 3P + {0x0F0D, 0x004B}, // Hori Real Arcade Pro 3W + // RAP4 PS3 (0x006A/0x006B) are in PS3_IDS. RAP4/RAP4 Kai PS4 (0x008A/0x008B) are in PS4_IDS. + // RAP4 PC/XInput (0x008C) is unlisted so tuh_xinput or HIDGeneric can claim it. + {0x0F0D, 0x006F}, // Hori Real Arcade Pro 4 VLX + {0x0F0D, 0x0070}, // Hori Real Arcade Pro 4 VLX + {0x0F0D, 0x003D}, // Hori Real Arcade Pro N3 + {0x0F0D, 0x00AE}, // Hori Real Arcade Pro N4 + {0x0F0D, 0x00AA}, // Hori Real Arcade Pro S + {0x0F0D, 0x00D8}, // Hori Real Arcade Pro S + {0x0F0D, 0x0022}, // Hori Real Arcade Pro V3 + {0x0F0D, 0x005B}, // Hori Real Arcade Pro V4 + {0x0F0D, 0x005C}, // Hori Real Arcade Pro V4 + {0x0F0D, 0x00AF}, // Hori Real Arcade Pro VHS + {0x0F0D, 0x001B}, // Hori Real Arcade Pro VX + {0x1BAD, 0xF502}, // Hori Real Arcade Pro VX + {0x0F0D, 0x0025}, // Hori Fighting Commander 3 + {0x0F0D, 0x002D}, // Hori Fighting Commander 3 Pro + {0x0F0D, 0x0084}, // Hori Fighting Commander 5 + {0x0F0D, 0x0162}, // Hori Fighting Commander Octa + {0x0F0D, 0x0164}, // Hori Fighting Commander Octa + {0x0738, 0xB738}, // Mad Catz Fightstick TE + {0x0738, 0x3187}, // Mad Catz PlayStation Fightstick + {0x0738, 0x4738}, // Street Fighter Fightstick TE + {0x0925, 0x2801}, // Mayflash Arcade Stick + {0x0079, 0x1830}, // Mayflash F300 Arcade Joystick + {0x2F24, 0x0039}, // Mayflash F300 Elite Arcade Joystick + {0x1BAD, 0xF03E}, // MLG Fightstick TE + {0x1BAD, 0xF028}, // Fightpad (Xbox) + {0x1BAD, 0xF02E}, // Fightpad (Xbox) + {0x1BAD, 0xF038}, // Fightpad TE (Xbox) + {0x1292, 0x4E47}, // NeoGeo X Arcade Stick + {0x0F30, 0x1101}, // Qanba 2 + {0x0F30, 0x1102}, // Qanba 2P + {0x0F30, 0x1100}, // Qanba Arcade Stick 1008 + {0x0F30, 0x1116}, // Qanba Arcade Stick 4018 + {0x0F30, 0x1112}, // Qanba Joystick + {0x0F30, 0x1012}, // Qanba Joystick Plus + {0x1A34, 0x0401}, // Qanba Joystick Q4RAF + {0x2C22, 0x2500}, // Qanba Dragon Arcade Joystick + {0x2C22, 0x2000}, // Qanba Drone Arcade Stick + {0x2C22, 0x2302}, // Qanba Obsidian Arcade Stick PS3 (PC mode) + {0x1DD8, 0x000E}, // iBuffalo AC02 Arcade Joystick + {0x16C0, 0x05E1}, // XinMo Dual Arcade + {0x3507, 0x0004}, // Zenaim Arcade Controller + {0x1C1A, 0x0100}, // Datel Arcade Joystick + {0x0E6F, 0x0185}, // PDP Fightpad Pro GameCube + // PowerA Switch-mode pads are in SWITCH_WIRED_IDS (SwitchWiredHost). + // PowerA Xbox/PC pads are intentionally *not* listed here: DInputHost uses a fixed + // PS3-style report layout. On Sakura's minimal ID list these fall through to HIDGeneric + // (descriptor parse) or tuh_xinput — both work. Forcing DINPUT breaks them. + // Elecom (from SDL_GameControllerDB) + {0x056E, 0x200A}, // Elecom DUX60 MMO + {0x056E, 0x2003}, // Elecom U3613M + {0x056E, 0x2005}, // Elecom P301U PlayStation Controller Adapter + {0x056E, 0x2007}, // Elecom W01U + {0x056E, 0x200E}, // Elecom U3912T + {0x056E, 0x200F}, // Elecom U4013S + {0x056E, 0x2010}, // Elecom U4113S + {0x056E, 0x2013}, // Elecom U4113 + {0x05B8, 0x1004}, // Elecom Gamepad + {0x05B8, 0x1006}, // Elecom Gamepad + {0x1241, 0x5044}, // Elecom U1012 + {0x0925, 0x1802}, // Elecom PlayStation Adapter (Mayflash OEM) + // Brook adapters (DInput; Brook Mars PS4 in PS4_IDS) + {0x120C, 0x0C31}, // Brook Super Converter + {0x120C, 0x0EF1}, // Brook PS2 Adapter + // Mad Catz PC / generic (from SDL_GameControllerDB) + {0x0738, 0x5262}, // Mad Catz Micro CTRLR + {0x0738, 0x5263}, // Mad Catz CTRLR + {0x0738, 0x5266}, // Mad Catz CTRLR + {0x0738, 0x3282}, // Mad Catz PlayStation Brawlpad + // Hori pads / misc (remaining from SDL_GameControllerDB) + {0x0F0D, 0x000A}, // Hori DOA + {0x0F0D, 0x0009}, // Hori Pad 3 Turbo + {0x0F0D, 0x0013}, // Horipad 3W + {0x0F0D, 0x0042}, // Horipad A + {0x0F0D, 0x004D}, // Hori Pad A + {0x0F0D, 0x0054}, // Hori Pad 3 + {0x0F0D, 0x0055}, // Horipad 4 FPS + {0x0F0D, 0x0064}, // Horipad 3TP + {0x0F0D, 0x0067}, // Horipad One + {0x0F0D, 0x00A0}, // Hori Grip TAC4 + {0x0F0D, 0x009C}, // Hori TAC Pro + {0x0F0D, 0x00C9}, // Hori Taiko Controller + {0x0F0D, 0x0101}, // Hori Mini Hatsune Miku FT + {0x0F0D, 0x0138}, // Hori PC Engine Mini Controller + {0x0F0D, 0x0196}, // Horipad Steam + {0x1BAD, 0xF501}, // Horipad EXT2 + // Sony PlayStation (adapters / DualShock 2; DS3/DS4/DS5 in dedicated arrays) + {0x0E8F, 0x1009}, // Sony DualShock 2 + {0x05E3, 0x0596}, // Sony PlayStation Adapter + {0x14FE, 0x232A}, // Sony PlayStation Adapter + {0x6666, 0x0667}, // Sony PlayStation Adapter + {0x04D9, 0x0F16}, // Sony PlayStation Controller Adapter + // Steam / Valve (from SDL_GameControllerDB) + {0x28DE, 0x11FC}, // Steam Virtual Gamepad + {0x28DE, 0x11FF}, // Steam Virtual Gamepad + {0x28DE, 0x1102}, // Steam Controller + {0x28DE, 0x1105}, // Steam Controller + {0x28DE, 0x1106}, // Steam Controller + {0x28DE, 0x1142}, // Steam Controller + {0x28DE, 0x1201}, // Steam Controller + {0x28DE, 0x1205}, // Steam Deck + // Scuf Envision (Scuf PS4 in PS4_IDS; Envision PC/HID) + {0x2E95, 0x434B}, // Scuf Envision (Linux/HID) + {0x2E95, 0x434D}, // Scuf Envision (Linux/HID) + {0x2E95, 0x434E}, // Scuf Envision (Linux/HID) + // Logitech (from SDL_GameControllerDB; one entry already above) + {0x046D, 0xC209}, // Logitech WingMan + {0x046D, 0xC20A}, // Logitech WingMan RumblePad + {0x046D, 0xC20B}, // Logitech WingMan Action Pad + {0x046D, 0xC21A}, // Logitech Precision + {0x046D, 0xC21D}, // Logitech F310 + {0x046D, 0xC21E}, // Logitech F510 + {0x046D, 0xC21F}, // Logitech F710 + {0x046D, 0xC216}, // Logitech Dual Action + {0x046D, 0xC211}, // Logitech Cordless Wingman + {0x046D, 0xCAD1}, // Logitech ChillStream + {0x046D, 0xCAD2}, // Logitech Cordless Precision + // Razer (from SDL_GameControllerDB; PS4 Razer in PS4_IDS) + {0x27F8, 0x0BBF}, // Razer Kishi + {0x1532, 0x0300}, // Razer Hydra + {0x1532, 0x0900}, // Razer Serval + {0x1532, 0x1000}, // Razer Raiju + {0x1532, 0x0705}, // Razer Raiju Mobile + {0x1532, 0x0707}, // Razer Raiju Mobile + {0x1532, 0x1007}, // Razer Raiju TE + {0x1532, 0x100A}, // Razer Raiju TE + {0x1532, 0x1004}, // Razer Raiju UE + {0x1532, 0x1009}, // Razer Raiju UE }; +// PS3 controllers (from SDL_GameControllerDB + existing); many third-party pads use PS3 protocol static const HardwareID PS3_IDS[] = { - {0x054C, 0x0268}, // Sony Batoh (Dualshock 3) + {0x054C, 0x0268}, // Sony Dualshock 3 (Batoh) + {0x7331, 0x0001}, // Sony DualShock 3 (alt/clone VID) + {0x1532, 0x0402}, // Razer Panthera PS3 + {0x1A34, 0x0836}, // Afterglow PS3 + {0x0E6F, 0x6302}, // Afterglow PS3 + {0x0E6F, 0x0111}, // Afterglow PS3 + {0x0E6F, 0x0114}, // Afterglow PS3 + {0x0E6F, 0x0214}, // Afterglow PS3 + {0x0E6F, 0x0119}, // Afterglow PS3 + {0x0E6F, 0x011A}, // Afterglow PS3 + {0x0010, 0x0082}, // Akishop Customs PS360 + {0x0E6F, 0x0132}, // Battlefield 4 PS3 + {0x146B, 0x0055}, // Bigben PS3 + {0x146B, 0x0301}, // Bigben PS3 + {0x25F0, 0xC121}, // Gioteck PS3 + {0x25F0, 0xC131}, // Gioteck PS3 + {0x0F0D, 0x0049}, // Hatsune Miku / Hori PS3 + {0x0F0D, 0x0085}, // Hori Fighting Commander 2016 PS3 + {0x0F0D, 0x005F}, // Hori Fighting Commander 4 PS3 + {0x0F0D, 0x0051}, // Hori Fighting Commander PS3 + {0x0F0D, 0x0088}, // Hori Fighting Stick mini 4 PS3 + {0x0F0D, 0x006E}, // Horipad 4 PS3 + {0x0F0D, 0x006A}, // Hori Real Arcade Pro 4 PS3 + {0x0F0D, 0x006B}, // Hori Real Arcade Pro 4 PS3 (alt) + {0x0E6F, 0x0124}, // Injustice Fightstick PS3 + {0x0079, 0x0002}, // King PS3 + {0x0738, 0x8838}, // Mad Catz Arcade Fightstick TE S+ PS3 + {0x0738, 0x3285}, // Mad Catz Arcade Fightstick TE S PS3 + {0x0738, 0x3250}, // Mad Catz Fightpad Pro PS3 + {0x0738, 0x3180}, // Mad Catz FightStick Alpha PS3 + {0x0738, 0x3384}, // Mad Catz Fightstick TE S PS3 + {0x0738, 0x3481}, // Mad Catz Fightstick TE2 PS3 + {0x0738, 0xA856}, // Mad Catz PS3 + {0x0738, 0x8818}, // Mad Catz SFIV Fightstick PS3 + {0x0738, 0x3480}, // Mad Catz TE2 PS3 Fightstick + {0x0738, 0x8263}, // MLG PS3 + {0x20D6, 0x571D}, // Nyko Airflo PS3 + {0x20D6, 0x576D}, // OPP PS3 + {0x0E6F, 0x0109}, // PDP PS3 Versus Fighting + {0x0662, 0x70D5}, // PowerA PS3 (SDL DB VID) + {0x2606, 0x70D5}, // PowerA PS3 (alt VID) + {0x20D6, 0x5795}, // Pro Elite PS3 + {0x20D6, 0x319F}, // Pro Ex mini PS3 + {0x20D6, 0x57C7}, // Pro Ex mini PS3 + {0x120A, 0x0001}, // PS3 Controller (generic) + {0x120C, 0x0713}, // PS3 Controller (generic) + {0x120C, 0xF11C}, // PS3 Controller (generic) + {0x120C, 0x0EF9}, // PS3 Controller (generic) + {0x2509, 0x1801}, // PS3 Controller (generic) + {0x2509, 0x0005}, // PS3 Controller (generic) + {0x1F4F, 0x0008}, // PS3 Controller (generic) + {0x2563, 0x0575}, // SHANWAN / generic PS3 + {0x8888, 0x0308}, // PS3 Controller (generic) + {0x8888, 0x0408}, // PS3 Controller (generic) }; +// PS4 controllers (from SDL_GameControllerDB + existing) static const HardwareID PS4_IDS[] = { - {0x054C, 0x05C4}, // DS4 - {0x054C, 0x09CC}, // DS4 - {0x054C, 0x0BA0}, // DS4 wireless adapter + {0x054C, 0x05C4}, // Sony DS4 + {0x054C, 0x09CC}, // Sony DS4 + {0x054C, 0x0BA0}, // Sony DS4 wireless adapter {0x2563, 0x0357}, // MPOW Wired Gamepad (ShenZhen ShanWan) - {0x0F0D, 0x005E}, // Hori FC4 + {0x0F0D, 0x005E}, // Hori Fighting Commander 4 PS4 {0x0F0D, 0x00EE}, // Hori PS4 Mini (PS4-099U) - {0x1F4F, 0x1002} // ASW GG Xrd controller + {0x0F0D, 0x008A}, // Hori Real Arcade Pro 4 / RAP4 Kai (PS4 mode) + {0x0F0D, 0x008B}, // Hori Real Arcade Pro 4 (PS4 mode alt) + {0x1F4F, 0x1002}, // ASW GG Xrd controller + {0x9886, 0x0025}, // Astro C40 TR PS4 + {0x20D6, 0x792A}, // BDA PS4 Fightpad + {0x120C, 0x0E20}, // Brook Mars PS4 + {0x120C, 0x0E21}, // Brook Mars PS4 + {0x11C0, 0x4001}, // GameStop PS4 Fun Controller + {0x0F0D, 0x0087}, // Hori Fighting Stick mini 4 PS4 + {0x0F0D, 0x00A5}, // Hori Miku Project Diva X HD PS4 + {0x0F0D, 0x00A6}, // Hori Miku Project Diva X HD PS4 + {0x0F0D, 0x0123}, // Hori PS4 Controller Light + {0x0F0D, 0x0066}, // Horipad 4 PS4 + {0x0738, 0x8250}, // Mad Catz Fightpad Pro PS4 + {0x0738, 0x8384}, // Mad Catz Fightstick TE S PS4 + {0x0738, 0x8481}, // Mad Catz Fightstick TE2 PS4 + {0x0738, 0x8180}, // Mad Catz SFV Arcade Fightstick Alpha PS4 + {0x0738, 0x8480}, // Mad Catz TE2 PS4 Fightstick + {0x146B, 0x0611}, // Nacon Revolution 3 PS4 + {0x146B, 0x0D10}, // Nacon Revolution Infinity PS4 + {0x120C, 0x0708}, // PS4 Controller (generic) + {0x120C, 0x1E11}, // PS4 Controller (generic) + {0x120C, 0x1E12}, // PS4 Controller (generic) + {0x120C, 0x0E13}, // PS4 Controller (generic) + {0x120C, 0x0E15}, // PS4 Controller (generic) + {0x120C, 0x0E18}, // PS4 Controller (generic) + {0x120C, 0x1E18}, // PS4 Controller (generic) + {0x120C, 0x1E19}, // PS4 Controller (generic) + {0x120C, 0x0E1E}, // PS4 Controller (generic) + {0x120C, 0x57A9}, // PS4 Controller (generic) + {0x120C, 0x57AA}, // PS4 Controller (generic) + {0x120C, 0x1CF2}, // PS4 Controller (generic) + {0x120C, 0x1CF3}, // PS4 Controller (generic) + {0x120C, 0x1CF4}, // PS4 Controller (generic) + {0x120C, 0x1CF5}, // PS4 Controller (generic) + {0x120C, 0x0EF7}, // PS4 Controller (generic) + {0x120E, 0x0C12}, // PS4 Controller (generic) + {0x160E, 0x0C12}, // PS4 Controller (generic) + {0x1E1A, 0x0C12}, // PS4 Controller (generic) + {0x2C22, 0x2300}, // Qanba Obsidian Arcade Stick PS4 + {0x1532, 0x0401}, // Razer Panthera PS4 + {0x1532, 0x1100}, // Razer Raion PS4 Fightpad + {0x2E95, 0x7725}, // Scuf PS4 + {0x120C, 0x1E1C}, // SnakeByte 4S PS4 + {0x120C, 0x0E16}, // Steel Play Metaltech PS4 + {0x0079, 0x181A}, // Venom PS4 Arcade Joystick }; static const HardwareID PS5_IDS[] = { - {0x054C, 0x0CE6}, // dualsense - {0x054C, 0x0DF2} // dualsense edge + {0x054C, 0x0CE6}, // Sony DualSense (PS5) + {0x054C, 0x0DF2}, // Sony DualSense Edge (PS5) + {0x054C, 0x0E5F} // Sony PS5 Access Controller }; static const HardwareID PSCLASSIC_IDS[] = @@ -48,23 +380,65 @@ static const HardwareID PSCLASSIC_IDS[] = {0x054C, 0x0CDA} // psclassic }; +// Switch Pro / Joy-Con / Wii U Pro (from SDL_GameControllerDB + existing) static const HardwareID SWITCH_PRO_IDS[] = { - {0x057E, 0x2009}, // Switch Pro + {0x057E, 0x2009}, // Nintendo Switch Pro Controller + {0x057E, 0x0330}, // Wii U Pro + {0x057E, 0x2006}, // Joy-Con (L) + {0x057E, 0x2007}, // Joy-Con (R) + {0x057E, 0x2067}, // Joy-Con 2 (L) — same USB bulk bring-up as Pro 2 + {0x057E, 0x2066}, // Joy-Con 2 (R) + {0x057E, 0x2073}, // NSO GameCube Controller (Switch 2–family USB) // {0x20D6, 0xA711}, // OpenSteamController, emulated pro controller }; +// Switch 2 Pro only — uses Switch2ProHost (same InReport decode as SwitchProHost; see Wired_Controllers.md) +static const HardwareID SWITCH_PRO_2_IDS[] = +{ + {0x057E, 0x2069}, // Nintendo Switch 2 Pro Controller +}; + +// Wired Switch controllers (from SDL_GameControllerDB + existing) static const HardwareID SWITCH_WIRED_IDS[] = { {0x20D6, 0xA719}, // PowerA wired {0x20D6, 0xA713}, // PowerA Enhanced wired + {0x20D6, 0xA712}, // PowerA Fusion Fight Pad + {0x20D6, 0xA714}, // PowerA Spectra + {0x62DD, 0xA715}, // PowerA Fusion Arcade Stick + {0x62DD, 0xA716}, // PowerA Fusion Pro {0x0F0D, 0x0092}, // Hori Pokken Tournament Pro {0x0F0D, 0x00C1}, // Hori Pokken Horipad + {0x0F0D, 0x00F6}, // Hori Horipad Nintendo Switch + {0x0F0D, 0x0202}, // Hori Horipad O Nintendo Switch 2 + {0x0F0D, 0x00DC}, // Hori Horipad Switch + {0x0E6F, 0x0188}, // Afterglow Deluxe Nintendo Switch + {0x0E6F, 0x0184}, // Faceoff Deluxe Nintendo Switch + {0x0E6F, 0x0181}, // Faceoff Deluxe Pro Nintendo Switch + {0x0E6F, 0x0180}, // Faceoff Pro Nintendo Switch + {0x0E6F, 0x0189}, // PDP Realmz Nintendo Switch + {0x0E6F, 0x0187}, // Rock Candy Nintendo Switch + {0x146B, 0x0D08}, // Nacon Revolution Unlimited Pro + {0x146B, 0x0D01}, // Nacon Revolution Pro Controller + {0x146B, 0x0D02}, // Nacon Revolution Pro Controller 2 + {0x146B, 0x0D13}, // Nacon Revolution Pro Controller 3 + {0x11EC, 0xA7E1}, // Nintendo Switch (generic) + {0x044F, 0xD00E}, // Thrustmaster eSwap Pro Controller + {0x1DD8, 0x000B}, // Buffalo BSGP1601 (from SDL_GameControllerDB) + {0x1DD8, 0x000F}, // iBuffalo BSGP1204 + {0x1DD8, 0x0010}, // iBuffalo BSGP1204P }; static const HardwareID N64_IDS[] = { - {0x0079, 0x0006} // Retrolink N64 USB gamepad + {0x0079, 0x0006}, // Retrolink N64 USB gamepad + {0x2E24, 0x200B}, // Hyperkin Admiral N64 (from SDL_GameControllerDB) + {0x0079, 0x954E}, // Hyperkin N64 Controller Adapter + {0x0079, 0x1879}, // Mayflash N64 Adapter + {0x2F24, 0x00F4}, // Mayflash N64 Adapter + {0x057E, 0x2019}, // NSO N64 Controller + {0x1234, 0x0004}, // RetroUSB N64 RetroPort }; struct HostTypeMap @@ -81,6 +455,7 @@ static const HostTypeMap HOST_TYPE_MAP[] = { PS5_IDS, sizeof(PS5_IDS) / sizeof(HardwareID), HostDriverType::PS5 }, { PS3_IDS, sizeof(PS3_IDS) / sizeof(HardwareID), HostDriverType::PS3 }, { SWITCH_WIRED_IDS, sizeof(SWITCH_WIRED_IDS) / sizeof(HardwareID), HostDriverType::SWITCH }, + { SWITCH_PRO_2_IDS, sizeof(SWITCH_PRO_2_IDS) / sizeof(HardwareID), HostDriverType::SWITCH_PRO_2 }, { SWITCH_PRO_IDS, sizeof(SWITCH_PRO_IDS) / sizeof(HardwareID), HostDriverType::SWITCH_PRO }, { PSCLASSIC_IDS, sizeof(PSCLASSIC_IDS) / sizeof(HardwareID), HostDriverType::PSCLASSIC }, { N64_IDS, sizeof(N64_IDS) / sizeof(HardwareID), HostDriverType::N64 }, diff --git a/Firmware/RP2040/src/USBHost/HostDriver/HostDriverTypes.h b/Firmware/RP2040/src/USBHost/HostDriver/HostDriverTypes.h index 9099d191..1c06142f 100644 --- a/Firmware/RP2040/src/USBHost/HostDriver/HostDriverTypes.h +++ b/Firmware/RP2040/src/USBHost/HostDriver/HostDriverTypes.h @@ -7,6 +7,8 @@ enum class HostDriverType { UNKNOWN = 0, SWITCH_PRO, + /** Nintendo Switch 2 Pro (PID 0x2069): wired USB; Switch2ProHost maps full digital + sticks + ZL/ZR (see Wired_Controllers.md) */ + SWITCH_PRO_2, SWITCH, PSCLASSIC, DINPUT, diff --git a/Firmware/RP2040/src/USBHost/HostDriver/PS3/PS3.cpp b/Firmware/RP2040/src/USBHost/HostDriver/PS3/PS3.cpp index 075acfbb..a3e7b193 100644 --- a/Firmware/RP2040/src/USBHost/HostDriver/PS3/PS3.cpp +++ b/Firmware/RP2040/src/USBHost/HostDriver/PS3/PS3.cpp @@ -3,8 +3,55 @@ #include "host/usbh.h" #include "class/hid/hid_host.h" +#include "Board/Config.h" +#if defined(CONFIG_EN_USB_HOST) +#include "pio_usb.h" +#endif +#include "pico/time.h" + #include "USBHost/HostDriver/PS3/PS3.h" +#if defined(CONFIG_EN_BLUETOOTH) +extern "C" { +/* Do not include bt/uni_bt.h here: it pulls BTstack, whose btstack_hid.h redefines + * HID_REPORT_TYPE_* / hid_report_type_t and breaks compilation with TinyUSB hid.h. */ +typedef uint8_t uni_bt_bd_addr_t[6]; +void uni_bt_get_local_bd_addr_safe(uni_bt_bd_addr_t addr); +} +#endif + +namespace { + +struct SyncXferCtx { + bool done{false}; +}; + +void sync_xfer_cb(tuh_xfer_s* xfer) { + auto* ctx = reinterpret_cast(xfer->user_data); + ctx->done = true; +} + +#if defined(CONFIG_EN_USB_HOST) +void service_usb_host() { + pio_usb_host_frame(); + tuh_task(); +} +#else +void service_usb_host() { + tuh_task(); +} +#endif + +} // namespace + +const tusb_control_request_t PS3Host::ENABLE_USB_REQUEST = { + .bmRequestType = 0x21, + .bRequest = 0x09, // SET_REPORT + .wValue = static_cast((HID_REPORT_TYPE_FEATURE << 8) | PS3::ReportID::FEATURE_F4), + .wIndex = 0x0000, + .wLength = 4, +}; + const tusb_control_request_t PS3Host::RUMBLE_REQUEST = { .bmRequestType = 0x21, @@ -14,33 +61,61 @@ const tusb_control_request_t PS3Host::RUMBLE_REQUEST = .wLength = sizeof(PS3::OutReport) }; -void PS3Host::initialize(Gamepad& gamepad, uint8_t address, uint8_t instance, const uint8_t* report_desc, uint16_t desc_len) -{ - gamepad.set_analog_host(true); - - std::memcpy(reinterpret_cast(&out_report_), - PS3::DEFAULT_OUT_REPORT, - std::min(sizeof(PS3::OutReport), sizeof(PS3::DEFAULT_OUT_REPORT))); - - out_report_.leds_bitmap = 0x1 << (idx_ + 1); - out_report_.leds[idx_].time_enabled = 0xFF; +#if defined(CONFIG_EN_BLUETOOTH) +/* Same as bluepad32/tools/sixaxispairer — DualShock 3 stores host BD_ADDR for Bluetooth. */ +const tusb_control_request_t PS3Host::BT_MAC_FEATURE_REQUEST = { + .bmRequestType = 0x21, + .bRequest = 0x09, // SET_REPORT + .wValue = static_cast((HID_REPORT_TYPE_FEATURE << 8) | 0xF5), + .wIndex = 0x0000, + .wLength = 8, +}; - init_state_.out_report = &out_report_; - init_state_.dev_addr = address; - init_state_.init_buffer.fill(0); +bool PS3Host::local_bt_addr_is_nonzero(const uint8_t addr[6]) { + for (int i = 0; i < 6; ++i) { + if (addr[i] != 0) { + return true; + } + } + return false; +} - tusb_control_request_t init_request = - { - .bmRequestType = 0xA1, - .bRequest = 0x01, // GET_REPORT - .wValue = (HID_REPORT_TYPE_FEATURE << 8) | 0xF2, - .wIndex = 0x0000, - .wLength = 17 - }; +bool PS3Host::program_ds3_bt_host(uint8_t address) { + uni_bt_bd_addr_t addr{}; + uni_bt_get_local_bd_addr_safe(addr); + if (!local_bt_addr_is_nonzero(addr)) { + return false; + } + bt_pair_buf_[0] = 0xF5; + bt_pair_buf_[1] = 0; + std::memcpy(bt_pair_buf_.data() + 2, addr, 6); + /* Sync transfer: PIO USB needs pio_usb_host_frame() while control completes. */ + (void)send_control_xfer_wait(address, &BT_MAC_FEATURE_REQUEST, bt_pair_buf_.data(), 150u); + return true; +} - send_control_xfer(address, &init_request, init_state_.init_buffer.data(), get_report_complete_cb, reinterpret_cast(&init_state_)); +void PS3Host::try_deferred_ds3_bt_pair(uint8_t address) { + if (!ds3_bt_pair_deferred_) { + return; + } + if (program_ds3_bt_host(address)) { + ds3_bt_pair_deferred_ = false; + } +} +#endif - tuh_hid_receive_report(address, instance); +void PS3Host::init_host_out_report(PS3::OutReport& out, uint8_t player_idx) +{ + /* USB Host Shield PS3_REPORT_BUFFER layout — not gadget DEFAULT_OUT_REPORT (leading 0x01). */ + std::memset(&out, 0, sizeof(out)); + for (int i = 0; i < 4; ++i) { + out.leds[i].time_enabled = 0xFF; + out.leds[i].duty_length = 0x27; + out.leds[i].enabled = 0x10; + out.leds[i].duty_off = 0x00; + out.leds[i].duty_on = 0x32; + } + out.leds_bitmap = static_cast(0x02u << player_idx); } bool PS3Host::send_control_xfer(uint8_t dev_addr, const tusb_control_request_t* request, uint8_t* buffer, tuh_xfer_cb_t complete_cb, uintptr_t user_data) @@ -57,46 +132,67 @@ bool PS3Host::send_control_xfer(uint8_t dev_addr, const tusb_control_request_t* return tuh_control_xfer(&transfer); } -void PS3Host::get_report_complete_cb(tuh_xfer_s *xfer) +bool PS3Host::send_control_xfer_wait(uint8_t dev_addr, const tusb_control_request_t* request, uint8_t* buffer, + uint32_t max_attempts_ms) { - InitState* init_state = reinterpret_cast(xfer->user_data); - if (init_state == nullptr || init_state->stage == InitStage::DONE) - { - return; + SyncXferCtx ctx; + if (!send_control_xfer(dev_addr, request, buffer, sync_xfer_cb, reinterpret_cast(&ctx))) { + return false; } + for (uint32_t i = 0; !ctx.done && i < max_attempts_ms; ++i) { + service_usb_host(); + sleep_ms(1); + } + return ctx.done; +} - tusb_control_request_t init_request = - { - .bmRequestType = 0xA1, - .bRequest = 0x01, // GET_REPORT - .wValue = (HID_REPORT_TYPE_FEATURE << 8) | 0xF2, - .wIndex = 0x0000, - .wLength = 17 - }; +bool PS3Host::send_control_output_sync(uint8_t address, PS3::OutReport* report) +{ + return send_control_xfer_wait(address, &RUMBLE_REQUEST, reinterpret_cast(report), 150u); +} - switch (init_state->stage) - { - case InitStage::RESP1: - init_state->stage = InitStage::RESP2; - send_control_xfer(init_state->dev_addr, &init_request, init_state->init_buffer.data(), get_report_complete_cb, xfer->user_data); - break; - case InitStage::RESP2: - init_state->stage = InitStage::RESP3; - init_request.wLength = 8; - send_control_xfer(init_state->dev_addr, &init_request, init_state->init_buffer.data(), get_report_complete_cb, xfer->user_data); - break; - case InitStage::RESP3: - init_state->stage = InitStage::DONE; - init_state->reports_enabled = true; - send_control_xfer(init_state->dev_addr, &PS3Host::RUMBLE_REQUEST, reinterpret_cast(init_state->out_report), nullptr, 0); - break; - default: - break; +bool PS3Host::send_control_output_async(uint8_t address, PS3::OutReport* report) +{ + return send_control_xfer(address, &RUMBLE_REQUEST, reinterpret_cast(report), nullptr, 0); +} + +void PS3Host::initialize(Gamepad& gamepad, uint8_t address, uint8_t instance, const uint8_t* report_desc, uint16_t desc_len) +{ + (void)report_desc; + (void)desc_len; + + gamepad.set_analog_host(true); + + init_host_out_report(out_report_, idx_); + reports_enabled_ = false; +#if defined(CONFIG_EN_BLUETOOTH) + ds3_bt_pair_deferred_ = false; +#endif + + /* DualShock 3 USB: SET feature 0xF4 {0x42,0x0c,0,0} then one LED OUT (USB Host Shield). */ + uint8_t enable_usb_data[] = {0x42, 0x0c, 0x00, 0x00}; + (void)send_control_xfer_wait(address, &ENABLE_USB_REQUEST, enable_usb_data, 150u); + (void)send_control_output_sync(address, &out_report_); + +#if defined(CONFIG_EN_BLUETOOTH) + /* Program master BD_ADDR so the pad can reconnect wirelessly after unplug (sixaxispairer). + * Must run here — the old GET 0xF2 async chain was skipped once wired init marked DONE. */ + if (!program_ds3_bt_host(address)) { + ds3_bt_pair_deferred_ = true; } +#endif + + reports_enabled_ = true; + last_keepalive_ms_ = time_us_32() / 1000; + + tuh_hid_receive_report(address, instance); } void PS3Host::process_report(Gamepad& gamepad, uint8_t address, uint8_t instance, const uint8_t* report, uint16_t len) { +#if defined(CONFIG_EN_BLUETOOTH) + try_deferred_ds3_bt_pair(address); +#endif const PS3::InReport* in_report = reinterpret_cast(report); if (std::memcmp(&prev_in_report_, in_report, std::min(static_cast(len), static_cast(26))) == 0) { @@ -151,25 +247,28 @@ void PS3Host::process_report(Gamepad& gamepad, uint8_t address, uint8_t instance bool PS3Host::send_feedback(Gamepad& gamepad, uint8_t address, uint8_t instance) { - static uint32_t last_rumble_ms = 0; + (void)instance; - uint32_t current_ms = time_us_32() / 1000; - - //Spamming set_report doesn't work, limit the rate - if (init_state_.reports_enabled && - current_ms - last_rumble_ms >= 300) - { - Gamepad::PadOut gp_out = gamepad.get_pad_out(); + if (!reports_enabled_) { + return false; + } - out_report_.rumble.right_duration = (gp_out.rumble_r > 0) ? 20 : 0; - out_report_.rumble.right_motor_on = (gp_out.rumble_r > 0) ? 1 : 0; + const uint32_t current_ms = time_us_32() / 1000; + Gamepad::PadOut gp_out = gamepad.get_pad_out(); + const bool has_rumble = (gp_out.rumble_l != 0 || gp_out.rumble_r != 0); + const bool keepalive_due = + KEEPALIVE_MS != 0u && (current_ms - last_keepalive_ms_ >= KEEPALIVE_MS); - out_report_.rumble.left_duration = (gp_out.rumble_l > 0) ? 20 : 0; - out_report_.rumble.left_motor_force = gp_out.rumble_l; + if (!has_rumble && !gamepad.new_pad_out() && !keepalive_due) { + return true; + } - last_rumble_ms = current_ms; + out_report_.rumble.right_duration = (gp_out.rumble_r > 0) ? 20 : 0; + out_report_.rumble.right_motor_on = (gp_out.rumble_r > 0) ? 1 : 0; + out_report_.rumble.left_duration = (gp_out.rumble_l > 0) ? 20 : 0; + out_report_.rumble.left_motor_force = gp_out.rumble_l; + out_report_.leds_bitmap = static_cast(0x02u << idx_); - return send_control_xfer(address, &PS3Host::RUMBLE_REQUEST, reinterpret_cast(&out_report_), nullptr, 0); - } - return true; -} \ No newline at end of file + last_keepalive_ms_ = current_ms; + return send_control_output_async(address, &out_report_); +} diff --git a/Firmware/RP2040/src/USBHost/HostDriver/PS3/PS3.h b/Firmware/RP2040/src/USBHost/HostDriver/PS3/PS3.h index 12655dde..3f88feb1 100644 --- a/Firmware/RP2040/src/USBHost/HostDriver/PS3/PS3.h +++ b/Firmware/RP2040/src/USBHost/HostDriver/PS3/PS3.h @@ -20,25 +20,39 @@ class PS3Host : public HostDriver bool send_feedback(Gamepad& gamepad, uint8_t address, uint8_t instance) override; private: - enum class InitStage { RESP1, RESP2, RESP3, DONE }; - - struct InitState - { - uint8_t dev_addr{0xFF}; - InitStage stage{InitStage::RESP1}; - std::array init_buffer; - PS3::OutReport* out_report{nullptr}; - bool reports_enabled{false}; - }; - + static void init_host_out_report(PS3::OutReport& out, uint8_t player_idx); + static bool send_control_xfer_wait(uint8_t dev_addr, const tusb_control_request_t* req, uint8_t* buffer, + uint32_t max_attempts_ms); + static bool send_control_output_sync(uint8_t address, PS3::OutReport* report); + static bool send_control_output_async(uint8_t address, PS3::OutReport* report); + static bool send_control_xfer(uint8_t dev_addr, const tusb_control_request_t* req, uint8_t* buffer, + tuh_xfer_cb_t complete_cb, uintptr_t user_data); + + static const tusb_control_request_t ENABLE_USB_REQUEST; static const tusb_control_request_t RUMBLE_REQUEST; +#if defined(CONFIG_EN_BLUETOOTH) + /** HID feature report: program DualShock 3 master Bluetooth address (see Bluepad32 sixaxispairer). */ + static const tusb_control_request_t BT_MAC_FEATURE_REQUEST; + static bool local_bt_addr_is_nonzero(const uint8_t addr[6]); + /** SET feature 0xF5 with local BD_ADDR; returns false if address not ready yet. */ + bool program_ds3_bt_host(uint8_t address); + void try_deferred_ds3_bt_pair(uint8_t address); +#endif PS3::InReport prev_in_report_; PS3::OutReport out_report_; - InitState init_state_; - - static bool send_control_xfer(uint8_t dev_addr, const tusb_control_request_t* req, uint8_t* buffer, tuh_xfer_cb_t complete_cb, uintptr_t user_data); - static void get_report_complete_cb(tuh_xfer_s *xfer); + bool reports_enabled_{false}; + uint32_t last_keepalive_ms_{0}; +#if defined(CONFIG_EN_USB_HOST) + static constexpr uint32_t KEEPALIVE_MS = 1000u; +#else + static constexpr uint32_t KEEPALIVE_MS = 0u; +#endif +#if defined(CONFIG_EN_BLUETOOTH) + /** Persistent buffer for SET_REPORT (feature 0xF5). */ + std::array bt_pair_buf_{}; + bool ds3_bt_pair_deferred_{false}; +#endif }; -#endif // _PS3_HOST_H_ \ No newline at end of file +#endif // _PS3_HOST_H_ diff --git a/Firmware/RP2040/src/USBHost/HostDriver/PS4/PS4.cpp b/Firmware/RP2040/src/USBHost/HostDriver/PS4/PS4.cpp index 8b8b39d8..aecf3d29 100644 --- a/Firmware/RP2040/src/USBHost/HostDriver/PS4/PS4.cpp +++ b/Firmware/RP2040/src/USBHost/HostDriver/PS4/PS4.cpp @@ -4,23 +4,56 @@ #include "host/usbh.h" #include "class/hid/hid_host.h" +#include "Board/Config.h" +#if defined(CONFIG_EN_USB_HOST) +#include "pio_usb.h" +#endif + #include "USBHost/HostDriver/PS4/PS4.h" +#include "Gamepad/MotionImu.h" void PS4Host::initialize(Gamepad& gamepad, uint8_t address, uint8_t instance, const uint8_t* report_desc, uint16_t desc_len) { + (void)gamepad; + (void)report_desc; + (void)desc_len; + out_report_.report_id = 0x05; out_report_.set_led = 1; out_report_.lightbar_blue = 0xFF / 2; + /* PS5Host pushes an initial OUT report; PS4 did not. Composite XInput+HID pads often need this + * before IN starts. Periodic send_feedback only runs when new_pad_out/has_rumble, so the LED + * and input path never woke when we stopped using XInput for feedback on these devices. */ + uint32_t attempts = 0; + while (!tuh_hid_send_report(address, instance, 0, reinterpret_cast(&out_report_), sizeof(PS4::OutReport))) + { +#if defined(CONFIG_EN_USB_HOST) + pio_usb_host_frame(); +#endif + tuh_task(); + if (++attempts > 4000u) + { + break; + } + } + tuh_hid_receive_report(address, instance); } void PS4Host::process_report(Gamepad& gamepad, uint8_t address, uint8_t instance, const uint8_t* report, uint16_t len) { std::memcpy(&in_report_, report, std::min(static_cast(len), sizeof(PS4::InReport))); - in_report_.buttons[2] &= PS4::COUNTER_MASK; - if (std::memcmp(reinterpret_cast(report), &prev_in_report_, sizeof(PS4::InReport)) == 0) + in_report_.buttons[2] &= static_cast(~PS4::COUNTER_MASK); + + bool imu_same = true; + if (len >= 25) { + imu_same = std::memcmp(&report[13], prev_imu_, sizeof(prev_imu_)) == 0; + } + if (std::memcmp(reinterpret_cast(report), &prev_in_report_, sizeof(PS4::InReport)) == 0 && + imu_same) { + tuh_hid_receive_report(address, instance); return; } @@ -75,10 +108,18 @@ void PS4Host::process_report(Gamepad& gamepad, uint8_t address, uint8_t instance std::tie(gp_in.joystick_lx, gp_in.joystick_ly) = gamepad.scale_joystick_l(in_report_.joystick_lx, in_report_.joystick_ly); std::tie(gp_in.joystick_rx, gp_in.joystick_ry) = gamepad.scale_joystick_r(in_report_.joystick_rx, in_report_.joystick_ry); + if (len >= 25) { + gp_in.motion_source = Gamepad::PadIn::MOTION_SRC_DS4_USB; + MotionImu::fill_from_ds4_usb_report(gp_in.accel, gp_in.gyro, report); + } + gamepad.set_pad_in(gp_in); tuh_hid_receive_report(address, instance); std::memcpy(&prev_in_report_, &in_report_, sizeof(PS4::InReport)); + if (len >= 25) { + std::memcpy(prev_imu_, &report[13], sizeof(prev_imu_)); + } } bool PS4Host::send_feedback(Gamepad& gamepad, uint8_t address, uint8_t instance) diff --git a/Firmware/RP2040/src/USBHost/HostDriver/PS4/PS4.h b/Firmware/RP2040/src/USBHost/HostDriver/PS4/PS4.h index f12547d2..81b61227 100644 --- a/Firmware/RP2040/src/USBHost/HostDriver/PS4/PS4.h +++ b/Firmware/RP2040/src/USBHost/HostDriver/PS4/PS4.h @@ -19,6 +19,7 @@ class PS4Host : public HostDriver private: PS4::InReport in_report_{}; PS4::InReport prev_in_report_{}; + uint8_t prev_imu_[12]{}; PS4::OutReport out_report_{}; }; diff --git a/Firmware/RP2040/src/USBHost/HostDriver/PS5/PS5.cpp b/Firmware/RP2040/src/USBHost/HostDriver/PS5/PS5.cpp index 0389b787..2f0a27d6 100644 --- a/Firmware/RP2040/src/USBHost/HostDriver/PS5/PS5.cpp +++ b/Firmware/RP2040/src/USBHost/HostDriver/PS5/PS5.cpp @@ -3,7 +3,11 @@ #include "host/usbh.h" #include "class/hid/hid_host.h" +#include "USBDevice/DeviceDriver/DeviceDriverTypes.h" +#include "USBDevice/DeviceDriver/Steam/SteamPassthrough.h" +#include "USBDevice/DeviceManager.h" #include "USBHost/HostDriver/PS5/PS5.h" +#include "Gamepad/MotionImu.h" void PS5Host::initialize(Gamepad& gamepad, uint8_t address, uint8_t instance, const uint8_t* report_desc, uint16_t desc_len) { @@ -34,12 +38,9 @@ void PS5Host::process_report(Gamepad& gamepad, uint8_t address, uint8_t instance { const PS5::InReport* in_report = reinterpret_cast(report); - if (std::memcmp(&prev_in_report_.joystick_lx, &in_report->joystick_lx, sizeof(uint8_t) * 6) == 0 && - std::memcmp(prev_in_report_.buttons, in_report->buttons, sizeof(in_report->buttons)) == 0) - { - tuh_hid_receive_report(address, instance); - return; - } + // Do not skip "unchanged" reports: polled outputs (e.g. OG Xbox) need every report so + // quick transitions and sustained input are not dropped when the host loop is slower + // than the DualSense report rate or when comparing only a subset of the report. Gamepad::PadIn gp_in; @@ -86,12 +87,25 @@ void PS5Host::process_report(Gamepad& gamepad, uint8_t address, uint8_t instance if (in_report->buttons[2] & PS5::Buttons2::PS) gp_in.buttons |= gamepad.MAP_BUTTON_SYS; if (in_report->buttons[2] & PS5::Buttons2::MUTE) gp_in.buttons |= gamepad.MAP_BUTTON_MISC; + if (DeviceManager::get_instance().get_driver_type() == DeviceDriverType::STEAM) { + SteamPassthrough::input_has_touchpad = true; + SteamPassthrough::store(report, len); + } + + if (in_report->points[0].touching || in_report->points[1].touching) { + gp_in.buttons |= gamepad.MAP_BUTTON_MISC; + } + gp_in.trigger_l = gamepad.scale_trigger_l(in_report->trigger_l); gp_in.trigger_r = gamepad.scale_trigger_r(in_report->trigger_r); std::tie(gp_in.joystick_lx, gp_in.joystick_ly) = gamepad.scale_joystick_l(in_report->joystick_lx, in_report->joystick_ly); std::tie(gp_in.joystick_rx, gp_in.joystick_ry) = gamepad.scale_joystick_r(in_report->joystick_rx, in_report->joystick_ry); - + + /* DualSense USB: normalize raw HID IMU to Bluepad DS4 units for PS3/PS4 gadget output. */ + gp_in.motion_source = Gamepad::PadIn::MOTION_SRC_DS5_USB; + MotionImu::fill_from_ds5_usb_raw(gp_in.accel, gp_in.gyro, in_report->accel, in_report->gyro); + gamepad.set_pad_in(gp_in); tuh_hid_receive_report(address, instance); diff --git a/Firmware/RP2040/src/USBHost/HostDriver/SwitchPro/Switch2ProCaptureTables.h b/Firmware/RP2040/src/USBHost/HostDriver/SwitchPro/Switch2ProCaptureTables.h new file mode 100644 index 00000000..494f791e --- /dev/null +++ b/Firmware/RP2040/src/USBHost/HostDriver/SwitchPro/Switch2ProCaptureTables.h @@ -0,0 +1,126 @@ +/* Reference only — firmware does not include this header (hidraw XOR bytes 16–17 drift with IMU). + * Autogenerated from Nintendo_Switch_2_Pro_Controller_20260324_063034.txt + * Sparse (idx, mask, val): for each changed byte i in [2,20], mask = baseline^active, + * val = active[i] & mask. Match: (report[i] & mask) == val for all tuples. + * Regenerate: Tools/controller_capture (same JSON extraction as Switch2ProCaptureTables). */ +#pragma once +#include + +namespace Switch2ProCapture { + +enum class Btn : uint8_t { + FACE_B = 0, + FACE_A = 1, + FACE_Y = 2, + FACE_X = 3, + SHOULDER_R = 4, + TRIGGER_ZR = 5, + SHOULDER_L = 6, + TRIGGER_ZL = 7, + CENTER_MINUS = 8, + CENTER_PLUS = 9, + CENTER_HOME = 10, + CENTER_CAPTURE = 11, + DPAD_UP = 12, + DPAD_DOWN = 13, + DPAD_LEFT = 14, + DPAD_RIGHT = 15, + STICK_L3 = 16, + STICK_R3 = 17, + COUNT +}; + +static constexpr uint8_t CAPTURE_BYTE_LO = 2; +static constexpr uint8_t CAPTURE_BYTE_HI = 20; + +static constexpr uint8_t FACE_B_N = 4; +static constexpr uint8_t FACE_B_IDX[4] = {7, 9, 16, 17}; +static constexpr uint8_t FACE_B_MASK[4] = {0x70, 0x07, 0x62, 0x05}; +static constexpr uint8_t FACE_B_VAL[4] = {0x40, 0x03, 0x20, 0x04}; + +static constexpr uint8_t FACE_A_N = 3; +static constexpr uint8_t FACE_A_IDX[3] = {6, 16, 17}; +static constexpr uint8_t FACE_A_MASK[3] = {0x01, 0xd5, 0x02}; +static constexpr uint8_t FACE_A_VAL[3] = {0x01, 0x10, 0x00}; + +static constexpr uint8_t FACE_Y_N = 3; +static constexpr uint8_t FACE_Y_IDX[3] = {6, 16, 17}; +static constexpr uint8_t FACE_Y_MASK[3] = {0x01, 0xb3, 0x7f}; +static constexpr uint8_t FACE_Y_VAL[3] = {0x00, 0x33, 0x3d}; + +static constexpr uint8_t FACE_X_N = 5; +static constexpr uint8_t FACE_X_IDX[5] = {6, 9, 10, 16, 17}; +static constexpr uint8_t FACE_X_MASK[5] = {0x01, 0x07, 0x70, 0xd4, 0x07}; +static constexpr uint8_t FACE_X_VAL[5] = {0x00, 0x04, 0x40, 0x94, 0x06}; + +static constexpr uint8_t SHOULDER_R_N = 4; +static constexpr uint8_t SHOULDER_R_IDX[4] = {6, 10, 16, 17}; +static constexpr uint8_t SHOULDER_R_MASK[4] = {0x01, 0x70, 0x43, 0x07}; +static constexpr uint8_t SHOULDER_R_VAL[4] = {0x00, 0x30, 0x02, 0x07}; + +static constexpr uint8_t TRIGGER_ZR_N = 4; +static constexpr uint8_t TRIGGER_ZR_IDX[4] = {6, 10, 16, 17}; +static constexpr uint8_t TRIGGER_ZR_MASK[4] = {0x01, 0x70, 0xcb, 0x09}; +static constexpr uint8_t TRIGGER_ZR_VAL[4] = {0x00, 0x40, 0x02, 0x09}; + +static constexpr uint8_t SHOULDER_L_N = 3; +static constexpr uint8_t SHOULDER_L_IDX[3] = {6, 16, 17}; +static constexpr uint8_t SHOULDER_L_MASK[3] = {0x01, 0x38, 0x75}; +static constexpr uint8_t SHOULDER_L_VAL[3] = {0x01, 0x08, 0x35}; + +static constexpr uint8_t TRIGGER_ZL_N = 3; +static constexpr uint8_t TRIGGER_ZL_IDX[3] = {6, 16, 17}; +static constexpr uint8_t TRIGGER_ZL_MASK[3] = {0x01, 0x3a, 0x0a}; +static constexpr uint8_t TRIGGER_ZL_VAL[3] = {0x01, 0x3a, 0x00}; + +static constexpr uint8_t CENTER_MINUS_N = 6; +static constexpr uint8_t CENTER_MINUS_IDX[6] = {6, 7, 9, 10, 16, 17}; +static constexpr uint8_t CENTER_MINUS_MASK[6] = {0x01, 0x70, 0x07, 0x70, 0x92, 0x77}; +static constexpr uint8_t CENTER_MINUS_VAL[6] = {0x00, 0x30, 0x04, 0x30, 0x80, 0x36}; + +static constexpr uint8_t CENTER_PLUS_N = 4; +static constexpr uint8_t CENTER_PLUS_IDX[4] = {6, 7, 16, 17}; +static constexpr uint8_t CENTER_PLUS_MASK[4] = {0x01, 0x70, 0xbc, 0x08}; +static constexpr uint8_t CENTER_PLUS_VAL[4] = {0x00, 0x30, 0xb4, 0x00}; + +static constexpr uint8_t CENTER_HOME_N = 3; +static constexpr uint8_t CENTER_HOME_IDX[3] = {9, 16, 17}; +static constexpr uint8_t CENTER_HOME_MASK[3] = {0x07, 0x37, 0x0b}; +static constexpr uint8_t CENTER_HOME_VAL[3] = {0x03, 0x36, 0x08}; + +static constexpr uint8_t CENTER_CAPTURE_N = 5; +static constexpr uint8_t CENTER_CAPTURE_IDX[5] = {6, 9, 10, 16, 17}; +static constexpr uint8_t CENTER_CAPTURE_MASK[5] = {0x12, 0x07, 0x10, 0x0d, 0x06}; +static constexpr uint8_t CENTER_CAPTURE_VAL[5] = {0x02, 0x03, 0x00, 0x05, 0x06}; + +static constexpr uint8_t DPAD_UP_N = 7; +static constexpr uint8_t DPAD_UP_IDX[7] = {6, 7, 8, 9, 10, 16, 17}; +static constexpr uint8_t DPAD_UP_MASK[7] = {0x07, 0x70, 0x07, 0xd4, 0x40, 0x22, 0x0e}; +static constexpr uint8_t DPAD_UP_VAL[7] = {0x03, 0x00, 0x04, 0x84, 0x40, 0x00, 0x02}; + +static constexpr uint8_t DPAD_DOWN_N = 4; +static constexpr uint8_t DPAD_DOWN_IDX[4] = {7, 10, 16, 17}; +static constexpr uint8_t DPAD_DOWN_MASK[4] = {0xf0, 0x30, 0x86, 0x0e}; +static constexpr uint8_t DPAD_DOWN_VAL[4] = {0x70, 0x20, 0x04, 0x0a}; + +static constexpr uint8_t DPAD_LEFT_N = 3; +static constexpr uint8_t DPAD_LEFT_IDX[3] = {7, 16, 17}; +static constexpr uint8_t DPAD_LEFT_MASK[3] = {0x10, 0x20, 0x7d}; +static constexpr uint8_t DPAD_LEFT_VAL[3] = {0x10, 0x00, 0x31}; + +static constexpr uint8_t DPAD_RIGHT_N = 5; +static constexpr uint8_t DPAD_RIGHT_IDX[5] = {4, 9, 10, 16, 17}; +static constexpr uint8_t DPAD_RIGHT_MASK[5] = {0x02, 0x01, 0x30, 0x7a, 0x04}; +static constexpr uint8_t DPAD_RIGHT_VAL[5] = {0x02, 0x01, 0x20, 0x0a, 0x04}; + +static constexpr uint8_t STICK_L3_N = 5; +static constexpr uint8_t STICK_L3_IDX[5] = {6, 7, 10, 16, 17}; +static constexpr uint8_t STICK_L3_MASK[5] = {0x03, 0x70, 0x30, 0x3a, 0x07}; +static constexpr uint8_t STICK_L3_VAL[5] = {0x02, 0x30, 0x10, 0x28, 0x06}; + +static constexpr uint8_t STICK_R3_N = 6; +static constexpr uint8_t STICK_R3_IDX[6] = {6, 7, 8, 9, 16, 17}; +static constexpr uint8_t STICK_R3_MASK[6] = {0x3f, 0xa0, 0xf2, 0x07, 0x86, 0x75}; +static constexpr uint8_t STICK_R3_VAL[6] = {0x14, 0x20, 0x82, 0x03, 0x82, 0x35}; + +} // namespace Switch2ProCapture diff --git a/Firmware/RP2040/src/USBHost/HostDriver/SwitchPro/Switch2ProHost.cpp b/Firmware/RP2040/src/USBHost/HostDriver/SwitchPro/Switch2ProHost.cpp new file mode 100644 index 00000000..80147650 --- /dev/null +++ b/Firmware/RP2040/src/USBHost/HostDriver/SwitchPro/Switch2ProHost.cpp @@ -0,0 +1,200 @@ +#include +#include + +#include "host/usbh.h" +#include "class/hid/hid_host.h" + +#include "USBHost/HostDriver/SwitchPro/Switch2ProHost.h" + +#include "Board/ogxm_log.h" +#include "Gamepad/MotionImu.h" + +namespace { + +/** Same 10-byte payload resolution as SwitchProHost (report id 0x09 / 0x30 / 0x31 + timer, or raw 10). */ +const uint8_t* switch2_payload(const uint8_t* report, uint16_t len, uint16_t& out_payload_len) +{ + out_payload_len = len; + if (len == sizeof(SwitchPro::InReport)) + { + return report; + } + if (len >= 2U + sizeof(SwitchPro::InReport)) + { + const uint8_t rid = report[0]; + if (rid == SwitchPro::REPORT_ID_STANDARD || rid == SwitchPro::REPORT_ID_SWITCH2_FULL || + rid == SwitchPro::REPORT_ID_FULL_ALT) + { + out_payload_len = static_cast(len - 2U); + return report + 2; + } + } + return nullptr; +} + +} // namespace + +void Switch2ProHost::process_report(Gamepad& gamepad, uint8_t address, uint8_t instance, const uint8_t* report, + uint16_t len) +{ + if (switch2_bringup_active_ || init_state_ != InitState::DONE) + { + SwitchProHost::process_report(gamepad, address, instance, report, len); + return; + } + +#ifdef OGXM_SWITCH2_HID_RAW_LOG + if (len > 0) + { + const std::uint16_t n = len < kSwitch2RawLogCap ? len : kSwitch2RawLogCap; + // Log only when the three digital button bytes change. Timer (byte1), sticks (6–11), and IMU (12+) + // all move nearly every packet and will flood UART if included in the compare. + std::uint8_t btn[3]{}; + bool have_btn = false; + if (len >= 2U + sizeof(SwitchPro::InReport)) + { + const std::uint8_t rid = report[0]; + if ((rid == SwitchPro::REPORT_ID_STANDARD || rid == SwitchPro::REPORT_ID_SWITCH2_FULL || + rid == SwitchPro::REPORT_ID_FULL_ALT) && + len >= 6U) + { + btn[0] = report[3]; + btn[1] = report[4]; + btn[2] = report[5]; + have_btn = true; + } + } + else if (len == sizeof(SwitchPro::InReport)) + { + btn[0] = report[1]; + btn[1] = report[2]; + btn[2] = report[3]; + have_btn = true; + } + const bool btn_changed = + have_btn && + (!switch2_dbg_have_btn_ || std::memcmp(btn, switch2_dbg_prev_btn_, sizeof(btn)) != 0); + const bool len_changed = (switch2_dbg_prev_len_ != n); + if (have_btn && (btn_changed || len_changed)) + { + OGXM_LOG("S2Pro HID len=%u (3 digital btn bytes changed; full dump follows)\n", static_cast(len)); + OGXM_LOG_HEX(report, n); + std::memcpy(switch2_dbg_prev_, report, n); + switch2_dbg_prev_len_ = n; + std::memcpy(switch2_dbg_prev_btn_, btn, sizeof(btn)); + switch2_dbg_have_btn_ = true; + } + } +#endif + + uint16_t payload_len = 0; + const uint8_t* payload = switch2_payload(report, len, payload_len); + if (payload == nullptr || payload_len < sizeof(SwitchPro::InReport)) + { + tuh_hid_receive_report(address, instance); + return; + } + + const SwitchPro::InReport* in_report = reinterpret_cast(payload); + if (std::memcmp(&prev_in_report_.buttons, in_report->buttons, 9) == 0 && + MotionImu::switch_usb_imu_unchanged(prev_imu_, payload, payload_len)) + { + tuh_hid_receive_report(address, instance); + return; + } + + Gamepad::PadIn gp_in{}; + + // Switch 2 Pro (USB 0x2069): same 10-byte InReport offsets as the original Pro, but the three + // "button" bytes do not follow classic Buttons0/1/2 semantics. Layout from maintainer capture. + const uint8_t br = in_report->buttons[0]; + const uint8_t bm = in_report->buttons[1]; + const uint8_t bl = in_report->buttons[2]; + + // Face: hardware order is B,A,Y,X on bits 0–3 (classic Pro uses Y,X,B,A on the same bits). + if (br & (1U << 2)) + gp_in.buttons |= gamepad.MAP_BUTTON_X; // west → Xbox X + if (br & (1U << 0)) + gp_in.buttons |= gamepad.MAP_BUTTON_A; // south → Xbox A + if (br & (1U << 1)) + gp_in.buttons |= gamepad.MAP_BUTTON_B; // east → Xbox B + if (br & (1U << 3)) + gp_in.buttons |= gamepad.MAP_BUTTON_Y; // north → Xbox Y + + // Bit 4 in first byte: hardware RB (capture: pressed 0x10, released 0x00 in bytes [3][4][5]). + if (br & (1U << 4)) + gp_in.buttons |= gamepad.MAP_BUTTON_RB; + + // Right byte: classic R is Start; classic ZR bit is stick L3 on Switch 2 Pro (not R3). + if (br & SwitchPro::Buttons0::R) + gp_in.buttons |= gamepad.MAP_BUTTON_START; + if (br & SwitchPro::Buttons0::ZR) + gp_in.buttons |= gamepad.MAP_BUTTON_L3; + + // Bit 5 in misc byte: hardware ZL digital (capture: 00 20 00 vs released; classic CAPTURE bit). + // Bit 5 in first byte: hardware ZR / RT digital (capture: 20 00 00 vs released). + // Analog travel may exist elsewhere in the 64-byte report; these are full-digital paths. + const bool zl_on = (bm & (1U << 5)) != 0; + const bool zr_on = (br & (1U << 5)) != 0; + + // Bit 6 in misc byte: hardware Minus (−) → Back/Select (capture: 00 40 00 vs released 00 00 00). + if (bm & (1U << 6)) + gp_in.buttons |= gamepad.MAP_BUTTON_BACK; + + // Bit 7 in misc byte: hardware R3 (was mapped to L3; swapped to match physical sticks). + if (bm & (1U << 7)) + gp_in.buttons |= gamepad.MAP_BUTTON_R3; + + // "Misc" byte: d-pad uses some classic − / + / L3 / R3 bit positions; L uses the Home bit. + // L3 here is d-pad left (capture: 00 04 00); stick R3 is bm bit 7 above. + if (bm & SwitchPro::Buttons1::MINUS) + gp_in.dpad |= gamepad.MAP_DPAD_DOWN; + if (bm & SwitchPro::Buttons1::L3) + gp_in.dpad |= gamepad.MAP_DPAD_LEFT; + if (bm & SwitchPro::Buttons1::PLUS) + gp_in.dpad |= gamepad.MAP_DPAD_RIGHT; + if (bm & SwitchPro::Buttons1::R3) + gp_in.dpad |= gamepad.MAP_DPAD_UP; + if (bm & SwitchPro::Buttons1::HOME) + gp_in.buttons |= gamepad.MAP_BUTTON_LB; + + // "Left" byte: Home on classic d-pad-up bit; Capture shares classic d-pad-down (left unmapped). + // + // Reference only — GL / GR / Chat (intentionally unmapped), all in bl: GL bit 3 (00 00 08); + // GR bit 2 (00 00 04); Chat bit 4 (00 00 10); released 00 00 00. + if (bl & SwitchPro::Buttons2::DPAD_UP) + gp_in.buttons |= gamepad.MAP_BUTTON_SYS; + + gp_in.trigger_l = zl_on ? Range::MAX : Range::MIN; + gp_in.trigger_r = zr_on ? Range::MAX : Range::MIN; + + uint16_t joy_lx = in_report->joysticks[0] | ((in_report->joysticks[1] & 0xF) << 8); + uint16_t joy_ly = (in_report->joysticks[1] >> 4) | (in_report->joysticks[2] << 4); + uint16_t joy_rx = in_report->joysticks[3] | ((in_report->joysticks[4] & 0xF) << 8); + uint16_t joy_ry = (in_report->joysticks[4] >> 4) | (in_report->joysticks[5] << 4); + + std::tie(gp_in.joystick_lx, gp_in.joystick_ly) = + gamepad.scale_joystick_l(normalize_axis(joy_lx), normalize_axis(joy_ly), true); + + std::tie(gp_in.joystick_rx, gp_in.joystick_ry) = + gamepad.scale_joystick_r(normalize_axis(joy_rx), normalize_axis(joy_ry), true); + + if (payload_len >= 22) { + gp_in.motion_source = Gamepad::PadIn::MOTION_SRC_SWITCH_USB; + MotionImu::fill_from_switch_usb_payload(gp_in.accel, gp_in.gyro, payload, payload_len); + } + + gamepad.set_pad_in(gp_in); + std::memcpy(&prev_in_report_, in_report, sizeof(SwitchPro::InReport)); + if (payload_len >= 22) { + constexpr uint16_t kInReportSize = 10; + constexpr uint16_t kImuSampleSize = sizeof(MotionImu::SwitchImuSample); + uint16_t imu_off = kInReportSize; + if (payload_len >= kInReportSize + (3 * kImuSampleSize)) { + imu_off = static_cast(kInReportSize + (2 * kImuSampleSize)); + } + std::memcpy(prev_imu_, payload + imu_off, sizeof(prev_imu_)); + } + + tuh_hid_receive_report(address, instance); +} diff --git a/Firmware/RP2040/src/USBHost/HostDriver/SwitchPro/Switch2ProHost.h b/Firmware/RP2040/src/USBHost/HostDriver/SwitchPro/Switch2ProHost.h new file mode 100644 index 00000000..0197e25f --- /dev/null +++ b/Firmware/RP2040/src/USBHost/HostDriver/SwitchPro/Switch2ProHost.h @@ -0,0 +1,31 @@ +#ifndef _SWITCH_2_PRO_HOST_H_ +#define _SWITCH_2_PRO_HOST_H_ + +#include + +#include "USBHost/HostDriver/SwitchPro/SwitchPro.h" + +/** + * Nintendo Switch 2 Pro Controller (USB PID 0x2069): same bulk bring-up + HID init as SwitchProHost. + * Decoding is **InReport only** (10-byte payload) for buttons/sticks. IMU bytes after the + * 10-byte payload are parsed separately for PS3/PS4 motion output (no button coupling). + */ +class Switch2ProHost : public SwitchProHost +{ +public: + explicit Switch2ProHost(uint8_t idx) + : SwitchProHost(idx) {} + + void process_report(Gamepad& gamepad, uint8_t address, uint8_t instance, const uint8_t* report, uint16_t len) override; + +#ifdef OGXM_SWITCH2_HID_RAW_LOG +private: + static constexpr std::uint16_t kSwitch2RawLogCap = 64; + std::uint8_t switch2_dbg_prev_[kSwitch2RawLogCap]{}; + std::uint16_t switch2_dbg_prev_len_{0}; + std::uint8_t switch2_dbg_prev_btn_[3]{}; + bool switch2_dbg_have_btn_{false}; +#endif +}; + +#endif diff --git a/Firmware/RP2040/src/USBHost/HostDriver/SwitchPro/Switch2UsbInitPackets.h b/Firmware/RP2040/src/USBHost/HostDriver/SwitchPro/Switch2UsbInitPackets.h new file mode 100644 index 00000000..2e7724a7 --- /dev/null +++ b/Firmware/RP2040/src/USBHost/HostDriver/SwitchPro/Switch2UsbInitPackets.h @@ -0,0 +1,51 @@ +/* Nintendo Switch 2–family USB bulk bring-up (interface 1), same sequence as + * Tools/controller_capture/switch2_usb_init.py / HandHeldLegend procon2tool. */ +#pragma once + +#include + +static const uint8_t S2_PKT_0[16] = { 0x03,0x91,0x00,0x0d,0x00,0x08,0x00,0x00,0x01,0x00,0xff,0xff,0xff,0xff,0xff,0xff }; +static const uint8_t S2_PKT_1[8] = { 0x07,0x91,0x00,0x01,0x00,0x00,0x00,0x00 }; +static const uint8_t S2_PKT_2[8] = { 0x16,0x91,0x00,0x01,0x00,0x00,0x00,0x00 }; +static const uint8_t S2_PKT_3[23] = { 0x15,0x91,0x00,0x01,0x00,0x0e,0x00,0x00,0x00,0x02,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff }; +static const uint8_t S2_PKT_4[25] = { 0x15,0x91,0x00,0x02,0x00,0x11,0x00,0x00,0x00,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff }; +static const uint8_t S2_PKT_5[9] = { 0x15,0x91,0x00,0x03,0x00,0x01,0x00,0x00,0x00 }; +static const uint8_t S2_PKT_6[16] = { 0x09,0x91,0x00,0x07,0x00,0x08,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00 }; +static const uint8_t S2_PKT_7[12] = { 0x0c,0x91,0x00,0x02,0x00,0x04,0x00,0x00,0x27,0x00,0x00,0x00 }; +static const uint8_t S2_PKT_8[8] = { 0x11,0x91,0x00,0x03,0x00,0x00,0x00,0x00 }; +static const uint8_t S2_PKT_9[29] = { 0x0a,0x91,0x00,0x08,0x00,0x14,0x00,0x00,0x01,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0x35,0x00,0x46,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00 }; +static const uint8_t S2_PKT_10[12] = { 0x0c,0x91,0x00,0x04,0x00,0x04,0x00,0x00,0x27,0x00,0x00,0x00 }; +static const uint8_t S2_PKT_11[12] = { 0x03,0x91,0x00,0x0a,0x00,0x04,0x00,0x00,0x09,0x00,0x00,0x00 }; +static const uint8_t S2_PKT_12[8] = { 0x10,0x91,0x00,0x01,0x00,0x00,0x00,0x00 }; +static const uint8_t S2_PKT_13[8] = { 0x01,0x91,0x00,0x0c,0x00,0x00,0x00,0x00 }; +static const uint8_t S2_PKT_14[7] = { 0x03,0x91,0x00,0x01,0x00,0x00,0x00 }; +static const uint8_t S2_PKT_15[11] = { 0x0a,0x91,0x00,0x02,0x00,0x04,0x00,0x00,0x03,0x00,0x00 }; +static const uint8_t S2_PKT_16[16] = { 0x09,0x91,0x00,0x07,0x00,0x08,0x00,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00 }; + +struct Switch2UsbBulkPacket { + const uint8_t* data; + uint8_t len; +}; + +static const Switch2UsbBulkPacket SWITCH2_USB_BULK_PACKETS[] = { + { S2_PKT_0, sizeof(S2_PKT_0) }, + { S2_PKT_1, sizeof(S2_PKT_1) }, + { S2_PKT_2, sizeof(S2_PKT_2) }, + { S2_PKT_3, sizeof(S2_PKT_3) }, + { S2_PKT_4, sizeof(S2_PKT_4) }, + { S2_PKT_5, sizeof(S2_PKT_5) }, + { S2_PKT_6, sizeof(S2_PKT_6) }, + { S2_PKT_7, sizeof(S2_PKT_7) }, + { S2_PKT_8, sizeof(S2_PKT_8) }, + { S2_PKT_9, sizeof(S2_PKT_9) }, + { S2_PKT_10, sizeof(S2_PKT_10) }, + { S2_PKT_11, sizeof(S2_PKT_11) }, + { S2_PKT_12, sizeof(S2_PKT_12) }, + { S2_PKT_13, sizeof(S2_PKT_13) }, + { S2_PKT_14, sizeof(S2_PKT_14) }, + { S2_PKT_15, sizeof(S2_PKT_15) }, + { S2_PKT_16, sizeof(S2_PKT_16) }, +}; + +static constexpr unsigned SWITCH2_USB_BULK_PACKET_COUNT = + sizeof(SWITCH2_USB_BULK_PACKETS) / sizeof(SWITCH2_USB_BULK_PACKETS[0]); diff --git a/Firmware/RP2040/src/USBHost/HostDriver/SwitchPro/SwitchPro.cpp b/Firmware/RP2040/src/USBHost/HostDriver/SwitchPro/SwitchPro.cpp index 105f3081..9f11bcc4 100644 --- a/Firmware/RP2040/src/USBHost/HostDriver/SwitchPro/SwitchPro.cpp +++ b/Firmware/RP2040/src/USBHost/HostDriver/SwitchPro/SwitchPro.cpp @@ -3,162 +3,718 @@ #include "host/usbh.h" #include "class/hid/hid_host.h" +#include "common/tusb_types.h" + +#include "Board/Config.h" +#include "Board/board_api.h" +#if defined(CONFIG_EN_USB_HOST) +#include "pio_usb.h" +#endif #include "USBHost/HostDriver/SwitchPro/SwitchPro.h" +#include "USBHost/HostDriver/SwitchPro/Switch2UsbInitPackets.h" +#include "USBHost/HostManager.h" +#include "Gamepad/MotionImu.h" + +namespace { + +#if defined(CONFIG_EN_USB_HOST) +void service_usb_host() { + pio_usb_host_frame(); + tuh_task(); +} +#else +void service_usb_host() { + tuh_task(); +} +#endif + +bool try_hid_out_id(uint8_t address, uint8_t instance, uint8_t report_id, const void* report, uint16_t len) { + if (!tuh_hid_send_ready(address, instance)) { + return false; + } + return tuh_hid_send_report(address, instance, report_id, report, len); +} -void SwitchProHost::initialize(Gamepad& gamepad, uint8_t address, uint8_t instance, const uint8_t* report_desc, uint16_t desc_len) +bool try_hid_out(uint8_t address, uint8_t instance, const void* report, uint16_t len) { + return try_hid_out_id(address, instance, 0, report, len); +} + +constexpr uint8_t kInitForceAdvanceRetries = 8; + +} // namespace + +bool SwitchProHost::is_switch2_usb_family(uint16_t vid, uint16_t pid) { - std::memset(&out_report_, 0, sizeof(out_report_)); - init_switch_host(gamepad, address, instance); + if (vid != 0x057Eu) + { + return false; + } + switch (pid) + { + case 0x2066u: + case 0x2067u: + case 0x2069u: + case 0x2073u: + return true; + default: + return false; + } } -uint8_t SwitchProHost::get_output_sequence_counter() +SwitchProHost::~SwitchProHost() { - uint8_t counter = sequence_counter_; - sequence_counter_ = (sequence_counter_ + 1) & 0x0F; - return counter; + switch2_cancel_bringup(); } -// The other way is to write a class driver just for switch pro, we'll see if there are issues with this -void SwitchProHost::init_switch_host(Gamepad& gamepad, uint8_t address, uint8_t instance) +void SwitchProHost::disconnect_cb(Gamepad& gamepad, uint8_t address, uint8_t instance) { - // See: https://github.com/Dan611/hid-procon - // https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering - // https://github.com/HisashiKato/USB_Host_Shield_Library_2.0 + (void)gamepad; + (void)address; + (void)instance; + switch2_cancel_bringup(); +} - std::memset(&out_report_, 0, sizeof(out_report_)); +uintptr_t SwitchProHost::switch2_make_cb_token(uint8_t daddr, uint8_t instance) +{ + return (static_cast(instance) << 8) | daddr; +} + +SwitchProHost* SwitchProHost::switch2_lookup(uint8_t daddr, uint8_t instance) +{ + return HostManager::get_instance().get_switch_pro_host(daddr, instance); +} - uint8_t report_size = 10; +void SwitchProHost::switch2_cancel_bringup() +{ + switch2_bringup_active_ = false; + switch2_bringup_pending_retry_ = false; + switch2_next_packet_ms_ = 0; + switch2_pkt_idx_ = 0; + switch2_bulk_keepalive_busy_ = false; - out_report_.command = SwitchPro::CMD::RUMBLE_ONLY; - out_report_.sequence_counter = get_output_sequence_counter(); + if (switch2_dev_addr_ != 0) + { + if (switch2_ep_out_ != 0) + { + (void)tuh_edpt_abort_xfer(switch2_dev_addr_, switch2_ep_out_); + } + if (switch2_ep_in_ != 0) + { + (void)tuh_edpt_abort_xfer(switch2_dev_addr_, switch2_ep_in_); + } + } - out_report_.rumble_l[0] = 0x00; - out_report_.rumble_l[1] = 0x01; - out_report_.rumble_l[2] = 0x40; - out_report_.rumble_l[3] = 0x40; + switch2_ep_out_ = 0; + switch2_ep_in_ = 0; +} - out_report_.rumble_r[0] = 0x00; - out_report_.rumble_r[1] = 0x01; - out_report_.rumble_r[2] = 0x40; - out_report_.rumble_r[3] = 0x40; +void SwitchProHost::initialize(Gamepad& gamepad, uint8_t address, uint8_t instance, + const uint8_t* report_desc, uint16_t desc_len) +{ + (void)report_desc; + (void)desc_len; - switch (init_state_) + std::memset(&out_report_, 0, sizeof(out_report_)); + std::memset(&prev_in_report_, 0, sizeof(prev_in_report_)); + sequence_counter_ = 0; + hid_instance_ = instance; + switch2_dev_addr_ = address; + switch2_gamepad_ = &gamepad; + init_state_ = InitState::USB_MAC; + pending_usb_ack_ = 0; + pending_subcmd_ack_ = 0; + init_step_retries_ = 0; + usb_timeout_sent_ = false; + switch2_cancel_bringup(); + + uint16_t vid = 0; + uint16_t pid = 0; + tuh_vid_pid_get(address, &vid, &pid); + + if (is_switch2_usb_family(vid, pid)) + { + switch2_start_bringup(); + } + else { - case InitState::HANDSHAKE: - report_size = 2; + start_usb_wired_init(address, instance); + } +} - out_report_.command = SwitchPro::CMD::HID; - out_report_.sequence_counter = SwitchPro::CMD::HANDSHAKE; +void SwitchProHost::switch2_start_bringup() +{ + switch2_bringup_active_ = true; + switch2_pkt_idx_ = 0; + switch2_ep_out_ = 0; + switch2_ep_in_ = 0; + switch2_next_packet_ms_ = 0; + switch2_start_config_read(); +} - if(tuh_hid_send_report(address, instance, 0, &out_report_, report_size)) +void SwitchProHost::switch2_start_config_read() +{ + if (!tuh_descriptor_get_configuration( + switch2_dev_addr_, + 0, + switch2_cfg_buf_.data(), + static_cast(switch2_cfg_buf_.size()), + switch2_cfg_cb, + switch2_make_cb_token(switch2_dev_addr_, hid_instance_))) + { + switch2_finish_bringup(); + } +} + +bool SwitchProHost::switch2_parse_open_iface1(const uint8_t* desc_cfg, uint16_t buflen) +{ + switch2_ep_out_ = 0; + switch2_ep_in_ = 0; + + if (buflen < sizeof(tusb_desc_configuration_t)) + { + return false; + } + + auto const* cfg = reinterpret_cast(desc_cfg); + uint16_t total = tu_le16toh(cfg->wTotalLength); + uint16_t len = TU_MIN(total, buflen); + uint8_t const* end = desc_cfg + len; + uint8_t const* p = tu_desc_next(desc_cfg); + + while (p < end) + { + if (tu_desc_type(p) != TUSB_DESC_INTERFACE) + { + p = tu_desc_next(p); + continue; + } + + auto const* itf = reinterpret_cast(p); + if (itf->bInterfaceNumber != 1 || itf->bAlternateSetting != 0) + { + p = tu_desc_next(p); + continue; + } + + p = tu_desc_next(p); + int endpoint = 0; + while (endpoint < itf->bNumEndpoints && p < end) + { + if (tu_desc_type(p) != TUSB_DESC_ENDPOINT) { - init_state_ = InitState::TIMEOUT; + p = tu_desc_next(p); + continue; } - break; - case InitState::TIMEOUT: - report_size = 2; - - out_report_.command = SwitchPro::CMD::HID; - out_report_.sequence_counter = SwitchPro::CMD::DISABLE_TIMEOUT; - if(tuh_hid_send_report(address, instance, 0, &out_report_, report_size)) + auto const* ep = reinterpret_cast(p); + if (ep->bmAttributes.xfer == TUSB_XFER_BULK) { - init_state_ = InitState::LED; + if (!tuh_edpt_open(switch2_dev_addr_, ep)) + { + return false; + } + if (tu_edpt_dir(ep->bEndpointAddress) == TUSB_DIR_OUT && switch2_ep_out_ == 0) + { + switch2_ep_out_ = ep->bEndpointAddress; + } + else if (tu_edpt_dir(ep->bEndpointAddress) == TUSB_DIR_IN && switch2_ep_in_ == 0) + { + switch2_ep_in_ = ep->bEndpointAddress; + } } + endpoint++; + p = tu_desc_next(p); + } + + return switch2_ep_out_ != 0; + } + + return false; +} + +void SwitchProHost::switch2_cfg_cb(tuh_xfer_s* xfer) +{ + const uint8_t daddr = xfer->daddr; + const uint8_t instance = static_cast(xfer->user_data >> 8); + SwitchProHost* self = switch2_lookup(daddr, instance); + if (self == nullptr || !self->switch2_bringup_active_) + { + return; + } + + if (xfer->result != XFER_RESULT_SUCCESS || + xfer->actual_len < sizeof(tusb_desc_configuration_t)) + { + self->switch2_finish_bringup(); + return; + } + + uint16_t actual = static_cast(xfer->actual_len); + auto const* cfg = reinterpret_cast(self->switch2_cfg_buf_.data()); + const uint16_t total = tu_le16toh(cfg->wTotalLength); + if (total > actual && total <= self->switch2_cfg_buf_.size()) + { + if (!tuh_descriptor_get_configuration( + self->switch2_dev_addr_, + 0, + self->switch2_cfg_buf_.data(), + total, + switch2_cfg_cb, + switch2_make_cb_token(self->switch2_dev_addr_, self->hid_instance_))) + { + self->switch2_finish_bringup(); + } + return; + } + + if (!self->switch2_parse_open_iface1(self->switch2_cfg_buf_.data(), actual)) + { + self->switch2_finish_bringup(); + return; + } + + self->switch2_pkt_idx_ = 0; + self->switch2_submit_out_packet(); +} + +void SwitchProHost::switch2_out_cb(tuh_xfer_s* xfer) +{ + const uint8_t daddr = xfer->daddr; + const uint8_t instance = static_cast(xfer->user_data >> 8); + SwitchProHost* self = switch2_lookup(daddr, instance); + if (self == nullptr || !self->switch2_bringup_active_) + { + return; + } + + if (xfer->result != XFER_RESULT_SUCCESS) + { + self->switch2_finish_bringup(); + return; + } + + if (self->switch2_ep_in_ != 0) + { + self->switch2_submit_in_read(); + } + else + { + self->switch2_after_packet_round(); + } +} + +void SwitchProHost::switch2_in_cb(tuh_xfer_s* xfer) +{ + const uint8_t daddr = xfer->daddr; + const uint8_t instance = static_cast(xfer->user_data >> 8); + SwitchProHost* self = switch2_lookup(daddr, instance); + if (self == nullptr || !self->switch2_bringup_active_) + { + return; + } + (void)xfer; + self->switch2_after_packet_round(); +} + +void SwitchProHost::switch2_keepalive_cb(tuh_xfer_s* xfer) +{ + const uint8_t daddr = xfer->daddr; + const uint8_t instance = static_cast(xfer->user_data >> 8); + SwitchProHost* self = switch2_lookup(daddr, instance); + if (self != nullptr) + { + self->switch2_bulk_keepalive_busy_ = false; + } + (void)daddr; + (void)instance; +} + +void SwitchProHost::switch2_send_bulk_keepalive() +{ + if (switch2_ep_out_ == 0 || switch2_bulk_keepalive_busy_) + { + return; + } + + static const uint8_t kKeepalive[] = {0x03, 0x91, 0x00, 0x01, 0x00, 0x00, 0x00}; + if (sizeof(kKeepalive) > switch2_bulk_out_buf_.size()) + { + return; + } + + std::memcpy(switch2_bulk_out_buf_.data(), kKeepalive, sizeof(kKeepalive)); + + tuh_xfer_t xfer{}; + xfer.daddr = switch2_dev_addr_; + xfer.ep_addr = switch2_ep_out_; + xfer.buflen = static_cast(sizeof(kKeepalive)); + xfer.buffer = switch2_bulk_out_buf_.data(); + xfer.complete_cb = switch2_keepalive_cb; + xfer.user_data = switch2_make_cb_token(switch2_dev_addr_, hid_instance_); + + switch2_bulk_keepalive_busy_ = true; + if (!tuh_edpt_xfer(&xfer)) + { + switch2_bulk_keepalive_busy_ = false; + } +} + +void SwitchProHost::switch2_submit_out_packet() +{ + const Switch2UsbBulkPacket& pkt = SWITCH2_USB_BULK_PACKETS[switch2_pkt_idx_]; + if (pkt.len > switch2_bulk_out_buf_.size()) + { + switch2_finish_bringup(); + return; + } + + std::memcpy(switch2_bulk_out_buf_.data(), pkt.data, pkt.len); + + tuh_xfer_t xfer{}; + xfer.daddr = switch2_dev_addr_; + xfer.ep_addr = switch2_ep_out_; + xfer.buflen = pkt.len; + xfer.buffer = switch2_bulk_out_buf_.data(); + xfer.complete_cb = switch2_out_cb; + xfer.user_data = switch2_make_cb_token(switch2_dev_addr_, hid_instance_); + + if (!tuh_edpt_xfer(&xfer)) + { + switch2_next_packet_ms_ = board_api::ms_since_boot() + 12U; + } +} + +void SwitchProHost::switch2_submit_in_read() +{ + tuh_xfer_t xfer{}; + xfer.daddr = switch2_dev_addr_; + xfer.ep_addr = switch2_ep_in_; + xfer.buflen = 64; + xfer.buffer = switch2_bulk_in_buf_.data(); + xfer.complete_cb = switch2_in_cb; + xfer.user_data = switch2_make_cb_token(switch2_dev_addr_, hid_instance_); + + if (!tuh_edpt_xfer(&xfer)) + { + switch2_next_packet_ms_ = board_api::ms_since_boot() + 12U; + } +} + +void SwitchProHost::switch2_after_packet_round() +{ + switch2_pkt_idx_++; + if (switch2_pkt_idx_ >= SWITCH2_USB_BULK_PACKET_COUNT) + { + switch2_finish_bringup(); + return; + } + switch2_next_packet_ms_ = board_api::ms_since_boot() + 12U; +} + +void SwitchProHost::switch2_poll_bringup() +{ + if (!switch2_bringup_active_ || switch2_next_packet_ms_ == 0) + { + return; + } + if (board_api::ms_since_boot() < switch2_next_packet_ms_) + { + return; + } + switch2_next_packet_ms_ = 0; + switch2_submit_out_packet(); +} + +void SwitchProHost::switch2_finish_bringup() +{ + const bool bulk_complete = switch2_pkt_idx_ >= SWITCH2_USB_BULK_PACKET_COUNT; + switch2_bringup_active_ = false; + switch2_next_packet_ms_ = 0; + + if (switch2_gamepad_ == nullptr) + { + return; + } + + if (bulk_complete) + { + /* Switch 2–family: bulk bring-up replaces Switch 1 wired 0x80 HID init. */ + init_state_ = InitState::DONE; + pending_usb_ack_ = 0; + pending_subcmd_ack_ = 0; + send_usb_disable_timeout(switch2_dev_addr_, hid_instance_); + switch2_last_bulk_keepalive_ms_ = board_api::ms_since_boot(); + switch2_send_bulk_keepalive(); + tuh_hid_receive_report(switch2_dev_addr_, hid_instance_); + return; + } + + switch2_bringup_pending_retry_ = true; +} + +bool SwitchProHost::send_usb_wired_command(uint8_t address, uint8_t instance, uint8_t subcmd) +{ + uint8_t payload[63] = {0}; + payload[0] = subcmd; + pending_usb_ack_ = subcmd; + return try_hid_out_id(address, instance, SwitchPro::REPORT_ID_USB_OUT, payload, sizeof(payload)); +} + +void SwitchProHost::start_usb_wired_init(uint8_t address, uint8_t instance) +{ + init_state_ = InitState::USB_MAC; + pending_usb_ack_ = SwitchPro::CMD::USB_SUB_MAC; + (void)send_usb_wired_command(address, instance, SwitchPro::CMD::USB_SUB_MAC); + tuh_hid_receive_report(address, instance); +} + +void SwitchProHost::fill_neutral_rumble(SwitchPro::OutReport& out) +{ + out.rumble_l[0] = 0x00; + out.rumble_l[1] = 0x01; + out.rumble_l[2] = 0x40; + out.rumble_l[3] = 0x40; + out.rumble_r[0] = 0x00; + out.rumble_r[1] = 0x01; + out.rumble_r[2] = 0x40; + out.rumble_r[3] = 0x40; +} + +void SwitchProHost::advance_after_usb_ack(Gamepad& gamepad, uint8_t address, uint8_t instance) +{ + switch (init_state_) + { + case InitState::USB_MAC: + init_state_ = InitState::USB_HANDSHAKE1; + (void)send_usb_wired_command(address, instance, SwitchPro::CMD::USB_SUB_HANDSHAKE); break; - case InitState::LED: - report_size = 12; + case InitState::USB_HANDSHAKE1: + init_state_ = InitState::USB_BAUD; + (void)send_usb_wired_command(address, instance, SwitchPro::CMD::USB_SUB_BAUD); + break; + case InitState::USB_BAUD: + init_state_ = InitState::USB_HANDSHAKE2; + (void)send_usb_wired_command(address, instance, SwitchPro::CMD::USB_SUB_HANDSHAKE); + break; + case InitState::USB_HANDSHAKE2: + init_state_ = InitState::USB_NO_TIMEOUT; + send_usb_disable_timeout_and_probe(address, instance); + break; + default: + break; + } +} - out_report_.command = SwitchPro::CMD::AND_RUMBLE; +void SwitchProHost::send_usb_disable_timeout_and_probe(uint8_t address, uint8_t instance) +{ + send_usb_disable_timeout(address, instance); + + std::memset(&out_report_, 0, sizeof(out_report_)); + fill_neutral_rumble(out_report_); + out_report_.command = SwitchPro::CMD::AND_RUMBLE; + out_report_.sequence_counter = get_output_sequence_counter(); + out_report_.sub_command = SwitchPro::CMD::USB_PROBE; + pending_subcmd_ack_ = SwitchPro::CMD::USB_PROBE; + (void)try_hid_out(address, instance, &out_report_, 11); + usb_timeout_sent_ = true; + init_step_retries_ = 0; +} + +void SwitchProHost::send_usb_disable_timeout(uint8_t address, uint8_t instance) +{ + uint8_t usb_out[63] = {0}; + usb_out[0] = SwitchPro::CMD::USB_SUB_DISABLE_TIMEOUT; + (void)try_hid_out_id(address, instance, SwitchPro::REPORT_ID_USB_OUT, usb_out, sizeof(usb_out)); +} + +void SwitchProHost::init_switch_host(Gamepad& gamepad, uint8_t address, uint8_t instance) +{ + (void)gamepad; + + if (init_state_ <= InitState::USB_HANDSHAKE2) + { + if (pending_usb_ack_ != 0) + { + (void)send_usb_wired_command(address, instance, pending_usb_ack_); + } + return; + } + + if (init_state_ == InitState::USB_NO_TIMEOUT) + { + if (!usb_timeout_sent_) + { + send_usb_disable_timeout_and_probe(address, instance); + } + return; + } + + if (!tuh_hid_send_ready(address, instance)) + { + return; + } + + std::memset(&out_report_, 0, sizeof(out_report_)); + fill_neutral_rumble(out_report_); + out_report_.command = SwitchPro::CMD::AND_RUMBLE; + out_report_.sequence_counter = get_output_sequence_counter(); + + switch (init_state_) + { + case InitState::LED: out_report_.sub_command = SwitchPro::CMD::LED; out_report_.sub_command_args[0] = idx_ + 1; - - if(tuh_hid_send_report(address, instance, 0, &out_report_, report_size)) + if (try_hid_out(address, instance, &out_report_, 12)) { init_state_ = InitState::LED_HOME; } break; case InitState::LED_HOME: - report_size = 14; - - out_report_.command = SwitchPro::CMD::AND_RUMBLE; out_report_.sub_command = SwitchPro::CMD::LED_HOME; - out_report_.sub_command_args[0] = (0 /* Number of cycles */ << 4) | (true ? 0xF : 0); - out_report_.sub_command_args[1] = (0xF /* LED start intensity */ << 4) | 0x0 /* Number of full cycles */; - out_report_.sub_command_args[2] = (0xF /* Mini Cycle 1 LED intensity */ << 4) | 0x0 /* Mini Cycle 2 LED intensity */; - - if(tuh_hid_send_report(address, instance, 0, &out_report_, report_size)) + out_report_.sub_command_args[0] = static_cast((0 << 4) | 0xF); + out_report_.sub_command_args[1] = static_cast((0xF << 4) | 0x0); + out_report_.sub_command_args[2] = static_cast((0xF << 4) | 0x0); + if (try_hid_out(address, instance, &out_report_, 14)) { init_state_ = InitState::FULL_REPORT; } break; case InitState::FULL_REPORT: - report_size = 12; - - out_report_.command = SwitchPro::CMD::AND_RUMBLE; out_report_.sub_command = SwitchPro::CMD::MODE; out_report_.sub_command_args[0] = SwitchPro::CMD::FULL_REPORT_MODE; - - if (tuh_hid_send_report(address, instance, 0, &out_report_, report_size)) + if (try_hid_out(address, instance, &out_report_, 12)) { init_state_ = InitState::IMU; } break; case InitState::IMU: - report_size = 12; - - out_report_.command = SwitchPro::CMD::AND_RUMBLE; out_report_.sub_command = SwitchPro::CMD::GYRO; - out_report_.sub_command_args[0] = 1 ? 1 : 0; - - if (tuh_hid_send_report(address, instance, 0, &out_report_, report_size)) + out_report_.sub_command_args[0] = 1; + if (try_hid_out(address, instance, &out_report_, 12)) { init_state_ = InitState::DONE; - tuh_hid_receive_report(address, instance); + pending_subcmd_ack_ = 0; } break; default: break; } +} - // tuh_hid_receive_report(address, instance); +uint8_t SwitchProHost::get_output_sequence_counter() +{ + uint8_t counter = sequence_counter_; + sequence_counter_ = (sequence_counter_ + 1) & 0x0F; + return counter; } -void SwitchProHost::process_report(Gamepad& gamepad, uint8_t address, uint8_t instance, const uint8_t* report, uint16_t len) +static const uint8_t* switchpro_report_payload(const uint8_t* report, uint16_t len, uint16_t& out_payload_len) { - if (init_state_ != InitState::DONE) + out_payload_len = len; + if (len == sizeof(SwitchPro::InReport)) { - init_switch_host(gamepad, address, instance); + return report; + } + if (len >= 2U + sizeof(SwitchPro::InReport)) + { + const uint8_t rid = report[0]; + if (rid == SwitchPro::REPORT_ID_STANDARD || rid == SwitchPro::REPORT_ID_SWITCH2_FULL || + rid == SwitchPro::REPORT_ID_FULL_ALT) + { + out_payload_len = static_cast(len - 2U); + return report + 2; + } + } + return nullptr; +} + +void SwitchProHost::process_report(Gamepad& gamepad, uint8_t address, uint8_t instance, const uint8_t* report, + uint16_t len) +{ + if (switch2_bringup_active_) + { + tuh_hid_receive_report(address, instance); return; } - const SwitchPro::InReport* in_report = reinterpret_cast(report); - if (std::memcmp(&prev_in_report_.buttons, in_report->buttons, 9) == 0) + if (init_state_ != InitState::DONE) { + if (len >= 2 && report[0] == SwitchPro::REPORT_ID_USB_INIT && + init_state_ <= InitState::USB_HANDSHAKE2) + { + const uint8_t ack = report[1]; + if (ack == pending_usb_ack_) + { + advance_after_usb_ack(gamepad, address, instance); + } + } + else if (init_state_ == InitState::USB_NO_TIMEOUT && + len >= 1 && report[0] == SwitchPro::REPORT_ID_SUBCMD) + { + init_state_ = InitState::LED; + pending_subcmd_ack_ = 0; + init_switch_host(gamepad, address, instance); + } + else if (init_state_ > InitState::USB_NO_TIMEOUT) + { + init_switch_host(gamepad, address, instance); + } tuh_hid_receive_report(address, instance); return; } - Gamepad::PadIn gp_in; + uint16_t payload_len = 0; + const uint8_t* payload = switchpro_report_payload(report, len, payload_len); + if (payload == nullptr || payload_len < sizeof(SwitchPro::InReport)) + { + tuh_hid_receive_report(address, instance); + return; + } - if (in_report->buttons[0] & SwitchPro::Buttons0::Y) gp_in.buttons |= gamepad.MAP_BUTTON_X; - if (in_report->buttons[0] & SwitchPro::Buttons0::B) gp_in.buttons |= gamepad.MAP_BUTTON_A; - if (in_report->buttons[0] & SwitchPro::Buttons0::A) gp_in.buttons |= gamepad.MAP_BUTTON_B; - if (in_report->buttons[0] & SwitchPro::Buttons0::X) gp_in.buttons |= gamepad.MAP_BUTTON_Y; - if (in_report->buttons[2] & SwitchPro::Buttons2::L) gp_in.buttons |= gamepad.MAP_BUTTON_LB; - if (in_report->buttons[0] & SwitchPro::Buttons0::R) gp_in.buttons |= gamepad.MAP_BUTTON_RB; - if (in_report->buttons[1] & SwitchPro::Buttons1::L3) gp_in.buttons |= gamepad.MAP_BUTTON_L3; - if (in_report->buttons[1] & SwitchPro::Buttons1::R3) gp_in.buttons |= gamepad.MAP_BUTTON_R3; - if (in_report->buttons[1] & SwitchPro::Buttons1::MINUS) gp_in.buttons |= gamepad.MAP_BUTTON_BACK; - if (in_report->buttons[1] & SwitchPro::Buttons1::PLUS) gp_in.buttons |= gamepad.MAP_BUTTON_START; - if (in_report->buttons[1] & SwitchPro::Buttons1::HOME) gp_in.buttons |= gamepad.MAP_BUTTON_SYS; - if (in_report->buttons[1] & SwitchPro::Buttons1::CAPTURE) gp_in.buttons |= gamepad.MAP_BUTTON_MISC; + const SwitchPro::InReport* in_report = reinterpret_cast(payload); + if (std::memcmp(&prev_in_report_.buttons, in_report->buttons, 9) == 0 && + MotionImu::switch_usb_imu_unchanged(prev_imu_, payload, payload_len)) + { + tuh_hid_receive_report(address, instance); + return; + } - if (in_report->buttons[2] & SwitchPro::Buttons2::DPAD_UP) gp_in.dpad |= gamepad.MAP_DPAD_UP; - if (in_report->buttons[2] & SwitchPro::Buttons2::DPAD_DOWN) gp_in.dpad |= gamepad.MAP_DPAD_DOWN; - if (in_report->buttons[2] & SwitchPro::Buttons2::DPAD_LEFT) gp_in.dpad |= gamepad.MAP_DPAD_LEFT; - if (in_report->buttons[2] & SwitchPro::Buttons2::DPAD_RIGHT) gp_in.dpad |= gamepad.MAP_DPAD_RIGHT; + Gamepad::PadIn gp_in; + + if (in_report->buttons[0] & SwitchPro::Buttons0::Y) + gp_in.buttons |= gamepad.MAP_BUTTON_X; + if (in_report->buttons[0] & SwitchPro::Buttons0::B) + gp_in.buttons |= gamepad.MAP_BUTTON_A; + if (in_report->buttons[0] & SwitchPro::Buttons0::A) + gp_in.buttons |= gamepad.MAP_BUTTON_B; + if (in_report->buttons[0] & SwitchPro::Buttons0::X) + gp_in.buttons |= gamepad.MAP_BUTTON_Y; + if (in_report->buttons[2] & SwitchPro::Buttons2::L) + gp_in.buttons |= gamepad.MAP_BUTTON_LB; + if (in_report->buttons[0] & SwitchPro::Buttons0::R) + gp_in.buttons |= gamepad.MAP_BUTTON_RB; + if (in_report->buttons[1] & SwitchPro::Buttons1::L3) + gp_in.buttons |= gamepad.MAP_BUTTON_R3; + if (in_report->buttons[1] & SwitchPro::Buttons1::R3) + gp_in.buttons |= gamepad.MAP_BUTTON_L3; + if (in_report->buttons[1] & SwitchPro::Buttons1::MINUS) + gp_in.buttons |= gamepad.MAP_BUTTON_BACK; + if (in_report->buttons[1] & SwitchPro::Buttons1::PLUS) + gp_in.buttons |= gamepad.MAP_BUTTON_START; + if (in_report->buttons[1] & SwitchPro::Buttons1::HOME) + gp_in.buttons |= gamepad.MAP_BUTTON_SYS; + if (in_report->buttons[1] & SwitchPro::Buttons1::CAPTURE) + gp_in.buttons |= gamepad.MAP_BUTTON_MISC; + + if (in_report->buttons[2] & SwitchPro::Buttons2::DPAD_UP) + gp_in.dpad |= gamepad.MAP_DPAD_DOWN; + if (in_report->buttons[2] & SwitchPro::Buttons2::DPAD_DOWN) + gp_in.dpad |= gamepad.MAP_DPAD_UP; + if (in_report->buttons[2] & SwitchPro::Buttons2::DPAD_LEFT) + gp_in.dpad |= gamepad.MAP_DPAD_RIGHT; + if (in_report->buttons[2] & SwitchPro::Buttons2::DPAD_RIGHT) + gp_in.dpad |= gamepad.MAP_DPAD_LEFT; gp_in.trigger_l = in_report->buttons[2] & SwitchPro::Buttons2::ZL ? Range::MAX : Range::MIN; gp_in.trigger_r = in_report->buttons[0] & SwitchPro::Buttons0::ZR ? Range::MAX : Range::MIN; @@ -168,57 +724,100 @@ void SwitchProHost::process_report(Gamepad& gamepad, uint8_t address, uint8_t in uint16_t joy_rx = in_report->joysticks[3] | ((in_report->joysticks[4] & 0xF) << 8); uint16_t joy_ry = (in_report->joysticks[4] >> 4) | (in_report->joysticks[5] << 4); - std::tie(gp_in.joystick_lx, gp_in.joystick_ly) = + std::tie(gp_in.joystick_lx, gp_in.joystick_ly) = gamepad.scale_joystick_l(normalize_axis(joy_lx), normalize_axis(joy_ly), true); - - std::tie(gp_in.joystick_rx, gp_in.joystick_ry) = + + std::tie(gp_in.joystick_rx, gp_in.joystick_ry) = gamepad.scale_joystick_r(normalize_axis(joy_rx), normalize_axis(joy_ry), true); + if (payload_len >= 22) { + gp_in.motion_source = Gamepad::PadIn::MOTION_SRC_SWITCH_USB; + MotionImu::fill_from_switch_usb_payload(gp_in.accel, gp_in.gyro, payload, payload_len); + } + gamepad.set_pad_in(gp_in); tuh_hid_receive_report(address, instance); std::memcpy(&prev_in_report_, in_report, sizeof(SwitchPro::InReport)); + if (payload_len >= 22) { + constexpr uint16_t kInReportSize = 10; + constexpr uint16_t kImuSampleSize = sizeof(MotionImu::SwitchImuSample); + uint16_t imu_off = kInReportSize; + if (payload_len >= kInReportSize + (3 * kImuSampleSize)) { + imu_off = static_cast(kInReportSize + (2 * kImuSampleSize)); + } + std::memcpy(prev_imu_, payload + imu_off, sizeof(prev_imu_)); + } } bool SwitchProHost::send_feedback(Gamepad& gamepad, uint8_t address, uint8_t instance) { + uint16_t vid = 0; + uint16_t pid = 0; + tuh_vid_pid_get(address, &vid, &pid); + const bool switch2_device = is_switch2_usb_family(vid, pid); + + if (switch2_device && switch2_bringup_active_) + { + switch2_poll_bringup(); + service_usb_host(); + return false; + } + + if (switch2_device && switch2_bringup_pending_retry_) + { + switch2_bringup_pending_retry_ = false; + switch2_start_bringup(); + service_usb_host(); + return false; + } + if (init_state_ != InitState::DONE) { + if (init_state_ == InitState::USB_NO_TIMEOUT) + { + if (++init_step_retries_ >= kInitForceAdvanceRetries) + { + init_state_ = InitState::LED; + pending_subcmd_ack_ = 0; + usb_timeout_sent_ = false; + } + } init_switch_host(gamepad, address, instance); + tuh_hid_receive_report(address, instance); + service_usb_host(); return false; } - // See: https://github.com/Dan611/hid-procon - // https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering - // https://github.com/HisashiKato/USB_Host_Shield_Library_2.0 + if (switch2_device) + { + const uint32_t now_ms = board_api::ms_since_boot(); + if (now_ms - switch2_last_bulk_keepalive_ms_ >= 2000U) + { + switch2_last_bulk_keepalive_ms_ = now_ms; + switch2_send_bulk_keepalive(); + } + } std::memset(&out_report_, 0, sizeof(out_report_)); - uint8_t report_size = 10; - out_report_.command = SwitchPro::CMD::RUMBLE_ONLY; out_report_.sequence_counter = get_output_sequence_counter(); + fill_neutral_rumble(out_report_); Gamepad::PadOut gp_out = gamepad.get_pad_out(); - if (gp_out.rumble_l > 0) + if (gp_out.rumble_l > 0) { uint8_t amplitude_l = static_cast(((gp_out.rumble_l / 255.0f) * 0.8f + 0.5f) * (0xC0 - 0x40) + 0x40); out_report_.rumble_l[0] = amplitude_l; out_report_.rumble_l[1] = 0x88; out_report_.rumble_l[2] = amplitude_l / 2; - out_report_.rumble_l[3] = 0x61; - } - else - { - out_report_.rumble_l[0] = 0x00; - out_report_.rumble_l[1] = 0x01; - out_report_.rumble_l[2] = 0x40; - out_report_.rumble_l[3] = 0x40; + out_report_.rumble_l[3] = 0x61; } - if (gp_out.rumble_r > 0) + if (gp_out.rumble_r > 0) { uint8_t amplitude_r = static_cast(((gp_out.rumble_r / 255.0f) * 0.8f + 0.5f) * (0xC0 - 0x40) + 0x40); @@ -226,14 +825,11 @@ bool SwitchProHost::send_feedback(Gamepad& gamepad, uint8_t address, uint8_t ins out_report_.rumble_r[1] = 0x88; out_report_.rumble_r[2] = amplitude_r / 2; out_report_.rumble_r[3] = 0x61; - } - else - { - out_report_.rumble_r[0] = 0x00; - out_report_.rumble_r[1] = 0x01; - out_report_.rumble_r[2] = 0x40; - out_report_.rumble_r[3] = 0x40; } - return tuh_hid_send_report(address, instance, 0, &out_report_, report_size); -} \ No newline at end of file + if (!tuh_hid_send_ready(address, instance)) + { + return false; + } + return tuh_hid_send_report(address, instance, 0, &out_report_, 10); +} diff --git a/Firmware/RP2040/src/USBHost/HostDriver/SwitchPro/SwitchPro.h b/Firmware/RP2040/src/USBHost/HostDriver/SwitchPro/SwitchPro.h index a599b722..248fe4f9 100644 --- a/Firmware/RP2040/src/USBHost/HostDriver/SwitchPro/SwitchPro.h +++ b/Firmware/RP2040/src/USBHost/HostDriver/SwitchPro/SwitchPro.h @@ -1,11 +1,13 @@ #ifndef _SWITCH_PRO_HOST_H_ #define _SWITCH_PRO_HOST_H_ +#include #include #include "Descriptors/SwitchPro.h" #include "USBHost/HostDriver/HostDriver.h" -#include "Board/ogxm_log.h" + +struct tuh_xfer_s; class SwitchProHost : public HostDriver { @@ -16,37 +18,83 @@ class SwitchProHost : public HostDriver void initialize(Gamepad& gamepad, uint8_t address, uint8_t instance, const uint8_t* report_desc, uint16_t desc_len) override; void process_report(Gamepad& gamepad, uint8_t address, uint8_t instance, const uint8_t* report, uint16_t len) override; bool send_feedback(Gamepad& gamepad, uint8_t address, uint8_t instance) override; + void disconnect_cb(Gamepad& gamepad, uint8_t address, uint8_t instance) override; + ~SwitchProHost() override; + + static bool is_switch2_usb_family(uint16_t vid, uint16_t pid); -private: +protected: enum class InitState { - HANDSHAKE, - TIMEOUT, - FULL_REPORT, + USB_MAC, + USB_HANDSHAKE1, + USB_BAUD, + USB_HANDSHAKE2, + USB_NO_TIMEOUT, LED, LED_HOME, + FULL_REPORT, IMU, DONE }; - InitState init_state_{InitState::HANDSHAKE}; + InitState init_state_{InitState::USB_MAC}; uint8_t sequence_counter_{0}; + uint8_t pending_usb_ack_{0}; + uint8_t pending_subcmd_ack_{0}; + uint8_t init_step_retries_{0}; + bool usb_timeout_sent_{false}; SwitchPro::InReport prev_in_report_{}; + uint8_t prev_imu_[12]{}; SwitchPro::OutReport out_report_{}; + uint8_t hid_instance_{0}; + uint8_t switch2_dev_addr_{0}; + uint8_t switch2_ep_out_{0}; + uint8_t switch2_ep_in_{0}; + bool switch2_bringup_active_{false}; + Gamepad* switch2_gamepad_{nullptr}; + std::array switch2_cfg_buf_{}; + std::array switch2_bulk_out_buf_{}; + std::array switch2_bulk_in_buf_{}; + unsigned switch2_pkt_idx_{0}; + uint32_t switch2_next_packet_ms_{0}; + bool switch2_bringup_pending_retry_{false}; + uint32_t switch2_last_bulk_keepalive_ms_{0}; + bool switch2_bulk_keepalive_busy_{false}; + + void switch2_poll_bringup(); + void switch2_start_bringup(); + void switch2_start_config_read(); + void switch2_cancel_bringup(); + + static uintptr_t switch2_make_cb_token(uint8_t daddr, uint8_t instance); + static SwitchProHost* switch2_lookup(uint8_t daddr, uint8_t instance); + bool switch2_parse_open_iface1(const uint8_t* desc_cfg, uint16_t buflen); + void switch2_submit_out_packet(); + void switch2_submit_in_read(); + void switch2_send_bulk_keepalive(); + void switch2_after_packet_round(); + void switch2_finish_bringup(); + + static void switch2_cfg_cb(tuh_xfer_s* xfer); + static void switch2_out_cb(tuh_xfer_s* xfer); + static void switch2_in_cb(tuh_xfer_s* xfer); + static void switch2_keepalive_cb(tuh_xfer_s* xfer); + void init_switch_host(Gamepad& gamepad, uint8_t address, uint8_t instance); + void start_usb_wired_init(uint8_t address, uint8_t instance); + bool send_usb_wired_command(uint8_t address, uint8_t instance, uint8_t subcmd); + void advance_after_usb_ack(Gamepad& gamepad, uint8_t address, uint8_t instance); + void send_usb_disable_timeout_and_probe(uint8_t address, uint8_t instance); + void send_usb_disable_timeout(uint8_t address, uint8_t instance); + void fill_neutral_rumble(SwitchPro::OutReport& out); uint8_t get_output_sequence_counter(); static inline int16_t normalize_axis(uint16_t value) { - /* 12bit value from the controller doesnt cover the full 12bit range seemingly, - isn't completely centered at 2047 either so I may be missing something here. - Tried to get as close as possible with the multiplier */ - - OGXM_LOG("Value: %d\n", value); int32_t normalized_value = (value - 2047) * 22; - OGXM_LOG("Normalized value: %d\n", normalized_value); return Range::clamp(normalized_value); } }; diff --git a/Firmware/RP2040/src/USBHost/HostDriver/SwitchWired/SwitchWired.cpp b/Firmware/RP2040/src/USBHost/HostDriver/SwitchWired/SwitchWired.cpp index b5c6210a..5f821c92 100644 --- a/Firmware/RP2040/src/USBHost/HostDriver/SwitchWired/SwitchWired.cpp +++ b/Firmware/RP2040/src/USBHost/HostDriver/SwitchWired/SwitchWired.cpp @@ -7,6 +7,7 @@ void SwitchWiredHost::initialize(Gamepad& gamepad, uint8_t address, uint8_t instance, const uint8_t* report_desc, uint16_t desc_len) { + tuh_hid_receive_report(address, instance); } @@ -64,9 +65,9 @@ void SwitchWiredHost::process_report(Gamepad& gamepad, uint8_t address, uint8_t if (in_report->buttons & SwitchWired::Buttons::L3) gp_in.buttons |= gamepad.MAP_BUTTON_L3; if (in_report->buttons & SwitchWired::Buttons::R3) gp_in.buttons |= gamepad.MAP_BUTTON_R3; - gp_in.trigger_l = (in_report->buttons & SwitchWired::Buttons::ZL) ? Range::MAX : Range::MIN; - gp_in.trigger_r = (in_report->buttons & SwitchWired::Buttons::ZR) ? Range::MAX : Range::MIN; - + if (in_report->buttons & SwitchWired::Buttons::ZR) gp_in.trigger_r = Range::MAX; + if (in_report->buttons & SwitchWired::Buttons::ZL) gp_in.trigger_l = Range::MAX; + std::tie(gp_in.joystick_lx, gp_in.joystick_ly) = gamepad.scale_joystick_l(in_report->joystick_lx, in_report->joystick_ly); std::tie(gp_in.joystick_rx, gp_in.joystick_ry) = gamepad.scale_joystick_r(in_report->joystick_rx, in_report->joystick_ry); diff --git a/Firmware/RP2040/src/USBHost/HostDriver/XInput/Xbox360.cpp b/Firmware/RP2040/src/USBHost/HostDriver/XInput/Xbox360.cpp index 58e75051..04fdca3d 100644 --- a/Firmware/RP2040/src/USBHost/HostDriver/XInput/Xbox360.cpp +++ b/Firmware/RP2040/src/USBHost/HostDriver/XInput/Xbox360.cpp @@ -1,13 +1,28 @@ #include #include "host/usbh.h" +#include "TaskQueue/TaskQueue.h" #include "USBHost/HostDriver/XInput/tuh_xinput/tuh_xinput.h" #include "USBHost/HostDriver/XInput/Xbox360.h" +#include "USBHost/HostDriver/XInput/XboxArcadeStick.h" void Xbox360Host::initialize(Gamepad& gamepad, uint8_t address, uint8_t instance, const uint8_t* report_desc, uint16_t desc_len) { tuh_xinput::set_led(address, instance, idx_ + 1, true); + + uint16_t vid, pid; + tuh_vid_pid_get(address, &vid, &pid); + bool is_8bitdo = (vid == 0x2DC8) && (pid == 0x3016 || pid == 0x3106); + + if (is_8bitdo) + { + tid_keepalive_ = TaskQueue::Core1::get_new_task_id(); + TaskQueue::Core1::queue_delayed_task(tid_keepalive_, 1000, true, [address, instance, this] { + tuh_xinput::set_led(address, instance, idx_ + 1, false); + }); + } + tuh_xinput::receive_report(address, instance); } @@ -39,8 +54,25 @@ void Xbox360Host::process_report(Gamepad& gamepad, uint8_t address, uint8_t inst if (in_report_->buttons[1] & XInput::Buttons1::X) gp_in.buttons |= gamepad.MAP_BUTTON_X; if (in_report_->buttons[1] & XInput::Buttons1::Y) gp_in.buttons |= gamepad.MAP_BUTTON_Y; - gp_in.trigger_l = gamepad.scale_trigger_l(in_report_->trigger_l); - gp_in.trigger_r = gamepad.scale_trigger_r(in_report_->trigger_r); + uint16_t vid = 0; + uint16_t pid = 0; + tuh_vid_pid_get(address, &vid, &pid); + if (XboxArcadeStick::is_xbox360_digital_triggers(vid, pid)) + { + if (in_report_->trigger_l != 0) + { + gp_in.trigger_l = 0xFF; + } + if (in_report_->trigger_r != 0) + { + gp_in.trigger_r = 0xFF; + } + } + else + { + gp_in.trigger_l = gamepad.scale_trigger_l(in_report_->trigger_l); + gp_in.trigger_r = gamepad.scale_trigger_r(in_report_->trigger_r); + } std::tie(gp_in.joystick_lx, gp_in.joystick_ly) = gamepad.scale_joystick_l(in_report_->joystick_lx, in_report_->joystick_ly, true); std::tie(gp_in.joystick_rx, gp_in.joystick_ry) = gamepad.scale_joystick_r(in_report_->joystick_rx, in_report_->joystick_ry, true); diff --git a/Firmware/RP2040/src/USBHost/HostDriver/XInput/Xbox360.h b/Firmware/RP2040/src/USBHost/HostDriver/XInput/Xbox360.h index 63511e5d..76c703ea 100644 --- a/Firmware/RP2040/src/USBHost/HostDriver/XInput/Xbox360.h +++ b/Firmware/RP2040/src/USBHost/HostDriver/XInput/Xbox360.h @@ -17,6 +17,7 @@ class Xbox360Host : public HostDriver bool send_feedback(Gamepad& gamepad, uint8_t address, uint8_t instance) override; private: + uint32_t tid_keepalive_{0}; XInput::InReport prev_in_report_; }; diff --git a/Firmware/RP2040/src/USBHost/HostDriver/XInput/Xbox360W.cpp b/Firmware/RP2040/src/USBHost/HostDriver/XInput/Xbox360W.cpp index 39a4803e..f516c8de 100644 --- a/Firmware/RP2040/src/USBHost/HostDriver/XInput/Xbox360W.cpp +++ b/Firmware/RP2040/src/USBHost/HostDriver/XInput/Xbox360W.cpp @@ -9,6 +9,7 @@ #include "USBHost/HostDriver/XInput/tuh_xinput/tuh_xinput.h" #include "USBHost/HostDriver/XInput/Xbox360W.h" #include "Board/ogxm_log.h" +#include "Descriptors/XInput.h" Xbox360WHost::~Xbox360WHost() { @@ -17,99 +18,124 @@ Xbox360WHost::~Xbox360WHost() void Xbox360WHost::initialize(Gamepad& gamepad, uint8_t address, uint8_t instance, const uint8_t* report_desc, uint16_t desc_len) { - tuh_xinput::receive_report(address, instance); + (void)gamepad; + (void)address; + (void)instance; + (void)report_desc; + (void)desc_len; + /* IN polling is started from tuh_xinput set_config for every wireless port. */ } void Xbox360WHost::process_report(Gamepad& gamepad, uint8_t address, uint8_t instance, const uint8_t* report, uint16_t len) { - const XInput::InReportWireless* in_report = reinterpret_cast(report); - - if (in_report->command[1] & 2) + if (report[1] & 2) { - if (in_report->chatpad_status == 0x00) - { - Gamepad::ChatpadIn gp_in_chatpad; - gp_in_chatpad[0] = in_report->chatpad[0]; - gp_in_chatpad[1] = in_report->chatpad[1]; - gp_in_chatpad[2] = in_report->chatpad[2]; - - gamepad.set_chatpad_in(gp_in_chatpad); - } - else if (in_report->chatpad_status == 0xF0 && in_report->chatpad[0] == 0x03) + if (len >= sizeof(XInput::InReportWireless)) { - tuh_xinput::xbox360_chatpad_init(address, instance); + const XInput::InReportWireless* in_report = reinterpret_cast(report); + if (in_report->chatpad_status == 0x00) + { + Gamepad::ChatpadIn gp_in_chatpad; + gp_in_chatpad[0] = in_report->chatpad[0]; + gp_in_chatpad[1] = in_report->chatpad[1]; + gp_in_chatpad[2] = in_report->chatpad[2]; + gamepad.set_chatpad_in(gp_in_chatpad); + } + else if (in_report->chatpad_status == 0xF0 && in_report->chatpad[0] == 0x03) + { + tuh_xinput::xbox360_chatpad_init(address, instance); + } } tuh_xinput::receive_report(address, instance); return; } - if (!(in_report->command[1] & 1) || - !(in_report->report_size == 0x13) || - std::memcmp(&prev_in_report_, in_report, std::min(static_cast(len), sizeof(XInput::InReportWireless))) == 0) + if (!(report[1] & 1) || len < 18 || + (report[5] != 0x13 && report[5] != 0x14)) + { + tuh_xinput::receive_report(address, instance); + return; + } + + if (std::memcmp(prev_in_report_, report, std::min(static_cast(len), sizeof(prev_in_report_))) == 0) { tuh_xinput::receive_report(address, instance); return; } + const uint16_t wButtons = static_cast(report[6]) | + (static_cast(report[7]) << 8); + Gamepad::PadIn gp_in; - if (in_report->buttons[0] & XInput::Buttons0::DPAD_UP) gp_in.dpad |= gamepad.MAP_DPAD_UP; - if (in_report->buttons[0] & XInput::Buttons0::DPAD_DOWN) gp_in.dpad |= gamepad.MAP_DPAD_DOWN; - if (in_report->buttons[0] & XInput::Buttons0::DPAD_LEFT) gp_in.dpad |= gamepad.MAP_DPAD_LEFT; - if (in_report->buttons[0] & XInput::Buttons0::DPAD_RIGHT) gp_in.dpad |= gamepad.MAP_DPAD_RIGHT; - - if (in_report->buttons[0] & XInput::Buttons0::START) gp_in.buttons |= gamepad.MAP_BUTTON_START; - if (in_report->buttons[0] & XInput::Buttons0::BACK) gp_in.buttons |= gamepad.MAP_BUTTON_BACK; - if (in_report->buttons[0] & XInput::Buttons0::L3) gp_in.buttons |= gamepad.MAP_BUTTON_L3; - if (in_report->buttons[0] & XInput::Buttons0::R3) gp_in.buttons |= gamepad.MAP_BUTTON_R3; - if (in_report->buttons[1] & XInput::Buttons1::LB) gp_in.buttons |= gamepad.MAP_BUTTON_LB; - if (in_report->buttons[1] & XInput::Buttons1::RB) gp_in.buttons |= gamepad.MAP_BUTTON_RB; - if (in_report->buttons[1] & XInput::Buttons1::HOME) gp_in.buttons |= gamepad.MAP_BUTTON_SYS; - if (in_report->buttons[1] & XInput::Buttons1::A) gp_in.buttons |= gamepad.MAP_BUTTON_A; - if (in_report->buttons[1] & XInput::Buttons1::B) gp_in.buttons |= gamepad.MAP_BUTTON_B; - if (in_report->buttons[1] & XInput::Buttons1::X) gp_in.buttons |= gamepad.MAP_BUTTON_X; - if (in_report->buttons[1] & XInput::Buttons1::Y) gp_in.buttons |= gamepad.MAP_BUTTON_Y; - - gp_in.trigger_l = gamepad.scale_trigger_l(in_report->trigger_l); - gp_in.trigger_r = gamepad.scale_trigger_r(in_report->trigger_r); - - std::tie(gp_in.joystick_lx, gp_in.joystick_ly) = gamepad.scale_joystick_l(in_report->joystick_lx, in_report->joystick_ly, true); - std::tie(gp_in.joystick_rx, gp_in.joystick_ry) = gamepad.scale_joystick_r(in_report->joystick_rx, in_report->joystick_ry, true); + if (wButtons & (1U << 0)) gp_in.dpad |= gamepad.MAP_DPAD_UP; + if (wButtons & (1U << 1)) gp_in.dpad |= gamepad.MAP_DPAD_DOWN; + if (wButtons & (1U << 2)) gp_in.dpad |= gamepad.MAP_DPAD_LEFT; + if (wButtons & (1U << 3)) gp_in.dpad |= gamepad.MAP_DPAD_RIGHT; + if (wButtons & (1U << 4)) gp_in.buttons |= gamepad.MAP_BUTTON_START; + if (wButtons & (1U << 5)) gp_in.buttons |= gamepad.MAP_BUTTON_BACK; + if (wButtons & (1U << 6)) gp_in.buttons |= gamepad.MAP_BUTTON_L3; + if (wButtons & (1U << 7)) gp_in.buttons |= gamepad.MAP_BUTTON_R3; + if (wButtons & (1U << 8)) gp_in.buttons |= gamepad.MAP_BUTTON_LB; + if (wButtons & (1U << 9)) gp_in.buttons |= gamepad.MAP_BUTTON_RB; + if (wButtons & (1U << 10)) gp_in.buttons |= gamepad.MAP_BUTTON_SYS; + if (wButtons & (1U << 12)) gp_in.buttons |= gamepad.MAP_BUTTON_A; + if (wButtons & (1U << 13)) gp_in.buttons |= gamepad.MAP_BUTTON_B; + if (wButtons & (1U << 14)) gp_in.buttons |= gamepad.MAP_BUTTON_X; + if (wButtons & (1U << 15)) gp_in.buttons |= gamepad.MAP_BUTTON_Y; + + gp_in.trigger_l = gamepad.scale_trigger_l(report[8]); + gp_in.trigger_r = gamepad.scale_trigger_r(report[9]); + + const int16_t joy_lx = static_cast(static_cast(report[10]) | + (static_cast(report[11]) << 8)); + const int16_t joy_ly = static_cast(static_cast(report[12]) | + (static_cast(report[13]) << 8)); + const int16_t joy_rx = static_cast(static_cast(report[14]) | + (static_cast(report[15]) << 8)); + const int16_t joy_ry = static_cast(static_cast(report[16]) | + (static_cast(report[17]) << 8)); + + std::tie(gp_in.joystick_lx, gp_in.joystick_ly) = gamepad.scale_joystick_l(joy_lx, joy_ly, true); + std::tie(gp_in.joystick_rx, gp_in.joystick_ry) = gamepad.scale_joystick_r(joy_rx, joy_ry, true); gamepad.set_pad_in(gp_in); + std::memcpy(prev_in_report_, report, std::min(static_cast(len), sizeof(prev_in_report_))); tuh_xinput::receive_report(address, instance); - prev_in_report_ = *in_report; } bool Xbox360WHost::send_feedback(Gamepad& gamepad, uint8_t address, uint8_t instance) { - Gamepad::PadOut gp_out = gamepad.get_pad_out(); - return tuh_xinput::set_rumble(address, instance, gp_out.rumble_l, gp_out.rumble_r, false); + const uint8_t rumble_instance = + (active_address_ == address && active_instance_ != 0xFF) ? active_instance_ : instance; + if (!tuh_xinput::is_connected(address, rumble_instance)) + { + return false; + } + Gamepad::PadOut gp_out = gamepad.get_pad_out(); + if (gp_out.rumble_l == 0 && gp_out.rumble_r == 0) + { + return false; + } + return tuh_xinput::set_rumble(address, rumble_instance, gp_out.rumble_l, gp_out.rumble_r, false); } void Xbox360WHost::connect_cb(Gamepad& gamepad, uint8_t address, uint8_t instance) { - tid_chatpad_keepalive_ = TaskQueue::Core1::get_new_task_id(); - - //Might not be ready for leds yet, needs delay - TaskQueue::Core1::queue_delayed_task(TaskQueue::Core1::get_new_task_id(), 1000, false, - [address, instance, this] - { - tuh_xinput::set_led(address, instance, idx_ + 1, false); - tuh_xinput::xbox360_chatpad_init(address, instance); - - TaskQueue::Core1::queue_delayed_task(tid_chatpad_keepalive_, tuh_xinput::KEEPALIVE_MS, true, - [address, instance] - { - OGXM_LOG("XInput Chatpad Keepalive\r\n"); - tuh_xinput::xbox360_chatpad_keepalive(address, instance); - }); - }); + (void)gamepad; + active_address_ = address; + active_instance_ = instance; } void Xbox360WHost::disconnect_cb(Gamepad& gamepad, uint8_t address, uint8_t instance) { + (void)gamepad; + (void)address; + (void)instance; TaskQueue::Core1::cancel_delayed_task(tid_chatpad_keepalive_); -} \ No newline at end of file + std::memset(prev_in_report_, 0, sizeof(prev_in_report_)); + active_instance_ = 0xFF; + active_address_ = 0xFF; +} diff --git a/Firmware/RP2040/src/USBHost/HostDriver/XInput/Xbox360W.h b/Firmware/RP2040/src/USBHost/HostDriver/XInput/Xbox360W.h index e048f2d0..dcbee3c5 100644 --- a/Firmware/RP2040/src/USBHost/HostDriver/XInput/Xbox360W.h +++ b/Firmware/RP2040/src/USBHost/HostDriver/XInput/Xbox360W.h @@ -3,7 +3,6 @@ #include -#include "Descriptors/XInput.h" #include "USBHost/HostDriver/HostDriver.h" class Xbox360WHost : public HostDriver @@ -23,7 +22,9 @@ class Xbox360WHost : public HostDriver private: uint32_t tid_chatpad_keepalive_{0}; - XInput::InReportWireless prev_in_report_; + uint8_t prev_in_report_[32]{0}; + uint8_t active_instance_{0xFF}; + uint8_t active_address_{0xFF}; }; #endif // _XBOX360_WIRELESS_HOST_H_ \ No newline at end of file diff --git a/Firmware/RP2040/src/USBHost/HostDriver/XInput/XboxArcadeStick.h b/Firmware/RP2040/src/USBHost/HostDriver/XInput/XboxArcadeStick.h new file mode 100644 index 00000000..c665d05c --- /dev/null +++ b/Firmware/RP2040/src/USBHost/HostDriver/XInput/XboxArcadeStick.h @@ -0,0 +1,136 @@ +#ifndef _XBOX_ARCADE_STICK_H_ +#define _XBOX_ARCADE_STICK_H_ + +#include + +#include "Gamepad/Gamepad.h" +#include "USBHost/HardwareIDs.h" + +/** Xbox One GIP arcade sticks (Razer Atrox XBO, Mad Catz TE2, etc.) + * Init/read from OOPMan/XBOFS.win; standard pads use 64-byte Linux xpad GIP. */ +namespace XboxArcadeStick +{ + /** XBOFS: WinUsb_ReadPipe(..., 30). */ + static constexpr uint16_t GIP_IN_XFER_SIZE = 30; + + static constexpr HardwareID XBOX_ONE_GIP_IDS[] = { + {0x1532, 0x0a00}, // Razer Atrox Arcade Stick (Xbox One) + {0x0738, 0x4a01}, // Mad Catz FightStick TE 2 (Xbox One) + {0x0e6f, 0x015c}, // PDP Xbox One Arcade Stick + {0x0f0d, 0x0063}, // Hori RAP Hayabusa (Xbox One) + {0x0f0d, 0x0078}, // Hori RAP V Kai (Xbox One) + {0x0f0d, 0x00c5}, // Hori Fighting Commander ONE + }; + + static constexpr HardwareID XBOX360_DIGITAL_TRIGGER_IDS[] = { + {0x24c6, 0x5000}, // Razer Atrox Arcade Stick (Xbox 360) + {0x0738, 0x4758}, // Mad Catz Arcade Game Stick + {0x0738, 0x4728}, // Mad Catz Fightpad + {0x0738, 0x4738}, // Mad Catz SFIV Fightstick + {0x146b, 0x0604}, // Bigben DAIJA Arcade Stick + }; + + inline bool matches_id(uint16_t vid, uint16_t pid, const HardwareID* ids, size_t count) + { + for (size_t i = 0; i < count; ++i) + { + if (ids[i].vid == vid && ids[i].pid == pid) + { + return true; + } + } + return false; + } + + inline bool is_xbox_one_gip(uint16_t vid, uint16_t pid) + { + return matches_id(vid, pid, XBOX_ONE_GIP_IDS, + sizeof(XBOX_ONE_GIP_IDS) / sizeof(XBOX_ONE_GIP_IDS[0])); + } + + inline bool is_xbox360_digital_triggers(uint16_t vid, uint16_t pid) + { + return matches_id(vid, pid, XBOX360_DIGITAL_TRIGGER_IDS, + sizeof(XBOX360_DIGITAL_TRIGGER_IDS) / sizeof(XBOX360_DIGITAL_TRIGGER_IDS[0])); + } + + /** XBOFS GIP 0x20 layout (face/LT/RT on byte 22). */ + inline void map_gip_arcade_report(const uint8_t* data, uint16_t len, Gamepad& gamepad, Gamepad::PadIn& gp_in) + { + if (len < 23) + { + return; + } + + if (data[4] & 0x04) + { + gp_in.buttons |= gamepad.MAP_BUTTON_START; + } + if (data[4] & 0x08) + { + gp_in.buttons |= gamepad.MAP_BUTTON_BACK; + } + + if (data[5] & 0x01) + { + gp_in.dpad |= gamepad.MAP_DPAD_UP; + } + if (data[5] & 0x02) + { + gp_in.dpad |= gamepad.MAP_DPAD_DOWN; + } + if (data[5] & 0x04) + { + gp_in.dpad |= gamepad.MAP_DPAD_LEFT; + } + if (data[5] & 0x08) + { + gp_in.dpad |= gamepad.MAP_DPAD_RIGHT; + } + + const uint8_t face = data[22]; + if (face & 0x01) + { + gp_in.buttons |= gamepad.MAP_BUTTON_X; + } + if (face & 0x02) + { + gp_in.buttons |= gamepad.MAP_BUTTON_Y; + } + if (face & 0x10) + { + gp_in.buttons |= gamepad.MAP_BUTTON_A; + } + if (face & 0x20) + { + gp_in.buttons |= gamepad.MAP_BUTTON_B; + } + if (face & 0x04) + { + gp_in.buttons |= gamepad.MAP_BUTTON_RB; + } + if (face & 0x08) + { + gp_in.buttons |= gamepad.MAP_BUTTON_LB; + } + if (face & 0x40) + { + gp_in.trigger_r = 0xFF; + } + if (face & 0x80) + { + gp_in.trigger_l = 0xFF; + } + } + + inline bool gip_arcade_report_changed(const uint8_t* data, uint16_t len, const uint8_t* prev, uint16_t prev_len) + { + if (len < 23 || prev_len < 23) + { + return true; + } + return data[4] != prev[4] || data[5] != prev[5] || data[22] != prev[22]; + } +} + +#endif // _XBOX_ARCADE_STICK_H_ diff --git a/Firmware/RP2040/src/USBHost/HostDriver/XInput/XboxOne.cpp b/Firmware/RP2040/src/USBHost/HostDriver/XInput/XboxOne.cpp index 6dab8113..9790e112 100644 --- a/Firmware/RP2040/src/USBHost/HostDriver/XInput/XboxOne.cpp +++ b/Firmware/RP2040/src/USBHost/HostDriver/XInput/XboxOne.cpp @@ -1,58 +1,157 @@ #include #include "host/usbh.h" +#include "Board/board_api.h" +#include "TaskQueue/TaskQueue.h" #include "USBHost/HostDriver/XInput/tuh_xinput/tuh_xinput.h" #include "USBHost/HostDriver/XInput/XboxOne.h" +#include "USBHost/HostDriver/XInput/XboxArcadeStick.h" + +static constexpr uint32_t GUIDE_STALE_MS = 400u; void XboxOneHost::initialize(Gamepad& gamepad, uint8_t address, uint8_t instance, const uint8_t* report_desc, uint16_t desc_len) { - tuh_xinput::receive_report(address, instance); + uint16_t vid = 0; + uint16_t pid = 0; + tuh_vid_pid_get(address, &vid, &pid); + gip_arcade_stick_ = XboxArcadeStick::is_xbox_one_gip(vid, pid); + prev_arcade_len_ = 0; + std::memset(&prev_in_report_, 0, sizeof(prev_in_report_)); + (void)gamepad; + (void)report_desc; + (void)desc_len; + const uint8_t addr = address; + const uint8_t inst = instance; + TaskQueue::Core1::queue_delayed_task( + TaskQueue::Core1::get_new_task_id(), 50, false, + [addr, inst]() { tuh_xinput::start_xboxone(addr, inst); }); +} + +static void map_gip_buttons(Gamepad& gamepad, const XboxOne::InReport* in_report, Gamepad::PadIn& gp_in, + uint8_t guide_pressed) +{ + const uint16_t b = in_report->buttons; + if (b & XboxOne::GipWireButtons::DPAD_UP) gp_in.dpad |= gamepad.MAP_DPAD_UP; + if (b & XboxOne::GipWireButtons::DPAD_DOWN) gp_in.dpad |= gamepad.MAP_DPAD_DOWN; + if (b & XboxOne::GipWireButtons::DPAD_LEFT) gp_in.dpad |= gamepad.MAP_DPAD_LEFT; + if (b & XboxOne::GipWireButtons::DPAD_RIGHT) gp_in.dpad |= gamepad.MAP_DPAD_RIGHT; + + if (b & XboxOne::GipWireButtons::LEFT_THUMB) gp_in.buttons |= gamepad.MAP_BUTTON_L3; + if (b & XboxOne::GipWireButtons::RIGHT_THUMB) gp_in.buttons |= gamepad.MAP_BUTTON_R3; + if (b & XboxOne::GipWireButtons::LEFT_SHOULDER) gp_in.buttons |= gamepad.MAP_BUTTON_LB; + if (b & XboxOne::GipWireButtons::RIGHT_SHOULDER) gp_in.buttons |= gamepad.MAP_BUTTON_RB; + if (b & XboxOne::GipWireButtons::BACK) gp_in.buttons |= gamepad.MAP_BUTTON_BACK; + if (b & XboxOne::GipWireButtons::START) gp_in.buttons |= gamepad.MAP_BUTTON_START; + if (b & XboxOne::GipWireButtons::SYNC) gp_in.buttons |= gamepad.MAP_BUTTON_MISC; + if (guide_pressed) gp_in.buttons |= gamepad.MAP_BUTTON_SYS; + if (b & XboxOne::GipWireButtons::A) gp_in.buttons |= gamepad.MAP_BUTTON_A; + if (b & XboxOne::GipWireButtons::B) gp_in.buttons |= gamepad.MAP_BUTTON_B; + if (b & XboxOne::GipWireButtons::X) gp_in.buttons |= gamepad.MAP_BUTTON_X; + if (b & XboxOne::GipWireButtons::Y) gp_in.buttons |= gamepad.MAP_BUTTON_Y; + + gp_in.trigger_l = gamepad.scale_trigger_l(static_cast(in_report->trigger_l >> 2)); + gp_in.trigger_r = gamepad.scale_trigger_r(static_cast(in_report->trigger_r >> 2)); + + std::tie(gp_in.joystick_lx, gp_in.joystick_ly) = gamepad.scale_joystick_l(in_report->joystick_lx, in_report->joystick_ly, true); + std::tie(gp_in.joystick_rx, gp_in.joystick_ry) = gamepad.scale_joystick_r(in_report->joystick_rx, in_report->joystick_ry, true); } void XboxOneHost::process_report(Gamepad& gamepad, uint8_t address, uint8_t instance, const uint8_t* report, uint16_t len) { - const XboxOne::InReport* in_report = reinterpret_cast(report); - if (std::memcmp(&prev_in_report_ + 4, in_report + 4, 14) == 0) + const uint8_t cmd = report[0]; + if (cmd == XboxOne::GIP_CMD_VIRTUAL_KEY) { + last_guide_07_ms_ = board_api::ms_since_boot(); + if (len >= 5) { + if (report[4] == 0x5B && len >= 6) + guide_pressed_ = (report[5] & 0x01) ? 1 : 0; + else + guide_pressed_ = (report[4] & 0x01) ? 1 : 0; + } + Gamepad::PadIn gp_in; + if (gip_arcade_stick_ && prev_arcade_len_ >= 23) + { + XboxArcadeStick::map_gip_arcade_report(prev_arcade_report_.data(), prev_arcade_len_, gamepad, gp_in); + } + else + { + map_gip_buttons(gamepad, &prev_in_report_, gp_in, guide_pressed_); + } + if (guide_pressed_) + { + gp_in.buttons |= gamepad.MAP_BUTTON_SYS; + } + gamepad.set_pad_in(gp_in); tuh_xinput::receive_report(address, instance); return; } - Gamepad::PadIn gp_in; + if (cmd != XboxOne::GIP_CMD_INPUT) + { + tuh_xinput::receive_report(address, instance); + return; + } - if (in_report->buttons[1] & XboxOne::Buttons1::DPAD_UP) gp_in.dpad |= gamepad.MAP_DPAD_UP; - if (in_report->buttons[1] & XboxOne::Buttons1::DPAD_DOWN) gp_in.dpad |= gamepad.MAP_DPAD_DOWN; - if (in_report->buttons[1] & XboxOne::Buttons1::DPAD_LEFT) gp_in.dpad |= gamepad.MAP_DPAD_LEFT; - if (in_report->buttons[1] & XboxOne::Buttons1::DPAD_RIGHT) gp_in.dpad |= gamepad.MAP_DPAD_RIGHT; - - if (in_report->buttons[1] & XboxOne::Buttons1::L3) gp_in.buttons |= gamepad.MAP_BUTTON_L3; - if (in_report->buttons[1] & XboxOne::Buttons1::R3) gp_in.buttons |= gamepad.MAP_BUTTON_R3; - if (in_report->buttons[1] & XboxOne::Buttons1::LB) gp_in.buttons |= gamepad.MAP_BUTTON_LB; - if (in_report->buttons[1] & XboxOne::Buttons1::RB) gp_in.buttons |= gamepad.MAP_BUTTON_RB; - if (in_report->buttons[0] & XboxOne::Buttons0::BACK) gp_in.buttons |= gamepad.MAP_BUTTON_BACK; - if (in_report->buttons[0] & XboxOne::Buttons0::START) gp_in.buttons |= gamepad.MAP_BUTTON_START; - if (in_report->buttons[0] & XboxOne::Buttons0::SYNC) gp_in.buttons |= gamepad.MAP_BUTTON_MISC; - if (in_report->buttons[0] & XboxOne::Buttons0::GUIDE) gp_in.buttons |= gamepad.MAP_BUTTON_SYS; - if (in_report->buttons[0] & XboxOne::Buttons0::A) gp_in.buttons |= gamepad.MAP_BUTTON_A; - if (in_report->buttons[0] & XboxOne::Buttons0::B) gp_in.buttons |= gamepad.MAP_BUTTON_B; - if (in_report->buttons[0] & XboxOne::Buttons0::X) gp_in.buttons |= gamepad.MAP_BUTTON_X; - if (in_report->buttons[0] & XboxOne::Buttons0::Y) gp_in.buttons |= gamepad.MAP_BUTTON_Y; + if (gip_arcade_stick_) + { + bool guide_cleared = false; + if (guide_pressed_ && (board_api::ms_since_boot() - last_guide_07_ms_) > GUIDE_STALE_MS) + { + guide_pressed_ = 0; + guide_cleared = true; + } + if (!guide_cleared && + !XboxArcadeStick::gip_arcade_report_changed(report, len, prev_arcade_report_.data(), prev_arcade_len_)) + { + tuh_xinput::receive_report(address, instance); + return; + } - gp_in.trigger_l = gamepad.scale_trigger_l(static_cast(in_report->trigger_l >> 2)); - gp_in.trigger_r = gamepad.scale_trigger_r(static_cast(in_report->trigger_r >> 2)); + Gamepad::PadIn gp_in; + XboxArcadeStick::map_gip_arcade_report(report, len, gamepad, gp_in); + if (guide_pressed_) + { + gp_in.buttons |= gamepad.MAP_BUTTON_SYS; + } - std::tie(gp_in.joystick_lx, gp_in.joystick_ly) = gamepad.scale_joystick_l(in_report->joystick_lx, in_report->joystick_ly, true); - std::tie(gp_in.joystick_rx, gp_in.joystick_ry) = gamepad.scale_joystick_r(in_report->joystick_rx, in_report->joystick_ry, true); + gamepad.set_pad_in(gp_in); + tuh_xinput::receive_report(address, instance); + + const uint16_t copy_len = std::min(len, static_cast(prev_arcade_report_.size())); + std::memcpy(prev_arcade_report_.data(), report, copy_len); + prev_arcade_len_ = copy_len; + return; + } + + if (len < sizeof(XboxOne::InReport)) + { + tuh_xinput::receive_report(address, instance); + return; + } + + const XboxOne::InReport* in_report = reinterpret_cast(report); + bool guide_cleared = false; + if (guide_pressed_ && (board_api::ms_since_boot() - last_guide_07_ms_) > GUIDE_STALE_MS) { + guide_pressed_ = 0; + guide_cleared = true; + } + if (std::memcmp(&prev_in_report_ + 4, in_report + 4, 14) == 0 && !guide_cleared) + { + tuh_xinput::receive_report(address, instance); + return; + } + Gamepad::PadIn gp_in; + map_gip_buttons(gamepad, in_report, gp_in, guide_pressed_); gamepad.set_pad_in(gp_in); tuh_xinput::receive_report(address, instance); - std::memcpy(&prev_in_report_, in_report, 18); + std::memcpy(&prev_in_report_, in_report, sizeof(XboxOne::InReport)); } bool XboxOneHost::send_feedback(Gamepad& gamepad, uint8_t address, uint8_t instance) { Gamepad::PadOut gp_out = gamepad.get_pad_out(); return tuh_xinput::set_rumble(address, instance, gp_out.rumble_l, gp_out.rumble_r, false); -} \ No newline at end of file +} diff --git a/Firmware/RP2040/src/USBHost/HostDriver/XInput/XboxOne.h b/Firmware/RP2040/src/USBHost/HostDriver/XInput/XboxOne.h index 84836004..58951bb7 100644 --- a/Firmware/RP2040/src/USBHost/HostDriver/XInput/XboxOne.h +++ b/Firmware/RP2040/src/USBHost/HostDriver/XInput/XboxOne.h @@ -1,6 +1,7 @@ #ifndef _XBOX_ONE_HOST_H_ #define _XBOX_ONE_HOST_H_ +#include #include #include "Descriptors/XboxOne.h" @@ -18,6 +19,11 @@ class XboxOneHost : public HostDriver private: XboxOne::InReport prev_in_report_; + std::array prev_arcade_report_{}; + uint16_t prev_arcade_len_{0}; + bool gip_arcade_stick_{false}; + uint8_t guide_pressed_{0}; + uint32_t last_guide_07_ms_{0}; }; -#endif // _XBOX_ONE_HOST_H_ \ No newline at end of file +#endif // _XBOX_ONE_HOST_H_ diff --git a/Firmware/RP2040/src/USBHost/HostDriver/XInput/tuh_xinput/tuh_xinput.cpp b/Firmware/RP2040/src/USBHost/HostDriver/XInput/tuh_xinput/tuh_xinput.cpp index c07e0ccb..203ac895 100644 --- a/Firmware/RP2040/src/USBHost/HostDriver/XInput/tuh_xinput/tuh_xinput.cpp +++ b/Firmware/RP2040/src/USBHost/HostDriver/XInput/tuh_xinput/tuh_xinput.cpp @@ -7,6 +7,13 @@ #include "USBHost/HostDriver/XInput/tuh_xinput/tuh_xinput.h" #include "USBHost/HostDriver/XInput/tuh_xinput/tuh_xinput_cmd.h" +#include "USBHost/HostDriver/XInput/XboxArcadeStick.h" + +#include "Board/Config.h" +#include "Board/board_api.h" +#if defined(CONFIG_EN_USB_HOST) +#include "pio_usb.h" +#endif namespace tuh_xinput { @@ -101,17 +108,49 @@ static void std_sleep_ms(uint32_t ms) while ( std::chrono::duration_cast( std::chrono::high_resolution_clock::now() - start).count() < ms) { +#if defined(CONFIG_EN_USB_HOST) + pio_usb_host_frame(); +#endif tuh_task(); } } -static void wait_for_tx_complete(uint8_t dev_addr, uint8_t ep_addr) +static void wait_for_tx_complete(uint8_t dev_addr, uint8_t ep_addr, uint32_t timeout_ms = 200) { + const auto start = std::chrono::high_resolution_clock::now(); while (usbh_edpt_busy(dev_addr, ep_addr)) { + const auto elapsed = std::chrono::duration_cast( + std::chrono::high_resolution_clock::now() - start).count(); + if (static_cast(elapsed) >= timeout_ms) + { + return; + } +#if defined(CONFIG_EN_USB_HOST) + pio_usb_host_frame(); +#endif + tuh_task(); + } +} + +static void prime_port_for_pairing(uint8_t dev_addr, uint8_t instance); + +#if defined(CONFIG_EN_USB_HOST) +static void service_usb_host_frames(uint8_t frames = 4) +{ + for (uint8_t i = 0; i < frames; ++i) + { + pio_usb_host_frame(); tuh_task(); } } +#else +static void service_usb_host_frames(uint8_t frames = 4) +{ + (void)frames; + tuh_task(); +} +#endif bool send_ctrl_xfer(uint8_t dev_addr, const tusb_control_request_t* request, uint8_t* buffer, tuh_xfer_cb_t complete_cb, uintptr_t user_data) { @@ -127,28 +166,71 @@ bool send_ctrl_xfer(uint8_t dev_addr, const tusb_control_request_t* request, uin return tuh_control_xfer(&transfer); } +static bool send_gip_packet(Interface* interface, uint8_t dev_addr, uint8_t instance, + const uint8_t* packet, uint16_t len, bool assign_seq = true) +{ + uint8_t buf[ENDPOINT_SIZE]; + if (len > sizeof(buf)) + { + return false; + } + std::memcpy(buf, packet, len); + if (assign_seq && len >= 3) + { + buf[2] = interface->gip_out_seq++; + } + return send_report(dev_addr, instance, buf, len); +} + +static void xboxone_ack_virtual_key(Interface* interface, uint8_t dev_addr, uint8_t instance, uint8_t seq) +{ + uint8_t ack[sizeof(XboxOne::VIRTUAL_KEY_ACK)]; + std::memcpy(ack, XboxOne::VIRTUAL_KEY_ACK, sizeof(ack)); + ack[2] = seq; + send_gip_packet(interface, dev_addr, instance, ack, sizeof(ack), false); +} + +static void xboxone_send_power_on(Interface* interface, uint8_t dev_addr, uint8_t instance) +{ + if (interface->gip_power_sent) + { + return; + } + send_gip_packet(interface, dev_addr, instance, XboxOne::POWER_ON, sizeof(XboxOne::POWER_ON)); + interface->gip_power_sent = true; +} + static void xboxone_init(Interface *interface, uint8_t dev_addr, uint8_t instance) { uint16_t PID, VID; tuh_vid_pid_get(dev_addr, &VID, &PID); - send_report(dev_addr, instance, XboxOne::POWER_ON, sizeof(XboxOne::POWER_ON)); + const bool arcade = interface->gip_arcade_stick || XboxArcadeStick::is_xbox_one_gip(VID, PID); + interface->gip_out_seq = 0; + + xboxone_send_power_on(interface, dev_addr, instance); + if (arcade) + { + return; + } wait_for_tx_complete(dev_addr, interface->ep_out); - send_report(dev_addr, instance, XboxOne::S_INIT, sizeof(XboxOne::S_INIT)); + + send_gip_packet(interface, dev_addr, instance, XboxOne::S_INIT, sizeof(XboxOne::S_INIT)); wait_for_tx_complete(dev_addr, interface->ep_out); if (VID == 0x045e && (PID == 0x0b00)) { - send_report(dev_addr, instance, XboxOne::EXTRA_INPUT_PACKET_INIT, sizeof(XboxOne::EXTRA_INPUT_PACKET_INIT)); + send_gip_packet(interface, dev_addr, instance, XboxOne::EXTRA_INPUT_PACKET_INIT, + sizeof(XboxOne::EXTRA_INPUT_PACKET_INIT)); wait_for_tx_complete(dev_addr, interface->ep_out); } //Required for PDP aftermarket controllers if (VID == 0x0e6f) { - send_report(dev_addr, instance, XboxOne::PDP_LED_ON, sizeof(XboxOne::PDP_LED_ON)); + send_gip_packet(interface, dev_addr, instance, XboxOne::PDP_LED_ON, sizeof(XboxOne::PDP_LED_ON)); wait_for_tx_complete(dev_addr, interface->ep_out); - send_report(dev_addr, instance, XboxOne::PDP_AUTH, sizeof(XboxOne::PDP_AUTH)); + send_gip_packet(interface, dev_addr, instance, XboxOne::PDP_AUTH, sizeof(XboxOne::PDP_AUTH)); wait_for_tx_complete(dev_addr, interface->ep_out); } } @@ -182,14 +264,29 @@ static bool open(uint8_t rhport, uint8_t dev_addr, tusb_desc_interface_t const * } else if (desc_itf->bInterfaceSubClass == 0x47 && desc_itf->bInterfaceProtocol == 0xD0) { - itf_type = ItfType::XID; - dev_type = DevType::XBOXONE; + if (desc_itf->bInterfaceNumber == 0) + { + itf_type = ItfType::XID; + dev_type = DevType::XBOXONE; + } } else if (desc_itf->bInterfaceClass == 0x58 && desc_itf->bInterfaceSubClass == 0x42) { itf_type = ItfType::XID; dev_type = DevType::XBOXOG; } + else if (desc_itf->bInterfaceClass == TUSB_CLASS_VENDOR_SPECIFIC && desc_itf->bNumEndpoints >= 2) + { + uint16_t vid = 0; + uint16_t pid = 0; + tuh_vid_pid_get(dev_addr, &vid, &pid); + if (desc_itf->bInterfaceNumber == 0 && + XboxArcadeStick::is_xbox_one_gip(vid, pid)) + { + itf_type = ItfType::XID; + dev_type = DevType::XBOXONE; + } + } TU_VERIFY(dev_type != DevType::UNKNOWN && itf_type != ItfType::UNKNOWN); @@ -217,6 +314,13 @@ static bool open(uint8_t rhport, uint8_t dev_addr, tusb_desc_interface_t const * interface->itf_type = itf_type; interface->dev_type = dev_type; interface->dev_addr = dev_addr; + if (dev_type == DevType::XBOXONE) + { + uint16_t vid = 0; + uint16_t pid = 0; + tuh_vid_pid_get(dev_addr, &vid, &pid); + interface->gip_arcade_stick = XboxArcadeStick::is_xbox_one_gip(vid, pid); + } if (tu_edpt_dir(desc_ep->bEndpointAddress) == TUSB_DIR_OUT) { @@ -254,9 +358,6 @@ static bool set_config(uint8_t dev_addr, uint8_t itf_num) send_report(dev_addr, instance, Xbox360W::INQUIRE_PRESENT, sizeof(Xbox360W::INQUIRE_PRESENT)); wait_for_tx_complete(dev_addr, interface->ep_out); break; - case DevType::XBOXONE: - xboxone_init(interface, dev_addr, instance); - break; default: break; } @@ -266,6 +367,12 @@ static bool set_config(uint8_t dev_addr, uint8_t itf_num) mount_cb(dev_addr, instance, interface); } + if (interface->dev_type == DevType::XBOX360W) + { + receive_report(dev_addr, instance); + prime_port_for_pairing(dev_addr, instance); + } + usbh_driver_set_config_complete(dev_addr, interface->itf_num); return true; } @@ -273,8 +380,9 @@ static bool set_config(uint8_t dev_addr, uint8_t itf_num) static bool xfer_cb(uint8_t dev_addr, uint8_t ep_addr, xfer_result_t result, uint32_t xferred_bytes) { Interface* interface = get_itf_by_ep(dev_addr, ep_addr); + TU_VERIFY(interface != nullptr); uint8_t instance = get_instance_by_itf_num(dev_addr, interface->itf_num); - TU_VERIFY(interface != nullptr && instance != INVALID_IDX); + TU_VERIFY(instance != INVALID_IDX); const uint8_t dir = tu_edpt_dir(ep_addr); @@ -282,16 +390,27 @@ static bool xfer_cb(uint8_t dev_addr, uint8_t ep_addr, xfer_result_t result, uin { if (dir == TUSB_DIR_IN) { - report_received_cb(dev_addr, instance, interface->ep_in_buffer.data(), interface->ep_in_size); + receive_report(dev_addr, instance); } else if (report_sent_cb) { report_sent_cb(dev_addr, instance, interface->ep_out_buffer.data(), interface->ep_out_size); } + return true; } if (dir == TUSB_DIR_IN) { + if (host_activity_cb) + { + host_activity_cb(dev_addr, instance); + } + + if (interface->dev_type == DevType::XBOXONE) + { + interface->gip_last_in_ok_ms = board_api::ms_since_boot(); + } + bool new_pad_data = false; uint8_t* in_buffer = interface->ep_in_buffer.data(); @@ -306,15 +425,16 @@ static bool xfer_cb(uint8_t dev_addr, uint8_t ep_addr, xfer_result_t result, uin case DevType::XBOX360W: if (in_buffer[0] & 0x08) { - if (in_buffer[1] != 0x00 && !interface->connected) + /* Byte1 bit7 = controller present (0x80). Do not treat headset-only 0x40 as paired. */ + if ((in_buffer[1] & 0x80) && !interface->connected) { interface->connected = true; TU_LOG1("Xbox 360 wireless controller connected\n"); - //I think some 3rd party adapters need this: - std_sleep_ms(1000); send_report(dev_addr, instance, Xbox360W::RUMBLE_ENABLE, sizeof(Xbox360W::RUMBLE_ENABLE)); + wait_for_tx_complete(dev_addr, interface->ep_out); + set_led(dev_addr, instance, wireless_led_quadrant(dev_addr, instance), true); if (xbox360w_connect_cb) { @@ -334,7 +454,9 @@ static bool xfer_cb(uint8_t dev_addr, uint8_t ep_addr, xfer_result_t result, uin } } } - if ((in_buffer[1] & 1 && in_buffer[5] == 0x13) || (in_buffer[1] & 2)) + if (((in_buffer[1] & 1) && xferred_bytes >= 18 && + (in_buffer[5] == 0x13 || in_buffer[5] == 0x14)) || + (in_buffer[1] & 2)) { new_pad_data = true; } @@ -346,19 +468,21 @@ static bool xfer_cb(uint8_t dev_addr, uint8_t ep_addr, xfer_result_t result, uin new_pad_data = true; break; case XboxOne::GIP_CMD_VIRTUAL_KEY: - if (in_buffer[4] == 0x01 && !(in_buffer[4] & (1 << 1))) + if (in_buffer[1] == (XboxOne::GIP_OPT_ACK | XboxOne::GIP_OPT_INTERNAL)) { - in_buffer[4] |= (1 << 1); - new_pad_data = true; + xboxone_ack_virtual_key(interface, dev_addr, instance, in_buffer[2]); } - else if (in_buffer[4] == 0x00 && (in_buffer[4] & (1 << 1))) + if (xferred_bytes >= 5) { - in_buffer[4] &= ~(1 << 1); new_pad_data = true; } break; case XboxOne::GIP_CMD_ANNOUNCE: - xboxone_init(interface, dev_addr, instance); + if (interface->gip_arcade_stick && !interface->gip_power_sent && + !usbh_edpt_busy(dev_addr, interface->ep_out)) + { + xboxone_send_power_on(interface, dev_addr, instance); + } break; } break; @@ -412,6 +536,9 @@ void close(uint8_t dev_addr) TU_LOG1("XInput unmount\r\n"); device->interfaces[i].itf_num = 0xFF; device->interfaces[i].connected = false; + device->interfaces[i].gip_power_sent = false; + device->interfaces[i].gip_last_in_ok_ms = 0; + device->interfaces[i].gip_last_in_arm_ms = 0; } } } @@ -458,14 +585,157 @@ bool receive_report(uint8_t dev_addr, uint8_t instance) Interface* interface = get_itf_by_instance(dev_addr, instance); TU_VERIFY(interface != nullptr); - if (!usbh_edpt_xfer(dev_addr, interface->ep_in, interface->ep_in_buffer.data(), interface->ep_in_size)) + uint16_t in_size = interface->ep_in_size; + if (interface->dev_type == DevType::XBOXONE) + { + if (interface->gip_arcade_stick) + { + in_size = XboxArcadeStick::GIP_IN_XFER_SIZE; + } + else + { + in_size = ENDPOINT_SIZE; + } + } + + TU_VERIFY(usbh_edpt_claim(dev_addr, interface->ep_in)); + + if (!usbh_edpt_xfer(dev_addr, interface->ep_in, interface->ep_in_buffer.data(), in_size)) { usbh_edpt_release(dev_addr, interface->ep_in); return false; } + return true; } +bool is_connected(uint8_t dev_addr, uint8_t instance) +{ + Interface* interface = get_itf_by_instance(dev_addr, instance); + return interface != nullptr && interface->connected; +} + +uint8_t wireless_led_quadrant(uint8_t dev_addr, uint8_t instance) +{ + Interface* interface = get_itf_by_instance(dev_addr, instance); + if (interface == nullptr || interface->dev_type != DevType::XBOX360W) + { + return static_cast(instance + 1); + } + /* Receiver USB layout: controller ports on interface numbers 0,2,4,6. */ + return static_cast((interface->itf_num / 2) + 1); +} + +void service_wireless_ports(uint8_t dev_addr) +{ + Device* device = get_device_by_addr(dev_addr); + if (device == nullptr) + { + return; + } + for (uint8_t i = 0; i < device->interfaces.size(); ++i) + { + if (device->interfaces[i].dev_type == DevType::XBOX360W && + device->interfaces[i].itf_num != INVALID_IDX) + { + prime_port_for_pairing(dev_addr, i); + } + } +} + +void start_xboxone(uint8_t dev_addr, uint8_t instance) +{ + Interface* interface = get_itf_by_instance(dev_addr, instance); + if (interface == nullptr || interface->dev_type != DevType::XBOXONE) + { + return; + } + + uint16_t vid = 0; + uint16_t pid = 0; + tuh_vid_pid_get(dev_addr, &vid, &pid); + const bool arcade = interface->gip_arcade_stick || XboxArcadeStick::is_xbox_one_gip(vid, pid); + + interface->gip_power_sent = false; + interface->gip_last_in_ok_ms = 0; + interface->gip_last_in_arm_ms = 0; + interface->gip_out_seq = 0; + + if (arcade) + { + /* XBOFS: write init, wait for OUT, then 30-byte read loop. */ + xboxone_send_power_on(interface, dev_addr, instance); + wait_for_tx_complete(dev_addr, interface->ep_out, 500); + service_usb_host_frames(8); + if (receive_report(dev_addr, instance)) + { + interface->gip_last_in_arm_ms = board_api::ms_since_boot(); + } + service_usb_host_frames(8); + return; + } + + if (receive_report(dev_addr, instance)) + { + interface->gip_last_in_arm_ms = board_api::ms_since_boot(); + } + service_usb_host_frames(); + xboxone_init(interface, dev_addr, instance); + service_usb_host_frames(); +} + +void service_gip(uint8_t dev_addr, uint8_t instance) +{ + Interface* interface = get_itf_by_instance(dev_addr, instance); + if (interface == nullptr || interface->dev_type != DevType::XBOXONE || + interface->itf_num == INVALID_IDX) + { + return; + } + + if (usbh_edpt_busy(dev_addr, interface->ep_in)) + { + return; + } + + const uint32_t now_ms = board_api::ms_since_boot(); + + if (interface->gip_arcade_stick) + { + static constexpr uint32_t ARCADE_REARM_MS = 250; + if (interface->gip_last_in_arm_ms != 0 && + (now_ms - interface->gip_last_in_arm_ms) < ARCADE_REARM_MS) + { + return; + } + if (receive_report(dev_addr, instance)) + { + interface->gip_last_in_arm_ms = now_ms; + } + return; + } + + static constexpr uint32_t IN_STALL_MS = 2000; + static constexpr uint32_t IN_REARM_MIN_MS = 500; + + if (interface->gip_last_in_ok_ms != 0 && + (now_ms - interface->gip_last_in_ok_ms) < IN_STALL_MS) + { + return; + } + + if (interface->gip_last_in_arm_ms != 0 && + (now_ms - interface->gip_last_in_arm_ms) < IN_REARM_MIN_MS) + { + return; + } + + if (receive_report(dev_addr, instance)) + { + interface->gip_last_in_arm_ms = now_ms; + } +} + bool set_led(uint8_t dev_addr, uint8_t instance, uint8_t quadrant, bool block) { Interface* interface = get_itf_by_instance(dev_addr, instance); @@ -498,6 +768,23 @@ bool set_led(uint8_t dev_addr, uint8_t instance, uint8_t quadrant, bool block) return ret; } +/** RUMBLE_ENABLE + player LED on an idle wireless port so sync can complete on any RF slot. */ +static void prime_port_for_pairing(uint8_t dev_addr, uint8_t instance) +{ + Interface* interface = get_itf_by_instance(dev_addr, instance); + if (interface == nullptr || interface->dev_type != DevType::XBOX360W || + interface->connected || interface->itf_num == INVALID_IDX) + { + return; + } + if (usbh_edpt_busy(dev_addr, interface->ep_out)) + { + return; + } + send_report(dev_addr, instance, Xbox360W::RUMBLE_ENABLE, sizeof(Xbox360W::RUMBLE_ENABLE)); + set_led(dev_addr, instance, wireless_led_quadrant(dev_addr, instance), false); +} + bool set_rumble(uint8_t dev_addr, uint8_t instance, uint8_t rumble_l, uint8_t rumble_r, bool block) { Interface* interface = get_itf_by_instance(dev_addr, instance); @@ -509,12 +796,19 @@ bool set_rumble(uint8_t dev_addr, uint8_t instance, uint8_t rumble_l, uint8_t ru switch (interface->dev_type) { case DevType::XBOX360W: + if (!interface->connected) + { + return false; + } std::memcpy(buffer, Xbox360W::RUMBLE, sizeof(Xbox360W::RUMBLE)); buffer[5] = rumble_l; buffer[6] = rumble_r; len = sizeof(Xbox360W::RUMBLE); break; case DevType::XBOX360: + send_report(dev_addr, instance, Xbox360::RUMBLE, sizeof(Xbox360::RUMBLE)); + wait_for_tx_complete(dev_addr, interface->ep_out); + std::memcpy(buffer, Xbox360::RUMBLE, sizeof(Xbox360::RUMBLE)); buffer[3] = rumble_l; buffer[4] = rumble_r; @@ -522,6 +816,7 @@ bool set_rumble(uint8_t dev_addr, uint8_t instance, uint8_t rumble_l, uint8_t ru break; case DevType::XBOXONE: std::memcpy(buffer, XboxOne::RUMBLE, sizeof(XboxOne::RUMBLE)); + buffer[2] = interface->gip_out_seq++; buffer[8] = rumble_l / 2; // 0 - 128 buffer[9] = rumble_r / 2; // 0 - 128 len = sizeof(XboxOne::RUMBLE); @@ -550,7 +845,7 @@ void xbox360_chatpad_init(uint8_t address, uint8_t instance) { TU_LOG1("XInput Chatpad Init\r\n"); - Interface* interface = get_itf_by_itf_num(address, instance); + Interface* interface = get_itf_by_instance(address, instance); TU_VERIFY(interface != nullptr && interface->connected, ); TU_VERIFY(interface->dev_type == DevType::XBOX360W, ); //Only supported on Xbox 360 Wireless atm, wired is more complicated @@ -574,7 +869,7 @@ void xbox360_chatpad_init(uint8_t address, uint8_t instance) bool xbox360_chatpad_keepalive(uint8_t address, uint8_t instance) { - Interface* interface = get_itf_by_itf_num(address, instance); + Interface* interface = get_itf_by_instance(address, instance); TU_VERIFY(interface != nullptr, false); TU_VERIFY(interface->connected && interface->chatpad_inited, false); diff --git a/Firmware/RP2040/src/USBHost/HostDriver/XInput/tuh_xinput/tuh_xinput.h b/Firmware/RP2040/src/USBHost/HostDriver/XInput/tuh_xinput/tuh_xinput.h index 76d46bc6..11e80773 100644 --- a/Firmware/RP2040/src/USBHost/HostDriver/XInput/tuh_xinput/tuh_xinput.h +++ b/Firmware/RP2040/src/USBHost/HostDriver/XInput/tuh_xinput/tuh_xinput.h @@ -59,6 +59,15 @@ namespace tuh_xinput struct Interface { bool connected{false}; + /** Vendor-class GIP arcade stick (Razer Atrox XBO, etc.) — xpad MAP_TRIGGERS_TO_BUTTONS layout. */ + bool gip_arcade_stick{false}; + /** GIP OUT sequence byte (Linux xpad odata_serial). */ + uint8_t gip_out_seq{0}; + /** Linux xpad sends xboxone_power_on once; repeats make arcade sticks flash/cycle power. */ + bool gip_power_sent{false}; + /** Last successful GIP IN xfer (ms); used to re-arm IN only when the chain stalls. */ + uint32_t gip_last_in_ok_ms{0}; + uint32_t gip_last_in_arm_ms{0}; DevType dev_type{DevType::UNKNOWN}; ItfType itf_type{ItfType::UNKNOWN}; @@ -87,6 +96,15 @@ namespace tuh_xinput bool receive_report(uint8_t address, uint8_t instance); bool set_rumble(uint8_t address, uint8_t instance, uint8_t rumble_l, uint8_t rumble_r, bool block); bool set_led(uint8_t address, uint8_t instance, uint8_t led_number, bool block); + bool is_connected(uint8_t address, uint8_t instance); + /** Player LED quadrant (1–4) from wireless receiver USB interface number (0,2,4,6). */ + uint8_t wireless_led_quadrant(uint8_t address, uint8_t instance); + /** Keep all four wireless ports primed (RUMBLE_ENABLE + LED) until a pad connects. */ + void service_wireless_ports(uint8_t dev_addr); + /** Arm IN + GIP OUT init (call from XboxOneHost::initialize after mount). */ + void start_xboxone(uint8_t dev_addr, uint8_t instance); + /** Re-arm stalled GIP IN on PIO USB (call from HostManager::send_feedback). */ + void service_gip(uint8_t dev_addr, uint8_t instance); //Wireless only atm void xbox360_chatpad_init(uint8_t address, uint8_t instance); @@ -99,6 +117,8 @@ namespace tuh_xinput TU_ATTR_WEAK void report_sent_cb(uint8_t dev_addr, uint8_t instance, uint8_t const* report, uint16_t len); TU_ATTR_WEAK void xbox360w_connect_cb(uint8_t dev_addr, uint8_t instance); TU_ATTR_WEAK void xbox360w_disconnect_cb(uint8_t dev_addr, uint8_t instance); + /** Called on every successful XInput IN transfer (keepalive for host idle watchdog). */ + TU_ATTR_WEAK void host_activity_cb(uint8_t dev_addr, uint8_t instance); void report_received_cb(uint8_t dev_addr, uint8_t instance, uint8_t const* report, uint16_t len); }; // namespace tuh_xinput diff --git a/Firmware/RP2040/src/USBHost/HostDriver/XInput/tuh_xinput/tuh_xinput_cmd.h b/Firmware/RP2040/src/USBHost/HostDriver/XInput/tuh_xinput/tuh_xinput_cmd.h index 17c2bab2..03f96d9c 100644 --- a/Firmware/RP2040/src/USBHost/HostDriver/XInput/tuh_xinput/tuh_xinput_cmd.h +++ b/Firmware/RP2040/src/USBHost/HostDriver/XInput/tuh_xinput/tuh_xinput_cmd.h @@ -42,6 +42,10 @@ namespace tuh_xinput static constexpr uint8_t POWER_ON[] = { GIP_CMD_POWER, GIP_OPT_INTERNAL, GIP_SEQ0, GIP_PL_LEN(1), GIP_PWR_ON }; static constexpr uint8_t S_INIT[] = { GIP_CMD_POWER, GIP_OPT_INTERNAL, GIP_SEQ0, GIP_PL_LEN(15), 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x55, 0x53, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; static constexpr uint8_t S_LED_INIT[] = { GIP_CMD_LED, GIP_OPT_INTERNAL, GIP_SEQ0, GIP_PL_LEN(3), 0x00, 0x01, 0x14 }; + static constexpr uint8_t VIRTUAL_KEY_ACK[] = { + GIP_CMD_ACK, GIP_OPT_INTERNAL, GIP_SEQ0, GIP_PL_LEN(9), + 0x00, GIP_CMD_VIRTUAL_KEY, GIP_OPT_INTERNAL, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00 + }; static constexpr uint8_t EXTRA_INPUT_PACKET_INIT[] = { 0x4d, 0x10, GIP_SEQ0, 0x02, 0x07, 0x00 }; static constexpr uint8_t PDP_LED_ON[] = { GIP_CMD_LED, GIP_OPT_INTERNAL, GIP_SEQ0, GIP_PL_LEN(3), 0x00, GIP_LED_ON, 0x14 }; static constexpr uint8_t PDP_AUTH[] = { GIP_CMD_AUTHENTICATE, GIP_OPT_INTERNAL, GIP_SEQ0, GIP_PL_LEN(2), 0x01, 0x00 }; diff --git a/Firmware/RP2040/src/USBHost/HostManager.h b/Firmware/RP2040/src/USBHost/HostManager.h index e8638fb2..1769b720 100644 --- a/Firmware/RP2040/src/USBHost/HostManager.h +++ b/Firmware/RP2040/src/USBHost/HostManager.h @@ -9,7 +9,20 @@ #include #include "Board/Config.h" +#include "Board/board_api.h" +#include "Board/ogxm_log.h" +#if defined(CONFIG_EN_USB_HOST) +#include "pio_usb.h" +#endif +#include "UserSettings/UserSettings.h" +#include "USBDevice/DeviceDriver/DeviceDriverTypes.h" #include "USBHost/HardwareIDs.h" + +#if defined(CONFIG_OGXM_DEBUG) +#define debug_printf OGXM_LOG +#else +#define debug_printf(...) ((void)0) +#endif #include "USBHost/HostDriver/XInput/tuh_xinput/tuh_xinput.h" #include "USBHost/HostDriver/HostDriver.h" #include "USBHost/HostDriver/PS5/PS5.h" @@ -19,6 +32,7 @@ #include "USBHost/HostDriver/DInput/DInput.h" #include "USBHost/HostDriver/SwitchWired/SwitchWired.h" #include "USBHost/HostDriver/SwitchPro/SwitchPro.h" +#include "USBHost/HostDriver/SwitchPro/Switch2ProHost.h" #include "USBHost/HostDriver/XInput/XboxOne.h" #include "USBHost/HostDriver/XInput/Xbox360.h" #include "USBHost/HostDriver/XInput/Xbox360W.h" @@ -26,7 +40,10 @@ #include "USBHost/HostDriver/N64/N64.h" #include "USBHost/HostDriver/HIDGeneric/HIDGeneric.h" -#define MAX_INTERFACES MAX_GAMEPADS //This may change if support is added for audio or other chatpads beside 360 wireless +/** Per USB device: TinyUSB HID instance indices and XInput instance indices are separate namespaces + * (both often start at 0). Reserve [0 .. MAX_GAMEPADS-1] for HID and [MAX_GAMEPADS ..] for XInput. + * OGXM_TUH_XINPUT_INSTANCES is 4 when MAX_GAMEPADS < 4 (360 wireless receiver). */ +#define MAX_INTERFACES (MAX_GAMEPADS + OGXM_TUH_XINPUT_INSTANCES) class HostManager { @@ -48,13 +65,14 @@ class HostManager { gamepads_[i] = &gamepads[i]; } + last_usb_host_input_ms_ = board_api::ms_since_boot(); } //XInput doesn't need report_desc or desc_len - inline bool setup_driver(const HostDriverType driver_type, const uint8_t address, const uint8_t instance, uint8_t const* report_desc = nullptr, uint16_t desc_len = 0) + inline bool setup_driver(const HostDriverType driver_type, DriverClass dclass, const uint8_t address, const uint8_t instance, uint8_t const* report_desc = nullptr, uint16_t desc_len = 0) { - uint8_t gp_idx = find_free_gamepad(); - if (gp_idx == INVALID_IDX || instance >= MAX_INTERFACES) + const uint8_t si = host_storage_index(dclass, instance); + if (si == INVALID_IDX || si >= MAX_INTERFACES) { return false; } @@ -68,50 +86,126 @@ class HostManager } Device& device_slot = device_slots_[dev_idx]; - Interface& interface = device_slot.interfaces[instance]; + + /* Xbox 360 wireless receiver: one HostManager driver, four tuh_xinput instances. */ + if (driver_type == HostDriverType::XBOX360W && device_slot.address == address) + { + for (uint8_t i = 0; i < MAX_INTERFACES; ++i) + { + if (i == si) + { + continue; + } + const Interface& other = device_slot.interfaces[i]; + if (other.driver && other.host_driver_type == HostDriverType::XBOX360W) + { + return true; + } + } + } + + /* One physical pad on a composite that exposes both XInput and HID (e.g. some third-party + * controllers): share gamepad index. Xbox 360 wireless receiver: 4 XInput interfaces share + * one gamepad when MAX_GAMEPADS is 1. */ + uint8_t gp_idx = INVALID_IDX; + if (device_slot.address == address) + { + for (uint8_t i = 0; i < MAX_INTERFACES; ++i) + { + if (i == si) + { + continue; + } + const Interface& other = device_slot.interfaces[i]; + if (!other.driver || other.gamepad_idx == INVALID_IDX || other.driver_class == DriverClass::NONE) + { + continue; + } + const bool complementary = + (dclass == DriverClass::HID && other.driver_class == DriverClass::XINPUT) || + (dclass == DriverClass::XINPUT && other.driver_class == DriverClass::HID); + const bool xbox360w_sibling = + (driver_type == HostDriverType::XBOX360W && + other.host_driver_type == HostDriverType::XBOX360W); + if (complementary || xbox360w_sibling) + { + gp_idx = other.gamepad_idx; + break; + } + } + } + if (gp_idx == INVALID_IDX) + { + gp_idx = find_free_gamepad(); + } + if (gp_idx == INVALID_IDX) + { + return false; + } + + Interface& interface = device_slot.interfaces[si]; + + debug_printf("Attempting to allocate driver for index %d\n", gp_idx); switch (driver_type) { case HostDriverType::PS5: + debug_printf("PS5 Loaded\n"); fflush(stdout); interface.driver = std::make_unique(gp_idx); break; case HostDriverType::PS4: + debug_printf("PS4 Loaded\n"); fflush(stdout); interface.driver = std::make_unique(gp_idx); break; case HostDriverType::PS3: + debug_printf("PS3 Loaded\n"); fflush(stdout); interface.driver = std::make_unique(gp_idx); break; case HostDriverType::DINPUT: + debug_printf("DINPUT Loaded\n"); fflush(stdout); interface.driver = std::make_unique(gp_idx); break; case HostDriverType::SWITCH: + debug_printf("SWITCH Loaded\n"); fflush(stdout); interface.driver = std::make_unique(gp_idx); break; + case HostDriverType::SWITCH_PRO_2: + debug_printf("SWITCH PRO 2 Loaded\n"); fflush(stdout); + interface.driver = std::make_unique(gp_idx); + break; case HostDriverType::SWITCH_PRO: + debug_printf("SWITCH PRO Loaded\n"); fflush(stdout); interface.driver = std::make_unique(gp_idx); break; case HostDriverType::N64: + debug_printf("N64 Loaded\n"); fflush(stdout); interface.driver = std::make_unique(gp_idx); break; case HostDriverType::PSCLASSIC: + debug_printf("PSCLASSIC Loaded\n"); fflush(stdout); interface.driver = std::make_unique(gp_idx); break; case HostDriverType::XBOXOG: + debug_printf("XBOXOG Loaded\n"); fflush(stdout); interface.driver = std::make_unique(gp_idx); break; case HostDriverType::XBOXONE: + debug_printf("XBOXONE Loaded\n"); fflush(stdout); interface.driver = std::make_unique(gp_idx); break; case HostDriverType::XBOX360: + debug_printf("XBOX360 Loaded\n"); fflush(stdout); interface.driver = std::make_unique(gp_idx); break; case HostDriverType::XBOX360W: //Composite device, takes up all 4 gamepads when mounted + debug_printf("XBOX360W Loaded\n"); fflush(stdout); interface.driver = std::make_unique(gp_idx); break; default: if (is_hid_gamepad(report_desc, desc_len)) { interface.driver = std::make_unique(gp_idx); + debug_printf("HIDHOST Loaded\n"); fflush(stdout); } else { @@ -120,81 +214,298 @@ class HostManager break; } + { + HostDriverType stored_type = driver_type; + if (stored_type == HostDriverType::UNKNOWN && dclass == DriverClass::HID && interface.driver) + { + stored_type = HostDriverType::HID_GENERIC; + } + interface.host_driver_type = stored_type; + } + device_slot.address = address; + interface.driver_class = dclass; + interface.usb_instance = instance; interface.gamepad_idx = gp_idx; interface.gamepad = gamepads_[gp_idx]; + // Wii U GC adapter: Xbox controllers report positive Y for up; Nintendo use negative + const bool xbox_stick_y = (driver_type == HostDriverType::XBOXONE || driver_type == HostDriverType::XBOX360 + || driver_type == HostDriverType::XBOX360W || driver_type == HostDriverType::XBOXOG); + interface.gamepad->set_stick_y_positive_is_up(xbox_stick_y); interface.driver->initialize(*interface.gamepad, device_slot.address, instance, report_desc, desc_len); + record_usb_host_input_activity(); return true; } - inline void process_report(uint8_t address, uint8_t instance, const uint8_t* report, uint16_t len) + inline void process_report(DriverClass dclass, uint8_t address, uint8_t instance, const uint8_t* report, uint16_t len) { + record_usb_host_input_activity(); + + /* Same physical pad often exposes XInput + PS HID. Both were calling set_pad_in() on one Gamepad: + * inverted sticks, missed input, and flaky state. Prefer HID for DS4/DS5; drop XInput *processing* + * only (still re-arm IN below). */ + if (dclass == DriverClass::XINPUT) + { + for (const auto& device_slot : device_slots_) + { + if (device_slot.address != address) + { + continue; + } + for (const auto& iface : device_slot.interfaces) + { + if (iface.driver && iface.driver_class == DriverClass::HID && + (iface.host_driver_type == HostDriverType::PS4 || iface.host_driver_type == HostDriverType::PS5)) + { + (void)report; + (void)len; + tuh_xinput::receive_report(address, instance); + return; + } + } + break; + } + } + + const uint8_t si = host_storage_index(dclass, instance); + if (si >= MAX_INTERFACES) + { + return; + } for (auto& device_slot : device_slots_) { - if (device_slot.address == address && - device_slot.interfaces[instance].driver && - device_slot.interfaces[instance].gamepad) + if (device_slot.address != address) { - device_slot.interfaces[instance].driver->process_report(*device_slot.interfaces[instance].gamepad, address, instance, report, len); + continue; } + HostDriver* driver = device_slot.interfaces[si].driver.get(); + Gamepad* gamepad = device_slot.interfaces[si].gamepad; + if (!driver || !gamepad) + { + /* 360 wireless receiver: one HostManager driver serves all TinyUSB instances. */ + for (const auto& iface : device_slot.interfaces) + { + if (iface.driver && iface.gamepad && + iface.driver_class == DriverClass::XINPUT && + iface.host_driver_type == HostDriverType::XBOX360W) + { + driver = iface.driver.get(); + gamepad = iface.gamepad; + break; + } + } + } + if (driver && gamepad) + { + driver->process_report(*gamepad, address, instance, report, len); + } + break; } } - inline void connect_cb(uint8_t address, uint8_t instance) + inline void connect_cb(DriverClass dclass, uint8_t address, uint8_t instance) { + const uint8_t si = host_storage_index(dclass, instance); + if (si >= MAX_INTERFACES) + { + return; + } for (auto& device_slot : device_slots_) { - if (device_slot.address == address && - device_slot.interfaces[instance].driver && - device_slot.interfaces[instance].gamepad) + if (device_slot.address != address) { - device_slot.interfaces[instance].driver->connect_cb(*device_slot.interfaces[instance].gamepad, address, instance); + continue; } + HostDriver* driver = device_slot.interfaces[si].driver.get(); + Gamepad* gamepad = device_slot.interfaces[si].gamepad; + if (!driver || !gamepad) + { + for (const auto& iface : device_slot.interfaces) + { + if (iface.driver && iface.gamepad && + iface.driver_class == DriverClass::XINPUT && + iface.host_driver_type == HostDriverType::XBOX360W) + { + driver = iface.driver.get(); + gamepad = iface.gamepad; + break; + } + } + } + if (driver && gamepad) + { + driver->connect_cb(*gamepad, address, instance); + } + break; } } - inline void disconnect_cb(uint8_t address, uint8_t instance) + inline void disconnect_cb(DriverClass dclass, uint8_t address, uint8_t instance) { + const uint8_t si = host_storage_index(dclass, instance); + if (si >= MAX_INTERFACES) + { + return; + } for (auto& device_slot : device_slots_) { - if (device_slot.address == address && - device_slot.interfaces[instance].driver && - device_slot.interfaces[instance].gamepad) + if (device_slot.address != address) + { + continue; + } + HostDriver* driver = device_slot.interfaces[si].driver.get(); + Gamepad* gamepad = device_slot.interfaces[si].gamepad; + if (!driver || !gamepad) + { + for (const auto& iface : device_slot.interfaces) + { + if (iface.driver && iface.gamepad && + iface.driver_class == DriverClass::XINPUT && + iface.host_driver_type == HostDriverType::XBOX360W) + { + driver = iface.driver.get(); + gamepad = iface.gamepad; + break; + } + } + } + if (driver && gamepad) { - device_slot.interfaces[instance].driver->disconnect_cb(*device_slot.interfaces[instance].gamepad, address, instance); + driver->disconnect_cb(*gamepad, address, instance); } + break; } } //Call on a timer inline void send_feedback() { + const uint32_t now_ms = board_api::ms_since_boot(); for (auto& device_slot : device_slots_) { if (device_slot.address == INVALID_IDX) { continue; } + /* Composite "PS4 mode" pads often expose both XInput (GIP) and HID. Sending Xbox rumble + + * PS4 HID output together breaks many third-party firmwares (input dies after a few seconds). */ + bool ps_style_hid = false; + for (const auto& iface : device_slot.interfaces) + { + if (!iface.driver || iface.driver_class != DriverClass::HID) + { + continue; + } + if (iface.host_driver_type == HostDriverType::PS4 || iface.host_driver_type == HostDriverType::PS5) + { + ps_style_hid = true; + break; + } + } for (uint8_t i = 0; i < MAX_INTERFACES; ++i) { - if (device_slot.interfaces[i].driver && device_slot.interfaces[i].gamepad->new_pad_out()) + Interface& iface = device_slot.interfaces[i]; + if (!iface.driver) + { + continue; + } + if (iface.host_driver_type == HostDriverType::XBOX360W) { - device_slot.interfaces[i].driver->send_feedback(*device_slot.interfaces[i].gamepad, device_slot.address, i); - tuh_task(); + tuh_xinput::service_wireless_ports(device_slot.address); } + if (iface.driver_class == DriverClass::XINPUT && + iface.host_driver_type == HostDriverType::XBOXONE) + { + tuh_xinput::service_gip(device_slot.address, iface.usb_instance); + } + /* PS4-only (no GIP): 200 ms OUT refresh is OK. Composite pads choke if we OUT at 200 ms — + * use rare keepalive + init + console-driven updates only. */ + const bool ps4_hid_periodic = + iface.driver_class == DriverClass::HID && iface.host_driver_type == HostDriverType::PS4 && + !ps_style_hid; + /* PS3: invoke send_feedback on timer; driver rate-limits OUT to 1 Hz on PIO USB. */ + const bool ps3_hid_periodic = + iface.driver_class == DriverClass::HID && iface.host_driver_type == HostDriverType::PS3; + /* Switch Pro / Pro 2: init steps and idle rumble keepalive need periodic OUT on PIO USB. */ + const bool switch_pro_hid_periodic = + iface.driver_class == DriverClass::HID && + (iface.host_driver_type == HostDriverType::SWITCH_PRO || + iface.host_driver_type == HostDriverType::SWITCH_PRO_2); + bool ps4_composite_keepalive = false; + if (ps_style_hid && iface.driver_class == DriverClass::HID && + iface.host_driver_type == HostDriverType::PS4 && iface.gamepad_idx < MAX_GAMEPADS) + { + const uint8_t gi = iface.gamepad_idx; + if (now_ms >= ps4_composite_next_keepalive_ms_[gi]) + { + ps4_composite_next_keepalive_ms_[gi] = now_ms + 4000u; + ps4_composite_keepalive = true; + } + } + if (!(iface.gamepad->new_pad_out() || iface.gamepad->has_rumble() || ps4_hid_periodic || + ps3_hid_periodic || switch_pro_hid_periodic || ps4_composite_keepalive)) + { + continue; + } + if (ps_style_hid && iface.driver_class == DriverClass::XINPUT) + { + continue; + } + iface.driver->send_feedback(*iface.gamepad, device_slot.address, iface.usb_instance); + tuh_task(); +#if defined(CONFIG_EN_USB_HOST) + /* Nested tuh_task during OUT must not starve PIO USB SOF or wired IN dies in ~1–2 s. */ + pio_usb_host_frame(); +#endif } } } void deinit_driver(DriverClass driver_class, uint8_t address, uint8_t instance) { + const uint8_t si = host_storage_index(driver_class, instance); + if (si >= MAX_INTERFACES) + { + return; + } for (auto& device_slot : device_slots_) { - if (device_slot.address == address) + if (device_slot.address != address) { + continue; + } + Interface& iface = device_slot.interfaces[si]; + const uint8_t cleared_gamepad_idx = iface.gamepad_idx; + if (iface.driver && iface.gamepad) + { + iface.driver->disconnect_cb(*iface.gamepad, address, instance); + } + iface.driver.reset(); + iface.driver_class = DriverClass::NONE; + iface.host_driver_type = HostDriverType::UNKNOWN; + iface.gamepad_idx = INVALID_IDX; + iface.gamepad = nullptr; + iface.usb_instance = INVALID_IDX; + + bool any_driver = false; + for (auto& i : device_slot.interfaces) + { + if (i.driver) + { + any_driver = true; + break; + } + } + if (!any_driver) + { + if (cleared_gamepad_idx < MAX_GAMEPADS) + { + ps4_composite_next_keepalive_ms_[cleared_gamepad_idx] = 0; + } device_slot.reset(); } + return; } } @@ -234,16 +545,61 @@ class HostManager inline uint8_t get_gamepad_idx(DriverClass driver_class, uint8_t address, uint8_t instance) { + const uint8_t si = host_storage_index(driver_class, instance); + if (si >= MAX_INTERFACES) + { + return INVALID_IDX; + } for (auto& device_slot : device_slots_) { - if (device_slot.address == address && instance < MAX_INTERFACES) + if (device_slot.address != address) + { + continue; + } + if (device_slot.interfaces[si].gamepad_idx != INVALID_IDX) + { + return device_slot.interfaces[si].gamepad_idx; + } + for (const auto& iface : device_slot.interfaces) { - return device_slot.interfaces[instance].gamepad_idx; + if (iface.driver_class == DriverClass::XINPUT && + iface.host_driver_type == HostDriverType::XBOX360W && + iface.gamepad_idx != INVALID_IDX) + { + return iface.gamepad_idx; + } } + break; } return INVALID_IDX; } + /** Live Switch Pro / Switch 2 Pro host driver for async bulk bring-up callbacks. */ + inline SwitchProHost* get_switch_pro_host(uint8_t address, uint8_t instance) const + { + const uint8_t si = host_storage_index(DriverClass::HID, instance); + if (si >= MAX_INTERFACES) + { + return nullptr; + } + for (const auto& device_slot : device_slots_) + { + if (device_slot.address != address) + { + continue; + } + const Interface& iface = device_slot.interfaces[si]; + if (!iface.driver || + (iface.host_driver_type != HostDriverType::SWITCH_PRO && + iface.host_driver_type != HostDriverType::SWITCH_PRO_2)) + { + return nullptr; + } + return static_cast(iface.driver.get()); + } + return nullptr; + } + inline bool any_mounted() { for (auto& device_slot : device_slots_) @@ -256,14 +612,63 @@ class HostManager return false; } + /** Last time a USB IN report reached process_report (Pico W unplug watchdog when D+/D− GPIO lies under PIO). */ + inline void record_usb_host_input_activity() + { + last_usb_host_input_ms_ = board_api::ms_since_boot(); + } + + inline uint32_t usb_host_input_idle_ms() const + { + return board_api::ms_since_boot() - last_usb_host_input_ms_; + } + + /** True if this device address already has any host driver (e.g. HID). Used to prefer XInput over HID for the same device. */ + inline bool address_has_driver(uint8_t address) const + { + for (const auto& device_slot : device_slots_) + { + if (device_slot.address != address) + continue; + for (const auto& iface : device_slot.interfaces) + { + if (iface.driver != nullptr) + return true; + } + return false; + } + return false; + } + private: static constexpr uint8_t INVALID_IDX = 0xFF; + static inline uint8_t host_storage_index(DriverClass cls, uint8_t tinusb_instance) + { + if (cls == DriverClass::XINPUT) + { + if (tinusb_instance >= OGXM_TUH_XINPUT_INSTANCES) + { + return INVALID_IDX; + } + return static_cast(MAX_GAMEPADS + tinusb_instance); + } + if (tinusb_instance >= MAX_GAMEPADS) + { + return INVALID_IDX; + } + return tinusb_instance; + } + struct Interface { std::unique_ptr driver{nullptr}; Gamepad* gamepad{nullptr}; uint8_t gamepad_idx{INVALID_IDX}; + /** TinyUSB HID or XInput instance number for control/out transfers */ + uint8_t usb_instance{INVALID_IDX}; + DriverClass driver_class{DriverClass::NONE}; + HostDriverType host_driver_type{HostDriverType::UNKNOWN}; }; struct Device { @@ -276,14 +681,20 @@ class HostManager for (auto& interface : interfaces) { interface.driver.reset(); + interface.driver_class = DriverClass::NONE; + interface.host_driver_type = HostDriverType::UNKNOWN; interface.gamepad_idx = INVALID_IDX; interface.gamepad = nullptr; + interface.usb_instance = INVALID_IDX; } } }; Device device_slots_[MAX_GAMEPADS]; Gamepad* gamepads_[MAX_GAMEPADS]; + /** PS4+GIP composite: wall-time spacing for optional lightbar/rumble OUT refresh (see send_feedback). */ + uint32_t ps4_composite_next_keepalive_ms_[MAX_GAMEPADS]{}; + uint32_t last_usb_host_input_ms_{0}; HostManager() {} @@ -301,19 +712,35 @@ class HostManager inline uint8_t find_free_gamepad() { - uint8_t count = 0; - + bool used[MAX_GAMEPADS] = {false}; + UserSettings& us = UserSettings::get_instance(); + DeviceDriverType dr = us.get_current_driver(); + HostInputSource src = us.get_input_source(); + if ((dr == DeviceDriverType::PS1PS2 && src == HostInputSource::PSX_GPIO) || + (dr == DeviceDriverType::GAMECUBE && src == HostInputSource::GAMECUBE_GPIO) || + (dr == DeviceDriverType::DREAMCAST && src == HostInputSource::DREAMCAST_GPIO) || + (src == HostInputSource::N64_GPIO)) + { + used[0] = true; + } for (auto& device_slot : device_slots_) { for (auto& interface : device_slot.interfaces) { if (interface.gamepad_idx != INVALID_IDX) { - ++count; + used[interface.gamepad_idx] = true; } } } - return (count < MAX_GAMEPADS) ? count : INVALID_IDX; + for (uint8_t i = 0; i < MAX_GAMEPADS; ++i) + { + if (!used[i]) + { + return i; + } + } + return INVALID_IDX; } inline uint8_t get_device_slot(uint8_t address) diff --git a/Firmware/RP2040/src/USBHost/tuh_callbacks.cpp b/Firmware/RP2040/src/USBHost/tuh_callbacks.cpp index 379e795e..726a8881 100644 --- a/Firmware/RP2040/src/USBHost/tuh_callbacks.cpp +++ b/Firmware/RP2040/src/USBHost/tuh_callbacks.cpp @@ -21,7 +21,8 @@ void tuh_hid_mount_cb(uint8_t dev_addr, uint8_t instance, uint8_t const* desc_re HostManager& host_manager = HostManager::get_instance(); - if (host_manager.setup_driver(HostManager::get_type({ vid, pid }), dev_addr, instance, desc_report, desc_len)) { + if (host_manager.setup_driver(HostManager::get_type({ vid, pid }), HostManager::DriverClass::HID, + dev_addr, instance, desc_report, desc_len)) { OGXMini::host_mounted(true); } } @@ -36,7 +37,7 @@ void tuh_hid_umount_cb(uint8_t dev_addr, uint8_t instance) { } void tuh_hid_report_received_cb(uint8_t dev_addr, uint8_t instance, uint8_t const* report, uint16_t len) { - HostManager::get_instance().process_report(dev_addr, instance, report, len); + HostManager::get_instance().process_report(HostManager::DriverClass::HID, dev_addr, instance, report, len); } //XINPUT @@ -45,7 +46,7 @@ void tuh_xinput::mount_cb(uint8_t dev_addr, uint8_t instance, const tuh_xinput:: HostManager& host_manager = HostManager::get_instance(); HostDriverType host_type = HostManager::get_type(interface->dev_type); - if (host_manager.setup_driver(host_type, dev_addr, instance)) { + if (host_manager.setup_driver(host_type, HostManager::DriverClass::XINPUT, dev_addr, instance)) { OGXMini::host_mounted(true, host_type); } } @@ -60,19 +61,25 @@ void tuh_xinput::unmount_cb(uint8_t dev_addr, uint8_t instance, const tuh_xinput } void tuh_xinput::report_received_cb(uint8_t dev_addr, uint8_t instance, const uint8_t* report, uint16_t len) { - HostManager::get_instance().process_report(dev_addr, instance, report, len); + HostManager::get_instance().process_report(HostManager::DriverClass::XINPUT, dev_addr, instance, report, len); } void tuh_xinput::xbox360w_connect_cb(uint8_t dev_addr, uint8_t instance) { uint8_t idx = HostManager::get_instance().get_gamepad_idx( HostManager::DriverClass::XINPUT, dev_addr, instance); OGXMini::wireless_connected(true, idx); - HostManager::get_instance().connect_cb(dev_addr, instance); + HostManager::get_instance().connect_cb(HostManager::DriverClass::XINPUT, dev_addr, instance); } void tuh_xinput::xbox360w_disconnect_cb(uint8_t dev_addr, uint8_t instance) { uint8_t idx = HostManager::get_instance().get_gamepad_idx( HostManager::DriverClass::XINPUT, dev_addr, instance); OGXMini::wireless_connected(false, idx); - HostManager::get_instance().disconnect_cb(dev_addr, instance); + HostManager::get_instance().disconnect_cb(HostManager::DriverClass::XINPUT, dev_addr, instance); +} + +void tuh_xinput::host_activity_cb(uint8_t dev_addr, uint8_t instance) { + (void)dev_addr; + (void)instance; + HostManager::get_instance().record_usb_host_input_activity(); } \ No newline at end of file diff --git a/Firmware/RP2040/src/UserSettings/JoystickSettings.cpp b/Firmware/RP2040/src/UserSettings/JoystickSettings.cpp index e50321d6..3e6f90fd 100644 --- a/Firmware/RP2040/src/UserSettings/JoystickSettings.cpp +++ b/Firmware/RP2040/src/UserSettings/JoystickSettings.cpp @@ -20,6 +20,27 @@ bool JoystickSettings::is_same(const JoystickSettingsRaw& raw) const invert_x == raw.invert_x; } +bool JoystickSettings::raw_has_customization(const JoystickSettingsRaw& raw) +{ + static const JoystickSettingsRaw defaults{}; + return raw.dz_inner != defaults.dz_inner || + raw.dz_outer != defaults.dz_outer || + raw.anti_dz_circle != defaults.anti_dz_circle || + raw.anti_dz_circle_y_scale != defaults.anti_dz_circle_y_scale || + raw.anti_dz_square != defaults.anti_dz_square || + raw.anti_dz_square_y_scale != defaults.anti_dz_square_y_scale || + raw.anti_dz_angular != defaults.anti_dz_angular || + raw.anti_dz_outer != defaults.anti_dz_outer || + raw.axis_restrict != defaults.axis_restrict || + raw.angle_restrict != defaults.angle_restrict || + raw.diag_scale_min != defaults.diag_scale_min || + raw.diag_scale_max != defaults.diag_scale_max || + raw.curve != defaults.curve || + raw.uncap_radius != defaults.uncap_radius || + raw.invert_y != defaults.invert_y || + raw.invert_x != defaults.invert_x; +} + void JoystickSettings::set_from_raw(const JoystickSettingsRaw& raw) { dz_inner = Fix16(raw.dz_inner); @@ -38,4 +59,16 @@ void JoystickSettings::set_from_raw(const JoystickSettingsRaw& raw) uncap_radius = raw.uncap_radius; invert_y = raw.invert_y; invert_x = raw.invert_x; +} + +void JoystickSettings::set_xbox360_stock_feel(bool right_stick) +{ + /* Reset to firmware defaults, then apply 360-like shaping. + * Keep remapped deadzone mild (not Microsoft's ~24% software filter) so console games + * that also apply deadzones are not double-dulled. Curve < 1 softens the center and + * steepens the outer third — closer to a stock 360 pad than a near-linear Series stick. */ + *this = JoystickSettings{}; + dz_inner = Fix16(right_stick ? 0.09f : 0.08f); + curve = Fix16(0.35f); + uncap_radius = false; } \ No newline at end of file diff --git a/Firmware/RP2040/src/UserSettings/JoystickSettings.h b/Firmware/RP2040/src/UserSettings/JoystickSettings.h index 2e7e06c1..11d5cd54 100644 --- a/Firmware/RP2040/src/UserSettings/JoystickSettings.h +++ b/Firmware/RP2040/src/UserSettings/JoystickSettings.h @@ -33,6 +33,13 @@ struct JoystickSettings bool is_same(const JoystickSettingsRaw& raw) const; void set_from_raw(const JoystickSettingsRaw& raw); + static bool raw_has_customization(const JoystickSettingsRaw& raw); + /** + * Xbox 360–like response for XInput mode when the WebApp has not customized sticks. + * Mild remapped inner deadzone + softer early / steeper outer curve + circular clamp. + * (@param right_stick slightly larger deadzone, matching Microsoft L/R asymmetry). + */ + void set_xbox360_stock_feel(bool right_stick = false); }; #pragma pack(push, 1) diff --git a/Firmware/RP2040/src/UserSettings/UserSettings.cpp b/Firmware/RP2040/src/UserSettings/UserSettings.cpp index b0cd1805..f8a78f4e 100644 --- a/Firmware/RP2040/src/UserSettings/UserSettings.cpp +++ b/Firmware/RP2040/src/UserSettings/UserSettings.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -13,16 +14,34 @@ static constexpr uint32_t BUTTON_COMBO(const uint16_t& buttons, const uint8_t& d return (static_cast(buttons) << 16) | static_cast(dpad); } +/** Required buttons/dpad pressed (extra face buttons or triggers do not block the combo). */ +static bool combo_matches(uint32_t current, uint32_t expected) +{ + const uint16_t req_buttons = static_cast(expected >> 16); + const uint16_t cur_buttons = static_cast(current >> 16); + const uint8_t req_dpad = static_cast(expected & 0xFF); + const uint8_t cur_dpad = static_cast(current & 0xFF); + return ((cur_buttons & req_buttons) == req_buttons) && (cur_dpad == req_dpad); +} + namespace ButtonCombo { static constexpr uint32_t PS3 = BUTTON_COMBO(Gamepad::BUTTON_START, Gamepad::DPAD_LEFT); + static constexpr uint32_t PS4 = BUTTON_COMBO(Gamepad::BUTTON_START | Gamepad::BUTTON_LB, Gamepad::DPAD_LEFT); + static constexpr uint32_t STEAM = BUTTON_COMBO(Gamepad::BUTTON_START | Gamepad::BUTTON_LB, Gamepad::DPAD_UP); static constexpr uint32_t DINPUT = BUTTON_COMBO(Gamepad::BUTTON_START | Gamepad::BUTTON_RB, Gamepad::DPAD_LEFT); static constexpr uint32_t XINPUT = BUTTON_COMBO(Gamepad::BUTTON_START, Gamepad::DPAD_UP); static constexpr uint32_t SWITCH = BUTTON_COMBO(Gamepad::BUTTON_START, Gamepad::DPAD_DOWN); + static constexpr uint32_t WIIU = BUTTON_COMBO(Gamepad::BUTTON_START | Gamepad::BUTTON_LB, Gamepad::DPAD_DOWN); static constexpr uint32_t XBOXOG = BUTTON_COMBO(Gamepad::BUTTON_START, Gamepad::DPAD_RIGHT); static constexpr uint32_t XBOXOG_SB = BUTTON_COMBO(Gamepad::BUTTON_START | Gamepad::BUTTON_RB, Gamepad::DPAD_RIGHT); static constexpr uint32_t XBOXOG_XR = BUTTON_COMBO(Gamepad::BUTTON_START | Gamepad::BUTTON_LB, Gamepad::DPAD_RIGHT); static constexpr uint32_t PSCLASSIC = BUTTON_COMBO(Gamepad::BUTTON_START | Gamepad::BUTTON_A); + static constexpr uint32_t PS1PS2 = BUTTON_COMBO(Gamepad::BUTTON_START | Gamepad::BUTTON_B); + static constexpr uint32_t GAMECUBE = BUTTON_COMBO(Gamepad::BUTTON_START | Gamepad::BUTTON_X); + static constexpr uint32_t DREAMCAST = BUTTON_COMBO(Gamepad::BUTTON_START | Gamepad::BUTTON_Y); + static constexpr uint32_t N64 = BUTTON_COMBO(Gamepad::BUTTON_START | Gamepad::BUTTON_RB); static constexpr uint32_t WEBAPP = BUTTON_COMBO(Gamepad::BUTTON_START | Gamepad::BUTTON_LB | Gamepad::BUTTON_RB); + // WII is build-option only (OGXM_FIXED_DRIVER=WII), not in combo map }; static constexpr DeviceDriverType VALID_DRIVER_TYPES[] = { @@ -31,7 +50,11 @@ static constexpr DeviceDriverType VALID_DRIVER_TYPES[] = { DeviceDriverType::XBOXOG_SB, DeviceDriverType::XINPUT, DeviceDriverType::PS3, + DeviceDriverType::PS4, + DeviceDriverType::STEAM, DeviceDriverType::PSCLASSIC, + DeviceDriverType::WIIU, + DeviceDriverType::WII, DeviceDriverType::WEBAPP, #if defined(XREMOTE_ROM_AVAILABLE) DeviceDriverType::XBOXOG_XR, @@ -40,6 +63,8 @@ static constexpr DeviceDriverType VALID_DRIVER_TYPES[] = { #elif MAX_GAMEPADS > 1 DeviceDriverType::DINPUT, DeviceDriverType::SWITCH, + DeviceDriverType::WIIU, + DeviceDriverType::WII, DeviceDriverType::WEBAPP, #else // MAX_GAMEPADS == 1 @@ -47,9 +72,17 @@ static constexpr DeviceDriverType VALID_DRIVER_TYPES[] = { DeviceDriverType::XBOXOG_SB, DeviceDriverType::DINPUT, DeviceDriverType::SWITCH, + DeviceDriverType::WIIU, + DeviceDriverType::WII, DeviceDriverType::WEBAPP, DeviceDriverType::PS3, - DeviceDriverType::PSCLASSIC, + DeviceDriverType::PS4, + DeviceDriverType::STEAM, + DeviceDriverType::PSCLASSIC, + DeviceDriverType::PS1PS2, + DeviceDriverType::GAMECUBE, + DeviceDriverType::DREAMCAST, + DeviceDriverType::N64, DeviceDriverType::XINPUT, #if defined(XREMOTE_ROM_AVAILABLE) DeviceDriverType::XBOXOG_XR, @@ -63,18 +96,46 @@ struct ComboMap { DeviceDriverType driver; }; -static constexpr std::array BUTTON_COMBO_MAP = {{ +// GAMECUBE and N64 are build-only (fixed driver), not in combo map; use -DOGXM_FIXED_DRIVER=GAMECUBE or N64. +static constexpr std::array BUTTON_COMBO_MAP = {{ { ButtonCombo::XBOXOG, DeviceDriverType::XBOXOG }, { ButtonCombo::XBOXOG_SB, DeviceDriverType::XBOXOG_SB }, { ButtonCombo::XBOXOG_XR, DeviceDriverType::XBOXOG_XR }, { ButtonCombo::WEBAPP, DeviceDriverType::WEBAPP }, { ButtonCombo::DINPUT, DeviceDriverType::DINPUT }, { ButtonCombo::SWITCH, DeviceDriverType::SWITCH }, + { ButtonCombo::WIIU, DeviceDriverType::WIIU }, { ButtonCombo::XINPUT, DeviceDriverType::XINPUT }, { ButtonCombo::PS3, DeviceDriverType::PS3 }, - { ButtonCombo::PSCLASSIC, DeviceDriverType::PSCLASSIC } + { ButtonCombo::PS4, DeviceDriverType::PS4 }, + { ButtonCombo::STEAM, DeviceDriverType::STEAM }, + { ButtonCombo::PSCLASSIC, DeviceDriverType::PSCLASSIC }, + { ButtonCombo::PS1PS2, DeviceDriverType::PS1PS2 }, + { ButtonCombo::DREAMCAST, DeviceDriverType::DREAMCAST }, }}; +/** Prefer the combo that requires the most buttons (e.g. STEAM over XInput when LB is held). */ +static uint32_t find_matching_combo(uint32_t current) +{ + uint32_t best = 0; + uint8_t best_button_count = 0; + for (const auto& entry : BUTTON_COMBO_MAP) { + if (!combo_matches(current, entry.combo)) { + continue; + } + const uint16_t req = static_cast(entry.combo >> 16); + uint8_t count = 0; + for (uint16_t bit = req; bit != 0; bit &= static_cast(bit - 1)) { + ++count; + } + if (count >= best_button_count) { + best_button_count = count; + best = entry.combo; + } + } + return best; +} + const std::string UserSettings::INIT_FLAG_KEY() { return std::string("init_flag"); @@ -95,6 +156,11 @@ const std::string UserSettings::DRIVER_TYPE_KEY() return std::string("driver_type"); } +const std::string UserSettings::INPUT_SOURCE_KEY() +{ + return std::string("input_source"); +} + const std::string UserSettings::DATETIME_KEY() { return std::string("datetime"); @@ -102,22 +168,32 @@ const std::string UserSettings::DATETIME_KEY() DeviceDriverType UserSettings::DEFAULT_DRIVER() { +#if defined(CONFIG_OGXM_FIXED_DRIVER) + return static_cast(OGXM_FIXED_DRIVER_TYPE); +#else return VALID_DRIVER_TYPES[0]; +#endif } //Checks if button combo has been held for 3 seconds, returns true if mode has been changed bool UserSettings::check_for_driver_change(Gamepad& gamepad) { +#if defined(CONFIG_OGXM_FIXED_DRIVER) && !defined(CONFIG_OGXM_FIXED_DRIVER_ALLOW_COMBOS) + (void)gamepad; + return false; // Fixed output build: combos disabled +#else Gamepad::PadIn gp_in = gamepad.get_pad_in(); - static uint32_t last_button_combo = BUTTON_COMBO(gp_in.buttons, gp_in.dpad); + static uint32_t last_button_combo = 0; static uint8_t call_count = 0; - uint32_t current_button_combo = BUTTON_COMBO(gp_in.buttons, gp_in.dpad); + const uint32_t current_button_combo = BUTTON_COMBO(gp_in.buttons, gp_in.dpad); + const uint32_t active_combo = find_matching_combo(current_button_combo); - if (!(current_button_combo & (static_cast(Gamepad::BUTTON_START) << 16)) || - last_button_combo != current_button_combo) + if (!(current_button_combo & (static_cast(Gamepad::BUTTON_START) << 16)) || + active_combo == 0 || + last_button_combo != active_combo) { - last_button_combo = current_button_combo; + last_button_combo = active_combo; call_count = 0; return false; } @@ -135,7 +211,7 @@ bool UserSettings::check_for_driver_change(Gamepad& gamepad) for (const auto& combo_map : BUTTON_COMBO_MAP) { - if (combo_map.combo == current_button_combo && is_valid_driver(combo_map.driver)) + if (combo_map.combo == active_combo && is_valid_driver(combo_map.driver)) { new_driver = combo_map.driver; break; @@ -144,12 +220,19 @@ bool UserSettings::check_for_driver_change(Gamepad& gamepad) if (new_driver == DeviceDriverType::NONE || new_driver == current_driver_) { +#if defined(CONFIG_OGXM_DEBUG) + if (new_driver == DeviceDriverType::NONE) + OGXM_LOG("WII driver check: combo 0x" + std::to_string(current_button_combo) + " no match\n"); + else + OGXM_LOG("WII driver check: same driver " + OGXM_TO_STRING(current_driver_) + ", no change\n"); +#endif return false; } current_driver_ = new_driver; - + OGXM_LOG("WII driver check: returning true, new_driver=" + OGXM_TO_STRING(new_driver) + "\n"); return true; +#endif } //Disconnects usb and resets pico, call from core0 @@ -212,7 +295,7 @@ bool UserSettings::store_profile_and_driver_type(DeviceDriverType new_driver_typ } //Disconnects usb and resets pico if it's a new & valid mode, call from core0 -void UserSettings::store_driver_type(DeviceDriverType new_driver) +void UserSettings::store_driver_type(DeviceDriverType new_driver) { if (!is_valid_driver(new_driver)) { @@ -229,6 +312,40 @@ void UserSettings::store_driver_type(DeviceDriverType new_driver) board_api::reboot(); } +// Store driver and reboot only; no disconnect_all(). Use from Core0. +void UserSettings::store_driver_type_and_reboot(DeviceDriverType new_driver) +{ + if (!is_valid_driver(new_driver)) + { + OGXM_LOG("Invalid driver type during store_and_reboot: " + OGXM_TO_STRING(new_driver) + "\n"); + return; + } + + OGXM_LOG("Storing new driver type and rebooting: " + OGXM_TO_STRING(new_driver) + "\n"); + + nvs_tool_.write(DRIVER_TYPE_KEY(), &new_driver, sizeof(uint8_t)); + + board_api::reboot(); +} + +// Store driver to flash only. Used from Core0 after watchdog reboot (Wii exit) or from flash_safe_execute callback. +// Updates in-memory current_driver_ so the rest of run() sees the new mode. +bool UserSettings::store_driver_type_only(DeviceDriverType new_driver) +{ + if (!is_valid_driver(new_driver)) + { + OGXM_LOG("Invalid driver type in store_driver_type_only: " + OGXM_TO_STRING(new_driver) + "\n"); + return false; + } + + OGXM_LOG("Storing new driver type (flash only): " + OGXM_TO_STRING(new_driver) + "\n"); + + nvs_tool_.write(DRIVER_TYPE_KEY(), &new_driver, sizeof(uint8_t)); + current_driver_ = new_driver; + + return true; +} + uint8_t UserSettings::get_active_profile_id(const uint8_t index) { if (index > MAX_GAMEPADS - 1) @@ -280,15 +397,18 @@ bool UserSettings::is_valid_driver(DeviceDriverType driver) DeviceDriverType UserSettings::get_current_driver() { +#if defined(CONFIG_OGXM_FIXED_DRIVER) && !defined(CONFIG_OGXM_FIXED_DRIVER_ALLOW_COMBOS) + return static_cast(OGXM_FIXED_DRIVER_TYPE); +#else if (current_driver_ != DeviceDriverType::NONE) { return current_driver_; } uint8_t stored_value = 0; - nvs_tool_.read(DRIVER_TYPE_KEY(), &stored_value, sizeof(uint8_t)); + bool read_ok = nvs_tool_.read(DRIVER_TYPE_KEY(), &stored_value, sizeof(uint8_t)); - if (is_valid_driver(static_cast(stored_value))) + if (read_ok && is_valid_driver(static_cast(stored_value))) { OGXM_LOG("Driver type read from flash: " + OGXM_TO_STRING(static_cast(stored_value)) + "\n"); @@ -296,10 +416,103 @@ DeviceDriverType UserSettings::get_current_driver() return current_driver_; } +#if defined(CONFIG_OGXM_DEBUG) + if (!read_ok) + OGXM_LOG("Driver type read failed or key missing, using default\n"); + else + OGXM_LOG("Driver type from flash raw=0x" + std::to_string(stored_value) + " invalid, using default " + OGXM_TO_STRING(DEFAULT_DRIVER()) + "\n"); +#else OGXM_LOG("Invalid driver type read from flash, setting default driver\n"); +#endif current_driver_ = DEFAULT_DRIVER(); return current_driver_; +#endif // !(CONFIG_OGXM_FIXED_DRIVER && !CONFIG_OGXM_FIXED_DRIVER_ALLOW_COMBOS) +} + +HostInputSource UserSettings::get_input_source() +{ + if (!input_source_loaded_) + { + uint8_t stored = 0; + if (nvs_tool_.read(INPUT_SOURCE_KEY(), &stored, sizeof(uint8_t)) && stored <= 3) + { + current_input_source_ = static_cast(stored); + } + input_source_loaded_ = true; + } + return current_input_source_; +} + +void UserSettings::store_input_source(HostInputSource source) +{ + uint8_t v = static_cast(source); + nvs_tool_.write(INPUT_SOURCE_KEY(), &v, sizeof(uint8_t)); + current_input_source_ = source; + input_source_loaded_ = true; +} + +static constexpr uint32_t INPUT_SOURCE_COMBO = BUTTON_COMBO(Gamepad::BUTTON_START | Gamepad::BUTTON_BACK, 0); +static constexpr unsigned INPUT_SOURCE_CHECK_COUNT = 2500 / 600; /* 2.5s hold to cycle input source */ + +bool UserSettings::check_for_input_source_change(Gamepad& gamepad) +{ + Gamepad::PadIn gp = gamepad.get_pad_in(); + uint32_t combo = BUTTON_COMBO(gp.buttons, gp.dpad); + static uint32_t last_combo = 0; + static uint8_t hold_count = 0; + + if (combo != INPUT_SOURCE_COMBO) + { + last_combo = combo; + hold_count = 0; + return false; + } + if (last_combo != INPUT_SOURCE_COMBO) + { + hold_count = 0; + } + last_combo = INPUT_SOURCE_COMBO; + ++hold_count; + if (hold_count < INPUT_SOURCE_CHECK_COUNT) + { + return false; + } + hold_count = 0; + DeviceDriverType dr = get_current_driver(); + HostInputSource cur = get_input_source(); + HostInputSource next = HostInputSource::USB; + if (dr == DeviceDriverType::PS1PS2) + { + next = (cur == HostInputSource::USB) ? HostInputSource::PSX_GPIO : HostInputSource::USB; + } + else if (dr == DeviceDriverType::GAMECUBE) + { + next = (cur == HostInputSource::USB) ? HostInputSource::GAMECUBE_GPIO : HostInputSource::USB; + } + else if (dr == DeviceDriverType::DREAMCAST) + { + next = (cur == HostInputSource::USB) ? HostInputSource::DREAMCAST_GPIO : HostInputSource::USB; + } + else + { + /* Any other output (Switch, DInput, Xbox, etc.): cycle USB -> PSX_GPIO -> GAMECUBE_GPIO -> DREAMCAST_GPIO -> N64_GPIO -> USB */ + if (cur == HostInputSource::USB) + next = HostInputSource::PSX_GPIO; + else if (cur == HostInputSource::PSX_GPIO) + next = HostInputSource::GAMECUBE_GPIO; + else if (cur == HostInputSource::GAMECUBE_GPIO) + next = HostInputSource::DREAMCAST_GPIO; + else if (cur == HostInputSource::DREAMCAST_GPIO) + next = HostInputSource::N64_GPIO; + else if (cur == HostInputSource::N64_GPIO) + next = HostInputSource::USB; + else + next = HostInputSource::USB; + } + store_input_source(next); + OGXM_LOG("Input source changed to " + std::to_string(static_cast(next)) + "\n"); + return true; } void UserSettings::write_datetime() @@ -341,6 +554,9 @@ void UserSettings::initialize_flash() uint8_t device_mode_buffer = static_cast(DEFAULT_DRIVER()); nvs_tool_.write(DRIVER_TYPE_KEY(), &device_mode_buffer, sizeof(uint8_t)); + uint8_t input_source_buffer = static_cast(HostInputSource::USB); + nvs_tool_.write(INPUT_SOURCE_KEY(), &input_source_buffer, sizeof(uint8_t)); + OGXM_LOG("Writing default profile ids\n"); for (uint8_t i = 0; i < MAX_GAMEPADS; i++) diff --git a/Firmware/RP2040/src/UserSettings/UserSettings.h b/Firmware/RP2040/src/UserSettings/UserSettings.h index ae406942..fce3cbdd 100644 --- a/Firmware/RP2040/src/UserSettings/UserSettings.h +++ b/Firmware/RP2040/src/UserSettings/UserSettings.h @@ -10,6 +10,16 @@ #include "UserSettings/NVSTool.h" #include "Gamepad/Gamepad.h" +/** Input source for gamepad 0: USB/BT (default) or GPIO (PS1/PS2, GameCube, or Dreamcast controller). */ +enum class HostInputSource : uint8_t +{ + USB = 0, + PSX_GPIO = 1, + GAMECUBE_GPIO = 2, + DREAMCAST_GPIO = 3, + N64_GPIO = 4 +}; + /* Only write/store flash from Core0 */ class UserSettings { @@ -31,12 +41,21 @@ class UserSettings DeviceDriverType get_current_driver(); bool check_for_driver_change(Gamepad& gamepad); + + HostInputSource get_input_source(); + void store_input_source(HostInputSource source); + /** When driver is PS1PS2 or GAMECUBE, returns true if Start+Select held long enough and cycles input source (stores and returns true). */ + bool check_for_input_source_change(Gamepad& gamepad); UserProfile get_profile_by_index(const uint8_t index); UserProfile get_profile_by_id(const uint8_t profile_id); uint8_t get_active_profile_id(const uint8_t index); void store_driver_type(DeviceDriverType new_driver_type); + /** Store driver and reboot without disconnect_all(); use from Core0. */ + void store_driver_type_and_reboot(DeviceDriverType new_driver_type); + /** Store driver to flash only (no disconnect, no reboot). Use from flash_safe_execute callback (Wii exit); caller must reboot after. */ + bool store_driver_type_only(DeviceDriverType new_driver_type); bool store_profile(uint8_t index, const UserProfile& profile); bool store_profile_and_driver_type(DeviceDriverType new_driver_type, uint8_t index, const UserProfile& profile); @@ -58,7 +77,10 @@ class UserSettings const std::string PROFILE_KEY(const uint8_t profile_id); const std::string ACTIVE_PROFILE_KEY(const uint8_t index); const std::string DRIVER_TYPE_KEY(); + const std::string INPUT_SOURCE_KEY(); const std::string DATETIME_KEY(); + HostInputSource current_input_source_{HostInputSource::USB}; + bool input_source_loaded_{false}; }; #endif // _USER_SETTINGS_H_ \ No newline at end of file diff --git a/Firmware/RP2040/src/Wii/WiiReportConverter.cpp b/Firmware/RP2040/src/Wii/WiiReportConverter.cpp new file mode 100644 index 00000000..5266b50e --- /dev/null +++ b/Firmware/RP2040/src/Wii/WiiReportConverter.cpp @@ -0,0 +1,225 @@ +/** + * Converts OGX-Mini Gamepad state to WiimoteReport for Wii mode BT emulation. + * Mappings match PicoGamepadConverter convert_data.c WII case (XInput layout). + */ +#include "Wii/WiiReportConverter.h" +#include "Gamepad/Gamepad.h" +#include "Wii/wiimote-lib/wiimote.h" +#include +#include + +#if defined(OGXM_BOARD_USES_PICO_W_FIRMWARE) && defined(CONFIG_EN_USB_HOST) +#include "Board/board_api.h" +#include "pico/time.h" + +namespace Wii { + +/** Flash LED n times to indicate mode: 1 = NO_EXTENSION, 2 = Nunchuk, 3 = Classic */ +static void flash_led_times(unsigned n) { + for (unsigned i = 0; i < n; ++i) { + board_api::set_led(false); + sleep_ms(80); + board_api::set_led(true); + if (i + 1 < n) + sleep_ms(100); + } +} + +// Require Home+Down held this many consecutive frames before switching mode (avoids stuck/noisy Home) +static constexpr unsigned MODE_SWITCH_HOLD_FRAMES = 10; +static unsigned s_home_down_hold_count = 0; + +// Same as PicoGamepadConverter: int16 stick -> int8 for IR (-128..127) +static inline int8_t stick_to_wiimote_ir(int16_t val) { + return (int8_t)(val >> 8); +} +// int16 stick -> uint8 for nunchuk stick (0-255, center 128). val>>8 is -128..127; +128 -> 0..255 +static inline uint8_t stick_to_nunchuk_xy(int16_t val) { + int v = (val >> 8) + 128; + return (uint8_t)std::clamp(v, 0, 255); +} +// int16 stick -> uint8 for classic stick (32..63 center 32). PicoGamepadConverter: (sThumbLX/32768.0)*31+32 +static inline uint8_t stick_to_classic_xy(int16_t val) { + int v = (int)((float)val / 32768.f * 31.f + 32.f); + return (uint8_t)std::clamp(v, 0, 63); +} + +// Classic right stick: center 15, range 0..30. Deadzone to avoid pointer drift at rest. +static constexpr int16_t CLASSIC_RS_DEADZONE = 2048; +static inline int16_t classic_rs_apply_deadzone(int16_t val) { + return (val > -CLASSIC_RS_DEADZONE && val < CLASSIC_RS_DEADZONE) ? 0 : val; +} + +void gamepad_to_wiimote_report(Gamepad& gamepad, void* out_wiimote_report) +{ + WiimoteReport* out = static_cast(out_wiimote_report); + if (!out) return; + + Gamepad::PadIn pad = gamepad.get_pad_in(); + + // Preserve console_info so the Wiimote stack keeps the saved Wii address across frames + // (we only leave it unchanged; first boot it's zero so we stay discoverable) + + // Default mode to NO_EXTENSION if invalid (first frame or corrupted) + if (out->mode != NO_EXTENSION && out->mode != WIIMOTE_AND_NUNCHUCK && out->mode != CLASSIC_CONTROLLER) + out->mode = NO_EXTENSION; + + // --- Wiimote buttons (match PicoGamepadConverter NO_EXTENSION / WIIMOTE_AND_NUNCHUCK) --- + // XInput: A=0x1000 B=0x2000 X=0x4000 Y=0x8000 BACK=0x0020 START=0x0010 GUIDE=0x0400 DPAD_*=0x0001/2/4/8 LB=0x0100 RB=0x0200 + // Our Gamepad: BUTTON_A=0x0001 B=0x0002 X=0x0004 Y=0x0008 BACK=0x0040 START=0x0080 SYS=0x0400 LB=0x0100 RB=0x0200 + out->wiimote.a = (pad.buttons & Gamepad::BUTTON_A) != 0; + out->wiimote.b = (pad.buttons & Gamepad::BUTTON_B) != 0; + out->wiimote.minus = (pad.buttons & Gamepad::BUTTON_BACK) != 0; + out->wiimote.plus = (pad.buttons & Gamepad::BUTTON_START) != 0; + out->wiimote.home = (pad.buttons & Gamepad::BUTTON_SYS) != 0; + out->wiimote.one = (pad.buttons & Gamepad::BUTTON_X) != 0; + out->wiimote.two = (pad.buttons & Gamepad::BUTTON_Y) != 0; + out->wiimote.up = (pad.dpad & Gamepad::DPAD_UP) != 0; + out->wiimote.down = (pad.dpad & Gamepad::DPAD_DOWN) != 0; + out->wiimote.left = (pad.dpad & Gamepad::DPAD_LEFT) != 0; + out->wiimote.right = (pad.dpad & Gamepad::DPAD_RIGHT) != 0; + out->wiimote.sync = false; + out->wiimote.power = false; + + // Accelerometer: neutral ~0x200 (10-bit). Wii expects this for pointer/motion + out->wiimote.accel_x = 0x200; + out->wiimote.accel_y = 0x200; + out->wiimote.accel_z = 0x200; + + // IR pointer: left stick for NO_EXTENSION (right→right, up→up; deadzone to prevent drift) + out->wiimote.ir_x = stick_to_wiimote_ir(classic_rs_apply_deadzone(pad.joystick_lx)); + out->wiimote.ir_y = stick_to_wiimote_ir(classic_rs_apply_deadzone(-pad.joystick_ly)); + + // Mode and extension data (match PicoGamepadConverter) + switch (out->mode) { + case NO_EXTENSION: + // Sideway: home+LB toggles (we don't change sideway here; leave as-is or 0) + if ((out->wiimote.home) && (pad.buttons & Gamepad::BUTTON_LB)) { + out->wiimote.home = false; + out->sideway = (0xF << 1) | (out->sideway & 1); + } else if ((out->sideway >> 1) == 0xF) { + out->sideway = (out->sideway & 1) ? 0 : 1; + } + // Mode switch: home+down held for MODE_SWITCH_HOLD_FRAMES -> next mode (NO_EXTENSION -> WIIMOTE_AND_NUNCHUCK) + if (out->wiimote.home && out->wiimote.down) { + if (++s_home_down_hold_count >= MODE_SWITCH_HOLD_FRAMES) { + s_home_down_hold_count = 0; + out->wiimote.home = false; + out->wiimote.down = false; + out->switch_mode = 1; + } + } else { + s_home_down_hold_count = 0; + } + if (out->switch_mode) { + out->mode = WIIMOTE_AND_NUNCHUCK; + out->switch_mode = 0; + out->reset_ir = 1; + flash_led_times(2); + } + break; + + case WIIMOTE_AND_NUNCHUCK: + // Wiimote IR from right stick (right→right, up→up; deadzone to prevent drift) + out->wiimote.ir_x = stick_to_wiimote_ir(classic_rs_apply_deadzone(pad.joystick_rx)); + out->wiimote.ir_y = stick_to_wiimote_ir(classic_rs_apply_deadzone(-pad.joystick_ry)); + // Nunchuk stick from left stick (right→right, up→forward); deadzone so center is true 128 (stops drift in aiming) + out->nunchuk.x = stick_to_nunchuk_xy(classic_rs_apply_deadzone(pad.joystick_lx)); + out->nunchuk.y = stick_to_nunchuk_xy(classic_rs_apply_deadzone(-pad.joystick_ly)); + out->nunchuk.c = (pad.trigger_l > 127); + out->nunchuk.z = (pad.buttons & Gamepad::BUTTON_RB) != 0; + out->nunchuk.accel_x = 0x200; + out->nunchuk.accel_y = 0x200; + out->nunchuk.accel_z = 0x200; + // Fake motion: LB held + if (pad.buttons & Gamepad::BUTTON_LB) { + out->fake_motion = 1; + } else if (out->fake_motion) { + out->wiimote.accel_x = 0x82 << 2; + out->nunchuk.accel_x = 0x82 << 2; + out->wiimote.accel_y = 0x82 << 2; + out->nunchuk.accel_y = 0x82 << 2; + out->wiimote.accel_z = 0x9f << 2; + out->nunchuk.accel_z = 0x9f << 2; + out->fake_motion = 0; + out->center_accel = 1; + } + // Mode switch: home+down held -> CLASSIC + if (out->wiimote.home && out->wiimote.down) { + if (++s_home_down_hold_count >= MODE_SWITCH_HOLD_FRAMES) { + s_home_down_hold_count = 0; + out->wiimote.home = false; + out->wiimote.down = false; + out->switch_mode = 1; + } + } else { + s_home_down_hold_count = 0; + } + if (out->switch_mode) { + out->mode = CLASSIC_CONTROLLER; + out->switch_mode = 0; + out->reset_ir = 1; + flash_led_times(3); + } + break; + + case CLASSIC_CONTROLLER: { + // IR pointer from right stick (correct direction: +rx = right, -ry = up; deadzone avoids drift) + out->wiimote.ir_x = stick_to_wiimote_ir(classic_rs_apply_deadzone(pad.joystick_rx)); + out->wiimote.ir_y = stick_to_wiimote_ir(classic_rs_apply_deadzone(-pad.joystick_ry)); + out->classic.a = (pad.buttons & Gamepad::BUTTON_A) != 0; + out->classic.b = (pad.buttons & Gamepad::BUTTON_B) != 0; + out->classic.x = (pad.buttons & Gamepad::BUTTON_Y) != 0; // PicoGamepadConverter: x=Y, y=X + out->classic.y = (pad.buttons & Gamepad::BUTTON_X) != 0; + out->classic.minus = (pad.buttons & Gamepad::BUTTON_BACK) != 0; + out->classic.plus = (pad.buttons & Gamepad::BUTTON_START) != 0; + out->classic.home = (pad.buttons & Gamepad::BUTTON_SYS) != 0; + out->classic.up = (pad.dpad & Gamepad::DPAD_UP) != 0; + out->classic.down = (pad.dpad & Gamepad::DPAD_DOWN) != 0; + out->classic.left = (pad.dpad & Gamepad::DPAD_LEFT) != 0; + out->classic.right = (pad.dpad & Gamepad::DPAD_RIGHT) != 0; + out->classic.lz = (pad.buttons & Gamepad::BUTTON_LB) != 0; + out->classic.rz = (pad.buttons & Gamepad::BUTTON_RB) != 0; + out->classic.lt = pad.trigger_l; + out->classic.rt = pad.trigger_r; + out->classic.ltrigger = (pad.trigger_l > 32); + out->classic.rtrigger = (pad.trigger_r > 32); + // Left stick: correct direction (right→right, up→forward) + out->classic.ls_x = stick_to_classic_xy(pad.joystick_lx); + out->classic.ls_y = stick_to_classic_xy(-pad.joystick_ly); + // Right stick → classic RS (used for pointer): fix X/Y direction, deadzone to prevent drift up + int16_t rs_x = classic_rs_apply_deadzone(pad.joystick_rx); + int16_t rs_y = classic_rs_apply_deadzone(-pad.joystick_ry); + out->classic.rs_x = (uint8_t)std::clamp((int)((float)rs_x / 32768.f * 15.f + 15.f), 0, 30); + out->classic.rs_y = (uint8_t)std::clamp((int)((float)rs_y / 32768.f * 15.f + 15.f), 0, 30); + // Mode switch: home+down held -> NO_EXTENSION + if (out->classic.home && out->classic.down) { + if (++s_home_down_hold_count >= MODE_SWITCH_HOLD_FRAMES) { + s_home_down_hold_count = 0; + out->classic.home = false; + out->classic.down = false; + out->switch_mode = 1; + } + } else { + s_home_down_hold_count = 0; + } + if (out->switch_mode) { + out->mode = NO_EXTENSION; + out->switch_mode = 0; + out->reset_ir = 1; + flash_led_times(1); + } + break; + } + + default: + out->mode = NO_EXTENSION; + out->switch_mode = 0; + out->reset_ir = 0; + break; + } +} + +} // namespace Wii + +#endif diff --git a/Firmware/RP2040/src/Wii/WiiReportConverter.h b/Firmware/RP2040/src/Wii/WiiReportConverter.h new file mode 100644 index 00000000..87ba8333 --- /dev/null +++ b/Firmware/RP2040/src/Wii/WiiReportConverter.h @@ -0,0 +1,19 @@ +#ifndef OGXM_WII_REPORT_CONVERTER_H +#define OGXM_WII_REPORT_CONVERTER_H + +#include "Board/Config.h" + +#if defined(OGXM_BOARD_USES_PICO_W_FIRMWARE) && defined(CONFIG_EN_USB_HOST) + +class Gamepad; + +namespace Wii { + +/** Fill WiimoteReport (C struct from wiimote.h) from Gamepad (for Wii mode BT). */ +void gamepad_to_wiimote_report(Gamepad& gamepad, void* out_wiimote_report); + +} // namespace Wii + +#endif + +#endif diff --git a/Firmware/RP2040/src/Wii/utils.c b/Firmware/RP2040/src/Wii/utils.c new file mode 100644 index 00000000..79fcf7b0 --- /dev/null +++ b/Firmware/RP2040/src/Wii/utils.c @@ -0,0 +1,8 @@ +#include "utils.h" +#include + +void save_wii_addr(void *wii_addr) +{ + (void)wii_addr; + /* TODO: persist to flash (e.g. UserSettings sector) for reconnection */ +} diff --git a/Firmware/RP2040/src/Wii/utils.h b/Firmware/RP2040/src/Wii/utils.h new file mode 100644 index 00000000..05ed98fd --- /dev/null +++ b/Firmware/RP2040/src/Wii/utils.h @@ -0,0 +1,19 @@ +#ifndef OGXM_WII_UTILS_H +#define OGXM_WII_UTILS_H + +#ifndef UNUSED +#define UNUSED(x) ((void)(x)) +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +/** Save Wii console BD address to flash (for reconnection). Stub: no-op until we add persistence. */ +void save_wii_addr(void *wii_addr); + +#ifdef __cplusplus +} +#endif + +#endif /* OGXM_WII_UTILS_H */ diff --git a/Firmware/RP2040/src/Wii/wiimote-lib/CMakeLists.txt b/Firmware/RP2040/src/Wii/wiimote-lib/CMakeLists.txt new file mode 100644 index 00000000..9fac7517 --- /dev/null +++ b/Firmware/RP2040/src/Wii/wiimote-lib/CMakeLists.txt @@ -0,0 +1,11 @@ +add_library(wiimote-lib INTERFACE) +target_include_directories(wiimote-lib INTERFACE ${CMAKE_CURRENT_LIST_DIR}) +target_link_libraries(wiimote-lib INTERFACE pico_stdlib pico_btstack_classic pico_btstack_cyw43 + pico_cyw43_arch_none) +target_sources(wiimote-lib INTERFACE + ${CMAKE_CURRENT_LIST_DIR}/wiimote_btstack.c + ${CMAKE_CURRENT_LIST_DIR}/wiimote.c + ${CMAKE_CURRENT_LIST_DIR}/wm_reports.c + ${CMAKE_CURRENT_LIST_DIR}/motion.c + ${CMAKE_CURRENT_LIST_DIR}/wm_crypto.c + ${CMAKE_CURRENT_LIST_DIR}/inet.c) diff --git a/Firmware/RP2040/src/Wii/wiimote-lib/btstack_config.h b/Firmware/RP2040/src/Wii/wiimote-lib/btstack_config.h new file mode 100644 index 00000000..b3b19083 --- /dev/null +++ b/Firmware/RP2040/src/Wii/wiimote-lib/btstack_config.h @@ -0,0 +1,79 @@ +#ifndef _PICO_BTSTACK_BTSTACK_CONFIG_H +#define _PICO_BTSTACK_BTSTACK_CONFIG_H + +// BTstack features that can be enabled +#ifdef ENABLE_BLE +#define ENABLE_LE_PERIPHERAL +#define ENABLE_LE_CENTRAL +#define ENABLE_L2CAP_LE_CREDIT_BASED_FLOW_CONTROL_MODE +#endif +#define ENABLE_LOG_INFO +#define ENABLE_LOG_ERROR +#define ENABLE_PRINTF_HEXDUMP +#define ENABLE_SCO_OVER_HCI + +// BTstack configuration. buffers, sizes, ... +#define HCI_OUTGOING_PRE_BUFFER_SIZE 4 +#define HCI_ACL_PAYLOAD_SIZE (1691 + 4) +#define HCI_ACL_CHUNK_SIZE_ALIGNMENT 4 +#define MAX_NR_AVDTP_CONNECTIONS 1 +#define MAX_NR_AVDTP_STREAM_ENDPOINTS 1 +#define MAX_NR_AVRCP_CONNECTIONS 2 +#define MAX_NR_BNEP_CHANNELS 1 +#define MAX_NR_BNEP_SERVICES 1 +#define MAX_NR_BTSTACK_LINK_KEY_DB_MEMORY_ENTRIES 2 +#define MAX_NR_GATT_CLIENTS 1 +#define MAX_NR_HCI_CONNECTIONS 2 +#define MAX_NR_HID_HOST_CONNECTIONS 1 +#define MAX_NR_HIDS_CLIENTS 1 +#define MAX_NR_HFP_CONNECTIONS 1 +#define MAX_NR_L2CAP_CHANNELS 4 +#define MAX_NR_L2CAP_SERVICES 3 +#define MAX_NR_RFCOMM_CHANNELS 1 +#define MAX_NR_RFCOMM_MULTIPLEXERS 1 +#define MAX_NR_RFCOMM_SERVICES 1 +#define MAX_NR_SERVICE_RECORD_ITEMS 4 +#define MAX_NR_SM_LOOKUP_ENTRIES 3 +#define MAX_NR_WHITELIST_ENTRIES 16 +#define MAX_NR_LE_DEVICE_DB_ENTRIES 16 + +// Limit number of ACL/SCO Buffer to use by stack to avoid cyw43 shared bus overrun +#define MAX_NR_CONTROLLER_ACL_BUFFERS 3 +#define MAX_NR_CONTROLLER_SCO_PACKETS 3 + +// Enable and configure HCI Controller to Host Flow Control to avoid cyw43 shared bus overrun +#define ENABLE_HCI_CONTROLLER_TO_HOST_FLOW_CONTROL +#define HCI_HOST_ACL_PACKET_LEN 1024 +#define HCI_HOST_ACL_PACKET_NUM 3 +#define HCI_HOST_SCO_PACKET_LEN 120 +#define HCI_HOST_SCO_PACKET_NUM 3 + +// Link Key DB and LE Device DB using TLV on top of Flash Sector interface +#define NVM_NUM_DEVICE_DB_ENTRIES 16 +#define NVM_NUM_LINK_KEYS 16 + +// We don't give btstack a malloc, so use a fixed-size ATT DB. +#define MAX_ATT_DB_SIZE 512 + +// BTstack HAL configuration +#define HAVE_EMBEDDED_TIME_MS + +// map btstack_assert onto Pico SDK assert() +#define HAVE_ASSERT + +// Some USB dongles take longer to respond to HCI reset (e.g. BCM20702A). +#define HCI_RESET_RESEND_TIMEOUT_MS 1000 + +#define ENABLE_SOFTWARE_AES128 +#define ENABLE_MICRO_ECC_FOR_LE_SECURE_CONNECTIONS + +#define HAVE_BTSTACK_STDIN + +// To get the audio demos working even with HCI dump at 115200, this truncates long ACL packets +//#define HCI_DUMP_STDOUT_MAX_SIZE_ACL 100 + +#ifdef ENABLE_CLASSIC +// #define ENABLE_L2CAP_ENHANCED_RETRANSMISSION_MODE +#endif + +#endif // _PICO_BTSTACK_BTSTACK_CONFIG_H diff --git a/Firmware/RP2040/src/Wii/wiimote-lib/eeprom.bin.h b/Firmware/RP2040/src/Wii/wiimote-lib/eeprom.bin.h new file mode 100644 index 00000000..73a743e1 --- /dev/null +++ b/Firmware/RP2040/src/Wii/wiimote-lib/eeprom.bin.h @@ -0,0 +1,20 @@ +#ifndef EEPROM_BIN_H +#define EEPROM_BIN_H + +#define TOTAL_SIZE_EEPROM 5888 +#define LAST_DATA_BEGIN_POS 5843 + +const unsigned char eeprom_bin_1[] = { + 0x66, 0x4d, 0x08, 0x66, 0xb3, 0x9a, 0xb3, 0xb3, 0x9a, 0x4d, 0x97, 0x66, + 0x4d, 0x08, 0x66, 0xb3, 0x9a, 0xb3, 0xb3, 0x9a, 0x4d, 0x97, 0x85, 0x85, + 0x85, 0x00, 0xa0, 0xa0, 0xa0, 0x00, 0x40, 0x04, 0x85, 0x85, 0x85, 0x00, + 0xa0, 0xa0, 0xa0, 0x00, 0x40, 0x04 +}; + +const unsigned char eeprom_bin_2[] = { + 0xff, + 0x11, 0xee, 0x00, 0x00, 0x33, 0xcc, 0x44, 0xbb, 0x00, 0x00, 0x66, 0x99, + 0x77, 0x88, 0x00, 0x00, 0x2b, 0x01, 0x30, 0x13 +}; + +#endif /* EEPROM_BIN_H */ diff --git a/Firmware/RP2040/src/Wii/wiimote-lib/inet.c b/Firmware/RP2040/src/Wii/wiimote-lib/inet.c new file mode 100644 index 00000000..8cf0629a --- /dev/null +++ b/Firmware/RP2040/src/Wii/wiimote-lib/inet.c @@ -0,0 +1,44 @@ +#include +#include + +/* variables */ +const int _test = 0x1234; +const char * _ptest = (char *)&_test; + + +/* functions */ +/* htonl */ +uint32_t htonl(uint32_t host32) +{ + if(*_ptest == 0x12) + return host32; + return ((host32 & 0xff) << 24) | ((host32 & 0xff00) << 8) + | ((host32 & 0xff0000) >> 8) | ((host32 & 0xff000000) >> 24); +} + + +/* htons */ +uint16_t htons(uint16_t host16) +{ + if(*_ptest == 0x12) + return host16; + return ((host16 & 0xff) << 8) | ((host16 & 0xff00) >> 8); +} + + +/* ntohl */ +uint32_t ntohl(uint32_t net32) +{ + return htonl(net32); +} + + +/* ntohs */ +uint16_t ntohs(uint16_t net16) +{ + return htons(net16); +} + + +//https://codereview.stackexchange.com/questions/149717/implementation-of-c-standard-library-function-ntohl +//https://github.com/joeferner/stm32-network/blob/master/interface.h \ No newline at end of file diff --git a/Firmware/RP2040/src/Wii/wiimote-lib/inet.h b/Firmware/RP2040/src/Wii/wiimote-lib/inet.h new file mode 100644 index 00000000..02424312 --- /dev/null +++ b/Firmware/RP2040/src/Wii/wiimote-lib/inet.h @@ -0,0 +1,6 @@ +#include + +uint16_t htons(uint16_t host16); +uint32_t ntohl(uint32_t net32); +uint16_t htons(uint16_t host16); +uint16_t ntohs(uint16_t net16); \ No newline at end of file diff --git a/Firmware/RP2040/src/Wii/wiimote-lib/motion.c b/Firmware/RP2040/src/Wii/wiimote-lib/motion.c new file mode 100644 index 00000000..254297f6 --- /dev/null +++ b/Firmware/RP2040/src/Wii/wiimote-lib/motion.c @@ -0,0 +1,155 @@ +#include "motion.h" + +#include "vector_math.h" + +//units in meters +static const double screen_distance = 2; +static const double screen_width = 1.0; +static const double screen_aspect = 4.0 / 3.0; + +static const double sensor_bar_y = screen_width / screen_aspect * 0.5; +static const double sensor_bar_width = 0.20; + +static const double cam_aspect = 1024.0 / 768.0; +static const double cam_fov = 42.0; +static const double cam_far = 4.0; +static const double cam_near = 0.5; + +static const uint16_t accelerometer_zero = 0x85 << 2; +static const uint16_t accelerometer_unit = 0x6C; + +void look_at_pointer(mat4 * wiimote_mat, float pointer_x, float pointer_y) +{ + vec3 pointer_world = { + (pointer_x - 0.5) * screen_width, + (pointer_y - 0.5) * screen_width / screen_aspect, + -screen_distance + }; + + vec3 dir = { pointer_world.x, pointer_world.y, pointer_world.z }; + vec3_normalize(&dir); + + vec3 up = { 0.0, 1.0, 0.0 }; + vec3 z = { -dir.x, -dir.y, -dir.z }; + + vec3 x; + vec3_cross(&x, &up, &z); + vec3_normalize(&x); + + vec3 y; + vec3_cross(&y, &z, &x); + // vec3_normalize(&y); + + wiimote_mat->v0 = (vec4){ x.x, x.y, x.z, 0.0 }; + wiimote_mat->v1 = (vec4){ y.x, y.y, y.z, 0.0 }; + wiimote_mat->v2 = (vec4){ z.x, z.y, z.z, 0.0 }; + wiimote_mat->v3 = (vec4){ 0.0, 0.0, 0.0, 1.0 }; +} + +void make_cam_projection_mat(mat4 * proj_mat) +{ + double near = cam_near; + double far = cam_far; + + double top = near * tan(cam_fov / 180.0 * M_PI * 0.5); + double height = 2.0 * top; + double width = cam_aspect * height; + double left = -0.5 * width; + + double right = left + width; + double bottom = top - height; + + proj_mat->v0 = (vec4){ 2.0 * near / (right - left), 0.0, 0.0, 0.0 }; + proj_mat->v1 = (vec4){ 0.0, 2.0 * near / (top - bottom), 0.0, 0.0 }; + proj_mat->v2 = (vec4){ (right + left) / (right - left), (top + bottom) / (top - bottom), -(far + near) / (far - near), -1.0 }; + proj_mat->v3 = (vec4){ 0.0, 0.0, -2.0 * far * near / (far - near), 0.0 }; +} + +void set_accelerometer(struct wiimote_state * state, const mat4 * wiimote_mat) +{ + vec3 accel = { 0, -1.0, 0 }; + mat3 accel_m; + mat3_from_mat4(&accel_m, wiimote_mat); + mat3_invert(&accel_m); + mat3_transpose(&accel_m); + vec3_apply_mat3(&accel, &accel_m); + + accel.x = fmax(-3.4, fmin(3.4, accel.x)); + accel.y = fmax(-3.4, fmin(3.4, accel.y)); + accel.z = fmax(-3.4, fmin(3.4, accel.z)); + + //transform to wiimote's accelerometer coordinate system + state->usr.accel_x = accelerometer_zero + + (int)round((double)accelerometer_unit * -accel.x); + state->usr.accel_z = accelerometer_zero + + (int)round((double)accelerometer_unit * -accel.y); + state->usr.accel_y = accelerometer_zero + + (int)round((double)accelerometer_unit * accel.z); +} + +void set_motionplus(struct wiimote_state * state, const mat4 * wiimote_mat) +{ + +} + +void set_motion_state(struct wiimote_state * state, float pointer_x, float pointer_y) +{ + mat4 wiimote_mat; + look_at_pointer(&wiimote_mat, pointer_x, pointer_y); + + mat4 view_mat = wiimote_mat; + mat4_invert(&view_mat); + + mat4 model_mat; + vec3 model_pos = (vec3){ 0.0, sensor_bar_y, -screen_distance }; + mat4_make_translation(&model_mat, &model_pos); + + mat4 proj_mat; + make_cam_projection_mat(&proj_mat); + + mat4_mult(&view_mat, &model_mat); + mat4_mult(&proj_mat, &view_mat); + + vec4 sensor_pt0 = { -sensor_bar_width * 0.5, 0.0, 0.0, 1.0 }; + vec4 sensor_pt1 = { sensor_bar_width * 0.5, 0.0, 0.0, 1.0 }; + + vec4_apply_mat4(&sensor_pt0, &proj_mat); + vec4_apply_mat4(&sensor_pt1, &proj_mat); + + vec4_multiply_scalar(&sensor_pt0, 1 / sensor_pt0.w); + vec4_multiply_scalar(&sensor_pt1, 1 / sensor_pt1.w); + + vec4_add_scalar(&sensor_pt0, 1.0); + vec4_multiply_scalar(&sensor_pt0, 0.5); + + vec4_add_scalar(&sensor_pt1, 1.0); + vec4_multiply_scalar(&sensor_pt1, 0.5); + + double min_pt_size = 1.0; + double max_pt_size = 15.0; + + reset_ir_object(&state->usr.ir_object[0]); + reset_ir_object(&state->usr.ir_object[1]); + + if (sensor_pt0.x > 0 && sensor_pt0.x < 1 && + sensor_pt0.y > 0 && sensor_pt0.y < 1 && + sensor_pt0.z > 0 && sensor_pt0.z < 1) + { + state->usr.ir_object[0].x = round(sensor_pt0.x * 1023); + state->usr.ir_object[0].y = round(sensor_pt0.y * 767); + state->usr.ir_object[0].size = round(min_pt_size + + pow(1.0 - sensor_pt0.z, 2.0) * (max_pt_size - min_pt_size)); + } + + if (sensor_pt1.x > 0 && sensor_pt1.x < 1 && + sensor_pt1.y > 0 && sensor_pt1.y < 1 && + sensor_pt1.z > 0 && sensor_pt1.z < 1) + { + state->usr.ir_object[1].x = round(sensor_pt1.x * 1023); + state->usr.ir_object[1].y = round(sensor_pt1.y * 767); + state->usr.ir_object[1].size = round(min_pt_size + + pow(1.0 - sensor_pt1.z, 2.0) * (max_pt_size - min_pt_size)); + } + + set_accelerometer(state, &wiimote_mat); +} \ No newline at end of file diff --git a/Firmware/RP2040/src/Wii/wiimote-lib/motion.h b/Firmware/RP2040/src/Wii/wiimote-lib/motion.h new file mode 100644 index 00000000..266e50ab --- /dev/null +++ b/Firmware/RP2040/src/Wii/wiimote-lib/motion.h @@ -0,0 +1,8 @@ +#ifndef MOTION_H +#define MOTION_H + +#include "wiimote.h" + +void set_motion_state(struct wiimote_state * state, float pointer_x, float pointer_y); + +#endif \ No newline at end of file diff --git a/Firmware/RP2040/src/Wii/wiimote-lib/sdp_consts.h b/Firmware/RP2040/src/Wii/wiimote-lib/sdp_consts.h new file mode 100644 index 00000000..4cdc0e59 --- /dev/null +++ b/Firmware/RP2040/src/Wii/wiimote-lib/sdp_consts.h @@ -0,0 +1,183 @@ +#include + +static const uint8_t resp0[14] = +{ 0x03, 0x00, 0x00, 0x00, 0x09, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00 }; + +static const uint8_t resp1[128] = +{ + 0x05, 0x00, 0x01, 0x00, 0x7B, 0x00, 0x76, 0x36, 0x01, 0xCC, 0x09, 0x00, 0x00, + 0x0A, 0x00, 0x01, 0x00, 0x00, 0x09, 0x00, 0x01, 0x35, 0x03, 0x19, 0x11, 0x24, + 0x09, 0x00, 0x04, 0x35, 0x0D, 0x35, 0x06, 0x19, 0x01, 0x00, 0x09, 0x00, 0x11, + 0x35, 0x03, 0x19, 0x00, 0x11, 0x09, 0x00, 0x05, 0x35, 0x03, 0x19, 0x10, 0x02, + 0x09, 0x00, 0x06, 0x35, 0x09, 0x09, 0x65, 0x6E, 0x09, 0x00, 0x6A, 0x09, 0x01, + 0x00, 0x09, 0x00, 0x09, 0x35, 0x08, 0x35, 0x06, 0x19, 0x11, 0x24, 0x09, 0x01, + 0x00, 0x09, 0x00, 0x0D, 0x35, 0x0F, 0x35, 0x0D, 0x35, 0x06, 0x19, 0x01, 0x00, + 0x09, 0x00, 0x13, 0x35, 0x03, 0x19, 0x00, 0x11, 0x09, 0x01, 0x00, 0x25, 0x13, + 0x4E, 0x69, 0x6E, 0x74, 0x65, 0x6E, 0x64, 0x6F, 0x20, 0x52, 0x56, 0x4C, 0x2D, + 0x43, 0x4E, 0x54, 0x2D, 0x30, 0x31, 0x09, 0x01, 0x02, 0x00, 0x76 +}; + +static const uint8_t resp2[128] = +{ + 0x05, 0x00, 0x02, 0x00, 0x7B, 0x00, 0x76, 0x01, 0x25, 0x13, 0x4E, 0x69, 0x6E, + 0x74, 0x65, 0x6E, 0x64, 0x6F, 0x20, 0x52, 0x56, 0x4C, 0x2D, 0x43, 0x4E, 0x54, + 0x2D, 0x30, 0x31, 0x09, 0x01, 0x02, 0x25, 0x08, 0x4E, 0x69, 0x6E, 0x74, 0x65, + 0x6E, 0x64, 0x6F, 0x09, 0x02, 0x00, 0x09, 0x01, 0x00, 0x09, 0x02, 0x01, 0x09, + 0x01, 0x11, 0x09, 0x02, 0x02, 0x08, 0x04, 0x09, 0x02, 0x03, 0x08, 0x33, 0x09, + 0x02, 0x04, 0x28, 0x00, 0x09, 0x02, 0x05, 0x28, 0x01, 0x09, 0x02, 0x06, 0x35, + 0xDF, 0x35, 0xDD, 0x08, 0x22, 0x25, 0xD9, 0x05, 0x01, 0x09, 0x05, 0xA1, 0x01, + 0x85, 0x10, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x01, 0x06, 0x00, + 0xFF, 0x09, 0x01, 0x91, 0x00, 0x85, 0x11, 0x95, 0x01, 0x09, 0x01, 0x91, 0x00, + 0x85, 0x12, 0x95, 0x02, 0x09, 0x01, 0x91, 0x00, 0x02, 0x00, 0xEC +}; + +static const uint8_t resp3[128] = +{ + 0x05, 0x00, 0x03, 0x00, 0x7B, 0x00, 0x76, 0x85, 0x13, 0x95, 0x01, 0x09, 0x01, + 0x91, 0x00, 0x85, 0x14, 0x95, 0x01, 0x09, 0x01, 0x91, 0x00, 0x85, 0x15, 0x95, + 0x01, 0x09, 0x01, 0x91, 0x00, 0x85, 0x16, 0x95, 0x15, 0x09, 0x01, 0x91, 0x00, + 0x85, 0x17, 0x95, 0x06, 0x09, 0x01, 0x91, 0x00, 0x85, 0x18, 0x95, 0x15, 0x09, + 0x01, 0x91, 0x00, 0x85, 0x19, 0x95, 0x01, 0x09, 0x01, 0x91, 0x00, 0x85, 0x1A, + 0x95, 0x01, 0x09, 0x01, 0x91, 0x00, 0x85, 0x20, 0x95, 0x06, 0x09, 0x01, 0x81, + 0x00, 0x85, 0x21, 0x95, 0x15, 0x09, 0x01, 0x81, 0x00, 0x85, 0x22, 0x95, 0x04, + 0x09, 0x01, 0x81, 0x00, 0x85, 0x30, 0x95, 0x02, 0x09, 0x01, 0x81, 0x00, 0x85, + 0x31, 0x95, 0x05, 0x09, 0x01, 0x81, 0x00, 0x85, 0x32, 0x95, 0x0A, 0x09, 0x01, + 0x81, 0x00, 0x85, 0x33, 0x95, 0x11, 0x09, 0x01, 0x02, 0x01, 0x62 +}; + +static const uint8_t resp4[117] = +{ + 0x05, 0x00, 0x04, 0x00, 0x70, 0x00, 0x6D, 0x81, 0x00, 0x85, 0x34, 0x95, 0x15, + 0x09, 0x01, 0x81, 0x00, 0x85, 0x35, 0x95, 0x15, 0x09, 0x01, 0x81, 0x00, 0x85, + 0x36, 0x95, 0x15, 0x09, 0x01, 0x81, 0x00, 0x85, 0x37, 0x95, 0x15, 0x09, 0x01, + 0x81, 0x00, 0x85, 0x3D, 0x95, 0x15, 0x09, 0x01, 0x81, 0x00, 0x85, 0x3E, 0x95, + 0x15, 0x09, 0x01, 0x81, 0x00, 0x85, 0x3F, 0x95, 0x15, 0x09, 0x01, 0x81, 0x00, + 0xC0, 0x09, 0x02, 0x07, 0x35, 0x08, 0x35, 0x06, 0x09, 0x04, 0x09, 0x09, 0x01, + 0x00, 0x09, 0x02, 0x08, 0x28, 0x00, 0x09, 0x02, 0x09, 0x28, 0x01, 0x09, 0x02, + 0x0A, 0x28, 0x01, 0x09, 0x02, 0x0B, 0x09, 0x01, 0x00, 0x09, 0x02, 0x0C, 0x09, + 0x0C, 0x80, 0x09, 0x02, 0x0D, 0x28, 0x00, 0x09, 0x02, 0x0E, 0x28, 0x00, 0x00 +}; + + +static const uint8_t wiimote_report_descriptor[] = { +0x05, 0x01, // Usage Page (Generic Desktop Ctrls) +0x09, 0x05, // Usage (Game Pad) +0xA1, 0x01, // Collection (Application) +0x85, 0x10, // Report ID (16) +0x15, 0x00, // Logical Minimum (0) +0x26, 0xFF, 0x00, // Logical Maximum (255) +0x75, 0x08, // Report Size (8) +0x95, 0x01, // Report Count (1) +0x06, 0x00, 0xFF, // Usage Page (Vendor Defined 0xFF00) +0x09, 0x01, // Usage (0x01) +0x91, 0x00, // Output (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) +0x85, 0x11, // Report ID (17) +0x75, 0x08, // Report Size (8) +0x95, 0x01, // Report Count (1) +0x09, 0x01, // Usage (0x01) +0x91, 0x00, // Output (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) +0x85, 0x12, // Report ID (18) +0x75, 0x08, // Report Size (8) +0x95, 0x02, // Report Count (2) +0x09, 0x01, // Usage (0x01) +0x91, 0x00, // Output (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) +0x85, 0x13, // Report ID (19) +0x75, 0x08, // Report Size (8) +0x95, 0x01, // Report Count (1) +0x09, 0x01, // Usage (0x01) +0x91, 0x00, // Output (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) +0x85, 0x14, // Report ID (20) +0x75, 0x08, // Report Size (8) +0x95, 0x01, // Report Count (1) +0x09, 0x01, // Usage (0x01) +0x91, 0x00, // Output (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) +0x85, 0x15, // Report ID (21) +0x75, 0x08, // Report Size (8) +0x95, 0x01, // Report Count (1) +0x09, 0x01, // Usage (0x01) +0x91, 0x00, // Output (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) +0x85, 0x16, // Report ID (22) +0x75, 0x08, // Report Size (8) +0x95, 0x15, // Report Count (21) +0x09, 0x01, // Usage (0x01) +0x91, 0x00, // Output (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) +0x85, 0x17, // Report ID (23) +//0x95, 0x06, // Report Count (6) +0x75, 0x08, // Report Size (8) +0x95, 0x06, // Report Count (6) +0x09, 0x01, // Usage (0x01) +0x91, 0x00, // Output (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) +0x85, 0x18, // Report ID (24) +0x75, 0x08, // Report Size (8) +0x95, 0x15, // Report Count (21) +0x09, 0x01, // Usage (0x01) +0x91, 0x00, // Output (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) +0x85, 0x19, // Report ID (25) +0x75, 0x08, // Report Size (8) +0x95, 0x01, // Report Count (1) +0x09, 0x01, // Usage (0x01) +0x91, 0x00, // Output (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) +0x85, 0x1A, // Report ID (26) +0x75, 0x08, // Report Size (8) +0x95, 0x01, // Report Count (1) +0x09, 0x01, // Usage (0x01) +0x91, 0x00, // Output (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) +0x85, 0x20, // Report ID (32) +0x95, 0x06, // Report Count (6) +0x09, 0x01, // Usage (0x01) +0x81, 0x00, // Input (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position) +0x85, 0x21, // Report ID (33) +0x95, 0x15, // Report Count (21) +0x09, 0x01, // Usage (0x01) +0x81, 0x00, // Input (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position) +0x85, 0x22, // Report ID (34) +0x95, 0x04, // Report Count (4) +0x09, 0x01, // Usage (0x01) +0x81, 0x00, // Input (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position) +0x85, 0x30, // Report ID (48) +0x95, 0x02, // Report Count (2) +0x09, 0x01, // Usage (0x01) +0x81, 0x00, // Input (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position) +0x85, 0x31, // Report ID (49) +0x95, 0x05, // Report Count (5) +0x09, 0x01, // Usage (0x01) +0x81, 0x00, // Input (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position) +0x85, 0x32, // Report ID (50) +0x95, 0x0A, // Report Count (10) +0x09, 0x01, // Usage (0x01) +0x81, 0x00, // Input (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position) +0x85, 0x33, // Report ID (51) +0x95, 0x11, // Report Count (17) +0x09, 0x01, // Usage (0x01) +0x81, 0x00, // Input (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position) +0x85, 0x34, // Report ID (52) +0x95, 0x15, // Report Count (21) +0x09, 0x01, // Usage (0x01) +0x81, 0x00, // Input (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position) +0x85, 0x35, // Report ID (53) +0x95, 0x15, // Report Count (21) +0x09, 0x01, // Usage (0x01) +0x81, 0x00, // Input (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position) +0x85, 0x36, // Report ID (54) +0x95, 0x15, // Report Count (21) +0x09, 0x01, // Usage (0x01) +0x81, 0x00, // Input (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position) +0x85, 0x37, // Report ID (55) +0x95, 0x15, // Report Count (21) +0x09, 0x01, // Usage (0x01) +0x81, 0x00, // Input (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position) +0x85, 0x3D, // Report ID (61) +0x95, 0x15, // Report Count (21) +0x09, 0x01, // Usage (0x01) +0x81, 0x00, // Input (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position) +0x85, 0x3E, // Report ID (62) +0x95, 0x15, // Report Count (21) +0x09, 0x01, // Usage (0x01) +0x81, 0x00, // Input (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position) +0x85, 0x3F, // Report ID (63) +0x95, 0x15, // Report Count (21) +0x09, 0x01, // Usage (0x01) +0x81, 0x00, // Input (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position) +0xC0, // End Collection +0x00 // Unknown (bTag: 0x00, bType: 0x00) +}; diff --git a/Firmware/RP2040/src/Wii/wiimote-lib/vector_math.h b/Firmware/RP2040/src/Wii/wiimote-lib/vector_math.h new file mode 100644 index 00000000..87138e3a --- /dev/null +++ b/Firmware/RP2040/src/Wii/wiimote-lib/vector_math.h @@ -0,0 +1,263 @@ +#ifndef VECTOR_MATH_H +#define VECTOR_MATH_H + +#include + +//for debug/printing functions +#include + +typedef struct +{ + double x; + double y; + double z; +} vec3; + +typedef struct +{ + double x; + double y; + double z; + double w; +} vec4; + +typedef struct +{ + vec3 v0; + vec3 v1; + vec3 v2; +} mat3; + +typedef struct +{ + vec4 v0; + vec4 v1; + vec4 v2; + vec4 v3; +} mat4; + +double vec3_len(const vec3 * vec) +{ + return sqrt(vec->x * vec->x + vec->y * vec->y + vec->z * vec->z); +} + +void vec3_normalize(vec3 * vec) +{ + double len = vec3_len(vec); + vec->x /= len; + vec->y /= len; + vec->z /= len; +} + +void vec3_cross(vec3 * out, const vec3 * first, const vec3 * second) +{ + out->x = first->y * second->z - first->z * second->y; + out->y = first->z * second->x - first->x * second->z; + out->z = first->x * second->y - first->y * second->x; +} + +void mat4_make_translation(mat4 * mat, const vec3 * translate) +{ + mat->v0 = (vec4){ 1.0, 0.0, 0.0, 0.0 }; + mat->v1 = (vec4){ 0.0, 1.0, 0.0, 0.0 }; + mat->v2 = (vec4){ 0.0, 0.0, 1.0, 0.0 }; + mat->v3 = (vec4){ translate->x, translate->y, translate->z, 1.0 }; +} + +void mat4_mult(mat4 * a, const mat4 * b) +{ + double a11 = a->v0.x, a12 = a->v1.x, a13 = a->v2.x, a14 = a->v3.x; + double a21 = a->v0.y, a22 = a->v1.y, a23 = a->v2.y, a24 = a->v3.y; + double a31 = a->v0.z, a32 = a->v1.z, a33 = a->v2.z, a34 = a->v3.z; + double a41 = a->v0.w, a42 = a->v1.w, a43 = a->v2.w, a44 = a->v3.w; + + double b11 = b->v0.x, b12 = b->v1.x, b13 = b->v2.x, b14 = b->v3.x; + double b21 = b->v0.y, b22 = b->v1.y, b23 = b->v2.y, b24 = b->v3.y; + double b31 = b->v0.z, b32 = b->v1.z, b33 = b->v2.z, b34 = b->v3.z; + double b41 = b->v0.w, b42 = b->v1.w, b43 = b->v2.w, b44 = b->v3.w; + + a->v0.x = a11 * b11 + a12 * b21 + a13 * b31 + a14 * b41; + a->v1.x = a11 * b12 + a12 * b22 + a13 * b32 + a14 * b42; + a->v2.x = a11 * b13 + a12 * b23 + a13 * b33 + a14 * b43; + a->v3.x = a11 * b14 + a12 * b24 + a13 * b34 + a14 * b44; + + a->v0.y = a21 * b11 + a22 * b21 + a23 * b31 + a24 * b41; + a->v1.y = a21 * b12 + a22 * b22 + a23 * b32 + a24 * b42; + a->v2.y = a21 * b13 + a22 * b23 + a23 * b33 + a24 * b43; + a->v3.y = a21 * b14 + a22 * b24 + a23 * b34 + a24 * b44; + + a->v0.z = a31 * b11 + a32 * b21 + a33 * b31 + a34 * b41; + a->v1.z = a31 * b12 + a32 * b22 + a33 * b32 + a34 * b42; + a->v2.z = a31 * b13 + a32 * b23 + a33 * b33 + a34 * b43; + a->v3.z = a31 * b14 + a32 * b24 + a33 * b34 + a34 * b44; + + a->v0.w = a41 * b11 + a42 * b21 + a43 * b31 + a44 * b41; + a->v1.w = a41 * b12 + a42 * b22 + a43 * b32 + a44 * b42; + a->v2.w = a41 * b13 + a42 * b23 + a43 * b33 + a44 * b43; + a->v3.w = a41 * b14 + a42 * b24 + a43 * b34 + a44 * b44; +} + +void mat4_invert(mat4 * m) +{ + double n11 = m->v0.x, n21 = m->v0.y, n31 = m->v0.z, n41 = m->v0.w, + n12 = m->v1.x, n22 = m->v1.y, n32 = m->v1.z, n42 = m->v1.w, + n13 = m->v2.x, n23 = m->v2.y, n33 = m->v2.z, n43 = m->v2.w, + n14 = m->v3.x, n24 = m->v3.y, n34 = m->v3.z, n44 = m->v3.w; + + double t11 = n23 * n34 * n42 - n24 * n33 * n42 + n24 * n32 * n43 - n22 * n34 * n43 - n23 * n32 * n44 + n22 * n33 * n44; + double t12 = n14 * n33 * n42 - n13 * n34 * n42 - n14 * n32 * n43 + n12 * n34 * n43 + n13 * n32 * n44 - n12 * n33 * n44; + double t13 = n13 * n24 * n42 - n14 * n23 * n42 + n14 * n22 * n43 - n12 * n24 * n43 - n13 * n22 * n44 + n12 * n23 * n44; + double t14 = n14 * n23 * n32 - n13 * n24 * n32 - n14 * n22 * n33 + n12 * n24 * n33 + n13 * n22 * n34 - n12 * n23 * n34; + + double det = n11 * t11 + n21 * t12 + n31 * t13 + n41 * t14; + + if (det == 0) + { + return; + } + + double detInv = 1 / det; + + m->v0.x = t11 * detInv; + m->v0.y = (n24 * n33 * n41 - n23 * n34 * n41 - n24 * n31 * n43 + n21 * n34 * n43 + n23 * n31 * n44 - n21 * n33 * n44) * detInv; + m->v0.z = (n22 * n34 * n41 - n24 * n32 * n41 + n24 * n31 * n42 - n21 * n34 * n42 - n22 * n31 * n44 + n21 * n32 * n44) * detInv; + m->v0.w = (n23 * n32 * n41 - n22 * n33 * n41 - n23 * n31 * n42 + n21 * n33 * n42 + n22 * n31 * n43 - n21 * n32 * n43) * detInv; + + m->v1.x = t12 * detInv; + m->v1.y = (n13 * n34 * n41 - n14 * n33 * n41 + n14 * n31 * n43 - n11 * n34 * n43 - n13 * n31 * n44 + n11 * n33 * n44) * detInv; + m->v1.z = (n14 * n32 * n41 - n12 * n34 * n41 - n14 * n31 * n42 + n11 * n34 * n42 + n12 * n31 * n44 - n11 * n32 * n44) * detInv; + m->v1.w = (n12 * n33 * n41 - n13 * n32 * n41 + n13 * n31 * n42 - n11 * n33 * n42 - n12 * n31 * n43 + n11 * n32 * n43) * detInv; + + m->v2.x = t13 * detInv; + m->v2.y = (n14 * n23 * n41 - n13 * n24 * n41 - n14 * n21 * n43 + n11 * n24 * n43 + n13 * n21 * n44 - n11 * n23 * n44) * detInv; + m->v2.z = (n12 * n24 * n41 - n14 * n22 * n41 + n14 * n21 * n42 - n11 * n24 * n42 - n12 * n21 * n44 + n11 * n22 * n44) * detInv; + m->v2.w = (n13 * n22 * n41 - n12 * n23 * n41 - n13 * n21 * n42 + n11 * n23 * n42 + n12 * n21 * n43 - n11 * n22 * n43) * detInv; + + m->v3.x = t14 * detInv; + m->v3.y = (n13 * n24 * n31 - n14 * n23 * n31 + n14 * n21 * n33 - n11 * n24 * n33 - n13 * n21 * n34 + n11 * n23 * n34) * detInv; + m->v3.z = (n14 * n22 * n31 - n12 * n24 * n31 - n14 * n21 * n32 + n11 * n24 * n32 + n12 * n21 * n34 - n11 * n22 * n34) * detInv; + m->v3.w = (n12 * n23 * n31 - n13 * n22 * n31 + n13 * n21 * n32 - n11 * n23 * n32 - n12 * n21 * n33 + n11 * n22 * n33) * detInv; +} + +void vec4_apply_mat4(vec4 * vec, const mat4 * mat) +{ + double x = vec->x, y = vec->y, z = vec->z, w = vec->w; + + vec->x = mat->v0.x * x + mat->v1.x * y + mat->v2.x * z + mat->v3.x * w; + vec->y = mat->v0.y * x + mat->v1.y * y + mat->v2.y * z + mat->v3.y * w; + vec->z = mat->v0.z * x + mat->v1.z * y + mat->v2.z * z + mat->v3.z * w; + vec->w = mat->v0.w * x + mat->v1.w * y + mat->v2.w * z + mat->v3.w * w; +} + +void vec3_apply_mat3(vec3 * vec, const mat3 * mat) +{ + double x = vec->x, y = vec->y, z = vec->z; + + vec->x = mat->v0.x * x + mat->v1.x * y + mat->v2.x * z; + vec->y = mat->v0.y * x + mat->v1.y * y + mat->v2.y * z; + vec->z = mat->v0.z * x + mat->v1.z * y + mat->v2.z * z; +} + +void vec4_multiply_scalar(vec4 * vec, double scalar) +{ + vec->x *= scalar; + vec->y *= scalar; + vec->z *= scalar; + vec->w *= scalar; +} + +void vec4_add_scalar(vec4 * vec, double scalar) +{ + vec->x += scalar; + vec->y += scalar; + vec->z += scalar; + vec->w += scalar; +} + +void vec3_multiply_scalar(vec3 * vec, double scalar) +{ + vec->x *= scalar; + vec->y *= scalar; + vec->z *= scalar; +} + +void vec3_add_scalar(vec3 * vec, double scalar) +{ + vec->x += scalar; + vec->y += scalar; + vec->z += scalar; +} + +void mat3_invert(mat3 * m) +{ + double n11 = m->v0.x, n21 = m->v0.y, n31 = m->v0.z, + n12 = m->v1.x, n22 = m->v1.y, n32 = m->v1.z, + n13 = m->v2.x, n23 = m->v2.y, n33 = m->v2.z; + + double t11 = n33 * n22 - n32 * n23; + double t12 = n32 * n13 - n33 * n12; + double t13 = n23 * n12 - n22 * n13; + + double det = n11 * t11 + n21 * t12 + n31 * t13; + + if (det == 0) + { + return; + } + + double detInv = 1 / det; + + m->v0.x = t11 * detInv; + m->v0.y = (n31 * n23 - n33 * n21) * detInv; + m->v0.z = (n32 * n21 - n31 * n22) * detInv; + + m->v1.x = t12 * detInv; + m->v1.y = (n33 * n11 - n31 * n13) * detInv; + m->v1.z = (n31 * n12 - n32 * n11) * detInv; + + m->v2.x = t13 * detInv; + m->v2.y = (n21 * n13 - n23 * n11) * detInv; + m->v2.z = (n22 * n11 - n21 * n12) * detInv; +} + +void mat3_transpose(mat3 * m) +{ + double tmp; + tmp = m->v0.y; m->v0.y = m->v1.x; m->v1.x = tmp; + tmp = m->v0.z; m->v0.z = m->v2.x; m->v2.x = tmp; + tmp = m->v1.z; m->v1.z = m->v2.y; m->v2.y = tmp; +} + +void mat3_from_mat4(mat3 * out, const mat4 * mat) +{ + out->v0 = (vec3){ mat->v0.x, mat->v0.y, mat->v0.z }; + out->v1 = (vec3){ mat->v1.x, mat->v1.y, mat->v1.z }; + out->v2 = (vec3){ mat->v2.x, mat->v2.y, mat->v2.z }; +} + +void vec3_print(const vec3 * vec) +{ + printf("%f %f %f\n", vec->x, vec->y, vec->z); +} + +void vec4_print(const vec4 * vec) +{ + printf("%f %f %f %f\n", vec->x, vec->y, vec->z, vec->w); +} + +void mat3_print(const mat3 * mat) +{ + printf("%f %f %f\n", mat->v0.x, mat->v1.x, mat->v2.x); + printf("%f %f %f\n", mat->v0.y, mat->v1.y, mat->v2.y); + printf("%f %f %f\n", mat->v0.z, mat->v1.z, mat->v2.z); +} + +void mat4_print(const mat4 * mat) +{ + printf("%f %f %f %f\n", mat->v0.x, mat->v1.x, mat->v2.x, mat->v3.x); + printf("%f %f %f %f\n", mat->v0.y, mat->v1.y, mat->v2.y, mat->v3.y); + printf("%f %f %f %f\n", mat->v0.z, mat->v1.z, mat->v2.z, mat->v3.z); + printf("%f %f %f %f\n", mat->v0.w, mat->v1.w, mat->v2.w, mat->v3.w); +} + +#endif diff --git a/Firmware/RP2040/src/Wii/wiimote-lib/wiimote.c b/Firmware/RP2040/src/Wii/wiimote-lib/wiimote.c new file mode 100644 index 00000000..b220a710 --- /dev/null +++ b/Firmware/RP2040/src/Wii/wiimote-lib/wiimote.c @@ -0,0 +1,860 @@ +#include +#include +#include + +#include "inet.h" +#include "wiimote.h" +#include "wm_reports.h" +#include "eeprom.bin.h" + +static uint8_t *eeprom_bin; +int tries = 0; + +static uint8_t classic_calibration[16] = +{ + // 0xF8, 0x04, 0x7A, 0xF8, 0x04, 0x7A, 0xF8, 0x04, 0x7A, 0xF8, 0x04, 0x7A, 0x00, 0x00, 0x00, 0x00 + 0xE1, 0x19, 0x7C, 0xEF, 0x22, 0x7C, 0xE6, 0x1E, 0x85, 0xDE, 0x15, 0x8B, 0x0E, 0x22, 0x8F, 0xE4 +}; + +static uint8_t nunchuk_calibration[16] = +{ + 0x81, 0x80, 0x7F, 0x22, 0xB5, 0xB3, 0xB3, 0x03, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x83, 0x14, 0x69 +}; + +int process_report(struct wiimote_state *state, const uint8_t * buf, int len) +{ + struct report_data * data = (struct report_data *)buf; + + //every output report contains rumble info + state->sys.rumble = data->buf[0] & 0x01; + + switch (data->type) + { + case 0x11: //player LEDs + { + struct report_leds * rpt = (struct report_leds *)data->buf; + + state->sys.led_1 = rpt->led_1; + state->sys.led_2 = rpt->led_2; + state->sys.led_3 = rpt->led_3; + state->sys.led_4 = rpt->led_4; + + report_queue_push_ack(state, data->type, 0x00); + break; + } + case 0x12: //data reporting mode + { + struct report_mode * rpt = (struct report_mode *)data->buf; + + state->sys.reporting_continuous = rpt->continuous; + state->sys.reporting_mode = rpt->mode; + + report_queue_push_ack(state, data->type, 0x00); + break; + } + case 0x13: + case 0x1a: //ir camera enable + { + struct report_ir_enable * rpt = (struct report_ir_enable *)data->buf; + + state->sys.ircam_enabled = rpt->enabled; + + report_queue_push_ack(state, data->type, 0x00); + break; + } + case 0x14: + case 0x19: //speaker enable + { + struct report_speaker_enable * rpt = (struct report_speaker_enable *)data->buf; + + state->sys.speaker_enabled = !rpt->muted; + + report_queue_push_ack(state, data->type, 0x00); + break; + } + case 0x15: //status information request + //send a status report 0x20 + report_queue_push_status(state); + break; + case 0x16: //write memory + { + struct report_mem_write * rpt = (struct report_mem_write *)data->buf; + + if (rpt->source0 || rpt->source1) + { + write_register(state, ntohl(OFFSET24(rpt->offset)), rpt->size, rpt->data); + } + else + { + write_eeprom(state, ntohl(OFFSET24(rpt->offset)), rpt->size, rpt->data); + } + break; + } + case 0x17: //read memory + { + struct report_mem_read * rpt = (struct report_mem_read *)data->buf; + + if (rpt->source0 || rpt->source1) + { + read_register(state, ntohl(OFFSET24(rpt->offset)), ntohs(rpt->size)); + } + else + { + read_eeprom(state, ntohl(OFFSET24(rpt->offset)), ntohs(rpt->size)); + } + + break; + } + } + + return 0; +} + +int generate_report(struct wiimote_state * state, uint8_t * buf) +{ + int len; + + struct report_data * data = (struct report_data *)buf; + uint8_t * contents; + + if (state->usr.connected_extension_type != state->sys.connected_extension_type) + { + if (state->sys.extension_connected) + { + state->sys.extension_connected = 0; + state->sys.connected_extension_type = NoExtension; + state->sys.extension_hotplug_timer = 30; + report_queue_push_status(state); + } + + bool extension_connected = (state->usr.connected_extension_type != NoExtension); + if (extension_connected && --state->sys.extension_hotplug_timer <= 0) + { + state->sys.extension_connected = extension_connected; + state->sys.connected_extension_type = state->usr.connected_extension_type; + report_queue_push_status(state); + init_extension(state); + } + } + + if (!state->sys.reporting_continuous && !state->sys.report_changed && state->sys.queue == NULL) + return 0; + + if (state->sys.queue == NULL) + { + //regular report + memset(data, 0, sizeof(struct report_data)); + len = 2; + data->io = 0xa1; + data->type = state->sys.reporting_mode; + } + else + { + //queued report (acknowledgement, response, etc) + struct report * rpt; + rpt = report_queue_peek(state); + len = rpt->len; + memcpy(data, &rpt->data, sizeof(struct report_data)); + report_queue_pop(state); + } + + contents = data->buf; + + //fill report + switch (data->type) + { + case 0x30: // core buttons + report_append_buttons(state, contents); + len += 2; + break; + case 0x31: // core buttons + accelerometer + report_append_buttons(state, contents); + report_append_accelerometer(state, contents); + len += 2 + 3; + break; + case 0x32: // core buttons + 8 extension bytes + report_append_buttons(state, contents); + len += 2 + 8; + break; + case 0x33: // core buttons + accelerometer + 12 ir bytes + report_append_buttons(state, contents); + report_append_accelerometer(state, contents); + report_append_ir_12(state, contents + 5); + len += 2 + 3 + 12; + break; + case 0x34: // core buttons + 19 extension bytes + report_append_buttons(state, contents); + report_append_extension(state, contents + 2, 19); + len += 2 + 19; + break; + case 0x35: // core buttons + accelerometer + 16 extension bytes + report_append_buttons(state, contents); + report_append_accelerometer(state, contents); + report_append_extension(state, contents + 5, 16); + len += 2 + 3 + 16; + break; + case 0x36: // core buttons + 10 ir bytes + 9 extension bytes + report_append_buttons(state, contents); + report_append_ir_10(state, contents + 2); + report_append_extension(state, contents + 12, 9); + len += 2 + 10 + 9; + break; + case 0x37: // core buttons + accelerometer + 10 ir bytes + 6 extension bytes + report_append_buttons(state, contents); + report_append_accelerometer(state, contents); + report_append_ir_10(state, contents + 5); + report_append_extension(state, contents + 15, 6); + len += 2 + 3 + 10 + 6; + break; + case 0x3d: // 21 extension bytes + report_append_extension(state, contents, 21); + len += 21; + break; + case 0x3e: // interleaved core buttons + accelerometer with 36 ir bytes pt I + case 0x3f: // interleaved core buttons + accelerometer with 36 ir bytes pt II + report_append_buttons(state, contents); + report_append_interleaved(state, contents); + len += 21; + break; + default: // special output report (acknowledgement, status, or memory read) + report_append_buttons(state, contents); + break; + } + + return len; +} + +void read_eeprom(struct wiimote_state * state, uint32_t offset, uint16_t size) +{ + uint8_t *file; + uint8_t *buffer; + struct report * rpt; + int i; + + offset = offset & 0xFFFF; + + //addresses greater than 0x16FF cannot be read or written + if (offset + size > 0x16FF) + { + rpt = report_queue_push(state); + report_format_mem_resp(state, rpt, 0x10, 0x8, offset, NULL, false); + return; + } + + //fseek + file = &eeprom_bin[0 + offset]; + + buffer = (uint8_t *)malloc(size); + if (!buffer) + { + return; + } + + //fread + memcpy(buffer, file, size); + + //equivalent to ceil(size / 0x10) + int total_packets = (size + 0x10 - 1) / 0x10; + + //allocate all the needed reports + for (i = 0; i < total_packets; i++) + { + report_queue_push(state); + } + + //copy packet data + struct queued_report * queue_item = state->sys.queue; + + for (i = 0; i < total_packets; i++) + { + rpt = &queue_item->rpt; + int packet_size = (i == total_packets - 1) ? (size - i * 0x10) : 0x10; + report_format_mem_resp(state, rpt, packet_size, 0x0, offset + i*0x10, &buffer[i*0x10], false); + queue_item = queue_item->next; + } + + free(buffer); +} + +void write_eeprom(struct wiimote_state * state, uint32_t offset, uint8_t size, const uint8_t * buf) +{ + uint8_t *file; + struct report * rpt; + + offset = offset & 0xFFFF; + + //addresses greater than 0x16FF cannot be read or written + if (offset + size > 0x16FF) + { + rpt = report_queue_push(state); + report_format_mem_resp(state, rpt, 0x10, 0x8, offset, NULL, false); + return; + } + + //fseek + file = &eeprom_bin[0 + offset]; + + //fwrite + memcpy(file, buf, size); + report_queue_push_ack(state, 0x16, 0x00); +} + +void read_register(struct wiimote_state *state, uint32_t offset, uint16_t size) +{ + uint8_t * buffer; + struct report * rpt; + int i; + bool encrypt = false; + + switch ((offset >> 16) & 0xfe) //select register, ignore lsb + { + case 0xa2: //speaker + buffer = state->sys.register_a2 + (offset & 0xff); + break; + case 0xa4: //extension + if (state->sys.wmp_state == 1) + { + //i guess this isn't needed after all + //^^this is an old comment, so is this needed or not? + if (((offset & 0xff) == 0xf6) || ((offset & 0xff) == 0xf7)) + { + tries += 1; + printf("%d \n", tries); + if (tries == 5) + { + state->sys.register_a6[0xf7] = 0x0e; + } + } + + buffer = state->sys.register_a6 + (offset & 0xff); + } + else + { + buffer = state->sys.register_a4 + (offset & 0xff); + } + + if (state->sys.extension_encrypted) + { + encrypt = true; + } + + break; + case 0xa6: //motionplus + if (state->sys.wmp_state == 1) + { + rpt = report_queue_push(state); + report_format_mem_resp(state, rpt, 0x10, 0x7, offset, NULL, false); + return; + } + buffer = state->sys.register_a6 + (offset & 0xff); + break; + case 0xb0: //ir camera + buffer = state->sys.register_b0 + (offset & 0xff); + break; + default: //??? + break; + } + + //equivalent to ceil(size / 0x10) + int total_packets = (size + 0x10 - 1) / 0x10; + + //allocate all the needed reports + for (i = 0; i < total_packets; i++) + { + report_queue_push(state); + } + + struct queued_report * queue_item = state->sys.queue; + + for (i = 0; i < total_packets; i++) + { + rpt = &queue_item->rpt; + int packet_size = (i == total_packets - 1) ? (size - i * 0x10) : 0x10; + report_format_mem_resp(state, rpt, packet_size, 0x0, offset + i*0x10, &buffer[i*0x10], encrypt); + queue_item = queue_item->next; + } +} + +void write_register(struct wiimote_state *state, uint32_t offset, uint8_t size, const uint8_t * buf) +{ + uint8_t * reg; + int result = 0x00; + + switch ((offset >> 16) & 0xfe) //select register, ignore lsb + { + case 0xa2: //speaker + reg = state->sys.register_a2; + memcpy(reg + (offset & 0xff), buf, size); + break; + case 0xa4: //extension + reg = (state->sys.wmp_state == 1) ? state->sys.register_a6 : state->sys.register_a4; + + memcpy(reg + (offset & 0xff), buf, size); + + //TODO: double check what this does, the buf location it's looking for + if (((offset & 0xff) == 0xf0) && (buf[0] == 0x55) && (state->sys.wmp_state == 1)) //deactivate wmp + { + state->sys.wmp_state = 3; + + init_extension(state); + + report_queue_push_ack(state, 0x16, 0x00); + state->sys.extension_connected = 0; + report_queue_push_status(state); + state->sys.extension_connected = 1; + report_queue_push_status(state); + return; + } + else if (((offset & 0xff) == 0xfe) && (buf[0] == 0x00) && (state->sys.wmp_state == 1)) //also deactivate wmp? + { + state->sys.wmp_state = 0; + + init_extension(state); + + report_queue_push_ack(state, 0x16, 0x00); + state->sys.extension_connected = 0; + report_queue_push_status(state); + state->sys.extension_connected = 1; + report_queue_push_status(state); + return; + } + else if ((offset & 0xff) == 0x4c) //last part of encryption code + { + ext_generate_tables(&state->sys.extension_crypto_state, ®[0x40]); + state->sys.extension_encrypted = 1; + } + else if ((offset & 0xff) == 0xf0) + { + if (buf[0] == 0xaa) + { + state->sys.extension_encrypted = 1; + } + else if (buf[0] == 0x55) + { + state->sys.extension_encrypted = 0; + } + } + else if ((offset & 0xff) == 0xf1) + { + state->sys.register_a6[0xf7] = 0x1a; + + //idk why or how, but sometimes this must be updated + state->sys.register_a6[0x50] = 0xe7; + state->sys.register_a6[0x51] = 0x98; + state->sys.register_a6[0x52] = 0x31; + state->sys.register_a6[0x53] = 0x8a; + state->sys.register_a6[0x54] = 0x18; + state->sys.register_a6[0x55] = 0x82; + state->sys.register_a6[0x56] = 0x37; + state->sys.register_a6[0x57] = 0x5e; + state->sys.register_a6[0x58] = 0x02; + state->sys.register_a6[0x59] = 0x4f; + state->sys.register_a6[0x5a] = 0x68; + state->sys.register_a6[0x5b] = 0x47; + state->sys.register_a6[0x5c] = 0x78; + state->sys.register_a6[0x5d] = 0xef; + state->sys.register_a6[0x5e] = 0xbb; + state->sys.register_a6[0x5f] = 0xd7; + + state->sys.register_a6[0x60] = 0x86; + state->sys.register_a6[0x61] = 0xc8; + state->sys.register_a6[0x62] = 0x95; + state->sys.register_a6[0x63] = 0xbd; + state->sys.register_a6[0x64] = 0x20; + state->sys.register_a6[0x65] = 0x9b; + state->sys.register_a6[0x66] = 0xeb; + state->sys.register_a6[0x67] = 0x8b; + state->sys.register_a6[0x68] = 0x79; + state->sys.register_a6[0x69] = 0x81; + state->sys.register_a6[0x6a] = 0xdc; + state->sys.register_a6[0x6b] = 0x61; + state->sys.register_a6[0x6c] = 0x13; + state->sys.register_a6[0x6d] = 0x54; + state->sys.register_a6[0x6e] = 0x79; + state->sys.register_a6[0x6f] = 0x4c; + + state->sys.register_a6[0x70] = 0xb7; + state->sys.register_a6[0x71] = 0x26; + state->sys.register_a6[0x72] = 0x82; + state->sys.register_a6[0x73] = 0x17; + state->sys.register_a6[0x74] = 0xe8; + state->sys.register_a6[0x75] = 0x0f; + state->sys.register_a6[0x76] = 0xa9; + state->sys.register_a6[0x77] = 0xb5; + state->sys.register_a6[0x78] = 0x45; + state->sys.register_a6[0x79] = 0xa0; + state->sys.register_a6[0x7a] = 0x38; + state->sys.register_a6[0x7b] = 0x8e; + state->sys.register_a6[0x7c] = 0x9e; + state->sys.register_a6[0x7d] = 0x86; + state->sys.register_a6[0x7e] = 0x72; + state->sys.register_a6[0x7f] = 0x55; + + state->sys.register_a6[0x80] = 0x3d; + state->sys.register_a6[0x81] = 0x46; + state->sys.register_a6[0x82] = 0x2e; + state->sys.register_a6[0x83] = 0x3e; + state->sys.register_a6[0x84] = 0x10; + state->sys.register_a6[0x85] = 0x1f; + state->sys.register_a6[0x86] = 0x8e; + state->sys.register_a6[0x87] = 0x0c; + state->sys.register_a6[0x88] = 0xf4; + state->sys.register_a6[0x89] = 0x04; + state->sys.register_a6[0x8a] = 0x89; + state->sys.register_a6[0x8b] = 0x4c; + state->sys.register_a6[0x8c] = 0xca; + state->sys.register_a6[0x8d] = 0x3e; + state->sys.register_a6[0x8e] = 0x9f; + state->sys.register_a6[0x8f] = 0x36; + } + + break; + case 0xa6: //motionplus + reg = state->sys.register_a6; + memcpy(reg + (offset & 0xff), buf, size); + + if (((offset & 0xff) == 0xfe) && ((buf[0] >> 2) & 0x1)) //activate wmp + { + state->sys.wmp_state = 1; + state->sys.extension_report_type = (buf[0] & 0x7); + printf("activate wmp\n"); + + init_extension(state); + + report_queue_push_ack(state, 0x16, 0x00); + state->sys.extension_connected = 0; + report_queue_push_status(state); + state->sys.extension_connected = 1; + report_queue_push_status(state); + return; + } + + break; + case 0xb0: //ir camera + reg = state->sys.register_b0; + memcpy(reg + (offset & 0xff), buf, size); + break; + default: //??? + break; + } + + report_queue_push_ack(state, 0x16, result); +} + +void reset_ir_object(struct wiimote_ir_object * ir_object) +{ + memset(ir_object, 0xff, sizeof(struct wiimote_ir_object)); +} + +void reset_input_ir(struct wiimote_ir_object ir_object[4]) +{ + memset(ir_object, 0xff, sizeof(struct wiimote_ir_object) * 4); +} + +void reset_input_nunchuk(struct wiimote_nunchuk * nunchuk) +{ + memset(nunchuk, 0, sizeof(struct wiimote_nunchuk)); + + nunchuk->x = 128; + nunchuk->y = 128; + nunchuk->accel_x = 512; + nunchuk->accel_y = 512; + nunchuk->accel_z = 760; +} + +void reset_input_classic(struct wiimote_classic * classic) +{ + memset(classic, 0, sizeof(struct wiimote_classic)); + + classic->ls_x = 32; + classic->ls_y = 32; + classic->rs_x = 15; + classic->rs_y = 15; +} + +void reset_input_motionplus(struct wiimote_motionplus * motionplus) +{ + memset(motionplus, 0, sizeof(struct wiimote_motionplus)); + + motionplus->yaw_down = 0x1F7F; + motionplus->roll_left = 0x1F7F; + motionplus->pitch_left = 0x1F7F; + motionplus->yaw_slow = 1; + motionplus->roll_slow = 1; + motionplus->pitch_slow = 1; +} + +void init_extension(struct wiimote_state * state) +{ + if (state->sys.connected_extension_type == NoExtension) + { + memset(state->sys.register_a4, 0xff, sizeof(state->sys.register_a4)); + } + else + { + memset(state->sys.register_a4, 0, sizeof(state->sys.register_a4)); + } + + if (state->sys.wmp_state == 1) + { + //register_a6[0xfa] = 0x00; + //register_a6[0xfb] = 0x00; + //register_a6[0xfc] = 0xa4; + //register_a6[0xfd] = 0x20; + //this might not be needed now that memory is proper + //register_a6[0xfe] = state->sys.extension_report_type; + //register_a6[0xff] = 0x05; + state->sys.register_a6[0xfc] = 0xa4; + + state->sys.extension_encrypted = 0; + + //random guess, pulled from wiimote, not sure what this is for + state->sys.register_a6[0xf0] = 0x55; + state->sys.register_a6[0xf1] = 0xff; + state->sys.register_a6[0xf2] = 0xff; + state->sys.register_a6[0xf3] = 0xff; + state->sys.register_a6[0xf4] = 0xff; + state->sys.register_a6[0xf5] = 0xff; + state->sys.register_a6[0xf6] = 0x00; + + //a4 40 post init + // state->sys.register_a6[0x40] = 0x81; + // state->sys.register_a6[0x41] = 0x80; + // state->sys.register_a6[0x42] = 0x80; + // state->sys.register_a6[0x43] = 0x28; + // state->sys.register_a6[0x44] = 0xb4; + // state->sys.register_a6[0x45] = 0xb3; + // state->sys.register_a6[0x46] = 0xb3; + // state->sys.register_a6[0x47] = 0x26; + // state->sys.register_a6[0x48] = 0xe3; + // state->sys.register_a6[0x49] = 0x22; + // state->sys.register_a6[0x4a] = 0x7a; + // state->sys.register_a6[0x4b] = 0xd8; + // state->sys.register_a6[0x4c] = 0x1b; + // state->sys.register_a6[0x4d] = 0x81; + // state->sys.register_a6[0x4e] = 0x31; + // state->sys.register_a6[0x4f] = 0x86; + + state->sys.register_a6[0x20] = 0x7c; + state->sys.register_a6[0x21] = 0x97; + state->sys.register_a6[0x22] = 0x7f; + state->sys.register_a6[0x23] = 0x0a; + state->sys.register_a6[0x24] = 0x7c; + state->sys.register_a6[0x25] = 0xa8; + state->sys.register_a6[0x26] = 0x33; + state->sys.register_a6[0x27] = 0xb7; + state->sys.register_a6[0x28] = 0xcc; + state->sys.register_a6[0x29] = 0x12; + state->sys.register_a6[0x2a] = 0x33; + state->sys.register_a6[0x2b] = 0x08; + state->sys.register_a6[0x2c] = 0xc8; + state->sys.register_a6[0x2d] = 0x01; + state->sys.register_a6[0x2e] = 0x72; + state->sys.register_a6[0x2f] = 0xd4; + + state->sys.register_a6[0x30] = 0x7c; + state->sys.register_a6[0x31] = 0x53; + state->sys.register_a6[0x32] = 0x87; + state->sys.register_a6[0x33] = 0x58; + state->sys.register_a6[0x34] = 0x7c; + state->sys.register_a6[0x35] = 0x9f; + state->sys.register_a6[0x36] = 0x36; + state->sys.register_a6[0x37] = 0xb2; + state->sys.register_a6[0x38] = 0xc9; + state->sys.register_a6[0x39] = 0x34; + state->sys.register_a6[0x3a] = 0x35; + state->sys.register_a6[0x3b] = 0xf8; + state->sys.register_a6[0x3c] = 0x2d; + state->sys.register_a6[0x3d] = 0x60; + state->sys.register_a6[0x3e] = 0xd7; + state->sys.register_a6[0x3f] = 0xd5; + + //not sure block, this may not be needed + state->sys.register_a6[0x50] = 0x15; + state->sys.register_a6[0x51] = 0x6d; + state->sys.register_a6[0x52] = 0xe0; + state->sys.register_a6[0x53] = 0x23; + state->sys.register_a6[0x54] = 0x20; + state->sys.register_a6[0x55] = 0x79; + state->sys.register_a6[0x56] = 0xd3; + state->sys.register_a6[0x57] = 0x73; + state->sys.register_a6[0x58] = 0x01; + state->sys.register_a6[0x59] = 0xa9; + state->sys.register_a6[0x5a] = 0xf0; + state->sys.register_a6[0x5b] = 0x25; + state->sys.register_a6[0x5c] = 0xb0; + state->sys.register_a6[0x5d] = 0xbc; + state->sys.register_a6[0x5e] = 0xff; + state->sys.register_a6[0x5f] = 0xe1; + + state->sys.register_a6[0x60] = 0xd8; + state->sys.register_a6[0x61] = 0x3f; + state->sys.register_a6[0x62] = 0x82; + state->sys.register_a6[0x63] = 0x52; + state->sys.register_a6[0x64] = 0x75; + state->sys.register_a6[0x65] = 0x99; + state->sys.register_a6[0x66] = 0xbe; + state->sys.register_a6[0x67] = 0xdb; + state->sys.register_a6[0x68] = 0xcb; + state->sys.register_a6[0x69] = 0x61; + state->sys.register_a6[0x6a] = 0x60; + state->sys.register_a6[0x6b] = 0x0f; + state->sys.register_a6[0x6c] = 0x35; + state->sys.register_a6[0x6d] = 0xbd; + state->sys.register_a6[0x6e] = 0xd4; + state->sys.register_a6[0x6f] = 0x4d; + + state->sys.register_a6[0x70] = 0x5c; + state->sys.register_a6[0x71] = 0x9f; + state->sys.register_a6[0x72] = 0x5d; + state->sys.register_a6[0x73] = 0x81; + state->sys.register_a6[0x74] = 0x71; + state->sys.register_a6[0x75] = 0xde; + state->sys.register_a6[0x76] = 0x22; + state->sys.register_a6[0x77] = 0xe6; + state->sys.register_a6[0x78] = 0xb9; + state->sys.register_a6[0x79] = 0x23; + state->sys.register_a6[0x7a] = 0xa4; + state->sys.register_a6[0x7b] = 0x58; + state->sys.register_a6[0x7c] = 0xb7; + state->sys.register_a6[0x7d] = 0x62; + state->sys.register_a6[0x7e] = 0x33; + state->sys.register_a6[0x7f] = 0xa4; + + state->sys.register_a6[0x80] = 0xcd; + state->sys.register_a6[0x81] = 0x8b; + state->sys.register_a6[0x82] = 0x3a; + state->sys.register_a6[0x83] = 0xfe; + state->sys.register_a6[0x84] = 0x98; + state->sys.register_a6[0x85] = 0xf0; + state->sys.register_a6[0x86] = 0xd9; + state->sys.register_a6[0x87] = 0x57; + state->sys.register_a6[0x88] = 0x0c; + state->sys.register_a6[0x89] = 0xe8; + state->sys.register_a6[0x8a] = 0x27; + state->sys.register_a6[0x8b] = 0x51; + state->sys.register_a6[0x8c] = 0xb6; + state->sys.register_a6[0x8d] = 0xea; + state->sys.register_a6[0x8e] = 0xe5; + state->sys.register_a6[0x8f] = 0x78; + + + //init progress byte, set it to done + state->sys.register_a6[0xf7] = 0x0c; + state->sys.register_a6[0xf8] = 0x00; + state->sys.register_a6[0xf9] = 0x00; + } + else + { + //state->sys.register_a6[0xf7] = 0x0c; + + state->sys.register_a6[0xf0] = 0x55; + state->sys.register_a6[0xf1] = 0xff; + state->sys.register_a6[0xf2] = 0xff; + state->sys.register_a6[0xf3] = 0xff; + state->sys.register_a6[0xf4] = 0xff; + state->sys.register_a6[0xf5] = 0xff; + state->sys.register_a6[0xf6] = 0xff; + state->sys.register_a6[0xf7] = 0x02; + state->sys.register_a6[0xf8] = 0xff; + state->sys.register_a6[0xf9] = 0xff; + state->sys.register_a6[0xfa] = 0x01; + state->sys.register_a6[0xfb] = 0x00; + state->sys.register_a6[0xfc] = 0xa6; + state->sys.register_a6[0xfd] = 0x20; + state->sys.register_a6[0xfe] = 0x00; + state->sys.register_a6[0xff] = 0x05; + + if (state->sys.connected_extension_type == NoExtension) + { + return; + } + + memset(&state->sys.register_a4[0xf0], 0x0, 0x10); + + state->sys.register_a4[0xf0] = 0x55; + state->sys.register_a4[0xfc] = 0xa4; + state->sys.register_a4[0xfd] = 0x20; + + switch (state->sys.connected_extension_type) + { + default: + case Nunchuk: + state->sys.register_a4[0xfe] = 0x00; + state->sys.register_a4[0xff] = 0x00; + memcpy(&state->sys.register_a4[0x20], nunchuk_calibration, 0x10); + memcpy(&state->sys.register_a4[0x30], nunchuk_calibration, 0x10); + break; + case Classic: + state->sys.register_a4[0xfe] = 0x01; + state->sys.register_a4[0xff] = 0x01; + memcpy(&state->sys.register_a4[0x20], classic_calibration, 0x10); + memcpy(&state->sys.register_a4[0x30], classic_calibration, 0x10); + break; + case BalanceBoard: + state->sys.register_a4[0xfe] = 0x04; + state->sys.register_a4[0xff] = 0x02; + break; + } + + state->sys.extension_report_type = state->sys.register_a4[0xfe]; + state->sys.extension_type = state->sys.register_a4[0xff]; + } +} + +void wiimote_destroy(struct wiimote_state *state) +{ + while (state->sys.queue != NULL) + { + struct queued_report * rpt = state->sys.queue; + state->sys.queue = rpt->next; + free(rpt); + } +} + +void wiimote_init(struct wiimote_state *state) +{ + + /*INIT EEPROM*/ + //eeprom allocate memory + eeprom_bin = (uint8_t*)malloc(TOTAL_SIZE_EEPROM * sizeof(uint8_t)); + //Copy the first data + memcpy(eeprom_bin, &eeprom_bin_1[0], sizeof(eeprom_bin_1)); + //Copy the last data + memcpy(&eeprom_bin[LAST_DATA_BEGIN_POS], &eeprom_bin_2[0], sizeof(eeprom_bin_2)); + /*INIT EEPROM*/ + + memset(state, 0, sizeof(struct wiimote_state)); + + //flat + state->usr.accel_x = 0x82 << 2; + state->usr.accel_y = 0x82 << 2; + state->usr.accel_z = 0x9f << 2; + + reset_input_ir(state->usr.ir_object); + reset_input_nunchuk(&state->usr.nunchuk); + reset_input_classic(&state->usr.classic); + reset_input_motionplus(&state->usr.motionplus); + + state->usr.connected_extension_type = NoExtension; + + wiimote_reset(state); + + //power on report + struct report * rpt = report_queue_push(state); + rpt->len = 4; + rpt->data.io = 0xa1; + rpt->data.type = 0x30; +} + +void wiimote_reset(struct wiimote_state *state) +{ + memset(&state->sys, 0, sizeof(struct wiimote_state_sys)); + + state->sys.reporting_mode = 0x30; + state->sys.battery_level = 0xff; + + state->sys.connected_extension_type = NoExtension; + + init_extension(state); +} diff --git a/Firmware/RP2040/src/Wii/wiimote-lib/wiimote.h b/Firmware/RP2040/src/Wii/wiimote-lib/wiimote.h new file mode 100644 index 00000000..18b49895 --- /dev/null +++ b/Firmware/RP2040/src/Wii/wiimote-lib/wiimote.h @@ -0,0 +1,229 @@ +#ifndef WIIMOTE_H +#define WIIMOTE_H + +#include +#include +#include "wm_crypto.h" + +enum wiimote_connected_extension_type +{ + Nunchuk = 0x0, + Classic = 0x1, + BalanceBoard = 0x2, + NoExtension = 0xff +}; + +struct wiimote_ir_object +{ + uint16_t x; + uint16_t y; + uint8_t size; + uint8_t xmin; + uint8_t ymin; + uint8_t xmax; + uint8_t ymax; + uint8_t intensity; +}; + +struct wiimote_nunchuk +{ + uint16_t accel_x; + uint16_t accel_y; + uint16_t accel_z; + uint8_t x; + uint8_t y; + bool c; + bool z; +}; + +struct wiimote_classic +{ + bool a; + bool b; + bool x; + bool y; + bool minus; + bool plus; + bool rtrigger; + bool ltrigger; + bool home; + bool rz; + bool lz; + bool up; + bool down; + bool left; + bool right; + uint8_t ls_x; + uint8_t ls_y; + uint8_t rs_x; + uint8_t rs_y; + uint8_t lt; + uint8_t rt; + +}; + +struct wiimote_motionplus +{ + uint16_t yaw_down; + uint16_t roll_left; + uint16_t pitch_left; + bool yaw_slow; + bool roll_slow; + bool pitch_slow; +}; + +struct wiimote_state_usr +{ + bool a; + bool b; + bool minus; + bool plus; + bool home; + bool one; + bool two; + bool up; + bool down; + bool left; + bool right; + + //special buttons + bool sync; + bool power; + + //accelerometer (10 bit range) + //0 acceleration is approximately 0x200 + uint16_t accel_x; + uint16_t accel_y; + uint16_t accel_z; + + struct wiimote_ir_object ir_object[4]; + + enum wiimote_connected_extension_type connected_extension_type; + + struct wiimote_nunchuk nunchuk; + struct wiimote_classic classic; + struct wiimote_motionplus motionplus; +}; + +void reset_ir_object(struct wiimote_ir_object * object); +void reset_input_ir(struct wiimote_ir_object ir_object[4]); +void reset_input_nunchuk(struct wiimote_nunchuk * nunchuk); +void reset_input_classic(struct wiimote_classic * classic); +void reset_input_motionplus(struct wiimote_motionplus * motionplus); + +struct wiimote_state_sys +{ + bool led_1; + bool led_2; + bool led_3; + bool led_4; + + bool rumble; + + bool ircam_enabled; + bool speaker_enabled; + + uint8_t battery_level; + bool low_battery; + + int extension_hotplug_timer; + bool extension_connected; + enum wiimote_connected_extension_type connected_extension_type; + + struct ext_crypto_state extension_crypto_state; + bool extension_report; + bool extension_encrypted; + uint8_t extension_report_type; + uint8_t extension_type; + uint8_t wmp_state; //0 inactive, 1 active, 2 deactivated + + uint8_t reporting_mode; + bool reporting_continuous; + bool report_changed; + + struct queued_report * queue; + struct queued_report * queue_end; + + uint8_t register_a2[10]; //speaker + uint8_t register_a4[256]; //extension + uint8_t register_a6[256]; //wii motion plus + uint8_t register_b0[52]; //ir camera +}; + +struct wiimote_state +{ + struct wiimote_state_sys sys; + struct wiimote_state_usr usr; +}; + +//new adding -> only buttons struture for wiimote +struct wiimote_buttons{ + + bool a; + bool b; + bool minus; + bool plus; + bool home; + bool one; + bool two; + bool up; + bool down; + bool left; + bool right; + + //special buttons + bool sync; + bool power; + + //accelerometer (10 bit range) + //0 acceleration is approximately 0x200 + uint16_t accel_x; + uint16_t accel_y; + uint16_t accel_z; + + //ir + int8_t ir_x; + int8_t ir_y; +}; + +struct wii_console_info{ + uint8_t wii_addr_saved; + uint8_t wii_addr[6]; +}; + +//new adding -> wiimote + classic +typedef struct{ + struct wiimote_buttons wiimote; + struct wiimote_nunchuk nunchuk; + struct wiimote_classic classic; + uint8_t switch_mode; + uint8_t mode; + uint8_t reset_ir; + uint8_t fake_motion; + uint8_t center_accel; + uint8_t sideway; + struct wii_console_info console_info; +}WiimoteReport; + +enum MODE_WIIMOTE{ + NO_EXTENSION = 0xFF, + WIIMOTE_AND_NUNCHUCK = 0x00, + CLASSIC_CONTROLLER = 0x01 +}; + +void wiimote_init(struct wiimote_state *state); +void wiimote_destroy(struct wiimote_state *state); + +void wiimote_reset(struct wiimote_state *state); + +int process_report(struct wiimote_state *state, const uint8_t *buf, int len); +int generate_report(struct wiimote_state * state, uint8_t * buf); + +void read_eeprom(struct wiimote_state * state, uint32_t offset, uint16_t size); +void write_eeprom(struct wiimote_state * state, uint32_t offset, uint8_t size, const uint8_t * buf); +void read_register(struct wiimote_state *state, uint32_t offset, uint16_t size); +void write_register(struct wiimote_state *state, uint32_t offset, uint8_t size, const uint8_t * buf); + +void init_extension(struct wiimote_state *state); + +#endif //WIIMOTE_H diff --git a/Firmware/RP2040/src/Wii/wiimote-lib/wiimote_btstack.c b/Firmware/RP2040/src/Wii/wiimote-lib/wiimote_btstack.c new file mode 100644 index 00000000..9283a5b9 --- /dev/null +++ b/Firmware/RP2040/src/Wii/wiimote-lib/wiimote_btstack.c @@ -0,0 +1,559 @@ +#include "wiimote_btstack.h" +#include +#include +#include "btstack.h" + +#include "sdp_consts.h" +#include "wiimote.h" +#include "motion.h" +#include +#include "pico/flash.h" +#include "utils.h" + +#define SDP_RESPONSE_BUFFER_SIZE (HCI_ACL_PAYLOAD_SIZE-L2CAP_HEADER_SIZE) + +// TAG to store remote device address in TLV +#define TLV_TAG_HIDR ((((uint32_t) 'H') << 24 ) | (((uint32_t) 'I') << 16) | (((uint32_t) 'D') << 8) | 'R') + +// used to store remote device in TLV +static const btstack_tlv_t * btstack_tlv_singleton_impl; +static void * btstack_tlv_singleton_context; +bd_addr_t wii_baddr; +static uint8_t hid_service_buffer[700]; +static uint8_t pnp_service_buffer[200]; +static const char hid_device_name[] = "Nintendo RVL-CNT-01"; +static uint16_t hid_cid; +static struct wiimote_state wiimote; +static uint8_t buf[256]; +static int len; +static btstack_timer_source_t loop_wii; +static btstack_timer_source_t led_state; +void (*callback_led_func[2])(); +int state = -1; +//convert_data +static const double pointer_margin = 0.5; +float pointer_x = 0.5; +float pointer_y = 0.5; +volatile WiimoteReport *input_report; + +static void l2cap_sdp_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size); +static void input_update_wiimote(); + + +/** + * The default command format always writes 2 IACs, but we only want one + * @param num_current_iac must be 1 + * @param iac_lap + */ +static const hci_cmd_t hci_write_current_iac_lap_one_iac = { + HCI_OPCODE_HCI_WRITE_CURRENT_IAC_LAP_TWO_IACS, "13" +}; + +static btstack_packet_callback_registration_t hci_event_callback_registration; + +static uint16_t sdp_server_l2cap_cid; +static uint16_t sdp_server_response_size; +static uint8_t sdp_response_buffer[SDP_RESPONSE_BUFFER_SIZE]; + + +static void send_data(){ + input_update_wiimote(); + process_report(&wiimote, buf, len); + len = generate_report(&wiimote, buf); + if (len > 0){ + hid_device_send_interrupt_message(hid_cid, &buf[0], len); + } +} + + +static void get_data_wii(uint16_t cid, hid_report_type_t report_type, uint16_t report_id, int report_size, uint8_t * report){ + + UNUSED(cid); + + if(report_type == HID_REPORT_TYPE_OUTPUT){ + buf[0] = 0xA2; + buf[1] = report_id; + memcpy(&buf[2], report, report_size); + len = report_size + 2; + } + +} + +void task_wiimote(struct btstack_timer_source *ts){ + + hid_device_request_can_send_now_event(hid_cid); + + // Restart timer + btstack_run_loop_set_timer(ts, 1); + btstack_run_loop_add_timer(ts); + +} + +static void wiimote_emulator_reset(void) +{ + printf("Resetting...\n"); + + if (sdp_server_l2cap_cid) { + l2cap_disconnect(sdp_server_l2cap_cid); + sdp_server_l2cap_cid = 0; + } + + // Reset buffer data + memset(buf, 0, sizeof(buf)); + len = 0; +} + + +static void sdp_respond(void) +{ + if (!sdp_server_response_size || !sdp_server_l2cap_cid) { + return; + } + + // update state before sending packet + uint16_t size = sdp_server_response_size; + sdp_server_response_size = 0; + l2cap_send(sdp_server_l2cap_cid, sdp_response_buffer, size); + +} + + + +static void hci_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t* packet, uint16_t size) +{ + UNUSED(channel); + UNUSED(size); + + static int set_iac_lap = 0; + + uint8_t status; + + // We only care about HCI packets + if (packet_type != HCI_EVENT_PACKET) { + return; + } + + // printf("hci_packet_handler %x\n", hci_event_packet_get_type(packet)); + + switch(hci_event_packet_get_type(packet)) { + case BTSTACK_EVENT_STATE: + // Wait for the stack to enter the initializing state, before setting the IAC LAP + if (btstack_event_state_get_state(packet) == HCI_STATE_WORKING) { + set_iac_lap = 1; + btstack_tlv_get_instance(&btstack_tlv_singleton_impl, &btstack_tlv_singleton_context); + // Load last Wii address from TLV for auto-connect (like a real Wiimote) + if (btstack_tlv_singleton_impl && input_report) { + int len = btstack_tlv_singleton_impl->get_tag(btstack_tlv_singleton_context, TLV_TAG_HIDR, + (uint8_t *)input_report->console_info.wii_addr, 6); + if (len == 6) { + input_report->console_info.wii_addr_saved = 1; + printf("Auto-connect: loaded last Wii address from flash\n"); + } + } + } + break; + case HCI_EVENT_COMMAND_COMPLETE: + // Check IAC LAP result + if (hci_event_command_complete_get_command_opcode(packet) == hci_write_current_iac_lap_one_iac.opcode) { + uint8_t status = hci_event_command_complete_get_return_parameters(packet)[0]; + printf("Set IAC LAP: %s\n", (status != ERROR_CODE_SUCCESS) ? "Failed" : "OK"); + memcpy(wii_baddr, (const uint8_t *)input_report->console_info.wii_addr, 6); + // Only initiate connection if we have a saved Wii address (non-zero). Otherwise stay discoverable and let Wii/PC connect to us. + int addr_valid = (wii_baddr[0] | wii_baddr[1] | wii_baddr[2] | wii_baddr[3] | wii_baddr[4] | wii_baddr[5]) != 0; + if (addr_valid) { + hid_device_connect(wii_baddr, &hid_cid); + } else { + printf("No Wii addr saved; discoverable as Nintendo RVL-CNT-01 (press Sync on Wii or pair from PC)\n"); + } + } + break; + case HCI_EVENT_PIN_CODE_REQUEST: + // inform about pin code request + uint8_t pin_code[6]; + hci_event_pin_code_request_get_bd_addr(packet, wii_baddr); + reverse_bd_addr(wii_baddr, pin_code); + //printf_hexdump(pin_code, sizeof(pin_code)); + gap_pin_code_response_binary(wii_baddr, pin_code, sizeof(pin_code)); + break; + case L2CAP_EVENT_CHANNEL_OPENED: { + uint16_t cid = l2cap_event_channel_opened_get_local_cid(packet); + uint8_t status = l2cap_event_channel_opened_get_status(packet); + + if (status == ERROR_CODE_SUCCESS) { + printf("L2CAP channel opened (cid %d)\n", (int)cid); + } else { + if (status == L2CAP_CONNECTION_RESPONSE_RESULT_REFUSED_SECURITY) { + // Assume remote has forgotten link key, delete it and try again + printf("Dropping link key due to L2CAP_CONNECTION_RESPONSE_RESULT_REFUSED_SECURITY\n"); + bd_addr_t addr; + l2cap_event_channel_opened_get_address(packet, addr); + gap_drop_link_key_for_bd_addr(addr); + gap_disconnect(l2cap_event_channel_opened_get_handle(packet)); + } else { + printf("L2CAP connection failed (cid %d, status 0x%02x)\n", (int)cid, (int)status); + } + } + } + break; + case HCI_EVENT_HID_META: + switch (hci_event_hid_meta_get_subevent_code(packet)){ + case HID_SUBEVENT_CONNECTION_OPENED: + status = hid_subevent_connection_opened_get_status(packet); + if (status != ERROR_CODE_SUCCESS) { + // outgoing connection failed + printf("Connection failed, status 0x%x\n", status); + hid_cid = 0; + // Authentication Failure -> Need to Sync again with the console + if(status == 0x66){ + input_report->console_info.wii_addr_saved = 0; + if (btstack_tlv_singleton_impl) + btstack_tlv_singleton_impl->delete_tag(btstack_tlv_singleton_context, TLV_TAG_HIDR); + printf("Fail to connect -> Need to Sync again\n"); + } + return; + } + hid_cid = hid_subevent_connection_opened_get_hid_cid(packet); + /* Always get remote address from event (for incoming connection we didn't have it yet) */ + hid_subevent_connection_opened_get_bd_addr(packet, wii_baddr); + printf("Connected to "); + printf_hexdump(wii_baddr, sizeof(wii_baddr)); + /* Update console_info and TLV so next boot we auto-connect to this Wii */ + memcpy((uint8_t *)input_report->console_info.wii_addr, wii_baddr, 6); + input_report->console_info.wii_addr_saved = 1; + flash_safe_execute(save_wii_addr, wii_baddr, 100); + if (btstack_tlv_singleton_impl) + btstack_tlv_singleton_impl->store_tag(btstack_tlv_singleton_context, TLV_TAG_HIDR, (const uint8_t *) &wii_baddr, sizeof(bd_addr_t)); + printf("Wii addr saved for auto-connect\n"); + // Remove timer led + btstack_run_loop_remove_timer(&led_state); + // Set the led on + (*callback_led_func[1])(); + // Run wiimote process + btstack_run_loop_set_timer(&loop_wii, 1); + btstack_run_loop_add_timer(&loop_wii); + break; + case HID_SUBEVENT_CONNECTION_CLOSED: + printf("HID Disconnected\n"); + hid_cid = 0; + // Stop wiimote process + btstack_run_loop_remove_timer(&loop_wii); + // Start led timer + btstack_run_loop_set_timer(&led_state, 100); + btstack_run_loop_add_timer(&led_state); + hid_device_connect(wii_baddr, &hid_cid); + break; + case HID_SUBEVENT_CAN_SEND_NOW: + send_data(); + break; + default: + break; + } + break; + default: + break; + } + + // Write IAC LAP when ready + if (set_iac_lap && hci_can_send_command_packet_now()) { + set_iac_lap = 0; + // Limited Inquiry Access Code + hci_send_cmd(&hci_write_current_iac_lap_one_iac, 1, GAP_IAC_LIMITED_INQUIRY); + + // Set device discoverable now, to emit a write scan enable after IAC LAP is set + gap_discoverable_control(1); + + // It's weird but this fix the lag on sticks + gap_set_default_link_policy_settings(LM_LINK_POLICY_ENABLE_SNIFF_MODE); + + // HID Device + hid_device_init(1, sizeof(wiimote_report_descriptor),wiimote_report_descriptor); + + // HID Device report callback + hid_device_register_report_data_callback(&get_data_wii); + } +} + + +static void l2cap_sdp_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size) +{ + // printf("sdp_packet_handler %x\n", packet_type); + + switch (packet_type) { + case L2CAP_DATA_PACKET: { + + uint16_t transaction_id = big_endian_read_16(packet, 1); + + //printf("SDP Request: type %u, transaction id %u, len %u, mtu %u\n", pdu_id, transaction_id, param_len, remote_mtu); + + //use the transaction id to determine the response to send + state = transaction_id; + + if (state >= 0){ + switch (state) { + case 0: + sdp_server_response_size = sizeof(resp0); + memcpy(sdp_response_buffer, resp0, sizeof(resp0)); + break; + case 1: + sdp_server_response_size = sizeof(resp1); + memcpy(sdp_response_buffer, resp1, sizeof(resp1)); + break; + case 2: + sdp_server_response_size = sizeof(resp2); + memcpy(sdp_response_buffer, resp2, sizeof(resp2)); + break; + case 3: + sdp_server_response_size = sizeof(resp3); + memcpy(sdp_response_buffer, resp3, sizeof(resp3)); + break; + case 4: + sdp_server_response_size = sizeof(resp4); + memcpy(sdp_response_buffer, resp4, sizeof(resp4)); + break; + default: + break; + } + } + + if (!sdp_server_response_size) { + break; + } + + l2cap_request_can_send_now_event(sdp_server_l2cap_cid); + break; + } + + case HCI_EVENT_PACKET: + switch (hci_event_packet_get_type(packet)) { + case L2CAP_EVENT_INCOMING_CONNECTION: + if (sdp_server_l2cap_cid) { + // Just reject other incoming connections + l2cap_decline_connection(channel); + break; + } + + // Accept connection + sdp_server_l2cap_cid = channel; + sdp_server_response_size = 0; + l2cap_accept_connection(sdp_server_l2cap_cid); + break; + case L2CAP_EVENT_CHANNEL_OPENED: + // Reset if open failed + if (packet[2]) { + wiimote_emulator_reset(); + } + break; + case L2CAP_EVENT_CAN_SEND_NOW: + sdp_respond(); + break; + case L2CAP_EVENT_CHANNEL_CLOSED: + // Reset if channel closed + if (channel == sdp_server_l2cap_cid) { + wiimote_emulator_reset(); + } + break; + default: + break; + } + break; + default: + break; + } +} + + +static void led_handler(struct btstack_timer_source *ts) +{ + // Invert the led + static uint8_t led_on = 0; + led_on = !led_on; + (*callback_led_func[led_on])(); + + // Restart timer + btstack_run_loop_set_timer(ts, 100); + btstack_run_loop_add_timer(ts); +} + + +void wiimote_emulator(void *report){ + + printf("Init Wiimote Emulator\n"); + + // Data from gamepad + input_report = report; + + // Disable SSP + gap_ssp_set_enable(0); + + // Set device connectable + gap_connectable_control(1); + + // Set device non discoverable for now, we'll set this after setting IAC LAP + gap_discoverable_control(0); + + // Set device bondable + gap_set_bondable_mode(1); + + // Set local name + gap_set_local_name("Nintendo RVL-CNT-01"); + + // Set class + gap_set_class_of_device(0x002504); + + // Register HCI callback to set IAC LAP once HCI is working + // and to catch the connection handle + hci_event_callback_registration.callback = &hci_packet_handler; + hci_add_event_handler(&hci_event_callback_registration); + + // init L2CAP + l2cap_init(); + + // Register SDP service with max possible MTU + l2cap_register_service(l2cap_sdp_packet_handler, BLUETOOTH_PSM_SDP, 0xffff, LEVEL_0); + + // SDP Server + sdp_init(); + memset(hid_service_buffer, 0, sizeof(hid_service_buffer)); + + hid_sdp_record_t hid_sdp_record = { + // hid sevice subclass 2504 Gamepad?, hid counntry code 33 US + 0x2504, + 33, + 1, + 1, + 1, + 0, + 0, + 0xFFFF, + 0xFFFF, + 3200, + wiimote_report_descriptor, + sizeof(wiimote_report_descriptor), + hid_device_name + }; + + + hid_create_sdp_record(hid_service_buffer, sdp_create_service_record_handle(), &hid_sdp_record); + btstack_assert(de_get_len( hid_service_buffer) <= sizeof(hid_service_buffer)); + sdp_register_service(hid_service_buffer); + + // See https://www.bluetooth.com/specifications/assigned-numbers/company-identifiers if you don't have a USB Vendor ID and need a Bluetooth Vendor ID + // device info: BlueKitchen GmbH, product 1, version 1 + device_id_create_sdp_record(pnp_service_buffer, sdp_create_service_record_handle(), DEVICE_ID_VENDOR_ID_SOURCE_BLUETOOTH, BLUETOOTH_COMPANY_ID_BLUEKITCHEN_GMBH, 1, 1); + btstack_assert(de_get_len(pnp_service_buffer) <= sizeof(pnp_service_buffer)); + sdp_register_service(pnp_service_buffer); + + // register for HID events + hid_device_register_packet_handler(&hci_packet_handler); + + // Power on! + hci_power_control(HCI_POWER_ON); + + // Init wiimote structure + wiimote_init(&wiimote); + + //No extension for default + reset_input_ir(wiimote.usr.ir_object); + wiimote.usr.connected_extension_type = NoExtension; + + // Loop control to send report + loop_wii.process = &task_wiimote; + + btstack_run_loop_execute(); +} + + +static void input_update_wiimote(){ + + if(input_report->reset_ir){ + reset_input_classic(&wiimote.usr.classic); + reset_input_nunchuk(&wiimote.usr.nunchuk); + reset_input_ir(wiimote.usr.ir_object); + // Change the extension too + wiimote.usr.connected_extension_type = input_report->mode; + input_report->reset_ir = 0; + } + + switch(input_report->mode){ + case NO_EXTENSION:{ + float pointer_delta_x = 0, pointer_delta_y = 0; + + memcpy(&wiimote.usr, &input_report->wiimote, 14); + + pointer_delta_x += (input_report->wiimote.ir_x / 127.0) * 0.008; + pointer_delta_y += (input_report->wiimote.ir_y / 127.0) * 0.008; + + pointer_x = fmax(-pointer_margin, fmin(1.0 + pointer_margin, pointer_x + pointer_delta_x)); + pointer_y = fmax(-pointer_margin, fmin(1.0 + pointer_margin, pointer_y + pointer_delta_y)); + + set_motion_state(&wiimote, pointer_x, pointer_y); + } + break; + case WIIMOTE_AND_NUNCHUCK:{ + // Wiimote + float pointer_delta_x = 0, pointer_delta_y = 0; + memcpy(&wiimote.usr, &input_report->wiimote, 14); + + // Nunchuck + memcpy(&wiimote.usr.nunchuk, &input_report->nunchuk, sizeof(struct wiimote_nunchuk)); + + pointer_delta_x += (input_report->wiimote.ir_x / 127.0) * 0.008; + pointer_delta_y += (input_report->wiimote.ir_y / 127.0) * 0.008; + + pointer_x = fmax(-pointer_margin, fmin(1.0 + pointer_margin, pointer_x + pointer_delta_x)); + pointer_y = fmax(-pointer_margin, fmin(1.0 + pointer_margin, pointer_y + pointer_delta_y)); + + set_motion_state(&wiimote, pointer_x, pointer_y); + + if(input_report->fake_motion || input_report->center_accel){ + + if(input_report->fake_motion) { + static int step = 48; + + if(input_report->wiimote.accel_x >= 1000 || input_report->wiimote.accel_x <= 100){ + step = -step; + } + + // Start moving all directions + + int temp_value = step + input_report->wiimote.accel_x; + + input_report->wiimote.accel_x = (uint16_t)temp_value; + input_report->nunchuk.accel_x = (uint16_t)temp_value; + + input_report->wiimote.accel_y = (uint16_t)temp_value; + input_report->nunchuk.accel_y = (uint16_t)temp_value; + + input_report->wiimote.accel_z = (uint16_t)temp_value; + input_report->nunchuk.accel_z = (uint16_t)temp_value; + } + + wiimote.usr.accel_x = input_report->wiimote.accel_x; + wiimote.usr.accel_y = input_report->wiimote.accel_y; + wiimote.usr.accel_z = input_report->wiimote.accel_z; + + if(input_report->center_accel){ + input_report->center_accel = 0; + } + } + } + break; + case CLASSIC_CONTROLLER: + memcpy(&wiimote.usr.classic, &input_report->classic, sizeof(struct wiimote_classic)); + break; + default: + break; + } + +} + + +void wiimote_emulator_set_led(void (*led_on)(), void (*led_off)()){ + callback_led_func[0] = led_off; + callback_led_func[1] = led_on; + + led_state.process = &led_handler; + btstack_run_loop_set_timer(&led_state, 100); + btstack_run_loop_add_timer(&led_state); +} + diff --git a/Firmware/RP2040/src/Wii/wiimote-lib/wiimote_btstack.h b/Firmware/RP2040/src/Wii/wiimote-lib/wiimote_btstack.h new file mode 100644 index 00000000..c97d099c --- /dev/null +++ b/Firmware/RP2040/src/Wii/wiimote-lib/wiimote_btstack.h @@ -0,0 +1,8 @@ +#ifndef _WIIMOTE_BTSTACK_H_ +#define _WIIMOTE_BTSTACK_H_ + +void wiimote_emulator_set_led(void (*led_on)(), void (*led_off)()); + +void wiimote_emulator(void *report); + +#endif // _WIIMOTE_BTSTACK_H_ diff --git a/Firmware/RP2040/src/Wii/wiimote-lib/wm_crypto.c b/Firmware/RP2040/src/Wii/wiimote-lib/wm_crypto.c new file mode 100644 index 00000000..a425f0ae --- /dev/null +++ b/Firmware/RP2040/src/Wii/wiimote-lib/wm_crypto.c @@ -0,0 +1,239 @@ +#include "wm_crypto.h" + +//extension crypto (2602 bytes) + +static const uint8_t ans_tbl[7][6] = { + {0xA8,0x77,0xA6,0xE0,0xF7,0x43}, + {0x5A,0x35,0x85,0xE2,0x72,0x97}, + {0x8F,0xB7,0x1A,0x62,0x87,0x38}, + { 0xD,0x67,0xC7,0xBE,0x4F,0x3E}, + {0x20,0x76,0x37,0x8F,0x68,0xB7}, + {0xA9,0x26,0x3F,0x2B,0x10,0xE3}, + {0x30,0x7E,0x90, 0xE,0x85, 0xA} +}; + +static const uint8_t sboxes[10][256] = { + { + 0x70,0x51, 3,0x86,0x40, 0xD,0x4F,0xEB,0x3E,0xCC,0xD1,0x87,0x35,0xBD,0xF5, 0xB, + 0x5E,0xD0,0xF8,0xF2,0xD5,0xE2,0x6C,0x31, 0xC,0xAD,0xFC,0x21,0xC3,0x78,0xC1, 6, + 0xC2,0x4C,0x55,0xE6,0x4A,0x34,0x48,0x11,0x1E,0xDA,0xE7,0x1A,0x84,0xA0,0x96,0xA7, + 0xE3,0x7F,0xAF,0x63,0x9C,0xFA,0x23,0x5B,0x79,0xC8,0x9E,0xBA,0xB2,0xC9,0x22,0x12, + 0x4B,0xB3,0xA1,0xB6,0x32,0x49,0xA2,0xE1,0x89,0x39,0x10,0x66,0xC5, 7,0x8F,0x54, + 0xEA,0x91,0xCA,0x3F,0xF9,0x19,0xF0,0xD7,0x46,0xBC,0x28,0x1B,0x61,0xE8,0x2F,0x6A, + 0xAE,0x9D,0xF6,0x4E, 9,0x14,0x77,0x4D,0xDB,0x1F,0x2E,0x7B,0x7C,0xF1,0x43,0xA3, + 0,0xB8,0x13,0x8C,0x85,0xB9,0x29,0x75,0x88,0xFD,0xD2,0x56,0x1C,0x50,0x97,0x41, + 0xE5,0x3B,0x60,0xB5,0xC0,0x64,0xEE,0x98,0xD6,0x2D,0x25,0xA4,0xAA,0xCD,0x7D,0xA8, + 0x83,0xC6,0xAB,0xBE,0x44,0x99,0x26,0x3C,0xCE,0x9F,0xBF,0xD3,0xCB,0x76,0x7A,0x7E, + 0x82, 1,0x8A,0x9A,0x80,0x1D, 0xE,0xB0,0x5C,0xD4,0x38,0x62,0xF4,0x30,0xE0,0x8E, + 0x53,0xB7, 2,0x57,0xAC,0xA6,0x52, 0xA,0x6D,0x92,0x65,0x17,0x24,0x33,0x45,0x72, + 0x74,0xB1,0xB4,0xF7,0x5D,0xED,0x2C,0xFF,0x47,0x37,0x5A,0x90,0xBB,0xDF,0x2A,0x16, + 0x59,0x95,0xD9,0xC4,0x27,0x67,0x73,0xC7,0x68,0xFE,0xA5,0xDD,0x6B,0x5F,0x93,0xD8, + 0xEC, 5,0x3A,0x8D,0x6E,0xFB,0x3D,0xA9,0x69,0x36,0xF3,0x94,0xDE,0xEF,0x15,0x6F, + 0x8B,0x9B, 8, 0xF,0xDC,0x81,0x18,0x20, 4,0xE4,0x71,0xCF,0xE9,0x2B,0x42,0x58, + }, + { + 1,0xA0,0xA9,0x62,0xD6,0x3F,0x85,0xA7,0xB6,0xD4,0xFA,0x15,0x66,0x17, 9,0xBD, + 0x5D,0x14,0x34,0x26,0x59,0x72,0x91,0x54, 6,0x4F,0xF8,0xB0,0x5B,0x74,0x93,0x99, + 0x8C,0xF2,0x45,0xCD,0xEA,0x4E,0xAD,0x10,0x4A,0xE5,0xCA,0xEE,0xDF,0xC6,0x6F,0x9F, + 0x88,0x8E, 2,0xCC, 8,0xA8,0x77,0x94,0x6D,0x21,0xB1,0x28,0xE4,0x39,0x79,0x96, + 0x60,0x71,0x81,0x16,0x2E,0xE6,0x78,0xB9,0xC4,0x46,0x9A,0x42,0xAE,0xB7,0x7C,0x43, + 0xB3,0x22,0x1A,0x86,0xC2,0x32,0x3D,0x2D,0x9C,0xD2,0x29,0xE9,0x63,0x9B,0xD1,0x31, + 0x38,0x5E,0x1E,0x36,0x41,0xBB, 3,0x18,0x2B,0x3E,0xBF,0x68,0x61,0xFC,0x52,0xC0, + 0xDE,0xE0, 0xA,0x58,0x13,0x5A, 0,0xBE,0x1C,0x90, 0xE,0x53,0x12,0xFD,0xE2,0x6E, + 0xBA,0xCE,0x24,0x27,0x44,0x7F,0x87,0xA3,0xA1,0xD5,0x50,0x40,0xE3,0xF9,0x83,0xF7, + 0xC7,0xA2,0x35,0xC8,0xDB,0x19,0xAB,0x2F,0x11,0x25,0xED,0x33,0x9E,0x55,0xE1,0x48, + 0xAF,0x73,0x84,0xDA,0x2A,0xAA,0x51,0xEB,0x9D,0x95,0xB2,0xCB,0xE7,0x70,0x80,0xFE, + 0x4C,0x65, 4,0xEF,0xC5,0xF1,0xC3,0x3A,0xB4,0xF5,0x5F,0x23,0x89,0xDD,0x30,0xA5, + 0x8B,0xD3,0xF6,0xDC,0x4D,0x64,0xD7,0xF0,0x8F,0xEC,0x56,0x37,0x5C,0xA4, 0xD, 7, + 0x76,0x8A,0x2C, 0xB,0xB5,0xD8,0xC1,0x1F,0xE8,0x3B,0xF4,0x4B,0x1B,0x47,0x6C,0x49, + 0x67,0x7B,0x92,0xCF,0x75,0x7E,0x20,0xD9,0x7D,0x3C,0x97,0x7A,0xD0, 5,0x6B, 0xF, + 0x1D,0xFB,0x82,0x98,0x57,0x8D,0xF3,0x6A,0xBC,0xAC,0xC9,0xA6,0xFF,0xB8,0x69, 0xC, + }, + { + 0x4C,0x4D,0x72, 7,0x5A,0x49,0x33,0x8D,0xA2,0xAB,0x46,0x3D,0x63, 0xD,0xA0,0x97, + 0xFF,0xF0,0xF5,0xFA,0xC0,0xE9,0xDB,0x62,0xE4,0xE1,0x74,0x43,0xDC,0x86,0x18,0x29, + 0x37,0xF4, 6,0xE2,0xED,0x6F,0x90,0x48,0x1E,0x2D,0x1D,0xEA,0x73,0x94,0x54,0xDF, + 0x25,0xF6,0x47,0x27,0xD9,0x11,0x77,0xC9,0x84,0x1C,0x5B,0x5C,0x51,0x81,0xA6,0x22, + 0x3E,0x24,0x96,0xC8,0x8A,0xEC,0x82,0x7C, 9,0xB8,0x45,0x4A,0x57,0xBB,0x2F,0x50, + 0x75,0x8E,0x61,0x70,0x8C,0x6C,0xAF,0xD0,0xFD,0xB4,0x1B,0xAE,0xDE,0xFE,0x3B,0xB5, + 0x36,0xBD,0x55, 1, 0xE,0x9C,0x41,0x56,0x5F,0xB3,0x26, 3,0x83,0xBA,0x13,0x4B, + 0xCA,0xC5, 0xA,0xF8,0x60,0xA5,0xB9,0xC7,0xC3,0x98,0x32,0xFB,0x12,0xF9,0xA7,0x92, + 0xAA,0x68,0xF3,0x78,0x7E, 5,0x20,0x21, 2,0xE8,0xBF,0xF2,0xB0,0x59,0x8F,0xD2, + 0xCB,0x87,0x65,0x15,0xF1,0x1A,0xB2,0x30,0xAD,0xEE,0x58,0xA3,0x8B,0x66,0x1F,0x2C, + 0xD7,0x5D,0x19,0x85,0xA8,0xE6,0xD3,0x6B,0xA1, 0xC,0x91,0x93,0x6A,0x5E, 0xB,0x79, + 0xE3,0xDD, 0,0x4F,0x3C,0x89,0x6E,0x71,0x69,0xA9,0xAC,0x40,0xE5,0x99,0x28,0xC6, + 0x31,0x4E,0x7A,0xCD, 8,0x9E,0x7D,0xEF,0x17,0xFC,0x88,0xD8,0xA4,0x6D,0x44,0x95, + 0xD1,0xB7,0xD4,0x9B,0xBE,0x2A,0x34,0x64,0x2B,0xCF,0x2E,0xEB,0x38,0xCE,0x23,0xE0, + 0x3A,0x3F,0xF7,0x7B,0x9F,0x10,0x53,0xBC,0x52,0x67,0x16,0xE7,0x80,0x76, 4,0xC4, + 0xB6,0xC1,0xC2,0x7F,0x9A,0xDA,0xD5,0x39,0x42,0x14,0x9D,0xB1, 0xF,0x35,0xD6,0xCC, + }, + { + 0xB9,0xDA,0x38, 0xC,0xA2,0x9C, 9,0x1F, 6,0xB1,0xB6,0xFD,0x1A,0x69,0x23,0x30, + 0xC4,0xDE, 1,0xD1,0xF4,0x58,0x29,0x37,0x1C,0x7D,0xD5,0xBF,0xFF,0xBD,0xC8,0xC9, + 0xCF,0x65,0xBE,0x7B,0x78,0x97,0x98,0x67, 8,0xB3,0x26,0x57,0xF7,0xFA,0x40,0xAD, + 0x8E,0x75,0xA6,0x7C,0xDB,0x91,0x8B,0x51,0x99,0xD4,0x17,0x7A,0x90,0x8D,0xCE,0x63, + 0xCB,0x4E,0xA0,0xAB,0x18,0x3A,0x5B,0x50,0x7F,0x21,0x74,0xC1,0xBB,0xB8,0xB7,0xBA, + 0xB,0x35,0x95,0x31,0x59,0x9A,0x4D, 4, 7,0x1E,0x5A,0x76,0x13,0xF3,0x71,0x83, + 0xD0,0x86, 3,0xA8,0x39,0x42,0xAA,0x28,0xE6,0xE4,0xD8,0x5D,0xD3,0xD0,0x6E,0x6F, + 0x96,0xFB,0x5E,0xBC,0x56,0xC2,0x5F,0x85,0x9B,0xE7,0xAF,0xD2,0x3B,0x84,0x6A,0xA7, + 0x53,0xC5,0x44,0x49,0xA5,0xF9,0x36,0x72,0x3D,0x2C,0xD9,0x1B,0xA1,0xF5,0x4F,0x93, + 0x9D,0x68,0x47,0x41,0x16,0xCA,0x2A,0x4C,0xA3,0x87,0xD6,0xE5,0x19,0x2E,0x77,0x15, + 0x6D,0x70,0xC0,0xDF,0xB2, 0,0x46,0xED,0xC6,0x6C,0x43,0x60,0x92,0x2D,0xA9,0x22, + 0x45,0x8F,0x34,0x55,0xAE,0xA4, 0xA,0x66,0x32,0xE0,0xDC, 2,0xAC,0xE8,0x20,0x8C, + 0x89,0x62,0x4A,0xFE,0xEE,0xC3,0xE3,0x3C,0xF1,0x79, 5,0xE9,0xF6,0x27,0x33,0xCC, + 0xF2,0x9E,0x11,0x81,0x7E,0x80,0x10,0x8A,0x82,0x9F,0x48, 0xD,0xD7,0xB4,0xFC,0x2F, + 0xB5,0xC7,0xDD,0x88,0x14,0x6B,0x2B,0x54,0xEA,0x1D,0x94,0x5C,0xB0,0xEF,0x12,0x24, + 0xCD,0xEB,0xE1,0xE2,0x64,0x73,0x3F, 0xE,0x52,0x61,0x25,0x3E,0xF8, 0xF,0x4B,0xEC, + }, + { + 0xC0, 0,0x30,0xF6, 2,0x49,0x3D,0x10,0x6E,0x20,0xC9,0xA6,0x2F,0xFE,0x2C,0x2B, + 0x75,0x2E,0x45,0x26,0xAB,0x48,0xA9,0x80,0xFC, 4,0xCC,0xD3,0xB5,0xBA,0xA3,0x38, + 0x31,0x7D, 1,0xD9,0xA7,0x7B,0x96,0xB6,0x63,0x69,0x4E,0xF7,0xDE,0xE0,0x78,0xCA, + 0x50,0xAA,0x41,0x91,0x65,0x88,0xE4,0x21,0x85,0xDA,0x3A,0x27,0xBE,0x1C,0x3E,0x42, + 0x5E,0x17,0x52,0x7F,0x1F,0x89,0x24,0x6F,0x8F,0x5C,0x67,0x74, 0xE,0x12,0x87,0x8D, + 0xE9,0x34,0xED,0x73,0xC4,0xF8,0x61,0x5B, 5,0xDF,0x59,0x4C,0x97,0x79,0x83,0x18, + 0xA4,0x55,0x95,0xEB,0xBD,0x53,0xF5,0xF1,0x57,0x66,0x46,0x9F,0xB2,0x81, 9,0x51, + 0x86,0x22,0x16,0xDD,0x23,0x93,0x76,0x29,0xC2,0xD7,0x1D,0xD4,0xBF,0x36,0x3F,0xEA, + 0x4B,0x11,0x32,0xB9,0x62,0x54,0x60,0xD6,0x6D,0x43,0x9A, 0xD,0x92,0x9C,0xB0,0xEF, + 0x58,0x6C,0x9D,0x77,0x2D,0x70,0xFA,0xF3,0xB3, 0xB,0xE2,0x40,0x7E,0xF4,0x8A,0xE5, + 0x8C,0x3C,0x56,0x71,0xD1,0x64,0xE1,0x82, 0xA,0xCB,0x13,0x15,0x90,0xEC, 3,0x99, + 0xAF,0x14,0x5D, 0xF,0x33,0x4A,0x94,0xA5,0xA8,0x35,0x1B,0xE3,0x6A,0xC6,0x28,0xFF, + 0x4D,0xE7,0x25,0x84,0xAC, 8,0xAE,0xC5,0xA2,0x2A,0xB8,0x37, 0xC,0x7A,0xA0,0xC3, + 0xCE,0xAD, 6,0x1A,0x9E,0x8B,0xFB,0xD5,0xD0,0xC1,0x1E,0xD0,0xB4,0x9B,0xB1,0x44, + 0xF2,0x47,0xC7,0x68,0xCF,0x72,0xBB,0x4F,0x5A,0xF9,0xDC,0x6B,0xDB,0xD2,0xE8,0x7C, + 0xC8,0xEE,0x98,0xA1,0xE6,0xD8,0x39, 7,0x5F,0xFD,0x8E,0x19,0xB7,0x3B,0xBC,0xCD, + }, + { + 0x7C,0xE3,0x81,0x73,0xB2,0x11,0xBF,0x6F,0x20,0x98,0xFE,0x75,0x96,0xEF,0x6C,0xDA, + 0x50,0xE1, 9,0x72,0x54,0x45,0xBA,0x34,0x80,0x5B,0xED,0x3E,0x53,0x2C,0x87,0xA4, + 0x57,0xF3,0x33,0x3F,0x3C,0xB7,0x67,0xB4,0xA3,0x25,0x60,0x4F, 7,0x6B,0x1B,0x47, + 0x15, 0xF,0xE4, 0xA,0xEA,0xD1,0x32,0x78,0x36,0x49,0x8D,0x4B,0xD2,0xBC,0xA5,0xDC, + 0x1D, 0xD,0x4D,0xCD,0x9A,0x82,0x5F,0xFC,0x94,0x65,0xBE,0xE2,0xF4,0xC9,0x1E,0x44, + 0xCB,0x9E, 0xC,0x64,0x71,0x26,0x63,0xB3,0x14,0xE8,0x40,0x70,0x8A, 0xE,0x19,0x42, + 0x6D,0xAC,0x88,0x10,0x5C,0xDF,0x41,0xA9,0xAD,0xE5,0xFB,0x74,0xCC,0xD5, 6,0x8E, + 0x59,0x86,0xCE,0x1F,0x3D,0x76,0xE0,0x8F,0xB9,0x77,0x27,0x7B,0xA6,0xD8,0x29,0xD3, + 0xEC,0xB8,0x13,0xF7,0xFA,0xC3,0x51,0x6A,0xDE,0x4A,0x5A,0xEB,0xC2,0x8B,0x23,0x48, + 0x92,0xCF,0x62,0xA8,0x99,0xF8,0xD0,0x2E,0x85,0x61,0x43,0xC8,0xBD,0xF0, 5,0x93, + 0xCA,0x4E,0xF1,0x7D,0x30,0xFD,0xC4,0x69,0x66,0x2F, 8,0xB1,0x52,0xF9,0x21,0xE6, + 0x7A,0x2B,0xDD,0x39,0x84,0xFF,0xC0,0x91,0xD6,0x37,0xD4,0x7F,0x2D,0x9B,0x5D,0xA1, + 0x3B,0x6E,0xB5,0xC5,0x46, 4,0xF5,0x90,0xEE,0x7E,0x83,0x1C, 3,0x56,0xB6,0xAA, + 0,0x17, 1,0x35,0x55,0x79, 0xB,0x12,0xBB,0x1A,0x31,0xE7, 2,0x28,0x16,0xC1, + 0xF6,0xA2,0xDB,0x18,0x9C,0x89,0x68,0x38,0x97,0xAB,0xC7,0x2A,0xD7,0x3A,0xF2,0xC6, + 0x24,0x4C,0xB0,0x58,0xA0,0x22,0x5E,0x9D,0xD9,0xA7,0xE9,0xAE,0xAF,0x8C,0x95,0x9F, + }, + { + 0x28,0xB7,0x20,0xD7,0xB0,0x30,0xC3, 9,0x19,0xC0,0x67,0xD6, 0,0x3C,0x7E,0xE7, + 0xE9,0xF4, 8,0x5A,0xF8,0xB8,0x2E, 5,0xA6,0x25,0x9E,0x5C,0xD8,0x15, 0xD,0xE1, + 0xF6,0x11,0x54,0x6B,0xCD,0x21,0x46,0x66,0x5E,0x84,0xAD, 6,0x38,0x29,0x44,0xC5, + 0xA2,0xCE,0xF1,0xAA,0xC1,0x40,0x71,0x86,0xB5,0xEF,0xFC,0x36,0xA8,0xCB, 0xA,0x48, + 0x27,0x45,0x64,0xA3,0xAF,0x8C,0xB2,0xC6,0x9F, 7,0x89,0xDC,0x17,0xD3,0x49,0x79, + 0xFB,0xFE,0x1D,0xD0,0xB9,0x88,0x43,0x52,0xBC, 1,0x78,0x2B,0x7D,0x94,0xC7, 0xE, + 0xDE,0xA5,0xD5,0x9B,0xCC,0xF7,0x61,0x7A,0xC2,0x74,0x81,0x39, 3,0xAB,0x96,0xA0, + 0x37,0xBD,0x2D,0x72,0x75,0x3F,0xC9,0xD4,0x8E,0x6F,0xF9,0x8D,0xED,0x62,0xDB,0x1C, + 0xDF, 4,0xAC,0x1B,0x6C,0x14,0x4B,0x63,0xD0,0xBF,0xB4,0x82,0xEC,0x7B,0x1A,0x59, + 0x92,0xD2,0x10,0x60,0xB6,0x3D,0x5F,0xE6,0x80,0x6E,0x70,0xC4,0xF2,0x35,0xD9,0x7C, + 0xEE,0xE5,0x41,0xA4,0x5B,0x50,0xDD,0xBB,0x4C,0xF3,0x1F,0x9D,0x5D,0x57,0x55,0x51, + 0x97,0xE3,0x58,0x42,0x4D,0x9C,0x73,0xBA,0xC8,0x77,0x31,0x69,0x26,0xAE,0xEA,0x8A, + 0xDA,0x22,0xB3,0x87,0x56,0xFA,0x93, 0xB,0x34,0x16,0x33,0xE8,0xE4,0x53,0xBE,0xA9, + 0xB1,0x3A,0x3E,0xF5,0x90,0x6A,0xCF,0x3B,0x12,0xFD,0x8F,0x9A,0xA7,0x47,0x91,0x99, + 0xEB, 0xF,0x24,0xFF,0x23,0x18,0x85,0x4E,0x7F, 0xC,0xE0,0xA1,0xD2,0xD1,0x2C,0x2A, + 0x4A, 2,0x4F,0x1E,0x95,0x68,0x8B,0x98,0x83,0x6D,0x76,0xCA,0x65,0x32,0x13,0x2F, + }, + { + 0xC3,0x82,0x9A,0xA4,0xBA,0x81,0x60,0x37,0x34,0x35,0xFC,0x80,0xA8,0x51,0x65,0x67, + 0xED,0x30,0x5F,0x10,0xD3,0x4A,0x27,0x2F,0x13,0xB9,0x2A,0xD2,0xCC,0xE1,0xEF,0xAE, + 0xEB,0xBE,0xF4,0xBD,0xCF,0x43,0xB3,0xC5,0x88,0x84,0xB7,0xDD,0x39,0x40,0xCE,0x48, + 0x6D,0x9B,0x72,0x61,0x7E,0xE7,0xA1,0x4E,0x53,0x2E,0x77,0x3B,0xE2,0xC9,0x36,0x22, + 0x1B,0x6E,0x73,0xB1, 3,0xB2,0x4C,0x87,0xA9,0xD4,0x4D, 0xF,0xD8,0x15,0x6C,0xAA, + 0x18,0xF6,0x49,0x57,0x5D,0xFB,0x7A,0x14,0x94,0x63,0xA0,0x11,0xB0,0x9E,0xDE, 5, + 0x46,0xC8,0xEE,0x47,0xDB,0xDC,0x24,0x89,0x9C,0x91,0x97,0x29,0xE9,0x7B,0xC1, 7, + 0x1E,0xB8,0xFD,0xFE,0xAC,0xC6,0x62,0x98,0x4F,0xF1,0x79,0xE0,0xE8,0x6B,0x78,0x56, + 0xB6,0x8D, 4,0x50,0x86,0xCA,0x6F,0x20,0xE6,0xEA,0xE5,0x76,0x17,0x1C,0x74,0x7F, + 0xBC, 0xD,0x2C,0x85,0xF7,0x66,0x96,0xE4,0x8B,0x75,0x3F,0x4B,0xD9,0x38,0xAF,0x7C, + 0xDA, 0xB,0x83,0x2D,0x31,0x32,0xA2,0xF5,0x1D,0x59,0x41,0x45,0xBF,0x3C,0x1F,0xF8, + 0xF9,0x8A,0xD0,0x16,0x25,0x69,0x12,0x99,0x9D,0x21,0x95,0xAB, 1,0xA6,0xD7,0xB5, + 0xC0,0x7D,0xFF,0x58, 0xE,0x3A,0x92,0xD1,0x55,0xE3, 8,0x9F,0xD6,0x3E,0x52,0x8E, + 0xFA,0xA3,0xC7, 2,0xCD,0xDF,0x8F,0x64,0x19,0x8C,0xF3,0xA7, 0xC,0x5E, 0xA,0x6A, + 9,0xF0,0x93,0x5B,0x42,0xC2, 6,0x23,0xEC,0x71,0xAD,0xB4,0xCB,0xBB,0x70,0x28, + 0xD5,0x1A,0x5C,0x33,0x68,0x5A, 0,0x44,0x90,0xA5,0xC4,0x26,0x3D,0x2B,0xF2,0x54, + }, + { + 0x96,0xAD,0xDA,0x1F,0xED,0x33,0xE1,0x81,0x69, 8, 0xD, 0xA,0xDB,0x35,0x77,0x9A, + 0x64,0xD1,0xFC,0x78,0xAA,0x1B,0xD0,0x67,0xA0,0xDD,0xFA,0x6C,0x63,0x71, 5,0x84, + 0x17,0x6A,0x89,0x4F,0x66,0x7F,0xC6,0x50,0x55,0x92,0x6F,0xBD,0xE7,0xD2,0x40,0x72, + 0x8D,0xBB,0xEC, 6,0x42,0x8A,0xE4,0x88,0x9D,0x7E,0x7A,0x82,0x27,0x13,0x41,0x1A, + 0xAF,0xC8,0xA4,0x76,0xB4,0xC2,0xFE,0x6D,0x1C,0xD9,0x61,0x30,0xB3,0x7C,0xEA,0xF7, + 0x29, 0xF,0xF2,0x3B,0x51,0xC1,0xDE,0x5F,0xE5,0x2A,0x2F,0x99, 0xB,0x5D,0xA3,0x2B, + 0x4A,0xAB,0x95,0xA5,0xD3,0x58,0x56,0xEE,0x28,0x31, 0,0xCC,0x15,0x46,0xCA,0xE6, + 0x86,0x38,0x3C,0x65,0xF5,0xE3,0x9F,0xD6,0x5B, 9,0x49,0x83,0x70,0x2D,0x53,0xA9, + 0x7D,0xE2,0xC4,0xAC,0x8E,0x5E,0xB8,0x25,0xF4,0xB9,0x57,0xF3,0xF1,0x68,0x47,0xB2, + 0xA2,0x59,0x20,0xCE,0x34,0x79,0x5C,0x90, 0xE,0x1E,0xBE,0xD5,0x22,0x23,0xB1,0xC9, + 0x18,0x62,0x16,0x2E,0x91,0x3E, 7,0x8F,0xD8,0x3F,0x93,0x3D,0xD4,0x9B,0xDF,0x85, + 0x21,0xFB,0x11,0x74,0x97,0xC7,0xD7,0xDC,0x4C,0x19,0x45,0x98,0xE9,0x43, 2,0x4B, + 0xBC,0xC3, 4,0x9C,0x6B,0xF0,0x75,0x52,0xA7,0x26,0xF6,0xC5,0xBA,0xCF,0xB0,0xB7, + 0xAE,0x5A,0xA1,0xBF, 3,0x8B,0x80,0x12,0x6E, 0xC,0xEB,0xF9,0xC0,0x44,0x24,0xEF, + 0x10,0xF8,0xA8,0x8C,0xE8,0x7B,0xFF,0x9E,0x2C,0xCD,0x60,0x36,0x87,0xB5,0x94,0xA6, + 0x54,0x73,0x3A,0x14,0x4E, 1,0x1D,0xB6,0xFD,0x37,0x48,0x4D,0x39,0xCB,0xE0,0x32, + } +}; + +static inline uint8_t ror8(uint8_t a, uint8_t b) +{ + return (a>>b) | ((a<<(8-b))&0xff); +} + +void ext_generate_tables(struct ext_crypto_state * state, const uint8_t key[16]) +{ + int idx, i; + const uint8_t * ans; + uint8_t t0[10]; + + //determine idx with simple brute force + for (idx = 0; idx < 7; idx++) + { + ans = ans_tbl[idx]; + + for(i = 0; i < 10; i++) + { + t0[i] = sboxes[0][key[9 - i]]; + } + + //todo: clean up this nasty mess + if ((key[0xf] == (uint8_t)((ror8(ans[0]^t0[5],t0[2]%8) - t0[9]) ^ t0[4])) && + (key[0xe] == (uint8_t)((ror8(ans[1]^t0[1],t0[0]%8) - t0[5]) ^ t0[7])) && + (key[0xd] == (uint8_t)((ror8(ans[2]^t0[6],t0[8]%8) - t0[2]) ^ t0[0])) && + (key[0xc] == (uint8_t)((ror8(ans[3]^t0[4],t0[7]%8) - t0[3]) ^ t0[2])) && + (key[0xb] == (uint8_t)((ror8(ans[4]^t0[1],t0[6]%8) - t0[3]) ^ t0[4])) && + (key[0xa] == (uint8_t)((ror8(ans[5]^t0[7],t0[8]%8) - t0[5]) ^ t0[9]))) + { + break; + } + } + + state->ft[0] = sboxes[idx+1][key[0xb]] ^ sboxes[idx+2][key[0x6]]; + state->ft[1] = sboxes[idx+1][key[0xd]] ^ sboxes[idx+2][key[0x4]]; + state->ft[2] = sboxes[idx+1][key[0xa]] ^ sboxes[idx+2][key[0x2]]; + state->ft[3] = sboxes[idx+1][key[0xf]] ^ sboxes[idx+2][key[0x7]]; + state->ft[4] = sboxes[idx+1][key[0xe]] ^ sboxes[idx+2][key[0x5]]; + state->ft[5] = sboxes[idx+1][key[0xc]] ^ sboxes[idx+2][key[0x0]]; + state->ft[6] = sboxes[idx+1][key[0x9]] ^ sboxes[idx+2][key[0x3]]; + state->ft[7] = sboxes[idx+1][key[0x8]] ^ sboxes[idx+2][key[0x1]]; + + state->sb[0] = sboxes[idx+1][key[0xf]] ^ sboxes[idx+2][key[0x8]]; + state->sb[1] = sboxes[idx+1][key[0xa]] ^ sboxes[idx+2][key[0x5]]; + state->sb[2] = sboxes[idx+1][key[0xc]] ^ sboxes[idx+2][key[0x9]]; + state->sb[3] = sboxes[idx+1][key[0xd]] ^ sboxes[idx+2][key[0x0]]; + state->sb[4] = sboxes[idx+1][key[0xb]] ^ sboxes[idx+2][key[0x2]]; + state->sb[5] = sboxes[idx+1][key[0xe]] ^ sboxes[idx+2][key[0x1]]; + state->sb[6] = sboxes[idx+1][key[0x6]] ^ sboxes[idx+2][key[0x4]]; + state->sb[7] = sboxes[idx+1][key[0x7]] ^ sboxes[idx+2][key[0x3]]; +} + +void ext_encrypt_bytes(const struct ext_crypto_state * state, uint8_t * buffer, + int addr_offset, int length) +{ + for (int i = 0; i < length; i++) + { + buffer[i] = (buffer[i] - state->ft[(i + addr_offset) % 8]) ^ state->sb[(i + addr_offset) % 8]; + } +} \ No newline at end of file diff --git a/Firmware/RP2040/src/Wii/wiimote-lib/wm_crypto.h b/Firmware/RP2040/src/Wii/wiimote-lib/wm_crypto.h new file mode 100644 index 00000000..46812774 --- /dev/null +++ b/Firmware/RP2040/src/Wii/wiimote-lib/wm_crypto.h @@ -0,0 +1,16 @@ +#ifndef WM_CRYPTO_H +#define WM_CRYPTO_H + +#include + +struct ext_crypto_state +{ + uint8_t ft[8]; + uint8_t sb[8]; +}; + +void ext_generate_tables(struct ext_crypto_state * state, const uint8_t key[16]); +void ext_encrypt_bytes(const struct ext_crypto_state * state, uint8_t * buffer, + int addr_offset, int length); + +#endif diff --git a/Firmware/RP2040/src/Wii/wiimote-lib/wm_reports.c b/Firmware/RP2040/src/Wii/wiimote-lib/wm_reports.c new file mode 100644 index 00000000..0f8f90c1 --- /dev/null +++ b/Firmware/RP2040/src/Wii/wiimote-lib/wm_reports.c @@ -0,0 +1,440 @@ +#include "wm_reports.h" +#include "wm_crypto.h" +#include "inet.h" + +#include +#include + +struct report * report_queue_push(struct wiimote_state * state) +{ + struct queued_report * rpt; + + //allocate new report + rpt = (struct queued_report *)malloc(sizeof(struct queued_report)); + memset(rpt, 0, sizeof(struct queued_report)); + + //append to the end of the queue + if (state->sys.queue_end != NULL) + { + state->sys.queue_end->next = rpt; + } + else + { + state->sys.queue = rpt; + } + state->sys.queue_end = rpt; + + return &(rpt->rpt); +} + +struct report * report_queue_peek(struct wiimote_state * state) +{ + if (state->sys.queue == NULL) return NULL; //empty queue + + return &(state->sys.queue->rpt); +} + +void report_queue_pop(struct wiimote_state * state) +{ + struct queued_report * rpt; + + if (state->sys.queue == NULL) return; //nothing to remove + + //remove from the queue + rpt = state->sys.queue; + if (state->sys.queue_end == rpt) + { + state->sys.queue_end = NULL; + } + state->sys.queue = rpt->next; + + //free report mem + free(rpt); +} + +void report_queue_push_ack(struct wiimote_state *state, uint8_t report, uint8_t result) +{ + //push acknowledgement report x22 + struct report * rpt = report_queue_push(state); + rpt->len = 6; + rpt->data.io = 0xa1; + rpt->data.type = 0x22; + + struct report_ack * ack = (struct report_ack *)rpt->data.buf; + ack->report = report; + ack->result = result; +} + +void report_queue_push_status(struct wiimote_state * state) +{ + //push status report x20 + struct report * rpt = report_queue_push(state); + rpt->len = 8; + rpt->data.io = 0xa1; + rpt->data.type = 0x20; + + struct report_status * status = (struct report_status *)rpt->data.buf; + status->low_battery = state->sys.low_battery; + status->extension_connected = state->sys.extension_connected; + status->speaker_enabled = state->sys.speaker_enabled; + status->ircam_enabled = state->sys.ircam_enabled; + status->led_1 = state->sys.led_1; + status->led_2 = state->sys.led_2; + status->led_3 = state->sys.led_3; + status->led_4 = state->sys.led_4; + status->battery_level = state->sys.battery_level; +} + +void report_format_mem_resp(struct wiimote_state * state, struct report * rpt, + int size, int error, uint16_t addr, uint8_t * buf, bool encrypt) +{ + struct report_mem_resp * resp = (struct report_mem_resp *)rpt->data.buf; + + rpt->len = 23; + rpt->data.io = 0xa1; + rpt->data.type = 0x21; + + resp->size = size - 1; + resp->error = error; + resp->addr = htons(addr); + if (buf != NULL) //buf will be null for error reports + { + memcpy(resp->data, buf, size); + + if (encrypt) + { + ext_encrypt_bytes(&state->sys.extension_crypto_state, resp->data, addr & 0x7, size); + } + } +} + +void report_append_buttons(struct wiimote_state * state, uint8_t * buf) +{ + struct report_buttons * rpt = (struct report_buttons *)buf; + + rpt->left = state->usr.left; + rpt->right = state->usr.right; + rpt->down = state->usr.down; + rpt->up = state->usr.up; + rpt->one = state->usr.one; + rpt->two = state->usr.two; + rpt->a = state->usr.a; + rpt->b = state->usr.b; + rpt->plus = state->usr.plus; + rpt->minus = state->usr.minus; + rpt->home = state->usr.home; +} + +void report_append_accelerometer(struct wiimote_state * state, uint8_t * buf) +{ + struct report_accelerometer * rpt = (struct report_accelerometer *)buf; + + rpt->buttons.accel_0 = state->usr.accel_x; + rpt->buttons.accel_1 = (state->usr.accel_z & 0x2) | + ((state->usr.accel_y >> 1) & 0x1); + + rpt->x = state->usr.accel_x >> 2; + rpt->y = state->usr.accel_y >> 2; + rpt->z = state->usr.accel_z >> 2; +} + +void report_append_ir_10(struct wiimote_state * state, uint8_t * buf) +{ + struct report_ir_basic * rpt = (struct report_ir_basic *)buf; + + rpt->x1_lo = state->usr.ir_object[0].x; + rpt->y1_lo = state->usr.ir_object[0].y; + rpt->x1_hi = state->usr.ir_object[0].x >> 8; + rpt->y1_hi = state->usr.ir_object[0].y >> 8; + + rpt->x2_lo = state->usr.ir_object[1].x; + rpt->y2_lo = state->usr.ir_object[1].y; + rpt->x2_hi = state->usr.ir_object[1].x >> 8; + rpt->y2_hi = state->usr.ir_object[1].y >> 8; + + rpt->x3_lo = state->usr.ir_object[2].x; + rpt->y3_lo = state->usr.ir_object[2].y; + rpt->x3_hi = state->usr.ir_object[2].x >> 8; + rpt->y3_hi = state->usr.ir_object[2].y >> 8; + + rpt->x4_lo = state->usr.ir_object[3].x; + rpt->y4_lo = state->usr.ir_object[3].y; + rpt->x4_hi = state->usr.ir_object[3].x >> 8; + rpt->y4_hi = state->usr.ir_object[3].y >> 8; + +} + +void report_append_ir_12(struct wiimote_state * state, uint8_t * buf) +{ + struct report_ir_ext * rpt = (struct report_ir_ext *)buf; + int i; + + for (i=0; i<4; i++) + { + rpt->obj[i].x_lo = state->usr.ir_object[i].x; + rpt->obj[i].y_lo = state->usr.ir_object[i].y; + rpt->obj[i].x_hi = state->usr.ir_object[i].x >> 8; + rpt->obj[i].y_hi = state->usr.ir_object[i].y >> 8; + rpt->obj[i].size = state->usr.ir_object[i].size; + } + +} + +void report_append_interleaved(struct wiimote_state * state, uint8_t * buf) +{ + struct report_interleaved * rpt = (struct report_interleaved *)buf; + int i; + + if (state->sys.reporting_mode == 0x3e) + { + rpt->buttons.accel_0 = state->usr.accel_z >> 4; + rpt->buttons.accel_1 = state->usr.accel_z >> 6; + rpt->accel = state->usr.accel_x >> 2; + + for (i=0; i<2; i++) + { + rpt->obj[i].x_lo = state->usr.ir_object[i].x; + rpt->obj[i].y_lo = state->usr.ir_object[i].y; + rpt->obj[i].x_hi = state->usr.ir_object[i].x >> 8; + rpt->obj[i].y_hi = state->usr.ir_object[i].y >> 8; + rpt->obj[i].size = state->usr.ir_object[i].size; + rpt->obj[i].x_min = state->usr.ir_object[i].xmin; + rpt->obj[i].y_min = state->usr.ir_object[i].ymin; + rpt->obj[i].y_max = state->usr.ir_object[i].xmax; + rpt->obj[i].y_max = state->usr.ir_object[i].ymax; + rpt->obj[i].intensity = state->usr.ir_object[i].intensity; + } + + state->sys.reporting_mode = 0x3f; + } + else + { + rpt->buttons.accel_0 = state->usr.accel_z; + rpt->buttons.accel_1 = state->usr.accel_z >> 2; + rpt->accel = state->usr.accel_y >> 2; + + for (i=0; i<2; i++) + { + rpt->obj[i].x_lo = state->usr.ir_object[i+2].x; + rpt->obj[i].y_lo = state->usr.ir_object[i+2].y; + rpt->obj[i].x_hi = state->usr.ir_object[i+2].x >> 8; + rpt->obj[i].y_hi = state->usr.ir_object[i+2].y >> 8; + rpt->obj[i].size = state->usr.ir_object[i+2].size; + rpt->obj[i].x_min = state->usr.ir_object[i+2].xmin; + rpt->obj[i].y_min = state->usr.ir_object[i+2].ymin; + rpt->obj[i].y_max = state->usr.ir_object[i+2].xmax; + rpt->obj[i].y_max = state->usr.ir_object[i+2].ymax; + rpt->obj[i].intensity = state->usr.ir_object[i+2].intensity; + } + + state->sys.reporting_mode = 0x3e; + } +} + +void report_append_extension(struct wiimote_state * state, uint8_t * buf, uint8_t bytes) +{ + //a600fe = 0x04 activate motionplus, 0x05 activate nunchuk passthrough, 0x07 activate classic passthrough + //if no other extension, send 0x20 + + //a600f0 = 0x55 deactivate motionplus + //send report 0x20 twice (once for unplugged, once for plugged in) + + //0xa400fa contents + //0000 A420 0000 Nunchuk + //0000 A420 0101 Classic + //0000 A420 0405 WMP + //0000 A420 0505 WMP nunchuk + //0000 A420 0705 WMP classic + + //0000 A620 0005 Inactive WMP + // ^=6 Deactivated WMP + + //these should be set to the the address offset of the extension data + //and the length in bytes of the extesnion data + //right now, they are always the same in all situations + int addr_offset = 0x08, length = 6; + + switch (state->sys.extension_report_type) + { + case 0x00: //nunchuk + { + struct report_ext_nunchuk * rpt = (struct report_ext_nunchuk *)buf; + + rpt->x = state->usr.nunchuk.x; + rpt->y = state->usr.nunchuk.y; + + rpt->accel_x_hi = state->usr.nunchuk.accel_x >> 2; + rpt->accel_y_hi = state->usr.nunchuk.accel_y >> 2; + rpt->accel_z_hi = state->usr.nunchuk.accel_z >> 2; + rpt->accel_x_lo = state->usr.nunchuk.accel_x; + rpt->accel_y_lo = state->usr.nunchuk.accel_y; + rpt->accel_z_lo = state->usr.nunchuk.accel_z; + + rpt->c = !state->usr.nunchuk.c; + rpt->z = !state->usr.nunchuk.z; + + break; + } + case 0x01: //classic + { + struct report_ext_classic * rpt = (struct report_ext_classic *)buf; + + rpt->lx = state->usr.classic.ls_x; + rpt->ly = state->usr.classic.ls_y; + rpt->rx_hi = state->usr.classic.rs_x >> 3; + rpt->rx_m = state->usr.classic.rs_x >> 1; + rpt->rx_lo = state->usr.classic.rs_x; + rpt->ry = state->usr.classic.rs_y; + + rpt->lt_hi = state->usr.classic.lt >> 3; + rpt->lt_lo = state->usr.classic.lt; + rpt->rt = state->usr.classic.rt; + + rpt->left = !state->usr.classic.left; + rpt->right = !state->usr.classic.right; + rpt->up = !state->usr.classic.up; + rpt->down = !state->usr.classic.down; + rpt->ltrigger = !state->usr.classic.ltrigger; + rpt->rtrigger = !state->usr.classic.rtrigger; + rpt->lz = !state->usr.classic.lz; + rpt->rz = !state->usr.classic.rz; + rpt->a = !state->usr.classic.a; + rpt->b = !state->usr.classic.b; + rpt->x = !state->usr.classic.x; + rpt->y = !state->usr.classic.y; + rpt->plus = !state->usr.classic.plus; + rpt->minus = !state->usr.classic.minus; + rpt->home = !state->usr.classic.home; + + rpt->unused = 1; + + break; + } + case 0x04: //motionplus + { + struct report_ext_motionplus * rpt = (struct report_ext_motionplus *)buf; + + rpt->yaw_hi = state->usr.motionplus.yaw_down >> 8; + rpt->yaw_lo = state->usr.motionplus.yaw_down; + rpt->roll_hi = state->usr.motionplus.roll_left >> 8; + rpt->roll_lo = state->usr.motionplus.roll_left; + rpt->pitch_hi = state->usr.motionplus.pitch_left >> 8; + rpt->pitch_lo = state->usr.motionplus.pitch_left; + + rpt->yaw_slow = state->usr.motionplus.yaw_slow; + rpt->pitch_slow = state->usr.motionplus.pitch_slow; + rpt->roll_slow = state->usr.motionplus.roll_slow; + + rpt->ext = 0; + rpt->unused_0 = 1; + + break; + } + case 0x05: //motionplus + nunchuk + if (state->sys.extension_report) + { + struct report_ext_motionplus * rpt = (struct report_ext_motionplus *)buf; + + rpt->yaw_hi = state->usr.motionplus.yaw_down >> 8; + rpt->yaw_lo = state->usr.motionplus.yaw_down; + rpt->roll_hi = state->usr.motionplus.roll_left >> 8; + rpt->roll_lo = state->usr.motionplus.roll_left; + rpt->pitch_hi = state->usr.motionplus.pitch_left >> 8; + rpt->pitch_lo = state->usr.motionplus.pitch_left; + + rpt->yaw_slow = state->usr.motionplus.yaw_slow; + rpt->pitch_slow = state->usr.motionplus.pitch_slow; + rpt->roll_slow = state->usr.motionplus.roll_slow; + + rpt->ext = 1; + rpt->unused_0 = 1; + + state->sys.extension_report = 0; + } + else + { + struct report_ext_nunchuk_pt * rpt = (struct report_ext_nunchuk_pt *)buf; + + rpt->x = state->usr.nunchuk.x; + rpt->y = state->usr.nunchuk.y; + + rpt->accel_x_hi = state->usr.nunchuk.accel_x >> 2; + rpt->accel_y_hi = state->usr.nunchuk.accel_y >> 2; + rpt->accel_z_hi = state->usr.nunchuk.accel_z >> 3; + rpt->accel_x_lo = state->usr.nunchuk.accel_x >> 1; + rpt->accel_y_lo = state->usr.nunchuk.accel_y >> 1; + rpt->accel_z_lo = state->usr.nunchuk.accel_z >> 1; + + rpt->c = !state->usr.nunchuk.c; + rpt->z = !state->usr.nunchuk.z; + + rpt->ext = 1; + + state->sys.extension_report = 1; + } + break; + case 0x07: //motionplus + classic + if (state->sys.extension_report) + { + struct report_ext_motionplus * rpt = (struct report_ext_motionplus *)buf; + + rpt->yaw_hi = state->usr.motionplus.yaw_down >> 8; + rpt->yaw_lo = state->usr.motionplus.yaw_down; + rpt->roll_hi = state->usr.motionplus.roll_left >> 8; + rpt->roll_lo = state->usr.motionplus.roll_left; + rpt->pitch_hi = state->usr.motionplus.pitch_left >> 8; + rpt->pitch_lo = state->usr.motionplus.pitch_left; + + rpt->yaw_slow = state->usr.motionplus.yaw_slow; + rpt->pitch_slow = state->usr.motionplus.pitch_slow; + rpt->roll_slow = state->usr.motionplus.roll_slow; + + rpt->ext = 1; + rpt->unused_0 = 1; + + state->sys.extension_report = 0; + } + else + { + struct report_ext_classic_pt * rpt = (struct report_ext_classic_pt *)buf; + + rpt->lx = state->usr.classic.ls_x >> 1; + rpt->ly = state->usr.classic.ls_y >> 1; + rpt->rx_hi = state->usr.classic.rs_x >> 3; + rpt->rx_m = state->usr.classic.rs_x >> 1; + rpt->rx_lo = state->usr.classic.rs_x; + rpt->ry = state->usr.classic.rs_y; + + rpt->lt_hi = state->usr.classic.lt >> 3; + rpt->lt_lo = state->usr.classic.lt; + rpt->rt = state->usr.classic.rt; + + rpt->left = !state->usr.classic.left; + rpt->right = !state->usr.classic.right; + rpt->up = !state->usr.classic.up; + rpt->down = !state->usr.classic.down; + rpt->ltrigger = !state->usr.classic.ltrigger; + rpt->rtrigger = !state->usr.classic.rtrigger; + rpt->lz = !state->usr.classic.lz; + rpt->rz = !state->usr.classic.rz; + rpt->a = !state->usr.classic.a; + rpt->b = !state->usr.classic.b; + rpt->x = !state->usr.classic.x; + rpt->y = !state->usr.classic.y; + rpt->plus = !state->usr.classic.plus; + rpt->minus = !state->usr.classic.minus; + rpt->home = !state->usr.classic.home; + + rpt->ext = 1; + + state->sys.extension_report = 1; + } + break; + } + + + if (state->sys.extension_encrypted) + { + ext_encrypt_bytes(&state->sys.extension_crypto_state, buf, addr_offset, length); + } +} diff --git a/Firmware/RP2040/src/Wii/wiimote-lib/wm_reports.h b/Firmware/RP2040/src/Wii/wiimote-lib/wm_reports.h new file mode 100644 index 00000000..8d8d1edb --- /dev/null +++ b/Firmware/RP2040/src/Wii/wiimote-lib/wm_reports.h @@ -0,0 +1,381 @@ +//Portability note: may need modifcation for different targets (endianness, etc.) + +#ifndef WM_REPORTS_H +#define WM_REPORTS_H + +#include "wiimote.h" +#include + +#define OFFSET24(offset32) ((offset32)<<8) + +struct report_data +{ + uint8_t io; + uint8_t type; + uint8_t buf[21]; + uint8_t padding; +} __attribute__((packed)); + +struct report +{ + uint32_t len; //data (packet) length + struct report_data data; +}; + +struct queued_report +{ + struct queued_report * next; + struct report rpt; +}; + +/* Output reports (from controller) */ + +struct report_buttons +{ + int left:1; + int right:1; + int down:1; + int up:1; + int plus:1; + int accel_0:2; + int unused:1; + + int two:1; + int one:1; + int b:1; + int a:1; + int minus:1; + int accel_1:2; + int home:1; +} __attribute__((packed)); + +struct report_accelerometer +{ + struct report_buttons buttons; + + uint8_t x; + uint8_t y; + uint8_t z; +} __attribute__((packed)); + +struct report_ir_basic +{ + uint8_t x1_lo; + uint8_t y1_lo; + + int x2_hi:2; + int y2_hi:2; + int x1_hi:2; + int y1_hi:2; + + uint8_t x2_lo; + uint8_t y2_lo; + + uint8_t x3_lo; + uint8_t y3_lo; + + int x4_hi:2; + int y4_hi:2; + int x3_hi:2; + int y3_hi:2; + + uint8_t x4_lo; + uint8_t y4_lo; +} __attribute__((packed)); + +struct report_ir_ext_obj +{ + uint8_t x_lo; + uint8_t y_lo; + + int size:4; + int x_hi:2; + int y_hi:2; +} __attribute__((packed)); + +struct report_ir_ext +{ + struct report_ir_ext_obj obj[4]; +} __attribute__((packed)); + +struct report_ir_full_obj +{ + uint8_t x_lo; + uint8_t y_lo; + + int size:4; + int x_hi:2; + int y_hi:2; + + int x_min:7; + int unused0:1; + int y_min:7; + int unused1:1; + int x_max:7; + int unused2:1; + int y_max:7; + int unused3:1; + + uint8_t unused4; + uint8_t intensity; +} __attribute__((packed)); + +struct report_interleaved +{ + struct report_buttons buttons; + + uint8_t accel; + + struct report_ir_full_obj obj[2]; +} __attribute__((packed)); + +struct report_ext_nunchuk +{ + uint8_t x; + uint8_t y; + uint8_t accel_x_hi; + uint8_t accel_y_hi; + uint8_t accel_z_hi; + + int z:1; + int c:1; + int accel_x_lo:2; + int accel_y_lo:2; + int accel_z_lo:2; +} __attribute__((packed)); + +struct report_ext_nunchuk_pt +{ + //different format if in passthrough mode with motionplus + uint8_t x; + uint8_t y; + uint8_t accel_x_hi; + uint8_t accel_y_hi; + + int accel_z_hi:7; + int ext:1; + + int unused:2; + int z:1; + int c:1; + int accel_x_lo:1; + int accel_y_lo:1; + int accel_z_lo:2; +} __attribute__((packed)); + +struct report_ext_classic +{ + int lx:6; + int rx_hi:2; + + int ly:6; + int rx_m:2; + + int ry:5; + int lt_hi:2; + int rx_lo:1; + + int rt:5; + int lt_lo:3; + + int unused:1; + int rtrigger:1; + int plus:1; + int home:1; + int minus:1; + int ltrigger:1; + int down:1; + int right:1; + + int up:1; + int left:1; + int rz:1; + int x:1; + int a:1; + int y:1; + int b:1; + int lz:1; +} __attribute__((packed)); + +struct report_ext_classic_pt +{ + //different format if in passthrough mode with motionplus + int up:1; + int lx:5; + int rx_hi:2; + + int left:1; + int ly:5; + int rx_m:2; + + int ry:5; + int lt_hi:2; + int rx_lo:1; + + int lt_lo:3; + int rt:5; + + int ext:1; + int rtrigger:1; + int plus:1; + int home:1; + int minus:1; + int ltrigger:1; + int down:1; + int right:1; + + int unused:2; + int rz:1; + int x:1; + int a:1; + int y:1; + int b:1; + int lz:1; +} __attribute__((packed)); + +struct report_ext_motionplus +{ + uint8_t yaw_lo; + uint8_t roll_lo; + uint8_t pitch_lo; + + int pitch_slow:1; + int yaw_slow:1; + int yaw_hi:6; + + int ext:1; + int roll_slow:1; + int roll_hi:6; + + int unused_1:1; + int unused_0:1; + int pitch_hi:6; +} __attribute__((packed)); + +struct report_status +{ + struct report_buttons buttons; + + int low_battery:1; + int extension_connected:1; + int speaker_enabled:1; + int ircam_enabled:1; + int led_1:1; + int led_2:1; + int led_3:1; + int led_4:1; + + uint8_t unused0; + uint8_t unused1; + + uint8_t battery_level; +} __attribute__((packed)); + +struct report_mem_resp +{ + struct report_buttons buttons; + + int error:4; + int size:4; + + uint16_t addr; //big endian + + uint8_t data[16]; +} __attribute__((packed)); + +struct report_ack +{ + struct report_buttons buttons; + + uint8_t report; + uint8_t result; +} __attribute__((packed)); + +/* Input reports (from wii) */ + +struct report_rumble +{ + int rumble:1; + int unused:7; +} __attribute__((packed)); + +struct report_leds +{ + int rumble:1; + int unused:3; + int led_1:1; + int led_2:1; + int led_3:1; + int led_4:1; +} __attribute__((packed)); + +struct report_mode +{ + int rumble:1; + int unused1:1; + int continuous:1; + int unused0:5; + + uint8_t mode; +} __attribute__((packed)); + +struct report_ir_enable +{ + int rumble:1; + int ack_requested:1; + int enabled:1; + int unused0:5; +} __attribute__((packed)); + +struct report_speaker_enable +{ + int rumble:1; + int unused1:1; + int muted:1; + int unused0:5; +} __attribute__((packed)); + +struct report_mem_read +{ + int rumble:1; + int unused1:1; + int source0:1; + int source1:1; + int unused0:4; + + uint32_t offset:24; //big endian + uint16_t size; //big endian +} __attribute__((packed)); + +struct report_mem_write +{ + int rumble:1; + int unused1:1; + int source0:1; + int source1:1; + int unused0:4; + + uint32_t offset:24; //big endian + uint8_t size; + + uint8_t data[16]; +} __attribute__((packed)); + +struct report * report_queue_push(struct wiimote_state * state); +struct report * report_queue_peek(struct wiimote_state * state); +void report_queue_pop(struct wiimote_state * state); + +void report_queue_push_ack(struct wiimote_state *state, uint8_t report, uint8_t result); +void report_queue_push_status(struct wiimote_state * state); + +void report_format_mem_resp(struct wiimote_state * state, struct report * rpt, + int size, int error, uint16_t addr, uint8_t * buf, bool encrypt); + +void report_append_buttons(struct wiimote_state * state, uint8_t * buf); +void report_append_accelerometer(struct wiimote_state * state, uint8_t * buf); +void report_append_ir_10(struct wiimote_state * state, uint8_t * buf); +void report_append_ir_12(struct wiimote_state * state, uint8_t * buf); +void report_append_interleaved(struct wiimote_state * state, uint8_t * buf); +void report_append_extension(struct wiimote_state * state, uint8_t * buf, uint8_t bytes); + +#endif diff --git a/Firmware/RP2040/src/btstack_config.h b/Firmware/RP2040/src/btstack_config.h index c4f404e5..24c6004e 100644 --- a/Firmware/RP2040/src/btstack_config.h +++ b/Firmware/RP2040/src/btstack_config.h @@ -27,7 +27,7 @@ #define MAX_NR_GATT_CLIENTS 4 #define MAX_NR_HCI_CONNECTIONS 4 #define MAX_NR_HID_HOST_CONNECTIONS 4 -#define MAX_NR_HIDS_CLIENTS 4 +#define MAX_NR_HIDS_HOSTS 4 #define MAX_NR_HFP_CONNECTIONS 1 #define MAX_NR_L2CAP_CHANNELS 6 #define MAX_NR_L2CAP_SERVICES 5 @@ -72,6 +72,9 @@ #define HCI_RESET_RESEND_TIMEOUT_MS 1000 #define ENABLE_SOFTWARE_AES128 +/* Steam Controller 2026 (and many modern BLE pads) reject legacy SMP with + * AUTH_REQUIREMENTS_MISMATCH unless LE Secure Connections is available. */ +#define ENABLE_LE_SECURE_CONNECTIONS #define ENABLE_MICRO_ECC_FOR_LE_SECURE_CONNECTIONS #define HAVE_BTSTACK_STDIN diff --git a/Firmware/RP2040/src/main.cpp b/Firmware/RP2040/src/main.cpp index afdb49d4..0fb12b1b 100644 --- a/Firmware/RP2040/src/main.cpp +++ b/Firmware/RP2040/src/main.cpp @@ -1,8 +1,34 @@ #include +#include #include "OGXMini/OGXMini.h" +#include "pico/stdlib.h" +#include "pico/flash.h" +#include "hardware/uart.h" +#include "hardware/gpio.h" + +#ifndef PICO_DEFAULT_UART_BAUD_RATE +#define PICO_DEFAULT_UART_BAUD_RATE 115200 +#endif int main() { + stdio_init_all(); + +#if defined(PICO_DEFAULT_UART_TX_PIN) && defined(PICO_DEFAULT_UART_RX_PIN) + // Use build-defined UART pins (e.g. Debug: GP4/GP5 UART1 for Pico W/2W) + uart_inst_t* uart = (PICO_DEFAULT_UART == 0) ? uart0 : uart1; + uart_init(uart, PICO_DEFAULT_UART_BAUD_RATE); + gpio_set_function(PICO_DEFAULT_UART_TX_PIN, GPIO_FUNC_UART); + gpio_set_function(PICO_DEFAULT_UART_RX_PIN, GPIO_FUNC_UART); +#else + uart_init(uart0, PICO_DEFAULT_UART_BAUD_RATE); + gpio_set_function(0, GPIO_FUNC_UART); + gpio_set_function(1, GPIO_FUNC_UART); +#endif + printf("Debug ready\n"); + + flash_safe_execute_core_init(); + OGXMini::initialize(); OGXMini::run(); return 0; diff --git a/Firmware/RP2040/src/sdkconfig.h b/Firmware/RP2040/src/sdkconfig.h index cb3ffce6..88b61d71 100644 --- a/Firmware/RP2040/src/sdkconfig.h +++ b/Firmware/RP2040/src/sdkconfig.h @@ -6,7 +6,12 @@ // // Emulate "menuconfig" // +/* Joy-Con L+R pair needs two BLE connections while USB may still expose one player. */ +#if MAX_GAMEPADS >= 2 #define CONFIG_BLUEPAD32_MAX_DEVICES MAX_GAMEPADS +#else +#define CONFIG_BLUEPAD32_MAX_DEVICES 2 +#endif #define CONFIG_BLUEPAD32_MAX_ALLOWLIST 4 #define CONFIG_BLUEPAD32_GAP_SECURITY 1 #define CONFIG_BLUEPAD32_ENABLE_BLE_BY_DEFAULT 1 @@ -15,7 +20,11 @@ #define CONFIG_BLUEPAD32_PLATFORM_CUSTOM #define CONFIG_TARGET_PICO_W -// 2 == Info +// 2 == Info (Debug builds only; Release stays silent) +#if defined(CONFIG_OGXM_DEBUG) +#define CONFIG_BLUEPAD32_LOG_LEVEL 2 +#else #define CONFIG_BLUEPAD32_LOG_LEVEL 0 +#endif #endif //_SDK_CONFIG_H_ \ No newline at end of file diff --git a/Firmware/RP2040/src/tusb_config.h b/Firmware/RP2040/src/tusb_config.h index 2021a814..f0fa0b0a 100644 --- a/Firmware/RP2040/src/tusb_config.h +++ b/Firmware/RP2040/src/tusb_config.h @@ -104,7 +104,8 @@ //------------- CLASS -------------// #define CFG_TUD_CDC 1 #define CFG_TUD_MSC 0 -#define CFG_TUD_HID MAX_GAMEPADS +/* +1 HID interface for Steam mode composite mouse (unused by other output drivers). */ +#define CFG_TUD_HID (MAX_GAMEPADS + 1) #define CFG_TUD_MIDI 0 #define CFG_TUD_VENDOR 0 #define CFG_TUD_XID 1 @@ -148,7 +149,7 @@ #define CFG_TUH_MSC 0 #define CFG_TUH_VENDOR 0 -#define CFG_TUH_XINPUT MAX_GAMEPADS +#define CFG_TUH_XINPUT OGXM_TUH_XINPUT_INSTANCES // max device support (excluding hub device) #define CFG_TUH_DEVICE_MAX (CFG_TUH_HUB ? 4 : 1) // hub typically has 4 ports @@ -156,6 +157,9 @@ #define CFG_TUH_HID_EPIN_BUFSIZE 64 #define CFG_TUH_HID_EPOUT_BUFSIZE 64 +/* Required for Switch 2 Pro bulk bring-up (tuh_edpt_xfer completion callbacks). */ +#define CFG_TUH_API_EDPT_XFER 1 + #endif //defined(CONFIG_EN_USB_HOST) #ifdef __cplusplus diff --git a/Firmware/cmake/init_submodules.cmake b/Firmware/cmake/init_submodules.cmake index d7ff9852..61300cb3 100644 --- a/Firmware/cmake/init_submodules.cmake +++ b/Firmware/cmake/init_submodules.cmake @@ -14,7 +14,7 @@ function(init_git_submodules EXTERNAL_DIR SUBMODULE_PATHS) if(INIT_SUBMODULE_RESULT EQUAL 0) message(STATUS "Submodules initialized successfully.") else() - message(FATAL_ERROR "Failed to initialize submodules: ${INIT_SUBMODULES_ERROR}") + message(FATAL_ERROR "Failed to initialize submodules: ${INIT_SUBMODULE_ERROR}") endif() endforeach() endfunction() \ No newline at end of file diff --git a/Firmware/cmake/patch_libs.cmake b/Firmware/cmake/patch_libs.cmake index d991aea0..bd788691 100644 --- a/Firmware/cmake/patch_libs.cmake +++ b/Firmware/cmake/patch_libs.cmake @@ -20,6 +20,23 @@ function(apply_lib_patches EXTERNAL_DIR) message(FATAL_ERROR "Failed to apply BTStack patch: ${BTSTACK_PATCH_ERROR}") endif () + set(BTSTACK_HIDS_REPORTS_PATCH "${EXTERNAL_DIR}/patches/btstack_hids_num_reports.diff") + message(STATUS "Applying BTStack HIDS reports patch: ${BTSTACK_HIDS_REPORTS_PATCH}") + execute_process( + COMMAND git apply --ignore-whitespace ${BTSTACK_HIDS_REPORTS_PATCH} + WORKING_DIRECTORY ${BTSTACK_PATH} + RESULT_VARIABLE BTSTACK_HIDS_REPORTS_RESULT + OUTPUT_VARIABLE BTSTACK_HIDS_REPORTS_OUTPUT + ERROR_VARIABLE BTSTACK_HIDS_REPORTS_ERROR + ) + if (BTSTACK_HIDS_REPORTS_RESULT EQUAL 0) + message(STATUS "BTStack HIDS reports patch applied successfully.") + elseif (BTSTACK_HIDS_REPORTS_ERROR MATCHES "patch does not apply") + message(STATUS "BTStack HIDS reports patch already applied.") + else () + message(FATAL_ERROR "Failed to apply BTStack HIDS reports patch: ${BTSTACK_HIDS_REPORTS_ERROR}") + endif () + set(BLUEPAD32_PATCH "${EXTERNAL_DIR}/patches/bluepad32_uni.diff") set(BLUEPAD32_PATH "${EXTERNAL_DIR}/bluepad32") @@ -40,4 +57,27 @@ function(apply_lib_patches EXTERNAL_DIR) else () message(FATAL_ERROR "Failed to apply Bluepad32 patch: ${BLUEPAD32_PATCH_ERROR}") endif () -endfunction() \ No newline at end of file + + # Pico SDK 2.1.x still lists BTstack's old hids_client.c; Bluepad32's BTstack + # v1.8 renamed it to hids_host.c. Patch the SDK cmake when using that tree. + set(PICO_SDK_HIDS_PATCH "${EXTERNAL_DIR}/patches/pico_sdk_hids_host.diff") + set(PICO_SDK_PATH_LOCAL "${EXTERNAL_DIR}/pico-sdk") + if (EXISTS "${PICO_SDK_PATH_LOCAL}/src/rp2_common/pico_btstack/CMakeLists.txt") + message(STATUS "Applying Pico SDK HIDS host patch: ${PICO_SDK_HIDS_PATCH}") + execute_process( + COMMAND git apply --ignore-whitespace ${PICO_SDK_HIDS_PATCH} + WORKING_DIRECTORY ${PICO_SDK_PATH_LOCAL} + RESULT_VARIABLE PICO_SDK_HIDS_PATCH_RESULT + OUTPUT_VARIABLE PICO_SDK_HIDS_PATCH_OUTPUT + ERROR_VARIABLE PICO_SDK_HIDS_PATCH_ERROR + ) + if (PICO_SDK_HIDS_PATCH_RESULT EQUAL 0) + message(STATUS "Pico SDK HIDS host patch applied successfully.") + elseif (PICO_SDK_HIDS_PATCH_ERROR MATCHES "patch does not apply") + message(STATUS "Pico SDK HIDS host patch already applied.") + else () + message(FATAL_ERROR "Failed to apply Pico SDK HIDS host patch: ${PICO_SDK_HIDS_PATCH_ERROR}") + endif () + endif () + +endfunction() diff --git a/Firmware/docs/Submodule_forks.md b/Firmware/docs/Submodule_forks.md new file mode 100644 index 00000000..dbd8e2a1 --- /dev/null +++ b/Firmware/docs/Submodule_forks.md @@ -0,0 +1,28 @@ +# Pushing submodule forks (bluepad32 & tinyusb) + +`.gitmodules` points **bluepad32** and **tinyusb** at **MegaCadeDev** forks so OGX-Mini patches are fetchable. If those repos do not exist yet: + +1. On GitHub, **fork** [ricardoquesada/bluepad32](https://github.com/ricardoquesada/bluepad32) → `MegaCadeDev/bluepad32`. +2. **Fork** [hathach/tinyusb](https://github.com/hathach/tinyusb) → `MegaCadeDev/tinyusb`. + +Then push the patched branches **before** (or right after) pushing **OGX-Mini-2026**: + +```bash +# From repo root — bluepad32 (branch ogxm-mini-2026) +cd Firmware/external/bluepad32 +git remote add megacade https://github.com/MegaCadeDev/bluepad32.git 2>/dev/null || true +git push megacade ogxm-mini-2026:main + +# tinyusb +cd ../tinyusb +git remote add megacade https://github.com/MegaCadeDev/tinyusb.git 2>/dev/null || true +git push megacade ogxm-mini-2026:main +``` + +If `main` on the fork rejects (non-fast-forward), use a named branch instead, e.g. `git push megacade ogxm-mini-2026:ogxm-mini-2026`, then set the fork’s default branch to that or merge via PR. + +After forks are updated, clone with: + +```bash +git clone --recursive https://github.com/MegaCadeDev/OGX-Mini-2026.git +``` diff --git a/Firmware/external/bluepad32 b/Firmware/external/bluepad32 index 2152d0b7..9c95e43a 160000 --- a/Firmware/external/bluepad32 +++ b/Firmware/external/bluepad32 @@ -1 +1 @@ -Subproject commit 2152d0b7aba0b7cf5f54146e56b21c35dbb87024 +Subproject commit 9c95e43a87d3bd8a68565da0836d8a758bd8d8af diff --git a/Firmware/external/libxsm3/.gitignore b/Firmware/external/libxsm3/.gitignore new file mode 100644 index 00000000..496ee2ca --- /dev/null +++ b/Firmware/external/libxsm3/.gitignore @@ -0,0 +1 @@ +.DS_Store \ No newline at end of file diff --git a/Firmware/external/libxsm3/LICENSE.txt b/Firmware/external/libxsm3/LICENSE.txt new file mode 100644 index 00000000..f166cc57 --- /dev/null +++ b/Firmware/external/libxsm3/LICENSE.txt @@ -0,0 +1,502 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random Hacker. + + , 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! \ No newline at end of file diff --git a/Firmware/external/libxsm3/README.md b/Firmware/external/libxsm3/README.md new file mode 100644 index 00000000..c592cee1 --- /dev/null +++ b/Firmware/external/libxsm3/README.md @@ -0,0 +1,59 @@ +# libxsm3 + +A "library" for completing Xbox Security Method 3 challenges used by the Xbox 360 video game console to authenticate controllers. + +## What this does + +This library allows an Xbox 360 controller emulator to authenticate with retail consoles. + +## TODO + +- Make sure this is able to work on embedded platforms (e.g. Pi Pico). +- Document more of the values used in the identification packet. +- Unit tests and being able to verify packets from a controller to be valid. + +## Uses + +libxsm3 is used in the [Santroller](https://github.com/santroller/santroller) and [portal_of_flipper](https://github.com/sanjay900/portal_of_flipper) projects. + +## Credits + +- [oct0xor](https://github.com/oct0xor) for reversing, documenting and implementing much of the process. ([blog post](https://oct0xor.github.io/2017/05/03/xsm3/), [source code](https://github.com/oct0xor/xbox_security_method_3)) +- [emoose](https://github.com/emoose) for reimplementing many of the XeCrypt functions in [ExCrypt](https://github.com/emoose/ExCrypt). +- [sanjay900](https://github.com/sanjay900) and an anonymous contributor for helping discover the retail keys. + +## License + +libxsm3 is licensed under the GNU Lesser General Public License version 2.1, or (at your option) any later version. + +This project uses [ExCrypt](https://github.com/emoose/ExCrypt), licensed under the [3-clause BSD license](https://github.com/emoose/ExCrypt/blob/b2e037c3102de22d1107d1e362df4ce407d964ac/LICENSE) - all files included are prefixed with "excrypt". + +## Example + +Being used in a handler for setup requests for the XSM3 interface descriptor + +```c +#include "xsm3.h" +void xsm3_setup_request_handler(usb_setup_request* ctrl) { + switch (ctrl->bRequest) { + case 0x81: + xsm3_initialise_state(); + xsm3_set_identification_data(xsm3_id_data_ms_controller); + ctrl->send_data(xsm3_id_data_ms_controller); + break; + case 0x82: + xsm3_do_challenge_init(ctrl->in_data); + break; + case 0x87: + xsm3_do_challenge_verify(ctrl->in_data); + break; + case 0x83: + ctrl->send_data(xsm3_challenge_response); + break; + case 0x86: + short state = 2; // 1 = in-progress, 2 = complete + ctrl->send_data(&state); + break; + } +} +``` diff --git a/Firmware/external/libxsm3/excrypt.h b/Firmware/external/libxsm3/excrypt.h new file mode 100644 index 00000000..47049c72 --- /dev/null +++ b/Firmware/external/libxsm3/excrypt.h @@ -0,0 +1,39 @@ +#ifndef EXCRYPT_H_ +#define EXCRYPT_H_ + +#ifdef __cplusplus +extern "C" { +#endif +#include + +#if(__BYTE_ORDER__ == __ORDER_BIG_ENDIAN__) +#define SWAP16(i) i +#define SWAP32(i) i +#define SWAP64(i) i +#else +#define SWAP16(i) ((((i) & 0xFF) << 8 | ((i) >> 8) & 0xFF) & 0xFFFF) +#define SWAP32(i) (((i) & 0xff) << 24 | ((i) & 0xff00) << 8 | ((i) & 0xff0000) >> 8 | ((i) >> 24) & 0xff) +#define SWAP64(i) (SWAP32((i) & 0xFFFFFFFF) << 32 | SWAP32(((i) >> 32) & 0xFFFFFFFF)) +#endif + +#define U8V(data) ((uint8_t)(data) & 0xFF) +#define ROTL8(data, bits) (U8V((data) << (bits)) | ((data) >> (8 - (bits)))) + +#define U16V(data) ((uint16_t)(data) & 0xFFFF) +#define ROTL16(data, bits) (U16V((data) << (bits)) | ((data) >> (16 - (bits)))) + +#define U32V(data) ((uint32_t)(data) & 0xFFFFFFFF) +#define ROTL32(data, bits) (U32V((data) << (bits)) | ((data) >> (32 - (bits)))) + +#define ROTL64(data, bits) (((data) << (bits)) | ((data) >> (64 - (bits)))) + +typedef int BOOL; + +#include "excrypt_des.h" +#include "excrypt_sha.h" +#include "excrypt_parve.h" +#ifdef __cplusplus +} +#endif + +#endif // EXCRYPT_H_ \ No newline at end of file diff --git a/Firmware/external/libxsm3/excrypt_des.c b/Firmware/external/libxsm3/excrypt_des.c new file mode 100644 index 00000000..a8c27599 --- /dev/null +++ b/Firmware/external/libxsm3/excrypt_des.c @@ -0,0 +1,198 @@ +#include "excrypt.h" +#include "excrypt_des_data.h" +#include + +#define LODWORD(_qw) ((uint32_t)(_qw)) +#define HIDWORD(_qw) ((uint32_t)(((_qw) >> 32) & 0xffffffff)) + +// DES code based on https://github.com/fffaraz/cppDES + +void ExCryptDesParity(const uint8_t* input, uint32_t input_size, uint8_t* output) +{ + for (uint32_t i = 0; i < input_size; i++) + { + uint8_t parity = input[i]; + + parity ^= parity >> 4; + parity ^= parity >> 2; + parity ^= parity >> 1; + + output[i] = (input[i] & 0xFE) | (~parity & 1); + } +} + +void ExCryptDesKey(EXCRYPT_DES_STATE* state, const uint8_t* key) +{ + uint64_t qkey = SWAP64(*(const uint64_t*)key); + + // initial key schedule calculation + uint64_t permuted_choice_1 = 0; // 56 bits + for (int i = 0; i < 56; i++) + { + permuted_choice_1 <<= 1; + permuted_choice_1 |= (qkey >> (64 - PC1[i])) & LB64_MASK; + } + + // 28 bits + uint32_t C = (uint32_t)((permuted_choice_1 >> 28) & 0x000000000fffffff); + uint32_t D = (uint32_t)(permuted_choice_1 & 0x000000000fffffff); + + // Calculation of the 16 keys + for (int i = 0; i < 16; i++) + { + // key schedule, shifting Ci and Di + for (int j = 0; j < ITERATION_SHIFT[i]; j++) + { + C = (0x0fffffff & (C << 1)) | (0x00000001 & (C >> 27)); + D = (0x0fffffff & (D << 1)) | (0x00000001 & (D >> 27)); + } + + uint64_t permuted_choice_2 = (((uint64_t)C) << 28) | (uint64_t)D; + + uint64_t sub_key = 0; // 48 bits (2*24) + for (int j = 0; j < 48; j++) + { + sub_key <<= 1; + sub_key |= (permuted_choice_2 >> (56 - PC2[j])) & LB64_MASK; + } + state->keytab[i] = sub_key; + } +} + +uint32_t f(uint32_t R, uint64_t k) +{ + // applying expansion permutation and returning 48-bit data + uint64_t s_input = 0; + for (int i = 0; i < 48; i++) + { + s_input <<= 1; + s_input |= (uint64_t)((R >> (32 - EXPANSION[i])) & LB32_MASK); + } + + // XORing expanded Ri with Ki, the round key + s_input = s_input ^ k; + + // applying S-Boxes function and returning 32-bit data + uint32_t s_output = 0; + for (int i = 0; i < 8; i++) + { + // Outer bits + char row = (char)((s_input & (0x0000840000000000 >> 6 * i)) >> (42 - 6 * i)); + row = (row >> 4) | (row & 0x01); + + // Middle 4 bits of input + char column = (char)((s_input & (0x0000780000000000 >> 6 * i)) >> (43 - 6 * i)); + + s_output <<= 4; + s_output |= (uint32_t)(SBOX[i][16 * row + column] & 0x0f); + } + + // applying the round permutation + uint32_t f_result = 0; + for (int i = 0; i < 32; i++) + { + f_result <<= 1; + f_result |= (s_output >> (32 - PBOX[i])) & LB32_MASK; + } + + return f_result; +} + +void feistel(uint32_t* L, uint32_t* R, uint32_t F) +{ + uint32_t temp = *R; + *R = *L ^ F; + *L = temp; +} + +void ExCryptDesEcb(const EXCRYPT_DES_STATE* state, const uint8_t* input, uint8_t* output, uint8_t encrypt) +{ + uint64_t block; + memcpy(&block, input, sizeof(uint64_t)); + block = SWAP64(block); + //uint64_t block = SWAP64(*(uint64_t*)input) + + + // initial permutation + uint64_t result = 0; + for (int i = 0; i < 64; i++) + { + result <<= 1; + result |= (block >> (64 - IP[i])) & LB64_MASK; + } + + // dividing T' into two 32-bit parts + uint32_t L = HIDWORD(result); + uint32_t R = LODWORD(result); + + // 16 rounds + for (int i = 0; i < 16; i++) + { + uint32_t F = !encrypt ? f(R, state->keytab[15 - i]) : f(R, state->keytab[i]); + feistel(&L, &R, F); + } + + // swapping the two parts + block = (((uint64_t)R) << 32) | (uint64_t)L; + + // inverse initial permutation + result = 0; + for (int i = 0; i < 64; i++) + { + result <<= 1; + result |= (block >> (64 - FP[i])) & LB64_MASK; + } + result = SWAP64(result); + memcpy(output, &result, sizeof(result)); +} + +void ExCryptDes3Key(EXCRYPT_DES3_STATE* state, const uint64_t* keys) +{ + ExCryptDesKey(&state->des_state[0], (const uint8_t*)& keys[0]); + ExCryptDesKey(&state->des_state[1], (const uint8_t*)& keys[1]); + ExCryptDesKey(&state->des_state[2], (const uint8_t*)& keys[2]); +} + +void ExCryptDes3Ecb(const EXCRYPT_DES3_STATE* state, const uint8_t* input, uint8_t* output, uint8_t encrypt) +{ + if (encrypt) + { + ExCryptDesEcb(&state->des_state[0], input, output, encrypt); + ExCryptDesEcb(&state->des_state[1], output, output, !encrypt); + ExCryptDesEcb(&state->des_state[2], output, output, encrypt); + } + else + { + ExCryptDesEcb(&state->des_state[2], input, output, encrypt); + ExCryptDesEcb(&state->des_state[1], output, output, !encrypt); + ExCryptDesEcb(&state->des_state[0], output, output, encrypt); + } +} + +void ExCryptDes3Cbc(const EXCRYPT_DES3_STATE* state, const uint8_t* input, uint32_t input_size, uint8_t* output, uint8_t* feed, uint8_t encrypt) +{ + uint64_t last_block = *(uint64_t*)feed; + for (uint32_t i = 0; i < input_size / 8; i++) + { + if (encrypt) { + uint64_t temp; + memcpy(&temp, input, sizeof(temp)); + temp = temp ^ last_block; + memcpy(output, &temp, sizeof(temp)); + ExCryptDes3Ecb(state, output, output, encrypt); + memcpy(&last_block, output, sizeof(last_block)); + } + else + { + ExCryptDes3Ecb(state, input, output, encrypt); + uint64_t temp; + memcpy(&temp, output, sizeof(temp)); + temp = temp ^ last_block; + memcpy(output, &temp, sizeof(temp)); + memcpy(&last_block, input, sizeof(last_block)); + } + input += 8; + output += 8; + } + *(uint64_t*)feed = last_block; +} diff --git a/Firmware/external/libxsm3/excrypt_des.h b/Firmware/external/libxsm3/excrypt_des.h new file mode 100644 index 00000000..f4b3ee17 --- /dev/null +++ b/Firmware/external/libxsm3/excrypt_des.h @@ -0,0 +1,24 @@ +#ifndef EXCRYPT_DES_H_ +#define EXCRYPT_DES_H_ +// DES & 3DES functions + +typedef struct _EXCRYPT_DES_STATE +{ + uint64_t keytab[16]; +} EXCRYPT_DES_STATE; + +void ExCryptDesParity(const uint8_t* input, uint32_t input_size, uint8_t* output); + +void ExCryptDesKey(EXCRYPT_DES_STATE* state, const uint8_t* key); +void ExCryptDesEcb(const EXCRYPT_DES_STATE* state, const uint8_t* input, uint8_t* output, uint8_t encrypt); + +typedef struct _EXCRYPT_DES3_STATE +{ + EXCRYPT_DES_STATE des_state[3]; +} EXCRYPT_DES3_STATE; + +void ExCryptDes3Key(EXCRYPT_DES3_STATE* state, const uint64_t* keys); +void ExCryptDes3Ecb(const EXCRYPT_DES3_STATE* state, const uint8_t* input, uint8_t* output, uint8_t encrypt); +void ExCryptDes3Cbc(const EXCRYPT_DES3_STATE* state, const uint8_t* input, uint32_t input_size, uint8_t* output, uint8_t* feed, uint8_t encrypt); + +#endif // EXCRYPT_DES_H_ \ No newline at end of file diff --git a/Firmware/external/libxsm3/excrypt_des_data.h b/Firmware/external/libxsm3/excrypt_des_data.h new file mode 100644 index 00000000..d585e875 --- /dev/null +++ b/Firmware/external/libxsm3/excrypt_des_data.h @@ -0,0 +1,157 @@ +#ifndef EXCRYPT_DES_DATA_H_ +#define EXCRYPT_DES_DATA_H_ +// Data needed by DES/3DES functions +// (only included by excrypt_des.c - no headers should include this!) + +#define LB32_MASK 0x00000001 +#define LB64_MASK 0x0000000000000001 +#define L64_MASK 0x00000000ffffffff + +// Initial Permutation Table [8*8] +static const char IP[] = +{ + 58, 50, 42, 34, 26, 18, 10, 2, + 60, 52, 44, 36, 28, 20, 12, 4, + 62, 54, 46, 38, 30, 22, 14, 6, + 64, 56, 48, 40, 32, 24, 16, 8, + 57, 49, 41, 33, 25, 17, 9, 1, + 59, 51, 43, 35, 27, 19, 11, 3, + 61, 53, 45, 37, 29, 21, 13, 5, + 63, 55, 47, 39, 31, 23, 15, 7 +}; + +// Inverse Initial Permutation Table [8*8] +static const char FP[] = +{ + 40, 8, 48, 16, 56, 24, 64, 32, + 39, 7, 47, 15, 55, 23, 63, 31, + 38, 6, 46, 14, 54, 22, 62, 30, + 37, 5, 45, 13, 53, 21, 61, 29, + 36, 4, 44, 12, 52, 20, 60, 28, + 35, 3, 43, 11, 51, 19, 59, 27, + 34, 2, 42, 10, 50, 18, 58, 26, + 33, 1, 41, 9, 49, 17, 57, 25 +}; + +// Expansion table [6*8] +static const char EXPANSION[] = +{ + 32, 1, 2, 3, 4, 5, + 4, 5, 6, 7, 8, 9, + 8, 9, 10, 11, 12, 13, + 12, 13, 14, 15, 16, 17, + 16, 17, 18, 19, 20, 21, + 20, 21, 22, 23, 24, 25, + 24, 25, 26, 27, 28, 29, + 28, 29, 30, 31, 32, 1 +}; + +// The S-Box tables [8*16*4] +static const char SBOX[8][64] = +{ + { + // S1 + 14, 4, 13, 1, 2, 15, 11, 8, 3, 10, 6, 12, 5, 9, 0, 7, + 0, 15, 7, 4, 14, 2, 13, 1, 10, 6, 12, 11, 9, 5, 3, 8, + 4, 1, 14, 8, 13, 6, 2, 11, 15, 12, 9, 7, 3, 10, 5, 0, + 15, 12, 8, 2, 4, 9, 1, 7, 5, 11, 3, 14, 10, 0, 6, 13 +}, +{ + // S2 + 15, 1, 8, 14, 6, 11, 3, 4, 9, 7, 2, 13, 12, 0, 5, 10, + 3, 13, 4, 7, 15, 2, 8, 14, 12, 0, 1, 10, 6, 9, 11, 5, + 0, 14, 7, 11, 10, 4, 13, 1, 5, 8, 12, 6, 9, 3, 2, 15, + 13, 8, 10, 1, 3, 15, 4, 2, 11, 6, 7, 12, 0, 5, 14, 9 +}, +{ + // S3 + 10, 0, 9, 14, 6, 3, 15, 5, 1, 13, 12, 7, 11, 4, 2, 8, + 13, 7, 0, 9, 3, 4, 6, 10, 2, 8, 5, 14, 12, 11, 15, 1, + 13, 6, 4, 9, 8, 15, 3, 0, 11, 1, 2, 12, 5, 10, 14, 7, + 1, 10, 13, 0, 6, 9, 8, 7, 4, 15, 14, 3, 11, 5, 2, 12 +}, +{ + // S4 + 7, 13, 14, 3, 0, 6, 9, 10, 1, 2, 8, 5, 11, 12, 4, 15, + 13, 8, 11, 5, 6, 15, 0, 3, 4, 7, 2, 12, 1, 10, 14, 9, + 10, 6, 9, 0, 12, 11, 7, 13, 15, 1, 3, 14, 5, 2, 8, 4, + 3, 15, 0, 6, 10, 1, 13, 8, 9, 4, 5, 11, 12, 7, 2, 14 +}, +{ + // S5 + 2, 12, 4, 1, 7, 10, 11, 6, 8, 5, 3, 15, 13, 0, 14, 9, + 14, 11, 2, 12, 4, 7, 13, 1, 5, 0, 15, 10, 3, 9, 8, 6, + 4, 2, 1, 11, 10, 13, 7, 8, 15, 9, 12, 5, 6, 3, 0, 14, + 11, 8, 12, 7, 1, 14, 2, 13, 6, 15, 0, 9, 10, 4, 5, 3 +}, +{ + // S6 + 12, 1, 10, 15, 9, 2, 6, 8, 0, 13, 3, 4, 14, 7, 5, 11, + 10, 15, 4, 2, 7, 12, 9, 5, 6, 1, 13, 14, 0, 11, 3, 8, + 9, 14, 15, 5, 2, 8, 12, 3, 7, 0, 4, 10, 1, 13, 11, 6, + 4, 3, 2, 12, 9, 5, 15, 10, 11, 14, 1, 7, 6, 0, 8, 13 +}, +{ + // S7 + 4, 11, 2, 14, 15, 0, 8, 13, 3, 12, 9, 7, 5, 10, 6, 1, + 13, 0, 11, 7, 4, 9, 1, 10, 14, 3, 5, 12, 2, 15, 8, 6, + 1, 4, 11, 13, 12, 3, 7, 14, 10, 15, 6, 8, 0, 5, 9, 2, + 6, 11, 13, 8, 1, 4, 10, 7, 9, 5, 0, 15, 14, 2, 3, 12 +}, +{ + // S8 + 13, 2, 8, 4, 6, 15, 11, 1, 10, 9, 3, 14, 5, 0, 12, 7, + 1, 15, 13, 8, 10, 3, 7, 4, 12, 5, 6, 11, 0, 14, 9, 2, + 7, 11, 4, 1, 9, 12, 14, 2, 0, 6, 10, 13, 15, 3, 5, 8, + 2, 1, 14, 7, 4, 10, 8, 13, 15, 12, 9, 0, 3, 5, 6, 11 +} +}; + +// Post S-Box permutation [4*8] +static const char PBOX[] = +{ + 16, 7, 20, 21, + 29, 12, 28, 17, + 1, 15, 23, 26, + 5, 18, 31, 10, + 2, 8, 24, 14, + 32, 27, 3, 9, + 19, 13, 30, 6, + 22, 11, 4, 25 +}; + +// Permuted Choice 1 Table [7*8] +static const char PC1[] = +{ + 57, 49, 41, 33, 25, 17, 9, + 1, 58, 50, 42, 34, 26, 18, + 10, 2, 59, 51, 43, 35, 27, + 19, 11, 3, 60, 52, 44, 36, + + 63, 55, 47, 39, 31, 23, 15, + 7, 62, 54, 46, 38, 30, 22, + 14, 6, 61, 53, 45, 37, 29, + 21, 13, 5, 28, 20, 12, 4 +}; + +// Permuted Choice 2 Table [6*8] +static const char PC2[] = +{ + 14, 17, 11, 24, 1, 5, + 3, 28, 15, 6, 21, 10, + 23, 19, 12, 4, 26, 8, + 16, 7, 27, 20, 13, 2, + 41, 52, 31, 37, 47, 55, + 30, 40, 51, 45, 33, 48, + 44, 49, 39, 56, 34, 53, + 46, 42, 50, 36, 29, 32 +}; + +// Iteration Shift Array +static const char ITERATION_SHIFT[] = +{ + // 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 + 1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1 +}; + +#endif // EXCRYPT_DES_DATA_H_ \ No newline at end of file diff --git a/Firmware/external/libxsm3/excrypt_parve.c b/Firmware/external/libxsm3/excrypt_parve.c new file mode 100644 index 00000000..bebb023e --- /dev/null +++ b/Firmware/external/libxsm3/excrypt_parve.c @@ -0,0 +1,78 @@ +#include +#include + +#include "excrypt.h" + +void ExCryptParveEcb(const uint8_t* key, const uint8_t* sbox, const uint8_t* input, uint8_t* output) +{ + uint8_t block[9]; + + memcpy(block, input, 8); + block[8] = block[0]; + + for (int i = 8; i > 0; i--) + { + for (int j = 0; j < 8; j++) + { + uint8_t x = key[j] + block[j] + i; + uint8_t y = sbox[x] + block[j + 1]; + block[j + 1] = ROTL8(y, 1); + } + + block[0] = block[8]; + } + + memcpy(output, block, 8); +} + +void ExCryptParveCbcMac(const uint8_t* key, const uint8_t* sbox, const uint8_t* iv, const uint8_t* input, uint32_t input_size, uint8_t* output) +{ + uint64_t block; + uint64_t temp; + memcpy(&block, iv, 8); + + if (input_size >= 8) + { + for (uint32_t i = 0; i < input_size / 8; i++) + { + memcpy(&temp, input + (i * 8), sizeof(temp)); + block ^= temp; + ExCryptParveEcb(key, sbox, (uint8_t*)&block, (uint8_t*)&block); + } + } + + memcpy(output, &block, 8); +} + +void ExCryptChainAndSumMac(const uint32_t* cd, const uint32_t* ab, const uint32_t* input, uint32_t input_dwords, uint32_t* output) +{ + uint64_t out0 = 0; + uint64_t out1 = 0; + + uint32_t ab0 = SWAP32(ab[0]) % 0x7FFFFFFF; + uint32_t ab1 = SWAP32(ab[1]) % 0x7FFFFFFF; + uint32_t cd0 = SWAP32(cd[0]) % 0x7FFFFFFF; + uint32_t cd1 = SWAP32(cd[1]) % 0x7FFFFFFF; + + for (uint32_t i = 0; i < input_dwords / 2; i++) + { + out0 += (uint64_t)SWAP32(input[0]) * 0xE79A9C1; + out0 = (out0 % 0x7FFFFFFF) * ab0; + out0 += ab1; + out0 = out0 % 0x7FFFFFFF; + + out1 += out0; + + out0 = (uint64_t)(SWAP32(input[1]) + out0) * cd0; + out0 = (out0 % 0x7FFFFFFF) + cd1; + out0 = out0 % 0x7FFFFFFF; + + out1 += out0; + + input += 2; + } + out0 = SWAP32((out0 + ab1) % 0x7FFFFFFF); + out1 = SWAP32((out1 + cd1) % 0x7FFFFFFF); + memcpy(output, &out0, sizeof(uint32_t)); + memcpy(output+1, &out1, sizeof(uint32_t)); +} diff --git a/Firmware/external/libxsm3/excrypt_parve.h b/Firmware/external/libxsm3/excrypt_parve.h new file mode 100644 index 00000000..b7082dfa --- /dev/null +++ b/Firmware/external/libxsm3/excrypt_parve.h @@ -0,0 +1,9 @@ +#ifndef EXCRYPT_PARVE_H_ +#define EXCRYPT_PARVE_H_ +// "Parve" functions, seem to be used during controller auth + +void ExCryptParveEcb(const uint8_t* key, const uint8_t* sbox, const uint8_t* input, uint8_t* output); +void ExCryptParveCbcMac(const uint8_t* key, const uint8_t* sbox, const uint8_t* iv, const uint8_t* input, uint32_t input_size, uint8_t* output); +void ExCryptChainAndSumMac(const uint32_t* cd, const uint32_t* ab, const uint32_t* input, uint32_t input_dwords, uint32_t* output); + +#endif // EXCRYPT_PARVE_H_ \ No newline at end of file diff --git a/Firmware/external/libxsm3/excrypt_sha.c b/Firmware/external/libxsm3/excrypt_sha.c new file mode 100644 index 00000000..a68e3468 --- /dev/null +++ b/Firmware/external/libxsm3/excrypt_sha.c @@ -0,0 +1,155 @@ +#include +#include + +#include "excrypt.h" + +// SHA1 code based on https://github.com/mohaps/TinySHA1 + +void sha1_process_block(EXCRYPT_SHA_STATE* state) +{ + uint32_t w[80]; + for (size_t i = 0; i < 16; i++) { + w[i] = ((uint32_t)state->buffer[i * 4 + 0] << 24); + w[i] |= ((uint32_t)state->buffer[i * 4 + 1] << 16); + w[i] |= ((uint32_t)state->buffer[i * 4 + 2] << 8); + w[i] |= ((uint32_t)state->buffer[i * 4 + 3]); + } + for (size_t i = 16; i < 80; i++) { + w[i] = ROTL32((w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16]), 1); + } + + uint32_t a = state->state[0]; + uint32_t b = state->state[1]; + uint32_t c = state->state[2]; + uint32_t d = state->state[3]; + uint32_t e = state->state[4]; + + for (int i = 0; i < 80; ++i) { + uint32_t f = 0; + uint32_t k = 0; + + if (i < 20) { + f = (b & c) | (~b & d); + k = 0x5A827999; + } + else if (i < 40) { + f = b ^ c ^ d; + k = 0x6ED9EBA1; + } + else if (i < 60) { + f = (b & c) | (b & d) | (c & d); + k = 0x8F1BBCDC; + } + else { + f = b ^ c ^ d; + k = 0xCA62C1D6; + } + uint32_t temp = ROTL32(a, 5) + f + e + k + w[i]; + e = d; + d = c; + c = ROTL32(b, 30); + b = a; + a = temp; + } + + state->state[0] += a; + state->state[1] += b; + state->state[2] += c; + state->state[3] += d; + state->state[4] += e; +} + +void sha1_process_byte(EXCRYPT_SHA_STATE* state, uint8_t octet) +{ + uint32_t offset = state->count++ & 0x3F; + state->buffer[offset] = octet; + if ((state->count & 0x3F) == 0) + { + sha1_process_block(state); + } +} + +void ExCryptShaInit(EXCRYPT_SHA_STATE* state) +{ + state->count = 0; + state->state[0] = 0x67452301; + state->state[1] = 0xEFCDAB89; + state->state[2] = 0x98BADCFE; + state->state[3] = 0x10325476; + state->state[4] = 0xC3D2E1F0; +} + +void ExCryptShaUpdate(EXCRYPT_SHA_STATE* state, const uint8_t* input, uint32_t input_size) +{ + for (uint32_t i = 0; i < input_size; i++) + { + sha1_process_byte(state, input[i]); + } +} + +void ExCryptShaFinal(EXCRYPT_SHA_STATE* state, uint8_t* output, uint32_t output_size) +{ + uint64_t bit_count = (uint64_t)state->count * 8; + + sha1_process_byte(state, 0x80); + + if ((state->count & 0x3F) > 56) + { + while ((state->count & 0x3F) != 0) + { + sha1_process_byte(state, 0); + } + while ((state->count & 0x3F) < 56) + { + sha1_process_byte(state, 0); + } + } + else + { + while ((state->count & 0x3F) < 56) + { + sha1_process_byte(state, 0); + } + } + + sha1_process_byte(state, 0); + sha1_process_byte(state, 0); + sha1_process_byte(state, 0); + sha1_process_byte(state, 0); + + sha1_process_byte(state, (uint8_t)((bit_count >> 24) & 0xFF)); + sha1_process_byte(state, (uint8_t)((bit_count >> 16) & 0xFF)); + sha1_process_byte(state, (uint8_t)((bit_count >> 8) & 0xFF)); + sha1_process_byte(state, (uint8_t)((bit_count) & 0xFF)); + + //sha1_process_block(state); + uint32_t result[5]; + result[0] = SWAP32(state->state[0]); + result[1] = SWAP32(state->state[1]); + result[2] = SWAP32(state->state[2]); + result[3] = SWAP32(state->state[3]); + result[4] = SWAP32(state->state[4]); + memcpy(output, result, 0x14); +} + +void ExCryptSha(const uint8_t* input1, uint32_t input1_size, const uint8_t* input2, uint32_t input2_size, + const uint8_t* input3, uint32_t input3_size, uint8_t* output, uint32_t output_size) +{ + EXCRYPT_SHA_STATE state[1]; + ExCryptShaInit(state); + + if (input1 && input1_size) + { + ExCryptShaUpdate(state, input1, input1_size); + } + if (input2 && input2_size) + { + ExCryptShaUpdate(state, input2, input2_size); + } + if (input3 && input3_size) + { + ExCryptShaUpdate(state, input3, input3_size); + } + + ExCryptShaFinal(state, output, output_size); +} \ No newline at end of file diff --git a/Firmware/external/libxsm3/excrypt_sha.h b/Firmware/external/libxsm3/excrypt_sha.h new file mode 100644 index 00000000..80024d85 --- /dev/null +++ b/Firmware/external/libxsm3/excrypt_sha.h @@ -0,0 +1,18 @@ +#ifndef EXCRYPT_SHA_H_ +#define EXCRYPT_SHA_H_ +// SHA1 hash & HMAC algorithm + +typedef struct _EXCRYPT_SHA_STATE +{ + uint32_t count; + uint32_t state[5]; + uint8_t buffer[64]; +} EXCRYPT_SHA_STATE; + +void ExCryptShaInit(EXCRYPT_SHA_STATE* state); +void ExCryptShaUpdate(EXCRYPT_SHA_STATE* state, const uint8_t* input, uint32_t input_size); +void ExCryptShaFinal(EXCRYPT_SHA_STATE* state, uint8_t* output, uint32_t output_size); +void ExCryptSha(const uint8_t* input1, uint32_t input1_size, const uint8_t* input2, uint32_t input2_size, + const uint8_t* input3, uint32_t input3_size, uint8_t* output, uint32_t output_size); + +#endif // EXCRYPT_SHA_H_ \ No newline at end of file diff --git a/Firmware/external/libxsm3/usbdsec.c b/Firmware/external/libxsm3/usbdsec.c new file mode 100644 index 00000000..d5ab64e1 --- /dev/null +++ b/Firmware/external/libxsm3/usbdsec.c @@ -0,0 +1,153 @@ +/* + usbdsec.c - part of libxsm3 + Copyright (C) 2013 oct0xor + Copyright (C) 2022 InvoxiPlayGames + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include +#include +#include "excrypt.h" + +static uint8_t UsbdSecSboxData[256] __attribute__ ((aligned(4))) = { + 0xB0, 0x3D, 0x9B, 0x70, 0xF3, 0xC7, 0x80, 0x60, + 0x73, 0x9F, 0x6C, 0xC0, 0xF1, 0x3D, 0xBB, 0x40, + 0xB3, 0xC8, 0x37, 0x14, 0xDF, 0x49, 0xDA, 0xD4, + 0x48, 0x22, 0x78, 0x80, 0x6E, 0xCD, 0xE7, 0x00, + 0x81, 0x86, 0x68, 0xE1, 0x5D, 0x7C, 0x54, 0x2C, + 0x55, 0x7B, 0xEF, 0x48, 0x42, 0x7B, 0x3B, 0x68, + 0xE3, 0xDB, 0xAA, 0xC0, 0x0F, 0xA9, 0x96, 0x20, + 0x95, 0x05, 0x93, 0x94, 0x9A, 0xF6, 0xA3, 0x64, + 0x5D, 0xCC, 0x76, 0x00, 0xE5, 0x08, 0x19, 0xE8, + 0x8D, 0x29, 0xD7, 0x4C, 0x21, 0x91, 0x17, 0xF4, + 0xBC, 0x6A, 0xB3, 0x80, 0x83, 0xC6, 0xD4, 0x90, + 0x9B, 0xAE, 0x0E, 0xFE, 0x2E, 0x4A, 0xF2, 0x00, + 0x73, 0x88, 0xD9, 0x40, 0x66, 0xC5, 0xD4, 0x08, + 0x57, 0xB1, 0x89, 0x48, 0xDC, 0x54, 0xFC, 0x43, + 0x6A, 0x26, 0x87, 0xB8, 0x09, 0x5F, 0xCE, 0x80, + 0xE4, 0x0B, 0x05, 0x9C, 0x24, 0xF3, 0xDE, 0xE2, + 0x3E, 0xEC, 0x38, 0x8A, 0xA2, 0x55, 0xA4, 0x50, + 0x4E, 0x4B, 0xE9, 0x58, 0x7F, 0x9F, 0x7D, 0x80, + 0x23, 0x0C, 0x4D, 0x80, 0x05, 0x44, 0x26, 0xB8, + 0xE9, 0xD8, 0xBC, 0xE6, 0x76, 0x3A, 0x6E, 0xA4, + 0x19, 0xDE, 0xC2, 0xD0, 0xC4, 0xBC, 0xC3, 0x5C, + 0x59, 0xDF, 0x16, 0x46, 0x39, 0x70, 0xF4, 0xEE, + 0x2D, 0x58, 0x5A, 0xA8, 0x17, 0x86, 0x6B, 0x60, + 0x29, 0x58, 0x4D, 0xD2, 0x5F, 0x28, 0x7A, 0xD8, + 0x8E, 0x79, 0xEA, 0x82, 0x94, 0x33, 0x31, 0x81, + 0xD9, 0x22, 0xD5, 0x10, 0xDA, 0x92, 0xA0, 0x7D, + 0x3D, 0xDA, 0xAC, 0x1C, 0xA2, 0x53, 0x31, 0xB8, + 0x3C, 0x96, 0x52, 0x00, 0x82, 0x6B, 0x56, 0xA0, + 0xD3, 0xC2, 0x40, 0xC7, 0x1B, 0x7F, 0xDC, 0x01, + 0x72, 0x70, 0xB1, 0x8C, 0x01, 0x09, 0x09, 0x36, + 0xFC, 0x97, 0xEA, 0xDE, 0xE3, 0x0D, 0xAE, 0x7E, + 0xE3, 0x0D, 0xAE, 0x7E, 0x33, 0x69, 0x80, 0x40 +}; + +static uint8_t UsbdSecPlainTextData[128] __attribute__ ((aligned(4))) = { + 0xD1, 0xD2, 0xF2, 0x80, 0x6E, 0xBA, 0x0C, 0xC0, + 0xB6, 0xC4, 0xC9, 0xD8, 0x61, 0x75, 0x1D, 0x1A, + 0x3F, 0x95, 0x58, 0xBE, 0xD8, 0x0D, 0xE2, 0xC0, + 0xD0, 0x21, 0x79, 0x20, 0x65, 0x2D, 0x99, 0x40, + 0x3C, 0x96, 0x52, 0x00, 0x1B, 0x7F, 0xDC, 0x01, + 0x82, 0x1C, 0x13, 0xD8, 0x33, 0x69, 0x80, 0x40, + 0xFC, 0x97, 0xEA, 0xDE, 0x08, 0xEA, 0x14, 0xDC, + 0xEB, 0x0F, 0x6A, 0x18, 0x6F, 0x78, 0x2C, 0xB0, + 0xD3, 0xC2, 0x40, 0xC7, 0x82, 0x6B, 0x56, 0xA0, + 0x19, 0x09, 0x36, 0xE0, 0x72, 0x70, 0xB1, 0x8C, + 0xE3, 0x0D, 0xAE, 0x7E, 0x50, 0xA5, 0x2B, 0xE2, + 0xC9, 0xAF, 0xC7, 0x70, 0x1C, 0x29, 0x80, 0x56, + 0x24, 0xF0, 0x66, 0xFA, 0x02, 0x2B, 0x58, 0x98, + 0x8F, 0xE4, 0xD1, 0x3C, 0x6E, 0x38, 0x2A, 0xFF, + 0xB8, 0xFA, 0x35, 0xB0, 0x52, 0x49, 0xC5, 0xB4, + 0x66, 0xFA, 0x47, 0x55, 0x6C, 0x8D, 0x40, 0x08 +}; + +void UsbdSecXSM3AuthenticationCrypt(const uint8_t *key, const uint8_t *input, size_t length, uint8_t *output, uint8_t encrypt) { + EXCRYPT_DES3_STATE des; + uint64_t sk[3]; + uint8_t iv[8]; + + // clear local variables + memset(iv, 0, sizeof(iv)); + // run parity on the key + ExCryptDesParity(key, 0x10, (uint8_t *)sk); + sk[2] = sk[0]; + // set the key in the state and run triple-des cbc en/decryption on it + ExCryptDes3Key(&des, sk); + ExCryptDes3Cbc(&des, input, length, output, iv, encrypt); +} + +void UsbdSecXSM3AuthenticationMac(const uint8_t *key, uint8_t *salt, uint8_t *input, size_t length, uint8_t *output) { + EXCRYPT_DES3_STATE des3; + EXCRYPT_DES_STATE des; + uint64_t sk[3]; + uint8_t iv[8]; + uint8_t temp[8]; + uint64_t input_temp; + int i; + + // clear iv + temp value of stack junk + memset(iv, 0, sizeof(iv)); + memset(temp, 0, sizeof(temp)); + // run parity on the key + ExCryptDesParity(key, 0x10, (uint8_t *)sk); + sk[2] = sk[0]; + // set the key in our initial des state + ExCryptDesKey(&des, (uint8_t *)&sk[0]); + // if we have a salt, encrypt it into the temp value + if (salt) { + memcpy(&input_temp, salt, sizeof(input_temp)); + input_temp = SWAP64(SWAP64(input_temp) + 1); + memcpy(salt, &input_temp, sizeof(input_temp)); // no idea what this does + ExCryptDesEcb(&des, salt, temp, 1); + } + // for every 8 byte input block, xor the temp value with it and encrypt over itself + for (i = 0; i < length; i += 8) { + memcpy(&input_temp, input+i, sizeof(input_temp)); + *(uint64_t *)temp ^= input_temp; + ExCryptDesEcb(&des, temp, temp, 1); + } + // xor the highest bit of the temp value + temp[0] ^= 0x80; + // set the key and perform the final triple-des encryption + ExCryptDes3Key(&des3, sk); + ExCryptDes3Cbc(&des3, temp, 8, output, iv, 1); + // real kernel does the following, but the above works: + // XeCryptDesEcb(des_state_1, temp, temp, 1); + // XeCryptDesEcb(des_state_2, temp, temp, 0); + // XeCryptDesEcb(des_state_1, temp, output, 1); +} + +void UsbdSecXSMAuthenticationAcr(const uint8_t *console_id, const uint8_t *input, const uint8_t *key, uint8_t *output) { + uint8_t block[8]; + uint8_t iv[8]; + uint8_t ab[8]; + uint8_t cd[8]; + + // fill in the input block with the first 4 bytes of input data and the first 4 bytes of the console ID + memcpy(block, input, 4); + memcpy(block+4, console_id, 4); + // run custom "parve" crypto algorithms. idk whar they do + ExCryptParveEcb(key, UsbdSecSboxData, input + 0x10, iv); + ExCryptParveEcb(key, UsbdSecSboxData, block, cd); + ExCryptParveCbcMac(key, UsbdSecSboxData, iv, UsbdSecPlainTextData, 0x80, ab); + ExCryptChainAndSumMac((uint32_t *)cd, (uint32_t *)ab, (uint32_t *)UsbdSecPlainTextData, 0x20, (uint32_t *)output); + uint64_t current; + memcpy(¤t, output, sizeof(current)); + current ^= *(uint64_t *)ab; + memcpy(output, ¤t, sizeof(current)); +} \ No newline at end of file diff --git a/Firmware/external/libxsm3/usbdsec.h b/Firmware/external/libxsm3/usbdsec.h new file mode 100644 index 00000000..3e647147 --- /dev/null +++ b/Firmware/external/libxsm3/usbdsec.h @@ -0,0 +1,29 @@ +/* + usbdsec.h - part of libxsm3 + Copyright (C) 2022 InvoxiPlayGames + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#ifndef USBDSEC_H_ +#define USBDSEC_H_ + +#include + +void UsbdSecXSM3AuthenticationCrypt(const uint8_t *key, const uint8_t *input, size_t length, uint8_t *output, uint8_t encrypt); +void UsbdSecXSM3AuthenticationMac(const uint8_t *key, const uint8_t *salt, uint8_t *input, size_t length, uint8_t *output); +void UsbdSecXSMAuthenticationAcr(const uint8_t *console_id, const uint8_t *input, const uint8_t *key, uint8_t *output); + +#endif // USBDSEC_H_ \ No newline at end of file diff --git a/Firmware/external/libxsm3/xsm3.c b/Firmware/external/libxsm3/xsm3.c new file mode 100644 index 00000000..f7e1a761 --- /dev/null +++ b/Firmware/external/libxsm3/xsm3.c @@ -0,0 +1,301 @@ +/* + xsm3.c - part of libxsm3 + Copyright (C) 2022-2023 InvoxiPlayGames + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include +#include +#include +#include +#include +#if !defined(XSM3_EMBEDDED) +#include +#endif +#include "xsm3.h" +#if defined(XSM3_EMBEDDED) +#include "pico/rand.h" +#endif +#include "excrypt.h" +#include "usbdsec.h" + +// disable debugging by specifying XSM3_NO_DEBUGGING at compile time +#ifndef XSM3_NO_DEBUGGING +#define XSM3_printf printf +#else +#define XSM3_printf +#endif // XSM3_NO_DEBUGGING + +// constant variables +const uint8_t xsm3_id_data_ms_controller[0x1D] = { + 0x49, 0x4B, 0x00, 0x00, 0x17, 0x41, 0x41, 0x41, + 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, + 0x00, 0x00, 0x80, 0x02, 0x5E, 0x04, 0x8E, 0x02, + 0x03, 0x00, 0x01, 0x01, 0x16 +}; + +// static global keys from the keyvault (shared across every retail system) +static const uint8_t xsm3_key_0x1D[0x10] = { + 0xE3, 0x5B, 0xFB, 0x1C, 0xCD, 0xAD, 0x32, 0x5B, + 0xF7, 0x0E, 0x07, 0xFD, 0x62, 0x3D, 0xA7, 0xC4 +}; +static const uint8_t xsm3_key_0x1E[0x10] = { + 0x8F, 0x29, 0x08, 0x38, 0x0B, 0x5B, 0xFE, 0x68, + 0x7C, 0x26, 0x46, 0x2A, 0x51, 0xF2, 0xBC, 0x19 +}; + +// retail keys for generating 0x23/0x24 keys from console ID +static const uint8_t xsm3_root_key_0x23[0x10] = { + 0x82, 0x80, 0x78, 0x68, 0x3A, 0x52, 0x3A, 0x98, + 0x10, 0xF4, 0x0C, 0x12, 0x70, 0x66, 0xDC, 0xBA +}; +static const uint8_t xsm3_root_key_0x24[0x10] = { + 0x66, 0x62, 0x1A, 0x78, 0xF8, 0x60, 0x9C, 0x8A, + 0x26, 0x9A, 0x04, 0xAE, 0xD8, 0x5C, 0x1E, 0xC8 +}; + +/* +global device keys from devkit keyvaults +static const uint8_t xsm3_key_0x1D[0x10] = { + 0xC2, 0x15, 0xE5, 0x5E, 0xE5, 0x51, 0x94, 0x2A, + 0xEC, 0x3D, 0x45, 0xEC, 0xB6, 0xE6, 0xF2, 0x16 +}; +static const uint8_t xsm3_key_0x1E[0x10] = { + 0xC7, 0x45, 0xAD, 0x1F, 0x08, 0x0B, 0xD9, 0xE9, + 0x9B, 0x1C, 0x34, 0xE3, 0xA4, 0x6D, 0xC8, 0xC4 +}; +*/ + +// devkit recovery keys for generating 0x23/0x24 keys from console ID +// static const uint8_t xsm3_root_key_0x23[0x10] = { +// 0xB9, 0xE0, 0x9E, 0x68, 0x04, 0x83, 0x91, 0xB3, +// 0x32, 0x45, 0x7A, 0xDA, 0x43, 0x6B, 0x80, 0xAD +// }; +// static const uint8_t xsm3_root_key_0x24[0x10] = { +// 0x92, 0x5D, 0x29, 0x6E, 0xB0, 0x61, 0x0B, 0xF1, +// 0xD6, 0x29, 0x3B, 0xC8, 0xC7, 0xD9, 0x32, 0xBC +// }; + +// response to give to the given challenge command +uint8_t xsm3_challenge_response[0x30]; +// console id fetched from challenge init packet +uint8_t xsm3_console_id[0x8]; + +// buffer to store the first key fetched from the KV (0x23) +static uint8_t xsm3_kv_2des_key_1[0x10]; +// buffer to store the second key fetched from the KV (0x24) +static uint8_t xsm3_kv_2des_key_2[0x10]; +// have we been given a KV key yet? +static bool xsm3_has_kv_keys = false; + +// temporary buffer for decrypting packets +static uint8_t xsm3_decryption_buffer[0x30]; +// identification data for the current device +static uint8_t xsm3_identification_data[0x20]; +// random data sent from the console during the challenge init stage +static uint8_t xsm3_random_console_data[0x10]; +static uint8_t xsm3_random_console_data_enc[0x10]; +static uint8_t xsm3_random_console_data_swap[0x10]; +static uint8_t xsm3_random_console_data_swap_enc[0x10]; +// random data set by the controller during the challenge init stage +static uint8_t xsm3_random_controller_data[0x10]; +// hash of the decrypted data sent by the controller during challenge init +static uint8_t xsm3_challenge_init_hash[0x14]; + +void xsm3_initialise_state() { + // set all variables to all zeroes + memset(xsm3_challenge_response, 0, sizeof(xsm3_challenge_response)); + memset(xsm3_console_id, 0, sizeof(xsm3_console_id)); + memset(xsm3_kv_2des_key_1, 0, sizeof(xsm3_kv_2des_key_1)); + memset(xsm3_kv_2des_key_2, 0, sizeof(xsm3_kv_2des_key_2)); + memset(xsm3_decryption_buffer, 0, sizeof(xsm3_decryption_buffer)); + memset(xsm3_identification_data, 0, sizeof(xsm3_identification_data)); + memset(xsm3_random_console_data, 0, sizeof(xsm3_random_console_data)); + memset(xsm3_random_console_data_enc, 0, sizeof(xsm3_random_console_data_enc)); + memset(xsm3_random_console_data_swap, 0, sizeof(xsm3_random_console_data_swap)); + memset(xsm3_random_console_data_swap_enc, 0, sizeof(xsm3_random_console_data_swap_enc)); + memset(xsm3_random_controller_data, 0, sizeof(xsm3_random_controller_data)); + memset(xsm3_challenge_init_hash, 0, sizeof(xsm3_challenge_init_hash)); +} + +static uint8_t xsm3_calculate_checksum(const uint8_t * packet) { + // packet length in header doesn't include the header itself + uint8_t packet_length = packet[0x4] + 0x5; + uint8_t checksum = 0x00; + int i = 0; + // checksum is just a XOR over all packet bytes + for(i = 0x5; i < packet_length; i++) { + checksum ^= packet[i]; + } + // last byte of the packet is the checksum + return checksum; +} + +static bool xsm3_verify_checksum(const uint8_t * packet) { + // packet length in header doesn't include the header itself + uint8_t packet_length = packet[0x4] + 0x5; + // last byte of the packet is the checksum + return (xsm3_calculate_checksum(packet) == packet[packet_length]); +} + +void xsm3_set_identification_data(const uint8_t id_data[0x1D]) { + // validate the checksum + if (!xsm3_verify_checksum(id_data)) { + XSM3_printf("[ Checksum failed when setting identification data! ]\n"); + } + + // skip over the packet header + id_data += 0x5; + + // prepare the xsm3_identification_data buffer + + // contains serial number (len: 0xC), unknown (len: 0x2) and the "category node" to use (len: 0x1) + memcpy(xsm3_identification_data, id_data, 0xF); + // vendor ID + memcpy(xsm3_identification_data + 0x10, id_data + 0xF, sizeof(unsigned short)); + // product ID + memcpy(xsm3_identification_data + 0x12, id_data + 0x11, sizeof(unsigned short)); + // unknown + memcpy(xsm3_identification_data + 0x14, id_data + 0x13, sizeof(unsigned char)); + // unknown + memcpy(xsm3_identification_data + 0x15, id_data + 0x16, sizeof(unsigned char)); + // unknown + memcpy(xsm3_identification_data + 0x16, id_data + 0x14, sizeof(unsigned short)); +} + +void xsm3_import_kv_keys(const uint8_t key1[0x10], const uint8_t key2[0x10]) { + xsm3_has_kv_keys = true; + // copy the provided keys into our buffers + memcpy(xsm3_kv_2des_key_1, key1, sizeof(xsm3_kv_2des_key_1)); + memcpy(xsm3_kv_2des_key_2, key2, sizeof(xsm3_kv_2des_key_2)); +} + +void xsm3_generate_kv_keys(const uint8_t console_id[0x8]) { + xsm3_has_kv_keys = true; + // make a sha-1 hash of the console id + uint8_t console_id_hash[0x14]; + ExCryptSha(console_id, 0x8, NULL, 0, NULL, 0, console_id_hash, 0x14); + // encrypt it with the root keys for 1st party controllers + UsbdSecXSM3AuthenticationCrypt(xsm3_root_key_0x23, console_id_hash, 0x10, xsm3_kv_2des_key_1, 1); + UsbdSecXSM3AuthenticationCrypt(xsm3_root_key_0x24, console_id_hash + 0x4, 0x10, xsm3_kv_2des_key_2, 1); +} + +void xsm3_do_challenge_init(uint8_t challenge_packet[0x22]) { + uint8_t incoming_packet_mac[0x8]; + uint8_t response_packet_mac[0x8]; + int i = 0; + + // validate the checksum + if (!xsm3_verify_checksum(challenge_packet)) { + XSM3_printf("[ Checksum failed when validating challenge init! ]\n"); + } + + // decrypt the packet content using the static key from the keyvault + UsbdSecXSM3AuthenticationCrypt(xsm3_key_0x1D, challenge_packet + 0x5, 0x18, xsm3_decryption_buffer, 0); + // first 0x10 bytes are random data + memcpy(xsm3_random_console_data, xsm3_decryption_buffer, 0x10); + // next 0x8 bytes are from the console certificate + memcpy(xsm3_console_id, xsm3_decryption_buffer + 0x10, 0x8); + // last 4 bytes of the packet are the last 4 bytes of the MAC + UsbdSecXSM3AuthenticationMac(xsm3_key_0x1E, NULL, challenge_packet + 5, 0x18, incoming_packet_mac); + // validate the MAC + if (memcmp(incoming_packet_mac + 4, challenge_packet + 0x5 + 0x18, 0x4) != 0) { + XSM3_printf("[ MAC failed when validating challenge init! ]\n"); + } + + // if we haven't got our KV keys yet, generate it + if (!xsm3_has_kv_keys) xsm3_generate_kv_keys(xsm3_console_id); + + // the random value is swapped at an 8 byte boundary + memcpy(xsm3_random_console_data_swap, xsm3_random_console_data + 0x8, 0x8); + memcpy(xsm3_random_console_data_swap + 0x8, xsm3_random_console_data, 0x8); + // and then encrypted - the regular value encrypted with key 1, the swapped value encrypted with key 2 + UsbdSecXSM3AuthenticationCrypt(xsm3_kv_2des_key_1, xsm3_random_console_data, 0x10, xsm3_random_console_data_enc, 1); + UsbdSecXSM3AuthenticationCrypt(xsm3_kv_2des_key_2, xsm3_random_console_data_swap, 0x10, xsm3_random_console_data_swap_enc, 1); + + // generate random data +#if defined(XSM3_EMBEDDED) + for (i = 0; i < 0x10; i++) { + xsm3_random_controller_data[i] = (uint8_t)(get_rand_32() & 0xFF); + } +#else + srand(time(NULL)); + for (i = 0; i < 0x10; i++) { + xsm3_random_controller_data[i] = rand() & 0xFF; + } +#endif + + // clear response buffers + memset(xsm3_challenge_response, 0, sizeof(xsm3_challenge_response)); + memset(xsm3_decryption_buffer, 0, sizeof(xsm3_decryption_buffer)); + // set header and packet length of challenge response + xsm3_challenge_response[0] = 0x49; // packet magic + xsm3_challenge_response[1] = 0x4C; + xsm3_challenge_response[4] = 0x28; // packet length + // copy random controller, random console data to the encryption buffer + memcpy(xsm3_decryption_buffer, xsm3_random_controller_data, 0x10); + memcpy(xsm3_decryption_buffer + 0x10, xsm3_random_console_data, 0x10); + // save the sha1 hash of the decrypted contents for later + ExCryptSha(xsm3_decryption_buffer, 0x20, NULL, 0, NULL, 0, xsm3_challenge_init_hash, 0x14); + + // encrypt challenge response packet using the encrypted random key + UsbdSecXSM3AuthenticationCrypt(xsm3_random_console_data_enc, xsm3_decryption_buffer, 0x20, xsm3_challenge_response + 0x5, 1); + // calculate MAC using the encrypted swapped random key and use it to calculate ACR + UsbdSecXSM3AuthenticationMac(xsm3_random_console_data_swap_enc, NULL, xsm3_challenge_response + 0x5, 0x20, response_packet_mac); + // calculate ACR and append to the end of the xsm3_challenge_response + UsbdSecXSMAuthenticationAcr(xsm3_console_id, xsm3_identification_data, response_packet_mac, xsm3_challenge_response + 0x5 + 0x20); + // calculate the checksum for the response packet + xsm3_challenge_response[0x5 + 0x28] = xsm3_calculate_checksum(xsm3_challenge_response); + + // the console random value changes slightly after this point + memcpy(xsm3_random_console_data, xsm3_random_controller_data + 0xC, 0x4); + memcpy(xsm3_random_console_data + 0x4, xsm3_random_console_data + 0xC, 0x4); +} + +void xsm3_do_challenge_verify(uint8_t challenge_packet[0x16]) { + uint8_t incoming_packet_mac[0x8]; + + // validate the checksum + if (!xsm3_verify_checksum(challenge_packet)) { + XSM3_printf("[ Checksum failed when validating challenge verify! ]\n"); + } + + // decrypt the packet using the controller generated random value + UsbdSecXSM3AuthenticationCrypt(xsm3_random_controller_data, challenge_packet + 0x5, 0x8, xsm3_decryption_buffer, 0); + // replace part of our random encryption value with the decrypted buffer + memcpy(xsm3_random_console_data + 0x8, xsm3_decryption_buffer, 0x8); + + // calculate the MAC of the incoming packet + UsbdSecXSM3AuthenticationMac(xsm3_challenge_init_hash, xsm3_random_console_data, challenge_packet + 0x5, 0x8, incoming_packet_mac); + // validate the MAC + if (memcmp(incoming_packet_mac, challenge_packet + 0x5 + 0x8, 0x8) != 0) { + XSM3_printf("[ MAC failed when validating challenge verify! ]\n"); + } + // clear response buffers + memset(xsm3_challenge_response, 0, sizeof(xsm3_challenge_response)); + memset(xsm3_decryption_buffer, 0, sizeof(xsm3_decryption_buffer)); + // set header and packet length of challenge response + xsm3_challenge_response[0] = 0x49; // packet magic + xsm3_challenge_response[1] = 0x4C; + xsm3_challenge_response[4] = 0x10; // packet length + // calculate the ACR value and encrypt it into the outgoing packet using the encrypted random + UsbdSecXSMAuthenticationAcr(xsm3_console_id, xsm3_identification_data, xsm3_random_console_data + 0x8, xsm3_decryption_buffer); + UsbdSecXSM3AuthenticationCrypt(xsm3_random_console_data_enc, xsm3_decryption_buffer, 0x8, xsm3_challenge_response + 0x5, 1); + // calculate the MAC of the encrypted packet and append it to the end + UsbdSecXSM3AuthenticationMac(xsm3_random_console_data_swap_enc, xsm3_random_console_data, xsm3_challenge_response + 0x5, 0x8, xsm3_challenge_response + 0x5 + 0x8); + // calculate the checksum for the response packet + xsm3_challenge_response[0x5 + 0x10] = xsm3_calculate_checksum(xsm3_challenge_response); +} diff --git a/Firmware/external/libxsm3/xsm3.h b/Firmware/external/libxsm3/xsm3.h new file mode 100644 index 00000000..93e985b3 --- /dev/null +++ b/Firmware/external/libxsm3/xsm3.h @@ -0,0 +1,51 @@ +/* + xsm3.h - part of libxsm3 + Copyright (C) 2022 InvoxiPlayGames + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#ifndef XSM3_H_ +#define XSM3_H_ + +#include +#include +#include + +// Identification data taken from an official wired controller. (Serial number is static.) +extern const uint8_t xsm3_id_data_ms_controller[0x1D]; + +// The response data from the previously completed challenge. +extern uint8_t xsm3_challenge_response[0x30]; + +// The console ID fetched from the console after request 0x82. +extern uint8_t xsm3_console_id[0x8]; + +// Clears the state of the XSM3 internal variables. +void xsm3_initialise_state(); + +// Sets the identification data to use. +void xsm3_set_identification_data(const uint8_t id_data[0x1D]); + +// Imports the console-specific keys (GetKey index 0x23 and 0x24 for wired controllers) into the current state. +void xsm3_import_kv_keys(const uint8_t key1[0x10], const uint8_t key2[0x10]); + +// Initialises the XSM3 state using information from the challenge init packet (0x82) and places a response in xsm3_challenge_response. +void xsm3_do_challenge_init(uint8_t challenge_packet[0x22]); + +// Completes a verify challenge passed from request 0x87 and places the response data in xsm3_challenge_response. +void xsm3_do_challenge_verify(uint8_t challenge_packet[0x16]); + +#endif // XSM3_H_ \ No newline at end of file diff --git a/Firmware/external/patches/bluepad32_ps4_ps5_latency.diff b/Firmware/external/patches/bluepad32_ps4_ps5_latency.diff new file mode 100644 index 00000000..74357105 --- /dev/null +++ b/Firmware/external/patches/bluepad32_ps4_ps5_latency.diff @@ -0,0 +1,176 @@ +diff --git a/src/components/bluepad32/parser/uni_hid_parser_ds4.c b/src/components/bluepad32/parser/uni_hid_parser_ds4.c +index e2622a2..147d13e 100644 +--- a/src/components/bluepad32/parser/uni_hid_parser_ds4.c ++++ b/src/components/bluepad32/parser/uni_hid_parser_ds4.c +@@ -425,6 +425,43 @@ static void ds4_parse_input_report_01(uni_hid_device_t* d, const ds4_input_repor + ctl->gamepad.throttle = r->throttle * 4; + } + ++/* Parse only gamepad from first 9 bytes of payload when report is short. Ensures we never drop a report (like Xbox One): every packet delivers at least gamepad state. */ ++static void ds4_parse_input_report_11_minimal(uni_hid_device_t* d, const uint8_t* report, uint16_t len) { ++ uni_controller_t* ctl = &d->controller; ++ if (len < 3 + 9) ++ return; ++ const uint8_t* p = &report[3]; ++ ctl->gamepad.axis_x = (p[0] - 127) * 4; ++ ctl->gamepad.axis_y = (p[1] - 127) * 4; ++ ctl->gamepad.axis_rx = (p[2] - 127) * 4; ++ ctl->gamepad.axis_ry = (p[3] - 127) * 4; ++ uint8_t value = p[4] & 0xf; ++ if (value > 7) ++ value = 0xff; ++ ctl->gamepad.dpad = uni_hid_parser_hat_to_dpad(value); ++ ctl->gamepad.buttons = 0; ++ ctl->gamepad.misc_buttons = 0; ++ if (p[4] & 0x10) ctl->gamepad.buttons |= BUTTON_X; ++ if (p[4] & 0x20) ctl->gamepad.buttons |= BUTTON_A; ++ if (p[4] & 0x40) ctl->gamepad.buttons |= BUTTON_B; ++ if (p[4] & 0x80) ctl->gamepad.buttons |= BUTTON_Y; ++ if (p[5] & 0x01) ctl->gamepad.buttons |= BUTTON_SHOULDER_L; ++ if (p[5] & 0x02) ctl->gamepad.buttons |= BUTTON_SHOULDER_R; ++ if (p[5] & 0x04) ctl->gamepad.buttons |= BUTTON_TRIGGER_L; ++ if (p[5] & 0x08) ctl->gamepad.buttons |= BUTTON_TRIGGER_R; ++ if (p[5] & 0x10) ctl->gamepad.misc_buttons |= MISC_BUTTON_SELECT; ++ if (p[5] & 0x20) ctl->gamepad.misc_buttons |= MISC_BUTTON_START; ++ if (p[5] & 0x40) ctl->gamepad.buttons |= BUTTON_THUMB_L; ++ if (p[5] & 0x80) ctl->gamepad.buttons |= BUTTON_THUMB_R; ++ if (p[6] & 0x01) ctl->gamepad.misc_buttons |= MISC_BUTTON_SYSTEM; ++ ctl->gamepad.brake = p[7] * 4; ++ ctl->gamepad.throttle = p[8] * 4; ++ for (size_t i = 0; i < ARRAY_SIZE(ctl->gamepad.gyro); i++) ++ ctl->gamepad.gyro[i] = 0; ++ for (size_t i = 0; i < ARRAY_SIZE(ctl->gamepad.accel); i++) ++ ctl->gamepad.accel[i] = 0; ++} ++ + static void ds4_parse_input_report_11(uni_hid_device_t* d, const ds4_input_report_11_t* r) { + ds4_instance_t* ins = get_ds4_instance(d); + uni_controller_t* ctl = &d->controller; +@@ -500,9 +537,14 @@ static void ds4_parse_input_report_11(uni_hid_device_t* d, const ds4_input_repor + } + + void uni_hid_parser_ds4_parse_input_report(uni_hid_device_t* d, const uint8_t* report, uint16_t len) { +- if (report[0] == 0x11 && len == 78) { +- const ds4_input_report_11_t* r = (ds4_input_report_11_t*)&report[3]; +- ds4_parse_input_report_11(d, r); ++ if (report[0] == 0x11 && len >= 12) { ++ if (len >= 78) { ++ const ds4_input_report_11_t* r = (ds4_input_report_11_t*)&report[3]; ++ ds4_parse_input_report_11(d, r); ++ } else { ++ /* Short report (e.g. 77 bytes over BT): parse gamepad only so we don't drop packets. */ ++ ds4_parse_input_report_11_minimal(d, report, len); ++ } + } else if (report[0] == 0x01 && len == 10) { + const ds4_input_report_01_t* r = (ds4_input_report_01_t*)&report[1]; + ds4_parse_input_report_01(d, r); +diff --git a/src/components/bluepad32/parser/uni_hid_parser_ds5.c b/src/components/bluepad32/parser/uni_hid_parser_ds5.c +index 9316260..111595e 100644 +--- a/src/components/bluepad32/parser/uni_hid_parser_ds5.c ++++ b/src/components/bluepad32/parser/uni_hid_parser_ds5.c +@@ -554,26 +554,97 @@ void uni_hid_parser_ds5_parse_feature_report(uni_hid_device_t* d, const uint8_t* + } + } + ++/* Parse only gamepad from first 10 bytes of payload when report is short. Ensures we never drop a report (like Xbox One): every packet delivers at least gamepad state. */ ++static void ds5_parse_input_report_minimal(uni_hid_device_t* d, const uint8_t* report, uint16_t len) { ++ uni_controller_t* ctl = &d->controller; ++ if (len < 2 + 10) ++ return; ++ const uint8_t* p = &report[2]; ++ ctl->gamepad.axis_x = (p[0] - 127) * 4; ++ ctl->gamepad.axis_y = (p[1] - 127) * 4; ++ ctl->gamepad.axis_rx = (p[2] - 127) * 4; ++ ctl->gamepad.axis_ry = (p[3] - 127) * 4; ++ ctl->gamepad.brake = p[4] * 4; ++ ctl->gamepad.throttle = p[5] * 4; ++ uint8_t value = p[7] & 0xf; ++ if (value > 7) ++ value = 0xff; ++ ctl->gamepad.dpad = uni_hid_parser_hat_to_dpad(value); ++ ctl->gamepad.buttons = 0; ++ ctl->gamepad.misc_buttons = 0; ++ if (p[7] & 0x10) ctl->gamepad.buttons |= BUTTON_X; ++ if (p[7] & 0x20) ctl->gamepad.buttons |= BUTTON_A; ++ if (p[7] & 0x40) ctl->gamepad.buttons |= BUTTON_B; ++ if (p[7] & 0x80) ctl->gamepad.buttons |= BUTTON_Y; ++ if (p[8] & 0x01) ctl->gamepad.buttons |= BUTTON_SHOULDER_L; ++ if (p[8] & 0x02) ctl->gamepad.buttons |= BUTTON_SHOULDER_R; ++ if (p[8] & 0x04) ctl->gamepad.buttons |= BUTTON_TRIGGER_L; ++ if (p[8] & 0x08) ctl->gamepad.buttons |= BUTTON_TRIGGER_R; ++ if (p[8] & 0x10) ctl->gamepad.misc_buttons |= MISC_BUTTON_SELECT; ++ if (p[8] & 0x20) ctl->gamepad.misc_buttons |= MISC_BUTTON_START; ++ if (p[8] & 0x40) ctl->gamepad.buttons |= BUTTON_THUMB_L; ++ if (p[8] & 0x80) ctl->gamepad.buttons |= BUTTON_THUMB_R; ++ if (p[9] & 0x01) ctl->gamepad.misc_buttons |= MISC_BUTTON_SYSTEM; ++ if (p[9] & 0x04) ctl->gamepad.misc_buttons |= MISC_BUTTON_CAPTURE; ++ for (size_t i = 0; i < ARRAY_SIZE(ctl->gamepad.gyro); i++) ++ ctl->gamepad.gyro[i] = 0; ++ for (size_t i = 0; i < ARRAY_SIZE(ctl->gamepad.accel); i++) ++ ctl->gamepad.accel[i] = 0; ++} ++ + void uni_hid_parser_ds5_parse_input_report(uni_hid_device_t* d, const uint8_t* report, uint16_t len) { + ds5_instance_t* ins = get_ds5_instance(d); +- +- // Don't process reports until state is ready. Prevents possible div-by-0 on calibration +- // and ignores other warnings. +- if (ins->state != DS5_STATE_READY) +- return; ++ uni_controller_t* ctl = &d->controller; + + if (report[0] != 0x31) { + loge("DS5: Unexpected report type: got 0x%02x, want: 0x31\n", report[0]); + return; + } +- if (len != 78) { +- loge("DS5: Unexpected report len: got %d, want: 78\n", len); ++ /* Accept any length that has at least gamepad bytes (2+10=12). Never reject a report and leave state zeroed (like Xbox One). */ ++ if (len < 12) { ++ loge("DS5: Report too short: got %d, need >= 12\n", len); ++ return; ++ } ++ ++ if (len < 76) { ++ ds5_parse_input_report_minimal(d, report, len); + return; + } + +- uni_controller_t* ctl = &d->controller; + const ds5_input_report_t* r = (ds5_input_report_t*)&report[2]; + ++ /* When not READY, still deliver gamepad (no gyro/accel) so input is responsive during calibration. */ ++ if (ins->state != DS5_STATE_READY) { ++ ctl->gamepad.buttons = 0; ++ ctl->gamepad.misc_buttons = 0; ++ ctl->gamepad.axis_x = (r->x - 127) * 4; ++ ctl->gamepad.axis_y = (r->y - 127) * 4; ++ ctl->gamepad.axis_rx = (r->rx - 127) * 4; ++ ctl->gamepad.axis_ry = (r->ry - 127) * 4; ++ ctl->gamepad.brake = r->brake * 4; ++ ctl->gamepad.throttle = r->throttle * 4; ++ uint8_t value = r->buttons[0] & 0xf; ++ if (value > 7) ++ value = 0xff; ++ ctl->gamepad.dpad = uni_hid_parser_hat_to_dpad(value); ++ if (r->buttons[0] & 0x10) ctl->gamepad.buttons |= BUTTON_X; ++ if (r->buttons[0] & 0x20) ctl->gamepad.buttons |= BUTTON_A; ++ if (r->buttons[0] & 0x40) ctl->gamepad.buttons |= BUTTON_B; ++ if (r->buttons[0] & 0x80) ctl->gamepad.buttons |= BUTTON_Y; ++ if (r->buttons[1] & 0x01) ctl->gamepad.buttons |= BUTTON_SHOULDER_L; ++ if (r->buttons[1] & 0x02) ctl->gamepad.buttons |= BUTTON_SHOULDER_R; ++ if (r->buttons[1] & 0x04) ctl->gamepad.buttons |= BUTTON_TRIGGER_L; ++ if (r->buttons[1] & 0x08) ctl->gamepad.buttons |= BUTTON_TRIGGER_R; ++ if (r->buttons[1] & 0x10) ctl->gamepad.misc_buttons |= MISC_BUTTON_SELECT; ++ if (r->buttons[1] & 0x20) ctl->gamepad.misc_buttons |= MISC_BUTTON_START; ++ if (r->buttons[1] & 0x40) ctl->gamepad.buttons |= BUTTON_THUMB_L; ++ if (r->buttons[1] & 0x80) ctl->gamepad.buttons |= BUTTON_THUMB_R; ++ if (r->buttons[2] & 0x01) ctl->gamepad.misc_buttons |= MISC_BUTTON_SYSTEM; ++ if (r->buttons[2] & 0x04) ctl->gamepad.misc_buttons |= MISC_BUTTON_CAPTURE; ++ ctl->battery = (r->status & DS5_STATUS_BATTERY_CAPACITY) * 25 + 1; ++ return; ++ } ++ + // Axis + ctl->gamepad.axis_x = (r->x - 127) * 4; + ctl->gamepad.axis_y = (r->y - 127) * 4; diff --git a/Firmware/external/patches/btstack_hids_num_reports.diff b/Firmware/external/patches/btstack_hids_num_reports.diff new file mode 100644 index 00000000..6cc58d8b --- /dev/null +++ b/Firmware/external/patches/btstack_hids_num_reports.diff @@ -0,0 +1,13 @@ +diff --git a/src/ble/gatt-service/hids_host.h b/src/ble/gatt-service/hids_host.h +index de529831a..d02c067f1 100644 +--- a/src/ble/gatt-service/hids_host.h ++++ b/src/ble/gatt-service/hids_host.h +@@ -63,7 +63,7 @@ extern "C" { + #endif + #define HIDS_HOST_INVALID_REPORT_INDEX 0xFF + +-#define HIDS_HOST_NUM_REPORTS 15 ++#define HIDS_HOST_NUM_REPORTS 32 + + typedef enum { + HIDS_HOST_STATE_IDLE, diff --git a/Firmware/external/patches/pico_sdk_hids_host.diff b/Firmware/external/patches/pico_sdk_hids_host.diff new file mode 100644 index 00000000..e26f248a --- /dev/null +++ b/Firmware/external/patches/pico_sdk_hids_host.diff @@ -0,0 +1,39 @@ +diff --git a/src/rp2_common/pico_btstack/CMakeLists.txt b/src/rp2_common/pico_btstack/CMakeLists.txt +index c622cac..2078825 100644 +--- a/src/rp2_common/pico_btstack/CMakeLists.txt ++++ b/src/rp2_common/pico_btstack/CMakeLists.txt +@@ -53,6 +53,7 @@ if (EXISTS ${PICO_BTSTACK_PATH}/${BTSTACK_TEST_PATH}) + ${PICO_BTSTACK_PATH}/src/hci_cmd.c + ${PICO_BTSTACK_PATH}/src/hci_dump.c + ${PICO_BTSTACK_PATH}/src/hci_event.c ++ ${PICO_BTSTACK_PATH}/src/hci_event_builder.c + ${PICO_BTSTACK_PATH}/src/l2cap.c + ${PICO_BTSTACK_PATH}/src/l2cap_signaling.c + ${PICO_BTSTACK_PATH}/3rd-party/md5/md5.c +@@ -90,7 +91,7 @@ if (EXISTS ${PICO_BTSTACK_PATH}/${BTSTACK_TEST_PATH}) + ${PICO_BTSTACK_PATH}/src/ble/gatt-service/device_information_service_server.c + ${PICO_BTSTACK_PATH}/src/ble/gatt-service/device_information_service_client.c + ${PICO_BTSTACK_PATH}/src/ble/gatt-service/heart_rate_service_server.c +- ${PICO_BTSTACK_PATH}/src/ble/gatt-service/hids_client.c ++ ${PICO_BTSTACK_PATH}/src/ble/gatt-service/hids_host.c + ${PICO_BTSTACK_PATH}/src/ble/gatt-service/hids_device.c + ${PICO_BTSTACK_PATH}/src/ble/gatt-service/nordic_spp_service_server.c + ${PICO_BTSTACK_PATH}/src/ble/gatt-service/ublox_spp_service_server.c +@@ -318,7 +319,7 @@ if (EXISTS ${PICO_BTSTACK_PATH}/${BTSTACK_TEST_PATH}) + set_source_files_properties( + ${PICO_BTSTACK_PATH}/src/ble/att_server.c + ${PICO_BTSTACK_PATH}/src/ble/gatt-service/device_information_service_server.c +- ${PICO_BTSTACK_PATH}/src/ble/gatt-service/hids_client.c ++ ${PICO_BTSTACK_PATH}/src/ble/gatt-service/hids_host.c + ${PICO_BTSTACK_PATH}/src/btstack_util.c + ${PICO_BTSTACK_PATH}/src/btstack_crypto.c + ${PICO_BTSTACK_PATH}/src/classic/a2dp.c +@@ -381,7 +382,7 @@ if (EXISTS ${PICO_BTSTACK_PATH}/${BTSTACK_TEST_PATH}) + COMPILE_OPTIONS "-Wno-unused-parameter;-Wno-sign-compare" + ) + set_source_files_properties( +- ${PICO_BTSTACK_PATH}/src/ble/gatt-service/hids_client.c ++ ${PICO_BTSTACK_PATH}/src/ble/gatt-service/hids_host.c + PROPERTIES + COMPILE_OPTIONS "-Wno-cast-qual;-Wno-null-dereference" + ) diff --git a/Firmware/external/tinyusb b/Firmware/external/tinyusb index cb22301f..8a0c0c06 160000 --- a/Firmware/external/tinyusb +++ b/Firmware/external/tinyusb @@ -1 +1 @@ -Subproject commit cb22301f91f0465a5578be35d9be284657ddd31d +Subproject commit 8a0c0c0643db0f8b0b7a76d85968d2c4e79411fd diff --git a/LICENSE b/LICENSE index 9ee7aa25..14b8de53 100644 --- a/LICENSE +++ b/LICENSE @@ -6,6 +6,17 @@ under Apache 2.0 (Bluepad32) and BTStack's proprietary (non-commercial) license. For full details, please refer to the repositories contained in the Firmware/external directory of this project. +Xbox 360 console authentication: Builds that emulate Xbox 360 controllers use +libxsm3 (Xbox Security Method 3) for authentication with retail Xbox 360 +consoles. libxsm3 is licensed under the GNU Lesser General Public License +v2.1 (or later). A copy of the LGPL v2.1 is included in +Firmware/external/libxsm3/LICENSE.txt. libxsm3 also uses ExCrypt (3-clause BSD); +see Firmware/external/libxsm3/README.md for credits. + +Xbox 360 controller rumble/vibration: Host-side rumble handling (including +wireless RUMBLE_ENABLE sequence) is attributed to faithvoid +(https://github.com/faithvoid). + Copyright (c) 2024 wiredOpposite (wiredopposite.com) Copyright (c) 2024 OpenStickCommunity (gp2040-ce.info) Copyright (c) 2021 Jason Skuby (mytechtoybox.com) diff --git a/README.md b/README.md index 3aa52f23..8672bd16 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,49 @@ -# OGX-Mini -![OGX-Mini Boards](images/OGX-Mini-github.jpg "OGX-Mini Boards") +# OGX-Mini 2026 -Firmware for the RP2040, capable of emulating gamepads for several game consoles. The firmware comes in many flavors, supported on the [Adafruit Feather USB Host board](https://www.adafruit.com/product/5723), Pi Pico, Pi Pico 2, Pi Pico W, Pi Pico 2 W, Waveshare RP2040-Zero, Pico/ESP32 hybrid, and a 4-Channel RP2040-Zero setup. +**Support development** — If this firmware helps you, consider **[donating on Ko-fi](https://ko-fi.com/megacadedev)**. Contributions go toward testing and adding support for **more boards and controllers** so the project can grow for everyone. -[**Visit the web app here**](https://wiredopposite.github.io/OGX-Mini-WebApp/) to change your mappings and deadzone settings. To pair the OGX-Mini with the web app via USB, plug your controller in, then connect it to your PC, hold **Start + Left Bumper + Right Bumper** to enter web app mode. Click "Connect via USB" in the web app and select the OGX-Mini. You can also pair via Bluetooth, no extra steps are needed in that case. +--- + +![OGX-Mini Boards](images/logo.png "OGX-Mini Boards") + +Firmware for the RP2040, capable of emulating gamepads for several game consoles. The firmware comes in many flavors, supported on the [Adafruit Feather USB Host board](https://www.adafruit.com/product/5723), Pi Pico, Pi Pico 2, Pi Pico W, Pi Pico 2 W, Waveshare RP2040-Zero, Waveshare RP2350-USB-A, Waveshare RP2350-Zero, Seeed Studio XIAO RP2040, RP2354, Pico/ESP32 hybrid, and a 4-Channel RP2040-Zero setup. + +**Development & maintainer support:** Firmware development, testing, and issue triage focus on **official or OEM boards**—hardware from the original creators, named retail/partner boards in this repo’s build options, and equivalent OEM designs. Unofficial clones, random AliExpress spin-offs, or homebrew PCBs that only “look like” a supported board are **not** guaranteed to work and are **out of scope** for development support (you may still flash and experiment at your own risk). + +[**Visit the web app here**](https://megacadedev.github.io/OGX-Mini-2026-WebApp/) to change your mappings and deadzone settings. To pair the OGX-Mini with the web app via USB, plug your controller in, then connect it to your PC, hold **Start + Left Bumper + Right Bumper** to enter web app mode. Click "Connect via USB" in the web app and select the OGX-Mini. You can also pair via Bluetooth, no extra steps are needed in that case. + +[**Join the discord here!**](https://discord.gg/HhZuSaSc4) + +[**Follow on TikTok!**](https://www.tiktok.com/@ogxmini2026) + +[**Subscribe on YouTube!**](https://www.youtube.com/@ogxmini2026) ## Supported platforms - Original Xbox - Playstation 3 -- Nintendo Switch (docked) -- XInput (use [**UsbdSecPatch**](https://github.com/InvoxiPlayGames/UsbdSecPatch) for Xbox 360, or select the patch in J-Runner while flashing your NAND) +- Nintendo Switch 1 & 2 (docked) — **Switch Pro Controller** emulation over USB +- XInput (Xbox 360, UsbdSecPatch no longer required) - Playstation Classic - DInput +- **SteamOS / Bazzite** — Linux desktop / Steam Deck (desktop mode): **DualSense** USB gamepad + **touchpad → HID mouse** (see [SteamOS / Bazzite output mode](#steamos--bazzite-output-mode)) +- **PS3 / PS4 motion** — Sixaxis / accelerometer passthrough from DS4, DualSense, Switch Pro, and Wii Remote (see [Motion controls](#ps3--ps4-motion-controls)) +- Wii U (GameCube Adapter) +- **Wii (Wiimote)** — Pico W / Pico 2 W only; build with `-DOGXM_FIXED_DRIVER=WII`. See [Wii Mode Guide](Firmware/RP2040/docs/Wii_Mode_Guide.md). +- **PlayStation 1 & 2** — GPIO output to console controller port (DualShock-style). +- **Dreamcast** — GPIO output over Maple Bus to console controller port. +- **GameCube / Wii (GameCube ports)** — GPIO output over single-wire JoyBus. **Build-only (no combo):** use `-DOGXM_FIXED_DRIVER=GAMECUBE`; see below. +- **Nintendo 64** — GPIO output over single-wire to controller port. **Build-only (no combo):** use `-DOGXM_FIXED_DRIVER=N64`; see below. + +![Supported platforms](images/platform-list.png "Supported platforms") + +**RP2040 output modes:** **USB device:** XInput (Xbox 360 + XSM3), DInput, PS3, **PS4 (DualShock 4 USB)**, **STEAM (SteamOS / Bazzite — DualSense + touchpad mouse)**, **Switch Pro** (Nintendo Switch Pro Controller emulation), Wii U, **Wii (Wiimote, build-option only)**, Xbox OG (Gamepad / Steel Battalion / XRemote), PS Classic, Web App. **GPIO (no USB device):** PS1/PS2, Dreamcast, GameCube, N64 — use any wired USB or (on Pico W / Pico 2 W) Bluetooth controller as input. **Wii, GameCube, and N64 are not in the combo list** — use a dedicated build for those modes (see below). ## Changing platforms By default the OGX-Mini will emulate an OG Xbox controller, you must hold a button combo for 3 seconds to change which platform you want to play on. Your chosen mode will persist after powering off the device. Start = Plus (Switch) = Options (Dualsense/DS4) -- XInput +- XInput (Xbox 360) - Start + Dpad Up - Original Xbox - Start + Dpad Right @@ -26,26 +51,126 @@ Start = Plus (Switch) = Options (Dualsense/DS4) - Start + Dpad Right + Right Bumper - Original Xbox DVD Remote - Start + Dpad Right + Left Bumper -- Switch +- Switch (Pro Controller emulation) - Start + Dpad Down - PlayStation 3 - Start + Dpad Left +- PlayStation 4 (DualShock 4 USB) + - Start + Left Bumper + D-pad **Left** +- SteamOS / Bazzite (DualSense USB + touchpad mouse) + - Start + Left Bumper + D-pad **Up** - PlayStation Classic - Start + A (Cross for PlayStation and B for Switch gamepads) +- PlayStation 1 / PlayStation 2 (GPIO output) + - Start + B +- Dreamcast (GPIO output) + - Start + Y +- Wii U (GameCube Adapter) + - Start + Left Bumper + D-Pad Down - Web Application Mode - Start + Left Bumper + Right Bumper +**Wii, GameCube, and N64 are not selectable by combo.** Use a dedicated build for those modes: + +- **Wii (Wiimote):** `-DOGXM_FIXED_DRIVER=WII`. See [Wii Mode Guide](Firmware/RP2040/docs/Wii_Mode_Guide.md). +- **GameCube (GPIO):** `-DOGXM_FIXED_DRIVER=GAMECUBE`. Example (Pico 2 W Debug): + `cmake -DOGXM_BOARD=PI_PICO2W -DCMAKE_BUILD_TYPE=Debug -DOGXM_FIXED_DRIVER=GAMECUBE ..` then `ninja`. Flash `OGX-Mini-*-GAMECUBE.uf2`. Use `PI_PICOW` for Pico W. +- **N64 (GPIO):** `-DOGXM_FIXED_DRIVER=N64`. Example (Pico 2 W Debug): + `cmake -DOGXM_BOARD=PI_PICO2W -DCMAKE_BUILD_TYPE=Debug -DOGXM_FIXED_DRIVER=N64 ..` then `ninja`. Flash `OGX-Mini-*-N64.uf2`. + +Input for GPIO modes is Bluetooth or USB; no combo switching in these builds. + After a new mode is stored, the RP2040 will reset itself so you don't need to unplug it. +## PS3 / PS4 motion controls + +**PS3** and **PS4 (DualShock 4 USB)** output modes can forward **tilt / motion** from compatible input controllers into the emulated report (Sixaxis on PS3, accelerometer on PS4). **Switch** output mode does **not** pass through motion for now. + +| | | +|--|--| +| **Select PS3 mode** | **Start + D-pad Left** (~3 s) | +| **Select PS4 mode** | **Start + Left Bumper + D-pad Left** (~3 s) | +| **Supported input (BT — Pico W / Pico 2 W)** | DualShock 4, DualSense, Switch Pro, **Wii Remote** (accelerometer) | +| **Supported input (wired USB host)** | DualShock 4, DualSense, Switch 1 Pro, Switch 2 Pro | +| **Wii Remote** | Point the **IR end at the TV**; motion is enabled automatically when motion output is active | +| **Play on a real PS3 or PS4** | OGX-Mini does **not** plug straight into the console for this — use a **USB adapter** between OGX-Mini and the console. **Tested with [Brook Wingman XE 2 Converter](https://www.brookaccessories.com/products/wingman-xe2).** Works on **PS3** (PS3 mode) and **PS4** (PS4 mode on OGX-Mini → Brook → PS4). | +| **PC testing (PS3/PS4)** | No Brook required — use PS3/PS4 mode on a PC/emulator to verify tilt | + +**Typical chain for a PS3 motion game (e.g. *Flower*):** + +```text +Wii Remote or DualSense (Bluetooth) → OGX-Mini (Pico W) → USB → Brook Wingman XE 2 → PS3 +``` + +Details: [IMPROVEMENTS.md — motion passthrough](Firmware/RP2040/docs/IMPROVEMENTS.md#ps3--ps4-output--motion-passthrough). + +## SteamOS / Bazzite output mode + +Use this mode on **SteamOS**, **Bazzite**, or other **Linux desktops** (including **Steam Deck** in desktop mode) when you want the host to see a **PS5 DualSense** gamepad **and** use the **touchpad as a USB mouse** (cursor movement + tap to click). Games and Steam Input see a normal DualSense; the desktop gets a separate **HID mouse** for navigating outside games. + +### Select mode + +| Method | Combo / option | +|--------|----------------| +| **Button combo** | Hold **Start + Left Bumper + D-pad Up** for ~3 seconds (saved to flash; device resets) | +| **Web app** | Output mode **SteamOS / Bazzite** | +| **Fixed build** | `-DOGXM_FIXED_DRIVER=STEAM` (also in `./scripts/build.sh` / `build.ps1` fixed-mode menu) | + +### DualSense USB emulation + +The adapter presents **two USB interfaces** to the PC: + +| Interface | Identity | Purpose | +|-----------|----------|---------| +| **Gamepad** | Sony **DualSense** `054c:0ce6`, 64-byte HID input report | Buttons, sticks, triggers, PS button — for Steam / Proton / desktop gamepad APIs | +| **Mouse** | Standard **relative HID mouse** (separate interface) | Cursor from **DualSense touchpad** only | + +**Input → USB behavior:** + +| Input controller | Gamepad report | Touchpad / mouse | +|------------------|----------------|------------------| +| **DualSense (PS5)** — Bluetooth or wired USB | **Passthrough** of the real DualSense report (sticks/triggers from host when wired) | Touchpad finger position → **relative mouse** movement; touchpad click (`TP`) → **left click** | +| **Other pads** (Xbox, DS4, Switch Pro, etc.) | **Synthesized** DualSense report — face buttons, shoulders, triggers, sticks, D-pad, **Share/Options**, **PS**, touchpad-click bit mapped from PadIn | No hardware touchpad — **no mouse** (gamepad only) | + +Face-button mapping to DualSense: **A→Cross**, **B→Circle**, **X→Square**, **Y→Triangle**; **LB/RB→L1/R1**; **Back/Start→Share/Options**; **SYS→PS**. Full table: [Controller_Mappings.md — STEAM mode](Firmware/RP2040/docs/Controller_Mappings.md#steamos--bazzite-steam-mode). + +After switching to STEAM mode, **unplug and replug** the adapter’s USB cable to the PC so Linux re-enumerates both interfaces. Verify with `lsusb -d 054c:0ce6` (two interfaces) and `evtest` on the mouse device. + +### Touchpad → mouse (DualSense input) + +When the **input** pad is a **DualSense** (Bluetooth on Pico W / Pico 2 W, or wired into the adapter’s USB host port): + +- **Drag** on the touchpad → **relative mouse** movement on the PC (second USB interface). +- **Tap / click** the touchpad → **left mouse button**. +- The touchpad data is also embedded in the DualSense gamepad report (offset compatible with Linux `hid-playstation`). + +Controllers **without** a DualSense touchpad still work as a DualSense gamepad; they do **not** drive the mouse interface. + +### Other notes + +- **On-screen keyboard:** Steam handles this — **Steam + X** on the controller (**PS + Square** on DualSense), same as Steam Deck desktop. +- **Input sources:** Most supported **USB** and **Bluetooth** controllers work as gamepad input; **touchpad → mouse** requires a **DualSense** (or other pad that reports touchpad data). + +Fixed-build example (Pico 2 W): +`cmake -DOGXM_BOARD=PI_PICO2W -DCMAKE_BUILD_TYPE=Release -DOGXM_FIXED_DRIVER=STEAM ..` then `ninja`. + +Technical detail: [IMPROVEMENTS.md — STEAM mode](Firmware/RP2040/docs/IMPROVEMENTS.md#steam-mode--steamos--bazzite-linux-desktop). + +## Disconnecting Controllers +For most controllers pressing and holding Start+Select (+/-, etc) for the controller will disconnect it and restart pairing mode. +For the OUYA controller there is no Start+Select, the disconnection combo has been set to L3+R3. + ## Supported devices -### Wired controllers +### Wired controllers (USB input) +See [**Wired Controllers**](Firmware/RP2040/docs/Wired_Controllers.md) for a full list by driver type. The following work when the adapter is outputting to any supported platform (Xbox, Switch, XInput, PS3, DInput, **SteamOS / Bazzite**, **PS1/PS2**, **Dreamcast**, **GameCube**, **N64**, etc.): - Original Xbox Duke and S - Xbox 360, One, Series, and Elite +- **Razer Atrox Arcade Stick** (Xbox One `1532:0a00`, Xbox 360 `24c6:5000`; wired USB host) - Dualshock 3 (PS3) - Dualshock 4 (PS4) - Dualsense (PS5) - Nintendo Switch Pro -- Nintendo Switch wired +- Nintendo Switch wired (including **Switch 2** family — **Pro 2** `0x2069`, **Joy-Con 2** `0x2066`/`0x2067`; **wired USB** or **Bluetooth LE** on Pico W / Pico 2 W; see [Wired Controllers](Firmware/RP2040/docs/Wired_Controllers.md) and [IMPROVEMENTS — Switch 2 BLE](Firmware/RP2040/docs/IMPROVEMENTS.md#nintendo-switch-2--bluetooth-pico-w--pico-2-w)) - Nintendo 64 Generic USB - Playstation Classic - Generic DInput @@ -54,38 +179,74 @@ After a new mode is stored, the RP2040 will reset itself so you don't need to un Note: There are some third party controllers that can change their VID/PID, these might not work correctly. ### Wireless adapters -- Xbox 360 PC adapter (Microsoft or clones) +- Xbox 360 PC adapter (Microsoft) - 8Bitdo v1 and v2 Bluetooth adapters (set to XInput mode) - Most wireless adapters that present themselves as Switch/XInput/PlayStation controllers should work ### Wireless Bluetooth controllers (Pico W & ESP32) -**Note:** Bluetooth functionality is in early testing, some may have quirks. +**Note:** Bluetooth functionality is in early testing; some controllers may have quirks. BT pads work as input in **any** USB output mode (OG Xbox, XInput, PS3, Switch, etc.) on Pico W / Pico 2 W, and also when outputting over **GPIO** to **PS1/PS2**, **Dreamcast**, **GameCube**, or **N64** (same as wired USB in those modes). + +**DualShock 3 (PS3) — USB pairing to the adapter’s Bluetooth (Pico W / Pico 2 W / RP2354):** The DS3 does not pair like a normal Bluetooth device. If you plug a **DualShock 3 into the adapter’s USB host port** while the firmware is running, it will **automatically program the controller with this adapter’s Bluetooth address** (HID feature **0xF5**, same mechanism as the [Bluepad32 sixaxispairer](https://bluepad32.readthedocs.io/en/latest/pair_ds3/) tool — restored in **v1.0.0.12a** as a sync SET after wired enable). Then **unplug the USB cable** and press the **PS** button to connect wirelessly. If the radio was not ready at the moment the pad enumerated, pairing is retried on the next input reports until the address is available. + +**Pico W / Pico 2 W — important for DualShock 4 (DS4):** DS4 uses **Classic Bluetooth (BR/EDR)**. The same CYW43 radio also runs **BLE advertising** for the **phone / web app**. Running both at once often **drops the DS4 link** seconds after connect (blue LED then off). Firmware therefore **pauses BLE advertising** while a **Classic BT** gamepad is connected (DS4, DS3 over BT, Xbox 360 wireless adapter style links). **DualSense** and **Xbox Series (BLE)** mostly use **LE** and are unaffected by that pause. + +| Situation | What to do | +|-----------|------------| +| Use **DS4 / Classic BT** pad | Connect and play; web app BLE discovery is off until that pad disconnects. | +| Use **Bluetooth** in the **web app** | Disconnect **Classic BT** controllers first, **or** plug the adapter in via **USB** and use **Connect via USB** in the app. | + +Full technical detail: **[Firmware/RP2040/docs/IMPROVEMENTS.md](Firmware/RP2040/docs/IMPROVEMENTS.md)** → *Pico W / Pico 2 W — DualShock 4 and Classic Bluetooth stability*. + +**Nintendo Switch 2 (Pico W / Pico 2 W):** **Switch 2 Pro** (PID **0x2069**) and **Joy-Con 2** Left (**0x2067**) / Right (**0x2066**) use a **custom BLE GATT protocol** (not standard gamepad HID). Put the controller in **pairing / SYNC** mode; the adapter discovers and connects automatically — **do not** pair it in your PC or phone Bluetooth settings first. **Joy-Con 2:** pair **Left** first, then **Right** while left stays connected — both merge into **player 1** like on a Switch. **Home** maps to **PS / Guide (SYS)** on PS3, XInput, OG Xbox, etc. Protocol credits: [Nadeflore/switch2-controllers](https://github.com/Nadeflore/switch2-controllers), [TommyWabg/switch2-controllers-windows10-gyro](https://github.com/TommyWabg/switch2-controllers-windows10-gyro), and [BlueRetro #1249](https://github.com/darthcloud/BlueRetro/issues/1249). Details: [IMPROVEMENTS — Switch 2 Bluetooth](Firmware/RP2040/docs/IMPROVEMENTS.md#nintendo-switch-2--bluetooth-pico-w--pico-2-w). + +**Steam Controller 2026 / Triton (Pico W / Pico 2 W / RP2354 BT):** Hold **RB + B + Steam** for BLE pairing (`28de:1303`, HID-over-GATT). **Remove/disconnect it from PC or phone Bluetooth first** — only one host can own the link. Uses **LE Secure Connections**. **View → Start**, **Menu → Back/Select**, **Steam → Guide / SYS**. Details: [IMPROVEMENTS — Steam Controller 2026](Firmware/RP2040/docs/IMPROVEMENTS.md#steam-controller-2026-triton--bluetooth). + - Xbox Series, One, and Elite 2 - Dualshock 3 - Dualshock 4 - Dualsense - Switch Pro -- Steam +- **Switch 2 Pro** and **Joy-Con 2 (L/R)** (BLE on Pico W / Pico 2 W / RP2354; wired USB also supported) +- Steam (original SC) +- **Steam Controller 2026 (Triton)** — BLE on Pico W / Pico 2 W / RP2354 - Stadia -- And more +- Wii U Pro +- Wii Remote + - Supported Extensions: + - Gamepad + - Nunchuck + - GameCube Controller +- 8BitDo Ultimate Wireless (Switch layout) Please visit [**this page**](https://bluepad32.readthedocs.io/en/latest/supported_gamepads/) for a more comprehensive list of supported controllers and Bluetooth pairing instructions. -## Features new to v1.0.0 -- Bluetooth functionality for the Pico W, Pico 2 W, and Pico+ESP32. -- Web application (connectable via USB or Bluetooth) for configuring deadzones and buttons mappings, supports up to 8 saved profiles. -- Pi Pico 2 and Pico 2 W (RP2350) support. -- Reduced latency by about 3-4 ms, graphs showing comparisons are coming. -- 4 channel functionality, connect 4 Picos and use one Xbox 360 wireless adapter to control all 4. -- Delayed USB mount until a controller is plugged in, useful for internal installation (non-Bluetooth boards only). -- Generic HID controller support. -- Dualshock 3 emulation (minus gyros), rumble now works. -- Steel Battalion controller emulation with a wireless Xbox 360 chatpad. -- Xbox DVD dongle emulation. You must provide or dump your own dongle firmware, see the Tools directory. -- Analog button support on OG Xbox and PS3. -- RGB LED support for RP2040-Zero and Adafruit Feather boards. - -## Planned additions +# Features new to this fork + +Version history and release notes are in **[CHANGELOG.md](CHANGELOG.md)**. For detailed firmware improvements — **PS3**, **XInput / XSM3**, **OG Xbox**, **PS2/OPL**, **latency**, **SteamOS / Bazzite (STEAM mode)**, **PS3 / PS4 motion passthrough**, **Pico W Bluetooth (DS4 Classic BT, BLE coexistence, Xbox Series BLE, Switch 2 Pro + Joy-Con 2 BLE, Steam Controller 2026 / Triton, DualShock 3 USB→BT auto-pair)**, **Pico W / Pico 2 W PIO USB wired unplug detection**, etc. — see **[Firmware/RP2040/docs/IMPROVEMENTS.md](Firmware/RP2040/docs/IMPROVEMENTS.md)**. + +Highlights: + +- **Steam Controller 2026 (Triton) over Bluetooth** — Pair with **RB + B + Steam** on Pico W / Pico 2 W / RP2354 (**`28de:1303`**). Requires LE Secure Connections; disconnect from any PC/phone first. **View → Start**, **Menu → Back**, **Steam → Guide**. Details: [IMPROVEMENTS — Steam Controller 2026](Firmware/RP2040/docs/IMPROVEMENTS.md#steam-controller-2026-triton--bluetooth). + +- **DualShock 3 — USB → Bluetooth auto-pair (restored in v1.0.0.12a)** — Plug a DS3 into the adapter’s USB host port to program it with this unit’s BD_ADDR (**feature `0xF5`**), then unplug and press **PS**. Details: [Wireless Bluetooth](#wireless-bluetooth-controllers-pico-w--esp32), [IMPROVEMENTS — DS3 USB pairing](Firmware/RP2040/docs/IMPROVEMENTS.md#dualshock-3--automatic-usb-programming-for-bluetooth-pairing). + +- **XInput / Xbox 360 — stock stick feel (#38)** — In **XInput mode**, default (non–WebApp-customized) sticks get a milder 360-like deadzone/curve so Series pads are less twitchy. Details: [IMPROVEMENTS — stock stick feel](Firmware/RP2040/docs/IMPROVEMENTS.md#xinput--xbox-360--stock-stick-feel). + +- **Switch mode — HD rumble to input pads** — Console HD rumble is decoded and forwarded to Bluetooth/USB input pads (DualSense, Xbox, etc.). Details: [IMPROVEMENTS — Switch HD rumble](Firmware/RP2040/docs/IMPROVEMENTS.md#switch-mode--hd-rumble-passthrough). + +- **Pico W / Pico 2 W / RP2354 — USB resume restores BT pairing** — After console USB standby, scans restart without unplugging the dongle. Details: [IMPROVEMENTS — USB resume pairing](Firmware/RP2040/docs/IMPROVEMENTS.md#pico-w--pico-2-w--rp2354-bt--usb-resume-restores-pairing-scans). + +- **Two adapters on one console** — **Not supported yet** as two independent players: units share the same USB identity (VID/PID / serial / XSM3 on 360). Use **one OGX + a native or other-brand pad**, or a **multi-port** host. See [IMPROVEMENTS — Summary](Firmware/RP2040/docs/IMPROVEMENTS.md#summary). + +- **PS3 / PS4 motion (tilt / IMU passthrough)** — **PS3** and **PS4** output modes forward accelerometer (and gyro where available) from **DualShock 4**, **DualSense**, **Switch Pro**, and **Wii Remote** (BT/USB where supported). **Switch** output does not pass through motion for now. For **real PS3/PS4 consoles**, use a **USB adapter** between OGX-Mini and the console — **tested with [Brook Wingman XE 2 Converter](https://www.brookaccessories.com/products/wingman-xe2)** (works on **PS3** and **PS4**). Details: [Motion controls](#ps3--ps4-motion-controls), [IMPROVEMENTS.md — motion passthrough](Firmware/RP2040/docs/IMPROVEMENTS.md#ps3--ps4-output--motion-passthrough). + +- **SteamOS / Bazzite (STEAM output mode)** — **Start + Left Bumper + D-pad Up** (~3 s), **web app**, or `-DOGXM_FIXED_DRIVER=STEAM` / build-script fixed mode. USB enumerates as **Sony DualSense** (`054c:0ce6`) **plus** a separate **HID mouse** interface. **DualSense emulation:** passthrough of the real PS5 report when input is DualSense (BT or wired USB); other controllers get a **synthesized** DualSense report (Cross/Circle/Square/Triangle, L1/R2, Share/Options, PS). **Touchpad → mouse:** DualSense touchpad drag moves the desktop cursor; tap = left click. Controllers without a touchpad are gamepad-only. Works with **Bluetooth** (Pico W / Pico 2 W) or **wired USB** input. Details: [SteamOS / Bazzite output mode](#steamos--bazzite-output-mode), [IMPROVEMENTS.md — STEAM mode](Firmware/RP2040/docs/IMPROVEMENTS.md#steam-mode--steamos--bazzite-linux-desktop), [Controller_Mappings.md — STEAM](Firmware/RP2040/docs/Controller_Mappings.md#steamos--bazzite-steam-mode). + +- **Pico W / Pico 2 W — PIO USB wired unplug:** When you unplug the gamepad from the adapter’s USB host port, the firmware now **detects disconnect reliably** even though PIO USB owns D+/D− (GPIO line state often never shows a true “disconnected” idle). Detection uses **debounced** hints: **HCD port connect status**, **no TinyUSB configured device** (all `tuh_mounted` addresses), and **no host input reports** for a few seconds — so the TinyUSB/PIO host can **tear down**, **GPIO line IRQ monitoring** can resume, and **Bluetooth pairing** can work again without power-cycling or “shorting” the port. Details: [IMPROVEMENTS.md — Pico W PIO USB unplug](Firmware/RP2040/docs/IMPROVEMENTS.md#pico-w--pico-2-w--pio-usb-wired-controller-unplug-detection). + +*Note: Many features were added and tested on Pico W / Pico 2 W; other boards may not have been tested for every change.* + +## Planned additions from the original creator - More accurate report parser for unknown HID controllers - Hardware design for internal OG Xbox install - Hardware design for 4 channel RP2040-Zero adapter @@ -97,6 +258,23 @@ Please visit [**this page**](https://bluepad32.readthedocs.io/en/latest/supporte - Button macros - Rumble settings (intensity, enabled/disable, etc.) +## Planned additions for this fork +- **Web app bindings for OG Xbox mode** — Allow users to rebind the Guide tap (Start) and the IGR/shutdown hold combos (e.g. which button triggers 1 s soft IGR or 3 s shutdown) via the web app. +- Output to the following consoles: + - ~~PS2~~ (done: PS1/PS2 GPIO mode, see changelog) + - ~~GameCube~~ (done: GameCube GPIO mode, see changelog) + - ~~DreamCast~~ + - NES + - SNES + - Genesis + - Master System + - Sega Saturn + - ~~PS4/PS5~~ (needs an authentication dongle to work) + - ~~Xbox One~~ (needs an authentication dongle to work) + - Atari + - PS2 MultiTap + - PS1 MultiTap + ## Hardware For Pi Pico, RP2040-Zero, 4 channel, and ESP32 configurations, please see the hardware folder for diagrams. @@ -104,36 +282,112 @@ I've designed a PCB for the RP2040-Zero so you can make a small form-factor adap OGX-Mini Boards -If you would like a prebuilt unit, you can purchase one, with cable and Xbox adapter included, from my [**Etsy store**](https://www.etsy.com/listing/1426992904/ogx-mini-controller-adapter-for-original). +If you would like a prebuilt unit, you can purchase one, with cable and Xbox adapter included, from the original creators store: [**Etsy store**](https://www.etsy.com/listing/1426992904/ogx-mini-controller-adapter-for-original). ## Adding supported controllers -If your third party controller isn't working, but the original version is listed above, send me the device's VID and PID and I'll add it so it's recognized properly. + +If your controller isn’t working but a similar one (e.g. same brand or protocol) is listed in [Wired Controllers](Firmware/RP2040/docs/Wired_Controllers.md), support often starts by registering its **VID** (Vendor ID) and **PID** (Product ID) in `Firmware/RP2040/src/USBHost/HardwareIDs.h`. + +**VID/PID alone is not always enough.** Many pads need the correct **USB mode** (XInput vs DInput vs Switch), matching **report layout**, or **driver-specific** handling. Dropping IDs into the list may do nothing or behave wrong if the device doesn’t match an existing parser. + +**Getting support added:** I have a **PO box** — we can **schedule borrowing** your controller so support can be developed and tested, then returned. Otherwise, **[donations via Ko-fi](https://ko-fi.com/megacadedev)** will be needed toward **buying that controller** so it can be tested and supported **when time and stock allow**—this is not a guaranteed timeline or commitment for every request. Reach out (e.g. Discord or GitHub) to arrange either option. + +**What we prioritize for support (disclaimer):** Maintainer effort focuses on **OEM and name‑brand** controllers (first‑party console pads and established third‑party brands with stable USB identities). **Knock‑offs, unbranded clones, and “compatible” pads** that reuse random VID/PIDs or change behavior by batch are often **not practical to support**: we may be unable to source the **exact same** unit, and fixes may not transfer to other clones. You can still open an issue; just expect clone/no‑name hardware to be **lower priority or declined**. + +When you write in, include **VID**, **PID**, and controller **name/model**. If the controller has multiple modes (XInput / DInput / Switch), say which mode you used. + +### How to get VID and PID + +**Windows** + +- **Device Manager:** Plug in the controller → open Device Manager → find it under “Sound, video and game controllers” or “Human Interface Devices” → right‑click → **Properties** → **Details** → **Property:** “Hardware Ids”. You’ll see something like `HID\VID_054C&PID_09CC`; **VID** is the hex after `VID_`, **PID** is the hex after `PID_` (e.g. VID `054C`, PID `09CC`). +- **USBDeview** (NirSoft): Lists all USB devices with VID and PID in columns. + +**Linux** + +- Run `lsusb` with the controller plugged in. Each line is `Bus ... Device ... ID **vid:pid** Manufacturer Product`. Use the `vid` and `pid` from the line that matches your controller (e.g. `054c:09cc` → VID `054C`, PID `09CC`; we use uppercase hex in the code). +- Alternatively: `lsusb -v` and look at `idVendor` and `idProduct` for the gamepad interface. + +**macOS** + +- **System Information:** Apple menu → **About This Mac** → **System Report** → **USB** (or **Bluetooth** if wireless). Select the controller; **Vendor ID** and **Product ID** are shown (often in decimal — convert to hex for the code, e.g. 1356 decimal → `054C` hex). +- Or use **System Profiler** and open the USB section. + +**Web / gamepad API** + +- Some browser-based tools (e.g. [gamepad tester](https://gamepad-tester.com/) or similar) show the connected gamepad’s VID/PID in the page or in the browser’s device/Gamepad API info. Check the site’s instructions. + +When submitting without a full capture, still include **VID**, **PID**, and controller name (e.g. “8BitDo Pro 2 in DInput mode”), and note which mode you used when reading the IDs. ## Build ### RP2040 -You can compile this for different boards with the CMake argument ```OGXM_BOARD``` while configuring the project. +Build with **CMake** from the `Firmware/RP2040` directory (or use the build scripts below). Prerequisites: git, python3, CMake, Ninja, and the GCC ARM toolchain. CMake will patch Bluepad32/BTStack and fetch submodules as needed. + +#### Board options (OGXM_BOARD) + +Please Note that I currently only have Pico W/ 2 W, Waveshare RP2350 USB A and AdaFruit Feather. I am unable to test on other boards at this time, if you wish for more boards to be tested and compatible with my build please consider a donation on KoFi. There may be unexpected bugs on other boards that I am unable to test directly until the board is made available to me. + +Additionally, clone boards or knock offs are not supported. I cannot source and test everything on every variation of clone boards as their builds may effect other official boards as well and break other features. If you wish for your clone board to be supported you will need to edit and compile the firmware yourself. + +Use one of these values for **`OGXM_BOARD`** in a manual build, or pick the same board from the build script’s numbered list: -The options are: - ```PI_PICO``` - ```PI_PICO2``` -- ```PI_PICOW``` +- ```PI_PICOW``` - ```PI_PICO2W``` -- ```RP2040_ZERO``` -- ```ADAFRUIT_FEATHER``` +- ```RP2040_ZERO``` (Waveshare RP2040-Zero) +- ```RP2350_USB_A``` (Waveshare RP2350-USB-A) +- ```RP2350_ZERO``` (Waveshare RP2350-Zero) +- ```RP2040_XIAO``` (Seeed Studio XIAO RP2040) +- ```RP2354``` (RP2350 + **Pi Radio Module 2** / CYW43439 — **Bluetooth** wireless controllers like **Pico 2 W**, plus PIO USB host on **GP0/GP1**, LED **GP25**) +- ```ADAFRUIT_FEATHER``` use the board’s **USB Type-A** port for the controller; 5V enable is GPIO 18, D+/D− are GPIO 16/17. If the controller is not detected, plug it in **after** the board has booted and ensure the green 5V LED next to the USB-A port is on.) - ```ESP32_BLUEPAD32_I2C``` - ```ESP32_BLUERETRO_I2C``` - ```EXTERNAL_4CH_I2C``` -You can also set ```MAX_GAMEPADS``` which, if greater than one, will only support DInput (PS3) and Switch. +You can also set ```MAX_GAMEPADS``` (if > 1, only DInput/PS3 and Switch Pro are supported). **Optional:** ```OGXM_FIXED_DRIVER``` to lock output mode (e.g. ```XINPUT```, ```PS3```, ```STEAM```, ```PS4```); ```OGXM_FIXED_DRIVER_ALLOW_COMBOS=ON``` to keep combos when fixed. ```MAIN_LOOP_DELAY_US``` (default ```0```) sets main-loop delay for lower CPU use (e.g. ```250```). + +You'll need git, python3, CMake, Ninja and the GCC ARM toolchain installed. CMake scripts will patch some files in Bluepad32 and BTStack and also make sure all git submodules (plus their submodules and dependencies) are downloaded. + +#### Build scripts (recommended for new users) + +We provide interactive build scripts so you can build firmware without memorizing CMake options: + +| Platform | Command (run from project root) | +|----------|----------------------------------| +| **Linux / macOS** | `./scripts/build.sh` | +| **Windows (PowerShell)** | `.\scripts\build.ps1` | + +The script will: -You'll need git, python3, CMake, Ninja and the GCC ARM toolchain installed. CMake scripts will patch some files in Bluepad32 and BTStack and also make sure all git submodules (plus their submodules and dependencies) are downloaded. Here's an example on Windows: +1. **Check prerequisites** — Verifies that `git`, `python3` (or `python`), `cmake`, `ninja`, and `arm-none-eabi-gcc` are installed. If something is missing, it prints install hints (e.g. `apt install` on Debian/Ubuntu, `brew install` on macOS, Chocolatey on Windows). +2. **Ask for your board** — Choose from a numbered list (Pi Pico, Pi Pico W, Pi Pico 2 W, RP2040-Zero, RP2350-Zero, RP2350-USB-A, XIAO, **RP2354**, Adafruit Feather, 4CH I2C, ESP32 hybrid, etc.). +3. **Ask for build type** — **Default** (all output modes available via button combos) or **Fixed output mode** (e.g. Wii, GameCube, N64, **STEAM**, PS4 — one mode only, no combo switching). +4. **Ask for configuration** — **Release** (smaller, faster) or **Debug** (UART logging for troubleshooting). + +Build output (`.uf2`, `.elf`, etc.) is written to **`scripts/build/`**. To flash the board, copy the `.uf2` file from that folder to the Pico’s USB drive. If the build fails, the script can save a log to `scripts/build_log.txt` for debugging. + +#### Manual build (CMake from command line) + +If you prefer to run CMake yourself or use an IDE (e.g. VSCode CMake Tools), use the same options the scripts pass. Example (Windows, Pico W, Release): ``` -git clone --recursive https://github.com/wiredopposite/OGX-Mini.git -cd OGX-Mini/Firmware/RP2040 +git clone --recursive https://github.com/MegaCadeDev/OGX-Mini-2026.git +cd OGX-Mini-2026/Firmware/RP2040 cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DOGXM_BOARD=PI_PICOW -DMAX_GAMEPADS=1 cmake --build build ``` -Or just install the GCC ARM toolchain and use the CMake Tools extension in VSCode. +Outputs (`.elf`, `.uf2`, etc.) are in the build directory; flash the `.uf2` to the board. Or use the GCC ARM toolchain and CMake Tools extension in VSCode. + +**Latency:** The adapter sends the latest gamepad state whenever the USB endpoint is free (XInput and similar), and the main loop has no added delay by default, so the host gets updates at its poll rate with minimal latency. See [Firmware/RP2040/docs/IMPROVEMENTS.md](Firmware/RP2040/docs/IMPROVEMENTS.md#latency-reduction) for details. + +### Firmware documentation +| Document | Description | +|---------|-------------| +| [**Controller_Mappings.md**](Firmware/RP2040/docs/Controller_Mappings.md) | **Button/stick mappings** for all input controllers and USB/GPIO output modes (PadIn reference). | +| [Wii_Mode_Guide.md](Firmware/RP2040/docs/Wii_Mode_Guide.md) | Wii mode (build-option only): No Extension / Nunchuk / Classic, USB host, sync and auto-connect, button mapping. | +| [PICO2W_WII_USB_SETUP.md](Firmware/RP2040/docs/PICO2W_WII_USB_SETUP.md) | Pico 2 W / Pico W: USB host wiring (PIO USB), pins, build, troubleshooting for Wii mode. | +| [GPIO_Output_Pinout_and_Mappings.md](Firmware/RP2040/docs/GPIO_Output_Pinout_and_Mappings.md) | GPIO pin-outs and mappings for PS1/PS2, Dreamcast, GameCube, N64. | +| [IMPROVEMENTS.md](Firmware/RP2040/docs/IMPROVEMENTS.md) | Firmware improvements: PS3 fixes, **PS3/PS4 motion passthrough**, latency, XInput/360, Pico W Bluetooth, **Switch 2 Pro + Joy-Con 2 BLE**, **Steam Controller 2026 (Triton) BLE**, **DS3 USB→BT auto-pair**, **Pico W PIO USB wired unplug detection**. | ### ESP32 Please see the Hardware directory for a diagram showing how to hookup the ESP32 to your RP2040. @@ -141,3 +395,25 @@ Please see the Hardware directory for a diagram showing how to hookup the ESP32 You will need ESP-IDF v5.1, esptool, python3, and git installed. If you use VSCode, you can install the ESP-IDF extension and configure the project for ESP-IDF v5.1, it'll download everything for you and then you just click the build button at the bottom of the window. When you build with ESP-IDF, Cmake will run a python script that copies the necessary BTStack files into the components directory, this is needed since BTStack isn't configured as an ESP-IDF component when you download it with git. + + +# Credit to the original creator [https://wiredopposite.github.io/](https://github.com/wiredopposite/OGX-Mini/tree/master) for the original base of the project! + +## Other projects that have helped enhance this fork + +- **[Sakura-Research-Lab/OGX-Mini-2026-Testing](https://github.com/Sakura-Research-Lab/OGX-Mini-2026-Testing)** — OG Xbox Guide button (tap/hold IGR/shutdown), board support for RP2350_ZERO, RP2040_XIAO, RP2354, and 8BitDo XInput (Xbox 360) LED keepalive fix. +- **[Joypad OS](https://github.com/joypad-ai/joypad-os)** — Reference for Xbox 360 (XSM3) authentication with retail consoles; XInput mode on 360 follows the same approach. Descriptors and XSM3 flow are aligned with joypad-os; single config, full XSM3 security string, init/verify in main loop; USB is initialized before Core1 so the 360 can enumerate while BT loads. +- **[Bluepad32](https://github.com/ricardoquesada/bluepad32)** — Bluetooth controller support (Pico W / Pico 2 W / RP2354). +- **[faithvoid](https://github.com/faithvoid)** — Xbox 360 controller vibration/rumble fix (host-side rumble handling, including wireless RUMBLE_ENABLE sequence). +- **[PicoGamepadConverter](https://github.com/wiredopposite/PicoGamepadConverter)** — Wii (Wiimote) output mode: approach of USB gamepad on PIO USB with Bluetooth reserved for the Wiimote link, and button/stick mappings for No Extension, Nunchuk, and Classic Controller report modes. +- **[retro-pico-switch](https://github.com/DavidPagels/retro-pico-switch)** — Reference for Nintendo Switch Pro Controller protocol and report layout (N64/GameCube → Raspberry Pi Pico → Switch via USB or Bluetooth). Used to align OGX-Mini’s Switch mode with correct Pro Controller button byte layout and emulation behavior. +- **[Nadeflore/switch2-controllers](https://github.com/Nadeflore/switch2-controllers)** — Foundational **Switch 2** BLE discovery, pairing, and GATT protocol work used to implement wireless **Switch 2 Pro** support on Pico W / Pico 2 W. +- **[TommyWabg/switch2-controllers-windows10-gyro](https://github.com/TommyWabg/switch2-controllers-windows10-gyro)** — Extended Switch 2 fork: **63-byte composed input report**, button bitfield, init/feature commands, and **Pro Controller** rumble packet format (fork of Nadeflore’s project). +- **[BlueRetro #1249](https://github.com/darthcloud/BlueRetro/issues/1249)** (darthcloud) — Community reverse engineering of Switch 2 **63-byte BLE reports** and GATT notify handles. +- **[SDL3 `SDL_hidapi_steam_triton`](https://github.com/libsdl-org/SDL/blob/main/src/joystick/hidapi/SDL_hidapi_steam_triton.c)** / [`controller_structs.h`](https://github.com/libsdl-org/SDL/blob/main/src/joystick/hidapi/steam/controller_structs.h) — Steam Controller 2026 (Triton) BLE report IDs, button bits, lizard mode, and rumble layout used for OGX Bluepad32 support. + +## Licenses and third-party code + +- **Xbox 360 console authentication:** XInput builds that work on retail Xbox 360 consoles use [libxsm3](https://github.com/InvoxiPlayGames/libxsm3) (Xbox Security Method 3). libxsm3 is licensed under the GNU Lesser General Public License v2.1 or later; see `Firmware/external/libxsm3/LICENSE.txt`. libxsm3 credits oct0xor, emoose (ExCrypt), and sanjay900; see `Firmware/external/libxsm3/README.md`. +- **Other dependencies:** See the root `LICENSE` file and the `Firmware/external` directory for Bluepad32, BTStack, TinyUSB, Pico SDK, and other third-party licenses. + diff --git a/Tools/README.md b/Tools/README.md index 9b181361..e8ec30c2 100644 --- a/Tools/README.md +++ b/Tools/README.md @@ -1,2 +1,9 @@ -# Dumping Xbox DVD dongle firmware -The firmware for the DVD Playback Kit is not included here, but you can dump your own or place a `.BIN` dump in this directory. Whichever you do, you'll have to run `dump-xremote-firmware.py` to have it included with the firmware when you compile it. \ No newline at end of file +# Tools + +## Controller mapping capture (new gamepad support) + +See **[controller_capture/README.md](controller_capture/README.md)**. Run `controller_capture.py` on your PC to record button/axis mappings and VID/PID for maintainers when requesting support for a new controller. + +## Dumping Xbox DVD dongle firmware + +The firmware for the DVD Playback Kit is not included here, but you can dump your own or place a `.BIN` dump in this directory. Whichever you do, you'll have to run `dump-xremote-firmware.py` to have it included with the firmware when you compile it. \ No newline at end of file diff --git a/Tools/controller_capture/README.md b/Tools/controller_capture/README.md new file mode 100644 index 00000000..03059f02 --- /dev/null +++ b/Tools/controller_capture/README.md @@ -0,0 +1,117 @@ +# Controller mapping capture (`controller_capture.py`) + +Interactive helper for **OGX-Mini** users who want a **new gamepad** supported. It records how your PC sees the controller through **SDL / pygame** (button indices, axis indices, hat values, SDL GUID) and **USB VID:PID**, then writes a **single text file** you can attach to an issue or send to a maintainer. + +**USB and Bluetooth** are both supported: if the OS exposes the pad to SDL (pair Bluetooth in system settings first), it will appear in the list. + +### Nintendo Switch 2 Pro / Joy-Con 2 (USB) + +**Switch 2–family** pads (e.g. **Pro Controller 2**, **Joy-Con 2**) often **do not show up in SDL** on a PC until a host sends Nintendo’s **USB bulk** bring-up sequence on **interface 1** (same approach as [HandHeldLegend’s ProCon 2 Enabler](https://github.com/HandHeldLegend/handheldlegend.github.io) — `procon2tool`). + +In this tool, pick the platform: + +**“Nintendo Switch 2 Pro / Joy-Con 2 (USB — libusb handshake, then capture)”** + +The script will run **`switch2_usb_init.py`** (PyUSB + **libusb**) first, then start pygame so you can map controls. You still need **PyUSB** and OS access to the device (Linux: `libusb-1.0` + permissions; Windows: may need **WinUSB** on the bulk interface via Zadig). + +This does **not** flash firmware; it only runs on your computer. + +## Requirements + +- **Python 3.10+** +- `pygame` 2.x +- **Windows, macOS, or Linux** (same script; VID/PID auto-detection is only attempted on Linux) + +```bash +cd Tools/controller_capture +python3 -m pip install -r requirements.txt +python3 controller_capture.py +``` + +On Windows you may use `py` instead of `python3`. + +Optional CLI flags: + +- `python3 controller_capture.py -o my_report.txt` — set the output path up front (`-o` wins over auto-naming). +- `python3 controller_capture.py --use-sdl-name` — use the **SDL joystick name** as `controller_full_name` (no typing). +- `python3 controller_capture.py --auto-output-name` — save as **`_.txt`** without filename prompt (implies save; only when `-o` is not set). +- Combine: `python3 controller_capture.py --use-sdl-name --auto-output-name` for a quick non-interactive name + file. +- **`python3 controller_capture.py --raw-hid-only`** — **Linux only**: skips SDL/pygame entirely; runs only the **hidraw** hex capture (pick the same platform layout for prompts; use the **Switch 2 USB** platform if you need the bulk handshake first). Pair with `--auto-output-name` to save without prompts. + +Interactive shortcuts (without flags): + +- At the controller name prompt, **press Enter** to use the SDL-reported name. +- When saving, the suggested default filename is **`_.txt`** (press Enter to accept). + +## What you’ll do + +1. Choose the **platform layout** your controller matches (Xbox, PlayStation, Switch, etc.). +2. Pick the **controller** from the list SDL sees (**USB cable or paired Bluetooth**). +3. Enter the **full product name** of the pad (box / manufacturer name), **or press Enter** to use the SDL name, **or** run with **`--use-sdl-name`**. The report records the source (`controller_full_name_source` in JSON). +4. For each on-screen prompt, press **only** that button or move that stick direction. After each capture, **release everything and re-center the sticks** — the script waits for a neutral pose before the next prompt so it doesn’t pick up the previous control again. +5. At the end, follow the **on-screen steps** to find **VID and PID** for your OS (Windows / macOS / Linux). On Linux, values may be pre-filled; you can confirm or override. +6. Save the generated **`.txt` file** (default suggestion is derived from the controller name) and upload it when requesting support. + +On **Linux**, after SDL mapping the tool will ask whether to capture **raw HID input reports** from **`/dev/hidraw*`** (full report bytes in hex, per control, with an XOR diff vs idle). **Say yes** when asking for firmware support — that data matches what embedded code decodes (not SDL indices alone). + +On **Linux**, after VID/PID is known the tool asks whether to record **raw HID** **alongside pygame**: after each SDL capture it reads the latest **hidraw** packet, so **pygame decides when** you pressed (no IMU/timer false triggers). Those fields live on each binding: `hidraw_baseline_hex`, `hidraw_active_hex`, `hidraw_xor_hex`. Use **`--no-hidraw-sidecar`** to skip that prompt. + +The file includes both **human-readable** lines and a **JSON** block (`schema_version` **9**) with the same data for tooling. When the **Linux hidraw sidecar** is enabled, each binding includes **full** `hidraw_*_hex` strings plus lengths; the text report also has a **“Full hidraw packets”** section (16 bytes per line) so you do not need a separate dump script for that run. **`capture_mode`** is `full` (SDL, optional raw) or `raw_hid_only`. **`hidraw_gated_by_sdl`** summarizes the sidecar path. Optional **`raw_hid_reports`** is from the separate interactive hidraw walkthrough. **`controller_full_name_source`** is `sdl`, `user_entered`, or `hidraw` (raw-only). Use **`hidraw_full_report_dump.py`** only if you want a **live** stream unrelated to the SDL prompts. + +Use **`python3 controller_capture.py --skip-raw-hid`** if you only want SDL/VID capture and no hidraw walkthrough. + +## Finding which HID bytes a control uses (for firmware) + +When SDL indices are not enough (common on **Switch 2 Pro USB**), you need the **`hidraw_*` hex** on each binding. + +1. **Capture** with **Linux + SDL+hidraw sidecar** (or raw-hid flow) so every binding has `hidraw_baseline_hex` and `hidraw_active_hex` (same length, typically **64** bytes for full reports). +2. **XOR** baseline and active byte-for-byte. Any index where the result is **non‑zero** changed between “idle” and “pressed” for that sample. +3. **Ignore index 1** when reasoning about buttons: it is usually the **USB report timer** and toggles every frame. +4. **Start with indices 2–12** (report id + timer + the **10‑byte Nintendo “core”**: battery, three button bytes, six stick bytes). Many controls only touch those. If two different physical controls share the same XOR pattern, you need **more bytes** in the signature or a different test (see below). +5. **Sticks share bytes with some digitals** on Switch 2–style reports: the same byte may encode stick data *and* a button transition. Always compare your candidate XOR to **every axis binding** in the same file (`axis_lstick_*`, `axis_rstick_*`). If an axis **active** report matches the same **(byte21, byte22, …)** pattern as a button, a firmware decoder cannot use that short prefix alone. +6. **Indices 13+** often include **IMU / vendor fields** that move every frame even at rest. Treat them as **high risk** for ghost inputs unless you prove they are stable when idle on real hardware, or you use **relative** decoding (difference from a calibrated neutral) plus debouncing. +7. **Baselines differ per prompt**: each binding’s `hidraw_baseline_hex` was taken when you were “neutral” for *that* step, so absolute values at index 21+ are **not** comparable across rows—**XOR within the same row** is the reliable operation. + +**Helper script** (prints XOR indices and `active[21:24]` for collision spotting): + +```bash +python3 analyze_hidraw_xor.py Nintendo_Switch_2_Pro_Controller_YYYYMMDD_HHMMSS.txt --skip-timer +``` + +**Live full-report dump** (Linux): stream every hidraw read as one hex line so you can press **one** button at a time and see which indices change, separate from the interactive mapper: + +```bash +cd Tools/controller_capture +python3 hidraw_full_report_dump.py --vid 057e --pid 2069 +# or: python3 hidraw_full_report_dump.py --path /dev/hidraw4 +# only print when something changes: +python3 hidraw_full_report_dump.py --vid 057e --pid 2069 --diff +# ignore pure IMU tail jitter; print when “core” bytes 2:12 (masked) change: +python3 hidraw_full_report_dump.py --vid 057e --pid 2069 --diff-core +``` + +Redirect to a file if you want a log: `… > dump.txt`. Compare **idle** vs **held** lines by eye or `diff`; bytes that **always** change every line even at rest are not good solo signatures. + +After you identify a safe **mask + value** (or multi-byte signature), add it to the host driver and **verify on hardware** with sticks moved to extremes while tapping the control. + +## What maintainers use this for + +- **`capture_mode`**: `full` (SDL bindings + optional raw) vs `raw_hid_only` (hex only, no pygame indices). +- **`controller_full_name`**: human-readable name for changelog / Wired Controllers lists / issue tracking (retail name, SDL name, or hidraw `HID_NAME`). +- **`controller_full_name_source`**: `sdl`, `user_entered`, or `hidraw`. +- **`connection_guess`**: on Linux, best-effort `usb` / `bluetooth` / `unknown` from sysfs (optional context). +- **`HardwareIDs.h`**: `{ VID, PID }` entries for the right host driver (DInput, PS3, PS4, …). +- **SDL name/GUID**: cross-check with other databases and real device names. +- **Binding table**: map logical controls to **button / axis / hat indices** as the host stack reports them (cross-check vs other hosts). +- **Per-binding `hidraw_*` fields** (Linux, SDL+hidraw sidecar): raw bytes when pygame saw the control — preferred over the standalone walkthrough for Nintendo pads. +- **`raw_hid_reports`** (optional second phase): interactive hid-only capture — use **`xor_hex`** if you need it; often redundant if sidecar succeeded. + +## Limitations + +- Captures **what your OS + SDL expose** for the mapping table. If the device is in the wrong mode (e.g. XInput vs DInput), unplug/replug or toggle mode per the manual and run again. +- **Bluetooth** must be **paired and connected** in the OS before running; if capture is unstable, try **USB** for the same mapping run. +- **Raw hidraw** capture is **Linux-only** (needs a matching `/dev/hidraw` for your VID:PID and read access). On Windows/macOS, use Linux for the hex dump or a USB analyzer; the SDL mapping file is still useful alone. + +## Troubleshooting + +The script prints tips if inputs don’t register (Steam Input, USB ports, drivers, Linux permissions, Bluetooth pairing, etc.). See also the main repo **[Wired Controllers](../../Firmware/RP2040/docs/Wired_Controllers.md)** doc. diff --git a/Tools/controller_capture/analyze_hidraw_xor.py b/Tools/controller_capture/analyze_hidraw_xor.py new file mode 100644 index 00000000..0b3c9eb0 --- /dev/null +++ b/Tools/controller_capture/analyze_hidraw_xor.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 +""" +Load a controller_capture .txt (schema_version 8+ JSON) and summarize hidraw XOR patterns. + +Use this to find which report byte indices change for each control, and to check +collisions (same bytes changing for two different controls — often means you need +more bytes in the signature or a different neutral pose). + +Usage: + python3 analyze_hidraw_xor.py path/to/capture.txt + python3 analyze_hidraw_xor.py path/to/capture.txt --max-index 31 +""" + +from __future__ import annotations + +import argparse +import json +import re +import sys +from pathlib import Path + + +def load_json_block(path: Path) -> dict: + text = path.read_text(encoding="utf-8") + start = text.find('{\n "schema_version"') + if start < 0: + start = text.find('{"schema_version"') + if start < 0: + raise ValueError("No JSON block with schema_version found") + depth = 0 + for i in range(start, len(text)): + if text[i] == "{": + depth += 1 + elif text[i] == "}": + depth -= 1 + if depth == 0: + return json.loads(text[start : i + 1]) + raise ValueError("Unclosed JSON") + + +def hx(s: str) -> bytes: + s = re.sub(r"\s+", "", s) + return bytes.fromhex(s) + + +def nz(x: bytes, hi: int) -> list[tuple[int, int]]: + return [(i, x[i]) for i in range(min(len(x), hi)) if x[i] != 0] + + +def main() -> int: + ap = argparse.ArgumentParser(description="Summarize hidraw XOR per binding from capture JSON") + ap.add_argument("capture_txt", type=Path) + ap.add_argument("--max-index", type=int, default=24, help="Max byte index to list (exclusive)") + ap.add_argument("--skip-timer", action="store_true", help="Omit index 1 from printed XOR list") + args = ap.parse_args() + + data = load_json_block(args.capture_txt) + bindings = data.get("maintainer_fields", {}).get("bindings", {}) + if not bindings: + print("No bindings in JSON", file=sys.stderr) + return 1 + + hi = max(16, args.max_index) + rows: list[tuple[str, bytes, bytes]] = [] + for name in sorted(bindings.keys()): + b = bindings[name] + if "hidraw_baseline_hex" not in b or "hidraw_active_hex" not in b: + continue + base = hx(b["hidraw_baseline_hex"]) + act = hx(b["hidraw_active_hex"]) + if len(base) != len(act): + print(f"{name}: baseline/active length mismatch", file=sys.stderr) + continue + x = bytes(a ^ b for a, b in zip(base, act)) + rows.append((name, x, act)) + + print(f"# Report length: {len(rows[0][1])} bytes (first binding)\n") + for name, x, act in rows: + pairs = nz(x, hi) + if args.skip_timer: + pairs = [(i, v) for i, v in pairs if i != 1] + sig = act[21:24].hex() if len(act) >= 24 else "" + print(f"{name}:") + print(f" xor[0..{hi}): " + " ".join(f"{i:02d}:{v:02x}" for i, v in pairs)) + if sig: + print(f" active[21:24]: {sig}") + print() + + # Collision check on active[21:22] for controls that have hidraw + d: dict[tuple[int, int], list[str]] = {} + for name, _x, act in rows: + if len(act) < 23: + continue + key = (act[21], act[22]) + d.setdefault(key, []).append(name) + dup = {k: v for k, v in d.items() if len(v) > 1} + if dup: + print("# active[21:22] collisions (need longer signature or not discriminant):") + for k, names in sorted(dup.items()): + print(f" {k[0]:02x} {k[1]:02x}: {', '.join(names)}") + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Tools/controller_capture/controller_capture.py b/Tools/controller_capture/controller_capture.py new file mode 100644 index 00000000..41da830d --- /dev/null +++ b/Tools/controller_capture/controller_capture.py @@ -0,0 +1,1299 @@ +#!/usr/bin/env python3 +""" +OGX-Mini controller input capture tool. + +Walks the user through selecting a target platform, choosing a connected +gamepad (via SDL/pygame), then mapping each control. Works with **USB** and +**Bluetooth** controllers as long as the OS exposes them to SDL. Produces a +text/JSON report for maintainers to add HardwareIDs / HID layout support. + +Usage: + python3 controller_capture.py + python3 controller_capture.py -o my_pad.txt + python3 controller_capture.py --skip-raw-hid # SDL/VID only; no hidraw hex phase + python3 controller_capture.py --use-sdl-name --auto-output-name # SDL name + auto .txt filename + python3 controller_capture.py --raw-hid-only --auto-output-name # Linux: hidraw hex only, no pygame + python3 controller_capture.py --no-hidraw-sidecar # SDL only; no hidraw alongside pygame +""" + +from __future__ import annotations + +import argparse +import json +import os +import re +import platform +import sys +import textwrap +import time +from dataclasses import dataclass, field +from datetime import datetime, timezone +from typing import Any + +from switch2_usb_init import platform_requires_switch2_usb_init, run_switch2_usb_handshake + +# --------------------------------------------------------------------------- +# Platform → ordered list of logical controls to capture +# Labels are what we show the user; ids are stable keys in the output. +# --------------------------------------------------------------------------- + +_SWITCH_PRO_LAYOUT: list[tuple[str, str]] = [ + ("face_b", "B (bottom)"), + ("face_a", "A (right)"), + ("face_y", "Y (left)"), + ("face_x", "X (top)"), + ("shoulder_l", "L"), + ("shoulder_r", "R"), + ("trigger_zl", "ZL — press fully"), + ("trigger_zr", "ZR — press fully"), + ("center_minus", "Minus (−)"), + ("center_plus", "Plus (+)"), + ("center_home", "Home"), + ("center_capture", "Capture (or skip)"), + ("stick_l3", "Left stick click"), + ("stick_r3", "Right stick click"), + ("dpad_up", "D-pad UP"), + ("dpad_down", "D-pad DOWN"), + ("dpad_left", "D-pad LEFT"), + ("dpad_right", "D-pad RIGHT"), + ("axis_lstick_left", "Left stick — fully LEFT"), + ("axis_lstick_right", "Left stick — fully RIGHT"), + ("axis_lstick_up", "Left stick — fully UP"), + ("axis_lstick_down", "Left stick — fully DOWN"), + ("axis_rstick_left", "Right stick — fully LEFT"), + ("axis_rstick_right", "Right stick — fully RIGHT"), + ("axis_rstick_up", "Right stick — fully UP"), + ("axis_rstick_down", "Right stick — fully DOWN"), +] + +PLATFORM_CONTROLS: dict[str, list[tuple[str, str]]] = { + "Xbox / XInput (360, One, Series, Elite)": [ + ("face_a", "A (bottom face)"), + ("face_b", "B (right face)"), + ("face_x", "X (left face)"), + ("face_y", "Y (top face)"), + ("shoulder_lb", "Left bumper (LB)"), + ("shoulder_rb", "Right bumper (RB)"), + ("trigger_lt", "Left trigger (LT) — press fully"), + ("trigger_rt", "Right trigger (RT) — press fully"), + ("center_view", "View / Back / Select"), + ("center_menu", "Menu / Start"), + ("center_guide", "Guide / Xbox / Nexus button"), + ("stick_l3", "Left stick click (L3)"), + ("stick_r3", "Right stick click (R3)"), + ("dpad_up", "D-pad UP"), + ("dpad_down", "D-pad DOWN"), + ("dpad_left", "D-pad LEFT"), + ("dpad_right", "D-pad RIGHT"), + ("axis_lstick_left", "Left stick — push fully LEFT"), + ("axis_lstick_right", "Left stick — push fully RIGHT"), + ("axis_lstick_up", "Left stick — push fully UP"), + ("axis_lstick_down", "Left stick — push fully DOWN"), + ("axis_rstick_left", "Right stick — push fully LEFT"), + ("axis_rstick_right", "Right stick — push fully RIGHT"), + ("axis_rstick_up", "Right stick — push fully UP"), + ("axis_rstick_down", "Right stick — push fully DOWN"), + ], + "PlayStation (DS3 / DS4 / DualSense / generic)": [ + ("face_cross", "Cross (×)"), + ("face_circle", "Circle (○)"), + ("face_square", "Square (□)"), + ("face_triangle", "Triangle (△)"), + ("shoulder_l1", "L1"), + ("shoulder_r1", "R1"), + ("trigger_l2", "L2 — press fully"), + ("trigger_r2", "R2 — press fully"), + ("center_share", "Share / Select"), + ("center_options", "Options / Start"), + ("center_ps", "PlayStation / PS button"), + ("touchpad_click", "Touchpad click (or skip if none)"), + ("stick_l3", "L3 (left stick click)"), + ("stick_r3", "R3 (right stick click)"), + ("dpad_up", "D-pad UP"), + ("dpad_down", "D-pad DOWN"), + ("dpad_left", "D-pad LEFT"), + ("dpad_right", "D-pad RIGHT"), + ("axis_lstick_left", "Left stick — fully LEFT"), + ("axis_lstick_right", "Left stick — fully RIGHT"), + ("axis_lstick_up", "Left stick — fully UP"), + ("axis_lstick_down", "Left stick — fully DOWN"), + ("axis_rstick_left", "Right stick — fully LEFT"), + ("axis_rstick_right", "Right stick — fully RIGHT"), + ("axis_rstick_up", "Right stick — fully UP"), + ("axis_rstick_down", "Right stick — fully DOWN"), + ], + "Nintendo Switch Pro / wired Switch layout": list(_SWITCH_PRO_LAYOUT), + # Switch 2 / Joy-Con 2: PC does not expose SDL input until USB bulk handshake (see switch2_usb_init.py). + "Nintendo Switch 2 Pro / Joy-Con 2 (USB — libusb handshake, then capture)": list(_SWITCH_PRO_LAYOUT), + "Generic DInput / PC gamepad": [ + ("face_1", "Primary action (often '1' or South)"), + ("face_2", "Secondary (often '2' or East)"), + ("face_3", "Tertiary"), + ("face_4", "Quaternary"), + ("shoulder_l", "Left shoulder"), + ("shoulder_r", "Right shoulder"), + ("trigger_l", "Left trigger — fully"), + ("trigger_r", "Right trigger — fully"), + ("select", "Select / Back"), + ("start", "Start"), + ("mode", "Mode / Home / Guide (if present)"), + ("stick_l3", "Left stick click"), + ("stick_r3", "Right stick click"), + ("dpad_up", "D-pad UP"), + ("dpad_down", "D-pad DOWN"), + ("dpad_left", "D-pad LEFT"), + ("dpad_right", "D-pad RIGHT"), + ("axis_lstick_left", "Left stick — fully LEFT"), + ("axis_lstick_right", "Left stick — fully RIGHT"), + ("axis_lstick_up", "Left stick — fully UP"), + ("axis_lstick_down", "Left stick — fully DOWN"), + ("axis_rstick_left", "Right stick — fully LEFT"), + ("axis_rstick_right", "Right stick — fully RIGHT"), + ("axis_rstick_up", "Right stick — fully UP"), + ("axis_rstick_down", "Right stick — fully DOWN"), + ], + "Wii / Wii U style (Classic / adapter — what PC sees)": [ + ("face_a", "A"), + ("face_b", "B"), + ("face_x", "X"), + ("face_y", "Y"), + ("shoulder_l", "L"), + ("shoulder_r", "R"), + ("trigger_zl", "ZL"), + ("trigger_zr", "ZR"), + ("minus", "Minus"), + ("plus", "Plus"), + ("home", "Home"), + ("stick_l3", "Left stick click (if any)"), + ("stick_r3", "Right stick click (if any)"), + ("dpad_up", "D-pad UP"), + ("dpad_down", "D-pad DOWN"), + ("dpad_left", "D-pad LEFT"), + ("dpad_right", "D-pad RIGHT"), + ("axis_lstick_left", "Left stick — LEFT"), + ("axis_lstick_right", "Left stick — RIGHT"), + ("axis_lstick_up", "Left stick — UP"), + ("axis_lstick_down", "Left stick — DOWN"), + ("axis_rstick_left", "Right stick — LEFT"), + ("axis_rstick_right", "Right stick — RIGHT"), + ("axis_rstick_up", "Right stick — UP"), + ("axis_rstick_down", "Right stick — DOWN"), + ], + "Original Xbox (Duke / S) — if exposed as HID on PC": [ + ("face_a", "A"), + ("face_b", "B"), + ("face_x", "X"), + ("face_y", "Y"), + ("black", "Black"), + ("white", "White"), + ("trigger_l", "Left trigger"), + ("trigger_r", "Right trigger"), + ("back", "Back"), + ("start", "Start"), + ("stick_l3", "Left stick click"), + ("stick_r3", "Right stick click"), + ("dpad_up", "D-pad UP"), + ("dpad_down", "D-pad DOWN"), + ("dpad_left", "D-pad LEFT"), + ("dpad_right", "D-pad RIGHT"), + ("axis_lstick_left", "Left stick — LEFT"), + ("axis_lstick_right", "Left stick — RIGHT"), + ("axis_lstick_up", "Left stick — UP"), + ("axis_lstick_down", "Left stick — DOWN"), + ("axis_rstick_left", "Right stick — LEFT"), + ("axis_rstick_right", "Right stick — RIGHT"), + ("axis_rstick_up", "Right stick — UP"), + ("axis_rstick_down", "Right stick — DOWN"), + ], + "Other / not listed (custom list)": [ + ("ctrl_01", "Control 1 — press or move as instructed next"), + ("ctrl_02", "Control 2"), + ("ctrl_03", "Control 3"), + ("ctrl_04", "Control 4"), + ("ctrl_05", "Control 5"), + ("ctrl_06", "Control 6"), + ("ctrl_07", "Control 7"), + ("ctrl_08", "Control 8"), + ("ctrl_09", "Control 9"), + ("ctrl_10", "Control 10"), + ("ctrl_11", "Control 11"), + ("ctrl_12", "Control 12"), + ], +} + +TROUBLESHOOTING = """ +If this program does not see your controller or inputs do not register: + + • USB: unplug and replug; try a different port (USB 2.0 often best) and a + data-capable cable (many cables are charge-only). + • Bluetooth: pair and connect the controller in **Windows Settings / macOS + System Settings / Linux BlueZ** first, then run this script. Put the pad in + pairing mode if it won’t connect. Stay within range; low battery can drop input. + • If Bluetooth is flaky, try **USB** for this capture (same mapping is still useful). + • Close Steam (Steam Input can remap or hide raw gamepad input). In Steam: + Settings → Controller → disable Steam Input for the gamepad, or exit Steam. + • On Windows: check Device Manager — controller should appear under + "Human Interface Devices", "Bluetooth", or "Xbox peripherals" without a yellow + warning. Update or reinstall the vendor driver if needed. + • On Linux: your user may need access to /dev/input/js* and event nodes. + Install `steam-devices` or add a udev rule; try: `sudo chmod a+r /dev/input/*` + temporarily to test (not a permanent fix). + • Multi-mode pads (8BitDo, etc.): switch to DInput, XInput, or Switch mode + per the manual — use the mode that matches how OGX-Mini will see the pad. + • Run this script from a normal terminal with the gamepad connected before start. + • If nothing helps: note your OS version, controller model, USB vs Bluetooth, + and how it appears in Device Manager / System Information / `lsusb`, and send + that with your mapping file. +""" + + +def print_troubleshooting() -> None: + print(TROUBLESHOOTING) + + +# --------------------------------------------------------------------------- +# USB VID:PID (Linux sysfs) +# --------------------------------------------------------------------------- + + +def try_linux_connection_guess(js_index: int) -> str: + """Best-effort: whether the js device path looks USB- or Bluetooth-attached.""" + try: + path = os.path.realpath(f"/sys/class/input/js{js_index}/device") + except OSError: + return "unknown" + segments = [s.lower() for s in path.split(os.sep)] + if "bluetooth" in segments: + return "bluetooth" + for seg in segments: + if len(seg) >= 4 and seg.startswith("usb") and seg[3:].isdigit(): + return "usb" + if "/usb" in path.replace("\\", "/").lower(): + return "usb" + return "unknown" + + +def try_linux_vid_pid_for_js(js_index: int) -> tuple[str | None, str | None, str | None]: + """Walk sysfs from /sys/class/input/jsN to find idVendor/idProduct.""" + base = f"/sys/class/input/js{js_index}/device" + if not os.path.isdir(base): + return None, None, None + path = os.path.realpath(base) + for _ in range(12): + vid_p = os.path.join(path, "idVendor") + pid_p = os.path.join(path, "idProduct") + if os.path.isfile(vid_p) and os.path.isfile(pid_p): + try: + with open(vid_p, encoding="utf-8") as f: + vid = f.read().strip().lower() + with open(pid_p, encoding="utf-8") as f: + pid = f.read().strip().lower() + return vid, pid, path + except OSError: + break + parent = os.path.dirname(path) + if parent == path: + break + path = parent + return None, None, None + + +def print_vid_pid_instructions() -> None: + """Step-by-step VID/PID lookup for the current OS (for HardwareIDs.h).""" + sysname = platform.system() + print( + textwrap.dedent( + """ + Why we need VID and PID + ----------------------- + Maintainers add a line like { VID, PID } in HardwareIDs.h so the + firmware recognizes your controller. The values are **four hex digits + each** (Vendor ID and Product ID), e.g. VID 045e and PID 0b12. + + """ + ).rstrip() + ) + + if sysname == "Windows": + print( + textwrap.dedent( + """ + Windows — find VID & PID + ------------------------ + 1. Right-click **Start** → **Device Manager**. + 2. Find your controller. Try expanding: + • **Human Interface Devices** (many gamepads) + • **Xbox peripherals** (some Microsoft / Xbox pads) + • **Bluetooth** (if you use the controller wirelessly) + 3. Right-click the device → **Properties**. + 4. Open the **Details** tab. + 5. In **Property**, choose **Hardware Ids** (or **Device instance path**). + 6. In the list, look for a line containing **VID_xxxx** and **PID_yyyy** + (example: `USB\\VID_045E&PID_0B12` or `HID\\VID_045E&PID_0B12`). + 7. Copy **xxxx** as the VID and **yyyy** as the PID (hex, no 0x prefix + needed when you type them below). + + Tip: If you see multiple lines, prefer the one that matches **USB** or + **HID** for the gamepad you are holding. + + """ + ).rstrip() + ) + elif sysname == "Darwin": + print( + textwrap.dedent( + """ + macOS — find VID & PID + ---------------------- + **Option A — System Information** + 1. Hold **Option** → click the Apple menu → **System Information**. + 2. Under **Hardware**, click **USB** (wired) or **Bluetooth** (wireless). + 3. Select your controller in the list. + 4. Look for **Vendor ID** and **Product ID** (often shown in hex). + + **Option B — Terminal (works for many devices)** + • USB: open **Terminal** and run: + system_profiler SPUSBDataType + Scroll to your controller and note **Vendor ID** and **Product ID**. + • Bluetooth: run: + system_profiler SPBluetoothDataType + Find your device; note vendor/product if listed. + + Use the hex values (you may see 0x054C — type **054c** for VID below). + + """ + ).rstrip() + ) + else: + # Linux and other Unix-like + print( + textwrap.dedent( + """ + Linux — find VID & PID + ---------------------- + **USB controller** + 1. Plug the controller in, then in a terminal run: + lsusb + 2. Find the line for your device (name on the right). The form is: + Bus 001 Device 012: ID **1234:5678** Vendor Name + Here **1234** is the VID and **5678** is the PID (hex). + + **Bluetooth controller** + 1. Pair and connect the controller first. + 2. Try: + lsusb + Some adapters show the pad here. If not, try: + bluetoothctl devices + (VID/PID may still appear in **udev** for the input device.) + 3. With the controller connected, replace **0** with your joystick index + if needed (often **js0**): + udevadm info -q property -n /dev/input/js0 | grep -E 'ID_VENDOR_ID|ID_MODEL_ID' + You should see **ID_VENDOR_ID**= and **ID_MODEL_ID**= in hex (0x prefix OK). + + If this script auto-detected VID/PID on Linux, you can still verify with + the commands above. + + """ + ).rstrip() + ) + + +# --------------------------------------------------------------------------- +# Pygame capture +# --------------------------------------------------------------------------- + + +@dataclass +class CaptureState: + """User-provided retail name plus SDL-reported identity for maintainer tracking.""" + + platform: str + controller_full_name: str + pygame_name: str + pygame_guid: str + num_axes: int + num_buttons: int + num_hats: int + os_name: str + vid: str | None = None + pid: str | None = None + vid_pid_source: str = "unknown" + connection_guess: str | None = None # "usb" | "bluetooth" | "unknown" (Linux); None if N/A + bindings: dict[str, Any] = field(default_factory=dict) + baseline_axes: list[float] = field(default_factory=list) + notes: str = "" + skipped: list[str] = field(default_factory=list) + # Result of Switch 2–family USB bulk init (PyUSB), if that platform was selected. + switch2_usb_handshake: dict[str, Any] | None = None + # Linux hidraw: per-control raw input report hex (baseline vs active, xor) for firmware decode. + raw_hid_reports: dict[str, Any] | None = None + # Linux: raw hidraw bytes sampled right after pygame saw each control (no idle guessing). + hidraw_gated_by_sdl: dict[str, Any] | None = None + # Whether controller_full_name was typed by the user, SDL, or Linux hidraw HID_NAME. + controller_full_name_source: str = "user_entered" # "user_entered" | "sdl" | "hidraw" + # "full" = SDL capture (optional raw phase); "raw_hid_only" = --raw-hid-only (Linux). + capture_mode: str = "full" + + +def sanitize_filename_stem(name: str, max_len: int = 80) -> str: + """Make a single path component from a controller name (no dirs).""" + s = name.strip() + for c in '<>:"/\\|?\x00-\x1f': + s = s.replace(c, "_") + s = re.sub(r"\s+", "_", s) + s = re.sub(r"_+", "_", s).strip("._ ") + return (s or "controller")[:max_len] + + +def default_report_filename(st: CaptureState) -> str: + """Suggested report path under cwd: _.txt""" + stem = sanitize_filename_stem(st.controller_full_name) + ts = datetime.now().strftime("%Y%m%d_%H%M%S") + return f"{stem}_{ts}.txt" + + +def init_pygame() -> None: + os.environ.setdefault("SDL_JOYSTICK_ALLOW_BACKGROUND_EVENTS", "1") + import pygame + + pygame.init() + pygame.joystick.init() + + +def poll_joystick_state(joy) -> tuple[list[float], list[int], list[tuple[int, int]]]: + import pygame + + axes = [float(joy.get_axis(i)) for i in range(joy.get_numaxes())] + buttons = [joy.get_button(i) for i in range(joy.get_numbuttons())] + hats = [] + for i in range(joy.get_numhats()): + hats.append(joy.get_hat(i)) + return axes, buttons, hats + + +def wait_for_quiet(joy, settle_s: float = 0.35) -> tuple[list[float], list[int], list[tuple[int, int]]]: + """Read stable resting state.""" + import pygame + + last: tuple | None = None + stable_since: float | None = None + while True: + pygame.event.pump() + state = poll_joystick_state(joy) + if last is not None and state == last: + if stable_since is None: + stable_since = time.monotonic() + elif time.monotonic() - stable_since >= settle_s: + return state + else: + stable_since = None + last = state + time.sleep(0.02) + + +def rebaseline_for_capture(joy) -> tuple[list[float], list[int], list[tuple[int, int]]]: + """ + Clear pygame's queue and sample a new neutral pose before listening for the + *next* control. Without this, a held button or deflected stick from the + previous step immediately matches the next prompt (false 'auto' capture). + """ + import pygame + + pygame.event.clear() + return wait_for_quiet(joy) + + +def _joy_event_matches(event, joy) -> bool: + """Pygame 2 uses instance_id on events; older code used joy index.""" + if hasattr(event, "instance_id"): + try: + return int(event.instance_id) == int(joy.get_instance_id()) + except (AttributeError, TypeError): + pass + return getattr(event, "joy", None) == joy.get_id() + + +def capture_next_input( + joy, + baseline_axes: list[float], + baseline_buttons: list[int], + baseline_hats: list[tuple[int, int]], + timeout_s: float = 45.0, + axis_threshold: float = 0.45, +) -> tuple[str, dict[str, Any]] | tuple[None, None]: + """ + Block until a new button, hat, or significant axis change vs baseline. + Returns (kind, detail_dict). + """ + import pygame + + from pygame import JOYBUTTONDOWN, JOYHATMOTION, JOYAXISMOTION, QUIT + + pygame.event.clear() + + start = time.monotonic() + while time.monotonic() - start < timeout_s: + for event in pygame.event.get(): + if event.type == QUIT: + return None, None + if event.type == JOYBUTTONDOWN and _joy_event_matches(event, joy): + bi = event.button + if bi < len(baseline_buttons) and baseline_buttons[bi]: + continue + return "button", {"button": bi} + if event.type == JOYHATMOTION and _joy_event_matches(event, joy): + if event.value != (0, 0): + return "hat", {"hat": event.hat, "value": list(event.value)} + if event.type == JOYAXISMOTION and _joy_event_matches(event, joy): + ax = event.axis + val = event.value + if ax < len(baseline_axes) and abs(val - baseline_axes[ax]) >= axis_threshold: + return "axis", {"axis": ax, "value": val, "delta_vs_baseline": val - baseline_axes[ax]} + + pygame.event.pump() + axes, buttons, hats = poll_joystick_state(joy) + # Polling fallback for buttons not seen as events on some drivers + for bi, b in enumerate(buttons): + if bi < len(baseline_buttons) and b and not baseline_buttons[bi]: + return "button", {"button": bi} + for hi, h in enumerate(hats): + if hi < len(baseline_hats) and h != baseline_hats[hi] and h != (0, 0): + return "hat", {"hat": hi, "value": list(h)} + for ai, val in enumerate(axes): + if ai < len(baseline_axes) and abs(val - baseline_axes[ai]) >= axis_threshold: + return "axis", {"axis": ai, "value": val, "delta_vs_baseline": val - baseline_axes[ai]} + time.sleep(0.01) + + return None, None + + +def choose_platform() -> str: + names = list(PLATFORM_CONTROLS.keys()) + print("\nSelect the platform / layout this physical controller is meant for:\n") + for i, n in enumerate(names, 1): + print(f" {i}. {n}") + while True: + raw = input("\nEnter number (or 'q' to quit): ").strip().lower() + if raw == "q": + sys.exit(0) + try: + idx = int(raw) + if 1 <= idx <= len(names): + return names[idx - 1] + except ValueError: + pass + print("Invalid choice.") + + +def choose_joystick(): + import pygame + + n = pygame.joystick.get_count() + if n == 0: + print("\nNo joysticks detected. Connect a controller and try again.\n") + print_troubleshooting() + sys.exit(1) + print( + "\nConnected controllers (USB **or** Bluetooth — pair BT devices in the OS first):\n" + ) + for i in range(n): + j = pygame.joystick.Joystick(i) + j.init() + print(f" {i}. {j.get_name()}") + j.quit() + while True: + raw = input("\nEnter index of the controller to capture (or 'q'): ").strip().lower() + if raw == "q": + sys.exit(0) + try: + idx = int(raw) + if 0 <= idx < n: + joy = pygame.joystick.Joystick(idx) + joy.init() + return joy, idx + except ValueError: + pass + print("Invalid index.") + + +def prompt_controller_full_name( + sdl_reported_name: str, + *, + use_sdl_only: bool = False, +) -> tuple[str, str]: + """ + Ask for the full retail / marketing name, or use SDL's name. + + Returns (controller_full_name, source) where source is 'user_entered' or 'sdl'. + """ + header = "\n" + "=" * 72 + "\nController identity\n" + "=" * 72 + "\n\n" + + if use_sdl_only: + print( + header + + "Using **SDL-reported name** as the controller full name (`--use-sdl-name`):\n " + f"{sdl_reported_name!r}\n" + ) + return sdl_reported_name, "sdl" + + print( + header + + "Enter the **full product name** of this controller " + "(as printed on the box, manual, or the manufacturer’s website).\n\n" + f"Your PC / SDL currently reports this device as:\n {sdl_reported_name!r}\n\n" + "**Press Enter** to use that SDL name as-is (optional shortcut).\n\n" + "Examples if you prefer to type a retail name: “Sony DualSense Wireless Controller”, " + "“PowerA Enhanced Wired Controller for Xbox Series X|S”, " + "“8BitDo Pro 2”.\n" + ) + name = input("Full controller name (Enter = use SDL name): ").strip() + if name: + return name, "user_entered" + return sdl_reported_name, "sdl" + + +def prompt_vid_pid_with_instructions(st: CaptureState) -> None: + """ + Show OS-specific VID/PID instructions, then optional manual entry or override. + Updates st.vid, st.pid, st.vid_pid_source in place. + """ + had_auto = bool(st.vid and st.pid) + + print("\n" + "=" * 72) + print("USB Vendor ID (VID) and Product ID (PID)") + print("=" * 72 + "\n") + + print_vid_pid_instructions() + + if had_auto: + print( + f"\n>>> This session already detected: VID: {st.vid} PID: {st.pid}\n" + f" Source: {st.vid_pid_source}\n" + "\n Leave the next two lines **empty** to keep these values in the report,\n" + " or type new hex values if you verified different ones using the steps above.\n" + ) + else: + print( + "\nEnter the **VID** and **PID** you found (hex digits only, no 0x prefix required).\n" + "Leave both **empty** to skip — the report will still help, but adding VID/PID\n" + "makes it much easier to add your controller to the firmware list.\n" + ) + + v = input("VID (hex, or Enter to " + ("keep auto-detected" if had_auto else "skip") + "): ").strip().lower().replace("0x", "") + p = input("PID (hex, or Enter to " + ("keep auto-detected" if had_auto else "skip") + "): ").strip().lower().replace("0x", "") + + if v and p: + st.vid, st.pid = v, p + st.vid_pid_source = "user entered" + elif v or p: + print("\nNeed **both** VID and PID, or leave **both** blank. Keeping previous values.\n") + elif not had_auto: + # leave st.vid/st.pid as None + pass + # else: had_auto and blank input — keep sysfs values already in st + + +def run_capture_loop( + joy, + platform_name: str, + js_index: int, + *, + switch2_usb_handshake: dict[str, Any] | None = None, + use_sdl_name: bool = False, + enable_hidraw_sidecar: bool = True, +) -> CaptureState: + import pygame + + name = joy.get_name() + controller_full_name, name_src = prompt_controller_full_name(name, use_sdl_only=use_sdl_name) + guid = joy.get_guid() + na = joy.get_numaxes() + nb = joy.get_numbuttons() + nh = joy.get_numhats() + + st = CaptureState( + platform=platform_name, + controller_full_name=controller_full_name, + controller_full_name_source=name_src, + pygame_name=name, + pygame_guid=guid, + num_axes=na, + num_buttons=nb, + num_hats=nh, + os_name=f"{platform.system()} {platform.release()}", + switch2_usb_handshake=switch2_usb_handshake, + ) + + if platform.system() == "Linux": + st.connection_guess = try_linux_connection_guess(js_index) + vid, pid, sysfs = try_linux_vid_pid_for_js(js_index) + if vid and pid: + st.vid, st.pid = vid, pid + st.vid_pid_source = f"linux sysfs ({sysfs})" + else: + st.vid_pid_source = "not found in sysfs — use instructions at end" + elif platform.system() == "Windows": + st.vid_pid_source = "not auto-detected — use instructions at end" + else: + st.vid_pid_source = "not auto-detected — use instructions at end" + + controls = PLATFORM_CONTROLS[platform_name] + + hidraw_fd: int | None = None + hidraw_path: str | None = None + if ( + enable_hidraw_sidecar + and platform.system() == "Linux" + and st.vid + and st.pid + ): + try: + vid_i, pid_i = int(st.vid, 16), int(st.pid, 16) + except ValueError: + vid_i = pid_i = -1 + if vid_i >= 0: + from raw_hid_capture import try_open_hidraw_for_sidecar + + side_ans = input( + "\nRecord **raw HID bytes** from hidraw when **pygame** sees each press " + "(recommended on Linux — avoids false captures from IMU/timer noise)? [Y/n]: " + ).strip().lower() + if side_ans in ("", "y", "yes"): + hidraw_fd, hidraw_path = try_open_hidraw_for_sidecar(vid_i, pid_i) + + print( + "\n" + + "=" * 72 + + "\nFor each prompt, press or move ONLY that control.\n" + "After each capture, return sticks to center and release all buttons — the\n" + "script waits for neutral before the next step (avoids false inputs).\n" + "Type 's' + Enter to skip a control, 'r' + Enter to re-capture the previous,\n" + "'t' + Enter to print troubleshooting tips, 'q' + Enter to quit.\n" + + "=" * 72 + + "\n" + ) + + print("Release all buttons and sticks to neutral. Calibrating rest position...") + pygame.event.clear() + axes, buttons, hats = wait_for_quiet(joy) + st.baseline_axes = axes + baseline_buttons = buttons + baseline_hats = hats + + print(f"Baseline axes (approx): {[round(x, 3) for x in axes]}") + print(f"Buttons: {nb}, Axes: {na}, Hats: {nh}\n") + + prev_binding: tuple[str, Any] | None = None + i = 0 + try: + while i < len(controls): + cid, label = controls[i] + print(f"\n>>> [{i + 1}/{len(controls)}] {label}") + print(" Release all buttons and center the sticks — sampling neutral...") + axes, baseline_buttons, baseline_hats = rebaseline_for_capture(joy) + st.baseline_axes = axes + + baseline_hid: bytes | None = None + if hidraw_fd is not None: + from raw_hid_capture import peek_latest_hidraw + + baseline_hid = peek_latest_hidraw(hidraw_fd, 0.08) + + print(" Press or move ONLY the control named above.") + print(" Waiting for input... ", end="", flush=True) + kind, detail = capture_next_input(joy, st.baseline_axes, baseline_buttons, baseline_hats) + if kind is None: + print("\n\nNo input detected for a long time.\n") + print_troubleshooting() + ans = input("\nTry again this step? [Y/n/s=skip]: ").strip().lower() + if ans == "s": + st.skipped.append(cid) + i += 1 + continue + if ans == "n": + break + print("Release all controls to neutral (re-calibrating)...") + pygame.event.clear() + axes, buttons, hats = wait_for_quiet(joy) + st.baseline_axes = axes + baseline_buttons = buttons + baseline_hats = hats + continue + + print(f"captured: {kind} {detail}") + binding: dict[str, Any] = {"type": kind, **detail} + if hidraw_fd is not None: + from raw_hid_capture import peek_latest_hidraw, xor_hex_reports + + active_hid = peek_latest_hidraw(hidraw_fd, 0.12) + if baseline_hid and active_hid and baseline_hid == active_hid: + time.sleep(0.03) + active_hid = peek_latest_hidraw(hidraw_fd, 0.18) + if baseline_hid is not None and active_hid is not None: + binding["hidraw_baseline_hex"] = baseline_hid.hex() + binding["hidraw_active_hex"] = active_hid.hex() + xor_s = xor_hex_reports(baseline_hid, active_hid) + binding["hidraw_xor_hex"] = xor_s + binding["hidraw_baseline_len"] = len(baseline_hid) + binding["hidraw_active_len"] = len(active_hid) + print( + "\n [hidraw] Full baseline packet " + f"(len={len(baseline_hid)}):\n " + + _wrap_hex_lines(baseline_hid.hex()).replace("\n", "\n ") + + "\n [hidraw] Full active packet " + f"(len={len(active_hid)}):\n " + + _wrap_hex_lines(active_hid.hex()).replace("\n", "\n ") + + "\n [hidraw] XOR (baseline ⊕ active):\n " + + _wrap_hex_lines(xor_s).replace("\n", "\n ") + ) + else: + binding["hidraw_note"] = "partial (could not read baseline or active packet)" + + st.bindings[cid] = binding + prev_binding = (cid, st.bindings[cid]) + i += 1 + + # Optional command from user after each capture (stdin) + hint = input("Enter=s continue | s=skip | r=redo last | t=tips | q=quit : ").strip().lower() + if hint == "q": + break + if hint == "t": + print_troubleshooting() + elif hint == "s" and i < len(controls): + cid2, _ = controls[i] + st.skipped.append(cid2) + i += 1 + elif hint == "r" and prev_binding is not None: + i -= 1 + del st.bindings[prev_binding[0]] + finally: + if hidraw_fd is not None: + from raw_hid_capture import close_hidraw_fd + + close_hidraw_fd(hidraw_fd) + + if hidraw_path and any("hidraw_active_hex" in v for v in st.bindings.values() if isinstance(v, dict)): + st.hidraw_gated_by_sdl = { + "hidraw_path": hidraw_path, + "method": "Pygame detects input; latest hidraw report read immediately after (baseline after neutral).", + } + elif hidraw_path: + st.hidraw_gated_by_sdl = { + "hidraw_path": hidraw_path, + "method": "hidraw opened but no complete per-control samples (check hidraw_note on bindings).", + } + + extra = input("\nAny extra notes for the developer (optional): ").strip() + if extra: + st.notes = extra + + prompt_vid_pid_with_instructions(st) + + return st + + +def resolve_vid_pid_for_raw_capture(st: CaptureState) -> tuple[int, int] | None: + """VID/PID as integers for hidraw matching (Linux sysfs or Switch 2 handshake).""" + if st.vid and st.pid: + try: + return int(st.vid, 16), int(st.pid, 16) + except ValueError: + pass + h = st.switch2_usb_handshake + if h and h.get("product_id"): + try: + return 0x057E, int(str(h["product_id"]), 16) + except ValueError: + pass + return None + + +def run_raw_hid_only_capture( + platform_name: str, + switch2_handshake: dict[str, Any] | None, +) -> CaptureState: + """ + Linux only: skip pygame; open hidraw by VID/PID and run per-control raw hex capture. + """ + if platform.system() != "Linux": + print("Error: --raw-hid-only requires Linux (/dev/hidraw*).") + sys.exit(1) + + vid: str | None = None + pid: str | None = None + vid_pid_src = "unknown" + + if switch2_handshake and switch2_handshake.get("ok") and switch2_handshake.get("product_id"): + vid = "057e" + pid = str(switch2_handshake["product_id"]).lower().replace("0x", "") + vid_pid_src = "switch2_usb_handshake" + + if not vid or not pid: + print("\nUSB **VID** and **PID** (hex) are required to find the hidraw node.\n") + v = input("VID [Enter = 057e Nintendo]: ").strip().lower().replace("0x", "") + p = input("PID: ").strip().lower().replace("0x", "") + if not p: + print("PID is required for raw-hid-only.") + sys.exit(1) + vid = v or "057e" + pid = p + vid_pid_src = "user entered (raw-hid-only)" + + from raw_hid_capture import try_capture_raw_hid_reports + + raw = try_capture_raw_hid_reports(int(vid, 16), int(pid, 16), PLATFORM_CONTROLS[platform_name]) + label = (raw.get("hid_name") or "").strip() or f"VID_{vid}_PID_{pid}" + + return CaptureState( + platform=platform_name, + controller_full_name=label, + controller_full_name_source="hidraw", + pygame_name="(SDL/pygame not used — raw-hid-only)", + pygame_guid="", + num_axes=0, + num_buttons=0, + num_hats=0, + os_name=f"{platform.system()} {platform.release()}", + vid=vid, + pid=pid, + vid_pid_source=vid_pid_src, + connection_guess="usb", + switch2_usb_handshake=switch2_handshake, + raw_hid_reports=raw, + capture_mode="raw_hid_only", + ) + + +def _wrap_hex_lines(hexstr: str, step: int = 32) -> str: + """Split hex string into lines (default step=32 nibbles = 16 bytes per line).""" + if not hexstr: + return "" + return "\n".join(hexstr[i : i + step] for i in range(0, len(hexstr), step)) + + +def dump_report(st: CaptureState) -> dict[str, Any]: + return { + "schema_version": 9, + "generated_utc": datetime.now(timezone.utc).isoformat(), + "purpose": "OGX-Mini controller support — maintainer mapping capture", + "maintainer_fields": { + "capture_mode": st.capture_mode, + "controller_full_name": st.controller_full_name, + "controller_full_name_source": st.controller_full_name_source, + "connection_guess": st.connection_guess, + "hardware_id": { + "vid_hex": st.vid, + "pid_hex": st.pid, + "vid_pid_source": st.vid_pid_source, + }, + "sdl": { + "name": st.pygame_name, + "guid": st.pygame_guid, + "axes": st.num_axes, + "buttons": st.num_buttons, + "hats": st.num_hats, + }, + "target_platform_label": st.platform, + "bindings": st.bindings, + "baseline_axes": st.baseline_axes, + "skipped_controls": st.skipped, + "user_notes": st.notes, + "host_os": st.os_name, + "switch2_usb_handshake": st.switch2_usb_handshake, + "raw_hid_reports": st.raw_hid_reports, + "hidraw_gated_by_sdl": st.hidraw_gated_by_sdl, + }, + } + + +def render_human_text(st: CaptureState, data: dict[str, Any]) -> str: + lines = [ + "OGX-Mini Controller Mapping Capture", + "===================================", + f"Generated (UTC): {data['generated_utc']}", + f"Host OS: {st.os_name}", + "", + "Controller full name (for tracking supported devices):", + f" {st.controller_full_name}", + " Source: " + + ( + "SDL / OS reported name" + if st.controller_full_name_source == "sdl" + else "Linux hidraw HID_NAME" + if st.controller_full_name_source == "hidraw" + else "user-entered retail name" + ), + "", + "Connection (best guess; Linux sysfs only — USB and Bluetooth both supported):", + f" {st.connection_guess if st.connection_guess is not None else 'not determined (use USB or Bluetooth; both work with this tool)'}", + "", + f"Capture mode: {st.capture_mode}", + f"Target platform (user selected): {st.platform}", + f"SDL joystick name (OS report): {st.pygame_name}", + f"SDL GUID: {st.pygame_guid}", + f"Axes: {st.num_axes} Buttons: {st.num_buttons} Hats: {st.num_hats}", + "", + "USB identifiers (for HardwareIDs.h / driver matching):", + f" VID: {st.vid or '(unknown)'}", + f" PID: {st.pid or '(unknown)'}", + f" Source: {st.vid_pid_source}", + "", + ] + if st.switch2_usb_handshake: + lines.append("Switch 2–family USB bulk handshake (PyUSB, before SDL capture):") + lines.extend(textwrap.indent(json.dumps(st.switch2_usb_handshake, indent=2), " ").splitlines()) + lines.append("") + if st.raw_hid_reports: + lines.append("Raw HID input reports (Linux hidraw — for firmware bit/byte mapping):") + lines.extend(textwrap.indent(json.dumps(st.raw_hid_reports, indent=2), " ").splitlines()) + lines.append("") + if st.hidraw_gated_by_sdl: + lines.append("Raw HID alongside SDL (pygame detected input; hidraw bytes in each binding below):") + lines.extend(textwrap.indent(json.dumps(st.hidraw_gated_by_sdl, indent=2), " ").splitlines()) + lines.append("") + lines.append("Per-control mapping (SDL/pygame indices as seen on this PC):") + if st.capture_mode == "raw_hid_only" and not st.bindings: + lines.append(" (none — raw-hid-only run; see raw JSON above for hex per control)") + else: + for cid, val in sorted(st.bindings.items()): + lines.append(f" {cid}: {val}") + + hidraw_blocks: list[str] = [] + for cid, val in sorted(st.bindings.items()): + if not isinstance(val, dict) or "hidraw_active_hex" not in val: + continue + hidraw_blocks.append(f" [{cid}]") + if "hidraw_baseline_len" in val: + hidraw_blocks.append(f" baseline_len: {val['hidraw_baseline_len']}") + if "hidraw_active_len" in val: + hidraw_blocks.append(f" active_len: {val['hidraw_active_len']}") + bh = val.get("hidraw_baseline_hex", "") + ah = val.get("hidraw_active_hex", "") + xh = val.get("hidraw_xor_hex", "") + hidraw_blocks.append(" baseline_hex:") + for ln in _wrap_hex_lines(bh).splitlines() or [""]: + hidraw_blocks.append(f" {ln}") + hidraw_blocks.append(" active_hex:") + for ln in _wrap_hex_lines(ah).splitlines() or [""]: + hidraw_blocks.append(f" {ln}") + hidraw_blocks.append(" xor_hex:") + for ln in _wrap_hex_lines(xh).splitlines() or [""]: + hidraw_blocks.append(f" {ln}") + hidraw_blocks.append("") + if hidraw_blocks: + lines.append("") + lines.append("Full hidraw packets (per control — same data as JSON bindings; 16 bytes per line):") + lines.extend(hidraw_blocks) + if st.skipped: + lines.append("") + lines.append("Skipped:") + for s in st.skipped: + lines.append(f" - {s}") + if st.notes: + lines.append("") + lines.append("User notes:") + lines.append(textwrap.indent(st.notes, " ")) + lines.append("") + lines.append("--- JSON (same data, machine-readable) ---") + lines.append(json.dumps(data, indent=2)) + return "\n".join(lines) + + +def main() -> None: + parser = argparse.ArgumentParser(description="Capture gamepad mappings for OGX-Mini support.") + parser.add_argument( + "-o", + "--output", + help="Write report to this file (UTF-8). If omitted, prompted at end.", + ) + parser.add_argument( + "--skip-raw-hid", + action="store_true", + help="Do not offer the Linux hidraw raw report capture phase (SDL/VID capture only).", + ) + parser.add_argument( + "--use-sdl-name", + action="store_true", + help="Use the SDL joystick name as controller_full_name (skip typing a retail name).", + ) + parser.add_argument( + "--auto-output-name", + action="store_true", + help="Save the report as _.txt without asking (ignored if -o is set).", + ) + parser.add_argument( + "--raw-hid-only", + action="store_true", + help="Linux only: skip SDL/pygame mapping; only run hidraw raw hex capture (use with Switch 2 USB handshake platform as needed).", + ) + parser.add_argument( + "--no-hidraw-sidecar", + action="store_true", + help="Do not offer hidraw sampling alongside pygame (Linux).", + ) + args = parser.parse_args() + + if args.raw_hid_only and args.skip_raw_hid: + parser.error("--raw-hid-only and --skip-raw-hid cannot be used together.") + + if args.raw_hid_only: + print( + textwrap.dedent( + """ + OGX-Mini — Raw HID capture only (Linux hidraw) + ----------------------------------------------- + Skips SDL/pygame. You map each control against **raw report bytes** only. + Pick the same **platform layout** as your controller so prompts match. + For Switch 2 USB, choose the Switch 2 platform so the bulk handshake runs first. + """ + ).strip() + ) + else: + print( + textwrap.dedent( + """ + OGX-Mini — Controller mapping capture + ------------------------------------- + This tool records how your PC sees each stick, button, and trigger + (via SDL/pygame). Use a **USB** cable **or** **Bluetooth** — if the + controller shows up in the list below, you can capture it. + Upload the saved file when requesting support for a new controller + in OGX-Mini. + """ + ).strip() + ) + + platform_name = choose_platform() + switch2_handshake: dict[str, Any] | None = None + if platform_requires_switch2_usb_init(platform_name): + print( + textwrap.dedent( + """ + + ------------------------------------------------------------------ + Switch 2 Pro / Joy-Con 2 (USB) — USB bulk handshake + ------------------------------------------------------------------ + These controllers often do **not** appear in SDL until a host sends + Nintendo's USB vendor sequence on **interface 1** (same idea as + HandHeldLegend's ProCon 2 Enabler in Chrome). + + • Install: pip install pyusb + • Linux: needs **libusb**; you may need **udev** rules or run + briefly with sudo if access to the device is denied. + • Windows: interface 1 may need **WinUSB** (e.g. Zadig) — YMMV. + • Use a **data** USB cable; plug the controller in **before** Enter. + + """ + ).rstrip() + ) + input("Press Enter when the controller is connected over USB… ") + switch2_handshake = run_switch2_usb_handshake() + print(switch2_handshake.get("message", "")) + if not switch2_handshake.get("ok"): + if args.raw_hid_only: + print( + "\nHandshake did not complete. You can still enter VID/PID manually for hidraw " + "(e.g. PID 2069 for Pro Controller 2).\n" + ) + else: + cont = input("Handshake failed. Try SDL capture anyway? [y/N]: ").strip().lower() + if cont not in ("y", "yes"): + print("Exiting.") + return + time.sleep(0.8) + + if args.raw_hid_only: + st = run_raw_hid_only_capture(platform_name, switch2_handshake) + else: + init_pygame() + joy, js_index = choose_joystick() + + try: + st = run_capture_loop( + joy, + platform_name, + js_index, + switch2_usb_handshake=switch2_handshake, + use_sdl_name=args.use_sdl_name, + enable_hidraw_sidecar=not args.no_hidraw_sidecar, + ) + finally: + joy.quit() + import pygame + + pygame.quit() + + # Optional second phase: full raw HID report bytes (Linux hidraw) for maintainers. + if ( + not args.raw_hid_only + and not args.skip_raw_hid + and platform.system() == "Linux" + and st.raw_hid_reports is None + ): + pair = resolve_vid_pid_for_raw_capture(st) + if pair is not None: + vid_i, pid_i = pair + has_sdl_hidraw = any( + isinstance(v, dict) and "hidraw_active_hex" in v for v in st.bindings.values() + ) + if has_sdl_hidraw: + ans = ( + input( + "\nThis report already has **raw hidraw hex** on each binding (SDL detected the press).\n" + "Run the **extra** interactive hidraw walkthrough too (idle-based)? Usually **not** needed. [y/N]: " + ) + .strip() + .lower() + ) + do_walkthrough = ans in ("y", "yes") + else: + ans = ( + input( + "\nAlso capture **raw HID** in the **interactive hidraw-only** walkthrough?\n" + "(Enable **SDL+hidraw** sidecar next time to avoid this — pygame gates each sample.) [Y/n]: " + ) + .strip() + .lower() + ) + do_walkthrough = ans in ("", "y", "yes") + if do_walkthrough: + from raw_hid_capture import try_capture_raw_hid_reports + + st.raw_hid_reports = try_capture_raw_hid_reports( + vid_i, pid_i, PLATFORM_CONTROLS[platform_name] + ) + if not st.raw_hid_reports.get("ok"): + print(st.raw_hid_reports.get("message", "Raw hidraw capture did not complete.")) + else: + print( + "\n(Skip raw hidraw: no VID/PID in report — fill VID/PID at end of capture next time, " + "or use Switch 2 USB handshake so product_id is known.)" + ) + + data = dump_report(st) + text = render_human_text(st, data) + + out_path = args.output + if not out_path: + suggested = default_report_filename(st) + if args.auto_output_name: + out_path = suggested + else: + ans = input("\nSave report to file? [Y/n]: ").strip().lower() + if ans in ("", "y", "yes"): + path = input(f"Filename [{suggested}]: ").strip() or suggested + out_path = path + + if out_path: + with open(out_path, "w", encoding="utf-8") as f: + f.write(text) + print(f"\nWrote: {out_path}\n") + else: + print("\n--- Report ---\n") + print(text) + + print("Thank you. Attach the file to your issue or send it to the maintainer.") + + +if __name__ == "__main__": + main() diff --git a/Tools/controller_capture/hidraw_full_report_dump.py b/Tools/controller_capture/hidraw_full_report_dump.py new file mode 100644 index 00000000..cab89f1b --- /dev/null +++ b/Tools/controller_capture/hidraw_full_report_dump.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python3 +""" +Stream every full HID input report from Linux hidraw as hex (one line per read). + +Use this to see **which byte indices** change when you hold one button vs idle, without +going through the interactive per-control capture. Helps separate: + - stable button bits (good for firmware) + - byte 1 (timer) and IMU tail bytes that move every frame (bad to match naïvely) + +Examples: + python3 hidraw_full_report_dump.py --vid 057e --pid 2069 + python3 hidraw_full_report_dump.py --path /dev/hidraw4 --diff + python3 hidraw_full_report_dump.py --vid 057e --pid 2069 --diff-core --max 500 + +Requires: Linux, read access on /dev/hidraw* (plugdev/udev). +""" + +from __future__ import annotations + +import argparse +import os +import select +import sys +import time + +from raw_hid_capture import _idle_compare_key, choose_hidraw_device, list_hidraw_nodes_for_vid_pid + + +def _open_nonblock(path: str) -> int: + return os.open(path, os.O_RDONLY | os.O_NONBLOCK) + + +def _drain_one_report(fd: int, bufsize: int = 512) -> bytes | None: + r, _, _ = select.select([fd], [], [], 0.25) + if not r: + return None + try: + return os.read(fd, bufsize) + except BlockingIOError: + return None + + +def main() -> int: + ap = argparse.ArgumentParser(description="Dump full hidraw input reports as hex (Linux).") + ap.add_argument("--vid", help="USB VID hex, e.g. 057e") + ap.add_argument("--pid", help="USB PID hex, e.g. 2069") + ap.add_argument("--path", help="Explicit hidraw device (skips VID/PID lookup)") + ap.add_argument( + "--diff", + action="store_true", + help="Only print lines when the full report differs from the previous printed line.", + ) + ap.add_argument( + "--diff-core", + action="store_true", + help="Only print when Nintendo-style core key (bytes 2:12, stick LSBs masked) changes. " + "Useful to ignore pure IMU tail jitter while still seeing button/stick-core changes.", + ) + ap.add_argument("--max", type=int, default=0, metavar="N", help="Stop after N printed lines (0 = unlimited)") + ap.add_argument("--count", action="store_true", help="Prefix each line with a monotonic line number") + args = ap.parse_args() + + if sys.platform != "linux" and not sys.platform.startswith("linux"): + print("This tool only runs on Linux (hidraw).", file=sys.stderr) + return 1 + + path: str | None = args.path + if not path: + if not args.vid or not args.pid: + print("Provide --path or both --vid and --pid.", file=sys.stderr) + return 1 + try: + vid = int(args.vid, 16) + pid = int(args.pid, 16) + except ValueError: + print("Invalid --vid or --pid (use hex, e.g. 057e).", file=sys.stderr) + return 1 + candidates = list_hidraw_nodes_for_vid_pid(vid, pid) + if not candidates: + print(f"No hidraw node for VID {vid:04x} PID {pid:04x}.", file=sys.stderr) + return 1 + chosen = candidates[0] if len(candidates) == 1 else choose_hidraw_device(candidates) + if chosen is None: + return 1 + path = chosen["path"] + print(f"# {chosen.get('hid_name', '')} {path} {chosen.get('hid_id', '')}", flush=True) + + assert path is not None + try: + fd = _open_nonblock(path) + except OSError as e: + print(f"Open {path}: {e}", file=sys.stderr) + return 1 + + print( + "# byte indices (hex): " + " ".join(f"{i:02x}" for i in range(16)), + "\n# " + " ".join(f"{i:02x}" for i in range(16, 32)), + "\n# Ctrl+C to stop.\n", + sep="", + flush=True, + ) + + prev_full: bytes | None = None + prev_key: bytes | None = None + n_printed = 0 + line_no = 0 + try: + while args.max <= 0 or n_printed < args.max: + pkt = _drain_one_report(fd) + if pkt is None: + continue + + if args.diff_core: + key = _idle_compare_key(pkt) + if prev_key is not None and key == prev_key: + continue + prev_key = key + elif args.diff: + if prev_full is not None and pkt == prev_full: + continue + prev_full = pkt + else: + prev_full = pkt + + line_no += 1 + h = pkt.hex() + prefix = f"{line_no:6d} " if args.count else "" + print(f"{prefix}len={len(pkt):3d} {h}", flush=True) + n_printed += 1 + except KeyboardInterrupt: + print("\n# stopped", flush=True) + finally: + os.close(fd) + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Tools/controller_capture/raw_hid_capture.py b/Tools/controller_capture/raw_hid_capture.py new file mode 100644 index 00000000..bbe0f652 --- /dev/null +++ b/Tools/controller_capture/raw_hid_capture.py @@ -0,0 +1,458 @@ +#!/usr/bin/env python3 +""" +Read raw HID input reports from Linux hidraw nodes (by USB VID/PID). + +Used by controller_capture.py so maintainers get **actual report bytes** (hex), +not only SDL button/axis indices — needed to match SwitchPro-style packed layouts +in firmware. +""" + +from __future__ import annotations + +import glob +import os +import select +import sys +import time +from typing import Any + + +def _parse_uevent(content: str) -> dict[str, str]: + out: dict[str, str] = {} + for line in content.splitlines(): + if "=" in line: + k, v = line.split("=", 1) + out[k.strip()] = v.strip() + return out + + +def list_hidraw_nodes_for_vid_pid(vid: int, pid: int) -> list[dict[str, Any]]: + """Return hidraw device paths whose HID_ID matches vid:pid (USB HID).""" + found: list[dict[str, Any]] = [] + for hidraw_dir in sorted(glob.glob("/sys/class/hidraw/hidraw*")): + uevent_path = os.path.join(hidraw_dir, "device", "uevent") + if not os.path.isfile(uevent_path): + continue + try: + with open(uevent_path, encoding="utf-8") as f: + props = _parse_uevent(f.read()) + except OSError: + continue + hid_id = props.get("HID_ID") + if not hid_id: + continue + parts = hid_id.split(":") + if len(parts) != 3: + continue + try: + _bus, v_s, p_s = parts + v = int(v_s, 16) + p = int(p_s, 16) + except ValueError: + continue + if v != vid or p != pid: + continue + name = os.path.basename(hidraw_dir) + found.append( + { + "path": f"/dev/{name}", + "hid_name": props.get("HID_NAME", ""), + "hid_id": hid_id, + "sysfs": hidraw_dir, + } + ) + return found + + +def _open_hidraw_nonblock(path: str) -> int: + return os.open(path, os.O_RDONLY | os.O_NONBLOCK) + + +def _read_latest_packet(fd: int, timeout_s: float, bufsize: int = 512) -> bytes | None: + """Drain readable data within timeout; return last chunk read (one report per read typical).""" + deadline = time.monotonic() + timeout_s + last: bytes | None = None + while time.monotonic() < deadline: + r, _, _ = select.select([fd], [], [], min(0.08, max(0.0, deadline - time.monotonic()))) + if not r: + continue + try: + while True: + chunk = os.read(fd, bufsize) + if not chunk: + break + last = chunk + except BlockingIOError: + break + return last + + +# Pro / Switch 2 style full input: byte0 report ID, byte1 timer/counter, then 10-byte +# SwitchPro::InReport (battery + 3 button bytes + 6 stick bytes). IMU follows in tail. +_NINTENDO_FULL_INPUT_IDS = frozenset({0x30, 0x09, 0x31}) + + +def _nintendo_gamepad_core_key(pkt: bytes) -> bytes: + """10 bytes at offset 2; relax stick packing bytes (indices 4..9) for idle match.""" + core = bytearray(pkt[2:12]) + if len(core) >= 10: + for i in range(4, 10): + core[i] &= 0xFC # stronger than 0xFE — Switch 2 sticks often jitter 1–2 LSBs + return bytes(core) + + +def _idle_compare_key(pkt: bytes) -> bytes: + """ + Bytes that should stay fixed at neutral. Full 64-byte reports include a + per-frame timer (byte 1) and IMU tail — never compare the whole packet. + + Switch 2 Pro often uses report ID **0x09** (not 0x30); layout matches the same core. + """ + if len(pkt) >= 12 and pkt[0] in _NINTENDO_FULL_INPUT_IDS: + return _nintendo_gamepad_core_key(pkt) + # Long reports (other vendors): compare head only, drop IMU tail. + if len(pkt) >= 48: + head = bytearray(pkt[:24]) + for i in range(3, min(24, len(head))): + if i >= 6: + head[i] &= 0xFC + return bytes(head) + return pkt + + +def _wait_stable_report( + fd: int, + *, + settle_s: float = 0.22, + overall_timeout_s: float = 45.0, +) -> bytes | None: + """Wait until consecutive samples match on idle-compare key (not full packet).""" + last_pkt: bytes | None = None + last_key: bytes | None = None + stable_since: float | None = None + start = time.monotonic() + packet_count = 0 + last_progress = start + saw_packet = False + + while time.monotonic() - start < overall_timeout_s: + elapsed = time.monotonic() - start + # After input, sticks may take a moment to settle; allow shorter "quiet" window. + effective_settle = settle_s + if elapsed > 8.0: + effective_settle = min(settle_s, 0.14) + if elapsed > 16.0: + effective_settle = min(effective_settle, 0.09) + if elapsed > 24.0: + effective_settle = min(effective_settle, 0.06) + + pkt = _read_latest_packet(fd, timeout_s=0.12) + now = time.monotonic() + + if pkt is None: + if saw_packet and now - last_progress >= 0.6: + print( + "\r … waiting for hidraw data (move a stick slightly if nothing arrives) ", + end="", + flush=True, + ) + last_progress = now + time.sleep(0.02) + continue + + saw_packet = True + packet_count += 1 + key = _idle_compare_key(pkt) + + if now - last_progress >= 0.45: + print( + f"\r … {packet_count} reports, last len={len(pkt)} byte0=0x{pkt[0]:02x} (idle: core bytes 2:12) ", + end="", + flush=True, + ) + last_progress = now + + if last_key is not None and key == last_key: + last_pkt = pkt # keep latest full frame while idle-core is stable + if stable_since is None: + stable_since = now + elif now - stable_since >= effective_settle: + print() # newline after progress + return last_pkt + else: + stable_since = None + last_pkt = pkt + last_key = key + time.sleep(0.008) + + print() + return last_pkt + + +def _wait_changed_report(fd: int, baseline: bytes, timeout_s: float = 60.0) -> bytes | None: + base_key = _idle_compare_key(baseline) + start = time.monotonic() + while time.monotonic() - start < timeout_s: + pkt = _read_latest_packet(fd, timeout_s=0.12) + if pkt is not None and _idle_compare_key(pkt) != base_key: + return pkt + time.sleep(0.008) + return None + + +def _hex(b: bytes) -> str: + return b.hex() + + +def peek_latest_hidraw(fd: int, timeout_s: float = 0.1) -> bytes | None: + """Drain hidraw within timeout; return the last full report read (for SDL-gated sampling).""" + return _read_latest_packet(fd, timeout_s) + + +def xor_hex_reports(a: bytes, b: bytes) -> str: + """Hex string of a XOR b (padded to max length).""" + return _xor_hex(a, b) + + +def _xor_hex(a: bytes, b: bytes) -> str: + n = max(len(a), len(b)) + aa = a.ljust(n, b"\x00") + bb = b.ljust(n, b"\x00") + return bytes(x ^ y for x, y in zip(aa, bb)).hex() + + +def choose_hidraw_device( + candidates: list[dict[str, Any]], + *, + quit_hint: str = "q to skip", +) -> dict[str, Any] | None: + if not candidates: + return None + if len(candidates) == 1: + return candidates[0] + print("\nMultiple hidraw nodes match this VID:PID. Pick the one that is the **gamepad** input:\n") + for i, c in enumerate(candidates, 1): + hn = c.get("hid_name") or "(no HID_NAME)" + print(f" {i}. {c['path']}\n HID_NAME: {hn}\n {c['hid_id']}") + while True: + raw = input(f"\nEnter number (or '{quit_hint}'): ").strip().lower() + if raw == "q": + return None + try: + idx = int(raw) + if 1 <= idx <= len(candidates): + return candidates[idx - 1] + except ValueError: + pass + print("Invalid choice.") + + +def try_open_hidraw_for_sidecar(vid: int, pid: int) -> tuple[int | None, str | None]: + """ + Open one hidraw fd for SDL-gated sampling: pygame decides *when* input happened; + caller peeks this fd right after. + """ + candidates = list_hidraw_nodes_for_vid_pid(vid, pid) + if not candidates: + print( + f"(No /dev/hidraw* for VID {vid:04x} PID {pid:04x} — raw bytes will not be recorded alongside SDL.)" + ) + return None, None + if len(candidates) == 1: + chosen = candidates[0] + else: + print("\nSeveral hidraw nodes match — pick the **gamepad** for raw HID alongside pygame:") + chosen = choose_hidraw_device(candidates, quit_hint="q to skip sidecar") + if chosen is None: + return None, None + path = chosen["path"] + try: + fd = _open_hidraw_nonblock(path) + print( + f"(Opened {path} for raw HID. **Pygame** still decides when you pressed; we only read bytes then.)\n" + ) + return fd, path + except OSError as e: + print(f"(Could not open hidraw for sidecar: {e})") + return None, None + + +def close_hidraw_fd(fd: int | None) -> None: + if fd is not None: + try: + os.close(fd) + except OSError: + pass + + +def try_capture_raw_hid_reports( + vid: int, + pid: int, + controls: list[tuple[str, str]], + *, + hidraw_path: str | None = None, +) -> dict[str, Any]: + """ + Interactive session: for each logical control, capture baseline vs active raw report. + + Returns a dict suitable for JSON under maintainer_fields.raw_hid_reports. + """ + if sys.platform != "linux" and not sys.platform.startswith("linux"): + return { + "ok": False, + "platform": "non-linux", + "message": "Raw hidraw capture is only implemented on Linux. Use Linux for full byte dumps, " + "or attach a USB capture / Wireshark for other OSes.", + } + + candidates = list_hidraw_nodes_for_vid_pid(vid, pid) + if not candidates: + return { + "ok": False, + "platform": "linux_hidraw", + "message": f"No /dev/hidraw* found for VID {vid:04x} PID {pid:04x}. " + "Check lsusb, permissions (plugdev/udev), and that the controller exposes HID.", + "vid_hex": f"{vid:04x}", + "pid_hex": f"{pid:04x}", + } + + chosen = None + if hidraw_path: + for c in candidates: + if c["path"] == hidraw_path: + chosen = c + break + if chosen is None: + return { + "ok": False, + "platform": "linux_hidraw", + "message": f"Given hidraw path {hidraw_path!r} does not match enumerated nodes for this VID:PID.", + "candidates": candidates, + } + else: + chosen = choose_hidraw_device(candidates, quit_hint="q to skip raw capture") + if chosen is None: + return { + "ok": False, + "platform": "linux_hidraw", + "skipped": True, + "message": "User skipped hidraw selection.", + } + + path = chosen["path"] + fd: int | None = None + try: + fd = _open_hidraw_nonblock(path) + except OSError as e: + return { + "ok": False, + "platform": "linux_hidraw", + "path": path, + "message": f"Could not open {path}: {e}. Try udev rules or `sudo` for a one-off test.", + "candidates": candidates, + } + + samples: list[dict[str, Any]] = [] + max_len = 0 + + print( + "\n" + + "=" * 72 + + "\nRaw HID reports (linux hidraw)\n" + + "=" * 72 + + f"\nReading from: {path}\n" + f"HID_NAME: {chosen.get('hid_name', '')}\n\n" + "For each step: return to **neutral** (release buttons, center sticks), then press/move **only**\n" + "the named control. The tool records full report bytes (hex) for firmware maintainers.\n\n" + "Note: Nintendo full reports change every frame (timer + IMU). Idle detection uses bytes **2:12**\n" + "(gamepad core; works for report IDs **0x30** and **Switch 2’s 0x09**). If neutral takes long after a\n" + "button, release fully and center sticks — settle time shortens automatically after ~8s.\n" + "Commands: Enter=continue s=skip control q=quit raw phase\n" + ) + + try: + i = 0 + while i < len(controls): + cid, label = controls[i] + print(f"\n>>> [{i + 1}/{len(controls)}] RAW {label}") + print(" Release everything; center sticks — waiting for stable idle report...") + baseline = _wait_stable_report(fd) + if baseline is None: + print(" No HID data (timeout). Wiggle a stick slightly or press a button once, then release.") + retry = input(" Retry this step? [Y/n/q quit]: ").strip().lower() + if retry == "q": + break + if retry == "n": + samples.append( + { + "control_id": cid, + "label": label, + "error": "timeout waiting for stable baseline", + } + ) + i += 1 + continue + + max_len = max(max_len, len(baseline)) + print(f" Baseline length: {len(baseline)} bytes") + print(" Now press or move ONLY the control above. Waiting for report change...") + active = _wait_changed_report(fd, baseline) + if active is None: + print(" No change seen.") + ans = input(" Retry [Y/n/s skip]: ").strip().lower() + if ans == "q": + break + if ans == "s": + samples.append( + { + "control_id": cid, + "label": label, + "baseline_hex": _hex(baseline), + "skipped": True, + } + ) + i += 1 + continue + + max_len = max(max_len, len(active)) + samples.append( + { + "control_id": cid, + "label": label, + "baseline_hex": _hex(baseline), + "active_hex": _hex(active), + "xor_hex": _xor_hex(baseline, active), + "baseline_len": len(baseline), + "active_len": len(active), + } + ) + print(f" Captured: +{len(active) - len(baseline)} len delta" if len(active) != len(baseline) else " Captured.") + i += 1 + cmd = input("Enter=continue | s=skip next | q=quit raw: ").strip().lower() + if cmd == "q": + break + if cmd == "s" and i < len(controls): + cid2, lab2 = controls[i] + samples.append({"control_id": cid2, "label": lab2, "skipped": True}) + i += 1 + finally: + if fd is not None: + os.close(fd) + + session_ok = bool(samples) + return { + "ok": session_ok, + "platform": "linux_hidraw", + "path": path, + "hid_name": chosen.get("hid_name", ""), + "hid_id": chosen.get("hid_id", ""), + "vid_hex": f"{vid:04x}", + "pid_hex": f"{pid:04x}", + "max_report_len_seen": max_len, + "sample_count": len(samples), + "samples": samples, + "note": "Bytes are as returned by read(2) on hidraw (typically includes report ID as byte 0). " + "Compare xor_hex to map bits/bytes for firmware decode tables. " + "Idle detection uses bytes 2:12 gamepad core (report IDs 0x30 / 0x09 / 0x31), not timer/IMU; " + "stick bytes masked with 0xFC for neutral matching.", + } diff --git a/Tools/controller_capture/requirements.txt b/Tools/controller_capture/requirements.txt new file mode 100644 index 00000000..de9b227a --- /dev/null +++ b/Tools/controller_capture/requirements.txt @@ -0,0 +1,4 @@ +# Controller mapping capture for OGX-Mini maintainers +pygame>=2.5.0 +# Switch 2 Pro / Joy-Con 2 USB bulk handshake (needs system libusb-1.0) +pyusb>=1.2.0 diff --git a/Tools/controller_capture/switch2_usb_init.py b/Tools/controller_capture/switch2_usb_init.py new file mode 100644 index 00000000..1ec533fb --- /dev/null +++ b/Tools/controller_capture/switch2_usb_init.py @@ -0,0 +1,373 @@ +""" +Nintendo Switch 2–family USB bulk handshake for PC capture. + +The Pro Controller 2 (PID 0x2069), Joy-Con 2 L/R (0x2067/0x2066), and related +devices do not stream standard HID gamepad input over USB until the host sends +a vendor protocol on USB interface 1 (bulk OUT), matching the sequence used +by HandHeldLegend's browser tool: + + https://github.com/HandHeldLegend/handheldlegend.github.io + (procon2tool/index.html — "ProCon 2 Enabler Tool") + +After this runs, the OS may expose the pad to SDL/pygame for mapping capture. + +Requires: PyUSB + system libusb (Linux: libusb-1.0; Windows often needs WinUSB via Zadig for the bulk interface). +""" + +from __future__ import annotations + +import time +from typing import Any + +# Nintendo Co., Ltd. +VENDOR_ID = 0x057E + +# Switch 2–era USB IDs (same as HHL dumper / enabler) +PRODUCT_IDS: dict[int, str] = { + 0x2066: "Joy-Con 2 (R)", + 0x2067: "Joy-Con 2 (L)", + 0x2069: "Pro Controller 2", + 0x2073: "NSO GameCube Controller", +} + +USB_CONFIGURATION = 1 +USB_INTERFACE = 1 + +# Packets copied from handheldlegend.github.io procon2tool/index.html (auto sequence on USB connect). +# Placeholder 0xFF bytes in MAC/LTK fields match the reference tool for PC use. + +_INIT_COMMAND_0x03 = bytes( + [ + 0x03, + 0x91, + 0x00, + 0x0D, + 0x00, + 0x08, + 0x00, + 0x00, + 0x01, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + ] +) + +_UNKNOWN_COMMAND_0x07 = bytes([0x07, 0x91, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00]) + +_UNKNOWN_COMMAND_0x16 = bytes([0x16, 0x91, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00]) + +_REQUEST_CONTROLLER_MAC = bytes( + [ + 0x15, + 0x91, + 0x00, + 0x01, + 0x00, + 0x0E, + 0x00, + 0x00, + 0x00, + 0x02, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + ] +) + +_LTK_REQUEST = bytes( + [ + 0x15, + 0x91, + 0x00, + 0x02, + 0x00, + 0x11, + 0x00, + 0x00, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + ] +) + +_UNKNOWN_COMMAND_0x15_ARG_0x03 = bytes([0x15, 0x91, 0x00, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00]) + +_UNKNOWN_COMMAND_0x09 = bytes( + [ + 0x09, + 0x91, + 0x00, + 0x07, + 0x00, + 0x08, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + ] +) + +_IMU_COMMAND_0x02 = bytes([0x0C, 0x91, 0x00, 0x02, 0x00, 0x04, 0x00, 0x00, 0x27, 0x00, 0x00, 0x00]) + +_OUT_UNKNOWN_COMMAND_0x11 = bytes([0x11, 0x91, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00]) + +_UNKNOWN_COMMAND_0x0A = bytes( + [ + 0x0A, + 0x91, + 0x00, + 0x08, + 0x00, + 0x14, + 0x00, + 0x00, + 0x01, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x35, + 0x00, + 0x46, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + ] +) + +_IMU_COMMAND_0x04 = bytes([0x0C, 0x91, 0x00, 0x04, 0x00, 0x04, 0x00, 0x00, 0x27, 0x00, 0x00, 0x00]) + +_ENABLE_HAPTICS = bytes([0x03, 0x91, 0x00, 0x0A, 0x00, 0x04, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00]) + +_OUT_UNKNOWN_COMMAND_0x10 = bytes([0x10, 0x91, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00]) + +_OUT_UNKNOWN_COMMAND_0x01 = bytes([0x01, 0x91, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00]) + +_OUT_UNKNOWN_COMMAND_0x03 = bytes([0x03, 0x91, 0x00, 0x01, 0x00, 0x00, 0x00]) + +_OUT_UNKNOWN_COMMAND_0x0A_ALT = bytes([0x0A, 0x91, 0x00, 0x02, 0x00, 0x04, 0x00, 0x00, 0x03, 0x00, 0x00]) + +_SET_PLAYER_LED = bytes( + [ + 0x09, + 0x91, + 0x00, + 0x07, + 0x00, + 0x08, + 0x00, + 0x00, + 0x01, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + ] +) + +# Same order as HHL connectUsb() automatic sequence (SPI reads omitted there too). +SWITCH2_USB_INIT_PACKETS: list[bytes] = [ + _INIT_COMMAND_0x03, + _UNKNOWN_COMMAND_0x07, + _UNKNOWN_COMMAND_0x16, + _REQUEST_CONTROLLER_MAC, + _LTK_REQUEST, + _UNKNOWN_COMMAND_0x15_ARG_0x03, + _UNKNOWN_COMMAND_0x09, + _IMU_COMMAND_0x02, + _OUT_UNKNOWN_COMMAND_0x11, + _UNKNOWN_COMMAND_0x0A, + _IMU_COMMAND_0x04, + _ENABLE_HAPTICS, + _OUT_UNKNOWN_COMMAND_0x10, + _OUT_UNKNOWN_COMMAND_0x01, + _OUT_UNKNOWN_COMMAND_0x03, + _OUT_UNKNOWN_COMMAND_0x0A_ALT, + _SET_PLAYER_LED, +] + + +def _find_bulk_endpoints(interface) -> tuple[Any, Any]: + import usb.util + + ep_out = None + ep_in = None + for ep in interface: + t = usb.util.endpoint_type(ep.bEndpointAddress) + d = usb.util.endpoint_direction(ep.bEndpointAddress) + if t != usb.util.ENDPOINT_TYPE_BULK: + continue + if d == usb.util.ENDPOINT_OUT: + ep_out = ep + elif d == usb.util.ENDPOINT_IN: + ep_in = ep + return ep_out, ep_in + + +def run_switch2_usb_handshake( + *, + read_timeout_ms: int = 80, + pause_s: float = 0.012, + read_size: int = 64, +) -> dict[str, Any]: + """ + Open the first matching Switch 2–family device, claim interface 1, send init packets. + + Returns a dict with keys: ok (bool), product_id, product_name, message, error (optional). + """ + try: + import usb.core + import usb.util + except ImportError as e: + return { + "ok": False, + "product_id": None, + "product_name": None, + "message": "PyUSB is not installed. Run: pip install pyusb", + "error": str(e), + } + + dev = None + found_pid = None + for pid in PRODUCT_IDS: + dev = usb.core.find(idVendor=VENDOR_ID, idProduct=pid) + if dev is not None: + found_pid = pid + break + + if dev is None: + return { + "ok": False, + "product_id": None, + "product_name": None, + "message": ( + f"No USB device found for VID {VENDOR_ID:04X} and PIDs " + + ", ".join(f"{p:04X}" for p in PRODUCT_IDS) + + ". Plug the controller in with a data cable and try again." + ), + "error": None, + } + + name = PRODUCT_IDS.get(found_pid or 0, "unknown") + out: dict[str, Any] = { + "ok": False, + "product_id": f"{found_pid:04x}" if found_pid is not None else None, + "product_name": name, + "message": "", + "error": None, + } + + detached_kernel = False + try: + try: + if dev.is_kernel_driver_active(USB_INTERFACE): + dev.detach_kernel_driver(USB_INTERFACE) + detached_kernel = True + out["detached_kernel_driver"] = True + else: + out["detached_kernel_driver"] = False + except (NotImplementedError, ValueError, usb.core.USBError): + out["detached_kernel_driver"] = None + + try: + dev.set_configuration() + except usb.core.USBError: + pass # already configured + + cfg = dev.get_active_configuration() + intf = cfg[(USB_INTERFACE, 0)] + ep_out, ep_in = _find_bulk_endpoints(intf) + + if ep_out is None: + out["message"] = "No bulk OUT endpoint on interface 1 — wrong device or driver?" + out["error"] = "missing_bulk_out" + return out + + usb.util.claim_interface(dev, USB_INTERFACE) + try: + for pkt in SWITCH2_USB_INIT_PACKETS: + ep_out.write(pkt) + time.sleep(pause_s) + if ep_in is not None: + try: + ep_in.read(read_size, timeout=read_timeout_ms) + except usb.core.USBError: + pass + out["ok"] = True + out["packets_sent"] = len(SWITCH2_USB_INIT_PACKETS) + out["message"] = ( + f"Handshake OK ({name}, PID {found_pid:04X}). " + "Wait a moment, then SDL should list the controller." + ) + finally: + usb.util.release_interface(dev, USB_INTERFACE) + + except Exception as e: + out["message"] = f"USB handshake failed: {e}" + out["error"] = repr(e) + try: + usb.util.dispose_resources(dev) + except Exception: + pass + return out + finally: + if detached_kernel: + try: + dev.attach_kernel_driver(USB_INTERFACE) + except (usb.core.USBError, NotImplementedError, ValueError): + pass + + return out + + +def platform_requires_switch2_usb_init(platform_label: str) -> bool: + """True for the capture platform that runs the HHL-compatible bulk handshake first.""" + return "libusb handshake" in platform_label diff --git a/images/logo.png b/images/logo.png new file mode 100644 index 00000000..0e894adf Binary files /dev/null and b/images/logo.png differ diff --git a/images/platform-list.png b/images/platform-list.png new file mode 100644 index 00000000..88052936 Binary files /dev/null and b/images/platform-list.png differ diff --git a/scripts/build.ps1 b/scripts/build.ps1 new file mode 100644 index 00000000..d919b53d --- /dev/null +++ b/scripts/build.ps1 @@ -0,0 +1,184 @@ +# OGX-Mini 2026 firmware build script (Windows PowerShell) +# Run from anywhere. Creates scripts\build\ and runs CMake/Ninja there; source is Firmware/RP2040. Optional log in scripts\ on failure. + +$ErrorActionPreference = "Stop" +$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$RepoRoot = (Resolve-Path (Join-Path $ScriptDir "..")).Path +$FirmwareRp2040 = Join-Path $RepoRoot "Firmware\RP2040" + +if (-not (Test-Path (Join-Path $FirmwareRp2040 "CMakeLists.txt"))) { + Write-Host "Error: Firmware/RP2040/CMakeLists.txt not found. Run this script from the OGX-Mini-2026 repo (scripts\ folder)." -ForegroundColor Red + exit 1 +} + +$Boards = @( + @{ Id = "PI_PICO"; Desc = "Pi Pico" }, + @{ Id = "PI_PICO2"; Desc = "Pi Pico 2" }, + @{ Id = "PI_PICOW"; Desc = "Pi Pico W" }, + @{ Id = "PI_PICO2W"; Desc = "Pi Pico 2 W" }, + @{ Id = "RP2040_ZERO"; Desc = "Waveshare RP2040-Zero" }, + @{ Id = "RP2350_ZERO"; Desc = "Waveshare RP2350-Zero" }, + @{ Id = "RP2350_USB_A"; Desc = "Waveshare RP2350-USB-A" }, + @{ Id = "RP2040_XIAO"; Desc = "Seeed Studio XIAO RP2040" }, + @{ Id = "RP2354"; Desc = "RP2354 (RP2350 + Pi Radio Module 2 — BT + PIO USB host GP0/GP1)" }, + @{ Id = "ADAFRUIT_FEATHER"; Desc = "Adafruit Feather USB Host" }, + @{ Id = "EXTERNAL_4CH_I2C"; Desc = "External 4CH I2C" }, + @{ Id = "ESP32_BLUEPAD32_I2C"; Desc = "ESP32 Bluepad32 I2C" }, + @{ Id = "ESP32_BLUERETRO_I2C"; Desc = "ESP32 BlueRetro I2C" } +) + +$FixedDrivers = @( + @{ Id = "XINPUT"; Desc = "Xbox 360 (XInput)" }, + @{ Id = "XBOXOG"; Desc = "Original Xbox (Gamepad)" }, + @{ Id = "PS3"; Desc = "PlayStation 3" }, + @{ Id = "SWITCH"; Desc = "Nintendo Switch Pro" }, + @{ Id = "WIIU"; Desc = "Wii U (GameCube Adapter)" }, + @{ Id = "WII"; Desc = "Wii (Wiimote)" }, + @{ Id = "PS1PS2"; Desc = "PlayStation 1/2 (GPIO)" }, + @{ Id = "GAMECUBE"; Desc = "GameCube (GPIO)" }, + @{ Id = "DREAMCAST"; Desc = "Dreamcast (GPIO)" }, + @{ Id = "N64"; Desc = "Nintendo 64 (GPIO)" }, + @{ Id = "DINPUT"; Desc = "DInput" }, + @{ Id = "PS4"; Desc = "PlayStation 4 (DualShock 4 USB)" }, + @{ Id = "STEAM"; Desc = "SteamOS / Bazzite (DualSense + touchpad mouse)" }, + @{ Id = "PSCLASSIC"; Desc = "PlayStation Classic" }, + @{ Id = "WEBAPP"; Desc = "Web App" } +) + +function Test-Command($name) { + $null -ne (Get-Command $name -ErrorAction SilentlyContinue) +} + +Write-Host "=== OGX-Mini 2026 Build Script ===" -ForegroundColor Cyan +Write-Host "" + +# --- Prerequisites --- +$Missing = @() +if (-not (Test-Command "git")) { $Missing += "git" } +if (-not (Test-Command "python") -and -not (Test-Command "py")) { $Missing += "python" } +if (-not (Test-Command "cmake")) { $Missing += "cmake" } +if (-not (Test-Command "ninja")) { $Missing += "ninja" } +if (-not (Test-Command "arm-none-eabi-gcc")) { $Missing += "arm-none-eabi-gcc" } + +if ($Missing.Count -gt 0) { + Write-Host "Missing required tools: $($Missing -join ', ')" -ForegroundColor Red + Write-Host "" + Write-Host "Install options:" + Write-Host " - Chocolatey: choco install git python cmake ninja gcc-arm-embedded" + Write-Host " - ARM GCC: https://developer.arm.com/downloads/-/gnu-rm (add to PATH)" + Write-Host " - See README.md in the project root for details." + Write-Host "" + $ans = Read-Host "Exit now? [Y/n]" + if ($ans -ne "n" -and $ans -ne "N") { exit 1 } +} + +# --- Board selection --- +Write-Host "Select board:" +for ($i = 0; $i -lt $Boards.Count; $i++) { + Write-Host " $($i+1)) $($Boards[$i].Desc) ($($Boards[$i].Id))" +} +Write-Host " $($Boards.Count + 1)) Cancel" +$boardChoice = Read-Host "Choice [1-$($Boards.Count)]" +if ([int]$boardChoice -eq $Boards.Count + 1) { Write-Host "Cancelled."; exit 0 } +if ([int]$boardChoice -lt 1 -or [int]$boardChoice -gt $Boards.Count) { Write-Host "Invalid choice."; exit 1 } +$OGXM_BOARD = $Boards[[int]$boardChoice - 1].Id + +# --- Combo vs fixed driver --- +Write-Host "" +Write-Host "Build type:" +Write-Host " 1) Default (combos enabled, all modes selectable)" +Write-Host " 2) Fixed output mode (combos disabled, single mode)" +$buildTypeChoice = Read-Host "Choice [1-2]" +$OGXM_FIXED_DRIVER = "" +if ($buildTypeChoice -eq "2") { + Write-Host "Select fixed output mode:" + for ($i = 0; $i -lt $FixedDrivers.Count; $i++) { + Write-Host " $($i+1)) $($FixedDrivers[$i].Desc) ($($FixedDrivers[$i].Id))" + } + Write-Host " $($FixedDrivers.Count + 1)) Cancel" + $driverChoice = Read-Host "Choice [1-$($FixedDrivers.Count)]" + if ([int]$driverChoice -eq $FixedDrivers.Count + 1) { Write-Host "Cancelled."; exit 0 } + if ([int]$driverChoice -lt 1 -or [int]$driverChoice -gt $FixedDrivers.Count) { Write-Host "Invalid choice."; exit 1 } + $OGXM_FIXED_DRIVER = $FixedDrivers[[int]$driverChoice - 1].Id +} + +# --- Release vs Debug --- +Write-Host "" +Write-Host "Build configuration:" +Write-Host " 1) Release (smaller, faster)" +Write-Host " 2) Debug (UART logging, no optimization)" +$configChoice = Read-Host "Choice [1-2]" +$CMAKE_BUILD_TYPE = if ($configChoice -eq "2") { "Debug" } else { "Release" } + +$OGXM_SWITCH2_HID_RAW_LOG = $false +if ($configChoice -eq "2") { + Write-Host "" + Write-Host "Switch 2 Pro USB (PID 0x2069) debugging (Debug builds only):" + Write-Host " 1) Off (default)" + Write-Host " 2) UART: raw HID hex when report buffer changes (-DOGXM_SWITCH2_HID_RAW_LOG=ON)" + $s2RawChoice = Read-Host "Choice [1-2]" + if ($s2RawChoice -eq "2") { $OGXM_SWITCH2_HID_RAW_LOG = $true } +} + +# --- Run build --- +$BuildDir = Join-Path $ScriptDir "build" +if (Test-Path $BuildDir) { Remove-Item -Recurse -Force $BuildDir } +New-Item -ItemType Directory -Path $BuildDir | Out-Null + +Write-Host "" +$fixedInfo = if ($OGXM_FIXED_DRIVER) { " fixed=$OGXM_FIXED_DRIVER" } else { "" } +$s2Info = if ($OGXM_SWITCH2_HID_RAW_LOG) { " switch2_raw_hid=ON" } else { "" } +Write-Host "Building: board=$OGXM_BOARD type=$CMAKE_BUILD_TYPE$fixedInfo$s2Info" +Write-Host "Output directory: $BuildDir" +Write-Host "" + +$cmakeArgs = @( + "-G", "Ninja", + "-DOGXM_BOARD=$OGXM_BOARD", + "-DCMAKE_BUILD_TYPE=$CMAKE_BUILD_TYPE" +) +if ($OGXM_FIXED_DRIVER) { + $cmakeArgs += "-DOGXM_FIXED_DRIVER=$OGXM_FIXED_DRIVER" + $cmakeArgs += "-DOGXM_FIXED_DRIVER_ALLOW_COMBOS=OFF" +} +if ($OGXM_SWITCH2_HID_RAW_LOG) { + $cmakeArgs += "-DOGXM_SWITCH2_HID_RAW_LOG=ON" +} + +$buildLog = Join-Path $env:TEMP "ogxm_build_log.txt" +$buildSuccess = $false +try { + Push-Location $BuildDir + & cmake $cmakeArgs $FirmwareRp2040 2>&1 | Tee-Object -FilePath $buildLog + if ($LASTEXITCODE -ne 0) { throw "CMake failed" } + & ninja 2>&1 | Tee-Object -FilePath $buildLog -Append + if ($LASTEXITCODE -ne 0) { throw "Ninja failed" } + $buildSuccess = $true +} finally { + Pop-Location +} + +if ($buildSuccess) { + Write-Host "" + Write-Host "Build completed successfully. Output is in: $BuildDir" -ForegroundColor Green + if ($CMAKE_BUILD_TYPE -eq "Debug") { + Write-Host "" + Write-Host "Debug logging (OGXM_LOG, Switch 2 raw HID): UART only at 115200 8N1 — not the Pico USB cable." -ForegroundColor Yellow + if ($OGXM_BOARD -eq "PI_PICOW" -or $OGXM_BOARD -eq "PI_PICO2W") { + Write-Host " Wire USB-serial RX to GP4 (board TX), GND to GND." + } else { + Write-Host " Wire USB-serial RX to GP0 (board TX), GND to GND." + } + Write-Host " Then open that COM port in PuTTY / VS Code Serial Monitor at 115200." + } +} else { + Write-Host "" + Write-Host "Build failed. You can save the output to a log file for troubleshooting." -ForegroundColor Yellow + $logPath = Join-Path $ScriptDir "build_log.txt" + $saveLog = Read-Host "Save log to $logPath? [y/N]" + if ($saveLog -match "^[yY]") { + Copy-Item $buildLog $logPath -Force + Write-Host "Log saved to $logPath" + } + exit 1 +} diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100644 index 00000000..8ea18281 --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,185 @@ +#!/usr/bin/env bash +# OGX-Mini 2026 firmware build script (Linux / macOS) +# Run from anywhere. Creates scripts/build/ and runs CMake/Ninja there; source is Firmware/RP2040. Optional log in scripts/ on failure. + +set -e +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +FIRMWARE_RP2040="$REPO_ROOT/Firmware/RP2040" + +if [ ! -f "$FIRMWARE_RP2040/CMakeLists.txt" ]; then + echo "Error: Firmware/RP2040/CMakeLists.txt not found. Run this script from the OGX-Mini-2026 repo (scripts/ folder)." + exit 1 +fi + +BOARDS=( + "PI_PICO:Pi Pico" + "PI_PICO2:Pi Pico 2" + "PI_PICOW:Pi Pico W" + "PI_PICO2W:Pi Pico 2 W" + "RP2040_ZERO:Waveshare RP2040-Zero" + "RP2350_ZERO:Waveshare RP2350-Zero" + "RP2350_USB_A:Waveshare RP2350-USB-A" + "RP2040_XIAO:Seeed Studio XIAO RP2040" + "RP2354:RP2354 (RP2350 + Pi Radio Module 2 — BT + PIO USB host GP0/GP1)" + "ADAFRUIT_FEATHER:Adafruit Feather USB Host" + "EXTERNAL_4CH_I2C:External 4CH I2C" + "ESP32_BLUEPAD32_I2C:ESP32 Bluepad32 I2C" + "ESP32_BLUERETRO_I2C:ESP32 BlueRetro I2C" +) + +FIXED_DRIVERS=( + "XINPUT:Xbox 360 (XInput)" + "XBOXOG:Original Xbox (Gamepad)" + "PS3:PlayStation 3" + "SWITCH:Nintendo Switch Pro" + "WIIU:Wii U (GameCube Adapter)" + "WII:Wii (Wiimote)" + "PS1PS2:PlayStation 1/2 (GPIO)" + "GAMECUBE:GameCube (GPIO)" + "DREAMCAST:Dreamcast (GPIO)" + "N64:Nintendo 64 (GPIO)" + "DINPUT:DInput" + "PS4:PlayStation 4 (DualShock 4 USB)" + "STEAM:SteamOS / Bazzite (DualSense + touchpad mouse)" + "PSCLASSIC:PlayStation Classic" + "WEBAPP:Web App" +) + +check_cmd() { command -v "$1" &>/dev/null; } + +echo "=== OGX-Mini 2026 Build Script ===" +echo "" + +# --- Prerequisites --- +MISSING=() +check_cmd git || MISSING+=(git) +check_cmd python3 || MISSING+=(python3) +check_cmd cmake || MISSING+=(cmake) +check_cmd ninja || MISSING+=(ninja) +check_cmd arm-none-eabi-gcc || MISSING+=(arm-none-eabi-gcc) + +if [ ${#MISSING[@]} -gt 0 ]; then + echo "Missing required tools: ${MISSING[*]}" + echo "" + echo "Install instructions:" + if check_cmd apt-get; then + echo " Debian/Ubuntu: sudo apt install git python3 cmake ninja-build gcc-arm-none-eabi" + elif check_cmd dnf; then + echo " Fedora: sudo dnf install git python3 cmake ninja gcc-arm-none-eabi-toolchain" + elif check_cmd pacman; then + echo " Arch: sudo pacman -S git python cmake ninja arm-none-eabi-gcc" + elif check_cmd brew; then + echo " macOS (Homebrew): brew install git python cmake ninja arm-none-eabi-gcc" + else + echo " Install: git, python3, cmake, ninja, and the ARM GCC toolchain (arm-none-eabi-gcc)." + echo " See README.md in the project root for details." + fi + echo "" + read -r -p "Exit now? [Y/n] " ans + case "${ans:-Y}" in [nN]) ;; *) exit 1;; esac +fi + +# --- Board selection --- +echo "Select board:" +for i in "${!BOARDS[@]}"; do + IFS=: read -r id desc <<< "${BOARDS[$i]}" + echo " $((i+1))) $desc ($id)" +done +echo " $(( ${#BOARDS[@]} + 1 ))) Cancel" +read -r -p "Choice [1-${#BOARDS[@]}]: " board_choice +if [ "$board_choice" = "$(( ${#BOARDS[@]} + 1 ))" ]; then echo "Cancelled."; exit 0; fi +if [ "$board_choice" -lt 1 ] || [ "$board_choice" -gt ${#BOARDS[@]} ]; then echo "Invalid choice."; exit 1; fi +IFS=: read -r OGXM_BOARD _ <<< "${BOARDS[$((board_choice-1))]}" + +# --- Combo vs fixed driver --- +echo "" +echo "Build type:" +echo " 1) Default (combos enabled, all modes selectable)" +echo " 2) Fixed output mode (combos disabled, single mode)" +read -r -p "Choice [1-2]: " build_type_choice +OGXM_FIXED_DRIVER="" +if [ "$build_type_choice" = "2" ]; then + echo "Select fixed output mode:" + for i in "${!FIXED_DRIVERS[@]}"; do + IFS=: read -r id desc <<< "${FIXED_DRIVERS[$i]}" + echo " $((i+1))) $desc ($id)" + done + echo " $(( ${#FIXED_DRIVERS[@]} + 1 ))) Cancel" + read -r -p "Choice [1-${#FIXED_DRIVERS[@]}]: " driver_choice + if [ "$driver_choice" = "$(( ${#FIXED_DRIVERS[@]} + 1 ))" ]; then echo "Cancelled."; exit 0; fi + if [ "$driver_choice" -lt 1 ] || [ "$driver_choice" -gt ${#FIXED_DRIVERS[@]} ]; then echo "Invalid choice."; exit 1; fi + IFS=: read -r OGXM_FIXED_DRIVER _ <<< "${FIXED_DRIVERS[$((driver_choice-1))]}" +fi + +# --- Release vs Debug --- +echo "" +echo "Build configuration:" +echo " 1) Release (smaller, faster)" +echo " 2) Debug (UART logging, no optimization)" +read -r -p "Choice [1-2]: " config_choice +if [ "$config_choice" = "2" ]; then + CMAKE_BUILD_TYPE=Debug +else + CMAKE_BUILD_TYPE=Release +fi + +OGXM_SWITCH2_HID_RAW_LOG=OFF +if [ "$config_choice" = "2" ]; then + echo "" + echo "Switch 2 Pro USB (PID 0x2069) debugging (Debug builds only):" + echo " 1) Off (default)" + echo " 2) UART: raw HID hex when report buffer changes (-DOGXM_SWITCH2_HID_RAW_LOG=ON)" + read -r -p "Choice [1-2]: " s2_raw_choice + if [ "$s2_raw_choice" = "2" ]; then + OGXM_SWITCH2_HID_RAW_LOG=ON + fi +fi + +# --- Run build --- +BUILD_DIR="$SCRIPT_DIR/build" +rm -rf "$BUILD_DIR" +mkdir -p "$BUILD_DIR" + +echo "" +echo "Building: board=$OGXM_BOARD type=$CMAKE_BUILD_TYPE ${OGXM_FIXED_DRIVER:+fixed=$OGXM_FIXED_DRIVER}${OGXM_SWITCH2_HID_RAW_LOG:+ switch2_raw_hid=ON}" +echo "Output directory: $BUILD_DIR" +echo "" + +BUILD_LOG=$(mktemp) +trap "rm -f '$BUILD_LOG'" EXIT + +( + cd "$BUILD_DIR" + CMAKE_ARGS=(-G Ninja -DOGXM_BOARD="$OGXM_BOARD" -DCMAKE_BUILD_TYPE="$CMAKE_BUILD_TYPE") + [ -n "$OGXM_FIXED_DRIVER" ] && CMAKE_ARGS+=(-DOGXM_FIXED_DRIVER="$OGXM_FIXED_DRIVER" -DOGXM_FIXED_DRIVER_ALLOW_COMBOS=OFF) + [ "$OGXM_SWITCH2_HID_RAW_LOG" = "ON" ] && CMAKE_ARGS+=(-DOGXM_SWITCH2_HID_RAW_LOG=ON) + cmake "${CMAKE_ARGS[@]}" "$FIRMWARE_RP2040" + ninja +) 2>&1 | tee "$BUILD_LOG" +BUILD_STATUS=${PIPESTATUS[0]} + +if [ "$BUILD_STATUS" -eq 0 ]; then + echo "" + echo "Build completed successfully. Output is in: $BUILD_DIR" + if [ "$CMAKE_BUILD_TYPE" = "Debug" ]; then + echo "" + echo "Debug logging (OGXM_LOG, Switch 2 raw HID): UART only at 115200 8N1 — not the Pico USB cable." + case "$OGXM_BOARD" in + PI_PICOW|PI_PICO2W) echo " Wire USB-serial RX to GP4 (board TX), GND to GND. (UART1; avoids PIO USB on GP0/1.)" ;; + *) echo " Wire USB-serial RX to GP0 (board TX), GND to GND." ;; + esac + echo " Then: minicom -D /dev/ttyUSB0 -b 115200 (or screen, PuTTY, VS Code Serial Monitor)" + fi +else + echo "" + echo "Build failed. You can save the output to a log file for troubleshooting." + read -r -p "Save log to $SCRIPT_DIR/build_log.txt? [y/N] " save_log + case "${save_log:-N}" in + [yY]|[yY][eE][sS]) + cp "$BUILD_LOG" "$SCRIPT_DIR/build_log.txt" + echo "Log saved to $SCRIPT_DIR/build_log.txt" + ;; + esac + exit 1 +fi