Compare commits

..

No commits in common. "master" and "feature/pico2w-bluepad32-aio" have entirely different histories.

56 changed files with 663 additions and 6635 deletions

2
.gitattributes vendored
View file

@ -1,2 +0,0 @@
# Unified diffs require a one-character context marker on blank lines.
*.patch -whitespace

View file

@ -90,20 +90,8 @@ add_executable(switch-pico
switch_haptics.cpp switch_haptics.cpp
) )
if(SWITCH_PICO_INPUT_BACKEND STREQUAL "BLUEPAD32") if(SWITCH_PICO_INPUT_BACKEND STREQUAL "BLUEPAD32")
target_sources(switch-pico PRIVATE target_sources(switch-pico PRIVATE bluepad32_input_backend.cpp)
bluepad32_input_backend.cpp target_compile_definitions(switch-pico PRIVATE SWITCH_PICO_BLUEPAD32=1)
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() endif()
pico_set_program_name(switch-pico "switch-pico") pico_set_program_name(switch-pico "switch-pico")
@ -130,7 +118,6 @@ if(SWITCH_PICO_INPUT_BACKEND STREQUAL "BLUEPAD32")
pico_btstack_classic pico_btstack_classic
pico_btstack_cyw43 pico_btstack_cyw43
pico_multicore pico_multicore
pico_flash
) )
endif() endif()

134
README.md
View file

@ -1,12 +1,12 @@
# Switch Pico Controller Bridge # Switch Pico Controller Bridge
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. 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.
## What you get ## What you get
- **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. - **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.
- **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). - **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).
- **Color configuration** (`controller_color_config.h`): compile-time RGB colors for emulated controller grips and supported Bluetooth controller LEDs. - **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 four concurrent Bluetooth controllers and sends their controls, calibrated motion, rumble, and slot identity through four separate Switch Pro USB interfaces without a computer. - **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.
## Quick start ## 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). 1. Flash the Pico with `firmware/switch-pico.uf2` (or build your own) using BOOTSEL drag-and-drop (see “Manual UF2 flashing” below).
@ -17,11 +17,7 @@ Raspberry Pi Pico firmware that emulates one or more Switch Pro controllers over
## Pico 2 W all-in-one Bluetooth option ## Pico 2 W all-in-one Bluetooth option
### Architecture 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.
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 ### Build and flash
@ -46,123 +42,30 @@ 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. 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.
### Pairing up to four controllers ### Pair a controller
1. Flash and connect the Pico 2 W to the Switch. 1. Flash and connect the Pico 2 W to the Switch.
2. Enable `System Settings → Controllers and Sensors → Pro Controller Wired Communication`. 2. Enable `System Settings → Controllers and Sensors → Pro Controller Wired Communication`.
3. Hold BOOTSEL for about two seconds until the onboard LED starts double-blinking. This enables new Bluetooth authentication for 60 seconds. 3. Put one controller into Bluetooth pairing mode:
4. Put a controller into Bluetooth pairing mode:
- DualSense: hold Create + PS. - DualSense: hold Create + PS.
- DualShock 4: hold Share + PS. - DualShock 4: hold Share + PS.
- Switch Pro: press its sync button. - Switch Pro: press its sync button.
- Xbox Bluetooth controller: hold its pair button. - Xbox Bluetooth controller: hold its pair button.
- 8BitDo: use a Bluetooth mode supported by Bluepad32; use Switch/S mode when motion is required. - 8BitDo: use a Bluetooth mode supported by Bluepad32; use Switch/S mode when motion is required.
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. 4. Wait for the controller to connect. Pairing keys persist across Pico reboots.
Pairing order determines the initial USB slot assignment. Up to four controllers map 1:1 to the four emulated Switch Pro Controller interfaces. 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.
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 capabilities
| Controller | Buttons/sticks | Rumble | Motion | | Controller | Buttons/sticks | Rumble | Motion |
|---|---:|---:|---:| |---|---:|---:|---:|
| DualSense / DualShock 4 | Yes | Yes | Yes | | DualSense / DualShock 4 | Yes | Yes | Yes |
| Switch Pro / Joy-Con | Yes | Yes | Yes | | Switch Pro | 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 | | 8BitDo in Switch-compatible Bluetooth mode | Yes | Model-dependent | Yes when the mode exposes IMU |
| Xbox Bluetooth controller | Yes | Yes | No hardware IMU | | Xbox Bluetooth controller | Yes | Yes | No hardware IMU |
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. 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.
### 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. Bluepad32 is Apache-2.0. BTstack use on Pico W/Pico 2 W is covered by Raspberry Pi's BTstack license.
@ -290,21 +193,20 @@ The generated files are:
- `firmware/switch-pico.elf` and `firmware/switch-pico.uf2`, refreshed from the - `firmware/switch-pico.elf` and `firmware/switch-pico.uf2`, refreshed from the
corresponding `build/` artifacts after every successful build. corresponding `build/` artifacts after every successful build.
To assign one color to every emulated controller slot while building, pass one To customize the controller grip color while building, pass one of these mutually
of these mutually exclusive options: exclusive options:
```sh ```sh
# Use one random color for all slots # Use a random color for both grips
python3 build.py --random-grip-color python3 build.py --random-grip-color
# Use one specific six-digit RGB color for all slots # Use a specific six-digit RGB color for both grips
python3 build.py --grip-color FF00AA python3 build.py --grip-color FF00AA
``` ```
Both options update all four slot definitions in Both options update `controller_color_config.h` before building. With no color
`controller_color_config.h` before building. With no color option, the option, that file is left unchanged. Run `python3 build.py --help` to see the
per-slot blue/red/yellow/green palette is left unchanged. Run available command-line options.
`python3 build.py --help` to see the available command-line options.
If the tools or artifacts are in non-default locations, use these environment If the tools or artifacts are in non-default locations, use these environment
variables: variables:

View file

@ -15,7 +15,6 @@
#define ENABLE_LE_DATA_LENGTH_EXTENSION #define ENABLE_LE_DATA_LENGTH_EXTENSION
#define ENABLE_LE_PERIPHERAL #define ENABLE_LE_PERIPHERAL
#define ENABLE_LE_PRIVACY_ADDRESS_RESOLUTION #define ENABLE_LE_PRIVACY_ADDRESS_RESOLUTION
#define ENABLE_LE_RESOLVING_LIST
#define ENABLE_LE_SECURE_CONNECTIONS #define ENABLE_LE_SECURE_CONNECTIONS
#else #else
#error "BP32: ENABLE_BLE should be defined" #error "BP32: ENABLE_BLE should be defined"
@ -41,12 +40,12 @@
#define MAX_NR_BNEP_CHANNELS 1 #define MAX_NR_BNEP_CHANNELS 1
#define MAX_NR_BNEP_SERVICES 1 #define MAX_NR_BNEP_SERVICES 1
#define MAX_NR_BTSTACK_LINK_KEY_DB_MEMORY_ENTRIES 2 #define MAX_NR_BTSTACK_LINK_KEY_DB_MEMORY_ENTRIES 2
#define MAX_NR_GATT_CLIENTS 4 #define MAX_NR_GATT_CLIENTS 1
#define MAX_NR_HCI_CONNECTIONS 4 #define MAX_NR_HCI_CONNECTIONS 4
#define MAX_NR_HID_HOST_CONNECTIONS 4 #define MAX_NR_HID_HOST_CONNECTIONS 1
#define MAX_NR_HIDS_CLIENTS 4 #define MAX_NR_HIDS_CLIENTS 1
#define MAX_NR_HFP_CONNECTIONS 1 #define MAX_NR_HFP_CONNECTIONS 1
#define MAX_NR_L2CAP_CHANNELS 10 #define MAX_NR_L2CAP_CHANNELS 6
#define MAX_NR_L2CAP_SERVICES 5 #define MAX_NR_L2CAP_SERVICES 5
#define MAX_NR_RFCOMM_CHANNELS 1 #define MAX_NR_RFCOMM_CHANNELS 1
#define MAX_NR_RFCOMM_MULTIPLEXERS 1 #define MAX_NR_RFCOMM_MULTIPLEXERS 1

View file

@ -1,242 +0,0 @@
#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);
}

View file

@ -1,8 +1,8 @@
#pragma once #pragma once
// The AIO firmware exposes one fixed Bluepad32 device slot per USB interface. // Bluepad32's Pico W example configuration, limited to one live controller.
#define CONFIG_BLUEPAD32_MAX_DEVICES 4 #define CONFIG_BLUEPAD32_MAX_DEVICES 1
#define CONFIG_BLUEPAD32_MAX_ALLOWLIST 4 #define CONFIG_BLUEPAD32_MAX_ALLOWLIST 1
#define CONFIG_BLUEPAD32_GAP_SECURITY 1 #define CONFIG_BLUEPAD32_GAP_SECURITY 1
#define CONFIG_BLUEPAD32_ENABLE_BLE_BY_DEFAULT 1 #define CONFIG_BLUEPAD32_ENABLE_BLE_BY_DEFAULT 1

File diff suppressed because it is too large Load diff

View file

@ -2,45 +2,11 @@
#include <stdint.h> #include <stdint.h>
#include "switch_haptics.h"
#include "switch_pro_driver.h" #include "switch_pro_driver.h"
#include "switch_haptics.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_init();
void bluepad32_input_backend_start(); void bluepad32_input_backend_start();
void bluepad32_input_backend_open_pairing_window(); bool bluepad32_input_backend_snapshot(SwitchInputState* out);
void bluepad32_input_backend_clear_pairings(); void bluepad32_input_backend_report_sent();
bool bluepad32_input_backend_snapshot(uint8_t slot, SwitchInputState* out); void bluepad32_input_backend_queue_rumble(const SwitchRumbleOutput& rumble);
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);

View file

@ -1,101 +0,0 @@
#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());
}

View file

@ -1,32 +0,0 @@
#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();

View file

@ -22,10 +22,13 @@ AIO_FIRMWARE_UF2_PATH = FIRMWARE_DIR / "switch-pico-aio.uf2"
ELF_PATH = Path(os.environ.get("ELF_PATH", BUILD_DIR / "switch-pico.elf")).expanduser() 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() UF2_PATH = Path(os.environ.get("UF2_PATH", BUILD_DIR / "switch-pico.uf2")).expanduser()
MACROS = tuple( MACROS = (
f"SWITCH_COLOR_SLOT_{slot}_{component}" "SWITCH_COLOR_LEFT_GRIP_R",
for slot in range(1, 5) "SWITCH_COLOR_LEFT_GRIP_G",
for component in ("R", "G", "B") "SWITCH_COLOR_LEFT_GRIP_B",
"SWITCH_COLOR_RIGHT_GRIP_R",
"SWITCH_COLOR_RIGHT_GRIP_G",
"SWITCH_COLOR_RIGHT_GRIP_B",
) )
def parse_args(): def parse_args():
@ -43,12 +46,12 @@ def parse_args():
group.add_argument( group.add_argument(
"--random-grip-color", "--random-grip-color",
action="store_true", action="store_true",
help="Assign one random color to every emulated controller slot.", help="Randomize both grip colors before building.",
) )
group.add_argument( group.add_argument(
"--grip-color", "--grip-color",
metavar="RRGGBB", metavar="RRGGBB",
help="Set every emulated controller slot to the provided hex color.", help="Set both grip colors to the provided hex value.",
) )
return parser.parse_args() return parser.parse_args()
@ -81,7 +84,7 @@ def update_grip_colors(rgb_hex):
sys.exit(1) sys.exit(1)
return updated return updated
values = (r, g, b) * 4 values = (r, g, b, r, g, b)
for macro, val in zip(MACROS, values): for macro, val in zip(MACROS, values):
text = replace(macro, val, text) text = replace(macro, val, text)

View file

@ -1,31 +1,25 @@
// Compile-time Switch grip colors. Physical controller lightbar values are // Optional override for Switch Pro colour fields.
// derived automatically; each value here is an 8-bit RGB component. // Copy/modify the values below and rebuild to change how the controller appears on the Switch.
// Each value is an 8-bit RGB component.
#pragma once #pragma once
// Body shell color // Body shell colour
#define SWITCH_COLOR_BODY_R 0x1B #define SWITCH_COLOR_BODY_R 0x1B
#define SWITCH_COLOR_BODY_G 0x1B #define SWITCH_COLOR_BODY_G 0x1B
#define SWITCH_COLOR_BODY_B 0x1D #define SWITCH_COLOR_BODY_B 0x1D
// Face/button cluster color // Face/button cluster colour
#define SWITCH_COLOR_BUTTON_R 0xFF #define SWITCH_COLOR_BUTTON_R 0xFF
#define SWITCH_COLOR_BUTTON_G 0xFF #define SWITCH_COLOR_BUTTON_G 0xFF
#define SWITCH_COLOR_BUTTON_B 0xFF #define SWITCH_COLOR_BUTTON_B 0xFF
// Per-slot Switch grip colors: blue, red, yellow, green. // Left grip colour
#define SWITCH_COLOR_SLOT_1_R 0x00 #define SWITCH_COLOR_LEFT_GRIP_R 0x00
#define SWITCH_COLOR_SLOT_1_G 0x89 #define SWITCH_COLOR_LEFT_GRIP_G 0x89
#define SWITCH_COLOR_SLOT_1_B 0xEB #define SWITCH_COLOR_LEFT_GRIP_B 0xEB
#define SWITCH_COLOR_SLOT_2_R 0xE6 // Right grip colour
#define SWITCH_COLOR_SLOT_2_G 0x39 #define SWITCH_COLOR_RIGHT_GRIP_R 0x00
#define SWITCH_COLOR_SLOT_2_B 0x46 #define SWITCH_COLOR_RIGHT_GRIP_G 0x89
#define SWITCH_COLOR_RIGHT_GRIP_B 0xEB
#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

View file

@ -1,31 +0,0 @@
// 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.

View file

@ -1,39 +1,3 @@
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 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 index ea063b8..7670caf 100644
--- a/src/components/bluepad32/parser/uni_hid_parser_ds4.c --- a/src/components/bluepad32/parser/uni_hid_parser_ds4.c
@ -120,149 +84,11 @@ index a22ef26..3d5ecef 100644
int32_t calib_data = int32_t calib_data =
mult_frac(ins->accel_calib_data[i].sens_numer, raw_data, ins->accel_calib_data[i].sens_denom); mult_frac(ins->accel_calib_data[i].sens_numer, raw_data, ins->accel_calib_data[i].sens_denom);
ctl->gamepad.accel[i] = calib_data; 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 diff --git a/src/components/bluepad32/parser/uni_hid_parser_switch.c b/src/components/bluepad32/parser/uni_hid_parser_switch.c
index 599fc35..9f073b4 100644 index 599fc35..c72f056 100644
--- a/src/components/bluepad32/parser/uni_hid_parser_switch.c --- a/src/components/bluepad32/parser/uni_hid_parser_switch.c
+++ b/src/components/bluepad32/parser/uni_hid_parser_switch.c +++ b/src/components/bluepad32/parser/uni_hid_parser_switch.c
@@ -51,13 +51,15 @@ static const int16_t DEFAULT_ACCEL_OFFSET = 0; @@ -51,7 +51,8 @@ static const int16_t DEFAULT_ACCEL_OFFSET = 0;
static const int16_t DEFAULT_ACCEL_SCALE = 16384; static const int16_t DEFAULT_ACCEL_SCALE = 16384;
static const int16_t DEFAULT_GYRO_OFFSET = 0; static const int16_t DEFAULT_GYRO_OFFSET = 0;
static const int16_t DEFAULT_GYRO_SCALE = 13371; static const int16_t DEFAULT_GYRO_SCALE = 13371;
@ -272,97 +98,7 @@ index 599fc35..9f073b4 100644
#define SWITCH_FACTORY_IMU_CAL_DATA_SIZE 24 #define SWITCH_FACTORY_IMU_CAL_DATA_SIZE 24
static const uint16_t SWITCH_FACTORY_IMU_CAL_DATA_ADDR = 0x6020; 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); switch_instance_t* ins = get_switch_instance(d);
uni_controller_t* ctl = &d->controller; uni_controller_t* ctl = &d->controller;
@ -398,7 +134,7 @@ index 599fc35..9f073b4 100644
if (ins->controller_type == SWITCH_CONTROLLER_TYPE_JCR) { if (ins->controller_type == SWITCH_CONTROLLER_TYPE_JCR) {
accel[1] = -accel[1]; accel[1] = -accel[1];
accel[2] = -accel[2]; accel[2] = -accel[2];
@@ -843,10 +874,13 @@ static void parse_imu(uni_hid_device_t* d, const struct switch_imu_data_s* r) { @@ -843,10 +851,13 @@ static void parse_imu(uni_hid_device_t* d, const struct switch_imu_data_s* r) {
gyro[2] = -gyro[2]; gyro[2] = -gyro[2];
} }
@ -416,153 +152,3 @@ index 599fc35..9f073b4 100644
} }
// Process 0x30 input report: SWITCH_INPUT_IMU_DATA // 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);

View file

@ -14,13 +14,11 @@ dependencies = [
"PySDL3", "PySDL3",
"rich", "rich",
"hidapi", "hidapi",
"pyusb",
] ]
[project.scripts] [project.scripts]
controller-uart-bridge = "switch_pico_bridge.controller_uart_bridge:main" controller-uart-bridge = "switch_pico_bridge.controller_uart_bridge:main"
host-uart-logger = "switch_pico_bridge.host_uart_logger:main" host-uart-logger = "switch_pico_bridge.host_uart_logger:main"
switch-pico-pairings = "switch_pico_bridge.pairing_manager:main"
[tool.setuptools] [tool.setuptools]
package-dir = {"" = "src"} package-dir = {"" = "src"}

View file

@ -1,292 +0,0 @@
#!/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())

View file

@ -7,7 +7,6 @@
#include "hardware/uart.h" #include "hardware/uart.h"
#else #else
#include "bluepad32_input_backend.h" #include "bluepad32_input_backend.h"
#include "bootsel_pairing_button.h"
#endif #endif
#ifdef SWITCH_PICO_LOG #ifdef SWITCH_PICO_LOG
@ -26,19 +25,11 @@
#define UART_RUMBLE_TYPE 0x02 #define UART_RUMBLE_TYPE 0x02
#endif #endif
#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;
static SwitchInputState g_user_state;
#endif
static bool g_last_mounted = false; static bool g_last_mounted = false;
static bool g_last_ready = false;
// Track the latest state provided by UART or the autopilot.
static SwitchInputState g_user_state;
#ifndef SWITCH_PICO_BLUEPAD32 #ifndef SWITCH_PICO_BLUEPAD32
static void init_uart_input() { static void init_uart_input() {
@ -75,17 +66,10 @@ static void send_rumble_uart_frame(const SwitchRumbleOutput& rumble) {
} }
#endif #endif
static void on_rumble_from_switch(uint8_t instance, static void on_rumble_from_switch(const SwitchRumbleOutput& rumble) {
const SwitchRumbleOutput& rumble) {
#ifdef SWITCH_PICO_BLUEPAD32 #ifdef SWITCH_PICO_BLUEPAD32
if (instance >= BLUEPAD32_INPUT_BACKEND_SLOT_COUNT) { bluepad32_input_backend_queue_rumble(rumble);
return;
}
bluepad32_input_backend_queue_rumble(instance, rumble);
#else #else
if (instance != SWITCH_HID_INSTANCE) {
return;
}
send_rumble_uart_frame(rumble); send_rumble_uart_frame(rumble);
#endif #endif
} }
@ -134,7 +118,7 @@ static bool poll_uart_frames() {
if (expected_len > 0 && index >= expected_len) { if (expected_len > 0 && index >= expected_len) {
SwitchInputState parsed{}; 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; g_user_state = parsed;
new_data = true; new_data = true;
LOG_PRINTF("[UART] packet buttons=0x%04x hat=%u lx=%u ly=%u rx=%u ry=%u\n", LOG_PRINTF("[UART] packet buttons=0x%04x hat=%u lx=%u ly=%u rx=%u ry=%u\n",
@ -169,29 +153,16 @@ static bool poll_uart_frames() {
static void log_usb_state() { static void log_usb_state() {
bool mounted = tud_mounted(); bool mounted = tud_mounted();
bool ready = switch_pro_is_ready();
if (mounted != g_last_mounted) { if (mounted != g_last_mounted) {
g_last_mounted = mounted; g_last_mounted = mounted;
LOG_PRINTF("[USB] %s\n", mounted ? "mounted" : "unmounted"); 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) { if (ready != g_last_ready) {
g_last_ready = ready; g_last_ready = ready;
LOG_PRINTF("[SWITCH] driver %s\n", LOG_PRINTF("[SWITCH] driver %s\n", ready ? "ready (handshake OK)" : "not ready");
ready ? "ready (handshake OK)" : "not ready");
} }
#endif
} }
int main() { int main() {
@ -205,21 +176,10 @@ int main() {
#endif #endif
tusb_init(); tusb_init();
#ifdef SWITCH_PICO_BLUEPAD32 switch_pro_init();
for (uint8_t instance = 0; switch_pro_set_rumble_callback(on_rumble_from_switch);
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(); g_user_state = neutral_input();
switch_pro_set_input(SWITCH_HID_INSTANCE, g_user_state); switch_pro_set_input(g_user_state);
#endif
#ifdef SWITCH_PICO_BLUEPAD32 #ifdef SWITCH_PICO_BLUEPAD32
bluepad32_input_backend_start(); bluepad32_input_backend_start();
@ -233,31 +193,19 @@ int main() {
while (true) { while (true) {
tud_task(); // USB device tasks tud_task(); // USB device tasks
#ifdef SWITCH_PICO_BLUEPAD32 #ifdef SWITCH_PICO_BLUEPAD32
switch (bootsel_pairing_button_task()) { bluepad32_input_backend_snapshot(&g_user_state);
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 #else
bool new_data = poll_uart_frames(); // Pull controller state from UART1 bool new_data = poll_uart_frames(); // Pull controller state from UART1
(void)new_data; (void)new_data;
#endif
SwitchInputState state = g_user_state; SwitchInputState state = g_user_state;
switch_pro_set_input(SWITCH_HID_INSTANCE, state); switch_pro_set_input(state);
(void)switch_pro_task(SWITCH_HID_INSTANCE); #ifdef SWITCH_PICO_BLUEPAD32
if (switch_pro_task()) {
bluepad32_input_backend_report_sent();
}
#else
(void)switch_pro_task();
#endif #endif
log_usb_state(); log_usb_state();
} }

View file

@ -8,14 +8,6 @@
#pragma once #pragma once
#include <stdint.h> #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 #define SWITCH_PRO_ENDPOINT_SIZE 64
@ -89,7 +81,7 @@ typedef enum {
GET_VOLTAGE = 0x50, GET_VOLTAGE = 0x50,
} SwitchCommands; } SwitchCommands;
struct SwitchAnalog { typedef struct {
uint8_t data[3]; uint8_t data[3];
void setX(uint16_t x) { void setX(uint16_t x) {
@ -109,10 +101,10 @@ struct SwitchAnalog {
uint16_t getY() { uint16_t getY() {
return static_cast<uint16_t>((data[1] >> 4)) | (data[2] << 4); 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 // left and right calibration are stored differently for some reason, so two structs
struct SwitchLeftCalibration { typedef struct {
uint8_t data[9]; uint8_t data[9];
void getMin(uint16_t& x, uint16_t& y) const { packCalib(6, x, y); } void getMin(uint16_t& x, uint16_t& y) const { packCalib(6, x, y); }
@ -145,9 +137,9 @@ struct SwitchLeftCalibration {
x = static_cast<uint16_t>(data[offset]) | ((data[offset + 1] & 0x0F) << 8); 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); y = static_cast<uint16_t>(data[offset + 2] << 4) | (data[offset + 1] >> 4);
} }
}; } SwitchLeftCalibration;
struct SwitchRightCalibration { typedef struct {
uint8_t data[9]; uint8_t data[9];
void getMin(uint16_t& x, uint16_t& y) const { packCalib(3, x, y); } void getMin(uint16_t& x, uint16_t& y) const { packCalib(3, x, y); }
@ -180,7 +172,7 @@ struct SwitchRightCalibration {
x = static_cast<uint16_t>(data[offset]) | ((data[offset + 1] & 0x0F) << 8); 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); y = static_cast<uint16_t>(data[offset + 2] << 4) | (data[offset + 1] >> 4);
} }
}; } SwitchRightCalibration;
typedef struct typedef struct
{ {
@ -377,19 +369,8 @@ static const uint8_t switch_pro_configuration_descriptor[] =
{ {
0x09, // bLength 0x09, // bLength
0x02, // bDescriptorType (Configuration) 0x02, // bDescriptorType (Configuration)
#if SWITCH_PICO_HID_INSTANCE_COUNT == 1
0x29, 0x00, // wTotalLength 41 0x29, 0x00, // wTotalLength 41
0x01, // bNumInterfaces 1 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 0x01, // bConfigurationValue
0x00, // iConfiguration (String Index) 0x00, // iConfiguration (String Index)
0xA0, // bmAttributes Remote Wakeup 0xA0, // bmAttributes Remote Wakeup
@ -426,108 +407,6 @@ static const uint8_t switch_pro_configuration_descriptor[] =
0x03, // bmAttributes (Interrupt) 0x03, // bmAttributes (Interrupt)
0x40, 0x00, // wMaxPacketSize 64 0x40, 0x00, // wMaxPacketSize 64
0x08, // bInterval 8 (unit depends on device speed) 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[] = static const uint8_t switch_pro_report_descriptor[] =

File diff suppressed because it is too large Load diff

View file

@ -11,7 +11,6 @@
#include "switch_haptics.h" #include "switch_haptics.h"
#include "switch_pro_descriptors.h" #include "switch_pro_descriptors.h"
typedef struct { typedef struct {
int16_t accel_x; int16_t accel_x;
int16_t accel_y; int16_t accel_y;
@ -50,68 +49,24 @@ typedef struct {
uint8_t imu_sample_count; // 0-3 uint8_t imu_sample_count; // 0-3
SwitchImuSample imu_samples[3]; SwitchImuSample imu_samples[3];
} SwitchInputState; } 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};
}
const auto calibrate = [minimum, chroma, peak](uint8_t component) { // Initialize USB state and calibration before entering the main loop.
const uint32_t delta = void switch_pro_init();
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);
// Return the configured Switch grip color and its automatically calibrated // Drive the Switch Pro USB state machine; returns true only when a regular
// 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. // 0x30 input report was successfully queued.
bool switch_pro_task(uint8_t instance); bool switch_pro_task();
// Convert a packed UART message into controller state (returns true if parsed). // Convert a packed UART message into controller state (returns true if parsed).
bool switch_pro_apply_uart_packet(const uint8_t* packet, uint8_t length, // If out_state is null the parsed state is written directly to the driver.
SwitchInputState& out_state); bool switch_pro_apply_uart_packet(const uint8_t* packet, uint8_t length, SwitchInputState* out_state = nullptr);
// Driver state helpers // Driver state helpers
bool switch_pro_is_ready(uint8_t instance); bool switch_pro_is_ready();
// Optional callback fired with decoded rumble intensities from one host // Optional callback fired with decoded rumble intensities from the host.
// interface. typedef void (*SwitchRumbleCallback)(const SwitchRumbleOutput& rumble);
typedef void (*SwitchRumbleCallback)(uint8_t instance, void switch_pro_set_rumble_callback(SwitchRumbleCallback cb);
const SwitchRumbleOutput& rumble);
void switch_pro_set_rumble_callback(uint8_t instance,
SwitchRumbleCallback callback);

File diff suppressed because it is too large Load diff

View file

@ -1,212 +0,0 @@
#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;
}

View file

@ -1,23 +0,0 @@
#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() {}

View file

@ -1,7 +0,0 @@
#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*) {}

View file

@ -1,6 +0,0 @@
#pragma once
#define CYW43_WL_GPIO_LED_PIN 0
int cyw43_arch_init();
void cyw43_arch_gpio_put(int pin, bool value);

View file

@ -1,3 +0,0 @@
#pragma once
bool flash_safe_execute_core_init();

View file

@ -1,3 +0,0 @@
#pragma once
void multicore_launch_core1(void (*entry)());

View file

@ -1,3 +0,0 @@
#pragma once
void tight_loop_contents();

View file

@ -1,190 +0,0 @@
#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);

View file

@ -1,27 +0,0 @@
#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

View file

@ -1,3 +0,0 @@
#pragma once
#define SIO_GPIO_HI_IN_QSPI_CSN_BITS 0x08000000u

View file

@ -1,14 +0,0 @@
#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;

View file

@ -1,10 +0,0 @@
#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;

View file

@ -1,10 +0,0 @@
#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);

View file

@ -1,8 +0,0 @@
#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);

View file

@ -1,266 +0,0 @@
#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;
}

View file

@ -1,13 +0,0 @@
#pragma once
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
uint32_t get_rand_32(void);
#ifdef __cplusplus
}
#endif

View file

@ -1,18 +0,0 @@
#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

View file

@ -1,45 +0,0 @@
#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

View file

@ -1,184 +0,0 @@
#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;
}

View file

@ -1,696 +0,0 @@
#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(&current_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;
}

View file

@ -1,46 +0,0 @@
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)

View file

@ -1,31 +0,0 @@
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)

View file

@ -1,34 +0,0 @@
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)

View file

@ -1,156 +0,0 @@
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

View file

@ -1,89 +0,0 @@
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"
)

View file

@ -1,34 +0,0 @@
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)

View file

@ -1,30 +0,0 @@
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)

View file

@ -1,43 +0,0 @@
#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

View file

@ -1,144 +0,0 @@
#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;
}

View file

@ -1,19 +1,11 @@
// TinyUSB configuration for one to four Switch Pro style HID interfaces. // TinyUSB configuration tailored for a single Switch Pro style HID interface.
// Each interface uses independent 64-byte interrupt IN and OUT endpoints. // Data is derived from TinyUSB examples and tuned for a 64-byte HID endpoint.
#ifndef _TUSB_CONFIG_H_ #ifndef _TUSB_CONFIG_H_
#define _TUSB_CONFIG_H_ #define _TUSB_CONFIG_H_
#ifdef __cplusplus #ifdef __cplusplus
extern "C" { extern "C" {
#endif #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) #define CFG_TUSB_RHPORT0_MODE (OPT_MODE_DEVICE | OPT_MODE_FULL_SPEED)
#ifndef CFG_TUSB_OS #ifndef CFG_TUSB_OS
@ -31,7 +23,7 @@ extern "C" {
#define CFG_TUD_ENDPOINT0_SIZE 64 #define CFG_TUD_ENDPOINT0_SIZE 64
// Device class configuration // Device class configuration
#define CFG_TUD_HID SWITCH_PICO_HID_INSTANCE_COUNT #define CFG_TUD_HID 1
#define CFG_TUD_CDC 0 #define CFG_TUD_CDC 0
#define CFG_TUD_MSC 0 #define CFG_TUD_MSC 0
#define CFG_TUD_MIDI 0 #define CFG_TUD_MIDI 0

View file

@ -1,92 +0,0 @@
#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;
}
}

View file

@ -1,25 +0,0 @@
#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
View file

@ -903,15 +903,6 @@ 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" }, { 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]] [[package]]
name = "requests" name = "requests"
version = "2.32.5" version = "2.32.5"
@ -949,7 +940,6 @@ dependencies = [
{ name = "hidapi" }, { name = "hidapi" },
{ name = "pysdl3" }, { name = "pysdl3" },
{ name = "pyserial" }, { name = "pyserial" },
{ name = "pyusb" },
{ name = "rich" }, { name = "rich" },
] ]
@ -958,7 +948,6 @@ requires-dist = [
{ name = "hidapi" }, { name = "hidapi" },
{ name = "pysdl3" }, { name = "pysdl3" },
{ name = "pyserial" }, { name = "pyserial" },
{ name = "pyusb" },
{ name = "rich" }, { name = "rich" },
] ]