Add DualSense native pair and Wii-only IMU settling
This commit is contained in:
parent
1748910316
commit
ab70536fdf
31 changed files with 2617 additions and 77 deletions
|
|
@ -176,27 +176,49 @@ if(SWITCH_PICO_SWITCH2_MEMORY_CAPTURE)
|
|||
add_compile_definitions(SWITCH_PICO_SWITCH2_MEMORY_CAPTURE=1)
|
||||
endif()
|
||||
set(SWITCH2_BRIDGE_WII_INPUT OFF)
|
||||
set(SWITCH2_BRIDGE_DUALSENSE_INPUT OFF)
|
||||
if(SWITCH_PICO_SWITCH2_USB_BRIDGE)
|
||||
if(NOT SWITCH_PICO_INPUT_BACKEND STREQUAL "BLUEPAD32"
|
||||
OR NOT SWITCH_PICO_ENABLE_BLE
|
||||
OR NOT SWITCH_PICO_SWITCH2_MOUSE_CAPTURE
|
||||
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_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")
|
||||
set(SWITCH2_BRIDGE_IMU_TARGET_MASK 1)
|
||||
elseif(SWITCH2_BRIDGE_IMU_TARGET STREQUAL "LEFT")
|
||||
set(SWITCH2_BRIDGE_IMU_TARGET_MASK 2)
|
||||
elseif(SWITCH2_BRIDGE_IMU_TARGET STREQUAL "BOTH")
|
||||
set(SWITCH2_BRIDGE_IMU_TARGET_MASK 3)
|
||||
else()
|
||||
message(FATAL_ERROR "SWITCH2_BRIDGE_IMU_TARGET must be LEFT, RIGHT or BOTH")
|
||||
endif()
|
||||
add_compile_definitions(SWITCH2_BRIDGE_IMU_TARGET_MASK=${SWITCH2_BRIDGE_IMU_TARGET_MASK})
|
||||
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")
|
||||
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
|
||||
"Switch 2 USB bridge requires BLUEPAD32 with BLE and native Joy-Con mouse capture")
|
||||
message(FATAL_ERROR "Joy-Con/Wii USB bridge requires BLE and native Joy-Con capture")
|
||||
endif()
|
||||
if(SWITCH_PICO_WII_IR_MOUSE OR SWITCH_PICO_WII_IR_GYRO)
|
||||
message(FATAL_ERROR "Switch 2 USB bridge owns USB; Wii IR USB experiments cannot be combined")
|
||||
endif()
|
||||
set(SWITCH2_BRIDGE_INPUT "JOYCON2" CACHE STRING "Native bridge source: JOYCON2 or WII")
|
||||
set_property(CACHE SWITCH2_BRIDGE_INPUT PROPERTY STRINGS JOYCON2 WII)
|
||||
if(SWITCH2_BRIDGE_INPUT STREQUAL "WII")
|
||||
if(NOT SWITCH_PICO_ENABLE_CLASSIC)
|
||||
message(FATAL_ERROR "Wii native bridge requires Bluetooth MIXED mode")
|
||||
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")
|
||||
message(FATAL_ERROR "SWITCH2_BRIDGE_INPUT must be JOYCON2 or WII")
|
||||
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")
|
||||
endif()
|
||||
if(NOT SWITCH2_BRIDGE_DUALSENSE_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)
|
||||
endif()
|
||||
|
|
@ -401,10 +423,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)
|
||||
target_sources(switch-pico PRIVATE
|
||||
${SWITCH2_USB_PROBE_DIR}/dualsense_input.cpp
|
||||
${SWITCH2_USB_PROBE_DIR}/native_imu.cpp)
|
||||
endif()
|
||||
target_compile_definitions(switch-pico PRIVATE
|
||||
SWITCH2_BRIDGE_SOURCE_ADDRESS_BYTES=${SWITCH2_BRIDGE_SOURCE_ADDRESS_BYTES})
|
||||
if(SWITCH2_PROBE_COMPOSITE OR SWITCH2_PROBE_HUB)
|
||||
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)
|
||||
target_compile_definitions(switch-pico PRIVATE
|
||||
SWITCH2_BRIDGE_SECOND_SOURCE_ADDRESS_BYTES=${SWITCH2_BRIDGE_SECOND_SOURCE_ADDRESS_BYTES})
|
||||
endif()
|
||||
|
|
|
|||
95
README.md
95
README.md
|
|
@ -759,11 +759,13 @@ wiring on GPIO pins. Each child retains its own native HID/vendor interfaces,
|
|||
EP1/EP2 state, identity, protocol state and pairing bank.
|
||||
|
||||
This mode requires `SWITCH_PICO_SWITCH2_USB_BRIDGE=ON`,
|
||||
`SWITCH2_BRIDGE_INPUT=JOYCON2`, `SWITCH2_PROBE_SIDE=RIGHT`,
|
||||
`SWITCH2_PROBE_COMPOSITE=OFF`, and `SWITCH_PICO_SYS_CLOCK_MHZ=240`.
|
||||
Configure both private donor captures and source addresses as for the paired
|
||||
native probe. Bluetooth runs cooperatively on Core 0; Core 1 is reserved for
|
||||
USB observation. Receive PID state is selected before accepting OUT traffic.
|
||||
`SWITCH2_PROBE_SIDE=RIGHT`, `SWITCH2_PROBE_COMPOSITE=OFF`, and
|
||||
`SWITCH_PICO_SYS_CLOCK_MHZ=240`. `SWITCH2_BRIDGE_INPUT=JOYCON2` forwards the
|
||||
two selected physical Joy-Cons; `DUALSENSE` translates one full DualSense into
|
||||
the same virtual pair. Both require the private R/L identity/factory captures.
|
||||
Joy-Con input additionally requires BLE/native capture and both source addresses.
|
||||
Bluetooth runs cooperatively on Core 0; Core 1 is reserved for USB observation.
|
||||
Receive PID state is selected before accepting OUT traffic.
|
||||
Transmit payloads are prepared outside the bank lock and published by Core 0;
|
||||
unavailable IN buffers NAK rather than expose another device's packet.
|
||||
|
||||
|
|
@ -794,6 +796,89 @@ with some noticeable input lag. This is console smoke-test evidence, not a
|
|||
latency measurement or exhaustive compatibility test. This mode does not add
|
||||
arbitrary full-controller splitting or continuous USB HD-rumble forwarding.
|
||||
|
||||
**Queue latency follow-up:** `0.67-native-hub-latency` coalesces adjacent analog/
|
||||
IMU-only Joy-Con updates when buttons, status, opaque fields and IMU format are
|
||||
unchanged and neither packet contains relative mouse motion. Discrete transitions
|
||||
and mouse packets keep their order, and a peeked packet is pinned until commit.
|
||||
A reproducible 125Hz producer/62.5Hz consumer simulation of the actual capture
|
||||
code reduced maximum queue age from 252ms to 4ms (mean 129.968ms to 4ms).
|
||||
This is a same-format continuous-state workload, not measured Bluetooth-to-Switch
|
||||
latency; different formats, discrete events and sustained mouse traffic still
|
||||
use the bounded FIFO. The console lag improvement remains to be compared.
|
||||
|
||||
**One DualSense, two native halves:** use a separate private build configured
|
||||
with `SWITCH2_BRIDGE_INPUT=DUALSENSE` and Classic Bluetooth enabled. The trial
|
||||
uses `SWITCH_PICO_BLUETOOTH_MODE=CLASSIC` with
|
||||
`SWITCH_PICO_SWITCH2_MOUSE_CAPTURE=OFF` and
|
||||
`SWITCH_PICO_SWITCH2_MOUSE_CAPTURE_NATIVE=OFF`. It does not require Joy-Con
|
||||
donors. Empty `SWITCH2_BRIDGE_SOURCE_ADDRESS` selects the uniquely eligible
|
||||
ready DualSense/Edge; an explicit address filters that source. Multiple eligible
|
||||
pads fail closed instead of mixing players. The secondary source address is unused.
|
||||
|
||||
In the dedicated DualSense mode, transport connections awaiting classification
|
||||
count against Bluetooth capacity but do not reserve logical player/colour slots.
|
||||
Only a supported PS5-parser source can enter those slots. Logical allocation
|
||||
uses the first free slot rather than the Bluetooth device index, so an earlier
|
||||
Pro Controller reconnect cannot move the first DualSense to the second colour.
|
||||
Unsupported ready devices are disconnected without deleting their bonds;
|
||||
normal AIO admission and slot assignment are unchanged.
|
||||
|
||||
The existing profile transform runs once for the full pad. R gets face buttons,
|
||||
right stick/shoulder/trigger, plus and home; L gets the D-pad, left stick/shoulder/
|
||||
trigger, minus and capture. Each uses its own advertised stick calibration.
|
||||
One shared motion integrator consumes only fresh, complete, CRC-checked and
|
||||
factory-calibrated DS5 sensor data. From 0.69, already-calibrated native sources
|
||||
initialize from their first usable fresh sensor pair; the extra stationary
|
||||
bias-estimation period is explicitly Wii-only. Invalid/stale sensors still
|
||||
withhold IMU while controls remain available. Each USB half has independent
|
||||
peek/commit, reset and backpressure state. No mouse movement or rail presses
|
||||
are invented.
|
||||
|
||||
Built-in cue requests become bounded compatibility vibration on the corresponding
|
||||
DualSense actuator, not Joy-Con HD waveforms or adaptive-trigger effects.
|
||||
Completion means accepted L2CAP submission to the source driver, **not** a
|
||||
DualSense application ACK or measured motor onset. Stop attempts are bounded; a persistently
|
||||
blocked OFF path disconnects the stuck link without deleting its bond.
|
||||
|
||||
`0.67-native-hub-dualsense` built and was flashed with current persistent storage
|
||||
verified unchanged. Its first PC run passed hub/child enumeration, native control
|
||||
reads, initialization and bulk isolation, but had no real DualSense input and
|
||||
therefore failed live-IMU qualification. Physical controls, native motion axes,
|
||||
motor feel and Switch acceptance for this source remain pending. For a new bond,
|
||||
open the Pico's two-second BOOTSEL pairing window, release it, then hold
|
||||
DualSense Create + PS. Previously bonded pads normally reconnect with PS.
|
||||
|
||||
`0.68-native-hub-slot` fixes the observed red/second-slot case: the trace showed
|
||||
a Pro Controller connecting first and the DualSense using Bluetooth index 1.
|
||||
The regression reproduces that ordering and verifies logical slot 0 and its
|
||||
lightbar colour, stable identity across a new Bluetooth index, unrelated
|
||||
connection churn, pending-capacity accounting and rejection of late ready
|
||||
callbacks. The update was flashed after a fresh full backup, with persistent
|
||||
storage verified unchanged. USB transport checks pass; the post-update physical
|
||||
DualSense reconnect and steady lightbar colour still need observation.
|
||||
|
||||
The subsequent Tears of the Kingdom wire trace showed IMU on both completed
|
||||
USB endpoints after the delayed startup: 38 sampled R blocks and 37 L blocks
|
||||
decoded with changing counters and quaternions. The Wii-style stationary gate
|
||||
had delayed readiness until about 30 seconds after boot in that run. Firmware
|
||||
0.69 removes that extra gate for factory-calibrated sources; the regression
|
||||
checks first-sample output even while rotating, fresh-data recovery, and the
|
||||
unchanged Wii settling behavior. Wii factory calibration is already used, but
|
||||
its residual gyro bias still needs estimation; cached per-device bias without
|
||||
revalidation can drift and is not implemented here.
|
||||
|
||||
Translated full-controller builds expose `SWITCH2_BRIDGE_IMU_TARGET`:
|
||||
`LEFT`, `RIGHT`, or `BOTH` (default). For example, configure the existing private
|
||||
DualSense build with `-DSWITCH2_BRIDGE_IMU_TARGET=RIGHT` and rebuild/reflash.
|
||||
This routes only IMU; both halves retain their controls. It consumes no controller
|
||||
chord and changes no saved profile or pairing. The full dual-IMU PC checker
|
||||
requires `BOTH`; use the USB-completion UART trace for single-target comparisons.
|
||||
|
||||
For the DualSense trial, pass `--build-dir build-switch2-native-dualsense` to
|
||||
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.
|
||||
|
||||
With the existing private build configured, qualify on a PC using:
|
||||
|
||||
```sh
|
||||
|
|
|
|||
|
|
@ -116,7 +116,7 @@ typedef struct {
|
|||
uint8_t command_data[32], rumble_data[33], cccd_data[2];
|
||||
uint8_t command_length;
|
||||
bool command_pending, command_sent, command_acked;
|
||||
#if SWITCH_PICO_SWITCH2_USB_BRIDGE
|
||||
#if SWITCH_PICO_SWITCH2_USB_BRIDGE && SWITCH_PICO_SWITCH2_MOUSE_CAPTURE
|
||||
uint64_t sample_token;
|
||||
#endif
|
||||
uint8_t step, calibration_slot;
|
||||
|
|
@ -219,7 +219,7 @@ void uni_hid_parser_switch2_teardown(uni_hid_device_t* d) {
|
|||
sw2_instance_t* ins = sw2_instance(d);
|
||||
if (!ins)
|
||||
return;
|
||||
#if SWITCH_PICO_SWITCH2_USB_BRIDGE
|
||||
#if SWITCH_PICO_SWITCH2_USB_BRIDGE && SWITCH_PICO_SWITCH2_MOUSE_CAPTURE
|
||||
if (ins->sample_token) {
|
||||
switch_pico_switch2_sample_result(d->product_id, ins->address, ins->sample_token,
|
||||
-1, btstack_run_loop_get_time_ms());
|
||||
|
|
@ -349,7 +349,7 @@ static bool sw2_transient_write_error(uint8_t status) {
|
|||
static void sw2_try_command(sw2_instance_t* ins) {
|
||||
if (!ins->command_pending || ins->command_sent || ins->query != SW2_QUERY_NONE)
|
||||
return;
|
||||
#if SWITCH_PICO_SWITCH2_USB_BRIDGE
|
||||
#if SWITCH_PICO_SWITCH2_USB_BRIDGE && SWITCH_PICO_SWITCH2_MOUSE_CAPTURE
|
||||
if (ins->sample_token &&
|
||||
!switch_pico_switch2_sample_result(ins->device->product_id, ins->address,
|
||||
ins->sample_token, 0, btstack_run_loop_get_time_ms())) {
|
||||
|
|
@ -482,7 +482,7 @@ static void sw2_continue(sw2_instance_t* ins) {
|
|||
break;
|
||||
#endif
|
||||
case SW2_FEATURES: {
|
||||
#if SWITCH_PICO_SWITCH2_USB_BRIDGE
|
||||
#if SWITCH_PICO_SWITCH2_USB_BRIDGE && SWITCH_PICO_SWITCH2_MOUSE_CAPTURE
|
||||
// Match the console's complete native feature set, including the
|
||||
// status associated with bit 5, rather than a mouse-only capture.
|
||||
const uint8_t features[4] = {sw2_mouse_capture_enabled(ins) ? 0x37 : 0x04, 0, 0, 0};
|
||||
|
|
@ -511,7 +511,7 @@ static void sw2_complete_command(sw2_instance_t* ins) {
|
|||
return;
|
||||
ins->command_pending = false;
|
||||
sw2_disarm_timeout(ins);
|
||||
#if SWITCH_PICO_SWITCH2_USB_BRIDGE
|
||||
#if SWITCH_PICO_SWITCH2_USB_BRIDGE && SWITCH_PICO_SWITCH2_MOUSE_CAPTURE
|
||||
if (ins->sample_token) {
|
||||
switch_pico_switch2_sample_result(ins->device->product_id, ins->address,
|
||||
ins->sample_token, 1, btstack_run_loop_get_time_ms());
|
||||
|
|
@ -590,7 +590,7 @@ static void sw2_response(sw2_instance_t* ins, const uint8_t* data, uint16_t leng
|
|||
sw2_fail(ins, "negative application ACK", data[5]);
|
||||
return;
|
||||
}
|
||||
#if SWITCH_PICO_SWITCH2_USB_BRIDGE
|
||||
#if SWITCH_PICO_SWITCH2_USB_BRIDGE && SWITCH_PICO_SWITCH2_MOUSE_CAPTURE
|
||||
// 0A/02 has an empty application ACK, not a returned sample ID or status
|
||||
// payload. Never interpret a truncated/extended response as its success.
|
||||
if (ins->sample_token && (length != 8 || data[4] != 0x10 || data[6] || data[7]))
|
||||
|
|
@ -1511,7 +1511,7 @@ static void sw2_output_tick(btstack_timer_source_t* timer) {
|
|||
if (ins->state != SW2_READY)
|
||||
return; // A blocked setup command rescheduled itself, or awaits its ACK.
|
||||
uint32_t now = btstack_run_loop_get_time_ms();
|
||||
#if SWITCH_PICO_SWITCH2_USB_BRIDGE
|
||||
#if SWITCH_PICO_SWITCH2_USB_BRIDGE && SWITCH_PICO_SWITCH2_MOUSE_CAPTURE
|
||||
if (!ins->command_pending && ins->query == SW2_QUERY_NONE) {
|
||||
uint8_t sample_id;
|
||||
if (switch_pico_switch2_sample_take(ins->device->product_id, ins->address, now,
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ void switch_pico_switch2_mouse_report(uint16_t product_id, const uint8_t address
|
|||
const uint8_t* report, uint16_t length, uint32_t received_ms);
|
||||
#endif
|
||||
|
||||
#if SWITCH_PICO_SWITCH2_USB_BRIDGE
|
||||
#if SWITCH_PICO_SWITCH2_USB_BRIDGE && SWITCH_PICO_SWITCH2_MOUSE_CAPTURE
|
||||
// BTstack-core mailbox hooks. Take only when READY with no command/query in
|
||||
// flight. Result 0 checks ownership before a deferred write, 1 records verified
|
||||
// command completion, and -1 fails it. False means canceled/stale/wrong source.
|
||||
|
|
|
|||
|
|
@ -518,14 +518,154 @@ 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;
|
||||
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
|
||||
@@ -7,6 +7,7 @@
|
||||
#define UNI_HID_PARSER_DS5_H
|
||||
|
||||
#include <stdint.h>
|
||||
+#include <stdbool.h>
|
||||
|
||||
#include "parser/uni_hid_parser.h"
|
||||
|
||||
@@ -33,6 +34,25 @@
|
||||
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);
|
||||
+// 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.
|
||||
+bool uni_hid_parser_ds5_bridge_rumble(struct uni_hid_device_s* d, uint16_t duration_ms,
|
||||
+ uint8_t right, uint8_t left);
|
||||
+// Disconnect/delete retire delayed and duration timers before parser memory reuse.
|
||||
+void uni_hid_parser_ds5_bridge_teardown(struct uni_hid_device_s* d);
|
||||
+#endif
|
||||
|
||||
// Unique to DualSense. Not part of the "hid_parser" interface
|
||||
// Warning: Adaptive trigger API is experimental. It might change in the future without further notice.
|
||||
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
|
||||
@@ -487,17 +487,17 @@
|
||||
@@ -106,6 +106,14 @@
|
||||
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;
|
||||
+ 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 @@
|
||||
_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
|
||||
+// 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);
|
||||
+ crc = ~uni_crc32_le(crc, report, len - 4);
|
||||
+ const uint8_t* expected = &report[len - 4];
|
||||
+ 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 @@
|
||||
|
||||
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;
|
||||
+#endif
|
||||
memset(ctl, 0, sizeof(*ctl));
|
||||
|
||||
ctl->klass = UNI_CONTROLLER_CLASS_GAMEPAD;
|
||||
@@ -423,10 +456,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 (report == NULL || len == 0)
|
||||
+ return;
|
||||
+#endif
|
||||
uint8_t report_id = report[0];
|
||||
|
||||
switch (report_id) {
|
||||
case DS5_FEATURE_REPORT_PAIRING_INFO:
|
||||
+#if SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
+ if (len != DS5_FEATURE_REPORT_PAIRING_INFO_SIZE ||
|
||||
+ !ds5_bridge_crc_valid(report, len, 0xa3))
|
||||
+ break;
|
||||
+#endif
|
||||
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 @@
|
||||
break;
|
||||
|
||||
case DS5_FEATURE_REPORT_FIRMWARE_VERSION: {
|
||||
+#if SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
+ if (len != DS5_FEATURE_REPORT_FIRMWARE_VERSION_SIZE ||
|
||||
+ !ds5_bridge_crc_valid(report, len, 0xa3))
|
||||
+ break;
|
||||
+#endif
|
||||
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 @@
|
||||
int speed_2x;
|
||||
int range_2g;
|
||||
|
||||
+#if SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
+ if (len != DS5_FEATURE_REPORT_CALIBRATION_SIZE ||
|
||||
+ !ds5_bridge_crc_valid(report, len, 0xa3))
|
||||
+ break;
|
||||
+#endif
|
||||
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 @@
|
||||
// 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
|
||||
+ ins->calibration_valid = speed_2x > 0;
|
||||
+ ins->bridge.motion_valid = false;
|
||||
+#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;
|
||||
ins->gyro_calib_data[0].sens_denom =
|
||||
|
|
@ -542,7 +682,47 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_ds5.c b/src/componen
|
|||
ins->gyro_calib_data[2].sens_numer = speed_2x * DS5_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);
|
||||
@@ -617,12 +617,12 @@
|
||||
|
||||
+#if SWITCH2_BRIDGE_DUALSENSE_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);
|
||||
+#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_DUALSENSE_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 @@
|
||||
// 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 (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 @@
|
||||
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 (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 @@
|
||||
ctl->gamepad.buttons |= BUTTON_THUMB_R; // Thumb R
|
||||
if (r->buttons[2] & 0x01)
|
||||
ctl->gamepad.misc_buttons |= MISC_BUTTON_SYSTEM; // PS
|
||||
|
|
@ -558,7 +738,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,7 +630,7 @@
|
||||
@@ -630,12 +704,29 @@
|
||||
|
||||
// Accel
|
||||
for (size_t i = 0; i < ARRAY_SIZE(r->accel); i++) {
|
||||
|
|
@ -567,6 +747,119 @@ diff --git a/src/components/bluepad32/parser/uni_hid_parser_ds5.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;
|
||||
}
|
||||
|
||||
+#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;
|
||||
+ }
|
||||
+ }
|
||||
+#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 @@
|
||||
}
|
||||
}
|
||||
|
||||
+#if SWITCH2_BRIDGE_DUALSENSE_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);
|
||||
+ if (uni_hid_parser_ds5_bridge_rumble(d, 0, 0, 0))
|
||||
+ return;
|
||||
+ if ((int32_t)(btstack_run_loop_get_time_ms() - ins->bridge_stop_deadline_ms) >= 0) {
|
||||
+ // A stuck output path must not leave a live vibrating connection.
|
||||
+ // Disconnect preserves its bond; lifecycle teardown retires this timer.
|
||||
+ uni_hid_device_disconnect(d);
|
||||
+ return;
|
||||
+ }
|
||||
+ btstack_run_loop_set_timer(timer, 5);
|
||||
+ btstack_run_loop_add_timer(timer);
|
||||
+}
|
||||
+
|
||||
+bool uni_hid_parser_ds5_bridge_rumble(uni_hid_device_t* d, uint16_t duration_ms,
|
||||
+ uint8_t right, uint8_t left) {
|
||||
+ if (d == NULL || d->report_parser.setup != uni_hid_parser_ds5_setup || duration_ms > 1000)
|
||||
+ return false;
|
||||
+ ds5_instance_t* ins = get_ds5_instance(d);
|
||||
+ if (ins->state != DS5_STATE_READY || d->conn.interrupt_cid == 0 ||
|
||||
+ !l2cap_can_send_packet_now(d->conn.interrupt_cid))
|
||||
+ return false;
|
||||
+ ds5_output_report_t out = {
|
||||
+ .valid_flag0 = DS5_FLAG0_HAPTICS_SELECT,
|
||||
+ .motor_right = duration_ms == 0 ? 0 : right,
|
||||
+ .motor_left = duration_ms == 0 ? 0 : left,
|
||||
+ };
|
||||
+ if (ins->use_vibration2)
|
||||
+ out.valid_flag2 |= DS5_FLAG2_COMPATIBLE_VIBRATION2;
|
||||
+ else
|
||||
+ out.valid_flag0 |= DS5_FLAG0_COMPATIBLE_VIBRATION;
|
||||
+ ds5_prepare_output_report(d, &out);
|
||||
+ // Never put a timed ON packet in the generic FIFO. A later retry evaluates
|
||||
+ // the current cue phase; it cannot drain a backlog of expired vibration.
|
||||
+ if (l2cap_send(d->conn.interrupt_cid, (uint8_t*)&out, sizeof(out)) != ERROR_CODE_SUCCESS)
|
||||
+ return false;
|
||||
+ btstack_run_loop_remove_timer(&ins->rumble_timer_delayed_start);
|
||||
+ btstack_run_loop_remove_timer(&ins->rumble_timer_duration);
|
||||
+ ins->rumble_state = duration_ms == 0 ? DS5_STATE_RUMBLE_DISABLED : DS5_STATE_RUMBLE_IN_PROGRESS;
|
||||
+ if (duration_ms != 0) {
|
||||
+ ins->bridge_stop_deadline_ms = btstack_run_loop_get_time_ms() + duration_ms + 2000;
|
||||
+ ins->rumble_timer_duration.context = d;
|
||||
+ ins->rumble_timer_duration.process = on_ds5_bridge_rumble_off;
|
||||
+ btstack_run_loop_set_timer(&ins->rumble_timer_duration, duration_ms);
|
||||
+ btstack_run_loop_add_timer(&ins->rumble_timer_duration);
|
||||
+ }
|
||||
+ return true; // Source transport submission, never an application ACK.
|
||||
+}
|
||||
+
|
||||
+void uni_hid_parser_ds5_bridge_teardown(uni_hid_device_t* d) {
|
||||
+ if (d == NULL || d->report_parser.setup != uni_hid_parser_ds5_setup)
|
||||
+ return;
|
||||
+ ds5_instance_t* ins = get_ds5_instance(d);
|
||||
+ 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;
|
||||
+}
|
||||
+#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 @@
|
||||
return (ds5_instance_t*)&d->parser_data[0];
|
||||
}
|
||||
|
||||
-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) {
|
||||
ds5_instance_t* ins = get_ds5_instance(d);
|
||||
|
||||
out->transaction_type = (HID_MESSAGE_TYPE_DATA << 4) | HID_REPORT_TYPE_OUTPUT;
|
||||
@@ -748,7 +902,10 @@
|
||||
ins->output_seq = 0;
|
||||
|
||||
out->crc32 = ~uni_crc32_le(0xffffffff, (uint8_t*)out, sizeof(*out) - 4);
|
||||
-
|
||||
+}
|
||||
+
|
||||
+static void ds5_send_output_report(uni_hid_device_t* d, ds5_output_report_t* out) {
|
||||
+ ds5_prepare_output_report(d, out);
|
||||
uni_hid_device_send_intr_report(d, (uint8_t*)out, sizeof(*out));
|
||||
}
|
||||
|
||||
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
|
||||
|
|
@ -2939,7 +3232,7 @@ diff --git a/src/components/bluepad32/uni_hid_device.c b/src/components/bluepad3
|
|||
#include "parser/uni_hid_parser_wii.h"
|
||||
#include "parser/uni_hid_parser_xboxone.h"
|
||||
#include "platform/uni_platform.h"
|
||||
@@ -447,6 +448,13 @@
|
||||
@@ -447,6 +448,17 @@
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -2949,11 +3242,15 @@ 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 (d->report_parser.setup == uni_hid_parser_ds5_setup)
|
||||
+ uni_hid_parser_ds5_bridge_teardown(d);
|
||||
+#endif
|
||||
+
|
||||
// Disconnect child first
|
||||
if (d->child)
|
||||
uni_hid_device_disconnect(d->child);
|
||||
@@ -465,9 +473,9 @@
|
||||
@@ -465,9 +477,9 @@
|
||||
// Cleanup
|
||||
if (!uni_hid_device_is_virtual_device(d)) {
|
||||
type = gap_get_connection_type(d->conn.handle);
|
||||
|
|
@ -2965,7 +3262,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 +498,13 @@
|
||||
@@ -490,6 +502,17 @@
|
||||
loge("uni_hid_device_delete: invalid hid device: NULL\n");
|
||||
return;
|
||||
}
|
||||
|
|
@ -2975,11 +3272,15 @@ 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 (d->report_parser.setup == uni_hid_parser_ds5_setup)
|
||||
+ uni_hid_parser_ds5_bridge_teardown(d);
|
||||
+#endif
|
||||
+
|
||||
|
||||
// Delete child first
|
||||
if (d->child)
|
||||
@@ -655,6 +670,7 @@
|
||||
@@ -655,6 +678,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;
|
||||
|
|
@ -2987,7 +3288,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 +734,15 @@
|
||||
@@ -718,6 +742,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;
|
||||
|
|
@ -3003,7 +3304,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 +834,11 @@
|
||||
@@ -809,7 +842,11 @@
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -36,6 +36,9 @@
|
|||
#include <uni.h>
|
||||
extern "C" {
|
||||
#include "parser/uni_hid_parser_wii.h"
|
||||
#if SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
#include "parser/uni_hid_parser_ds5.h"
|
||||
#endif
|
||||
}
|
||||
#include "parser/uni_hid_parser_switch2.h"
|
||||
#include "parser/uni_switch2_haptics.h"
|
||||
|
|
@ -254,6 +257,57 @@ constexpr WiiCuePattern kWiiCuePatterns[8] = {
|
|||
constexpr uint32_t kWiiCueDeadlineMs = 2000;
|
||||
#endif
|
||||
|
||||
#if SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
struct DualSenseIngress {
|
||||
uint32_t report_sequence = 0;
|
||||
uint32_t received_us = 0;
|
||||
uint32_t motion_sequence = 0;
|
||||
uint32_t motion_received_us = 0;
|
||||
bool has_report = false;
|
||||
bool motion_valid = false;
|
||||
int32_t accel_q13[3]{};
|
||||
int32_t gyro_q10[3]{};
|
||||
};
|
||||
|
||||
struct DualSenseCue {
|
||||
uint64_t token = 0;
|
||||
uint32_t connection_generation = 0;
|
||||
uint32_t requested_ms = 0;
|
||||
uint32_t started_ms = 0;
|
||||
uint8_t slot = 0xff;
|
||||
uint8_t sample_id = 0;
|
||||
int result = -1;
|
||||
bool active = false;
|
||||
bool consumed = false;
|
||||
bool in_flight = false;
|
||||
};
|
||||
|
||||
struct DualSenseMotorOutput {
|
||||
uint32_t deadline_ms = 0;
|
||||
uint8_t magnitude[2]{};
|
||||
bool owned = false;
|
||||
};
|
||||
|
||||
// Same bounded pulse vocabulary as Wii, with real per-motor magnitudes.
|
||||
// These are compatibility-vibration approximations, not uploaded HD waveforms.
|
||||
struct DualSenseCuePattern {
|
||||
uint16_t phases_ms[7];
|
||||
uint8_t count;
|
||||
uint8_t magnitude;
|
||||
};
|
||||
constexpr DualSenseCuePattern kDualSenseCuePatterns[8] = {
|
||||
{{0}, 0, 0},
|
||||
{{1000}, 1, 160},
|
||||
{{100, 180, 100, 180, 100, 180, 100}, 7, 200},
|
||||
{{25, 90, 25}, 3, 96},
|
||||
{{100, 140, 100}, 3, 220},
|
||||
{{70, 120, 70}, 3, 160},
|
||||
{{60}, 1, 96},
|
||||
{{120}, 1, 220},
|
||||
};
|
||||
constexpr uint32_t kDualSenseCueDeadlineMs = 2000;
|
||||
#endif
|
||||
|
||||
|
||||
|
||||
// Security Manager identity events arrive before Bluepad32 publishes a ready
|
||||
|
|
@ -291,6 +345,10 @@ struct BackendSlot {
|
|||
#ifdef SWITCH2_BRIDGE_WII_INPUT
|
||||
WiiMotionIngress wii_motion;
|
||||
#endif
|
||||
#if SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
DualSenseIngress dualsense_motion;
|
||||
DualSenseMotorOutput dualsense_output;
|
||||
#endif
|
||||
#ifdef SWITCH_PICO_WII_IR_GYRO
|
||||
WiiAimSource wii_aim;
|
||||
#endif
|
||||
|
|
@ -378,6 +436,83 @@ void retire_wii_slot(uint8_t slot_index) {
|
|||
}
|
||||
#endif
|
||||
|
||||
#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);
|
||||
}
|
||||
|
||||
bool eligible_dualsense(const BackendSlot& slot) {
|
||||
return slot.active && slot.companion == nullptr && dualsense_source_matches(slot.device);
|
||||
}
|
||||
|
||||
void cancel_dualsense_cue_locked(DualSenseCue& 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) {
|
||||
uint8_t selected = 0xff;
|
||||
for (uint8_t index = 0; index < kSlotCount; ++index) {
|
||||
if (!eligible_dualsense(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 &&
|
||||
(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;
|
||||
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;
|
||||
++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;
|
||||
}
|
||||
}
|
||||
|
||||
void retire_dualsense_slot(uint8_t index) {
|
||||
if (g_dualsense_slot == index) {
|
||||
g_dualsense_slot = 0xff;
|
||||
g_dualsense_generation = 0;
|
||||
g_dualsense_snapshot = {};
|
||||
}
|
||||
for (DualSenseCue& cue : g_dualsense_cues)
|
||||
if (cue.slot == index) cue = {};
|
||||
g_slots[index].dualsense_motion = {};
|
||||
g_slots[index].dualsense_output = {};
|
||||
}
|
||||
|
||||
bool dualsense_cue_current(const DualSenseCue& cue) {
|
||||
return cue.slot < kSlotCount && cue.slot == g_dualsense_slot &&
|
||||
cue.connection_generation == g_dualsense_generation &&
|
||||
cue.connection_generation == g_slots[cue.slot].connection_generation;
|
||||
}
|
||||
#endif
|
||||
|
||||
// These fields are only read or written by the BTstack execution context.
|
||||
btstack_timer_source_t g_rumble_timer{};
|
||||
btstack_timer_source_t g_configuration_timer{};
|
||||
|
|
@ -480,6 +615,9 @@ 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;
|
||||
#endif
|
||||
critical_section_exit(&g_state_lock);
|
||||
return physical_count < kSlotCount;
|
||||
}
|
||||
|
|
@ -522,9 +660,12 @@ int reserve_device_slot(uni_hid_device_t* device) {
|
|||
return -1;
|
||||
}
|
||||
const int tracked = slot_for_device(device);
|
||||
if (g_retired_devices[physical_index] == device &&
|
||||
uni_hid_parser_switch2_is_ble_device(device)) {
|
||||
if (g_retired_devices[physical_index] == device) {
|
||||
#if SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
return -1;
|
||||
#else
|
||||
if (uni_hid_parser_switch2_is_ble_device(device)) return -1;
|
||||
#endif
|
||||
}
|
||||
if (tracked >= 0) {
|
||||
return tracked;
|
||||
|
|
@ -537,9 +678,11 @@ int reserve_device_slot(uni_hid_device_t* device) {
|
|||
return -1;
|
||||
}
|
||||
}
|
||||
#if !SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
if (g_slots[physical_index].device == nullptr) {
|
||||
return physical_index;
|
||||
}
|
||||
#endif
|
||||
for (uint8_t index = 0; index < kSlotCount; ++index) {
|
||||
if (g_slots[index].device == nullptr) {
|
||||
return index;
|
||||
|
|
@ -1231,6 +1374,15 @@ 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 (pending != nullptr) {
|
||||
++physical_count;
|
||||
all_ready = false;
|
||||
any_connecting = true;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
critical_section_exit(&g_state_lock);
|
||||
|
||||
if (all_ready && physical_count == kSlotCount) {
|
||||
|
|
@ -1284,6 +1436,25 @@ void publish_device_state(uint8_t slot, uni_hid_device_t* device,
|
|||
memcpy(g_wii_snapshot.accel_q13, motion.accel_q13, sizeof(motion.accel_q13));
|
||||
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 = {
|
||||
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));
|
||||
}
|
||||
#endif
|
||||
g_macro_capture.observe(slot, target.connection_generation,
|
||||
time_us_32(), target.state);
|
||||
|
|
@ -1297,6 +1468,9 @@ void publish_all_neutral() {
|
|||
wii_ir_pointer_reset();
|
||||
#endif
|
||||
for (BackendSlot& slot : g_slots) {
|
||||
#if SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
retire_dualsense_slot(static_cast<uint8_t>(&slot - g_slots));
|
||||
#endif
|
||||
#ifdef SWITCH2_BRIDGE_WII_INPUT
|
||||
retire_wii_slot(static_cast<uint8_t>(&slot - g_slots));
|
||||
retire_wii_motion(slot.wii_motion);
|
||||
|
|
@ -1833,6 +2007,9 @@ 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<uint8_t>(&slot - g_slots));
|
||||
#endif
|
||||
#ifdef SWITCH2_BRIDGE_WII_INPUT
|
||||
retire_wii_slot(static_cast<uint8_t>(&slot - g_slots));
|
||||
retire_wii_motion(slot.wii_motion);
|
||||
|
|
@ -2400,6 +2577,16 @@ 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 (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
|
||||
// packets must not remain queued to overtake a later native cue.
|
||||
uni_hid_parser_ds5_bridge_rumble(
|
||||
device, duration_ms > 1000 ? 1000 : duration_ms, weak, strong);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
#ifdef SWITCH_PICO_NATIVE_SWITCH_RUMBLE
|
||||
if (switch_native_output_feedback(device, strong, weak, duration_ms)) return;
|
||||
#endif
|
||||
|
|
@ -2539,6 +2726,144 @@ void dispatch_wii_cue(const WiiCueDispatch& command) {
|
|||
}
|
||||
#endif
|
||||
|
||||
#if SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
struct DualSenseCueDispatch {
|
||||
uni_hid_device_t* device = nullptr;
|
||||
uint32_t connection_generation = 0;
|
||||
uint32_t prepared_ms = 0;
|
||||
uint64_t token[2]{};
|
||||
uint16_t duration_ms = 0;
|
||||
uint8_t magnitude[2]{};
|
||||
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,
|
||||
bool local_active, bool local_dispatch,
|
||||
DualSenseCueDispatch* command) {
|
||||
BackendSlot& slot = g_slots[index];
|
||||
DualSenseMotorOutput& previous = slot.dualsense_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];
|
||||
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 (local_active || local_dispatch) {
|
||||
if (cue.active) cancel_dualsense_cue_locked(cue);
|
||||
busy |= cue.result == 0;
|
||||
continue;
|
||||
}
|
||||
if (cue.in_flight) return true;
|
||||
if (cue.result != 0 && !cue.active) continue;
|
||||
command->token[side] = cue.token;
|
||||
pending |= cue.result == 0;
|
||||
if (cue.sample_id == 0) continue;
|
||||
const DualSenseCuePattern& pattern = kDualSenseCuePatterns[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])
|
||||
elapsed -= pattern.phases_ms[phase++];
|
||||
if (phase == pattern.count) {
|
||||
cue.active = false;
|
||||
command->token[side] = 0;
|
||||
continue;
|
||||
}
|
||||
busy = true;
|
||||
if ((phase & 1u) == 0) {
|
||||
magnitude[side] = pattern.magnitude;
|
||||
const uint16_t remaining = static_cast<uint16_t>(pattern.phases_ms[phase] - elapsed);
|
||||
if (remaining < duration) duration = remaining;
|
||||
}
|
||||
}
|
||||
if (local_active || local_dispatch) {
|
||||
// Higher-priority feedback replaces our finite timer; do not stop it.
|
||||
if (local_dispatch) previous = {};
|
||||
return busy || previous.owned;
|
||||
}
|
||||
if (duration == UINT16_MAX) duration = 0;
|
||||
const uint32_t deadline = duration == 0 ? 0 : now_ms + duration;
|
||||
const bool changed = magnitude[0] != previous.magnitude[0] ||
|
||||
magnitude[1] != previous.magnitude[1] ||
|
||||
(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);
|
||||
previous = {};
|
||||
return false;
|
||||
}
|
||||
command->device = slot.device;
|
||||
command->connection_generation = slot.connection_generation;
|
||||
command->prepared_ms = now_ms;
|
||||
command->duration_ms = duration;
|
||||
command->magnitude[0] = magnitude[0];
|
||||
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;
|
||||
return true;
|
||||
}
|
||||
|
||||
void dispatch_dualsense_cues(const DualSenseCueDispatch& 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<uint16_t>(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);
|
||||
}
|
||||
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];
|
||||
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) {
|
||||
cue.result = 1; // Accepted source submission, never a native ACK.
|
||||
cue.started_ms = command.prepared_ms;
|
||||
cue.active = cue.sample_id != 0;
|
||||
}
|
||||
}
|
||||
critical_section_exit(&g_state_lock);
|
||||
}
|
||||
#endif
|
||||
|
||||
// Core 1 only. The mailbox carries values, never a parser pointer supplied by
|
||||
// Core 0. Revalidate after lifecycle/topology work and before touching the parser.
|
||||
void process_wii_orientation(uint8_t slot_index) {
|
||||
|
|
@ -2695,6 +3020,9 @@ 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{};
|
||||
#endif
|
||||
|
||||
critical_section_enter_blocking(&g_state_lock);
|
||||
BackendSlot& slot = g_slots[slot_index];
|
||||
|
|
@ -2828,12 +3156,20 @@ void process_rumble_timer(btstack_timer_source_t* timer) {
|
|||
const bool wii_cue_owns_rumble = prepare_wii_cue(
|
||||
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(
|
||||
slot_index, now_ms, local_feedback_active,
|
||||
profile_rumble_dispatch || feedback_dispatch, &dualsense_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
|
||||
#endif
|
||||
#ifdef SWITCH_PICO_HAPTICS_EXPERIMENT
|
||||
&& !(xinput_host_mode &&
|
||||
haptics_experiment_gameplay_owns(slot.device))
|
||||
|
|
@ -2909,6 +3245,9 @@ 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);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
|
@ -3398,6 +3737,21 @@ void platform_on_device_connected(uni_hid_device_t* device) {
|
|||
uni_hid_device_disconnect(device);
|
||||
return;
|
||||
}
|
||||
#if SWITCH2_BRIDGE_DUALSENSE_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);
|
||||
if (pending_index < 0) {
|
||||
uni_hid_device_disconnect(device);
|
||||
return;
|
||||
}
|
||||
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;
|
||||
critical_section_exit(&g_state_lock);
|
||||
recompute_connection_status();
|
||||
#else
|
||||
const ControllerIdentity connection_identity = identity_for_device(device);
|
||||
critical_section_enter_blocking(&g_state_lock);
|
||||
const int physical_index = physical_index_for_device(device);
|
||||
|
|
@ -3422,11 +3776,25 @@ void platform_on_device_connected(uni_hid_device_t* device) {
|
|||
} else {
|
||||
uni_hid_device_disconnect(device);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void platform_on_device_disconnected(uni_hid_device_t* device) {
|
||||
#if SWITCH2_BRIDGE_DUALSENSE_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;
|
||||
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
|
||||
recompute_connection_status();
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
#ifdef SWITCH_PICO_NATIVE_SWITCH_RUMBLE
|
||||
|
|
@ -3458,6 +3826,9 @@ void platform_on_device_disconnected(uni_hid_device_t* device) {
|
|||
} else {
|
||||
release_slot(slot);
|
||||
}
|
||||
#if SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
refresh_dualsense_source_locked();
|
||||
#endif
|
||||
critical_section_exit(&g_state_lock);
|
||||
clear_ble_identity_for_device(device);
|
||||
if (survivor != nullptr) {
|
||||
|
|
@ -3492,11 +3863,22 @@ 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)) {
|
||||
critical_section_exit(&g_state_lock);
|
||||
return UNI_ERROR_INVALID_CONTROLLER;
|
||||
}
|
||||
#endif
|
||||
int slot_index = reserve_device_slot(device);
|
||||
if (slot_index < 0) {
|
||||
critical_section_exit(&g_state_lock);
|
||||
return UNI_ERROR_NO_SLOTS;
|
||||
}
|
||||
#if SWITCH2_BRIDGE_DUALSENSE_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;
|
||||
#endif
|
||||
BackendSlot& pending = g_slots[slot_index];
|
||||
if (!pending.active) {
|
||||
const int partner_index = joycon_partner_slot(device, slot_index);
|
||||
|
|
@ -3519,6 +3901,9 @@ uni_error_t platform_on_device_ready(uni_hid_device_t* device) {
|
|||
}
|
||||
became_active = true;
|
||||
}
|
||||
#if SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
refresh_dualsense_source_locked();
|
||||
#endif
|
||||
const BackendSlot& current = g_slots[slot_index];
|
||||
owner = current.device;
|
||||
companion = current.companion;
|
||||
|
|
@ -3583,6 +3968,29 @@ 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;
|
||||
}
|
||||
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));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#ifdef SWITCH_PICO_WII_IR
|
||||
uni_wii_ir_snapshot_t infrared{};
|
||||
const bool have_infrared =
|
||||
|
|
@ -3915,6 +4323,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;
|
||||
#endif
|
||||
}
|
||||
g_joycon_mode = JoyConMode::kPaired;
|
||||
g_joycon_reconcile_requested = false;
|
||||
|
|
@ -4120,6 +4531,76 @@ 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 (!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);
|
||||
critical_section_exit(&g_state_lock);
|
||||
}
|
||||
|
||||
void bluepad32_input_backend_dualsense_snapshot(Bluepad32DualSenseBridgeSnapshot* output) {
|
||||
if (output == nullptr) return;
|
||||
*output = {};
|
||||
if (!g_initialized) return;
|
||||
critical_section_enter_blocking(&g_state_lock);
|
||||
*output = g_dualsense_snapshot;
|
||||
critical_section_exit(&g_state_lock);
|
||||
}
|
||||
|
||||
bool bluepad32_input_backend_dualsense_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 &&
|
||||
!cue.in_flight && (sample_id == 0 || (cue.result != 0 && !cue.active));
|
||||
if (accepted) {
|
||||
cue = {};
|
||||
cue.token = g_next_dualsense_token++;
|
||||
cue.slot = index;
|
||||
cue.connection_generation = g_dualsense_generation;
|
||||
cue.requested_ms = btstack_run_loop_get_time_ms();
|
||||
cue.sample_id = sample_id;
|
||||
cue.result = 0;
|
||||
*token = cue.token;
|
||||
}
|
||||
critical_section_exit(&g_state_lock);
|
||||
return accepted;
|
||||
}
|
||||
|
||||
int bluepad32_input_backend_dualsense_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];
|
||||
int result = -1;
|
||||
if (cue.token == token && !cue.consumed) {
|
||||
if (!dualsense_cue_current(cue) ||
|
||||
(cue.result == 0 &&
|
||||
btstack_run_loop_get_time_ms() - cue.requested_ms >= kDualSenseCueDeadlineMs))
|
||||
cancel_dualsense_cue_locked(cue);
|
||||
result = cue.result;
|
||||
if (result != 0) cue.consumed = true;
|
||||
}
|
||||
critical_section_exit(&g_state_lock);
|
||||
return result;
|
||||
}
|
||||
|
||||
void bluepad32_input_backend_dualsense_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]);
|
||||
critical_section_exit(&g_state_lock);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef SWITCH2_BRIDGE_WII_INPUT
|
||||
void bluepad32_input_backend_select_wii_source(const uint8_t address[6]) {
|
||||
// Selection is configuration, not a live Core 0 parser mutation.
|
||||
|
|
|
|||
|
|
@ -108,6 +108,36 @@ 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 {
|
||||
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;
|
||||
int32_t accel_q13[3]{};
|
||||
int32_t gyro_q10[3]{};
|
||||
};
|
||||
|
||||
// nullptr selects the uniquely eligible ready DualSense; 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.
|
||||
// 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(
|
||||
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);
|
||||
#endif
|
||||
|
||||
// Side-effect-free raw input snapshot for management telemetry. Unlike the
|
||||
// report-path snapshot, reading this does not consume motion samples.
|
||||
struct Bluepad32PlaytestSnapshot {
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ struct Source {
|
|||
NativeReport native_reports[kNativeReportCapacity];
|
||||
uint8_t native_head;
|
||||
uint8_t native_count;
|
||||
uint32_t native_borrowed_serial;
|
||||
bool native_stream;
|
||||
bool source_active;
|
||||
Sample sample;
|
||||
|
|
@ -62,6 +63,28 @@ Source* selected_source(uint16_t product_id, const uint8_t address[6]) {
|
|||
void clear_native_reports(Source& source) {
|
||||
source.native_head = 0;
|
||||
source.native_count = 0;
|
||||
source.native_borrowed_serial = 0;
|
||||
}
|
||||
|
||||
bool can_replace_native_tail(const Source& source, const NativeReport& tail,
|
||||
const uint8_t* report) {
|
||||
// A handed-out packet is immutable until commit. Keep discrete transitions
|
||||
// and every relative mouse packet; only continuous state can be superseded.
|
||||
if (tail.serial == source.native_borrowed_serial ||
|
||||
memcmp(tail.report + 2, report + 2, 3) != 0 ||
|
||||
tail.report[8] != report[8] || tail.report[13] != report[13] ||
|
||||
(source.input_product_id == UNI_SW2_JOYCON_R_PID && tail.report[14] != report[14]))
|
||||
return false;
|
||||
for (unsigned i = 9; i < 13; ++i)
|
||||
if (tail.report[i] != 0 || report[i] != 0) return false;
|
||||
const unsigned length_offset = source.input_product_id == UNI_SW2_JOYCON_L_PID ? 14u : 15u;
|
||||
const unsigned motion_length = report[length_offset];
|
||||
if ((motion_length != 30u && motion_length != 40u) ||
|
||||
tail.report[length_offset] != motion_length) return false;
|
||||
const unsigned tail_offset = length_offset + 1u + motion_length;
|
||||
if (memcmp(tail.report + tail_offset, report + tail_offset,
|
||||
SWITCH2_MOUSE_CAPTURE_NATIVE_INPUT_SIZE - tail_offset) != 0) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool source_fresh(const Source& source, uint32_t now_ms) {
|
||||
|
|
@ -177,13 +200,21 @@ extern "C" void switch_pico_switch2_mouse_report(
|
|||
#if SWITCH_PICO_SWITCH2_USB_BRIDGE
|
||||
source.source_active = true;
|
||||
if (source.native_stream && g_total_records != UINT32_MAX) {
|
||||
if (source.native_count == kNativeReportCapacity) clear_native_reports(source);
|
||||
NativeReport& packet =
|
||||
source.native_reports[(source.native_head + source.native_count) % kNativeReportCapacity];
|
||||
memcpy(packet.report, report, sizeof(packet.report));
|
||||
packet.serial = g_total_records;
|
||||
packet.received_ms = received_ms;
|
||||
++source.native_count;
|
||||
NativeReport* packet = nullptr;
|
||||
if (source.native_count != 0) {
|
||||
NativeReport& tail = source.native_reports[
|
||||
(source.native_head + source.native_count - 1u) % kNativeReportCapacity];
|
||||
if (can_replace_native_tail(source, tail, report)) packet = &tail;
|
||||
}
|
||||
if (packet == nullptr) {
|
||||
if (source.native_count == kNativeReportCapacity) clear_native_reports(source);
|
||||
packet = &source.native_reports[
|
||||
(source.native_head + source.native_count) % kNativeReportCapacity];
|
||||
++source.native_count;
|
||||
}
|
||||
memcpy(packet->report, report, sizeof(packet->report));
|
||||
packet->serial = g_total_records;
|
||||
packet->received_ms = received_ms;
|
||||
}
|
||||
#endif
|
||||
memcpy(source.latest_input.buttons, report + 2, sizeof(source.latest_input.buttons));
|
||||
|
|
@ -295,6 +326,7 @@ uint32_t switch2_mouse_capture_peek_native_report(
|
|||
const NativeReport& packet = source.native_reports[source.native_head];
|
||||
memcpy(report, packet.report, sizeof(packet.report));
|
||||
serial = packet.serial;
|
||||
source.native_borrowed_serial = serial;
|
||||
}
|
||||
critical_section_exit(&g_lock);
|
||||
return serial;
|
||||
|
|
@ -309,6 +341,7 @@ bool switch2_mouse_capture_commit_native_report(uint8_t instance, uint32_t seria
|
|||
if (accepted) {
|
||||
source.native_head = static_cast<uint8_t>((source.native_head + 1) % kNativeReportCapacity);
|
||||
--source.native_count;
|
||||
source.native_borrowed_serial = 0;
|
||||
}
|
||||
critical_section_exit(&g_lock);
|
||||
return accepted;
|
||||
|
|
|
|||
|
|
@ -70,12 +70,16 @@ bool switch2_mouse_capture_latest_input(uint8_t instance, uint32_t after_serial,
|
|||
Switch2MouseCaptureInput* output);
|
||||
|
||||
#if SWITCH_PICO_SWITCH2_USB_BRIDGE
|
||||
// Core 0 explicitly enables the selected source's ordered native 07/08 relay.
|
||||
// Core 0 explicitly enables the selected source's native 07/08 relay.
|
||||
// Starts disabled; false clears the FIFO, repeated true preserves it. Enable
|
||||
// never replays earlier capture-ring/latest-input data. Selection disables it;
|
||||
// teardown and capture-serial exhaustion clear it without changing selection.
|
||||
// Only exact selected Joy-Con 63-byte native payloads (07 left, 08 right) queue.
|
||||
// The 32-entry FIFO is independent of the raw capture ring; overflow discards
|
||||
// Adjacent, unborrowed updates with unchanged buttons/status/opaque fields and
|
||||
// zero relative mouse motion coalesce to the newest analog/IMU state. Different
|
||||
// IMU formats, discrete transitions and mouse packets retain their FIFO order.
|
||||
// Peek pins the head until commit; coalescing cannot change a submitted packet.
|
||||
// The 32-entry bound is independent of the raw capture ring; overflow discards
|
||||
// queued history and retains only the arriving packet.
|
||||
void switch2_mouse_capture_set_native_stream(uint8_t instance, bool enabled);
|
||||
|
||||
|
|
|
|||
14
tests/bluepad32_native_stubs/parser/uni_hid_parser_ds5.h
Normal file
14
tests/bluepad32_native_stubs/parser/uni_hid_parser_ds5.h
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
#pragma once
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <uni.h>
|
||||
|
||||
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*);
|
||||
|
|
@ -86,6 +86,7 @@ enum {
|
|||
typedef enum {
|
||||
CONTROLLER_TYPE_UnknownController = 0,
|
||||
CONTROLLER_TYPE_WiiController = 35,
|
||||
CONTROLLER_TYPE_PS5Controller = 46,
|
||||
} uni_controller_type_t;
|
||||
|
||||
typedef enum {
|
||||
|
|
@ -140,6 +141,7 @@ struct uni_report_parser_t {
|
|||
uni_set_player_leds_t set_player_leds;
|
||||
uni_set_lightbar_color_t set_lightbar_color;
|
||||
uni_play_dual_rumble_t play_dual_rumble;
|
||||
void (*parse_input_report)(uni_hid_device_t*, const uint8_t*, uint16_t);
|
||||
};
|
||||
|
||||
enum uni_bt_conn_protocol_t {
|
||||
|
|
|
|||
291
tests/dualsense_backend_test.cpp
Normal file
291
tests/dualsense_backend_test.cpp
Normal file
|
|
@ -0,0 +1,291 @@
|
|||
// 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;
|
||||
}
|
||||
262
tests/dualsense_parser_native_test.c
Normal file
262
tests/dualsense_parser_native_test.c
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
#include <assert.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "parser/uni_hid_parser_ds5.h"
|
||||
#include "uni_hid_device.h"
|
||||
#include "uni_utils.h"
|
||||
|
||||
// Real staged DS5 parser; substitute only the radio, virtual mouse and clock.
|
||||
static uni_hid_device_t device;
|
||||
static unsigned ready_count;
|
||||
static uint8_t requested_feature;
|
||||
static uint8_t output[79];
|
||||
static unsigned output_count;
|
||||
static uint32_t now_ms;
|
||||
static bool transport_available = true;
|
||||
static int send_status = ERROR_CODE_SUCCESS;
|
||||
static unsigned disconnect_count;
|
||||
static struct {
|
||||
btstack_timer_source_t* timer;
|
||||
uint32_t deadline;
|
||||
bool active;
|
||||
} timers[2];
|
||||
|
||||
void uni_log(const char* fmt, ...) { (void)fmt; }
|
||||
void uni_hid_device_send_ctrl_report(uni_hid_device_t* d, const uint8_t* bytes, uint16_t len) {
|
||||
assert(d == &device && len == 2 && bytes[0] == 0x43);
|
||||
requested_feature = bytes[1];
|
||||
}
|
||||
void uni_hid_device_send_intr_report(uni_hid_device_t* d, const uint8_t* bytes, uint16_t len) {
|
||||
assert(d == &device && len == sizeof(output));
|
||||
memcpy(output, bytes, len);
|
||||
++output_count;
|
||||
}
|
||||
uint32_t btstack_run_loop_get_time_ms(void) { return now_ms; }
|
||||
int l2cap_can_send_packet_now(uint16_t cid) {
|
||||
assert(cid == device.conn.interrupt_cid);
|
||||
return transport_available;
|
||||
}
|
||||
int l2cap_send(uint16_t cid, uint8_t* bytes, uint16_t len) {
|
||||
assert(cid == device.conn.interrupt_cid);
|
||||
if (send_status != ERROR_CODE_SUCCESS) return send_status;
|
||||
uni_hid_device_send_intr_report(&device, bytes, len);
|
||||
return ERROR_CODE_SUCCESS;
|
||||
}
|
||||
void uni_hid_device_disconnect(uni_hid_device_t* d) {
|
||||
assert(d == &device);
|
||||
++disconnect_count;
|
||||
uni_hid_parser_ds5_bridge_teardown(d);
|
||||
}
|
||||
bool uni_hid_device_set_ready_complete(uni_hid_device_t* d) {
|
||||
assert(d == &device);
|
||||
++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; }
|
||||
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,
|
||||
DPAD_LEFT, DPAD_LEFT | DPAD_UP};
|
||||
return hat < 8 ? values[hat] : 0;
|
||||
}
|
||||
|
||||
void btstack_run_loop_set_timer(btstack_timer_source_t* timer, uint32_t ms) {
|
||||
for (unsigned i = 0; i < 2; ++i) {
|
||||
if (timers[i].timer != timer && timers[i].timer != NULL) continue;
|
||||
timers[i].timer = timer;
|
||||
timers[i].deadline = now_ms + ms;
|
||||
return;
|
||||
}
|
||||
assert(false);
|
||||
}
|
||||
void btstack_run_loop_add_timer(btstack_timer_source_t* timer) {
|
||||
for (unsigned i = 0; i < 2; ++i)
|
||||
if (timers[i].timer == timer) { timers[i].active = true; return; }
|
||||
assert(false);
|
||||
}
|
||||
bool btstack_run_loop_remove_timer(btstack_timer_source_t* timer) {
|
||||
for (unsigned i = 0; i < 2; ++i) {
|
||||
if (timers[i].timer != timer) continue;
|
||||
const bool was_active = timers[i].active;
|
||||
timers[i].active = false;
|
||||
return was_active;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
static void advance(uint32_t time) {
|
||||
now_ms = time;
|
||||
for (unsigned i = 0; i < 2; ++i) {
|
||||
if (!timers[i].active || (int32_t)(now_ms - timers[i].deadline) < 0) continue;
|
||||
timers[i].active = false;
|
||||
timers[i].timer->process(timers[i].timer);
|
||||
}
|
||||
}
|
||||
static void put16(uint8_t* bytes, int16_t value) {
|
||||
bytes[0] = (uint16_t)value;
|
||||
bytes[1] = (uint16_t)value >> 8;
|
||||
}
|
||||
static void put32(uint8_t* bytes, uint32_t value) {
|
||||
for (unsigned i = 0; i < 4; ++i) bytes[i] = value >> (8 * i);
|
||||
}
|
||||
static void seal(uint8_t* bytes, size_t size, uint8_t transaction) {
|
||||
uint32_t crc = uni_crc32_le(UINT32_MAX, &transaction, 1);
|
||||
crc = ~uni_crc32_le(crc, bytes, size - 4);
|
||||
put32(bytes + size - 4, crc);
|
||||
}
|
||||
static void feature(uint8_t* bytes, uint16_t len) {
|
||||
seal(bytes, len, 0xa3);
|
||||
uni_hid_parser_ds5_parse_feature_report(&device, bytes, len);
|
||||
}
|
||||
static void calibration(uint8_t bytes[41], bool fallback) {
|
||||
memset(bytes, 0, 41);
|
||||
bytes[0] = 5;
|
||||
const int16_t bias[3] = {10, -20, 30};
|
||||
for (unsigned axis = 0; axis < 3; ++axis) {
|
||||
put16(bytes + 1 + axis * 2, bias[axis]);
|
||||
put16(bytes + 7 + axis * 4, bias[axis] + 100);
|
||||
put16(bytes + 9 + axis * 4, bias[axis] - 100);
|
||||
put16(bytes + 23 + axis * 4, 8192);
|
||||
put16(bytes + 25 + axis * 4, -8192);
|
||||
}
|
||||
put16(bytes + 19, 100);
|
||||
put16(bytes + 21, 100);
|
||||
if (fallback) {
|
||||
put16(bytes + 23, 0);
|
||||
put16(bytes + 25, 0);
|
||||
}
|
||||
}
|
||||
static void input(uint8_t bytes[78], uint32_t timestamp) {
|
||||
memset(bytes, 0, 78);
|
||||
bytes[0] = 0x31;
|
||||
bytes[2] = bytes[3] = bytes[4] = bytes[5] = 127;
|
||||
bytes[9] = 0x28; // Cross + neutral hat.
|
||||
bytes[11] = 0x02; // Touchpad click, not mute.
|
||||
put16(bytes + 17, 11);
|
||||
put16(bytes + 19, -20);
|
||||
put16(bytes + 21, 30);
|
||||
put16(bytes + 25, 8192);
|
||||
put32(bytes + 29, timestamp);
|
||||
seal(bytes, 78, 0xa1);
|
||||
}
|
||||
static uni_ds5_bridge_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);
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
device.controller_type = CONTROLLER_TYPE_PS5Controller;
|
||||
device.product_id = 0x0df2; // Edge takes the real PS5 path.
|
||||
device.report_parser.setup = uni_hid_parser_ds5_setup;
|
||||
device.conn.interrupt_cid = 0x40;
|
||||
uni_hid_parser_ds5_setup(&device);
|
||||
assert(requested_feature == 9);
|
||||
uint8_t pairing[20] = {9};
|
||||
feature(pairing, sizeof(pairing));
|
||||
assert(requested_feature == 0x20);
|
||||
uint8_t firmware[64] = {0x20};
|
||||
feature(firmware, sizeof(firmware));
|
||||
assert(requested_feature == 5);
|
||||
uint8_t calib[41];
|
||||
calibration(calib, false);
|
||||
seal(calib, sizeof(calib), 0xa3);
|
||||
uni_hid_parser_ds5_parse_feature_report(&device, calib, 40);
|
||||
assert(ready_count == 0); // A partial feature cannot initialize calibration.
|
||||
calib[25] ^= 1;
|
||||
uni_hid_parser_ds5_parse_feature_report(&device, calib, sizeof(calib));
|
||||
assert(ready_count == 0); // Nor can a full feature with a corrupt CRC.
|
||||
calibration(calib, true);
|
||||
feature(calib, sizeof(calib));
|
||||
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);
|
||||
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.
|
||||
input(report, 101);
|
||||
snapshot = feed(report, sizeof(report), true);
|
||||
assert(snapshot.motion_valid && snapshot.motion_sequence == 1);
|
||||
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);
|
||||
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);
|
||||
snapshot = feed(report, sizeof(report), true);
|
||||
assert(!snapshot.motion_valid && snapshot.motion_sequence == 1);
|
||||
input(report, 99);
|
||||
snapshot = feed(report, sizeof(report), true);
|
||||
assert(!snapshot.motion_valid && snapshot.motion_sequence == 1);
|
||||
input(report, 102);
|
||||
feed(report, 77, false);
|
||||
report[9] ^= 0x20;
|
||||
feed(report, sizeof(report), false);
|
||||
feed(NULL, 0, false);
|
||||
input(report, 102);
|
||||
snapshot = feed(report, sizeof(report), true);
|
||||
assert(snapshot.motion_valid && snapshot.motion_sequence == 2);
|
||||
|
||||
// A real uint32 sensor-clock wrap is forward progress, not a duplicate.
|
||||
uni_hid_parser_ds5_setup(&device);
|
||||
calibration(calib, false);
|
||||
feature(calib, sizeof(calib));
|
||||
input(report, UINT32_MAX - 15);
|
||||
snapshot = feed(report, sizeof(report), true);
|
||||
assert(snapshot.motion_valid && snapshot.motion_sequence == 1);
|
||||
input(report, 16);
|
||||
snapshot = feed(report, sizeof(report), true);
|
||||
assert(snapshot.motion_valid && snapshot.motion_sequence == 2);
|
||||
|
||||
const unsigned before_busy = output_count;
|
||||
transport_available = false;
|
||||
assert(!uni_hid_parser_ds5_bridge_rumble(&device, 60, 31, 217) && output_count == before_busy);
|
||||
transport_available = true;
|
||||
send_status = BTSTACK_ACL_BUFFERS_FULL;
|
||||
assert(!uni_hid_parser_ds5_bridge_rumble(&device, 60, 31, 217) && output_count == before_busy);
|
||||
send_status = ERROR_CODE_SUCCESS;
|
||||
assert(uni_hid_parser_ds5_bridge_rumble(&device, 60, 31, 217));
|
||||
assert(output[6] == 31 && output[7] == 217); // Wire motor right/left, not callback echoes.
|
||||
advance(59);
|
||||
assert(output[6] == 31 && output[7] == 217);
|
||||
advance(60);
|
||||
assert(output[6] == 0 && output[7] == 0); // Real parser's finite duration timer stops both.
|
||||
uni_hid_parser_ds5_play_dual_rumble(&device, 100, 1000, 90, 0);
|
||||
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));
|
||||
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);
|
||||
advance(4000);
|
||||
assert(output_count == active_sent); // No timer callback into reused parser memory.
|
||||
assert(uni_hid_parser_ds5_bridge_rumble(&device, 60, 31, 217));
|
||||
transport_available = false;
|
||||
advance(4060);
|
||||
assert(output[6] == 31 && output[7] == 217 && disconnect_count == 0);
|
||||
transport_available = true;
|
||||
advance(4065);
|
||||
assert(output[6] == 0 && output[7] == 0 && disconnect_count == 0);
|
||||
assert(uni_hid_parser_ds5_bridge_rumble(&device, 60, 31, 217));
|
||||
transport_available = false;
|
||||
advance(4125);
|
||||
advance(6125);
|
||||
assert(disconnect_count == 1); // OFF cannot stall forever on a live link.
|
||||
const unsigned after_disconnect = output_count;
|
||||
advance(9000);
|
||||
assert(output_count == after_disconnect);
|
||||
puts("DualSense calibrated admission and bounded driver lifetime passed");
|
||||
return 0;
|
||||
}
|
||||
335
tests/switch2_dualsense_bridge_test.cpp
Normal file
335
tests/switch2_dualsense_bridge_test.cpp
Normal file
|
|
@ -0,0 +1,335 @@
|
|||
#include <assert.h>
|
||||
#include <math.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "controller_input.h"
|
||||
#include "input/bluepad32_input_backend.h"
|
||||
#include "model.h"
|
||||
#include "pico/stdlib.h"
|
||||
#include "platform/pico/bootsel_pairing_button.h"
|
||||
#include "profile/controller_profile_runtime.h"
|
||||
|
||||
namespace {
|
||||
uint64_t now_us = 1000000;
|
||||
uint32_t stage;
|
||||
Bluepad32DualSenseBridgeSnapshot source;
|
||||
ControllerProfile profile;
|
||||
bool alternating_shortcut;
|
||||
bool shortcut_phase;
|
||||
probe_controller_input controls[2];
|
||||
uint8_t reports[2][63];
|
||||
}
|
||||
|
||||
uint32_t time_us_32() { return static_cast<uint32_t>(now_us); }
|
||||
absolute_time_t get_absolute_time() { return now_us; }
|
||||
uint32_t to_ms_since_boot(absolute_time_t time) { return static_cast<uint32_t>(time / 1000); }
|
||||
void system_clock_initialize() {}
|
||||
extern "C" int probe_debug_printf(const char*, ...) { return 0; }
|
||||
BootselPairingButtonEvent bootsel_pairing_button_task() { return BootselPairingButtonEvent::kNone; }
|
||||
void bluepad32_input_backend_init() { stage = 1; }
|
||||
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_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; }
|
||||
bool controller_profile_runtime_take_profile_change(uint8_t, ControllerProfileRuntimeProfileChangeEvent*) { return false; }
|
||||
ControllerProfileTransformResult controller_profile_runtime_transform(
|
||||
uint8_t, const Bluepad32SlotSnapshot& input, uint32_t, AdapterUsbMode) {
|
||||
if (!input.active) return {};
|
||||
auto result = controller_profile_transform(input.state, profile);
|
||||
if (alternating_shortcut) {
|
||||
// Model a runtime synthetic transition spanning the two halves. Two
|
||||
// evaluations for one paired report would expose contradictory states.
|
||||
shortcut_phase = !shortcut_phase;
|
||||
result.state.button_system = result.state.button_capture = shortcut_phase;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
namespace {
|
||||
uint32_t now_ms() { return to_ms_since_boot(now_us); }
|
||||
void put_pair(uint8_t* out, uint16_t x, uint16_t y) {
|
||||
out[0] = static_cast<uint8_t>(x);
|
||||
out[1] = static_cast<uint8_t>((x >> 8) | (y << 4));
|
||||
out[2] = static_cast<uint8_t>(y >> 4);
|
||||
}
|
||||
void calibrate(uint8_t instance, uint16_t x, uint16_t y, uint16_t px, uint16_t py, uint16_t nx, uint16_t ny) {
|
||||
uint8_t record[9];
|
||||
put_pair(record, x, y);
|
||||
put_pair(record + 3, px, py);
|
||||
put_pair(record + 6, nx, ny);
|
||||
probe_controller_input_set_full_stick_calibration(instance, record);
|
||||
}
|
||||
uint16_t stick_x(uint8_t instance) { return reports[instance][5] | ((reports[instance][6] & 15u) << 8); }
|
||||
uint16_t stick_y(uint8_t instance) { return (reports[instance][6] >> 4) | (reports[instance][7] << 4); }
|
||||
uint8_t imu_length(uint8_t instance) { return reports[instance][probe_model_imu_length_offset(instance)]; }
|
||||
uint32_t bits(const uint8_t* bytes, unsigned offset, unsigned count) {
|
||||
uint32_t value = 0;
|
||||
for (unsigned i = 0; i < count; ++i) value |= uint32_t((bytes[(offset + i) / 8] >> ((offset + i) % 8)) & 1) << i;
|
||||
return value;
|
||||
}
|
||||
void quaternion(uint8_t instance, double out[4]) {
|
||||
const uint8_t* imu = reports[instance] + probe_model_imu_data_offset(instance);
|
||||
assert(imu_length(instance) == 30);
|
||||
const unsigned largest = bits(imu, 32, 3);
|
||||
assert(largest < 4);
|
||||
double ratios[3], norm = 1;
|
||||
for (unsigned i = 0; i < 3; ++i) {
|
||||
ratios[i] = bits(imu, 35 + 31 * i, 31) / 1073741824.0 - 1;
|
||||
norm += ratios[i] * ratios[i];
|
||||
}
|
||||
out[largest] = 1 / sqrt(norm);
|
||||
for (unsigned i = 0; i < 3; ++i) out[(largest + i + 1) & 3] = ratios[i] * out[largest];
|
||||
}
|
||||
void publish(bool motion = true) {
|
||||
now_us += 4000;
|
||||
source.received_us = time_us_32();
|
||||
++source.state_generation;
|
||||
if (motion) {
|
||||
source.motion_received_us = time_us_32();
|
||||
++source.motion_sequence;
|
||||
}
|
||||
}
|
||||
uint32_t peek(uint8_t instance) {
|
||||
probe_controller_input_poll(instance, now_ms(), &controls[instance]);
|
||||
return probe_controller_input_peek_native_report(instance, now_ms(), reports[instance]);
|
||||
}
|
||||
void consume(uint8_t instance) {
|
||||
const uint32_t token = peek(instance);
|
||||
assert(token && probe_controller_input_commit_native_report(instance, token));
|
||||
}
|
||||
void pair() { consume(0); consume(1); }
|
||||
void no_mouse_or_rails() {
|
||||
for (unsigned i = 0; i < 2; ++i) {
|
||||
assert((reports[i][3] & 0xc0) == 0);
|
||||
assert(reports[i][9] == 0 && reports[i][10] == 0 && reports[i][11] == 0 && reports[i][12] == 0);
|
||||
assert(reports[i][13] == 0xff);
|
||||
}
|
||||
}
|
||||
|
||||
void mapped_halves_and_calibration() {
|
||||
source.slot = 2;
|
||||
source.controller.active = true;
|
||||
source.controller.connection_generation = 7;
|
||||
source.controller.identity = controller_identity_global();
|
||||
publish();
|
||||
// Neither an absent source nor an uncalibrated child masquerades as active.
|
||||
assert(!peek(0) && !controls[0].active);
|
||||
calibrate(0, 2000, 2100, 1500, 1400, 1600, 1700);
|
||||
assert(peek(0));
|
||||
assert(!peek(1) && !controls[1].active);
|
||||
calibrate(1, 1800, 1900, 1700, 1800, 1400, 1500);
|
||||
pair();
|
||||
assert(stick_x(0) == 2000 && stick_y(0) == 2100);
|
||||
assert(stick_x(1) == 1800 && stick_y(1) == 1900);
|
||||
// Actual profile transforms can move controls across native children.
|
||||
profile.button_map[static_cast<unsigned>(ControllerProfileLogicalButton::kSouth)] =
|
||||
static_cast<uint8_t>(ControllerProfileLogicalButton::kDpadRight);
|
||||
profile.button_map[static_cast<unsigned>(ControllerProfileLogicalButton::kDpadLeft)] =
|
||||
static_cast<uint8_t>(ControllerProfileLogicalButton::kEast);
|
||||
profile.triggers[0].digital_threshold = 20000;
|
||||
profile.triggers[1].digital_threshold = 30000;
|
||||
ControllerState& state = source.controller.state;
|
||||
state.button_south = state.dpad_left = true;
|
||||
state.button_left_shoulder = state.button_right_shoulder = true;
|
||||
state.button_select = state.button_start = true;
|
||||
state.button_left_stick = state.button_right_stick = true;
|
||||
state.button_system = state.button_capture = true;
|
||||
state.left_trigger = 19999;
|
||||
state.right_trigger = 30000;
|
||||
state.right_stick_x = INT16_MAX;
|
||||
state.left_stick_y = INT16_MIN;
|
||||
publish(); pair();
|
||||
assert(reports[0][2] == 0xf2 && reports[1][2] == 0xd2);
|
||||
assert(reports[0][3] == 1 && reports[1][3] == 1);
|
||||
assert(stick_x(0) == 3500 && stick_y(0) == 2100);
|
||||
assert(stick_x(1) == 1800 && stick_y(1) == 3700);
|
||||
no_mouse_or_rails();
|
||||
state = {};
|
||||
state.button_west = state.button_north = true;
|
||||
state.dpad_up = state.dpad_down = true;
|
||||
state.left_trigger = 20000;
|
||||
state.right_stick_x = INT16_MIN;
|
||||
state.left_stick_y = INT16_MAX;
|
||||
publish(); pair();
|
||||
assert(reports[0][2] == 0x0c && reports[1][2] == 0x29);
|
||||
assert(stick_x(0) == 400 && stick_y(1) == 400);
|
||||
// A malformed calibration may not spill a 12-bit axis into its neighbor.
|
||||
const uint32_t left_pending = peek(1);
|
||||
calibrate(0, 2000, 2100, 3000, 1400, 1600, 1700);
|
||||
assert(!peek(0));
|
||||
assert(probe_controller_input_commit_native_report(1, left_pending));
|
||||
calibrate(0, 2000, 2100, 1500, 1400, 1600, 1700);
|
||||
state = {};
|
||||
profile = controller_profile_default(controller_identity_global(), 0);
|
||||
alternating_shortcut = true;
|
||||
for (unsigned i = 0; i < 4; ++i) {
|
||||
publish(); pair();
|
||||
assert(reports[0][3] == reports[1][3]);
|
||||
}
|
||||
alternating_shortcut = false;
|
||||
}
|
||||
|
||||
void independent_backpressure_and_resets() {
|
||||
publish();
|
||||
const uint32_t blocked_left = peek(1);
|
||||
const uint32_t right = peek(0);
|
||||
uint8_t saved[63]; memcpy(saved, reports[0], sizeof(saved));
|
||||
assert(peek(0) == right && memcmp(saved, reports[0], sizeof(saved)) == 0);
|
||||
assert(!probe_controller_input_commit_native_report(1, right));
|
||||
assert(probe_controller_input_commit_native_report(0, right));
|
||||
assert(!probe_controller_input_commit_native_report(0, right));
|
||||
assert(probe_controller_input_commit_native_report(1, blocked_left));
|
||||
publish();
|
||||
const uint32_t obsolete = peek(1);
|
||||
for (unsigned i = 0; i < 40; ++i) {
|
||||
source.controller.state.dpad_down = (i & 1) != 0;
|
||||
source.controller.state.button_east = (i & 1) != 0;
|
||||
publish(); consume(0);
|
||||
}
|
||||
const uint32_t latest = peek(1);
|
||||
assert(latest != obsolete && reports[1][2] == 1);
|
||||
assert(!probe_controller_input_commit_native_report(1, obsolete));
|
||||
probe_controller_input_set_native_stream(0, false);
|
||||
assert(!peek(0));
|
||||
assert(probe_controller_input_commit_native_report(1, latest));
|
||||
probe_controller_input_set_native_stream(0, true);
|
||||
const uint32_t right_pending = peek(0);
|
||||
probe_controller_input_set_native_stream(1, false);
|
||||
assert(probe_controller_input_commit_native_report(0, right_pending));
|
||||
probe_controller_input_set_native_stream(1, true);
|
||||
source.controller.state = {};
|
||||
}
|
||||
|
||||
void real_motion_admission_and_loss() {
|
||||
source.motion_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.
|
||||
source.gyro_q10[0] = 0;
|
||||
source.gyro_q10[1] = 90 * 1024;
|
||||
source.gyro_q10[2] = 0;
|
||||
publish(); pair();
|
||||
assert(imu_length(0) == 30 && imu_length(1) == 30);
|
||||
source.gyro_q10[1] = 0;
|
||||
publish(); pair();
|
||||
// Polling and fresh button packets cannot create additional IMU samples.
|
||||
for (unsigned i = 0; i < 420; ++i) {
|
||||
publish(false); pair();
|
||||
assert(controls[0].active && controls[1].active);
|
||||
assert(imu_length(0) == 0 && imu_length(1) == 0);
|
||||
}
|
||||
publish(); pair(); // Fresh factory-calibrated data recovers without another settling delay.
|
||||
assert(imu_length(0) == 30 && imu_length(1) == 30);
|
||||
const uint8_t* right_imu = reports[0] + probe_model_imu_data_offset(0);
|
||||
const uint8_t* left_imu = reports[1] + probe_model_imu_data_offset(1);
|
||||
assert(memcmp(right_imu, left_imu, 30) == 0);
|
||||
assert(bits(right_imu, 128, 32) == 0 && bits(right_imu, 160, 32) == 0);
|
||||
assert(bits(right_imu, 192, 32) == (1u << 28));
|
||||
double initial[4]; quaternion(0, initial);
|
||||
// A new controls packet with no new IMU cannot emit the old sample again.
|
||||
source.controller.state.button_east = true;
|
||||
publish(false); pair();
|
||||
assert(reports[0][2] == 2 && imu_length(0) == 0 && imu_length(1) == 0);
|
||||
// A blocked child's motion is not consumed by the other child's endpoint.
|
||||
publish(); consume(0);
|
||||
const uint32_t left_pending = peek(1);
|
||||
assert(imu_length(1) == 30);
|
||||
consume(0); assert(imu_length(0) == 0);
|
||||
probe_controller_input_set_native_stream(0, false);
|
||||
assert(probe_controller_input_commit_native_report(1, left_pending));
|
||||
probe_controller_input_set_native_stream(0, true);
|
||||
publish(); pair();
|
||||
assert(imu_length(0) == 30 && imu_length(1) == 30); // No shared recalibration on USB reset.
|
||||
// One second of genuine 90dps yaw advances the same rigid orientation once,
|
||||
// not twice because two virtual endpoints happen to consume it.
|
||||
source.gyro_q10[1] += 90 * 1024;
|
||||
for (unsigned i = 0; i < 250; ++i) { publish(); pair(); }
|
||||
double turned[4]; quaternion(0, turned);
|
||||
double dot = 0;
|
||||
for (unsigned i = 0; i < 4; ++i) dot += initial[i] * turned[i];
|
||||
assert(fabs(fabs(dot) - sqrt(.5)) < .015);
|
||||
quaternion(1, initial);
|
||||
for (unsigned i = 0; i < 4; ++i) assert(fabs(initial[i] - turned[i]) < 1e-8);
|
||||
source.gyro_q10[1] -= 90 * 1024;
|
||||
publish();
|
||||
const uint32_t obsolete = peek(0);
|
||||
source.motion_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;
|
||||
publish(); pair();
|
||||
assert(imu_length(0) == 30);
|
||||
for (unsigned i = 0; i < 38; ++i) { publish(false); pair(); }
|
||||
assert(controls[0].active && reports[0][2] == 2 && imu_length(0) == 0 && imu_length(1) == 0);
|
||||
publish();
|
||||
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.
|
||||
assert(!probe_controller_input_commit_native_report(0, old_right));
|
||||
assert(!probe_controller_input_commit_native_report(1, old_left));
|
||||
pair();
|
||||
assert(controls[0].active && imu_length(0) == 0 && imu_length(1) == 0);
|
||||
source.controller.active = false;
|
||||
memset(reports[0], 0x5a, 63);
|
||||
assert(!peek(0) && !controls[0].active);
|
||||
for (uint8_t byte : reports[0]) assert(byte == 0x5a);
|
||||
assert(!peek(1) && !controls[1].active);
|
||||
source.controller.active = true;
|
||||
++source.controller.connection_generation;
|
||||
publish(); pair();
|
||||
now_us += 500000;
|
||||
assert(!peek(0) && !peek(1));
|
||||
assert(!controls[0].active && !controls[1].active);
|
||||
}
|
||||
void selected_motion_target_keeps_both_control_halves() {
|
||||
source = {};
|
||||
source.slot = 0;
|
||||
source.controller.active = true;
|
||||
source.controller.connection_generation = 99;
|
||||
source.controller.state.button_south = true;
|
||||
source.controller.state.dpad_up = true;
|
||||
source.motion_valid = true;
|
||||
source.accel_q13[1] = 8192;
|
||||
profile = controller_profile_default(controller_identity_global(), 0);
|
||||
calibrate(0, 2048, 2048, 2047, 2047, 2048, 2048);
|
||||
calibrate(1, 2048, 2048, 2047, 2047, 2048, 2048);
|
||||
publish(); pair();
|
||||
for (uint8_t instance = 0; instance < 2; ++instance) {
|
||||
assert(controls[instance].active);
|
||||
const bool enabled = (SWITCH2_BRIDGE_IMU_TARGET_MASK & (1u << instance)) != 0;
|
||||
assert(imu_length(instance) == (enabled ? 30 : 0));
|
||||
}
|
||||
assert(reports[0][2] == 0x01 && reports[1][2] == 0x08);
|
||||
source.gyro_q10[1] = 90 * 1024;
|
||||
publish(); pair();
|
||||
assert(imu_length(0) == ((SWITCH2_BRIDGE_IMU_TARGET_MASK & 1) ? 30 : 0));
|
||||
assert(imu_length(1) == ((SWITCH2_BRIDGE_IMU_TARGET_MASK & 2) ? 30 : 0));
|
||||
no_mouse_or_rails();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
assert(!probe_controller_input_peek_native_report(0, now_ms(), reports[0]));
|
||||
probe_controller_input_init();
|
||||
assert(probe_controller_input_start());
|
||||
probe_controller_input_set_native_stream(0, true);
|
||||
probe_controller_input_set_native_stream(1, true);
|
||||
assert(!peek(0) && !peek(1));
|
||||
mapped_halves_and_calibration();
|
||||
independent_backpressure_and_resets();
|
||||
if (SWITCH2_BRIDGE_IMU_TARGET_MASK == 3) real_motion_admission_and_loss();
|
||||
selected_motion_target_keeps_both_control_halves();
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -409,6 +409,63 @@ static void test_bounded_overflow() {
|
|||
expect_empty(); // Overflow discarded all prior history, not merely its head.
|
||||
}
|
||||
|
||||
static void test_continuous_state_coalescing_preserves_events_and_borrowed_head() {
|
||||
now = 1500;
|
||||
disconnect();
|
||||
probe_controller_input_set_native_stream(0, true);
|
||||
const auto first = native_report(0x80, 30, 0, 0);
|
||||
auto newest = first;
|
||||
emit(first);
|
||||
for (unsigned i = 1; i <= 24; ++i) {
|
||||
now += 8;
|
||||
newest[0] = static_cast<uint8_t>(0x80 + i);
|
||||
newest[5] = static_cast<uint8_t>(first[5] + i);
|
||||
newest[PROBE_IMU_LENGTH_OFFSET + 1] = static_cast<uint8_t>(i);
|
||||
emit(newest);
|
||||
}
|
||||
// No 192ms history of analog/IMU-only updates is replayed to a slow consumer.
|
||||
assert(probe_controller_input_commit_native_report(0, expect_report(newest)));
|
||||
expect_empty();
|
||||
|
||||
emit(first);
|
||||
const uint32_t borrowed = expect_report(first);
|
||||
for (unsigned i = 0; i < 6; ++i) {
|
||||
now += 8;
|
||||
++newest[0];
|
||||
++newest[6];
|
||||
emit(newest);
|
||||
}
|
||||
assert(expect_report(first) == borrowed);
|
||||
assert(probe_controller_input_commit_native_report(0, borrowed));
|
||||
assert(probe_controller_input_commit_native_report(0, expect_report(newest)));
|
||||
expect_empty();
|
||||
|
||||
auto pressed = first;
|
||||
pressed[2] ^= 1;
|
||||
auto last_pressed = pressed;
|
||||
++last_pressed[0]; ++last_pressed[5];
|
||||
auto released = last_pressed;
|
||||
released[2] = first[2];
|
||||
auto surface = released;
|
||||
surface[13] ^= 0x10;
|
||||
auto opaque_tail = surface;
|
||||
opaque_tail.back() ^= 0x80;
|
||||
auto other_format = opaque_tail;
|
||||
other_format[PROBE_IMU_LENGTH_OFFSET] = 40;
|
||||
emit(first); emit(pressed); emit(last_pressed); emit(released);
|
||||
emit(surface); emit(opaque_tail); emit(other_format);
|
||||
for (const auto& expected : {first, last_pressed, released, surface, opaque_tail, other_format})
|
||||
assert(probe_controller_input_commit_native_report(0, expect_report(expected)));
|
||||
expect_empty();
|
||||
|
||||
auto mouse = first;
|
||||
mouse[9] = 7;
|
||||
emit(first); emit(mouse); emit(first);
|
||||
for (const auto& expected : {first, mouse, first})
|
||||
assert(probe_controller_input_commit_native_report(0, expect_report(expected)));
|
||||
expect_empty();
|
||||
}
|
||||
|
||||
static void test_expiry_and_wrapping_clock() {
|
||||
now = 2000;
|
||||
const auto first = native_report(0x61, 30);
|
||||
|
|
@ -580,6 +637,7 @@ int main() {
|
|||
test_selected_source_isolation_and_reconnect();
|
||||
test_side_switch_and_sample_ownership();
|
||||
test_bounded_overflow();
|
||||
test_continuous_state_coalescing_preserves_events_and_borrowed_head();
|
||||
test_expiry_and_wrapping_clock();
|
||||
#if SWITCH2_PROBE_COMPOSITE
|
||||
test_simultaneous_sources();
|
||||
|
|
|
|||
|
|
@ -193,7 +193,7 @@ struct Rig {
|
|||
++sample.gyro_sequence;
|
||||
sample.gyro_us = now;
|
||||
}
|
||||
motion.update(now, generation, sample);
|
||||
motion.update(now, generation, sample, ProbeNativeMotionBias::kEstimateStationary);
|
||||
}
|
||||
void settle() {
|
||||
for (unsigned i = 0; i < 65; ++i) fresh();
|
||||
|
|
|
|||
|
|
@ -143,7 +143,9 @@ int main() {
|
|||
source.accel_valid = source.gyro_valid = true;
|
||||
source.accel_q13[1] = 8192; // SDL up -> virtual native rail-down +X.
|
||||
memcpy(source.gyro_q10, bias_q10, sizeof(bias_q10));
|
||||
for (unsigned i = 0; i < 450; ++i) assert(poll());
|
||||
assert(poll());
|
||||
assert(controls.active && packet[15] == 0); // Wii alone still estimates residual bias before IMU output.
|
||||
for (unsigned i = 1; i < 450; ++i) assert(poll());
|
||||
assert(controls.active && packet[15] == 30 && packet[19] == 0x0c);
|
||||
assert(signed32(packet+32) == (1 << 28));
|
||||
assert(signed32(packet+36) == 0 && signed32(packet+40) == 0);
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ typedef struct btstack_timer_source {
|
|||
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);
|
||||
bool btstack_run_loop_remove_timer(btstack_timer_source_t* timer);
|
||||
uint32_t btstack_run_loop_get_time_ms(void);
|
||||
void btstack_run_loop_set_timer_context(btstack_timer_source_t* timer, void* context);
|
||||
void btstack_run_loop_set_timer_handler(btstack_timer_source_t* timer, void (*handler)(btstack_timer_source_t*));
|
||||
void* btstack_run_loop_get_timer_context(btstack_timer_source_t* timer);
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
#include "bt/uni_bt_service.h"
|
||||
#include "parser/uni_hid_parser_switch.h"
|
||||
#include "parser/uni_hid_parser_switch2.h"
|
||||
#include "parser/uni_hid_parser_wii.h"
|
||||
#include "platform/uni_platform.h"
|
||||
#include "uni_hid_device.h"
|
||||
|
||||
|
|
@ -31,6 +32,14 @@ void uni_hid_parser_switch2_teardown(uni_hid_device_t* d) {
|
|||
(void)d;
|
||||
assert(!"Switch 2 teardown reached a Classic Switch fixture");
|
||||
}
|
||||
void uni_hid_parser_wii_setup(uni_hid_device_t* d) {
|
||||
(void)d;
|
||||
assert(!"Wii setup reached a Classic Switch fixture");
|
||||
}
|
||||
void uni_hid_parser_wii_teardown(uni_hid_device_t* d) {
|
||||
(void)d;
|
||||
assert(!"Wii teardown reached a Classic Switch fixture");
|
||||
}
|
||||
|
||||
static unsigned timer_index(btstack_timer_source_t* timer) {
|
||||
for (unsigned i = 0; i < 32; ++i) {
|
||||
|
|
|
|||
54
tests/test_dualsense_backend_native.py
Normal file
54
tests/test_dualsense_backend_native.py
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def test_dualsense_backend_native(tmp_path: Path) -> None:
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
compiler = shutil.which("c++") or shutil.which("g++")
|
||||
assert compiler is not None, "a host C++ compiler is required"
|
||||
executable = tmp_path / "dualsense_backend_test"
|
||||
firmware = root / "src" / "firmware"
|
||||
sources = [
|
||||
root / "tests" / "dualsense_backend_test.cpp",
|
||||
firmware / "profile" / "controller_profile.cpp",
|
||||
firmware / "profile" / "controller_profile_transform.cpp",
|
||||
firmware / "profile" / "controller_synthetic_input.cpp",
|
||||
firmware / "profile" / "controller_profile_runtime.cpp",
|
||||
firmware / "profile" / "profile_storage.cpp",
|
||||
firmware / "input" / "wii_swing.cpp",
|
||||
firmware / "input" / "controller_macro_capture.cpp",
|
||||
root / "bluepad32_config" / "parser" / "uni_switch2_haptics.c",
|
||||
]
|
||||
subprocess.run(
|
||||
[
|
||||
compiler,
|
||||
"-std=c++17",
|
||||
"-Wall",
|
||||
"-Wextra",
|
||||
"-Werror",
|
||||
"-pedantic",
|
||||
"-DSWITCH_PICO_HID_INSTANCE_COUNT=4",
|
||||
"-DSWITCH_PICO_USB_OUTPUT_MODES=1",
|
||||
"-DSWITCH_PICO_ENABLE_BLE=1",
|
||||
"-DSWITCH_PICO_ENABLE_CLASSIC=1",
|
||||
"-DSWITCH2_BRIDGE_DUALSENSE_INPUT=1",
|
||||
f"-I{root / 'tests' / 'bluepad32_native_stubs'}",
|
||||
f"-I{firmware}",
|
||||
f"-I{root / 'bluepad32_config'}",
|
||||
*(str(source) for source in sources),
|
||||
"-o",
|
||||
str(executable),
|
||||
],
|
||||
check=True,
|
||||
cwd=root,
|
||||
)
|
||||
for scenario in (
|
||||
"stable-logical-slot",
|
||||
"source-isolation",
|
||||
"cue-lifetime",
|
||||
"cue-races",
|
||||
):
|
||||
subprocess.run([str(executable), scenario], check=True, cwd=root)
|
||||
51
tests/test_dualsense_parser_native.py
Normal file
51
tests/test_dualsense_parser_native.py
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
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_dualsense_parser_native(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 / "dualsense_parser_native_test"
|
||||
subprocess.run(
|
||||
[
|
||||
compiler,
|
||||
"-std=gnu11",
|
||||
"-O1",
|
||||
"-Wall",
|
||||
"-Wextra",
|
||||
"-ffunction-sections",
|
||||
"-fdata-sections",
|
||||
"-DENABLE_BLE",
|
||||
"-DENABLE_CLASSIC",
|
||||
"-DSWITCH2_BRIDGE_DUALSENSE_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" / "dualsense_parser_native_test.c"),
|
||||
str(component / "parser" / "uni_hid_parser_ds5.c"),
|
||||
str(component / "uni_utils.c"),
|
||||
"-Wl,--gc-sections",
|
||||
"-o",
|
||||
str(executable),
|
||||
],
|
||||
check=True,
|
||||
cwd=root,
|
||||
)
|
||||
subprocess.run([str(executable)], check=True, cwd=root)
|
||||
57
tests/test_switch2_dualsense_bridge_native.py
Normal file
57
tests/test_switch2_dualsense_bridge_native.py
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.parametrize("imu_target", [1, 2, 3], ids=["right", "left", "both"])
|
||||
def test_dualsense_native_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"
|
||||
subprocess.run(
|
||||
[
|
||||
compiler,
|
||||
"-std=c++17",
|
||||
"-Wall",
|
||||
"-Wextra",
|
||||
"-Werror",
|
||||
"-pedantic",
|
||||
"-DSWITCH_PICO_SWITCH2_USB_BRIDGE=1",
|
||||
"-DSWITCH2_BRIDGE_DUALSENSE_INPUT=1",
|
||||
"-DSWITCH2_BRIDGE_SOURCE_AUTO=1",
|
||||
"-DSWITCH2_PROBE_HUB=1",
|
||||
f"-DSWITCH2_BRIDGE_IMU_TARGET_MASK={imu_target}",
|
||||
"-DSWITCH_PICO_BLUEPAD32=1",
|
||||
"-DSWITCH_PICO_ENABLE_CLASSIC=1",
|
||||
f"-I{root / 'tests' / 'switch2_mouse_bridge_native_stubs'}",
|
||||
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 / "tools" / "switch2_usb_probe" / "controller_input.cpp"),
|
||||
str(root / "tools" / "switch2_usb_probe" / "dualsense_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"),
|
||||
str(
|
||||
root
|
||||
/ "src"
|
||||
/ "firmware"
|
||||
/ "profile"
|
||||
/ "controller_profile_transform.cpp"
|
||||
),
|
||||
"-o",
|
||||
str(executable),
|
||||
],
|
||||
check=True,
|
||||
cwd=root,
|
||||
)
|
||||
subprocess.run([str(executable)], check=True, cwd=root)
|
||||
|
|
@ -43,6 +43,14 @@ def model_references(build_dir: Path) -> dict[str, dict[str, Any]]:
|
|||
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"
|
||||
and cache.get("SWITCH2_BRIDGE_IMU_TARGET", "BOTH") != "BOTH"
|
||||
):
|
||||
raise ValueError(
|
||||
"Full dual-IMU qualification requires SWITCH2_BRIDGE_IMU_TARGET=BOTH; "
|
||||
"use the USB-completion UART trace for LEFT/RIGHT routing comparisons"
|
||||
)
|
||||
models = {}
|
||||
for side, constants in MODELS.items():
|
||||
prefix = "SWITCH2_PROBE" if side == "R" else "SWITCH2_PROBE_SECOND"
|
||||
|
|
@ -68,6 +76,7 @@ def model_references(build_dir: Path) -> dict[str, dict[str, Any]]:
|
|||
"version": version.hex(),
|
||||
"factory_extension": factory[64:81].hex(),
|
||||
"mac_wire": address[::-1].hex(),
|
||||
"source_mode": cache.get("SWITCH2_BRIDGE_INPUT", "JOYCON2"),
|
||||
}
|
||||
return models
|
||||
|
||||
|
|
@ -842,8 +851,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"
|
||||
self.result["imu_isolation"] = {
|
||||
"sample_limit_per_side": 512,
|
||||
"policy": "shared_physical_source"
|
||||
if shared_source
|
||||
else "independent_physical_sources",
|
||||
"identical_blocks_seen_on_both_sides": len(shared),
|
||||
"unique_blocks": {
|
||||
side: len(blocks) for side, blocks in self.imu_evidence.items()
|
||||
|
|
@ -853,9 +866,16 @@ class Check:
|
|||
},
|
||||
}
|
||||
for side in SIDES:
|
||||
if len(self.imu_evidence[side] - shared) < 2:
|
||||
evidence = (
|
||||
self.imu_evidence[side]
|
||||
if shared_source
|
||||
else self.imu_evidence[side] - shared
|
||||
)
|
||||
if len(evidence) < 2:
|
||||
self.error(
|
||||
"donor IMU evidence is frozen or duplicated across child devices",
|
||||
"IMU evidence is frozen"
|
||||
if shared_source
|
||||
else "donor IMU evidence is frozen or duplicated across child devices",
|
||||
side,
|
||||
)
|
||||
for side in SIDES:
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@
|
|||
#include <inttypes.h>
|
||||
extern "C" int probe_debug_printf(const char* format, ...);
|
||||
#endif
|
||||
#if SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
#include "dualsense_input.h"
|
||||
#endif
|
||||
#if SWITCH2_BRIDGE_WII_INPUT
|
||||
#include <math.h>
|
||||
#include "input/wii_ir_pointer.h"
|
||||
|
|
@ -21,13 +24,19 @@ extern "C" int probe_debug_printf(const char* format, ...);
|
|||
extern "C" int probe_debug_printf(const char* format, ...);
|
||||
#endif
|
||||
|
||||
#if !SWITCH_PICO_SWITCH2_USB_BRIDGE || !SWITCH_PICO_BLUEPAD32 || \
|
||||
!SWITCH_PICO_ENABLE_BLE || !SWITCH_PICO_SWITCH2_MOUSE_CAPTURE || \
|
||||
#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"
|
||||
#endif
|
||||
#elif !SWITCH_PICO_ENABLE_BLE || !SWITCH_PICO_SWITCH2_MOUSE_CAPTURE || \
|
||||
!SWITCH_PICO_SWITCH2_MOUSE_CAPTURE_NATIVE
|
||||
#error "The controller bridge requires Bluepad32 BLE and native Switch 2 capture"
|
||||
#error "The Joy-Con/Wii bridge requires Bluepad32 BLE and native Switch 2 capture"
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
#if !SWITCH2_BRIDGE_DUALSENSE_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
|
||||
|
|
@ -35,6 +44,7 @@ constexpr uint8_t kSecondSourceAddress[] = {SWITCH2_BRIDGE_SECOND_SOURCE_ADDRESS
|
|||
static_assert(sizeof(kSecondSourceAddress) == 6, "Select the second physical Bluetooth address");
|
||||
#endif
|
||||
constexpr uint32_t kInputDeadlineMs = 500;
|
||||
#endif
|
||||
#if !SWITCH2_PROBE_HUB
|
||||
constexpr uint32_t kFlashCoordinationTimeoutMs = 1000;
|
||||
#endif
|
||||
|
|
@ -46,7 +56,7 @@ bool g_start_attempted;
|
|||
bool g_flash_ready;
|
||||
#if SWITCH2_BRIDGE_WII_INPUT
|
||||
probe_controller_input g_input;
|
||||
#else
|
||||
#elif !SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
probe_controller_input g_inputs[PROBE_CONTROLLER_COUNT];
|
||||
uint32_t g_received_times[PROBE_CONTROLLER_COUNT];
|
||||
#endif
|
||||
|
|
@ -280,7 +290,7 @@ void poll_wii_source(uint32_t now_ms) {
|
|||
motion.gyro_dps[0] = static_cast<float>(g_wii.gyro_q10[1]) / 1024.0f;
|
||||
motion.gyro_dps[1] = -static_cast<float>(g_wii.gyro_q10[2]) / 1024.0f;
|
||||
motion.gyro_dps[2] = -static_cast<float>(g_wii.gyro_q10[0]) / 1024.0f;
|
||||
g_motion.update(now_us, g_wii_generation, motion);
|
||||
g_motion.update(now_us, g_wii_generation, motion, ProbeNativeMotionBias::kEstimateStationary);
|
||||
WiiIrMouseReport optical{};
|
||||
(void)wii_ir_mouse_peek(&optical, 0);
|
||||
// Core 1 may publish during the peek. Read the clock after the snapshot.
|
||||
|
|
@ -372,7 +382,10 @@ extern "C" void probe_controller_input_clock_init(void) {
|
|||
|
||||
extern "C" void probe_controller_input_init(void) {
|
||||
if (g_initialized) return;
|
||||
#if SWITCH2_BRIDGE_WII_INPUT
|
||||
#if SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
bluepad32_input_backend_init();
|
||||
probe_dualsense_input_init();
|
||||
#elif SWITCH2_BRIDGE_WII_INPUT
|
||||
bluepad32_input_backend_init();
|
||||
bluepad32_input_backend_select_wii_source(kSourceAddress);
|
||||
g_screen_configured = wii_ir_pointer_configure_screen(
|
||||
|
|
@ -471,9 +484,18 @@ extern "C" void probe_controller_input_set_native_features(uint8_t features) {
|
|||
}
|
||||
#endif
|
||||
|
||||
#if SWITCH2_BRIDGE_DUALSENSE_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);
|
||||
}
|
||||
#endif
|
||||
|
||||
extern "C" void probe_controller_input_set_native_stream(uint8_t instance, bool enabled) {
|
||||
if (instance >= PROBE_CONTROLLER_COUNT) return;
|
||||
#if SWITCH2_BRIDGE_WII_INPUT
|
||||
#if SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
probe_dualsense_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();
|
||||
g_native_stream = enabled;
|
||||
|
|
@ -486,7 +508,9 @@ 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_WII_INPUT
|
||||
#if SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
return probe_dualsense_input_peek_native_report(instance, now_ms, report);
|
||||
#elif SWITCH2_BRIDGE_WII_INPUT
|
||||
(void)now_ms;
|
||||
return prepare_wii_report(report);
|
||||
#else
|
||||
|
|
@ -496,7 +520,9 @@ 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_WII_INPUT
|
||||
#if SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
return probe_dualsense_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;
|
||||
wii_ir_mouse_commit(g_pending_pointer);
|
||||
|
|
@ -517,7 +543,9 @@ extern "C" bool probe_controller_input_play_sample(uint8_t instance, uint8_t sam
|
|||
if (token != nullptr) *token = 0;
|
||||
return false;
|
||||
}
|
||||
#if SWITCH2_BRIDGE_WII_INPUT
|
||||
#if SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
return bluepad32_input_backend_dualsense_sample_request(instance, sample_id, token);
|
||||
#elif SWITCH2_BRIDGE_WII_INPUT
|
||||
return bluepad32_input_backend_wii_sample_request(sample_id, token);
|
||||
#else
|
||||
return switch2_mouse_capture_request_sample(
|
||||
|
|
@ -527,7 +555,10 @@ 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_WII_INPUT
|
||||
#if SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
(void)now_ms;
|
||||
return bluepad32_input_backend_dualsense_sample_result(instance, token);
|
||||
#elif SWITCH2_BRIDGE_WII_INPUT
|
||||
(void)now_ms;
|
||||
return bluepad32_input_backend_wii_sample_result(token);
|
||||
#else
|
||||
|
|
@ -537,7 +568,9 @@ 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_WII_INPUT
|
||||
#if SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
bluepad32_input_backend_dualsense_sample_cancel(instance);
|
||||
#elif SWITCH2_BRIDGE_WII_INPUT
|
||||
bluepad32_input_backend_wii_sample_cancel();
|
||||
#else
|
||||
switch2_mouse_capture_cancel_sample(instance);
|
||||
|
|
@ -551,7 +584,10 @@ extern "C" void probe_controller_input_poll(uint8_t instance, uint32_t now_ms,
|
|||
*out = {};
|
||||
return;
|
||||
}
|
||||
#if SWITCH2_BRIDGE_WII_INPUT
|
||||
#if SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
probe_dualsense_input_poll(instance, now_ms, out);
|
||||
return;
|
||||
#elif SWITCH2_BRIDGE_WII_INPUT
|
||||
poll_wii_source(now_ms);
|
||||
#else
|
||||
probe_controller_input& g_input = g_inputs[instance];
|
||||
|
|
@ -582,5 +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
|
||||
*out = g_input;
|
||||
#endif
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,9 +47,16 @@ 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
|
||||
// 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]);
|
||||
#endif
|
||||
// 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;
|
||||
// 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);
|
||||
// Copy one63-byte payload without report ID. Returns a boot-unique token, or0
|
||||
|
|
@ -61,8 +68,9 @@ uint32_t probe_controller_input_peek_native_report(uint8_t instance, uint32_t no
|
|||
bool probe_controller_input_commit_native_report(uint8_t instance, uint32_t serial);
|
||||
// Built-in vibration samples only; raw HD-rumble output is not forwarded.
|
||||
// A nonzero token means queued, not completed. Result:0 pending,1 completion,
|
||||
// -1 failed/stale. Joy-Con completion is its application ACK; Wii completion is
|
||||
// actual bounded rumble-driver dispatch (not an HD-waveform fidelity claim).
|
||||
// -1 failed/stale. Joy-Con completion is its application ACK; Wii/DualSense
|
||||
// completion is actual bounded rumble-driver dispatch, not a source application
|
||||
// ACK or an HD-waveform fidelity claim.
|
||||
// Reset cancels the request, never stored pairing.
|
||||
bool probe_controller_input_play_sample(uint8_t instance, uint8_t sample_id, uint64_t* token);
|
||||
int probe_controller_input_sample_result(uint8_t instance, uint64_t token, uint32_t now_ms);
|
||||
|
|
|
|||
330
tools/switch2_usb_probe/dualsense_input.cpp
Normal file
330
tools/switch2_usb_probe/dualsense_input.cpp
Normal file
|
|
@ -0,0 +1,330 @@
|
|||
#include "dualsense_input.h"
|
||||
|
||||
#if SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
#include <limits.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "input/bluepad32_input_backend.h"
|
||||
#include "model.h"
|
||||
#include "native_imu.h"
|
||||
#include "pico/time.h"
|
||||
#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"
|
||||
#endif
|
||||
static_assert(PROBE_CONTROLLER_COUNT == 2);
|
||||
extern "C" int probe_debug_printf(const char* format, ...);
|
||||
#ifndef SWITCH2_BRIDGE_IMU_TARGET_MASK
|
||||
#define SWITCH2_BRIDGE_IMU_TARGET_MASK 3
|
||||
#endif
|
||||
static_assert(SWITCH2_BRIDGE_IMU_TARGET_MASK >= 1 && SWITCH2_BRIDGE_IMU_TARGET_MASK <= 3);
|
||||
|
||||
namespace {
|
||||
constexpr uint32_t kInputDeadlineUs = 500000;
|
||||
constexpr uint32_t kSensorDeadlineUs = 150000;
|
||||
constexpr uint32_t kOutputDeadlineUs = 100000;
|
||||
#if !SWITCH2_BRIDGE_SOURCE_AUTO
|
||||
constexpr uint8_t kSourceAddress[] = {SWITCH2_BRIDGE_SOURCE_ADDRESS_BYTES};
|
||||
static_assert(sizeof(kSourceAddress) == 6);
|
||||
#endif
|
||||
|
||||
struct Child {
|
||||
bool enabled = false;
|
||||
bool calibrated = false;
|
||||
uint16_t center[2]{};
|
||||
uint16_t positive[2]{};
|
||||
uint16_t negative[2]{};
|
||||
probe_controller_input input{};
|
||||
uint8_t counter = 0;
|
||||
uint32_t pending_token = 0;
|
||||
uint32_t pending_us = 0;
|
||||
uint32_t pending_ticks = 0;
|
||||
uint32_t pending_motion_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_ticks = 0;
|
||||
};
|
||||
Child g_children[PROBE_CONTROLLER_COUNT];
|
||||
Bluepad32DualSenseBridgeSnapshot g_source;
|
||||
ControllerProfileTransformResult g_mapped;
|
||||
ProbeNativeMotion g_motion;
|
||||
bool g_active;
|
||||
bool g_evaluated;
|
||||
uint32_t g_evaluated_ms;
|
||||
uint32_t g_report_token;
|
||||
int g_sensor_status = -1;
|
||||
bool g_clock_started;
|
||||
uint32_t g_clock_us;
|
||||
uint32_t g_clock_ticks;
|
||||
uint32_t g_clock_fraction;
|
||||
|
||||
void advance_clock(uint32_t now_us) {
|
||||
if (!g_clock_started) {
|
||||
g_clock_started = true;
|
||||
g_clock_us = now_us;
|
||||
return;
|
||||
}
|
||||
const uint64_t scaled = static_cast<uint64_t>(now_us - g_clock_us) * 960u + g_clock_fraction;
|
||||
g_clock_us = now_us;
|
||||
g_clock_ticks += static_cast<uint32_t>(scaled / 1000000u);
|
||||
g_clock_fraction = static_cast<uint32_t>(scaled % 1000000u);
|
||||
}
|
||||
|
||||
void discard_output(Child& child) {
|
||||
child.pending_token = 0;
|
||||
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;
|
||||
}
|
||||
|
||||
void unpack_stick_pair(const uint8_t* bytes, uint16_t pair[2]) {
|
||||
pair[0] = bytes[0] | (static_cast<uint16_t>(bytes[1] & 15) << 8);
|
||||
pair[1] = (bytes[1] >> 4) | (static_cast<uint16_t>(bytes[2]) << 4);
|
||||
}
|
||||
|
||||
uint16_t calibrated_axis(const Child& child, int16_t value, unsigned axis, bool invert) {
|
||||
// Same signed endpoint/rounding convention as the native Wii adapter.
|
||||
const int32_t input = value;
|
||||
const bool input_positive = input >= 0;
|
||||
const bool output_positive = input_positive != invert;
|
||||
const int32_t magnitude = input_positive ? input : -input;
|
||||
const int32_t denominator = input_positive ? INT16_MAX : 32768;
|
||||
const int32_t travel = output_positive ? child.positive[axis] : child.negative[axis];
|
||||
const int32_t displacement = (magnitude * travel + denominator / 2) / denominator;
|
||||
return static_cast<uint16_t>(child.center[axis] +
|
||||
(output_positive ? displacement : -displacement));
|
||||
}
|
||||
|
||||
void pack_controls(uint8_t instance) {
|
||||
Child& child = g_children[instance];
|
||||
child.input = {};
|
||||
child.input.serial = g_source.state_generation;
|
||||
if (!g_active || !child.calibrated) return;
|
||||
child.input.active = true;
|
||||
child.input.native_status = 0x30; // Host feature status is gated per model in main.
|
||||
child.input.mouse_surface = 0xff; // No optical sensor, clicks, or invented movement.
|
||||
const ControllerState& state = g_mapped.state;
|
||||
const bool left = probe_model_is_left(instance);
|
||||
if (left) {
|
||||
child.input.buttons[0] = static_cast<uint8_t>(
|
||||
(state.dpad_down ? 0x01 : 0) | (state.dpad_right ? 0x02 : 0) |
|
||||
(state.dpad_left ? 0x04 : 0) | (state.dpad_up ? 0x08 : 0) |
|
||||
(state.button_left_shoulder ? 0x10 : 0) |
|
||||
(state.left_trigger != 0 && state.left_trigger >= g_mapped.left_trigger_digital_threshold ? 0x20 : 0) |
|
||||
(state.button_select ? 0x40 : 0) | (state.button_left_stick ? 0x80 : 0));
|
||||
child.input.buttons[1] = static_cast<uint8_t>(
|
||||
(state.button_capture ? 0x01 : 0) |
|
||||
((state.extra_buttons & (1u << 3)) ? 0x80 : 0) |
|
||||
((state.extra_buttons & (1u << 4)) ? 0x40 : 0));
|
||||
} else {
|
||||
child.input.buttons[0] = static_cast<uint8_t>(
|
||||
(state.button_south ? 0x01 : 0) | (state.button_east ? 0x02 : 0) |
|
||||
(state.button_west ? 0x04 : 0) | (state.button_north ? 0x08 : 0) |
|
||||
(state.button_right_shoulder ? 0x10 : 0) |
|
||||
(state.right_trigger != 0 && state.right_trigger >= g_mapped.right_trigger_digital_threshold ? 0x20 : 0) |
|
||||
(state.button_start ? 0x40 : 0) | (state.button_right_stick ? 0x80 : 0));
|
||||
child.input.buttons[1] = static_cast<uint8_t>(
|
||||
(state.button_system ? 0x01 : 0) | ((state.extra_buttons & 1) ? 0x10 : 0) |
|
||||
((state.extra_buttons & (1u << 5)) ? 0x80 : 0) |
|
||||
((state.extra_buttons & (1u << 6)) ? 0x40 : 0));
|
||||
}
|
||||
const uint16_t x = calibrated_axis(child, left ? state.left_stick_x : state.right_stick_x, 0, false);
|
||||
const uint16_t y = calibrated_axis(child, left ? state.left_stick_y : state.right_stick_y, 1, true);
|
||||
child.input.stick[0] = static_cast<uint8_t>(x);
|
||||
child.input.stick[1] = static_cast<uint8_t>((x >> 8) | (y << 4));
|
||||
child.input.stick[2] = static_cast<uint8_t>(y >> 4);
|
||||
}
|
||||
|
||||
void lose_source(uint32_t now_ms) {
|
||||
if (g_active) {
|
||||
Bluepad32SlotSnapshot inactive{};
|
||||
(void)controller_profile_runtime_transform(g_source.slot, inactive, now_ms, AdapterUsbMode::kSwitch);
|
||||
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);
|
||||
}
|
||||
g_sensor_status = -1;
|
||||
}
|
||||
g_active = false;
|
||||
for (Child& child : g_children) child.input = {};
|
||||
}
|
||||
|
||||
void refresh(uint32_t now_ms) {
|
||||
Bluepad32DualSenseBridgeSnapshot source;
|
||||
bluepad32_input_backend_dualsense_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);
|
||||
if (!source.controller.active || source.slot >= BLUEPAD32_INPUT_BACKEND_SLOT_COUNT ||
|
||||
now_us - source.received_us >= kInputDeadlineUs) {
|
||||
lose_source(now_ms);
|
||||
g_source = source;
|
||||
g_evaluated = false;
|
||||
return;
|
||||
}
|
||||
const bool changed_connection = !g_active || source.slot != g_source.slot ||
|
||||
source.controller.connection_generation != g_source.controller.connection_generation;
|
||||
// Both polls and both peeks in a paired output round share one profile and
|
||||
// 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;
|
||||
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);
|
||||
}
|
||||
g_source = source;
|
||||
g_active = true;
|
||||
g_evaluated = true;
|
||||
g_evaluated_ms = now_ms;
|
||||
g_mapped = controller_profile_runtime_transform(source.slot, source.controller, now_ms, AdapterUsbMode::kSwitch);
|
||||
ControllerProfileRuntimeProfileChangeEvent feedback{};
|
||||
if (controller_profile_runtime_take_initial_profile_indication(source.slot, &feedback) ||
|
||||
controller_profile_runtime_take_profile_change(source.slot, &feedback)) {
|
||||
bluepad32_input_backend_queue_profile_feedback(source.slot, feedback.connection_generation,
|
||||
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;
|
||||
// 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.
|
||||
sample.accel_g[0] = static_cast<float>(source.accel_q13[0]) / 8192.0f;
|
||||
sample.accel_g[1] = -static_cast<float>(source.accel_q13[2]) / 8192.0f;
|
||||
sample.accel_g[2] = static_cast<float>(source.accel_q13[1]) / 8192.0f;
|
||||
sample.gyro_dps[0] = static_cast<float>(source.gyro_q10[0]) / 1024.0f;
|
||||
sample.gyro_dps[1] = -static_cast<float>(source.gyro_q10[2]) / 1024.0f;
|
||||
sample.gyro_dps[2] = static_cast<float>(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;
|
||||
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");
|
||||
}
|
||||
for (uint8_t i = 0; i < PROBE_CONTROLLER_COUNT; ++i) {
|
||||
// Latest-only: a blocked endpoint never queues obsolete controls/IMU.
|
||||
g_children[i].pending_token = 0;
|
||||
pack_controls(i);
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void probe_dualsense_input_init() {
|
||||
#if SWITCH2_BRIDGE_SOURCE_AUTO
|
||||
bluepad32_input_backend_select_dualsense_source(nullptr);
|
||||
#else
|
||||
bluepad32_input_backend_select_dualsense_source(kSourceAddress);
|
||||
#endif
|
||||
}
|
||||
|
||||
void probe_dualsense_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;
|
||||
discard_output(child);
|
||||
if (calibration) {
|
||||
unpack_stick_pair(calibration, child.center);
|
||||
unpack_stick_pair(calibration + 3, child.positive);
|
||||
unpack_stick_pair(calibration + 6, child.negative);
|
||||
child.calibrated = true;
|
||||
for (unsigned axis = 0; axis < 2; ++axis) {
|
||||
if (!child.positive[axis] || !child.negative[axis] ||
|
||||
child.center[axis] + child.positive[axis] > 4095 ||
|
||||
child.negative[axis] > child.center[axis]) child.calibrated = false;
|
||||
}
|
||||
}
|
||||
pack_controls(instance);
|
||||
}
|
||||
|
||||
void probe_dualsense_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);
|
||||
}
|
||||
|
||||
void probe_dualsense_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]) {
|
||||
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);
|
||||
if (child.pending_token && (now_us - child.pending_us >= kOutputDeadlineUs ||
|
||||
child.pending_motion != motion_ready)) child.pending_token = 0;
|
||||
if (!child.pending_token) {
|
||||
if (g_report_token == UINT32_MAX) return 0; // Boot-unique, including across children/resets.
|
||||
memset(child.pending_report, 0, sizeof(child.pending_report));
|
||||
child.pending_report[0] = child.counter;
|
||||
// Bluepad32 zero denotes unknown; do not manufacture a full battery.
|
||||
const unsigned battery_level = (static_cast<unsigned>(g_source.battery) * 9u + 127u) / 255u;
|
||||
child.pending_report[1] = static_cast<uint8_t>(battery_level << 2);
|
||||
memcpy(child.pending_report + 2, child.input.buttons, sizeof(child.input.buttons));
|
||||
child.pending_report[4] = 7;
|
||||
memcpy(child.pending_report + 5, child.input.stick, sizeof(child.input.stick));
|
||||
child.pending_report[8] = child.input.native_status;
|
||||
child.pending_report[13] = 0xff;
|
||||
child.pending_ticks = g_clock_ticks;
|
||||
const uint32_t elapsed = child.have_committed_motion ? child.pending_ticks - child.committed_ticks : 1;
|
||||
const uint16_t wire_elapsed = static_cast<uint16_t>(elapsed <= 0xfff ? elapsed : 1);
|
||||
child.pending_motion = motion_ready && probe_native_imu_pack(
|
||||
g_motion.quaternion(), g_motion.acceleration(), static_cast<uint16_t>(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_us = now_us;
|
||||
child.pending_token = ++g_report_token;
|
||||
}
|
||||
memcpy(report, child.pending_report, sizeof(child.pending_report));
|
||||
return child.pending_token;
|
||||
}
|
||||
|
||||
bool probe_dualsense_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);
|
||||
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 ||
|
||||
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_token = 0;
|
||||
if (child.pending_motion) {
|
||||
child.have_committed_motion = true;
|
||||
child.committed_motion_sequence = child.pending_motion_sequence;
|
||||
child.committed_ticks = child.pending_ticks;
|
||||
}
|
||||
++child.counter;
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
13
tools/switch2_usb_probe/dualsense_input.h
Normal file
13
tools/switch2_usb_probe/dualsense_input.h
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
#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
|
||||
|
|
@ -319,6 +319,8 @@ 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
|
||||
probe_controller_input_set_full_stick_calibration(instance, stick_calibration);
|
||||
#endif
|
||||
protocol->read_memory = read_memory;
|
||||
#endif
|
||||
|
|
@ -485,6 +487,9 @@ 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",
|
||||
instance, reply->deferred_token);
|
||||
#else
|
||||
probe_debug_printf("[PROBE] Source sample ACK itf=%u token=%" PRIu64 "\n",
|
||||
instance, reply->deferred_token);
|
||||
|
|
@ -523,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
|
||||
#if SWITCH2_BRIDGE_WII_INPUT || SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
input[8] = (uint8_t)(0x30 | ((protocol->enabled_features & 0x20) ? 8 : 0));
|
||||
#endif
|
||||
probe_protocol_gate_native_report(protocol, input);
|
||||
|
|
|
|||
|
|
@ -319,11 +319,12 @@ void ProbeNativeMotion::observe_optical_heading(uint32_t now_us, uint32_t refere
|
|||
}
|
||||
|
||||
void ProbeNativeMotion::update(uint32_t now_us, uint32_t connection_generation,
|
||||
const ProbeNativeMotionSample& sample) {
|
||||
if (!have_generation_ || connection_generation != connection_generation_) {
|
||||
const ProbeNativeMotionSample& sample, ProbeNativeMotionBias bias_mode) {
|
||||
if (!have_generation_ || connection_generation != connection_generation_ || bias_mode != bias_mode_) {
|
||||
reset();
|
||||
have_generation_ = true;
|
||||
connection_generation_ = connection_generation;
|
||||
bias_mode_ = bias_mode;
|
||||
}
|
||||
const uint32_t elapsed_us = have_update_ ? now_us - update_us_ : 0;
|
||||
update_us_ = now_us;
|
||||
|
|
@ -385,6 +386,14 @@ void ProbeNativeMotion::update(uint32_t now_us, uint32_t connection_generation,
|
|||
return;
|
||||
}
|
||||
|
||||
if (bias_mode_ == ProbeNativeMotionBias::kAlreadyCalibrated) {
|
||||
// Trust only the caller's validated calibrated samples, not an estimated
|
||||
// stationary bias. The first acceleration fixes a relative gravity frame.
|
||||
for (unsigned i = 0; i < 3; ++i) mean_accel_[i] = acceleration_[i];
|
||||
ready_ = initialize_orientation();
|
||||
return;
|
||||
}
|
||||
|
||||
const float acceleration_norm_squared = squared_norm(acceleration_);
|
||||
if (acceleration_norm_squared < 0.85f * 0.85f ||
|
||||
acceleration_norm_squared > 1.15f * 1.15f) {
|
||||
|
|
|
|||
|
|
@ -29,18 +29,25 @@ struct ProbeNativeMotionSample {
|
|||
float gyro_dps[3]{};
|
||||
};
|
||||
|
||||
enum class ProbeNativeMotionBias : uint8_t {
|
||||
kAlreadyCalibrated,
|
||||
kEstimateStationary,
|
||||
};
|
||||
|
||||
// Core 0 only. Call update even when sensors are unavailable, and consult ready
|
||||
// before using the orientation. Sequence identities, not polling, admit samples;
|
||||
// repeated sequences cannot refresh timestamps or contribute to calibration.
|
||||
// Startup requires the user to rest the controller: constant rotation about
|
||||
// gravity is indistinguishable from an unknown gyro bias without another sensor.
|
||||
// Fresh near-1g acceleration corrects tilt drift after startup. Heading remains
|
||||
// gyro-derived unless a reliable optical heading observation is supplied.
|
||||
// Already-calibrated sources initialize from the first usable fresh sensor pair.
|
||||
// Wii explicitly requests stationary residual-bias estimation: constant rotation
|
||||
// about gravity is indistinguishable from an unknown bias without another sensor.
|
||||
// Fresh near-1g acceleration corrects tilt drift. Heading remains gyro-derived
|
||||
// unless a reliable optical heading observation is supplied.
|
||||
class ProbeNativeMotion {
|
||||
public:
|
||||
void reset();
|
||||
void update(uint32_t now_us, uint32_t connection_generation,
|
||||
const ProbeNativeMotionSample& sample);
|
||||
const ProbeNativeMotionSample& sample,
|
||||
ProbeNativeMotionBias bias_mode = ProbeNativeMotionBias::kAlreadyCalibrated);
|
||||
// Call after update, using a full observed sensor-bar pair (never inferred).
|
||||
// Positive optical yaw means aim-right, the negative reference-world turn.
|
||||
// The first sample in each optical generation anchors the current heading;
|
||||
|
|
@ -62,6 +69,7 @@ private:
|
|||
bool normalize_orientation();
|
||||
|
||||
bool have_generation_ = false;
|
||||
ProbeNativeMotionBias bias_mode_ = ProbeNativeMotionBias::kAlreadyCalibrated;
|
||||
bool have_update_ = false;
|
||||
bool seen_accel_sequence_ = false;
|
||||
bool seen_gyro_sequence_ = false;
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
message(FATAL_ERROR "Composite Joy-Con 2 requires SWITCH_PICO_SWITCH2_USB_BRIDGE=ON and SWITCH2_BRIDGE_INPUT=JOYCON2")
|
||||
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")
|
||||
endif()
|
||||
set(probe_composite 0)
|
||||
if(SWITCH2_PROBE_COMPOSITE)
|
||||
|
|
@ -51,9 +51,13 @@ set(SWITCH2_BRIDGE_SOURCE_ADDRESS "" CACHE STRING
|
|||
"Primary physical Bluetooth source address (xx:xx:xx:xx:xx:xx)")
|
||||
set(SWITCH2_BRIDGE_SECOND_SOURCE_ADDRESS "" CACHE STRING
|
||||
"Secondary left physical Bluetooth source address (xx:xx:xx:xx:xx:xx)")
|
||||
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_PROBE_COMPOSITE OR SWITCH2_PROBE_HUB)
|
||||
if(SWITCH2_BRIDGE_DUALSENSE_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)
|
||||
list(APPEND probe_source_fields SWITCH2_BRIDGE_SECOND_SOURCE_ADDRESS)
|
||||
endif()
|
||||
set(probe_source_addresses "")
|
||||
|
|
@ -74,6 +78,7 @@ if(SWITCH_PICO_SWITCH2_USB_BRIDGE OR SWITCH2_PROBE_COMPOSITE)
|
|||
string(PREPEND ${field}_BYTES "0x")
|
||||
endforeach()
|
||||
endif()
|
||||
add_compile_definitions(SWITCH2_BRIDGE_SOURCE_AUTO=$<BOOL:${SWITCH2_BRIDGE_SOURCE_AUTO}>)
|
||||
|
||||
function(switch2_usb_probe_configure target)
|
||||
set(probe_sources
|
||||
|
|
@ -362,8 +367,14 @@ function(switch2_usb_probe_configure target)
|
|||
else()
|
||||
pico_set_program_name(${target} "Switch 2 USB initialization capture")
|
||||
endif()
|
||||
if(SWITCH2_PROBE_HUB)
|
||||
pico_set_program_version(${target} "0.66-native-hub-input")
|
||||
if(SWITCH2_PROBE_HUB AND SWITCH2_BRIDGE_DUALSENSE_INPUT)
|
||||
if(SWITCH2_PROBE_TRACE_NATIVE_INPUT)
|
||||
pico_set_program_version(${target} "0.69-native-hub-imu-trace")
|
||||
else()
|
||||
pico_set_program_version(${target} "0.69-native-hub-imu")
|
||||
endif()
|
||||
elseif(SWITCH2_PROBE_HUB)
|
||||
pico_set_program_version(${target} "0.67-native-hub-latency")
|
||||
elseif(SWITCH2_PROBE_JOIN_CHORD_GATE)
|
||||
if(SWITCH2_PROBE_TRACE_NATIVE_INPUT)
|
||||
pico_set_program_version(${target} "0.37-pair-chord-trace")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue