diff --git a/CMakeLists.txt b/CMakeLists.txt index 16d4a87..9273c80 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -177,9 +177,11 @@ if(SWITCH_PICO_SWITCH2_MEMORY_CAPTURE) endif() set(SWITCH2_BRIDGE_WII_INPUT OFF) set(SWITCH2_BRIDGE_DUALSENSE_INPUT OFF) +set(SWITCH2_BRIDGE_GAMEPAD_INPUT OFF) +set(SWITCH2_BRIDGE_FULL_INPUT OFF) if(SWITCH_PICO_SWITCH2_USB_BRIDGE) - set(SWITCH2_BRIDGE_INPUT "JOYCON2" CACHE STRING "Native bridge source: JOYCON2, WII or DUALSENSE") - set_property(CACHE SWITCH2_BRIDGE_INPUT PROPERTY STRINGS JOYCON2 WII DUALSENSE) + set(SWITCH2_BRIDGE_INPUT "JOYCON2" CACHE STRING "Native source: JOYCON2, WII, DUALSENSE or GAMEPAD") + set_property(CACHE SWITCH2_BRIDGE_INPUT PROPERTY STRINGS JOYCON2 WII DUALSENSE GAMEPAD) set(SWITCH2_BRIDGE_IMU_TARGET "BOTH" CACHE STRING "Translated native-pair IMU target: LEFT, RIGHT or BOTH") set_property(CACHE SWITCH2_BRIDGE_IMU_TARGET PROPERTY STRINGS LEFT RIGHT BOTH) if(SWITCH2_BRIDGE_IMU_TARGET STREQUAL "RIGHT") @@ -195,12 +197,22 @@ if(SWITCH_PICO_SWITCH2_USB_BRIDGE) if(NOT SWITCH_PICO_INPUT_BACKEND STREQUAL "BLUEPAD32") message(FATAL_ERROR "Switch 2 USB bridge requires BLUEPAD32") endif() - if(SWITCH2_BRIDGE_INPUT STREQUAL "DUALSENSE") - if(NOT SWITCH2_PROBE_HUB OR NOT SWITCH_PICO_ENABLE_CLASSIC) - message(FATAL_ERROR "DualSense native R/L bridge requires HUB and Classic Bluetooth") + if(SWITCH2_BRIDGE_INPUT STREQUAL "DUALSENSE" OR SWITCH2_BRIDGE_INPUT STREQUAL "GAMEPAD") + if(NOT SWITCH2_PROBE_HUB) + message(FATAL_ERROR "Translated full-controller input requires the native R/L HUB") + endif() + set(SWITCH2_BRIDGE_FULL_INPUT ON) + add_compile_definitions(SWITCH2_BRIDGE_FULL_INPUT=1) + if(SWITCH2_BRIDGE_INPUT STREQUAL "DUALSENSE") + if(NOT SWITCH_PICO_ENABLE_CLASSIC) + message(FATAL_ERROR "DualSense input requires Classic Bluetooth") + endif() + set(SWITCH2_BRIDGE_DUALSENSE_INPUT ON) + add_compile_definitions(SWITCH2_BRIDGE_DUALSENSE_INPUT=1) + else() + set(SWITCH2_BRIDGE_GAMEPAD_INPUT ON) + add_compile_definitions(SWITCH2_BRIDGE_GAMEPAD_INPUT=1) endif() - set(SWITCH2_BRIDGE_DUALSENSE_INPUT ON) - add_compile_definitions(SWITCH2_BRIDGE_DUALSENSE_INPUT=1) elseif(NOT SWITCH_PICO_ENABLE_BLE OR NOT SWITCH_PICO_SWITCH2_MOUSE_CAPTURE OR NOT SWITCH_PICO_SWITCH2_MOUSE_CAPTURE_NATIVE) message(FATAL_ERROR "Joy-Con/Wii USB bridge requires BLE and native Joy-Con capture") @@ -214,10 +226,10 @@ if(SWITCH_PICO_SWITCH2_USB_BRIDGE) endif() set(SWITCH2_BRIDGE_WII_INPUT ON) add_compile_definitions(SWITCH2_BRIDGE_WII_INPUT=1 SWITCH_PICO_WII_IR=1) - elseif(NOT SWITCH2_BRIDGE_INPUT STREQUAL "JOYCON2" AND NOT SWITCH2_BRIDGE_DUALSENSE_INPUT) - message(FATAL_ERROR "SWITCH2_BRIDGE_INPUT must be JOYCON2, WII or DUALSENSE") + elseif(NOT SWITCH2_BRIDGE_INPUT STREQUAL "JOYCON2" AND NOT SWITCH2_BRIDGE_FULL_INPUT) + message(FATAL_ERROR "SWITCH2_BRIDGE_INPUT must be JOYCON2, WII, DUALSENSE or GAMEPAD") endif() - if(NOT SWITCH2_BRIDGE_DUALSENSE_INPUT AND NOT SWITCH2_BRIDGE_IMU_TARGET STREQUAL "BOTH") + if(NOT SWITCH2_BRIDGE_FULL_INPUT AND NOT SWITCH2_BRIDGE_IMU_TARGET STREQUAL "BOTH") message(FATAL_ERROR "LEFT/RIGHT IMU routing requires a translated full-controller native pair") endif() add_compile_definitions(SWITCH_PICO_SWITCH2_USB_BRIDGE=1) @@ -382,6 +394,10 @@ if(SWITCH_PICO_INPUT_BACKEND STREQUAL "BLUEPAD32") ${CMAKE_CURRENT_LIST_DIR}/bluepad32_config/parser/uni_hid_parser_switch2.c ${CMAKE_CURRENT_LIST_DIR}/bluepad32_config/parser/uni_switch2_pairing.c ${CMAKE_CURRENT_LIST_DIR}/bluepad32_config/parser/uni_switch2_haptics.c) + if(SWITCH2_BRIDGE_FULL_INPUT) + target_sources(bluepad32 PRIVATE + ${CMAKE_CURRENT_LIST_DIR}/bluepad32_config/parser/uni_hid_parser_native_motion.c) + endif() endif() # Firmware sources live under one include root and are grouped by responsibility. @@ -423,16 +439,16 @@ if(SWITCH_PICO_SWITCH2_USB_BRIDGE) endif() target_include_directories(switch-pico PRIVATE ${CMAKE_CURRENT_LIST_DIR}/external) target_sources(switch-pico PRIVATE ${SWITCH2_USB_PROBE_DIR}/native_imu.cpp ${LIBOGC_IR_GENERATED}) - elseif(SWITCH2_BRIDGE_DUALSENSE_INPUT) + elseif(SWITCH2_BRIDGE_FULL_INPUT) target_sources(switch-pico PRIVATE - ${SWITCH2_USB_PROBE_DIR}/dualsense_input.cpp + ${SWITCH2_USB_PROBE_DIR}/native_gamepad_input.cpp ${SWITCH2_USB_PROBE_DIR}/native_imu.cpp) endif() if(NOT SWITCH2_BRIDGE_SOURCE_AUTO) target_compile_definitions(switch-pico PRIVATE SWITCH2_BRIDGE_SOURCE_ADDRESS_BYTES=${SWITCH2_BRIDGE_SOURCE_ADDRESS_BYTES}) endif() - if((SWITCH2_PROBE_COMPOSITE OR SWITCH2_PROBE_HUB) AND NOT SWITCH2_BRIDGE_DUALSENSE_INPUT) + if((SWITCH2_PROBE_COMPOSITE OR SWITCH2_PROBE_HUB) AND NOT SWITCH2_BRIDGE_FULL_INPUT) target_compile_definitions(switch-pico PRIVATE SWITCH2_BRIDGE_SECOND_SOURCE_ADDRESS_BYTES=${SWITCH2_BRIDGE_SECOND_SOURCE_ADDRESS_BYTES}) endif() diff --git a/README.md b/README.md index 438b2d5..ef82373 100644 --- a/README.md +++ b/README.md @@ -879,6 +879,50 @@ the checker below. Its configured shared-source policy permits identical IMU blocks across the halves while retaining per-child identity, report-ID, fresh-counter and control/bulk isolation checks. +**Any supported gamepad (0.70):** `SWITCH2_BRIDGE_INPUT=GAMEPAD` uses the same +native R/L hub and private identity/calibration captures, but accepts the normal +Bluepad32 gamepad families instead of filtering for a DualSense. Use a separate +private hub build with `SWITCH_PICO_BLUETOOTH_MODE=MIXED` to enable both Classic +and BLE controllers, and disable both `SWITCH_PICO_SWITCH2_MOUSE_CAPTURE` and +`SWITCH_PICO_SWITCH2_MOUSE_CAPTURE_NATIVE`. This is one logical controller +feeding one virtual R/L pair, not additional players. An empty source address +requires one uniquely eligible logical controller; multiple eligible sources +fail closed. An explicit address selects that controller (either member of an +existing Switch2 Joy-Con pair). Ordinary AIO pairing, identity, layout and +profile behavior is retained; original Switch Joy-Con grouping is not added. + +- Buttons, sticks and profiles work independently of motion capability. +- Calibrated motion providers cover DS4, DS5/Edge, Switch/Joy-Con-compatible + parsers, Switch2 Pro/Joy-Con, PS Move, and Wii/MotionPlus. Motion requires actual + supported, calibrated and fresh acceleration **and** gyro samples. A pad with + absent/invalid sensors remains usable for controls; no IMU is invented. +- Sensor counters advance at parser ingress, not when polled or when buttons + arrive. A paired left Joy-Con cannot refresh the right-owned sensor stream. + Wii acceleration cannot refresh a stalled MotionPlus gyro stream. +- `SWITCH2_BRIDGE_IMU_TARGET=LEFT|RIGHT|BOTH` also applies to `GAMEPAD`; Wii alone + estimates residual bias while stationary. Factory calibration already runs; + caching residual Wii bias across boots without revalidation is not implemented. +- Native cue requests use each source driver's bounded compatibility vibration. + Mono drivers combine the two logical contributions; paired Switch2 Joy-Cons + target their actual halves. This does not promise stereo, HD-waveform fidelity + or physical actuator onset. Completion means driver dispatch (accepted L2CAP + submission for DS5), not a remote application ACK. Missing rumble capability + fails the request rather than claiming a motor response. +- The dedicated `WII` source remains the IR/native-mouse path. `GAMEPAD` does not + synthesize mouse movement or rail buttons. + +The private `build-switch2-native-gamepad` image uses mixed Bluetooth and the +unchanged stock USB socket. Software regressions cover real parser calibration, +report integrity/freshness, source selection, split/reset/backpressure, Wii-only +settling and cue lifetimes. DualSense, generic, existing Joy-Con/Wii, and ordinary +AIO mixed/BLE/Classic firmware builds pass. The new generic image has not been +flashed or physically qualified across these controller families. + +For sensorless hardware, the checker supports `--input-only`: press real buttons +and keep changing controls on both halves during the run. Neutral fallback +alone cannot qualify. The result explicitly records that IMU was not required; +omit this option to retain the strict dual-IMU check. + With the existing private build configured, qualify on a PC using: ```sh diff --git a/bluepad32_config/parser/uni_hid_parser_imu.h b/bluepad32_config/parser/uni_hid_parser_imu.h index fdbc607..cd05250 100644 --- a/bluepad32_config/parser/uni_hid_parser_imu.h +++ b/bluepad32_config/parser/uni_hid_parser_imu.h @@ -209,6 +209,15 @@ static inline bool uni_psmove_normalize_imu( const int32_t gyro_bias_value = uni_psmove_read_calibration_value( calibration->data, model, gyro_bias[axis]); +#if SWITCH2_BRIDGE_FULL_INPUT + // Complete feature blocks alone do not make erased/degenerate factory + // extrema calibrated. Never label the helper's zero-span fallback IMU. + if (accel_low_value >= accel_high_value || + uni_psmove_read_calibration_value(calibration->data, model, gyro_high[axis]) <= gyro_bias_value || + (model == UNI_PSMOVE_IMU_MODEL_ZCM2 && + uni_psmove_read_calibration_value(calibration->data, model, zcm2_gyro_low[axis]) >= gyro_bias_value)) + return false; +#endif const int32_t gyro_raw = (uni_psmove_decode_value(model, gyro_first[axis]) + uni_psmove_decode_value(model, gyro_second[axis])) / diff --git a/bluepad32_config/parser/uni_hid_parser_native_motion.c b/bluepad32_config/parser/uni_hid_parser_native_motion.c new file mode 100644 index 0000000..50390b1 --- /dev/null +++ b/bluepad32_config/parser/uni_hid_parser_native_motion.c @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: Apache-2.0 +#include "parser/uni_hid_parser_native_motion.h" + +#if SWITCH2_BRIDGE_FULL_INPUT +#include + +#include "parser/uni_hid_parser_wii.h" +#include "sdkconfig.h" +#include "uni_hid_device.h" + +// Parser data is already close to its fixed capacity on some families. Keep +// only native-bridge provenance here, not in every normal AIO parser instance. +typedef struct { + uni_hid_device_t* device; + void (*setup)(uni_hid_device_t*); + uint8_t address[6]; + uni_native_motion_snapshot_t sample; + uint32_t timestamp; + bool have_timestamp; +} native_motion_t; +static native_motion_t providers[CONFIG_BLUEPAD32_MAX_DEVICES]; +static uint32_t sequence; + +uint32_t uni_hid_parser_native_motion_next_sequence(void) { + if (++sequence == 0) + ++sequence; + return sequence; +} + +static native_motion_t* provider(uni_hid_device_t* d) { + if (!d) + return NULL; + for (unsigned i = 0; i < CONFIG_BLUEPAD32_MAX_DEVICES; ++i) { + native_motion_t* p = &providers[i]; + if (p->device == d && p->setup == d->report_parser.setup && + memcmp(p->address, d->conn.btaddr, sizeof(p->address)) == 0) + return p; + } + return NULL; +} + +void uni_hid_parser_native_motion_forget(uni_hid_device_t* d) { + for (unsigned i = 0; i < CONFIG_BLUEPAD32_MAX_DEVICES; ++i) + if (providers[i].device == d) + memset(&providers[i], 0, sizeof(providers[i])); +} + +void uni_hid_parser_native_motion_reset(uni_hid_device_t* d) { + if (!d) + return; + uni_hid_parser_native_motion_forget(d); + for (unsigned i = 0; i < CONFIG_BLUEPAD32_MAX_DEVICES; ++i) { + native_motion_t* p = &providers[i]; + if (p->device) + continue; + p->device = d; + p->setup = d->report_parser.setup; + memcpy(p->address, d->conn.btaddr, sizeof(p->address)); + p->sample.report_tracked = true; + return; + } +} + +void uni_hid_parser_native_motion_begin(uni_hid_device_t* d) { + native_motion_t* p = provider(d); + if (p) + p->sample.report_valid = false; +} + +void uni_hid_parser_native_motion_accept(uni_hid_device_t* d) { + native_motion_t* p = provider(d); + if (p) { + p->sample.report_valid = true; + p->sample.report_sequence = uni_hid_parser_native_motion_next_sequence(); + } +} + +void uni_hid_parser_native_motion_accel(uni_hid_device_t* d, const int32_t* value) { + native_motion_t* p = provider(d); + if (!p) + return; + p->sample.accel_valid = value != NULL; + if (value) { + memcpy(p->sample.accel_q13, value, sizeof(p->sample.accel_q13)); + p->sample.accel_sequence = uni_hid_parser_native_motion_next_sequence(); + } +} + +void uni_hid_parser_native_motion_gyro(uni_hid_device_t* d, const int32_t* value) { + native_motion_t* p = provider(d); + if (!p) + return; + p->sample.gyro_valid = value != NULL; + if (value) { + memcpy(p->sample.gyro_q10, value, sizeof(p->sample.gyro_q10)); + p->sample.gyro_sequence = uni_hid_parser_native_motion_next_sequence(); + } +} + +bool uni_hid_parser_native_motion_fresh(uni_hid_device_t* d, uint32_t timestamp, uint32_t mask) { + native_motion_t* p = provider(d); + if (!p) + return false; + uint32_t delta = (timestamp - p->timestamp) & mask; + if (p->have_timestamp && (delta == 0 || delta > (mask >> 1))) + return false; + p->timestamp = timestamp; + p->have_timestamp = true; + return true; +} + +bool uni_hid_parser_native_motion_snapshot(uni_hid_device_t* d, uni_native_motion_snapshot_t* out) { + if (!out) + return false; + memset(out, 0, sizeof(*out)); + if (!d) + return false; + // Wii already owns independent, topology-aware calibration and sample IDs. + // ACK/status/extension packets must not refresh the Remote or MotionPlus. + if (d->report_parser.setup == uni_hid_parser_wii_setup) { + out->accel_valid = uni_hid_parser_wii_accel_snapshot(d, out->accel_q13, &out->accel_sequence); + out->gyro_valid = uni_hid_parser_wii_gyro_snapshot(d, out->gyro_q10, &out->gyro_sequence); + return true; + } + native_motion_t* p = provider(d); + if (!p) + return false; + *out = p->sample; + return true; +} +#endif diff --git a/bluepad32_config/parser/uni_hid_parser_native_motion.h b/bluepad32_config/parser/uni_hid_parser_native_motion.h new file mode 100644 index 0000000..3388a0e --- /dev/null +++ b/bluepad32_config/parser/uni_hid_parser_native_motion.h @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +struct uni_hid_device_s; +typedef struct { + bool report_tracked; + bool report_valid; + uint32_t report_sequence; + bool accel_valid; + bool gyro_valid; + uint32_t accel_sequence; + uint32_t gyro_sequence; + int32_t accel_q13[3]; + int32_t gyro_q10[3]; +} uni_native_motion_snapshot_t; + +#if SWITCH2_BRIDGE_FULL_INPUT +// Bluetooth owner only. Reads never advance sequences or rejuvenate samples. +// False/zero means no motion provider, not an unsupported controls device. +bool uni_hid_parser_native_motion_snapshot(struct uni_hid_device_s* d, + uni_native_motion_snapshot_t* out); + +// Parser ingress/lifecycle hooks. Only setup allocates a bounded slot; late +// reports cannot resurrect a retired provider. NULL sensor data invalidates +// that sensor without changing its last sequence. Samples use SDL Q13/Q10 axes. +void uni_hid_parser_native_motion_reset(struct uni_hid_device_s* d); +void uni_hid_parser_native_motion_forget(struct uni_hid_device_s* d); +void uni_hid_parser_native_motion_begin(struct uni_hid_device_s* d); +void uni_hid_parser_native_motion_accept(struct uni_hid_device_s* d); +void uni_hid_parser_native_motion_accel(struct uni_hid_device_s* d, const int32_t* value); +void uni_hid_parser_native_motion_gyro(struct uni_hid_device_s* d, const int32_t* value); +// Test a complete report's wrapping hardware clock, before publishing sensors. +// mask is UINT8_MAX/UINT16_MAX/UINT32_MAX; backwards/duplicate ticks are rejected. +bool uni_hid_parser_native_motion_fresh(struct uni_hid_device_s* d, uint32_t timestamp, uint32_t mask); +// Parser ingress only: process-wide nonzero IDs do not alias across reconnects. +uint32_t uni_hid_parser_native_motion_next_sequence(void); +#endif + +#ifdef __cplusplus +} +#endif diff --git a/bluepad32_config/parser/uni_hid_parser_switch2.c b/bluepad32_config/parser/uni_hid_parser_switch2.c index 24fb903..0a61ab4 100644 --- a/bluepad32_config/parser/uni_hid_parser_switch2.c +++ b/bluepad32_config/parser/uni_hid_parser_switch2.c @@ -6,6 +6,9 @@ // Sensor conversion independently adapted from SDL_hidapi_switch2.c (SDL/Valve). #include "parser/uni_hid_parser_switch2.h" +#if SWITCH2_BRIDGE_FULL_INPUT +#include "parser/uni_hid_parser_native_motion.h" +#endif #include #include @@ -125,6 +128,9 @@ typedef struct { uint8_t memory_length; sw2_stick_t sticks[2]; int32_t gyro_bias[3]; +#if SWITCH2_BRIDGE_FULL_INPUT + bool gyro_calibrated; +#endif uint8_t extra_buttons, leds; bool leds_pending; uint8_t rumble_id, weak, strong; @@ -216,6 +222,9 @@ static void sw2_disarm_timeout(sw2_instance_t* ins) { } void uni_hid_parser_switch2_teardown(uni_hid_device_t* d) { +#if SWITCH2_BRIDGE_FULL_INPUT + uni_hid_parser_native_motion_forget(d); +#endif sw2_instance_t* ins = sw2_instance(d); if (!ins) return; @@ -643,13 +652,21 @@ static void sw2_response(sw2_instance_t* ins, const uint8_t* data, uint16_t leng ins->calibration_done = true; } } else if (ins->state == SW2_GYRO_CALIBRATION) { +#if SWITCH2_BRIDGE_FULL_INPUT + ins->gyro_calibrated = true; +#endif for (unsigned i = 0; i < 3; ++i) { uint32_t bits = little_endian_read_32(value, 4 * i); float bias; memcpy(&bias, &bits, sizeof(bias)); // Erased/invalid flash is not a floating-point sensor value. - if (isfinite(bias) && bias >= -40.0f && bias <= 40.0f) + bool valid = isfinite(bias) && bias >= -40.0f && bias <= 40.0f; + if (valid) ins->gyro_bias[i] = (int32_t)(bias * (57.295779513f * UNI_IMU_GYRO_RES_PER_DEG_S)); +#if SWITCH2_BRIDGE_FULL_INPUT + if (!valid) + ins->gyro_calibrated = false; +#endif } } } else if (ins->state == SW2_PAIR) { @@ -1029,6 +1046,9 @@ void uni_hid_parser_switch2_setup(uni_hid_device_t* d) { if (!ins || ins->state != SW2_ADMITTED) return; ins->handle = d->conn.handle; +#if SWITCH2_BRIDGE_FULL_INPUT + uni_hid_parser_native_motion_reset(d); +#endif // Standard-compliant 7.5ms minimum; negotiation failure is not setup failure. int status = gap_update_connection_parameters(ins->handle, 6, 6, 0, 600); if (status != ERROR_CODE_SUCCESS) @@ -1053,6 +1073,9 @@ bool uni_hid_parser_switch2_identity_address_type(const uni_hid_device_t* d, uin } void uni_hid_parser_switch2_init_report(uni_hid_device_t* d) { +#if SWITCH2_BRIDGE_FULL_INPUT + uni_hid_parser_native_motion_begin(d); +#endif (void)d; // Full snapshots replace state only after their complete length is validated. } @@ -1066,6 +1089,16 @@ static int32_t sw2_axis(uint16_t raw, const sw2_stick_t* stick, unsigned axis, b static void sw2_motion(sw2_instance_t* ins, uni_gamepad_t* gp, const uint8_t* report) { uint32_t timestamp = little_endian_read_32(report, 42); +#if SWITCH2_BRIDGE_FULL_INPUT + if (!timestamp || !uni_hid_parser_native_motion_fresh(ins->device, timestamp, UINT32_MAX)) + return; + // Acceleration has a known fixed range and does not need gyro clock + // detection or factory gyro bias. Keep its provenance independent. + gp->accel[0] = uni_imu_scale((int16_t)little_endian_read_16(report, 48), 32767, 8 * UNI_IMU_ACCEL_RES_PER_G); + gp->accel[1] = uni_imu_scale((int16_t)little_endian_read_16(report, 52), 32767, 8 * UNI_IMU_ACCEL_RES_PER_G); + gp->accel[2] = -uni_imu_scale((int16_t)little_endian_read_16(report, 50), 32767, 8 * UNI_IMU_ACCEL_RES_PER_G); + uni_hid_parser_native_motion_accel(ins->device, gp->accel); +#endif if (!timestamp || (timestamp == ins->sensor_last && !ins->gyro_full_scale)) return; ins->sensor_last = timestamp; @@ -1099,18 +1132,30 @@ static void sw2_motion(sw2_instance_t* ins, uni_gamepad_t* gp, const uint8_t* re static const uint8_t axes[3] = {0, 2, 1}; for (unsigned i = 0; i < 3; ++i) { unsigned axis = axes[i]; +#if !SWITCH2_BRIDGE_FULL_INPUT int32_t accel = (int16_t)little_endian_read_16(report, 48 + 2 * axis); +#endif int32_t gyro = (int16_t)little_endian_read_16(report, 54 + 2 * axis); +#if !SWITCH2_BRIDGE_FULL_INPUT gp->accel[i] = uni_imu_scale(accel, 32767, 8 * UNI_IMU_ACCEL_RES_PER_G); +#endif gp->gyro[i] = uni_imu_scale(gyro, 32767, ins->gyro_full_scale) - ins->gyro_bias[axis]; if (i == 2) { +#if !SWITCH2_BRIDGE_FULL_INPUT gp->accel[i] = -gp->accel[i]; +#endif gp->gyro[i] = -gp->gyro[i]; } } +#if SWITCH2_BRIDGE_FULL_INPUT + uni_hid_parser_native_motion_gyro(ins->device, ins->gyro_calibrated ? gp->gyro : NULL); +#endif } void uni_hid_parser_switch2_parse_input_report(uni_hid_device_t* d, const uint8_t* report, uint16_t len) { +#if SWITCH2_BRIDGE_FULL_INPUT + uni_hid_parser_native_motion_begin(d); +#endif sw2_instance_t* ins = sw2_instance(d); if (!ins || ins->state != SW2_READY || !report || len != SW2_REPORT_SIZE) return; @@ -1148,6 +1193,9 @@ void uni_hid_parser_switch2_parse_input_report(uni_hid_device_t* d, const uint8_ } sw2_motion(ins, gp, report); d->controller.klass = UNI_CONTROLLER_CLASS_GAMEPAD; +#if SWITCH2_BRIDGE_FULL_INPUT + uni_hid_parser_native_motion_accept(d); +#endif } void uni_hid_parser_switch2_set_player_leds(uni_hid_device_t* d, uint8_t leds) { diff --git a/patches/bluepad32-sdl3-imu.patch b/patches/bluepad32-sdl3-imu.patch index 1df1b7f..8bfbb46 100644 --- a/patches/bluepad32-sdl3-imu.patch +++ b/patches/bluepad32-sdl3-imu.patch @@ -479,7 +479,73 @@ diff --git a/src/components/bluepad32/include/uni_config.h b/src/components/blue diff --git a/src/components/bluepad32/parser/uni_hid_parser_ds4.c b/src/components/bluepad32/parser/uni_hid_parser_ds4.c --- a/src/components/bluepad32/parser/uni_hid_parser_ds4.c +++ b/src/components/bluepad32/parser/uni_hid_parser_ds4.c -@@ -297,17 +297,17 @@ +@@ -10,6 +10,9 @@ + // https://github.com/chrippa/ds4drv/blob/master/ds4drv/device.py + + #include "parser/uni_hid_parser_ds4.h" ++#if SWITCH2_BRIDGE_FULL_INPUT ++#include "parser/uni_hid_parser_native_motion.h" ++#endif + + #include + +@@ -69,6 +72,9 @@ + + struct ds4_calibration_data gyro_calib_data[3]; + struct ds4_calibration_data accel_calib_data[3]; ++#if SWITCH2_BRIDGE_FULL_INPUT ++ bool accel_calibrated, gyro_calibrated; ++#endif + + // Prev Touchpad values, to convert them from absolute + // coordinates into relative ones. +@@ -220,6 +226,9 @@ + void uni_hid_parser_ds4_setup(struct uni_hid_device_s* d) { + ds4_instance_t* ins = get_ds4_instance(d); + memset(ins, 0, sizeof(*ins)); ++#if SWITCH2_BRIDGE_FULL_INPUT ++ uni_hid_parser_native_motion_reset(d); ++#endif + + // Default values for Accel / Gyro calibration data, until calibration is supported. + for (size_t i = 0; i < ARRAY_SIZE(ins->accel_calib_data); i++) { +@@ -263,6 +272,9 @@ + } + + void uni_hid_parser_ds4_init_report(uni_hid_device_t* d) { ++#if SWITCH2_BRIDGE_FULL_INPUT ++ uni_hid_parser_native_motion_begin(d); ++#endif + uni_controller_t* ctl = &d->controller; + memset(ctl, 0, sizeof(*ctl)); + +@@ -279,6 +291,10 @@ + + void uni_hid_parser_ds4_parse_feature_report(uni_hid_device_t* d, const uint8_t* report, uint16_t len) { + ds4_instance_t* ins = get_ds4_instance(d); ++#if SWITCH2_BRIDGE_FULL_INPUT ++ if (!report || !len) ++ return; ++#endif + uint8_t report_id = report[0]; + + switch (report_id) { +@@ -286,6 +302,14 @@ + int speed_2x; + int range_2g; + ++#if SWITCH2_BRIDGE_FULL_INPUT ++ if (len != DS4_FEATURE_REPORT_CALIBRATION_SIZE) ++ break; ++ ins->accel_calibrated = true; ++ ins->gyro_calibrated = true; ++ uni_hid_parser_native_motion_accel(d, NULL); ++ uni_hid_parser_native_motion_gyro(d, NULL); ++#endif + if (len != DS4_FEATURE_REPORT_CALIBRATION_SIZE) { + loge("DS4: Unexpected calibration size: got %d, want: %d\n", len, DS4_FEATURE_REPORT_CALIBRATION_SIZE); + /* fallthrough */ +@@ -297,25 +321,34 @@ // Set gyroscope calibration and normalization parameters. // Data values will be normalized to 1/DS_GYRO_RES_PER_DEG_S degree/s. speed_2x = r->gyro_speed_plus + r->gyro_speed_minus; @@ -500,7 +566,46 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_ds4.c b/src/componen ins->gyro_calib_data[2].sens_numer = speed_2x * DS4_GYRO_RES_PER_DEG_S; ins->gyro_calib_data[2].sens_denom = abs(r->gyro_roll_plus - r->gyro_roll_bias) + abs(r->gyro_roll_minus - r->gyro_roll_bias); -@@ -476,7 +476,7 @@ + ++#if SWITCH2_BRIDGE_FULL_INPUT ++ ins->gyro_calib_data[0].sens_denom = ++ abs(r->gyro_pitch_plus - r->gyro_pitch_bias) + abs(r->gyro_pitch_minus - r->gyro_pitch_bias); ++ ins->gyro_calibrated = speed_2x > 0; ++#endif + // Sanity check gyro calibration data. This is needed to prevent crashes + // during report handling of virtual, clone or broken devices not implementing + // calibration data properly. + for (size_t i = 0; i < ARRAY_SIZE(ins->gyro_calib_data); i++) { ++#if SWITCH2_BRIDGE_FULL_INPUT ++ if (ins->gyro_calib_data[i].sens_denom <= 0) ++ ins->gyro_calibrated = false; ++#endif + if (ins->gyro_calib_data[i].sens_denom == 0) { + loge("Invalid gyro calibration data for axis (%d), disabling calibration for axis = %d\n", i); + ins->gyro_calib_data[i].bias = 0; +@@ -345,6 +378,10 @@ + // during report handling of virtual, clone or broken devices not implementing calibration + // data properly. + for (size_t i = 0; i < ARRAY_SIZE(ins->accel_calib_data); i++) { ++#if SWITCH2_BRIDGE_FULL_INPUT ++ if (ins->accel_calib_data[i].sens_denom <= 0) ++ ins->accel_calibrated = false; ++#endif + if (ins->accel_calib_data[i].sens_denom == 0) { + loge("Invalid accelerometer calibration data for axis (%d), disabling calibration for axis=%d\n", + i); +@@ -357,6 +394,10 @@ + break; + } + case DS4_FEATURE_REPORT_FIRMWARE_VERSION: { ++#if SWITCH2_BRIDGE_FULL_INPUT ++ if (len != DS4_FEATURE_REPORT_FIRMWARE_VERSION_SIZE) ++ break; ++#endif + if (len != DS4_FEATURE_REPORT_FIRMWARE_VERSION_SIZE) { + loge("DS4: Unexpected firmware version size: got %d, want: %d\n", len, + DS4_FEATURE_REPORT_FIRMWARE_VERSION_SIZE); +@@ -476,7 +517,7 @@ // Gyro for (size_t i = 0; i < ARRAY_SIZE(r->gyro); i++) { @@ -509,7 +614,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_ds4.c b/src/componen int32_t calib_data = mult_frac(ins->gyro_calib_data[i].sens_numer, raw_data, ins->gyro_calib_data[i].sens_denom); ctl->gamepad.gyro[i] = calib_data; -@@ -484,7 +484,7 @@ +@@ -484,7 +525,7 @@ // Accel for (size_t i = 0; i < ARRAY_SIZE(r->accel); i++) { @@ -518,6 +623,42 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_ds4.c b/src/componen 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; +@@ -500,12 +541,35 @@ + } + + void uni_hid_parser_ds4_parse_input_report(uni_hid_device_t* d, const uint8_t* report, uint16_t len) { ++#if SWITCH2_BRIDGE_FULL_INPUT ++ uni_hid_parser_native_motion_begin(d); ++ if (!report || !len) ++ return; ++#endif + if (report[0] == 0x11 && len == 78) { ++#if SWITCH2_BRIDGE_FULL_INPUT ++ const uint8_t transaction = 0xa1; ++ uint32_t crc = uni_crc32_le(UINT32_MAX, &transaction, 1); ++ crc = ~uni_crc32_le(crc, report, len - 4); ++ if (crc != little_endian_read_32(report, len - 4)) ++ return; ++#endif + const ds4_input_report_11_t* r = (ds4_input_report_11_t*)&report[3]; + ds4_parse_input_report_11(d, r); ++#if SWITCH2_BRIDGE_FULL_INPUT ++ ds4_instance_t* ins = get_ds4_instance(d); ++ uni_hid_parser_native_motion_accept(d); ++ if (uni_hid_parser_native_motion_fresh(d, r->sensor_timestamp, UINT16_MAX)) { ++ uni_hid_parser_native_motion_accel(d, ins->accel_calibrated ? d->controller.gamepad.accel : NULL); ++ uni_hid_parser_native_motion_gyro(d, ins->gyro_calibrated ? d->controller.gamepad.gyro : NULL); ++ } ++#endif + } else if (report[0] == 0x01 && len == 10) { + const ds4_input_report_01_t* r = (ds4_input_report_01_t*)&report[1]; + ds4_parse_input_report_01(d, r); ++#if SWITCH2_BRIDGE_FULL_INPUT ++ uni_hid_parser_native_motion_accept(d); ++#endif + } else { + loge("DS4: Unexpected report type and len: report id=0x%02x, len=%d\n", report[0], len); + } diff --git a/src/components/bluepad32/include/parser/uni_hid_parser_ds5.h b/src/components/bluepad32/include/parser/uni_hid_parser_ds5.h --- a/src/components/bluepad32/include/parser/uni_hid_parser_ds5.h +++ b/src/components/bluepad32/include/parser/uni_hid_parser_ds5.h @@ -529,21 +670,12 @@ diff --git a/src/components/bluepad32/include/parser/uni_hid_parser_ds5.h b/src/ #include "parser/uni_hid_parser.h" -@@ -33,6 +34,25 @@ +@@ -33,6 +34,16 @@ uint8_t weak_magnitude, uint8_t strong_magnitude); void uni_hid_parser_ds5_device_dump(struct uni_hid_device_s* d); + -+#if SWITCH2_BRIDGE_DUALSENSE_INPUT -+typedef struct { -+ uint32_t report_sequence; -+ uint32_t motion_sequence; -+ bool motion_valid; -+} uni_ds5_bridge_snapshot_t; -+// Only the Bluetooth owner may call this. False means no complete CRC-checked -+// input report; motion_valid additionally requires factory calibration and a -+// forward-moving sensor timestamp. Counters never advance on accessor reads. -+bool uni_hid_parser_ds5_bridge_snapshot(struct uni_hid_device_s* d, uni_ds5_bridge_snapshot_t* out); ++#if SWITCH2_BRIDGE_FULL_INPUT +// Bounded native bridge writer: false means not submitted (never queued). +// The existing duration timer retries OFF for at most 2 seconds, then closes +// a stuck connection without deleting its bond. No application ACK exists. @@ -558,26 +690,32 @@ diff --git a/src/components/bluepad32/include/parser/uni_hid_parser_ds5.h b/src/ diff --git a/src/components/bluepad32/parser/uni_hid_parser_ds5.c b/src/components/bluepad32/parser/uni_hid_parser_ds5.c --- a/src/components/bluepad32/parser/uni_hid_parser_ds5.c +++ b/src/components/bluepad32/parser/uni_hid_parser_ds5.c -@@ -106,6 +106,14 @@ +@@ -9,6 +9,9 @@ + // https://controllers.fandom.com/wiki/Sony_DualSense + + #include "parser/uni_hid_parser_ds5.h" ++#if SWITCH2_BRIDGE_FULL_INPUT ++#include "parser/uni_hid_parser_native_motion.h" ++#endif + + #include + +@@ -106,6 +109,10 @@ uint32_t fw_version; uint16_t update_version; bool use_vibration2; -+#if SWITCH2_BRIDGE_DUALSENSE_INPUT -+ uni_ds5_bridge_snapshot_t bridge; -+ uint32_t sensor_timestamp; -+ bool have_sensor_timestamp; ++#if SWITCH2_BRIDGE_FULL_INPUT + bool calibration_valid; -+ bool input_valid; + uint32_t bridge_stop_deadline_ms; +#endif struct ds5_calibration_data gyro_calib_data[3]; struct ds5_calibration_data accel_calib_data[3]; -@@ -228,7 +236,29 @@ +@@ -228,7 +235,18 @@ _Static_assert(sizeof(ds5_feature_report_calibration_t) == DS5_FEATURE_REPORT_CALIBRATION_SIZE, "Invalid size"); static ds5_instance_t* get_ds5_instance(uni_hid_device_t* d); -+#if SWITCH2_BRIDGE_DUALSENSE_INPUT ++#if SWITCH2_BRIDGE_FULL_INPUT +// Bluetooth HID CRC includes its transaction byte, omitted by the parser API. +static bool ds5_bridge_crc_valid(const uint8_t* report, uint16_t len, uint8_t transaction) { + uint32_t crc = uni_crc32_le(0xffffffff, &transaction, 1); @@ -586,38 +724,37 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_ds5.c b/src/componen + return crc == ((uint32_t)expected[0] | ((uint32_t)expected[1] << 8) | + ((uint32_t)expected[2] << 16) | ((uint32_t)expected[3] << 24)); +} -+ -+bool uni_hid_parser_ds5_bridge_snapshot(uni_hid_device_t* d, uni_ds5_bridge_snapshot_t* out) { -+ if (d == NULL || out == NULL || d->controller_type != CONTROLLER_TYPE_PS5Controller || -+ d->report_parser.setup != uni_hid_parser_ds5_setup) -+ return false; -+ const ds5_instance_t* ins = get_ds5_instance(d); -+ if (ins->state != DS5_STATE_READY || !ins->input_valid) -+ return false; -+ *out = ins->bridge; -+ return true; -+} +#endif static void ds5_send_output_report(uni_hid_device_t* d, ds5_output_report_t* out); +static void ds5_prepare_output_report(uni_hid_device_t* d, ds5_output_report_t* out); static void ds5_send_enable_lightbar_report(uni_hid_device_t* d); static void ds5_request_pairing_info_report(uni_hid_device_t* d); static void ds5_request_firmware_version_report(uni_hid_device_t* d); -@@ -390,6 +420,9 @@ +@@ -390,6 +408,9 @@ void uni_hid_parser_ds5_init_report(uni_hid_device_t* d) { uni_controller_t* ctl = &d->controller; -+#if SWITCH2_BRIDGE_DUALSENSE_INPUT -+ get_ds5_instance(d)->input_valid = false; ++#if SWITCH2_BRIDGE_FULL_INPUT ++ uni_hid_parser_native_motion_begin(d); +#endif memset(ctl, 0, sizeof(*ctl)); ctl->klass = UNI_CONTROLLER_CLASS_GAMEPAD; -@@ -423,10 +456,19 @@ +@@ -406,6 +427,9 @@ + void uni_hid_parser_ds5_setup(uni_hid_device_t* d) { + ds5_instance_t* ins = get_ds5_instance(d); + memset(ins, 0, sizeof(*ins)); ++#if SWITCH2_BRIDGE_FULL_INPUT ++ uni_hid_parser_native_motion_reset(d); ++#endif + + // Default values for Accel / Gyro calibration data, until calibration is supported. + for (size_t i = 0; i < ARRAY_SIZE(ins->accel_calib_data); i++) { +@@ -423,10 +447,19 @@ void uni_hid_parser_ds5_parse_feature_report(uni_hid_device_t* d, const uint8_t* report, uint16_t len) { ds5_instance_t* ins = get_ds5_instance(d); -+#if SWITCH2_BRIDGE_DUALSENSE_INPUT ++#if SWITCH2_BRIDGE_FULL_INPUT + if (report == NULL || len == 0) + return; +#endif @@ -625,7 +762,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_ds5.c b/src/componen switch (report_id) { case DS5_FEATURE_REPORT_PAIRING_INFO: -+#if SWITCH2_BRIDGE_DUALSENSE_INPUT ++#if SWITCH2_BRIDGE_FULL_INPUT + if (len != DS5_FEATURE_REPORT_PAIRING_INFO_SIZE || + !ds5_bridge_crc_valid(report, len, 0xa3)) + break; @@ -633,11 +770,11 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_ds5.c b/src/componen if (len != DS5_FEATURE_REPORT_PAIRING_INFO_SIZE) { loge("DS5: Unexpected pairing info size: got %d, want: %d\n", len, DS5_FEATURE_REPORT_PAIRING_INFO_SIZE); -@@ -444,6 +486,11 @@ +@@ -444,6 +477,11 @@ break; case DS5_FEATURE_REPORT_FIRMWARE_VERSION: { -+#if SWITCH2_BRIDGE_DUALSENSE_INPUT ++#if SWITCH2_BRIDGE_FULL_INPUT + if (len != DS5_FEATURE_REPORT_FIRMWARE_VERSION_SIZE || + !ds5_bridge_crc_valid(report, len, 0xa3)) + break; @@ -645,11 +782,11 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_ds5.c b/src/componen if (len != DS5_FEATURE_REPORT_FIRMWARE_VERSION_SIZE) { loge("DS5: Unexpected firmware version size: got %d, want: %d\n", len, DS5_FEATURE_REPORT_FIRMWARE_VERSION_SIZE); -@@ -476,6 +523,11 @@ +@@ -476,6 +514,11 @@ int speed_2x; int range_2g; -+#if SWITCH2_BRIDGE_DUALSENSE_INPUT ++#if SWITCH2_BRIDGE_FULL_INPUT + if (len != DS5_FEATURE_REPORT_CALIBRATION_SIZE || + !ds5_bridge_crc_valid(report, len, 0xa3)) + break; @@ -657,14 +794,15 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_ds5.c b/src/componen if (len != DS5_FEATURE_REPORT_CALIBRATION_SIZE) { loge("DS5: Unexpected calibration size: got %d, want: %d\n", len, DS5_FEATURE_REPORT_CALIBRATION_SIZE); /* fallthrough */ -@@ -487,25 +539,38 @@ +@@ -487,25 +530,39 @@ // Set gyroscope calibration and normalization parameters. // Data values will be normalized to 1/DS_GYRO_RES_PER_DEG_S degree/s. speed_2x = r->gyro_speed_plus + r->gyro_speed_minus; - ins->gyro_calib_data[0].bias = 0; -+#if SWITCH2_BRIDGE_DUALSENSE_INPUT ++#if SWITCH2_BRIDGE_FULL_INPUT + ins->calibration_valid = speed_2x > 0; -+ ins->bridge.motion_valid = false; ++ uni_hid_parser_native_motion_accel(d, NULL); ++ uni_hid_parser_native_motion_gyro(d, NULL); +#endif + ins->gyro_calib_data[0].bias = r->gyro_pitch_bias; ins->gyro_calib_data[0].sens_numer = speed_2x * DS5_GYRO_RES_PER_DEG_S; @@ -683,7 +821,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_ds5.c b/src/componen ins->gyro_calib_data[2].sens_denom = abs(r->gyro_roll_plus - r->gyro_roll_bias) + abs(r->gyro_roll_minus - r->gyro_roll_bias); -+#if SWITCH2_BRIDGE_DUALSENSE_INPUT ++#if SWITCH2_BRIDGE_FULL_INPUT + // Both extrema are measured relative to the same factory bias. + ins->gyro_calib_data[0].sens_denom = + abs(r->gyro_pitch_plus - r->gyro_pitch_bias) + abs(r->gyro_pitch_minus - r->gyro_pitch_bias); @@ -692,37 +830,37 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_ds5.c b/src/componen // during report handling of virtual, clone or broken devices not implementing // calibration data properly. for (size_t i = 0; i < ARRAY_SIZE(ins->gyro_calib_data); i++) { -+#if SWITCH2_BRIDGE_DUALSENSE_INPUT ++#if SWITCH2_BRIDGE_FULL_INPUT + if (ins->gyro_calib_data[i].sens_denom <= 0) + ins->calibration_valid = false; +#endif if (ins->gyro_calib_data[i].sens_denom == 0) { loge("Invalid gyro calibration data for axis (%d), disabling calibration for axis = %d\n", i); ins->gyro_calib_data[i].bias = 0; -@@ -535,6 +600,10 @@ +@@ -535,6 +592,10 @@ // during report handling of virtual, clone or broken devices not implementing calibration // data properly. for (size_t i = 0; i < ARRAY_SIZE(ins->accel_calib_data); i++) { -+#if SWITCH2_BRIDGE_DUALSENSE_INPUT ++#if SWITCH2_BRIDGE_FULL_INPUT + if (ins->accel_calib_data[i].sens_denom <= 0) + ins->calibration_valid = false; +#endif if (ins->accel_calib_data[i].sens_denom == 0) { loge("Invalid accelerometer calibration data for axis (%d), disabling calibration for axis=%d\n", i); -@@ -557,6 +626,11 @@ +@@ -557,6 +618,11 @@ void uni_hid_parser_ds5_parse_input_report(uni_hid_device_t* d, const uint8_t* report, uint16_t len) { ds5_instance_t* ins = get_ds5_instance(d); -+#if SWITCH2_BRIDGE_DUALSENSE_INPUT -+ ins->input_valid = false; ++#if SWITCH2_BRIDGE_FULL_INPUT ++ uni_hid_parser_native_motion_begin(d); + if (report == NULL || len != 78 || !ds5_bridge_crc_valid(report, len, 0xa1)) + return; +#endif // Don't process reports until state is ready. Prevents possible div-by-0 on calibration // and ignores other warnings. if (ins->state != DS5_STATE_READY) -@@ -617,12 +691,12 @@ +@@ -617,12 +683,12 @@ ctl->gamepad.buttons |= BUTTON_THUMB_R; // Thumb R if (r->buttons[2] & 0x01) ctl->gamepad.misc_buttons |= MISC_BUTTON_SYSTEM; // PS @@ -738,7 +876,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_ds5.c b/src/componen int32_t calib_data = mult_frac(ins->gyro_calib_data[i].sens_numer, raw_data, ins->gyro_calib_data[i].sens_denom); ctl->gamepad.gyro[i] = calib_data; -@@ -630,12 +704,29 @@ +@@ -630,12 +696,21 @@ // Accel for (size_t i = 0; i < ARRAY_SIZE(r->accel); i++) { @@ -749,31 +887,23 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_ds5.c b/src/componen ctl->gamepad.accel[i] = calib_data; } -+#if SWITCH2_BRIDGE_DUALSENSE_INPUT -+ ins->input_valid = true; -+ if (++ins->bridge.report_sequence == 0) -+ ++ins->bridge.report_sequence; -+ const uint32_t timestamp = r->sensor_timestamp; -+ const uint32_t delta = timestamp - ins->sensor_timestamp; -+ ins->bridge.motion_valid = false; -+ if (!ins->have_sensor_timestamp || (delta != 0 && delta < 0x80000000u)) { -+ ins->sensor_timestamp = timestamp; -+ ins->have_sensor_timestamp = true; -+ if (ins->calibration_valid) { -+ if (++ins->bridge.motion_sequence == 0) -+ ++ins->bridge.motion_sequence; -+ ins->bridge.motion_valid = true; -+ } ++#if SWITCH2_BRIDGE_FULL_INPUT ++ uni_hid_parser_native_motion_accept(d); ++ uni_hid_parser_native_motion_accel(d, NULL); ++ uni_hid_parser_native_motion_gyro(d, NULL); ++ if (uni_hid_parser_native_motion_fresh(d, r->sensor_timestamp, UINT32_MAX) && ins->calibration_valid) { ++ uni_hid_parser_native_motion_accel(d, ctl->gamepad.accel); ++ uni_hid_parser_native_motion_gyro(d, ctl->gamepad.gyro); + } +#endif // Value goes from 0 to 10. Make it from 0 to 250. // The +1 is to avoid having a value of 0, which means "battery unavailable". ctl->battery = (r->status & DS5_STATUS_BATTERY_CAPACITY) * 25 + 1; -@@ -721,6 +812,69 @@ +@@ -721,6 +796,68 @@ } } -+#if SWITCH2_BRIDGE_DUALSENSE_INPUT ++#if SWITCH2_BRIDGE_FULL_INPUT +static void on_ds5_bridge_rumble_off(btstack_timer_source_t* timer) { + uni_hid_device_t* d = timer->context; + ds5_instance_t* ins = get_ds5_instance(d); @@ -831,15 +961,14 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_ds5.c b/src/componen + btstack_run_loop_remove_timer(&ins->rumble_timer_delayed_start); + btstack_run_loop_remove_timer(&ins->rumble_timer_duration); + ins->rumble_state = DS5_STATE_RUMBLE_DISABLED; -+ ins->input_valid = false; -+ ins->bridge.motion_valid = false; ++ uni_hid_parser_native_motion_forget(d); +} +#endif + void uni_hid_parser_ds5_device_dump(uni_hid_device_t* d) { ds5_instance_t* ins = get_ds5_instance(d); logi("\tDS5: FW version: %#x, HW version: %#x, update version: %#x, use vibration2: %d\n", ins->fw_version, -@@ -734,7 +888,7 @@ +@@ -734,7 +871,7 @@ return (ds5_instance_t*)&d->parser_data[0]; } @@ -848,7 +977,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_ds5.c b/src/componen ds5_instance_t* ins = get_ds5_instance(d); out->transaction_type = (HID_MESSAGE_TYPE_DATA << 4) | HID_REPORT_TYPE_OUTPUT; -@@ -748,7 +902,10 @@ +@@ -748,7 +885,10 @@ ins->output_seq = 0; out->crc32 = ~uni_crc32_le(0xffffffff, (uint8_t*)out, sizeof(*out) - 4); @@ -863,15 +992,18 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_ds5.c b/src/componen diff --git a/src/components/bluepad32/parser/uni_hid_parser_psmove.c b/src/components/bluepad32/parser/uni_hid_parser_psmove.c --- a/src/components/bluepad32/parser/uni_hid_parser_psmove.c +++ b/src/components/bluepad32/parser/uni_hid_parser_psmove.c -@@ -8,6 +8,7 @@ +@@ -8,6 +8,10 @@ */ #include "parser/uni_hid_parser_psmove.h" +#include "parser/uni_hid_parser_imu.h" ++#if SWITCH2_BRIDGE_FULL_INPUT ++#include "parser/uni_hid_parser_native_motion.h" ++#endif #include -@@ -27,11 +28,6 @@ +@@ -27,11 +31,6 @@ PSMOVE_FSM_LED_UPDATED, // LED updated } psmove_fsm_t; @@ -883,7 +1015,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_psmove.c b/src/compo typedef enum { PSMOVE_STATE_RUMBLE_DISABLED, -@@ -41,9 +37,10 @@ +@@ -41,9 +40,10 @@ // psmove_instance_t represents data used by the psmove driver instance. typedef struct psmove_instance_s { @@ -895,23 +1027,46 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_psmove.c b/src/compo btstack_timer_source_t rumble_timer_duration; btstack_timer_source_t rumble_timer_delayed_start; -@@ -127,6 +124,7 @@ +@@ -127,8 +127,12 @@ 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) { ++#if SWITCH2_BRIDGE_FULL_INPUT ++ uni_hid_parser_native_motion_begin(d); ++#endif uni_controller_t* ctl = &d->controller; -@@ -154,6 +152,7 @@ + memset(ctl, 0, sizeof(*ctl)); + +@@ -136,6 +140,12 @@ + } + + void uni_hid_parser_psmove_parse_input_report(uni_hid_device_t* d, const uint8_t* report, uint16_t len) { ++#if SWITCH2_BRIDGE_FULL_INPUT ++ uni_hid_parser_native_motion_begin(d); ++ // Both model-specific layouts carry the low clock byte at offset 43. ++ if (!report || len < sizeof(psmove_input_zcm2_report_t)) ++ return; ++#endif + // printf_hexdump(report, len); + + // FIXME: parse ZCM1 / ZCM2 parts. For the moment only the "common" is being parsed. +@@ -154,6 +164,12 @@ } uni_controller_t* ctl = &d->controller; + psmove_instance_t* ins = get_psmove_instance(d); ++#if SWITCH2_BRIDGE_FULL_INPUT ++ uni_hid_parser_native_motion_accept(d); ++ bool fresh = uni_hid_parser_native_motion_fresh( ++ d, ((uint32_t)r->time_high << 8) | report[43], UINT16_MAX); ++#endif // Buttons if (r->buttons[0] & 0x01) -@@ -187,16 +186,37 @@ +@@ -187,16 +203,50 @@ ctl->gamepad.throttle = r->trigger * 4; @@ -934,6 +1089,12 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_psmove.c b/src/compo + gyro_first, gyro_second, &motion)) { + memcpy(ctl->gamepad.accel, motion.accel, sizeof(motion.accel)); + memcpy(ctl->gamepad.gyro, motion.gyro, sizeof(motion.gyro)); ++#if SWITCH2_BRIDGE_FULL_INPUT ++ if (fresh) { ++ uni_hid_parser_native_motion_accel(d, motion.accel); ++ uni_hid_parser_native_motion_gyro(d, motion.gyro); ++ } ++#endif + } if (r->battery <= 5) @@ -946,6 +1107,13 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_psmove.c b/src/compo + const uni_psmove_calibration_result_t result = + uni_psmove_add_calibration_report( + &ins->imu_calibration, ins->model, report, len); ++#if SWITCH2_BRIDGE_FULL_INPUT ++ // A changed calibration bank is not itself a new physical sample. ++ if (result != UNI_PSMOVE_CALIBRATION_INVALID) { ++ uni_hid_parser_native_motion_accel(d, NULL); ++ uni_hid_parser_native_motion_gyro(d, NULL); ++ } ++#endif + if (result == UNI_PSMOVE_CALIBRATION_INCOMPLETE) { + psmove_request_calibration_report(d); + } else if (result == UNI_PSMOVE_CALIBRATION_COMPLETE) { @@ -956,7 +1124,13 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_psmove.c b/src/compo } void uni_hid_parser_psmove_play_dual_rumble(struct uni_hid_device_s* d, -@@ -261,25 +281,34 @@ +@@ -258,28 +308,40 @@ + void uni_hid_parser_psmove_setup(struct uni_hid_device_s* d) { + psmove_instance_t* ins = get_psmove_instance(d); + memset(ins, 0, sizeof(*ins)); ++#if SWITCH2_BRIDGE_FULL_INPUT ++ uni_hid_parser_native_motion_reset(d); ++#endif switch (d->product_id) { case ZCM1_PID: @@ -1001,7 +1175,17 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_psmove.c b/src/compo diff --git a/src/components/bluepad32/parser/uni_hid_parser_switch.c b/src/components/bluepad32/parser/uni_hid_parser_switch.c --- a/src/components/bluepad32/parser/uni_hid_parser_switch.c +++ b/src/components/bluepad32/parser/uni_hid_parser_switch.c -@@ -51,13 +51,22 @@ +@@ -7,6 +7,9 @@ + // https://github.com/DanielOgorchock/linux/blob/ogorchock/drivers/hid/hid-nintendo.c + + #include "parser/uni_hid_parser_switch.h" ++#if SWITCH2_BRIDGE_FULL_INPUT ++#include "parser/uni_hid_parser_native_motion.h" ++#endif + + #include + +@@ -51,13 +54,22 @@ static const int16_t DEFAULT_ACCEL_SCALE = 16384; static const int16_t DEFAULT_GYRO_OFFSET = 0; static const int16_t DEFAULT_GYRO_SCALE = 13371; @@ -1025,7 +1209,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_switch.c b/src/compo #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 +81,7 @@ +@@ -72,6 +84,7 @@ STATE_READ_FACTORY_IMU_CALIBRATION, // Factory IMU calibration info STATE_SET_FULL_REPORT, // Request report 0x30 STATE_ENABLE_IMU, // Enable/Disable gyro/accel @@ -1033,7 +1217,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_switch.c b/src/compo STATE_DUMP_FLASH, // Dump SPI Flash memory STATE_UPDATE_LED, // Update LEDs STATE_READY, // Gamepad setup ready! -@@ -111,6 +121,7 @@ +@@ -111,6 +124,7 @@ SUBCMD_SPI_FLASH_READ = 0x10, SUBCMD_SET_PLAYER_LEDS = 0x30, SUBCMD_ENABLE_IMU = 0x40, @@ -1041,7 +1225,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_switch.c b/src/compo }; typedef enum { -@@ -137,7 +148,12 @@ +@@ -137,7 +151,12 @@ // 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; @@ -1054,7 +1238,17 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_switch.c b/src/compo btstack_timer_source_t setup_timer; -@@ -322,6 +338,7 @@ +@@ -164,6 +183,9 @@ + + int32_t imu_cal_accel_divisor[3]; + int32_t imu_cal_gyro_divisor[3]; ++#if SWITCH2_BRIDGE_FULL_INPUT ++ bool accel_calibrated, gyro_calibrated; ++#endif + + // Debug only + int debug_fd; // File descriptor where dump is saved +@@ -322,6 +344,7 @@ 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); @@ -1062,7 +1256,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_switch.c b/src/compo 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,23 +350,33 @@ +@@ -333,23 +356,36 @@ 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); @@ -1091,12 +1285,46 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_switch.c b/src/compo switch_instance_t* ins = get_switch_instance(d); memset(ins, 0, sizeof(*ins)); - +- ++#if SWITCH2_BRIDGE_FULL_INPUT ++ uni_hid_parser_native_motion_reset(d); ++#endif ++ + memcpy(ins->effective_rumble, switch_neutral_rumble, sizeof(ins->effective_rumble)); ins->state = STATE_SETUP; ins->mode = SWITCH_MODE_NONE; // In case the controller doesn't answer to SUBCMD_REQ_DEV_INFO command, set a default one. -@@ -451,6 +478,10 @@ +@@ -389,11 +425,19 @@ + } + + void uni_hid_parser_switch_init_report(uni_hid_device_t* d) { ++#if SWITCH2_BRIDGE_FULL_INPUT ++ uni_hid_parser_native_motion_begin(d); ++#endif + ARG_UNUSED(d); + // Nothing + } + + void uni_hid_parser_switch_parse_input_report(struct uni_hid_device_s* d, const uint8_t* report, uint16_t len) { ++#if SWITCH2_BRIDGE_FULL_INPUT ++ uni_hid_parser_native_motion_begin(d); ++ if (!report) ++ return; ++#endif + if (len < 12) { + loge("Nintendo Switch: Invalid packet len; got %d, want >= 12\n", len); + return; +@@ -408,6 +452,9 @@ + break; + case SWITCH_INPUT_BUTTON_EVENT: + parse_report_3f(d, report, len); ++#if SWITCH2_BRIDGE_FULL_INPUT ++ uni_hid_parser_native_motion_accept(d); ++#endif + break; + default: + loge("Nintendo Switch: unsupported report id: 0x%02x\n", report[0]); +@@ -451,6 +498,10 @@ break; case STATE_ENABLE_IMU: logd("STATE_ENABLE_IMU\n"); @@ -1107,7 +1335,32 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_switch.c b/src/compo fsm_dump_rom(d); break; case STATE_DUMP_FLASH: -@@ -648,7 +679,8 @@ +@@ -619,6 +670,11 @@ + SWITCH_FACTORY_IMU_CAL_DATA_SIZE); + return; + } ++#if SWITCH2_BRIDGE_FULL_INPUT ++ ins->accel_calibrated = ins->gyro_calibrated = true; ++ uni_hid_parser_native_motion_accel(d, NULL); ++ uni_hid_parser_native_motion_gyro(d, NULL); ++#endif + + for (int i = 0; i < 3; i++) { + int j = i * 2; +@@ -635,6 +691,12 @@ + // FIXME: move to its own function + ins->imu_cal_accel_divisor[i] = ins->cal_accel.scale[i] - ins->cal_accel.offset[i]; + ins->imu_cal_gyro_divisor[i] = ins->cal_gyro.scale[i] - ins->cal_gyro.offset[i]; ++#if SWITCH2_BRIDGE_FULL_INPUT ++ if (ins->imu_cal_accel_divisor[i] <= 0) ++ ins->accel_calibrated = false; ++ if (ins->imu_cal_gyro_divisor[i] <= 0) ++ ins->gyro_calibrated = false; ++#endif + } + + logi( +@@ -648,7 +710,8 @@ // Reply to SUBCMD_REQ_DEV_INFO static void process_reply_req_dev_info(struct uni_hid_device_s* d, const struct switch_report_21_s* r, int len) { @@ -1117,7 +1370,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_switch.c b/src/compo switch_instance_t* ins = get_switch_instance(d); if (ins->state > STATE_SETUP && ins->mode == SWITCH_MODE_NONE) { bool enable_imu; -@@ -669,6 +701,7 @@ +@@ -669,6 +732,7 @@ ins->firmware_version_hi = r->data[0]; ins->firmware_version_lo = r->data[1]; ins->controller_type = r->data[2]; @@ -1125,7 +1378,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_switch.c b/src/compo logi("Switch: Firmware version: %d.%d. Controller type=%d\n", r->data[0], r->data[1], r->data[2]); } -@@ -725,6 +758,12 @@ +@@ -725,6 +789,12 @@ ARG_UNUSED(r); ARG_UNUSED(len); } @@ -1138,16 +1391,31 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_switch.c b/src/compo // 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) { -@@ -733,6 +772,8 @@ +@@ -733,6 +803,23 @@ // 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 // 00 const struct switch_report_21_s* r = (const struct switch_report_21_s*)report; + if (len < (int)sizeof(*r)) + return; ++#if SWITCH2_BRIDGE_FULL_INPUT ++ if (!(r->ack & 0x80)) ++ return; ++ if (r->subcmd_id == SUBCMD_REQ_DEV_INFO && len < (int)sizeof(*r) + 3) ++ return; ++ if (r->subcmd_id == SUBCMD_SPI_FLASH_READ) { ++ if (len < (int)sizeof(*r) + 5 || len < (int)sizeof(*r) + 5 + r->data[4]) ++ return; ++ // An unrelated/stale SPI response cannot certify IMU calibration. ++ if (get_switch_instance(d)->state == STATE_READ_FACTORY_IMU_CALIBRATION && ++ (r->data[4] != SWITCH_FACTORY_IMU_CAL_DATA_SIZE || ++ little_endian_read_32(r->data, 0) != SWITCH_FACTORY_IMU_CAL_DATA_ADDR)) ++ return; ++ } ++#endif if ((r->ack & 0b10000000) == 0) { loge("Switch: Error, subcommand id=0x%02x was not successful.\n", r->subcmd_id); } -@@ -751,6 +792,9 @@ +@@ -751,6 +838,9 @@ break; case SUBCMD_ENABLE_IMU: process_reply_enable_imu(d, r, len); @@ -1157,7 +1425,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_switch.c b/src/compo break; default: loge("Switch: Error, unexpected subcmd_id=0x%02x in report 0x21\n", r->subcmd_id); -@@ -823,19 +867,26 @@ +@@ -823,19 +913,26 @@ switch_instance_t* ins = get_switch_instance(d); uni_controller_t* ctl = &d->controller; @@ -1195,7 +1463,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_switch.c b/src/compo if (ins->controller_type == SWITCH_CONTROLLER_TYPE_JCR) { accel[1] = -accel[1]; accel[2] = -accel[2]; -@@ -843,10 +894,13 @@ +@@ -843,10 +940,13 @@ gyro[2] = -gyro[2]; } @@ -1213,7 +1481,33 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_switch.c b/src/compo } // Process 0x30 input report: SWITCH_INPUT_IMU_DATA -@@ -1172,6 +1226,18 @@ +@@ -856,6 +956,10 @@ + // 9D FF 72 FD 01 00 72 10 35 00 C1 FF 9B FF 75 FD FF FF 6C 10 34 00 C2 FF + // 9A FF + ++#if SWITCH2_BRIDGE_FULL_INPUT ++ if (len < 3 + (int)sizeof(struct switch_report_30_s)) ++ return; ++#endif + ARG_UNUSED(len); + + switch_instance_t* ins = get_switch_instance(d); +@@ -887,6 +991,14 @@ + // We just take the latest one. If it is not accurate enough, we can average them. + if (ins->mode == SWITCH_MODE_IMU) + parse_imu(d, &r->imu[2]); ++#if SWITCH2_BRIDGE_FULL_INPUT ++ uni_hid_parser_native_motion_accept(d); ++ if (ins->mode == SWITCH_MODE_IMU && ins->controller_type != SWITCH_CONTROLLER_TYPE_SNES && ++ uni_hid_parser_native_motion_fresh(d, report[1], UINT8_MAX)) { ++ uni_hid_parser_native_motion_accel(d, ins->accel_calibrated ? ctl->gamepad.accel : NULL); ++ uni_hid_parser_native_motion_gyro(d, ins->gyro_calibrated ? ctl->gamepad.gyro : NULL); ++ } ++#endif + } + + // Shared both by Switch Pro Controller and Switch SNES. +@@ -1172,6 +1284,18 @@ req->data[0] = (ins->mode == SWITCH_MODE_IMU); send_subcmd(d, req, sizeof(out)); } @@ -1232,7 +1526,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_switch.c b/src/compo static void fsm_update_led(struct uni_hid_device_s* d) { switch_instance_t* ins = get_switch_instance(d); -@@ -1203,6 +1269,13 @@ +@@ -1203,6 +1327,13 @@ return rumble_freqs[i]; } @@ -1246,7 +1540,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_switch.c b/src/compo static struct switch_rumble_amp_data find_rumble_amp(uint16_t amp) { unsigned int i = 0; if (amp > rumble_amps[0].amp) { -@@ -1253,12 +1326,15 @@ +@@ -1253,12 +1384,15 @@ } switch_instance_t* ins = get_switch_instance(d); @@ -1262,7 +1556,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_switch.c b/src/compo break; default: // Do nothing -@@ -1338,12 +1414,157 @@ +@@ -1338,12 +1472,160 @@ } static void send_subcmd(uni_hid_device_t* d, struct switch_subcmd_request* r, int len) { @@ -1414,6 +1708,9 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_switch.c b/src/compo + if (!d || d->report_parser.setup != uni_hid_parser_switch_setup) + return; + switch_instance_t* ins = get_switch_instance(d); ++#if SWITCH2_BRIDGE_FULL_INPUT ++ uni_hid_parser_native_motion_forget(d); ++#endif + switch_cancel_rumble_timers(ins); + btstack_run_loop_remove_timer(&ins->setup_timer); + ins->setup_timer.context = NULL; @@ -1424,7 +1721,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_switch.c b/src/compo } static int32_t calibrate_axis(int32_t v, switch_cal_stick_t cal) { -@@ -1364,8 +1585,9 @@ +@@ -1364,8 +1646,9 @@ static void switch_stop_rumble_now(uni_hid_device_t* d) { switch_instance_t* ins = get_switch_instance(d); @@ -1436,7 +1733,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_switch.c b/src/compo ins->rumble_state = SWITCH_STATE_RUMBLE_DISABLED; struct switch_subcmd_request req = {0}; -@@ -1379,11 +1601,31 @@ +@@ -1379,11 +1662,31 @@ send_subcmd(d, (struct switch_subcmd_request*)&req, sizeof(req) - 1); } @@ -1468,7 +1765,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_switch.c b/src/compo if (duration_ms == 0) { if (ins->rumble_state != SWITCH_STATE_RUMBLE_DISABLED) -@@ -1391,14 +1633,17 @@ +@@ -1391,14 +1694,17 @@ return; } @@ -1494,7 +1791,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_switch.c b/src/compo // Set timer to turn off rumble ins->rumble_timer_duration.process = &on_switch_set_rumble_off; -@@ -1410,19 +1655,52 @@ +@@ -1410,19 +1716,52 @@ static void on_switch_set_rumble_on(btstack_timer_source_t* ts) { uni_hid_device_t* d = ts->context; @@ -1585,18 +1882,21 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen #include #include -@@ -19,6 +21,10 @@ +@@ -19,6 +21,13 @@ #endif // ENABLE_EEPROM_DUMP #include "parser/uni_hid_parser_wii.h" +#include "parser/uni_hid_parser_imu.h" ++#if SWITCH2_BRIDGE_FULL_INPUT ++#include "parser/uni_hid_parser_native_motion.h" ++#endif +#if SWITCH_PICO_WII_IR +#include "parser/uni_hid_parser_wii_ir.h" +#endif #include "controller/uni_controller.h" #include "hid_usage.h" -@@ -82,10 +88,23 @@ +@@ -82,10 +91,23 @@ WII_FSM_EXT_DID_INIT, // Extension initialized WII_FSM_EXT_DID_NO_ENCRYPTION, // Extension no encryption WII_FSM_EXT_DID_READ_REGISTER, // Extension read register @@ -1620,7 +1920,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen WII_FSM_DEV_GUESSED, // Device type guessed WII_FSM_DEV_ASSIGNED, // Device type assigned WII_FSM_LED_UPDATED, // After a device was assigned, update LEDs. -@@ -104,16 +123,6 @@ +@@ -104,16 +126,6 @@ WII_READ_FROM_REGISTERS = 0x04, } wii_read_type_t; @@ -1637,7 +1937,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen // balance_board_t represents the data provided by the Balance Board. typedef struct balance_board_s { -@@ -144,7 +153,6 @@ +@@ -144,7 +156,6 @@ // wii_instance_t represents data used by the Wii driver instance. typedef struct wii_instance_s { uint8_t state; @@ -1645,7 +1945,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen wii_mode_t mode; /* horizontal, accel, vertical, rumble, etc.. */ enum wii_devtype dev_type; enum wii_exttype ext_type; -@@ -159,6 +167,39 @@ +@@ -159,6 +170,39 @@ uint16_t rumble_duration_ms; balance_board_calibration_t balance_board_calibration; @@ -1685,7 +1985,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen // Debug only int debug_fd; // File descriptor where dump is saved -@@ -177,7 +218,6 @@ +@@ -177,7 +221,6 @@ static void process_drm_kae(uni_hid_device_t* d, const uint8_t* report, uint16_t len); static void process_drm_kee(uni_hid_device_t* d, const uint8_t* report, uint16_t len); static void process_drm_e(uni_hid_device_t* d, const uint8_t* report, uint16_t len); @@ -1693,7 +1993,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen static udraw_tablet_t process_udraw_tablet(const uint8_t* e, uint16_t len); static balance_board_t process_balance_board(uni_hid_device_t* d, const uint8_t* e, uint16_t len); -@@ -196,6 +236,43 @@ +@@ -196,6 +239,43 @@ static void on_wii_set_rumble_on(btstack_timer_source_t* ts); static void on_wii_set_rumble_off(btstack_timer_source_t* ts); static void wii_play_dual_rumble_now(struct uni_hid_device_s* d, uint16_t duration_ms); @@ -1737,7 +2037,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen // Constants static const char* wii_devtype_names[] = { -@@ -217,124 +294,450 @@ +@@ -217,124 +297,450 @@ // process_ functions @@ -2298,7 +2598,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen } static void process_req_data_read_calibration_data(uni_hid_device_t* d, const uint8_t* report, uint16_t len) { -@@ -444,17 +847,90 @@ +@@ -444,17 +850,90 @@ // Defined here: http://wiibrew.org/wiki/Wiimote#0x21:_Read_Memory_Data static void process_req_data(uni_hid_device_t* d, const uint8_t* report, uint16_t len) { @@ -2396,7 +2696,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen case WII_FSM_BALANCE_BOARD_DID_READ_CALIBRATION: process_req_data_read_calibration_data(d, report, len); break; -@@ -465,7 +941,6 @@ +@@ -465,7 +944,6 @@ process_req_data_dump_eeprom(d, report, len); break; default: @@ -2404,7 +2704,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen break; } } -@@ -473,29 +948,44 @@ +@@ -473,29 +951,44 @@ // Defined here: // http://wiibrew.org/wiki/Wiimote#0x22:_Acknowledge_output_report.2C_return_function_result static void process_req_return(uni_hid_device_t* d, const uint8_t* report, uint16_t len) { @@ -2472,7 +2772,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen } } -@@ -514,15 +1004,26 @@ +@@ -514,15 +1007,26 @@ const uint8_t* data = &report[1]; wii_instance_t* ins = get_wii_instance(d); @@ -2508,7 +2808,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen } // Process misc buttons ctl->gamepad.misc_buttons |= (data[1] & 0x80) ? MISC_BUTTON_SYSTEM : 0; // Button "home" -@@ -563,46 +1064,160 @@ +@@ -563,46 +1067,168 @@ // Used for WiiMote in Accelerometer Mode. Defined here: // http://wiibrew.org/wiki/Wiimote#0x31:_Core_Buttons_and_Accelerometer static void process_drm_ka(uni_hid_device_t* d, const uint8_t* report, uint16_t len) { @@ -2565,8 +2865,12 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen + ins->accel[0] = uni_imu_scale(ins->accel_zero[0] - x, ins->accel_span[0], UNI_IMU_ACCEL_RES_PER_G); + ins->accel[1] = uni_imu_scale(z - ins->accel_zero[2], ins->accel_span[2], UNI_IMU_ACCEL_RES_PER_G); + ins->accel[2] = uni_imu_scale(y - ins->accel_zero[1], ins->accel_span[1], UNI_IMU_ACCEL_RES_PER_G); ++#if SWITCH2_BRIDGE_FULL_INPUT ++ ins->accel_sequence = uni_hid_parser_native_motion_next_sequence(); ++#else + if (++ins->accel_sequence == 0) + ++ins->accel_sequence; ++#endif + for (int i = 0; i < 3; i++) + d->controller.gamepad.accel[i] = ins->accel[i]; +} @@ -2699,8 +3003,12 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen + ins->gyro[0] = wii_calibrate_gyro(ins, 2, pitch, e[3] & 1); + ins->gyro[1] = -wii_calibrate_gyro(ins, 0, yaw, e[3] & 2); + ins->gyro[2] = wii_calibrate_gyro(ins, 1, roll, e[4] & 2); ++#if SWITCH2_BRIDGE_FULL_INPUT ++ ins->gyro_sequence = uni_hid_parser_native_motion_next_sequence(); ++#else + if (++ins->gyro_sequence == 0) + ++ins->gyro_sequence; ++#endif + ins->gyro_valid = true; + for (int i = 0; i < 3; i++) + d->controller.gamepad.gyro[i] = ins->gyro[i]; @@ -2709,7 +3017,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen } // Used in WiiMote + Nunchuk Mode -@@ -617,31 +1232,17 @@ +@@ -617,31 +1243,17 @@ } wii_instance_t* ins = get_wii_instance(d); @@ -2749,7 +3057,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen // Better if uDraw reports its own "type", but for the moment // it gets reported and the 2nd half of a gamepad. -@@ -668,59 +1269,59 @@ +@@ -668,59 +1280,59 @@ ctl->gamepad.buttons |= n.button_lower ? BUTTON_Y : 0; } @@ -2857,7 +3165,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen static udraw_tablet_t process_udraw_tablet(const uint8_t* e, uint16_t len) { // uDraw Tablet format here: -@@ -1002,52 +1603,18 @@ +@@ -1002,52 +1614,18 @@ logi("fsm: req_status\n"); wii_instance_t* ins = get_wii_instance(d); ins->state = WII_FSM_DID_REQ_STATUS; @@ -2915,7 +3223,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen } static void wii_fsm_ext_read_register(uni_hid_device_t* d) { -@@ -1055,8 +1622,7 @@ +@@ -1055,8 +1633,7 @@ wii_instance_t* ins = get_wii_instance(d); ins->state = WII_FSM_EXT_DID_READ_REGISTER; @@ -2925,7 +3233,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen uint16_t bytes_to_read = 6; wii_read_mem(d, WII_READ_FROM_REGISTERS, offset, bytes_to_read); } -@@ -1066,8 +1632,7 @@ +@@ -1066,8 +1643,7 @@ wii_instance_t* ins = get_wii_instance(d); ins->state = WII_FSM_BALANCE_BOARD_DID_READ_CALIBRATION; @@ -2935,7 +3243,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen uint16_t bytes_to_read = 16; wii_read_mem(d, WII_READ_FROM_REGISTERS, offset, bytes_to_read); } -@@ -1077,8 +1642,7 @@ +@@ -1077,8 +1653,7 @@ wii_instance_t* ins = get_wii_instance(d); ins->state = WII_FSM_BALANCE_BOARD_DID_READ_CALIBRATION2; @@ -2945,7 +3253,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen uint16_t bytes_to_read = 8; wii_read_mem(d, WII_READ_FROM_REGISTERS, offset, bytes_to_read); } -@@ -1086,6 +1650,10 @@ +@@ -1086,6 +1661,10 @@ static void wii_fsm_assign_device(uni_hid_device_t* d) { logi("fsm: assign_device\n"); wii_instance_t* ins = get_wii_instance(d); @@ -2956,7 +3264,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen uint8_t dev = ins->dev_type; switch (dev) { case WII_DEVTYPE_UNK: -@@ -1100,18 +1668,8 @@ +@@ -1100,18 +1679,8 @@ } uint8_t report_type = 0xff; if (ins->ext_type == WII_EXT_NUNCHUK) { @@ -2977,7 +3285,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen } else if (ins->ext_type == WII_EXT_CLASSIC_CONTROLLER) { logi("Wii: requesting E (Classic Controller)\n"); d->controller_subtype = CONTROLLER_SUBTYPE_WII_CLASSIC; -@@ -1125,21 +1683,17 @@ +@@ -1125,21 +1694,17 @@ d->controller_subtype = CONTROLLER_SUBTYPE_WIIMOTE_UDRAW_TABLET; report_type = WII_REPORT_TYPE_KE; } else { @@ -3009,7 +3317,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen uni_hid_parser_wii_request_report_type(d, report_type); break; } -@@ -1164,7 +1718,10 @@ +@@ -1164,7 +1729,10 @@ ins->state = WII_FSM_LED_UPDATED; wii_process_fsm(d); @@ -3021,7 +3329,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen } static void wii_fsm_dump_eeprom(struct uni_hid_device_s* d) { -@@ -1218,7 +1775,15 @@ +@@ -1218,7 +1786,15 @@ // Do nothing break; case WII_FSM_DEV_GUESSED: @@ -3038,7 +3346,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen break; case WII_FSM_BALANCE_BOARD_READ_CALIBRATION: wii_fsm_balance_board_read_calibration(d); -@@ -1234,6 +1799,19 @@ +@@ -1234,6 +1810,19 @@ wii_fsm_update_led(d); break; case WII_FSM_LED_UPDATED: @@ -3058,7 +3366,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen break; default: loge("Wii: wii_process_fsm() unexpected state: %d\n", ins->state); -@@ -1249,10 +1827,10 @@ +@@ -1249,10 +1838,10 @@ ins->mode = WII_MODE_HORIZONTAL; ins->state = WII_FSM_SETUP; @@ -3073,13 +3381,18 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen // Dump EEPROM #if ENABLE_EEPROM_DUMP -@@ -1267,13 +1845,44 @@ +@@ -1266,14 +1855,49 @@ + wii_process_fsm(d); } +void uni_hid_parser_wii_teardown(uni_hid_device_t* d) { + if (!d || d->report_parser.setup != uni_hid_parser_wii_setup) + return; + wii_instance_t* ins = get_wii_instance(d); ++#if SWITCH2_BRIDGE_FULL_INPUT ++ ins->accel_calibrated = false; ++ ins->nunchuk_accel_calibrated = ins->nunchuk_accel_valid = false; ++#endif + ins->gyro_valid = false; + ins->mp_active = false; + ins->mp_calibrated = false; @@ -3120,7 +3433,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen return; switch (report[0]) { case WIIPROTO_REQ_STATUS: -@@ -1291,6 +1900,14 @@ +@@ -1291,6 +1915,14 @@ case WII_REPORT_TYPE_KAE: process_drm_kae(d, report, len); break; @@ -3135,7 +3448,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen case WII_REPORT_TYPE_KEE: process_drm_kee(d, report, len); break; -@@ -1307,6 +1924,9 @@ +@@ -1307,6 +1939,9 @@ logi("Wii parser: unknown report type: 0x%02x\n", report[0]); printf_hexdump(report, len); } @@ -3145,7 +3458,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen } void uni_hid_parser_wii_set_player_leds(uni_hid_device_t* d, uint8_t leds) { -@@ -1371,29 +1991,25 @@ +@@ -1371,29 +2006,25 @@ void uni_hid_parser_wii_set_mode(uni_hid_device_t* d, wii_mode_t mode) { wii_instance_t* ins = get_wii_instance(d); @@ -3190,7 +3503,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen uni_hid_device_send_intr_report(d, report, sizeof(report)); } -@@ -1480,10 +2096,13 @@ +@@ -1480,10 +2111,13 @@ static void wii_read_mem(uni_hid_device_t* d, wii_read_type_t t, uint32_t offset, uint16_t size) { logi("****** read_mem: offset=0x%04x, size=%d from=%d\n", offset, size, t); @@ -3205,7 +3518,7 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen (offset & 0xff0000) >> 16, (offset & 0xff00) >> 8, (offset & 0xff), // Offset (size & 0xff00) >> 8, (size & 0xff), // Size in bytes // clang-format on -@@ -1491,6 +2110,15 @@ +@@ -1491,6 +2125,15 @@ uni_hid_device_send_intr_report(d, report, sizeof(report)); } @@ -3224,15 +3537,18 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_wii.c b/src/componen diff --git a/src/components/bluepad32/uni_hid_device.c b/src/components/bluepad32/uni_hid_device.c --- a/src/components/bluepad32/uni_hid_device.c +++ b/src/components/bluepad32/uni_hid_device.c -@@ -32,6 +32,7 @@ +@@ -32,6 +32,10 @@ #include "parser/uni_hid_parser_stadia.h" #include "parser/uni_hid_parser_steam.h" #include "parser/uni_hid_parser_switch.h" +#include "parser/uni_hid_parser_switch2.h" ++#if SWITCH2_BRIDGE_FULL_INPUT ++#include "parser/uni_hid_parser_native_motion.h" ++#endif #include "parser/uni_hid_parser_wii.h" #include "parser/uni_hid_parser_xboxone.h" #include "platform/uni_platform.h" -@@ -447,6 +448,17 @@ +@@ -447,6 +451,18 @@ return; } @@ -3242,7 +3558,8 @@ diff --git a/src/components/bluepad32/uni_hid_device.c b/src/components/bluepad3 + uni_hid_parser_switch2_teardown(d); + if (d->report_parser.setup == uni_hid_parser_wii_setup) + uni_hid_parser_wii_teardown(d); -+#if SWITCH2_BRIDGE_DUALSENSE_INPUT ++#if SWITCH2_BRIDGE_FULL_INPUT ++ uni_hid_parser_native_motion_forget(d); + if (d->report_parser.setup == uni_hid_parser_ds5_setup) + uni_hid_parser_ds5_bridge_teardown(d); +#endif @@ -3250,7 +3567,7 @@ diff --git a/src/components/bluepad32/uni_hid_device.c b/src/components/bluepad3 // Disconnect child first if (d->child) uni_hid_device_disconnect(d->child); -@@ -465,9 +477,9 @@ +@@ -465,9 +481,9 @@ // Cleanup if (!uni_hid_device_is_virtual_device(d)) { type = gap_get_connection_type(d->conn.handle); @@ -3262,7 +3579,7 @@ diff --git a/src/components/bluepad32/uni_hid_device.c b/src/components/bluepad3 uni_bt_bredr_disconnect(d); else loge("uni_hid_device_disconnect: Unknown GAP connection type: %d\n", type); -@@ -490,6 +502,17 @@ +@@ -490,6 +506,18 @@ loge("uni_hid_device_delete: invalid hid device: NULL\n"); return; } @@ -3272,7 +3589,8 @@ diff --git a/src/components/bluepad32/uni_hid_device.c b/src/components/bluepad3 + uni_hid_parser_switch2_teardown(d); + if (d->report_parser.setup == uni_hid_parser_wii_setup) + uni_hid_parser_wii_teardown(d); -+#if SWITCH2_BRIDGE_DUALSENSE_INPUT ++#if SWITCH2_BRIDGE_FULL_INPUT ++ uni_hid_parser_native_motion_forget(d); + if (d->report_parser.setup == uni_hid_parser_ds5_setup) + uni_hid_parser_ds5_bridge_teardown(d); +#endif @@ -3280,7 +3598,7 @@ diff --git a/src/components/bluepad32/uni_hid_device.c b/src/components/bluepad3 // Delete child first if (d->child) -@@ -655,6 +678,7 @@ +@@ -655,6 +683,7 @@ 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; @@ -3288,7 +3606,7 @@ diff --git a/src/components/bluepad32/uni_hid_device.c b/src/components/bluepad3 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); -@@ -718,6 +742,15 @@ +@@ -718,6 +747,15 @@ d->report_parser.device_dump = uni_hid_parser_switch_device_dump; logi("Device detected as Nintendo Switch Pro controller: 0x%02x\n", type); break; @@ -3304,7 +3622,7 @@ diff --git a/src/components/bluepad32/uni_hid_device.c b/src/components/bluepad3 case CONTROLLER_TYPE_SteamController: d->report_parser.setup = uni_hid_parser_steam_setup; d->report_parser.init_report = uni_hid_parser_steam_init_report; -@@ -809,7 +842,11 @@ +@@ -809,7 +847,11 @@ return; } diff --git a/src/firmware/input/bluepad32_input_backend.cpp b/src/firmware/input/bluepad32_input_backend.cpp index 1a6d8dc..359e60b 100644 --- a/src/firmware/input/bluepad32_input_backend.cpp +++ b/src/firmware/input/bluepad32_input_backend.cpp @@ -36,8 +36,9 @@ #include extern "C" { #include "parser/uni_hid_parser_wii.h" -#if SWITCH2_BRIDGE_DUALSENSE_INPUT +#if SWITCH2_BRIDGE_FULL_INPUT #include "parser/uni_hid_parser_ds5.h" +#include "parser/uni_hid_parser_native_motion.h" #endif } #include "parser/uni_hid_parser_switch2.h" @@ -257,19 +258,30 @@ constexpr WiiCuePattern kWiiCuePatterns[8] = { constexpr uint32_t kWiiCueDeadlineMs = 2000; #endif -#if SWITCH2_BRIDGE_DUALSENSE_INPUT -struct DualSenseIngress { - uint32_t report_sequence = 0; +#if SWITCH2_BRIDGE_FULL_INPUT +struct NativeGamepadIngress { uint32_t received_us = 0; - uint32_t motion_sequence = 0; - uint32_t motion_received_us = 0; + uint32_t accel_sequence = 0; + uint32_t gyro_sequence = 0; + uint32_t accel_received_us = 0; + uint32_t gyro_received_us = 0; bool has_report = false; - bool motion_valid = false; + bool accel_valid = false; + bool gyro_valid = false; int32_t accel_q13[3]{}; int32_t gyro_q10[3]{}; }; -struct DualSenseCue { +// Physical parser counters survive logical pairing/reselection epochs. They +// are retired only with the Bluetooth connection, never by a snapshot getter. +struct NativeGamepadReportIngress { + uni_hid_device_t* device = nullptr; + uint32_t report_sequence = 0; + uint32_t accel_sequence = 0; + uint32_t gyro_sequence = 0; +}; + +struct NativeGamepadCue { uint64_t token = 0; uint32_t connection_generation = 0; uint32_t requested_ms = 0; @@ -282,20 +294,20 @@ struct DualSenseCue { bool in_flight = false; }; -struct DualSenseMotorOutput { +struct NativeGamepadMotorOutput { uint32_t deadline_ms = 0; uint8_t magnitude[2]{}; bool owned = false; }; -// Same bounded pulse vocabulary as Wii, with real per-motor magnitudes. +// Same bounded pulse vocabulary as Wii, with source-driver motor magnitudes. // These are compatibility-vibration approximations, not uploaded HD waveforms. -struct DualSenseCuePattern { +struct NativeGamepadCuePattern { uint16_t phases_ms[7]; uint8_t count; uint8_t magnitude; }; -constexpr DualSenseCuePattern kDualSenseCuePatterns[8] = { +constexpr NativeGamepadCuePattern kNativeGamepadCuePatterns[8] = { {{0}, 0, 0}, {{1000}, 1, 160}, {{100, 180, 100, 180, 100, 180, 100}, 7, 200}, @@ -305,7 +317,7 @@ constexpr DualSenseCuePattern kDualSenseCuePatterns[8] = { {{60}, 1, 96}, {{120}, 1, 220}, }; -constexpr uint32_t kDualSenseCueDeadlineMs = 2000; +constexpr uint32_t kNativeGamepadCueDeadlineMs = 2000; #endif @@ -345,9 +357,9 @@ struct BackendSlot { #ifdef SWITCH2_BRIDGE_WII_INPUT WiiMotionIngress wii_motion; #endif -#if SWITCH2_BRIDGE_DUALSENSE_INPUT - DualSenseIngress dualsense_motion; - DualSenseMotorOutput dualsense_output; +#if SWITCH2_BRIDGE_FULL_INPUT + NativeGamepadIngress native_motion; + NativeGamepadMotorOutput native_output; #endif #ifdef SWITCH_PICO_WII_IR_GYRO WiiAimSource wii_aim; @@ -436,79 +448,97 @@ void retire_wii_slot(uint8_t slot_index) { } #endif +#if SWITCH2_BRIDGE_FULL_INPUT +bool g_native_explicit_address = false; +uint8_t g_native_address[6]{}; +uint8_t g_native_slot = 0xff; +uint32_t g_native_generation = 0; +Bluepad32NativeGamepadSnapshot g_native_snapshot{}; +NativeGamepadCue g_native_cues[2]{}; +uint64_t g_next_native_token = 1; +uni_hid_device_t* g_native_pending_devices[kSlotCount]{}; +NativeGamepadReportIngress g_native_reports[kSlotCount]{}; + +bool native_device_allowed(const uni_hid_device_t* device) { + if (device == nullptr || !uni_hid_device_is_gamepad(device)) return false; #if SWITCH2_BRIDGE_DUALSENSE_INPUT -bool g_dualsense_explicit_address = false; -uint8_t g_dualsense_address[6]{}; -uint8_t g_dualsense_slot = 0xff; -uint32_t g_dualsense_generation = 0; -Bluepad32DualSenseBridgeSnapshot g_dualsense_snapshot{}; -DualSenseCue g_dualsense_cues[2]{}; -uint64_t g_next_dualsense_token = 1; -uni_hid_device_t* g_dualsense_pending_devices[kSlotCount]{}; - -bool dualsense_source_matches(const uni_hid_device_t* device) { - return device != nullptr && - device->controller_type == CONTROLLER_TYPE_PS5Controller && - device->report_parser.parse_input_report == uni_hid_parser_ds5_parse_input_report && - (!g_dualsense_explicit_address || - memcmp(device->conn.btaddr, g_dualsense_address, 6) == 0); + return device->controller_type == CONTROLLER_TYPE_PS5Controller && + device->report_parser.parse_input_report == uni_hid_parser_ds5_parse_input_report; +#else + return true; +#endif } -bool eligible_dualsense(const BackendSlot& slot) { - return slot.active && slot.companion == nullptr && dualsense_source_matches(slot.device); +bool native_address_matches(const uni_hid_device_t* device) { + return device != nullptr && memcmp(device->conn.btaddr, g_native_address, 6) == 0; } -void cancel_dualsense_cue_locked(DualSenseCue& cue) { +bool eligible_native_gamepad(const BackendSlot& slot) { + return slot.active && native_device_allowed(slot.device) && + (slot.companion == nullptr || native_device_allowed(slot.companion)) && + (!g_native_explicit_address || native_address_matches(slot.device) || + native_address_matches(slot.companion)); +} + +uni_hid_device_t* native_rumble_target(const BackendSlot& slot, uint8_t side) { + // Paired Joy-Cons have a real left owner and right companion. A solo pad's + // driver owns its motor topology, including mono OR/max mixing. + return slot.companion != nullptr && side == 0 ? slot.companion : slot.device; +} + +bool native_rumble_capable(const BackendSlot& slot, uint8_t side) { + const uni_hid_device_t* device = native_rumble_target(slot, side); + return device != nullptr && device->report_parser.play_dual_rumble != nullptr; +} + +void cancel_native_cue_locked(NativeGamepadCue& cue) { cue.active = false; cue.result = -1; // The slot's last motor output remains owned until the timer replaces it. } -void refresh_dualsense_source_locked(bool reselection = false) { +void refresh_native_source_locked(bool reselection = false) { uint8_t selected = 0xff; for (uint8_t index = 0; index < kSlotCount; ++index) { - if (!eligible_dualsense(g_slots[index])) continue; + if (!eligible_native_gamepad(g_slots[index])) continue; if (selected != 0xff) { selected = 0xff; // Never blend or choose by connection order. break; } selected = index; } - if (!reselection && selected == g_dualsense_slot && + if (!reselection && selected == g_native_slot && (selected == 0xff || - g_slots[selected].connection_generation == g_dualsense_generation)) return; - for (DualSenseCue& cue : g_dualsense_cues) cancel_dualsense_cue_locked(cue); - g_dualsense_snapshot = {}; - g_dualsense_slot = selected; - g_dualsense_generation = 0; + g_slots[selected].connection_generation == g_native_generation)) return; + for (NativeGamepadCue& cue : g_native_cues) cancel_native_cue_locked(cue); + g_native_snapshot = {}; + g_native_slot = selected; + g_native_generation = 0; if (selected != 0xff) { BackendSlot& slot = g_slots[selected]; g_macro_capture.disconnect(selected, slot.connection_generation, time_us_32()); // A missed inactive snapshot must still retire the adapter's old epoch. - g_dualsense_generation = ++slot.connection_generation; + g_native_generation = ++slot.connection_generation; ++slot.state_generation; - slot.dualsense_motion.has_report = false; - slot.dualsense_motion.motion_valid = false; - slot.dualsense_motion.received_us = 0; - slot.dualsense_motion.motion_received_us = 0; + slot.native_motion = {}; } } -void retire_dualsense_slot(uint8_t index) { - if (g_dualsense_slot == index) { - g_dualsense_slot = 0xff; - g_dualsense_generation = 0; - g_dualsense_snapshot = {}; +void retire_native_slot(uint8_t index) { + if (g_native_slot == index) { + g_native_slot = 0xff; + g_native_generation = 0; + g_native_snapshot = {}; } - for (DualSenseCue& cue : g_dualsense_cues) + for (NativeGamepadCue& cue : g_native_cues) if (cue.slot == index) cue = {}; - g_slots[index].dualsense_motion = {}; - g_slots[index].dualsense_output = {}; + g_slots[index].native_motion = {}; + g_slots[index].native_output = {}; } -bool dualsense_cue_current(const DualSenseCue& cue) { - return cue.slot < kSlotCount && cue.slot == g_dualsense_slot && - cue.connection_generation == g_dualsense_generation && +bool native_cue_current(const NativeGamepadCue& cue) { + return cue.slot < kSlotCount && cue.slot == g_native_slot && + cue.connection_generation == g_native_generation && cue.connection_generation == g_slots[cue.slot].connection_generation; } #endif @@ -615,8 +645,8 @@ bool has_free_slot() { physical_count += slot.device != nullptr; physical_count += slot.companion != nullptr; } - #if SWITCH2_BRIDGE_DUALSENSE_INPUT - for (const auto* pending : g_dualsense_pending_devices) physical_count += pending != nullptr; + #if SWITCH2_BRIDGE_FULL_INPUT + for (const auto* pending : g_native_pending_devices) physical_count += pending != nullptr; #endif critical_section_exit(&g_state_lock); return physical_count < kSlotCount; @@ -661,7 +691,7 @@ int reserve_device_slot(uni_hid_device_t* device) { } const int tracked = slot_for_device(device); if (g_retired_devices[physical_index] == device) { -#if SWITCH2_BRIDGE_DUALSENSE_INPUT +#if SWITCH2_BRIDGE_FULL_INPUT return -1; #else if (uni_hid_parser_switch2_is_ble_device(device)) return -1; @@ -678,7 +708,7 @@ int reserve_device_slot(uni_hid_device_t* device) { return -1; } } -#if !SWITCH2_BRIDGE_DUALSENSE_INPUT +#if !SWITCH2_BRIDGE_FULL_INPUT if (g_slots[physical_index].device == nullptr) { return physical_index; } @@ -1374,8 +1404,8 @@ ConnectionStatus compute_connection_status() { all_ready = all_ready && (!has_device || slot.active); any_connecting = any_connecting || (!slot.active && has_device); } - #if SWITCH2_BRIDGE_DUALSENSE_INPUT - for (const auto* pending : g_dualsense_pending_devices) { + #if SWITCH2_BRIDGE_FULL_INPUT + for (const auto* pending : g_native_pending_devices) { if (pending != nullptr) { ++physical_count; all_ready = false; @@ -1437,23 +1467,28 @@ void publish_device_state(uint8_t slot, uni_hid_device_t* device, memcpy(g_wii_snapshot.gyro_q10, motion.gyro_q10, sizeof(motion.gyro_q10)); } #endif -#if SWITCH2_BRIDGE_DUALSENSE_INPUT - if (slot == g_dualsense_slot && target.dualsense_motion.has_report && - target.connection_generation == g_dualsense_generation) { - const DualSenseIngress& motion = target.dualsense_motion; - g_dualsense_snapshot.slot = slot; - g_dualsense_snapshot.controller = { +#if SWITCH2_BRIDGE_FULL_INPUT + if (slot == g_native_slot && target.native_motion.has_report && + target.connection_generation == g_native_generation) { + const NativeGamepadIngress& motion = target.native_motion; + g_native_snapshot.slot = slot; + g_native_snapshot.controller = { target.active, target.connection_generation, target.identity, target.pre_hotkey_button_mask, target.state, target.accelerometer, target.nunchuk_accelerometer}; - g_dualsense_snapshot.state_generation = target.state_generation; - g_dualsense_snapshot.received_us = motion.received_us; - g_dualsense_snapshot.battery = device->controller.battery; - g_dualsense_snapshot.motion_valid = motion.motion_valid; - g_dualsense_snapshot.motion_sequence = motion.motion_sequence; - g_dualsense_snapshot.motion_received_us = motion.motion_received_us; - memcpy(g_dualsense_snapshot.accel_q13, motion.accel_q13, sizeof(motion.accel_q13)); - memcpy(g_dualsense_snapshot.gyro_q10, motion.gyro_q10, sizeof(motion.gyro_q10)); + g_native_snapshot.state_generation = target.state_generation; + g_native_snapshot.received_us = motion.received_us; + g_native_snapshot.battery = device->controller.battery; + g_native_snapshot.requires_stationary_bias = + device->controller_type == CONTROLLER_TYPE_WiiController; + g_native_snapshot.accel_valid = motion.accel_valid; + g_native_snapshot.gyro_valid = motion.gyro_valid; + g_native_snapshot.accel_sequence = motion.accel_sequence; + g_native_snapshot.gyro_sequence = motion.gyro_sequence; + g_native_snapshot.accel_received_us = motion.accel_received_us; + g_native_snapshot.gyro_received_us = motion.gyro_received_us; + memcpy(g_native_snapshot.accel_q13, motion.accel_q13, sizeof(motion.accel_q13)); + memcpy(g_native_snapshot.gyro_q10, motion.gyro_q10, sizeof(motion.gyro_q10)); } #endif g_macro_capture.observe(slot, target.connection_generation, @@ -1468,8 +1503,8 @@ void publish_all_neutral() { wii_ir_pointer_reset(); #endif for (BackendSlot& slot : g_slots) { -#if SWITCH2_BRIDGE_DUALSENSE_INPUT - retire_dualsense_slot(static_cast(&slot - g_slots)); +#if SWITCH2_BRIDGE_FULL_INPUT + retire_native_slot(static_cast(&slot - g_slots)); #endif #ifdef SWITCH2_BRIDGE_WII_INPUT retire_wii_slot(static_cast(&slot - g_slots)); @@ -2007,8 +2042,8 @@ void reset_slot_hotkeys(BackendSlot& slot) { slot.connection_generation, time_us_32()); slot.wii_orientation_pending = false; slot.pending_wii_orientation = {}; -#if SWITCH2_BRIDGE_DUALSENSE_INPUT - retire_dualsense_slot(static_cast(&slot - g_slots)); +#if SWITCH2_BRIDGE_FULL_INPUT + retire_native_slot(static_cast(&slot - g_slots)); #endif #ifdef SWITCH2_BRIDGE_WII_INPUT retire_wii_slot(static_cast(&slot - g_slots)); @@ -2577,7 +2612,7 @@ void process_configuration_timer(btstack_timer_source_t* timer) { void dispatch_rumble(uni_hid_device_t* device, uint16_t duration_ms, uint8_t weak, uint8_t strong) { -#if SWITCH2_BRIDGE_DUALSENSE_INPUT +#if SWITCH2_BRIDGE_FULL_INPUT if (device->controller_type == CONTROLLER_TYPE_PS5Controller && device->report_parser.parse_input_report == uni_hid_parser_ds5_parse_input_report) { // Local feedback shares the bounded writer. Its stale compatibility @@ -2726,9 +2761,10 @@ void dispatch_wii_cue(const WiiCueDispatch& command) { } #endif -#if SWITCH2_BRIDGE_DUALSENSE_INPUT -struct DualSenseCueDispatch { +#if SWITCH2_BRIDGE_FULL_INPUT +struct NativeGamepadCueDispatch { uni_hid_device_t* device = nullptr; + uni_hid_device_t* companion = nullptr; uint32_t connection_generation = 0; uint32_t prepared_ms = 0; uint64_t token[2]{}; @@ -2737,27 +2773,27 @@ struct DualSenseCueDispatch { uint8_t slot = 0xff; }; -// One DS5 driver timer controls both motors. Recompute a combined packet at -// each boundary; use the shortest ON remainder, then refresh the surviving -// side. Absolute cue timelines skip missed pulses, never accumulate a backlog. -bool prepare_dualsense_cues(uint8_t index, uint32_t now_ms, +// Source drivers use a shared finite timer (or one per paired half). Recompute +// both contributions at each boundary; the shortest ON remainder protects a +// mono actuator too. Absolute timelines skip missed pulses, never queue them. +bool prepare_native_cues(uint8_t index, uint32_t now_ms, bool local_active, bool local_dispatch, - DualSenseCueDispatch* command) { + NativeGamepadCueDispatch* command) { BackendSlot& slot = g_slots[index]; - DualSenseMotorOutput& previous = slot.dualsense_output; + NativeGamepadMotorOutput& previous = slot.native_output; bool busy = false; bool pending = false; uint16_t duration = UINT16_MAX; uint8_t magnitude[2]{}; for (uint8_t side = 0; side < 2; ++side) { - DualSenseCue& cue = g_dualsense_cues[side]; + NativeGamepadCue& cue = g_native_cues[side]; if (cue.slot != index) continue; - if (!dualsense_cue_current(cue) || - (cue.result == 0 && now_ms - cue.requested_ms >= kDualSenseCueDeadlineMs) || - (cue.active && now_ms - cue.started_ms >= kDualSenseCueDeadlineMs)) - cancel_dualsense_cue_locked(cue); + if (!native_cue_current(cue) || + (cue.result == 0 && now_ms - cue.requested_ms >= kNativeGamepadCueDeadlineMs) || + (cue.active && now_ms - cue.started_ms >= kNativeGamepadCueDeadlineMs)) + cancel_native_cue_locked(cue); if (local_active || local_dispatch) { - if (cue.active) cancel_dualsense_cue_locked(cue); + if (cue.active) cancel_native_cue_locked(cue); busy |= cue.result == 0; continue; } @@ -2766,7 +2802,7 @@ bool prepare_dualsense_cues(uint8_t index, uint32_t now_ms, command->token[side] = cue.token; pending |= cue.result == 0; if (cue.sample_id == 0) continue; - const DualSenseCuePattern& pattern = kDualSenseCuePatterns[cue.sample_id]; + const NativeGamepadCuePattern& pattern = kNativeGamepadCuePatterns[cue.sample_id]; uint32_t elapsed = cue.active ? now_ms - cue.started_ms : 0; uint8_t phase = 0; while (phase < pattern.count && elapsed >= pattern.phases_ms[phase]) @@ -2795,13 +2831,14 @@ bool prepare_dualsense_cues(uint8_t index, uint32_t now_ms, (duration != 0 && deadline != previous.deadline_ms); if (!pending && !changed) return busy || previous.owned; if (!slot.active || slot.device == nullptr || - slot.device->report_parser.play_dual_rumble == nullptr) { - for (DualSenseCue& cue : g_dualsense_cues) - if (cue.slot == index) cancel_dualsense_cue_locked(cue); + (!native_rumble_capable(slot, 0) && !native_rumble_capable(slot, 1))) { + for (NativeGamepadCue& cue : g_native_cues) + if (cue.slot == index) cancel_native_cue_locked(cue); previous = {}; return false; } command->device = slot.device; + command->companion = slot.companion; command->connection_generation = slot.connection_generation; command->prepared_ms = now_ms; command->duration_ms = duration; @@ -2809,52 +2846,88 @@ bool prepare_dualsense_cues(uint8_t index, uint32_t now_ms, command->magnitude[1] = magnitude[1]; command->slot = index; for (uint8_t side = 0; side < 2; ++side) - if (command->token[side] != 0) g_dualsense_cues[side].in_flight = true; + if (command->token[side] != 0) g_native_cues[side].in_flight = true; return true; } -void dispatch_dualsense_cues(const DualSenseCueDispatch& command) { +bool submit_native_rumble(uni_hid_device_t* device, uint16_t duration, + uint8_t right, uint8_t left) { + if (device == nullptr || device->report_parser.play_dual_rumble == nullptr) + return false; + if (device->controller_type == CONTROLLER_TYPE_PS5Controller && + device->report_parser.parse_input_report == uni_hid_parser_ds5_parse_input_report) { + return uni_hid_parser_ds5_bridge_rumble(device, duration, right, left); + } + // Existing finite-duration dispatch is the strongest observable result + // most drivers expose. It is not transport acceptance or a remote ACK. + dispatch_rumble(device, duration, right, left); + return true; +} + +void dispatch_native_cues(const NativeGamepadCueDispatch& command) { if (command.device == nullptr) return; - critical_section_enter_blocking(&g_state_lock); BackendSlot& slot = g_slots[command.slot]; - bool current = slot.active && slot.device == command.device && - slot.connection_generation == command.connection_generation; - for (uint8_t side = 0; side < 2; ++side) { - const DualSenseCue& cue = g_dualsense_cues[side]; - if (command.token[side] != 0) - current &= cue.token == command.token[side] && cue.in_flight && - cue.result != -1 && dualsense_cue_current(cue); - } - critical_section_exit(&g_state_lock); - // BTstack owns parser/lifecycle callbacks (Core 0 in HUB). No backend lock - // crosses a driver call. Account for time spent in higher-priority output. - const uint32_t dispatch_ms = btstack_run_loop_get_time_ms(); - const uint32_t delay = dispatch_ms - command.prepared_ms; - current &= delay < kRumblePollIntervalMs; - const uint16_t duration = command.duration_ms > delay - ? static_cast(command.duration_ms - delay) : 0; - current &= command.duration_ms == 0 || duration != 0; - if (current) { - current = uni_hid_parser_ds5_bridge_rumble( - command.device, duration, command.magnitude[0], command.magnitude[1]); - if (current) __atomic_add_fetch(&g_rumble_dispatches, 1, __ATOMIC_RELAXED); + const bool paired = command.companion != nullptr; + bool submitted[2]{}; + uint32_t dispatch_ms = btstack_run_loop_get_time_ms(); + for (uint8_t target = 0; target < (paired ? 2 : 1); ++target) { + critical_section_enter_blocking(&g_state_lock); + bool current = slot.active && slot.device == command.device && + slot.companion == command.companion && + slot.connection_generation == command.connection_generation; + for (uint8_t side = 0; side < 2; ++side) { + if (paired && side != target) continue; + const NativeGamepadCue& cue = g_native_cues[side]; + if (command.token[side] != 0) + current &= cue.token == command.token[side] && cue.in_flight && + cue.result != -1 && native_cue_current(cue); + } + critical_section_exit(&g_state_lock); + // No backend lock crosses a driver call. Recheck every real target: + // cancel/reselection or a stall during R dispatch must not send stale L. + dispatch_ms = btstack_run_loop_get_time_ms(); + const uint32_t delay = dispatch_ms - command.prepared_ms; + const uint16_t duration = command.duration_ms > delay + ? static_cast(command.duration_ms - delay) : 0; + current &= delay < kRumblePollIntervalMs && + (command.duration_ms == 0 || duration != 0); + if (!current) continue; + uni_hid_device_t* device = paired && target == 0 + ? command.companion : command.device; + const uint8_t right = command.magnitude[paired ? target : 0]; + const uint8_t left = command.magnitude[paired ? target : 1]; + if (!submit_native_rumble( + device, (right | left) == 0 ? 0 : duration, right, left)) continue; + __atomic_add_fetch(&g_rumble_dispatches, 1, __ATOMIC_RELAXED); + if (paired) submitted[target] = true; + else submitted[0] = submitted[1] = true; + + critical_section_enter_blocking(&g_state_lock); + if (slot.active && slot.device == command.device && + slot.companion == command.companion) { + // Retain each actual submission even if USB canceled/reselected + // during its driver call, or the other half cannot be submitted. + NativeGamepadMotorOutput& output = slot.native_output; + if (paired) output.magnitude[target] = command.magnitude[target]; + else { + output.magnitude[0] = command.magnitude[0]; + output.magnitude[1] = command.magnitude[1]; + } + output.owned = (output.magnitude[0] | output.magnitude[1]) != 0; + output.deadline_ms = output.owned + ? command.prepared_ms + command.duration_ms : 0; + } + critical_section_exit(&g_state_lock); } critical_section_enter_blocking(&g_state_lock); - if (current && slot.active && slot.device == command.device) { - // Retain ownership even if USB canceled/reselected during dispatch: - // the next timer must stop/replace exactly the packet just submitted. - slot.dualsense_output = { - duration == 0 ? 0 : dispatch_ms + duration, - {command.magnitude[0], command.magnitude[1]}, duration != 0}; - } for (uint8_t side = 0; side < 2; ++side) { - DualSenseCue& cue = g_dualsense_cues[side]; + NativeGamepadCue& cue = g_native_cues[side]; if (command.token[side] == 0 || cue.token != command.token[side]) continue; cue.in_flight = false; - if (!dualsense_cue_current(cue) || - (cue.result == 0 && dispatch_ms - cue.requested_ms >= kDualSenseCueDeadlineMs)) { - cancel_dualsense_cue_locked(cue); - } else if (current && cue.result == 0) { + if (!native_cue_current(cue) || + (cue.result == 0 && dispatch_ms - cue.requested_ms >= kNativeGamepadCueDeadlineMs)) { + cancel_native_cue_locked(cue); + } else if (submitted[side] && cue.result == 0) { cue.result = 1; // Accepted source submission, never a native ACK. cue.started_ms = command.prepared_ms; cue.active = cue.sample_id != 0; @@ -2890,6 +2963,9 @@ void process_wii_orientation(uint8_t slot_index) { invalidate_slot(slot); slot.gamepad = {}; slot.extra_buttons = 0; +#if SWITCH2_BRIDGE_FULL_INPUT + refresh_native_source_locked(); +#endif critical_section_exit(&g_state_lock); // The setter can synchronously re-enter the platform ready callback, so @@ -3020,8 +3096,8 @@ void process_rumble_timer(btstack_timer_source_t* timer) { #ifdef SWITCH2_BRIDGE_WII_INPUT WiiCueDispatch wii_cue_dispatch{}; #endif -#if SWITCH2_BRIDGE_DUALSENSE_INPUT - DualSenseCueDispatch dualsense_dispatch{}; +#if SWITCH2_BRIDGE_FULL_INPUT + NativeGamepadCueDispatch native_dispatch{}; #endif critical_section_enter_blocking(&g_state_lock); @@ -3157,18 +3233,18 @@ void process_rumble_timer(btstack_timer_source_t* timer) { slot_index, now_ms, local_feedback_active, profile_rumble_dispatch || feedback_dispatch, &wii_cue_dispatch); #endif -#if SWITCH2_BRIDGE_DUALSENSE_INPUT - const bool dualsense_owns_rumble = prepare_dualsense_cues( +#if SWITCH2_BRIDGE_FULL_INPUT + const bool native_owns_rumble = prepare_native_cues( slot_index, now_ms, local_feedback_active, - profile_rumble_dispatch || feedback_dispatch, &dualsense_dispatch); + profile_rumble_dispatch || feedback_dispatch, &native_dispatch); #endif if (!profile_rumble_dispatch && !feedback_dispatch && !local_feedback_active && slot.rumble_pending #ifdef SWITCH2_BRIDGE_WII_INPUT && !wii_cue_owns_rumble #endif -#if SWITCH2_BRIDGE_DUALSENSE_INPUT - && !dualsense_owns_rumble +#if SWITCH2_BRIDGE_FULL_INPUT + && !native_owns_rumble #endif #ifdef SWITCH_PICO_HAPTICS_EXPERIMENT && !(xinput_host_mode && @@ -3246,8 +3322,8 @@ void process_rumble_timer(btstack_timer_source_t* timer) { #ifdef SWITCH2_BRIDGE_WII_INPUT dispatch_wii_cue(wii_cue_dispatch); #endif -#if SWITCH2_BRIDGE_DUALSENSE_INPUT - dispatch_dualsense_cues(dualsense_dispatch); +#if SWITCH2_BRIDGE_FULL_INPUT + dispatch_native_cues(native_dispatch); #endif } @@ -3459,6 +3535,9 @@ bool merge_joycon_slots(int owner_index, int joining_index, {owner.companion, static_cast(owner_index)}; g_joycon_pair_hints[physical_index_for_device(owner.companion)] = {owner.device, static_cast(owner_index)}; +#if SWITCH2_BRIDGE_FULL_INPUT + refresh_native_source_locked(); +#endif critical_section_exit(&g_state_lock); apply_slot_lighting(static_cast(owner_index), owner.device); apply_slot_lighting(static_cast(owner_index), owner.companion); @@ -3529,6 +3608,9 @@ bool split_joycon_slot(uint8_t owner_index, bool gesture = false) { kProfileFeedbackWeakMagnitude, kProfileFeedbackStrongMagnitude); } +#if SWITCH2_BRIDGE_FULL_INPUT + refresh_native_source_locked(); +#endif critical_section_exit(&g_state_lock); apply_slot_lighting(owner_index, owner.device); apply_slot_lighting(static_cast(right_index), right.device); @@ -3737,7 +3819,7 @@ void platform_on_device_connected(uni_hid_device_t* device) { uni_hid_device_disconnect(device); return; } -#if SWITCH2_BRIDGE_DUALSENSE_INPUT +#if SWITCH2_BRIDGE_FULL_INPUT // Classification completes after connection. Reserve transport capacity, // not a player/color slot, until a supported source reaches ready. const int pending_index = physical_index_for_device(device); @@ -3748,7 +3830,11 @@ void platform_on_device_connected(uni_hid_device_t* device) { critical_section_enter_blocking(&g_state_lock); g_retired_devices[pending_index] = nullptr; g_switch2_interval_requests[pending_index] = {}; - if (slot_for_device(device) < 0) g_dualsense_pending_devices[pending_index] = device; + if (slot_for_device(device) < 0) { + g_native_reports[pending_index] = {}; + reset_joycon_connection(device); + } + if (slot_for_device(device) < 0) g_native_pending_devices[pending_index] = device; critical_section_exit(&g_state_lock); recompute_connection_status(); #else @@ -3780,23 +3866,30 @@ void platform_on_device_connected(uni_hid_device_t* device) { } void platform_on_device_disconnected(uni_hid_device_t* device) { -#if SWITCH2_BRIDGE_DUALSENSE_INPUT +#if SWITCH2_BRIDGE_FULL_INPUT const int pending_index = physical_index_for_device(device); if (pending_index >= 0) { critical_section_enter_blocking(&g_state_lock); - if (g_dualsense_pending_devices[pending_index] == device) - g_dualsense_pending_devices[pending_index] = nullptr; + if (g_native_pending_devices[pending_index] == device) + g_native_pending_devices[pending_index] = nullptr; g_retired_devices[pending_index] = device; critical_section_exit(&g_state_lock); } #endif const int slot_index = slot_for_device(device); if (slot_index < 0) { - #if SWITCH2_BRIDGE_DUALSENSE_INPUT + #if SWITCH2_BRIDGE_FULL_INPUT recompute_connection_status(); #endif return; } +#if SWITCH2_BRIDGE_FULL_INPUT + // DS4/PSMove and other finite-rumble drivers keep timers in parser_data. + // Retire those timers before Bluepad32 reuses that memory. Call the real + // driver directly: a feedback scheduler must not defer this local teardown. + if (device->report_parser.play_dual_rumble != nullptr) + device->report_parser.play_dual_rumble(device, 0, 0, 0, 0); +#endif #ifdef SWITCH_PICO_NATIVE_SWITCH_RUMBLE switch_native_output_detach(device); #endif @@ -3826,8 +3919,8 @@ void platform_on_device_disconnected(uni_hid_device_t* device) { } else { release_slot(slot); } -#if SWITCH2_BRIDGE_DUALSENSE_INPUT - refresh_dualsense_source_locked(); +#if SWITCH2_BRIDGE_FULL_INPUT + refresh_native_source_locked(); #endif critical_section_exit(&g_state_lock); clear_ble_identity_for_device(device); @@ -3863,8 +3956,8 @@ uni_error_t platform_on_device_ready(uni_hid_device_t* device) { uni_hid_device_t* companion = nullptr; ControllerIdentity connection_identity = identity_for_device(device); critical_section_enter_blocking(&g_state_lock); - #if SWITCH2_BRIDGE_DUALSENSE_INPUT - if (!dualsense_source_matches(device)) { + #if SWITCH2_BRIDGE_FULL_INPUT + if (!native_device_allowed(device)) { critical_section_exit(&g_state_lock); return UNI_ERROR_INVALID_CONTROLLER; } @@ -3874,10 +3967,10 @@ uni_error_t platform_on_device_ready(uni_hid_device_t* device) { critical_section_exit(&g_state_lock); return UNI_ERROR_NO_SLOTS; } - #if SWITCH2_BRIDGE_DUALSENSE_INPUT + #if SWITCH2_BRIDGE_FULL_INPUT const int pending_index = physical_index_for_device(device); - if (g_dualsense_pending_devices[pending_index] == device) - g_dualsense_pending_devices[pending_index] = nullptr; + if (g_native_pending_devices[pending_index] == device) + g_native_pending_devices[pending_index] = nullptr; #endif BackendSlot& pending = g_slots[slot_index]; if (!pending.active) { @@ -3901,8 +3994,8 @@ uni_error_t platform_on_device_ready(uni_hid_device_t* device) { } became_active = true; } -#if SWITCH2_BRIDGE_DUALSENSE_INPUT - refresh_dualsense_source_locked(); +#if SWITCH2_BRIDGE_FULL_INPUT + refresh_native_source_locked(); #endif const BackendSlot& current = g_slots[slot_index]; owner = current.device; @@ -3968,28 +4061,45 @@ void platform_on_controller_data(uni_hid_device_t* device, critical_section_exit(&g_state_lock); return; } -#if SWITCH2_BRIDGE_DUALSENSE_INPUT - if (device->controller_type == CONTROLLER_TYPE_PS5Controller) { - uni_ds5_bridge_snapshot_t sensor{}; - DualSenseIngress& motion = slot.dualsense_motion; - if (!uni_hid_parser_ds5_bridge_snapshot(device, &sensor) || - sensor.report_sequence == motion.report_sequence) { - critical_section_exit(&g_state_lock); - return; +#if SWITCH2_BRIDGE_FULL_INPUT + const int physical_index = physical_index_for_device(device); + if (physical_index < 0) { + critical_section_exit(&g_state_lock); + return; + } + uni_native_motion_snapshot_t sensor{}; + uni_hid_parser_native_motion_snapshot(device, &sensor); + NativeGamepadReportIngress& ingress = g_native_reports[physical_index]; + if (ingress.device != device) ingress = {device, 0, 0, 0}; + if (sensor.report_tracked && + (!sensor.report_valid || sensor.report_sequence == ingress.report_sequence)) { + critical_section_exit(&g_state_lock); + return; + } + ingress.report_sequence = sensor.report_sequence; + NativeGamepadIngress& motion = slot.native_motion; + motion.has_report = true; + motion.received_us = time_us_32(); + // Read only the reporting physical device. The right half is the existing + // pair's motion owner; left controls must not refresh or invalidate its IMU. + if (slot.companion == nullptr || slot.companion == device) { + if (!sensor.accel_valid) motion.accel_valid = false; + else if (sensor.accel_sequence != ingress.accel_sequence) { + motion.accel_valid = true; + motion.accel_sequence = sensor.accel_sequence; + motion.accel_received_us = motion.received_us; + memcpy(motion.accel_q13, sensor.accel_q13, sizeof(motion.accel_q13)); } - motion.has_report = true; - motion.report_sequence = sensor.report_sequence; - motion.received_us = time_us_32(); - motion.motion_valid = sensor.motion_valid && - sensor.motion_sequence != motion.motion_sequence; - if (sensor.motion_valid && sensor.motion_sequence != motion.motion_sequence) { - motion.motion_sequence = sensor.motion_sequence; - motion.motion_received_us = motion.received_us; - motion.motion_valid = true; - memcpy(motion.accel_q13, controller->gamepad.accel, sizeof(motion.accel_q13)); - memcpy(motion.gyro_q10, controller->gamepad.gyro, sizeof(motion.gyro_q10)); + if (!sensor.gyro_valid) motion.gyro_valid = false; + else if (sensor.gyro_sequence != ingress.gyro_sequence) { + motion.gyro_valid = true; + motion.gyro_sequence = sensor.gyro_sequence; + motion.gyro_received_us = motion.received_us; + memcpy(motion.gyro_q10, sensor.gyro_q10, sizeof(motion.gyro_q10)); } } + if (sensor.accel_valid) ingress.accel_sequence = sensor.accel_sequence; + if (sensor.gyro_valid) ingress.gyro_sequence = sensor.gyro_sequence; #endif #ifdef SWITCH_PICO_WII_IR uni_wii_ir_snapshot_t infrared{}; @@ -4323,8 +4433,9 @@ void bluepad32_input_backend_init() { g_joycon_pair_hints[slot_index] = {}; g_joycon_gestures[slot_index] = {}; g_joycon_overrides[slot_index] = {}; - #if SWITCH2_BRIDGE_DUALSENSE_INPUT - g_dualsense_pending_devices[slot_index] = nullptr; + #if SWITCH2_BRIDGE_FULL_INPUT + g_native_pending_devices[slot_index] = nullptr; + g_native_reports[slot_index] = {}; #endif } g_joycon_mode = JoyConMode::kPaired; @@ -4531,42 +4642,42 @@ void bluepad32_input_backend_snapshot(uint8_t slot_index, g_last_snapshot_generation[slot_index] = state_generation; } -#if SWITCH2_BRIDGE_DUALSENSE_INPUT -void bluepad32_input_backend_select_dualsense_source(const uint8_t address[6]) { +#if SWITCH2_BRIDGE_FULL_INPUT +void bluepad32_input_backend_select_native_source(const uint8_t address[6]) { if (!g_initialized) return; critical_section_enter_blocking(&g_state_lock); - g_dualsense_explicit_address = address != nullptr; - if (address != nullptr) memcpy(g_dualsense_address, address, 6); - else memset(g_dualsense_address, 0, sizeof(g_dualsense_address)); - refresh_dualsense_source_locked(true); + g_native_explicit_address = address != nullptr; + if (address != nullptr) memcpy(g_native_address, address, 6); + else memset(g_native_address, 0, sizeof(g_native_address)); + refresh_native_source_locked(true); critical_section_exit(&g_state_lock); } -void bluepad32_input_backend_dualsense_snapshot(Bluepad32DualSenseBridgeSnapshot* output) { +void bluepad32_input_backend_native_snapshot(Bluepad32NativeGamepadSnapshot* output) { if (output == nullptr) return; *output = {}; if (!g_initialized) return; critical_section_enter_blocking(&g_state_lock); - *output = g_dualsense_snapshot; + *output = g_native_snapshot; critical_section_exit(&g_state_lock); } -bool bluepad32_input_backend_dualsense_sample_request( +bool bluepad32_input_backend_native_sample_request( uint8_t instance, uint8_t sample_id, uint64_t* token) { if (token == nullptr) return false; *token = 0; if (!g_initialized || instance >= 2 || sample_id >= 8) return false; critical_section_enter_blocking(&g_state_lock); - DualSenseCue& cue = g_dualsense_cues[instance]; - const uint8_t index = g_dualsense_slot; - const bool accepted = index < kSlotCount && g_next_dualsense_token != 0 && - g_slots[index].device->report_parser.play_dual_rumble != nullptr && + NativeGamepadCue& cue = g_native_cues[instance]; + const uint8_t index = g_native_slot; + const bool accepted = index < kSlotCount && g_next_native_token != 0 && + native_rumble_capable(g_slots[index], instance) && !cue.in_flight && (sample_id == 0 || (cue.result != 0 && !cue.active)); if (accepted) { cue = {}; - cue.token = g_next_dualsense_token++; + cue.token = g_next_native_token++; cue.slot = index; - cue.connection_generation = g_dualsense_generation; + cue.connection_generation = g_native_generation; cue.requested_ms = btstack_run_loop_get_time_ms(); cue.sample_id = sample_id; cue.result = 0; @@ -4576,16 +4687,16 @@ bool bluepad32_input_backend_dualsense_sample_request( return accepted; } -int bluepad32_input_backend_dualsense_sample_result(uint8_t instance, uint64_t token) { +int bluepad32_input_backend_native_sample_result(uint8_t instance, uint64_t token) { if (!g_initialized || instance >= 2 || token == 0) return -1; critical_section_enter_blocking(&g_state_lock); - DualSenseCue& cue = g_dualsense_cues[instance]; + NativeGamepadCue& cue = g_native_cues[instance]; int result = -1; if (cue.token == token && !cue.consumed) { - if (!dualsense_cue_current(cue) || + if (!native_cue_current(cue) || (cue.result == 0 && - btstack_run_loop_get_time_ms() - cue.requested_ms >= kDualSenseCueDeadlineMs)) - cancel_dualsense_cue_locked(cue); + btstack_run_loop_get_time_ms() - cue.requested_ms >= kNativeGamepadCueDeadlineMs)) + cancel_native_cue_locked(cue); result = cue.result; if (result != 0) cue.consumed = true; } @@ -4593,10 +4704,10 @@ int bluepad32_input_backend_dualsense_sample_result(uint8_t instance, uint64_t t return result; } -void bluepad32_input_backend_dualsense_sample_cancel(uint8_t instance) { +void bluepad32_input_backend_native_sample_cancel(uint8_t instance) { if (!g_initialized || instance >= 2) return; critical_section_enter_blocking(&g_state_lock); - cancel_dualsense_cue_locked(g_dualsense_cues[instance]); + cancel_native_cue_locked(g_native_cues[instance]); critical_section_exit(&g_state_lock); } #endif diff --git a/src/firmware/input/bluepad32_input_backend.h b/src/firmware/input/bluepad32_input_backend.h index 7786fd2..7979951 100644 --- a/src/firmware/input/bluepad32_input_backend.h +++ b/src/firmware/input/bluepad32_input_backend.h @@ -108,34 +108,39 @@ int bluepad32_input_backend_wii_sample_result(uint64_t token); void bluepad32_input_backend_wii_sample_cancel(); #endif -#if SWITCH2_BRIDGE_DUALSENSE_INPUT -// One physical DualSense/Edge, calibrated SDL axes before legacy conversion. -// Receipt times and motion sequences advance only on admitted parser reports. -struct Bluepad32DualSenseBridgeSnapshot { +#if SWITCH2_BRIDGE_FULL_INPUT +// One logical gamepad, calibrated SDL axes before legacy int16 conversion. +// Sensor receipt times advance independently, only on actual parser ingress. +struct Bluepad32NativeGamepadSnapshot { uint8_t slot = 0xff; Bluepad32SlotSnapshot controller{}; uint32_t state_generation = 0; uint32_t received_us = 0; uint8_t battery = 0; - bool motion_valid = false; - uint32_t motion_sequence = 0; - uint32_t motion_received_us = 0; + bool accel_valid = false; + bool gyro_valid = false; + bool requires_stationary_bias = false; + uint32_t accel_sequence = 0; + uint32_t gyro_sequence = 0; + uint32_t accel_received_us = 0; + uint32_t gyro_received_us = 0; int32_t accel_q13[3]{}; int32_t gyro_q10[3]{}; }; -// nullptr selects the uniquely eligible ready DualSense; ambiguity fails closed. +// nullptr selects the uniquely eligible ready logical gamepad; ambiguity fails closed. // Reselection invalidates input and cue tokens without modifying pairings. -void bluepad32_input_backend_select_dualsense_source(const uint8_t address[6]); -void bluepad32_input_backend_dualsense_snapshot(Bluepad32DualSenseBridgeSnapshot* output); -// Instance 0 is R/right motor, 1 is L/left motor. Samples 0..7 are bounded -// compatibility-motor cues, not HD haptics. Zero stops only the requested side. +void bluepad32_input_backend_select_native_source(const uint8_t address[6]); +void bluepad32_input_backend_native_snapshot(Bluepad32NativeGamepadSnapshot* output); +// Instance 0 is R, 1 is L. Samples 0..7 are bounded compatibility cues, not HD +// haptics. A side stop removes only that side's contribution. Mono drivers combine +// both contributions on their one actuator; this does not promise stereo output. // Result: 0 pending, 1 source-driver dispatch, -1 retired/failed/consumed. // Dispatch is NOT an application ACK or proof of physical actuator onset. -bool bluepad32_input_backend_dualsense_sample_request( +bool bluepad32_input_backend_native_sample_request( uint8_t instance, uint8_t sample_id, uint64_t* token); -int bluepad32_input_backend_dualsense_sample_result(uint8_t instance, uint64_t token); -void bluepad32_input_backend_dualsense_sample_cancel(uint8_t instance); +int bluepad32_input_backend_native_sample_result(uint8_t instance, uint64_t token); +void bluepad32_input_backend_native_sample_cancel(uint8_t instance); #endif // Side-effect-free raw input snapshot for management telemetry. Unlike the diff --git a/tests/bluepad32_native_stubs/parser/uni_hid_parser_ds5.h b/tests/bluepad32_native_stubs/parser/uni_hid_parser_ds5.h index d9d5686..003d8b1 100644 --- a/tests/bluepad32_native_stubs/parser/uni_hid_parser_ds5.h +++ b/tests/bluepad32_native_stubs/parser/uni_hid_parser_ds5.h @@ -3,12 +3,5 @@ #include #include -typedef struct { - uint32_t report_sequence; - uint32_t motion_sequence; - bool motion_valid; -} uni_ds5_bridge_snapshot_t; - void uni_hid_parser_ds5_parse_input_report(uni_hid_device_t*, const uint8_t*, uint16_t); bool uni_hid_parser_ds5_bridge_rumble(uni_hid_device_t*, uint16_t, uint8_t, uint8_t); -bool uni_hid_parser_ds5_bridge_snapshot(uni_hid_device_t*, uni_ds5_bridge_snapshot_t*); diff --git a/tests/bluepad32_native_stubs/uni.h b/tests/bluepad32_native_stubs/uni.h index 57fa5d5..c6ae0ca 100644 --- a/tests/bluepad32_native_stubs/uni.h +++ b/tests/bluepad32_native_stubs/uni.h @@ -85,8 +85,12 @@ enum { typedef enum { CONTROLLER_TYPE_UnknownController = 0, + CONTROLLER_TYPE_XBoxOneController = 32, + CONTROLLER_TYPE_PS4Controller = 34, CONTROLLER_TYPE_WiiController = 35, - CONTROLLER_TYPE_PS5Controller = 46, + CONTROLLER_TYPE_SwitchProController = 38, + CONTROLLER_TYPE_PS5Controller = 45, + CONTROLLER_TYPE_PSMoveController = 56, } uni_controller_type_t; typedef enum { diff --git a/tests/dualsense_backend_test.cpp b/tests/dualsense_backend_test.cpp deleted file mode 100644 index 3f1dfec..0000000 --- a/tests/dualsense_backend_test.cpp +++ /dev/null @@ -1,291 +0,0 @@ -// Reuse the backend's transport/storage fixture; these scenarios exercise only -// the native DualSense contract, not a second implementation of its scheduler. -#define main backend_fixture_main -#include "bluepad32_backend_lifecycle_test.cpp" -#undef main - -namespace { -struct SensorFixture { - uni_hid_device_t* device = nullptr; - uni_ds5_bridge_snapshot_t metadata{}; - bool valid = false; -}; -SensorFixture sensors[4]; -void (*during_dualsense_dispatch)() = nullptr; -bool dualsense_transport_available = true; - -void observe_dualsense_rumble(uni_hid_device_t* target, uint16_t delay, - uint16_t duration, uint8_t right, uint8_t left) { - require(state_lock_depth == 0, "DS5 driver dispatch must not hold the shared state lock"); - play_rumble(target, delay, duration, right, left); - if (during_dualsense_dispatch) during_dualsense_dispatch(); -} - -uni_hid_device_t dualsense(int index) { - auto result = device(index, true, UNI_BT_CONN_PROTOCOL_BR_EDR); - result.vendor_id = 0x054c; - result.product_id = index == 0 ? 0x0ce6 : 0x0df2; - result.controller_type = CONTROLLER_TYPE_PS5Controller; - result.report_parser.parse_input_report = uni_hid_parser_ds5_parse_input_report; - result.report_parser.play_dual_rumble = observe_dualsense_rumble; - return result; -} - -void report_dualsense(uni_hid_device_t& pad, bool fresh_motion = true) { - SensorFixture& sensor = sensors[pad.idx]; - sensor.device = &pad; - sensor.valid = true; - ++sensor.metadata.report_sequence; - if (fresh_motion) ++sensor.metadata.motion_sequence; - sensor.metadata.motion_valid = fresh_motion; - pad.controller.klass = UNI_CONTROLLER_CLASS_GAMEPAD; - pad.controller.gamepad.buttons = BUTTON_A | BUTTON_SHOULDER_L; - pad.controller.gamepad.accel[1] = 8193; - pad.controller.gamepad.gyro[2] = -123456; - pad.controller.battery = 176; - platform_on_controller_data(&pad, &pad.controller); -} - -Bluepad32DualSenseBridgeSnapshot bridge_snapshot() { - Bluepad32DualSenseBridgeSnapshot result{}; - bluepad32_input_backend_dualsense_snapshot(&result); - return result; -} - -void source_isolation() { - start_pairing_backend(); - auto ordinary = device(0, true, UNI_BT_CONN_PROTOCOL_BR_EDR); - ordinary.vendor_id = 0x054c; - ordinary.product_id = 0x0ce6; - require(platform_on_device_ready(&ordinary) == UNI_ERROR_INVALID_CONTROLLER, - "an unsupported parser must not enter the dedicated DualSense output slots"); - bluepad32_input_backend_select_dualsense_source(ordinary.conn.btaddr); - require(!bridge_snapshot().controller.active, "VID/PID/address alone must never select a non-PS5 parser"); - platform_on_device_disconnected(&ordinary); - bluepad32_input_backend_select_dualsense_source(nullptr); - auto first = dualsense(0); - auto second = dualsense(1); - require(platform_on_device_ready(&first) == UNI_ERROR_SUCCESS, "first DS5 must connect"); - now_ms = 100; - report_dualsense(first); - const auto initial = bridge_snapshot(); - require(initial.slot == 0 && initial.controller.active && initial.controller.state.button_south && - initial.battery == 176 && initial.motion_valid && initial.accel_q13[1] == 8193 && - initial.gyro_q10[2] == -123456 && initial.motion_received_us == 100000, - "native snapshot must preserve coherent physical controls and calibrated precision"); - now_ms = 120; - platform_on_controller_data(&first, &first.controller); - bluepad32_input_backend_report_sent(0); - auto snapshot = bridge_snapshot(); - require(snapshot.state_generation == initial.state_generation && snapshot.received_us == 100000 && - snapshot.motion_sequence == initial.motion_sequence, - "polling/cached callbacks and USB consumption must not freshen input or motion"); - report_dualsense(first, false); - snapshot = bridge_snapshot(); - require(snapshot.received_us == 120000 && snapshot.motion_received_us == 100000 && !snapshot.motion_valid, - "a controls-only admission must not refresh a duplicate sensor timestamp"); - sensors[0].valid = false; - first.controller.gamepad.buttons = 0; - platform_on_controller_data(&first, &first.controller); - require(bridge_snapshot().controller.state.button_south, - "malformed parser input must not publish an invented button release"); - uint64_t old_token; - require(bluepad32_input_backend_dualsense_sample_request(0, 1, &old_token), "first source cue must queue"); - require(platform_on_device_ready(&second) == UNI_ERROR_SUCCESS, "Edge must connect"); - report_dualsense(second); - require(!bridge_snapshot().controller.active && - bluepad32_input_backend_dualsense_sample_result(0, old_token) == -1, - "auto ambiguity must fail closed and retire source work immediately"); - platform_on_device_disconnected(&second); - platform_on_controller_data(&first, &first.controller); - require(!bridge_snapshot().controller.active, "returning to a source cannot resurrect cached state"); - report_dualsense(first); - snapshot = bridge_snapshot(); - require(snapshot.controller.active && snapshot.controller.connection_generation != initial.controller.connection_generation, - "a missed ambiguous interval still needs a new adapter epoch"); - platform_on_device_connected(&second); - require(platform_on_device_ready(&second) == UNI_ERROR_SUCCESS, "Edge reconnect must succeed"); - bluepad32_input_backend_select_dualsense_source(first.conn.btaddr); - report_dualsense(first); - report_dualsense(second); - require(bridge_snapshot().slot == 0, "explicit source must ignore another live PS5"); - platform_on_device_disconnected(&first); - require(!bridge_snapshot().controller.active, "disconnect must not migrate an explicit source"); -} - -void stable_logical_slot() { - start_pairing_backend(); - auto unrelated = device(0, true, UNI_BT_CONN_PROTOCOL_BR_EDR); - auto pad = dualsense(1); - pad.report_parser.set_lightbar_color = set_lightbar; - // An earlier, still-unclassified transport connection must not reserve player 1. - platform_on_device_connected(&unrelated); - platform_on_device_connected(&pad); - require(platform_on_device_ready(&pad) == UNI_ERROR_SUCCESS, "DS5 must complete setup"); - now_ms = 100; - report_dualsense(pad); - const auto first = bridge_snapshot(); - const auto color = switch_pro_get_slot_light_color(0); - require(first.controller.active && first.slot == 0 && - pad.lightbar_red == color.red && pad.lightbar_green == color.green && - pad.lightbar_blue == color.blue, - "a transport-index-1 DualSense must own logical slot 0 and its lightbar color"); - require(platform_on_device_ready(&unrelated) == UNI_ERROR_INVALID_CONTROLLER, - "dedicated DualSense mode must reject an unrelated ready controller"); - platform_on_device_disconnected(&unrelated); - platform_on_device_connected(&unrelated); - require(platform_on_device_ready(&unrelated) == UNI_ERROR_INVALID_CONTROLLER, - "remembered unrelated reconnects must remain outside logical slots"); - platform_on_device_disconnected(&unrelated); - require(bridge_snapshot().slot == 0 && - bridge_snapshot().controller.connection_generation == first.controller.connection_generation && - pad.lightbar_calls == 1, - "unrelated connection churn must not rebind or recolor the active DualSense"); - - platform_on_device_disconnected(&pad); - auto reconnected = dualsense(2); - memcpy(reconnected.conn.btaddr, pad.conn.btaddr, sizeof(pad.conn.btaddr)); - reconnected.report_parser.set_lightbar_color = set_lightbar; - platform_on_device_connected(&reconnected); - require(platform_on_device_ready(&reconnected) == UNI_ERROR_SUCCESS, "DS5 reconnect must succeed"); - now_ms = 200; - report_dualsense(reconnected); - const auto next = bridge_snapshot(); - require(next.controller.active && next.slot == 0 && - controller_identity_equal(next.controller.identity, first.controller.identity) && - next.controller.connection_generation != first.controller.connection_generation && - reconnected.lightbar_red == color.red && reconnected.lightbar_green == color.green && - reconnected.lightbar_blue == color.blue, - "reusing another Bluetooth index must preserve identity and the first logical slot"); - auto aborted = dualsense(3); - platform_on_device_connected(&aborted); - platform_on_device_disconnected(&aborted); - require(platform_on_device_ready(&aborted) == UNI_ERROR_NO_SLOTS, - "a late ready callback must not resurrect an unassigned disconnected transport"); - require(bridge_snapshot().controller.active && bridge_snapshot().slot == 0, - "an aborted second setup must not disturb the active source"); - auto pending0 = device(0, true, UNI_BT_CONN_PROTOCOL_BR_EDR); - auto pending1 = device(1, true, UNI_BT_CONN_PROTOCOL_BR_EDR); - auto pending3 = device(3, true, UNI_BT_CONN_PROTOCOL_BR_EDR); - platform_on_device_connected(&pending0); - platform_on_device_connected(&pending1); - platform_on_device_connected(&pending3); - require(!incoming_connections && !scanning_enabled, - "unclassified transports still consume physical connection capacity"); - platform_on_device_disconnected(&pending0); - require(incoming_connections && bridge_snapshot().slot == 0 && - bridge_snapshot().controller.connection_generation == next.controller.connection_generation, - "freeing pending transport capacity must not move the logical source"); - platform_on_device_disconnected(&pending1); - platform_on_device_disconnected(&pending3); -} - -void cue_lifetime() { - start_pairing_backend(); - auto pad = dualsense(0); - require(platform_on_device_ready(&pad) == UNI_ERROR_SUCCESS, "DS5 must connect"); - uint64_t right, left, stop; - require(bluepad32_input_backend_dualsense_sample_request(0, 6, &right) && - bluepad32_input_backend_dualsense_sample_request(1, 1, &left) && right != left && - bluepad32_input_backend_dualsense_sample_result(0, right) == 0 && pad.rumble_calls == 0, - "independent acceptance is not driver completion"); - dualsense_transport_available = false; - process_rumble_timer(&g_rumble_timer); - require(bluepad32_input_backend_dualsense_sample_result(0, right) == 0 && pad.rumble_calls == 0, - "a busy source driver must not count as dispatch completion"); - dualsense_transport_available = true; - process_rumble_timer(&g_rumble_timer); - require(pad.last_high == 96 && pad.last_low == 160 && pad.last_rumble_duration_ms == 60 && - bluepad32_input_backend_dualsense_sample_result(0, right) == 1 && - bluepad32_input_backend_dualsense_sample_result(1, right) == -1 && - bluepad32_input_backend_dualsense_sample_result(1, left) == 1, - "combined output must route R weak/right and L strong/left with the shortest safe timer"); - now_ms = 60; - process_rumble_timer(&g_rumble_timer); - require(pad.last_high == 0 && pad.last_low == 160 && pad.last_rumble_duration_ms == 940, - "ending right must preserve only the left pulse's original remaining lifetime"); - require(bluepad32_input_backend_dualsense_sample_request(0, 3, &right), "right can restart independently"); - process_rumble_timer(&g_rumble_timer); - now_ms = 85; - process_rumble_timer(&g_rumble_timer); - require(pad.last_high == 0 && pad.last_low == 160, "right gap must not stop the left motor"); - now_ms = 175; - process_rumble_timer(&g_rumble_timer); - require(pad.last_high == 96 && pad.last_low == 160, "later pulse must resume after its gap"); - require(bluepad32_input_backend_dualsense_sample_request(1, 0, &stop), "left stop must replace only left"); - process_rumble_timer(&g_rumble_timer); - require(pad.last_high == 96 && pad.last_low == 0 && - bluepad32_input_backend_dualsense_sample_result(1, stop) == 1 && - bluepad32_input_backend_dualsense_sample_result(1, left) == -1, - "a side stop needs actual dispatch and cannot stop its sibling"); - now_ms = 200; - process_rumble_timer(&g_rumble_timer); - require(pad.last_rumble_duration_ms == 0, "final pulse expiry must release both motors"); - const int stopped = pad.rumble_calls; - now_ms = 5000; - process_rumble_timer(&g_rumble_timer); - require(pad.rumble_calls == stopped, "expired pulses must never replay after a stall"); - require(bluepad32_input_backend_dualsense_sample_request(0, 1, &right), "pending timeout cue must queue"); - now_ms += 2000; - process_rumble_timer(&g_rumble_timer); - require(bluepad32_input_backend_dualsense_sample_result(0, right) == -1 && pad.rumble_calls == stopped, - "an undispatched expired cue must fail without producing a late pulse"); -} - -void cue_races() { - start_pairing_backend(); - auto pad = dualsense(0); - require(platform_on_device_ready(&pad) == UNI_ERROR_SUCCESS, "DS5 must connect"); - uint64_t token; - require(bluepad32_input_backend_dualsense_sample_request(0, 1, &token), "race cue must queue"); - during_dualsense_dispatch = [] { bluepad32_input_backend_dualsense_sample_cancel(0); }; - process_rumble_timer(&g_rumble_timer); - during_dualsense_dispatch = nullptr; - require(bluepad32_input_backend_dualsense_sample_result(0, token) == -1, - "cancellation during dispatch must defeat a late completion"); - process_rumble_timer(&g_rumble_timer); - require(pad.last_rumble_duration_ms == 0, "in-flight cancellation must retain a bounded stop obligation"); - require(bluepad32_input_backend_dualsense_sample_request(1, 1, &token), "reselection race must queue"); - during_dualsense_dispatch = [] { bluepad32_input_backend_select_dualsense_source(nullptr); }; - process_rumble_timer(&g_rumble_timer); - during_dualsense_dispatch = nullptr; - require(bluepad32_input_backend_dualsense_sample_result(1, token) == -1, - "reselection must retire an in-flight token even for the same physical source"); - process_rumble_timer(&g_rumble_timer); - require(pad.last_rumble_duration_ms == 0, "reselection cannot orphan the just-dispatched motor"); - require(bluepad32_input_backend_dualsense_sample_request(0, 1, &token), "disconnect race must queue"); - platform_on_device_disconnected(&pad); - auto replacement = dualsense(0); - require(platform_on_device_ready(&replacement) == UNI_ERROR_SUCCESS, "replacement must connect"); - process_rumble_timer(&g_rumble_timer); - require(bluepad32_input_backend_dualsense_sample_result(0, token) == -1 && replacement.rumble_calls == 0, - "old tokens and deferred stops must never enter a replacement connection"); -} -} // namespace - -extern "C" void uni_hid_parser_ds5_parse_input_report(uni_hid_device_t*, const uint8_t*, uint16_t) {} -extern "C" bool uni_hid_parser_ds5_bridge_rumble( - uni_hid_device_t* pad, uint16_t duration, uint8_t right, uint8_t left) { - if (!dualsense_transport_available) return false; - observe_dualsense_rumble(pad, 0, duration, right, left); - return true; -} -extern "C" bool uni_hid_parser_ds5_bridge_snapshot(uni_hid_device_t* pad, uni_ds5_bridge_snapshot_t* out) { - for (const auto& fixture : sensors) { - if (fixture.device != pad || !fixture.valid) continue; - *out = fixture.metadata; - return true; - } - return false; -} - -int main(int argc, char** argv) { - require(argc == 2, "scenario required"); - const std::string scenario = argv[1]; - if (scenario == "source-isolation") source_isolation(); - else if (scenario == "cue-lifetime") cue_lifetime(); - else if (scenario == "cue-races") cue_races(); - else if (scenario == "stable-logical-slot") stable_logical_slot(); - else require(false, "unknown DualSense scenario"); - return 0; -} diff --git a/tests/dualsense_parser_native_test.c b/tests/dualsense_parser_native_test.c index ff248ba..1e9748b 100644 --- a/tests/dualsense_parser_native_test.c +++ b/tests/dualsense_parser_native_test.c @@ -5,6 +5,7 @@ #include #include "parser/uni_hid_parser_ds5.h" +#include "parser/uni_hid_parser_native_motion.h" #include "uni_hid_device.h" #include "uni_utils.h" @@ -59,6 +60,14 @@ uni_hid_device_t* uni_hid_device_create_virtual(uni_hid_device_t* d) { (void)d; void uni_hid_device_set_cod(uni_hid_device_t* d, uint32_t cod) { (void)d; (void)cod; } void uni_hid_device_connect(uni_hid_device_t* d) { (void)d; } void uni_hid_device_process_controller(uni_hid_device_t* d) { (void)d; } +// The common accessor must not select a different family in this fixture. +void uni_hid_parser_wii_setup(uni_hid_device_t* d) { (void)d; assert(false); } +bool uni_hid_parser_wii_accel_snapshot(uni_hid_device_t* d, int32_t v[3], uint32_t* seq) { + (void)d; (void)v; (void)seq; assert(false); return false; +} +bool uni_hid_parser_wii_gyro_snapshot(uni_hid_device_t* d, int32_t v[3], uint32_t* seq) { + (void)d; (void)v; (void)seq; assert(false); return false; +} uint8_t uni_hid_parser_hat_to_dpad(uint8_t hat) { const uint8_t values[8] = {DPAD_UP, DPAD_UP | DPAD_RIGHT, DPAD_RIGHT, DPAD_RIGHT | DPAD_DOWN, DPAD_DOWN, DPAD_DOWN | DPAD_LEFT, @@ -144,11 +153,12 @@ static void input(uint8_t bytes[78], uint32_t timestamp) { put32(bytes + 29, timestamp); seal(bytes, 78, 0xa1); } -static uni_ds5_bridge_snapshot_t feed(uint8_t* bytes, uint16_t len, bool admitted) { +static uni_native_motion_snapshot_t feed(uint8_t* bytes, uint16_t len, bool admitted) { uni_hid_parser_ds5_init_report(&device); uni_hid_parser_ds5_parse_input_report(&device, bytes, len); - uni_ds5_bridge_snapshot_t snapshot = {0}; - assert(uni_hid_parser_ds5_bridge_snapshot(&device, &snapshot) == admitted); + uni_native_motion_snapshot_t snapshot; + assert(uni_hid_parser_native_motion_snapshot(&device, &snapshot)); + assert(snapshot.report_tracked && snapshot.report_valid == admitted); return snapshot; } @@ -178,27 +188,32 @@ int main(void) { assert(ready_count == 1); uint8_t report[78]; input(report, 100); - uni_ds5_bridge_snapshot_t snapshot = feed(report, sizeof(report), true); - assert(!snapshot.motion_valid && snapshot.motion_sequence == 0); + uni_native_motion_snapshot_t snapshot = feed(report, sizeof(report), true); + assert(!snapshot.accel_valid && !snapshot.gyro_valid); assert(device.controller.gamepad.buttons & BUTTON_A); // Controls survive fallback. calibration(calib, false); feature(calib, sizeof(calib)); snapshot = feed(report, sizeof(report), true); - assert(!snapshot.motion_valid && snapshot.motion_sequence == 0); // Calibration alone is not fresh motion. + assert(!snapshot.accel_valid && !snapshot.gyro_valid); // Calibration alone is not fresh motion. input(report, 101); snapshot = feed(report, sizeof(report), true); - assert(snapshot.motion_valid && snapshot.motion_sequence == 1); + assert(snapshot.accel_valid && snapshot.gyro_valid); assert(device.controller.gamepad.gyro[0] == 1024 && device.controller.gamepad.gyro[1] == 0); assert(device.controller.gamepad.accel[1] == 8192); assert(device.controller.gamepad.misc_buttons & MISC_BUTTON_CAPTURE); + uni_native_motion_snapshot_t common; + assert(uni_hid_parser_native_motion_snapshot(&device, &common)); + assert(common.accel_valid && common.gyro_valid && + common.accel_q13[1] == 8192 && common.gyro_q10[0] == 1024); + const uint32_t common_sequence = common.accel_sequence; const uint32_t report_sequence = snapshot.report_sequence; - uni_ds5_bridge_snapshot_t polled; - assert(uni_hid_parser_ds5_bridge_snapshot(&device, &polled) && polled.report_sequence == report_sequence); + uni_native_motion_snapshot_t polled; + assert(uni_hid_parser_native_motion_snapshot(&device, &polled) && polled.report_sequence == report_sequence); snapshot = feed(report, sizeof(report), true); - assert(!snapshot.motion_valid && snapshot.motion_sequence == 1); + assert(snapshot.accel_sequence == common_sequence && !snapshot.accel_valid && !snapshot.gyro_valid); input(report, 99); snapshot = feed(report, sizeof(report), true); - assert(!snapshot.motion_valid && snapshot.motion_sequence == 1); + assert(snapshot.accel_sequence == common_sequence && !snapshot.accel_valid && !snapshot.gyro_valid); input(report, 102); feed(report, 77, false); report[9] ^= 0x20; @@ -206,7 +221,7 @@ int main(void) { feed(NULL, 0, false); input(report, 102); snapshot = feed(report, sizeof(report), true); - assert(snapshot.motion_valid && snapshot.motion_sequence == 2); + assert(snapshot.accel_valid && snapshot.gyro_valid && snapshot.accel_sequence != common_sequence); // A real uint32 sensor-clock wrap is forward progress, not a duplicate. uni_hid_parser_ds5_setup(&device); @@ -214,10 +229,12 @@ int main(void) { feature(calib, sizeof(calib)); input(report, UINT32_MAX - 15); snapshot = feed(report, sizeof(report), true); - assert(snapshot.motion_valid && snapshot.motion_sequence == 1); + assert(snapshot.accel_valid && snapshot.gyro_valid); + assert(uni_hid_parser_native_motion_snapshot(&device, &common)); + assert(common.accel_valid && common.accel_sequence != common_sequence); input(report, 16); snapshot = feed(report, sizeof(report), true); - assert(snapshot.motion_valid && snapshot.motion_sequence == 2); + assert(snapshot.accel_valid && snapshot.gyro_valid && snapshot.accel_sequence != common.accel_sequence); const unsigned before_busy = output_count; transport_available = false; @@ -236,7 +253,9 @@ int main(void) { const unsigned sent = output_count; uni_hid_parser_ds5_bridge_teardown(&device); advance(2000); - assert(output_count == sent && !uni_hid_parser_ds5_bridge_snapshot(&device, &snapshot)); + assert(output_count == sent); + assert(!uni_hid_parser_native_motion_snapshot(&device, &common)); + assert(!common.report_valid && !common.accel_valid && !common.gyro_valid); uni_hid_parser_ds5_play_dual_rumble(&device, 0, 1000, 0, 90); const unsigned active_sent = output_count; uni_hid_parser_ds5_bridge_teardown(&device); diff --git a/tests/native_gamepad_backend_test.cpp b/tests/native_gamepad_backend_test.cpp new file mode 100644 index 0000000..ad0e519 --- /dev/null +++ b/tests/native_gamepad_backend_test.cpp @@ -0,0 +1,641 @@ +// Reuse the backend's transport/storage fixture; these scenarios exercise only +// the native gamepad contract, not a second implementation of its scheduler. +#define main backend_fixture_main +#include "bluepad32_backend_lifecycle_test.cpp" +#undef main + +namespace { +struct SensorFixture { + uni_hid_device_t* device = nullptr; + uni_native_motion_snapshot_t metadata{}; + bool valid = false; +}; +SensorFixture sensors[4]; +void (*during_dualsense_dispatch)() = nullptr; +bool dualsense_transport_available = true; + +void observe_dualsense_rumble(uni_hid_device_t* target, uint16_t delay, + uint16_t duration, uint8_t right, uint8_t left) { + require(state_lock_depth == 0, "DS5 driver dispatch must not hold the shared state lock"); + play_rumble(target, delay, duration, right, left); + if (during_dualsense_dispatch) during_dualsense_dispatch(); +} + +uni_hid_device_t dualsense(int index) { + auto result = device(index, true, UNI_BT_CONN_PROTOCOL_BR_EDR); + result.vendor_id = 0x054c; + result.product_id = index == 0 ? 0x0ce6 : 0x0df2; + result.controller_type = CONTROLLER_TYPE_PS5Controller; + result.report_parser.parse_input_report = uni_hid_parser_ds5_parse_input_report; + result.report_parser.play_dual_rumble = observe_dualsense_rumble; + return result; +} + +void report_dualsense(uni_hid_device_t& pad, bool fresh_motion = true) { + SensorFixture& sensor = sensors[pad.idx]; + sensor.device = &pad; + sensor.valid = true; + sensor.metadata.report_tracked = true; + sensor.metadata.report_valid = true; + ++sensor.metadata.report_sequence; + if (fresh_motion) { + ++sensor.metadata.accel_sequence; + ++sensor.metadata.gyro_sequence; + } + sensor.metadata.accel_valid = fresh_motion; + sensor.metadata.gyro_valid = fresh_motion; + sensor.metadata.accel_q13[1] = 8193; + sensor.metadata.gyro_q10[2] = -123456; + pad.controller.klass = UNI_CONTROLLER_CLASS_GAMEPAD; + pad.controller.gamepad.buttons = BUTTON_A | BUTTON_SHOULDER_L; + // Parser init_report may clear legacy fields; native precision comes from + // the independent sensor provider rather than cached controller values. + pad.controller.gamepad.accel[1] = 0; + pad.controller.gamepad.gyro[2] = 0; + pad.controller.battery = 176; + platform_on_controller_data(&pad, &pad.controller); +} + +Bluepad32NativeGamepadSnapshot bridge_snapshot() { + Bluepad32NativeGamepadSnapshot result{}; + bluepad32_input_backend_native_snapshot(&result); + return result; +} + +void source_isolation() { + start_pairing_backend(); + auto ordinary = device(0, true, UNI_BT_CONN_PROTOCOL_BR_EDR); + ordinary.vendor_id = 0x054c; + ordinary.product_id = 0x0ce6; +#if !SWITCH2_BRIDGE_DUALSENSE_INPUT + ordinary.gamepad = false; +#endif + require(platform_on_device_ready(&ordinary) == UNI_ERROR_INVALID_CONTROLLER, + "an ineligible controller must not enter dedicated output slots"); + bluepad32_input_backend_select_native_source(ordinary.conn.btaddr); + require(!bridge_snapshot().controller.active, "an ineligible device cannot become the native source"); + platform_on_device_disconnected(&ordinary); + bluepad32_input_backend_select_native_source(nullptr); + auto first = dualsense(0); + auto second = dualsense(1); + require(platform_on_device_ready(&first) == UNI_ERROR_SUCCESS, "first DS5 must connect"); + now_ms = 100; + report_dualsense(first); + const auto initial = bridge_snapshot(); + require(initial.slot == 0 && initial.controller.active && initial.controller.state.button_south && + initial.battery == 176 && initial.accel_valid && initial.gyro_valid && + !initial.requires_stationary_bias && initial.accel_q13[1] == 8193 && + initial.gyro_q10[2] == -123456 && initial.gyro_received_us == 100000, + "native snapshot must preserve coherent physical controls and calibrated precision"); + now_ms = 120; + platform_on_controller_data(&first, &first.controller); + bluepad32_input_backend_report_sent(0); + auto snapshot = bridge_snapshot(); + require(snapshot.state_generation == initial.state_generation && snapshot.received_us == 100000 && + snapshot.accel_sequence == initial.accel_sequence && + snapshot.gyro_sequence == initial.gyro_sequence, + "polling/cached callbacks and USB consumption must not freshen input or motion"); + report_dualsense(first, false); + snapshot = bridge_snapshot(); + require(snapshot.received_us == 120000 && snapshot.gyro_received_us == 100000 && + !snapshot.accel_valid && !snapshot.gyro_valid, + "a controls-only admission must not refresh a duplicate sensor timestamp"); + sensors[0].valid = false; + first.controller.gamepad.buttons = 0; + platform_on_controller_data(&first, &first.controller); + require(bridge_snapshot().controller.state.button_south, + "malformed parser input must not publish an invented button release"); + uint64_t old_token; + require(bluepad32_input_backend_native_sample_request(0, 1, &old_token), "first source cue must queue"); + require(platform_on_device_ready(&second) == UNI_ERROR_SUCCESS, "Edge must connect"); + report_dualsense(second); + require(!bridge_snapshot().controller.active && + bluepad32_input_backend_native_sample_result(0, old_token) == -1, + "auto ambiguity must fail closed and retire source work immediately"); + platform_on_device_disconnected(&second); + platform_on_controller_data(&first, &first.controller); + require(!bridge_snapshot().controller.active, "returning to a source cannot resurrect cached state"); + report_dualsense(first); + snapshot = bridge_snapshot(); + require(snapshot.controller.active && snapshot.controller.connection_generation != initial.controller.connection_generation, + "a missed ambiguous interval still needs a new adapter epoch"); + platform_on_device_connected(&second); + require(platform_on_device_ready(&second) == UNI_ERROR_SUCCESS, "Edge reconnect must succeed"); + bluepad32_input_backend_select_native_source(first.conn.btaddr); + report_dualsense(first); + report_dualsense(second); + require(bridge_snapshot().slot == 0, "explicit source must ignore another live PS5"); + platform_on_device_disconnected(&first); + require(!bridge_snapshot().controller.active, "disconnect must not migrate an explicit source"); +} + +void stable_logical_slot() { + start_pairing_backend(); + auto unrelated = device(0, true, UNI_BT_CONN_PROTOCOL_BR_EDR); +#if !SWITCH2_BRIDGE_DUALSENSE_INPUT + unrelated.gamepad = false; +#endif + auto pad = dualsense(1); + pad.report_parser.set_lightbar_color = set_lightbar; + // An earlier, still-unclassified transport connection must not reserve player 1. + platform_on_device_connected(&unrelated); + platform_on_device_connected(&pad); + require(platform_on_device_ready(&pad) == UNI_ERROR_SUCCESS, "DS5 must complete setup"); + now_ms = 100; + report_dualsense(pad); + const auto first = bridge_snapshot(); + const auto color = switch_pro_get_slot_light_color(0); + require(first.controller.active && first.slot == 0 && + pad.lightbar_red == color.red && pad.lightbar_green == color.green && + pad.lightbar_blue == color.blue, + "a transport-index-1 DualSense must own logical slot 0 and its lightbar color"); + require(platform_on_device_ready(&unrelated) == UNI_ERROR_INVALID_CONTROLLER, + "dedicated DualSense mode must reject an unrelated ready controller"); + platform_on_device_disconnected(&unrelated); + platform_on_device_connected(&unrelated); + require(platform_on_device_ready(&unrelated) == UNI_ERROR_INVALID_CONTROLLER, + "remembered unrelated reconnects must remain outside logical slots"); + platform_on_device_disconnected(&unrelated); + require(bridge_snapshot().slot == 0 && + bridge_snapshot().controller.connection_generation == first.controller.connection_generation && + pad.lightbar_calls == 1, + "unrelated connection churn must not rebind or recolor the active DualSense"); + + platform_on_device_disconnected(&pad); + auto reconnected = dualsense(2); + memcpy(reconnected.conn.btaddr, pad.conn.btaddr, sizeof(pad.conn.btaddr)); + reconnected.report_parser.set_lightbar_color = set_lightbar; + platform_on_device_connected(&reconnected); + require(platform_on_device_ready(&reconnected) == UNI_ERROR_SUCCESS, "DS5 reconnect must succeed"); + now_ms = 200; + report_dualsense(reconnected); + const auto next = bridge_snapshot(); + require(next.controller.active && next.slot == 0 && + controller_identity_equal(next.controller.identity, first.controller.identity) && + next.controller.connection_generation != first.controller.connection_generation && + reconnected.lightbar_red == color.red && reconnected.lightbar_green == color.green && + reconnected.lightbar_blue == color.blue, + "reusing another Bluetooth index must preserve identity and the first logical slot"); + auto aborted = dualsense(3); + platform_on_device_connected(&aborted); + platform_on_device_disconnected(&aborted); + require(platform_on_device_ready(&aborted) == UNI_ERROR_NO_SLOTS, + "a late ready callback must not resurrect an unassigned disconnected transport"); + require(bridge_snapshot().controller.active && bridge_snapshot().slot == 0, + "an aborted second setup must not disturb the active source"); + auto pending0 = device(0, true, UNI_BT_CONN_PROTOCOL_BR_EDR); + auto pending1 = device(1, true, UNI_BT_CONN_PROTOCOL_BR_EDR); + auto pending3 = device(3, true, UNI_BT_CONN_PROTOCOL_BR_EDR); + platform_on_device_connected(&pending0); + platform_on_device_connected(&pending1); + platform_on_device_connected(&pending3); + require(!incoming_connections && !scanning_enabled, + "unclassified transports still consume physical connection capacity"); + platform_on_device_disconnected(&pending0); + require(incoming_connections && bridge_snapshot().slot == 0 && + bridge_snapshot().controller.connection_generation == next.controller.connection_generation, + "freeing pending transport capacity must not move the logical source"); + platform_on_device_disconnected(&pending1); + platform_on_device_disconnected(&pending3); +} + +void cue_lifetime() { + start_pairing_backend(); + auto pad = dualsense(0); + require(platform_on_device_ready(&pad) == UNI_ERROR_SUCCESS, "DS5 must connect"); + uint64_t right, left, stop; + require(bluepad32_input_backend_native_sample_request(0, 6, &right) && + bluepad32_input_backend_native_sample_request(1, 1, &left) && right != left && + bluepad32_input_backend_native_sample_result(0, right) == 0 && pad.rumble_calls == 0, + "independent acceptance is not driver completion"); + dualsense_transport_available = false; + process_rumble_timer(&g_rumble_timer); + require(bluepad32_input_backend_native_sample_result(0, right) == 0 && pad.rumble_calls == 0, + "a busy source driver must not count as dispatch completion"); + dualsense_transport_available = true; + process_rumble_timer(&g_rumble_timer); + require(pad.last_high == 96 && pad.last_low == 160 && pad.last_rumble_duration_ms == 60 && + bluepad32_input_backend_native_sample_result(0, right) == 1 && + bluepad32_input_backend_native_sample_result(1, right) == -1 && + bluepad32_input_backend_native_sample_result(1, left) == 1, + "combined output must route R weak/right and L strong/left with the shortest safe timer"); + now_ms = 60; + process_rumble_timer(&g_rumble_timer); + require(pad.last_high == 0 && pad.last_low == 160 && pad.last_rumble_duration_ms == 940, + "ending right must preserve only the left pulse's original remaining lifetime"); + require(bluepad32_input_backend_native_sample_request(0, 3, &right), "right can restart independently"); + process_rumble_timer(&g_rumble_timer); + now_ms = 85; + process_rumble_timer(&g_rumble_timer); + require(pad.last_high == 0 && pad.last_low == 160, "right gap must not stop the left motor"); + now_ms = 175; + process_rumble_timer(&g_rumble_timer); + require(pad.last_high == 96 && pad.last_low == 160, "later pulse must resume after its gap"); + require(bluepad32_input_backend_native_sample_request(1, 0, &stop), "left stop must replace only left"); + process_rumble_timer(&g_rumble_timer); + require(pad.last_high == 96 && pad.last_low == 0 && + bluepad32_input_backend_native_sample_result(1, stop) == 1 && + bluepad32_input_backend_native_sample_result(1, left) == -1, + "a side stop needs actual dispatch and cannot stop its sibling"); + now_ms = 200; + process_rumble_timer(&g_rumble_timer); + require(pad.last_rumble_duration_ms == 0, "final pulse expiry must release both motors"); + const int stopped = pad.rumble_calls; + now_ms = 5000; + process_rumble_timer(&g_rumble_timer); + require(pad.rumble_calls == stopped, "expired pulses must never replay after a stall"); + require(bluepad32_input_backend_native_sample_request(0, 1, &right), "pending timeout cue must queue"); + now_ms += 2000; + process_rumble_timer(&g_rumble_timer); + require(bluepad32_input_backend_native_sample_result(0, right) == -1 && pad.rumble_calls == stopped, + "an undispatched expired cue must fail without producing a late pulse"); +} + +void cue_races() { + start_pairing_backend(); + auto pad = dualsense(0); + require(platform_on_device_ready(&pad) == UNI_ERROR_SUCCESS, "DS5 must connect"); + uint64_t token; + require(bluepad32_input_backend_native_sample_request(0, 1, &token), "race cue must queue"); + during_dualsense_dispatch = [] { bluepad32_input_backend_native_sample_cancel(0); }; + process_rumble_timer(&g_rumble_timer); + during_dualsense_dispatch = nullptr; + require(bluepad32_input_backend_native_sample_result(0, token) == -1, + "cancellation during dispatch must defeat a late completion"); + process_rumble_timer(&g_rumble_timer); + require(pad.last_rumble_duration_ms == 0, "in-flight cancellation must retain a bounded stop obligation"); + require(bluepad32_input_backend_native_sample_request(1, 1, &token), "reselection race must queue"); + during_dualsense_dispatch = [] { bluepad32_input_backend_select_native_source(nullptr); }; + process_rumble_timer(&g_rumble_timer); + during_dualsense_dispatch = nullptr; + require(bluepad32_input_backend_native_sample_result(1, token) == -1, + "reselection must retire an in-flight token even for the same physical source"); + process_rumble_timer(&g_rumble_timer); + require(pad.last_rumble_duration_ms == 0, "reselection cannot orphan the just-dispatched motor"); + require(bluepad32_input_backend_native_sample_request(0, 1, &token), "disconnect race must queue"); + platform_on_device_disconnected(&pad); + auto replacement = dualsense(0); + require(platform_on_device_ready(&replacement) == UNI_ERROR_SUCCESS, "replacement must connect"); + process_rumble_timer(&g_rumble_timer); + require(bluepad32_input_backend_native_sample_result(0, token) == -1 && replacement.rumble_calls == 0, + "old tokens and deferred stops must never enter a replacement connection"); +} +void report_gamepad(uni_hid_device_t& pad) { + pad.controller.klass = UNI_CONTROLLER_CLASS_GAMEPAD; + platform_on_controller_data(&pad, &pad.controller); +} + +SensorFixture& motion_fixture(uni_hid_device_t& pad, bool tracked = true) { + SensorFixture& fixture = sensors[pad.idx]; + fixture = {}; + fixture.device = &pad; + fixture.valid = true; + fixture.metadata.report_tracked = tracked; + fixture.metadata.report_valid = true; + fixture.metadata.report_sequence = 1; + fixture.metadata.accel_sequence = 1; + fixture.metadata.gyro_sequence = 1; + fixture.metadata.accel_valid = true; + fixture.metadata.gyro_valid = true; + fixture.metadata.accel_q13[1] = 8193; + fixture.metadata.gyro_q10[2] = -123456; + return fixture; +} + +void sensorless_admission() { + start_pairing_backend(); + auto xbox = device(2, true, UNI_BT_CONN_PROTOCOL_BR_EDR); + xbox.controller_type = CONTROLLER_TYPE_XBoxOneController; + xbox.report_parser.play_dual_rumble = nullptr; + platform_on_device_connected(&xbox); + require(platform_on_device_ready(&xbox) == UNI_ERROR_SUCCESS, + "normal gamepad admission must not require a motion parser"); + xbox.controller.gamepad.buttons = BUTTON_A; + xbox.controller.gamepad.accel[1] = 8192; + xbox.controller.gamepad.gyro[0] = 65536; + now_ms = 100; + report_gamepad(xbox); + const auto initial = bridge_snapshot(); + uint64_t cue = 99; + require(initial.controller.active && initial.slot == 0 && + initial.controller.state.button_south && !initial.accel_valid && + !initial.gyro_valid && !initial.requires_stationary_bias && + initial.accel_sequence == 0 && initial.gyro_sequence == 0 && + !bluepad32_input_backend_native_sample_request(0, 1, &cue) && cue == 0, + "sensorless controls remain live without invented IMU or rumble capability"); + auto generic = device(0, true, UNI_BT_CONN_PROTOCOL_BLE); + platform_on_device_connected(&generic); + require(platform_on_device_ready(&generic) == UNI_ERROR_SUCCESS, + "unknown-family normal AIO gamepads must not face a native brand whitelist"); + report_gamepad(generic); + require(!bridge_snapshot().controller.active, "two logical gamepads are ambiguous"); + bluepad32_input_backend_select_native_source(xbox.conn.btaddr); + now_ms = 110; + report_gamepad(xbox); + require(bridge_snapshot().controller.active && bridge_snapshot().slot == 0 && + controller_identity_equal(bridge_snapshot().controller.identity, initial.controller.identity), + "explicit sensorless selection retains its real identity and logical slot"); + platform_on_device_disconnected(&xbox); + report_gamepad(generic); + require(!bridge_snapshot().controller.active, "explicit selection cannot migrate on disconnect"); + bluepad32_input_backend_select_native_source(nullptr); + generic.controller.gamepad.buttons = BUTTON_B; + report_gamepad(generic); + require(bridge_snapshot().controller.active && bridge_snapshot().controller.state.button_east && + !bridge_snapshot().controller.state.button_south, + "unique sensorless reselection must publish only the new source's controls"); +} + +void independent_motion() { + start_pairing_backend(); + auto ds4 = device(0, true, UNI_BT_CONN_PROTOCOL_BR_EDR); + ds4.controller_type = CONTROLLER_TYPE_PS4Controller; + require(platform_on_device_ready(&ds4) == UNI_ERROR_SUCCESS, "DS4 controls must be admitted"); + auto& ds = motion_fixture(ds4).metadata; + ds4.controller.gamepad.buttons = BUTTON_A; + now_ms = 100; + report_gamepad(ds4); + const auto first = bridge_snapshot(); + require(first.accel_valid && first.gyro_valid && !first.requires_stationary_bias && + first.accel_q13[1] == 8193 && first.gyro_q10[2] == -123456, + "calibrated DS4 motion keeps raw precision without Wii settling"); + now_ms = 110; + ++ds.report_sequence; + ++ds.accel_sequence; + report_gamepad(ds4); + require(bridge_snapshot().accel_received_us == 110000 && + bridge_snapshot().gyro_received_us == 100000 && bridge_snapshot().gyro_valid, + "acceleration ingress must not refresh an unchanged gyro"); + now_ms = 120; + ds.report_valid = false; + ds4.controller.gamepad.buttons = 0; + report_gamepad(ds4); + require(bridge_snapshot().controller.state.button_south && + bridge_snapshot().received_us == 110000, + "a tracked partial/malformed report cannot publish phantom releases"); + ds.report_valid = true; + ++ds.report_sequence; + ds.gyro_valid = false; + report_gamepad(ds4); + require(!bridge_snapshot().gyro_valid && bridge_snapshot().accel_valid, + "gyro capability loss must not suppress working acceleration or controls"); + bluepad32_input_backend_select_native_source(nullptr); + ++ds.report_sequence; + ds.gyro_valid = true; + report_gamepad(ds4); + require(bridge_snapshot().controller.active && !bridge_snapshot().accel_valid && + !bridge_snapshot().gyro_valid, + "reselection cannot rehabilitate cached sensor sequences"); + ++ds.report_sequence; + ++ds.gyro_sequence; + report_gamepad(ds4); + require(bridge_snapshot().gyro_valid && !bridge_snapshot().accel_valid, + "each sensor must earn freshness separately in a new source epoch"); + platform_on_device_disconnected(&ds4); + + auto pro = device(1, true, UNI_BT_CONN_PROTOCOL_BR_EDR); + pro.controller_type = CONTROLLER_TYPE_SwitchProController; + require(platform_on_device_ready(&pro) == UNI_ERROR_SUCCESS, "Switch Pro controls must be admitted"); + auto& sw = motion_fixture(pro).metadata; + sw.accel_valid = sw.gyro_valid = false; + pro.controller.gamepad.buttons = BUTTON_B; + report_gamepad(pro); + require(bridge_snapshot().controller.state.button_east && !bridge_snapshot().gyro_valid, + "Switch controls must not wait for motion calibration"); + ++sw.report_sequence; + ++sw.accel_sequence; + ++sw.gyro_sequence; + sw.accel_valid = sw.gyro_valid = true; + report_gamepad(pro); + require(bridge_snapshot().accel_valid && bridge_snapshot().gyro_valid && + !bridge_snapshot().requires_stationary_bias, + "validated Switch motion starts without Wii stationary settling"); + platform_on_device_disconnected(&pro); + + auto remote = wii_device(2); + require(platform_on_device_ready(&remote) == UNI_ERROR_SUCCESS, "Wii controls must be admitted"); + auto& wii = motion_fixture(remote, false).metadata; + wii.gyro_valid = false; + remote.controller.gamepad.buttons = BUTTON_A; + now_ms = 200; + report_gamepad(remote); + require(bridge_snapshot().controller.state.button_south && bridge_snapshot().accel_valid && + !bridge_snapshot().gyro_valid && bridge_snapshot().requires_stationary_bias, + "Wii without MotionPlus remains acceleration-capable, not a fabricated full IMU"); + now_ms = 210; + ++wii.gyro_sequence; + wii.gyro_valid = true; + report_gamepad(remote); + require(bridge_snapshot().gyro_valid && bridge_snapshot().gyro_received_us == 210000 && + bridge_snapshot().accel_received_us == 200000 && + bridge_snapshot().requires_stationary_bias, + "Wii MotionPlus ingress must retain independent acceleration age and Wii bias policy"); +} + +void paired_source() { + start_pairing_backend(); + auto left = switch2_device(2, UNI_SW2_JOYCON_L_PID); + auto right = switch2_device(1, UNI_SW2_JOYCON_R_PID); + ready_switch2(left); + ready_switch2(right); + auto& r = motion_fixture(right).metadata; + auto& l = motion_fixture(left).metadata; + right.controller.gamepad.buttons = BUTTON_A; + left.controller.gamepad.dpad = DPAD_LEFT; + now_ms = 100; + report_gamepad(right); + const auto initial = bridge_snapshot(); + require_pair_owner(initial.controller.identity, left, right); + require(initial.controller.active && initial.slot == 0 && initial.gyro_valid && + !slot_snapshot(1).active && !slot_snapshot(2).active, + "an existing Joy-Con pair is one logical native source, not auto ambiguity"); + now_ms = 110; + l.gyro_q10[2] = 654321; + report_gamepad(left); + require(bridge_snapshot().controller.state.button_south && + bridge_snapshot().controller.state.dpad_left && + bridge_snapshot().received_us == 110000 && + bridge_snapshot().gyro_received_us == 100000 && + bridge_snapshot().gyro_q10[2] == initial.gyro_q10[2], + "left controls merge without refreshing or replacing the right motion owner"); + bluepad32_input_backend_select_native_source(right.conn.btaddr); + ++l.report_sequence; + report_gamepad(left); + require(bridge_snapshot().controller.active && !bridge_snapshot().gyro_valid, + "either member address selects the logical pair, not that member's cached IMU"); + ++r.report_sequence; + report_gamepad(right); + require(!bridge_snapshot().gyro_valid, "reselection cannot freshen the right parser cache"); + ++r.report_sequence; + ++r.accel_sequence; + ++r.gyro_sequence; + report_gamepad(right); + require(bridge_snapshot().gyro_valid, "new right samples restore paired motion"); + uint64_t rc, lc, stop; + require(bluepad32_input_backend_native_sample_request(0, 6, &rc) && + bluepad32_input_backend_native_sample_request(1, 1, &lc), + "both real halves expose their own driver capability"); + process_rumble_timer(&g_rumble_timer); + require(right.last_high == 96 && right.last_low == 96 && + left.last_high == 160 && left.last_low == 160 && + bluepad32_input_backend_native_sample_result(0, rc) == 1 && + bluepad32_input_backend_native_sample_result(1, lc) == 1, + "paired cues must dispatch only each half's contribution to its real driver"); + now_ms = 120; + require(bluepad32_input_backend_native_sample_request(0, 0, &stop), "right stop must queue"); + process_rumble_timer(&g_rumble_timer); + require(right.last_rumble_duration_ms == 0 && left.last_rumble_duration_ms == 990 && + bluepad32_input_backend_native_sample_result(0, stop) == 1, + "stopping one paired side preserves the other side's original finite deadline"); + bluepad32_input_backend_select_native_source(nullptr); + set_runtime_joycon_mode(JoyConMode::kIndividual); + require(!bridge_snapshot().controller.active && + bluepad32_input_backend_native_sample_result(1, lc) == -1, + "live split retires the pair immediately and fails auto selection closed"); + bluepad32_input_backend_select_native_source(right.conn.btaddr); + ++r.report_sequence; + ++r.accel_sequence; + ++r.gyro_sequence; + report_gamepad(right); + require(bridge_snapshot().controller.active && + controller_identity_equal(bridge_snapshot().controller.identity, identity_for_device(&right)), + "explicit selection after a split retains the real surviving member identity"); +} + +void pair_cue_races() { + start_pairing_backend(); + auto left = switch2_device(0, UNI_SW2_JOYCON_L_PID); + auto right = switch2_device(1, UNI_SW2_JOYCON_R_PID); + ready_switch2(left); + ready_switch2(right); + left.report_parser.play_dual_rumble = nullptr; + uint64_t rc, lc; + require(!bluepad32_input_backend_native_sample_request(1, 1, &lc) && lc == 0, + "a paired half without a rumble driver cannot borrow its sibling's capability"); + left.report_parser.play_dual_rumble = play_rumble; + right.report_parser.play_dual_rumble = []( + uni_hid_device_t* pad, uint16_t delay, uint16_t duration, uint8_t weak, uint8_t strong) { + require(state_lock_depth == 0, "paired driver dispatch must not hold the shared lock"); + play_rumble(pad, delay, duration, weak, strong); + bluepad32_input_backend_native_sample_cancel(1); + }; + require(bluepad32_input_backend_native_sample_request(0, 1, &rc) && + bluepad32_input_backend_native_sample_request(1, 1, &lc), "paired race cues must queue"); + const int before = left.rumble_calls; + process_rumble_timer(&g_rumble_timer); + require(left.rumble_calls == before && right.last_rumble_duration_ms == 1000 && + bluepad32_input_backend_native_sample_result(0, rc) == 1 && + bluepad32_input_backend_native_sample_result(1, lc) == -1, + "canceling L during R dispatch must not submit stale L or retire the accepted R cue"); + bluepad32_input_backend_native_sample_cancel(0); + process_rumble_timer(&g_rumble_timer); + const int before_stall = left.rumble_calls; + require(right.last_rumble_duration_ms == 0, "partial pair submission retains a real stop obligation"); + right.report_parser.play_dual_rumble = []( + uni_hid_device_t* pad, uint16_t delay, uint16_t duration, uint8_t weak, uint8_t strong) { + play_rumble(pad, delay, duration, weak, strong); + now_ms += 2000; + bluepad32_input_backend_select_native_source(nullptr); + }; + require(bluepad32_input_backend_native_sample_request(0, 1, &rc) && + bluepad32_input_backend_native_sample_request(1, 1, &lc), "reselection race cues must queue"); + process_rumble_timer(&g_rumble_timer); + right.report_parser.play_dual_rumble = play_rumble; + require(left.rumble_calls == before_stall && + bluepad32_input_backend_native_sample_result(0, rc) == -1 && + bluepad32_input_backend_native_sample_result(1, lc) == -1, + "reselection and a dispatch stall cannot submit the old epoch's remaining half"); + process_rumble_timer(&g_rumble_timer); + require(right.last_rumble_duration_ms == 0, "reselection cannot orphan a partial pair output"); +} + +uint8_t mono_magnitude = 0; +void observe_mono_rumble(uni_hid_device_t* pad, uint16_t delay, uint16_t duration, + uint8_t weak, uint8_t strong) { + require(state_lock_depth == 0 && delay == 0, "mono driver dispatch is immediate and outside the lock"); + mono_magnitude = duration == 0 ? 0 : (weak > strong ? weak : strong); + play_rumble(pad, delay, duration, mono_magnitude, 0); +} + +void mono_rumble() { + start_pairing_backend(); + auto move = device(0, true, UNI_BT_CONN_PROTOCOL_BR_EDR); + move.controller_type = CONTROLLER_TYPE_PSMoveController; + move.report_parser.play_dual_rumble = observe_mono_rumble; + require(platform_on_device_ready(&move) == UNI_ERROR_SUCCESS, "mono-rumble gamepad must connect"); + uint64_t right, left; + require(bluepad32_input_backend_native_sample_request(0, 1, &right) && + bluepad32_input_backend_native_sample_request(1, 7, &left), + "mono devices accept independent logical contributions, not separate actuators"); + process_rumble_timer(&g_rumble_timer); + require(mono_magnitude == 220 && move.last_rumble_duration_ms == 120, + "one mono actuator combines both contributions with the shortest finite boundary"); + bluepad32_input_backend_native_sample_cancel(1); + now_ms = 40; + process_rumble_timer(&g_rumble_timer); + require(mono_magnitude == 160 && move.last_rumble_duration_ms == 960 && + bluepad32_input_backend_native_sample_result(1, left) == -1, + "canceling one mono contribution must preserve the other's remaining pulse"); + now_ms = 1000; + process_rumble_timer(&g_rumble_timer); + require(mono_magnitude == 0, "the last mono contribution must stop at its original deadline"); + require(bluepad32_input_backend_native_sample_request(0, 1, &right), "new mono cue must queue"); + const int calls = move.rumble_calls; + now_ms += 2000; + process_rumble_timer(&g_rumble_timer); + require(move.rumble_calls == calls && bluepad32_input_backend_native_sample_result(0, right) == -1, + "expired undispatched mono work cannot replay after a timer stall"); + require(bluepad32_input_backend_native_sample_request(1, 1, &left), "disconnect cue must queue"); + process_rumble_timer(&g_rumble_timer); + require(mono_magnitude == 160, "disconnect regression must own a real active driver pulse"); + platform_on_device_disconnected(&move); + require(mono_magnitude == 0 && move.last_rumble_duration_ms == 0, + "disconnect must retire the source driver's timer before parser memory reuse"); + auto replacement = device(0, true, UNI_BT_CONN_PROTOCOL_BR_EDR); + replacement.report_parser.play_dual_rumble = observe_mono_rumble; + require(platform_on_device_ready(&replacement) == UNI_ERROR_SUCCESS, "new mono connection must succeed"); + process_rumble_timer(&g_rumble_timer); + require(replacement.rumble_calls == 0 && + bluepad32_input_backend_native_sample_result(1, left) == -1, + "retired mono work must never enter a replacement connection"); +} + +} // namespace + +extern "C" void uni_hid_parser_ds5_parse_input_report(uni_hid_device_t*, const uint8_t*, uint16_t) {} +extern "C" bool uni_hid_parser_ds5_bridge_rumble( + uni_hid_device_t* pad, uint16_t duration, uint8_t right, uint8_t left) { + if (!dualsense_transport_available) return false; + observe_dualsense_rumble(pad, 0, duration, right, left); + return true; +} +extern "C" bool uni_hid_parser_native_motion_snapshot( + uni_hid_device_t* pad, uni_native_motion_snapshot_t* out) { + *out = {}; + for (const auto& fixture : sensors) { + if (fixture.device != pad) continue; + *out = fixture.metadata; + if (!fixture.valid) out->report_valid = false; + return fixture.valid; + } + // DS5 must be guarded even before its first good full report. + out->report_tracked = + pad->report_parser.parse_input_report == uni_hid_parser_ds5_parse_input_report; + return false; +} + +int main(int argc, char** argv) { + require(argc == 2, "scenario required"); + const std::string scenario = argv[1]; + if (scenario == "source-isolation") source_isolation(); + else if (scenario == "cue-lifetime") cue_lifetime(); + else if (scenario == "cue-races") cue_races(); + else if (scenario == "stable-logical-slot") stable_logical_slot(); + else if (scenario == "sensorless-admission") sensorless_admission(); + else if (scenario == "independent-motion") independent_motion(); + else if (scenario == "paired-source") paired_source(); + else if (scenario == "pair-cue-races") pair_cue_races(); + else if (scenario == "mono-rumble") mono_rumble(); + else require(false, "unknown native gamepad scenario"); + return 0; +} diff --git a/tests/native_gamepad_motion_parser_test.c b/tests/native_gamepad_motion_parser_test.c new file mode 100644 index 0000000..f7c34ee --- /dev/null +++ b/tests/native_gamepad_motion_parser_test.c @@ -0,0 +1,264 @@ +#include +#include +#include + +#include "parser/uni_hid_parser_ds4.h" +#include "parser/uni_hid_parser_psmove.h" +#include "parser/uni_hid_parser_switch.h" +#include "parser/uni_hid_parser_native_motion.h" +#include "uni_hid_device.h" +#include "uni_utils.h" + +// Real staged parsers and normalization. Only radio, clock and platform edges +// are substituted; fixtures enter public setup/feature/input APIs. +static uint8_t command[128]; +static unsigned command_len, ready_count; +void uni_log(const char* fmt, ...) { (void)fmt; } +void printf_hexdump(const void* data, int len) { (void)data; (void)len; } +void uni_hid_device_send_ctrl_report(uni_hid_device_t* d, const uint8_t* bytes, uint16_t len) { + (void)d; assert(len <= sizeof(command)); memcpy(command, bytes, len); command_len = len; +} +void uni_hid_device_send_intr_report(uni_hid_device_t* d, const uint8_t* bytes, uint16_t len) { + uni_hid_device_send_ctrl_report(d, bytes, len); +} +int l2cap_can_send_packet_now(uint16_t cid) { (void)cid; return 1; } +int l2cap_send(uint16_t cid, uint8_t* bytes, uint16_t len) { + (void)cid; uni_hid_device_send_intr_report(NULL, bytes, len); return ERROR_CODE_SUCCESS; +} +uint8_t l2cap_request_can_send_now_event(uint16_t cid) { (void)cid; return 0; } +uint32_t btstack_run_loop_get_time_ms(void) { return 0; } +void btstack_run_loop_set_timer(btstack_timer_source_t* t, uint32_t ms) { (void)t; (void)ms; } +void btstack_run_loop_add_timer(btstack_timer_source_t* t) { (void)t; } +bool btstack_run_loop_remove_timer(btstack_timer_source_t* t) { (void)t; return false; } +void btstack_run_loop_set_timer_context(btstack_timer_source_t* t, void* p) { t->context = p; } +void btstack_run_loop_set_timer_handler(btstack_timer_source_t* t, void (*fn)(btstack_timer_source_t*)) { t->process = fn; } +void* btstack_run_loop_get_timer_context(btstack_timer_source_t* t) { return t->context; } +bool uni_hid_device_set_ready_complete(uni_hid_device_t* d) { (void)d; ++ready_count; return true; } +uni_hid_device_t* uni_hid_device_create_virtual(uni_hid_device_t* d) { (void)d; return NULL; } +void uni_hid_device_set_cod(uni_hid_device_t* d, uint32_t cod) { (void)d; (void)cod; } +void uni_hid_device_connect(uni_hid_device_t* d) { (void)d; } +void uni_hid_device_process_controller(uni_hid_device_t* d) { (void)d; } +void uni_hid_device_set_product_id(uni_hid_device_t* d, uint16_t pid) { d->product_id = pid; } +void uni_hid_device_set_vendor_id(uni_hid_device_t* d, uint16_t vid) { d->vendor_id = vid; } +uint8_t uni_hid_parser_hat_to_dpad(uint8_t hat) { return hat == 0 ? DPAD_UP : 0; } +void uni_hid_parser_wii_setup(uni_hid_device_t* d) { (void)d; assert(false); } +bool uni_hid_parser_wii_accel_snapshot(uni_hid_device_t* d, int32_t v[3], uint32_t* s) { + (void)d; (void)v; (void)s; assert(false); return false; +} +bool uni_hid_parser_wii_gyro_snapshot(uni_hid_device_t* d, int32_t v[3], uint32_t* s) { + (void)d; (void)v; (void)s; assert(false); return false; +} +static void put16(uint8_t* bytes, int value) { + bytes[0] = (uint16_t)value; bytes[1] = (uint16_t)value >> 8; +} +static uni_native_motion_snapshot_t snapshot(uni_hid_device_t* d) { + uni_native_motion_snapshot_t result; + assert(uni_hid_parser_native_motion_snapshot(d, &result)); + return result; +} +static void ds4_seal(uint8_t bytes[78]) { + const uint8_t transaction = 0xa1; + uint32_t crc = ~uni_crc32_le(uni_crc32_le(UINT32_MAX, &transaction, 1), bytes, 74); + for (unsigned i = 0; i < 4; ++i) bytes[74 + i] = crc >> (8 * i); +} +static void ds4_input(uni_hid_device_t* d, uint8_t bytes[78], uint16_t tick) { + put16(bytes + 12, tick); + ds4_seal(bytes); + uni_hid_parser_ds4_init_report(d); + uni_hid_parser_ds4_parse_input_report(d, bytes, 78); +} +static void ds4_provenance(void) { + uni_hid_device_t d = {0}; + d.report_parser.setup = uni_hid_parser_ds4_setup; + uni_hid_parser_ds4_setup(&d); + uint8_t bytes[78] = {0x11}; + bytes[7] = 0x28; // Cross, neutral hat. + put16(bytes + 19, 8192); // Gyro Z. + put16(bytes + 23, 8192); // Acceleration Y. + ds4_input(&d, bytes, 65530); + uni_native_motion_snapshot_t first = snapshot(&d); + assert(first.report_valid && !first.accel_valid && !first.gyro_valid); + assert(d.controller.gamepad.buttons & BUTTON_A); + uint8_t calibration[37] = {2}; + for (unsigned i = 0; i < 3; ++i) { + put16(calibration + 7 + 2 * i, 100); + put16(calibration + 13 + 2 * i, -100); + put16(calibration + 23 + 4 * i, 8192); + put16(calibration + 25 + 4 * i, -8192); + } + put16(calibration + 19, 100); put16(calibration + 21, 100); + uni_hid_parser_ds4_parse_feature_report(&d, calibration, 36); + ds4_input(&d, bytes, 65531); + assert(!snapshot(&d).accel_valid); + uni_hid_parser_ds4_parse_feature_report(&d, calibration, 37); + ds4_input(&d, bytes, 65531); + assert(!snapshot(&d).accel_valid); // Calibration cannot rejuvenate a cached tick. + ds4_input(&d, bytes, 2); // Wrapped sensor clock is forward progress. + first = snapshot(&d); + assert(first.accel_valid && first.gyro_valid && first.accel_q13[1] == 8192); + ds4_input(&d, bytes, 2); + assert(snapshot(&d).accel_sequence == first.accel_sequence); + ds4_input(&d, bytes, 1); + assert(snapshot(&d).gyro_sequence == first.gyro_sequence); + bytes[7] ^= 0x20; // Corrupt input must not be admitted as button release. + uni_hid_parser_ds4_parse_input_report(&d, bytes, 78); + assert(!snapshot(&d).report_valid); + assert(snapshot(&d).accel_sequence == first.accel_sequence); + uint8_t buttons[10] = {1}; + uni_hid_parser_ds4_init_report(&d); + uni_hid_parser_ds4_parse_input_report(&d, buttons, sizeof(buttons)); + assert(snapshot(&d).report_valid && snapshot(&d).accel_sequence == first.accel_sequence); + // Valid accel calibration with degenerate gyro extrema is accel-only. + memset(calibration + 7, 0, 12); + uni_hid_parser_ds4_parse_feature_report(&d, calibration, 37); + ds4_input(&d, bytes, 3); + assert(snapshot(&d).accel_valid && !snapshot(&d).gyro_valid); + uni_hid_parser_native_motion_forget(&d); + uni_hid_parser_ds4_setup(&d); // Same pointer, new parser lifetime. + assert(!snapshot(&d).report_valid && !snapshot(&d).accel_valid); + uni_hid_parser_ds4_parse_feature_report(&d, calibration, 37); + ds4_input(&d, bytes, 3); + assert(snapshot(&d).accel_sequence != first.accel_sequence); + uni_hid_parser_native_motion_forget(&d); +} + +static void move_calibration(uni_hid_device_t* d, bool zcm2, bool valid) { + uint8_t blob[143] = {0}; + const uint8_t lo1[] = {0x0a, 0x24, 0x14}, hi1[] = {0x16, 0x1e, 0x08}; + const uint8_t lo2[] = {0x08, 0x16, 0x24}, hi2[] = {0x02, 0x10, 0x1e}; + const uint8_t bias1[] = {0x2a, 0x2c, 0x2e}, high1[] = {0x46, 0x50, 0x5a}; + const uint8_t bias2[] = {0x26, 0x28, 0x2a}, high2[] = {0x30, 0x38, 0x40}, low2[] = {0x42, 0x4a, 0x52}; + for (unsigned i = 0; i < 3; ++i) { + int center = zcm2 ? 0 : 0x8000; + put16(blob + (zcm2 ? lo2[i] : lo1[i]), center - (valid ? 1000 : 0)); + put16(blob + (zcm2 ? hi2[i] : hi1[i]), center + (valid ? 1000 : 0)); + put16(blob + (zcm2 ? bias2[i] : bias1[i]), center); + put16(blob + (zcm2 ? high2[i] : high1[i]), center + 1000); + if (zcm2) put16(blob + low2[i], -1000); + } + blob[0] = 0x10; + uni_hid_parser_psmove_parse_feature_report(d, blob, 49); + uint8_t continuation[49] = {0x10, zcm2 ? 0x81 : 1}; + memcpy(continuation + 2, blob + 49, 47); + uni_hid_parser_psmove_parse_feature_report(d, continuation, 49); + if (!zcm2) { + continuation[1] = 0x82; + memcpy(continuation + 2, blob + 96, 47); + uni_hid_parser_psmove_parse_feature_report(d, continuation, 49); + } +} +static void move_provenance(bool zcm2) { + uni_hid_device_t d = {0}; + d.report_parser.setup = uni_hid_parser_psmove_setup; + d.product_id = zcm2 ? 0x0c5e : 0x03d5; + uni_hid_parser_psmove_setup(&d); + uint8_t report[49] = {1}; + report[2] = 0x40; // Cross. + report[43] = 1; + for (unsigned i = 0; i < 12; ++i) + put16(report + 13 + 2 * i, (zcm2 ? 0 : 0x8000) + 1000); + uni_hid_parser_psmove_parse_input_report(&d, report, sizeof(report)); + assert(snapshot(&d).report_valid && !snapshot(&d).accel_valid); + move_calibration(&d, zcm2, false); + report[43] = 2; + uni_hid_parser_psmove_parse_input_report(&d, report, sizeof(report)); + assert(!snapshot(&d).accel_valid && !snapshot(&d).gyro_valid); + move_calibration(&d, zcm2, true); + uni_hid_parser_psmove_parse_input_report(&d, report, sizeof(report)); + assert(!snapshot(&d).accel_valid); + report[43] = 3; + uni_hid_parser_psmove_parse_input_report(&d, report, sizeof(report)); + uni_native_motion_snapshot_t first = snapshot(&d); + assert(first.accel_valid && first.gyro_valid && first.accel_q13[0] == 8192); + assert(first.gyro_q10[0] == (zcm2 ? 540 : 480) * 1024); + uni_hid_parser_psmove_init_report(&d); + uni_hid_parser_psmove_parse_input_report(&d, report, sizeof(report)); + assert(snapshot(&d).gyro_sequence == first.gyro_sequence); + report[43] = 2; + uni_hid_parser_psmove_parse_input_report(&d, report, sizeof(report)); + assert(snapshot(&d).accel_sequence == first.accel_sequence); + uni_hid_parser_psmove_parse_input_report(&d, report, 43); + assert(!snapshot(&d).report_valid && snapshot(&d).gyro_sequence == first.gyro_sequence); + uni_hid_parser_native_motion_forget(&d); +} + +static void switch_setup(uni_hid_device_t* d, uint8_t type, bool calibrated) { + memset(d, 0, sizeof(*d)); + d->report_parser.setup = uni_hid_parser_switch_setup; + d->conn.interrupt_cid = 0x40; + unsigned ready_before = ready_count; + uni_hid_parser_switch_setup(d); + for (unsigned step = 0; ready_count == ready_before && step < 12; ++step) { + assert(command_len >= 12); + uint8_t reply[49] = {0x21}; + reply[13] = 0x80; + reply[14] = command[11]; + if (reply[14] == 2) reply[17] = type; + if (reply[14] == 0x10) { + memcpy(reply + 15, command + 12, 5); + // Centered sticks, nonzero spans; no user calibration magic. + if (little_endian_read_32(reply, 15) == 0x603d || little_endian_read_32(reply, 15) == 0x6046) + memset(reply + 20, 0x80, reply[19]); + if (little_endian_read_32(reply, 15) == 0x6020) { + for (unsigned i = 0; i < 3; ++i) { + put16(reply + 26 + 2 * i, calibrated ? 16384 : 0); + put16(reply + 38 + 2 * i, calibrated ? 13371 : 0); + } + // Truncation with a convincing declared length cannot certify sensors. + uni_hid_parser_switch_parse_input_report(d, reply, 22); + assert(!snapshot(d).accel_valid); + } + } + uni_hid_parser_switch_parse_input_report(d, reply, sizeof(reply)); + } + assert(ready_count != ready_before); +} +static void switch_provenance(uint8_t type, bool calibrated) { + uni_hid_device_t d; + switch_setup(&d, type, calibrated); + uint8_t report[49] = {0x30, 254}; + memset(report + 6, 0x80, 6); + put16(report + 37, 4096); // Latest accelerometer sample, native X. + put16(report + 43, 1000); // Latest gyro sample, native X. + uni_hid_parser_switch_parse_input_report(&d, report, sizeof(report)); + uni_native_motion_snapshot_t first = snapshot(&d); + assert(first.report_tracked && first.report_valid); + if (type == 0x0b || !calibrated) { + assert(!first.accel_valid && !first.gyro_valid); + } else { + assert(first.accel_valid && first.gyro_valid && first.accel_q13[2] == -8192); + uni_hid_parser_switch_parse_input_report(&d, report, sizeof(report)); + assert(snapshot(&d).accel_sequence == first.accel_sequence); + report[1] = 1; + uni_hid_parser_switch_parse_input_report(&d, report, sizeof(report)); + assert(snapshot(&d).gyro_sequence != first.gyro_sequence); + first = snapshot(&d); + report[1] = 0; + uni_hid_parser_switch_parse_input_report(&d, report, sizeof(report)); + assert(snapshot(&d).gyro_sequence == first.gyro_sequence); + } + uni_hid_parser_switch_parse_input_report(&d, report, 48); + assert(!snapshot(&d).report_valid && snapshot(&d).accel_sequence == first.accel_sequence); + uint8_t buttons[12] = {0x3f}; + uni_hid_parser_switch_parse_input_report(&d, buttons, sizeof(buttons)); + assert(snapshot(&d).report_valid && snapshot(&d).accel_sequence == first.accel_sequence); + uni_hid_parser_switch_teardown(&d); + assert(!uni_hid_parser_native_motion_snapshot(&d, &first)); +} +int main(void) { + ds4_provenance(); + move_provenance(false); + move_provenance(true); + switch_provenance(3, true); + switch_provenance(1, true); + switch_provenance(2, true); + switch_provenance(3, false); + switch_provenance(0x0b, false); + uni_hid_device_t sensorless = {0}; + uni_native_motion_snapshot_t absent; + memset(&absent, 0xff, sizeof(absent)); + assert(!uni_hid_parser_native_motion_snapshot(&sensorless, &absent)); + assert(!absent.report_tracked && !absent.accel_valid && !absent.gyro_valid && absent.gyro_sequence == 0); + puts("Native DS4, Move, Switch motion provenance and sensorless absence passed"); + return 0; +} diff --git a/tests/switch2_dualsense_bridge_test.cpp b/tests/switch2_native_gamepad_bridge_test.cpp similarity index 85% rename from tests/switch2_dualsense_bridge_test.cpp rename to tests/switch2_native_gamepad_bridge_test.cpp index 0fe9c89..101e40b 100644 --- a/tests/switch2_dualsense_bridge_test.cpp +++ b/tests/switch2_native_gamepad_bridge_test.cpp @@ -13,7 +13,7 @@ namespace { uint64_t now_us = 1000000; uint32_t stage; -Bluepad32DualSenseBridgeSnapshot source; +Bluepad32NativeGamepadSnapshot source; ControllerProfile profile; bool alternating_shortcut; bool shortcut_phase; @@ -32,11 +32,11 @@ void bluepad32_input_backend_start() { stage = 2; } void bluepad32_input_backend_poll() {} void bluepad32_input_backend_diagnostics(Bluepad32BackendDiagnostics* out) { *out = {}; out->initialization_stage = stage; } void bluepad32_input_backend_open_pairing_window() {} -void bluepad32_input_backend_select_dualsense_source(const uint8_t*) {} -void bluepad32_input_backend_dualsense_snapshot(Bluepad32DualSenseBridgeSnapshot* out) { *out = source; } -bool bluepad32_input_backend_dualsense_sample_request(uint8_t, uint8_t, uint64_t*) { return false; } -int bluepad32_input_backend_dualsense_sample_result(uint8_t, uint64_t) { return -1; } -void bluepad32_input_backend_dualsense_sample_cancel(uint8_t) {} +void bluepad32_input_backend_select_native_source(const uint8_t*) {} +void bluepad32_input_backend_native_snapshot(Bluepad32NativeGamepadSnapshot* out) { *out = source; } +bool bluepad32_input_backend_native_sample_request(uint8_t, uint8_t, uint64_t*) { return false; } +int bluepad32_input_backend_native_sample_result(uint8_t, uint64_t) { return -1; } +void bluepad32_input_backend_native_sample_cancel(uint8_t) {} void bluepad32_input_backend_queue_profile_feedback(uint8_t, uint32_t, uint8_t, ControllerProfileConfirmationPolicy) {} void controller_profile_runtime_reset() { profile = controller_profile_default(controller_identity_global(), 0); } bool controller_profile_runtime_take_initial_profile_indication(uint8_t, ControllerProfileRuntimeProfileChangeEvent*) { return false; } @@ -94,8 +94,9 @@ void publish(bool motion = true) { source.received_us = time_us_32(); ++source.state_generation; if (motion) { - source.motion_received_us = time_us_32(); - ++source.motion_sequence; + source.accel_received_us = source.gyro_received_us = time_us_32(); + ++source.accel_sequence; + ++source.gyro_sequence; } } uint32_t peek(uint8_t instance) { @@ -210,7 +211,7 @@ void independent_backpressure_and_resets() { } void real_motion_admission_and_loss() { - source.motion_valid = true; + source.accel_valid = source.gyro_valid = true; source.accel_q13[1] = 8192; // SDL face-up gravity -> native +Z, no mouse mounting. // These values have already passed the DS5 factory-calibration path. // Even a controller rotating at connection must not wait for stationary bias estimation. @@ -262,11 +263,11 @@ void real_motion_admission_and_loss() { source.gyro_q10[1] -= 90 * 1024; publish(); const uint32_t obsolete = peek(0); - source.motion_valid = false; + source.accel_valid = source.gyro_valid = false; assert(!probe_controller_input_commit_native_report(0, obsolete)); publish(false); pair(); assert(controls[0].active && reports[0][2] == 2 && imu_length(0) == 0 && imu_length(1) == 0); - source.motion_valid = true; + source.accel_valid = source.gyro_valid = true; publish(); pair(); assert(imu_length(0) == 30); for (unsigned i = 0; i < 38; ++i) { publish(false); pair(); } @@ -275,7 +276,7 @@ void real_motion_admission_and_loss() { const uint32_t old_right = peek(0), old_left = peek(1); // Even a reconnect whose teardown was missed retires both USB identities. ++source.controller.connection_generation; - source.motion_valid = false; // The backend withholds motion until a new report in the new epoch. + source.accel_valid = source.gyro_valid = false; // New epochs require fresh reports. assert(!probe_controller_input_commit_native_report(0, old_right)); assert(!probe_controller_input_commit_native_report(1, old_left)); pair(); @@ -299,7 +300,7 @@ void selected_motion_target_keeps_both_control_halves() { source.controller.connection_generation = 99; source.controller.state.button_south = true; source.controller.state.dpad_up = true; - source.motion_valid = true; + source.accel_valid = source.gyro_valid = true; source.accel_q13[1] = 8192; profile = controller_profile_default(controller_identity_global(), 0); calibrate(0, 2048, 2048, 2047, 2047, 2048, 2048); @@ -318,6 +319,43 @@ void selected_motion_target_keeps_both_control_halves() { no_mouse_or_rails(); } +void wii_bias_and_independent_sensor_freshness() { + ++source.controller.connection_generation; + source.requires_stationary_bias = true; + source.gyro_q10[1] = 2 * 1024; + publish(); pair(); + assert(controls[0].active && controls[1].active); + assert(reports[0][2] == 0x01 && reports[1][2] == 0x08); + assert(imu_length(0) == 0 && imu_length(1) == 0); + for (unsigned i = 0; i < 400; ++i) { publish(); pair(); } + for (uint8_t instance = 0; instance < 2; ++instance) { + assert(imu_length(instance) == ((SWITCH2_BRIDGE_IMU_TARGET_MASK & (1u << instance)) ? 30 : 0)); + } + // Accelerometer-only reports must not refresh a stalled MotionPlus stream. + for (unsigned i = 0; i < 38; ++i) { + publish(false); + source.accel_received_us = time_us_32(); + ++source.accel_sequence; + pair(); + } + assert(controls[0].active && controls[1].active); + assert(imu_length(0) == 0 && imu_length(1) == 0); + source.gyro_valid = false; + publish(); pair(); + assert(reports[0][2] == 0x01 && reports[1][2] == 0x08); + assert(imu_length(0) == 0 && imu_length(1) == 0); + // Returning real Wii sensors must settle again, not reuse the old bias. + source.gyro_valid = true; + publish(); pair(); + assert(imu_length(0) == 0 && imu_length(1) == 0); + // A factory-calibrated source switching policy must initialize immediately. + source.requires_stationary_bias = false; + publish(); pair(); + for (uint8_t instance = 0; instance < 2; ++instance) { + assert(imu_length(instance) == ((SWITCH2_BRIDGE_IMU_TARGET_MASK & (1u << instance)) ? 30 : 0)); + } +} + } // namespace int main() { @@ -331,5 +369,6 @@ int main() { independent_backpressure_and_resets(); if (SWITCH2_BRIDGE_IMU_TARGET_MASK == 3) real_motion_admission_and_loss(); selected_motion_target_keeps_both_control_halves(); + wii_bias_and_independent_sensor_freshness(); return 0; } diff --git a/tests/switch2_parser_native_test.c b/tests/switch2_parser_native_test.c index 0f6260b..373b5ee 100644 --- a/tests/switch2_parser_native_test.c +++ b/tests/switch2_parser_native_test.c @@ -4,9 +4,19 @@ #include "protocol_fixture.h" #include "parser/uni_hid_parser_switch2.h" +#include "parser/uni_hid_parser_native_motion.h" #include "parser/uni_switch2_pairing.h" #include "sdkconfig.h" +#if SWITCH2_BRIDGE_FULL_INPUT +void uni_hid_parser_wii_setup(uni_hid_device_t* d) { (void)d; assert(false); } +bool uni_hid_parser_wii_accel_snapshot(uni_hid_device_t* d, int32_t v[3], uint32_t* s) { + (void)d; (void)v; (void)s; assert(false); return false; +} +bool uni_hid_parser_wii_gyro_snapshot(uni_hid_device_t* d, int32_t v[3], uint32_t* s) { + (void)d; (void)v; (void)s; assert(false); return false; +} +#endif #define PEERS CONFIG_BLUEPAD32_MAX_DEVICES #define SERVICE_START 0x100 #define INPUT_HANDLE 0x104 @@ -367,7 +377,10 @@ static unsigned response_data(struct fixture_peer* peer, uint8_t* out, bool eras if (address == 0x13000) { little_endian_store_16(out, 16 + 18, UNI_SW2_NINTENDO_VID); little_endian_store_16(out, 16 + 20, peer->device.product_id); - } else if (address != 0x13044) { + } else if (address == 0x13044) { + if (erased) + memset(out + 16, 0xff, length); + } else { if (erased) memset(out + 16, 0xff, length); else { @@ -657,8 +670,19 @@ static void test_calibration_physical_inputs_and_sensor_units(void) { } assert(gp->accel[0] == 8192 && gp->accel[1] == -8192 && gp->accel[2] == -16384); assert(gp->gyro[0] >= 2041740 && gp->gyro[0] <= 2041750); +#if SWITCH2_BRIDGE_FULL_INPUT + uni_native_motion_snapshot_t first, duplicate; + assert(uni_hid_parser_native_motion_snapshot(&peer->device, &first)); + assert(first.accel_valid && first.gyro_valid && first.accel_q13[2] == -16384); +#endif notify(peer, INPUT_HANDLE, report, sizeof(report)); // Repeated sensor sample must not become zero. +#if SWITCH2_BRIDGE_FULL_INPUT + assert(uni_hid_parser_native_motion_snapshot(&peer->device, &duplicate)); + assert(duplicate.accel_sequence == first.accel_sequence && duplicate.gyro_sequence == first.gyro_sequence); + assert(duplicate.gyro_q10[0] >= 2041740); +#else assert(gp->gyro[0] >= 2041740); +#endif reset(); peer = connect_peer(UNI_SW2_JOYCON_L_PID, false); discover(peer); @@ -672,6 +696,59 @@ static void test_calibration_physical_inputs_and_sensor_units(void) { assert(uni_hid_parser_switch2_extra_buttons(&peer->device) == (UNI_SW2_BUTTON_LEFT_SL | UNI_SW2_BUTTON_LEFT_SR)); } +#if SWITCH2_BRIDGE_FULL_INPUT +static void test_native_independent_motion_and_lifetime(void) { + const uint16_t products[] = {UNI_SW2_PRO_PID, UNI_SW2_JOYCON_L_PID, UNI_SW2_JOYCON_R_PID}; + uint32_t previous_sequence = 0; + for (unsigned product = 0; product < 3; ++product) { + for (unsigned invalid_bias = 0; invalid_bias < 2; ++invalid_bias) { + reset(); + struct fixture_peer* peer = connect_peer(products[product], false); + discover(peer); + subscribe_response(peer); + for (unsigned step = 0; step < 10; ++step) { + if (peer->command[0] == 2 && little_endian_read_32(peer->command, 12) == 0x13044) + break; + acknowledge(peer, false); + } + assert(little_endian_read_32(peer->command, 12) == 0x13044); + acknowledge(peer, invalid_bias); + finish_setup(peer); + uni_native_motion_snapshot_t native; + assert(uni_hid_parser_native_motion_snapshot(&peer->device, &native)); + assert(!native.report_valid && !native.accel_valid && !native.gyro_valid); + uint8_t report[63] = {0}; + little_endian_store_16(report, 48, 4096); + little_endian_store_16(report, 54, 32767); + little_endian_store_32(report, 42, 1000); + notify(peer, INPUT_HANDLE, report, sizeof(report)); + assert(uni_hid_parser_native_motion_snapshot(&peer->device, &native)); + assert(native.report_valid && native.accel_valid && native.accel_q13[0] == 8192); + assert(!native.gyro_valid && native.accel_sequence != previous_sequence); + for (unsigned sample = 1; sample < 46; ++sample) { + advance(10); + little_endian_store_32(report, 42, 1000 + sample * 10000); + notify(peer, INPUT_HANDLE, report, sizeof(report)); + } + assert(uni_hid_parser_native_motion_snapshot(&peer->device, &native)); + assert(native.accel_valid && native.gyro_valid == !invalid_bias); + uint32_t accel = native.accel_sequence, gyro = native.gyro_sequence; + little_endian_store_32(report, 42, 1000); // Regressed sensor clock. + notify(peer, INPUT_HANDLE, report, sizeof(report)); + assert(uni_hid_parser_native_motion_snapshot(&peer->device, &native)); + assert(native.accel_sequence == accel && native.gyro_sequence == gyro); + uni_hid_parser_switch2_parse_input_report(&peer->device, report, 62); + assert(uni_hid_parser_native_motion_snapshot(&peer->device, &native)); + assert(!native.report_valid && native.accel_sequence == accel); + previous_sequence = accel; + uni_hid_parser_switch2_teardown(&peer->device); + assert(!uni_hid_parser_native_motion_snapshot(&peer->device, &native)); + assert(!native.accel_valid && !native.gyro_valid); + } + } +} +#endif + static uint64_t rumble_frame(const struct fixture_peer* peer) { uint64_t value = 0; for (unsigned i = 0; i < 5; ++i) @@ -1223,6 +1300,9 @@ static void test_neutral_budget_waits_for_success_and_stale_active_completion(vo } int main(void) { +#if SWITCH2_BRIDGE_FULL_INPUT + test_native_independent_motion_and_lifetime(); +#endif test_connected_callback_rejection(); test_advertisement_bounds_and_admission(); test_discovery_metadata_and_rejection_isolation(); diff --git a/tests/switch_parser_native_stubs/btstack.h b/tests/switch_parser_native_stubs/btstack.h index d5936e0..c01557b 100644 --- a/tests/switch_parser_native_stubs/btstack.h +++ b/tests/switch_parser_native_stubs/btstack.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -23,6 +24,12 @@ typedef struct btstack_timer_source { #define HID_MESSAGE_TYPE_DATA 0x0a #define HID_REPORT_TYPE_OUTPUT 0x02 #define btstack_min(a, b) ((a) < (b) ? (a) : (b)) +#define btstack_max(a, b) ((a) > (b) ? (a) : (b)) + +static inline uint32_t little_endian_read_32(const uint8_t* bytes, int offset) { + return (uint32_t)bytes[offset] | ((uint32_t)bytes[offset + 1] << 8) | + ((uint32_t)bytes[offset + 2] << 16) | ((uint32_t)bytes[offset + 3] << 24); +} void btstack_run_loop_set_timer(btstack_timer_source_t* timer, uint32_t ms); void btstack_run_loop_add_timer(btstack_timer_source_t* timer); diff --git a/tests/test_dualsense_parser_native.py b/tests/test_dualsense_parser_native.py index fd762a4..10d3e09 100644 --- a/tests/test_dualsense_parser_native.py +++ b/tests/test_dualsense_parser_native.py @@ -32,7 +32,7 @@ def test_dualsense_parser_native(tmp_path: Path) -> None: "-fdata-sections", "-DENABLE_BLE", "-DENABLE_CLASSIC", - "-DSWITCH2_BRIDGE_DUALSENSE_INPUT=1", + "-DSWITCH2_BRIDGE_FULL_INPUT=1", "-DHID_MESSAGE_TYPE_GET_REPORT=4", "-DHID_REPORT_TYPE_FEATURE=3", f"-I{root / 'tests' / 'switch_parser_native_stubs'}", @@ -40,6 +40,9 @@ def test_dualsense_parser_native(tmp_path: Path) -> None: f"-I{component / 'include'}", str(root / "tests" / "dualsense_parser_native_test.c"), str(component / "parser" / "uni_hid_parser_ds5.c"), + str( + root / "bluepad32_config" / "parser" / "uni_hid_parser_native_motion.c" + ), str(component / "uni_utils.c"), "-Wl,--gc-sections", "-o", diff --git a/tests/test_dualsense_backend_native.py b/tests/test_native_gamepad_backend_native.py similarity index 71% rename from tests/test_dualsense_backend_native.py rename to tests/test_native_gamepad_backend_native.py index 026801f..28aa85d 100644 --- a/tests/test_dualsense_backend_native.py +++ b/tests/test_native_gamepad_backend_native.py @@ -4,15 +4,18 @@ import shutil import subprocess from pathlib import Path +import pytest -def test_dualsense_backend_native(tmp_path: Path) -> None: + +@pytest.mark.parametrize("source", ("GAMEPAD", "DUALSENSE")) +def test_native_gamepad_backend_native(tmp_path: Path, source: str) -> 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 / "dualsense_backend_test" + executable = tmp_path / "native_gamepad_backend_test" firmware = root / "src" / "firmware" sources = [ - root / "tests" / "dualsense_backend_test.cpp", + root / "tests" / "native_gamepad_backend_test.cpp", firmware / "profile" / "controller_profile.cpp", firmware / "profile" / "controller_profile_transform.cpp", firmware / "profile" / "controller_synthetic_input.cpp", @@ -34,7 +37,8 @@ def test_dualsense_backend_native(tmp_path: Path) -> None: "-DSWITCH_PICO_USB_OUTPUT_MODES=1", "-DSWITCH_PICO_ENABLE_BLE=1", "-DSWITCH_PICO_ENABLE_CLASSIC=1", - "-DSWITCH2_BRIDGE_DUALSENSE_INPUT=1", + "-DSWITCH2_BRIDGE_FULL_INPUT=1", + f"-DSWITCH2_BRIDGE_{source}_INPUT=1", f"-I{root / 'tests' / 'bluepad32_native_stubs'}", f"-I{firmware}", f"-I{root / 'bluepad32_config'}", @@ -52,3 +56,12 @@ def test_dualsense_backend_native(tmp_path: Path) -> None: "cue-races", ): subprocess.run([str(executable), scenario], check=True, cwd=root) + if source == "GAMEPAD": + for scenario in ( + "sensorless-admission", + "independent-motion", + "paired-source", + "pair-cue-races", + "mono-rumble", + ): + subprocess.run([str(executable), scenario], check=True, cwd=root) diff --git a/tests/test_native_gamepad_motion_parser_native.py b/tests/test_native_gamepad_motion_parser_native.py new file mode 100644 index 0000000..b84d75e --- /dev/null +++ b/tests/test_native_gamepad_motion_parser_native.py @@ -0,0 +1,58 @@ +from __future__ import annotations + +import shutil +import subprocess +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "tools")) + +from prepare_bluepad32 import prepare_bluepad32 + + +def test_native_gamepad_motion_parser_provenance(tmp_path: Path) -> None: + root = Path(__file__).resolve().parents[1] + compiler = shutil.which("cc") or shutil.which("gcc") + assert compiler is not None, "a host C compiler is required" + prepared = prepare_bluepad32( + root / "external" / "bluepad32", + root / "patches" / "bluepad32-sdl3-imu.patch", + tmp_path / "bluepad32-src", + ) + component = prepared / "src" / "components" / "bluepad32" + executable = tmp_path / "native_gamepad_motion_parser_test" + subprocess.run( + [ + compiler, + "-std=gnu11", + "-O1", + "-Wall", + "-Wextra", + "-ffunction-sections", + "-fdata-sections", + "-DENABLE_BLE", + "-DENABLE_CLASSIC", + "-DSWITCH2_BRIDGE_FULL_INPUT=1", + "-DHID_MESSAGE_TYPE_GET_REPORT=4", + "-DHID_REPORT_TYPE_FEATURE=3", + f"-I{root / 'tests' / 'switch_parser_native_stubs'}", + f"-I{root / 'bluepad32_config'}", + f"-I{component / 'include'}", + str(root / "tests" / "native_gamepad_motion_parser_test.c"), + str( + root / "bluepad32_config" / "parser" / "uni_hid_parser_native_motion.c" + ), + str(component / "parser" / "uni_hid_parser_ds4.c"), + str(component / "parser" / "uni_hid_parser_psmove.c"), + str(component / "parser" / "uni_hid_parser_switch.c"), + str(component / "uni_circular_buffer.c"), + str(component / "uni_utils.c"), + str(component / "controller" / "uni_gamepad.c"), + "-Wl,--gc-sections", + "-o", + str(executable), + ], + check=True, + cwd=root, + ) + subprocess.run([str(executable)], check=True, cwd=root) diff --git a/tests/test_switch2_dualsense_bridge_native.py b/tests/test_switch2_native_gamepad_bridge_native.py similarity index 82% rename from tests/test_switch2_dualsense_bridge_native.py rename to tests/test_switch2_native_gamepad_bridge_native.py index 9c437a8..c289588 100644 --- a/tests/test_switch2_dualsense_bridge_native.py +++ b/tests/test_switch2_native_gamepad_bridge_native.py @@ -8,14 +8,14 @@ import pytest @pytest.mark.parametrize("imu_target", [1, 2, 3], ids=["right", "left", "both"]) -def test_dualsense_native_bridge_mapping_motion_and_backpressure( +def test_native_gamepad_bridge_mapping_motion_and_backpressure( tmp_path: Path, imu_target: int, ) -> 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 / "switch2_dualsense_bridge_test" + executable = tmp_path / "switch2_native_gamepad_bridge_test" subprocess.run( [ compiler, @@ -25,7 +25,8 @@ def test_dualsense_native_bridge_mapping_motion_and_backpressure( "-Werror", "-pedantic", "-DSWITCH_PICO_SWITCH2_USB_BRIDGE=1", - "-DSWITCH2_BRIDGE_DUALSENSE_INPUT=1", + "-DSWITCH2_BRIDGE_GAMEPAD_INPUT=1", + "-DSWITCH2_BRIDGE_FULL_INPUT=1", "-DSWITCH2_BRIDGE_SOURCE_AUTO=1", "-DSWITCH2_PROBE_HUB=1", f"-DSWITCH2_BRIDGE_IMU_TARGET_MASK={imu_target}", @@ -35,9 +36,9 @@ def test_dualsense_native_bridge_mapping_motion_and_backpressure( f"-I{root / 'tests' / 'wii_ir_aiming_native_stubs'}", f"-I{root / 'tools' / 'switch2_usb_probe'}", f"-I{root / 'src' / 'firmware'}", - str(root / "tests" / "switch2_dualsense_bridge_test.cpp"), + str(root / "tests" / "switch2_native_gamepad_bridge_test.cpp"), str(root / "tools" / "switch2_usb_probe" / "controller_input.cpp"), - str(root / "tools" / "switch2_usb_probe" / "dualsense_input.cpp"), + str(root / "tools" / "switch2_usb_probe" / "native_gamepad_input.cpp"), str(root / "tools" / "switch2_usb_probe" / "native_imu.cpp"), str(root / "src" / "firmware" / "core" / "controller_identity.cpp"), str(root / "src" / "firmware" / "profile" / "controller_profile.cpp"), diff --git a/tests/test_switch2_parser_native.py b/tests/test_switch2_parser_native.py index 11776d1..d06a45f 100644 --- a/tests/test_switch2_parser_native.py +++ b/tests/test_switch2_parser_native.py @@ -13,7 +13,10 @@ sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "tools")) from prepare_bluepad32 import prepare_bluepad32 -def test_switch2_parser_protocol_and_lifecycle(tmp_path: Path) -> None: +@pytest.mark.parametrize("full_input", [False, True]) +def test_switch2_parser_protocol_and_lifecycle( + tmp_path: Path, full_input: bool +) -> None: root = Path(__file__).resolve().parents[1] compiler = shutil.which("cc") or shutil.which("gcc") assert compiler is not None, "a host C compiler is required" @@ -24,12 +27,17 @@ def test_switch2_parser_protocol_and_lifecycle(tmp_path: Path) -> None: if sdk_path := os.environ.get("PICO_SDK_PATH"): sdk_candidates.insert(0, Path(sdk_path)) btstack = next( - (sdk / "lib" / "btstack" / "src" for sdk in sdk_candidates - if (sdk / "lib" / "btstack" / "src" / "ble" / "gatt_client.h").is_file()), + ( + sdk / "lib" / "btstack" / "src" + for sdk in sdk_candidates + if (sdk / "lib" / "btstack" / "src" / "ble" / "gatt_client.h").is_file() + ), None, ) if btstack is None: - pytest.skip("Pico SDK BTstack headers required; configure firmware or set PICO_SDK_PATH") + pytest.skip( + "Pico SDK BTstack headers required; configure firmware or set PICO_SDK_PATH" + ) prepared = prepare_bluepad32( root / "external" / "bluepad32", root / "patches" / "bluepad32-sdl3-imu.patch", @@ -48,6 +56,7 @@ def test_switch2_parser_protocol_and_lifecycle(tmp_path: Path) -> None: "-fdata-sections", "-DENABLE_BLE", "-DENABLE_CLASSIC", + f"-DSWITCH2_BRIDGE_FULL_INPUT={int(full_input)}", f"-I{root / 'tests' / 'switch2_parser_native_stubs'}", f"-I{root / 'bluepad32_config'}", f"-I{component / 'include'}", @@ -58,6 +67,9 @@ def test_switch2_parser_protocol_and_lifecycle(tmp_path: Path) -> None: str(root / "tests" / "switch2_parser_native_test.c"), str(root / "bluepad32_config" / "parser" / "uni_hid_parser_switch2.c"), str(root / "bluepad32_config" / "parser" / "uni_switch2_haptics.c"), + str( + root / "bluepad32_config" / "parser" / "uni_hid_parser_native_motion.c" + ), str(btstack / "btstack_util.c"), "-Wl,--gc-sections", "-o", diff --git a/tests/test_wii_parser_native.py b/tests/test_wii_parser_native.py index 1fb17c5..6c5513e 100644 --- a/tests/test_wii_parser_native.py +++ b/tests/test_wii_parser_native.py @@ -5,12 +5,17 @@ import subprocess import sys from pathlib import Path +import pytest + sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "tools")) from prepare_bluepad32 import prepare_bluepad32 -def test_wii_parser_native_motion_and_lifecycle(tmp_path: Path) -> None: +@pytest.mark.parametrize("full_input", [False, True]) +def test_wii_parser_native_motion_and_lifecycle( + tmp_path: Path, full_input: bool +) -> None: root = Path(__file__).resolve().parents[1] compiler = shutil.which("cc") or shutil.which("gcc") assert compiler is not None, "a host C compiler is required" @@ -32,11 +37,15 @@ def test_wii_parser_native_motion_and_lifecycle(tmp_path: Path) -> None: "-fdata-sections", "-DENABLE_BLE", "-DENABLE_CLASSIC", + f"-DSWITCH2_BRIDGE_FULL_INPUT={int(full_input)}", f"-I{root / 'tests' / 'switch_parser_native_stubs'}", f"-I{root / 'bluepad32_config'}", f"-I{component / 'include'}", str(root / "tests" / "wii_parser_native_test.c"), str(component / "parser" / "uni_hid_parser_wii.c"), + str( + root / "bluepad32_config" / "parser" / "uni_hid_parser_native_motion.c" + ), str(component / "controller" / "uni_gamepad.c"), "-Wl,--gc-sections", "-o", diff --git a/tests/wii_parser_native_test.c b/tests/wii_parser_native_test.c index 4d99a85..564c2ca 100644 --- a/tests/wii_parser_native_test.c +++ b/tests/wii_parser_native_test.c @@ -5,6 +5,7 @@ #include #include "parser/uni_hid_parser_wii.h" +#include "parser/uni_hid_parser_native_motion.h" #include "uni_hid_device.h" // The real staged parser and gamepad definitions are linked. Only transport and @@ -501,11 +502,21 @@ static void accelerometer_snapshot_requires_fresh_calibrated_reports(void) { assert(uni_hid_parser_wii_accel_snapshot(&f.device, acceleration, &sequence)); expect_vector(acceleration, -8192, 2048, -4096); const uint32_t first = sequence; +#if SWITCH2_BRIDGE_FULL_INPUT + uni_native_motion_snapshot_t native; + assert(uni_hid_parser_native_motion_snapshot(&f.device, &native)); + assert(native.accel_valid && !native.gyro_valid && !native.report_tracked); + expect_vector(native.accel_q13, -8192, 2048, -4096); +#endif send_ack(0x16, 0); const uint8_t short_report[5] = {0x31}; feed(short_report, sizeof(short_report)); assert(uni_hid_parser_wii_accel_snapshot(&f.device, acceleration, &sequence)); assert(sequence == first); +#if SWITCH2_BRIDGE_FULL_INPUT + assert(uni_hid_parser_native_motion_snapshot(&f.device, &native)); + assert(native.accel_sequence == first && !native.gyro_valid); +#endif send_core_and_accel(); assert(uni_hid_parser_wii_accel_snapshot(&f.device, acceleration, &sequence)); assert(sequence != first); // Identical readings can still be fresh. @@ -528,6 +539,12 @@ static void gyro_snapshot_advances_only_on_calibrated_motionplus_packets(void) { assert(uni_hid_parser_wii_gyro_snapshot(&f.device, gyro, &sequence)); expect_vector(gyro, 360 * 1024, -120 * 1024, 40 * 1024); const uint32_t first = sequence; +#if SWITCH2_BRIDGE_FULL_INPUT + uni_native_motion_snapshot_t native; + assert(uni_hid_parser_native_motion_snapshot(&f.device, &native)); + assert(native.accel_valid && native.gyro_valid); + expect_vector(native.gyro_q10, 360 * 1024, -120 * 1024, 40 * 1024); +#endif send_nunchuk_controls(true); send_ack(0x16, 0); send_status(true); @@ -537,6 +554,10 @@ static void gyro_snapshot_advances_only_on_calibrated_motionplus_packets(void) { assert(uni_hid_parser_wii_gyro_snapshot(&f.device, gyro, &sequence)); assert(sequence == first); expect_vector(gyro, 360 * 1024, -120 * 1024, 40 * 1024); +#if SWITCH2_BRIDGE_FULL_INPUT + assert(uni_hid_parser_native_motion_snapshot(&f.device, &native)); + assert(native.gyro_sequence == first); +#endif send_motion(7760, 8200, 7560, false, true, false); assert(uni_hid_parser_wii_gyro_snapshot(&f.device, gyro, &sequence)); assert(sequence != first); // Equal values do not mean a duplicate packet. @@ -592,10 +613,23 @@ static void gyro_snapshot_invalidates_on_topology_and_teardown(void) { uni_hid_parser_wii_teardown(&f.device); assert(!uni_hid_parser_wii_gyro_snapshot(&f.device, gyro, &sequence)); assert(!uni_hid_parser_wii_rumble_ready(&f.device)); +#if SWITCH2_BRIDGE_FULL_INPUT + uni_native_motion_snapshot_t retired; + assert(uni_hid_parser_native_motion_snapshot(&f.device, &retired)); + assert(!retired.accel_valid && !retired.gyro_valid); +#endif f.ready_count = 0; // A new parser connection gets its own ready notification. uni_hid_parser_wii_setup(&f.device); finish_setup(); assert(!uni_hid_parser_wii_gyro_snapshot(&f.device, gyro, &sequence)); +#if SWITCH2_BRIDGE_FULL_INPUT + send_motion(7760, 8200, 7560, false, true, false); + // An already-active MP first resolves its downstream topology after reconnect. + finish_setup(); + send_motion(7760, 8200, 7560, false, true, false); + assert(uni_hid_parser_native_motion_snapshot(&f.device, &retired)); + assert(retired.gyro_valid && retired.gyro_sequence != first); +#endif } static void setup_read_and_write_errors_leave_buttons_ready(void) { @@ -808,6 +842,11 @@ static void legacy_plain_nunchuk_and_wii_u_pro(void) { assert(f.device.controller.gamepad.misc_buttons == MISC_BUTTON_START); expect_vector(f.device.controller.gamepad.accel, 0, 0, 0); expect_vector(f.device.controller.gamepad.gyro, 0, 0, 0); +#if SWITCH2_BRIDGE_FULL_INPUT + uni_native_motion_snapshot_t native; + assert(uni_hid_parser_native_motion_snapshot(&f.device, &native)); + assert(!native.accel_valid && !native.gyro_valid); +#endif } static void plus_selects_vertical_without_disabling_motion(void) { diff --git a/tools/native_joycon_hub_check.py b/tools/native_joycon_hub_check.py index 1f23501..44352be 100755 --- a/tools/native_joycon_hub_check.py +++ b/tools/native_joycon_hub_check.py @@ -2,8 +2,8 @@ """Bounded, non-pairing qualification of the switch-pico native Joy-Con USB hub. Requires Linux, PyUSB/libusb, and the existing sudo -n setfacl permission policy. -Uses the already-paired real R/L donors; it cannot wake or pair them. The JSON -capture is created exclusively before USB access and retains partial failures. +Uses already-paired R/L donors or one full gamepad; it cannot wake or pair them. +The JSON capture is created exclusively before USB access and retains failures. No reset, configuration change, pairing exchange, profile access, flash write, or HID output is sent. Motor sample playback requires --rumble-sample explicitly. """ @@ -37,14 +37,17 @@ MODELS = { } -def model_references(build_dir: Path) -> dict[str, dict[str, Any]]: +def model_references( + build_dir: Path, *, require_imu: bool = True +) -> dict[str, dict[str, Any]]: cache = {} for line in (build_dir / "CMakeCache.txt").read_text().splitlines(): if line.startswith("SWITCH2_") and ":" in line and "=" in line: field, value = line.split("=", 1) cache[field.split(":", 1)[0]] = value if ( - cache.get("SWITCH2_BRIDGE_INPUT") == "DUALSENSE" + require_imu + and cache.get("SWITCH2_BRIDGE_INPUT") in ("DUALSENSE", "GAMEPAD") and cache.get("SWITCH2_BRIDGE_IMU_TARGET", "BOTH") != "BOTH" ): raise ValueError( @@ -145,6 +148,7 @@ class Check: "duration_seconds": args.duration, "usb_timeout_ms": args.usb_timeout_ms, "rumble_sample": args.rumble_sample, + "require_imu": not args.input_only, }, "safety": { "pairing_writes": False, @@ -170,6 +174,7 @@ class Check: side: { "packets": 0, "valid_native_imu": 0, + "valid_native_packets": 0, "imu_counter_changes": 0, "wrong_side": 0, "unexpected_report": 0, @@ -186,6 +191,7 @@ class Check: "first_valid_seconds": None, "last_valid_seconds": None, "last_counter_change_seconds": None, + "last_control_change_seconds": None, } for side in SIDES }, @@ -697,15 +703,29 @@ class Check: rejection = f"native report is {len(packet)} bytes, expected 64" else: payload = packet[1:] + stream["valid_native_packets"] += 1 + controls = (payload[2:4], payload[5:8]) + sample["buttons_hex"], sample["stick_hex"] = ( + part.hex() for part in controls + ) + if any(controls[0]): + stream["buttons_nonzero"] += 1 + if side in self.last_controls and self.last_controls[side] != controls: + stream["control_changes"] += 1 + stream["last_control_change_seconds"] = self.elapsed() + self.last_controls[side] = controls length_offset = 14 if side == "L" else 15 length = payload[length_offset] key = str(length) stream["imu_lengths"][key] = stream["imu_lengths"].get(key, 0) + 1 if length == 0: stream["zero_length_imu"] += 1 - rejection = ( - "zero-length IMU: inactive donor/neutral fallback is not live input" - ) + if self.args.input_only: + if len(stream["samples"]) < 4: + stream["samples"].append(sample) + stream["last_sample"] = sample + else: + rejection = "zero-length IMU: inactive donor/neutral fallback is not live motion" else: try: block = ( @@ -743,18 +763,6 @@ class Check: self.last_counter[side] = counter if len(self.imu_evidence[side]) < 512: self.imu_evidence[side].add(block) - controls = (payload[2:4], payload[5:8]) - sample["buttons_hex"], sample["stick_hex"] = ( - part.hex() for part in controls - ) - if any(controls[0]): - stream["buttons_nonzero"] += 1 - if ( - side in self.last_controls - and self.last_controls[side] != controls - ): - stream["control_changes"] += 1 - self.last_controls[side] = controls format_key = f"{decoded['format']:02x}" first_format = format_key not in stream["imu_formats"] stream["imu_formats"][format_key] = ( @@ -783,7 +791,10 @@ class Check: def counts(self) -> dict[str, int]: return { - side: self.result["streams"][side]["valid_native_imu"] for side in SIDES + side: self.result["streams"][side][ + "valid_native_packets" if self.args.input_only else "valid_native_imu" + ] + for side in SIDES } def donors_ready(self) -> None: @@ -793,23 +804,31 @@ class Check: self.poll_pair(until, bool(iteration % 2)) iteration += 1 if all( - stream["valid_native_imu"] >= 3 and stream["imu_counter_changes"] >= 2 + (stream["buttons_nonzero"] > 0 and stream["control_changes"] >= 2) + if self.args.input_only + else ( + stream["valid_native_imu"] >= 3 + and stream["imu_counter_changes"] >= 2 + ) for stream in self.result["streams"].values() ): return details = "; ".join( - f"{side}: valid={stream['valid_native_imu']}, counter_changes={stream['imu_counter_changes']}, zero_imu={stream['zero_length_imu']}, invalid={stream['invalid']}, timeouts={stream['timeouts']}" + f"{side}: imu={stream['valid_native_imu']}, counter_changes={stream['imu_counter_changes']}, buttons={stream['buttons_nonzero']}, control_changes={stream['control_changes']}, zero_imu={stream['zero_length_imu']}, invalid={stream['invalid']}, timeouts={stream['timeouts']}" for side, stream in self.result["streams"].items() ) raise RuntimeError( - f"donors did not produce fresh decodable native IMU during the manual-wake window; {details}; no pairing/wake/reset was attempted" + f"sources did not satisfy the requested live-input evidence during the manual-input window; {details}; no pairing/wake/reset was attempted" ) def active(self) -> None: until = min(self.deadline, time.monotonic() + self.args.duration) initial_counts = self.counts() initial_changes = { - side: self.result["streams"][side]["imu_counter_changes"] for side in SIDES + side: self.result["streams"][side][ + "control_changes" if self.args.input_only else "imu_counter_changes" + ] + for side in SIDES } next_query = time.monotonic() round_number = 0 @@ -851,10 +870,12 @@ class Check: "no interleaved control/bulk round was bracketed by valid input from both donors" ) shared = self.imu_evidence["R"] & self.imu_evidence["L"] - shared_source = self.models["R"].get("source_mode") == "DUALSENSE" + shared_source = self.models["R"].get("source_mode") in ("DUALSENSE", "GAMEPAD") self.result["imu_isolation"] = { "sample_limit_per_side": 512, - "policy": "shared_physical_source" + "policy": "not_required_input_only" + if self.args.input_only + else "shared_physical_source" if shared_source else "independent_physical_sources", "identical_blocks_seen_on_both_sides": len(shared), @@ -865,7 +886,7 @@ class Check: side: len(blocks - shared) for side, blocks in self.imu_evidence.items() }, } - for side in SIDES: + for side in () if self.args.input_only else SIDES: evidence = ( self.imu_evidence[side] if shared_source @@ -881,18 +902,26 @@ class Check: for side in SIDES: stream = self.result["streams"][side] if ( - stream["valid_native_imu"] - initial_counts[side] < 3 - or stream["imu_counter_changes"] - initial_changes[side] < 2 + self.counts()[side] - initial_counts[side] < 3 + or stream[ + "control_changes" if self.args.input_only else "imu_counter_changes" + ] + - initial_changes[side] + < 2 ): self.error( - "insufficient fresh native donor IMU during active control/bulk reads", + "insufficient fresh controller transitions" + if self.args.input_only + else "insufficient fresh native source IMU during active control/bulk reads", side, ) - last_change = stream["last_counter_change_seconds"] + last_change = stream[ + "last_control_change_seconds" + if self.args.input_only + else "last_counter_change_seconds" + ] if last_change is None or self.elapsed() - last_change > 2: - self.error( - "donor IMU stopped advancing before streaming finished", side - ) + self.error("source stopped advancing before streaming finished", side) if stream["wrong_side"] or stream["unexpected_report"] or stream["invalid"]: self.error( f"rejected wrong-side={stream['wrong_side']}, unexpected={stream['unexpected_report']}, malformed={stream['invalid']} HID packets", @@ -938,7 +967,9 @@ class Check: import usb.util with self.stage("references"): - self.models = model_references(self.args.build_dir) + self.models = model_references( + self.args.build_dir, require_imu=not self.args.input_only + ) self.core, self.util = usb.core, usb.util with self.stage("discovery"): self.discover() @@ -1044,6 +1075,11 @@ def main() -> int: default=500, help="per control/bulk transfer timeout, 20..3000 ms (default: 500)", ) + parser.add_argument( + "--input-only", + action="store_true", + help="qualify controllers without IMU; requires real button presses and continued control changes on both halves", + ) parser.add_argument( "--rumble-sample", type=int, diff --git a/tools/switch2_usb_probe/controller_input.cpp b/tools/switch2_usb_probe/controller_input.cpp index a01cab4..1f6af17 100644 --- a/tools/switch2_usb_probe/controller_input.cpp +++ b/tools/switch2_usb_probe/controller_input.cpp @@ -13,8 +13,8 @@ #include extern "C" int probe_debug_printf(const char* format, ...); #endif -#if SWITCH2_BRIDGE_DUALSENSE_INPUT -#include "dualsense_input.h" +#if SWITCH2_BRIDGE_FULL_INPUT +#include "native_gamepad_input.h" #endif #if SWITCH2_BRIDGE_WII_INPUT #include @@ -26,9 +26,9 @@ extern "C" int probe_debug_printf(const char* format, ...); #if !SWITCH_PICO_SWITCH2_USB_BRIDGE || !SWITCH_PICO_BLUEPAD32 #error "The controller bridge requires Bluepad32" -#elif SWITCH2_BRIDGE_DUALSENSE_INPUT -#if !SWITCH_PICO_ENABLE_CLASSIC -#error "The DualSense bridge requires Classic Bluetooth" +#elif SWITCH2_BRIDGE_FULL_INPUT +#if SWITCH2_BRIDGE_DUALSENSE_INPUT && !SWITCH_PICO_ENABLE_CLASSIC +#error "The DualSense source requires Classic Bluetooth" #endif #elif !SWITCH_PICO_ENABLE_BLE || !SWITCH_PICO_SWITCH2_MOUSE_CAPTURE || \ !SWITCH_PICO_SWITCH2_MOUSE_CAPTURE_NATIVE @@ -36,7 +36,7 @@ extern "C" int probe_debug_printf(const char* format, ...); #endif namespace { -#if !SWITCH2_BRIDGE_DUALSENSE_INPUT +#if !SWITCH2_BRIDGE_FULL_INPUT constexpr uint8_t kSourceAddress[] = {SWITCH2_BRIDGE_SOURCE_ADDRESS_BYTES}; static_assert(sizeof(kSourceAddress) == 6, "Select one physical Bluetooth address"); #if SWITCH2_PROBE_COMPOSITE || SWITCH2_PROBE_HUB @@ -56,7 +56,7 @@ bool g_start_attempted; bool g_flash_ready; #if SWITCH2_BRIDGE_WII_INPUT probe_controller_input g_input; -#elif !SWITCH2_BRIDGE_DUALSENSE_INPUT +#elif !SWITCH2_BRIDGE_FULL_INPUT probe_controller_input g_inputs[PROBE_CONTROLLER_COUNT]; uint32_t g_received_times[PROBE_CONTROLLER_COUNT]; #endif @@ -382,9 +382,9 @@ extern "C" void probe_controller_input_clock_init(void) { extern "C" void probe_controller_input_init(void) { if (g_initialized) return; -#if SWITCH2_BRIDGE_DUALSENSE_INPUT +#if SWITCH2_BRIDGE_FULL_INPUT bluepad32_input_backend_init(); - probe_dualsense_input_init(); + probe_native_gamepad_input_init(); #elif SWITCH2_BRIDGE_WII_INPUT bluepad32_input_backend_init(); bluepad32_input_backend_select_wii_source(kSourceAddress); @@ -484,17 +484,17 @@ extern "C" void probe_controller_input_set_native_features(uint8_t features) { } #endif -#if SWITCH2_BRIDGE_DUALSENSE_INPUT +#if SWITCH2_BRIDGE_FULL_INPUT extern "C" void probe_controller_input_set_full_stick_calibration( uint8_t instance, const uint8_t calibration[9]) { - probe_dualsense_input_set_stick_calibration(instance, calibration); + probe_native_gamepad_input_set_stick_calibration(instance, calibration); } #endif extern "C" void probe_controller_input_set_native_stream(uint8_t instance, bool enabled) { if (instance >= PROBE_CONTROLLER_COUNT) return; -#if SWITCH2_BRIDGE_DUALSENSE_INPUT - probe_dualsense_input_set_native_stream(instance, enabled && g_flash_ready); +#if SWITCH2_BRIDGE_FULL_INPUT + probe_native_gamepad_input_set_native_stream(instance, enabled && g_flash_ready); #elif SWITCH2_BRIDGE_WII_INPUT enabled = enabled && g_flash_ready; if (g_native_stream != enabled || !enabled) discard_wii_output(); @@ -508,8 +508,8 @@ extern "C" void probe_controller_input_set_native_stream(uint8_t instance, bool extern "C" uint32_t probe_controller_input_peek_native_report( uint8_t instance, uint32_t now_ms, uint8_t report[63]) { if (instance >= PROBE_CONTROLLER_COUNT || !g_flash_ready) return 0; -#if SWITCH2_BRIDGE_DUALSENSE_INPUT - return probe_dualsense_input_peek_native_report(instance, now_ms, report); +#if SWITCH2_BRIDGE_FULL_INPUT + return probe_native_gamepad_input_peek_native_report(instance, now_ms, report); #elif SWITCH2_BRIDGE_WII_INPUT (void)now_ms; return prepare_wii_report(report); @@ -520,8 +520,8 @@ extern "C" uint32_t probe_controller_input_peek_native_report( extern "C" bool probe_controller_input_commit_native_report(uint8_t instance, uint32_t serial) { if (instance >= PROBE_CONTROLLER_COUNT || !g_flash_ready) return false; -#if SWITCH2_BRIDGE_DUALSENSE_INPUT - return probe_dualsense_input_commit_native_report(instance, serial); +#if SWITCH2_BRIDGE_FULL_INPUT + return probe_native_gamepad_input_commit_native_report(instance, serial); #elif SWITCH2_BRIDGE_WII_INPUT if (!g_native_stream || !serial || serial != g_pending_serial || g_pending_generation != g_wii_generation || !g_wii_active) return false; @@ -543,8 +543,8 @@ extern "C" bool probe_controller_input_play_sample(uint8_t instance, uint8_t sam if (token != nullptr) *token = 0; return false; } -#if SWITCH2_BRIDGE_DUALSENSE_INPUT - return bluepad32_input_backend_dualsense_sample_request(instance, sample_id, token); +#if SWITCH2_BRIDGE_FULL_INPUT + return bluepad32_input_backend_native_sample_request(instance, sample_id, token); #elif SWITCH2_BRIDGE_WII_INPUT return bluepad32_input_backend_wii_sample_request(sample_id, token); #else @@ -555,9 +555,9 @@ extern "C" bool probe_controller_input_play_sample(uint8_t instance, uint8_t sam extern "C" int probe_controller_input_sample_result(uint8_t instance, uint64_t token, uint32_t now_ms) { if (instance >= PROBE_CONTROLLER_COUNT || !g_flash_ready) return -1; -#if SWITCH2_BRIDGE_DUALSENSE_INPUT +#if SWITCH2_BRIDGE_FULL_INPUT (void)now_ms; - return bluepad32_input_backend_dualsense_sample_result(instance, token); + return bluepad32_input_backend_native_sample_result(instance, token); #elif SWITCH2_BRIDGE_WII_INPUT (void)now_ms; return bluepad32_input_backend_wii_sample_result(token); @@ -568,8 +568,8 @@ extern "C" int probe_controller_input_sample_result(uint8_t instance, uint64_t t extern "C" void probe_controller_input_cancel_sample(uint8_t instance) { if (instance >= PROBE_CONTROLLER_COUNT) return; -#if SWITCH2_BRIDGE_DUALSENSE_INPUT - bluepad32_input_backend_dualsense_sample_cancel(instance); +#if SWITCH2_BRIDGE_FULL_INPUT + bluepad32_input_backend_native_sample_cancel(instance); #elif SWITCH2_BRIDGE_WII_INPUT bluepad32_input_backend_wii_sample_cancel(); #else @@ -584,8 +584,8 @@ extern "C" void probe_controller_input_poll(uint8_t instance, uint32_t now_ms, *out = {}; return; } -#if SWITCH2_BRIDGE_DUALSENSE_INPUT - probe_dualsense_input_poll(instance, now_ms, out); +#if SWITCH2_BRIDGE_FULL_INPUT + probe_native_gamepad_input_poll(instance, now_ms, out); return; #elif SWITCH2_BRIDGE_WII_INPUT poll_wii_source(now_ms); @@ -618,7 +618,7 @@ extern "C" void probe_controller_input_poll(uint8_t instance, uint32_t now_ms, g_input.mouse_surface = 0; } #endif -#if !SWITCH2_BRIDGE_DUALSENSE_INPUT +#if !SWITCH2_BRIDGE_FULL_INPUT *out = g_input; #endif } diff --git a/tools/switch2_usb_probe/controller_input.h b/tools/switch2_usb_probe/controller_input.h index cc8cb9b..ee93b7e 100644 --- a/tools/switch2_usb_probe/controller_input.h +++ b/tools/switch2_usb_probe/controller_input.h @@ -47,7 +47,7 @@ void probe_controller_input_set_stick_calibration(const uint8_t calibration[9]); // Native feature changes are output barriers, not Bluetooth/IMU resets. void probe_controller_input_set_native_features(uint8_t features); #endif -#if SWITCH2_BRIDGE_DUALSENSE_INPUT +#if SWITCH2_BRIDGE_FULL_INPUT // Supply each child's advertised, validated nine-byte stick record. Native // output stays unavailable until that child's calibration has been supplied. void probe_controller_input_set_full_stick_calibration(uint8_t instance, const uint8_t calibration[9]); @@ -55,7 +55,7 @@ void probe_controller_input_set_full_stick_calibration(uint8_t instance, const u // Core0 native07/08 output. Disable discards queued/prepared data; repeated // enable preserves it. Joy-Con mode relays its bounded FIFO; right-only Wii // mode synthesizes fresh calibrated sensors and the selected IR pointer. -// DualSense mode splits one full controller into independent R/L output streams; +// Full-controller mode splits one supported gamepad into independent R/L output streams; // controls remain live while motion is unavailable. Only Wii estimates stationary bias. // No pairing changes. void probe_controller_input_set_native_stream(uint8_t instance, bool enabled); diff --git a/tools/switch2_usb_probe/dualsense_input.h b/tools/switch2_usb_probe/dualsense_input.h deleted file mode 100644 index 383d71f..0000000 --- a/tools/switch2_usb_probe/dualsense_input.h +++ /dev/null @@ -1,13 +0,0 @@ -#pragma once - -#include "controller_input.h" - -#if SWITCH2_BRIDGE_DUALSENSE_INPUT -// Core 0 only. One coherent profile/motion evaluation feeds both native children. -void probe_dualsense_input_init(); -void probe_dualsense_input_set_stick_calibration(uint8_t instance, const uint8_t calibration[9]); -void probe_dualsense_input_set_native_stream(uint8_t instance, bool enabled); -void probe_dualsense_input_poll(uint8_t instance, uint32_t now_ms, probe_controller_input* out); -uint32_t probe_dualsense_input_peek_native_report(uint8_t instance, uint32_t now_ms, uint8_t report[63]); -bool probe_dualsense_input_commit_native_report(uint8_t instance, uint32_t token); -#endif diff --git a/tools/switch2_usb_probe/main.c b/tools/switch2_usb_probe/main.c index 0d15879..98261f6 100644 --- a/tools/switch2_usb_probe/main.c +++ b/tools/switch2_usb_probe/main.c @@ -319,7 +319,7 @@ static void reset_controller_protocol(uint8_t instance) { #if SWITCH2_BRIDGE_WII_INPUT probe_controller_input_set_stick_calibration(stick_calibration); probe_controller_input_set_native_features(0); -#elif SWITCH2_BRIDGE_DUALSENSE_INPUT +#elif SWITCH2_BRIDGE_FULL_INPUT probe_controller_input_set_full_stick_calibration(instance, stick_calibration); #endif protocol->read_memory = read_memory; @@ -487,8 +487,8 @@ static void protocol_task(probe_usb_controller* controller, uint32_t now) { #if SWITCH2_BRIDGE_WII_INPUT probe_debug_printf("[PROBE] Wii cue dispatched itf=%u token=%" PRIu64 "\n", instance, reply->deferred_token); -#elif SWITCH2_BRIDGE_DUALSENSE_INPUT - probe_debug_printf("[PROBE] DualSense cue dispatched itf=%u token=%" PRIu64 "\n", +#elif SWITCH2_BRIDGE_FULL_INPUT + probe_debug_printf("[PROBE] Native gamepad cue dispatched itf=%u token=%" PRIu64 "\n", instance, reply->deferred_token); #else probe_debug_printf("[PROBE] Source sample ACK itf=%u token=%" PRIu64 "\n", @@ -528,7 +528,7 @@ static void protocol_task(probe_usb_controller* controller, uint32_t now) { native_serial = probe_controller_input_peek_native_report(instance, now, input); if (!native_serial) return; length = sizeof(input); -#if SWITCH2_BRIDGE_WII_INPUT || SWITCH2_BRIDGE_DUALSENSE_INPUT +#if SWITCH2_BRIDGE_WII_INPUT || SWITCH2_BRIDGE_FULL_INPUT input[8] = (uint8_t)(0x30 | ((protocol->enabled_features & 0x20) ? 8 : 0)); #endif probe_protocol_gate_native_report(protocol, input); @@ -792,6 +792,8 @@ int main(void) { probe_controller_input_init(); #if SWITCH2_BRIDGE_WII_INPUT probe_debug_printf("[PROBE] UART0 GP0=TX, 115200 8N1; selected Wii IR/MotionPlus source enabled\n"); +#elif SWITCH2_BRIDGE_FULL_INPUT + probe_debug_printf("[PROBE] UART0 GP0=TX, 115200 8N1; one supported gamepad feeds the native R/L pair\n"); #else probe_debug_printf("[PROBE] UART0 GP0=TX, 115200 8N1; %u selected Joy-Con Bluetooth source(s)\n", PROBE_CONTROLLER_COUNT); @@ -819,6 +821,10 @@ int main(void) { #if SWITCH2_BRIDGE_WII_INPUT probe_debug_printf("[PROBE] Wii IR drives native mouse movement; buttons retain profile mapping; keep Wii still for MotionPlus calibration\n"); probe_debug_printf("[PROBE] Hold BOOTSEL2s for pairing; Wii cue feedback uses bounded ERM patterns, not HD audio waveforms\n"); +#elif SWITCH2_BRIDGE_FULL_INPUT + probe_debug_printf("[PROBE] Full gamepad controls on R/L; IMU mask=%u; only Wii requires settling\n", + (unsigned)SWITCH2_BRIDGE_IMU_TARGET_MASK); + probe_debug_printf("[PROBE] Hold BOOTSEL 2s for Bluetooth pairing (never clears pairings); cues use source capabilities\n"); #else probe_debug_printf("[PROBE] Live Joy-Con buttons/stick/native mouse; hold BOOTSEL 2s for Bluetooth pairing (never clears pairings)\n"); #endif diff --git a/tools/switch2_usb_probe/dualsense_input.cpp b/tools/switch2_usb_probe/native_gamepad_input.cpp similarity index 76% rename from tools/switch2_usb_probe/dualsense_input.cpp rename to tools/switch2_usb_probe/native_gamepad_input.cpp index 1b0c80e..87b2619 100644 --- a/tools/switch2_usb_probe/dualsense_input.cpp +++ b/tools/switch2_usb_probe/native_gamepad_input.cpp @@ -1,6 +1,6 @@ -#include "dualsense_input.h" +#include "native_gamepad_input.h" -#if SWITCH2_BRIDGE_DUALSENSE_INPUT +#if SWITCH2_BRIDGE_FULL_INPUT #include #include @@ -11,7 +11,7 @@ #include "profile/controller_profile_runtime.h" #if !SWITCH2_PROBE_HUB || SWITCH2_BRIDGE_WII_INPUT -#error "One DualSense requires the native R/L USB hub source mode" +#error "A full gamepad source requires the native R/L USB hub" #endif static_assert(PROBE_CONTROLLER_COUNT == 2); extern "C" int probe_debug_printf(const char* format, ...); @@ -40,15 +40,17 @@ struct Child { uint32_t pending_token = 0; uint32_t pending_us = 0; uint32_t pending_ticks = 0; - uint32_t pending_motion_sequence = 0; + uint32_t pending_accel_sequence = 0; + uint32_t pending_gyro_sequence = 0; bool pending_motion = false; uint8_t pending_report[63]{}; bool have_committed_motion = false; - uint32_t committed_motion_sequence = 0; + uint32_t committed_accel_sequence = 0; + uint32_t committed_gyro_sequence = 0; uint32_t committed_ticks = 0; }; Child g_children[PROBE_CONTROLLER_COUNT]; -Bluepad32DualSenseBridgeSnapshot g_source; +Bluepad32NativeGamepadSnapshot g_source; ControllerProfileTransformResult g_mapped; ProbeNativeMotion g_motion; bool g_active; @@ -78,8 +80,10 @@ void discard_output(Child& child) { child.have_committed_motion = false; } -bool sensors_fresh(uint32_t now_us) { - return g_source.motion_valid && now_us - g_source.motion_received_us < kSensorDeadlineUs; +bool sensors_fresh(const Bluepad32NativeGamepadSnapshot& source, uint32_t now_us) { + return source.accel_valid && source.gyro_valid && + now_us - source.accel_received_us < kSensorDeadlineUs && + now_us - source.gyro_received_us < kSensorDeadlineUs; } void unpack_stick_pair(const uint8_t* bytes, uint16_t pair[2]) { @@ -147,7 +151,7 @@ void lose_source(uint32_t now_ms) { g_motion.reset(); for (uint8_t i = 0; i < PROBE_CONTROLLER_COUNT; ++i) { discard_output(g_children[i]); - bluepad32_input_backend_dualsense_sample_cancel(i); + bluepad32_input_backend_native_sample_cancel(i); } g_sensor_status = -1; } @@ -156,8 +160,8 @@ void lose_source(uint32_t now_ms) { } void refresh(uint32_t now_ms) { - Bluepad32DualSenseBridgeSnapshot source; - bluepad32_input_backend_dualsense_snapshot(&source); + Bluepad32NativeGamepadSnapshot source; + bluepad32_input_backend_native_snapshot(&source); // Snapshot first: source receipt timestamps must not be ahead of this clock. const uint32_t now_us = time_us_32(); advance_clock(now_us); @@ -174,13 +178,15 @@ void refresh(uint32_t now_ms) { // motion evaluation. A real publication in the same millisecond still wins. if (!changed_connection && g_evaluated && g_evaluated_ms == now_ms && source.state_generation == g_source.state_generation && source.received_us == g_source.received_us && - source.motion_sequence == g_source.motion_sequence && - source.motion_received_us == g_source.motion_received_us && source.motion_valid == g_source.motion_valid) return; + source.accel_sequence == g_source.accel_sequence && source.gyro_sequence == g_source.gyro_sequence && + source.accel_received_us == g_source.accel_received_us && source.gyro_received_us == g_source.gyro_received_us && + source.accel_valid == g_source.accel_valid && source.gyro_valid == g_source.gyro_valid && + source.requires_stationary_bias == g_source.requires_stationary_bias) return; if (changed_connection) { lose_source(now_ms); g_motion.reset(); for (Child& child : g_children) discard_output(child); - probe_debug_printf("[PROBE] DualSense source active in slot %u; using validated factory IMU calibration\n", source.slot); + probe_debug_printf("[PROBE] Native gamepad source active in slot %u\n", source.slot); } g_source = source; g_active = true; @@ -194,9 +200,12 @@ void refresh(uint32_t now_ms) { feedback.active_profile_number, feedback.policy); } ProbeNativeMotionSample sample{}; - sample.accel_valid = sample.gyro_valid = source.motion_valid; - sample.accel_sequence = sample.gyro_sequence = source.motion_sequence; - sample.accel_us = sample.gyro_us = source.motion_received_us; + sample.accel_valid = source.accel_valid; + sample.gyro_valid = source.gyro_valid; + sample.accel_sequence = source.accel_sequence; + sample.gyro_sequence = source.gyro_sequence; + sample.accel_us = source.accel_received_us; + sample.gyro_us = source.gyro_received_us; // SDL -> upright native body [X,-Z,Y], the same physical transform used by // the Wii adapter before its mouse-mount rotation. Both halves represent // one rigid, full controller: no solo-Joy-Con or mouse mounting rotation. @@ -206,12 +215,15 @@ void refresh(uint32_t now_ms) { sample.gyro_dps[0] = static_cast(source.gyro_q10[0]) / 1024.0f; sample.gyro_dps[1] = -static_cast(source.gyro_q10[2]) / 1024.0f; sample.gyro_dps[2] = static_cast(source.gyro_q10[1]) / 1024.0f; - g_motion.update(now_us, source.controller.connection_generation, sample); - const int status = !sensors_fresh(now_us) ? 0 : g_motion.ready() ? 2 : 1; + g_motion.update(now_us, source.controller.connection_generation, sample, + source.requires_stationary_bias ? ProbeNativeMotionBias::kEstimateStationary : + ProbeNativeMotionBias::kAlreadyCalibrated); + const int status = !sensors_fresh(g_source, now_us) ? 0 : g_motion.ready() ? 2 : 1; if (status != g_sensor_status) { g_sensor_status = status; - probe_debug_printf("[PROBE] DualSense native IMU %s\n", status == 2 ? "ready" : - status == 1 ? "waiting for a usable acceleration sample" : "waiting for fresh complete sensors"); + probe_debug_printf("[PROBE] Native gamepad IMU %s\n", status == 2 ? "ready" : + status == 1 ? (source.requires_stationary_bias ? "Wii calibrating: keep still" : + "waiting for a usable acceleration sample") : "waiting for supported fresh sensors"); } for (uint8_t i = 0; i < PROBE_CONTROLLER_COUNT; ++i) { // Latest-only: a blocked endpoint never queues obsolete controls/IMU. @@ -221,15 +233,15 @@ void refresh(uint32_t now_ms) { } } // namespace -void probe_dualsense_input_init() { +void probe_native_gamepad_input_init() { #if SWITCH2_BRIDGE_SOURCE_AUTO - bluepad32_input_backend_select_dualsense_source(nullptr); + bluepad32_input_backend_select_native_source(nullptr); #else - bluepad32_input_backend_select_dualsense_source(kSourceAddress); + bluepad32_input_backend_select_native_source(kSourceAddress); #endif } -void probe_dualsense_input_set_stick_calibration(uint8_t instance, const uint8_t calibration[9]) { +void probe_native_gamepad_input_set_stick_calibration(uint8_t instance, const uint8_t calibration[9]) { if (instance >= PROBE_CONTROLLER_COUNT) return; Child& child = g_children[instance]; child.calibrated = false; @@ -248,30 +260,31 @@ void probe_dualsense_input_set_stick_calibration(uint8_t instance, const uint8_t pack_controls(instance); } -void probe_dualsense_input_set_native_stream(uint8_t instance, bool enabled) { +void probe_native_gamepad_input_set_native_stream(uint8_t instance, bool enabled) { if (instance >= PROBE_CONTROLLER_COUNT) return; Child& child = g_children[instance]; if (child.enabled != enabled || !enabled) discard_output(child); child.enabled = enabled; - if (!enabled) bluepad32_input_backend_dualsense_sample_cancel(instance); + if (!enabled) bluepad32_input_backend_native_sample_cancel(instance); } -void probe_dualsense_input_poll(uint8_t instance, uint32_t now_ms, probe_controller_input* out) { +void probe_native_gamepad_input_poll(uint8_t instance, uint32_t now_ms, probe_controller_input* out) { if (!out) return; if (instance >= PROBE_CONTROLLER_COUNT) { *out = {}; return; } refresh(now_ms); *out = g_children[instance].input; } -uint32_t probe_dualsense_input_peek_native_report(uint8_t instance, uint32_t now_ms, uint8_t report[63]) { +uint32_t probe_native_gamepad_input_peek_native_report(uint8_t instance, uint32_t now_ms, uint8_t report[63]) { if (instance >= PROBE_CONTROLLER_COUNT || !report) return 0; refresh(now_ms); Child& child = g_children[instance]; if (!child.enabled || !child.input.active) return 0; const uint32_t now_us = time_us_32(); const bool motion_ready = (SWITCH2_BRIDGE_IMU_TARGET_MASK & (1u << instance)) != 0 && - g_motion.ready() && sensors_fresh(now_us) && - (!child.have_committed_motion || child.committed_motion_sequence != g_source.motion_sequence); + g_motion.ready() && sensors_fresh(g_source, now_us) && + (!child.have_committed_motion || child.committed_accel_sequence != g_source.accel_sequence || + child.committed_gyro_sequence != g_source.gyro_sequence); if (child.pending_token && (now_us - child.pending_us >= kOutputDeadlineUs || child.pending_motion != motion_ready)) child.pending_token = 0; if (!child.pending_token) { @@ -293,7 +306,8 @@ uint32_t probe_dualsense_input_peek_native_report(uint8_t instance, uint32_t now g_motion.quaternion(), g_motion.acceleration(), static_cast(child.pending_ticks & 0xfff), wire_elapsed, 0, child.pending_report + probe_model_imu_data_offset(instance)); if (child.pending_motion) child.pending_report[probe_model_imu_length_offset(instance)] = 30; - child.pending_motion_sequence = g_source.motion_sequence; + child.pending_accel_sequence = g_source.accel_sequence; + child.pending_gyro_sequence = g_source.gyro_sequence; child.pending_us = now_us; child.pending_token = ++g_report_token; } @@ -301,27 +315,28 @@ uint32_t probe_dualsense_input_peek_native_report(uint8_t instance, uint32_t now return child.pending_token; } -bool probe_dualsense_input_commit_native_report(uint8_t instance, uint32_t token) { +bool probe_native_gamepad_input_commit_native_report(uint8_t instance, uint32_t token) { if (instance >= PROBE_CONTROLLER_COUNT || !token) return false; Child& child = g_children[instance]; // Check the live source even when the caller did not poll after a disconnect. - Bluepad32DualSenseBridgeSnapshot source; - bluepad32_input_backend_dualsense_snapshot(&source); + Bluepad32NativeGamepadSnapshot source; + bluepad32_input_backend_native_snapshot(&source); const uint32_t now_us = time_us_32(); if (!child.enabled || !g_active || child.pending_token != token || !source.controller.active || source.slot != g_source.slot || source.controller.connection_generation != g_source.controller.connection_generation || source.state_generation != g_source.state_generation || - source.motion_sequence != g_source.motion_sequence || - source.motion_received_us != g_source.motion_received_us || - source.motion_valid != g_source.motion_valid || + source.accel_sequence != g_source.accel_sequence || source.gyro_sequence != g_source.gyro_sequence || + source.accel_received_us != g_source.accel_received_us || source.gyro_received_us != g_source.gyro_received_us || + source.accel_valid != g_source.accel_valid || source.gyro_valid != g_source.gyro_valid || + source.requires_stationary_bias != g_source.requires_stationary_bias || now_us - source.received_us >= kInputDeadlineUs || now_us - child.pending_us >= kOutputDeadlineUs || - (child.pending_motion && (!source.motion_valid || - now_us - g_source.motion_received_us >= kSensorDeadlineUs))) return false; + (child.pending_motion && !sensors_fresh(source, now_us))) return false; child.pending_token = 0; if (child.pending_motion) { child.have_committed_motion = true; - child.committed_motion_sequence = child.pending_motion_sequence; + child.committed_accel_sequence = child.pending_accel_sequence; + child.committed_gyro_sequence = child.pending_gyro_sequence; child.committed_ticks = child.pending_ticks; } ++child.counter; diff --git a/tools/switch2_usb_probe/native_gamepad_input.h b/tools/switch2_usb_probe/native_gamepad_input.h new file mode 100644 index 0000000..3e8cd3d --- /dev/null +++ b/tools/switch2_usb_probe/native_gamepad_input.h @@ -0,0 +1,13 @@ +#pragma once + +#include "controller_input.h" + +#if SWITCH2_BRIDGE_FULL_INPUT +// Core 0 only. One coherent profile/motion evaluation feeds both native children. +void probe_native_gamepad_input_init(); +void probe_native_gamepad_input_set_stick_calibration(uint8_t instance, const uint8_t calibration[9]); +void probe_native_gamepad_input_set_native_stream(uint8_t instance, bool enabled); +void probe_native_gamepad_input_poll(uint8_t instance, uint32_t now_ms, probe_controller_input* out); +uint32_t probe_native_gamepad_input_peek_native_report(uint8_t instance, uint32_t now_ms, uint8_t report[63]); +bool probe_native_gamepad_input_commit_native_report(uint8_t instance, uint32_t token); +#endif diff --git a/tools/switch2_usb_probe/probe_build.cmake b/tools/switch2_usb_probe/probe_build.cmake index 08ab918..66aae78 100644 --- a/tools/switch2_usb_probe/probe_build.cmake +++ b/tools/switch2_usb_probe/probe_build.cmake @@ -25,8 +25,8 @@ else() endif() if(SWITCH2_PROBE_COMPOSITE OR SWITCH2_PROBE_HUB) if(NOT SWITCH_PICO_SWITCH2_USB_BRIDGE OR SWITCH2_BRIDGE_WII_INPUT - OR (NOT SWITCH2_BRIDGE_INPUT STREQUAL "JOYCON2" AND NOT SWITCH2_BRIDGE_DUALSENSE_INPUT)) - message(FATAL_ERROR "Native R/L output requires JOYCON2 input, or DUALSENSE with HUB") + OR (NOT SWITCH2_BRIDGE_INPUT STREQUAL "JOYCON2" AND NOT SWITCH2_BRIDGE_FULL_INPUT)) + message(FATAL_ERROR "Native R/L output requires JOYCON2 input or a full-controller HUB source") endif() set(probe_composite 0) if(SWITCH2_PROBE_COMPOSITE) @@ -54,10 +54,10 @@ set(SWITCH2_BRIDGE_SECOND_SOURCE_ADDRESS "" CACHE STRING set(SWITCH2_BRIDGE_SOURCE_AUTO OFF) if(SWITCH_PICO_SWITCH2_USB_BRIDGE OR SWITCH2_PROBE_COMPOSITE) set(probe_source_fields SWITCH2_BRIDGE_SOURCE_ADDRESS) - if(SWITCH2_BRIDGE_DUALSENSE_INPUT AND SWITCH2_BRIDGE_SOURCE_ADDRESS STREQUAL "") + if(SWITCH2_BRIDGE_FULL_INPUT AND SWITCH2_BRIDGE_SOURCE_ADDRESS STREQUAL "") set(SWITCH2_BRIDGE_SOURCE_AUTO ON) set(probe_source_fields "") - elseif((SWITCH2_PROBE_COMPOSITE OR SWITCH2_PROBE_HUB) AND NOT SWITCH2_BRIDGE_DUALSENSE_INPUT) + elseif((SWITCH2_PROBE_COMPOSITE OR SWITCH2_PROBE_HUB) AND NOT SWITCH2_BRIDGE_FULL_INPUT) list(APPEND probe_source_fields SWITCH2_BRIDGE_SECOND_SOURCE_ADDRESS) endif() set(probe_source_addresses "") @@ -367,11 +367,11 @@ function(switch2_usb_probe_configure target) else() pico_set_program_name(${target} "Switch 2 USB initialization capture") endif() - if(SWITCH2_PROBE_HUB AND SWITCH2_BRIDGE_DUALSENSE_INPUT) + if(SWITCH2_PROBE_HUB AND SWITCH2_BRIDGE_FULL_INPUT) if(SWITCH2_PROBE_TRACE_NATIVE_INPUT) - pico_set_program_version(${target} "0.69-native-hub-imu-trace") + pico_set_program_version(${target} "0.70-native-gamepad-trace") else() - pico_set_program_version(${target} "0.69-native-hub-imu") + pico_set_program_version(${target} "0.70-native-gamepad") endif() elseif(SWITCH2_PROBE_HUB) pico_set_program_version(${target} "0.67-native-hub-latency")