diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index 36d4f5f..0000000 --- a/.gitattributes +++ /dev/null @@ -1,2 +0,0 @@ -# Unified diffs require a one-character context marker on blank lines. -*.patch -whitespace diff --git a/CMakeLists.txt b/CMakeLists.txt index 5f69a35..89d43ea 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -90,20 +90,8 @@ add_executable(switch-pico switch_haptics.cpp ) if(SWITCH_PICO_INPUT_BACKEND STREQUAL "BLUEPAD32") - target_sources(switch-pico PRIVATE - bluepad32_input_backend.cpp - bootsel_pairing_button.cpp - usb_pairing_management.cpp - ) - target_compile_definitions(switch-pico PRIVATE - SWITCH_PICO_BLUEPAD32=1 - SWITCH_PICO_HID_INSTANCE_COUNT=4 - PICO_FLASH_ASSUME_CORE1_SAFE=0 - ) -else() - target_compile_definitions(switch-pico PRIVATE - SWITCH_PICO_HID_INSTANCE_COUNT=1 - ) + target_sources(switch-pico PRIVATE bluepad32_input_backend.cpp) + target_compile_definitions(switch-pico PRIVATE SWITCH_PICO_BLUEPAD32=1) endif() pico_set_program_name(switch-pico "switch-pico") @@ -130,7 +118,6 @@ if(SWITCH_PICO_INPUT_BACKEND STREQUAL "BLUEPAD32") pico_btstack_classic pico_btstack_cyw43 pico_multicore - pico_flash ) endif() diff --git a/README.md b/README.md index e918518..ff2dacb 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ # 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 -- **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). -- **Color configuration** (`controller_color_config.h`): compile-time RGB colors for emulated controller grips and supported Bluetooth controller LEDs. -- **Pico 2 W AIO firmware** (`firmware/switch-pico-aio.uf2`): hosts four concurrent Bluetooth controllers and sends their controls, calibrated motion, rumble, and slot identity through four separate Switch Pro USB interfaces without a computer. +- **Colour override** (`controller_color_config.h`): compile‑time RGB overrides for body/buttons/grips as seen by the Switch. +- **Pico 2 W AIO firmware** (`firmware/switch-pico-aio.uf2`): hosts one Bluetooth controller and sends its controls, calibrated motion, and rumble through the same Switch Pro USB device without a computer. ## Quick start 1. Flash the Pico with `firmware/switch-pico.uf2` (or build your own) using BOOTSEL drag-and-drop (see “Manual UF2 flashing” below). @@ -17,11 +17,7 @@ Raspberry Pi Pico firmware that emulates one or more Switch Pro controllers over ## Pico 2 W all-in-one Bluetooth option -### Architecture - -The AIO build accepts up to four concurrent Bluetooth controllers on a single Pico 2 W. TinyUSB and the four Switch report generators run on Core 0; Bluepad32, BTstack, and the CYW43439 radio run on Core 1. Each Bluetooth device index maps directly to one always-present USB Pro HID interface. Per-slot state snapshots and generation-tagged latest-value rumble mailboxes are the only cross-core data paths. - -All four USB interfaces are always present to the Switch as separate Pro Controllers on one physical USB device. Input, motion, rumble, lifecycle, and displayed grip color remain isolated per slot. +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. ### 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. -### Pairing up to four controllers +### Pair a controller 1. Flash and connect the Pico 2 W to the Switch. 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. -4. Put a controller into Bluetooth pairing mode: +3. Put one controller into Bluetooth pairing mode: - DualSense: hold Create + PS. - DualShock 4: hold Share + PS. - Switch Pro: press its sync button. - Xbox Bluetooth controller: hold its pair button. - 8BitDo: use a Bluetooth mode supported by Bluepad32; use Switch/S mode when motion is required. -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. - -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. +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. ### Controller capabilities | Controller | Buttons/sticks | Rumble | Motion | |---|---:|---:|---:| | DualSense / DualShock 4 | Yes | Yes | Yes | -| Switch Pro / Joy-Con | Yes | Yes | Yes | -| PS Move ZCM1/ZCM2 | Buttons/trigger | Yes | Yes, after calibration | -| Wii Remote | Mode-dependent | Yes | Accelerometer | +| Switch Pro | Yes | Yes | Yes | | 8BitDo in Switch-compatible Bluetooth mode | Yes | Model-dependent | Yes when the mode exposes 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. - -### 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. +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. 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 corresponding `build/` artifacts after every successful build. -To assign one color to every emulated controller slot while building, pass one -of these mutually exclusive options: +To customize the controller grip color while building, pass one of these mutually +exclusive options: ```sh -# Use one random color for all slots +# Use a random color for both grips 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 ``` -Both options update all four slot definitions in -`controller_color_config.h` before building. With no color option, the -per-slot blue/red/yellow/green palette is left unchanged. Run -`python3 build.py --help` to see the available command-line options. +Both options update `controller_color_config.h` before building. With no color +option, that file is left unchanged. Run `python3 build.py --help` to see the +available command-line options. If the tools or artifacts are in non-default locations, use these environment variables: diff --git a/bluepad32_config/btstack_config.h b/bluepad32_config/btstack_config.h index 424d437..95072bf 100644 --- a/bluepad32_config/btstack_config.h +++ b/bluepad32_config/btstack_config.h @@ -15,7 +15,6 @@ #define ENABLE_LE_DATA_LENGTH_EXTENSION #define ENABLE_LE_PERIPHERAL #define ENABLE_LE_PRIVACY_ADDRESS_RESOLUTION -#define ENABLE_LE_RESOLVING_LIST #define ENABLE_LE_SECURE_CONNECTIONS #else #error "BP32: ENABLE_BLE should be defined" @@ -41,12 +40,12 @@ #define MAX_NR_BNEP_CHANNELS 1 #define MAX_NR_BNEP_SERVICES 1 #define MAX_NR_BTSTACK_LINK_KEY_DB_MEMORY_ENTRIES 2 -#define MAX_NR_GATT_CLIENTS 4 +#define MAX_NR_GATT_CLIENTS 1 #define MAX_NR_HCI_CONNECTIONS 4 -#define MAX_NR_HID_HOST_CONNECTIONS 4 -#define MAX_NR_HIDS_CLIENTS 4 +#define MAX_NR_HID_HOST_CONNECTIONS 1 +#define MAX_NR_HIDS_CLIENTS 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_RFCOMM_CHANNELS 1 #define MAX_NR_RFCOMM_MULTIPLEXERS 1 diff --git a/bluepad32_config/parser/uni_hid_parser_imu.h b/bluepad32_config/parser/uni_hid_parser_imu.h deleted file mode 100644 index 4e4cf73..0000000 --- a/bluepad32_config/parser/uni_hid_parser_imu.h +++ /dev/null @@ -1,242 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include - -#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); -} diff --git a/bluepad32_config/sdkconfig.h b/bluepad32_config/sdkconfig.h index 90866ec..83002a9 100644 --- a/bluepad32_config/sdkconfig.h +++ b/bluepad32_config/sdkconfig.h @@ -1,8 +1,8 @@ #pragma once -// The AIO firmware exposes one fixed Bluepad32 device slot per USB interface. -#define CONFIG_BLUEPAD32_MAX_DEVICES 4 -#define CONFIG_BLUEPAD32_MAX_ALLOWLIST 4 +// Bluepad32's Pico W example configuration, limited to one live controller. +#define CONFIG_BLUEPAD32_MAX_DEVICES 1 +#define CONFIG_BLUEPAD32_MAX_ALLOWLIST 1 #define CONFIG_BLUEPAD32_GAP_SECURITY 1 #define CONFIG_BLUEPAD32_ENABLE_BLE_BY_DEFAULT 1 diff --git a/bluepad32_input_backend.cpp b/bluepad32_input_backend.cpp index bc5fda4..6b2231e 100644 --- a/bluepad32_input_backend.cpp +++ b/bluepad32_input_backend.cpp @@ -1,16 +1,14 @@ #include "bluepad32_input_backend.h" -#include "controller_hotkey_config.h" #include #include -#include #include #include #include -#include #include #include +#include #include namespace { @@ -22,130 +20,23 @@ constexpr int32_t kTriggerMaximum = 1023; constexpr int32_t kTriggerThreshold = (kTriggerMaximum * 35) / 100; constexpr uint16_t kRumbleDurationMs = 50; constexpr uint32_t kRumblePollIntervalMs = 5; -constexpr uint8_t kSlotCount = BLUEPAD32_INPUT_BACKEND_SLOT_COUNT; -constexpr uint32_t kPairingWindowDurationMs = 60000; -constexpr uint32_t kPairingResetFeedbackDurationMs = 2000; -// Bluetooth Classic units are 0.625 ms: 0x1900 = 4 seconds. -constexpr uint16_t kClassicLinkSupervisionTimeout = 0x1900; -constexpr uint8_t kAllBlePairingMethods = - SM_STK_GENERATION_METHOD_JUST_WORKS | - SM_STK_GENERATION_METHOD_OOB | - SM_STK_GENERATION_METHOD_PASSKEY | - SM_STK_GENERATION_METHOD_NUMERIC_COMPARISON; -constexpr uint32_t kAbxyHotkeyButtonMask = - SWITCH_ABXY_HOTKEY_BUTTON_MASK; -constexpr uint32_t kAbxyHotkeyMiscMask = SWITCH_ABXY_HOTKEY_MISC_MASK; -constexpr bool kDefaultSwapAbxy = SWITCH_ABXY_DEFAULT_SWAPPED != 0; -constexpr uint32_t kAbxyFeedbackDurationMs = - SWITCH_ABXY_FEEDBACK_DURATION_MS; -constexpr uint8_t kAbxyFeedbackWeakMagnitude = - SWITCH_ABXY_FEEDBACK_WEAK_MAGNITUDE; -constexpr uint8_t kAbxyFeedbackStrongMagnitude = - SWITCH_ABXY_FEEDBACK_STRONG_MAGNITUDE; -constexpr uint32_t kMotionHotkeyDpadMask = - SWITCH_MOTION_HOTKEY_DPAD_MASK; -constexpr uint32_t kMotionHotkeyButtonMask = - SWITCH_MOTION_HOTKEY_BUTTON_MASK; -constexpr uint32_t kMotionHotkeyMiscMask = - SWITCH_MOTION_HOTKEY_MISC_MASK; -constexpr bool kDefaultMotionEnabled = - SWITCH_MOTION_DEFAULT_ENABLED != 0; -constexpr uint16_t kMotionDisabledFeedbackDurationMs = - SWITCH_MOTION_DISABLED_FEEDBACK_DURATION_MS; -constexpr uint8_t kMotionDisabledFeedbackWeakMagnitude = - SWITCH_MOTION_DISABLED_FEEDBACK_WEAK_MAGNITUDE; -constexpr uint8_t kMotionDisabledFeedbackStrongMagnitude = - SWITCH_MOTION_DISABLED_FEEDBACK_STRONG_MAGNITUDE; -constexpr uint16_t kMotionEnabledFeedbackDurationMs = - SWITCH_MOTION_ENABLED_FEEDBACK_DURATION_MS; -constexpr uint8_t kMotionEnabledFeedbackWeakMagnitude = - SWITCH_MOTION_ENABLED_FEEDBACK_WEAK_MAGNITUDE; -constexpr uint8_t kMotionEnabledFeedbackStrongMagnitude = - SWITCH_MOTION_ENABLED_FEEDBACK_STRONG_MAGNITUDE; - -static_assert(kAbxyHotkeyButtonMask != 0); -static_assert(kAbxyHotkeyMiscMask != 0); -static_assert(kAbxyFeedbackDurationMs > 0); -static_assert(kMotionHotkeyDpadMask != 0); -static_assert(kMotionHotkeyButtonMask != 0); -static_assert(kMotionHotkeyMiscMask != 0); -static_assert(kMotionDisabledFeedbackDurationMs > 0); -static_assert(kMotionEnabledFeedbackDurationMs > 0); - -static_assert(kSlotCount == 4); -static_assert(SWITCH_PICO_HID_INSTANCE_COUNT == kSlotCount); - -enum class ConnectionStatus { - Initializing, - Scanning, - Connecting, - Ready, -}; - -enum class ConnectionPolicyState { - Uninitialized, - Open, - Passive, - Paused, - FailedClosed, -}; - -struct RumbleEnvelope { - uint8_t slot; - uint32_t connection_generation; - SwitchRumbleOutput rumble; -}; -struct FeedbackEnvelope { - uint32_t connection_generation; - uint16_t duration_ms; - uint8_t weak_magnitude; - uint8_t strong_magnitude; -}; - - -struct BackendSlot { - SwitchInputState state; - // Non-null with active=false is a connected device still becoming ready. - uni_hid_device_t* device; - uint32_t state_generation; - uint32_t connection_generation; - bool active; - bool rumble_pending; - bool swap_abxy; - bool abxy_hotkey_latched; - bool motion_enabled; - bool motion_hotkey_latched; - bool feedback_pending; - uint32_t feedback_until_ms; - RumbleEnvelope pending_rumble; - FeedbackEnvelope pending_feedback; -}; +constexpr uint kRumbleQueueDepth = 8; critical_section_t g_state_lock; -BackendSlot g_slots[kSlotCount]; +queue_t g_rumble_queue; +SwitchInputState g_shared_state; +bool g_shared_controller_active = false; +uint32_t g_shared_generation = 0; -// These acknowledgement generations and the pairing request producer are only -// used by Core 0. The request is transferred under the cross-core state lock. -uint32_t g_consumed_generation[kSlotCount]{}; -uint32_t g_last_snapshot_generation[kSlotCount]{}; -bool g_pairing_window_requested = false; -bool g_clear_pairings_requested = false; -bool g_pairing_snapshot_requested = false; +// These generations are only read or written by Core 0. +uint32_t g_consumed_generation = 0; +uint32_t g_last_snapshot_generation = 0; bool g_initialized = false; bool g_started = false; -// These fields are only read or written by Core 1 / BTstack. +// This pointer and the timer are only read or written by Core 1 / BTstack. +uni_hid_device_t* g_active_device = nullptr; btstack_timer_source_t g_rumble_timer{}; -ConnectionStatus g_connection_status = ConnectionStatus::Initializing; -btstack_packet_callback_registration_t g_pairing_event_callback{}; -ConnectionPolicyState g_connection_policy_state = - ConnectionPolicyState::Uninitialized; -uint32_t g_pairing_window_deadline_ms = 0; -uint32_t g_pairing_reset_feedback_deadline_ms = 0; -uint16_t g_status_led_tick = 0; -bool g_pairing_window_open = false; -bool g_status_led_on = false; -Bluepad32PairingSnapshot g_pairing_snapshot{}; SwitchInputState make_neutral_state() { SwitchInputState state{}; @@ -156,94 +47,12 @@ SwitchInputState make_neutral_state() { return state; } -bool valid_slot(uint8_t slot) { - return slot < kSlotCount; -} - -bool has_free_slot() { +void publish_state(const SwitchInputState& state, bool controller_active) { critical_section_enter_blocking(&g_state_lock); - bool free_slot = false; - for (const BackendSlot& slot : g_slots) { - free_slot = free_slot || slot.device == nullptr; - } + g_shared_state = state; + g_shared_controller_active = controller_active; + ++g_shared_generation; critical_section_exit(&g_state_lock); - return free_slot; -} - -bool has_active_controller() { - critical_section_enter_blocking(&g_state_lock); - bool active_controller = false; - for (const BackendSlot& slot : g_slots) { - active_controller = active_controller || slot.active; - } - critical_section_exit(&g_state_lock); - return active_controller; -} - -int slot_for_device(const uni_hid_device_t* device) { - if (device == nullptr) { - return -1; - } - const int slot = uni_hid_device_get_idx_for_instance(device); - return slot >= 0 && slot < kSlotCount ? slot : -1; -} -void apply_slot_lighting(uint8_t slot_index, uni_hid_device_t* device) { - const SwitchRgbColor color = - switch_pro_get_slot_light_color(slot_index); - if (device->report_parser.set_lightbar_color != nullptr) { - device->report_parser.set_lightbar_color( - device, color.red, color.green, color.blue); - } else if (device->report_parser.set_player_leds != nullptr) { - device->report_parser.set_player_leds( - device, static_cast(1u << slot_index)); - } -} - - -ConnectionStatus compute_connection_status() { - critical_section_enter_blocking(&g_state_lock); - bool all_ready = true; - bool any_connecting = false; - for (const BackendSlot& slot : g_slots) { - const bool has_device = slot.device != nullptr; - all_ready = all_ready && slot.active && has_device; - any_connecting = any_connecting || (!slot.active && has_device); - } - critical_section_exit(&g_state_lock); - - if (all_ready) { - return ConnectionStatus::Ready; - } - return any_connecting ? ConnectionStatus::Connecting - : ConnectionStatus::Scanning; -} - -void publish_device_state(uint8_t slot, uni_hid_device_t* device, - const SwitchInputState& state) { - critical_section_enter_blocking(&g_state_lock); - BackendSlot& target = g_slots[slot]; - if (target.active && target.device == device) { - target.state = state; - ++target.state_generation; - } - critical_section_exit(&g_state_lock); -} - -void publish_all_neutral() { - critical_section_enter_blocking(&g_state_lock); - for (BackendSlot& slot : g_slots) { - slot.state = make_neutral_state(); - slot.device = nullptr; - slot.active = false; - slot.rumble_pending = false; - ++slot.state_generation; - ++slot.connection_generation; - } - critical_section_exit(&g_state_lock); - g_connection_status = ConnectionStatus::Initializing; - g_connection_policy_state = ConnectionPolicyState::FailedClosed; - g_pairing_window_open = false; - g_status_led_tick = 0; } constexpr int32_t clamp_axis(int32_t value) { @@ -276,8 +85,7 @@ constexpr int16_t clamp_int16(int64_t value) { return static_cast(value); } -constexpr int64_t divide_round_nearest(int64_t numerator, - int64_t denominator) { +constexpr int64_t divide_round_nearest(int64_t numerator, int64_t denominator) { if (numerator >= 0) { return (numerator + denominator / 2) / denominator; } @@ -311,9 +119,7 @@ bool has_motion(const uni_gamepad_t& gamepad) { return false; } -SwitchInputState map_gamepad(const uni_gamepad_t& gamepad, - bool swap_abxy, - bool motion_enabled) { +SwitchInputState map_gamepad(const uni_gamepad_t& gamepad) { SwitchInputState state = make_neutral_state(); state.dpad_up = (gamepad.dpad & DPAD_UP) != 0; @@ -326,14 +132,6 @@ SwitchInputState map_gamepad(const uni_gamepad_t& gamepad, state.button_a = (gamepad.buttons & BUTTON_B) != 0; state.button_y = (gamepad.buttons & BUTTON_X) != 0; state.button_x = (gamepad.buttons & BUTTON_Y) != 0; - if (swap_abxy) { - bool temporary = state.button_a; - state.button_a = state.button_b; - state.button_b = temporary; - temporary = state.button_x; - state.button_x = state.button_y; - state.button_y = temporary; - } state.button_l = (gamepad.buttons & BUTTON_SHOULDER_L) != 0; state.button_r = (gamepad.buttons & BUTTON_SHOULDER_R) != 0; state.button_zl = (gamepad.buttons & BUTTON_TRIGGER_L) != 0 || gamepad.brake >= kTriggerThreshold; @@ -351,7 +149,7 @@ SwitchInputState map_gamepad(const uni_gamepad_t& gamepad, state.rx = scale_stick(gamepad.axis_rx); state.ry = scale_stick(gamepad.axis_ry); - if (motion_enabled && has_motion(gamepad)) { + if (has_motion(gamepad)) { // Dependency patches normalize both arrays to SDL3 PlayStation axes. SwitchImuSample sample{}; sample.accel_x = convert_accel(-static_cast(gamepad.accel[2])); @@ -368,555 +166,88 @@ SwitchInputState map_gamepad(const uni_gamepad_t& gamepad, return state; } -struct HotkeyDecision { - bool swap_abxy; - bool motion_enabled; - uint32_t suppress_dpad; - uint32_t suppress_buttons; - uint32_t suppress_misc_buttons; -}; - -void queue_local_feedback(BackendSlot& slot, uint16_t duration_ms, - uint8_t weak_magnitude, - uint8_t strong_magnitude) { - slot.feedback_pending = true; - slot.pending_feedback = { - slot.connection_generation, duration_ms, weak_magnitude, - strong_magnitude}; -} -void reset_slot_hotkeys(BackendSlot& slot) { - slot.swap_abxy = kDefaultSwapAbxy; - slot.abxy_hotkey_latched = false; - slot.motion_enabled = kDefaultMotionEnabled; - slot.motion_hotkey_latched = false; - slot.feedback_pending = false; - slot.feedback_until_ms = 0; - slot.pending_feedback = {}; -} - - -HotkeyDecision update_controller_hotkeys( - uint8_t slot_index, uni_hid_device_t* device, - const uni_gamepad_t& gamepad) { - const bool abxy_pressed = - (gamepad.buttons & kAbxyHotkeyButtonMask) == - kAbxyHotkeyButtonMask && - (gamepad.misc_buttons & kAbxyHotkeyMiscMask) == - kAbxyHotkeyMiscMask; - const bool motion_pressed = - (gamepad.dpad & kMotionHotkeyDpadMask) == - kMotionHotkeyDpadMask && - (gamepad.buttons & kMotionHotkeyButtonMask) == - kMotionHotkeyButtonMask && - (gamepad.misc_buttons & kMotionHotkeyMiscMask) == - kMotionHotkeyMiscMask; - HotkeyDecision decision{ - kDefaultSwapAbxy, kDefaultMotionEnabled, 0, 0, 0}; - - critical_section_enter_blocking(&g_state_lock); - BackendSlot& slot = g_slots[slot_index]; - if (slot.active && slot.device == device) { - if (abxy_pressed && !slot.abxy_hotkey_latched) { - slot.swap_abxy = !slot.swap_abxy; - queue_local_feedback( - slot, static_cast(kAbxyFeedbackDurationMs), - kAbxyFeedbackWeakMagnitude, - kAbxyFeedbackStrongMagnitude); - } else if (motion_pressed && !slot.motion_hotkey_latched) { - slot.motion_enabled = !slot.motion_enabled; - if (slot.motion_enabled) { - queue_local_feedback( - slot, kMotionEnabledFeedbackDurationMs, - kMotionEnabledFeedbackWeakMagnitude, - kMotionEnabledFeedbackStrongMagnitude); - } else { - queue_local_feedback( - slot, kMotionDisabledFeedbackDurationMs, - kMotionDisabledFeedbackWeakMagnitude, - kMotionDisabledFeedbackStrongMagnitude); - } - } - slot.abxy_hotkey_latched = abxy_pressed; - slot.motion_hotkey_latched = motion_pressed; - decision.swap_abxy = slot.swap_abxy; - decision.motion_enabled = slot.motion_enabled; - if (abxy_pressed) { - decision.suppress_buttons |= kAbxyHotkeyButtonMask; - decision.suppress_misc_buttons |= kAbxyHotkeyMiscMask; - } - if (motion_pressed) { - decision.suppress_dpad |= kMotionHotkeyDpadMask; - decision.suppress_buttons |= kMotionHotkeyButtonMask; - decision.suppress_misc_buttons |= kMotionHotkeyMiscMask; - } - } - critical_section_exit(&g_state_lock); - return decision; -} - - -bool pairing_window_active_at(uint32_t now_ms) { - return g_pairing_window_open && - static_cast(now_ms - g_pairing_window_deadline_ms) < 0; -} - -void handle_pairing_hci_event(uint8_t packet_type, uint16_t channel, - uint8_t* packet, uint16_t size) { - (void)channel; - if (packet_type != HCI_EVENT_PACKET || packet == nullptr || size < 8) { - return; - } - - bd_addr_t address{}; - const bool pairing_open = - pairing_window_active_at(btstack_run_loop_get_time_ms()); - switch (hci_event_packet_get_type(packet)) { - case HCI_EVENT_USER_CONFIRMATION_REQUEST: - hci_event_user_confirmation_request_get_bd_addr(packet, address); - if (pairing_open) { - gap_ssp_confirmation_response(address); - } else { - gap_ssp_confirmation_negative(address); - } - break; - case HCI_EVENT_USER_PASSKEY_REQUEST: - hci_event_user_passkey_request_get_bd_addr(packet, address); - if (pairing_open) { - gap_ssp_passkey_response(address, 0); - } else { - gap_ssp_passkey_negative(address); - } - break; - default: - break; - } -} - -bool update_pairing_window(uint32_t now_ms) { - critical_section_enter_blocking(&g_state_lock); - const bool requested = g_pairing_window_requested; - g_pairing_window_requested = false; - critical_section_exit(&g_state_lock); - - if (requested) { - g_pairing_window_open = true; - g_pairing_window_deadline_ms = now_ms + kPairingWindowDurationMs; - gap_set_bondable_mode(true); - sm_set_accepted_stk_generation_methods(kAllBlePairingMethods); - g_status_led_tick = 0; - return true; - } - if (g_pairing_window_open && !pairing_window_active_at(now_ms)) { - g_pairing_window_open = false; - sm_set_accepted_stk_generation_methods(0); - g_status_led_tick = 0; - gap_set_bondable_mode(false); - return true; - } - return false; -} -void append_pairing_record( - Bluepad32PairingSnapshot& snapshot, - Bluepad32PairingTransport transport, uint8_t address_type, - const bd_addr_t address) { - if (snapshot.record_count >= BLUEPAD32_PAIRING_RECORD_CAPACITY) { - snapshot.overflow = true; - return; - } - Bluepad32PairingRecord& record = - snapshot.records[snapshot.record_count++]; - record.transport = transport; - record.address_type = address_type; - memcpy(record.address, address, sizeof(record.address)); -} - -void refresh_pairing_snapshot() { - Bluepad32PairingSnapshot snapshot{}; - snapshot.status = Bluepad32PairingSnapshotStatus::kReady; - - btstack_link_key_iterator_t iterator{}; - if (gap_link_key_iterator_init(&iterator)) { - bd_addr_t address{}; - link_key_t link_key{}; - link_key_type_t link_key_type{}; - while (gap_link_key_iterator_get_next( - &iterator, address, link_key, &link_key_type)) { - append_pairing_record( - snapshot, Bluepad32PairingTransport::kClassic, - BD_ADDR_TYPE_UNKNOWN, address); - } - gap_link_key_iterator_done(&iterator); - } - - for (int index = 0; index < le_device_db_max_count(); ++index) { - int address_type = BD_ADDR_TYPE_UNKNOWN; - bd_addr_t address{}; - le_device_db_info(index, &address_type, address, nullptr); - if (address_type == BD_ADDR_TYPE_UNKNOWN) { - continue; - } - append_pairing_record( - snapshot, Bluepad32PairingTransport::kBle, - static_cast(address_type), address); - } - - critical_section_enter_blocking(&g_state_lock); - snapshot.generation = g_pairing_snapshot.generation + 1; - g_pairing_snapshot = snapshot; - g_pairing_snapshot_requested = false; - critical_section_exit(&g_state_lock); -} - -void process_pairing_snapshot_request() { - critical_section_enter_blocking(&g_state_lock); - const bool requested = g_pairing_snapshot_requested; - critical_section_exit(&g_state_lock); - if (requested) { - refresh_pairing_snapshot(); - } -} - -void apply_connection_policy(); - -void process_clear_pairings(uint32_t now_ms) { - uni_hid_device_t* devices[kSlotCount]{}; - critical_section_enter_blocking(&g_state_lock); - const bool requested = g_clear_pairings_requested; - g_clear_pairings_requested = false; - if (requested) { - g_pairing_window_requested = false; - for (uint8_t slot_index = 0; slot_index < kSlotCount; ++slot_index) { - BackendSlot& slot = g_slots[slot_index]; - devices[slot_index] = slot.device; - slot.state = make_neutral_state(); - slot.device = nullptr; - slot.active = false; - slot.rumble_pending = false; - slot.feedback_pending = false; - slot.feedback_until_ms = 0; - reset_slot_hotkeys(slot); - ++slot.state_generation; - ++slot.connection_generation; - } - } - critical_section_exit(&g_state_lock); - if (!requested) { - return; - } - - g_pairing_window_open = false; - gap_set_bondable_mode(false); - sm_set_accepted_stk_generation_methods(0); - uni_bt_del_keys_unsafe(); - for (uni_hid_device_t* device : devices) { - if (device != nullptr) { - uni_hid_device_disconnect(device); - } - } - refresh_pairing_snapshot(); - - g_connection_status = ConnectionStatus::Scanning; - g_status_led_tick = 0; - g_pairing_reset_feedback_deadline_ms = - now_ms + kPairingResetFeedbackDurationMs; - apply_connection_policy(); -} - - -void apply_connection_policy() { - const bool free_slot = has_free_slot(); - const bool active_controller = has_active_controller(); - const bool pairing_open = - pairing_window_active_at(btstack_run_loop_get_time_ms()); - const bool active_scan = - free_slot && (!active_controller || pairing_open); - const ConnectionPolicyState desired_state = - !free_slot - ? ConnectionPolicyState::Paused - : (active_scan ? ConnectionPolicyState::Open - : ConnectionPolicyState::Passive); - if (g_connection_policy_state == desired_state) { - return; - } - - // Classic inquiry and BLE scanning consume radio time and measurably delay - // active controller HID traffic. Stop them before every policy transition. - uni_bt_stop_scanning_unsafe(); - - if (!free_slot) { - uni_bt_allow_incoming_connections(false); - g_connection_policy_state = ConnectionPolicyState::Paused; - return; - } - - // Passive mode still accepts controller-initiated reconnects without - // running inquiry. Active discovery is reserved for zero-controller idle - // state and the explicit BOOTSEL pairing window. - uni_bt_allow_incoming_connections(true); - if (active_scan) { - uni_bt_start_scanning_and_autoconnect_unsafe(); - g_connection_policy_state = ConnectionPolicyState::Open; - } else { - g_connection_policy_state = ConnectionPolicyState::Passive; - } -} - -void update_status_led() { - ++g_status_led_tick; - const uint32_t now_ms = btstack_run_loop_get_time_ms(); - bool led_on = false; - - if (static_cast( - now_ms - g_pairing_reset_feedback_deadline_ms) < 0) { - led_on = (g_status_led_tick % 20) < 10; - } else if (pairing_window_active_at(now_ms)) { - const uint16_t phase = g_status_led_tick % 200; - led_on = phase < 20 || (phase >= 40 && phase < 60); - } else if (g_connection_status == ConnectionStatus::Connecting) { - led_on = (g_status_led_tick % 40) < 20; - } else if (g_connection_status == ConnectionStatus::Initializing || - has_active_controller()) { - led_on = true; - } else { - led_on = (g_status_led_tick % 200) < 100; - } - - if (led_on != g_status_led_on) { - cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, led_on); - g_status_led_on = led_on; - } -} void process_rumble_timer(btstack_timer_source_t* timer) { - const uint32_t now_ms = btstack_run_loop_get_time_ms(); - process_clear_pairings(now_ms); - process_pairing_snapshot_request(); - if (update_pairing_window(now_ms)) { - apply_connection_policy(); + SwitchRumbleOutput packet{}; + SwitchRumbleOutput latest{}; + bool have_packet = false; + while (queue_try_remove(&g_rumble_queue, &packet)) { + latest = packet; + have_packet = true; } - for (uint8_t slot_index = 0; slot_index < kSlotCount; ++slot_index) { - RumbleEnvelope envelope{}; - FeedbackEnvelope feedback{}; - uni_hid_device_t* device = nullptr; - bool feedback_dispatch = false; - bool host_dispatch = false; - - critical_section_enter_blocking(&g_state_lock); - BackendSlot& slot = g_slots[slot_index]; - if (slot.feedback_pending) { - feedback = slot.pending_feedback; - feedback_dispatch = - slot.active && slot.device != nullptr && - feedback.connection_generation == - slot.connection_generation && - slot.device->report_parser.play_dual_rumble != nullptr; - slot.feedback_pending = false; - if (feedback_dispatch) { - device = slot.device; - slot.feedback_until_ms = - now_ms + feedback.duration_ms; - } - } - - const bool feedback_active = - static_cast(now_ms - slot.feedback_until_ms) < 0; - if (!feedback_dispatch && !feedback_active && - slot.rumble_pending) { - envelope = slot.pending_rumble; - slot.rumble_pending = false; - host_dispatch = - envelope.slot == slot_index && slot.active && - slot.device != nullptr && - envelope.connection_generation == - slot.connection_generation; - if (host_dispatch) { - device = slot.device; - } - } - critical_section_exit(&g_state_lock); - - if (feedback_dispatch) { - device->report_parser.play_dual_rumble( - device, 0, feedback.duration_ms, - feedback.weak_magnitude, feedback.strong_magnitude); - } else if (host_dispatch && - device->report_parser.play_dual_rumble != nullptr) { - device->report_parser.play_dual_rumble( - device, 0, kRumbleDurationMs, - envelope.rumble.high_frequency_magnitude, - envelope.rumble.low_frequency_magnitude); - } + if (have_packet && g_active_device != nullptr && + g_active_device->report_parser.play_dual_rumble != nullptr) { + g_active_device->report_parser.play_dual_rumble( + g_active_device, 0, kRumbleDurationMs, + latest.high_frequency_magnitude, latest.low_frequency_magnitude); } - update_status_led(); btstack_run_loop_set_timer(timer, kRumblePollIntervalMs); btstack_run_loop_add_timer(timer); } -void recompute_connection_status() { - const uint32_t now_ms = btstack_run_loop_get_time_ms(); - update_pairing_window(now_ms); - g_connection_status = compute_connection_status(); - g_status_led_tick = 0; - apply_connection_policy(); -} - void platform_init(int argc, const char** argv) { (void)argc; (void)argv; } void platform_on_init_complete() { - gap_set_link_supervision_timeout(kClassicLinkSupervisionTimeout); - gap_set_bondable_mode(false); - sm_set_accepted_stk_generation_methods(0); - gap_ssp_set_auto_accept(false); - g_pairing_event_callback.callback = handle_pairing_hci_event; - hci_add_event_handler(&g_pairing_event_callback); - refresh_pairing_snapshot(); - // Keep Bluepad32 autoconnect active whenever at least one slot is free. btstack_run_loop_set_timer_handler(&g_rumble_timer, process_rumble_timer); btstack_run_loop_set_timer(&g_rumble_timer, kRumblePollIntervalMs); btstack_run_loop_add_timer(&g_rumble_timer); - recompute_connection_status(); + + uni_bt_allow_incoming_connections(true); + uni_bt_start_scanning_and_autoconnect_unsafe(); } -uni_error_t platform_on_device_discovered(bd_addr_t addr, const char* name, - uint16_t cod, uint8_t rssi) { +uni_error_t platform_on_device_discovered(bd_addr_t addr, const char* name, uint16_t cod, uint8_t rssi) { (void)addr; (void)name; (void)cod; (void)rssi; - return has_free_slot() && - g_connection_policy_state == ConnectionPolicyState::Open - ? UNI_ERROR_SUCCESS - : UNI_ERROR_IGNORE_DEVICE; + return g_active_device == nullptr ? UNI_ERROR_SUCCESS : UNI_ERROR_IGNORE_DEVICE; } void platform_on_device_connected(uni_hid_device_t* device) { - if (device == nullptr) { - return; - } - if (g_connection_policy_state != ConnectionPolicyState::Open && - g_connection_policy_state != ConnectionPolicyState::Passive) { - uni_hid_device_disconnect(device); - return; - } + (void)device; +} - const int slot_index = slot_for_device(device); - if (slot_index < 0) { - return; - } - - bool tracked_connection = false; - critical_section_enter_blocking(&g_state_lock); - BackendSlot& slot = g_slots[slot_index]; - if (!slot.active && slot.device == nullptr) { - slot.device = device; - slot.rumble_pending = false; - reset_slot_hotkeys(slot); - tracked_connection = true; - } else { - tracked_connection = slot.device == device; - } - critical_section_exit(&g_state_lock); - - if (tracked_connection) { - recompute_connection_status(); - } +void resume_connections() { + uni_bt_allow_incoming_connections(true); + uni_bt_start_scanning_and_autoconnect_unsafe(); } void platform_on_device_disconnected(uni_hid_device_t* device) { - const int slot_index = slot_for_device(device); - if (slot_index < 0) { - return; - } - - bool disconnected_tracked_device = false; - critical_section_enter_blocking(&g_state_lock); - BackendSlot& slot = g_slots[slot_index]; - if (slot.device == device) { - if (slot.active) { - slot.state = make_neutral_state(); - ++slot.state_generation; - } - slot.device = nullptr; - slot.active = false; - slot.rumble_pending = false; - reset_slot_hotkeys(slot); - ++slot.connection_generation; - disconnected_tracked_device = true; - } - critical_section_exit(&g_state_lock); - - if (disconnected_tracked_device) { - // Re-evaluate from scratch: resume discovery only after the final - // active controller disconnects; otherwise keep passive incoming - // reconnect support without inquiry-induced latency. - g_connection_policy_state = ConnectionPolicyState::Uninitialized; - recompute_connection_status(); + if (device == g_active_device) { + g_active_device = nullptr; + publish_state(make_neutral_state(), false); + resume_connections(); + } else if (g_active_device == nullptr) { + resume_connections(); } } uni_error_t platform_on_device_ready(uni_hid_device_t* device) { - if (device == nullptr || !uni_hid_device_is_gamepad(device)) { + if (!uni_hid_device_is_gamepad(device)) { return UNI_ERROR_INVALID_CONTROLLER; } - - const int slot_index = slot_for_device(device); - if (slot_index < 0) { + if (g_active_device != nullptr && g_active_device != device) { return UNI_ERROR_NO_SLOTS; } - bool occupied_mismatch = false; - bool became_active = false; - critical_section_enter_blocking(&g_state_lock); - BackendSlot& slot = g_slots[slot_index]; - occupied_mismatch = slot.device != nullptr && slot.device != device; - if (!occupied_mismatch) { - slot.device = device; - if (!slot.active) { - slot.state = make_neutral_state(); - slot.active = true; - slot.rumble_pending = false; - reset_slot_hotkeys(slot); - ++slot.state_generation; - became_active = true; - } - } - critical_section_exit(&g_state_lock); - - if (occupied_mismatch) { - return UNI_ERROR_NO_SLOTS; - } - if (became_active) { - apply_slot_lighting(static_cast(slot_index), device); - } - - - recompute_connection_status(); + g_active_device = device; + publish_state(make_neutral_state(), true); + uni_bt_stop_scanning_unsafe(); + uni_bt_allow_incoming_connections(false); return UNI_ERROR_SUCCESS; } -void platform_on_controller_data(uni_hid_device_t* device, - uni_controller_t* controller) { - const int slot_index = slot_for_device(device); - if (slot_index < 0 || controller == nullptr || - controller->klass != UNI_CONTROLLER_CLASS_GAMEPAD) { +void platform_on_controller_data(uni_hid_device_t* device, uni_controller_t* controller) { + if (device != g_active_device || controller == nullptr || controller->klass != UNI_CONTROLLER_CLASS_GAMEPAD) { return; } - - uni_gamepad_t gamepad = controller->gamepad; - const HotkeyDecision hotkeys = update_controller_hotkeys( - static_cast(slot_index), device, gamepad); - gamepad.dpad &= ~hotkeys.suppress_dpad; - gamepad.buttons &= ~hotkeys.suppress_buttons; - gamepad.misc_buttons &= ~hotkeys.suppress_misc_buttons; - publish_device_state( - static_cast(slot_index), device, - map_gamepad(gamepad, hotkeys.swap_abxy, - hotkeys.motion_enabled)); + publish_state(map_gamepad(controller->gamepad), true); } const uni_property_t* platform_get_property(uni_property_idx_t index) { @@ -948,26 +279,20 @@ uni_platform* get_platform() { return &platform; } -[[noreturn]] void halt_wireless_backend() { - publish_all_neutral(); - while (true) { - tight_loop_contents(); - } -} - [[noreturn]] void core1_main() { - if (!flash_safe_execute_core_init()) { - halt_wireless_backend(); - } if (cyw43_arch_init() != 0) { - halt_wireless_backend(); + publish_state(make_neutral_state(), false); + while (true) { + tight_loop_contents(); + } } - cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, true); - g_status_led_on = true; uni_platform_set_custom(get_platform()); if (uni_init(0, nullptr) != 0) { - halt_wireless_backend(); + publish_state(make_neutral_state(), false); + while (true) { + tight_loop_contents(); + } } btstack_run_loop_execute(); @@ -984,26 +309,12 @@ void bluepad32_input_backend_init() { } critical_section_init(&g_state_lock); - for (uint8_t slot_index = 0; slot_index < kSlotCount; ++slot_index) { - BackendSlot& slot = g_slots[slot_index]; - slot = {}; - slot.state = make_neutral_state(); - slot.pending_rumble.slot = slot_index; - reset_slot_hotkeys(slot); - g_consumed_generation[slot_index] = 0; - g_last_snapshot_generation[slot_index] = 0; - } - g_pairing_window_requested = false; - g_pairing_snapshot_requested = false; - g_pairing_snapshot = {}; - g_pairing_snapshot.status = - Bluepad32PairingSnapshotStatus::kPending; - g_clear_pairings_requested = false; - g_connection_status = ConnectionStatus::Initializing; - g_connection_policy_state = ConnectionPolicyState::Uninitialized; - g_pairing_window_deadline_ms = 0; - g_pairing_reset_feedback_deadline_ms = 0; - g_pairing_window_open = false; + queue_init(&g_rumble_queue, sizeof(SwitchRumbleOutput), kRumbleQueueDepth); + g_shared_state = make_neutral_state(); + g_shared_controller_active = false; + g_shared_generation = 0; + g_consumed_generation = 0; + g_last_snapshot_generation = 0; g_initialized = true; } @@ -1014,68 +325,13 @@ void bluepad32_input_backend_start() { if (g_started) { return; } - // Core 0 services USB from flash while Core 1 owns BTstack. Register both - // cores before either side can initiate a flash-backed BTstack TLV write. - if (!flash_safe_execute_core_init()) { - g_connection_policy_state = ConnectionPolicyState::FailedClosed; - return; - } - g_started = true; multicore_launch_core1(core1_main); } -void bluepad32_input_backend_open_pairing_window() { - if (!g_initialized) { - bluepad32_input_backend_init(); - } - - critical_section_enter_blocking(&g_state_lock); - g_pairing_window_requested = true; - critical_section_exit(&g_state_lock); -} -void bluepad32_input_backend_clear_pairings() { - if (!g_initialized) { - bluepad32_input_backend_init(); - } - - critical_section_enter_blocking(&g_state_lock); - g_clear_pairings_requested = true; - g_pairing_snapshot.status = - Bluepad32PairingSnapshotStatus::kPending; - critical_section_exit(&g_state_lock); -} - -void bluepad32_input_backend_request_pairing_snapshot() { - if (!g_initialized) { - bluepad32_input_backend_init(); - } - - critical_section_enter_blocking(&g_state_lock); - g_pairing_snapshot_requested = true; - g_pairing_snapshot.status = - Bluepad32PairingSnapshotStatus::kPending; - critical_section_exit(&g_state_lock); -} - -void bluepad32_input_backend_pairing_snapshot( - Bluepad32PairingSnapshot* out) { +bool bluepad32_input_backend_snapshot(SwitchInputState* out) { if (out == nullptr) { - return; - } - if (!g_initialized) { - bluepad32_input_backend_init(); - } - - critical_section_enter_blocking(&g_state_lock); - *out = g_pairing_snapshot; - critical_section_exit(&g_state_lock); -} - - -bool bluepad32_input_backend_snapshot(uint8_t slot_index, SwitchInputState* out) { - if (out == nullptr || !valid_slot(slot_index)) { return false; } if (!g_initialized) { @@ -1084,36 +340,33 @@ bool bluepad32_input_backend_snapshot(uint8_t slot_index, SwitchInputState* out) } critical_section_enter_blocking(&g_state_lock); - *out = g_slots[slot_index].state; - const bool controller_active = g_slots[slot_index].active; - const uint32_t generation = g_slots[slot_index].state_generation; + *out = g_shared_state; + const bool controller_active = g_shared_controller_active; + const uint32_t generation = g_shared_generation; critical_section_exit(&g_state_lock); - if (generation == g_consumed_generation[slot_index]) { + if (generation == g_consumed_generation) { out->imu_sample_count = 0; } - g_last_snapshot_generation[slot_index] = generation; + g_last_snapshot_generation = generation; return controller_active; } -void bluepad32_input_backend_report_sent(uint8_t slot_index) { - if (!g_initialized || !valid_slot(slot_index)) { +void bluepad32_input_backend_report_sent() { + if (!g_initialized) { return; } - g_consumed_generation[slot_index] = g_last_snapshot_generation[slot_index]; + g_consumed_generation = g_last_snapshot_generation; } -void bluepad32_input_backend_queue_rumble(uint8_t slot_index, - const SwitchRumbleOutput& rumble) { - if (!g_initialized || !valid_slot(slot_index)) { +void bluepad32_input_backend_queue_rumble(const SwitchRumbleOutput& rumble) { + if (!g_initialized) { return; } - critical_section_enter_blocking(&g_state_lock); - BackendSlot& slot = g_slots[slot_index]; - if (slot.active && slot.device != nullptr) { - slot.pending_rumble = {slot_index, slot.connection_generation, rumble}; - slot.rumble_pending = true; + if (!queue_try_add(&g_rumble_queue, &rumble)) { + SwitchRumbleOutput discarded{}; + (void)queue_try_remove(&g_rumble_queue, &discarded); + (void)queue_try_add(&g_rumble_queue, &rumble); } - critical_section_exit(&g_state_lock); } diff --git a/bluepad32_input_backend.h b/bluepad32_input_backend.h index 74ed309..2360860 100644 --- a/bluepad32_input_backend.h +++ b/bluepad32_input_backend.h @@ -2,45 +2,11 @@ #include -#include "switch_haptics.h" #include "switch_pro_driver.h" - -constexpr uint8_t BLUEPAD32_INPUT_BACKEND_SLOT_COUNT = 4; -constexpr uint8_t BLUEPAD32_PAIRING_RECORD_CAPACITY = 16; - -enum class Bluepad32PairingTransport : uint8_t { - kClassic = 1, - kBle = 2, -}; - -enum class Bluepad32PairingSnapshotStatus : uint8_t { - kReady = 0, - kPending = 1, -}; - -struct Bluepad32PairingRecord { - Bluepad32PairingTransport transport; - uint8_t address_type; - uint8_t address[6]; -}; - -struct Bluepad32PairingSnapshot { - uint32_t generation; - Bluepad32PairingSnapshotStatus status; - uint8_t record_count; - bool overflow; - Bluepad32PairingRecord records[BLUEPAD32_PAIRING_RECORD_CAPACITY]; -}; - +#include "switch_haptics.h" void bluepad32_input_backend_init(); void bluepad32_input_backend_start(); -void bluepad32_input_backend_open_pairing_window(); -void bluepad32_input_backend_clear_pairings(); -bool bluepad32_input_backend_snapshot(uint8_t slot, SwitchInputState* out); -void bluepad32_input_backend_request_pairing_snapshot(); -void bluepad32_input_backend_pairing_snapshot( - Bluepad32PairingSnapshot* out); -void bluepad32_input_backend_report_sent(uint8_t slot); -void bluepad32_input_backend_queue_rumble(uint8_t slot, - const SwitchRumbleOutput& rumble); +bool bluepad32_input_backend_snapshot(SwitchInputState* out); +void bluepad32_input_backend_report_sent(); +void bluepad32_input_backend_queue_rumble(const SwitchRumbleOutput& rumble); diff --git a/bootsel_pairing_button.cpp b/bootsel_pairing_button.cpp deleted file mode 100644 index fc50a46..0000000 --- a/bootsel_pairing_button.cpp +++ /dev/null @@ -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(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(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()); -} diff --git a/bootsel_pairing_button.h b/bootsel_pairing_button.h deleted file mode 100644 index 9cc37bc..0000000 --- a/bootsel_pairing_button.h +++ /dev/null @@ -1,32 +0,0 @@ -#pragma once - -#include - -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(); diff --git a/build.py b/build.py index 5bb0a45..8b728d3 100644 --- a/build.py +++ b/build.py @@ -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() UF2_PATH = Path(os.environ.get("UF2_PATH", BUILD_DIR / "switch-pico.uf2")).expanduser() -MACROS = tuple( - f"SWITCH_COLOR_SLOT_{slot}_{component}" - for slot in range(1, 5) - for component in ("R", "G", "B") +MACROS = ( + "SWITCH_COLOR_LEFT_GRIP_R", + "SWITCH_COLOR_LEFT_GRIP_G", + "SWITCH_COLOR_LEFT_GRIP_B", + "SWITCH_COLOR_RIGHT_GRIP_R", + "SWITCH_COLOR_RIGHT_GRIP_G", + "SWITCH_COLOR_RIGHT_GRIP_B", ) def parse_args(): @@ -43,12 +46,12 @@ def parse_args(): group.add_argument( "--random-grip-color", action="store_true", - help="Assign one random color to every emulated controller slot.", + help="Randomize both grip colors before building.", ) group.add_argument( "--grip-color", 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() @@ -81,7 +84,7 @@ def update_grip_colors(rgb_hex): sys.exit(1) return updated - values = (r, g, b) * 4 + values = (r, g, b, r, g, b) for macro, val in zip(MACROS, values): text = replace(macro, val, text) diff --git a/controller_color_config.h b/controller_color_config.h index cc9316a..4abdf51 100644 --- a/controller_color_config.h +++ b/controller_color_config.h @@ -1,31 +1,25 @@ -// Compile-time Switch grip colors. Physical controller lightbar values are -// derived automatically; each value here is an 8-bit RGB component. +// Optional override for Switch Pro colour fields. +// Copy/modify the values below and rebuild to change how the controller appears on the Switch. +// Each value is an 8-bit RGB component. #pragma once -// Body shell color +// Body shell colour #define SWITCH_COLOR_BODY_R 0x1B #define SWITCH_COLOR_BODY_G 0x1B #define SWITCH_COLOR_BODY_B 0x1D -// Face/button cluster color +// Face/button cluster colour #define SWITCH_COLOR_BUTTON_R 0xFF #define SWITCH_COLOR_BUTTON_G 0xFF #define SWITCH_COLOR_BUTTON_B 0xFF -// Per-slot Switch grip colors: blue, red, yellow, green. -#define SWITCH_COLOR_SLOT_1_R 0x00 -#define SWITCH_COLOR_SLOT_1_G 0x89 -#define SWITCH_COLOR_SLOT_1_B 0xEB +// Left grip colour +#define SWITCH_COLOR_LEFT_GRIP_R 0x00 +#define SWITCH_COLOR_LEFT_GRIP_G 0x89 +#define SWITCH_COLOR_LEFT_GRIP_B 0xEB -#define SWITCH_COLOR_SLOT_2_R 0xE6 -#define SWITCH_COLOR_SLOT_2_G 0x39 -#define SWITCH_COLOR_SLOT_2_B 0x46 - -#define SWITCH_COLOR_SLOT_3_R 0xF6 -#define SWITCH_COLOR_SLOT_3_G 0xC9 -#define SWITCH_COLOR_SLOT_3_B 0x45 - -#define SWITCH_COLOR_SLOT_4_R 0x2E -#define SWITCH_COLOR_SLOT_4_G 0xCC -#define SWITCH_COLOR_SLOT_4_B 0x71 +// Right grip colour +#define SWITCH_COLOR_RIGHT_GRIP_R 0x00 +#define SWITCH_COLOR_RIGHT_GRIP_G 0x89 +#define SWITCH_COLOR_RIGHT_GRIP_B 0xEB diff --git a/controller_hotkey_config.h b/controller_hotkey_config.h deleted file mode 100644 index ebd2cc0..0000000 --- a/controller_hotkey_config.h +++ /dev/null @@ -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 diff --git a/firmware/switch-pico-aio.elf b/firmware/switch-pico-aio.elf index c490d09..5bb03c2 100755 Binary files a/firmware/switch-pico-aio.elf and b/firmware/switch-pico-aio.elf differ diff --git a/firmware/switch-pico-aio.uf2 b/firmware/switch-pico-aio.uf2 index 1fd9861..79327bd 100644 Binary files a/firmware/switch-pico-aio.uf2 and b/firmware/switch-pico-aio.uf2 differ diff --git a/patches/bluepad32-sdl3-imu.patch b/patches/bluepad32-sdl3-imu.patch index 5ba7ef7..7060f78 100644 --- a/patches/bluepad32-sdl3-imu.patch +++ b/patches/bluepad32-sdl3-imu.patch @@ -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 index ea063b8..7670caf 100644 --- a/src/components/bluepad32/parser/uni_hid_parser_ds4.c @@ -120,149 +84,11 @@ index a22ef26..3d5ecef 100644 int32_t calib_data = mult_frac(ins->accel_calib_data[i].sens_numer, raw_data, ins->accel_calib_data[i].sens_denom); ctl->gamepad.accel[i] = calib_data; -diff --git a/src/components/bluepad32/parser/uni_hid_parser_psmove.c b/src/components/bluepad32/parser/uni_hid_parser_psmove.c -index 0265f93..5c0f2bb 100644 ---- a/src/components/bluepad32/parser/uni_hid_parser_psmove.c -+++ b/src/components/bluepad32/parser/uni_hid_parser_psmove.c -@@ -8,6 +8,7 @@ - */ - - #include "parser/uni_hid_parser_psmove.h" -+#include "parser/uni_hid_parser_imu.h" - - #include - -@@ -27,11 +28,6 @@ typedef enum psmove_fsm { - PSMOVE_FSM_LED_UPDATED, // LED updated - } psmove_fsm_t; - --typedef enum psmove_model { -- PSMOVE_MODEL_UNK, -- PSMOVE_MODEL_ZCM1, -- PSMOVE_MODEL_ZCM2, --} psmove_model_t; - - typedef enum { - PSMOVE_STATE_RUMBLE_DISABLED, -@@ -41,9 +37,10 @@ typedef enum { - - // psmove_instance_t represents data used by the psmove driver instance. - typedef struct psmove_instance_s { -- psmove_model_t model; -+ uni_psmove_imu_model_t model; - psmove_fsm_t state; - uint8_t led_rgb[3]; -+ uni_psmove_imu_calibration_t imu_calibration; - - btstack_timer_source_t rumble_timer_duration; - btstack_timer_source_t rumble_timer_delayed_start; -@@ -127,6 +124,7 @@ static void psmove_send_output_report(uni_hid_device_t* d, psmove_output_report_ - static void on_psmove_set_rumble_on(btstack_timer_source_t* ts); - static void on_psmove_set_rumble_off(btstack_timer_source_t* ts); - static void psmove_play_dual_rumble_now(uni_hid_device_t* d, uint16_t duration_ms, uint8_t magnitude); -+static void psmove_request_calibration_report(uni_hid_device_t* d); - - void uni_hid_parser_psmove_init_report(uni_hid_device_t* d) { - uni_controller_t* ctl = &d->controller; -@@ -154,6 +152,7 @@ void uni_hid_parser_psmove_parse_input_report(uni_hid_device_t* d, const uint8_t - } - - uni_controller_t* ctl = &d->controller; -+ psmove_instance_t* ins = get_psmove_instance(d); - - // Buttons - if (r->buttons[0] & 0x01) -@@ -187,18 +186,39 @@ void uni_hid_parser_psmove_parse_input_report(uni_hid_device_t* d, const uint8_t - - ctl->gamepad.throttle = r->trigger * 4; - -- ctl->gamepad.accel[0] = r->accel_x; -- ctl->gamepad.accel[1] = r->accel_y; -- ctl->gamepad.accel[2] = r->accel_z; -- -- ctl->gamepad.gyro[0] = r->gyro_x; -- ctl->gamepad.gyro[1] = r->gyro_y; -- ctl->gamepad.gyro[2] = r->gyro_z; -+ const uint16_t accel_first[3] = {r->accel_x, r->accel_y, r->accel_z}; -+ const uint16_t accel_second[3] = { -+ r->accel_x2, r->accel_y2, r->accel_z2}; -+ const uint16_t gyro_first[3] = {r->gyro_x, r->gyro_y, r->gyro_z}; -+ const uint16_t gyro_second[3] = { -+ r->gyro_x2, r->gyro_y2, r->gyro_z2}; -+ uni_imu_fixed_sample_t motion; -+ if (uni_psmove_normalize_imu( -+ ins->model, &ins->imu_calibration, accel_first, accel_second, -+ gyro_first, gyro_second, &motion)) { -+ memcpy(ctl->gamepad.accel, motion.accel, sizeof(motion.accel)); -+ memcpy(ctl->gamepad.gyro, motion.gyro, sizeof(motion.gyro)); -+ } - - if (r->battery <= 5) - ctl->battery = r->battery * 51; - } - -+void uni_hid_parser_psmove_parse_feature_report( -+ uni_hid_device_t* d, const uint8_t* report, uint16_t len) { -+ psmove_instance_t* ins = get_psmove_instance(d); -+ const uni_psmove_calibration_result_t result = -+ uni_psmove_add_calibration_report( -+ &ins->imu_calibration, ins->model, report, len); -+ if (result == UNI_PSMOVE_CALIBRATION_INCOMPLETE) { -+ psmove_request_calibration_report(d); -+ } else if (result == UNI_PSMOVE_CALIBRATION_COMPLETE) { -+ logi("psmove: IMU calibration ready\n"); -+ } else if (result == UNI_PSMOVE_CALIBRATION_INVALID) { -+ loge("psmove: invalid IMU calibration; motion disabled\n"); -+ } -+} -+ - void uni_hid_parser_psmove_play_dual_rumble(struct uni_hid_device_s* d, - uint16_t start_delay_ms, - uint16_t duration_ms, -@@ -261,25 +281,34 @@ void uni_hid_parser_psmove_setup(struct uni_hid_device_s* d) { - - switch (d->product_id) { - case ZCM1_PID: -- ins->model = PSMOVE_MODEL_ZCM1; -+ ins->model = UNI_PSMOVE_IMU_MODEL_ZCM1; - logi("psmove: Detected ZCM1 model\n"); - break; - case ZCM2_PID: -- ins->model = PSMOVE_MODEL_ZCM2; -+ ins->model = UNI_PSMOVE_IMU_MODEL_ZCM2; - logi("psmove: Detected ZCM2 model\n"); - break; - default: -- loge("psmove: Unknown PSMove PID = %#x, assuming ZCM1\n", ins->model); -- ins->model = PSMOVE_MODEL_ZCM1; -+ loge("psmove: Unknown PSMove PID = %#x, assuming ZCM1\n", d->product_id); -+ ins->model = UNI_PSMOVE_IMU_MODEL_ZCM1; - break; - } - -+ psmove_request_calibration_report(d); - uni_hid_device_set_ready_complete(d); - } - - // - // Helpers - // -+static void psmove_request_calibration_report(uni_hid_device_t* d) { -+ static const uint8_t report[] = { -+ ((HID_MESSAGE_TYPE_GET_REPORT << 4) | HID_REPORT_TYPE_FEATURE), -+ 0x10, -+ }; -+ uni_hid_device_send_ctrl_report(d, report, sizeof(report)); -+} -+ - static psmove_instance_t* get_psmove_instance(uni_hid_device_t* d) { - return (psmove_instance_t*)&d->parser_data[0]; - } diff --git a/src/components/bluepad32/parser/uni_hid_parser_switch.c b/src/components/bluepad32/parser/uni_hid_parser_switch.c -index 599fc35..9f073b4 100644 +index 599fc35..c72f056 100644 --- a/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_GYRO_OFFSET = 0; static const int16_t DEFAULT_GYRO_SCALE = 13371; @@ -272,97 +98,7 @@ index 599fc35..9f073b4 100644 #define SWITCH_FACTORY_IMU_CAL_DATA_SIZE 24 static const uint16_t SWITCH_FACTORY_IMU_CAL_DATA_ADDR = 0x6020; - - #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) { +@@ -823,19 +824,26 @@ static void parse_imu(uni_hid_device_t* d, const struct switch_imu_data_s* r) { switch_instance_t* ins = get_switch_instance(d); uni_controller_t* ctl = &d->controller; @@ -398,7 +134,7 @@ index 599fc35..9f073b4 100644 if (ins->controller_type == SWITCH_CONTROLLER_TYPE_JCR) { accel[1] = -accel[1]; 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]; } @@ -416,153 +152,3 @@ index 599fc35..9f073b4 100644 } // Process 0x30 input report: SWITCH_INPUT_IMU_DATA -@@ -1172,6 +1206,18 @@ static void fsm_enable_imu(struct uni_hid_device_s* d) { - req->data[0] = (ins->mode == SWITCH_MODE_IMU); - send_subcmd(d, req, sizeof(out)); - } -+static void fsm_enable_rumble(struct uni_hid_device_s* d) { -+ switch_instance_t* ins = get_switch_instance(d); -+ ins->state = STATE_ENABLE_RUMBLE; -+ -+ uint8_t out[sizeof(struct switch_subcmd_request) + 1] = {0}; -+ struct switch_subcmd_request* req = (struct switch_subcmd_request*)&out[0]; -+ req->report_id = OUTPUT_RUMBLE_AND_SUBCMD; -+ req->subcmd_id = SUBCMD_ENABLE_RUMBLE; -+ req->data[0] = 0x01; -+ send_subcmd(d, req, sizeof(out)); -+} -+ - - static void fsm_update_led(struct uni_hid_device_s* d) { - switch_instance_t* ins = get_switch_instance(d); -@@ -1203,6 +1249,10 @@ static struct switch_rumble_freq_data find_rumble_freq(uint16_t freq) { - return rumble_freqs[i]; - } - -+static uint16_t switch_magnitude_to_amp(uint8_t magnitude) { -+ return (uint16_t)(((uint32_t)magnitude * 1003 + 127) / 255); -+} -+ - static struct switch_rumble_amp_data find_rumble_amp(uint16_t amp) { - unsigned int i = 0; - if (amp > rumble_amps[0].amp) { -@@ -1259,6 +1309,7 @@ void uni_hid_parser_switch_play_dual_rumble(struct uni_hid_device_s* d, - break; - case SWITCH_STATE_RUMBLE_IN_PROGRESS: - btstack_run_loop_remove_timer(&ins->rumble_timer_duration); -+ btstack_run_loop_remove_timer(&ins->rumble_timer_refresh); - break; - default: - // Do nothing -@@ -1366,6 +1417,7 @@ static void switch_stop_rumble_now(uni_hid_device_t* d) { - - // No need to protect it with a mutex since it runs in the same main thread - assert(ins->rumble_state == SWITCH_STATE_RUMBLE_IN_PROGRESS); -+ btstack_run_loop_remove_timer(&ins->rumble_timer_refresh); - ins->rumble_state = SWITCH_STATE_RUMBLE_DISABLED; - - struct switch_subcmd_request req = {0}; -@@ -1379,6 +1431,22 @@ static void switch_stop_rumble_now(uni_hid_device_t* d) { - send_subcmd(d, (struct switch_subcmd_request*)&req, sizeof(req) - 1); - } - -+static void switch_send_dual_rumble_now(uni_hid_device_t* d, -+ uint8_t weak_magnitude, -+ uint8_t strong_magnitude) { -+ struct switch_subcmd_request req = { -+ .report_id = OUTPUT_RUMBLE_ONLY, -+ }; -+ // Fixed frequencies match the standard Switch LRA envelope and the -+ // 8BitDo Switch-mode implementation. Magnitudes control amplitude only. -+ switch_encode_rumble(req.rumble_left, 453, 135, -+ switch_magnitude_to_amp(weak_magnitude)); -+ switch_encode_rumble(req.rumble_right, 453, 99, -+ switch_magnitude_to_amp(strong_magnitude)); -+ // Rumble request don't include the last byte of "switch_subcmd_request": subcmd_id -+ send_subcmd(d, &req, sizeof(req) - 1); -+} -+ - static void switch_play_dual_rumble_now(uni_hid_device_t* d, - uint16_t duration_ms, - uint8_t weak_magnitude, -@@ -1391,14 +1459,17 @@ static void switch_play_dual_rumble_now(uni_hid_device_t* d, - return; - } - -- struct switch_subcmd_request req = { -- .report_id = OUTPUT_RUMBLE_ONLY, -- }; -- switch_encode_rumble(req.rumble_left, weak_magnitude << 2, weak_magnitude, 500); -- switch_encode_rumble(req.rumble_right, strong_magnitude << 2, strong_magnitude, 500); -+ ins->rumble_weak_magnitude = weak_magnitude; -+ ins->rumble_strong_magnitude = strong_magnitude; -+ switch_send_dual_rumble_now(d, weak_magnitude, strong_magnitude); - -- // Rumble request don't include the last byte of "switch_subcmd_request": subcmd_id -- send_subcmd(d, &req, sizeof(req) - 1); -+ // Refresh active rumble for Switch-compatible controllers that do not -+ // retain a single output packet, including 8BitDo Switch mode. -+ ins->rumble_timer_refresh.process = &on_switch_refresh_rumble; -+ ins->rumble_timer_refresh.context = d; -+ btstack_run_loop_set_timer(&ins->rumble_timer_refresh, -+ SWITCH_RUMBLE_REFRESH_MS); -+ btstack_run_loop_add_timer(&ins->rumble_timer_refresh); - - // Set timer to turn off rumble - ins->rumble_timer_duration.process = &on_switch_set_rumble_off; -@@ -1414,6 +1485,20 @@ static void on_switch_set_rumble_on(btstack_timer_source_t* ts) { - - switch_play_dual_rumble_now(d, ins->rumble_duration_ms, ins->rumble_weak_magnitude, ins->rumble_strong_magnitude); - } -+static void on_switch_refresh_rumble(btstack_timer_source_t* ts) { -+ uni_hid_device_t* d = btstack_run_loop_get_timer_context(ts); -+ switch_instance_t* ins = get_switch_instance(d); -+ if (ins->rumble_state != SWITCH_STATE_RUMBLE_IN_PROGRESS) { -+ return; -+ } -+ switch_send_dual_rumble_now( -+ d, (uint8_t)ins->rumble_weak_magnitude, -+ (uint8_t)ins->rumble_strong_magnitude); -+ btstack_run_loop_set_timer(&ins->rumble_timer_refresh, -+ SWITCH_RUMBLE_REFRESH_MS); -+ btstack_run_loop_add_timer(&ins->rumble_timer_refresh); -+} -+ - - static void on_switch_set_rumble_off(btstack_timer_source_t* ts) { - uni_hid_device_t* d = btstack_run_loop_get_timer_context(ts); -diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/components/bluepad32/parser/uni_hid_parser_wii.c -index be2103e..4819639 100644 ---- a/src/components/bluepad32/parser/uni_hid_parser_wii.c -+++ b/src/components/bluepad32/parser/uni_hid_parser_wii.c -@@ -19,6 +19,7 @@ - #endif // ENABLE_EEPROM_DUMP - - #include "parser/uni_hid_parser_wii.h" -+#include "parser/uni_hid_parser_imu.h" - - #include "controller/uni_controller.h" - #include "hid_usage.h" -@@ -585,9 +586,7 @@ static void process_drm_ka(uni_hid_device_t* d, const uint8_t* report, uint16_t - - uni_controller_t* ctl = &d->controller; - -- ctl->gamepad.accel[0] = sx; -- ctl->gamepad.accel[1] = sy; -- ctl->gamepad.accel[2] = sz; -+ uni_imu_normalize_wii_accel(sx, sy, sz, ctl->gamepad.accel); - - // Dpad works as dpad, useful to navigate menus. - ctl->gamepad.dpad |= (report[1] & 0x01) ? DPAD_DOWN : 0; -diff --git a/src/components/bluepad32/uni_hid_device.c b/src/components/bluepad32/uni_hid_device.c -index 67841e8..9fe7134 100644 ---- a/src/components/bluepad32/uni_hid_device.c -+++ b/src/components/bluepad32/uni_hid_device.c -@@ -655,6 +655,7 @@ void uni_hid_device_guess_controller_type_from_pid_vid(uni_hid_device_t* d) { - d->report_parser.setup = uni_hid_parser_psmove_setup; - d->report_parser.init_report = uni_hid_parser_psmove_init_report; - d->report_parser.parse_input_report = uni_hid_parser_psmove_parse_input_report; -+ d->report_parser.parse_feature_report = uni_hid_parser_psmove_parse_feature_report; - d->report_parser.set_lightbar_color = uni_hid_parser_psmove_set_lightbar_color; - d->report_parser.play_dual_rumble = uni_hid_parser_psmove_play_dual_rumble; - logi("Device detected as PS Move: 0x%02x\n", type); diff --git a/pyproject.toml b/pyproject.toml index 9eefdf6..79a0508 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,13 +14,11 @@ dependencies = [ "PySDL3", "rich", "hidapi", - "pyusb", ] [project.scripts] controller-uart-bridge = "switch_pico_bridge.controller_uart_bridge:main" host-uart-logger = "switch_pico_bridge.host_uart_logger:main" -switch-pico-pairings = "switch_pico_bridge.pairing_manager:main" [tool.setuptools] package-dir = {"" = "src"} diff --git a/src/switch_pico_bridge/pairing_manager.py b/src/switch_pico_bridge/pairing_manager.py deleted file mode 100755 index 837fbd5..0000000 --- a/src/switch_pico_bridge/pairing_manager.py +++ /dev/null @@ -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(" 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()) diff --git a/switch-pico.cpp b/switch-pico.cpp index 04a2dcb..ab91bed 100644 --- a/switch-pico.cpp +++ b/switch-pico.cpp @@ -7,7 +7,6 @@ #include "hardware/uart.h" #else #include "bluepad32_input_backend.h" -#include "bootsel_pairing_button.h" #endif #ifdef SWITCH_PICO_LOG @@ -26,19 +25,11 @@ #define UART_RUMBLE_TYPE 0x02 #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_ready = false; + +// Track the latest state provided by UART or the autopilot. +static SwitchInputState g_user_state; #ifndef SWITCH_PICO_BLUEPAD32 static void init_uart_input() { @@ -75,17 +66,10 @@ static void send_rumble_uart_frame(const SwitchRumbleOutput& rumble) { } #endif -static void on_rumble_from_switch(uint8_t instance, - const SwitchRumbleOutput& rumble) { +static void on_rumble_from_switch(const SwitchRumbleOutput& rumble) { #ifdef SWITCH_PICO_BLUEPAD32 - if (instance >= BLUEPAD32_INPUT_BACKEND_SLOT_COUNT) { - return; - } - bluepad32_input_backend_queue_rumble(instance, rumble); + bluepad32_input_backend_queue_rumble(rumble); #else - if (instance != SWITCH_HID_INSTANCE) { - return; - } send_rumble_uart_frame(rumble); #endif } @@ -134,7 +118,7 @@ static bool poll_uart_frames() { if (expected_len > 0 && index >= expected_len) { SwitchInputState parsed{}; - if (switch_pro_apply_uart_packet(buffer, expected_len, parsed)) { + if (switch_pro_apply_uart_packet(buffer, expected_len, &parsed)) { g_user_state = parsed; new_data = true; LOG_PRINTF("[UART] packet buttons=0x%04x hat=%u lx=%u ly=%u rx=%u ry=%u\n", @@ -169,29 +153,16 @@ static bool poll_uart_frames() { static void log_usb_state() { bool mounted = tud_mounted(); + bool ready = switch_pro_is_ready(); + if (mounted != g_last_mounted) { g_last_mounted = mounted; LOG_PRINTF("[USB] %s\n", mounted ? "mounted" : "unmounted"); } - -#ifdef SWITCH_PICO_BLUEPAD32 - for (uint8_t instance = 0; - instance < BLUEPAD32_INPUT_BACKEND_SLOT_COUNT; ++instance) { - const bool ready = switch_pro_is_ready(instance); - if (ready != g_last_ready[instance]) { - g_last_ready[instance] = ready; - LOG_PRINTF("[SWITCH %u] driver %s\n", instance, - ready ? "ready (handshake OK)" : "not ready"); - } - } -#else - const bool ready = switch_pro_is_ready(SWITCH_HID_INSTANCE); if (ready != g_last_ready) { g_last_ready = ready; - LOG_PRINTF("[SWITCH] driver %s\n", - ready ? "ready (handshake OK)" : "not ready"); + LOG_PRINTF("[SWITCH] driver %s\n", ready ? "ready (handshake OK)" : "not ready"); } -#endif } int main() { @@ -205,21 +176,10 @@ int main() { #endif tusb_init(); -#ifdef SWITCH_PICO_BLUEPAD32 - for (uint8_t instance = 0; - instance < BLUEPAD32_INPUT_BACKEND_SLOT_COUNT; ++instance) { - switch_pro_init(instance); - switch_pro_set_rumble_callback(instance, on_rumble_from_switch); - g_user_states[instance] = neutral_input(); - switch_pro_set_input(instance, g_user_states[instance]); - } -#else - switch_pro_init(SWITCH_HID_INSTANCE); - switch_pro_set_rumble_callback(SWITCH_HID_INSTANCE, - on_rumble_from_switch); + switch_pro_init(); + switch_pro_set_rumble_callback(on_rumble_from_switch); g_user_state = neutral_input(); - switch_pro_set_input(SWITCH_HID_INSTANCE, g_user_state); -#endif + switch_pro_set_input(g_user_state); #ifdef SWITCH_PICO_BLUEPAD32 bluepad32_input_backend_start(); @@ -233,31 +193,19 @@ int main() { while (true) { tud_task(); // USB device tasks #ifdef SWITCH_PICO_BLUEPAD32 - switch (bootsel_pairing_button_task()) { - case BootselPairingButtonEvent::kOpenPairing: - bluepad32_input_backend_open_pairing_window(); - break; - case BootselPairingButtonEvent::kClearPairings: - bluepad32_input_backend_clear_pairings(); - break; - case BootselPairingButtonEvent::kNone: - break; - } - for (uint8_t instance = 0; - instance < BLUEPAD32_INPUT_BACKEND_SLOT_COUNT; ++instance) { - bluepad32_input_backend_snapshot(instance, - &g_user_states[instance]); - switch_pro_set_input(instance, g_user_states[instance]); - if (switch_pro_task(instance)) { - bluepad32_input_backend_report_sent(instance); - } - } + bluepad32_input_backend_snapshot(&g_user_state); #else bool new_data = poll_uart_frames(); // Pull controller state from UART1 (void)new_data; +#endif SwitchInputState state = g_user_state; - switch_pro_set_input(SWITCH_HID_INSTANCE, state); - (void)switch_pro_task(SWITCH_HID_INSTANCE); + switch_pro_set_input(state); +#ifdef SWITCH_PICO_BLUEPAD32 + if (switch_pro_task()) { + bluepad32_input_backend_report_sent(); + } +#else + (void)switch_pro_task(); #endif log_usb_state(); } diff --git a/switch_pro_descriptors.h b/switch_pro_descriptors.h index fd0d1a2..2ec3444 100644 --- a/switch_pro_descriptors.h +++ b/switch_pro_descriptors.h @@ -8,14 +8,6 @@ #pragma once #include -#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 @@ -89,7 +81,7 @@ typedef enum { GET_VOLTAGE = 0x50, } SwitchCommands; -struct SwitchAnalog { +typedef struct { uint8_t data[3]; void setX(uint16_t x) { @@ -109,10 +101,10 @@ struct SwitchAnalog { uint16_t getY() { return static_cast((data[1] >> 4)) | (data[2] << 4); } -}; +} SwitchAnalog; // left and right calibration are stored differently for some reason, so two structs -struct SwitchLeftCalibration { +typedef struct { uint8_t data[9]; void getMin(uint16_t& x, uint16_t& y) const { packCalib(6, x, y); } @@ -145,9 +137,9 @@ struct SwitchLeftCalibration { x = static_cast(data[offset]) | ((data[offset + 1] & 0x0F) << 8); y = static_cast(data[offset + 2] << 4) | (data[offset + 1] >> 4); } -}; +} SwitchLeftCalibration; -struct SwitchRightCalibration { +typedef struct { uint8_t data[9]; void getMin(uint16_t& x, uint16_t& y) const { packCalib(3, x, y); } @@ -180,7 +172,7 @@ struct SwitchRightCalibration { x = static_cast(data[offset]) | ((data[offset + 1] & 0x0F) << 8); y = static_cast(data[offset + 2] << 4) | (data[offset + 1] >> 4); } -}; +} SwitchRightCalibration; typedef struct { @@ -377,19 +369,8 @@ static const uint8_t switch_pro_configuration_descriptor[] = { 0x09, // bLength 0x02, // bDescriptorType (Configuration) -#if SWITCH_PICO_HID_INSTANCE_COUNT == 1 0x29, 0x00, // wTotalLength 41 0x01, // bNumInterfaces 1 -#elif SWITCH_PICO_HID_INSTANCE_COUNT == 2 - 0x49, 0x00, // wTotalLength 73 - 0x02, // bNumInterfaces 2 -#elif SWITCH_PICO_HID_INSTANCE_COUNT == 3 - 0x69, 0x00, // wTotalLength 105 - 0x03, // bNumInterfaces 3 -#else - 0x89, 0x00, // wTotalLength 137 - 0x04, // bNumInterfaces 4 -#endif 0x01, // bConfigurationValue 0x00, // iConfiguration (String Index) 0xA0, // bmAttributes Remote Wakeup @@ -426,108 +407,6 @@ static const uint8_t switch_pro_configuration_descriptor[] = 0x03, // bmAttributes (Interrupt) 0x40, 0x00, // wMaxPacketSize 64 0x08, // bInterval 8 (unit depends on device speed) - -#if SWITCH_PICO_HID_INSTANCE_COUNT >= 2 - 0x09, // bLength - 0x04, // bDescriptorType (Interface) - 0x01, // bInterfaceNumber 1 - 0x00, // bAlternateSetting - 0x02, // bNumEndpoints 2 - 0x03, // bInterfaceClass - 0x00, // bInterfaceSubClass - 0x00, // bInterfaceProtocol - 0x00, // iInterface (String Index) - - 0x09, // bLength - 0x21, // bDescriptorType (HID) - 0x11, 0x01, // bcdHID 1.11 - 0x00, // bCountryCode - 0x01, // bNumDescriptors - 0x22, // bDescriptorType[0] (HID) - 0xCB, 0x00, // wDescriptorLength[0] 203 - - 0x07, // bLength - 0x05, // bDescriptorType (Endpoint) - 0x82, // bEndpointAddress (IN/D2H) - 0x03, // bmAttributes (Interrupt) - 0x40, 0x00, // wMaxPacketSize 64 - 0x08, // bInterval 8 (unit depends on device speed) - - 0x07, // bLength - 0x05, // bDescriptorType (Endpoint) - 0x02, // bEndpointAddress (OUT/H2D) - 0x03, // bmAttributes (Interrupt) - 0x40, 0x00, // wMaxPacketSize 64 - 0x08, // bInterval 8 (unit depends on device speed) -#endif - -#if SWITCH_PICO_HID_INSTANCE_COUNT >= 3 - 0x09, // bLength - 0x04, // bDescriptorType (Interface) - 0x02, // bInterfaceNumber 2 - 0x00, // bAlternateSetting - 0x02, // bNumEndpoints 2 - 0x03, // bInterfaceClass - 0x00, // bInterfaceSubClass - 0x00, // bInterfaceProtocol - 0x00, // iInterface (String Index) - - 0x09, // bLength - 0x21, // bDescriptorType (HID) - 0x11, 0x01, // bcdHID 1.11 - 0x00, // bCountryCode - 0x01, // bNumDescriptors - 0x22, // bDescriptorType[0] (HID) - 0xCB, 0x00, // wDescriptorLength[0] 203 - - 0x07, // bLength - 0x05, // bDescriptorType (Endpoint) - 0x83, // bEndpointAddress (IN/D2H) - 0x03, // bmAttributes (Interrupt) - 0x40, 0x00, // wMaxPacketSize 64 - 0x08, // bInterval 8 (unit depends on device speed) - - 0x07, // bLength - 0x05, // bDescriptorType (Endpoint) - 0x03, // bEndpointAddress (OUT/H2D) - 0x03, // bmAttributes (Interrupt) - 0x40, 0x00, // wMaxPacketSize 64 - 0x08, // bInterval 8 (unit depends on device speed) -#endif - -#if SWITCH_PICO_HID_INSTANCE_COUNT >= 4 - 0x09, // bLength - 0x04, // bDescriptorType (Interface) - 0x03, // bInterfaceNumber 3 - 0x00, // bAlternateSetting - 0x02, // bNumEndpoints 2 - 0x03, // bInterfaceClass - 0x00, // bInterfaceSubClass - 0x00, // bInterfaceProtocol - 0x00, // iInterface (String Index) - - 0x09, // bLength - 0x21, // bDescriptorType (HID) - 0x11, 0x01, // bcdHID 1.11 - 0x00, // bCountryCode - 0x01, // bNumDescriptors - 0x22, // bDescriptorType[0] (HID) - 0xCB, 0x00, // wDescriptorLength[0] 203 - - 0x07, // bLength - 0x05, // bDescriptorType (Endpoint) - 0x84, // bEndpointAddress (IN/D2H) - 0x03, // bmAttributes (Interrupt) - 0x40, 0x00, // wMaxPacketSize 64 - 0x08, // bInterval 8 (unit depends on device speed) - - 0x07, // bLength - 0x05, // bDescriptorType (Endpoint) - 0x04, // bEndpointAddress (OUT/H2D) - 0x03, // bmAttributes (Interrupt) - 0x40, 0x00, // wMaxPacketSize 64 - 0x08, // bInterval 8 (unit depends on device speed) -#endif }; static const uint8_t switch_pro_report_descriptor[] = diff --git a/switch_pro_driver.cpp b/switch_pro_driver.cpp index 3cb8a2f..76fe99c 100644 --- a/switch_pro_driver.cpp +++ b/switch_pro_driver.cpp @@ -1,9 +1,9 @@ #include "switch_pro_driver.h" #include -#include #include #include +#include #include #include "pico/rand.h" #include "pico/time.h" @@ -22,62 +22,40 @@ // gyro (3 frames assumed 5ms apart delivered too often) => wild camera swing. #define SWITCH_PRO_IMU_REPORT_TIMER 15 +static SwitchInputState g_input_state{ + false, false, false, false, + false, false, false, false, false, false, false, false, + false, false, false, false, false, false, + SWITCH_PRO_JOYSTICK_MID, SWITCH_PRO_JOYSTICK_MID, + SWITCH_PRO_JOYSTICK_MID, SWITCH_PRO_JOYSTICK_MID}; + +static uint8_t report_buffer[SWITCH_PRO_ENDPOINT_SIZE] = {}; +static uint8_t last_report[SWITCH_PRO_ENDPOINT_SIZE] = {}; +static SwitchProReport switch_report{}; +static uint8_t last_report_counter = 0; +static uint32_t last_report_timer = 0; +static uint32_t last_host_activity_ms = 0; +static bool is_ready = false; +static bool is_initialized = false; +static bool is_report_queued = false; +static bool report_sent = false; +static uint8_t queued_report_id = 0; +static bool forced_ready = false; +static uint8_t handshake_counter = 0; + +static SwitchDeviceInfo device_info{}; +static uint8_t player_id = 0; +static uint8_t input_mode = 0x30; enum class SwitchImuMode : uint8_t { Off = 0, Raw = 1, Quaternion = 2, }; -struct MotionQuaternion { - float x; - float y; - float z; - float w; - int16_t accel_x; - int16_t accel_y; - int16_t accel_z; -}; +static SwitchImuMode imu_mode = SwitchImuMode::Off; +static bool is_vibration_enabled = false; -struct SwitchProContext { - SwitchInputState input_state{}; - uint8_t report_buffer[SWITCH_PRO_ENDPOINT_SIZE]{}; - SwitchProReport switch_report{}; - uint8_t last_report_counter = 0; - uint32_t last_report_timer = 0; - bool is_ready = false; - bool is_initialized = false; - bool is_report_queued = false; - SwitchDeviceInfo device_info{}; - SwitchRgbColor grip_color{}; - uint8_t player_id = 0; - uint8_t input_mode = 0x30; - SwitchImuMode imu_mode = SwitchImuMode::Off; - bool is_vibration_enabled = false; - uint16_t left_min_x = 0; - uint16_t left_min_y = 0; - uint16_t left_max_x = 0; - uint16_t left_max_y = 0; - uint16_t right_min_x = 0; - uint16_t right_min_y = 0; - uint16_t right_max_x = 0; - uint16_t right_max_y = 0; - SwitchRumbleCallback rumble_callback = nullptr; - SwitchHapticsDecoder rumble_decoder{}; - MotionQuaternion motion_quaternion{0.0f, 0.0f, 0.0f, 1.0f, 0, 0, 0}; -}; - -static_assert(SWITCH_PICO_HID_INSTANCE_COUNT > 0, - "at least one HID instance is required"); -static SwitchProContext contexts[SWITCH_PICO_HID_INSTANCE_COUNT]{}; - -static SwitchProContext* context_for(uint8_t instance) { - if (instance >= SWITCH_PICO_HID_INSTANCE_COUNT) { - return nullptr; - } - return &contexts[instance]; -} - -// Optional compile-time color palette shared with Bluetooth controller LEDs. +// Optional compile-time colour override (body/buttons/grips). #if __has_include("controller_color_config.h") #include "controller_color_config.h" #endif @@ -91,46 +69,25 @@ static SwitchProContext* context_for(uint8_t instance) { #define SWITCH_COLOR_BUTTON_G 0xFF #define SWITCH_COLOR_BUTTON_B 0xFF #endif -#ifndef SWITCH_COLOR_SLOT_1_R -#define SWITCH_COLOR_SLOT_1_R 0x00 -#define SWITCH_COLOR_SLOT_1_G 0x89 -#define SWITCH_COLOR_SLOT_1_B 0xEB -#define SWITCH_COLOR_SLOT_2_R 0xE6 -#define SWITCH_COLOR_SLOT_2_G 0x39 -#define SWITCH_COLOR_SLOT_2_B 0x46 -#define SWITCH_COLOR_SLOT_3_R 0xF6 -#define SWITCH_COLOR_SLOT_3_G 0xC9 -#define SWITCH_COLOR_SLOT_3_B 0x45 -#define SWITCH_COLOR_SLOT_4_R 0x2E -#define SWITCH_COLOR_SLOT_4_G 0xCC -#define SWITCH_COLOR_SLOT_4_B 0x71 +#ifndef SWITCH_COLOR_LEFT_GRIP_R +#define SWITCH_COLOR_LEFT_GRIP_R 0xEC +#define SWITCH_COLOR_LEFT_GRIP_G 0x00 +#define SWITCH_COLOR_LEFT_GRIP_B 0x8C +#endif +#ifndef SWITCH_COLOR_RIGHT_GRIP_R +#define SWITCH_COLOR_RIGHT_GRIP_R 0xEC +#define SWITCH_COLOR_RIGHT_GRIP_G 0x00 +#define SWITCH_COLOR_RIGHT_GRIP_B 0x8C #endif - -static constexpr SwitchRgbColor slot_colors[] = { - {SWITCH_COLOR_SLOT_1_R, SWITCH_COLOR_SLOT_1_G, SWITCH_COLOR_SLOT_1_B}, - {SWITCH_COLOR_SLOT_2_R, SWITCH_COLOR_SLOT_2_G, SWITCH_COLOR_SLOT_2_B}, - {SWITCH_COLOR_SLOT_3_R, SWITCH_COLOR_SLOT_3_G, SWITCH_COLOR_SLOT_3_B}, - {SWITCH_COLOR_SLOT_4_R, SWITCH_COLOR_SLOT_4_G, SWITCH_COLOR_SLOT_4_B}, -}; - -static_assert(SWITCH_PICO_HID_INSTANCE_COUNT <= - sizeof(slot_colors) / sizeof(slot_colors[0])); - -SwitchRgbColor switch_pro_get_slot_color(uint8_t instance) { - if (instance >= sizeof(slot_colors) / sizeof(slot_colors[0])) { - return {}; - } - return slot_colors[instance]; -} -SwitchRgbColor switch_pro_get_slot_light_color(uint8_t instance) { - if (instance >= sizeof(slot_colors) / sizeof(slot_colors[0])) { - return {}; - } - return switch_pro_calibrate_light_color(slot_colors[instance]); -} - - +static uint16_t leftMinX, leftMinY; +static uint16_t leftCenX, leftCenY; +static uint16_t leftMaxX, leftMaxY; +static uint16_t rightMinX, rightMinY; +static uint16_t rightCenX, rightCenY; +static uint16_t rightMaxX, rightMaxY; +static SwitchRumbleCallback rumble_callback = nullptr; +static SwitchHapticsDecoder rumble_decoder; static const uint8_t factory_config_data[0xEFF] = { // serial number @@ -179,10 +136,10 @@ static const uint8_t factory_config_data[0xEFF] = { SWITCH_COLOR_BUTTON_R, SWITCH_COLOR_BUTTON_G, SWITCH_COLOR_BUTTON_B, // left grip color - SWITCH_COLOR_SLOT_1_R, SWITCH_COLOR_SLOT_1_G, SWITCH_COLOR_SLOT_1_B, + SWITCH_COLOR_LEFT_GRIP_R, SWITCH_COLOR_LEFT_GRIP_G, SWITCH_COLOR_LEFT_GRIP_B, // right grip color - SWITCH_COLOR_SLOT_1_R, SWITCH_COLOR_SLOT_1_G, SWITCH_COLOR_SLOT_1_B, + SWITCH_COLOR_RIGHT_GRIP_R, SWITCH_COLOR_RIGHT_GRIP_G, SWITCH_COLOR_RIGHT_GRIP_B, 0x01, @@ -231,14 +188,30 @@ static const uint8_t user_calibration_data[0x3F] = { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff }; -static const SwitchFactoryConfig* const factory_config = - reinterpret_cast(factory_config_data); +static const SwitchFactoryConfig* factory_config = reinterpret_cast(factory_config_data); +static const SwitchUserCalibration* user_calibration [[maybe_unused]] = reinterpret_cast(user_calibration_data); + +static std::map spi_flash_data = { + {0x6000, factory_config_data}, + {0x8000, user_calibration_data} +}; static inline uint16_t scale16To12(uint16_t pos) { return pos >> 4; } -static void reset_motion_quaternion(SwitchProContext& context) { - context.motion_quaternion = - {0.0f, 0.0f, 0.0f, 1.0f, 0, 0, 0}; +struct MotionQuaternion { + float x; + float y; + float z; + float w; + int16_t accel_x; + int16_t accel_y; + int16_t accel_z; +}; + +static MotionQuaternion motion_quaternion{0.0f, 0.0f, 0.0f, 1.0f, 0, 0, 0}; + +static void reset_motion_quaternion() { + motion_quaternion = {0.0f, 0.0f, 0.0f, 1.0f, 0, 0, 0}; } static void write_int16_le(uint8_t* dst, int16_t value) { @@ -246,32 +219,24 @@ static void write_int16_le(uint8_t* dst, int16_t value) { dst[1] = static_cast((value >> 8) & 0xFF); } -static void write_bits_le(uint8_t* dst, uint16_t bit_offset, uint32_t value, - uint8_t width) { +static void write_bits_le(uint8_t* dst, uint16_t bit_offset, uint32_t value, uint8_t width) { for (uint8_t bit = 0; bit < width; ++bit) { if ((value & (1u << bit)) != 0) { uint16_t output_bit = static_cast(bit_offset + bit); - dst[output_bit >> 3] |= - static_cast(1u << (output_bit & 7u)); + dst[output_bit >> 3] |= static_cast(1u << (output_bit & 7u)); } } } -static void integrate_motion_sample(SwitchProContext& context, - const SwitchImuSample& sample) { +static void integrate_motion_sample(const SwitchImuSample& sample) { constexpr float sample_dt = 0.005f; constexpr float gyro_rad_per_lsb = 1.0f / 818.5f; - MotionQuaternion& quaternion = context.motion_quaternion; // Nintendo mode 2 uses Y, X, Z sensor order for quaternion axes. - float angle_x = - static_cast(sample.gyro_y) * gyro_rad_per_lsb * sample_dt; - float angle_y = - static_cast(sample.gyro_x) * gyro_rad_per_lsb * sample_dt; - float angle_z = - static_cast(sample.gyro_z) * gyro_rad_per_lsb * sample_dt; - float norm = - sqrtf(angle_x * angle_x + angle_y * angle_y + angle_z * angle_z); + float angle_x = static_cast(sample.gyro_y) * gyro_rad_per_lsb * sample_dt; + float angle_y = static_cast(sample.gyro_x) * gyro_rad_per_lsb * sample_dt; + float angle_z = static_cast(sample.gyro_z) * gyro_rad_per_lsb * sample_dt; + float norm = sqrtf(angle_x * angle_x + angle_y * angle_y + angle_z * angle_z); float half = 0.5f * norm; float vector_scale = norm > 1e-12f ? sinf(half) / norm : 0.5f; float scalar = norm > 1e-12f ? cosf(half) : 1.0f; @@ -281,44 +246,40 @@ static void integrate_motion_sample(SwitchProContext& context, float dz = angle_z * vector_scale; float dw = scalar; - float x = quaternion.w * dx + quaternion.x * dw - + quaternion.y * dz - quaternion.z * dy; - float y = quaternion.w * dy - quaternion.x * dz - + quaternion.y * dw + quaternion.z * dx; - float z = quaternion.w * dz + quaternion.x * dy - - quaternion.y * dx + quaternion.z * dw; - float w = quaternion.w * dw - quaternion.x * dx - - quaternion.y * dy - quaternion.z * dz; + float x = motion_quaternion.w * dx + motion_quaternion.x * dw + + motion_quaternion.y * dz - motion_quaternion.z * dy; + float y = motion_quaternion.w * dy - motion_quaternion.x * dz + + motion_quaternion.y * dw + motion_quaternion.z * dx; + float z = motion_quaternion.w * dz + motion_quaternion.x * dy + - motion_quaternion.y * dx + motion_quaternion.z * dw; + float w = motion_quaternion.w * dw - motion_quaternion.x * dx + - motion_quaternion.y * dy - motion_quaternion.z * dz; float magnitude = sqrtf(x * x + y * y + z * z + w * w); if (magnitude > 1e-12f) { float inverse = 1.0f / magnitude; - quaternion.x = x * inverse; - quaternion.y = y * inverse; - quaternion.z = z * inverse; - quaternion.w = w * inverse; + motion_quaternion.x = x * inverse; + motion_quaternion.y = y * inverse; + motion_quaternion.z = z * inverse; + motion_quaternion.w = w * inverse; } else { - reset_motion_quaternion(context); + reset_motion_quaternion(); } - quaternion.accel_x = sample.accel_x; - quaternion.accel_y = sample.accel_y; - quaternion.accel_z = sample.accel_z; + motion_quaternion.accel_x = sample.accel_x; + motion_quaternion.accel_y = sample.accel_y; + motion_quaternion.accel_z = sample.accel_z; } -static void fill_raw_imu_report_data(SwitchProContext& context, - const SwitchInputState& state) { +static void fill_raw_imu_report_data(const SwitchInputState& state) { if (state.imu_sample_count == 0) { - memset(context.switch_report.imuData, 0x00, - sizeof(context.switch_report.imuData)); + memset(switch_report.imuData, 0x00, sizeof(switch_report.imuData)); return; } - uint8_t sample_count = - state.imu_sample_count > 3 ? 3 : state.imu_sample_count; - uint8_t* dst = context.switch_report.imuData; + uint8_t sample_count = state.imu_sample_count > 3 ? 3 : state.imu_sample_count; + uint8_t* dst = switch_report.imuData; for (uint8_t i = 0; i < 3; ++i) { const SwitchImuSample& sample = - (i < sample_count) ? state.imu_samples[i] - : state.imu_samples[sample_count - 1]; + (i < sample_count) ? state.imu_samples[i] : state.imu_samples[sample_count - 1]; write_int16_le(dst + 0, sample.accel_x); write_int16_le(dst + 2, sample.accel_y); write_int16_le(dst + 4, sample.accel_z); @@ -329,49 +290,43 @@ static void fill_raw_imu_report_data(SwitchProContext& context, } } -static void fill_quaternion_imu_report_data(SwitchProContext& context, - const SwitchInputState& state, - uint32_t now_ms) { +static void fill_quaternion_imu_report_data(const SwitchInputState& state, uint32_t now_ms) { if (state.imu_sample_count > 0) { - uint8_t sample_count = - state.imu_sample_count > 3 ? 3 : state.imu_sample_count; + uint8_t sample_count = state.imu_sample_count > 3 ? 3 : state.imu_sample_count; for (uint8_t i = 0; i < 3; ++i) { const SwitchImuSample& sample = - (i < sample_count) ? state.imu_samples[i] - : state.imu_samples[sample_count - 1]; - integrate_motion_sample(context, sample); + (i < sample_count) ? state.imu_samples[i] : state.imu_samples[sample_count - 1]; + integrate_motion_sample(sample); } } - MotionQuaternion& quaternion = context.motion_quaternion; - uint8_t* dst = context.switch_report.imuData; - memset(dst, 0x00, sizeof(context.switch_report.imuData)); + uint8_t* dst = switch_report.imuData; + memset(dst, 0x00, sizeof(switch_report.imuData)); // Mode 2 accelerometer vectors are encoded in Y, X, Z order. - write_int16_le(dst + 0, quaternion.accel_y); - write_int16_le(dst + 2, quaternion.accel_x); - write_int16_le(dst + 4, quaternion.accel_z); + write_int16_le(dst + 0, motion_quaternion.accel_y); + write_int16_le(dst + 2, motion_quaternion.accel_x); + write_int16_le(dst + 4, motion_quaternion.accel_z); - float components[4] = { - quaternion.x, - quaternion.y, - quaternion.z, - quaternion.w, + float quaternion[4] = { + motion_quaternion.x, + motion_quaternion.y, + motion_quaternion.z, + motion_quaternion.w, }; uint8_t max_index = 0; for (uint8_t i = 1; i < 4; ++i) { - if (fabsf(components[i]) > fabsf(components[max_index])) { + if (fabsf(quaternion[i]) > fabsf(quaternion[max_index])) { max_index = i; } } uint32_t packed_component[3]{}; - float sign = components[max_index] < 0.0f ? -1.0f : 1.0f; + float sign = quaternion[max_index] < 0.0f ? -1.0f : 1.0f; for (uint8_t i = 0; i < 3; ++i) { int32_t component = static_cast( - components[(max_index + i + 1) & 3] * 1073741824.0f * sign); - packed_component[i] = - static_cast(component >> 10) & 0x1FFFFFu; + quaternion[(max_index + i + 1) & 3] * 1073741824.0f * sign); + packed_component[i] = static_cast(component >> 10) & 0x1FFFFFu; } write_bits_le(dst, 48, 2, 2); @@ -386,26 +341,21 @@ static void fill_quaternion_imu_report_data(SwitchProContext& context, write_bits_le(dst, 282, 3, 6); } -static void fill_imu_report_data(SwitchProContext& context, - const SwitchInputState& state, - uint32_t now_ms) { - switch (context.imu_mode) { +static void fill_imu_report_data(const SwitchInputState& state, uint32_t now_ms) { + switch (imu_mode) { case SwitchImuMode::Raw: - fill_raw_imu_report_data(context, state); + fill_raw_imu_report_data(state); break; case SwitchImuMode::Quaternion: - fill_quaternion_imu_report_data(context, state, now_ms); + fill_quaternion_imu_report_data(state, now_ms); break; case SwitchImuMode::Off: default: - memset(context.switch_report.imuData, 0x00, - sizeof(context.switch_report.imuData)); + memset(switch_report.imuData, 0x00, sizeof(switch_report.imuData)); break; } } -static void update_switch_report_from_state(SwitchProContext& context); - static SwitchInputState make_neutral_state() { SwitchInputState s{}; s.lx = SWITCH_PRO_JOYSTICK_MID; @@ -416,468 +366,449 @@ static SwitchInputState make_neutral_state() { return s; } -static void reset_context_runtime(SwitchProContext& context, uint32_t now, - bool ready_before_mount) { - context.input_state = make_neutral_state(); - memset(context.report_buffer, 0x00, sizeof(context.report_buffer)); - context.switch_report = {}; - context.switch_report.reportID = 0x30; - context.switch_report.inputs.connectionInfo = 0x01; - context.switch_report.inputs.batteryLevel = 0x08; - update_switch_report_from_state(context); - context.last_report_counter = 0; - context.last_report_timer = now; - context.is_ready = ready_before_mount; - context.is_initialized = ready_before_mount; - context.is_report_queued = false; - context.player_id = 0; - context.input_mode = 0x30; - context.imu_mode = SwitchImuMode::Off; - context.is_vibration_enabled = false; - context.rumble_decoder.reset(); - reset_motion_quaternion(context); -} - -static void reset_all_contexts(bool ready_before_mount) { - uint32_t now = to_ms_since_boot(get_absolute_time()); - for (uint8_t instance = 0; instance < SWITCH_PICO_HID_INSTANCE_COUNT; - ++instance) { - reset_context_runtime(contexts[instance], now, ready_before_mount); +static void send_identify() { + memset(report_buffer, 0x00, sizeof(report_buffer)); + report_buffer[0] = REPORT_USB_INPUT_81; + report_buffer[1] = IDENTIFY; + report_buffer[2] = 0x00; + report_buffer[3] = device_info.controllerType; + for (uint8_t i = 0; i < 6; i++) { + report_buffer[4 + i] = device_info.macAddress[5 - i]; } } -static void send_identify(SwitchProContext& context) { - memset(context.report_buffer, 0x00, sizeof(context.report_buffer)); - context.report_buffer[0] = REPORT_USB_INPUT_81; - context.report_buffer[1] = IDENTIFY; - context.report_buffer[2] = 0x00; - context.report_buffer[3] = context.device_info.controllerType; - for (uint8_t i = 0; i < 6; ++i) { - context.report_buffer[4 + i] = - context.device_info.macAddress[5 - i]; +static bool send_report(uint8_t reportID, const void* reportData, uint16_t reportLength) { + bool result = tud_hid_report(reportID, reportData, reportLength); + if (last_report_counter < 255) { + last_report_counter++; + } else { + last_report_counter = 0; } -} - -static bool send_report(uint8_t instance, SwitchProContext& context, - uint8_t report_id, const void* report_data, - uint16_t report_length) { - bool result = - tud_hid_n_report(instance, report_id, report_data, report_length); - ++context.last_report_counter; if (!result) { - LOG_PRINTF("[HID %u] send_report failed id=%u len=%u\n", instance, - report_id, report_length); + LOG_PRINTF("[HID] send_report failed id=%u len=%u\n", reportID, reportLength); } return result; } -static void read_spi_flash(const SwitchProContext& context, uint8_t* dest, - uint32_t address, uint8_t size) { - uint32_t address_bank = address & 0xFFFFFF00u; - uint32_t address_offset = address & 0x000000FFu; - const uint8_t* data = nullptr; - if (address_bank == 0x6000u) { - data = factory_config_data; - } else if (address_bank == 0x8000u) { - data = user_calibration_data; - } +static void read_spi_flash(uint8_t* dest, uint32_t address, uint8_t size) { + uint32_t addressBank = address & 0xFFFFFF00; + uint32_t addressOffset = address & 0x000000FF; + auto it = spi_flash_data.find(addressBank); - if (data == nullptr) { + if (it != spi_flash_data.end()) { + const uint8_t* data = it->second; + memcpy(dest, data + addressOffset, size); + } else { memset(dest, 0xFF, size); - return; - } - memcpy(dest, data + address_offset, size); - if (address_bank != 0x6000u) { - return; - } - - static_assert(sizeof(SwitchRgbColor) == sizeof(SwitchColorDefinition)); - const uint8_t* color = - reinterpret_cast(&context.grip_color); - constexpr size_t left_offset = - offsetof(SwitchFactoryConfig, leftGripColor); - constexpr size_t right_offset = - offsetof(SwitchFactoryConfig, rightGripColor); - for (uint8_t index = 0; index < size; ++index) { - const size_t offset = address_offset + index; - if (offset >= left_offset && - offset < left_offset + sizeof(SwitchRgbColor)) { - dest[index] = color[offset - left_offset]; - } else if (offset >= right_offset && - offset < right_offset + sizeof(SwitchRgbColor)) { - dest[index] = color[offset - right_offset]; - } } } -static void forward_decoded_rumble(uint8_t instance, - SwitchProContext& context, - const uint8_t* report, uint16_t length) { +static void forward_decoded_rumble(const uint8_t* report, uint16_t length) { // Output reports 0x10/0x01 include 8 rumble bytes starting at offset 2. if (length < 10) { return; } - SwitchRumbleOutput rumble = context.rumble_decoder.decode(report + 2); - if (context.rumble_callback != nullptr) { - context.rumble_callback(instance, rumble); + SwitchRumbleOutput rumble = rumble_decoder.decode(report + 2); + if (rumble_callback) { + rumble_callback(rumble); } } -static void handle_config_report(SwitchProContext& context, - uint8_t switch_report_sub_id) { - switch (switch_report_sub_id) { +static void handle_config_report(uint8_t switchReportID, uint8_t switchReportSubID, const uint8_t *reportData, uint16_t reportLength) { + bool canSend = false; + last_host_activity_ms = to_ms_since_boot(get_absolute_time()); + + switch (switchReportSubID) { case IDENTIFY: - send_identify(context); + send_identify(); + canSend = true; LOG_PRINTF("[HID] CONFIG IDENTIFY\n"); break; case HANDSHAKE: - context.report_buffer[0] = REPORT_USB_INPUT_81; - context.report_buffer[1] = HANDSHAKE; + report_buffer[0] = REPORT_USB_INPUT_81; + report_buffer[1] = HANDSHAKE; + canSend = true; LOG_PRINTF("[HID] CONFIG HANDSHAKE\n"); break; case BAUD_RATE: - context.report_buffer[0] = REPORT_USB_INPUT_81; - context.report_buffer[1] = BAUD_RATE; + report_buffer[0] = REPORT_USB_INPUT_81; + report_buffer[1] = BAUD_RATE; + canSend = true; LOG_PRINTF("[HID] CONFIG BAUD_RATE\n"); break; case DISABLE_USB_TIMEOUT: - context.report_buffer[0] = REPORT_OUTPUT_30; - context.report_buffer[1] = switch_report_sub_id; - context.is_ready = true; + report_buffer[0] = REPORT_OUTPUT_30; + report_buffer[1] = switchReportSubID; + //if (handshakeCounter < 4) { + // handshakeCounter++; + //} else { + is_ready = true; + //} + canSend = true; LOG_PRINTF("[HID] CONFIG DISABLE_USB_TIMEOUT -> ready\n"); break; case ENABLE_USB_TIMEOUT: - context.report_buffer[0] = REPORT_OUTPUT_30; - context.report_buffer[1] = switch_report_sub_id; + report_buffer[0] = REPORT_OUTPUT_30; + report_buffer[1] = switchReportSubID; + canSend = true; LOG_PRINTF("[HID] CONFIG ENABLE_USB_TIMEOUT\n"); break; default: - context.report_buffer[0] = REPORT_OUTPUT_30; - context.report_buffer[1] = switch_report_sub_id; - LOG_PRINTF("[HID] CONFIG unknown subid=0x%02x\n", - switch_report_sub_id); + report_buffer[0] = REPORT_OUTPUT_30; + report_buffer[1] = switchReportSubID; + canSend = true; + LOG_PRINTF("[HID] CONFIG unknown subid=0x%02x\n", switchReportSubID); break; } - context.is_report_queued = true; + if (canSend) is_report_queued = true; } -static void handle_feature_report(SwitchProContext& context, - const uint8_t* report_data) { - uint8_t command_id = report_data[10]; - uint32_t spi_read_address = 0; - uint8_t spi_read_size = 0; +static void handle_feature_report(uint8_t switchReportID, uint8_t switchReportSubID, const uint8_t *reportData, uint16_t reportLength) { + uint8_t commandID = reportData[10]; + uint32_t spiReadAddress = 0; + uint8_t spiReadSize = 0; + bool canSend = false; + last_host_activity_ms = to_ms_since_boot(get_absolute_time()); - context.report_buffer[0] = REPORT_OUTPUT_21; - context.report_buffer[1] = context.last_report_counter; - memcpy(context.report_buffer + 2, &context.switch_report.inputs, - sizeof(SwitchInputReport)); + report_buffer[0] = REPORT_OUTPUT_21; + report_buffer[1] = last_report_counter; + memcpy(report_buffer + 2, &switch_report.inputs, sizeof(SwitchInputReport)); - switch (command_id) { + switch (commandID) { case GET_CONTROLLER_STATE: - context.report_buffer[13] = 0x80; - context.report_buffer[14] = command_id; - context.report_buffer[15] = 0x03; + report_buffer[13] = 0x80; + report_buffer[14] = commandID; + report_buffer[15] = 0x03; + canSend = true; LOG_PRINTF("[HID] FEATURE GET_CONTROLLER_STATE\n"); break; case BLUETOOTH_PAIR_REQUEST: - context.report_buffer[13] = 0x81; - context.report_buffer[14] = command_id; - context.report_buffer[15] = 0x03; + report_buffer[13] = 0x81; + report_buffer[14] = commandID; + report_buffer[15] = 0x03; + canSend = true; LOG_PRINTF("[HID] FEATURE BLUETOOTH_PAIR_REQUEST\n"); break; case REQUEST_DEVICE_INFO: - context.report_buffer[13] = 0x82; - context.report_buffer[14] = 0x02; - memcpy(&context.report_buffer[15], &context.device_info, - sizeof(context.device_info)); + report_buffer[13] = 0x82; + report_buffer[14] = 0x02; + memcpy(&report_buffer[15], &device_info, sizeof(device_info)); + canSend = true; LOG_PRINTF("[HID] FEATURE REQUEST_DEVICE_INFO\n"); break; case SET_MODE: - context.input_mode = report_data[11]; - context.report_buffer[13] = 0x80; - context.report_buffer[14] = 0x03; - context.report_buffer[15] = context.input_mode; - LOG_PRINTF("[HID] FEATURE SET_MODE 0x%02x\n", - context.input_mode); + input_mode = reportData[11]; + report_buffer[13] = 0x80; + report_buffer[14] = 0x03; + report_buffer[15] = input_mode; + canSend = true; + LOG_PRINTF("[HID] FEATURE SET_MODE 0x%02x\n", input_mode); break; case TRIGGER_BUTTONS: - context.report_buffer[13] = 0x83; - context.report_buffer[14] = 0x04; + report_buffer[13] = 0x83; + report_buffer[14] = 0x04; + canSend = true; LOG_PRINTF("[HID] FEATURE TRIGGER_BUTTONS\n"); break; case SET_SHIPMENT: - context.report_buffer[13] = 0x80; - context.report_buffer[14] = command_id; + report_buffer[13] = 0x80; + report_buffer[14] = commandID; + canSend = true; LOG_PRINTF("[HID] FEATURE SET_SHIPMENT\n"); break; case SPI_READ: - spi_read_address = - (static_cast(report_data[14]) << 24u) | - (static_cast(report_data[13]) << 16u) | - (static_cast(report_data[12]) << 8u) | - static_cast(report_data[11]); - spi_read_size = report_data[15]; - context.report_buffer[13] = 0x90; - context.report_buffer[14] = report_data[10]; - context.report_buffer[15] = report_data[11]; - context.report_buffer[16] = report_data[12]; - context.report_buffer[17] = report_data[13]; - context.report_buffer[18] = report_data[14]; - context.report_buffer[19] = report_data[15]; - read_spi_flash(context, &context.report_buffer[20], - spi_read_address, spi_read_size); - LOG_PRINTF("[HID] FEATURE SPI_READ addr=0x%08lx size=%u\n", - static_cast(spi_read_address), - spi_read_size); + spiReadAddress = (reportData[14] << 24) | (reportData[13] << 16) | (reportData[12] << 8) | (reportData[11]); + spiReadSize = reportData[15]; + report_buffer[13] = 0x90; + report_buffer[14] = reportData[10]; + report_buffer[15] = reportData[11]; + report_buffer[16] = reportData[12]; + report_buffer[17] = reportData[13]; + report_buffer[18] = reportData[14]; + report_buffer[19] = reportData[15]; + read_spi_flash(&report_buffer[20], spiReadAddress, spiReadSize); + canSend = true; + LOG_PRINTF("[HID] FEATURE SPI_READ addr=0x%08lx size=%u\n", (unsigned long)spiReadAddress, spiReadSize); break; case SET_NFC_IR_CONFIG: - context.report_buffer[13] = 0x80; - context.report_buffer[14] = command_id; + report_buffer[13] = 0x80; + report_buffer[14] = commandID; + canSend = true; LOG_PRINTF("[HID] FEATURE SET_NFC_IR_CONFIG\n"); break; case SET_NFC_IR_STATE: - context.report_buffer[13] = 0x80; - context.report_buffer[14] = command_id; + report_buffer[13] = 0x80; + report_buffer[14] = commandID; + canSend = true; LOG_PRINTF("[HID] FEATURE SET_NFC_IR_STATE\n"); break; case SET_PLAYER_LIGHTS: - context.player_id = report_data[11]; - context.report_buffer[13] = 0x80; - context.report_buffer[14] = command_id; - LOG_PRINTF("[HID] FEATURE SET_PLAYER_LIGHTS player=%u\n", - context.player_id); + player_id = reportData[11]; + report_buffer[13] = 0x80; + report_buffer[14] = commandID; + canSend = true; + LOG_PRINTF("[HID] FEATURE SET_PLAYER_LIGHTS player=%u\n", player_id); break; case GET_PLAYER_LIGHTS: - context.player_id = report_data[11]; - context.report_buffer[13] = 0xB0; - context.report_buffer[14] = command_id; - context.report_buffer[15] = context.player_id; - LOG_PRINTF("[HID] FEATURE GET_PLAYER_LIGHTS player=%u\n", - context.player_id); + player_id = reportData[11]; + report_buffer[13] = 0xB0; + report_buffer[14] = commandID; + report_buffer[15] = player_id; + canSend = true; + LOG_PRINTF("[HID] FEATURE GET_PLAYER_LIGHTS player=%u\n", player_id); break; case COMMAND_UNKNOWN_33: - context.report_buffer[13] = 0x80; - context.report_buffer[14] = command_id; - context.report_buffer[15] = 0x03; + report_buffer[13] = 0x80; + report_buffer[14] = commandID; + report_buffer[15] = 0x03; + canSend = true; LOG_PRINTF("[HID] FEATURE COMMAND_UNKNOWN_33\n"); break; case SET_HOME_LIGHT: - context.report_buffer[13] = 0x80; - context.report_buffer[14] = command_id; - context.report_buffer[15] = 0x00; + report_buffer[13] = 0x80; + report_buffer[14] = commandID; + report_buffer[15] = 0x00; + canSend = true; LOG_PRINTF("[HID] FEATURE SET_HOME_LIGHT\n"); break; case TOGGLE_IMU: { SwitchImuMode requested_mode = SwitchImuMode::Off; - if (report_data[11] == - static_cast(SwitchImuMode::Raw)) { + if (reportData[11] == static_cast(SwitchImuMode::Raw)) { requested_mode = SwitchImuMode::Raw; - } else if (report_data[11] == - static_cast(SwitchImuMode::Quaternion)) { + } else if (reportData[11] == static_cast(SwitchImuMode::Quaternion)) { requested_mode = SwitchImuMode::Quaternion; } - if (requested_mode == SwitchImuMode::Quaternion && - context.imu_mode != requested_mode) { - reset_motion_quaternion(context); + if (requested_mode == SwitchImuMode::Quaternion && imu_mode != requested_mode) { + reset_motion_quaternion(); } - context.imu_mode = requested_mode; - context.report_buffer[13] = 0x80; - context.report_buffer[14] = command_id; - context.report_buffer[15] = 0x00; - LOG_PRINTF("[HID] FEATURE TOGGLE_IMU %u\n", - static_cast(context.imu_mode)); + imu_mode = requested_mode; + report_buffer[13] = 0x80; + report_buffer[14] = commandID; + report_buffer[15] = 0x00; + canSend = true; + LOG_PRINTF("[HID] FEATURE TOGGLE_IMU %u\n", static_cast(imu_mode)); break; } case IMU_SENSITIVITY: - context.report_buffer[13] = 0x80; - context.report_buffer[14] = command_id; + report_buffer[13] = 0x80; + report_buffer[14] = commandID; + canSend = true; LOG_PRINTF("[HID] FEATURE IMU_SENSITIVITY\n"); break; case ENABLE_VIBRATION: - context.is_vibration_enabled = report_data[11] != 0; - context.report_buffer[13] = 0x80; - context.report_buffer[14] = command_id; - context.report_buffer[15] = 0x00; - LOG_PRINTF("[HID] FEATURE ENABLE_VIBRATION %u\n", - context.is_vibration_enabled); + is_vibration_enabled = reportData[11]; + report_buffer[13] = 0x80; + report_buffer[14] = commandID; + report_buffer[15] = 0x00; + canSend = true; + LOG_PRINTF("[HID] FEATURE ENABLE_VIBRATION %u\n", is_vibration_enabled); break; case READ_IMU: - context.report_buffer[13] = 0xC0; - context.report_buffer[14] = command_id; - context.report_buffer[15] = report_data[11]; - context.report_buffer[16] = report_data[12]; - LOG_PRINTF("[HID] FEATURE READ_IMU addr=%u size=%u\n", - report_data[11], report_data[12]); + report_buffer[13] = 0xC0; + report_buffer[14] = commandID; + report_buffer[15] = reportData[11]; + report_buffer[16] = reportData[12]; + canSend = true; + LOG_PRINTF("[HID] FEATURE READ_IMU addr=%u size=%u\n", reportData[11], reportData[12]); break; case GET_VOLTAGE: - context.report_buffer[13] = 0xD0; - context.report_buffer[14] = 0x50; - context.report_buffer[15] = 0x83; - context.report_buffer[16] = 0x06; + report_buffer[13] = 0xD0; + report_buffer[14] = 0x50; + report_buffer[15] = 0x83; + report_buffer[16] = 0x06; + canSend = true; LOG_PRINTF("[HID] FEATURE GET_VOLTAGE\n"); break; default: - context.report_buffer[13] = 0x80; - context.report_buffer[14] = command_id; - context.report_buffer[15] = 0x03; - LOG_PRINTF("[HID] FEATURE unknown cmd=0x%02x\n", command_id); + report_buffer[13] = 0x80; + report_buffer[14] = commandID; + report_buffer[15] = 0x03; + canSend = true; + LOG_PRINTF("[HID] FEATURE unknown cmd=0x%02x\n", commandID); break; } - context.is_report_queued = true; + if (canSend) is_report_queued = true; } -static void update_switch_report_from_state(SwitchProContext& context) { - const SwitchInputState& state = context.input_state; - SwitchInputReport& inputs = context.switch_report.inputs; - inputs.dpadUp = state.dpad_up; - inputs.dpadDown = state.dpad_down; - inputs.dpadLeft = state.dpad_left; - inputs.dpadRight = state.dpad_right; - inputs.chargingGrip = 1; - inputs.buttonY = state.button_y; - inputs.buttonX = state.button_x; - inputs.buttonB = state.button_b; - inputs.buttonA = state.button_a; - inputs.buttonRightSR = 0; - inputs.buttonRightSL = 0; - inputs.buttonR = state.button_r; - inputs.buttonZR = state.button_zr; - inputs.buttonMinus = state.button_minus; - inputs.buttonPlus = state.button_plus; - inputs.buttonThumbR = state.button_r3; - inputs.buttonThumbL = state.button_l3; - inputs.buttonHome = state.button_home; - inputs.buttonCapture = state.button_capture; - inputs.buttonLeftSR = 0; - inputs.buttonLeftSL = 0; - inputs.buttonL = state.button_l; - inputs.buttonZL = state.button_zl; +static void update_switch_report_from_state() { + switch_report.inputs.dpadUp = g_input_state.dpad_up; + switch_report.inputs.dpadDown = g_input_state.dpad_down; + switch_report.inputs.dpadLeft = g_input_state.dpad_left; + switch_report.inputs.dpadRight = g_input_state.dpad_right; - uint16_t left_x = scale16To12(state.lx); - uint16_t left_y = scale16To12(state.ly); - uint16_t right_x = scale16To12(state.rx); - uint16_t right_y = scale16To12(state.ry); + switch_report.inputs.chargingGrip = 1; - inputs.leftStick.setX( - std::min(std::max(left_x, context.left_min_x), context.left_max_x)); - inputs.leftStick.setY(-std::min( - std::max(left_y, context.left_min_y), context.left_max_y)); - inputs.rightStick.setX(std::min( - std::max(right_x, context.right_min_x), context.right_max_x)); - inputs.rightStick.setY(-std::min( - std::max(right_y, context.right_min_y), context.right_max_y)); - context.switch_report.rumbleReport = 0x09; + switch_report.inputs.buttonY = g_input_state.button_y; + switch_report.inputs.buttonX = g_input_state.button_x; + switch_report.inputs.buttonB = g_input_state.button_b; + switch_report.inputs.buttonA = g_input_state.button_a; + switch_report.inputs.buttonRightSR = 0; + switch_report.inputs.buttonRightSL = 0; + switch_report.inputs.buttonR = g_input_state.button_r; + switch_report.inputs.buttonZR = g_input_state.button_zr; + switch_report.inputs.buttonMinus = g_input_state.button_minus; + switch_report.inputs.buttonPlus = g_input_state.button_plus; + switch_report.inputs.buttonThumbR = g_input_state.button_r3; + switch_report.inputs.buttonThumbL = g_input_state.button_l3; + switch_report.inputs.buttonHome = g_input_state.button_home; + switch_report.inputs.buttonCapture = g_input_state.button_capture; + switch_report.inputs.buttonLeftSR = 0; + switch_report.inputs.buttonLeftSL = 0; + switch_report.inputs.buttonL = g_input_state.button_l; + switch_report.inputs.buttonZL = g_input_state.button_zl; + + uint16_t scaleLeftStickX = scale16To12(g_input_state.lx); + uint16_t scaleLeftStickY = scale16To12(g_input_state.ly); + uint16_t scaleRightStickX = scale16To12(g_input_state.rx); + uint16_t scaleRightStickY = scale16To12(g_input_state.ry); + + switch_report.inputs.leftStick.setX(std::min(std::max(scaleLeftStickX,leftMinX), leftMaxX)); + switch_report.inputs.leftStick.setY(-std::min(std::max(scaleLeftStickY,leftMinY), leftMaxY)); + switch_report.inputs.rightStick.setX(std::min(std::max(scaleRightStickX,rightMinX), rightMaxX)); + switch_report.inputs.rightStick.setY(-std::min(std::max(scaleRightStickY,rightMinY), rightMaxY)); + + switch_report.rumbleReport = 0x09; } -void switch_pro_init(uint8_t instance) { - SwitchProContext* context = context_for(instance); - if (context == nullptr) { - return; - } +void switch_pro_init() { + imu_mode = SwitchImuMode::Off; + rumble_decoder.reset(); + reset_motion_quaternion(); + player_id = 0; + last_report_counter = 0; + handshake_counter = 0; + is_ready = false; + is_initialized = false; + is_report_queued = false; + report_sent = false; + forced_ready = false; + forced_ready = true; + is_ready = true; + is_initialized = true; + last_report_timer = 0; - context->grip_color = switch_pro_get_slot_color(instance); - context->device_info = { - 0x03, - 0x48, - SWITCH_TYPE_PRO_CONTROLLER, - 0x02, - {0x7c, 0xbb, 0x8a, - static_cast(get_rand_32() % 0xff), - static_cast(get_rand_32() % 0xff), - static_cast(get_rand_32() % 0xff)}, - 0x01, - 0x02, + device_info = { + .majorVersion = 0x03, + .minorVersion = 0x48, + .controllerType = SWITCH_TYPE_PRO_CONTROLLER, + .unknown00 = 0x02, + .macAddress = {0x7c, 0xbb, 0x8a, static_cast(get_rand_32() % 0xff), static_cast(get_rand_32() % 0xff), static_cast(get_rand_32() % 0xff)}, + .unknown01 = 0x01, + .storedColors = 0x02, }; - factory_config->leftStickCalibration.getRealMin( - context->left_min_x, context->left_min_y); - factory_config->leftStickCalibration.getRealMax( - context->left_max_x, context->left_max_y); - factory_config->rightStickCalibration.getRealMin( - context->right_min_x, context->right_min_y); - factory_config->rightStickCalibration.getRealMax( - context->right_max_x, context->right_max_y); + switch_report = { + .reportID = 0x30, + .timestamp = 0, - reset_context_runtime(*context, - to_ms_since_boot(get_absolute_time()), true); + .inputs { + .connectionInfo = 0x01, // Pro Controller powered by the console + .batteryLevel = 0x08, // full battery + + .buttonY = 0, + .buttonX = 0, + .buttonB = 0, + .buttonA = 0, + .buttonRightSR = 0, + .buttonRightSL = 0, + .buttonR = 0, + .buttonZR = 0, + + .buttonMinus = 0, + .buttonPlus = 0, + .buttonThumbR = 0, + .buttonThumbL = 0, + .buttonHome = 0, + .buttonCapture = 0, + .dummy = 0, + .chargingGrip = 0, + + .dpadDown = 0, + .dpadUp = 0, + .dpadRight = 0, + .dpadLeft = 0, + .buttonLeftSL = 0, + .buttonLeftSR = 0, + .buttonL = 0, + .buttonZL = 0, + .leftStick = {0xFF, 0xF7, 0x7F}, + .rightStick = {0xFF, 0xF7, 0x7F}, + }, + .rumbleReport = 0, + .imuData = {0x00}, + .padding = {0x00} + }; + + last_report_timer = to_ms_since_boot(get_absolute_time()); + last_host_activity_ms = last_report_timer; + + factory_config->leftStickCalibration.getRealMin(leftMinX, leftMinY); + factory_config->leftStickCalibration.getCenter(leftCenX, leftCenY); + factory_config->leftStickCalibration.getRealMax(leftMaxX, leftMaxY); + factory_config->rightStickCalibration.getRealMin(rightMinX, rightMinY); + factory_config->rightStickCalibration.getCenter(rightCenX, rightCenY); + factory_config->rightStickCalibration.getRealMax(rightMaxX, rightMaxY); } -void switch_pro_set_input(uint8_t instance, const SwitchInputState& state) { - SwitchProContext* context = context_for(instance); - if (context != nullptr) { - context->input_state = state; - } +void switch_pro_set_input(const SwitchInputState& state) { + g_input_state = state; } -bool switch_pro_task(uint8_t instance) { - SwitchProContext* context = context_for(instance); - if (context == nullptr) { - return false; - } - +bool switch_pro_task() { uint32_t now = to_ms_since_boot(get_absolute_time()); - bool report_sent = false; + report_sent = false; bool regular_report_sent = false; - update_switch_report_from_state(*context); + update_switch_report_from_state(); if (tud_suspended()) { tud_remote_wakeup(); } - if (context->is_report_queued) { - if ((now - context->last_report_timer) > - SWITCH_PRO_KEEPALIVE_TIMER) { - if (tud_hid_n_ready(instance) && - send_report(instance, *context, 0, context->report_buffer, - SWITCH_PRO_ENDPOINT_SIZE)) { - context->is_report_queued = false; - context->last_report_timer = now; + if (is_report_queued) { + if ((now - last_report_timer) > SWITCH_PRO_KEEPALIVE_TIMER) { + if (tud_hid_ready() && send_report(queued_report_id, report_buffer, 64) == true ) { + is_report_queued = false; + last_report_timer = now; } } report_sent = true; } - if (context->is_ready && !report_sent) { - if ((now - context->last_report_timer) >= - SWITCH_PRO_IMU_REPORT_TIMER) { + if (is_ready && !report_sent) { + if ((now - last_report_timer) >= SWITCH_PRO_IMU_REPORT_TIMER) { // One timer tick per 5ms IMU frame; three frames per report. - fill_imu_report_data(*context, context->input_state, now); - context->switch_report.timestamp += 3; - if (tud_hid_n_ready(instance) && - send_report(instance, *context, 0, &context->switch_report, - sizeof(context->switch_report))) { - context->input_state.imu_sample_count = 0; + fill_imu_report_data(g_input_state, now); + switch_report.timestamp += 3; + void * inputReport = &switch_report; + uint16_t report_size = sizeof(switch_report); + if (tud_hid_ready() && send_report(0, inputReport, report_size) == true ) { + memcpy(last_report, inputReport, report_size); + g_input_state.imu_sample_count = 0; + report_sent = true; regular_report_sent = true; } - context->last_report_timer = now; + + last_report_timer = now; } - } else if (!context->is_initialized) { - send_identify(*context); - if (tud_hid_n_ready(instance)) { - bool result = tud_hid_n_report( - instance, 0, context->report_buffer, - SWITCH_PRO_ENDPOINT_SIZE); - if (result) { - context->is_initialized = true; - } else { - LOG_PRINTF("[HID %u] send_report failed id=0 len=%u\n", - instance, SWITCH_PRO_ENDPOINT_SIZE); + } else { + if (!is_initialized) { + send_identify(); + if (tud_hid_ready() && tud_hid_report(0, report_buffer, 64) == true) { + is_initialized = true; + report_sent = true; } + + last_report_timer = now; } - context->last_report_timer = now; } return regular_report_sent; } -bool switch_pro_apply_uart_packet(const uint8_t* packet, uint8_t length, - SwitchInputState& out_state) { - if (packet == nullptr) { - return false; - } +bool switch_pro_apply_uart_packet(const uint8_t* packet, uint8_t length, SwitchInputState* out_state) { // v2 format: 0xAA + 0x02 + payload_len + payload... + checksum if (length < 12) { return false; @@ -976,48 +907,35 @@ bool switch_pro_apply_uart_packet(const uint8_t* packet, uint8_t length, state.rx = expand_axis(out.rx); state.ry = expand_axis(out.ry); - out_state = state; + if (!out_state) { + return false; + } + *out_state = state; return true; } -void switch_pro_set_rumble_callback(uint8_t instance, - SwitchRumbleCallback callback) { - SwitchProContext* context = context_for(instance); - if (context != nullptr) { - context->rumble_callback = callback; - } +void switch_pro_set_rumble_callback(SwitchRumbleCallback cb) { + rumble_callback = cb; } -bool switch_pro_is_ready(uint8_t instance) { - const SwitchProContext* context = context_for(instance); - return context != nullptr && context->is_ready; +bool switch_pro_is_ready() { + return is_ready; } // HID callbacks -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)report_id; - (void)report_type; - SwitchProContext* context = context_for(instance); - LOG_PRINTF("[HID %u] get_report id=%u type=%u len=%u\n", instance, - report_id, report_type, requested_length); - if (context == nullptr || buffer == nullptr) { - return 0; - } +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 reqlen) { + (void)instance; + LOG_PRINTF("[HID] get_report id=%u type=%u len=%u\n", report_id, report_type, reqlen); + if (!buffer) return 0; - // Serve the addressed instance's current input report. - uint16_t report_size = sizeof(context->switch_report); - if (requested_length < report_size) { - report_size = requested_length; - } - memcpy(buffer, &context->switch_report, report_size); + // Serve the current input report for any GET_REPORT request. + uint16_t report_size = sizeof(switch_report); + if (reqlen < report_size) report_size = reqlen; + memcpy(buffer, &switch_report, report_size); return report_size; } -static void process_output_report(uint8_t instance, - SwitchProContext& context, - uint8_t callback_report_id, +static void process_output_report(uint8_t callback_report_id, const uint8_t* payload, uint16_t payload_size) { uint8_t normalized[SWITCH_PRO_ENDPOINT_SIZE]{}; @@ -1027,89 +945,81 @@ static void process_output_report(uint8_t instance, return; } - memset(context.report_buffer, 0x00, sizeof(context.report_buffer)); - uint8_t switch_report_id = normalized[0]; - uint8_t switch_report_sub_id = normalized[1]; - LOG_PRINTF( - "[HID %u] output id=%u switchRID=0x%02x sub=0x%02x len=%u\n", - instance, callback_report_id, switch_report_id, switch_report_sub_id, - static_cast(normalized_size)); + memset(report_buffer, 0x00, sizeof(report_buffer)); + uint8_t switchReportID = normalized[0]; + uint8_t switchReportSubID = normalized[1]; + LOG_PRINTF("[HID] output id=%u switchRID=0x%02x sub=0x%02x len=%u\n", + callback_report_id, switchReportID, switchReportSubID, + static_cast(normalized_size)); - if (switch_report_id == REPORT_OUTPUT_10 || - switch_report_id == REPORT_FEATURE) { - forward_decoded_rumble( - instance, context, normalized, - static_cast(normalized_size)); + if (switchReportID == REPORT_OUTPUT_10 || switchReportID == REPORT_FEATURE) { + forward_decoded_rumble(normalized, static_cast(normalized_size)); } - if (switch_report_id == REPORT_OUTPUT_00) { + if (switchReportID == REPORT_OUTPUT_00) { return; } - if (switch_report_id == REPORT_FEATURE) { - if (normalized_size >= 16) { - handle_feature_report(context, normalized); - } - } else if (switch_report_id == REPORT_CONFIGURATION) { - handle_config_report(context, switch_report_sub_id); + if (switchReportID == REPORT_FEATURE) { + queued_report_id = 0; + handle_feature_report(switchReportID, switchReportSubID, normalized, + static_cast(normalized_size)); + } else if (switchReportID == REPORT_CONFIGURATION) { + queued_report_id = 0; + handle_config_report(switchReportID, switchReportSubID, normalized, + static_cast(normalized_size)); } } 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) { - SwitchProContext* context = context_for(instance); - if (context == nullptr || report_type != HID_REPORT_TYPE_OUTPUT) { + const uint8_t* buffer, uint16_t bufsize) { + (void)instance; + if (report_type != HID_REPORT_TYPE_OUTPUT) { return; } - process_output_report(instance, *context, report_id, buffer, buffer_size); + process_output_report(report_id, buffer, bufsize); } void tud_hid_report_received_cb(uint8_t instance, uint8_t report_id, - const uint8_t* buffer, - uint16_t buffer_size) { - SwitchProContext* context = context_for(instance); - if (context == nullptr) { - return; - } - process_output_report(instance, *context, report_id, buffer, buffer_size); + const uint8_t* buffer, uint16_t bufsize) { + (void)instance; + process_output_report(report_id, buffer, bufsize); } -uint8_t const* tud_hid_descriptor_report_cb(uint8_t instance) { - if (context_for(instance) == nullptr) { - return nullptr; - } +uint8_t const * tud_hid_descriptor_report_cb(uint8_t itf) { + (void)itf; return switch_pro_report_descriptor; } -uint8_t const* tud_descriptor_device_cb(void) { +uint8_t const * tud_descriptor_device_cb(void) { return switch_pro_device_descriptor; } -uint8_t const* tud_descriptor_configuration_cb(uint8_t index) { +uint8_t const * tud_descriptor_configuration_cb(uint8_t index) { (void)index; return switch_pro_configuration_descriptor; } -bool tud_control_request_cb(uint8_t rhport, - tusb_control_request_t const* request) { +bool tud_control_request_cb(uint8_t rhport, tusb_control_request_t const * request) { (void)rhport; - (void)request; - LOG_PRINTF( - "[CTRL] bmReq=0x%02x bReq=0x%02x wValue=0x%04x wIndex=0x%04x " - "wLen=%u\n", - request->bmRequestType, request->bRequest, request->wValue, - request->wIndex, request->wLength); - return false; // let TinyUSB handle it normally + LOG_PRINTF("[CTRL] bmReq=0x%02x bReq=0x%02x wValue=0x%04x wIndex=0x%04x wLen=%u\n", + request->bmRequestType, request->bRequest, request->wValue, request->wIndex, request->wLength); + return false; // let TinyUSB handle it normally } void tud_mount_cb(void) { LOG_PRINTF("[USB] mount_cb\n"); - reset_all_contexts(false); + last_host_activity_ms = to_ms_since_boot(get_absolute_time()); + forced_ready = false; + is_ready = false; + is_initialized = false; } void tud_umount_cb(void) { LOG_PRINTF("[USB] umount_cb\n"); - reset_all_contexts(false); + forced_ready = false; + is_ready = false; + is_initialized = false; } static uint16_t desc_str[32]; diff --git a/switch_pro_driver.h b/switch_pro_driver.h index 74e4346..15a04a9 100644 --- a/switch_pro_driver.h +++ b/switch_pro_driver.h @@ -11,7 +11,6 @@ #include "switch_haptics.h" #include "switch_pro_descriptors.h" - typedef struct { int16_t accel_x; int16_t accel_y; @@ -50,68 +49,24 @@ typedef struct { uint8_t imu_sample_count; // 0-3 SwitchImuSample imu_samples[3]; } SwitchInputState; -typedef struct { - uint8_t red; - uint8_t green; - uint8_t blue; -} SwitchRgbColor; -constexpr SwitchRgbColor switch_pro_calibrate_light_color( - SwitchRgbColor grip) { - const uint8_t minimum = - grip.red < grip.green - ? (grip.red < grip.blue ? grip.red : grip.blue) - : (grip.green < grip.blue ? grip.green : grip.blue); - const uint8_t maximum = - grip.red > grip.green - ? (grip.red > grip.blue ? grip.red : grip.blue) - : (grip.green > grip.blue ? grip.green : grip.blue); - const uint16_t chroma = static_cast(maximum - minimum); - const uint16_t peak = - static_cast((static_cast(maximum) * 2u + 1u) / - 3u); - if (chroma == 0) { - const uint8_t gray = static_cast(peak); - return {gray, gray, gray}; - } - const auto calibrate = [minimum, chroma, peak](uint8_t component) { - const uint32_t delta = - static_cast(component - minimum); - return static_cast( - (static_cast(peak) * delta * delta) / - (static_cast(chroma) * chroma)); - }; - return {calibrate(grip.red), calibrate(grip.green), - calibrate(grip.blue)}; -} +// Initialize USB state and calibration before entering the main loop. +void switch_pro_init(); +// 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 -// 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 +// Drive the Switch Pro USB state machine; returns true only when a regular // 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). -bool switch_pro_apply_uart_packet(const uint8_t* packet, uint8_t length, - SwitchInputState& out_state); +// If out_state is null the parsed state is written directly to the driver. +bool switch_pro_apply_uart_packet(const uint8_t* packet, uint8_t length, SwitchInputState* out_state = nullptr); // 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 -// interface. -typedef void (*SwitchRumbleCallback)(uint8_t instance, - const SwitchRumbleOutput& rumble); -void switch_pro_set_rumble_callback(uint8_t instance, - SwitchRumbleCallback callback); +// Optional callback fired with decoded rumble intensities from the host. +typedef void (*SwitchRumbleCallback)(const SwitchRumbleOutput& rumble); +void switch_pro_set_rumble_callback(SwitchRumbleCallback cb); diff --git a/tests/bluepad32_backend_lifecycle_test.cpp b/tests/bluepad32_backend_lifecycle_test.cpp deleted file mode 100644 index 50ad07c..0000000 --- a/tests/bluepad32_backend_lifecycle_test.cpp +++ /dev/null @@ -1,1168 +0,0 @@ -#include -#include -#include -#include - -#include -#include "../controller_color_config.h" - -namespace { - - -bool incoming_connections = false; -int scan_starts = 0; -int scan_stops = 0; -bool scanning_enabled = false; -int classic_scan_starts = 0; -int classic_scan_stops = 0; -bool classic_scanning_enabled = false; -uni_platform* installed_platform = nullptr; -bool observed_status_led_on = false; -int observed_status_led_writes = 0; -uint32_t now_ms = 0; -bool bondable = true; -bool ssp_auto_accept = true; -uint8_t accepted_stk_methods = 0xff; -uint16_t link_supervision_timeout = 0; -btstack_packet_handler_t pairing_event_handler = nullptr; -int confirmation_accepts = 0; -int confirmation_rejections = 0; -int passkey_accepts = 0; -int passkey_rejections = 0; -int delete_key_calls = 0; -bd_addr_t classic_bonds[4]{}; -int classic_bond_count = 0; -bd_addr_t ble_bonds[4]{}; -int ble_bond_types[4]{}; -int ble_bond_count = 0; - -bool flash_core_init_result = true; -int flash_core_init_calls = 0; -int core1_launch_calls = 0; -int cyw43_init_calls = 0; -int uni_init_calls = 0; -int device_disconnect_calls = 0; -uni_hid_device_t* last_disconnected_device = nullptr; - -struct CoreStopped {}; - - -void require(bool condition, const char* message) { - if (!condition) { - std::cerr << message << '\n'; - std::exit(1); - } -} - -void play_rumble(uni_hid_device_t* device, uint16_t, - uint16_t duration_ms, uint8_t high, uint8_t low) { - ++device->rumble_calls; - device->last_high = high; - device->last_low = low; - device->last_rumble_duration_ms = duration_ms; -} -void set_lightbar(uni_hid_device_t* device, uint8_t red, uint8_t green, - uint8_t blue) { - ++device->lightbar_calls; - device->lightbar_red = red; - device->lightbar_green = green; - device->lightbar_blue = blue; -} - -void set_player_leds(uni_hid_device_t* device, uint8_t leds) { - ++device->player_led_calls; - device->player_leds = leds; -} - - -uni_hid_device_t device( - int idx, bool gamepad = true, - uni_bt_conn_protocol_t protocol = UNI_BT_CONN_PROTOCOL_NONE) { - uni_hid_device_t result{}; - result.idx = idx; - result.gamepad = gamepad; - result.conn.protocol = protocol; - result.conn.btaddr[5] = static_cast(idx + 1); - result.report_parser.play_dual_rumble = play_rumble; - return result; -} - -} // namespace - - -bool uni_hid_device_is_gamepad(const uni_hid_device_t* device) { - return device != nullptr && device->gamepad; -} - -int uni_hid_device_get_idx_for_instance(const uni_hid_device_t* device) { - return device == nullptr ? -1 : device->idx; -} - -void uni_hid_device_disconnect(uni_hid_device_t* device) { - ++device_disconnect_calls; - last_disconnected_device = device; -} - -void uni_bt_allow_incoming_connections(bool enabled) { - incoming_connections = enabled; -} - - -void uni_bt_bredr_scan_start() { - ++classic_scan_starts; - classic_scanning_enabled = true; -} - -void uni_bt_bredr_scan_stop() { - if (classic_scanning_enabled) { - ++classic_scan_stops; - } - classic_scanning_enabled = false; -} - -void uni_bt_le_scan_start() { - ++scan_starts; - scanning_enabled = true; -} - -void uni_bt_le_scan_stop() { - if (scanning_enabled) { - ++scan_stops; - } - scanning_enabled = false; -} - -void uni_bt_start_scanning_and_autoconnect_unsafe() { - uni_bt_bredr_scan_start(); - uni_bt_le_scan_start(); -} - -void uni_bt_stop_scanning_unsafe() { - uni_bt_bredr_scan_stop(); - uni_bt_le_scan_stop(); -} -void uni_bt_del_keys_unsafe() { - ++delete_key_calls; - classic_bond_count = 0; - ble_bond_count = 0; -} - -int gap_link_key_iterator_init(btstack_link_key_iterator_t* iterator) { - iterator->index = 0; - return 1; -} - -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) { - if (iterator->index >= classic_bond_count) { - return 0; - } - memcpy(address, classic_bonds[iterator->index], sizeof(bd_addr_t)); - memset(link_key, iterator->index + 1, sizeof(link_key_t)); - *type = 0; - ++iterator->index; - return 1; -} - -void gap_link_key_iterator_done(btstack_link_key_iterator_t*) { -} - -int le_device_db_max_count() { - return 4; -} - -void le_device_db_info( - int index, int* address_type, bd_addr_t address, sm_key_t irk) { - if (index < ble_bond_count) { - *address_type = ble_bond_types[index]; - memcpy(address, ble_bonds[index], sizeof(bd_addr_t)); - if (irk != nullptr) { - memset(irk, index + 1, sizeof(sm_key_t)); - } - return; - } - *address_type = BD_ADDR_TYPE_UNKNOWN; -} - -void gap_set_bondable_mode(int enabled) { - bondable = enabled != 0; -} - -void gap_set_link_supervision_timeout(uint16_t timeout) { - link_supervision_timeout = timeout; -} - -void gap_ssp_set_auto_accept(int auto_accept) { - ssp_auto_accept = auto_accept != 0; -} -void sm_set_accepted_stk_generation_methods(uint8_t methods) { - accepted_stk_methods = methods; -} - - -int gap_ssp_confirmation_response(const bd_addr_t) { - ++confirmation_accepts; - return 0; -} - -int gap_ssp_confirmation_negative(const bd_addr_t) { - ++confirmation_rejections; - return 0; -} - -int gap_ssp_passkey_response(const bd_addr_t, uint32_t) { - ++passkey_accepts; - return 0; -} - -int gap_ssp_passkey_negative(const bd_addr_t) { - ++passkey_rejections; - return 0; -} - -void hci_add_event_handler( - btstack_packet_callback_registration_t* callback_handler) { - pairing_event_handler = callback_handler->callback; -} - -uint8_t hci_event_packet_get_type(const uint8_t* packet) { - return packet[0]; -} - -void copy_event_address(const uint8_t* packet, bd_addr_t address) { - for (size_t index = 0; index < sizeof(bd_addr_t); ++index) { - address[index] = packet[7 - index]; - } -} - -void hci_event_user_confirmation_request_get_bd_addr( - const uint8_t* packet, bd_addr_t address) { - copy_event_address(packet, address); -} - -void hci_event_user_passkey_request_get_bd_addr( - const uint8_t* packet, bd_addr_t address) { - copy_event_address(packet, address); -} - - -void uni_platform_set_custom(uni_platform* platform) { - installed_platform = platform; -} - -int uni_init(int, const char**) { - ++uni_init_calls; - return 0; -} - -bool flash_safe_execute_core_init() { - ++flash_core_init_calls; - return flash_core_init_result; -} - -int cyw43_arch_init() { - ++cyw43_init_calls; - return 0; -} - -void cyw43_arch_gpio_put(int, bool enabled) { - observed_status_led_on = enabled; - ++observed_status_led_writes; -} - -void multicore_launch_core1(void (*)()) { - ++core1_launch_calls; -} - -void tight_loop_contents() { - throw CoreStopped{}; -} - -uint32_t btstack_run_loop_get_time_ms() { - return now_ms; -} - - -#include "../bluepad32_input_backend.cpp" -SwitchRgbColor switch_pro_get_slot_light_color(uint8_t instance) { - static constexpr SwitchRgbColor grips[] = { - {SWITCH_COLOR_SLOT_1_R, SWITCH_COLOR_SLOT_1_G, - SWITCH_COLOR_SLOT_1_B}, - {SWITCH_COLOR_SLOT_2_R, SWITCH_COLOR_SLOT_2_G, - SWITCH_COLOR_SLOT_2_B}, - {SWITCH_COLOR_SLOT_3_R, SWITCH_COLOR_SLOT_3_G, - SWITCH_COLOR_SLOT_3_B}, - {SWITCH_COLOR_SLOT_4_R, SWITCH_COLOR_SLOT_4_G, - SWITCH_COLOR_SLOT_4_B}, - }; - return instance < sizeof(grips) / sizeof(grips[0]) - ? switch_pro_calibrate_light_color(grips[instance]) - : SwitchRgbColor{}; -} - -namespace { - -void start_backend() { - bluepad32_input_backend_init(); - platform_on_init_complete(); - require(incoming_connections && scanning_enabled && - classic_scanning_enabled && scan_starts == 1 && - link_supervision_timeout == - kClassicLinkSupervisionTimeout && - !bondable && accepted_stk_methods == 0 && - !ssp_auto_accept && pairing_event_handler != nullptr, - "initialization must configure liveness and pairing policy"); -} -void start_pairing_backend() { - start_backend(); - bluepad32_input_backend_open_pairing_window(); - process_rumble_timer(&g_rumble_timer); - require(g_connection_policy_state == ConnectionPolicyState::Open && - scanning_enabled && classic_scanning_enabled && - incoming_connections, - "test connection setup requires an open pairing window"); -} -void dispatch_pairing_event(uint8_t event_type) { - uint8_t packet[8] = {event_type, 6, 1, 2, 3, 4, 5, 6}; - pairing_event_handler(HCI_EVENT_PACKET, 0, packet, sizeof(packet)); -} - - - - -void tick_backend_timer(int ticks) { - for (int tick = 0; tick < ticks; ++tick) { - process_rumble_timer(&g_rumble_timer); - } -} - -void test_ready_order(bool reverse) { - start_pairing_backend(); - uni_hid_device_t devices[kSlotCount] = { - device(0), device(1), device(2), device(3)}; - uni_hid_device_t replacements[kSlotCount] = { - device(0), device(1), device(2), device(3)}; - const int forward[kSlotCount] = {0, 1, 2, 3}; - const int backward[kSlotCount] = {3, 2, 1, 0}; - const int* order = reverse ? backward : forward; - - platform_on_device_connected(&devices[order[0]]); - - for (int position = 0; position < kSlotCount; ++position) { - const int slot = order[position]; - require(platform_on_device_ready(&devices[slot]) == UNI_ERROR_SUCCESS, - "ready device must bind to its Bluepad index"); - - for (int candidate = 0; candidate < kSlotCount; ++candidate) { - SwitchInputState snapshot{}; - bool expected_active = false; - for (int ready = 0; ready <= position; ++ready) { - expected_active = expected_active || order[ready] == candidate; - } - require(bluepad32_input_backend_snapshot(candidate, &snapshot) == - expected_active, - "only ready indexed slots may become active"); - } - - if (position + 1 < kSlotCount) { - require(scan_stops == 0, - "scanning must continue while any slot remains free"); - require(incoming_connections, - "incoming connections must remain enabled before all slots are ready"); - } - } - - require(scan_stops == 1, - "scanning must stop exactly when all four slots are ready"); - require(!incoming_connections, - "incoming connections must be disabled only when all slots are full"); - - for (int slot = 0; slot < kSlotCount; ++slot) { - const int starts_before_disconnect = scan_starts; - platform_on_device_disconnected(&devices[slot]); - require(scan_starts == starts_before_disconnect + 1 && - scanning_enabled && incoming_connections, - "disconnecting any slot must resume connection policy"); - - for (int candidate = 0; candidate < kSlotCount; ++candidate) { - SwitchInputState snapshot{}; - require(bluepad32_input_backend_snapshot(candidate, &snapshot) == - (candidate != slot), - "disconnect must preserve every surviving slot"); - } - - require(platform_on_device_ready(&replacements[slot]) == - UNI_ERROR_SUCCESS, - "replacement must bind to each freed indexed slot"); - require(!scanning_enabled && !incoming_connections, - "restoring four ready slots must stop connection policy"); - devices[slot] = replacements[slot]; - } - - bd_addr_t address{}; - require(platform_on_device_discovered(address, "extra", 0, 0) == - UNI_ERROR_IGNORE_DEVICE, - "discovery must reject devices while all four slots are occupied"); -} - -void test_rejections() { - start_backend(); - uni_hid_device_t non_gamepad = device(0, false); - uni_hid_device_t out_of_range = device(4); - uni_hid_device_t slot_zero = device(0); - uni_hid_device_t collision = device(0); - - require(platform_on_device_ready(&non_gamepad) == - UNI_ERROR_INVALID_CONTROLLER, - "non-gamepad must be rejected"); - require(platform_on_device_ready(&out_of_range) == UNI_ERROR_NO_SLOTS, - "Bluepad index 4 must be rejected"); - require(platform_on_device_ready(&slot_zero) == UNI_ERROR_SUCCESS, - "valid device must occupy its indexed slot"); - require(platform_on_device_ready(&collision) == UNI_ERROR_NO_SLOTS, - "different device cannot replace an occupied slot"); - - uni_controller_t collision_data{}; - collision_data.klass = UNI_CONTROLLER_CLASS_GAMEPAD; - collision_data.gamepad.buttons = BUTTON_B; - platform_on_controller_data(&collision, &collision_data); - SwitchInputState snapshot{}; - require(bluepad32_input_backend_snapshot(0, &snapshot), - "occupied slot must stay active"); - require(!snapshot.button_a, - "mismatched device input must not enter the occupied slot"); - - uni_controller_t slot_zero_data{}; - slot_zero_data.klass = UNI_CONTROLLER_CLASS_GAMEPAD; - slot_zero_data.gamepad.accel[0] = 8192; - platform_on_controller_data(&slot_zero, &slot_zero_data); - require(bluepad32_input_backend_snapshot(0, &snapshot) && - snapshot.imu_sample_count == 3, - "valid slot input must remain observable"); - require(!bluepad32_input_backend_snapshot(4, &snapshot), - "public snapshot must reject slot 4"); - bluepad32_input_backend_report_sent(4); - require(bluepad32_input_backend_snapshot(0, &snapshot) && - snapshot.imu_sample_count == 3, - "slot 4 acknowledgement must not consume slot 0 IMU"); - bluepad32_input_backend_queue_rumble(4, SwitchRumbleOutput{1, 2}); - process_rumble_timer(&g_rumble_timer); - require(slot_zero.rumble_calls == 0, - "slot 4 rumble must not reach a valid controller"); -} - -void test_independent_lifecycle() { - start_pairing_backend(); - - uni_hid_device_t aborted = device(0); - const uint32_t aborted_generation = g_slots[0].connection_generation; - platform_on_device_connected(&aborted); - require(g_slots[0].device == &aborted && !g_slots[0].active, - "connected device must remain identifiable while becoming ready"); - - const int starts_before_aborted_disconnect = scan_starts; - const int classic_starts_before_aborted_disconnect = - classic_scan_starts; - const int stops_before_aborted_disconnect = scan_stops; - const int classic_stops_before_aborted_disconnect = - classic_scan_stops; - platform_on_device_disconnected(&aborted); - require(g_slots[0].device == nullptr && !g_slots[0].active, - "pre-ready disconnect must clear its pending slot identity"); - require(g_slots[0].connection_generation == aborted_generation + 1, - "pre-ready disconnect must invalidate its connection generation"); - require(g_connection_status == ConnectionStatus::Scanning && - scanning_enabled && classic_scanning_enabled && - incoming_connections && - scan_starts == starts_before_aborted_disconnect + 1 && - classic_scan_starts == - classic_starts_before_aborted_disconnect + 1 && - scan_stops == stops_before_aborted_disconnect + 1 && - classic_scan_stops == - classic_stops_before_aborted_disconnect + 1, - "pre-ready disconnect must restart Classic and BLE scans"); - - uni_hid_device_t devices[kSlotCount] = { - device(0), device(1), device(2), device(3)}; - for (int slot = 0; slot < kSlotCount; ++slot) { - platform_on_device_connected(&devices[slot]); - require(g_slots[slot].device == &devices[slot] && - !g_slots[slot].active, - "each pending device must retain its indexed identity"); - } - - const uint32_t first_pending_generation = - g_slots[0].connection_generation; - const int starts_before_first_pending_disconnect = scan_starts; - platform_on_device_disconnected(&devices[0]); - require(g_slots[0].device == nullptr && !g_slots[0].active, - "pre-ready disconnect must clear only its own pending identity"); - for (int slot = 1; slot < kSlotCount; ++slot) { - require(g_slots[slot].device == &devices[slot] && - !g_slots[slot].active, - "pre-ready disconnect must preserve all pending survivors"); - } - require(g_slots[0].connection_generation == - first_pending_generation + 1, - "pending disconnect beside peers must invalidate its generation"); - require(g_connection_status == ConnectionStatus::Connecting && - scanning_enabled && classic_scanning_enabled && - incoming_connections && - scan_starts == starts_before_first_pending_disconnect + 1, - "open pairing slot must preserve pending peers and resume scanning"); - - for (int slot = 1; slot < kSlotCount; ++slot) { - require(platform_on_device_ready(&devices[slot]) == UNI_ERROR_SUCCESS, - "each surviving pending device must still become ready"); - } - platform_on_device_connected(&devices[0]); - require(platform_on_device_ready(&devices[0]) == UNI_ERROR_SUCCESS, - "reconnected slot 0 device must complete all four slots"); - require(g_connection_status == ConnectionStatus::Ready && - !scanning_enabled && !incoming_connections, - "four ready lifecycle devices must stop connection policy"); - - const uint32_t buttons[kSlotCount] = { - BUTTON_B, BUTTON_A, BUTTON_X, BUTTON_Y}; - uni_controller_t data[kSlotCount]{}; - for (int slot = 0; slot < kSlotCount; ++slot) { - data[slot].klass = UNI_CONTROLLER_CLASS_GAMEPAD; - data[slot].gamepad.buttons = buttons[slot]; - data[slot].gamepad.accel[slot % 3] = 8192 + slot; - data[slot].gamepad.gyro[(slot + 1) % 3] = 1024 + slot; - platform_on_controller_data(&devices[slot], &data[slot]); - } - - SwitchInputState states[kSlotCount]{}; - for (int slot = 0; slot < kSlotCount; ++slot) { - require(bluepad32_input_backend_snapshot(slot, &states[slot]) && - states[slot].imu_sample_count == 3, - "every slot must expose independent input and IMU"); - } - require(states[0].button_a && !states[0].button_b && - !states[0].button_y && !states[0].button_x, - "slot 0 must contain only slot 0 input"); - require(states[1].button_b && !states[1].button_a && - !states[1].button_y && !states[1].button_x, - "slot 1 must contain only slot 1 input"); - require(states[2].button_y && !states[2].button_a && - !states[2].button_b && !states[2].button_x, - "slot 2 must contain only slot 2 input"); - require(states[3].button_x && !states[3].button_a && - !states[3].button_b && !states[3].button_y, - "slot 3 must contain only slot 3 input"); - - bluepad32_input_backend_report_sent(3); - for (int slot = 0; slot < kSlotCount; ++slot) { - require(bluepad32_input_backend_snapshot(slot, &states[slot]) && - states[slot].imu_sample_count == (slot == 3 ? 0 : 3), - "slot 3 acknowledgement must not consume slots 0-2 IMU"); - } - for (int slot = 0; slot < 3; ++slot) { - bluepad32_input_backend_report_sent(slot); - require(bluepad32_input_backend_snapshot(slot, &states[slot]) && - states[slot].imu_sample_count == 0, - "each slot acknowledgement must consume only its own IMU"); - } - - const SwitchRumbleOutput initial_rumble[kSlotCount] = { - {11, 21}, {12, 22}, {13, 23}, {14, 24}}; - for (int slot = 0; slot < kSlotCount; ++slot) { - bluepad32_input_backend_queue_rumble(slot, initial_rumble[slot]); - } - process_rumble_timer(&g_rumble_timer); - for (int slot = 0; slot < kSlotCount; ++slot) { - require(devices[slot].rumble_calls == 1 && - devices[slot].last_low == 11 + slot && - devices[slot].last_high == 21 + slot, - "each slot rumble must reach only its indexed controller"); - } - - bluepad32_input_backend_queue_rumble(3, SwitchRumbleOutput{55, 66}); - const uint32_t disconnected_generation = - g_slots[3].connection_generation; - const int starts_before_slot_three_disconnect = scan_starts; - platform_on_device_disconnected(&devices[3]); - require(scan_starts == starts_before_slot_three_disconnect + 1 && - scanning_enabled && incoming_connections, - "slot 3 disconnect must resume scanning and incoming connections"); - require(!bluepad32_input_backend_snapshot(3, &states[3]) && - !states[3].button_x && states[3].lx == 32768, - "slot 3 disconnect must neutralize only slot 3"); - require(bluepad32_input_backend_snapshot(0, &states[0]) && - states[0].button_a && - bluepad32_input_backend_snapshot(1, &states[1]) && - states[1].button_b && - bluepad32_input_backend_snapshot(2, &states[2]) && - states[2].button_y, - "slot 3 disconnect must preserve slots 0-2"); - platform_on_controller_data(&devices[0], &data[0]); - require(bluepad32_input_backend_snapshot(0, &states[0]) && - states[0].button_a, - "slot 0 input must continue while slot 3 is disconnected"); - const int slot_zero_calls_while_scanning = devices[0].rumble_calls; - bluepad32_input_backend_queue_rumble(0, SwitchRumbleOutput{115, 116}); - tick_backend_timer(99); - require(devices[0].rumble_calls == slot_zero_calls_while_scanning + 1 && - devices[0].last_low == 115 && - devices[0].last_high == 116, - "slot 0 rumble must continue while slot 3 is disconnected"); - - uni_hid_device_t slot_three_replacement = device(3); - require(platform_on_device_ready(&slot_three_replacement) == - UNI_ERROR_SUCCESS, - "slot 3 replacement must bind to the freed indexed slot"); - process_rumble_timer(&g_rumble_timer); - require(slot_three_replacement.rumble_calls == 0, - "slot 3 replacement must not receive disconnected device rumble"); - - g_slots[3].pending_rumble = { - 3, disconnected_generation, SwitchRumbleOutput{77, 88}}; - g_slots[3].rumble_pending = true; - process_rumble_timer(&g_rumble_timer); - require(slot_three_replacement.rumble_calls == 0, - "stale slot 3 connection generation must be rejected"); - - uni_controller_t replacement_data{}; - replacement_data.klass = UNI_CONTROLLER_CLASS_GAMEPAD; - replacement_data.gamepad.buttons = BUTTON_Y; - replacement_data.gamepad.accel[0] = 9000; - platform_on_controller_data(&slot_three_replacement, &replacement_data); - require(bluepad32_input_backend_snapshot(3, &states[3]) && - states[3].button_x && states[3].imu_sample_count == 3, - "replacement input and IMU must populate only slot 3"); - require(bluepad32_input_backend_snapshot(0, &states[0]) && - states[0].button_a && - bluepad32_input_backend_snapshot(1, &states[1]) && - states[1].button_b && - bluepad32_input_backend_snapshot(2, &states[2]) && - states[2].button_y, - "slot 3 replacement must not disturb slots 0-2"); - - const int survivor_calls[kSlotCount - 1] = { - devices[0].rumble_calls, - devices[1].rumble_calls, - devices[2].rumble_calls}; - bluepad32_input_backend_queue_rumble(3, SwitchRumbleOutput{90, 91}); - process_rumble_timer(&g_rumble_timer); - require(slot_three_replacement.rumble_calls == 1 && - slot_three_replacement.last_low == 90 && - slot_three_replacement.last_high == 91, - "new-generation slot 3 rumble must reach its replacement"); - for (int slot = 0; slot < 3; ++slot) { - require(devices[slot].rumble_calls == survivor_calls[slot], - "slot 3 rumble must not affect slots 0-2"); - } - const int all_slot_calls[kSlotCount] = { - devices[0].rumble_calls, - devices[1].rumble_calls, - devices[2].rumble_calls, - slot_three_replacement.rumble_calls}; - for (int slot = 0; slot < kSlotCount; ++slot) { - bluepad32_input_backend_queue_rumble( - slot, SwitchRumbleOutput{static_cast(100 + slot), - static_cast(110 + slot)}); - } - process_rumble_timer(&g_rumble_timer); - for (int slot = 0; slot < kSlotCount; ++slot) { - const uni_hid_device_t& target = - slot == 3 ? slot_three_replacement : devices[slot]; - require(target.rumble_calls == all_slot_calls[slot] + 1 && - target.last_low == 100 + slot && - target.last_high == 110 + slot, - "survivor rumble must continue after slot 3 replacement"); - } - - uni_hid_device_t replacements[kSlotCount] = { - device(0), device(1), device(2), device(3)}; - for (int slot = 0; slot < 3; ++slot) { - const int starts_before_disconnect = scan_starts; - platform_on_device_disconnected(&devices[slot]); - require(scan_starts == starts_before_disconnect + 1 && - scanning_enabled && incoming_connections, - "disconnecting slots 0-2 must resume connection policy"); - require(!bluepad32_input_backend_snapshot(slot, &states[slot]) && - states[slot].lx == 32768, - "disconnect must neutralize its indexed slot"); - for (int survivor = 0; survivor < kSlotCount; ++survivor) { - if (survivor == slot) { - continue; - } - require(bluepad32_input_backend_snapshot(survivor, - &states[survivor]), - "disconnect must preserve all three survivors"); - } - require(platform_on_device_ready(&replacements[slot]) == - UNI_ERROR_SUCCESS, - "replacement must bind to each freed slot"); - require(g_connection_status == ConnectionStatus::Ready && - !scanning_enabled && !incoming_connections, - "replacement must restore the full four-slot policy"); - } - - const int slot_zero_calls_before_mailboxes = replacements[0].rumble_calls; - const int slot_three_calls_before_mailboxes = - slot_three_replacement.rumble_calls; - bluepad32_input_backend_queue_rumble(3, SwitchRumbleOutput{119, 120}); - bluepad32_input_backend_queue_rumble(3, SwitchRumbleOutput{121, 122}); - bluepad32_input_backend_queue_rumble(0, SwitchRumbleOutput{123, 124}); - process_rumble_timer(&g_rumble_timer); - require(slot_three_replacement.rumble_calls == - slot_three_calls_before_mailboxes + 1 && - slot_three_replacement.last_low == 121 && - slot_three_replacement.last_high == 122, - "slot 3 mailbox must dispatch only its latest queued value"); - require(replacements[0].rumble_calls == - slot_zero_calls_before_mailboxes + 1 && - replacements[0].last_low == 123 && - replacements[0].last_high == 124, - "slot 0 activity must not evict the slot 3 mailbox"); -} - -void test_pairing_window_policy() { - bd_addr_t address = {1, 2, 3, 4, 5, 6}; - - bluepad32_input_backend_init(); - require(platform_on_device_discovered(address, "controller", 0, 0) == - UNI_ERROR_IGNORE_DEVICE, - "discovery must remain closed before backend initialization"); - - start_backend(); - require(g_connection_policy_state == ConnectionPolicyState::Open && - classic_scanning_enabled && scanning_enabled && - incoming_connections, - "boot must allow normal Bluepad32 autoconnect"); - require(platform_on_device_discovered(address, "controller", 0, 0) == - UNI_ERROR_SUCCESS, - "boot policy must accept a discovered controller"); - dispatch_pairing_event(HCI_EVENT_USER_CONFIRMATION_REQUEST); - dispatch_pairing_event(HCI_EVENT_USER_PASSKEY_REQUEST); - require(confirmation_rejections == 1 && passkey_rejections == 1 && - confirmation_accepts == 0 && passkey_accepts == 0, - "closed BOOTSEL window must reject new SSP authentication"); - - - uni_hid_device_t reconnecting = device(0); - platform_on_device_connected(&reconnecting); - require(device_disconnect_calls == 0 && - g_slots[0].device == &reconnecting, - "boot policy must retain a reconnecting controller"); - platform_on_device_disconnected(&reconnecting); - - bluepad32_input_backend_open_pairing_window(); - require(!g_pairing_window_open, - "Core0 request must wait for Core1 consumption"); - process_rumble_timer(&g_rumble_timer); - require(g_pairing_window_open && bondable && - accepted_stk_methods == kAllBlePairingMethods && - g_pairing_window_deadline_ms == 60000 && - g_connection_policy_state == ConnectionPolicyState::Open && - classic_scanning_enabled && scanning_enabled && - incoming_connections, - "BOOTSEL must enable Classic and BLE pairing without interrupting autoconnect"); - dispatch_pairing_event(HCI_EVENT_USER_CONFIRMATION_REQUEST); - dispatch_pairing_event(HCI_EVENT_USER_PASSKEY_REQUEST); - require(confirmation_accepts == 1 && passkey_accepts == 1, - "open BOOTSEL window must accept new SSP authentication"); - - tick_backend_timer(20); - require(!observed_status_led_on, - "pairing double blink must finish its first pulse"); - tick_backend_timer(20); - require(observed_status_led_on, - "pairing double blink must start its second pulse"); - tick_backend_timer(20); - require(!observed_status_led_on, - "pairing double blink must finish its second pulse"); - - now_ms = 30000; - bluepad32_input_backend_open_pairing_window(); - process_rumble_timer(&g_rumble_timer); - require(g_pairing_window_deadline_ms == 90000, - "pairing request must extend deadline from current Core1 time"); - - uni_hid_device_t devices[kSlotCount] = { - device(0), device(1), device(2), device(3)}; - for (int slot = 0; slot < kSlotCount; ++slot) { - require(platform_on_device_ready(&devices[slot]) == UNI_ERROR_SUCCESS, - "policy test devices must fill all slots"); - } - require(g_connection_policy_state == ConnectionPolicyState::Paused && - g_pairing_window_open && !scanning_enabled && - !classic_scanning_enabled && !incoming_connections, - "full slots must pause scanning without closing the deadline"); - - now_ms = 90000; - process_rumble_timer(&g_rumble_timer); - require(!g_pairing_window_open && !bondable && - accepted_stk_methods == 0 && - g_connection_policy_state == ConnectionPolicyState::Paused, - "Classic and BLE pairing authentication must close at the deadline"); - platform_on_device_disconnected(&devices[3]); - require(g_connection_policy_state == ConnectionPolicyState::Passive && - !classic_scanning_enabled && !scanning_enabled && - incoming_connections, - "a freed slot with active controllers must remain passive"); - require(platform_on_device_discovered(address, "controller", 0, 0) == - UNI_ERROR_IGNORE_DEVICE, - "passive policy must reject inquiry discoveries"); - - bluepad32_input_backend_open_pairing_window(); - process_rumble_timer(&g_rumble_timer); - require(g_connection_policy_state == ConnectionPolicyState::Open && - classic_scanning_enabled && scanning_enabled && - incoming_connections, - "explicit BOOTSEL window must resume active discovery"); - require(platform_on_device_discovered(address, "controller", 0, 0) == - UNI_ERROR_SUCCESS, - "pairing window discovery must accept a controller"); -} - - -void test_slot_lighting() { - start_pairing_backend(); - uni_hid_device_t devices[kSlotCount] = { - device(0), device(1), device(2), device(3)}; - - for (uint8_t slot = 0; slot < kSlotCount; ++slot) { - devices[slot].report_parser.set_lightbar_color = set_lightbar; - devices[slot].report_parser.set_player_leds = set_player_leds; - require(platform_on_device_ready(&devices[slot]) == UNI_ERROR_SUCCESS, - "color-capable controller did not become ready"); - const SwitchRgbColor expected = - switch_pro_get_slot_light_color(slot); - require(devices[slot].lightbar_calls == 1 && - devices[slot].lightbar_red == expected.red && - devices[slot].lightbar_green == expected.green && - devices[slot].lightbar_blue == expected.blue && - devices[slot].player_led_calls == 0, - "slot color did not reach the controller lightbar"); - require(platform_on_device_ready(&devices[slot]) == UNI_ERROR_SUCCESS && - devices[slot].lightbar_calls == 1, - "duplicate ready event rewrote controller lighting"); - } - - platform_on_device_disconnected(&devices[2]); - uni_hid_device_t fallback = device(2); - fallback.report_parser.set_player_leds = set_player_leds; - require(platform_on_device_ready(&fallback) == UNI_ERROR_SUCCESS && - fallback.lightbar_calls == 0 && - fallback.player_led_calls == 1 && - fallback.player_leds == (1u << 2u), - "controller without RGB support did not receive its slot LED"); -} - -void require_south_button_mapping(const SwitchInputState& state, - bool swapped, - const char* message) { - require(state.button_a == swapped && state.button_b == !swapped && - !state.button_x && !state.button_y, - message); -} - -void test_abxy_hotkey() { - start_pairing_backend(); - uni_hid_device_t slot_zero = device(0); - uni_hid_device_t slot_one = device(1); - require(platform_on_device_ready(&slot_zero) == UNI_ERROR_SUCCESS && - platform_on_device_ready(&slot_one) == UNI_ERROR_SUCCESS, - "ABXY test controllers did not become ready"); - - uni_controller_t input{}; - input.klass = UNI_CONTROLLER_CLASS_GAMEPAD; - input.gamepad.buttons = BUTTON_A; - platform_on_controller_data(&slot_zero, &input); - SwitchInputState snapshot{}; - require(bluepad32_input_backend_snapshot(0, &snapshot), - "slot 0 ABXY state was not published"); - require_south_button_mapping( - snapshot, kDefaultSwapAbxy, - "slot 0 did not start in the configured ABXY layout"); - - input.gamepad.buttons = - kAbxyHotkeyButtonMask | BUTTON_A; - input.gamepad.misc_buttons = kAbxyHotkeyMiscMask; - platform_on_controller_data(&slot_zero, &input); - require(bluepad32_input_backend_snapshot(0, &snapshot), - "toggled slot 0 state was not published"); - require_south_button_mapping( - snapshot, !kDefaultSwapAbxy, - "hotkey did not toggle slot 0 ABXY mapping"); - require(!snapshot.button_l && !snapshot.button_r && - !snapshot.button_minus && !snapshot.button_plus, - "hotkey chord leaked into the Switch report"); - - bluepad32_input_backend_queue_rumble( - 0, SwitchRumbleOutput{0x11, 0x22}); - process_rumble_timer(&g_rumble_timer); - require(slot_zero.rumble_calls == 1 && - slot_zero.last_high == kAbxyFeedbackWeakMagnitude && - slot_zero.last_low == kAbxyFeedbackStrongMagnitude && - slot_zero.last_high == UINT8_MAX && - slot_zero.last_low == UINT8_MAX && - g_slots[0].rumble_pending, - "ABXY confirmation was not full-strength or did not take priority"); - - platform_on_controller_data(&slot_zero, &input); - process_rumble_timer(&g_rumble_timer); - require(g_slots[0].swap_abxy == !kDefaultSwapAbxy && - slot_zero.rumble_calls == 1, - "held hotkey toggled or rumbled more than once"); - - input.gamepad = {}; - platform_on_controller_data(&slot_zero, &input); - input.gamepad.buttons = kAbxyHotkeyButtonMask | BUTTON_A; - input.gamepad.misc_buttons = kAbxyHotkeyMiscMask; - platform_on_controller_data(&slot_zero, &input); - process_rumble_timer(&g_rumble_timer); - require(g_slots[0].swap_abxy == kDefaultSwapAbxy && - slot_zero.rumble_calls == 2, - "released hotkey did not re-arm for a second toggle"); - - now_ms = kAbxyFeedbackDurationMs - 1; - process_rumble_timer(&g_rumble_timer); - require(slot_zero.rumble_calls == 2 && g_slots[0].rumble_pending, - "host rumble interrupted ABXY confirmation"); - now_ms = kAbxyFeedbackDurationMs; - process_rumble_timer(&g_rumble_timer); - require(slot_zero.rumble_calls == 3 && - slot_zero.last_high == 0x22 && - slot_zero.last_low == 0x11 && - !g_slots[0].rumble_pending, - "deferred host rumble did not resume after confirmation"); - - uni_controller_t peer_input{}; - peer_input.klass = UNI_CONTROLLER_CLASS_GAMEPAD; - peer_input.gamepad.buttons = BUTTON_A; - platform_on_controller_data(&slot_one, &peer_input); - require(bluepad32_input_backend_snapshot(1, &snapshot), - "slot 1 ABXY state was not published"); - require_south_button_mapping( - snapshot, kDefaultSwapAbxy, - "slot 0 hotkey changed slot 1 layout"); - - platform_on_device_disconnected(&slot_zero); - uni_hid_device_t replacement = device(0); - require(platform_on_device_ready(&replacement) == UNI_ERROR_SUCCESS && - g_slots[0].swap_abxy == kDefaultSwapAbxy && - !g_slots[0].abxy_hotkey_latched && - !g_slots[0].feedback_pending, - "disconnect did not reset slot 0 hotkey state"); -} - -void test_motion_hotkey() { - start_pairing_backend(); - uni_hid_device_t slot_zero = device(0); - uni_hid_device_t slot_one = device(1); - require(platform_on_device_ready(&slot_zero) == UNI_ERROR_SUCCESS && - platform_on_device_ready(&slot_one) == UNI_ERROR_SUCCESS, - "motion hotkey test controllers did not become ready"); - - uni_controller_t input{}; - input.klass = UNI_CONTROLLER_CLASS_GAMEPAD; - input.gamepad.accel[0] = 8192; - platform_on_controller_data(&slot_zero, &input); - SwitchInputState snapshot{}; - require(bluepad32_input_backend_snapshot(0, &snapshot) && - snapshot.imu_sample_count == - (kDefaultMotionEnabled ? 3 : 0), - "slot 0 did not start with configured motion state"); - - input.gamepad.dpad = kMotionHotkeyDpadMask; - input.gamepad.buttons = kMotionHotkeyButtonMask; - input.gamepad.misc_buttons = kMotionHotkeyMiscMask; - platform_on_controller_data(&slot_zero, &input); - require(bluepad32_input_backend_snapshot(0, &snapshot) && - snapshot.imu_sample_count == - (kDefaultMotionEnabled ? 0 : 3) && - !snapshot.dpad_up && !snapshot.button_r && - !snapshot.button_plus, - "motion chord did not toggle motion or suppress its inputs"); - - process_rumble_timer(&g_rumble_timer); - require(slot_zero.rumble_calls == 1 && - slot_zero.last_rumble_duration_ms == - (kDefaultMotionEnabled - ? kMotionDisabledFeedbackDurationMs - : kMotionEnabledFeedbackDurationMs) && - slot_zero.last_high == - (kDefaultMotionEnabled - ? kMotionDisabledFeedbackWeakMagnitude - : kMotionEnabledFeedbackWeakMagnitude) && - slot_zero.last_low == - (kDefaultMotionEnabled - ? kMotionDisabledFeedbackStrongMagnitude - : kMotionEnabledFeedbackStrongMagnitude), - "motion toggle did not send distinct state feedback"); - - platform_on_controller_data(&slot_zero, &input); - process_rumble_timer(&g_rumble_timer); - require(g_slots[0].motion_enabled == !kDefaultMotionEnabled && - slot_zero.rumble_calls == 1, - "held motion chord toggled or rumbled more than once"); - - uni_controller_t peer_input{}; - peer_input.klass = UNI_CONTROLLER_CLASS_GAMEPAD; - peer_input.gamepad.accel[0] = 8192; - platform_on_controller_data(&slot_one, &peer_input); - require(bluepad32_input_backend_snapshot(1, &snapshot) && - snapshot.imu_sample_count == - (kDefaultMotionEnabled ? 3 : 0), - "slot 0 motion chord changed slot 1 motion state"); - - input.gamepad = {}; - platform_on_controller_data(&slot_zero, &input); - input.gamepad.accel[0] = 8192; - input.gamepad.dpad = kMotionHotkeyDpadMask; - input.gamepad.buttons = kMotionHotkeyButtonMask; - input.gamepad.misc_buttons = kMotionHotkeyMiscMask; - platform_on_controller_data(&slot_zero, &input); - require(bluepad32_input_backend_snapshot(0, &snapshot) && - snapshot.imu_sample_count == - (kDefaultMotionEnabled ? 3 : 0), - "released motion chord did not re-arm or restore motion"); - process_rumble_timer(&g_rumble_timer); - require(g_slots[0].motion_enabled == kDefaultMotionEnabled && - slot_zero.rumble_calls == 2, - "second motion chord did not restore configured state"); - - platform_on_device_disconnected(&slot_zero); - uni_hid_device_t replacement = device(0); - require(platform_on_device_ready(&replacement) == UNI_ERROR_SUCCESS && - g_slots[0].motion_enabled == kDefaultMotionEnabled && - !g_slots[0].motion_hotkey_latched && - !g_slots[0].feedback_pending, - "disconnect did not reset slot 0 motion hotkey state"); -} - -void test_clear_pairings() { - classic_bond_count = 1; - classic_bonds[0][0] = 0x10; - ble_bond_count = 1; - ble_bond_types[0] = BD_ADDR_TYPE_LE_PUBLIC; - ble_bonds[0][0] = 0x20; - start_pairing_backend(); - require(g_pairing_snapshot.status == - Bluepad32PairingSnapshotStatus::kReady && - g_pairing_snapshot.record_count == 2 && - g_pairing_snapshot.records[0].transport == - Bluepad32PairingTransport::kClassic && - g_pairing_snapshot.records[1].transport == - Bluepad32PairingTransport::kBle, - "initial pairing snapshot must enumerate Classic and BLE bonds"); - const uint32_t snapshot_generation = - g_pairing_snapshot.generation; - uni_hid_device_t devices[2] = {device(0), device(1)}; - for (uni_hid_device_t& controller : devices) { - require(platform_on_device_ready(&controller) == UNI_ERROR_SUCCESS, - "pairing reset controller did not become ready"); - } - bluepad32_input_backend_queue_rumble( - 0, SwitchRumbleOutput{100, 101}); - - bluepad32_input_backend_clear_pairings(); - require(g_clear_pairings_requested && delete_key_calls == 0 && - device_disconnect_calls == 0, - "Core0 pairing reset request must wait for Core1"); - process_rumble_timer(&g_rumble_timer); - - require(delete_key_calls == 1 && device_disconnect_calls == 2, - "pairing reset must delete bonds and disconnect every session"); - require(g_pairing_snapshot.status == - Bluepad32PairingSnapshotStatus::kReady && - g_pairing_snapshot.record_count == 0 && - g_pairing_snapshot.generation == - snapshot_generation + 1, - "pairing reset must publish an empty refreshed snapshot"); - for (const BackendSlot& slot : g_slots) { - require(slot.device == nullptr && !slot.active && - !slot.rumble_pending && !slot.feedback_pending && - slot.state.lx == kStickMidpoint && - slot.state.ly == kStickMidpoint && - slot.state.rx == kStickMidpoint && - slot.state.ry == kStickMidpoint && - slot.state.imu_sample_count == 0, - "pairing reset must publish neutral empty slots"); - } - require(!g_pairing_window_open && !bondable && - accepted_stk_methods == 0 && - g_connection_policy_state == ConnectionPolicyState::Open && - scanning_enabled && classic_scanning_enabled && - incoming_connections && observed_status_led_on, - "pairing reset must close authentication and resume autoconnect"); - - tick_backend_timer(9); - require(!observed_status_led_on, - "pairing reset confirmation must use the rapid blink pattern"); - process_rumble_timer(&g_rumble_timer); - require(delete_key_calls == 1 && device_disconnect_calls == 2, - "pairing reset request must execute only once"); -} - -void test_flash_core_start_contract() { - bluepad32_input_backend_init(); - flash_core_init_result = false; - bluepad32_input_backend_start(); - require(flash_core_init_calls == 1 && core1_launch_calls == 0 && - g_connection_policy_state == - ConnectionPolicyState::FailedClosed, - "Core0 flash-safe init failure must prevent Core1 launch"); - - flash_core_init_result = true; - bluepad32_input_backend_start(); - require(flash_core_init_calls == 2 && core1_launch_calls == 1, - "Core0 must register as a flash-safe victim before Core1 launch"); - bluepad32_input_backend_start(); - require(flash_core_init_calls == 2 && core1_launch_calls == 1, - "backend start must remain idempotent"); -} - -void test_flash_core_init_fatal() { - bluepad32_input_backend_init(); - flash_core_init_result = false; - bool stopped = false; - try { - core1_main(); - } catch (const CoreStopped&) { - stopped = true; - } - require(stopped && flash_core_init_calls == 1 && - cyw43_init_calls == 0 && uni_init_calls == 0 && - g_connection_policy_state == - ConnectionPolicyState::FailedClosed, - "flash-safe Core1 init failure must halt before CYW43 init"); -} - -} // namespace - -int main(int argc, char** argv) { - require(argc == 2, "scenario argument required"); - const std::string scenario = argv[1]; - if (scenario == "ready-forward") { - test_ready_order(false); - } else if (scenario == "ready-reverse") { - test_ready_order(true); - } else if (scenario == "rejections") { - test_rejections(); - } else if (scenario == "lifecycle") { - test_independent_lifecycle(); - } else if (scenario == "pairing-policy") { - test_pairing_window_policy(); - } else if (scenario == "slot-lighting") { - test_slot_lighting(); - } else if (scenario == "abxy-hotkey") { - test_abxy_hotkey(); - } else if (scenario == "motion-hotkey") { - test_motion_hotkey(); - } else if (scenario == "clear-pairings") { - test_clear_pairings(); - } else if (scenario == "flash-core-start") { - test_flash_core_start_contract(); - } else if (scenario == "flash-core-failure") { - test_flash_core_init_fatal(); - } else { - require(false, "unknown scenario"); - } - return 0; -} diff --git a/tests/bluepad32_imu_normalization_test.cpp b/tests/bluepad32_imu_normalization_test.cpp deleted file mode 100644 index 96775a0..0000000 --- a/tests/bluepad32_imu_normalization_test.cpp +++ /dev/null @@ -1,212 +0,0 @@ -#include "parser/uni_hid_parser_imu.h" - -#include -#include -#include -#include - -namespace { - -int failures = 0; - -void expect(bool condition, const char* message) { - if (!condition) { - std::cerr << message << '\n'; - ++failures; - } -} - -void write_calibration_value( - std::array& 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(value + 0x8000) - : static_cast(static_cast(value)); - blob[offset] = static_cast(encoded); - blob[offset + 1] = static_cast(encoded >> 8u); -} - -uint16_t encode_input(uni_psmove_imu_model_t model, int32_t value) { - return model == UNI_PSMOVE_IMU_MODEL_ZCM1 - ? static_cast(value + 0x8000) - : static_cast(static_cast(value)); -} - -std::array first_report( - const std::array& blob) { - std::array report{}; - std::memcpy(report.data(), blob.data(), report.size()); - report[0] = 0x10; - report[1] = 0x00; - return report; -} - -std::array continuation_report( - const std::array& blob, - uint8_t block, size_t blob_offset) { - std::array 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& 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 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 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; -} diff --git a/tests/bluepad32_native_stubs/btstack_run_loop.h b/tests/bluepad32_native_stubs/btstack_run_loop.h deleted file mode 100644 index 5487f88..0000000 --- a/tests/bluepad32_native_stubs/btstack_run_loop.h +++ /dev/null @@ -1,23 +0,0 @@ -#pragma once - -#include - -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() {} diff --git a/tests/bluepad32_native_stubs/pico/critical_section.h b/tests/bluepad32_native_stubs/pico/critical_section.h deleted file mode 100644 index ee838ed..0000000 --- a/tests/bluepad32_native_stubs/pico/critical_section.h +++ /dev/null @@ -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*) {} diff --git a/tests/bluepad32_native_stubs/pico/cyw43_arch.h b/tests/bluepad32_native_stubs/pico/cyw43_arch.h deleted file mode 100644 index 909a028..0000000 --- a/tests/bluepad32_native_stubs/pico/cyw43_arch.h +++ /dev/null @@ -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); diff --git a/tests/bluepad32_native_stubs/pico/flash.h b/tests/bluepad32_native_stubs/pico/flash.h deleted file mode 100644 index fd6a2c8..0000000 --- a/tests/bluepad32_native_stubs/pico/flash.h +++ /dev/null @@ -1,3 +0,0 @@ -#pragma once - -bool flash_safe_execute_core_init(); diff --git a/tests/bluepad32_native_stubs/pico/multicore.h b/tests/bluepad32_native_stubs/pico/multicore.h deleted file mode 100644 index 33d06f7..0000000 --- a/tests/bluepad32_native_stubs/pico/multicore.h +++ /dev/null @@ -1,3 +0,0 @@ -#pragma once - -void multicore_launch_core1(void (*entry)()); diff --git a/tests/bluepad32_native_stubs/pico/stdlib.h b/tests/bluepad32_native_stubs/pico/stdlib.h deleted file mode 100644 index 510d071..0000000 --- a/tests/bluepad32_native_stubs/pico/stdlib.h +++ /dev/null @@ -1,3 +0,0 @@ -#pragma once - -void tight_loop_contents(); diff --git a/tests/bluepad32_native_stubs/uni.h b/tests/bluepad32_native_stubs/uni.h deleted file mode 100644 index 7ee86a8..0000000 --- a/tests/bluepad32_native_stubs/uni.h +++ /dev/null @@ -1,190 +0,0 @@ -#pragma once - -#include - -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); diff --git a/tests/bootsel_native_stubs/hardware/gpio.h b/tests/bootsel_native_stubs/hardware/gpio.h deleted file mode 100644 index 32af518..0000000 --- a/tests/bootsel_native_stubs/hardware/gpio.h +++ /dev/null @@ -1,27 +0,0 @@ -#pragma once - -#include - -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 diff --git a/tests/bootsel_native_stubs/hardware/regs/sio.h b/tests/bootsel_native_stubs/hardware/regs/sio.h deleted file mode 100644 index 6b0f9eb..0000000 --- a/tests/bootsel_native_stubs/hardware/regs/sio.h +++ /dev/null @@ -1,3 +0,0 @@ -#pragma once - -#define SIO_GPIO_HI_IN_QSPI_CSN_BITS 0x08000000u diff --git a/tests/bootsel_native_stubs/hardware/structs/ioqspi.h b/tests/bootsel_native_stubs/hardware/structs/ioqspi.h deleted file mode 100644 index 9a7e80b..0000000 --- a/tests/bootsel_native_stubs/hardware/structs/ioqspi.h +++ /dev/null @@ -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; diff --git a/tests/bootsel_native_stubs/hardware/structs/sio.h b/tests/bootsel_native_stubs/hardware/structs/sio.h deleted file mode 100644 index 9564835..0000000 --- a/tests/bootsel_native_stubs/hardware/structs/sio.h +++ /dev/null @@ -1,10 +0,0 @@ -#pragma once - -#include - -struct sio_hw_t { - volatile uint32_t gpio_in; - volatile uint32_t gpio_hi_in; -}; - -extern sio_hw_t* sio_hw; diff --git a/tests/bootsel_native_stubs/pico/flash.h b/tests/bootsel_native_stubs/pico/flash.h deleted file mode 100644 index e33f7f3..0000000 --- a/tests/bootsel_native_stubs/pico/flash.h +++ /dev/null @@ -1,10 +0,0 @@ -#pragma once - -#include - -#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); diff --git a/tests/bootsel_native_stubs/pico/time.h b/tests/bootsel_native_stubs/pico/time.h deleted file mode 100644 index c3921f1..0000000 --- a/tests/bootsel_native_stubs/pico/time.h +++ /dev/null @@ -1,8 +0,0 @@ -#pragma once - -#include - -using absolute_time_t = uint64_t; - -absolute_time_t get_absolute_time(); -uint64_t to_ms_since_boot(absolute_time_t time); diff --git a/tests/bootsel_pairing_button_test.cpp b/tests/bootsel_pairing_button_test.cpp deleted file mode 100644 index 1e6ab19..0000000 --- a/tests/bootsel_pairing_button_test.cpp +++ /dev/null @@ -1,266 +0,0 @@ -#include "bootsel_pairing_button.h" - -#include -#include -#include -#include - -#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 flash_responses; -std::size_t next_flash_response = 0; -std::vector 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 apply_pressed( - BootselPairingButtonHoldFsm& fsm, int count) { - std::vector 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; -} diff --git a/tests/native_stubs/pico/rand.h b/tests/native_stubs/pico/rand.h deleted file mode 100644 index 525ec44..0000000 --- a/tests/native_stubs/pico/rand.h +++ /dev/null @@ -1,13 +0,0 @@ -#pragma once - -#include - -#ifdef __cplusplus -extern "C" { -#endif - -uint32_t get_rand_32(void); - -#ifdef __cplusplus -} -#endif diff --git a/tests/native_stubs/pico/time.h b/tests/native_stubs/pico/time.h deleted file mode 100644 index 7ade2a8..0000000 --- a/tests/native_stubs/pico/time.h +++ /dev/null @@ -1,18 +0,0 @@ -#pragma once - -#include - -#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 diff --git a/tests/native_stubs/tusb.h b/tests/native_stubs/tusb.h deleted file mode 100644 index 0905dee..0000000 --- a/tests/native_stubs/tusb.h +++ /dev/null @@ -1,45 +0,0 @@ -#pragma once - -#include -#include - -#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 diff --git a/tests/switch_pro_descriptors_test.cpp b/tests/switch_pro_descriptors_test.cpp deleted file mode 100644 index edcf392..0000000 --- a/tests/switch_pro_descriptors_test.cpp +++ /dev/null @@ -1,184 +0,0 @@ -#include "switch_pro_descriptors.h" -#include "tusb_config.h" - -#include -#include -#include -#include -#include - -#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 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(bytes[0]) | - (static_cast(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 interfaces{}; - std::array 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(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(current_interface)]; - ++interface.endpoint_count; - const uint8_t endpoint_number = - static_cast(current_interface + 1); - if ((address & 0x80u) != 0) { - expect(address == static_cast(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; -} diff --git a/tests/switch_pro_driver_context_test.cpp b/tests/switch_pro_driver_context_test.cpp deleted file mode 100644 index baa62b4..0000000 --- a/tests/switch_pro_driver_context_test.cpp +++ /dev/null @@ -1,696 +0,0 @@ -#include "switch_pro_driver.h" -#include "controller_color_config.h" -#include "tusb.h" -#include "pico/time.h" - -#include -#include -#include -#include -#include - -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 data{}; -}; - -struct RumbleEvent { - unsigned count = 0; - uint8_t instance = 0xff; - SwitchRumbleOutput output{}; -}; - -uint64_t now_ms = 0; -uint32_t random_value = 1; -std::array hid_ready{}; -std::array hid_report_succeeds{}; -std::array hid_report_attempts{}; -std::array sent_reports{}; -unsigned sent_report_count = 0; -std::array 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 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(-static_cast(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(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( - static_cast(bytes[0]) | - (static_cast(bytes[1]) << 8u)); -} - -void send_feature(uint8_t instance, uint8_t command, uint8_t value) { - std::array 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 report{}; - report[0] = REPORT_FEATURE; - report[10] = SPI_READ; - report[11] = static_cast(address); - report[12] = static_cast(address >> 8u); - report[13] = static_cast(address >> 16u); - report[14] = static_cast(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(low_amplitude) & 0x7fu) << 23u) | - ((static_cast(low_frequency) & 0x7fu) << 16u) | - ((static_cast(high_amplitude) & 0x7fu) << 9u) | - ((static_cast(high_frequency) & 0x7fu) << 2u); -} - -uint32_t type_1_one_sample(uint8_t high_command, uint8_t low_command) { - return (1u << 30u) | - ((static_cast(low_command) & 0x1fu) << 25u) | - ((static_cast(high_command) & 0x1fu) << 20u); -} - -std::array rumble_payload(uint32_t left, uint32_t right) { - std::array payload{}; - const uint32_t words[] = {left, right}; - for (unsigned actuator = 0; actuator < 2; ++actuator) { - unsigned offset = actuator * 4u; - payload[offset] = static_cast(words[actuator]); - payload[offset + 1] = static_cast(words[actuator] >> 8u); - payload[offset + 2] = static_cast(words[actuator] >> 16u); - payload[offset + 3] = static_cast(words[actuator] >> 24u); - } - return payload; -} - -std::array complete_rumble_report( - const std::array& payload) { - std::array 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 states{}; - for (uint8_t instance = 0; instance < kInstanceCount; ++instance) { - SwitchInputState& state = states[instance]; - state.lx = static_cast(0x1111u * (instance + 1u)); - state.ly = static_cast(0x2222u + 0x1111u * instance); - state.rx = static_cast(0x5555u + 0x1111u * instance); - state.ry = static_cast(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 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, 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(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 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(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 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 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 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(buttons); - packet[4] = static_cast(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(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 current{}; - tud_hid_get_report_cb(0, 0, HID_REPORT_TYPE_INPUT, current.data(), - current.size()); - SwitchProReport current_report{}; - std::memcpy(¤t_report, current.data(), sizeof(current_report)); - expect(current_report.inputs.buttonX && !current_report.inputs.buttonA, - "UART parsing mutated driver context state"); - - SwitchInputState unchanged{}; - unchanged.button_home = true; - unchanged.lx = 123; - packet.back() ^= 0xffu; - expect(!switch_pro_apply_uart_packet(packet.data(), packet.size(), - unchanged), - "invalid UART checksum was accepted"); - expect(unchanged.button_home && unchanged.lx == 123, - "failed UART parse modified its output reference"); -} - -} // namespace - -extern "C" absolute_time_t get_absolute_time(void) { - return {now_ms}; -} -extern "C" uint32_t to_ms_since_boot(absolute_time_t time) { - return static_cast(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; -} diff --git a/tests/test_bluepad32_backend_lifecycle_native.py b/tests/test_bluepad32_backend_lifecycle_native.py deleted file mode 100644 index a583717..0000000 --- a/tests/test_bluepad32_backend_lifecycle_native.py +++ /dev/null @@ -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) diff --git a/tests/test_bluepad32_imu_normalization_native.py b/tests/test_bluepad32_imu_normalization_native.py deleted file mode 100644 index 54a465b..0000000 --- a/tests/test_bluepad32_imu_normalization_native.py +++ /dev/null @@ -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) diff --git a/tests/test_bootsel_pairing_button_native.py b/tests/test_bootsel_pairing_button_native.py deleted file mode 100644 index b05f1b7..0000000 --- a/tests/test_bootsel_pairing_button_native.py +++ /dev/null @@ -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) diff --git a/tests/test_pairing_manager.py b/tests/test_pairing_manager.py deleted file mode 100644 index a3901f8..0000000 --- a/tests/test_pairing_manager.py +++ /dev/null @@ -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(" 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 diff --git a/tests/test_switch_pro_descriptors_native.py b/tests/test_switch_pro_descriptors_native.py deleted file mode 100644 index a586843..0000000 --- a/tests/test_switch_pro_descriptors_native.py +++ /dev/null @@ -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" - ) diff --git a/tests/test_switch_pro_driver_context_native.py b/tests/test_switch_pro_driver_context_native.py deleted file mode 100644 index 45102de..0000000 --- a/tests/test_switch_pro_driver_context_native.py +++ /dev/null @@ -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) diff --git a/tests/test_usb_pairing_management_native.py b/tests/test_usb_pairing_management_native.py deleted file mode 100644 index d52aa08..0000000 --- a/tests/test_usb_pairing_management_native.py +++ /dev/null @@ -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) diff --git a/tests/usb_management_native_stubs/tusb.h b/tests/usb_management_native_stubs/tusb.h deleted file mode 100644 index 2196f72..0000000 --- a/tests/usb_management_native_stubs/tusb.h +++ /dev/null @@ -1,43 +0,0 @@ -#pragma once - -#include -#include - -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 diff --git a/tests/usb_pairing_management_test.cpp b/tests/usb_pairing_management_test.cpp deleted file mode 100644 index 9f79ea2..0000000 --- a/tests/usb_pairing_management_test.cpp +++ /dev/null @@ -1,144 +0,0 @@ -#include "usb_pairing_management.h" - -#include -#include -#include -#include - -#include - -namespace { - -Bluepad32PairingSnapshot current_snapshot{}; -bool refresh_requested = false; -bool clear_requested = false; -bool control_status_sent = false; -std::vector 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(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; -} diff --git a/tusb_config.h b/tusb_config.h index a6f81b6..7f291ec 100644 --- a/tusb_config.h +++ b/tusb_config.h @@ -1,19 +1,11 @@ -// TinyUSB configuration for one to four Switch Pro style HID interfaces. -// Each interface uses independent 64-byte interrupt IN and OUT endpoints. +// TinyUSB configuration tailored for a single Switch Pro style HID interface. +// Data is derived from TinyUSB examples and tuned for a 64-byte HID endpoint. #ifndef _TUSB_CONFIG_H_ #define _TUSB_CONFIG_H_ #ifdef __cplusplus extern "C" { #endif -#ifndef SWITCH_PICO_HID_INSTANCE_COUNT -#define SWITCH_PICO_HID_INSTANCE_COUNT 1 -#endif - -#if SWITCH_PICO_HID_INSTANCE_COUNT < 1 || SWITCH_PICO_HID_INSTANCE_COUNT > 4 -#error "SWITCH_PICO_HID_INSTANCE_COUNT must be between 1 and 4" -#endif - #define CFG_TUSB_RHPORT0_MODE (OPT_MODE_DEVICE | OPT_MODE_FULL_SPEED) #ifndef CFG_TUSB_OS @@ -31,7 +23,7 @@ extern "C" { #define CFG_TUD_ENDPOINT0_SIZE 64 // 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_MSC 0 #define CFG_TUD_MIDI 0 diff --git a/usb_pairing_management.cpp b/usb_pairing_management.cpp deleted file mode 100644 index 5360eeb..0000000 --- a/usb_pairing_management.cpp +++ /dev/null @@ -1,92 +0,0 @@ -#include "usb_pairing_management.h" - -#include - -#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(snapshot.status); - output[6] = snapshot.record_count; - output[7] = snapshot.overflow ? 1 : 0; - output[8] = static_cast(snapshot.generation); - output[9] = static_cast(snapshot.generation >> 8); - output[10] = static_cast(snapshot.generation >> 16); - output[11] = static_cast(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(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(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; - } -} diff --git a/usb_pairing_management.h b/usb_pairing_management.h deleted file mode 100644 index a952485..0000000 --- a/usb_pairing_management.h +++ /dev/null @@ -1,25 +0,0 @@ -#pragma once - -#include -#include - -#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 diff --git a/uv.lock b/uv.lock index f562a14..d400c30 100644 --- a/uv.lock +++ b/uv.lock @@ -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" }, ] -[[package]] -name = "pyusb" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/00/6b/ce3727395e52b7b76dfcf0c665e37d223b680b9becc60710d4bc08b7b7cb/pyusb-1.3.1.tar.gz", hash = "sha256:3af070b607467c1c164f49d5b0caabe8ac78dbed9298d703a8dbf9df4052d17e", size = 77281, upload-time = "2025-01-08T23:45:01.866Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/28/b8/27e6312e86408a44fe16bd28ee12dd98608b39f7e7e57884a24e8f29b573/pyusb-1.3.1-py3-none-any.whl", hash = "sha256:bf9b754557af4717fe80c2b07cc2b923a9151f5c08d17bdb5345dac09d6a0430", size = 58465, upload-time = "2025-01-08T23:45:00.029Z" }, -] - [[package]] name = "requests" version = "2.32.5" @@ -949,7 +940,6 @@ dependencies = [ { name = "hidapi" }, { name = "pysdl3" }, { name = "pyserial" }, - { name = "pyusb" }, { name = "rich" }, ] @@ -958,7 +948,6 @@ requires-dist = [ { name = "hidapi" }, { name = "pysdl3" }, { name = "pyserial" }, - { name = "pyusb" }, { name = "rich" }, ]