Compare commits
20 commits
feature/pi
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 7449d6d029 | |||
| ae4abbfeb4 | |||
| 8351cb1d15 | |||
| fd23842327 | |||
| 41c7021813 | |||
| 74a62f035b | |||
| 146c377311 | |||
| 81f6ac1236 | |||
| 94ccfc7220 | |||
| fa8ccf0b12 | |||
| 11eb44d3d1 | |||
| d9f0761949 | |||
| 23a3cbade5 | |||
| 4a73680bd2 | |||
| fac919a798 | |||
| 18c27b06ae | |||
| edf6ecaae1 | |||
| 7941294a8d | |||
| 04df6b88ad | |||
| 7bd3244006 |
56 changed files with 6636 additions and 664 deletions
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
# Unified diffs require a one-character context marker on blank lines.
|
||||
*.patch -whitespace
|
||||
|
|
@ -90,8 +90,20 @@ add_executable(switch-pico
|
|||
switch_haptics.cpp
|
||||
)
|
||||
if(SWITCH_PICO_INPUT_BACKEND STREQUAL "BLUEPAD32")
|
||||
target_sources(switch-pico PRIVATE bluepad32_input_backend.cpp)
|
||||
target_compile_definitions(switch-pico PRIVATE SWITCH_PICO_BLUEPAD32=1)
|
||||
target_sources(switch-pico PRIVATE
|
||||
bluepad32_input_backend.cpp
|
||||
bootsel_pairing_button.cpp
|
||||
usb_pairing_management.cpp
|
||||
)
|
||||
target_compile_definitions(switch-pico PRIVATE
|
||||
SWITCH_PICO_BLUEPAD32=1
|
||||
SWITCH_PICO_HID_INSTANCE_COUNT=4
|
||||
PICO_FLASH_ASSUME_CORE1_SAFE=0
|
||||
)
|
||||
else()
|
||||
target_compile_definitions(switch-pico PRIVATE
|
||||
SWITCH_PICO_HID_INSTANCE_COUNT=1
|
||||
)
|
||||
endif()
|
||||
|
||||
pico_set_program_name(switch-pico "switch-pico")
|
||||
|
|
@ -118,6 +130,7 @@ if(SWITCH_PICO_INPUT_BACKEND STREQUAL "BLUEPAD32")
|
|||
pico_btstack_classic
|
||||
pico_btstack_cyw43
|
||||
pico_multicore
|
||||
pico_flash
|
||||
)
|
||||
endif()
|
||||
|
||||
|
|
|
|||
134
README.md
134
README.md
|
|
@ -1,12 +1,12 @@
|
|||
# Switch Pico Controller Bridge
|
||||
|
||||
Raspberry Pi Pico firmware that emulates a Switch Pro controller over USB. Input can come from the SDL3-to-UART computer bridge or, on Pico 2 W, directly from a Bluetooth controller through Bluepad32.
|
||||
Raspberry Pi Pico firmware that emulates one or more Switch Pro controllers over USB. Input can come from the SDL3-to-UART computer bridge or, on Pico 2 W, directly from Bluetooth controllers through Bluepad32.
|
||||
|
||||
## What you get
|
||||
- **Firmware** (`switch-pico.cpp` + `switch_pro_driver.*`): acts as a wired Switch Pro, accepting either UART bridge reports or the optional Pico 2 W Bluepad32 backend.
|
||||
- **Firmware** (`switch-pico.cpp` + `switch_pro_driver.*`): acts as a Switch Pro controller (one on standard Pico, four on Pico 2 W AIO), accepting either UART bridge reports or the optional Pico 2 W Bluepad32 backend.
|
||||
- **Python bridge** (`switch_pico_bridge.controller_uart_bridge` / CLI `controller-uart-bridge`): reads SDL3 controllers on the host, sends reports over UART, and applies rumble locally. Hot‑plug friendly and cross‑platform (macOS/Windows/Linux).
|
||||
- **Colour override** (`controller_color_config.h`): compile‑time RGB overrides for body/buttons/grips as seen by the Switch.
|
||||
- **Pico 2 W AIO firmware** (`firmware/switch-pico-aio.uf2`): hosts one Bluetooth controller and sends its controls, calibrated motion, and rumble through the same Switch Pro USB device without a computer.
|
||||
- **Color configuration** (`controller_color_config.h`): compile-time RGB colors for emulated controller grips and supported Bluetooth controller LEDs.
|
||||
- **Pico 2 W AIO firmware** (`firmware/switch-pico-aio.uf2`): hosts four concurrent Bluetooth controllers and sends their controls, calibrated motion, rumble, and slot identity through four separate Switch Pro USB interfaces without a computer.
|
||||
|
||||
## Quick start
|
||||
1. Flash the Pico with `firmware/switch-pico.uf2` (or build your own) using BOOTSEL drag-and-drop (see “Manual UF2 flashing” below).
|
||||
|
|
@ -17,7 +17,11 @@ Raspberry Pi Pico firmware that emulates a Switch Pro controller over USB. Input
|
|||
|
||||
## Pico 2 W all-in-one Bluetooth option
|
||||
|
||||
The AIO build runs TinyUSB and Switch report generation on Core 0 while Bluepad32, BTstack, and the CYW43439 radio run on Core 1. A fixed state snapshot and bounded rumble queue are the only cross-core interfaces.
|
||||
### Architecture
|
||||
|
||||
The AIO build accepts up to four concurrent Bluetooth controllers on a single Pico 2 W. TinyUSB and the four Switch report generators run on Core 0; Bluepad32, BTstack, and the CYW43439 radio run on Core 1. Each Bluetooth device index maps directly to one always-present USB Pro HID interface. Per-slot state snapshots and generation-tagged latest-value rumble mailboxes are the only cross-core data paths.
|
||||
|
||||
All four USB interfaces are always present to the Switch as separate Pro Controllers on one physical USB device. Input, motion, rumble, lifecycle, and displayed grip color remain isolated per slot.
|
||||
|
||||
### Build and flash
|
||||
|
||||
|
|
@ -42,30 +46,123 @@ The default `python3 build.py` command and `firmware/switch-pico.*` artifacts re
|
|||
|
||||
Both `build.py --aio` and direct AIO CMake configuration apply `patches/bluepad32-sdl3-imu.patch` idempotently before compiling Bluepad32. The patch makes supported motion controllers use SDL3-equivalent axes and fixed-point units before conversion to Nintendo samples. It intentionally leaves the dependency worktree dirty; the committed submodule revision remains Bluepad32 4.2.0.
|
||||
|
||||
### Pair a controller
|
||||
### Pairing up to four controllers
|
||||
|
||||
1. Flash and connect the Pico 2 W to the Switch.
|
||||
2. Enable `System Settings → Controllers and Sensors → Pro Controller Wired Communication`.
|
||||
3. Put one controller into Bluetooth pairing mode:
|
||||
3. Hold BOOTSEL for about two seconds until the onboard LED starts double-blinking. This enables new Bluetooth authentication for 60 seconds.
|
||||
4. Put a controller into Bluetooth pairing mode:
|
||||
- DualSense: hold Create + PS.
|
||||
- DualShock 4: hold Share + PS.
|
||||
- Switch Pro: press its sync button.
|
||||
- Xbox Bluetooth controller: hold its pair button.
|
||||
- 8BitDo: use a Bluetooth mode supported by Bluepad32; use Switch/S mode when motion is required.
|
||||
4. Wait for the controller to connect. Pairing keys persist across Pico reboots.
|
||||
5. Wait for the controller's player light to settle. Repeat step 4 for additional controllers while the window remains open. Holding BOOTSEL again extends the deadline by 60 seconds from that point.
|
||||
|
||||
Only one wireless controller owns the emulated Pro Controller. Turn off or disconnect it before pairing another; scanning resumes automatically after disconnect. A disconnect immediately publishes neutral buttons, sticks, and motion.
|
||||
Pairing order determines the initial USB slot assignment. Up to four controllers map 1:1 to the four emulated Switch Pro Controller interfaces.
|
||||
|
||||
While a slot is free, the Pico continuously runs Bluepad32's normal Bluetooth discovery and autoconnect path. Pairing keys persist across Pico power cycles, so reconnect a previously paired controller by pressing its normal Home, PS, or Xbox power button; BOOTSEL is not required. Outside the BOOTSEL window, BTstack remains non-bondable, rejects new Classic SSP or legacy PIN authentication, and disables every BLE STK generation method. A controller in explicit pairing mode therefore cannot create a new Classic or BLE bond while the window is closed.
|
||||
|
||||
To clear every stored Classic and BLE pairing without a PC, hold BOOTSEL continuously for 10 seconds. The normal pairing window opens after two seconds; continuing to hold until the LED changes to a rapid blink clears all bonds, disconnects active controllers, publishes neutral state to every slot, and closes new authentication. Release BOOTSEL, open a new pairing window, and pair controllers again.
|
||||
|
||||
|
||||
### LED meanings and device state
|
||||
|
||||
The Pico 2 W onboard LED reports the overall Bluetooth state:
|
||||
- **Double blink**: new controller authentication is enabled for the bounded pairing window.
|
||||
- **Rapid blink for two seconds**: all stored pairings were cleared.
|
||||
- **Fast blink**: a controller connection is still completing its handshake.
|
||||
- **Solid**: at least one controller is active.
|
||||
- **Slow blink**: no controller is active; Bluetooth discovery and autoconnect are running.
|
||||
- **Solid immediately after boot that never transitions**: Bluepad32 initialization did not complete; check firmware flashing and UART logs.
|
||||
|
||||
### Managing controller disconnect and reconnect
|
||||
|
||||
- **Disconnect a controller**: its slot immediately publishes neutral buttons, sticks, and motion. Other connected controllers are unaffected.
|
||||
- **Reconnect a paired controller**: power it on normally with its Home, PS, or Xbox button.
|
||||
- **8BitDo Ultimate Bluetooth reconnect**: leave its selector in Bluetooth mode, press Home once, then shake it. After an abrupt controller power-off, the Pico can remain solid for up to four seconds while Bluetooth link supervision confirms the disconnect; scanning restarts immediately afterward.
|
||||
- **Pair a new controller**: hold BOOTSEL until the LED double-blinks, then put the controller into its explicit Bluetooth pairing mode.
|
||||
- **Pairing window expires**: new authentication is disabled; discovery and remembered-controller autoconnect continue while a slot is free.
|
||||
- **Clear all pairings**: hold BOOTSEL continuously for 10 seconds, through the initial double blink, until the rapid confirmation blink starts. All controllers are disconnected and must be paired again.
|
||||
|
||||
### Managing pairings from a PC
|
||||
|
||||
Connect the Pico 2 W to the PC while the AIO firmware is running normally; do not enter the ROM BOOTSEL drive. The management command uses private vendor requests on USB endpoint 0, so it does not add an interface or depend on Linux `hidraw` nodes.
|
||||
|
||||
```sh
|
||||
uv run switch-pico-pairings list
|
||||
uv run switch-pico-pairings clear --yes
|
||||
```
|
||||
|
||||
`list` refreshes and prints stored Bluetooth Classic and BLE addresses. `clear --yes` deletes all bonds, disconnects active controllers, closes new authentication, and leaves autoconnect scanning active. The destructive command requires `--yes`. If multiple compatible Picos are attached, select one with `--bus N --address N`; the error lists their locations. USB access errors require permission to the matching `/dev/bus/usb` device.
|
||||
|
||||
### Per-controller ABXY layout
|
||||
|
||||
Each connected AIO controller can toggle its own ABXY layout by pressing **L + R + Select + Start** together. On DualSense, use **L1 + R1 + Create + Options**. The controller gives one short rumble when the toggle is accepted; release the chord before toggling again.
|
||||
|
||||
- **Standard**: south→B, east→A, west→Y, north→X.
|
||||
- **Swapped**: south→A, east→B, west→X, north→Y.
|
||||
- The chord is consumed locally and is not forwarded to the Switch.
|
||||
- Other controller slots are unaffected.
|
||||
- Layout returns to the configured default after disconnect or reboot.
|
||||
|
||||
Edit `controller_hotkey_config.h` to change the chord, default layout, or confirmation pulse.
|
||||
|
||||
### Per-controller motion toggle
|
||||
|
||||
Press **D-pad Up + R + Start** together to disable or re-enable motion for one controller. On DualSense, use **D-pad Up + R1 + Options**.
|
||||
|
||||
- A longer rumble confirms motion disabled.
|
||||
- A shorter rumble confirms motion enabled.
|
||||
- The chord is consumed locally and is not forwarded to the Switch.
|
||||
- Other controller slots are unaffected.
|
||||
- Motion returns to enabled after disconnect or reboot.
|
||||
|
||||
Edit `controller_hotkey_config.h` to change the chord, default state, or feedback patterns.
|
||||
|
||||
### Per-slot controller colors
|
||||
|
||||
Each AIO slot has one color shared by its emulated Switch Pro grips and its physical Bluetooth controller:
|
||||
|
||||
1. Blue `#0089EB`
|
||||
2. Red `#E63946`
|
||||
3. Yellow `#F6C945`
|
||||
4. Green `#2ECC71`
|
||||
|
||||
When a controller becomes ready, RGB-capable devices such as DualSense and DualShock 4 receive a darker, more saturated RGB value derived automatically from the slot's Switch grip color. Controllers without an RGB light use player indicator 1, 2, 3, or 4 when Bluepad32 exposes player-LED control. Devices without either capability are left unchanged. Edit only the four grip colors in `controller_color_config.h`; rebuilding automatically recalibrates their lightbar colors.
|
||||
|
||||
### Controller capabilities
|
||||
|
||||
| Controller | Buttons/sticks | Rumble | Motion |
|
||||
|---|---:|---:|---:|
|
||||
| DualSense / DualShock 4 | Yes | Yes | Yes |
|
||||
| Switch Pro | Yes | Yes | Yes |
|
||||
| Switch Pro / Joy-Con | Yes | Yes | Yes |
|
||||
| PS Move ZCM1/ZCM2 | Buttons/trigger | Yes | Yes, after calibration |
|
||||
| Wii Remote | Mode-dependent | Yes | Accelerometer |
|
||||
| 8BitDo in Switch-compatible Bluetooth mode | Yes | Model-dependent | Yes when the mode exposes IMU |
|
||||
| Xbox Bluetooth controller | Yes | Yes | No hardware IMU |
|
||||
|
||||
Motion is normalized to 1024 units per degree/second and 8192 units per g in SDL3 axes, then converted to Nintendo axes and raw counts. The latest normalized sample is duplicated across the report's three nominal 5 ms slots; it remains pending until a regular `0x30` USB report successfully consumes it.
|
||||
Motion-producing Bluepad32 parsers normalize to 1024 units per degree/second and 8192 units per g in SDL-oriented axes before conversion to Nintendo samples. PS Move motion remains neutral until all model-specific calibration blocks have been received and validated; buttons and rumble remain available while calibration is pending or unavailable. The latest normalized sample is duplicated across the report's three nominal 5 ms slots and remains pending until a regular `0x30` USB report successfully consumes it.
|
||||
|
||||
### Rumble per controller
|
||||
|
||||
Rumble effects are per-slot and independent. The Switch sends rumble commands to a specific USB interface, and the Pico routes each command to the Bluetooth controller in the matching slot. Each slot has a critical-section-protected latest-value mailbox tagged with its connection generation; a newer pending command replaces the older one, and disconnect invalidates commands from the prior controller.
|
||||
|
||||
### Hardware validation
|
||||
|
||||
The four-interface AIO build has been verified on a real Switch with two DualSense controllers: the Switch assigned independent controller slots, and buttons, sticks, calibrated motion, rumble, and disconnect isolation worked per controller. Fresh DualSense pairing through the BOOTSEL-open window has also been verified on hardware.
|
||||
|
||||
To reproduce the validation:
|
||||
|
||||
1. **Verify USB enumeration**: Connect the Pico 2 W to a USB host or analyzer. Confirm that four HID interfaces are present, using IN/OUT endpoint pairs `0x81/0x01` through `0x84/0x04`.
|
||||
2. **Verify Bluetooth pairing**: Hold BOOTSEL until the LED double-blinks, put a controller into explicit pairing mode, and confirm its player light settles.
|
||||
3. **Verify input on one controller**: Move sticks and press buttons; confirm only its assigned Switch slot changes.
|
||||
4. **Verify input on two controllers**: Move the second controller independently and confirm the first controller's slot is unaffected.
|
||||
5. **Verify the pairing gate**: Power-cycle the Pico and confirm a paired controller reconnects with its normal Home/PS/Xbox button without BOOTSEL. Put an unpaired controller into explicit pairing mode and confirm it remains blocked until the BOOTSEL window opens.
|
||||
6. **Verify rumble per slot**: Send rumble to interface 0 and confirm only the slot 0 controller vibrates. Send rumble to interface 1 and confirm only the slot 1 controller vibrates.
|
||||
7. **Verify motion**: Enable gyro/accel on both controllers. Rotate each controller independently and confirm that motion is per-slot (rotating controller 0 does not affect controller 1's IMU output).
|
||||
|
||||
On the tested Linux host, all four HID interfaces enumerated, but `hid-nintendo` timed out (`-110`) while requesting controller information from the composite device and removed the transient hidraw nodes. This is an observed, undiagnosed composite interoperability limitation; its root cause has not been established. The timeout was not observed on the Switch, so successful `hid-nintendo` binding is not the release criterion for the four-interface AIO firmware. The pairing CLI uses vendor control transfers on endpoint 0 and does not depend on those hidraw nodes.
|
||||
|
||||
Bluepad32 is Apache-2.0. BTstack use on Pico W/Pico 2 W is covered by Raspberry Pi's BTstack license.
|
||||
|
||||
|
|
@ -193,20 +290,21 @@ The generated files are:
|
|||
- `firmware/switch-pico.elf` and `firmware/switch-pico.uf2`, refreshed from the
|
||||
corresponding `build/` artifacts after every successful build.
|
||||
|
||||
To customize the controller grip color while building, pass one of these mutually
|
||||
exclusive options:
|
||||
To assign one color to every emulated controller slot while building, pass one
|
||||
of these mutually exclusive options:
|
||||
|
||||
```sh
|
||||
# Use a random color for both grips
|
||||
# Use one random color for all slots
|
||||
python3 build.py --random-grip-color
|
||||
|
||||
# Use a specific six-digit RGB color for both grips
|
||||
# Use one specific six-digit RGB color for all slots
|
||||
python3 build.py --grip-color FF00AA
|
||||
```
|
||||
|
||||
Both options update `controller_color_config.h` before building. With no color
|
||||
option, that file is left unchanged. Run `python3 build.py --help` to see the
|
||||
available command-line options.
|
||||
Both options update all four slot definitions in
|
||||
`controller_color_config.h` before building. With no color option, the
|
||||
per-slot blue/red/yellow/green palette is left unchanged. Run
|
||||
`python3 build.py --help` to see the available command-line options.
|
||||
|
||||
If the tools or artifacts are in non-default locations, use these environment
|
||||
variables:
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
#define ENABLE_LE_DATA_LENGTH_EXTENSION
|
||||
#define ENABLE_LE_PERIPHERAL
|
||||
#define ENABLE_LE_PRIVACY_ADDRESS_RESOLUTION
|
||||
#define ENABLE_LE_RESOLVING_LIST
|
||||
#define ENABLE_LE_SECURE_CONNECTIONS
|
||||
#else
|
||||
#error "BP32: ENABLE_BLE should be defined"
|
||||
|
|
@ -40,12 +41,12 @@
|
|||
#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_GATT_CLIENTS 4
|
||||
#define MAX_NR_HCI_CONNECTIONS 4
|
||||
#define MAX_NR_HID_HOST_CONNECTIONS 1
|
||||
#define MAX_NR_HIDS_CLIENTS 1
|
||||
#define MAX_NR_HID_HOST_CONNECTIONS 4
|
||||
#define MAX_NR_HIDS_CLIENTS 4
|
||||
#define MAX_NR_HFP_CONNECTIONS 1
|
||||
#define MAX_NR_L2CAP_CHANNELS 6
|
||||
#define MAX_NR_L2CAP_CHANNELS 10
|
||||
#define MAX_NR_L2CAP_SERVICES 5
|
||||
#define MAX_NR_RFCOMM_CHANNELS 1
|
||||
#define MAX_NR_RFCOMM_MULTIPLEXERS 1
|
||||
|
|
|
|||
242
bluepad32_config/parser/uni_hid_parser_imu.h
Normal file
242
bluepad32_config/parser/uni_hid_parser_imu.h
Normal file
|
|
@ -0,0 +1,242 @@
|
|||
#pragma once
|
||||
|
||||
#include <limits.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
#define UNI_IMU_ACCEL_RES_PER_G 8192
|
||||
#define UNI_IMU_GYRO_RES_PER_DEG_S 1024
|
||||
#define UNI_PSMOVE_CALIBRATION_REPORT_SIZE 49
|
||||
#define UNI_PSMOVE_ZCM1_CALIBRATION_SIZE 143
|
||||
#define UNI_PSMOVE_ZCM2_CALIBRATION_SIZE 96
|
||||
|
||||
typedef enum {
|
||||
UNI_PSMOVE_IMU_MODEL_UNKNOWN = 0,
|
||||
UNI_PSMOVE_IMU_MODEL_ZCM1,
|
||||
UNI_PSMOVE_IMU_MODEL_ZCM2,
|
||||
} uni_psmove_imu_model_t;
|
||||
|
||||
typedef enum {
|
||||
UNI_PSMOVE_CALIBRATION_INVALID = 0,
|
||||
UNI_PSMOVE_CALIBRATION_INCOMPLETE,
|
||||
UNI_PSMOVE_CALIBRATION_COMPLETE,
|
||||
} uni_psmove_calibration_result_t;
|
||||
|
||||
typedef struct {
|
||||
int32_t accel[3];
|
||||
int32_t gyro[3];
|
||||
} uni_imu_fixed_sample_t;
|
||||
|
||||
typedef struct {
|
||||
uint8_t data[UNI_PSMOVE_ZCM1_CALIBRATION_SIZE];
|
||||
uni_psmove_imu_model_t model;
|
||||
uint8_t received_blocks;
|
||||
bool complete;
|
||||
} uni_psmove_imu_calibration_t;
|
||||
|
||||
static inline int32_t uni_imu_clamp_i64(int64_t value) {
|
||||
if (value > INT32_MAX) {
|
||||
return INT32_MAX;
|
||||
}
|
||||
if (value < INT32_MIN) {
|
||||
return INT32_MIN;
|
||||
}
|
||||
return (int32_t)value;
|
||||
}
|
||||
|
||||
static inline int32_t uni_imu_scale(int32_t value, int32_t span,
|
||||
int32_t full_scale) {
|
||||
if (span <= 0) {
|
||||
return 0;
|
||||
}
|
||||
return uni_imu_clamp_i64((int64_t)value * full_scale / span);
|
||||
}
|
||||
|
||||
static inline int32_t uni_psmove_scale_gyro(int32_t raw, int32_t bias,
|
||||
int32_t span,
|
||||
int32_t full_scale) {
|
||||
if (span <= 0) {
|
||||
return 0;
|
||||
}
|
||||
return uni_imu_clamp_i64(
|
||||
((int64_t)raw - bias) * full_scale / span);
|
||||
}
|
||||
|
||||
static inline int32_t uni_psmove_decode_value(
|
||||
uni_psmove_imu_model_t model, uint16_t value) {
|
||||
if (model == UNI_PSMOVE_IMU_MODEL_ZCM1) {
|
||||
return (int32_t)value - 0x8000;
|
||||
}
|
||||
return (int16_t)value;
|
||||
}
|
||||
|
||||
static inline int32_t uni_psmove_read_calibration_value(
|
||||
const uint8_t* data, uni_psmove_imu_model_t model, uint8_t offset) {
|
||||
const uint16_t value =
|
||||
(uint16_t)(data[offset] | ((uint16_t)data[offset + 1] << 8));
|
||||
return uni_psmove_decode_value(model, value);
|
||||
}
|
||||
|
||||
static inline uni_psmove_calibration_result_t
|
||||
uni_psmove_add_calibration_report(uni_psmove_imu_calibration_t* calibration,
|
||||
uni_psmove_imu_model_t model,
|
||||
const uint8_t* report, uint16_t length) {
|
||||
if (calibration == NULL || report == NULL ||
|
||||
length != UNI_PSMOVE_CALIBRATION_REPORT_SIZE || report[0] != 0x10 ||
|
||||
(model != UNI_PSMOVE_IMU_MODEL_ZCM1 &&
|
||||
model != UNI_PSMOVE_IMU_MODEL_ZCM2)) {
|
||||
return UNI_PSMOVE_CALIBRATION_INVALID;
|
||||
}
|
||||
if (calibration->model != UNI_PSMOVE_IMU_MODEL_UNKNOWN &&
|
||||
calibration->model != model) {
|
||||
return UNI_PSMOVE_CALIBRATION_INVALID;
|
||||
}
|
||||
calibration->model = model;
|
||||
|
||||
size_t offset;
|
||||
size_t source_offset;
|
||||
uint8_t block_mask;
|
||||
switch (report[1]) {
|
||||
case 0x00:
|
||||
offset = 0;
|
||||
source_offset = 0;
|
||||
block_mask = 0x01;
|
||||
break;
|
||||
case 0x01:
|
||||
if (model != UNI_PSMOVE_IMU_MODEL_ZCM1) {
|
||||
return UNI_PSMOVE_CALIBRATION_INVALID;
|
||||
}
|
||||
offset = UNI_PSMOVE_CALIBRATION_REPORT_SIZE;
|
||||
source_offset = 2;
|
||||
block_mask = 0x02;
|
||||
break;
|
||||
case 0x81:
|
||||
if (model != UNI_PSMOVE_IMU_MODEL_ZCM2) {
|
||||
return UNI_PSMOVE_CALIBRATION_INVALID;
|
||||
}
|
||||
offset = UNI_PSMOVE_CALIBRATION_REPORT_SIZE;
|
||||
source_offset = 2;
|
||||
block_mask = 0x02;
|
||||
break;
|
||||
case 0x82:
|
||||
if (model != UNI_PSMOVE_IMU_MODEL_ZCM1) {
|
||||
return UNI_PSMOVE_CALIBRATION_INVALID;
|
||||
}
|
||||
offset = 2 * UNI_PSMOVE_CALIBRATION_REPORT_SIZE - 2;
|
||||
source_offset = 2;
|
||||
block_mask = 0x04;
|
||||
break;
|
||||
default:
|
||||
return UNI_PSMOVE_CALIBRATION_INVALID;
|
||||
}
|
||||
|
||||
const size_t copy_size = length - source_offset;
|
||||
const size_t calibration_size =
|
||||
model == UNI_PSMOVE_IMU_MODEL_ZCM1
|
||||
? UNI_PSMOVE_ZCM1_CALIBRATION_SIZE
|
||||
: UNI_PSMOVE_ZCM2_CALIBRATION_SIZE;
|
||||
if (offset + copy_size > calibration_size) {
|
||||
return UNI_PSMOVE_CALIBRATION_INVALID;
|
||||
}
|
||||
memcpy(&calibration->data[offset], &report[source_offset], copy_size);
|
||||
calibration->received_blocks |= block_mask;
|
||||
|
||||
const uint8_t required_blocks =
|
||||
model == UNI_PSMOVE_IMU_MODEL_ZCM1 ? 0x07 : 0x03;
|
||||
calibration->complete =
|
||||
(calibration->received_blocks & required_blocks) == required_blocks;
|
||||
return calibration->complete ? UNI_PSMOVE_CALIBRATION_COMPLETE
|
||||
: UNI_PSMOVE_CALIBRATION_INCOMPLETE;
|
||||
}
|
||||
|
||||
static inline bool uni_psmove_normalize_imu(
|
||||
uni_psmove_imu_model_t model,
|
||||
const uni_psmove_imu_calibration_t* calibration,
|
||||
const uint16_t accel_first[3], const uint16_t accel_second[3],
|
||||
const uint16_t gyro_first[3], const uint16_t gyro_second[3],
|
||||
uni_imu_fixed_sample_t* output) {
|
||||
if (output == NULL) {
|
||||
return false;
|
||||
}
|
||||
memset(output, 0, sizeof(*output));
|
||||
if (calibration == NULL || !calibration->complete ||
|
||||
calibration->model != model || accel_first == NULL ||
|
||||
accel_second == NULL || gyro_first == NULL || gyro_second == NULL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
static const uint8_t zcm1_accel_low[] = {0x0a, 0x24, 0x14};
|
||||
static const uint8_t zcm1_accel_high[] = {0x16, 0x1e, 0x08};
|
||||
static const uint8_t zcm2_accel_low[] = {0x08, 0x16, 0x24};
|
||||
static const uint8_t zcm2_accel_high[] = {0x02, 0x10, 0x1e};
|
||||
static const uint8_t zcm1_gyro_bias[] = {0x2a, 0x2c, 0x2e};
|
||||
static const uint8_t zcm1_gyro_high[] = {0x46, 0x50, 0x5a};
|
||||
static const uint8_t zcm2_gyro_bias[] = {0x26, 0x28, 0x2a};
|
||||
static const uint8_t zcm2_gyro_low[] = {0x42, 0x4a, 0x52};
|
||||
static const uint8_t zcm2_gyro_high[] = {0x30, 0x38, 0x40};
|
||||
|
||||
const uint8_t* accel_low =
|
||||
model == UNI_PSMOVE_IMU_MODEL_ZCM1 ? zcm1_accel_low : zcm2_accel_low;
|
||||
const uint8_t* accel_high =
|
||||
model == UNI_PSMOVE_IMU_MODEL_ZCM1 ? zcm1_accel_high : zcm2_accel_high;
|
||||
const uint8_t* gyro_bias =
|
||||
model == UNI_PSMOVE_IMU_MODEL_ZCM1 ? zcm1_gyro_bias : zcm2_gyro_bias;
|
||||
const uint8_t* gyro_high =
|
||||
model == UNI_PSMOVE_IMU_MODEL_ZCM1 ? zcm1_gyro_high : zcm2_gyro_high;
|
||||
const int32_t gyro_full_scale =
|
||||
(model == UNI_PSMOVE_IMU_MODEL_ZCM1 ? 480 : 540) *
|
||||
UNI_IMU_GYRO_RES_PER_DEG_S;
|
||||
|
||||
for (uint8_t axis = 0; axis < 3; ++axis) {
|
||||
const int32_t accel_low_value = uni_psmove_read_calibration_value(
|
||||
calibration->data, model, accel_low[axis]);
|
||||
const int32_t accel_high_value = uni_psmove_read_calibration_value(
|
||||
calibration->data, model, accel_high[axis]);
|
||||
const int32_t accel_center =
|
||||
(accel_low_value + accel_high_value) / 2;
|
||||
const int32_t accel_raw =
|
||||
(uni_psmove_decode_value(model, accel_first[axis]) +
|
||||
uni_psmove_decode_value(model, accel_second[axis])) /
|
||||
2;
|
||||
const int32_t accel_delta = accel_raw - accel_center;
|
||||
const int32_t accel_span =
|
||||
accel_delta < 0 ? accel_center - accel_low_value
|
||||
: accel_high_value - accel_center;
|
||||
output->accel[axis] =
|
||||
uni_imu_scale(accel_delta, accel_span, UNI_IMU_ACCEL_RES_PER_G);
|
||||
|
||||
const int32_t gyro_bias_value = uni_psmove_read_calibration_value(
|
||||
calibration->data, model, gyro_bias[axis]);
|
||||
const int32_t gyro_raw =
|
||||
(uni_psmove_decode_value(model, gyro_first[axis]) +
|
||||
uni_psmove_decode_value(model, gyro_second[axis])) /
|
||||
2;
|
||||
int32_t gyro_span;
|
||||
if (model == UNI_PSMOVE_IMU_MODEL_ZCM1 ||
|
||||
gyro_raw >= gyro_bias_value) {
|
||||
gyro_span = uni_psmove_read_calibration_value(
|
||||
calibration->data, model, gyro_high[axis]) -
|
||||
gyro_bias_value;
|
||||
} else {
|
||||
gyro_span = gyro_bias_value - uni_psmove_read_calibration_value(
|
||||
calibration->data, model,
|
||||
zcm2_gyro_low[axis]);
|
||||
}
|
||||
output->gyro[axis] = uni_psmove_scale_gyro(
|
||||
gyro_raw, gyro_bias_value, gyro_span, gyro_full_scale);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static inline void uni_imu_normalize_wii_accel(int32_t x, int32_t y,
|
||||
int32_t z,
|
||||
int32_t output[3]) {
|
||||
if (output == NULL) {
|
||||
return;
|
||||
}
|
||||
output[0] = uni_imu_scale(-x, 100, UNI_IMU_ACCEL_RES_PER_G);
|
||||
output[1] = uni_imu_scale(z, 100, UNI_IMU_ACCEL_RES_PER_G);
|
||||
output[2] = uni_imu_scale(y, 100, UNI_IMU_ACCEL_RES_PER_G);
|
||||
}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
#pragma once
|
||||
|
||||
// Bluepad32's Pico W example configuration, limited to one live controller.
|
||||
#define CONFIG_BLUEPAD32_MAX_DEVICES 1
|
||||
#define CONFIG_BLUEPAD32_MAX_ALLOWLIST 1
|
||||
// The AIO firmware exposes one fixed Bluepad32 device slot per USB interface.
|
||||
#define CONFIG_BLUEPAD32_MAX_DEVICES 4
|
||||
#define CONFIG_BLUEPAD32_MAX_ALLOWLIST 4
|
||||
#define CONFIG_BLUEPAD32_GAP_SECURITY 1
|
||||
#define CONFIG_BLUEPAD32_ENABLE_BLE_BY_DEFAULT 1
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -2,11 +2,45 @@
|
|||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "switch_pro_driver.h"
|
||||
#include "switch_haptics.h"
|
||||
#include "switch_pro_driver.h"
|
||||
|
||||
constexpr uint8_t BLUEPAD32_INPUT_BACKEND_SLOT_COUNT = 4;
|
||||
constexpr uint8_t BLUEPAD32_PAIRING_RECORD_CAPACITY = 16;
|
||||
|
||||
enum class Bluepad32PairingTransport : uint8_t {
|
||||
kClassic = 1,
|
||||
kBle = 2,
|
||||
};
|
||||
|
||||
enum class Bluepad32PairingSnapshotStatus : uint8_t {
|
||||
kReady = 0,
|
||||
kPending = 1,
|
||||
};
|
||||
|
||||
struct Bluepad32PairingRecord {
|
||||
Bluepad32PairingTransport transport;
|
||||
uint8_t address_type;
|
||||
uint8_t address[6];
|
||||
};
|
||||
|
||||
struct Bluepad32PairingSnapshot {
|
||||
uint32_t generation;
|
||||
Bluepad32PairingSnapshotStatus status;
|
||||
uint8_t record_count;
|
||||
bool overflow;
|
||||
Bluepad32PairingRecord records[BLUEPAD32_PAIRING_RECORD_CAPACITY];
|
||||
};
|
||||
|
||||
|
||||
void bluepad32_input_backend_init();
|
||||
void bluepad32_input_backend_start();
|
||||
bool bluepad32_input_backend_snapshot(SwitchInputState* out);
|
||||
void bluepad32_input_backend_report_sent();
|
||||
void bluepad32_input_backend_queue_rumble(const SwitchRumbleOutput& rumble);
|
||||
void bluepad32_input_backend_open_pairing_window();
|
||||
void bluepad32_input_backend_clear_pairings();
|
||||
bool bluepad32_input_backend_snapshot(uint8_t slot, SwitchInputState* out);
|
||||
void bluepad32_input_backend_request_pairing_snapshot();
|
||||
void bluepad32_input_backend_pairing_snapshot(
|
||||
Bluepad32PairingSnapshot* out);
|
||||
void bluepad32_input_backend_report_sent(uint8_t slot);
|
||||
void bluepad32_input_backend_queue_rumble(uint8_t slot,
|
||||
const SwitchRumbleOutput& rumble);
|
||||
|
|
|
|||
101
bootsel_pairing_button.cpp
Normal file
101
bootsel_pairing_button.cpp
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
#include "bootsel_pairing_button.h"
|
||||
|
||||
#include "hardware/gpio.h"
|
||||
#include "hardware/structs/ioqspi.h"
|
||||
#include "hardware/structs/sio.h"
|
||||
#include "pico/flash.h"
|
||||
#include "pico/time.h"
|
||||
#if PICO_RP2350
|
||||
#include "hardware/regs/sio.h"
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr uint32_t kPollIntervalMs = 100;
|
||||
constexpr uint32_t kFlashSafeTimeoutMs = 100;
|
||||
constexpr uint32_t kQspiCsPinIndex = 1;
|
||||
|
||||
BootselPairingButtonHoldFsm g_hold_fsm;
|
||||
uint32_t g_last_sample_ms = 0;
|
||||
|
||||
// QSPI CSn sampling adapted from awalol/DS5Dongle's button_functions.cpp:
|
||||
// https://github.com/awalol/DS5Dongle/blob/master/src/button_functions.cpp
|
||||
// Copyright (c) 2026 awalol; used under the MIT License.
|
||||
//
|
||||
// This callback and everything it executes while CSn is floated must remain in
|
||||
// SRAM or be an inlined hardware-register operation. In particular, do not add
|
||||
// logging or ordinary flash-backed data access here.
|
||||
void __no_inline_not_in_flash_func(read_bootsel_callback)(void* parameter) {
|
||||
auto* pressed = static_cast<bool*>(parameter);
|
||||
|
||||
hw_write_masked(
|
||||
&ioqspi_hw->io[kQspiCsPinIndex].ctrl,
|
||||
GPIO_OVERRIDE_LOW << IO_QSPI_GPIO_QSPI_SS_CTRL_OEOVER_LSB,
|
||||
IO_QSPI_GPIO_QSPI_SS_CTRL_OEOVER_BITS);
|
||||
|
||||
for (volatile uint32_t delay = 0; delay < 1000; ++delay) {
|
||||
}
|
||||
|
||||
#if PICO_RP2350
|
||||
*pressed =
|
||||
(sio_hw->gpio_hi_in & SIO_GPIO_HI_IN_QSPI_CSN_BITS) == 0;
|
||||
#else
|
||||
*pressed = (sio_hw->gpio_hi_in & (1u << kQspiCsPinIndex)) == 0;
|
||||
#endif
|
||||
|
||||
hw_write_masked(
|
||||
&ioqspi_hw->io[kQspiCsPinIndex].ctrl,
|
||||
GPIO_OVERRIDE_NORMAL << IO_QSPI_GPIO_QSPI_SS_CTRL_OEOVER_LSB,
|
||||
IO_QSPI_GPIO_QSPI_SS_CTRL_OEOVER_BITS);
|
||||
}
|
||||
|
||||
BootselPairingButtonSample sample_bootsel() {
|
||||
bool pressed = false;
|
||||
const int result = flash_safe_execute(read_bootsel_callback, &pressed,
|
||||
kFlashSafeTimeoutMs);
|
||||
if (result != PICO_OK) {
|
||||
return BootselPairingButtonSample::kUnread;
|
||||
}
|
||||
return pressed ? BootselPairingButtonSample::kPressed
|
||||
: BootselPairingButtonSample::kReleased;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
BootselPairingButtonEvent BootselPairingButtonHoldFsm::update(
|
||||
BootselPairingButtonSample sample) {
|
||||
if (sample == BootselPairingButtonSample::kUnread) {
|
||||
return BootselPairingButtonEvent::kNone;
|
||||
}
|
||||
|
||||
if (sample == BootselPairingButtonSample::kReleased) {
|
||||
pressed_samples_ = 0;
|
||||
pairing_reported_ = false;
|
||||
clear_reported_ = false;
|
||||
return BootselPairingButtonEvent::kNone;
|
||||
}
|
||||
|
||||
if (pressed_samples_ < kClearHoldSamples) {
|
||||
++pressed_samples_;
|
||||
}
|
||||
if (pressed_samples_ >= kClearHoldSamples && !clear_reported_) {
|
||||
clear_reported_ = true;
|
||||
return BootselPairingButtonEvent::kClearPairings;
|
||||
}
|
||||
if (pressed_samples_ >= kPairingHoldSamples && !pairing_reported_) {
|
||||
pairing_reported_ = true;
|
||||
return BootselPairingButtonEvent::kOpenPairing;
|
||||
}
|
||||
return BootselPairingButtonEvent::kNone;
|
||||
}
|
||||
|
||||
BootselPairingButtonEvent bootsel_pairing_button_task() {
|
||||
const uint32_t now_ms =
|
||||
static_cast<uint32_t>(to_ms_since_boot(get_absolute_time()));
|
||||
if (now_ms - g_last_sample_ms < kPollIntervalMs) {
|
||||
return BootselPairingButtonEvent::kNone;
|
||||
}
|
||||
g_last_sample_ms = now_ms;
|
||||
|
||||
return g_hold_fsm.update(sample_bootsel());
|
||||
}
|
||||
32
bootsel_pairing_button.h
Normal file
32
bootsel_pairing_button.h
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
enum class BootselPairingButtonSample : uint8_t {
|
||||
kUnread,
|
||||
kReleased,
|
||||
kPressed,
|
||||
};
|
||||
enum class BootselPairingButtonEvent : uint8_t {
|
||||
kNone,
|
||||
kOpenPairing,
|
||||
kClearPairings,
|
||||
};
|
||||
|
||||
|
||||
class BootselPairingButtonHoldFsm {
|
||||
public:
|
||||
static constexpr uint8_t kPairingHoldSamples = 20;
|
||||
static constexpr uint8_t kClearHoldSamples = 100;
|
||||
|
||||
BootselPairingButtonEvent update(BootselPairingButtonSample sample);
|
||||
|
||||
private:
|
||||
uint8_t pressed_samples_ = 0;
|
||||
bool pairing_reported_ = false;
|
||||
bool clear_reported_ = false;
|
||||
};
|
||||
|
||||
// Polls BOOTSEL at 10 Hz. Reports pairing at 2 seconds and clearing at
|
||||
// 10 seconds; each event fires once per continuous hold.
|
||||
BootselPairingButtonEvent bootsel_pairing_button_task();
|
||||
17
build.py
17
build.py
|
|
@ -22,13 +22,10 @@ AIO_FIRMWARE_UF2_PATH = FIRMWARE_DIR / "switch-pico-aio.uf2"
|
|||
ELF_PATH = Path(os.environ.get("ELF_PATH", BUILD_DIR / "switch-pico.elf")).expanduser()
|
||||
UF2_PATH = Path(os.environ.get("UF2_PATH", BUILD_DIR / "switch-pico.uf2")).expanduser()
|
||||
|
||||
MACROS = (
|
||||
"SWITCH_COLOR_LEFT_GRIP_R",
|
||||
"SWITCH_COLOR_LEFT_GRIP_G",
|
||||
"SWITCH_COLOR_LEFT_GRIP_B",
|
||||
"SWITCH_COLOR_RIGHT_GRIP_R",
|
||||
"SWITCH_COLOR_RIGHT_GRIP_G",
|
||||
"SWITCH_COLOR_RIGHT_GRIP_B",
|
||||
MACROS = tuple(
|
||||
f"SWITCH_COLOR_SLOT_{slot}_{component}"
|
||||
for slot in range(1, 5)
|
||||
for component in ("R", "G", "B")
|
||||
)
|
||||
|
||||
def parse_args():
|
||||
|
|
@ -46,12 +43,12 @@ def parse_args():
|
|||
group.add_argument(
|
||||
"--random-grip-color",
|
||||
action="store_true",
|
||||
help="Randomize both grip colors before building.",
|
||||
help="Assign one random color to every emulated controller slot.",
|
||||
)
|
||||
group.add_argument(
|
||||
"--grip-color",
|
||||
metavar="RRGGBB",
|
||||
help="Set both grip colors to the provided hex value.",
|
||||
help="Set every emulated controller slot to the provided hex color.",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
|
@ -84,7 +81,7 @@ def update_grip_colors(rgb_hex):
|
|||
sys.exit(1)
|
||||
return updated
|
||||
|
||||
values = (r, g, b, r, g, b)
|
||||
values = (r, g, b) * 4
|
||||
for macro, val in zip(MACROS, values):
|
||||
text = replace(macro, val, text)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,25 +1,31 @@
|
|||
// Optional override for Switch Pro colour fields.
|
||||
// Copy/modify the values below and rebuild to change how the controller appears on the Switch.
|
||||
// Each value is an 8-bit RGB component.
|
||||
// Compile-time Switch grip colors. Physical controller lightbar values are
|
||||
// derived automatically; each value here is an 8-bit RGB component.
|
||||
|
||||
#pragma once
|
||||
|
||||
// Body shell colour
|
||||
// Body shell color
|
||||
#define SWITCH_COLOR_BODY_R 0x1B
|
||||
#define SWITCH_COLOR_BODY_G 0x1B
|
||||
#define SWITCH_COLOR_BODY_B 0x1D
|
||||
|
||||
// Face/button cluster colour
|
||||
// Face/button cluster color
|
||||
#define SWITCH_COLOR_BUTTON_R 0xFF
|
||||
#define SWITCH_COLOR_BUTTON_G 0xFF
|
||||
#define SWITCH_COLOR_BUTTON_B 0xFF
|
||||
|
||||
// Left grip colour
|
||||
#define SWITCH_COLOR_LEFT_GRIP_R 0x00
|
||||
#define SWITCH_COLOR_LEFT_GRIP_G 0x89
|
||||
#define SWITCH_COLOR_LEFT_GRIP_B 0xEB
|
||||
// Per-slot Switch grip colors: blue, red, yellow, green.
|
||||
#define SWITCH_COLOR_SLOT_1_R 0x00
|
||||
#define SWITCH_COLOR_SLOT_1_G 0x89
|
||||
#define SWITCH_COLOR_SLOT_1_B 0xEB
|
||||
|
||||
// Right grip colour
|
||||
#define SWITCH_COLOR_RIGHT_GRIP_R 0x00
|
||||
#define SWITCH_COLOR_RIGHT_GRIP_G 0x89
|
||||
#define SWITCH_COLOR_RIGHT_GRIP_B 0xEB
|
||||
#define SWITCH_COLOR_SLOT_2_R 0xE6
|
||||
#define SWITCH_COLOR_SLOT_2_G 0x39
|
||||
#define SWITCH_COLOR_SLOT_2_B 0x46
|
||||
|
||||
#define SWITCH_COLOR_SLOT_3_R 0xF6
|
||||
#define SWITCH_COLOR_SLOT_3_G 0xC9
|
||||
#define SWITCH_COLOR_SLOT_3_B 0x45
|
||||
|
||||
#define SWITCH_COLOR_SLOT_4_R 0x2E
|
||||
#define SWITCH_COLOR_SLOT_4_G 0xCC
|
||||
#define SWITCH_COLOR_SLOT_4_B 0x71
|
||||
|
|
|
|||
31
controller_hotkey_config.h
Normal file
31
controller_hotkey_config.h
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
// Compile-time AIO controller hotkey configuration.
|
||||
|
||||
#pragma once
|
||||
|
||||
// Bluepad32 button masks. Default chord: L + R + SELECT + START.
|
||||
#define SWITCH_ABXY_HOTKEY_BUTTON_MASK \
|
||||
(BUTTON_SHOULDER_L | BUTTON_SHOULDER_R)
|
||||
#define SWITCH_ABXY_HOTKEY_MISC_MASK \
|
||||
(MISC_BUTTON_SELECT | MISC_BUTTON_START)
|
||||
|
||||
// Motion toggle chord: D-pad Up + R + START / Options.
|
||||
#define SWITCH_MOTION_HOTKEY_DPAD_MASK DPAD_UP
|
||||
#define SWITCH_MOTION_HOTKEY_BUTTON_MASK BUTTON_SHOULDER_R
|
||||
#define SWITCH_MOTION_HOTKEY_MISC_MASK MISC_BUTTON_START
|
||||
#define SWITCH_MOTION_DEFAULT_ENABLED 1
|
||||
|
||||
// 0 starts each new connection in Nintendo positional layout; 1 starts swapped.
|
||||
#define SWITCH_ABXY_DEFAULT_SWAPPED 0
|
||||
|
||||
// Local confirmation pulse sent only to the controller that toggled.
|
||||
#define SWITCH_ABXY_FEEDBACK_DURATION_MS 120
|
||||
#define SWITCH_ABXY_FEEDBACK_WEAK_MAGNITUDE 0xFF
|
||||
#define SWITCH_ABXY_FEEDBACK_STRONG_MAGNITUDE 0xFF
|
||||
|
||||
// A longer pulse confirms disabled; a shorter pulse confirms enabled.
|
||||
#define SWITCH_MOTION_DISABLED_FEEDBACK_DURATION_MS 180
|
||||
#define SWITCH_MOTION_DISABLED_FEEDBACK_WEAK_MAGNITUDE 0xA0
|
||||
#define SWITCH_MOTION_DISABLED_FEEDBACK_STRONG_MAGNITUDE 0xA0
|
||||
#define SWITCH_MOTION_ENABLED_FEEDBACK_DURATION_MS 80
|
||||
#define SWITCH_MOTION_ENABLED_FEEDBACK_WEAK_MAGNITUDE 0x60
|
||||
#define SWITCH_MOTION_ENABLED_FEEDBACK_STRONG_MAGNITUDE 0x60
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -1,3 +1,39 @@
|
|||
diff --git a/src/components/bluepad32/bt/uni_bt_bredr.c b/src/components/bluepad32/bt/uni_bt_bredr.c
|
||||
index 955cc6f..4013cc1 100644
|
||||
--- a/src/components/bluepad32/bt/uni_bt_bredr.c
|
||||
+++ b/src/components/bluepad32/bt/uni_bt_bredr.c
|
||||
@@ -423,13 +423,14 @@ void uni_bt_bredr_on_l2cap_channel_opened(uint16_t channel, const uint8_t* packe
|
||||
status = l2cap_event_channel_opened_get_status(packet);
|
||||
if (status) {
|
||||
logi("L2CAP Connection failed: 0x%02x.\n", status);
|
||||
- // Practice showed that if the connection fails, just disconnect/remove
|
||||
- // so that the connection can start again.
|
||||
+ // Channel-open failures also include transient page timeouts when a
|
||||
+ // paired controller powers down or is temporarily unreachable. Keep
|
||||
+ // the persistent key so the controller can reconnect later. Users can
|
||||
+ // remove genuinely stale keys through the explicit pairing reset.
|
||||
if (status == L2CAP_CONNECTION_RESPONSE_RESULT_REFUSED_SECURITY) {
|
||||
logi("Probably GAP-security-related issues. Set GAP security to 2\n");
|
||||
}
|
||||
- logi("Removing key for device: %s.\n", bd_addr_to_str(address));
|
||||
- gap_drop_link_key_for_bd_addr(device->conn.btaddr);
|
||||
+ logi("Removing failed device instance for: %s; preserving link key.\n", bd_addr_to_str(address));
|
||||
uni_hid_device_disconnect(device);
|
||||
uni_hid_device_delete(device);
|
||||
/* 'device' is destroyed, don't use */
|
||||
diff --git a/src/components/bluepad32/include/parser/uni_hid_parser_psmove.h b/src/components/bluepad32/include/parser/uni_hid_parser_psmove.h
|
||||
index 6af4969..0aebb0a 100644
|
||||
--- a/src/components/bluepad32/include/parser/uni_hid_parser_psmove.h
|
||||
+++ b/src/components/bluepad32/include/parser/uni_hid_parser_psmove.h
|
||||
@@ -14,6 +14,8 @@
|
||||
void uni_hid_parser_psmove_setup(struct uni_hid_device_s* d);
|
||||
void uni_hid_parser_psmove_init_report(struct uni_hid_device_s* d);
|
||||
void uni_hid_parser_psmove_parse_input_report(struct uni_hid_device_s* d, const uint8_t* report, uint16_t len);
|
||||
+void uni_hid_parser_psmove_parse_feature_report(
|
||||
+ struct uni_hid_device_s* d, const uint8_t* report, uint16_t len);
|
||||
void uni_hid_parser_psmove_set_lightbar_color(struct uni_hid_device_s* d, uint8_t r, uint8_t g, uint8_t b);
|
||||
void uni_hid_parser_psmove_play_dual_rumble(struct uni_hid_device_s* d,
|
||||
uint16_t start_delay_ms,
|
||||
diff --git a/src/components/bluepad32/parser/uni_hid_parser_ds4.c b/src/components/bluepad32/parser/uni_hid_parser_ds4.c
|
||||
index ea063b8..7670caf 100644
|
||||
--- a/src/components/bluepad32/parser/uni_hid_parser_ds4.c
|
||||
|
|
@ -84,11 +120,149 @@ index a22ef26..3d5ecef 100644
|
|||
int32_t calib_data =
|
||||
mult_frac(ins->accel_calib_data[i].sens_numer, raw_data, ins->accel_calib_data[i].sens_denom);
|
||||
ctl->gamepad.accel[i] = calib_data;
|
||||
diff --git a/src/components/bluepad32/parser/uni_hid_parser_psmove.c b/src/components/bluepad32/parser/uni_hid_parser_psmove.c
|
||||
index 0265f93..5c0f2bb 100644
|
||||
--- a/src/components/bluepad32/parser/uni_hid_parser_psmove.c
|
||||
+++ b/src/components/bluepad32/parser/uni_hid_parser_psmove.c
|
||||
@@ -8,6 +8,7 @@
|
||||
*/
|
||||
|
||||
#include "parser/uni_hid_parser_psmove.h"
|
||||
+#include "parser/uni_hid_parser_imu.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
@@ -27,11 +28,6 @@ typedef enum psmove_fsm {
|
||||
PSMOVE_FSM_LED_UPDATED, // LED updated
|
||||
} psmove_fsm_t;
|
||||
|
||||
-typedef enum psmove_model {
|
||||
- PSMOVE_MODEL_UNK,
|
||||
- PSMOVE_MODEL_ZCM1,
|
||||
- PSMOVE_MODEL_ZCM2,
|
||||
-} psmove_model_t;
|
||||
|
||||
typedef enum {
|
||||
PSMOVE_STATE_RUMBLE_DISABLED,
|
||||
@@ -41,9 +37,10 @@ typedef enum {
|
||||
|
||||
// psmove_instance_t represents data used by the psmove driver instance.
|
||||
typedef struct psmove_instance_s {
|
||||
- psmove_model_t model;
|
||||
+ uni_psmove_imu_model_t model;
|
||||
psmove_fsm_t state;
|
||||
uint8_t led_rgb[3];
|
||||
+ uni_psmove_imu_calibration_t imu_calibration;
|
||||
|
||||
btstack_timer_source_t rumble_timer_duration;
|
||||
btstack_timer_source_t rumble_timer_delayed_start;
|
||||
@@ -127,6 +124,7 @@ static void psmove_send_output_report(uni_hid_device_t* d, psmove_output_report_
|
||||
static void on_psmove_set_rumble_on(btstack_timer_source_t* ts);
|
||||
static void on_psmove_set_rumble_off(btstack_timer_source_t* ts);
|
||||
static void psmove_play_dual_rumble_now(uni_hid_device_t* d, uint16_t duration_ms, uint8_t magnitude);
|
||||
+static void psmove_request_calibration_report(uni_hid_device_t* d);
|
||||
|
||||
void uni_hid_parser_psmove_init_report(uni_hid_device_t* d) {
|
||||
uni_controller_t* ctl = &d->controller;
|
||||
@@ -154,6 +152,7 @@ void uni_hid_parser_psmove_parse_input_report(uni_hid_device_t* d, const uint8_t
|
||||
}
|
||||
|
||||
uni_controller_t* ctl = &d->controller;
|
||||
+ psmove_instance_t* ins = get_psmove_instance(d);
|
||||
|
||||
// Buttons
|
||||
if (r->buttons[0] & 0x01)
|
||||
@@ -187,18 +186,39 @@ void uni_hid_parser_psmove_parse_input_report(uni_hid_device_t* d, const uint8_t
|
||||
|
||||
ctl->gamepad.throttle = r->trigger * 4;
|
||||
|
||||
- ctl->gamepad.accel[0] = r->accel_x;
|
||||
- ctl->gamepad.accel[1] = r->accel_y;
|
||||
- ctl->gamepad.accel[2] = r->accel_z;
|
||||
-
|
||||
- ctl->gamepad.gyro[0] = r->gyro_x;
|
||||
- ctl->gamepad.gyro[1] = r->gyro_y;
|
||||
- ctl->gamepad.gyro[2] = r->gyro_z;
|
||||
+ const uint16_t accel_first[3] = {r->accel_x, r->accel_y, r->accel_z};
|
||||
+ const uint16_t accel_second[3] = {
|
||||
+ r->accel_x2, r->accel_y2, r->accel_z2};
|
||||
+ const uint16_t gyro_first[3] = {r->gyro_x, r->gyro_y, r->gyro_z};
|
||||
+ const uint16_t gyro_second[3] = {
|
||||
+ r->gyro_x2, r->gyro_y2, r->gyro_z2};
|
||||
+ uni_imu_fixed_sample_t motion;
|
||||
+ if (uni_psmove_normalize_imu(
|
||||
+ ins->model, &ins->imu_calibration, accel_first, accel_second,
|
||||
+ gyro_first, gyro_second, &motion)) {
|
||||
+ memcpy(ctl->gamepad.accel, motion.accel, sizeof(motion.accel));
|
||||
+ memcpy(ctl->gamepad.gyro, motion.gyro, sizeof(motion.gyro));
|
||||
+ }
|
||||
|
||||
if (r->battery <= 5)
|
||||
ctl->battery = r->battery * 51;
|
||||
}
|
||||
|
||||
+void uni_hid_parser_psmove_parse_feature_report(
|
||||
+ uni_hid_device_t* d, const uint8_t* report, uint16_t len) {
|
||||
+ psmove_instance_t* ins = get_psmove_instance(d);
|
||||
+ const uni_psmove_calibration_result_t result =
|
||||
+ uni_psmove_add_calibration_report(
|
||||
+ &ins->imu_calibration, ins->model, report, len);
|
||||
+ if (result == UNI_PSMOVE_CALIBRATION_INCOMPLETE) {
|
||||
+ psmove_request_calibration_report(d);
|
||||
+ } else if (result == UNI_PSMOVE_CALIBRATION_COMPLETE) {
|
||||
+ logi("psmove: IMU calibration ready\n");
|
||||
+ } else if (result == UNI_PSMOVE_CALIBRATION_INVALID) {
|
||||
+ loge("psmove: invalid IMU calibration; motion disabled\n");
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
void uni_hid_parser_psmove_play_dual_rumble(struct uni_hid_device_s* d,
|
||||
uint16_t start_delay_ms,
|
||||
uint16_t duration_ms,
|
||||
@@ -261,25 +281,34 @@ void uni_hid_parser_psmove_setup(struct uni_hid_device_s* d) {
|
||||
|
||||
switch (d->product_id) {
|
||||
case ZCM1_PID:
|
||||
- ins->model = PSMOVE_MODEL_ZCM1;
|
||||
+ ins->model = UNI_PSMOVE_IMU_MODEL_ZCM1;
|
||||
logi("psmove: Detected ZCM1 model\n");
|
||||
break;
|
||||
case ZCM2_PID:
|
||||
- ins->model = PSMOVE_MODEL_ZCM2;
|
||||
+ ins->model = UNI_PSMOVE_IMU_MODEL_ZCM2;
|
||||
logi("psmove: Detected ZCM2 model\n");
|
||||
break;
|
||||
default:
|
||||
- loge("psmove: Unknown PSMove PID = %#x, assuming ZCM1\n", ins->model);
|
||||
- ins->model = PSMOVE_MODEL_ZCM1;
|
||||
+ loge("psmove: Unknown PSMove PID = %#x, assuming ZCM1\n", d->product_id);
|
||||
+ ins->model = UNI_PSMOVE_IMU_MODEL_ZCM1;
|
||||
break;
|
||||
}
|
||||
|
||||
+ psmove_request_calibration_report(d);
|
||||
uni_hid_device_set_ready_complete(d);
|
||||
}
|
||||
|
||||
//
|
||||
// Helpers
|
||||
//
|
||||
+static void psmove_request_calibration_report(uni_hid_device_t* d) {
|
||||
+ static const uint8_t report[] = {
|
||||
+ ((HID_MESSAGE_TYPE_GET_REPORT << 4) | HID_REPORT_TYPE_FEATURE),
|
||||
+ 0x10,
|
||||
+ };
|
||||
+ uni_hid_device_send_ctrl_report(d, report, sizeof(report));
|
||||
+}
|
||||
+
|
||||
static psmove_instance_t* get_psmove_instance(uni_hid_device_t* d) {
|
||||
return (psmove_instance_t*)&d->parser_data[0];
|
||||
}
|
||||
diff --git a/src/components/bluepad32/parser/uni_hid_parser_switch.c b/src/components/bluepad32/parser/uni_hid_parser_switch.c
|
||||
index 599fc35..c72f056 100644
|
||||
index 599fc35..9f073b4 100644
|
||||
--- a/src/components/bluepad32/parser/uni_hid_parser_switch.c
|
||||
+++ b/src/components/bluepad32/parser/uni_hid_parser_switch.c
|
||||
@@ -51,7 +51,8 @@ static const int16_t DEFAULT_ACCEL_OFFSET = 0;
|
||||
@@ -51,13 +51,15 @@ static const int16_t DEFAULT_ACCEL_OFFSET = 0;
|
||||
static const int16_t DEFAULT_ACCEL_SCALE = 16384;
|
||||
static const int16_t DEFAULT_GYRO_OFFSET = 0;
|
||||
static const int16_t DEFAULT_GYRO_SCALE = 13371;
|
||||
|
|
@ -98,7 +272,97 @@ index 599fc35..c72f056 100644
|
|||
|
||||
#define SWITCH_FACTORY_IMU_CAL_DATA_SIZE 24
|
||||
static const uint16_t SWITCH_FACTORY_IMU_CAL_DATA_ADDR = 0x6020;
|
||||
@@ -823,19 +824,26 @@ static void parse_imu(uni_hid_device_t* d, const struct switch_imu_data_s* r) {
|
||||
|
||||
#define SWITCH_DUMP_ROM_DATA_SIZE 24 // Max size is 24
|
||||
#define SWITCH_SETUP_TIMEOUT_MS 800
|
||||
+#define SWITCH_RUMBLE_REFRESH_MS 40
|
||||
#if ENABLE_SPI_FLASH_DUMP
|
||||
static const uint32_t SWITCH_DUMP_ROM_DATA_ADDR_START = 0x20000;
|
||||
static const uint32_t SWITCH_DUMP_ROM_DATA_ADDR_END = 0x30000;
|
||||
@@ -72,6 +74,7 @@ enum switch_state {
|
||||
STATE_READ_FACTORY_IMU_CALIBRATION, // Factory IMU calibration info
|
||||
STATE_SET_FULL_REPORT, // Request report 0x30
|
||||
STATE_ENABLE_IMU, // Enable/Disable gyro/accel
|
||||
+ STATE_ENABLE_RUMBLE, // Enable controller vibration
|
||||
STATE_DUMP_FLASH, // Dump SPI Flash memory
|
||||
STATE_UPDATE_LED, // Update LEDs
|
||||
STATE_READY, // Gamepad setup ready!
|
||||
@@ -111,6 +114,7 @@ enum switch_subcmd {
|
||||
SUBCMD_SPI_FLASH_READ = 0x10,
|
||||
SUBCMD_SET_PLAYER_LEDS = 0x30,
|
||||
SUBCMD_ENABLE_IMU = 0x40,
|
||||
+ SUBCMD_ENABLE_RUMBLE = 0x48,
|
||||
};
|
||||
|
||||
typedef enum {
|
||||
@@ -137,6 +141,7 @@ typedef struct switch_instance_s {
|
||||
// Although technically, we can use one timer for delay and duration, easier to debug/maintain if we have two.
|
||||
btstack_timer_source_t rumble_timer_duration;
|
||||
btstack_timer_source_t rumble_timer_delayed_start;
|
||||
+ btstack_timer_source_t rumble_timer_refresh;
|
||||
switch_state_rumble_t rumble_state;
|
||||
|
||||
btstack_timer_source_t setup_timer;
|
||||
@@ -322,6 +327,7 @@ static void fsm_read_user_stick_calibration(struct uni_hid_device_s* d);
|
||||
static void fsm_read_factory_imu_calibration(struct uni_hid_device_s* d);
|
||||
static void fsm_set_full_report(struct uni_hid_device_s* d);
|
||||
static void fsm_enable_imu(struct uni_hid_device_s* d);
|
||||
+static void fsm_enable_rumble(struct uni_hid_device_s* d);
|
||||
static void fsm_update_led(struct uni_hid_device_s* d);
|
||||
static void fsm_ready(struct uni_hid_device_s* d);
|
||||
static void process_reply_read_spi_dump(struct uni_hid_device_s* d, const uint8_t* data, int len);
|
||||
@@ -333,11 +339,16 @@ static void process_reply_set_report_mode(struct uni_hid_device_s* d, const stru
|
||||
static void process_reply_spi_flash_read(struct uni_hid_device_s* d, const struct switch_report_21_s* r, int len);
|
||||
static void process_reply_set_player_leds(struct uni_hid_device_s* d, const struct switch_report_21_s* r, int len);
|
||||
static void process_reply_enable_imu(struct uni_hid_device_s* d, const struct switch_report_21_s* r, int len);
|
||||
+static void process_reply_enable_rumble(struct uni_hid_device_s* d, const struct switch_report_21_s* r, int len);
|
||||
static int32_t calibrate_axis(int32_t v, switch_cal_stick_t cal);
|
||||
static void set_led(uni_hid_device_t* d, uint8_t leds);
|
||||
static void on_switch_set_rumble_on(btstack_timer_source_t* ts);
|
||||
static void on_switch_set_rumble_off(btstack_timer_source_t* ts);
|
||||
+static void on_switch_refresh_rumble(btstack_timer_source_t* ts);
|
||||
static void switch_stop_rumble_now(uni_hid_device_t* d);
|
||||
+static void switch_send_dual_rumble_now(uni_hid_device_t* d,
|
||||
+ uint8_t weak_magnitude,
|
||||
+ uint8_t strong_magnitude);
|
||||
static void switch_play_dual_rumble_now(uni_hid_device_t* d,
|
||||
uint16_t duration_ms,
|
||||
uint8_t weak_magnitude,
|
||||
@@ -451,6 +462,10 @@ static void process_fsm(struct uni_hid_device_s* d) {
|
||||
break;
|
||||
case STATE_ENABLE_IMU:
|
||||
logd("STATE_ENABLE_IMU\n");
|
||||
+ fsm_enable_rumble(d);
|
||||
+ break;
|
||||
+ case STATE_ENABLE_RUMBLE:
|
||||
+ logd("STATE_ENABLE_RUMBLE\n");
|
||||
fsm_dump_rom(d);
|
||||
break;
|
||||
case STATE_DUMP_FLASH:
|
||||
@@ -725,6 +740,12 @@ static void process_reply_enable_imu(struct uni_hid_device_s* d, const struct sw
|
||||
ARG_UNUSED(r);
|
||||
ARG_UNUSED(len);
|
||||
}
|
||||
+static void process_reply_enable_rumble(struct uni_hid_device_s* d, const struct switch_report_21_s* r, int len) {
|
||||
+ ARG_UNUSED(d);
|
||||
+ ARG_UNUSED(r);
|
||||
+ ARG_UNUSED(len);
|
||||
+}
|
||||
+
|
||||
|
||||
// Process 0x21 input report: SWITCH_INPUT_SUBCMD_REPLY
|
||||
static void process_input_subcmd_reply(struct uni_hid_device_s* d, const uint8_t* report, int len) {
|
||||
@@ -752,6 +773,9 @@ static void process_input_subcmd_reply(struct uni_hid_device_s* d, const uint8_t
|
||||
case SUBCMD_ENABLE_IMU:
|
||||
process_reply_enable_imu(d, r, len);
|
||||
break;
|
||||
+ case SUBCMD_ENABLE_RUMBLE:
|
||||
+ process_reply_enable_rumble(d, r, len);
|
||||
+ break;
|
||||
default:
|
||||
loge("Switch: Error, unexpected subcmd_id=0x%02x in report 0x21\n", r->subcmd_id);
|
||||
break;
|
||||
@@ -823,19 +847,26 @@ static void parse_imu(uni_hid_device_t* d, const struct switch_imu_data_s* r) {
|
||||
switch_instance_t* ins = get_switch_instance(d);
|
||||
uni_controller_t* ctl = &d->controller;
|
||||
|
||||
|
|
@ -134,7 +398,7 @@ index 599fc35..c72f056 100644
|
|||
if (ins->controller_type == SWITCH_CONTROLLER_TYPE_JCR) {
|
||||
accel[1] = -accel[1];
|
||||
accel[2] = -accel[2];
|
||||
@@ -843,10 +851,13 @@ static void parse_imu(uni_hid_device_t* d, const struct switch_imu_data_s* r) {
|
||||
@@ -843,10 +874,13 @@ static void parse_imu(uni_hid_device_t* d, const struct switch_imu_data_s* r) {
|
||||
gyro[2] = -gyro[2];
|
||||
}
|
||||
|
||||
|
|
@ -152,3 +416,153 @@ index 599fc35..c72f056 100644
|
|||
}
|
||||
|
||||
// Process 0x30 input report: SWITCH_INPUT_IMU_DATA
|
||||
@@ -1172,6 +1206,18 @@ static void fsm_enable_imu(struct uni_hid_device_s* d) {
|
||||
req->data[0] = (ins->mode == SWITCH_MODE_IMU);
|
||||
send_subcmd(d, req, sizeof(out));
|
||||
}
|
||||
+static void fsm_enable_rumble(struct uni_hid_device_s* d) {
|
||||
+ switch_instance_t* ins = get_switch_instance(d);
|
||||
+ ins->state = STATE_ENABLE_RUMBLE;
|
||||
+
|
||||
+ uint8_t out[sizeof(struct switch_subcmd_request) + 1] = {0};
|
||||
+ struct switch_subcmd_request* req = (struct switch_subcmd_request*)&out[0];
|
||||
+ req->report_id = OUTPUT_RUMBLE_AND_SUBCMD;
|
||||
+ req->subcmd_id = SUBCMD_ENABLE_RUMBLE;
|
||||
+ req->data[0] = 0x01;
|
||||
+ send_subcmd(d, req, sizeof(out));
|
||||
+}
|
||||
+
|
||||
|
||||
static void fsm_update_led(struct uni_hid_device_s* d) {
|
||||
switch_instance_t* ins = get_switch_instance(d);
|
||||
@@ -1203,6 +1249,10 @@ static struct switch_rumble_freq_data find_rumble_freq(uint16_t freq) {
|
||||
return rumble_freqs[i];
|
||||
}
|
||||
|
||||
+static uint16_t switch_magnitude_to_amp(uint8_t magnitude) {
|
||||
+ return (uint16_t)(((uint32_t)magnitude * 1003 + 127) / 255);
|
||||
+}
|
||||
+
|
||||
static struct switch_rumble_amp_data find_rumble_amp(uint16_t amp) {
|
||||
unsigned int i = 0;
|
||||
if (amp > rumble_amps[0].amp) {
|
||||
@@ -1259,6 +1309,7 @@ void uni_hid_parser_switch_play_dual_rumble(struct uni_hid_device_s* d,
|
||||
break;
|
||||
case SWITCH_STATE_RUMBLE_IN_PROGRESS:
|
||||
btstack_run_loop_remove_timer(&ins->rumble_timer_duration);
|
||||
+ btstack_run_loop_remove_timer(&ins->rumble_timer_refresh);
|
||||
break;
|
||||
default:
|
||||
// Do nothing
|
||||
@@ -1366,6 +1417,7 @@ static void switch_stop_rumble_now(uni_hid_device_t* d) {
|
||||
|
||||
// No need to protect it with a mutex since it runs in the same main thread
|
||||
assert(ins->rumble_state == SWITCH_STATE_RUMBLE_IN_PROGRESS);
|
||||
+ btstack_run_loop_remove_timer(&ins->rumble_timer_refresh);
|
||||
ins->rumble_state = SWITCH_STATE_RUMBLE_DISABLED;
|
||||
|
||||
struct switch_subcmd_request req = {0};
|
||||
@@ -1379,6 +1431,22 @@ static void switch_stop_rumble_now(uni_hid_device_t* d) {
|
||||
send_subcmd(d, (struct switch_subcmd_request*)&req, sizeof(req) - 1);
|
||||
}
|
||||
|
||||
+static void switch_send_dual_rumble_now(uni_hid_device_t* d,
|
||||
+ uint8_t weak_magnitude,
|
||||
+ uint8_t strong_magnitude) {
|
||||
+ struct switch_subcmd_request req = {
|
||||
+ .report_id = OUTPUT_RUMBLE_ONLY,
|
||||
+ };
|
||||
+ // Fixed frequencies match the standard Switch LRA envelope and the
|
||||
+ // 8BitDo Switch-mode implementation. Magnitudes control amplitude only.
|
||||
+ switch_encode_rumble(req.rumble_left, 453, 135,
|
||||
+ switch_magnitude_to_amp(weak_magnitude));
|
||||
+ switch_encode_rumble(req.rumble_right, 453, 99,
|
||||
+ switch_magnitude_to_amp(strong_magnitude));
|
||||
+ // Rumble request don't include the last byte of "switch_subcmd_request": subcmd_id
|
||||
+ send_subcmd(d, &req, sizeof(req) - 1);
|
||||
+}
|
||||
+
|
||||
static void switch_play_dual_rumble_now(uni_hid_device_t* d,
|
||||
uint16_t duration_ms,
|
||||
uint8_t weak_magnitude,
|
||||
@@ -1391,14 +1459,17 @@ static void switch_play_dual_rumble_now(uni_hid_device_t* d,
|
||||
return;
|
||||
}
|
||||
|
||||
- struct switch_subcmd_request req = {
|
||||
- .report_id = OUTPUT_RUMBLE_ONLY,
|
||||
- };
|
||||
- switch_encode_rumble(req.rumble_left, weak_magnitude << 2, weak_magnitude, 500);
|
||||
- switch_encode_rumble(req.rumble_right, strong_magnitude << 2, strong_magnitude, 500);
|
||||
+ ins->rumble_weak_magnitude = weak_magnitude;
|
||||
+ ins->rumble_strong_magnitude = strong_magnitude;
|
||||
+ switch_send_dual_rumble_now(d, weak_magnitude, strong_magnitude);
|
||||
|
||||
- // Rumble request don't include the last byte of "switch_subcmd_request": subcmd_id
|
||||
- send_subcmd(d, &req, sizeof(req) - 1);
|
||||
+ // Refresh active rumble for Switch-compatible controllers that do not
|
||||
+ // retain a single output packet, including 8BitDo Switch mode.
|
||||
+ ins->rumble_timer_refresh.process = &on_switch_refresh_rumble;
|
||||
+ ins->rumble_timer_refresh.context = d;
|
||||
+ btstack_run_loop_set_timer(&ins->rumble_timer_refresh,
|
||||
+ SWITCH_RUMBLE_REFRESH_MS);
|
||||
+ btstack_run_loop_add_timer(&ins->rumble_timer_refresh);
|
||||
|
||||
// Set timer to turn off rumble
|
||||
ins->rumble_timer_duration.process = &on_switch_set_rumble_off;
|
||||
@@ -1414,6 +1485,20 @@ static void on_switch_set_rumble_on(btstack_timer_source_t* ts) {
|
||||
|
||||
switch_play_dual_rumble_now(d, ins->rumble_duration_ms, ins->rumble_weak_magnitude, ins->rumble_strong_magnitude);
|
||||
}
|
||||
+static void on_switch_refresh_rumble(btstack_timer_source_t* ts) {
|
||||
+ uni_hid_device_t* d = btstack_run_loop_get_timer_context(ts);
|
||||
+ switch_instance_t* ins = get_switch_instance(d);
|
||||
+ if (ins->rumble_state != SWITCH_STATE_RUMBLE_IN_PROGRESS) {
|
||||
+ return;
|
||||
+ }
|
||||
+ switch_send_dual_rumble_now(
|
||||
+ d, (uint8_t)ins->rumble_weak_magnitude,
|
||||
+ (uint8_t)ins->rumble_strong_magnitude);
|
||||
+ btstack_run_loop_set_timer(&ins->rumble_timer_refresh,
|
||||
+ SWITCH_RUMBLE_REFRESH_MS);
|
||||
+ btstack_run_loop_add_timer(&ins->rumble_timer_refresh);
|
||||
+}
|
||||
+
|
||||
|
||||
static void on_switch_set_rumble_off(btstack_timer_source_t* ts) {
|
||||
uni_hid_device_t* d = btstack_run_loop_get_timer_context(ts);
|
||||
diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/components/bluepad32/parser/uni_hid_parser_wii.c
|
||||
index be2103e..4819639 100644
|
||||
--- a/src/components/bluepad32/parser/uni_hid_parser_wii.c
|
||||
+++ b/src/components/bluepad32/parser/uni_hid_parser_wii.c
|
||||
@@ -19,6 +19,7 @@
|
||||
#endif // ENABLE_EEPROM_DUMP
|
||||
|
||||
#include "parser/uni_hid_parser_wii.h"
|
||||
+#include "parser/uni_hid_parser_imu.h"
|
||||
|
||||
#include "controller/uni_controller.h"
|
||||
#include "hid_usage.h"
|
||||
@@ -585,9 +586,7 @@ static void process_drm_ka(uni_hid_device_t* d, const uint8_t* report, uint16_t
|
||||
|
||||
uni_controller_t* ctl = &d->controller;
|
||||
|
||||
- ctl->gamepad.accel[0] = sx;
|
||||
- ctl->gamepad.accel[1] = sy;
|
||||
- ctl->gamepad.accel[2] = sz;
|
||||
+ uni_imu_normalize_wii_accel(sx, sy, sz, ctl->gamepad.accel);
|
||||
|
||||
// Dpad works as dpad, useful to navigate menus.
|
||||
ctl->gamepad.dpad |= (report[1] & 0x01) ? DPAD_DOWN : 0;
|
||||
diff --git a/src/components/bluepad32/uni_hid_device.c b/src/components/bluepad32/uni_hid_device.c
|
||||
index 67841e8..9fe7134 100644
|
||||
--- a/src/components/bluepad32/uni_hid_device.c
|
||||
+++ b/src/components/bluepad32/uni_hid_device.c
|
||||
@@ -655,6 +655,7 @@ void uni_hid_device_guess_controller_type_from_pid_vid(uni_hid_device_t* d) {
|
||||
d->report_parser.setup = uni_hid_parser_psmove_setup;
|
||||
d->report_parser.init_report = uni_hid_parser_psmove_init_report;
|
||||
d->report_parser.parse_input_report = uni_hid_parser_psmove_parse_input_report;
|
||||
+ d->report_parser.parse_feature_report = uni_hid_parser_psmove_parse_feature_report;
|
||||
d->report_parser.set_lightbar_color = uni_hid_parser_psmove_set_lightbar_color;
|
||||
d->report_parser.play_dual_rumble = uni_hid_parser_psmove_play_dual_rumble;
|
||||
logi("Device detected as PS Move: 0x%02x\n", type);
|
||||
|
|
|
|||
|
|
@ -14,11 +14,13 @@ dependencies = [
|
|||
"PySDL3",
|
||||
"rich",
|
||||
"hidapi",
|
||||
"pyusb",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
controller-uart-bridge = "switch_pico_bridge.controller_uart_bridge:main"
|
||||
host-uart-logger = "switch_pico_bridge.host_uart_logger:main"
|
||||
switch-pico-pairings = "switch_pico_bridge.pairing_manager:main"
|
||||
|
||||
[tool.setuptools]
|
||||
package-dir = {"" = "src"}
|
||||
|
|
|
|||
292
src/switch_pico_bridge/pairing_manager.py
Executable file
292
src/switch_pico_bridge/pairing_manager.py
Executable file
|
|
@ -0,0 +1,292 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Manage Pico 2 W Bluetooth pairings over vendor requests on USB EP0."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import struct
|
||||
import sys
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from collections.abc import Iterable, Sequence
|
||||
from typing import Any, Protocol
|
||||
|
||||
import usb.core
|
||||
|
||||
USB_VENDOR_ID = 0x057E
|
||||
USB_PRODUCT_ID = 0x2009
|
||||
REQUEST_CLEAR = 0x50
|
||||
REQUEST_GET = 0x51
|
||||
REQUEST_REFRESH = 0x52
|
||||
REQUEST_VALUE = 0x5350
|
||||
REQUEST_INDEX = 0x4D47
|
||||
PROTOCOL_VERSION = 1
|
||||
RESPONSE_HEADER_SIZE = 12
|
||||
RECORD_SIZE = 8
|
||||
RECORD_CAPACITY = 16
|
||||
MAXIMUM_RESPONSE_SIZE = RESPONSE_HEADER_SIZE + RECORD_CAPACITY * RECORD_SIZE
|
||||
STATUS_READY = 0
|
||||
STATUS_PENDING = 1
|
||||
TRANSPORT_CLASSIC = 1
|
||||
TRANSPORT_BLE = 2
|
||||
USB_TIMEOUT_MS = 1000
|
||||
|
||||
|
||||
class PairingManagerError(RuntimeError):
|
||||
"""Expected discovery, USB transport, or protocol failure."""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PairingRecord:
|
||||
transport: int
|
||||
address_type: int
|
||||
address: bytes
|
||||
|
||||
@property
|
||||
def address_text(self) -> str:
|
||||
return ":".join(f"{octet:02X}" for octet in self.address)
|
||||
|
||||
@property
|
||||
def transport_text(self) -> str:
|
||||
if self.transport == TRANSPORT_CLASSIC:
|
||||
return "Classic"
|
||||
if self.transport == TRANSPORT_BLE:
|
||||
address_types = {
|
||||
0: "public",
|
||||
1: "random",
|
||||
2: "public identity",
|
||||
3: "random identity",
|
||||
}
|
||||
suffix = address_types.get(
|
||||
self.address_type, f"type {self.address_type}"
|
||||
)
|
||||
return f"BLE ({suffix})"
|
||||
return f"unknown transport {self.transport}"
|
||||
|
||||
|
||||
class UsbDevice(Protocol):
|
||||
bus: int | None
|
||||
address: int | None
|
||||
|
||||
def ctrl_transfer(
|
||||
self,
|
||||
bm_request_type: int,
|
||||
request: int,
|
||||
value: int = 0,
|
||||
index: int = 0,
|
||||
data_or_w_length: Any = None,
|
||||
timeout: int | None = None,
|
||||
) -> Any:
|
||||
...
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PairingSnapshot:
|
||||
generation: int
|
||||
status: int
|
||||
overflow: bool
|
||||
records: tuple[PairingRecord, ...]
|
||||
|
||||
|
||||
def parse_snapshot(payload: bytes) -> PairingSnapshot:
|
||||
if len(payload) < RESPONSE_HEADER_SIZE:
|
||||
raise PairingManagerError("short pairing-management response")
|
||||
if payload[:4] != b"SPPM":
|
||||
raise PairingManagerError("device does not implement pairing management")
|
||||
if payload[4] != PROTOCOL_VERSION:
|
||||
raise PairingManagerError(
|
||||
f"unsupported pairing protocol version {payload[4]}"
|
||||
)
|
||||
|
||||
status = payload[5]
|
||||
record_count = payload[6]
|
||||
required = RESPONSE_HEADER_SIZE + record_count * RECORD_SIZE
|
||||
if record_count > RECORD_CAPACITY or len(payload) < required:
|
||||
raise PairingManagerError("invalid pairing record count")
|
||||
|
||||
generation = int(struct.unpack_from("<I", payload, 8)[0])
|
||||
records: list[PairingRecord] = []
|
||||
offset = RESPONSE_HEADER_SIZE
|
||||
for _ in range(record_count):
|
||||
records.append(
|
||||
PairingRecord(
|
||||
transport=payload[offset],
|
||||
address_type=payload[offset + 1],
|
||||
address=bytes(payload[offset + 2 : offset + 8]),
|
||||
)
|
||||
)
|
||||
offset += RECORD_SIZE
|
||||
return PairingSnapshot(
|
||||
generation=generation,
|
||||
status=status,
|
||||
overflow=bool(payload[7] & 1),
|
||||
records=tuple(records),
|
||||
)
|
||||
|
||||
|
||||
def _control_in(device: UsbDevice) -> bytes:
|
||||
payload = device.ctrl_transfer(
|
||||
0xC0,
|
||||
REQUEST_GET,
|
||||
REQUEST_VALUE,
|
||||
REQUEST_INDEX,
|
||||
MAXIMUM_RESPONSE_SIZE,
|
||||
timeout=USB_TIMEOUT_MS,
|
||||
)
|
||||
return bytes(payload)
|
||||
|
||||
|
||||
def _control_out(device: UsbDevice, request: int) -> None:
|
||||
device.ctrl_transfer(
|
||||
0x40,
|
||||
request,
|
||||
REQUEST_VALUE,
|
||||
REQUEST_INDEX,
|
||||
None,
|
||||
timeout=USB_TIMEOUT_MS,
|
||||
)
|
||||
|
||||
|
||||
def read_snapshot(device: UsbDevice) -> PairingSnapshot:
|
||||
return parse_snapshot(_control_in(device))
|
||||
|
||||
|
||||
def wait_for_snapshot(
|
||||
device: UsbDevice, previous_generation: int, timeout: float
|
||||
) -> PairingSnapshot:
|
||||
deadline = time.monotonic() + timeout
|
||||
while time.monotonic() < deadline:
|
||||
snapshot = read_snapshot(device)
|
||||
if (
|
||||
snapshot.status == STATUS_READY
|
||||
and snapshot.generation != previous_generation
|
||||
):
|
||||
return snapshot
|
||||
time.sleep(0.05)
|
||||
raise PairingManagerError("Pico did not finish the pairing operation")
|
||||
|
||||
|
||||
def refresh_snapshot(device: UsbDevice, timeout: float) -> PairingSnapshot:
|
||||
initial = read_snapshot(device)
|
||||
_control_out(device, REQUEST_REFRESH)
|
||||
return wait_for_snapshot(device, initial.generation, timeout)
|
||||
|
||||
|
||||
def clear_pairings(device: UsbDevice, timeout: float) -> PairingSnapshot:
|
||||
initial = read_snapshot(device)
|
||||
_control_out(device, REQUEST_CLEAR)
|
||||
snapshot = wait_for_snapshot(device, initial.generation, timeout)
|
||||
if snapshot.records:
|
||||
raise PairingManagerError("Pico reported pairings after clear completed")
|
||||
return snapshot
|
||||
|
||||
|
||||
def _candidate_devices() -> Iterable[UsbDevice]:
|
||||
devices = usb.core.find(
|
||||
find_all=True,
|
||||
idVendor=USB_VENDOR_ID,
|
||||
idProduct=USB_PRODUCT_ID,
|
||||
)
|
||||
return () if devices is None else devices
|
||||
|
||||
|
||||
def find_pico(
|
||||
bus: int | None, address: int | None, timeout: float = 3.0
|
||||
) -> UsbDevice:
|
||||
deadline = time.monotonic() + timeout
|
||||
failures: list[Exception] = []
|
||||
while True:
|
||||
matches: list[UsbDevice] = []
|
||||
for device in _candidate_devices():
|
||||
if bus is not None and getattr(device, "bus", None) != bus:
|
||||
continue
|
||||
if address is not None and getattr(device, "address", None) != address:
|
||||
continue
|
||||
try:
|
||||
_ = read_snapshot(device)
|
||||
except (PairingManagerError, usb.core.USBError) as exc:
|
||||
failures.append(exc)
|
||||
continue
|
||||
matches.append(device)
|
||||
|
||||
if len(matches) == 1:
|
||||
return matches[0]
|
||||
if len(matches) > 1:
|
||||
locations = ", ".join(
|
||||
f"{device.bus}:{device.address}" for device in matches
|
||||
)
|
||||
raise PairingManagerError(
|
||||
f"multiple switch-pico devices found ({locations}); "
|
||||
"select one with --bus and --address"
|
||||
)
|
||||
if time.monotonic() >= deadline:
|
||||
break
|
||||
time.sleep(0.05)
|
||||
|
||||
if failures:
|
||||
raise PairingManagerError(
|
||||
"matching USB devices were found, but none accepted the "
|
||||
f"management request; last error: {failures[-1]}"
|
||||
) from failures[-1]
|
||||
raise PairingManagerError("no USB-connected switch-pico AIO firmware found")
|
||||
|
||||
|
||||
def _print_snapshot(snapshot: PairingSnapshot) -> None:
|
||||
if not snapshot.records:
|
||||
print("No stored pairings.")
|
||||
return
|
||||
for index, record in enumerate(snapshot.records, start=1):
|
||||
print(f"{index}: {record.transport_text} {record.address_text}")
|
||||
if snapshot.overflow:
|
||||
print("Warning: additional pairings did not fit in the response.")
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="switch-pico-pairings",
|
||||
description="List or clear switch-pico AIO Bluetooth pairings.",
|
||||
)
|
||||
parser.add_argument("--bus", type=int, help="USB bus number")
|
||||
parser.add_argument("--address", type=int, help="USB device address")
|
||||
parser.add_argument(
|
||||
"--timeout", type=float, default=3.0,
|
||||
help="operation timeout in seconds (default: 3)",
|
||||
)
|
||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||
subparsers.add_parser("list", help="list stored Classic and BLE pairings")
|
||||
clear_parser = subparsers.add_parser("clear", help="clear all pairings")
|
||||
clear_parser.add_argument(
|
||||
"--yes", action="store_true",
|
||||
help="confirm destructive clearing without prompting",
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
def main(argv: Sequence[str] | None = None) -> int:
|
||||
args = build_parser().parse_args(argv)
|
||||
if args.timeout <= 0:
|
||||
print("error: --timeout must be positive", file=sys.stderr)
|
||||
return 2
|
||||
if args.command == "clear" and not args.yes:
|
||||
print("error: clear requires --yes", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
try:
|
||||
device = find_pico(args.bus, args.address, args.timeout)
|
||||
if args.command == "list":
|
||||
_print_snapshot(refresh_snapshot(device, args.timeout))
|
||||
else:
|
||||
before = refresh_snapshot(device, args.timeout)
|
||||
clear_pairings(device, args.timeout)
|
||||
print(f"Cleared {len(before.records)} stored pairing(s).")
|
||||
except PairingManagerError as exc:
|
||||
print(f"error: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
except usb.core.USBError as exc:
|
||||
print(f"error: USB access failed: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -7,6 +7,7 @@
|
|||
#include "hardware/uart.h"
|
||||
#else
|
||||
#include "bluepad32_input_backend.h"
|
||||
#include "bootsel_pairing_button.h"
|
||||
#endif
|
||||
|
||||
#ifdef SWITCH_PICO_LOG
|
||||
|
|
@ -25,11 +26,19 @@
|
|||
#define UART_RUMBLE_TYPE 0x02
|
||||
#endif
|
||||
|
||||
static bool g_last_mounted = false;
|
||||
#ifdef SWITCH_PICO_BLUEPAD32
|
||||
static_assert(SWITCH_PICO_HID_INSTANCE_COUNT ==
|
||||
BLUEPAD32_INPUT_BACKEND_SLOT_COUNT);
|
||||
static bool g_last_ready[BLUEPAD32_INPUT_BACKEND_SLOT_COUNT]{};
|
||||
static SwitchInputState
|
||||
g_user_states[BLUEPAD32_INPUT_BACKEND_SLOT_COUNT]{};
|
||||
#else
|
||||
static constexpr uint8_t SWITCH_HID_INSTANCE = 0;
|
||||
static bool g_last_ready = false;
|
||||
|
||||
// Track the latest state provided by UART or the autopilot.
|
||||
static SwitchInputState g_user_state;
|
||||
#endif
|
||||
|
||||
static bool g_last_mounted = false;
|
||||
|
||||
#ifndef SWITCH_PICO_BLUEPAD32
|
||||
static void init_uart_input() {
|
||||
|
|
@ -66,10 +75,17 @@ static void send_rumble_uart_frame(const SwitchRumbleOutput& rumble) {
|
|||
}
|
||||
#endif
|
||||
|
||||
static void on_rumble_from_switch(const SwitchRumbleOutput& rumble) {
|
||||
static void on_rumble_from_switch(uint8_t instance,
|
||||
const SwitchRumbleOutput& rumble) {
|
||||
#ifdef SWITCH_PICO_BLUEPAD32
|
||||
bluepad32_input_backend_queue_rumble(rumble);
|
||||
if (instance >= BLUEPAD32_INPUT_BACKEND_SLOT_COUNT) {
|
||||
return;
|
||||
}
|
||||
bluepad32_input_backend_queue_rumble(instance, rumble);
|
||||
#else
|
||||
if (instance != SWITCH_HID_INSTANCE) {
|
||||
return;
|
||||
}
|
||||
send_rumble_uart_frame(rumble);
|
||||
#endif
|
||||
}
|
||||
|
|
@ -118,7 +134,7 @@ static bool poll_uart_frames() {
|
|||
|
||||
if (expected_len > 0 && index >= expected_len) {
|
||||
SwitchInputState parsed{};
|
||||
if (switch_pro_apply_uart_packet(buffer, expected_len, &parsed)) {
|
||||
if (switch_pro_apply_uart_packet(buffer, expected_len, parsed)) {
|
||||
g_user_state = parsed;
|
||||
new_data = true;
|
||||
LOG_PRINTF("[UART] packet buttons=0x%04x hat=%u lx=%u ly=%u rx=%u ry=%u\n",
|
||||
|
|
@ -153,16 +169,29 @@ static bool poll_uart_frames() {
|
|||
|
||||
static void log_usb_state() {
|
||||
bool mounted = tud_mounted();
|
||||
bool ready = switch_pro_is_ready();
|
||||
|
||||
if (mounted != g_last_mounted) {
|
||||
g_last_mounted = mounted;
|
||||
LOG_PRINTF("[USB] %s\n", mounted ? "mounted" : "unmounted");
|
||||
}
|
||||
|
||||
#ifdef SWITCH_PICO_BLUEPAD32
|
||||
for (uint8_t instance = 0;
|
||||
instance < BLUEPAD32_INPUT_BACKEND_SLOT_COUNT; ++instance) {
|
||||
const bool ready = switch_pro_is_ready(instance);
|
||||
if (ready != g_last_ready[instance]) {
|
||||
g_last_ready[instance] = ready;
|
||||
LOG_PRINTF("[SWITCH %u] driver %s\n", instance,
|
||||
ready ? "ready (handshake OK)" : "not ready");
|
||||
}
|
||||
}
|
||||
#else
|
||||
const bool ready = switch_pro_is_ready(SWITCH_HID_INSTANCE);
|
||||
if (ready != g_last_ready) {
|
||||
g_last_ready = ready;
|
||||
LOG_PRINTF("[SWITCH] driver %s\n", ready ? "ready (handshake OK)" : "not ready");
|
||||
LOG_PRINTF("[SWITCH] driver %s\n",
|
||||
ready ? "ready (handshake OK)" : "not ready");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
int main() {
|
||||
|
|
@ -176,10 +205,21 @@ int main() {
|
|||
#endif
|
||||
|
||||
tusb_init();
|
||||
switch_pro_init();
|
||||
switch_pro_set_rumble_callback(on_rumble_from_switch);
|
||||
#ifdef SWITCH_PICO_BLUEPAD32
|
||||
for (uint8_t instance = 0;
|
||||
instance < BLUEPAD32_INPUT_BACKEND_SLOT_COUNT; ++instance) {
|
||||
switch_pro_init(instance);
|
||||
switch_pro_set_rumble_callback(instance, on_rumble_from_switch);
|
||||
g_user_states[instance] = neutral_input();
|
||||
switch_pro_set_input(instance, g_user_states[instance]);
|
||||
}
|
||||
#else
|
||||
switch_pro_init(SWITCH_HID_INSTANCE);
|
||||
switch_pro_set_rumble_callback(SWITCH_HID_INSTANCE,
|
||||
on_rumble_from_switch);
|
||||
g_user_state = neutral_input();
|
||||
switch_pro_set_input(g_user_state);
|
||||
switch_pro_set_input(SWITCH_HID_INSTANCE, g_user_state);
|
||||
#endif
|
||||
|
||||
#ifdef SWITCH_PICO_BLUEPAD32
|
||||
bluepad32_input_backend_start();
|
||||
|
|
@ -193,19 +233,31 @@ int main() {
|
|||
while (true) {
|
||||
tud_task(); // USB device tasks
|
||||
#ifdef SWITCH_PICO_BLUEPAD32
|
||||
bluepad32_input_backend_snapshot(&g_user_state);
|
||||
switch (bootsel_pairing_button_task()) {
|
||||
case BootselPairingButtonEvent::kOpenPairing:
|
||||
bluepad32_input_backend_open_pairing_window();
|
||||
break;
|
||||
case BootselPairingButtonEvent::kClearPairings:
|
||||
bluepad32_input_backend_clear_pairings();
|
||||
break;
|
||||
case BootselPairingButtonEvent::kNone:
|
||||
break;
|
||||
}
|
||||
for (uint8_t instance = 0;
|
||||
instance < BLUEPAD32_INPUT_BACKEND_SLOT_COUNT; ++instance) {
|
||||
bluepad32_input_backend_snapshot(instance,
|
||||
&g_user_states[instance]);
|
||||
switch_pro_set_input(instance, g_user_states[instance]);
|
||||
if (switch_pro_task(instance)) {
|
||||
bluepad32_input_backend_report_sent(instance);
|
||||
}
|
||||
}
|
||||
#else
|
||||
bool new_data = poll_uart_frames(); // Pull controller state from UART1
|
||||
(void)new_data;
|
||||
#endif
|
||||
SwitchInputState state = g_user_state;
|
||||
switch_pro_set_input(state);
|
||||
#ifdef SWITCH_PICO_BLUEPAD32
|
||||
if (switch_pro_task()) {
|
||||
bluepad32_input_backend_report_sent();
|
||||
}
|
||||
#else
|
||||
(void)switch_pro_task();
|
||||
switch_pro_set_input(SWITCH_HID_INSTANCE, state);
|
||||
(void)switch_pro_task(SWITCH_HID_INSTANCE);
|
||||
#endif
|
||||
log_usb_state();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,14 @@
|
|||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#ifndef SWITCH_PICO_HID_INSTANCE_COUNT
|
||||
#define SWITCH_PICO_HID_INSTANCE_COUNT 1
|
||||
#endif
|
||||
|
||||
#if SWITCH_PICO_HID_INSTANCE_COUNT < 1 || SWITCH_PICO_HID_INSTANCE_COUNT > 4
|
||||
#error "SWITCH_PICO_HID_INSTANCE_COUNT must be between 1 and 4"
|
||||
#endif
|
||||
|
||||
|
||||
#define SWITCH_PRO_ENDPOINT_SIZE 64
|
||||
|
||||
|
|
@ -81,7 +89,7 @@ typedef enum {
|
|||
GET_VOLTAGE = 0x50,
|
||||
} SwitchCommands;
|
||||
|
||||
typedef struct {
|
||||
struct SwitchAnalog {
|
||||
uint8_t data[3];
|
||||
|
||||
void setX(uint16_t x) {
|
||||
|
|
@ -101,10 +109,10 @@ typedef struct {
|
|||
uint16_t getY() {
|
||||
return static_cast<uint16_t>((data[1] >> 4)) | (data[2] << 4);
|
||||
}
|
||||
} SwitchAnalog;
|
||||
};
|
||||
|
||||
// left and right calibration are stored differently for some reason, so two structs
|
||||
typedef struct {
|
||||
struct SwitchLeftCalibration {
|
||||
uint8_t data[9];
|
||||
|
||||
void getMin(uint16_t& x, uint16_t& y) const { packCalib(6, x, y); }
|
||||
|
|
@ -137,9 +145,9 @@ typedef struct {
|
|||
x = static_cast<uint16_t>(data[offset]) | ((data[offset + 1] & 0x0F) << 8);
|
||||
y = static_cast<uint16_t>(data[offset + 2] << 4) | (data[offset + 1] >> 4);
|
||||
}
|
||||
} SwitchLeftCalibration;
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
struct SwitchRightCalibration {
|
||||
uint8_t data[9];
|
||||
|
||||
void getMin(uint16_t& x, uint16_t& y) const { packCalib(3, x, y); }
|
||||
|
|
@ -172,7 +180,7 @@ typedef struct {
|
|||
x = static_cast<uint16_t>(data[offset]) | ((data[offset + 1] & 0x0F) << 8);
|
||||
y = static_cast<uint16_t>(data[offset + 2] << 4) | (data[offset + 1] >> 4);
|
||||
}
|
||||
} SwitchRightCalibration;
|
||||
};
|
||||
|
||||
typedef struct
|
||||
{
|
||||
|
|
@ -369,8 +377,19 @@ static const uint8_t switch_pro_configuration_descriptor[] =
|
|||
{
|
||||
0x09, // bLength
|
||||
0x02, // bDescriptorType (Configuration)
|
||||
#if SWITCH_PICO_HID_INSTANCE_COUNT == 1
|
||||
0x29, 0x00, // wTotalLength 41
|
||||
0x01, // bNumInterfaces 1
|
||||
#elif SWITCH_PICO_HID_INSTANCE_COUNT == 2
|
||||
0x49, 0x00, // wTotalLength 73
|
||||
0x02, // bNumInterfaces 2
|
||||
#elif SWITCH_PICO_HID_INSTANCE_COUNT == 3
|
||||
0x69, 0x00, // wTotalLength 105
|
||||
0x03, // bNumInterfaces 3
|
||||
#else
|
||||
0x89, 0x00, // wTotalLength 137
|
||||
0x04, // bNumInterfaces 4
|
||||
#endif
|
||||
0x01, // bConfigurationValue
|
||||
0x00, // iConfiguration (String Index)
|
||||
0xA0, // bmAttributes Remote Wakeup
|
||||
|
|
@ -407,6 +426,108 @@ static const uint8_t switch_pro_configuration_descriptor[] =
|
|||
0x03, // bmAttributes (Interrupt)
|
||||
0x40, 0x00, // wMaxPacketSize 64
|
||||
0x08, // bInterval 8 (unit depends on device speed)
|
||||
|
||||
#if SWITCH_PICO_HID_INSTANCE_COUNT >= 2
|
||||
0x09, // bLength
|
||||
0x04, // bDescriptorType (Interface)
|
||||
0x01, // bInterfaceNumber 1
|
||||
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)
|
||||
0x82, // bEndpointAddress (IN/D2H)
|
||||
0x03, // bmAttributes (Interrupt)
|
||||
0x40, 0x00, // wMaxPacketSize 64
|
||||
0x08, // bInterval 8 (unit depends on device speed)
|
||||
|
||||
0x07, // bLength
|
||||
0x05, // bDescriptorType (Endpoint)
|
||||
0x02, // bEndpointAddress (OUT/H2D)
|
||||
0x03, // bmAttributes (Interrupt)
|
||||
0x40, 0x00, // wMaxPacketSize 64
|
||||
0x08, // bInterval 8 (unit depends on device speed)
|
||||
#endif
|
||||
|
||||
#if SWITCH_PICO_HID_INSTANCE_COUNT >= 3
|
||||
0x09, // bLength
|
||||
0x04, // bDescriptorType (Interface)
|
||||
0x02, // bInterfaceNumber 2
|
||||
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)
|
||||
0x83, // bEndpointAddress (IN/D2H)
|
||||
0x03, // bmAttributes (Interrupt)
|
||||
0x40, 0x00, // wMaxPacketSize 64
|
||||
0x08, // bInterval 8 (unit depends on device speed)
|
||||
|
||||
0x07, // bLength
|
||||
0x05, // bDescriptorType (Endpoint)
|
||||
0x03, // bEndpointAddress (OUT/H2D)
|
||||
0x03, // bmAttributes (Interrupt)
|
||||
0x40, 0x00, // wMaxPacketSize 64
|
||||
0x08, // bInterval 8 (unit depends on device speed)
|
||||
#endif
|
||||
|
||||
#if SWITCH_PICO_HID_INSTANCE_COUNT >= 4
|
||||
0x09, // bLength
|
||||
0x04, // bDescriptorType (Interface)
|
||||
0x03, // bInterfaceNumber 3
|
||||
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)
|
||||
0x84, // bEndpointAddress (IN/D2H)
|
||||
0x03, // bmAttributes (Interrupt)
|
||||
0x40, 0x00, // wMaxPacketSize 64
|
||||
0x08, // bInterval 8 (unit depends on device speed)
|
||||
|
||||
0x07, // bLength
|
||||
0x05, // bDescriptorType (Endpoint)
|
||||
0x04, // bEndpointAddress (OUT/H2D)
|
||||
0x03, // bmAttributes (Interrupt)
|
||||
0x40, 0x00, // wMaxPacketSize 64
|
||||
0x08, // bInterval 8 (unit depends on device speed)
|
||||
#endif
|
||||
};
|
||||
|
||||
static const uint8_t switch_pro_report_descriptor[] =
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -11,6 +11,7 @@
|
|||
#include "switch_haptics.h"
|
||||
#include "switch_pro_descriptors.h"
|
||||
|
||||
|
||||
typedef struct {
|
||||
int16_t accel_x;
|
||||
int16_t accel_y;
|
||||
|
|
@ -49,24 +50,68 @@ typedef struct {
|
|||
uint8_t imu_sample_count; // 0-3
|
||||
SwitchImuSample imu_samples[3];
|
||||
} SwitchInputState;
|
||||
typedef struct {
|
||||
uint8_t red;
|
||||
uint8_t green;
|
||||
uint8_t blue;
|
||||
} SwitchRgbColor;
|
||||
constexpr SwitchRgbColor switch_pro_calibrate_light_color(
|
||||
SwitchRgbColor grip) {
|
||||
const uint8_t minimum =
|
||||
grip.red < grip.green
|
||||
? (grip.red < grip.blue ? grip.red : grip.blue)
|
||||
: (grip.green < grip.blue ? grip.green : grip.blue);
|
||||
const uint8_t maximum =
|
||||
grip.red > grip.green
|
||||
? (grip.red > grip.blue ? grip.red : grip.blue)
|
||||
: (grip.green > grip.blue ? grip.green : grip.blue);
|
||||
const uint16_t chroma = static_cast<uint16_t>(maximum - minimum);
|
||||
const uint16_t peak =
|
||||
static_cast<uint16_t>((static_cast<uint16_t>(maximum) * 2u + 1u) /
|
||||
3u);
|
||||
if (chroma == 0) {
|
||||
const uint8_t gray = static_cast<uint8_t>(peak);
|
||||
return {gray, gray, gray};
|
||||
}
|
||||
|
||||
// Initialize USB state and calibration before entering the main loop.
|
||||
void switch_pro_init();
|
||||
const auto calibrate = [minimum, chroma, peak](uint8_t component) {
|
||||
const uint32_t delta =
|
||||
static_cast<uint32_t>(component - minimum);
|
||||
return static_cast<uint8_t>(
|
||||
(static_cast<uint32_t>(peak) * delta * delta) /
|
||||
(static_cast<uint32_t>(chroma) * chroma));
|
||||
};
|
||||
return {calibrate(grip.red), calibrate(grip.green),
|
||||
calibrate(grip.blue)};
|
||||
}
|
||||
|
||||
// Update the desired controller state for the next USB report.
|
||||
void switch_pro_set_input(const SwitchInputState& state);
|
||||
|
||||
// Drive the Switch Pro USB state machine; returns true only when a regular
|
||||
// Return the configured Switch grip color and its automatically calibrated
|
||||
// physical LED color for one HID/controller slot.
|
||||
SwitchRgbColor switch_pro_get_slot_color(uint8_t instance);
|
||||
SwitchRgbColor switch_pro_get_slot_light_color(uint8_t instance);
|
||||
|
||||
|
||||
// Initialize one HID instance before entering the main loop.
|
||||
void switch_pro_init(uint8_t instance);
|
||||
|
||||
// Update the desired controller state for one HID instance.
|
||||
void switch_pro_set_input(uint8_t instance, const SwitchInputState& state);
|
||||
|
||||
// Drive one Switch Pro USB state machine; returns true only when a regular
|
||||
// 0x30 input report was successfully queued.
|
||||
bool switch_pro_task();
|
||||
bool switch_pro_task(uint8_t instance);
|
||||
|
||||
// Convert a packed UART message into controller state (returns true if parsed).
|
||||
// If out_state is null the parsed state is written directly to the driver.
|
||||
bool switch_pro_apply_uart_packet(const uint8_t* packet, uint8_t length, SwitchInputState* out_state = nullptr);
|
||||
bool switch_pro_apply_uart_packet(const uint8_t* packet, uint8_t length,
|
||||
SwitchInputState& out_state);
|
||||
|
||||
// Driver state helpers
|
||||
bool switch_pro_is_ready();
|
||||
bool switch_pro_is_ready(uint8_t instance);
|
||||
|
||||
// Optional callback fired with decoded rumble intensities from the host.
|
||||
typedef void (*SwitchRumbleCallback)(const SwitchRumbleOutput& rumble);
|
||||
void switch_pro_set_rumble_callback(SwitchRumbleCallback cb);
|
||||
// Optional callback fired with decoded rumble intensities from one host
|
||||
// interface.
|
||||
typedef void (*SwitchRumbleCallback)(uint8_t instance,
|
||||
const SwitchRumbleOutput& rumble);
|
||||
void switch_pro_set_rumble_callback(uint8_t instance,
|
||||
SwitchRumbleCallback callback);
|
||||
|
|
|
|||
1168
tests/bluepad32_backend_lifecycle_test.cpp
Normal file
1168
tests/bluepad32_backend_lifecycle_test.cpp
Normal file
File diff suppressed because it is too large
Load diff
212
tests/bluepad32_imu_normalization_test.cpp
Normal file
212
tests/bluepad32_imu_normalization_test.cpp
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
#include "parser/uni_hid_parser_imu.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
|
||||
namespace {
|
||||
|
||||
int failures = 0;
|
||||
|
||||
void expect(bool condition, const char* message) {
|
||||
if (!condition) {
|
||||
std::cerr << message << '\n';
|
||||
++failures;
|
||||
}
|
||||
}
|
||||
|
||||
void write_calibration_value(
|
||||
std::array<uint8_t, UNI_PSMOVE_ZCM1_CALIBRATION_SIZE>& blob,
|
||||
uni_psmove_imu_model_t model, uint8_t offset, int32_t value) {
|
||||
const uint16_t encoded =
|
||||
model == UNI_PSMOVE_IMU_MODEL_ZCM1
|
||||
? static_cast<uint16_t>(value + 0x8000)
|
||||
: static_cast<uint16_t>(static_cast<int16_t>(value));
|
||||
blob[offset] = static_cast<uint8_t>(encoded);
|
||||
blob[offset + 1] = static_cast<uint8_t>(encoded >> 8u);
|
||||
}
|
||||
|
||||
uint16_t encode_input(uni_psmove_imu_model_t model, int32_t value) {
|
||||
return model == UNI_PSMOVE_IMU_MODEL_ZCM1
|
||||
? static_cast<uint16_t>(value + 0x8000)
|
||||
: static_cast<uint16_t>(static_cast<int16_t>(value));
|
||||
}
|
||||
|
||||
std::array<uint8_t, UNI_PSMOVE_CALIBRATION_REPORT_SIZE> first_report(
|
||||
const std::array<uint8_t, UNI_PSMOVE_ZCM1_CALIBRATION_SIZE>& blob) {
|
||||
std::array<uint8_t, UNI_PSMOVE_CALIBRATION_REPORT_SIZE> report{};
|
||||
std::memcpy(report.data(), blob.data(), report.size());
|
||||
report[0] = 0x10;
|
||||
report[1] = 0x00;
|
||||
return report;
|
||||
}
|
||||
|
||||
std::array<uint8_t, UNI_PSMOVE_CALIBRATION_REPORT_SIZE> continuation_report(
|
||||
const std::array<uint8_t, UNI_PSMOVE_ZCM1_CALIBRATION_SIZE>& blob,
|
||||
uint8_t block, size_t blob_offset) {
|
||||
std::array<uint8_t, UNI_PSMOVE_CALIBRATION_REPORT_SIZE> report{};
|
||||
report[0] = 0x10;
|
||||
report[1] = block;
|
||||
std::memcpy(report.data() + 2, blob.data() + blob_offset,
|
||||
report.size() - 2);
|
||||
return report;
|
||||
}
|
||||
|
||||
void set_accel_calibration(
|
||||
std::array<uint8_t, UNI_PSMOVE_ZCM1_CALIBRATION_SIZE>& blob,
|
||||
uni_psmove_imu_model_t model, int32_t low, int32_t high) {
|
||||
const uint8_t* low_offsets;
|
||||
const uint8_t* high_offsets;
|
||||
static const uint8_t zcm1_low[] = {0x0a, 0x24, 0x14};
|
||||
static const uint8_t zcm1_high[] = {0x16, 0x1e, 0x08};
|
||||
static const uint8_t zcm2_low[] = {0x08, 0x16, 0x24};
|
||||
static const uint8_t zcm2_high[] = {0x02, 0x10, 0x1e};
|
||||
if (model == UNI_PSMOVE_IMU_MODEL_ZCM1) {
|
||||
low_offsets = zcm1_low;
|
||||
high_offsets = zcm1_high;
|
||||
} else {
|
||||
low_offsets = zcm2_low;
|
||||
high_offsets = zcm2_high;
|
||||
}
|
||||
for (uint8_t axis = 0; axis < 3; ++axis) {
|
||||
write_calibration_value(blob, model, low_offsets[axis], low);
|
||||
write_calibration_value(blob, model, high_offsets[axis], high);
|
||||
}
|
||||
}
|
||||
|
||||
void test_wii_accelerometer() {
|
||||
int32_t output[3]{};
|
||||
uni_imu_normalize_wii_accel(100, -50, 25, output);
|
||||
expect(output[0] == -8192 && output[1] == 2048 &&
|
||||
output[2] == -4096,
|
||||
"Wii accelerometer scale or SDL axis mapping is wrong");
|
||||
}
|
||||
|
||||
void test_zcm1_calibration_and_normalization() {
|
||||
constexpr auto model = UNI_PSMOVE_IMU_MODEL_ZCM1;
|
||||
std::array<uint8_t, UNI_PSMOVE_ZCM1_CALIBRATION_SIZE> blob{};
|
||||
set_accel_calibration(blob, model, -1000, 1000);
|
||||
expect(uni_psmove_scale_gyro(32767, -32768, 1,
|
||||
1080 * UNI_IMU_GYRO_RES_PER_DEG_S) ==
|
||||
INT32_MAX,
|
||||
"corrupt PS Move calibration overflow was not clamped");
|
||||
const uint8_t bias_offsets[] = {0x2a, 0x2c, 0x2e};
|
||||
const uint8_t high_offsets[] = {0x46, 0x50, 0x5a};
|
||||
for (uint8_t axis = 0; axis < 3; ++axis) {
|
||||
write_calibration_value(blob, model, bias_offsets[axis], 0);
|
||||
write_calibration_value(blob, model, high_offsets[axis], 1000);
|
||||
}
|
||||
|
||||
auto first = first_report(blob);
|
||||
auto second = continuation_report(blob, 0x01, 49);
|
||||
auto third = continuation_report(blob, 0x82, 96);
|
||||
uni_psmove_imu_calibration_t calibration{};
|
||||
expect(uni_psmove_add_calibration_report(
|
||||
&calibration, model, second.data(), second.size()) ==
|
||||
UNI_PSMOVE_CALIBRATION_INCOMPLETE,
|
||||
"ZCM1 second calibration block was not accepted out of order");
|
||||
expect(uni_psmove_add_calibration_report(
|
||||
&calibration, model, first.data(), first.size()) ==
|
||||
UNI_PSMOVE_CALIBRATION_INCOMPLETE,
|
||||
"ZCM1 first calibration block completed too early");
|
||||
expect(uni_psmove_add_calibration_report(
|
||||
&calibration, model, third.data(), third.size()) ==
|
||||
UNI_PSMOVE_CALIBRATION_COMPLETE,
|
||||
"ZCM1 calibration did not complete");
|
||||
|
||||
const uint16_t accel_first[] = {
|
||||
encode_input(model, 1000), encode_input(model, 0),
|
||||
encode_input(model, -1000)};
|
||||
const uint16_t accel_second[] = {
|
||||
encode_input(model, 0), encode_input(model, 0),
|
||||
encode_input(model, -1000)};
|
||||
const uint16_t gyro_first[] = {
|
||||
encode_input(model, 500), encode_input(model, 0),
|
||||
encode_input(model, -500)};
|
||||
const uint16_t gyro_second[] = {
|
||||
encode_input(model, 500), encode_input(model, 0),
|
||||
encode_input(model, -500)};
|
||||
uni_imu_fixed_sample_t output{};
|
||||
expect(uni_psmove_normalize_imu(
|
||||
model, &calibration, accel_first, accel_second, gyro_first,
|
||||
gyro_second, &output),
|
||||
"ZCM1 calibrated sample was rejected");
|
||||
expect(output.accel[0] == 4096 && output.accel[1] == 0 &&
|
||||
output.accel[2] == -8192,
|
||||
"ZCM1 accelerometer normalization is wrong");
|
||||
expect(output.gyro[0] == 245760 && output.gyro[1] == 0 &&
|
||||
output.gyro[2] == -245760,
|
||||
"ZCM1 gyroscope normalization is wrong");
|
||||
}
|
||||
|
||||
void test_zcm2_calibration_and_normalization() {
|
||||
constexpr auto model = UNI_PSMOVE_IMU_MODEL_ZCM2;
|
||||
std::array<uint8_t, UNI_PSMOVE_ZCM1_CALIBRATION_SIZE> blob{};
|
||||
set_accel_calibration(blob, model, -1000, 1000);
|
||||
const uint8_t bias_offsets[] = {0x26, 0x28, 0x2a};
|
||||
const uint8_t low_offsets[] = {0x42, 0x4a, 0x52};
|
||||
const uint8_t high_offsets[] = {0x30, 0x38, 0x40};
|
||||
for (uint8_t axis = 0; axis < 3; ++axis) {
|
||||
write_calibration_value(blob, model, bias_offsets[axis], 100);
|
||||
write_calibration_value(blob, model, low_offsets[axis], -900);
|
||||
write_calibration_value(blob, model, high_offsets[axis], 1100);
|
||||
}
|
||||
|
||||
auto first = first_report(blob);
|
||||
auto second = continuation_report(blob, 0x81, 49);
|
||||
uni_psmove_imu_calibration_t calibration{};
|
||||
expect(uni_psmove_add_calibration_report(
|
||||
&calibration, model, first.data(), first.size()) ==
|
||||
UNI_PSMOVE_CALIBRATION_INCOMPLETE,
|
||||
"ZCM2 first calibration block completed too early");
|
||||
expect(uni_psmove_add_calibration_report(
|
||||
&calibration, model, second.data(), second.size()) ==
|
||||
UNI_PSMOVE_CALIBRATION_COMPLETE,
|
||||
"ZCM2 calibration did not complete");
|
||||
|
||||
const uint16_t accel[] = {
|
||||
encode_input(model, -1000), encode_input(model, 0),
|
||||
encode_input(model, 1000)};
|
||||
const uint16_t gyro[] = {
|
||||
encode_input(model, -900), encode_input(model, 100),
|
||||
encode_input(model, 1100)};
|
||||
uni_imu_fixed_sample_t output{};
|
||||
expect(uni_psmove_normalize_imu(model, &calibration, accel, accel,
|
||||
gyro, gyro, &output),
|
||||
"ZCM2 calibrated sample was rejected");
|
||||
expect(output.accel[0] == -8192 && output.accel[1] == 0 &&
|
||||
output.accel[2] == 8192,
|
||||
"ZCM2 signed accelerometer normalization is wrong");
|
||||
expect(output.gyro[0] == -552960 && output.gyro[1] == 0 &&
|
||||
output.gyro[2] == 552960,
|
||||
"ZCM2 signed gyroscope normalization is wrong");
|
||||
}
|
||||
|
||||
void test_uncalibrated_psmove_is_suppressed() {
|
||||
uni_psmove_imu_calibration_t calibration{};
|
||||
const uint16_t values[] = {0xffff, 0xffff, 0xffff};
|
||||
uni_imu_fixed_sample_t output{{1, 2, 3}, {4, 5, 6}};
|
||||
expect(!uni_psmove_normalize_imu(
|
||||
UNI_PSMOVE_IMU_MODEL_ZCM1, &calibration, values, values,
|
||||
values, values, &output),
|
||||
"uncalibrated PS Move sample was accepted");
|
||||
expect(output.accel[0] == 0 && output.accel[1] == 0 &&
|
||||
output.accel[2] == 0 && output.gyro[0] == 0 &&
|
||||
output.gyro[1] == 0 && output.gyro[2] == 0,
|
||||
"uncalibrated PS Move motion was not neutralized");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
test_wii_accelerometer();
|
||||
test_zcm1_calibration_and_normalization();
|
||||
test_zcm2_calibration_and_normalization();
|
||||
test_uncalibrated_psmove_is_suppressed();
|
||||
if (failures != 0) {
|
||||
std::cerr << failures << " IMU normalization test(s) failed\n";
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
23
tests/bluepad32_native_stubs/btstack_run_loop.h
Normal file
23
tests/bluepad32_native_stubs/btstack_run_loop.h
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
struct btstack_timer_source_t {
|
||||
void (*handler)(btstack_timer_source_t*);
|
||||
uint32_t timeout_ms;
|
||||
};
|
||||
|
||||
inline void btstack_run_loop_set_timer_handler(
|
||||
btstack_timer_source_t* timer,
|
||||
void (*handler)(btstack_timer_source_t*)) {
|
||||
timer->handler = handler;
|
||||
}
|
||||
|
||||
inline void btstack_run_loop_set_timer(btstack_timer_source_t* timer,
|
||||
uint32_t timeout_ms) {
|
||||
timer->timeout_ms = timeout_ms;
|
||||
}
|
||||
|
||||
inline void btstack_run_loop_add_timer(btstack_timer_source_t*) {}
|
||||
uint32_t btstack_run_loop_get_time_ms();
|
||||
inline void btstack_run_loop_execute() {}
|
||||
7
tests/bluepad32_native_stubs/pico/critical_section.h
Normal file
7
tests/bluepad32_native_stubs/pico/critical_section.h
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
#pragma once
|
||||
|
||||
struct critical_section_t {};
|
||||
|
||||
inline void critical_section_init(critical_section_t*) {}
|
||||
inline void critical_section_enter_blocking(critical_section_t*) {}
|
||||
inline void critical_section_exit(critical_section_t*) {}
|
||||
6
tests/bluepad32_native_stubs/pico/cyw43_arch.h
Normal file
6
tests/bluepad32_native_stubs/pico/cyw43_arch.h
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
#pragma once
|
||||
|
||||
#define CYW43_WL_GPIO_LED_PIN 0
|
||||
|
||||
int cyw43_arch_init();
|
||||
void cyw43_arch_gpio_put(int pin, bool value);
|
||||
3
tests/bluepad32_native_stubs/pico/flash.h
Normal file
3
tests/bluepad32_native_stubs/pico/flash.h
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
#pragma once
|
||||
|
||||
bool flash_safe_execute_core_init();
|
||||
3
tests/bluepad32_native_stubs/pico/multicore.h
Normal file
3
tests/bluepad32_native_stubs/pico/multicore.h
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
#pragma once
|
||||
|
||||
void multicore_launch_core1(void (*entry)());
|
||||
3
tests/bluepad32_native_stubs/pico/stdlib.h
Normal file
3
tests/bluepad32_native_stubs/pico/stdlib.h
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
#pragma once
|
||||
|
||||
void tight_loop_contents();
|
||||
190
tests/bluepad32_native_stubs/uni.h
Normal file
190
tests/bluepad32_native_stubs/uni.h
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
typedef uint8_t bd_addr_t[6];
|
||||
typedef uint8_t link_key_t[16];
|
||||
typedef uint8_t sm_key_t[16];
|
||||
typedef int link_key_type_t;
|
||||
|
||||
enum bd_addr_type_t {
|
||||
BD_ADDR_TYPE_LE_PUBLIC = 0,
|
||||
BD_ADDR_TYPE_LE_RANDOM = 1,
|
||||
BD_ADDR_TYPE_LE_PUBLIC_IDENTITY = 2,
|
||||
BD_ADDR_TYPE_LE_RANDOM_IDENTITY = 3,
|
||||
BD_ADDR_TYPE_UNKNOWN = 0xfe,
|
||||
};
|
||||
|
||||
enum hci_link_type_t {
|
||||
HCI_LINK_TYPE_SCO = 0,
|
||||
HCI_LINK_TYPE_ACL = 1,
|
||||
};
|
||||
|
||||
struct btstack_link_key_iterator_t {
|
||||
int index;
|
||||
};
|
||||
|
||||
enum {
|
||||
ERROR_CODE_SUCCESS = 0,
|
||||
HCI_EVENT_PACKET = 4,
|
||||
HCI_EVENT_USER_CONFIRMATION_REQUEST = 0x33,
|
||||
HCI_EVENT_USER_PASSKEY_REQUEST = 0x34,
|
||||
SM_STK_GENERATION_METHOD_JUST_WORKS = 0x01,
|
||||
SM_STK_GENERATION_METHOD_OOB = 0x02,
|
||||
SM_STK_GENERATION_METHOD_PASSKEY = 0x04,
|
||||
SM_STK_GENERATION_METHOD_NUMERIC_COMPARISON = 0x08,
|
||||
};
|
||||
typedef int uni_property_idx_t;
|
||||
typedef int uni_platform_oob_event_t;
|
||||
struct uni_property_t {};
|
||||
|
||||
enum uni_error_t {
|
||||
UNI_ERROR_SUCCESS = 0,
|
||||
UNI_ERROR_IGNORE_DEVICE = 1,
|
||||
UNI_ERROR_INVALID_CONTROLLER = 2,
|
||||
UNI_ERROR_NO_SLOTS = 3,
|
||||
};
|
||||
|
||||
enum {
|
||||
UNI_CONTROLLER_CLASS_GAMEPAD = 1,
|
||||
DPAD_UP = 1 << 0,
|
||||
DPAD_DOWN = 1 << 1,
|
||||
DPAD_LEFT = 1 << 2,
|
||||
DPAD_RIGHT = 1 << 3,
|
||||
BUTTON_A = 1 << 0,
|
||||
BUTTON_B = 1 << 1,
|
||||
BUTTON_X = 1 << 2,
|
||||
BUTTON_Y = 1 << 3,
|
||||
BUTTON_SHOULDER_L = 1 << 4,
|
||||
BUTTON_SHOULDER_R = 1 << 5,
|
||||
BUTTON_TRIGGER_L = 1 << 6,
|
||||
BUTTON_TRIGGER_R = 1 << 7,
|
||||
BUTTON_THUMB_L = 1 << 8,
|
||||
BUTTON_THUMB_R = 1 << 9,
|
||||
MISC_BUTTON_SYSTEM = 1 << 0,
|
||||
MISC_BUTTON_SELECT = 1 << 1,
|
||||
MISC_BUTTON_START = 1 << 2,
|
||||
MISC_BUTTON_CAPTURE = 1 << 3,
|
||||
};
|
||||
|
||||
struct uni_gamepad_t {
|
||||
uint32_t dpad;
|
||||
uint32_t buttons;
|
||||
uint32_t misc_buttons;
|
||||
int32_t axis_x;
|
||||
int32_t axis_y;
|
||||
int32_t axis_rx;
|
||||
int32_t axis_ry;
|
||||
int32_t brake;
|
||||
int32_t throttle;
|
||||
int32_t accel[3];
|
||||
int32_t gyro[3];
|
||||
};
|
||||
|
||||
struct uni_controller_t {
|
||||
int klass;
|
||||
uni_gamepad_t gamepad;
|
||||
};
|
||||
|
||||
struct uni_hid_device_t;
|
||||
typedef void (*btstack_packet_handler_t)(uint8_t, uint16_t, uint8_t*,
|
||||
uint16_t);
|
||||
struct btstack_packet_callback_registration_t {
|
||||
void* item;
|
||||
btstack_packet_handler_t callback;
|
||||
};
|
||||
typedef void (*uni_play_dual_rumble_t)(uni_hid_device_t*, uint16_t,
|
||||
uint16_t, uint8_t, uint8_t);
|
||||
typedef void (*uni_set_player_leds_t)(uni_hid_device_t*, uint8_t);
|
||||
typedef void (*uni_set_lightbar_color_t)(uni_hid_device_t*, uint8_t, uint8_t,
|
||||
uint8_t);
|
||||
|
||||
struct uni_report_parser_t {
|
||||
uni_set_player_leds_t set_player_leds;
|
||||
uni_set_lightbar_color_t set_lightbar_color;
|
||||
uni_play_dual_rumble_t play_dual_rumble;
|
||||
};
|
||||
|
||||
enum uni_bt_conn_protocol_t {
|
||||
UNI_BT_CONN_PROTOCOL_NONE,
|
||||
UNI_BT_CONN_PROTOCOL_BR_EDR,
|
||||
UNI_BT_CONN_PROTOCOL_BLE,
|
||||
};
|
||||
|
||||
|
||||
struct uni_bt_conn_t {
|
||||
bd_addr_t btaddr;
|
||||
uni_bt_conn_protocol_t protocol;
|
||||
};
|
||||
|
||||
struct uni_hid_device_t {
|
||||
uni_bt_conn_t conn;
|
||||
int idx;
|
||||
bool gamepad;
|
||||
uni_report_parser_t report_parser;
|
||||
int rumble_calls;
|
||||
uint8_t last_high;
|
||||
uint8_t last_low;
|
||||
uint16_t last_rumble_duration_ms;
|
||||
int lightbar_calls;
|
||||
uint8_t lightbar_red;
|
||||
uint8_t lightbar_green;
|
||||
uint8_t lightbar_blue;
|
||||
int player_led_calls;
|
||||
uint8_t player_leds;
|
||||
};
|
||||
|
||||
struct uni_platform {
|
||||
const char* name;
|
||||
void (*init)(int, const char**);
|
||||
void (*on_init_complete)();
|
||||
uni_error_t (*on_device_discovered)(bd_addr_t, const char*, uint16_t,
|
||||
uint8_t);
|
||||
void (*on_device_connected)(uni_hid_device_t*);
|
||||
void (*on_device_disconnected)(uni_hid_device_t*);
|
||||
uni_error_t (*on_device_ready)(uni_hid_device_t*);
|
||||
void* on_device_oob_event;
|
||||
void (*on_controller_data)(uni_hid_device_t*, uni_controller_t*);
|
||||
const uni_property_t* (*get_property)(uni_property_idx_t);
|
||||
void (*on_oob_event)(uni_platform_oob_event_t, void*);
|
||||
void* on_device_dump;
|
||||
void* on_gamepad_seat;
|
||||
};
|
||||
|
||||
bool uni_hid_device_is_gamepad(const uni_hid_device_t* device);
|
||||
int uni_hid_device_get_idx_for_instance(const uni_hid_device_t* device);
|
||||
void uni_hid_device_disconnect(uni_hid_device_t* device);
|
||||
void uni_bt_allow_incoming_connections(bool enabled);
|
||||
void uni_bt_start_scanning_and_autoconnect_unsafe();
|
||||
void uni_bt_stop_scanning_unsafe();
|
||||
void uni_bt_bredr_scan_start();
|
||||
void uni_bt_bredr_scan_stop();
|
||||
void uni_bt_le_scan_start();
|
||||
void uni_bt_le_scan_stop();
|
||||
void uni_bt_del_keys_unsafe();
|
||||
int gap_link_key_iterator_init(btstack_link_key_iterator_t* iterator);
|
||||
int gap_link_key_iterator_get_next(
|
||||
btstack_link_key_iterator_t* iterator, bd_addr_t address,
|
||||
link_key_t link_key, link_key_type_t* type);
|
||||
void gap_link_key_iterator_done(btstack_link_key_iterator_t* iterator);
|
||||
int le_device_db_max_count();
|
||||
void le_device_db_info(
|
||||
int index, int* address_type, bd_addr_t address, sm_key_t irk);
|
||||
void gap_set_bondable_mode(int enabled);
|
||||
void gap_set_link_supervision_timeout(uint16_t link_supervision_timeout);
|
||||
void gap_ssp_set_auto_accept(int auto_accept);
|
||||
void sm_set_accepted_stk_generation_methods(
|
||||
uint8_t accepted_stk_generation_methods);
|
||||
int gap_ssp_confirmation_response(const bd_addr_t address);
|
||||
int gap_ssp_confirmation_negative(const bd_addr_t address);
|
||||
int gap_ssp_passkey_response(const bd_addr_t address, uint32_t passkey);
|
||||
int gap_ssp_passkey_negative(const bd_addr_t address);
|
||||
void hci_add_event_handler(
|
||||
btstack_packet_callback_registration_t* callback_handler);
|
||||
uint8_t hci_event_packet_get_type(const uint8_t* packet);
|
||||
void hci_event_user_confirmation_request_get_bd_addr(
|
||||
const uint8_t* packet, bd_addr_t address);
|
||||
void hci_event_user_passkey_request_get_bd_addr(
|
||||
const uint8_t* packet, bd_addr_t address);
|
||||
void uni_platform_set_custom(uni_platform* platform);
|
||||
int uni_init(int argc, const char** argv);
|
||||
27
tests/bootsel_native_stubs/hardware/gpio.h
Normal file
27
tests/bootsel_native_stubs/hardware/gpio.h
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
using io_rw_32 = volatile uint32_t;
|
||||
|
||||
enum gpio_override {
|
||||
GPIO_OVERRIDE_NORMAL = 0,
|
||||
GPIO_OVERRIDE_LOW = 2,
|
||||
};
|
||||
|
||||
void bootsel_test_masked_write(io_rw_32* address, uint32_t values,
|
||||
uint32_t mask);
|
||||
|
||||
inline void hw_write_masked(io_rw_32* address, uint32_t values,
|
||||
uint32_t mask) {
|
||||
*address = (*address & ~mask) | (values & mask);
|
||||
bootsel_test_masked_write(address, values, mask);
|
||||
}
|
||||
|
||||
#if PICO_RP2350
|
||||
#define IO_QSPI_GPIO_QSPI_SS_CTRL_OEOVER_LSB 14u
|
||||
#define IO_QSPI_GPIO_QSPI_SS_CTRL_OEOVER_BITS 0x0000c000u
|
||||
#else
|
||||
#define IO_QSPI_GPIO_QSPI_SS_CTRL_OEOVER_LSB 12u
|
||||
#define IO_QSPI_GPIO_QSPI_SS_CTRL_OEOVER_BITS 0x00003000u
|
||||
#endif
|
||||
3
tests/bootsel_native_stubs/hardware/regs/sio.h
Normal file
3
tests/bootsel_native_stubs/hardware/regs/sio.h
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
#pragma once
|
||||
|
||||
#define SIO_GPIO_HI_IN_QSPI_CSN_BITS 0x08000000u
|
||||
14
tests/bootsel_native_stubs/hardware/structs/ioqspi.h
Normal file
14
tests/bootsel_native_stubs/hardware/structs/ioqspi.h
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
#pragma once
|
||||
|
||||
#include "hardware/gpio.h"
|
||||
|
||||
struct ioqspi_status_ctrl_hw_t {
|
||||
io_rw_32 status;
|
||||
io_rw_32 ctrl;
|
||||
};
|
||||
|
||||
struct ioqspi_hw_t {
|
||||
ioqspi_status_ctrl_hw_t io[6];
|
||||
};
|
||||
|
||||
extern ioqspi_hw_t* ioqspi_hw;
|
||||
10
tests/bootsel_native_stubs/hardware/structs/sio.h
Normal file
10
tests/bootsel_native_stubs/hardware/structs/sio.h
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
struct sio_hw_t {
|
||||
volatile uint32_t gpio_in;
|
||||
volatile uint32_t gpio_hi_in;
|
||||
};
|
||||
|
||||
extern sio_hw_t* sio_hw;
|
||||
10
tests/bootsel_native_stubs/pico/flash.h
Normal file
10
tests/bootsel_native_stubs/pico/flash.h
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#define __no_inline_not_in_flash_func(function_name) function_name
|
||||
|
||||
constexpr int PICO_OK = 0;
|
||||
|
||||
int flash_safe_execute(void (*function)(void*), void* parameter,
|
||||
uint32_t enter_exit_timeout_ms);
|
||||
8
tests/bootsel_native_stubs/pico/time.h
Normal file
8
tests/bootsel_native_stubs/pico/time.h
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
using absolute_time_t = uint64_t;
|
||||
|
||||
absolute_time_t get_absolute_time();
|
||||
uint64_t to_ms_since_boot(absolute_time_t time);
|
||||
266
tests/bootsel_pairing_button_test.cpp
Normal file
266
tests/bootsel_pairing_button_test.cpp
Normal file
|
|
@ -0,0 +1,266 @@
|
|||
#include "bootsel_pairing_button.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cstdint>
|
||||
#include <iostream>
|
||||
#include <vector>
|
||||
|
||||
#include "hardware/gpio.h"
|
||||
#include "hardware/regs/sio.h"
|
||||
#include "hardware/structs/ioqspi.h"
|
||||
#include "hardware/structs/sio.h"
|
||||
#include "pico/flash.h"
|
||||
#include "pico/time.h"
|
||||
|
||||
namespace {
|
||||
|
||||
#if PICO_RP2350
|
||||
constexpr uint32_t kBootselInputMask = SIO_GPIO_HI_IN_QSPI_CSN_BITS;
|
||||
#else
|
||||
constexpr uint32_t kBootselInputMask = 1u << 1u;
|
||||
#endif
|
||||
|
||||
struct FlashResponse {
|
||||
int result;
|
||||
bool pressed;
|
||||
};
|
||||
|
||||
ioqspi_hw_t qspi_registers{};
|
||||
sio_hw_t sio_registers{};
|
||||
uint64_t now_ms = 0;
|
||||
std::vector<FlashResponse> flash_responses;
|
||||
std::size_t next_flash_response = 0;
|
||||
std::vector<uint32_t> qspi_override_writes;
|
||||
int flash_safe_calls = 0;
|
||||
bool inside_flash_safe_callback = false;
|
||||
|
||||
void require(bool condition, const char* message) {
|
||||
if (!condition) {
|
||||
std::cerr << message << '\n';
|
||||
std::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<BootselPairingButtonEvent> apply_pressed(
|
||||
BootselPairingButtonHoldFsm& fsm, int count) {
|
||||
std::vector<BootselPairingButtonEvent> events;
|
||||
for (int sample = 0; sample < count; ++sample) {
|
||||
const BootselPairingButtonEvent event =
|
||||
fsm.update(BootselPairingButtonSample::kPressed);
|
||||
if (event != BootselPairingButtonEvent::kNone) {
|
||||
events.push_back(event);
|
||||
}
|
||||
}
|
||||
return events;
|
||||
}
|
||||
|
||||
void test_short_press() {
|
||||
BootselPairingButtonHoldFsm fsm;
|
||||
require(apply_pressed(fsm, 19).empty(),
|
||||
"a 19-sample press must not complete the hold");
|
||||
require(fsm.update(BootselPairingButtonSample::kReleased) ==
|
||||
BootselPairingButtonEvent::kNone,
|
||||
"a short-press release must not report a hold");
|
||||
require(apply_pressed(fsm, 19).empty(),
|
||||
"a release must discard the previous short press");
|
||||
}
|
||||
|
||||
void test_pairing_and_clear_events_once() {
|
||||
BootselPairingButtonHoldFsm fsm;
|
||||
require(apply_pressed(fsm, 19).empty(),
|
||||
"the pairing hold must not fire before sample 20");
|
||||
require(fsm.update(BootselPairingButtonSample::kPressed) ==
|
||||
BootselPairingButtonEvent::kOpenPairing,
|
||||
"pairing must fire on exactly sample 20");
|
||||
require(apply_pressed(fsm, 79).empty(),
|
||||
"a long hold must not fire between pairing and clearing");
|
||||
require(fsm.update(BootselPairingButtonSample::kPressed) ==
|
||||
BootselPairingButtonEvent::kClearPairings,
|
||||
"clearing must fire on exactly sample 100");
|
||||
require(apply_pressed(fsm, 100).empty(),
|
||||
"a continuously held button must not repeat either event");
|
||||
}
|
||||
|
||||
void test_release_and_rearm() {
|
||||
BootselPairingButtonHoldFsm fsm;
|
||||
const auto first_events = apply_pressed(fsm, 100);
|
||||
require(first_events.size() == 2 &&
|
||||
first_events[0] ==
|
||||
BootselPairingButtonEvent::kOpenPairing &&
|
||||
first_events[1] ==
|
||||
BootselPairingButtonEvent::kClearPairings,
|
||||
"the initial long hold must report pairing then clearing");
|
||||
require(fsm.update(BootselPairingButtonSample::kReleased) ==
|
||||
BootselPairingButtonEvent::kNone,
|
||||
"release must rearm without reporting an event");
|
||||
const auto second_events = apply_pressed(fsm, 20);
|
||||
require(second_events.size() == 1 &&
|
||||
second_events[0] ==
|
||||
BootselPairingButtonEvent::kOpenPairing,
|
||||
"a valid release must permit a later pairing hold");
|
||||
}
|
||||
|
||||
void test_unread_samples_do_not_transition() {
|
||||
BootselPairingButtonHoldFsm fsm;
|
||||
require(apply_pressed(fsm, 10).empty(),
|
||||
"the first half of a pairing hold must not fire");
|
||||
for (int sample = 0; sample < 8; ++sample) {
|
||||
require(fsm.update(BootselPairingButtonSample::kUnread) ==
|
||||
BootselPairingButtonEvent::kNone,
|
||||
"unread press samples must not report or reset a hold");
|
||||
}
|
||||
require(apply_pressed(fsm, 9).empty(),
|
||||
"valid pressed samples must resume after unread samples");
|
||||
require(fsm.update(BootselPairingButtonSample::kPressed) ==
|
||||
BootselPairingButtonEvent::kOpenPairing,
|
||||
"20 valid pressed samples must fire despite unread samples");
|
||||
|
||||
require(fsm.update(BootselPairingButtonSample::kUnread) ==
|
||||
BootselPairingButtonEvent::kNone,
|
||||
"an unread release must not rearm a completed hold");
|
||||
require(apply_pressed(fsm, 79).empty(),
|
||||
"the long hold must continue across an unread sample");
|
||||
require(fsm.update(BootselPairingButtonSample::kPressed) ==
|
||||
BootselPairingButtonEvent::kClearPairings,
|
||||
"100 valid pressed samples must clear despite unread samples");
|
||||
require(fsm.update(BootselPairingButtonSample::kReleased) ==
|
||||
BootselPairingButtonEvent::kNone,
|
||||
"a valid release must only rearm");
|
||||
const auto events = apply_pressed(fsm, 20);
|
||||
require(events.size() == 1 &&
|
||||
events[0] == BootselPairingButtonEvent::kOpenPairing,
|
||||
"the FSM must fire after the eventual valid release");
|
||||
}
|
||||
|
||||
BootselPairingButtonEvent run_sample(
|
||||
uint64_t sample_time_ms, int result, bool pressed) {
|
||||
flash_responses.push_back({result, pressed});
|
||||
now_ms = sample_time_ms;
|
||||
const std::size_t expected_consumed = flash_responses.size();
|
||||
const BootselPairingButtonEvent event = bootsel_pairing_button_task();
|
||||
require(next_flash_response == expected_consumed,
|
||||
"a due poll must invoke flash_safe_execute exactly once");
|
||||
return event;
|
||||
}
|
||||
|
||||
void test_sampler_cadence_and_callback_failure() {
|
||||
now_ms = 0;
|
||||
require(bootsel_pairing_button_task() ==
|
||||
BootselPairingButtonEvent::kNone,
|
||||
"the sampler must wait for its first 100 ms cadence");
|
||||
now_ms = 99;
|
||||
require(bootsel_pairing_button_task() ==
|
||||
BootselPairingButtonEvent::kNone,
|
||||
"the sampler must not poll before 100 ms");
|
||||
require(flash_safe_calls == 0,
|
||||
"sub-cadence task calls must not enter flash-safe execution");
|
||||
|
||||
require(run_sample(100, PICO_OK, true) ==
|
||||
BootselPairingButtonEvent::kNone,
|
||||
"the first valid pressed sample must only start the hold");
|
||||
require(flash_safe_calls == 1 && qspi_override_writes.size() == 2,
|
||||
"a successful sample must float and restore QSPI CSn once");
|
||||
const uint32_t disabled =
|
||||
GPIO_OVERRIDE_LOW << IO_QSPI_GPIO_QSPI_SS_CTRL_OEOVER_LSB;
|
||||
require(qspi_override_writes[0] == disabled,
|
||||
"the callback must float QSPI CSn before reading BOOTSEL");
|
||||
require(qspi_override_writes[1] == 0,
|
||||
"the callback must restore normal QSPI CSn control");
|
||||
|
||||
now_ms = 199;
|
||||
require(bootsel_pairing_button_task() ==
|
||||
BootselPairingButtonEvent::kNone,
|
||||
"the sampler must remain gated between 10 Hz polls");
|
||||
require(flash_safe_calls == 1,
|
||||
"an early task call must not sample BOOTSEL");
|
||||
|
||||
const std::size_t writes_before_failure = qspi_override_writes.size();
|
||||
require(run_sample(200, -1, true) ==
|
||||
BootselPairingButtonEvent::kNone,
|
||||
"flash-safe failure must be treated as unread");
|
||||
require(qspi_override_writes.size() == writes_before_failure,
|
||||
"a failed flash-safe entry must not invoke the callback");
|
||||
|
||||
for (uint64_t time = 300; time < 2100; time += 100) {
|
||||
require(run_sample(time, PICO_OK, true) ==
|
||||
BootselPairingButtonEvent::kNone,
|
||||
"the sampler must wait for 20 valid pressed samples");
|
||||
}
|
||||
require(run_sample(2100, PICO_OK, true) ==
|
||||
BootselPairingButtonEvent::kOpenPairing,
|
||||
"a failed sample must not reset the valid pressed count");
|
||||
require(run_sample(2200, PICO_OK, true) ==
|
||||
BootselPairingButtonEvent::kNone,
|
||||
"a held button must not repeat pairing");
|
||||
|
||||
require(run_sample(2300, -1, false) ==
|
||||
BootselPairingButtonEvent::kNone,
|
||||
"a failed release sample must remain unread");
|
||||
require(run_sample(2400, PICO_OK, true) ==
|
||||
BootselPairingButtonEvent::kNone,
|
||||
"an unread release must not rearm the sampler FSM");
|
||||
require(run_sample(2500, PICO_OK, false) ==
|
||||
BootselPairingButtonEvent::kNone,
|
||||
"a valid release must rearm without firing");
|
||||
|
||||
for (uint64_t time = 2600; time < 4500; time += 100) {
|
||||
require(run_sample(time, PICO_OK, true) ==
|
||||
BootselPairingButtonEvent::kNone,
|
||||
"the rearmed sampler must count a fresh hold");
|
||||
}
|
||||
require(run_sample(4500, PICO_OK, true) ==
|
||||
BootselPairingButtonEvent::kOpenPairing,
|
||||
"a valid release must permit a second pairing hold");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ioqspi_hw_t* ioqspi_hw = &qspi_registers;
|
||||
sio_hw_t* sio_hw = &sio_registers;
|
||||
|
||||
absolute_time_t get_absolute_time() {
|
||||
return now_ms;
|
||||
}
|
||||
|
||||
uint64_t to_ms_since_boot(absolute_time_t time) {
|
||||
return time;
|
||||
}
|
||||
|
||||
void bootsel_test_masked_write(io_rw_32* address, uint32_t, uint32_t mask) {
|
||||
require(inside_flash_safe_callback,
|
||||
"QSPI override writes must occur inside flash_safe_execute");
|
||||
require(address == &ioqspi_hw->io[1].ctrl,
|
||||
"the callback must only override QSPI CSn");
|
||||
qspi_override_writes.push_back(*address & mask);
|
||||
}
|
||||
|
||||
int flash_safe_execute(void (*function)(void*), void* parameter,
|
||||
uint32_t enter_exit_timeout_ms) {
|
||||
require(enter_exit_timeout_ms == 100,
|
||||
"BOOTSEL sampling must use the 100 ms flash-safe timeout");
|
||||
require(next_flash_response < flash_responses.size(),
|
||||
"flash-safe execution requires a queued test response");
|
||||
++flash_safe_calls;
|
||||
const FlashResponse response = flash_responses[next_flash_response++];
|
||||
if (response.result != PICO_OK) {
|
||||
return response.result;
|
||||
}
|
||||
|
||||
sio_hw->gpio_hi_in = response.pressed ? 0 : kBootselInputMask;
|
||||
inside_flash_safe_callback = true;
|
||||
function(parameter);
|
||||
inside_flash_safe_callback = false;
|
||||
require((ioqspi_hw->io[1].ctrl &
|
||||
IO_QSPI_GPIO_QSPI_SS_CTRL_OEOVER_BITS) == 0,
|
||||
"the callback must restore QSPI CSn before returning");
|
||||
return PICO_OK;
|
||||
}
|
||||
|
||||
int main() {
|
||||
test_short_press();
|
||||
test_pairing_and_clear_events_once();
|
||||
test_release_and_rearm();
|
||||
test_unread_samples_do_not_transition();
|
||||
test_sampler_cadence_and_callback_failure();
|
||||
return 0;
|
||||
}
|
||||
13
tests/native_stubs/pico/rand.h
Normal file
13
tests/native_stubs/pico/rand.h
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
uint32_t get_rand_32(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
18
tests/native_stubs/pico/time.h
Normal file
18
tests/native_stubs/pico/time.h
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct {
|
||||
uint64_t milliseconds;
|
||||
} absolute_time_t;
|
||||
|
||||
absolute_time_t get_absolute_time(void);
|
||||
uint32_t to_ms_since_boot(absolute_time_t time);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
45
tests/native_stubs/tusb.h
Normal file
45
tests/native_stubs/tusb.h
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef enum {
|
||||
HID_REPORT_TYPE_INVALID = 0,
|
||||
HID_REPORT_TYPE_INPUT = 1,
|
||||
HID_REPORT_TYPE_OUTPUT = 2,
|
||||
HID_REPORT_TYPE_FEATURE = 3,
|
||||
} hid_report_type_t;
|
||||
|
||||
typedef struct {
|
||||
uint8_t bmRequestType;
|
||||
uint8_t bRequest;
|
||||
uint16_t wValue;
|
||||
uint16_t wIndex;
|
||||
uint16_t wLength;
|
||||
} tusb_control_request_t;
|
||||
|
||||
bool tud_hid_n_ready(uint8_t instance);
|
||||
bool tud_hid_n_report(uint8_t instance, uint8_t report_id,
|
||||
const void* report, uint16_t length);
|
||||
bool tud_suspended(void);
|
||||
bool tud_remote_wakeup(void);
|
||||
|
||||
uint16_t tud_hid_get_report_cb(uint8_t instance, uint8_t report_id,
|
||||
hid_report_type_t report_type, uint8_t* buffer,
|
||||
uint16_t requested_length);
|
||||
void tud_hid_set_report_cb(uint8_t instance, uint8_t report_id,
|
||||
hid_report_type_t report_type,
|
||||
const uint8_t* buffer, uint16_t buffer_size);
|
||||
void tud_hid_report_received_cb(uint8_t instance, uint8_t report_id,
|
||||
const uint8_t* buffer, uint16_t buffer_size);
|
||||
uint8_t const* tud_hid_descriptor_report_cb(uint8_t instance);
|
||||
void tud_mount_cb(void);
|
||||
void tud_umount_cb(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
184
tests/switch_pro_descriptors_test.cpp
Normal file
184
tests/switch_pro_descriptors_test.cpp
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
#include "switch_pro_descriptors.h"
|
||||
#include "tusb_config.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
|
||||
#ifndef EXPECTED_HID_INSTANCE_COUNT
|
||||
#error "EXPECTED_HID_INSTANCE_COUNT must be defined by the test build"
|
||||
#endif
|
||||
|
||||
static_assert(SWITCH_PICO_HID_INSTANCE_COUNT == EXPECTED_HID_INSTANCE_COUNT,
|
||||
"the requested HID instance count did not reach the descriptors");
|
||||
static_assert(CFG_TUD_HID == EXPECTED_HID_INSTANCE_COUNT,
|
||||
"TinyUSB HID count differs from the descriptor count");
|
||||
static_assert(sizeof(switch_pro_configuration_descriptor) ==
|
||||
9u + 32u * EXPECTED_HID_INSTANCE_COUNT,
|
||||
"configuration descriptor has the wrong total size");
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr uint8_t kConfigurationDescriptor = 0x02;
|
||||
constexpr uint8_t kInterfaceDescriptor = 0x04;
|
||||
constexpr uint8_t kEndpointDescriptor = 0x05;
|
||||
constexpr uint8_t kHidDescriptor = 0x21;
|
||||
|
||||
#if EXPECTED_HID_INSTANCE_COUNT == 1
|
||||
constexpr std::array<uint8_t, 41> kUartConfigurationDescriptor = {
|
||||
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,
|
||||
};
|
||||
#endif
|
||||
|
||||
int failures = 0;
|
||||
|
||||
void expect(bool condition, const char* message) {
|
||||
if (!condition) {
|
||||
std::cerr << message << '\n';
|
||||
++failures;
|
||||
}
|
||||
}
|
||||
|
||||
uint16_t read_u16(const uint8_t* bytes) {
|
||||
return static_cast<uint16_t>(bytes[0]) |
|
||||
(static_cast<uint16_t>(bytes[1]) << 8u);
|
||||
}
|
||||
|
||||
struct InterfaceContract {
|
||||
bool present = false;
|
||||
bool in_endpoint = false;
|
||||
bool out_endpoint = false;
|
||||
uint8_t endpoint_count = 0;
|
||||
uint8_t hid_count = 0;
|
||||
};
|
||||
|
||||
void inspect_configuration_descriptor() {
|
||||
const auto* descriptor = switch_pro_configuration_descriptor;
|
||||
constexpr size_t descriptor_size =
|
||||
sizeof(switch_pro_configuration_descriptor);
|
||||
#if EXPECTED_HID_INSTANCE_COUNT == 1
|
||||
expect(std::memcmp(descriptor, kUartConfigurationDescriptor.data(),
|
||||
descriptor_size) == 0,
|
||||
"UART configuration descriptor bytes changed");
|
||||
#endif
|
||||
|
||||
expect(descriptor[0] == 9 && descriptor[1] == kConfigurationDescriptor,
|
||||
"configuration header is malformed");
|
||||
expect(read_u16(descriptor + 2) == descriptor_size,
|
||||
"wTotalLength does not match the emitted descriptor");
|
||||
expect(descriptor[4] == EXPECTED_HID_INSTANCE_COUNT,
|
||||
"bNumInterfaces does not match the HID instance count");
|
||||
|
||||
std::array<InterfaceContract, EXPECTED_HID_INSTANCE_COUNT> interfaces{};
|
||||
std::array<bool, 256> endpoint_addresses{};
|
||||
int current_interface = -1;
|
||||
size_t offset = descriptor[0];
|
||||
|
||||
while (offset < descriptor_size) {
|
||||
const uint8_t length = descriptor[offset];
|
||||
expect(length >= 2, "descriptor block has an invalid length");
|
||||
if (length < 2) {
|
||||
break;
|
||||
}
|
||||
expect(offset + length <= descriptor_size,
|
||||
"descriptor block extends beyond wTotalLength");
|
||||
if (offset + length > descriptor_size) {
|
||||
break;
|
||||
}
|
||||
|
||||
const uint8_t type = descriptor[offset + 1];
|
||||
if (type == kInterfaceDescriptor) {
|
||||
expect(length == 9, "interface descriptor has the wrong length");
|
||||
const uint8_t number = descriptor[offset + 2];
|
||||
expect(number < interfaces.size(),
|
||||
"interface number is outside the configured range");
|
||||
if (number < interfaces.size()) {
|
||||
expect(!interfaces[number].present,
|
||||
"interface number is duplicated");
|
||||
interfaces[number].present = true;
|
||||
current_interface = number;
|
||||
} else {
|
||||
current_interface = -1;
|
||||
}
|
||||
expect(descriptor[offset + 3] == 0,
|
||||
"interface uses an unexpected alternate setting");
|
||||
expect(descriptor[offset + 4] == 2,
|
||||
"interface does not declare two endpoints");
|
||||
expect(descriptor[offset + 5] == 0x03,
|
||||
"interface is not HID class");
|
||||
} else if (type == kHidDescriptor) {
|
||||
expect(current_interface >= 0,
|
||||
"HID descriptor appears before an interface");
|
||||
expect(length == sizeof(switch_pro_hid_descriptor),
|
||||
"HID descriptor has the wrong length");
|
||||
expect(std::memcmp(descriptor + offset, switch_pro_hid_descriptor,
|
||||
sizeof(switch_pro_hid_descriptor)) == 0,
|
||||
"interfaces do not reuse the shared HID/report contract");
|
||||
expect(read_u16(descriptor + offset + 7) ==
|
||||
sizeof(switch_pro_report_descriptor),
|
||||
"HID descriptor advertises the wrong report descriptor size");
|
||||
if (current_interface >= 0) {
|
||||
++interfaces[static_cast<size_t>(current_interface)].hid_count;
|
||||
}
|
||||
} else if (type == kEndpointDescriptor) {
|
||||
expect(current_interface >= 0,
|
||||
"endpoint descriptor appears before an interface");
|
||||
expect(length == 7, "endpoint descriptor has the wrong length");
|
||||
const uint8_t address = descriptor[offset + 2];
|
||||
expect(!endpoint_addresses[address],
|
||||
"endpoint address is duplicated across interfaces");
|
||||
endpoint_addresses[address] = true;
|
||||
expect(descriptor[offset + 3] == 0x03,
|
||||
"endpoint is not interrupt type");
|
||||
expect(read_u16(descriptor + offset + 4) ==
|
||||
SWITCH_PRO_ENDPOINT_SIZE,
|
||||
"endpoint has the wrong maximum packet size");
|
||||
expect(descriptor[offset + 6] == 8,
|
||||
"endpoint has the wrong polling interval");
|
||||
|
||||
if (current_interface >= 0) {
|
||||
auto& interface =
|
||||
interfaces[static_cast<size_t>(current_interface)];
|
||||
++interface.endpoint_count;
|
||||
const uint8_t endpoint_number =
|
||||
static_cast<uint8_t>(current_interface + 1);
|
||||
if ((address & 0x80u) != 0) {
|
||||
expect(address == static_cast<uint8_t>(0x80u | endpoint_number),
|
||||
"IN endpoint does not belong to its interface");
|
||||
interface.in_endpoint = true;
|
||||
} else {
|
||||
expect(address == endpoint_number,
|
||||
"OUT endpoint does not belong to its interface");
|
||||
interface.out_endpoint = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
offset += length;
|
||||
}
|
||||
|
||||
expect(offset == descriptor_size,
|
||||
"descriptor parser did not finish at wTotalLength");
|
||||
for (const auto& interface : interfaces) {
|
||||
expect(interface.present, "configured HID interface is missing");
|
||||
expect(interface.hid_count == 1,
|
||||
"interface does not contain exactly one HID descriptor");
|
||||
expect(interface.endpoint_count == 2,
|
||||
"interface does not contain exactly two endpoints");
|
||||
expect(interface.in_endpoint && interface.out_endpoint,
|
||||
"interface is missing an IN or OUT endpoint");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
inspect_configuration_descriptor();
|
||||
return failures == 0 ? 0 : 1;
|
||||
}
|
||||
696
tests/switch_pro_driver_context_test.cpp
Normal file
696
tests/switch_pro_driver_context_test.cpp
Normal file
|
|
@ -0,0 +1,696 @@
|
|||
#include "switch_pro_driver.h"
|
||||
#include "controller_color_config.h"
|
||||
#include "tusb.h"
|
||||
#include "pico/time.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
|
||||
namespace {
|
||||
constexpr uint8_t kInstanceCount = SWITCH_PICO_HID_INSTANCE_COUNT;
|
||||
constexpr uint8_t kInvalidInstance = kInstanceCount;
|
||||
static_assert(kInstanceCount == 4,
|
||||
"the native driver harness must exercise four HID instances");
|
||||
|
||||
|
||||
struct SentReport {
|
||||
uint8_t instance = 0;
|
||||
uint8_t report_id = 0;
|
||||
uint16_t length = 0;
|
||||
std::array<uint8_t, SWITCH_PRO_ENDPOINT_SIZE> data{};
|
||||
};
|
||||
|
||||
struct RumbleEvent {
|
||||
unsigned count = 0;
|
||||
uint8_t instance = 0xff;
|
||||
SwitchRumbleOutput output{};
|
||||
};
|
||||
|
||||
uint64_t now_ms = 0;
|
||||
uint32_t random_value = 1;
|
||||
std::array<bool, kInstanceCount> hid_ready{};
|
||||
std::array<bool, kInstanceCount> hid_report_succeeds{};
|
||||
std::array<unsigned, kInstanceCount> hid_report_attempts{};
|
||||
std::array<SentReport, 32> sent_reports{};
|
||||
unsigned sent_report_count = 0;
|
||||
std::array<RumbleEvent, kInstanceCount> rumble_events{};
|
||||
int failures = 0;
|
||||
|
||||
void expect(bool condition, const char* message) {
|
||||
if (!condition) {
|
||||
std::cerr << message << '\n';
|
||||
++failures;
|
||||
}
|
||||
}
|
||||
|
||||
void clear_sent_reports() {
|
||||
sent_reports = {};
|
||||
sent_report_count = 0;
|
||||
}
|
||||
|
||||
void initialize_contexts() {
|
||||
now_ms = 0;
|
||||
hid_report_attempts = {};
|
||||
for (uint8_t instance = 0; instance < kInstanceCount; ++instance) {
|
||||
hid_ready[instance] = true;
|
||||
hid_report_succeeds[instance] = true;
|
||||
switch_pro_init(instance);
|
||||
}
|
||||
clear_sent_reports();
|
||||
}
|
||||
|
||||
const SentReport* latest_regular_report(uint8_t instance) {
|
||||
for (unsigned i = sent_report_count; i > 0; --i) {
|
||||
const SentReport& report = sent_reports[i - 1];
|
||||
if (report.instance == instance &&
|
||||
report.length == sizeof(SwitchProReport) &&
|
||||
report.data[0] == 0x30) {
|
||||
return &report;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
SwitchProReport copy_switch_report(const SentReport* sent) {
|
||||
SwitchProReport report{};
|
||||
if (sent != nullptr) {
|
||||
std::memcpy(&report, sent->data.data(), sizeof(report));
|
||||
}
|
||||
return report;
|
||||
}
|
||||
SwitchProReport get_current_report(uint8_t instance,
|
||||
const char* length_failure) {
|
||||
std::array<uint8_t, SWITCH_PRO_ENDPOINT_SIZE> data{};
|
||||
expect(tud_hid_get_report_cb(instance, 0, HID_REPORT_TYPE_INPUT,
|
||||
data.data(), data.size()) ==
|
||||
sizeof(SwitchProReport),
|
||||
length_failure);
|
||||
SwitchProReport report{};
|
||||
std::memcpy(&report, data.data(), sizeof(report));
|
||||
return report;
|
||||
}
|
||||
|
||||
void expect_neutral_sticks(SwitchProReport& report,
|
||||
const char* state_failure) {
|
||||
constexpr uint16_t packed_mid = SWITCH_PRO_JOYSTICK_MID >> 4u;
|
||||
constexpr uint16_t packed_inverted_mid =
|
||||
static_cast<uint16_t>(-static_cast<int32_t>(packed_mid)) & 0x0fffu;
|
||||
expect(report.inputs.leftStick.getX() == packed_mid &&
|
||||
report.inputs.leftStick.getY() == packed_inverted_mid &&
|
||||
report.inputs.rightStick.getX() == packed_mid &&
|
||||
report.inputs.rightStick.getY() == packed_inverted_mid,
|
||||
state_failure);
|
||||
}
|
||||
|
||||
|
||||
unsigned reports_for_instance(uint8_t instance) {
|
||||
unsigned count = 0;
|
||||
for (unsigned i = 0; i < sent_report_count; ++i) {
|
||||
if (sent_reports[i].instance == instance) {
|
||||
++count;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
uint32_t read_bits_le(const uint8_t* bytes, uint16_t bit_offset,
|
||||
uint8_t width) {
|
||||
uint32_t value = 0;
|
||||
for (uint8_t bit = 0; bit < width; ++bit) {
|
||||
uint16_t source_bit = static_cast<uint16_t>(bit_offset + bit);
|
||||
if ((bytes[source_bit >> 3] & (1u << (source_bit & 7u))) != 0) {
|
||||
value |= 1u << bit;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
int16_t read_int16_le(const uint8_t* bytes) {
|
||||
return static_cast<int16_t>(
|
||||
static_cast<uint16_t>(bytes[0]) |
|
||||
(static_cast<uint16_t>(bytes[1]) << 8u));
|
||||
}
|
||||
|
||||
void send_feature(uint8_t instance, uint8_t command, uint8_t value) {
|
||||
std::array<uint8_t, SWITCH_PRO_ENDPOINT_SIZE> report{};
|
||||
report[0] = REPORT_FEATURE;
|
||||
report[10] = command;
|
||||
report[11] = value;
|
||||
tud_hid_report_received_cb(instance, 0, report.data(), report.size());
|
||||
}
|
||||
void send_spi_read(uint8_t instance, uint32_t address, uint8_t size) {
|
||||
std::array<uint8_t, SWITCH_PRO_ENDPOINT_SIZE> report{};
|
||||
report[0] = REPORT_FEATURE;
|
||||
report[10] = SPI_READ;
|
||||
report[11] = static_cast<uint8_t>(address);
|
||||
report[12] = static_cast<uint8_t>(address >> 8u);
|
||||
report[13] = static_cast<uint8_t>(address >> 16u);
|
||||
report[14] = static_cast<uint8_t>(address >> 24u);
|
||||
report[15] = size;
|
||||
tud_hid_report_received_cb(instance, 0, report.data(), report.size());
|
||||
}
|
||||
|
||||
|
||||
void send_config(uint8_t instance, uint8_t subtype) {
|
||||
const uint8_t report[] = {REPORT_CONFIGURATION, subtype};
|
||||
tud_hid_report_received_cb(instance, 0, report, sizeof(report));
|
||||
}
|
||||
|
||||
uint32_t type_2(uint8_t high_frequency, uint8_t high_amplitude,
|
||||
uint8_t low_frequency, uint8_t low_amplitude) {
|
||||
return (1u << 30u) |
|
||||
((static_cast<uint32_t>(low_amplitude) & 0x7fu) << 23u) |
|
||||
((static_cast<uint32_t>(low_frequency) & 0x7fu) << 16u) |
|
||||
((static_cast<uint32_t>(high_amplitude) & 0x7fu) << 9u) |
|
||||
((static_cast<uint32_t>(high_frequency) & 0x7fu) << 2u);
|
||||
}
|
||||
|
||||
uint32_t type_1_one_sample(uint8_t high_command, uint8_t low_command) {
|
||||
return (1u << 30u) |
|
||||
((static_cast<uint32_t>(low_command) & 0x1fu) << 25u) |
|
||||
((static_cast<uint32_t>(high_command) & 0x1fu) << 20u);
|
||||
}
|
||||
|
||||
std::array<uint8_t, 8> rumble_payload(uint32_t left, uint32_t right) {
|
||||
std::array<uint8_t, 8> payload{};
|
||||
const uint32_t words[] = {left, right};
|
||||
for (unsigned actuator = 0; actuator < 2; ++actuator) {
|
||||
unsigned offset = actuator * 4u;
|
||||
payload[offset] = static_cast<uint8_t>(words[actuator]);
|
||||
payload[offset + 1] = static_cast<uint8_t>(words[actuator] >> 8u);
|
||||
payload[offset + 2] = static_cast<uint8_t>(words[actuator] >> 16u);
|
||||
payload[offset + 3] = static_cast<uint8_t>(words[actuator] >> 24u);
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
std::array<uint8_t, 10> complete_rumble_report(
|
||||
const std::array<uint8_t, 8>& payload) {
|
||||
std::array<uint8_t, 10> report{};
|
||||
report[0] = REPORT_OUTPUT_10;
|
||||
std::memcpy(report.data() + 2, payload.data(), payload.size());
|
||||
return report;
|
||||
}
|
||||
|
||||
void rumble_callback(uint8_t instance, const SwitchRumbleOutput& output) {
|
||||
expect(instance < rumble_events.size(),
|
||||
"rumble callback received an invalid instance");
|
||||
if (instance >= rumble_events.size()) {
|
||||
return;
|
||||
}
|
||||
RumbleEvent& event = rumble_events[instance];
|
||||
++event.count;
|
||||
event.instance = instance;
|
||||
event.output = output;
|
||||
}
|
||||
|
||||
void test_reset_materializes_neutral_sticks() {
|
||||
initialize_contexts();
|
||||
for (uint8_t instance = 0; instance < kInstanceCount; ++instance) {
|
||||
SwitchProReport initialized = get_current_report(
|
||||
instance, "GET_REPORT failed immediately after init");
|
||||
expect_neutral_sticks(
|
||||
initialized, "instance sticks were not neutral after init");
|
||||
}
|
||||
|
||||
tud_mount_cb();
|
||||
for (uint8_t instance = 0; instance < kInstanceCount; ++instance) {
|
||||
SwitchProReport mounted = get_current_report(
|
||||
instance, "GET_REPORT failed immediately after mount");
|
||||
expect_neutral_sticks(
|
||||
mounted, "instance sticks were not neutral after mount");
|
||||
}
|
||||
}
|
||||
|
||||
void test_startup_identify_preserves_first_reply_counter() {
|
||||
initialize_contexts();
|
||||
tud_mount_cb();
|
||||
|
||||
expect(!switch_pro_task(0), "startup identify counted as regular input");
|
||||
expect(sent_report_count == 1 && sent_reports[0].instance == 0 &&
|
||||
sent_reports[0].data[0] == REPORT_USB_INPUT_81 &&
|
||||
sent_reports[0].data[1] == IDENTIFY,
|
||||
"startup identify did not use the addressed raw HID route");
|
||||
|
||||
send_feature(0, GET_CONTROLLER_STATE, 0);
|
||||
now_ms = 6;
|
||||
expect(!switch_pro_task(0), "first subcommand reply counted as regular input");
|
||||
expect(sent_report_count == 2 &&
|
||||
sent_reports[1].data[0] == REPORT_OUTPUT_21 &&
|
||||
sent_reports[1].data[1] == 0,
|
||||
"startup identify consumed the first subcommand reply counter");
|
||||
}
|
||||
|
||||
void test_failed_startup_identify_retries_preserve_counter() {
|
||||
initialize_contexts();
|
||||
tud_mount_cb();
|
||||
hid_report_succeeds[0] = false;
|
||||
|
||||
switch_pro_task(0);
|
||||
switch_pro_task(0);
|
||||
expect(hid_report_attempts[0] == 2 && reports_for_instance(0) == 0,
|
||||
"failed startup identify was not retried");
|
||||
|
||||
hid_report_succeeds[0] = true;
|
||||
expect(!switch_pro_task(0), "retried startup identify counted as regular input");
|
||||
expect(hid_report_attempts[0] == 3 && reports_for_instance(0) == 1,
|
||||
"startup identify did not recover after failed sends");
|
||||
|
||||
send_feature(0, GET_CONTROLLER_STATE, 0);
|
||||
now_ms = 6;
|
||||
switch_pro_task(0);
|
||||
expect(sent_report_count == 2 &&
|
||||
sent_reports[1].data[0] == REPORT_OUTPUT_21 &&
|
||||
sent_reports[1].data[1] == 0,
|
||||
"failed startup identify retries consumed the reply counter");
|
||||
}
|
||||
|
||||
void test_input_reports_and_timers_are_isolated() {
|
||||
initialize_contexts();
|
||||
std::array<SwitchInputState, kInstanceCount> states{};
|
||||
for (uint8_t instance = 0; instance < kInstanceCount; ++instance) {
|
||||
SwitchInputState& state = states[instance];
|
||||
state.lx = static_cast<uint16_t>(0x1111u * (instance + 1u));
|
||||
state.ly = static_cast<uint16_t>(0x2222u + 0x1111u * instance);
|
||||
state.rx = static_cast<uint16_t>(0x5555u + 0x1111u * instance);
|
||||
state.ry = static_cast<uint16_t>(0x8888u + 0x1111u * instance);
|
||||
}
|
||||
states[0].button_a = true;
|
||||
states[1].button_b = true;
|
||||
states[2].button_x = true;
|
||||
states[3].button_y = true;
|
||||
|
||||
for (uint8_t instance = 0; instance < kInstanceCount; ++instance) {
|
||||
switch_pro_set_input(instance, states[instance]);
|
||||
}
|
||||
|
||||
now_ms = 15;
|
||||
std::array<SwitchProReport, kInstanceCount> sent{};
|
||||
for (uint8_t instance = 0; instance < kInstanceCount; ++instance) {
|
||||
expect(switch_pro_task(instance),
|
||||
"configured instance did not send its timed report");
|
||||
const SentReport* routed = latest_regular_report(instance);
|
||||
expect(routed != nullptr, "input report used the wrong HID route");
|
||||
sent[instance] = copy_switch_report(routed);
|
||||
expect(sent[instance].inputs.buttonA == (instance == 0) &&
|
||||
sent[instance].inputs.buttonB == (instance == 1) &&
|
||||
sent[instance].inputs.buttonX == (instance == 2) &&
|
||||
sent[instance].inputs.buttonY == (instance == 3),
|
||||
"button state crossed HID instances");
|
||||
}
|
||||
|
||||
std::array<std::array<uint8_t, SWITCH_PRO_ENDPOINT_SIZE>, kInstanceCount>
|
||||
current{};
|
||||
for (uint8_t instance = 0; instance < kInstanceCount; ++instance) {
|
||||
expect(tud_hid_get_report_cb(instance, 0, HID_REPORT_TYPE_INPUT,
|
||||
current[instance].data(),
|
||||
current[instance].size()) ==
|
||||
sizeof(SwitchProReport),
|
||||
"GET_REPORT rejected a configured instance");
|
||||
}
|
||||
for (uint8_t left = 0; left < kInstanceCount; ++left) {
|
||||
for (uint8_t right = static_cast<uint8_t>(left + 1u);
|
||||
right < kInstanceCount; ++right) {
|
||||
expect(std::memcmp(current[left].data(), current[right].data(),
|
||||
current[left].size()) != 0,
|
||||
"GET_REPORT returned shared state across HID instances");
|
||||
}
|
||||
}
|
||||
|
||||
SwitchInputState changed_zero = states[0];
|
||||
changed_zero.button_a = false;
|
||||
changed_zero.button_home = true;
|
||||
switch_pro_set_input(0, changed_zero);
|
||||
now_ms = 30;
|
||||
expect(switch_pro_task(0),
|
||||
"instance 0 did not apply its changed input state");
|
||||
SwitchProReport unchanged_three = get_current_report(
|
||||
3, "GET_REPORT failed for instance 3 after instance 0 changed");
|
||||
expect(unchanged_three.inputs.buttonY &&
|
||||
!unchanged_three.inputs.buttonHome,
|
||||
"instance 0 input change leaked into instance 3");
|
||||
|
||||
SwitchInputState changed_three = states[3];
|
||||
changed_three.button_y = false;
|
||||
changed_three.button_capture = true;
|
||||
switch_pro_set_input(3, changed_three);
|
||||
now_ms = 45;
|
||||
expect(switch_pro_task(3),
|
||||
"instance 3 did not apply its changed input state");
|
||||
SwitchProReport unchanged_zero = get_current_report(
|
||||
0, "GET_REPORT failed for instance 0 after instance 3 changed");
|
||||
expect(unchanged_zero.inputs.buttonHome &&
|
||||
!unchanged_zero.inputs.buttonCapture,
|
||||
"instance 3 input change leaked into instance 0");
|
||||
}
|
||||
|
||||
void test_callback_send_and_imu_modes_are_isolated() {
|
||||
initialize_contexts();
|
||||
send_feature(0, TOGGLE_IMU, 1);
|
||||
now_ms = 6;
|
||||
expect(!switch_pro_task(0), "feature reply was reported as regular input");
|
||||
expect(reports_for_instance(0) == 1,
|
||||
"feature callback reply did not use instance 0");
|
||||
expect(reports_for_instance(1) == 0,
|
||||
"feature callback queued a reply on instance 1");
|
||||
|
||||
SwitchInputState zero{};
|
||||
zero.lx = zero.ly = zero.rx = zero.ry = SWITCH_PRO_JOYSTICK_MID;
|
||||
zero.imu_sample_count = 1;
|
||||
zero.imu_samples[0] = {101, 202, 303, 404, 505, 606};
|
||||
SwitchInputState one = zero;
|
||||
one.button_x = true;
|
||||
one.imu_samples[0] = {1001, 2002, 3003, 4004, 5005, 6006};
|
||||
switch_pro_set_input(0, zero);
|
||||
switch_pro_set_input(1, one);
|
||||
now_ms = 21;
|
||||
expect(switch_pro_task(0), "raw-IMU instance did not send input");
|
||||
expect(switch_pro_task(1), "off-IMU instance timer did not send input");
|
||||
SwitchProReport raw = copy_switch_report(latest_regular_report(0));
|
||||
SwitchProReport off = copy_switch_report(latest_regular_report(1));
|
||||
expect(read_int16_le(raw.imuData) == 101 &&
|
||||
read_int16_le(raw.imuData + 6) == 404,
|
||||
"instance 0 raw IMU sample was not preserved");
|
||||
std::array<uint8_t, 36> zero_imu{};
|
||||
expect(std::memcmp(off.imuData, zero_imu.data(), zero_imu.size()) == 0,
|
||||
"instance 0 IMU mode leaked into instance 1");
|
||||
|
||||
initialize_contexts();
|
||||
send_feature(0, TOGGLE_IMU, 2);
|
||||
send_feature(1, TOGGLE_IMU, 2);
|
||||
now_ms = 6;
|
||||
switch_pro_task(0);
|
||||
switch_pro_task(1);
|
||||
SwitchInputState moving{};
|
||||
moving.lx = moving.ly = moving.rx = moving.ry = SWITCH_PRO_JOYSTICK_MID;
|
||||
moving.imu_sample_count = 1;
|
||||
moving.imu_samples[0] = {100, 200, 300, 20000, 0, 0};
|
||||
SwitchInputState stationary{};
|
||||
stationary.lx = stationary.ly = stationary.rx = stationary.ry =
|
||||
SWITCH_PRO_JOYSTICK_MID;
|
||||
stationary.imu_sample_count = 1;
|
||||
stationary.imu_samples[0] = {1000, 2000, 3000, 0, 0, 0};
|
||||
switch_pro_set_input(0, moving);
|
||||
switch_pro_set_input(1, stationary);
|
||||
now_ms = 21;
|
||||
expect(switch_pro_task(0), "moving quaternion instance did not report");
|
||||
expect(switch_pro_task(1), "stationary quaternion timer crossed instances");
|
||||
SwitchProReport moving_report =
|
||||
copy_switch_report(latest_regular_report(0));
|
||||
SwitchProReport stationary_report =
|
||||
copy_switch_report(latest_regular_report(1));
|
||||
bool moving_component =
|
||||
read_bits_le(moving_report.imuData, 52, 21) != 0 ||
|
||||
read_bits_le(moving_report.imuData, 73, 21) != 0 ||
|
||||
read_bits_le(moving_report.imuData, 94, 2) != 0 ||
|
||||
read_bits_le(moving_report.imuData, 144, 19) != 0;
|
||||
bool stationary_component =
|
||||
read_bits_le(stationary_report.imuData, 52, 21) != 0 ||
|
||||
read_bits_le(stationary_report.imuData, 73, 21) != 0 ||
|
||||
read_bits_le(stationary_report.imuData, 94, 2) != 0 ||
|
||||
read_bits_le(stationary_report.imuData, 144, 19) != 0;
|
||||
expect(moving_component, "moving quaternion did not integrate");
|
||||
expect(!stationary_component,
|
||||
"instance 0 quaternion state leaked into instance 1");
|
||||
expect(read_int16_le(stationary_report.imuData) == 2000 &&
|
||||
read_int16_le(stationary_report.imuData + 2) == 1000,
|
||||
"instance 1 quaternion accelerometer state was overwritten");
|
||||
}
|
||||
|
||||
void test_grip_colors_are_isolated() {
|
||||
initialize_contexts();
|
||||
constexpr uint32_t grip_address =
|
||||
0x6000u + offsetof(SwitchFactoryConfig, leftGripColor);
|
||||
constexpr uint8_t grip_bytes =
|
||||
sizeof(SwitchColorDefinition) * 2u;
|
||||
constexpr SwitchRgbColor calibrated_blue =
|
||||
switch_pro_calibrate_light_color({0x00, 0x89, 0xEB});
|
||||
constexpr SwitchRgbColor calibrated_gray =
|
||||
switch_pro_calibrate_light_color({0x96, 0x96, 0x96});
|
||||
static_assert(calibrated_blue.red == 0x00 &&
|
||||
calibrated_blue.green == 0x35 &&
|
||||
calibrated_blue.blue == 0x9D);
|
||||
static_assert(calibrated_gray.red == 0x64 &&
|
||||
calibrated_gray.green == 0x64 &&
|
||||
calibrated_gray.blue == 0x64);
|
||||
|
||||
|
||||
for (uint8_t instance = 0; instance < kInstanceCount; ++instance) {
|
||||
send_spi_read(instance, grip_address, grip_bytes);
|
||||
now_ms += 6;
|
||||
expect(!switch_pro_task(instance),
|
||||
"grip color SPI reply counted as regular input");
|
||||
expect(sent_report_count == static_cast<unsigned>(instance + 1u),
|
||||
"grip color SPI reply was not sent");
|
||||
const SentReport& response = sent_reports[sent_report_count - 1u];
|
||||
const SwitchRgbColor expected =
|
||||
switch_pro_get_slot_color(instance);
|
||||
const uint8_t expected_bytes[] = {
|
||||
expected.red, expected.green, expected.blue,
|
||||
expected.red, expected.green, expected.blue,
|
||||
};
|
||||
expect(response.instance == instance &&
|
||||
response.data[13] == 0x90 &&
|
||||
response.data[14] == SPI_READ &&
|
||||
std::memcmp(response.data.data() + 20, expected_bytes,
|
||||
sizeof(expected_bytes)) == 0,
|
||||
"Switch grip color did not match its HID slot");
|
||||
const SwitchRgbColor light =
|
||||
switch_pro_get_slot_light_color(instance);
|
||||
const SwitchRgbColor calibrated =
|
||||
switch_pro_calibrate_light_color(expected);
|
||||
expect(light.red == calibrated.red &&
|
||||
light.green == calibrated.green &&
|
||||
light.blue == calibrated.blue,
|
||||
"physical controller light was not derived from its grip");
|
||||
}
|
||||
|
||||
const SwitchRgbColor invalid_grip =
|
||||
switch_pro_get_slot_color(kInvalidInstance);
|
||||
const SwitchRgbColor invalid_light =
|
||||
switch_pro_get_slot_light_color(kInvalidInstance);
|
||||
expect(invalid_grip.red == 0 && invalid_grip.green == 0 &&
|
||||
invalid_grip.blue == 0 && invalid_light.red == 0 &&
|
||||
invalid_light.green == 0 && invalid_light.blue == 0,
|
||||
"invalid HID slot returned a configured color");
|
||||
}
|
||||
|
||||
void test_rumble_callbacks_and_decoders_are_isolated() {
|
||||
initialize_contexts();
|
||||
rumble_events = {};
|
||||
for (uint8_t instance = 0; instance < kInstanceCount; ++instance) {
|
||||
switch_pro_set_rumble_callback(instance, rumble_callback);
|
||||
}
|
||||
constexpr uint32_t neutral = 0x40400100u;
|
||||
auto full_payload = rumble_payload(type_2(64, 16, 64, 16), neutral);
|
||||
auto full_report = complete_rumble_report(full_payload);
|
||||
tud_hid_report_received_cb(kInvalidInstance, 0, full_report.data(),
|
||||
full_report.size());
|
||||
for (const auto& event : rumble_events) {
|
||||
expect(event.count == 0,
|
||||
"invalid output instance reached a rumble callback");
|
||||
}
|
||||
|
||||
std::array<uint8_t, 9> stripped{};
|
||||
std::memcpy(stripped.data() + 1, full_payload.data(), full_payload.size());
|
||||
tud_hid_set_report_cb(0, REPORT_OUTPUT_10, HID_REPORT_TYPE_OUTPUT,
|
||||
stripped.data(), stripped.size());
|
||||
expect(rumble_events[0].count == 1 && rumble_events[0].instance == 0,
|
||||
"control output did not route to instance 0 callback");
|
||||
expect(rumble_events[0].output.low_frequency_magnitude == 16 &&
|
||||
rumble_events[0].output.high_frequency_magnitude == 16,
|
||||
"instance 0 full rumble state decoded incorrectly");
|
||||
for (uint8_t instance = 1; instance < kInstanceCount; ++instance) {
|
||||
expect(rumble_events[instance].count == 0,
|
||||
"instance 0 rumble invoked another instance callback");
|
||||
}
|
||||
|
||||
auto delta_payload = rumble_payload(type_1_one_sample(17, 20), neutral);
|
||||
auto delta_report = complete_rumble_report(delta_payload);
|
||||
tud_hid_report_received_cb(1, 0, delta_report.data(), delta_report.size());
|
||||
expect(rumble_events[1].count == 1 && rumble_events[1].instance == 1,
|
||||
"interrupt output did not route to instance 1 callback");
|
||||
expect(rumble_events[1].output.low_frequency_magnitude == 0 &&
|
||||
rumble_events[1].output.high_frequency_magnitude == 1,
|
||||
"instance 1 decoder inherited instance 0 rumble state");
|
||||
tud_hid_report_received_cb(0, 0, delta_report.data(), delta_report.size());
|
||||
expect(rumble_events[0].count == 2 &&
|
||||
rumble_events[0].output.low_frequency_magnitude == 17 &&
|
||||
rumble_events[0].output.high_frequency_magnitude == 18,
|
||||
"instance 0 decoder lost its own prior rumble state");
|
||||
|
||||
for (uint8_t instance = 2; instance < kInstanceCount; ++instance) {
|
||||
const uint8_t magnitude = instance == 2 ? 16 : 32;
|
||||
auto payload =
|
||||
rumble_payload(type_2(64, magnitude, 64, magnitude), neutral);
|
||||
auto report = complete_rumble_report(payload);
|
||||
tud_hid_report_received_cb(instance, 0, report.data(), report.size());
|
||||
expect(rumble_events[instance].count == 1 &&
|
||||
rumble_events[instance].instance == instance,
|
||||
"rumble output did not route to its configured instance");
|
||||
expect(rumble_events[instance].output.low_frequency_magnitude ==
|
||||
magnitude &&
|
||||
rumble_events[instance].output.high_frequency_magnitude ==
|
||||
magnitude,
|
||||
"configured instance decoded another rumble context");
|
||||
}
|
||||
expect(rumble_events[1].count == 1,
|
||||
"another instance's rumble reached instance 1 callback");
|
||||
}
|
||||
|
||||
void test_lifecycle_and_invalid_instances() {
|
||||
initialize_contexts();
|
||||
for (uint8_t instance = 0; instance < kInstanceCount; ++instance) {
|
||||
expect(switch_pro_is_ready(instance),
|
||||
"initialized context was not ready");
|
||||
}
|
||||
|
||||
tud_mount_cb();
|
||||
for (uint8_t instance = 0; instance < kInstanceCount; ++instance) {
|
||||
expect(!switch_pro_is_ready(instance),
|
||||
"mount did not reset every configured context");
|
||||
}
|
||||
|
||||
for (uint8_t addressed = 0; addressed < kInstanceCount; ++addressed) {
|
||||
send_config(addressed, DISABLE_USB_TIMEOUT);
|
||||
for (uint8_t instance = 0; instance < kInstanceCount; ++instance) {
|
||||
expect(switch_pro_is_ready(instance) == (instance <= addressed),
|
||||
"handshake readiness crossed configured contexts");
|
||||
}
|
||||
}
|
||||
|
||||
tud_umount_cb();
|
||||
for (uint8_t instance = 0; instance < kInstanceCount; ++instance) {
|
||||
expect(!switch_pro_is_ready(instance),
|
||||
"unmount did not reset every configured context");
|
||||
}
|
||||
|
||||
SwitchInputState ignored{};
|
||||
ignored.button_home = true;
|
||||
switch_pro_init(kInvalidInstance);
|
||||
switch_pro_set_input(kInvalidInstance, ignored);
|
||||
switch_pro_set_rumble_callback(kInvalidInstance, rumble_callback);
|
||||
expect(!switch_pro_task(kInvalidInstance),
|
||||
"invalid instance ran a driver task");
|
||||
expect(!switch_pro_is_ready(kInvalidInstance),
|
||||
"invalid instance reported ready");
|
||||
std::array<uint8_t, SWITCH_PRO_ENDPOINT_SIZE> buffer{};
|
||||
expect(tud_hid_get_report_cb(kInvalidInstance, 0, HID_REPORT_TYPE_INPUT,
|
||||
buffer.data(), buffer.size()) == 0,
|
||||
"invalid instance served GET_REPORT data");
|
||||
expect(tud_hid_descriptor_report_cb(kInvalidInstance) == nullptr,
|
||||
"invalid instance served a report descriptor");
|
||||
}
|
||||
|
||||
void test_uart_parser_is_pure() {
|
||||
initialize_contexts();
|
||||
SwitchInputState driver_state{};
|
||||
driver_state.lx = driver_state.ly = driver_state.rx = driver_state.ry =
|
||||
SWITCH_PRO_JOYSTICK_MID;
|
||||
driver_state.button_x = true;
|
||||
switch_pro_set_input(0, driver_state);
|
||||
now_ms = 15;
|
||||
switch_pro_task(0);
|
||||
|
||||
std::array<uint8_t, 12> packet{};
|
||||
packet[0] = 0xaa;
|
||||
packet[1] = 0x02;
|
||||
packet[2] = 8;
|
||||
uint16_t buttons = SWITCH_PRO_MASK_A | SWITCH_PRO_MASK_L;
|
||||
packet[3] = static_cast<uint8_t>(buttons);
|
||||
packet[4] = static_cast<uint8_t>(buttons >> 8u);
|
||||
packet[5] = SWITCH_PRO_HAT_DOWNLEFT;
|
||||
packet[6] = 0x12;
|
||||
packet[7] = 0x34;
|
||||
packet[8] = 0x56;
|
||||
packet[9] = 0x78;
|
||||
for (unsigned i = 0; i < packet.size() - 1; ++i) {
|
||||
packet.back() = static_cast<uint8_t>(packet.back() + packet[i]);
|
||||
}
|
||||
SwitchInputState parsed{};
|
||||
expect(switch_pro_apply_uart_packet(packet.data(), packet.size(), parsed),
|
||||
"valid UART packet was rejected");
|
||||
expect(parsed.button_a && parsed.button_l && parsed.dpad_down &&
|
||||
parsed.dpad_left,
|
||||
"UART buttons or hat were parsed incorrectly");
|
||||
expect(parsed.lx == 0x1212 && parsed.ly == 0x3434 &&
|
||||
parsed.rx == 0x5656 && parsed.ry == 0x7878,
|
||||
"UART axes were parsed incorrectly");
|
||||
|
||||
std::array<uint8_t, SWITCH_PRO_ENDPOINT_SIZE> current{};
|
||||
tud_hid_get_report_cb(0, 0, HID_REPORT_TYPE_INPUT, current.data(),
|
||||
current.size());
|
||||
SwitchProReport current_report{};
|
||||
std::memcpy(¤t_report, current.data(), sizeof(current_report));
|
||||
expect(current_report.inputs.buttonX && !current_report.inputs.buttonA,
|
||||
"UART parsing mutated driver context state");
|
||||
|
||||
SwitchInputState unchanged{};
|
||||
unchanged.button_home = true;
|
||||
unchanged.lx = 123;
|
||||
packet.back() ^= 0xffu;
|
||||
expect(!switch_pro_apply_uart_packet(packet.data(), packet.size(),
|
||||
unchanged),
|
||||
"invalid UART checksum was accepted");
|
||||
expect(unchanged.button_home && unchanged.lx == 123,
|
||||
"failed UART parse modified its output reference");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" absolute_time_t get_absolute_time(void) {
|
||||
return {now_ms};
|
||||
}
|
||||
extern "C" uint32_t to_ms_since_boot(absolute_time_t time) {
|
||||
return static_cast<uint32_t>(time.milliseconds);
|
||||
}
|
||||
extern "C" uint32_t get_rand_32(void) {
|
||||
return random_value++;
|
||||
}
|
||||
extern "C" bool tud_hid_n_ready(uint8_t instance) {
|
||||
return instance < SWITCH_PICO_HID_INSTANCE_COUNT && hid_ready[instance];
|
||||
}
|
||||
extern "C" bool tud_hid_n_report(uint8_t instance, uint8_t report_id,
|
||||
const void* report, uint16_t length) {
|
||||
if (instance >= SWITCH_PICO_HID_INSTANCE_COUNT || report == nullptr ||
|
||||
length > SWITCH_PRO_ENDPOINT_SIZE) {
|
||||
return false;
|
||||
}
|
||||
++hid_report_attempts[instance];
|
||||
if (!hid_report_succeeds[instance] ||
|
||||
sent_report_count >= sent_reports.size()) {
|
||||
return false;
|
||||
}
|
||||
SentReport& sent = sent_reports[sent_report_count++];
|
||||
sent.instance = instance;
|
||||
sent.report_id = report_id;
|
||||
sent.length = length;
|
||||
std::memcpy(sent.data.data(), report, length);
|
||||
return true;
|
||||
}
|
||||
extern "C" bool tud_suspended(void) {
|
||||
return false;
|
||||
}
|
||||
extern "C" bool tud_remote_wakeup(void) {
|
||||
return true;
|
||||
}
|
||||
|
||||
int main() {
|
||||
test_reset_materializes_neutral_sticks();
|
||||
test_startup_identify_preserves_first_reply_counter();
|
||||
test_failed_startup_identify_retries_preserve_counter();
|
||||
test_input_reports_and_timers_are_isolated();
|
||||
test_callback_send_and_imu_modes_are_isolated();
|
||||
test_rumble_callbacks_and_decoders_are_isolated();
|
||||
test_grip_colors_are_isolated();
|
||||
test_lifecycle_and_invalid_instances();
|
||||
test_uart_parser_is_pure();
|
||||
if (failures != 0) {
|
||||
std::cerr << failures << " driver context test(s) failed\n";
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
46
tests/test_bluepad32_backend_lifecycle_native.py
Normal file
46
tests/test_bluepad32_backend_lifecycle_native.py
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def test_bluepad32_backend_lifecycle_native(tmp_path: Path) -> None:
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
compiler = shutil.which("c++") or shutil.which("g++")
|
||||
assert compiler is not None, "a host C++ compiler is required"
|
||||
|
||||
executable = tmp_path / "bluepad32_backend_lifecycle_test"
|
||||
subprocess.run(
|
||||
[
|
||||
compiler,
|
||||
"-std=c++17",
|
||||
"-Wall",
|
||||
"-Wextra",
|
||||
"-Werror",
|
||||
"-pedantic",
|
||||
"-DSWITCH_PICO_HID_INSTANCE_COUNT=4",
|
||||
f"-I{root / 'tests' / 'bluepad32_native_stubs'}",
|
||||
f"-I{root}",
|
||||
str(root / "tests" / "bluepad32_backend_lifecycle_test.cpp"),
|
||||
"-o",
|
||||
str(executable),
|
||||
],
|
||||
check=True,
|
||||
cwd=root,
|
||||
)
|
||||
|
||||
for scenario in (
|
||||
"ready-forward",
|
||||
"ready-reverse",
|
||||
"rejections",
|
||||
"lifecycle",
|
||||
"pairing-policy",
|
||||
"slot-lighting",
|
||||
"abxy-hotkey",
|
||||
"motion-hotkey",
|
||||
"clear-pairings",
|
||||
"flash-core-start",
|
||||
"flash-core-failure",
|
||||
):
|
||||
subprocess.run([str(executable), scenario], check=True, cwd=root)
|
||||
31
tests/test_bluepad32_imu_normalization_native.py
Normal file
31
tests/test_bluepad32_imu_normalization_native.py
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def test_bluepad32_imu_normalization_native(tmp_path: Path) -> None:
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
compiler = shutil.which("c++") or shutil.which("g++")
|
||||
assert compiler is not None, "a host C++ compiler is required"
|
||||
|
||||
executable = tmp_path / "bluepad32_imu_normalization_test"
|
||||
subprocess.run(
|
||||
[
|
||||
compiler,
|
||||
"-std=c++17",
|
||||
"-Wall",
|
||||
"-Wextra",
|
||||
"-Werror",
|
||||
"-pedantic",
|
||||
f"-I{root / 'bluepad32_config'}",
|
||||
f"-I{root / 'external' / 'bluepad32' / 'src' / 'components' / 'bluepad32' / 'include'}",
|
||||
str(root / "tests" / "bluepad32_imu_normalization_test.cpp"),
|
||||
"-o",
|
||||
str(executable),
|
||||
],
|
||||
check=True,
|
||||
cwd=root,
|
||||
)
|
||||
subprocess.run([str(executable)], check=True, cwd=root)
|
||||
34
tests/test_bootsel_pairing_button_native.py
Normal file
34
tests/test_bootsel_pairing_button_native.py
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def test_bootsel_pairing_button_native(tmp_path: Path) -> None:
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
compiler = shutil.which("c++") or shutil.which("g++")
|
||||
assert compiler is not None, "a host C++ compiler is required"
|
||||
|
||||
for platform, rp2350 in (("rp2350", 1), ("rp2040", 0)):
|
||||
executable = tmp_path / f"bootsel_pairing_button_test_{platform}"
|
||||
subprocess.run(
|
||||
[
|
||||
compiler,
|
||||
"-std=c++17",
|
||||
"-Wall",
|
||||
"-Wextra",
|
||||
"-Werror",
|
||||
"-pedantic",
|
||||
f"-DPICO_RP2350={rp2350}",
|
||||
f"-I{root / 'tests' / 'bootsel_native_stubs'}",
|
||||
f"-I{root}",
|
||||
str(root / "bootsel_pairing_button.cpp"),
|
||||
str(root / "tests" / "bootsel_pairing_button_test.cpp"),
|
||||
"-o",
|
||||
str(executable),
|
||||
],
|
||||
check=True,
|
||||
cwd=root,
|
||||
)
|
||||
subprocess.run([str(executable)], check=True, cwd=root)
|
||||
156
tests/test_pairing_manager.py
Normal file
156
tests/test_pairing_manager.py
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import struct
|
||||
|
||||
import pytest
|
||||
|
||||
import switch_pico_bridge.pairing_manager as pairing_manager
|
||||
|
||||
|
||||
def make_payload(
|
||||
generation: int,
|
||||
records: list[tuple[int, int, bytes]],
|
||||
*,
|
||||
status: int = pairing_manager.STATUS_READY,
|
||||
overflow: bool = False,
|
||||
) -> bytes:
|
||||
payload = bytearray(b"SPPM")
|
||||
payload.extend(
|
||||
[
|
||||
pairing_manager.PROTOCOL_VERSION,
|
||||
status,
|
||||
len(records),
|
||||
int(overflow),
|
||||
]
|
||||
)
|
||||
payload.extend(struct.pack("<I", generation))
|
||||
for transport, address_type, address in records:
|
||||
payload.extend([transport, address_type])
|
||||
payload.extend(address)
|
||||
return bytes(payload)
|
||||
|
||||
|
||||
class FakeDevice:
|
||||
bus = 1
|
||||
address = 7
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.generation = 3
|
||||
self.records = [
|
||||
(
|
||||
pairing_manager.TRANSPORT_CLASSIC,
|
||||
0xFE,
|
||||
bytes.fromhex("010203040506"),
|
||||
),
|
||||
(
|
||||
pairing_manager.TRANSPORT_BLE,
|
||||
2,
|
||||
bytes.fromhex("A1A2A3A4A5A6"),
|
||||
),
|
||||
]
|
||||
self.requests: list[int] = []
|
||||
|
||||
def ctrl_transfer(
|
||||
self,
|
||||
bm_request_type: int,
|
||||
request: int,
|
||||
value: int,
|
||||
index: int,
|
||||
data_or_w_length: object,
|
||||
timeout: int,
|
||||
) -> bytes | int:
|
||||
assert value == pairing_manager.REQUEST_VALUE
|
||||
assert index == pairing_manager.REQUEST_INDEX
|
||||
assert timeout == pairing_manager.USB_TIMEOUT_MS
|
||||
self.requests.append(request)
|
||||
if bm_request_type == 0xC0:
|
||||
assert request == pairing_manager.REQUEST_GET
|
||||
return make_payload(self.generation, self.records)
|
||||
assert bm_request_type == 0x40
|
||||
if request == pairing_manager.REQUEST_REFRESH:
|
||||
self.generation += 1
|
||||
elif request == pairing_manager.REQUEST_CLEAR:
|
||||
self.records = []
|
||||
self.generation += 1
|
||||
else:
|
||||
raise AssertionError(f"unexpected request {request}")
|
||||
return 0
|
||||
|
||||
|
||||
def test_parse_snapshot() -> None:
|
||||
snapshot = pairing_manager.parse_snapshot(
|
||||
make_payload(
|
||||
0x78563412,
|
||||
[
|
||||
(
|
||||
pairing_manager.TRANSPORT_CLASSIC,
|
||||
0xFE,
|
||||
bytes.fromhex("010203040506"),
|
||||
),
|
||||
(
|
||||
pairing_manager.TRANSPORT_BLE,
|
||||
3,
|
||||
bytes.fromhex("A1A2A3A4A5A6"),
|
||||
),
|
||||
],
|
||||
overflow=True,
|
||||
)
|
||||
)
|
||||
assert snapshot.generation == 0x78563412
|
||||
assert snapshot.overflow
|
||||
assert snapshot.records[0].transport_text == "Classic"
|
||||
assert snapshot.records[0].address_text == "01:02:03:04:05:06"
|
||||
assert snapshot.records[1].transport_text == "BLE (random identity)"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"payload",
|
||||
[
|
||||
b"",
|
||||
b"NOPE" + bytes(8),
|
||||
b"SPPM\x02" + bytes(7),
|
||||
b"SPPM\x01\x00\x11\x00" + bytes(4),
|
||||
],
|
||||
)
|
||||
def test_parse_rejects_invalid_payload(payload: bytes) -> None:
|
||||
with pytest.raises(pairing_manager.PairingManagerError):
|
||||
pairing_manager.parse_snapshot(payload)
|
||||
|
||||
|
||||
def test_list_and_clear_commands(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
) -> None:
|
||||
device = FakeDevice()
|
||||
monkeypatch.setattr(pairing_manager, "_candidate_devices", lambda: [device])
|
||||
|
||||
assert pairing_manager.main(["list"]) == 0
|
||||
output = capsys.readouterr().out
|
||||
assert "Classic 01:02:03:04:05:06" in output
|
||||
assert "BLE (public identity) A1:A2:A3:A4:A5:A6" in output
|
||||
|
||||
assert pairing_manager.main(["clear"]) == 2
|
||||
assert "requires --yes" in capsys.readouterr().err
|
||||
|
||||
assert pairing_manager.main(["clear", "--yes"]) == 0
|
||||
assert capsys.readouterr().out == "Cleared 2 stored pairing(s).\n"
|
||||
assert device.records == []
|
||||
assert pairing_manager.REQUEST_REFRESH in device.requests
|
||||
assert pairing_manager.REQUEST_CLEAR in device.requests
|
||||
|
||||
|
||||
def test_find_requires_selector_for_multiple_picos(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
first = FakeDevice()
|
||||
second = FakeDevice()
|
||||
second.address = 8
|
||||
monkeypatch.setattr(
|
||||
pairing_manager, "_candidate_devices", lambda: [first, second]
|
||||
)
|
||||
with pytest.raises(
|
||||
pairing_manager.PairingManagerError,
|
||||
match="multiple switch-pico devices",
|
||||
):
|
||||
pairing_manager.find_pico(None, None)
|
||||
assert pairing_manager.find_pico(1, 8) is second
|
||||
89
tests/test_switch_pro_descriptors_native.py
Normal file
89
tests/test_switch_pro_descriptors_native.py
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def compile_descriptor_test(
|
||||
root: Path,
|
||||
compiler: str,
|
||||
output: Path,
|
||||
expected_count: int,
|
||||
configured_count: int | None,
|
||||
) -> subprocess.CompletedProcess[str]:
|
||||
command = [
|
||||
compiler,
|
||||
"-std=c++17",
|
||||
"-Wall",
|
||||
"-Wextra",
|
||||
"-Werror",
|
||||
"-pedantic",
|
||||
f"-DEXPECTED_HID_INSTANCE_COUNT={expected_count}",
|
||||
]
|
||||
if configured_count is not None:
|
||||
command.append(f"-DSWITCH_PICO_HID_INSTANCE_COUNT={configured_count}")
|
||||
command.extend(
|
||||
[
|
||||
f"-I{root}",
|
||||
str(root / "tests" / "switch_pro_descriptors_test.cpp"),
|
||||
"-o",
|
||||
str(output),
|
||||
]
|
||||
)
|
||||
return subprocess.run(
|
||||
command,
|
||||
check=False,
|
||||
cwd=root,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
)
|
||||
|
||||
|
||||
def host_compiler() -> str:
|
||||
compiler = shutil.which("c++") or shutil.which("g++")
|
||||
assert compiler is not None, "a host C++ compiler is required"
|
||||
return compiler
|
||||
|
||||
|
||||
def test_default_descriptor_contract_is_single_hid(tmp_path: Path) -> None:
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
executable = tmp_path / "switch_pro_descriptors_default_test"
|
||||
result = compile_descriptor_test(root, host_compiler(), executable, 1, None)
|
||||
assert result.returncode == 0, result.stderr
|
||||
subprocess.run([str(executable)], check=True, cwd=root)
|
||||
|
||||
|
||||
def test_supported_descriptor_contracts(tmp_path: Path) -> None:
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
compiler = host_compiler()
|
||||
for instance_count in range(1, 5):
|
||||
executable = (
|
||||
tmp_path / f"switch_pro_descriptors_{instance_count}_test"
|
||||
)
|
||||
result = compile_descriptor_test(
|
||||
root,
|
||||
compiler,
|
||||
executable,
|
||||
instance_count,
|
||||
instance_count,
|
||||
)
|
||||
assert result.returncode == 0, result.stderr
|
||||
subprocess.run([str(executable)], check=True, cwd=root)
|
||||
|
||||
|
||||
def test_unsupported_hid_instance_counts_fail_to_compile(tmp_path: Path) -> None:
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
compiler = host_compiler()
|
||||
for unsupported_count in (0, 5):
|
||||
executable = tmp_path / f"switch_pro_descriptors_invalid_{unsupported_count}"
|
||||
result = compile_descriptor_test(
|
||||
root,
|
||||
compiler,
|
||||
executable,
|
||||
unsupported_count,
|
||||
unsupported_count,
|
||||
)
|
||||
assert result.returncode != 0, (
|
||||
f"unsupported HID instance count {unsupported_count} compiled successfully"
|
||||
)
|
||||
34
tests/test_switch_pro_driver_context_native.py
Normal file
34
tests/test_switch_pro_driver_context_native.py
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def test_switch_pro_driver_four_contexts_native(tmp_path: Path) -> None:
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
compiler = shutil.which("c++") or shutil.which("g++")
|
||||
assert compiler is not None, "a host C++ compiler is required"
|
||||
|
||||
executable = tmp_path / "switch_pro_driver_context_test"
|
||||
subprocess.run(
|
||||
[
|
||||
compiler,
|
||||
"-std=c++17",
|
||||
"-Wall",
|
||||
"-Wextra",
|
||||
"-Werror",
|
||||
"-pedantic",
|
||||
"-DSWITCH_PICO_HID_INSTANCE_COUNT=4",
|
||||
f"-I{root / 'tests' / 'native_stubs'}",
|
||||
f"-I{root}",
|
||||
str(root / "switch_pro_driver.cpp"),
|
||||
str(root / "switch_haptics.cpp"),
|
||||
str(root / "tests" / "switch_pro_driver_context_test.cpp"),
|
||||
"-o",
|
||||
str(executable),
|
||||
],
|
||||
check=True,
|
||||
cwd=root,
|
||||
)
|
||||
subprocess.run([str(executable)], check=True, cwd=root)
|
||||
30
tests/test_usb_pairing_management_native.py
Normal file
30
tests/test_usb_pairing_management_native.py
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
|
||||
def test_usb_pairing_management_native(tmp_path: Path) -> None:
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
compiler = shutil.which("c++") or shutil.which("g++")
|
||||
assert compiler is not None, "a host C++ compiler is required"
|
||||
|
||||
executable = tmp_path / "usb_pairing_management_test"
|
||||
subprocess.run(
|
||||
[
|
||||
compiler,
|
||||
"-std=c++17",
|
||||
"-Wall",
|
||||
"-Wextra",
|
||||
"-Werror",
|
||||
"-pedantic",
|
||||
f"-I{root / 'tests' / 'usb_management_native_stubs'}",
|
||||
f"-I{root}",
|
||||
str(root / "tests" / "usb_pairing_management_test.cpp"),
|
||||
"-o",
|
||||
str(executable),
|
||||
],
|
||||
check=True,
|
||||
cwd=root,
|
||||
)
|
||||
subprocess.run([str(executable)], check=True, cwd=root)
|
||||
43
tests/usb_management_native_stubs/tusb.h
Normal file
43
tests/usb_management_native_stubs/tusb.h
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
enum {
|
||||
CONTROL_STAGE_SETUP = 0,
|
||||
CONTROL_STAGE_DATA = 1,
|
||||
CONTROL_STAGE_ACK = 2,
|
||||
TUSB_REQ_RCPT_DEVICE = 0,
|
||||
TUSB_DIR_OUT = 0,
|
||||
TUSB_DIR_IN = 1,
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
uint8_t recipient;
|
||||
uint8_t type;
|
||||
uint8_t direction;
|
||||
} tusb_request_type_bits_t;
|
||||
|
||||
typedef struct {
|
||||
tusb_request_type_bits_t bmRequestType_bit;
|
||||
uint8_t bRequest;
|
||||
uint16_t wValue;
|
||||
uint16_t wIndex;
|
||||
uint16_t wLength;
|
||||
} tusb_control_request_t;
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
bool tud_control_xfer(uint8_t rhport,
|
||||
const tusb_control_request_t* request,
|
||||
void* buffer, uint16_t length);
|
||||
bool tud_control_status(uint8_t rhport,
|
||||
const tusb_control_request_t* request);
|
||||
bool tud_vendor_control_xfer_cb(
|
||||
uint8_t rhport, uint8_t stage,
|
||||
const tusb_control_request_t* request);
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
144
tests/usb_pairing_management_test.cpp
Normal file
144
tests/usb_pairing_management_test.cpp
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
#include "usb_pairing_management.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <vector>
|
||||
|
||||
#include <tusb.h>
|
||||
|
||||
namespace {
|
||||
|
||||
Bluepad32PairingSnapshot current_snapshot{};
|
||||
bool refresh_requested = false;
|
||||
bool clear_requested = false;
|
||||
bool control_status_sent = false;
|
||||
std::vector<uint8_t> control_payload;
|
||||
|
||||
void require(bool condition, const char* message) {
|
||||
if (!condition) {
|
||||
std::cerr << message << '\n';
|
||||
std::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
void test_encoding() {
|
||||
Bluepad32PairingSnapshot snapshot{};
|
||||
snapshot.generation = 0x78563412;
|
||||
snapshot.status = Bluepad32PairingSnapshotStatus::kReady;
|
||||
snapshot.record_count = 2;
|
||||
snapshot.overflow = true;
|
||||
snapshot.records[0].transport =
|
||||
Bluepad32PairingTransport::kClassic;
|
||||
snapshot.records[0].address_type = 0xfe;
|
||||
const uint8_t classic_address[6] = {1, 2, 3, 4, 5, 6};
|
||||
memcpy(snapshot.records[0].address, classic_address, 6);
|
||||
snapshot.records[1].transport = Bluepad32PairingTransport::kBle;
|
||||
snapshot.records[1].address_type = 2;
|
||||
const uint8_t ble_address[6] = {6, 5, 4, 3, 2, 1};
|
||||
memcpy(snapshot.records[1].address, ble_address, 6);
|
||||
|
||||
uint8_t payload[UsbPairingManagement::kMaximumResponseSize]{};
|
||||
const size_t size = UsbPairingManagement::encode_snapshot(
|
||||
snapshot, payload, sizeof(payload));
|
||||
require(size == UsbPairingManagement::kResponseHeaderSize +
|
||||
2 * UsbPairingManagement::kRecordSize,
|
||||
"snapshot encoded with the wrong size");
|
||||
require(memcmp(payload, "SPPM", 4) == 0 &&
|
||||
payload[4] == UsbPairingManagement::kProtocolVersion &&
|
||||
payload[5] == 0 && payload[6] == 2 && payload[7] == 1,
|
||||
"snapshot header encoding is invalid");
|
||||
require(payload[8] == 0x12 && payload[9] == 0x34 &&
|
||||
payload[10] == 0x56 && payload[11] == 0x78,
|
||||
"snapshot generation is not little endian");
|
||||
require(payload[12] == 1 && payload[13] == 0xfe &&
|
||||
memcmp(&payload[14], classic_address, 6) == 0 &&
|
||||
payload[20] == 2 && payload[21] == 2 &&
|
||||
memcmp(&payload[22], ble_address, 6) == 0,
|
||||
"pairing records are encoded incorrectly");
|
||||
require(UsbPairingManagement::encode_snapshot(
|
||||
snapshot, payload, size - 1) == 0,
|
||||
"encoder accepted a short destination buffer");
|
||||
}
|
||||
|
||||
void test_vendor_requests() {
|
||||
current_snapshot = {};
|
||||
current_snapshot.generation = 7;
|
||||
current_snapshot.status = Bluepad32PairingSnapshotStatus::kReady;
|
||||
current_snapshot.record_count = 1;
|
||||
current_snapshot.records[0].transport =
|
||||
Bluepad32PairingTransport::kClassic;
|
||||
|
||||
tusb_control_request_t request{};
|
||||
request.bmRequestType_bit.recipient = TUSB_REQ_RCPT_DEVICE;
|
||||
request.bmRequestType_bit.direction = TUSB_DIR_IN;
|
||||
request.bRequest = UsbPairingManagement::kRequestGet;
|
||||
request.wValue = UsbPairingManagement::kRequestValue;
|
||||
request.wIndex = UsbPairingManagement::kRequestIndex;
|
||||
request.wLength = UsbPairingManagement::kMaximumResponseSize;
|
||||
require(tud_vendor_control_xfer_cb(
|
||||
0, CONTROL_STAGE_SETUP, &request) &&
|
||||
control_payload.size() ==
|
||||
UsbPairingManagement::kResponseHeaderSize +
|
||||
UsbPairingManagement::kRecordSize &&
|
||||
control_payload[8] == 7,
|
||||
"GET request did not return the current pairing snapshot");
|
||||
|
||||
request.bmRequestType_bit.direction = TUSB_DIR_OUT;
|
||||
request.wLength = 0;
|
||||
request.bRequest = UsbPairingManagement::kRequestRefresh;
|
||||
require(tud_vendor_control_xfer_cb(
|
||||
0, CONTROL_STAGE_SETUP, &request) &&
|
||||
refresh_requested && control_status_sent,
|
||||
"REFRESH request was not acknowledged and queued");
|
||||
|
||||
control_status_sent = false;
|
||||
request.bRequest = UsbPairingManagement::kRequestClear;
|
||||
require(tud_vendor_control_xfer_cb(
|
||||
0, CONTROL_STAGE_SETUP, &request) &&
|
||||
clear_requested && control_status_sent,
|
||||
"CLEAR request was not acknowledged and queued");
|
||||
|
||||
request.wValue = 0;
|
||||
require(!tud_vendor_control_xfer_cb(
|
||||
0, CONTROL_STAGE_SETUP, &request),
|
||||
"request with invalid magic was accepted");
|
||||
require(tud_vendor_control_xfer_cb(
|
||||
0, CONTROL_STAGE_ACK, &request),
|
||||
"non-setup control stage was rejected");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void bluepad32_input_backend_request_pairing_snapshot() {
|
||||
refresh_requested = true;
|
||||
}
|
||||
|
||||
void bluepad32_input_backend_clear_pairings() {
|
||||
clear_requested = true;
|
||||
}
|
||||
|
||||
void bluepad32_input_backend_pairing_snapshot(
|
||||
Bluepad32PairingSnapshot* out) {
|
||||
*out = current_snapshot;
|
||||
}
|
||||
|
||||
bool tud_control_xfer(uint8_t, const tusb_control_request_t*,
|
||||
void* buffer, uint16_t length) {
|
||||
const auto* bytes = static_cast<const uint8_t*>(buffer);
|
||||
control_payload.assign(bytes, bytes + length);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool tud_control_status(uint8_t, const tusb_control_request_t*) {
|
||||
control_status_sent = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
#include "../usb_pairing_management.cpp"
|
||||
|
||||
int main() {
|
||||
test_encoding();
|
||||
test_vendor_requests();
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -1,11 +1,19 @@
|
|||
// TinyUSB configuration tailored for a single Switch Pro style HID interface.
|
||||
// Data is derived from TinyUSB examples and tuned for a 64-byte HID endpoint.
|
||||
// TinyUSB configuration for one to four Switch Pro style HID interfaces.
|
||||
// Each interface uses independent 64-byte interrupt IN and OUT endpoints.
|
||||
#ifndef _TUSB_CONFIG_H_
|
||||
#define _TUSB_CONFIG_H_
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
#ifndef SWITCH_PICO_HID_INSTANCE_COUNT
|
||||
#define SWITCH_PICO_HID_INSTANCE_COUNT 1
|
||||
#endif
|
||||
|
||||
#if SWITCH_PICO_HID_INSTANCE_COUNT < 1 || SWITCH_PICO_HID_INSTANCE_COUNT > 4
|
||||
#error "SWITCH_PICO_HID_INSTANCE_COUNT must be between 1 and 4"
|
||||
#endif
|
||||
|
||||
|
||||
#define CFG_TUSB_RHPORT0_MODE (OPT_MODE_DEVICE | OPT_MODE_FULL_SPEED)
|
||||
#ifndef CFG_TUSB_OS
|
||||
|
|
@ -23,7 +31,7 @@ extern "C" {
|
|||
#define CFG_TUD_ENDPOINT0_SIZE 64
|
||||
|
||||
// Device class configuration
|
||||
#define CFG_TUD_HID 1
|
||||
#define CFG_TUD_HID SWITCH_PICO_HID_INSTANCE_COUNT
|
||||
#define CFG_TUD_CDC 0
|
||||
#define CFG_TUD_MSC 0
|
||||
#define CFG_TUD_MIDI 0
|
||||
|
|
|
|||
92
usb_pairing_management.cpp
Normal file
92
usb_pairing_management.cpp
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
#include "usb_pairing_management.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include "tusb.h"
|
||||
|
||||
namespace UsbPairingManagement {
|
||||
|
||||
size_t encode_snapshot(const Bluepad32PairingSnapshot& snapshot,
|
||||
uint8_t* output, size_t output_size) {
|
||||
const size_t required =
|
||||
kResponseHeaderSize + snapshot.record_count * kRecordSize;
|
||||
if (output == nullptr || output_size < required ||
|
||||
snapshot.record_count > BLUEPAD32_PAIRING_RECORD_CAPACITY) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
output[0] = 'S';
|
||||
output[1] = 'P';
|
||||
output[2] = 'P';
|
||||
output[3] = 'M';
|
||||
output[4] = kProtocolVersion;
|
||||
output[5] = static_cast<uint8_t>(snapshot.status);
|
||||
output[6] = snapshot.record_count;
|
||||
output[7] = snapshot.overflow ? 1 : 0;
|
||||
output[8] = static_cast<uint8_t>(snapshot.generation);
|
||||
output[9] = static_cast<uint8_t>(snapshot.generation >> 8);
|
||||
output[10] = static_cast<uint8_t>(snapshot.generation >> 16);
|
||||
output[11] = static_cast<uint8_t>(snapshot.generation >> 24);
|
||||
|
||||
size_t offset = kResponseHeaderSize;
|
||||
for (uint8_t index = 0; index < snapshot.record_count; ++index) {
|
||||
const Bluepad32PairingRecord& record = snapshot.records[index];
|
||||
output[offset] = static_cast<uint8_t>(record.transport);
|
||||
output[offset + 1] = record.address_type;
|
||||
memcpy(&output[offset + 2], record.address,
|
||||
sizeof(record.address));
|
||||
offset += kRecordSize;
|
||||
}
|
||||
return required;
|
||||
}
|
||||
|
||||
} // namespace UsbPairingManagement
|
||||
|
||||
extern "C" bool tud_vendor_control_xfer_cb(
|
||||
uint8_t rhport, uint8_t stage,
|
||||
tusb_control_request_t const* request) {
|
||||
if (stage != CONTROL_STAGE_SETUP) {
|
||||
return true;
|
||||
}
|
||||
if (request == nullptr ||
|
||||
request->bmRequestType_bit.recipient != TUSB_REQ_RCPT_DEVICE ||
|
||||
request->wValue != UsbPairingManagement::kRequestValue ||
|
||||
request->wIndex != UsbPairingManagement::kRequestIndex) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (request->bRequest) {
|
||||
case UsbPairingManagement::kRequestGet: {
|
||||
if (request->bmRequestType_bit.direction != TUSB_DIR_IN) {
|
||||
return false;
|
||||
}
|
||||
static uint8_t response[
|
||||
UsbPairingManagement::kMaximumResponseSize];
|
||||
Bluepad32PairingSnapshot snapshot{};
|
||||
bluepad32_input_backend_pairing_snapshot(&snapshot);
|
||||
const size_t response_size =
|
||||
UsbPairingManagement::encode_snapshot(
|
||||
snapshot, response, sizeof(response));
|
||||
return response_size != 0 &&
|
||||
tud_control_xfer(
|
||||
rhport, request, response,
|
||||
static_cast<uint16_t>(response_size));
|
||||
}
|
||||
case UsbPairingManagement::kRequestRefresh:
|
||||
if (request->bmRequestType_bit.direction != TUSB_DIR_OUT ||
|
||||
request->wLength != 0) {
|
||||
return false;
|
||||
}
|
||||
bluepad32_input_backend_request_pairing_snapshot();
|
||||
return tud_control_status(rhport, request);
|
||||
case UsbPairingManagement::kRequestClear:
|
||||
if (request->bmRequestType_bit.direction != TUSB_DIR_OUT ||
|
||||
request->wLength != 0) {
|
||||
return false;
|
||||
}
|
||||
bluepad32_input_backend_clear_pairings();
|
||||
return tud_control_status(rhport, request);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
25
usb_pairing_management.h
Normal file
25
usb_pairing_management.h
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "bluepad32_input_backend.h"
|
||||
|
||||
namespace UsbPairingManagement {
|
||||
|
||||
constexpr uint8_t kRequestClear = 0x50;
|
||||
constexpr uint8_t kRequestGet = 0x51;
|
||||
constexpr uint8_t kRequestRefresh = 0x52;
|
||||
constexpr uint16_t kRequestValue = 0x5350;
|
||||
constexpr uint16_t kRequestIndex = 0x4d47;
|
||||
constexpr uint8_t kProtocolVersion = 1;
|
||||
constexpr size_t kResponseHeaderSize = 12;
|
||||
constexpr size_t kRecordSize = 8;
|
||||
constexpr size_t kMaximumResponseSize =
|
||||
kResponseHeaderSize +
|
||||
BLUEPAD32_PAIRING_RECORD_CAPACITY * kRecordSize;
|
||||
|
||||
size_t encode_snapshot(const Bluepad32PairingSnapshot& snapshot,
|
||||
uint8_t* output, size_t output_size);
|
||||
|
||||
} // namespace UsbPairingManagement
|
||||
11
uv.lock
generated
11
uv.lock
generated
|
|
@ -903,6 +903,15 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/07/bc/587a445451b253b285629263eb51c2d8e9bcea4fc97826266d186f96f558/pyserial-3.5-py2.py3-none-any.whl", hash = "sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0", size = 90585, upload-time = "2020-11-23T03:59:13.41Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyusb"
|
||||
version = "1.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/00/6b/ce3727395e52b7b76dfcf0c665e37d223b680b9becc60710d4bc08b7b7cb/pyusb-1.3.1.tar.gz", hash = "sha256:3af070b607467c1c164f49d5b0caabe8ac78dbed9298d703a8dbf9df4052d17e", size = 77281, upload-time = "2025-01-08T23:45:01.866Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/28/b8/27e6312e86408a44fe16bd28ee12dd98608b39f7e7e57884a24e8f29b573/pyusb-1.3.1-py3-none-any.whl", hash = "sha256:bf9b754557af4717fe80c2b07cc2b923a9151f5c08d17bdb5345dac09d6a0430", size = 58465, upload-time = "2025-01-08T23:45:00.029Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.32.5"
|
||||
|
|
@ -940,6 +949,7 @@ dependencies = [
|
|||
{ name = "hidapi" },
|
||||
{ name = "pysdl3" },
|
||||
{ name = "pyserial" },
|
||||
{ name = "pyusb" },
|
||||
{ name = "rich" },
|
||||
]
|
||||
|
||||
|
|
@ -948,6 +958,7 @@ requires-dist = [
|
|||
{ name = "hidapi" },
|
||||
{ name = "pysdl3" },
|
||||
{ name = "pyserial" },
|
||||
{ name = "pyusb" },
|
||||
{ name = "rich" },
|
||||
]
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue