Support Bluepad32 gamepads as native Joy-Con pair sources
This commit is contained in:
parent
ab70536fdf
commit
a17ca603aa
33 changed files with 2529 additions and 851 deletions
|
|
@ -177,9 +177,11 @@ if(SWITCH_PICO_SWITCH2_MEMORY_CAPTURE)
|
|||
endif()
|
||||
set(SWITCH2_BRIDGE_WII_INPUT OFF)
|
||||
set(SWITCH2_BRIDGE_DUALSENSE_INPUT OFF)
|
||||
set(SWITCH2_BRIDGE_GAMEPAD_INPUT OFF)
|
||||
set(SWITCH2_BRIDGE_FULL_INPUT OFF)
|
||||
if(SWITCH_PICO_SWITCH2_USB_BRIDGE)
|
||||
set(SWITCH2_BRIDGE_INPUT "JOYCON2" CACHE STRING "Native bridge source: JOYCON2, WII or DUALSENSE")
|
||||
set_property(CACHE SWITCH2_BRIDGE_INPUT PROPERTY STRINGS JOYCON2 WII DUALSENSE)
|
||||
set(SWITCH2_BRIDGE_INPUT "JOYCON2" CACHE STRING "Native source: JOYCON2, WII, DUALSENSE or GAMEPAD")
|
||||
set_property(CACHE SWITCH2_BRIDGE_INPUT PROPERTY STRINGS JOYCON2 WII DUALSENSE GAMEPAD)
|
||||
set(SWITCH2_BRIDGE_IMU_TARGET "BOTH" CACHE STRING "Translated native-pair IMU target: LEFT, RIGHT or BOTH")
|
||||
set_property(CACHE SWITCH2_BRIDGE_IMU_TARGET PROPERTY STRINGS LEFT RIGHT BOTH)
|
||||
if(SWITCH2_BRIDGE_IMU_TARGET STREQUAL "RIGHT")
|
||||
|
|
@ -195,12 +197,22 @@ if(SWITCH_PICO_SWITCH2_USB_BRIDGE)
|
|||
if(NOT SWITCH_PICO_INPUT_BACKEND STREQUAL "BLUEPAD32")
|
||||
message(FATAL_ERROR "Switch 2 USB bridge requires BLUEPAD32")
|
||||
endif()
|
||||
if(SWITCH2_BRIDGE_INPUT STREQUAL "DUALSENSE")
|
||||
if(NOT SWITCH2_PROBE_HUB OR NOT SWITCH_PICO_ENABLE_CLASSIC)
|
||||
message(FATAL_ERROR "DualSense native R/L bridge requires HUB and Classic Bluetooth")
|
||||
if(SWITCH2_BRIDGE_INPUT STREQUAL "DUALSENSE" OR SWITCH2_BRIDGE_INPUT STREQUAL "GAMEPAD")
|
||||
if(NOT SWITCH2_PROBE_HUB)
|
||||
message(FATAL_ERROR "Translated full-controller input requires the native R/L HUB")
|
||||
endif()
|
||||
set(SWITCH2_BRIDGE_FULL_INPUT ON)
|
||||
add_compile_definitions(SWITCH2_BRIDGE_FULL_INPUT=1)
|
||||
if(SWITCH2_BRIDGE_INPUT STREQUAL "DUALSENSE")
|
||||
if(NOT SWITCH_PICO_ENABLE_CLASSIC)
|
||||
message(FATAL_ERROR "DualSense input requires Classic Bluetooth")
|
||||
endif()
|
||||
set(SWITCH2_BRIDGE_DUALSENSE_INPUT ON)
|
||||
add_compile_definitions(SWITCH2_BRIDGE_DUALSENSE_INPUT=1)
|
||||
else()
|
||||
set(SWITCH2_BRIDGE_GAMEPAD_INPUT ON)
|
||||
add_compile_definitions(SWITCH2_BRIDGE_GAMEPAD_INPUT=1)
|
||||
endif()
|
||||
set(SWITCH2_BRIDGE_DUALSENSE_INPUT ON)
|
||||
add_compile_definitions(SWITCH2_BRIDGE_DUALSENSE_INPUT=1)
|
||||
elseif(NOT SWITCH_PICO_ENABLE_BLE OR NOT SWITCH_PICO_SWITCH2_MOUSE_CAPTURE
|
||||
OR NOT SWITCH_PICO_SWITCH2_MOUSE_CAPTURE_NATIVE)
|
||||
message(FATAL_ERROR "Joy-Con/Wii USB bridge requires BLE and native Joy-Con capture")
|
||||
|
|
@ -214,10 +226,10 @@ if(SWITCH_PICO_SWITCH2_USB_BRIDGE)
|
|||
endif()
|
||||
set(SWITCH2_BRIDGE_WII_INPUT ON)
|
||||
add_compile_definitions(SWITCH2_BRIDGE_WII_INPUT=1 SWITCH_PICO_WII_IR=1)
|
||||
elseif(NOT SWITCH2_BRIDGE_INPUT STREQUAL "JOYCON2" AND NOT SWITCH2_BRIDGE_DUALSENSE_INPUT)
|
||||
message(FATAL_ERROR "SWITCH2_BRIDGE_INPUT must be JOYCON2, WII or DUALSENSE")
|
||||
elseif(NOT SWITCH2_BRIDGE_INPUT STREQUAL "JOYCON2" AND NOT SWITCH2_BRIDGE_FULL_INPUT)
|
||||
message(FATAL_ERROR "SWITCH2_BRIDGE_INPUT must be JOYCON2, WII, DUALSENSE or GAMEPAD")
|
||||
endif()
|
||||
if(NOT SWITCH2_BRIDGE_DUALSENSE_INPUT AND NOT SWITCH2_BRIDGE_IMU_TARGET STREQUAL "BOTH")
|
||||
if(NOT SWITCH2_BRIDGE_FULL_INPUT AND NOT SWITCH2_BRIDGE_IMU_TARGET STREQUAL "BOTH")
|
||||
message(FATAL_ERROR "LEFT/RIGHT IMU routing requires a translated full-controller native pair")
|
||||
endif()
|
||||
add_compile_definitions(SWITCH_PICO_SWITCH2_USB_BRIDGE=1)
|
||||
|
|
@ -382,6 +394,10 @@ if(SWITCH_PICO_INPUT_BACKEND STREQUAL "BLUEPAD32")
|
|||
${CMAKE_CURRENT_LIST_DIR}/bluepad32_config/parser/uni_hid_parser_switch2.c
|
||||
${CMAKE_CURRENT_LIST_DIR}/bluepad32_config/parser/uni_switch2_pairing.c
|
||||
${CMAKE_CURRENT_LIST_DIR}/bluepad32_config/parser/uni_switch2_haptics.c)
|
||||
if(SWITCH2_BRIDGE_FULL_INPUT)
|
||||
target_sources(bluepad32 PRIVATE
|
||||
${CMAKE_CURRENT_LIST_DIR}/bluepad32_config/parser/uni_hid_parser_native_motion.c)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Firmware sources live under one include root and are grouped by responsibility.
|
||||
|
|
@ -423,16 +439,16 @@ if(SWITCH_PICO_SWITCH2_USB_BRIDGE)
|
|||
endif()
|
||||
target_include_directories(switch-pico PRIVATE ${CMAKE_CURRENT_LIST_DIR}/external)
|
||||
target_sources(switch-pico PRIVATE ${SWITCH2_USB_PROBE_DIR}/native_imu.cpp ${LIBOGC_IR_GENERATED})
|
||||
elseif(SWITCH2_BRIDGE_DUALSENSE_INPUT)
|
||||
elseif(SWITCH2_BRIDGE_FULL_INPUT)
|
||||
target_sources(switch-pico PRIVATE
|
||||
${SWITCH2_USB_PROBE_DIR}/dualsense_input.cpp
|
||||
${SWITCH2_USB_PROBE_DIR}/native_gamepad_input.cpp
|
||||
${SWITCH2_USB_PROBE_DIR}/native_imu.cpp)
|
||||
endif()
|
||||
if(NOT SWITCH2_BRIDGE_SOURCE_AUTO)
|
||||
target_compile_definitions(switch-pico PRIVATE
|
||||
SWITCH2_BRIDGE_SOURCE_ADDRESS_BYTES=${SWITCH2_BRIDGE_SOURCE_ADDRESS_BYTES})
|
||||
endif()
|
||||
if((SWITCH2_PROBE_COMPOSITE OR SWITCH2_PROBE_HUB) AND NOT SWITCH2_BRIDGE_DUALSENSE_INPUT)
|
||||
if((SWITCH2_PROBE_COMPOSITE OR SWITCH2_PROBE_HUB) AND NOT SWITCH2_BRIDGE_FULL_INPUT)
|
||||
target_compile_definitions(switch-pico PRIVATE
|
||||
SWITCH2_BRIDGE_SECOND_SOURCE_ADDRESS_BYTES=${SWITCH2_BRIDGE_SECOND_SOURCE_ADDRESS_BYTES})
|
||||
endif()
|
||||
|
|
|
|||
44
README.md
44
README.md
|
|
@ -879,6 +879,50 @@ the checker below. Its configured shared-source policy permits identical IMU
|
|||
blocks across the halves while retaining per-child identity, report-ID,
|
||||
fresh-counter and control/bulk isolation checks.
|
||||
|
||||
**Any supported gamepad (0.70):** `SWITCH2_BRIDGE_INPUT=GAMEPAD` uses the same
|
||||
native R/L hub and private identity/calibration captures, but accepts the normal
|
||||
Bluepad32 gamepad families instead of filtering for a DualSense. Use a separate
|
||||
private hub build with `SWITCH_PICO_BLUETOOTH_MODE=MIXED` to enable both Classic
|
||||
and BLE controllers, and disable both `SWITCH_PICO_SWITCH2_MOUSE_CAPTURE` and
|
||||
`SWITCH_PICO_SWITCH2_MOUSE_CAPTURE_NATIVE`. This is one logical controller
|
||||
feeding one virtual R/L pair, not additional players. An empty source address
|
||||
requires one uniquely eligible logical controller; multiple eligible sources
|
||||
fail closed. An explicit address selects that controller (either member of an
|
||||
existing Switch2 Joy-Con pair). Ordinary AIO pairing, identity, layout and
|
||||
profile behavior is retained; original Switch Joy-Con grouping is not added.
|
||||
|
||||
- Buttons, sticks and profiles work independently of motion capability.
|
||||
- Calibrated motion providers cover DS4, DS5/Edge, Switch/Joy-Con-compatible
|
||||
parsers, Switch2 Pro/Joy-Con, PS Move, and Wii/MotionPlus. Motion requires actual
|
||||
supported, calibrated and fresh acceleration **and** gyro samples. A pad with
|
||||
absent/invalid sensors remains usable for controls; no IMU is invented.
|
||||
- Sensor counters advance at parser ingress, not when polled or when buttons
|
||||
arrive. A paired left Joy-Con cannot refresh the right-owned sensor stream.
|
||||
Wii acceleration cannot refresh a stalled MotionPlus gyro stream.
|
||||
- `SWITCH2_BRIDGE_IMU_TARGET=LEFT|RIGHT|BOTH` also applies to `GAMEPAD`; Wii alone
|
||||
estimates residual bias while stationary. Factory calibration already runs;
|
||||
caching residual Wii bias across boots without revalidation is not implemented.
|
||||
- Native cue requests use each source driver's bounded compatibility vibration.
|
||||
Mono drivers combine the two logical contributions; paired Switch2 Joy-Cons
|
||||
target their actual halves. This does not promise stereo, HD-waveform fidelity
|
||||
or physical actuator onset. Completion means driver dispatch (accepted L2CAP
|
||||
submission for DS5), not a remote application ACK. Missing rumble capability
|
||||
fails the request rather than claiming a motor response.
|
||||
- The dedicated `WII` source remains the IR/native-mouse path. `GAMEPAD` does not
|
||||
synthesize mouse movement or rail buttons.
|
||||
|
||||
The private `build-switch2-native-gamepad` image uses mixed Bluetooth and the
|
||||
unchanged stock USB socket. Software regressions cover real parser calibration,
|
||||
report integrity/freshness, source selection, split/reset/backpressure, Wii-only
|
||||
settling and cue lifetimes. DualSense, generic, existing Joy-Con/Wii, and ordinary
|
||||
AIO mixed/BLE/Classic firmware builds pass. The new generic image has not been
|
||||
flashed or physically qualified across these controller families.
|
||||
|
||||
For sensorless hardware, the checker supports `--input-only`: press real buttons
|
||||
and keep changing controls on both halves during the run. Neutral fallback
|
||||
alone cannot qualify. The result explicitly records that IMU was not required;
|
||||
omit this option to retain the strict dual-IMU check.
|
||||
|
||||
With the existing private build configured, qualify on a PC using:
|
||||
|
||||
```sh
|
||||
|
|
|
|||
|
|
@ -209,6 +209,15 @@ static inline bool uni_psmove_normalize_imu(
|
|||
|
||||
const int32_t gyro_bias_value = uni_psmove_read_calibration_value(
|
||||
calibration->data, model, gyro_bias[axis]);
|
||||
#if SWITCH2_BRIDGE_FULL_INPUT
|
||||
// Complete feature blocks alone do not make erased/degenerate factory
|
||||
// extrema calibrated. Never label the helper's zero-span fallback IMU.
|
||||
if (accel_low_value >= accel_high_value ||
|
||||
uni_psmove_read_calibration_value(calibration->data, model, gyro_high[axis]) <= gyro_bias_value ||
|
||||
(model == UNI_PSMOVE_IMU_MODEL_ZCM2 &&
|
||||
uni_psmove_read_calibration_value(calibration->data, model, zcm2_gyro_low[axis]) >= gyro_bias_value))
|
||||
return false;
|
||||
#endif
|
||||
const int32_t gyro_raw =
|
||||
(uni_psmove_decode_value(model, gyro_first[axis]) +
|
||||
uni_psmove_decode_value(model, gyro_second[axis])) /
|
||||
|
|
|
|||
131
bluepad32_config/parser/uni_hid_parser_native_motion.c
Normal file
131
bluepad32_config/parser/uni_hid_parser_native_motion.c
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include "parser/uni_hid_parser_native_motion.h"
|
||||
|
||||
#if SWITCH2_BRIDGE_FULL_INPUT
|
||||
#include <string.h>
|
||||
|
||||
#include "parser/uni_hid_parser_wii.h"
|
||||
#include "sdkconfig.h"
|
||||
#include "uni_hid_device.h"
|
||||
|
||||
// Parser data is already close to its fixed capacity on some families. Keep
|
||||
// only native-bridge provenance here, not in every normal AIO parser instance.
|
||||
typedef struct {
|
||||
uni_hid_device_t* device;
|
||||
void (*setup)(uni_hid_device_t*);
|
||||
uint8_t address[6];
|
||||
uni_native_motion_snapshot_t sample;
|
||||
uint32_t timestamp;
|
||||
bool have_timestamp;
|
||||
} native_motion_t;
|
||||
static native_motion_t providers[CONFIG_BLUEPAD32_MAX_DEVICES];
|
||||
static uint32_t sequence;
|
||||
|
||||
uint32_t uni_hid_parser_native_motion_next_sequence(void) {
|
||||
if (++sequence == 0)
|
||||
++sequence;
|
||||
return sequence;
|
||||
}
|
||||
|
||||
static native_motion_t* provider(uni_hid_device_t* d) {
|
||||
if (!d)
|
||||
return NULL;
|
||||
for (unsigned i = 0; i < CONFIG_BLUEPAD32_MAX_DEVICES; ++i) {
|
||||
native_motion_t* p = &providers[i];
|
||||
if (p->device == d && p->setup == d->report_parser.setup &&
|
||||
memcmp(p->address, d->conn.btaddr, sizeof(p->address)) == 0)
|
||||
return p;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void uni_hid_parser_native_motion_forget(uni_hid_device_t* d) {
|
||||
for (unsigned i = 0; i < CONFIG_BLUEPAD32_MAX_DEVICES; ++i)
|
||||
if (providers[i].device == d)
|
||||
memset(&providers[i], 0, sizeof(providers[i]));
|
||||
}
|
||||
|
||||
void uni_hid_parser_native_motion_reset(uni_hid_device_t* d) {
|
||||
if (!d)
|
||||
return;
|
||||
uni_hid_parser_native_motion_forget(d);
|
||||
for (unsigned i = 0; i < CONFIG_BLUEPAD32_MAX_DEVICES; ++i) {
|
||||
native_motion_t* p = &providers[i];
|
||||
if (p->device)
|
||||
continue;
|
||||
p->device = d;
|
||||
p->setup = d->report_parser.setup;
|
||||
memcpy(p->address, d->conn.btaddr, sizeof(p->address));
|
||||
p->sample.report_tracked = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void uni_hid_parser_native_motion_begin(uni_hid_device_t* d) {
|
||||
native_motion_t* p = provider(d);
|
||||
if (p)
|
||||
p->sample.report_valid = false;
|
||||
}
|
||||
|
||||
void uni_hid_parser_native_motion_accept(uni_hid_device_t* d) {
|
||||
native_motion_t* p = provider(d);
|
||||
if (p) {
|
||||
p->sample.report_valid = true;
|
||||
p->sample.report_sequence = uni_hid_parser_native_motion_next_sequence();
|
||||
}
|
||||
}
|
||||
|
||||
void uni_hid_parser_native_motion_accel(uni_hid_device_t* d, const int32_t* value) {
|
||||
native_motion_t* p = provider(d);
|
||||
if (!p)
|
||||
return;
|
||||
p->sample.accel_valid = value != NULL;
|
||||
if (value) {
|
||||
memcpy(p->sample.accel_q13, value, sizeof(p->sample.accel_q13));
|
||||
p->sample.accel_sequence = uni_hid_parser_native_motion_next_sequence();
|
||||
}
|
||||
}
|
||||
|
||||
void uni_hid_parser_native_motion_gyro(uni_hid_device_t* d, const int32_t* value) {
|
||||
native_motion_t* p = provider(d);
|
||||
if (!p)
|
||||
return;
|
||||
p->sample.gyro_valid = value != NULL;
|
||||
if (value) {
|
||||
memcpy(p->sample.gyro_q10, value, sizeof(p->sample.gyro_q10));
|
||||
p->sample.gyro_sequence = uni_hid_parser_native_motion_next_sequence();
|
||||
}
|
||||
}
|
||||
|
||||
bool uni_hid_parser_native_motion_fresh(uni_hid_device_t* d, uint32_t timestamp, uint32_t mask) {
|
||||
native_motion_t* p = provider(d);
|
||||
if (!p)
|
||||
return false;
|
||||
uint32_t delta = (timestamp - p->timestamp) & mask;
|
||||
if (p->have_timestamp && (delta == 0 || delta > (mask >> 1)))
|
||||
return false;
|
||||
p->timestamp = timestamp;
|
||||
p->have_timestamp = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool uni_hid_parser_native_motion_snapshot(uni_hid_device_t* d, uni_native_motion_snapshot_t* out) {
|
||||
if (!out)
|
||||
return false;
|
||||
memset(out, 0, sizeof(*out));
|
||||
if (!d)
|
||||
return false;
|
||||
// Wii already owns independent, topology-aware calibration and sample IDs.
|
||||
// ACK/status/extension packets must not refresh the Remote or MotionPlus.
|
||||
if (d->report_parser.setup == uni_hid_parser_wii_setup) {
|
||||
out->accel_valid = uni_hid_parser_wii_accel_snapshot(d, out->accel_q13, &out->accel_sequence);
|
||||
out->gyro_valid = uni_hid_parser_wii_gyro_snapshot(d, out->gyro_q10, &out->gyro_sequence);
|
||||
return true;
|
||||
}
|
||||
native_motion_t* p = provider(d);
|
||||
if (!p)
|
||||
return false;
|
||||
*out = p->sample;
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
48
bluepad32_config/parser/uni_hid_parser_native_motion.h
Normal file
48
bluepad32_config/parser/uni_hid_parser_native_motion.h
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
struct uni_hid_device_s;
|
||||
typedef struct {
|
||||
bool report_tracked;
|
||||
bool report_valid;
|
||||
uint32_t report_sequence;
|
||||
bool accel_valid;
|
||||
bool gyro_valid;
|
||||
uint32_t accel_sequence;
|
||||
uint32_t gyro_sequence;
|
||||
int32_t accel_q13[3];
|
||||
int32_t gyro_q10[3];
|
||||
} uni_native_motion_snapshot_t;
|
||||
|
||||
#if SWITCH2_BRIDGE_FULL_INPUT
|
||||
// Bluetooth owner only. Reads never advance sequences or rejuvenate samples.
|
||||
// False/zero means no motion provider, not an unsupported controls device.
|
||||
bool uni_hid_parser_native_motion_snapshot(struct uni_hid_device_s* d,
|
||||
uni_native_motion_snapshot_t* out);
|
||||
|
||||
// Parser ingress/lifecycle hooks. Only setup allocates a bounded slot; late
|
||||
// reports cannot resurrect a retired provider. NULL sensor data invalidates
|
||||
// that sensor without changing its last sequence. Samples use SDL Q13/Q10 axes.
|
||||
void uni_hid_parser_native_motion_reset(struct uni_hid_device_s* d);
|
||||
void uni_hid_parser_native_motion_forget(struct uni_hid_device_s* d);
|
||||
void uni_hid_parser_native_motion_begin(struct uni_hid_device_s* d);
|
||||
void uni_hid_parser_native_motion_accept(struct uni_hid_device_s* d);
|
||||
void uni_hid_parser_native_motion_accel(struct uni_hid_device_s* d, const int32_t* value);
|
||||
void uni_hid_parser_native_motion_gyro(struct uni_hid_device_s* d, const int32_t* value);
|
||||
// Test a complete report's wrapping hardware clock, before publishing sensors.
|
||||
// mask is UINT8_MAX/UINT16_MAX/UINT32_MAX; backwards/duplicate ticks are rejected.
|
||||
bool uni_hid_parser_native_motion_fresh(struct uni_hid_device_s* d, uint32_t timestamp, uint32_t mask);
|
||||
// Parser ingress only: process-wide nonzero IDs do not alias across reconnects.
|
||||
uint32_t uni_hid_parser_native_motion_next_sequence(void);
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
|
@ -6,6 +6,9 @@
|
|||
// Sensor conversion independently adapted from SDL_hidapi_switch2.c (SDL/Valve).
|
||||
|
||||
#include "parser/uni_hid_parser_switch2.h"
|
||||
#if SWITCH2_BRIDGE_FULL_INPUT
|
||||
#include "parser/uni_hid_parser_native_motion.h"
|
||||
#endif
|
||||
|
||||
#include <math.h>
|
||||
#include <stdatomic.h>
|
||||
|
|
@ -125,6 +128,9 @@ typedef struct {
|
|||
uint8_t memory_length;
|
||||
sw2_stick_t sticks[2];
|
||||
int32_t gyro_bias[3];
|
||||
#if SWITCH2_BRIDGE_FULL_INPUT
|
||||
bool gyro_calibrated;
|
||||
#endif
|
||||
uint8_t extra_buttons, leds;
|
||||
bool leds_pending;
|
||||
uint8_t rumble_id, weak, strong;
|
||||
|
|
@ -216,6 +222,9 @@ static void sw2_disarm_timeout(sw2_instance_t* ins) {
|
|||
}
|
||||
|
||||
void uni_hid_parser_switch2_teardown(uni_hid_device_t* d) {
|
||||
#if SWITCH2_BRIDGE_FULL_INPUT
|
||||
uni_hid_parser_native_motion_forget(d);
|
||||
#endif
|
||||
sw2_instance_t* ins = sw2_instance(d);
|
||||
if (!ins)
|
||||
return;
|
||||
|
|
@ -643,13 +652,21 @@ static void sw2_response(sw2_instance_t* ins, const uint8_t* data, uint16_t leng
|
|||
ins->calibration_done = true;
|
||||
}
|
||||
} else if (ins->state == SW2_GYRO_CALIBRATION) {
|
||||
#if SWITCH2_BRIDGE_FULL_INPUT
|
||||
ins->gyro_calibrated = true;
|
||||
#endif
|
||||
for (unsigned i = 0; i < 3; ++i) {
|
||||
uint32_t bits = little_endian_read_32(value, 4 * i);
|
||||
float bias;
|
||||
memcpy(&bias, &bits, sizeof(bias));
|
||||
// Erased/invalid flash is not a floating-point sensor value.
|
||||
if (isfinite(bias) && bias >= -40.0f && bias <= 40.0f)
|
||||
bool valid = isfinite(bias) && bias >= -40.0f && bias <= 40.0f;
|
||||
if (valid)
|
||||
ins->gyro_bias[i] = (int32_t)(bias * (57.295779513f * UNI_IMU_GYRO_RES_PER_DEG_S));
|
||||
#if SWITCH2_BRIDGE_FULL_INPUT
|
||||
if (!valid)
|
||||
ins->gyro_calibrated = false;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
} else if (ins->state == SW2_PAIR) {
|
||||
|
|
@ -1029,6 +1046,9 @@ void uni_hid_parser_switch2_setup(uni_hid_device_t* d) {
|
|||
if (!ins || ins->state != SW2_ADMITTED)
|
||||
return;
|
||||
ins->handle = d->conn.handle;
|
||||
#if SWITCH2_BRIDGE_FULL_INPUT
|
||||
uni_hid_parser_native_motion_reset(d);
|
||||
#endif
|
||||
// Standard-compliant 7.5ms minimum; negotiation failure is not setup failure.
|
||||
int status = gap_update_connection_parameters(ins->handle, 6, 6, 0, 600);
|
||||
if (status != ERROR_CODE_SUCCESS)
|
||||
|
|
@ -1053,6 +1073,9 @@ bool uni_hid_parser_switch2_identity_address_type(const uni_hid_device_t* d, uin
|
|||
}
|
||||
|
||||
void uni_hid_parser_switch2_init_report(uni_hid_device_t* d) {
|
||||
#if SWITCH2_BRIDGE_FULL_INPUT
|
||||
uni_hid_parser_native_motion_begin(d);
|
||||
#endif
|
||||
(void)d; // Full snapshots replace state only after their complete length is validated.
|
||||
}
|
||||
|
||||
|
|
@ -1066,6 +1089,16 @@ static int32_t sw2_axis(uint16_t raw, const sw2_stick_t* stick, unsigned axis, b
|
|||
|
||||
static void sw2_motion(sw2_instance_t* ins, uni_gamepad_t* gp, const uint8_t* report) {
|
||||
uint32_t timestamp = little_endian_read_32(report, 42);
|
||||
#if SWITCH2_BRIDGE_FULL_INPUT
|
||||
if (!timestamp || !uni_hid_parser_native_motion_fresh(ins->device, timestamp, UINT32_MAX))
|
||||
return;
|
||||
// Acceleration has a known fixed range and does not need gyro clock
|
||||
// detection or factory gyro bias. Keep its provenance independent.
|
||||
gp->accel[0] = uni_imu_scale((int16_t)little_endian_read_16(report, 48), 32767, 8 * UNI_IMU_ACCEL_RES_PER_G);
|
||||
gp->accel[1] = uni_imu_scale((int16_t)little_endian_read_16(report, 52), 32767, 8 * UNI_IMU_ACCEL_RES_PER_G);
|
||||
gp->accel[2] = -uni_imu_scale((int16_t)little_endian_read_16(report, 50), 32767, 8 * UNI_IMU_ACCEL_RES_PER_G);
|
||||
uni_hid_parser_native_motion_accel(ins->device, gp->accel);
|
||||
#endif
|
||||
if (!timestamp || (timestamp == ins->sensor_last && !ins->gyro_full_scale))
|
||||
return;
|
||||
ins->sensor_last = timestamp;
|
||||
|
|
@ -1099,18 +1132,30 @@ static void sw2_motion(sw2_instance_t* ins, uni_gamepad_t* gp, const uint8_t* re
|
|||
static const uint8_t axes[3] = {0, 2, 1};
|
||||
for (unsigned i = 0; i < 3; ++i) {
|
||||
unsigned axis = axes[i];
|
||||
#if !SWITCH2_BRIDGE_FULL_INPUT
|
||||
int32_t accel = (int16_t)little_endian_read_16(report, 48 + 2 * axis);
|
||||
#endif
|
||||
int32_t gyro = (int16_t)little_endian_read_16(report, 54 + 2 * axis);
|
||||
#if !SWITCH2_BRIDGE_FULL_INPUT
|
||||
gp->accel[i] = uni_imu_scale(accel, 32767, 8 * UNI_IMU_ACCEL_RES_PER_G);
|
||||
#endif
|
||||
gp->gyro[i] = uni_imu_scale(gyro, 32767, ins->gyro_full_scale) - ins->gyro_bias[axis];
|
||||
if (i == 2) {
|
||||
#if !SWITCH2_BRIDGE_FULL_INPUT
|
||||
gp->accel[i] = -gp->accel[i];
|
||||
#endif
|
||||
gp->gyro[i] = -gp->gyro[i];
|
||||
}
|
||||
}
|
||||
#if SWITCH2_BRIDGE_FULL_INPUT
|
||||
uni_hid_parser_native_motion_gyro(ins->device, ins->gyro_calibrated ? gp->gyro : NULL);
|
||||
#endif
|
||||
}
|
||||
|
||||
void uni_hid_parser_switch2_parse_input_report(uni_hid_device_t* d, const uint8_t* report, uint16_t len) {
|
||||
#if SWITCH2_BRIDGE_FULL_INPUT
|
||||
uni_hid_parser_native_motion_begin(d);
|
||||
#endif
|
||||
sw2_instance_t* ins = sw2_instance(d);
|
||||
if (!ins || ins->state != SW2_READY || !report || len != SW2_REPORT_SIZE)
|
||||
return;
|
||||
|
|
@ -1148,6 +1193,9 @@ void uni_hid_parser_switch2_parse_input_report(uni_hid_device_t* d, const uint8_
|
|||
}
|
||||
sw2_motion(ins, gp, report);
|
||||
d->controller.klass = UNI_CONTROLLER_CLASS_GAMEPAD;
|
||||
#if SWITCH2_BRIDGE_FULL_INPUT
|
||||
uni_hid_parser_native_motion_accept(d);
|
||||
#endif
|
||||
}
|
||||
|
||||
void uni_hid_parser_switch2_set_player_leds(uni_hid_device_t* d, uint8_t leds) {
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -36,8 +36,9 @@
|
|||
#include <uni.h>
|
||||
extern "C" {
|
||||
#include "parser/uni_hid_parser_wii.h"
|
||||
#if SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
#if SWITCH2_BRIDGE_FULL_INPUT
|
||||
#include "parser/uni_hid_parser_ds5.h"
|
||||
#include "parser/uni_hid_parser_native_motion.h"
|
||||
#endif
|
||||
}
|
||||
#include "parser/uni_hid_parser_switch2.h"
|
||||
|
|
@ -257,19 +258,30 @@ constexpr WiiCuePattern kWiiCuePatterns[8] = {
|
|||
constexpr uint32_t kWiiCueDeadlineMs = 2000;
|
||||
#endif
|
||||
|
||||
#if SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
struct DualSenseIngress {
|
||||
uint32_t report_sequence = 0;
|
||||
#if SWITCH2_BRIDGE_FULL_INPUT
|
||||
struct NativeGamepadIngress {
|
||||
uint32_t received_us = 0;
|
||||
uint32_t motion_sequence = 0;
|
||||
uint32_t motion_received_us = 0;
|
||||
uint32_t accel_sequence = 0;
|
||||
uint32_t gyro_sequence = 0;
|
||||
uint32_t accel_received_us = 0;
|
||||
uint32_t gyro_received_us = 0;
|
||||
bool has_report = false;
|
||||
bool motion_valid = false;
|
||||
bool accel_valid = false;
|
||||
bool gyro_valid = false;
|
||||
int32_t accel_q13[3]{};
|
||||
int32_t gyro_q10[3]{};
|
||||
};
|
||||
|
||||
struct DualSenseCue {
|
||||
// Physical parser counters survive logical pairing/reselection epochs. They
|
||||
// are retired only with the Bluetooth connection, never by a snapshot getter.
|
||||
struct NativeGamepadReportIngress {
|
||||
uni_hid_device_t* device = nullptr;
|
||||
uint32_t report_sequence = 0;
|
||||
uint32_t accel_sequence = 0;
|
||||
uint32_t gyro_sequence = 0;
|
||||
};
|
||||
|
||||
struct NativeGamepadCue {
|
||||
uint64_t token = 0;
|
||||
uint32_t connection_generation = 0;
|
||||
uint32_t requested_ms = 0;
|
||||
|
|
@ -282,20 +294,20 @@ struct DualSenseCue {
|
|||
bool in_flight = false;
|
||||
};
|
||||
|
||||
struct DualSenseMotorOutput {
|
||||
struct NativeGamepadMotorOutput {
|
||||
uint32_t deadline_ms = 0;
|
||||
uint8_t magnitude[2]{};
|
||||
bool owned = false;
|
||||
};
|
||||
|
||||
// Same bounded pulse vocabulary as Wii, with real per-motor magnitudes.
|
||||
// Same bounded pulse vocabulary as Wii, with source-driver motor magnitudes.
|
||||
// These are compatibility-vibration approximations, not uploaded HD waveforms.
|
||||
struct DualSenseCuePattern {
|
||||
struct NativeGamepadCuePattern {
|
||||
uint16_t phases_ms[7];
|
||||
uint8_t count;
|
||||
uint8_t magnitude;
|
||||
};
|
||||
constexpr DualSenseCuePattern kDualSenseCuePatterns[8] = {
|
||||
constexpr NativeGamepadCuePattern kNativeGamepadCuePatterns[8] = {
|
||||
{{0}, 0, 0},
|
||||
{{1000}, 1, 160},
|
||||
{{100, 180, 100, 180, 100, 180, 100}, 7, 200},
|
||||
|
|
@ -305,7 +317,7 @@ constexpr DualSenseCuePattern kDualSenseCuePatterns[8] = {
|
|||
{{60}, 1, 96},
|
||||
{{120}, 1, 220},
|
||||
};
|
||||
constexpr uint32_t kDualSenseCueDeadlineMs = 2000;
|
||||
constexpr uint32_t kNativeGamepadCueDeadlineMs = 2000;
|
||||
#endif
|
||||
|
||||
|
||||
|
|
@ -345,9 +357,9 @@ struct BackendSlot {
|
|||
#ifdef SWITCH2_BRIDGE_WII_INPUT
|
||||
WiiMotionIngress wii_motion;
|
||||
#endif
|
||||
#if SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
DualSenseIngress dualsense_motion;
|
||||
DualSenseMotorOutput dualsense_output;
|
||||
#if SWITCH2_BRIDGE_FULL_INPUT
|
||||
NativeGamepadIngress native_motion;
|
||||
NativeGamepadMotorOutput native_output;
|
||||
#endif
|
||||
#ifdef SWITCH_PICO_WII_IR_GYRO
|
||||
WiiAimSource wii_aim;
|
||||
|
|
@ -436,79 +448,97 @@ void retire_wii_slot(uint8_t slot_index) {
|
|||
}
|
||||
#endif
|
||||
|
||||
#if SWITCH2_BRIDGE_FULL_INPUT
|
||||
bool g_native_explicit_address = false;
|
||||
uint8_t g_native_address[6]{};
|
||||
uint8_t g_native_slot = 0xff;
|
||||
uint32_t g_native_generation = 0;
|
||||
Bluepad32NativeGamepadSnapshot g_native_snapshot{};
|
||||
NativeGamepadCue g_native_cues[2]{};
|
||||
uint64_t g_next_native_token = 1;
|
||||
uni_hid_device_t* g_native_pending_devices[kSlotCount]{};
|
||||
NativeGamepadReportIngress g_native_reports[kSlotCount]{};
|
||||
|
||||
bool native_device_allowed(const uni_hid_device_t* device) {
|
||||
if (device == nullptr || !uni_hid_device_is_gamepad(device)) return false;
|
||||
#if SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
bool g_dualsense_explicit_address = false;
|
||||
uint8_t g_dualsense_address[6]{};
|
||||
uint8_t g_dualsense_slot = 0xff;
|
||||
uint32_t g_dualsense_generation = 0;
|
||||
Bluepad32DualSenseBridgeSnapshot g_dualsense_snapshot{};
|
||||
DualSenseCue g_dualsense_cues[2]{};
|
||||
uint64_t g_next_dualsense_token = 1;
|
||||
uni_hid_device_t* g_dualsense_pending_devices[kSlotCount]{};
|
||||
|
||||
bool dualsense_source_matches(const uni_hid_device_t* device) {
|
||||
return device != nullptr &&
|
||||
device->controller_type == CONTROLLER_TYPE_PS5Controller &&
|
||||
device->report_parser.parse_input_report == uni_hid_parser_ds5_parse_input_report &&
|
||||
(!g_dualsense_explicit_address ||
|
||||
memcmp(device->conn.btaddr, g_dualsense_address, 6) == 0);
|
||||
return device->controller_type == CONTROLLER_TYPE_PS5Controller &&
|
||||
device->report_parser.parse_input_report == uni_hid_parser_ds5_parse_input_report;
|
||||
#else
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool eligible_dualsense(const BackendSlot& slot) {
|
||||
return slot.active && slot.companion == nullptr && dualsense_source_matches(slot.device);
|
||||
bool native_address_matches(const uni_hid_device_t* device) {
|
||||
return device != nullptr && memcmp(device->conn.btaddr, g_native_address, 6) == 0;
|
||||
}
|
||||
|
||||
void cancel_dualsense_cue_locked(DualSenseCue& cue) {
|
||||
bool eligible_native_gamepad(const BackendSlot& slot) {
|
||||
return slot.active && native_device_allowed(slot.device) &&
|
||||
(slot.companion == nullptr || native_device_allowed(slot.companion)) &&
|
||||
(!g_native_explicit_address || native_address_matches(slot.device) ||
|
||||
native_address_matches(slot.companion));
|
||||
}
|
||||
|
||||
uni_hid_device_t* native_rumble_target(const BackendSlot& slot, uint8_t side) {
|
||||
// Paired Joy-Cons have a real left owner and right companion. A solo pad's
|
||||
// driver owns its motor topology, including mono OR/max mixing.
|
||||
return slot.companion != nullptr && side == 0 ? slot.companion : slot.device;
|
||||
}
|
||||
|
||||
bool native_rumble_capable(const BackendSlot& slot, uint8_t side) {
|
||||
const uni_hid_device_t* device = native_rumble_target(slot, side);
|
||||
return device != nullptr && device->report_parser.play_dual_rumble != nullptr;
|
||||
}
|
||||
|
||||
void cancel_native_cue_locked(NativeGamepadCue& cue) {
|
||||
cue.active = false;
|
||||
cue.result = -1;
|
||||
// The slot's last motor output remains owned until the timer replaces it.
|
||||
}
|
||||
|
||||
void refresh_dualsense_source_locked(bool reselection = false) {
|
||||
void refresh_native_source_locked(bool reselection = false) {
|
||||
uint8_t selected = 0xff;
|
||||
for (uint8_t index = 0; index < kSlotCount; ++index) {
|
||||
if (!eligible_dualsense(g_slots[index])) continue;
|
||||
if (!eligible_native_gamepad(g_slots[index])) continue;
|
||||
if (selected != 0xff) {
|
||||
selected = 0xff; // Never blend or choose by connection order.
|
||||
break;
|
||||
}
|
||||
selected = index;
|
||||
}
|
||||
if (!reselection && selected == g_dualsense_slot &&
|
||||
if (!reselection && selected == g_native_slot &&
|
||||
(selected == 0xff ||
|
||||
g_slots[selected].connection_generation == g_dualsense_generation)) return;
|
||||
for (DualSenseCue& cue : g_dualsense_cues) cancel_dualsense_cue_locked(cue);
|
||||
g_dualsense_snapshot = {};
|
||||
g_dualsense_slot = selected;
|
||||
g_dualsense_generation = 0;
|
||||
g_slots[selected].connection_generation == g_native_generation)) return;
|
||||
for (NativeGamepadCue& cue : g_native_cues) cancel_native_cue_locked(cue);
|
||||
g_native_snapshot = {};
|
||||
g_native_slot = selected;
|
||||
g_native_generation = 0;
|
||||
if (selected != 0xff) {
|
||||
BackendSlot& slot = g_slots[selected];
|
||||
g_macro_capture.disconnect(selected, slot.connection_generation, time_us_32());
|
||||
// A missed inactive snapshot must still retire the adapter's old epoch.
|
||||
g_dualsense_generation = ++slot.connection_generation;
|
||||
g_native_generation = ++slot.connection_generation;
|
||||
++slot.state_generation;
|
||||
slot.dualsense_motion.has_report = false;
|
||||
slot.dualsense_motion.motion_valid = false;
|
||||
slot.dualsense_motion.received_us = 0;
|
||||
slot.dualsense_motion.motion_received_us = 0;
|
||||
slot.native_motion = {};
|
||||
}
|
||||
}
|
||||
|
||||
void retire_dualsense_slot(uint8_t index) {
|
||||
if (g_dualsense_slot == index) {
|
||||
g_dualsense_slot = 0xff;
|
||||
g_dualsense_generation = 0;
|
||||
g_dualsense_snapshot = {};
|
||||
void retire_native_slot(uint8_t index) {
|
||||
if (g_native_slot == index) {
|
||||
g_native_slot = 0xff;
|
||||
g_native_generation = 0;
|
||||
g_native_snapshot = {};
|
||||
}
|
||||
for (DualSenseCue& cue : g_dualsense_cues)
|
||||
for (NativeGamepadCue& cue : g_native_cues)
|
||||
if (cue.slot == index) cue = {};
|
||||
g_slots[index].dualsense_motion = {};
|
||||
g_slots[index].dualsense_output = {};
|
||||
g_slots[index].native_motion = {};
|
||||
g_slots[index].native_output = {};
|
||||
}
|
||||
|
||||
bool dualsense_cue_current(const DualSenseCue& cue) {
|
||||
return cue.slot < kSlotCount && cue.slot == g_dualsense_slot &&
|
||||
cue.connection_generation == g_dualsense_generation &&
|
||||
bool native_cue_current(const NativeGamepadCue& cue) {
|
||||
return cue.slot < kSlotCount && cue.slot == g_native_slot &&
|
||||
cue.connection_generation == g_native_generation &&
|
||||
cue.connection_generation == g_slots[cue.slot].connection_generation;
|
||||
}
|
||||
#endif
|
||||
|
|
@ -615,8 +645,8 @@ bool has_free_slot() {
|
|||
physical_count += slot.device != nullptr;
|
||||
physical_count += slot.companion != nullptr;
|
||||
}
|
||||
#if SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
for (const auto* pending : g_dualsense_pending_devices) physical_count += pending != nullptr;
|
||||
#if SWITCH2_BRIDGE_FULL_INPUT
|
||||
for (const auto* pending : g_native_pending_devices) physical_count += pending != nullptr;
|
||||
#endif
|
||||
critical_section_exit(&g_state_lock);
|
||||
return physical_count < kSlotCount;
|
||||
|
|
@ -661,7 +691,7 @@ int reserve_device_slot(uni_hid_device_t* device) {
|
|||
}
|
||||
const int tracked = slot_for_device(device);
|
||||
if (g_retired_devices[physical_index] == device) {
|
||||
#if SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
#if SWITCH2_BRIDGE_FULL_INPUT
|
||||
return -1;
|
||||
#else
|
||||
if (uni_hid_parser_switch2_is_ble_device(device)) return -1;
|
||||
|
|
@ -678,7 +708,7 @@ int reserve_device_slot(uni_hid_device_t* device) {
|
|||
return -1;
|
||||
}
|
||||
}
|
||||
#if !SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
#if !SWITCH2_BRIDGE_FULL_INPUT
|
||||
if (g_slots[physical_index].device == nullptr) {
|
||||
return physical_index;
|
||||
}
|
||||
|
|
@ -1374,8 +1404,8 @@ ConnectionStatus compute_connection_status() {
|
|||
all_ready = all_ready && (!has_device || slot.active);
|
||||
any_connecting = any_connecting || (!slot.active && has_device);
|
||||
}
|
||||
#if SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
for (const auto* pending : g_dualsense_pending_devices) {
|
||||
#if SWITCH2_BRIDGE_FULL_INPUT
|
||||
for (const auto* pending : g_native_pending_devices) {
|
||||
if (pending != nullptr) {
|
||||
++physical_count;
|
||||
all_ready = false;
|
||||
|
|
@ -1437,23 +1467,28 @@ void publish_device_state(uint8_t slot, uni_hid_device_t* device,
|
|||
memcpy(g_wii_snapshot.gyro_q10, motion.gyro_q10, sizeof(motion.gyro_q10));
|
||||
}
|
||||
#endif
|
||||
#if SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
if (slot == g_dualsense_slot && target.dualsense_motion.has_report &&
|
||||
target.connection_generation == g_dualsense_generation) {
|
||||
const DualSenseIngress& motion = target.dualsense_motion;
|
||||
g_dualsense_snapshot.slot = slot;
|
||||
g_dualsense_snapshot.controller = {
|
||||
#if SWITCH2_BRIDGE_FULL_INPUT
|
||||
if (slot == g_native_slot && target.native_motion.has_report &&
|
||||
target.connection_generation == g_native_generation) {
|
||||
const NativeGamepadIngress& motion = target.native_motion;
|
||||
g_native_snapshot.slot = slot;
|
||||
g_native_snapshot.controller = {
|
||||
target.active, target.connection_generation, target.identity,
|
||||
target.pre_hotkey_button_mask, target.state,
|
||||
target.accelerometer, target.nunchuk_accelerometer};
|
||||
g_dualsense_snapshot.state_generation = target.state_generation;
|
||||
g_dualsense_snapshot.received_us = motion.received_us;
|
||||
g_dualsense_snapshot.battery = device->controller.battery;
|
||||
g_dualsense_snapshot.motion_valid = motion.motion_valid;
|
||||
g_dualsense_snapshot.motion_sequence = motion.motion_sequence;
|
||||
g_dualsense_snapshot.motion_received_us = motion.motion_received_us;
|
||||
memcpy(g_dualsense_snapshot.accel_q13, motion.accel_q13, sizeof(motion.accel_q13));
|
||||
memcpy(g_dualsense_snapshot.gyro_q10, motion.gyro_q10, sizeof(motion.gyro_q10));
|
||||
g_native_snapshot.state_generation = target.state_generation;
|
||||
g_native_snapshot.received_us = motion.received_us;
|
||||
g_native_snapshot.battery = device->controller.battery;
|
||||
g_native_snapshot.requires_stationary_bias =
|
||||
device->controller_type == CONTROLLER_TYPE_WiiController;
|
||||
g_native_snapshot.accel_valid = motion.accel_valid;
|
||||
g_native_snapshot.gyro_valid = motion.gyro_valid;
|
||||
g_native_snapshot.accel_sequence = motion.accel_sequence;
|
||||
g_native_snapshot.gyro_sequence = motion.gyro_sequence;
|
||||
g_native_snapshot.accel_received_us = motion.accel_received_us;
|
||||
g_native_snapshot.gyro_received_us = motion.gyro_received_us;
|
||||
memcpy(g_native_snapshot.accel_q13, motion.accel_q13, sizeof(motion.accel_q13));
|
||||
memcpy(g_native_snapshot.gyro_q10, motion.gyro_q10, sizeof(motion.gyro_q10));
|
||||
}
|
||||
#endif
|
||||
g_macro_capture.observe(slot, target.connection_generation,
|
||||
|
|
@ -1468,8 +1503,8 @@ void publish_all_neutral() {
|
|||
wii_ir_pointer_reset();
|
||||
#endif
|
||||
for (BackendSlot& slot : g_slots) {
|
||||
#if SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
retire_dualsense_slot(static_cast<uint8_t>(&slot - g_slots));
|
||||
#if SWITCH2_BRIDGE_FULL_INPUT
|
||||
retire_native_slot(static_cast<uint8_t>(&slot - g_slots));
|
||||
#endif
|
||||
#ifdef SWITCH2_BRIDGE_WII_INPUT
|
||||
retire_wii_slot(static_cast<uint8_t>(&slot - g_slots));
|
||||
|
|
@ -2007,8 +2042,8 @@ void reset_slot_hotkeys(BackendSlot& slot) {
|
|||
slot.connection_generation, time_us_32());
|
||||
slot.wii_orientation_pending = false;
|
||||
slot.pending_wii_orientation = {};
|
||||
#if SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
retire_dualsense_slot(static_cast<uint8_t>(&slot - g_slots));
|
||||
#if SWITCH2_BRIDGE_FULL_INPUT
|
||||
retire_native_slot(static_cast<uint8_t>(&slot - g_slots));
|
||||
#endif
|
||||
#ifdef SWITCH2_BRIDGE_WII_INPUT
|
||||
retire_wii_slot(static_cast<uint8_t>(&slot - g_slots));
|
||||
|
|
@ -2577,7 +2612,7 @@ void process_configuration_timer(btstack_timer_source_t* timer) {
|
|||
|
||||
void dispatch_rumble(uni_hid_device_t* device, uint16_t duration_ms,
|
||||
uint8_t weak, uint8_t strong) {
|
||||
#if SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
#if SWITCH2_BRIDGE_FULL_INPUT
|
||||
if (device->controller_type == CONTROLLER_TYPE_PS5Controller &&
|
||||
device->report_parser.parse_input_report == uni_hid_parser_ds5_parse_input_report) {
|
||||
// Local feedback shares the bounded writer. Its stale compatibility
|
||||
|
|
@ -2726,9 +2761,10 @@ void dispatch_wii_cue(const WiiCueDispatch& command) {
|
|||
}
|
||||
#endif
|
||||
|
||||
#if SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
struct DualSenseCueDispatch {
|
||||
#if SWITCH2_BRIDGE_FULL_INPUT
|
||||
struct NativeGamepadCueDispatch {
|
||||
uni_hid_device_t* device = nullptr;
|
||||
uni_hid_device_t* companion = nullptr;
|
||||
uint32_t connection_generation = 0;
|
||||
uint32_t prepared_ms = 0;
|
||||
uint64_t token[2]{};
|
||||
|
|
@ -2737,27 +2773,27 @@ struct DualSenseCueDispatch {
|
|||
uint8_t slot = 0xff;
|
||||
};
|
||||
|
||||
// One DS5 driver timer controls both motors. Recompute a combined packet at
|
||||
// each boundary; use the shortest ON remainder, then refresh the surviving
|
||||
// side. Absolute cue timelines skip missed pulses, never accumulate a backlog.
|
||||
bool prepare_dualsense_cues(uint8_t index, uint32_t now_ms,
|
||||
// Source drivers use a shared finite timer (or one per paired half). Recompute
|
||||
// both contributions at each boundary; the shortest ON remainder protects a
|
||||
// mono actuator too. Absolute timelines skip missed pulses, never queue them.
|
||||
bool prepare_native_cues(uint8_t index, uint32_t now_ms,
|
||||
bool local_active, bool local_dispatch,
|
||||
DualSenseCueDispatch* command) {
|
||||
NativeGamepadCueDispatch* command) {
|
||||
BackendSlot& slot = g_slots[index];
|
||||
DualSenseMotorOutput& previous = slot.dualsense_output;
|
||||
NativeGamepadMotorOutput& previous = slot.native_output;
|
||||
bool busy = false;
|
||||
bool pending = false;
|
||||
uint16_t duration = UINT16_MAX;
|
||||
uint8_t magnitude[2]{};
|
||||
for (uint8_t side = 0; side < 2; ++side) {
|
||||
DualSenseCue& cue = g_dualsense_cues[side];
|
||||
NativeGamepadCue& cue = g_native_cues[side];
|
||||
if (cue.slot != index) continue;
|
||||
if (!dualsense_cue_current(cue) ||
|
||||
(cue.result == 0 && now_ms - cue.requested_ms >= kDualSenseCueDeadlineMs) ||
|
||||
(cue.active && now_ms - cue.started_ms >= kDualSenseCueDeadlineMs))
|
||||
cancel_dualsense_cue_locked(cue);
|
||||
if (!native_cue_current(cue) ||
|
||||
(cue.result == 0 && now_ms - cue.requested_ms >= kNativeGamepadCueDeadlineMs) ||
|
||||
(cue.active && now_ms - cue.started_ms >= kNativeGamepadCueDeadlineMs))
|
||||
cancel_native_cue_locked(cue);
|
||||
if (local_active || local_dispatch) {
|
||||
if (cue.active) cancel_dualsense_cue_locked(cue);
|
||||
if (cue.active) cancel_native_cue_locked(cue);
|
||||
busy |= cue.result == 0;
|
||||
continue;
|
||||
}
|
||||
|
|
@ -2766,7 +2802,7 @@ bool prepare_dualsense_cues(uint8_t index, uint32_t now_ms,
|
|||
command->token[side] = cue.token;
|
||||
pending |= cue.result == 0;
|
||||
if (cue.sample_id == 0) continue;
|
||||
const DualSenseCuePattern& pattern = kDualSenseCuePatterns[cue.sample_id];
|
||||
const NativeGamepadCuePattern& pattern = kNativeGamepadCuePatterns[cue.sample_id];
|
||||
uint32_t elapsed = cue.active ? now_ms - cue.started_ms : 0;
|
||||
uint8_t phase = 0;
|
||||
while (phase < pattern.count && elapsed >= pattern.phases_ms[phase])
|
||||
|
|
@ -2795,13 +2831,14 @@ bool prepare_dualsense_cues(uint8_t index, uint32_t now_ms,
|
|||
(duration != 0 && deadline != previous.deadline_ms);
|
||||
if (!pending && !changed) return busy || previous.owned;
|
||||
if (!slot.active || slot.device == nullptr ||
|
||||
slot.device->report_parser.play_dual_rumble == nullptr) {
|
||||
for (DualSenseCue& cue : g_dualsense_cues)
|
||||
if (cue.slot == index) cancel_dualsense_cue_locked(cue);
|
||||
(!native_rumble_capable(slot, 0) && !native_rumble_capable(slot, 1))) {
|
||||
for (NativeGamepadCue& cue : g_native_cues)
|
||||
if (cue.slot == index) cancel_native_cue_locked(cue);
|
||||
previous = {};
|
||||
return false;
|
||||
}
|
||||
command->device = slot.device;
|
||||
command->companion = slot.companion;
|
||||
command->connection_generation = slot.connection_generation;
|
||||
command->prepared_ms = now_ms;
|
||||
command->duration_ms = duration;
|
||||
|
|
@ -2809,52 +2846,88 @@ bool prepare_dualsense_cues(uint8_t index, uint32_t now_ms,
|
|||
command->magnitude[1] = magnitude[1];
|
||||
command->slot = index;
|
||||
for (uint8_t side = 0; side < 2; ++side)
|
||||
if (command->token[side] != 0) g_dualsense_cues[side].in_flight = true;
|
||||
if (command->token[side] != 0) g_native_cues[side].in_flight = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void dispatch_dualsense_cues(const DualSenseCueDispatch& command) {
|
||||
bool submit_native_rumble(uni_hid_device_t* device, uint16_t duration,
|
||||
uint8_t right, uint8_t left) {
|
||||
if (device == nullptr || device->report_parser.play_dual_rumble == nullptr)
|
||||
return false;
|
||||
if (device->controller_type == CONTROLLER_TYPE_PS5Controller &&
|
||||
device->report_parser.parse_input_report == uni_hid_parser_ds5_parse_input_report) {
|
||||
return uni_hid_parser_ds5_bridge_rumble(device, duration, right, left);
|
||||
}
|
||||
// Existing finite-duration dispatch is the strongest observable result
|
||||
// most drivers expose. It is not transport acceptance or a remote ACK.
|
||||
dispatch_rumble(device, duration, right, left);
|
||||
return true;
|
||||
}
|
||||
|
||||
void dispatch_native_cues(const NativeGamepadCueDispatch& command) {
|
||||
if (command.device == nullptr) return;
|
||||
critical_section_enter_blocking(&g_state_lock);
|
||||
BackendSlot& slot = g_slots[command.slot];
|
||||
bool current = slot.active && slot.device == command.device &&
|
||||
slot.connection_generation == command.connection_generation;
|
||||
for (uint8_t side = 0; side < 2; ++side) {
|
||||
const DualSenseCue& cue = g_dualsense_cues[side];
|
||||
if (command.token[side] != 0)
|
||||
current &= cue.token == command.token[side] && cue.in_flight &&
|
||||
cue.result != -1 && dualsense_cue_current(cue);
|
||||
}
|
||||
critical_section_exit(&g_state_lock);
|
||||
// BTstack owns parser/lifecycle callbacks (Core 0 in HUB). No backend lock
|
||||
// crosses a driver call. Account for time spent in higher-priority output.
|
||||
const uint32_t dispatch_ms = btstack_run_loop_get_time_ms();
|
||||
const uint32_t delay = dispatch_ms - command.prepared_ms;
|
||||
current &= delay < kRumblePollIntervalMs;
|
||||
const uint16_t duration = command.duration_ms > delay
|
||||
? static_cast<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);
|
||||
const bool paired = command.companion != nullptr;
|
||||
bool submitted[2]{};
|
||||
uint32_t dispatch_ms = btstack_run_loop_get_time_ms();
|
||||
for (uint8_t target = 0; target < (paired ? 2 : 1); ++target) {
|
||||
critical_section_enter_blocking(&g_state_lock);
|
||||
bool current = slot.active && slot.device == command.device &&
|
||||
slot.companion == command.companion &&
|
||||
slot.connection_generation == command.connection_generation;
|
||||
for (uint8_t side = 0; side < 2; ++side) {
|
||||
if (paired && side != target) continue;
|
||||
const NativeGamepadCue& cue = g_native_cues[side];
|
||||
if (command.token[side] != 0)
|
||||
current &= cue.token == command.token[side] && cue.in_flight &&
|
||||
cue.result != -1 && native_cue_current(cue);
|
||||
}
|
||||
critical_section_exit(&g_state_lock);
|
||||
// No backend lock crosses a driver call. Recheck every real target:
|
||||
// cancel/reselection or a stall during R dispatch must not send stale L.
|
||||
dispatch_ms = btstack_run_loop_get_time_ms();
|
||||
const uint32_t delay = dispatch_ms - command.prepared_ms;
|
||||
const uint16_t duration = command.duration_ms > delay
|
||||
? static_cast<uint16_t>(command.duration_ms - delay) : 0;
|
||||
current &= delay < kRumblePollIntervalMs &&
|
||||
(command.duration_ms == 0 || duration != 0);
|
||||
if (!current) continue;
|
||||
uni_hid_device_t* device = paired && target == 0
|
||||
? command.companion : command.device;
|
||||
const uint8_t right = command.magnitude[paired ? target : 0];
|
||||
const uint8_t left = command.magnitude[paired ? target : 1];
|
||||
if (!submit_native_rumble(
|
||||
device, (right | left) == 0 ? 0 : duration, right, left)) continue;
|
||||
__atomic_add_fetch(&g_rumble_dispatches, 1, __ATOMIC_RELAXED);
|
||||
if (paired) submitted[target] = true;
|
||||
else submitted[0] = submitted[1] = true;
|
||||
|
||||
critical_section_enter_blocking(&g_state_lock);
|
||||
if (slot.active && slot.device == command.device &&
|
||||
slot.companion == command.companion) {
|
||||
// Retain each actual submission even if USB canceled/reselected
|
||||
// during its driver call, or the other half cannot be submitted.
|
||||
NativeGamepadMotorOutput& output = slot.native_output;
|
||||
if (paired) output.magnitude[target] = command.magnitude[target];
|
||||
else {
|
||||
output.magnitude[0] = command.magnitude[0];
|
||||
output.magnitude[1] = command.magnitude[1];
|
||||
}
|
||||
output.owned = (output.magnitude[0] | output.magnitude[1]) != 0;
|
||||
output.deadline_ms = output.owned
|
||||
? command.prepared_ms + command.duration_ms : 0;
|
||||
}
|
||||
critical_section_exit(&g_state_lock);
|
||||
}
|
||||
critical_section_enter_blocking(&g_state_lock);
|
||||
if (current && slot.active && slot.device == command.device) {
|
||||
// Retain ownership even if USB canceled/reselected during dispatch:
|
||||
// the next timer must stop/replace exactly the packet just submitted.
|
||||
slot.dualsense_output = {
|
||||
duration == 0 ? 0 : dispatch_ms + duration,
|
||||
{command.magnitude[0], command.magnitude[1]}, duration != 0};
|
||||
}
|
||||
for (uint8_t side = 0; side < 2; ++side) {
|
||||
DualSenseCue& cue = g_dualsense_cues[side];
|
||||
NativeGamepadCue& cue = g_native_cues[side];
|
||||
if (command.token[side] == 0 || cue.token != command.token[side]) continue;
|
||||
cue.in_flight = false;
|
||||
if (!dualsense_cue_current(cue) ||
|
||||
(cue.result == 0 && dispatch_ms - cue.requested_ms >= kDualSenseCueDeadlineMs)) {
|
||||
cancel_dualsense_cue_locked(cue);
|
||||
} else if (current && cue.result == 0) {
|
||||
if (!native_cue_current(cue) ||
|
||||
(cue.result == 0 && dispatch_ms - cue.requested_ms >= kNativeGamepadCueDeadlineMs)) {
|
||||
cancel_native_cue_locked(cue);
|
||||
} else if (submitted[side] && cue.result == 0) {
|
||||
cue.result = 1; // Accepted source submission, never a native ACK.
|
||||
cue.started_ms = command.prepared_ms;
|
||||
cue.active = cue.sample_id != 0;
|
||||
|
|
@ -2890,6 +2963,9 @@ void process_wii_orientation(uint8_t slot_index) {
|
|||
invalidate_slot(slot);
|
||||
slot.gamepad = {};
|
||||
slot.extra_buttons = 0;
|
||||
#if SWITCH2_BRIDGE_FULL_INPUT
|
||||
refresh_native_source_locked();
|
||||
#endif
|
||||
critical_section_exit(&g_state_lock);
|
||||
|
||||
// The setter can synchronously re-enter the platform ready callback, so
|
||||
|
|
@ -3020,8 +3096,8 @@ void process_rumble_timer(btstack_timer_source_t* timer) {
|
|||
#ifdef SWITCH2_BRIDGE_WII_INPUT
|
||||
WiiCueDispatch wii_cue_dispatch{};
|
||||
#endif
|
||||
#if SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
DualSenseCueDispatch dualsense_dispatch{};
|
||||
#if SWITCH2_BRIDGE_FULL_INPUT
|
||||
NativeGamepadCueDispatch native_dispatch{};
|
||||
#endif
|
||||
|
||||
critical_section_enter_blocking(&g_state_lock);
|
||||
|
|
@ -3157,18 +3233,18 @@ void process_rumble_timer(btstack_timer_source_t* timer) {
|
|||
slot_index, now_ms, local_feedback_active,
|
||||
profile_rumble_dispatch || feedback_dispatch, &wii_cue_dispatch);
|
||||
#endif
|
||||
#if SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
const bool dualsense_owns_rumble = prepare_dualsense_cues(
|
||||
#if SWITCH2_BRIDGE_FULL_INPUT
|
||||
const bool native_owns_rumble = prepare_native_cues(
|
||||
slot_index, now_ms, local_feedback_active,
|
||||
profile_rumble_dispatch || feedback_dispatch, &dualsense_dispatch);
|
||||
profile_rumble_dispatch || feedback_dispatch, &native_dispatch);
|
||||
#endif
|
||||
if (!profile_rumble_dispatch && !feedback_dispatch &&
|
||||
!local_feedback_active && slot.rumble_pending
|
||||
#ifdef SWITCH2_BRIDGE_WII_INPUT
|
||||
&& !wii_cue_owns_rumble
|
||||
#endif
|
||||
#if SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
&& !dualsense_owns_rumble
|
||||
#if SWITCH2_BRIDGE_FULL_INPUT
|
||||
&& !native_owns_rumble
|
||||
#endif
|
||||
#ifdef SWITCH_PICO_HAPTICS_EXPERIMENT
|
||||
&& !(xinput_host_mode &&
|
||||
|
|
@ -3246,8 +3322,8 @@ void process_rumble_timer(btstack_timer_source_t* timer) {
|
|||
#ifdef SWITCH2_BRIDGE_WII_INPUT
|
||||
dispatch_wii_cue(wii_cue_dispatch);
|
||||
#endif
|
||||
#if SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
dispatch_dualsense_cues(dualsense_dispatch);
|
||||
#if SWITCH2_BRIDGE_FULL_INPUT
|
||||
dispatch_native_cues(native_dispatch);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
|
@ -3459,6 +3535,9 @@ bool merge_joycon_slots(int owner_index, int joining_index,
|
|||
{owner.companion, static_cast<uint8_t>(owner_index)};
|
||||
g_joycon_pair_hints[physical_index_for_device(owner.companion)] =
|
||||
{owner.device, static_cast<uint8_t>(owner_index)};
|
||||
#if SWITCH2_BRIDGE_FULL_INPUT
|
||||
refresh_native_source_locked();
|
||||
#endif
|
||||
critical_section_exit(&g_state_lock);
|
||||
apply_slot_lighting(static_cast<uint8_t>(owner_index), owner.device);
|
||||
apply_slot_lighting(static_cast<uint8_t>(owner_index), owner.companion);
|
||||
|
|
@ -3529,6 +3608,9 @@ bool split_joycon_slot(uint8_t owner_index, bool gesture = false) {
|
|||
kProfileFeedbackWeakMagnitude,
|
||||
kProfileFeedbackStrongMagnitude);
|
||||
}
|
||||
#if SWITCH2_BRIDGE_FULL_INPUT
|
||||
refresh_native_source_locked();
|
||||
#endif
|
||||
critical_section_exit(&g_state_lock);
|
||||
apply_slot_lighting(owner_index, owner.device);
|
||||
apply_slot_lighting(static_cast<uint8_t>(right_index), right.device);
|
||||
|
|
@ -3737,7 +3819,7 @@ void platform_on_device_connected(uni_hid_device_t* device) {
|
|||
uni_hid_device_disconnect(device);
|
||||
return;
|
||||
}
|
||||
#if SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
#if SWITCH2_BRIDGE_FULL_INPUT
|
||||
// Classification completes after connection. Reserve transport capacity,
|
||||
// not a player/color slot, until a supported source reaches ready.
|
||||
const int pending_index = physical_index_for_device(device);
|
||||
|
|
@ -3748,7 +3830,11 @@ void platform_on_device_connected(uni_hid_device_t* device) {
|
|||
critical_section_enter_blocking(&g_state_lock);
|
||||
g_retired_devices[pending_index] = nullptr;
|
||||
g_switch2_interval_requests[pending_index] = {};
|
||||
if (slot_for_device(device) < 0) g_dualsense_pending_devices[pending_index] = device;
|
||||
if (slot_for_device(device) < 0) {
|
||||
g_native_reports[pending_index] = {};
|
||||
reset_joycon_connection(device);
|
||||
}
|
||||
if (slot_for_device(device) < 0) g_native_pending_devices[pending_index] = device;
|
||||
critical_section_exit(&g_state_lock);
|
||||
recompute_connection_status();
|
||||
#else
|
||||
|
|
@ -3780,23 +3866,30 @@ void platform_on_device_connected(uni_hid_device_t* device) {
|
|||
}
|
||||
|
||||
void platform_on_device_disconnected(uni_hid_device_t* device) {
|
||||
#if SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
#if SWITCH2_BRIDGE_FULL_INPUT
|
||||
const int pending_index = physical_index_for_device(device);
|
||||
if (pending_index >= 0) {
|
||||
critical_section_enter_blocking(&g_state_lock);
|
||||
if (g_dualsense_pending_devices[pending_index] == device)
|
||||
g_dualsense_pending_devices[pending_index] = nullptr;
|
||||
if (g_native_pending_devices[pending_index] == device)
|
||||
g_native_pending_devices[pending_index] = nullptr;
|
||||
g_retired_devices[pending_index] = device;
|
||||
critical_section_exit(&g_state_lock);
|
||||
}
|
||||
#endif
|
||||
const int slot_index = slot_for_device(device);
|
||||
if (slot_index < 0) {
|
||||
#if SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
#if SWITCH2_BRIDGE_FULL_INPUT
|
||||
recompute_connection_status();
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
#if SWITCH2_BRIDGE_FULL_INPUT
|
||||
// DS4/PSMove and other finite-rumble drivers keep timers in parser_data.
|
||||
// Retire those timers before Bluepad32 reuses that memory. Call the real
|
||||
// driver directly: a feedback scheduler must not defer this local teardown.
|
||||
if (device->report_parser.play_dual_rumble != nullptr)
|
||||
device->report_parser.play_dual_rumble(device, 0, 0, 0, 0);
|
||||
#endif
|
||||
#ifdef SWITCH_PICO_NATIVE_SWITCH_RUMBLE
|
||||
switch_native_output_detach(device);
|
||||
#endif
|
||||
|
|
@ -3826,8 +3919,8 @@ void platform_on_device_disconnected(uni_hid_device_t* device) {
|
|||
} else {
|
||||
release_slot(slot);
|
||||
}
|
||||
#if SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
refresh_dualsense_source_locked();
|
||||
#if SWITCH2_BRIDGE_FULL_INPUT
|
||||
refresh_native_source_locked();
|
||||
#endif
|
||||
critical_section_exit(&g_state_lock);
|
||||
clear_ble_identity_for_device(device);
|
||||
|
|
@ -3863,8 +3956,8 @@ uni_error_t platform_on_device_ready(uni_hid_device_t* device) {
|
|||
uni_hid_device_t* companion = nullptr;
|
||||
ControllerIdentity connection_identity = identity_for_device(device);
|
||||
critical_section_enter_blocking(&g_state_lock);
|
||||
#if SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
if (!dualsense_source_matches(device)) {
|
||||
#if SWITCH2_BRIDGE_FULL_INPUT
|
||||
if (!native_device_allowed(device)) {
|
||||
critical_section_exit(&g_state_lock);
|
||||
return UNI_ERROR_INVALID_CONTROLLER;
|
||||
}
|
||||
|
|
@ -3874,10 +3967,10 @@ uni_error_t platform_on_device_ready(uni_hid_device_t* device) {
|
|||
critical_section_exit(&g_state_lock);
|
||||
return UNI_ERROR_NO_SLOTS;
|
||||
}
|
||||
#if SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
#if SWITCH2_BRIDGE_FULL_INPUT
|
||||
const int pending_index = physical_index_for_device(device);
|
||||
if (g_dualsense_pending_devices[pending_index] == device)
|
||||
g_dualsense_pending_devices[pending_index] = nullptr;
|
||||
if (g_native_pending_devices[pending_index] == device)
|
||||
g_native_pending_devices[pending_index] = nullptr;
|
||||
#endif
|
||||
BackendSlot& pending = g_slots[slot_index];
|
||||
if (!pending.active) {
|
||||
|
|
@ -3901,8 +3994,8 @@ uni_error_t platform_on_device_ready(uni_hid_device_t* device) {
|
|||
}
|
||||
became_active = true;
|
||||
}
|
||||
#if SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
refresh_dualsense_source_locked();
|
||||
#if SWITCH2_BRIDGE_FULL_INPUT
|
||||
refresh_native_source_locked();
|
||||
#endif
|
||||
const BackendSlot& current = g_slots[slot_index];
|
||||
owner = current.device;
|
||||
|
|
@ -3968,28 +4061,45 @@ void platform_on_controller_data(uni_hid_device_t* device,
|
|||
critical_section_exit(&g_state_lock);
|
||||
return;
|
||||
}
|
||||
#if SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
if (device->controller_type == CONTROLLER_TYPE_PS5Controller) {
|
||||
uni_ds5_bridge_snapshot_t sensor{};
|
||||
DualSenseIngress& motion = slot.dualsense_motion;
|
||||
if (!uni_hid_parser_ds5_bridge_snapshot(device, &sensor) ||
|
||||
sensor.report_sequence == motion.report_sequence) {
|
||||
critical_section_exit(&g_state_lock);
|
||||
return;
|
||||
#if SWITCH2_BRIDGE_FULL_INPUT
|
||||
const int physical_index = physical_index_for_device(device);
|
||||
if (physical_index < 0) {
|
||||
critical_section_exit(&g_state_lock);
|
||||
return;
|
||||
}
|
||||
uni_native_motion_snapshot_t sensor{};
|
||||
uni_hid_parser_native_motion_snapshot(device, &sensor);
|
||||
NativeGamepadReportIngress& ingress = g_native_reports[physical_index];
|
||||
if (ingress.device != device) ingress = {device, 0, 0, 0};
|
||||
if (sensor.report_tracked &&
|
||||
(!sensor.report_valid || sensor.report_sequence == ingress.report_sequence)) {
|
||||
critical_section_exit(&g_state_lock);
|
||||
return;
|
||||
}
|
||||
ingress.report_sequence = sensor.report_sequence;
|
||||
NativeGamepadIngress& motion = slot.native_motion;
|
||||
motion.has_report = true;
|
||||
motion.received_us = time_us_32();
|
||||
// Read only the reporting physical device. The right half is the existing
|
||||
// pair's motion owner; left controls must not refresh or invalidate its IMU.
|
||||
if (slot.companion == nullptr || slot.companion == device) {
|
||||
if (!sensor.accel_valid) motion.accel_valid = false;
|
||||
else if (sensor.accel_sequence != ingress.accel_sequence) {
|
||||
motion.accel_valid = true;
|
||||
motion.accel_sequence = sensor.accel_sequence;
|
||||
motion.accel_received_us = motion.received_us;
|
||||
memcpy(motion.accel_q13, sensor.accel_q13, sizeof(motion.accel_q13));
|
||||
}
|
||||
motion.has_report = true;
|
||||
motion.report_sequence = sensor.report_sequence;
|
||||
motion.received_us = time_us_32();
|
||||
motion.motion_valid = sensor.motion_valid &&
|
||||
sensor.motion_sequence != motion.motion_sequence;
|
||||
if (sensor.motion_valid && sensor.motion_sequence != motion.motion_sequence) {
|
||||
motion.motion_sequence = sensor.motion_sequence;
|
||||
motion.motion_received_us = motion.received_us;
|
||||
motion.motion_valid = true;
|
||||
memcpy(motion.accel_q13, controller->gamepad.accel, sizeof(motion.accel_q13));
|
||||
memcpy(motion.gyro_q10, controller->gamepad.gyro, sizeof(motion.gyro_q10));
|
||||
if (!sensor.gyro_valid) motion.gyro_valid = false;
|
||||
else if (sensor.gyro_sequence != ingress.gyro_sequence) {
|
||||
motion.gyro_valid = true;
|
||||
motion.gyro_sequence = sensor.gyro_sequence;
|
||||
motion.gyro_received_us = motion.received_us;
|
||||
memcpy(motion.gyro_q10, sensor.gyro_q10, sizeof(motion.gyro_q10));
|
||||
}
|
||||
}
|
||||
if (sensor.accel_valid) ingress.accel_sequence = sensor.accel_sequence;
|
||||
if (sensor.gyro_valid) ingress.gyro_sequence = sensor.gyro_sequence;
|
||||
#endif
|
||||
#ifdef SWITCH_PICO_WII_IR
|
||||
uni_wii_ir_snapshot_t infrared{};
|
||||
|
|
@ -4323,8 +4433,9 @@ void bluepad32_input_backend_init() {
|
|||
g_joycon_pair_hints[slot_index] = {};
|
||||
g_joycon_gestures[slot_index] = {};
|
||||
g_joycon_overrides[slot_index] = {};
|
||||
#if SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
g_dualsense_pending_devices[slot_index] = nullptr;
|
||||
#if SWITCH2_BRIDGE_FULL_INPUT
|
||||
g_native_pending_devices[slot_index] = nullptr;
|
||||
g_native_reports[slot_index] = {};
|
||||
#endif
|
||||
}
|
||||
g_joycon_mode = JoyConMode::kPaired;
|
||||
|
|
@ -4531,42 +4642,42 @@ void bluepad32_input_backend_snapshot(uint8_t slot_index,
|
|||
g_last_snapshot_generation[slot_index] = state_generation;
|
||||
}
|
||||
|
||||
#if SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
void bluepad32_input_backend_select_dualsense_source(const uint8_t address[6]) {
|
||||
#if SWITCH2_BRIDGE_FULL_INPUT
|
||||
void bluepad32_input_backend_select_native_source(const uint8_t address[6]) {
|
||||
if (!g_initialized) return;
|
||||
critical_section_enter_blocking(&g_state_lock);
|
||||
g_dualsense_explicit_address = address != nullptr;
|
||||
if (address != nullptr) memcpy(g_dualsense_address, address, 6);
|
||||
else memset(g_dualsense_address, 0, sizeof(g_dualsense_address));
|
||||
refresh_dualsense_source_locked(true);
|
||||
g_native_explicit_address = address != nullptr;
|
||||
if (address != nullptr) memcpy(g_native_address, address, 6);
|
||||
else memset(g_native_address, 0, sizeof(g_native_address));
|
||||
refresh_native_source_locked(true);
|
||||
critical_section_exit(&g_state_lock);
|
||||
}
|
||||
|
||||
void bluepad32_input_backend_dualsense_snapshot(Bluepad32DualSenseBridgeSnapshot* output) {
|
||||
void bluepad32_input_backend_native_snapshot(Bluepad32NativeGamepadSnapshot* output) {
|
||||
if (output == nullptr) return;
|
||||
*output = {};
|
||||
if (!g_initialized) return;
|
||||
critical_section_enter_blocking(&g_state_lock);
|
||||
*output = g_dualsense_snapshot;
|
||||
*output = g_native_snapshot;
|
||||
critical_section_exit(&g_state_lock);
|
||||
}
|
||||
|
||||
bool bluepad32_input_backend_dualsense_sample_request(
|
||||
bool bluepad32_input_backend_native_sample_request(
|
||||
uint8_t instance, uint8_t sample_id, uint64_t* token) {
|
||||
if (token == nullptr) return false;
|
||||
*token = 0;
|
||||
if (!g_initialized || instance >= 2 || sample_id >= 8) return false;
|
||||
critical_section_enter_blocking(&g_state_lock);
|
||||
DualSenseCue& cue = g_dualsense_cues[instance];
|
||||
const uint8_t index = g_dualsense_slot;
|
||||
const bool accepted = index < kSlotCount && g_next_dualsense_token != 0 &&
|
||||
g_slots[index].device->report_parser.play_dual_rumble != nullptr &&
|
||||
NativeGamepadCue& cue = g_native_cues[instance];
|
||||
const uint8_t index = g_native_slot;
|
||||
const bool accepted = index < kSlotCount && g_next_native_token != 0 &&
|
||||
native_rumble_capable(g_slots[index], instance) &&
|
||||
!cue.in_flight && (sample_id == 0 || (cue.result != 0 && !cue.active));
|
||||
if (accepted) {
|
||||
cue = {};
|
||||
cue.token = g_next_dualsense_token++;
|
||||
cue.token = g_next_native_token++;
|
||||
cue.slot = index;
|
||||
cue.connection_generation = g_dualsense_generation;
|
||||
cue.connection_generation = g_native_generation;
|
||||
cue.requested_ms = btstack_run_loop_get_time_ms();
|
||||
cue.sample_id = sample_id;
|
||||
cue.result = 0;
|
||||
|
|
@ -4576,16 +4687,16 @@ bool bluepad32_input_backend_dualsense_sample_request(
|
|||
return accepted;
|
||||
}
|
||||
|
||||
int bluepad32_input_backend_dualsense_sample_result(uint8_t instance, uint64_t token) {
|
||||
int bluepad32_input_backend_native_sample_result(uint8_t instance, uint64_t token) {
|
||||
if (!g_initialized || instance >= 2 || token == 0) return -1;
|
||||
critical_section_enter_blocking(&g_state_lock);
|
||||
DualSenseCue& cue = g_dualsense_cues[instance];
|
||||
NativeGamepadCue& cue = g_native_cues[instance];
|
||||
int result = -1;
|
||||
if (cue.token == token && !cue.consumed) {
|
||||
if (!dualsense_cue_current(cue) ||
|
||||
if (!native_cue_current(cue) ||
|
||||
(cue.result == 0 &&
|
||||
btstack_run_loop_get_time_ms() - cue.requested_ms >= kDualSenseCueDeadlineMs))
|
||||
cancel_dualsense_cue_locked(cue);
|
||||
btstack_run_loop_get_time_ms() - cue.requested_ms >= kNativeGamepadCueDeadlineMs))
|
||||
cancel_native_cue_locked(cue);
|
||||
result = cue.result;
|
||||
if (result != 0) cue.consumed = true;
|
||||
}
|
||||
|
|
@ -4593,10 +4704,10 @@ int bluepad32_input_backend_dualsense_sample_result(uint8_t instance, uint64_t t
|
|||
return result;
|
||||
}
|
||||
|
||||
void bluepad32_input_backend_dualsense_sample_cancel(uint8_t instance) {
|
||||
void bluepad32_input_backend_native_sample_cancel(uint8_t instance) {
|
||||
if (!g_initialized || instance >= 2) return;
|
||||
critical_section_enter_blocking(&g_state_lock);
|
||||
cancel_dualsense_cue_locked(g_dualsense_cues[instance]);
|
||||
cancel_native_cue_locked(g_native_cues[instance]);
|
||||
critical_section_exit(&g_state_lock);
|
||||
}
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -108,34 +108,39 @@ int bluepad32_input_backend_wii_sample_result(uint64_t token);
|
|||
void bluepad32_input_backend_wii_sample_cancel();
|
||||
#endif
|
||||
|
||||
#if SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
// One physical DualSense/Edge, calibrated SDL axes before legacy conversion.
|
||||
// Receipt times and motion sequences advance only on admitted parser reports.
|
||||
struct Bluepad32DualSenseBridgeSnapshot {
|
||||
#if SWITCH2_BRIDGE_FULL_INPUT
|
||||
// One logical gamepad, calibrated SDL axes before legacy int16 conversion.
|
||||
// Sensor receipt times advance independently, only on actual parser ingress.
|
||||
struct Bluepad32NativeGamepadSnapshot {
|
||||
uint8_t slot = 0xff;
|
||||
Bluepad32SlotSnapshot controller{};
|
||||
uint32_t state_generation = 0;
|
||||
uint32_t received_us = 0;
|
||||
uint8_t battery = 0;
|
||||
bool motion_valid = false;
|
||||
uint32_t motion_sequence = 0;
|
||||
uint32_t motion_received_us = 0;
|
||||
bool accel_valid = false;
|
||||
bool gyro_valid = false;
|
||||
bool requires_stationary_bias = false;
|
||||
uint32_t accel_sequence = 0;
|
||||
uint32_t gyro_sequence = 0;
|
||||
uint32_t accel_received_us = 0;
|
||||
uint32_t gyro_received_us = 0;
|
||||
int32_t accel_q13[3]{};
|
||||
int32_t gyro_q10[3]{};
|
||||
};
|
||||
|
||||
// nullptr selects the uniquely eligible ready DualSense; ambiguity fails closed.
|
||||
// nullptr selects the uniquely eligible ready logical gamepad; ambiguity fails closed.
|
||||
// Reselection invalidates input and cue tokens without modifying pairings.
|
||||
void bluepad32_input_backend_select_dualsense_source(const uint8_t address[6]);
|
||||
void bluepad32_input_backend_dualsense_snapshot(Bluepad32DualSenseBridgeSnapshot* output);
|
||||
// Instance 0 is R/right motor, 1 is L/left motor. Samples 0..7 are bounded
|
||||
// compatibility-motor cues, not HD haptics. Zero stops only the requested side.
|
||||
void bluepad32_input_backend_select_native_source(const uint8_t address[6]);
|
||||
void bluepad32_input_backend_native_snapshot(Bluepad32NativeGamepadSnapshot* output);
|
||||
// Instance 0 is R, 1 is L. Samples 0..7 are bounded compatibility cues, not HD
|
||||
// haptics. A side stop removes only that side's contribution. Mono drivers combine
|
||||
// both contributions on their one actuator; this does not promise stereo output.
|
||||
// Result: 0 pending, 1 source-driver dispatch, -1 retired/failed/consumed.
|
||||
// Dispatch is NOT an application ACK or proof of physical actuator onset.
|
||||
bool bluepad32_input_backend_dualsense_sample_request(
|
||||
bool bluepad32_input_backend_native_sample_request(
|
||||
uint8_t instance, uint8_t sample_id, uint64_t* token);
|
||||
int bluepad32_input_backend_dualsense_sample_result(uint8_t instance, uint64_t token);
|
||||
void bluepad32_input_backend_dualsense_sample_cancel(uint8_t instance);
|
||||
int bluepad32_input_backend_native_sample_result(uint8_t instance, uint64_t token);
|
||||
void bluepad32_input_backend_native_sample_cancel(uint8_t instance);
|
||||
#endif
|
||||
|
||||
// Side-effect-free raw input snapshot for management telemetry. Unlike the
|
||||
|
|
|
|||
|
|
@ -3,12 +3,5 @@
|
|||
#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*);
|
||||
|
|
|
|||
|
|
@ -85,8 +85,12 @@ enum {
|
|||
|
||||
typedef enum {
|
||||
CONTROLLER_TYPE_UnknownController = 0,
|
||||
CONTROLLER_TYPE_XBoxOneController = 32,
|
||||
CONTROLLER_TYPE_PS4Controller = 34,
|
||||
CONTROLLER_TYPE_WiiController = 35,
|
||||
CONTROLLER_TYPE_PS5Controller = 46,
|
||||
CONTROLLER_TYPE_SwitchProController = 38,
|
||||
CONTROLLER_TYPE_PS5Controller = 45,
|
||||
CONTROLLER_TYPE_PSMoveController = 56,
|
||||
} uni_controller_type_t;
|
||||
|
||||
typedef enum {
|
||||
|
|
|
|||
|
|
@ -1,291 +0,0 @@
|
|||
// Reuse the backend's transport/storage fixture; these scenarios exercise only
|
||||
// the native DualSense contract, not a second implementation of its scheduler.
|
||||
#define main backend_fixture_main
|
||||
#include "bluepad32_backend_lifecycle_test.cpp"
|
||||
#undef main
|
||||
|
||||
namespace {
|
||||
struct SensorFixture {
|
||||
uni_hid_device_t* device = nullptr;
|
||||
uni_ds5_bridge_snapshot_t metadata{};
|
||||
bool valid = false;
|
||||
};
|
||||
SensorFixture sensors[4];
|
||||
void (*during_dualsense_dispatch)() = nullptr;
|
||||
bool dualsense_transport_available = true;
|
||||
|
||||
void observe_dualsense_rumble(uni_hid_device_t* target, uint16_t delay,
|
||||
uint16_t duration, uint8_t right, uint8_t left) {
|
||||
require(state_lock_depth == 0, "DS5 driver dispatch must not hold the shared state lock");
|
||||
play_rumble(target, delay, duration, right, left);
|
||||
if (during_dualsense_dispatch) during_dualsense_dispatch();
|
||||
}
|
||||
|
||||
uni_hid_device_t dualsense(int index) {
|
||||
auto result = device(index, true, UNI_BT_CONN_PROTOCOL_BR_EDR);
|
||||
result.vendor_id = 0x054c;
|
||||
result.product_id = index == 0 ? 0x0ce6 : 0x0df2;
|
||||
result.controller_type = CONTROLLER_TYPE_PS5Controller;
|
||||
result.report_parser.parse_input_report = uni_hid_parser_ds5_parse_input_report;
|
||||
result.report_parser.play_dual_rumble = observe_dualsense_rumble;
|
||||
return result;
|
||||
}
|
||||
|
||||
void report_dualsense(uni_hid_device_t& pad, bool fresh_motion = true) {
|
||||
SensorFixture& sensor = sensors[pad.idx];
|
||||
sensor.device = &pad;
|
||||
sensor.valid = true;
|
||||
++sensor.metadata.report_sequence;
|
||||
if (fresh_motion) ++sensor.metadata.motion_sequence;
|
||||
sensor.metadata.motion_valid = fresh_motion;
|
||||
pad.controller.klass = UNI_CONTROLLER_CLASS_GAMEPAD;
|
||||
pad.controller.gamepad.buttons = BUTTON_A | BUTTON_SHOULDER_L;
|
||||
pad.controller.gamepad.accel[1] = 8193;
|
||||
pad.controller.gamepad.gyro[2] = -123456;
|
||||
pad.controller.battery = 176;
|
||||
platform_on_controller_data(&pad, &pad.controller);
|
||||
}
|
||||
|
||||
Bluepad32DualSenseBridgeSnapshot bridge_snapshot() {
|
||||
Bluepad32DualSenseBridgeSnapshot result{};
|
||||
bluepad32_input_backend_dualsense_snapshot(&result);
|
||||
return result;
|
||||
}
|
||||
|
||||
void source_isolation() {
|
||||
start_pairing_backend();
|
||||
auto ordinary = device(0, true, UNI_BT_CONN_PROTOCOL_BR_EDR);
|
||||
ordinary.vendor_id = 0x054c;
|
||||
ordinary.product_id = 0x0ce6;
|
||||
require(platform_on_device_ready(&ordinary) == UNI_ERROR_INVALID_CONTROLLER,
|
||||
"an unsupported parser must not enter the dedicated DualSense output slots");
|
||||
bluepad32_input_backend_select_dualsense_source(ordinary.conn.btaddr);
|
||||
require(!bridge_snapshot().controller.active, "VID/PID/address alone must never select a non-PS5 parser");
|
||||
platform_on_device_disconnected(&ordinary);
|
||||
bluepad32_input_backend_select_dualsense_source(nullptr);
|
||||
auto first = dualsense(0);
|
||||
auto second = dualsense(1);
|
||||
require(platform_on_device_ready(&first) == UNI_ERROR_SUCCESS, "first DS5 must connect");
|
||||
now_ms = 100;
|
||||
report_dualsense(first);
|
||||
const auto initial = bridge_snapshot();
|
||||
require(initial.slot == 0 && initial.controller.active && initial.controller.state.button_south &&
|
||||
initial.battery == 176 && initial.motion_valid && initial.accel_q13[1] == 8193 &&
|
||||
initial.gyro_q10[2] == -123456 && initial.motion_received_us == 100000,
|
||||
"native snapshot must preserve coherent physical controls and calibrated precision");
|
||||
now_ms = 120;
|
||||
platform_on_controller_data(&first, &first.controller);
|
||||
bluepad32_input_backend_report_sent(0);
|
||||
auto snapshot = bridge_snapshot();
|
||||
require(snapshot.state_generation == initial.state_generation && snapshot.received_us == 100000 &&
|
||||
snapshot.motion_sequence == initial.motion_sequence,
|
||||
"polling/cached callbacks and USB consumption must not freshen input or motion");
|
||||
report_dualsense(first, false);
|
||||
snapshot = bridge_snapshot();
|
||||
require(snapshot.received_us == 120000 && snapshot.motion_received_us == 100000 && !snapshot.motion_valid,
|
||||
"a controls-only admission must not refresh a duplicate sensor timestamp");
|
||||
sensors[0].valid = false;
|
||||
first.controller.gamepad.buttons = 0;
|
||||
platform_on_controller_data(&first, &first.controller);
|
||||
require(bridge_snapshot().controller.state.button_south,
|
||||
"malformed parser input must not publish an invented button release");
|
||||
uint64_t old_token;
|
||||
require(bluepad32_input_backend_dualsense_sample_request(0, 1, &old_token), "first source cue must queue");
|
||||
require(platform_on_device_ready(&second) == UNI_ERROR_SUCCESS, "Edge must connect");
|
||||
report_dualsense(second);
|
||||
require(!bridge_snapshot().controller.active &&
|
||||
bluepad32_input_backend_dualsense_sample_result(0, old_token) == -1,
|
||||
"auto ambiguity must fail closed and retire source work immediately");
|
||||
platform_on_device_disconnected(&second);
|
||||
platform_on_controller_data(&first, &first.controller);
|
||||
require(!bridge_snapshot().controller.active, "returning to a source cannot resurrect cached state");
|
||||
report_dualsense(first);
|
||||
snapshot = bridge_snapshot();
|
||||
require(snapshot.controller.active && snapshot.controller.connection_generation != initial.controller.connection_generation,
|
||||
"a missed ambiguous interval still needs a new adapter epoch");
|
||||
platform_on_device_connected(&second);
|
||||
require(platform_on_device_ready(&second) == UNI_ERROR_SUCCESS, "Edge reconnect must succeed");
|
||||
bluepad32_input_backend_select_dualsense_source(first.conn.btaddr);
|
||||
report_dualsense(first);
|
||||
report_dualsense(second);
|
||||
require(bridge_snapshot().slot == 0, "explicit source must ignore another live PS5");
|
||||
platform_on_device_disconnected(&first);
|
||||
require(!bridge_snapshot().controller.active, "disconnect must not migrate an explicit source");
|
||||
}
|
||||
|
||||
void stable_logical_slot() {
|
||||
start_pairing_backend();
|
||||
auto unrelated = device(0, true, UNI_BT_CONN_PROTOCOL_BR_EDR);
|
||||
auto pad = dualsense(1);
|
||||
pad.report_parser.set_lightbar_color = set_lightbar;
|
||||
// An earlier, still-unclassified transport connection must not reserve player 1.
|
||||
platform_on_device_connected(&unrelated);
|
||||
platform_on_device_connected(&pad);
|
||||
require(platform_on_device_ready(&pad) == UNI_ERROR_SUCCESS, "DS5 must complete setup");
|
||||
now_ms = 100;
|
||||
report_dualsense(pad);
|
||||
const auto first = bridge_snapshot();
|
||||
const auto color = switch_pro_get_slot_light_color(0);
|
||||
require(first.controller.active && first.slot == 0 &&
|
||||
pad.lightbar_red == color.red && pad.lightbar_green == color.green &&
|
||||
pad.lightbar_blue == color.blue,
|
||||
"a transport-index-1 DualSense must own logical slot 0 and its lightbar color");
|
||||
require(platform_on_device_ready(&unrelated) == UNI_ERROR_INVALID_CONTROLLER,
|
||||
"dedicated DualSense mode must reject an unrelated ready controller");
|
||||
platform_on_device_disconnected(&unrelated);
|
||||
platform_on_device_connected(&unrelated);
|
||||
require(platform_on_device_ready(&unrelated) == UNI_ERROR_INVALID_CONTROLLER,
|
||||
"remembered unrelated reconnects must remain outside logical slots");
|
||||
platform_on_device_disconnected(&unrelated);
|
||||
require(bridge_snapshot().slot == 0 &&
|
||||
bridge_snapshot().controller.connection_generation == first.controller.connection_generation &&
|
||||
pad.lightbar_calls == 1,
|
||||
"unrelated connection churn must not rebind or recolor the active DualSense");
|
||||
|
||||
platform_on_device_disconnected(&pad);
|
||||
auto reconnected = dualsense(2);
|
||||
memcpy(reconnected.conn.btaddr, pad.conn.btaddr, sizeof(pad.conn.btaddr));
|
||||
reconnected.report_parser.set_lightbar_color = set_lightbar;
|
||||
platform_on_device_connected(&reconnected);
|
||||
require(platform_on_device_ready(&reconnected) == UNI_ERROR_SUCCESS, "DS5 reconnect must succeed");
|
||||
now_ms = 200;
|
||||
report_dualsense(reconnected);
|
||||
const auto next = bridge_snapshot();
|
||||
require(next.controller.active && next.slot == 0 &&
|
||||
controller_identity_equal(next.controller.identity, first.controller.identity) &&
|
||||
next.controller.connection_generation != first.controller.connection_generation &&
|
||||
reconnected.lightbar_red == color.red && reconnected.lightbar_green == color.green &&
|
||||
reconnected.lightbar_blue == color.blue,
|
||||
"reusing another Bluetooth index must preserve identity and the first logical slot");
|
||||
auto aborted = dualsense(3);
|
||||
platform_on_device_connected(&aborted);
|
||||
platform_on_device_disconnected(&aborted);
|
||||
require(platform_on_device_ready(&aborted) == UNI_ERROR_NO_SLOTS,
|
||||
"a late ready callback must not resurrect an unassigned disconnected transport");
|
||||
require(bridge_snapshot().controller.active && bridge_snapshot().slot == 0,
|
||||
"an aborted second setup must not disturb the active source");
|
||||
auto pending0 = device(0, true, UNI_BT_CONN_PROTOCOL_BR_EDR);
|
||||
auto pending1 = device(1, true, UNI_BT_CONN_PROTOCOL_BR_EDR);
|
||||
auto pending3 = device(3, true, UNI_BT_CONN_PROTOCOL_BR_EDR);
|
||||
platform_on_device_connected(&pending0);
|
||||
platform_on_device_connected(&pending1);
|
||||
platform_on_device_connected(&pending3);
|
||||
require(!incoming_connections && !scanning_enabled,
|
||||
"unclassified transports still consume physical connection capacity");
|
||||
platform_on_device_disconnected(&pending0);
|
||||
require(incoming_connections && bridge_snapshot().slot == 0 &&
|
||||
bridge_snapshot().controller.connection_generation == next.controller.connection_generation,
|
||||
"freeing pending transport capacity must not move the logical source");
|
||||
platform_on_device_disconnected(&pending1);
|
||||
platform_on_device_disconnected(&pending3);
|
||||
}
|
||||
|
||||
void cue_lifetime() {
|
||||
start_pairing_backend();
|
||||
auto pad = dualsense(0);
|
||||
require(platform_on_device_ready(&pad) == UNI_ERROR_SUCCESS, "DS5 must connect");
|
||||
uint64_t right, left, stop;
|
||||
require(bluepad32_input_backend_dualsense_sample_request(0, 6, &right) &&
|
||||
bluepad32_input_backend_dualsense_sample_request(1, 1, &left) && right != left &&
|
||||
bluepad32_input_backend_dualsense_sample_result(0, right) == 0 && pad.rumble_calls == 0,
|
||||
"independent acceptance is not driver completion");
|
||||
dualsense_transport_available = false;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(bluepad32_input_backend_dualsense_sample_result(0, right) == 0 && pad.rumble_calls == 0,
|
||||
"a busy source driver must not count as dispatch completion");
|
||||
dualsense_transport_available = true;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(pad.last_high == 96 && pad.last_low == 160 && pad.last_rumble_duration_ms == 60 &&
|
||||
bluepad32_input_backend_dualsense_sample_result(0, right) == 1 &&
|
||||
bluepad32_input_backend_dualsense_sample_result(1, right) == -1 &&
|
||||
bluepad32_input_backend_dualsense_sample_result(1, left) == 1,
|
||||
"combined output must route R weak/right and L strong/left with the shortest safe timer");
|
||||
now_ms = 60;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(pad.last_high == 0 && pad.last_low == 160 && pad.last_rumble_duration_ms == 940,
|
||||
"ending right must preserve only the left pulse's original remaining lifetime");
|
||||
require(bluepad32_input_backend_dualsense_sample_request(0, 3, &right), "right can restart independently");
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
now_ms = 85;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(pad.last_high == 0 && pad.last_low == 160, "right gap must not stop the left motor");
|
||||
now_ms = 175;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(pad.last_high == 96 && pad.last_low == 160, "later pulse must resume after its gap");
|
||||
require(bluepad32_input_backend_dualsense_sample_request(1, 0, &stop), "left stop must replace only left");
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(pad.last_high == 96 && pad.last_low == 0 &&
|
||||
bluepad32_input_backend_dualsense_sample_result(1, stop) == 1 &&
|
||||
bluepad32_input_backend_dualsense_sample_result(1, left) == -1,
|
||||
"a side stop needs actual dispatch and cannot stop its sibling");
|
||||
now_ms = 200;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(pad.last_rumble_duration_ms == 0, "final pulse expiry must release both motors");
|
||||
const int stopped = pad.rumble_calls;
|
||||
now_ms = 5000;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(pad.rumble_calls == stopped, "expired pulses must never replay after a stall");
|
||||
require(bluepad32_input_backend_dualsense_sample_request(0, 1, &right), "pending timeout cue must queue");
|
||||
now_ms += 2000;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(bluepad32_input_backend_dualsense_sample_result(0, right) == -1 && pad.rumble_calls == stopped,
|
||||
"an undispatched expired cue must fail without producing a late pulse");
|
||||
}
|
||||
|
||||
void cue_races() {
|
||||
start_pairing_backend();
|
||||
auto pad = dualsense(0);
|
||||
require(platform_on_device_ready(&pad) == UNI_ERROR_SUCCESS, "DS5 must connect");
|
||||
uint64_t token;
|
||||
require(bluepad32_input_backend_dualsense_sample_request(0, 1, &token), "race cue must queue");
|
||||
during_dualsense_dispatch = [] { bluepad32_input_backend_dualsense_sample_cancel(0); };
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
during_dualsense_dispatch = nullptr;
|
||||
require(bluepad32_input_backend_dualsense_sample_result(0, token) == -1,
|
||||
"cancellation during dispatch must defeat a late completion");
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(pad.last_rumble_duration_ms == 0, "in-flight cancellation must retain a bounded stop obligation");
|
||||
require(bluepad32_input_backend_dualsense_sample_request(1, 1, &token), "reselection race must queue");
|
||||
during_dualsense_dispatch = [] { bluepad32_input_backend_select_dualsense_source(nullptr); };
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
during_dualsense_dispatch = nullptr;
|
||||
require(bluepad32_input_backend_dualsense_sample_result(1, token) == -1,
|
||||
"reselection must retire an in-flight token even for the same physical source");
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(pad.last_rumble_duration_ms == 0, "reselection cannot orphan the just-dispatched motor");
|
||||
require(bluepad32_input_backend_dualsense_sample_request(0, 1, &token), "disconnect race must queue");
|
||||
platform_on_device_disconnected(&pad);
|
||||
auto replacement = dualsense(0);
|
||||
require(platform_on_device_ready(&replacement) == UNI_ERROR_SUCCESS, "replacement must connect");
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(bluepad32_input_backend_dualsense_sample_result(0, token) == -1 && replacement.rumble_calls == 0,
|
||||
"old tokens and deferred stops must never enter a replacement connection");
|
||||
}
|
||||
} // namespace
|
||||
|
||||
extern "C" void uni_hid_parser_ds5_parse_input_report(uni_hid_device_t*, const uint8_t*, uint16_t) {}
|
||||
extern "C" bool uni_hid_parser_ds5_bridge_rumble(
|
||||
uni_hid_device_t* pad, uint16_t duration, uint8_t right, uint8_t left) {
|
||||
if (!dualsense_transport_available) return false;
|
||||
observe_dualsense_rumble(pad, 0, duration, right, left);
|
||||
return true;
|
||||
}
|
||||
extern "C" bool uni_hid_parser_ds5_bridge_snapshot(uni_hid_device_t* pad, uni_ds5_bridge_snapshot_t* out) {
|
||||
for (const auto& fixture : sensors) {
|
||||
if (fixture.device != pad || !fixture.valid) continue;
|
||||
*out = fixture.metadata;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
require(argc == 2, "scenario required");
|
||||
const std::string scenario = argv[1];
|
||||
if (scenario == "source-isolation") source_isolation();
|
||||
else if (scenario == "cue-lifetime") cue_lifetime();
|
||||
else if (scenario == "cue-races") cue_races();
|
||||
else if (scenario == "stable-logical-slot") stable_logical_slot();
|
||||
else require(false, "unknown DualSense scenario");
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@
|
|||
#include <string.h>
|
||||
|
||||
#include "parser/uni_hid_parser_ds5.h"
|
||||
#include "parser/uni_hid_parser_native_motion.h"
|
||||
#include "uni_hid_device.h"
|
||||
#include "uni_utils.h"
|
||||
|
||||
|
|
@ -59,6 +60,14 @@ uni_hid_device_t* uni_hid_device_create_virtual(uni_hid_device_t* d) { (void)d;
|
|||
void uni_hid_device_set_cod(uni_hid_device_t* d, uint32_t cod) { (void)d; (void)cod; }
|
||||
void uni_hid_device_connect(uni_hid_device_t* d) { (void)d; }
|
||||
void uni_hid_device_process_controller(uni_hid_device_t* d) { (void)d; }
|
||||
// The common accessor must not select a different family in this fixture.
|
||||
void uni_hid_parser_wii_setup(uni_hid_device_t* d) { (void)d; assert(false); }
|
||||
bool uni_hid_parser_wii_accel_snapshot(uni_hid_device_t* d, int32_t v[3], uint32_t* seq) {
|
||||
(void)d; (void)v; (void)seq; assert(false); return false;
|
||||
}
|
||||
bool uni_hid_parser_wii_gyro_snapshot(uni_hid_device_t* d, int32_t v[3], uint32_t* seq) {
|
||||
(void)d; (void)v; (void)seq; assert(false); return false;
|
||||
}
|
||||
uint8_t uni_hid_parser_hat_to_dpad(uint8_t hat) {
|
||||
const uint8_t values[8] = {DPAD_UP, DPAD_UP | DPAD_RIGHT, DPAD_RIGHT,
|
||||
DPAD_RIGHT | DPAD_DOWN, DPAD_DOWN, DPAD_DOWN | DPAD_LEFT,
|
||||
|
|
@ -144,11 +153,12 @@ static void input(uint8_t bytes[78], uint32_t timestamp) {
|
|||
put32(bytes + 29, timestamp);
|
||||
seal(bytes, 78, 0xa1);
|
||||
}
|
||||
static uni_ds5_bridge_snapshot_t feed(uint8_t* bytes, uint16_t len, bool admitted) {
|
||||
static uni_native_motion_snapshot_t feed(uint8_t* bytes, uint16_t len, bool admitted) {
|
||||
uni_hid_parser_ds5_init_report(&device);
|
||||
uni_hid_parser_ds5_parse_input_report(&device, bytes, len);
|
||||
uni_ds5_bridge_snapshot_t snapshot = {0};
|
||||
assert(uni_hid_parser_ds5_bridge_snapshot(&device, &snapshot) == admitted);
|
||||
uni_native_motion_snapshot_t snapshot;
|
||||
assert(uni_hid_parser_native_motion_snapshot(&device, &snapshot));
|
||||
assert(snapshot.report_tracked && snapshot.report_valid == admitted);
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
|
|
@ -178,27 +188,32 @@ int main(void) {
|
|||
assert(ready_count == 1);
|
||||
uint8_t report[78];
|
||||
input(report, 100);
|
||||
uni_ds5_bridge_snapshot_t snapshot = feed(report, sizeof(report), true);
|
||||
assert(!snapshot.motion_valid && snapshot.motion_sequence == 0);
|
||||
uni_native_motion_snapshot_t snapshot = feed(report, sizeof(report), true);
|
||||
assert(!snapshot.accel_valid && !snapshot.gyro_valid);
|
||||
assert(device.controller.gamepad.buttons & BUTTON_A); // Controls survive fallback.
|
||||
calibration(calib, false);
|
||||
feature(calib, sizeof(calib));
|
||||
snapshot = feed(report, sizeof(report), true);
|
||||
assert(!snapshot.motion_valid && snapshot.motion_sequence == 0); // Calibration alone is not fresh motion.
|
||||
assert(!snapshot.accel_valid && !snapshot.gyro_valid); // Calibration alone is not fresh motion.
|
||||
input(report, 101);
|
||||
snapshot = feed(report, sizeof(report), true);
|
||||
assert(snapshot.motion_valid && snapshot.motion_sequence == 1);
|
||||
assert(snapshot.accel_valid && snapshot.gyro_valid);
|
||||
assert(device.controller.gamepad.gyro[0] == 1024 && device.controller.gamepad.gyro[1] == 0);
|
||||
assert(device.controller.gamepad.accel[1] == 8192);
|
||||
assert(device.controller.gamepad.misc_buttons & MISC_BUTTON_CAPTURE);
|
||||
uni_native_motion_snapshot_t common;
|
||||
assert(uni_hid_parser_native_motion_snapshot(&device, &common));
|
||||
assert(common.accel_valid && common.gyro_valid &&
|
||||
common.accel_q13[1] == 8192 && common.gyro_q10[0] == 1024);
|
||||
const uint32_t common_sequence = common.accel_sequence;
|
||||
const uint32_t report_sequence = snapshot.report_sequence;
|
||||
uni_ds5_bridge_snapshot_t polled;
|
||||
assert(uni_hid_parser_ds5_bridge_snapshot(&device, &polled) && polled.report_sequence == report_sequence);
|
||||
uni_native_motion_snapshot_t polled;
|
||||
assert(uni_hid_parser_native_motion_snapshot(&device, &polled) && polled.report_sequence == report_sequence);
|
||||
snapshot = feed(report, sizeof(report), true);
|
||||
assert(!snapshot.motion_valid && snapshot.motion_sequence == 1);
|
||||
assert(snapshot.accel_sequence == common_sequence && !snapshot.accel_valid && !snapshot.gyro_valid);
|
||||
input(report, 99);
|
||||
snapshot = feed(report, sizeof(report), true);
|
||||
assert(!snapshot.motion_valid && snapshot.motion_sequence == 1);
|
||||
assert(snapshot.accel_sequence == common_sequence && !snapshot.accel_valid && !snapshot.gyro_valid);
|
||||
input(report, 102);
|
||||
feed(report, 77, false);
|
||||
report[9] ^= 0x20;
|
||||
|
|
@ -206,7 +221,7 @@ int main(void) {
|
|||
feed(NULL, 0, false);
|
||||
input(report, 102);
|
||||
snapshot = feed(report, sizeof(report), true);
|
||||
assert(snapshot.motion_valid && snapshot.motion_sequence == 2);
|
||||
assert(snapshot.accel_valid && snapshot.gyro_valid && snapshot.accel_sequence != common_sequence);
|
||||
|
||||
// A real uint32 sensor-clock wrap is forward progress, not a duplicate.
|
||||
uni_hid_parser_ds5_setup(&device);
|
||||
|
|
@ -214,10 +229,12 @@ int main(void) {
|
|||
feature(calib, sizeof(calib));
|
||||
input(report, UINT32_MAX - 15);
|
||||
snapshot = feed(report, sizeof(report), true);
|
||||
assert(snapshot.motion_valid && snapshot.motion_sequence == 1);
|
||||
assert(snapshot.accel_valid && snapshot.gyro_valid);
|
||||
assert(uni_hid_parser_native_motion_snapshot(&device, &common));
|
||||
assert(common.accel_valid && common.accel_sequence != common_sequence);
|
||||
input(report, 16);
|
||||
snapshot = feed(report, sizeof(report), true);
|
||||
assert(snapshot.motion_valid && snapshot.motion_sequence == 2);
|
||||
assert(snapshot.accel_valid && snapshot.gyro_valid && snapshot.accel_sequence != common.accel_sequence);
|
||||
|
||||
const unsigned before_busy = output_count;
|
||||
transport_available = false;
|
||||
|
|
@ -236,7 +253,9 @@ int main(void) {
|
|||
const unsigned sent = output_count;
|
||||
uni_hid_parser_ds5_bridge_teardown(&device);
|
||||
advance(2000);
|
||||
assert(output_count == sent && !uni_hid_parser_ds5_bridge_snapshot(&device, &snapshot));
|
||||
assert(output_count == sent);
|
||||
assert(!uni_hid_parser_native_motion_snapshot(&device, &common));
|
||||
assert(!common.report_valid && !common.accel_valid && !common.gyro_valid);
|
||||
uni_hid_parser_ds5_play_dual_rumble(&device, 0, 1000, 0, 90);
|
||||
const unsigned active_sent = output_count;
|
||||
uni_hid_parser_ds5_bridge_teardown(&device);
|
||||
|
|
|
|||
641
tests/native_gamepad_backend_test.cpp
Normal file
641
tests/native_gamepad_backend_test.cpp
Normal file
|
|
@ -0,0 +1,641 @@
|
|||
// Reuse the backend's transport/storage fixture; these scenarios exercise only
|
||||
// the native gamepad contract, not a second implementation of its scheduler.
|
||||
#define main backend_fixture_main
|
||||
#include "bluepad32_backend_lifecycle_test.cpp"
|
||||
#undef main
|
||||
|
||||
namespace {
|
||||
struct SensorFixture {
|
||||
uni_hid_device_t* device = nullptr;
|
||||
uni_native_motion_snapshot_t metadata{};
|
||||
bool valid = false;
|
||||
};
|
||||
SensorFixture sensors[4];
|
||||
void (*during_dualsense_dispatch)() = nullptr;
|
||||
bool dualsense_transport_available = true;
|
||||
|
||||
void observe_dualsense_rumble(uni_hid_device_t* target, uint16_t delay,
|
||||
uint16_t duration, uint8_t right, uint8_t left) {
|
||||
require(state_lock_depth == 0, "DS5 driver dispatch must not hold the shared state lock");
|
||||
play_rumble(target, delay, duration, right, left);
|
||||
if (during_dualsense_dispatch) during_dualsense_dispatch();
|
||||
}
|
||||
|
||||
uni_hid_device_t dualsense(int index) {
|
||||
auto result = device(index, true, UNI_BT_CONN_PROTOCOL_BR_EDR);
|
||||
result.vendor_id = 0x054c;
|
||||
result.product_id = index == 0 ? 0x0ce6 : 0x0df2;
|
||||
result.controller_type = CONTROLLER_TYPE_PS5Controller;
|
||||
result.report_parser.parse_input_report = uni_hid_parser_ds5_parse_input_report;
|
||||
result.report_parser.play_dual_rumble = observe_dualsense_rumble;
|
||||
return result;
|
||||
}
|
||||
|
||||
void report_dualsense(uni_hid_device_t& pad, bool fresh_motion = true) {
|
||||
SensorFixture& sensor = sensors[pad.idx];
|
||||
sensor.device = &pad;
|
||||
sensor.valid = true;
|
||||
sensor.metadata.report_tracked = true;
|
||||
sensor.metadata.report_valid = true;
|
||||
++sensor.metadata.report_sequence;
|
||||
if (fresh_motion) {
|
||||
++sensor.metadata.accel_sequence;
|
||||
++sensor.metadata.gyro_sequence;
|
||||
}
|
||||
sensor.metadata.accel_valid = fresh_motion;
|
||||
sensor.metadata.gyro_valid = fresh_motion;
|
||||
sensor.metadata.accel_q13[1] = 8193;
|
||||
sensor.metadata.gyro_q10[2] = -123456;
|
||||
pad.controller.klass = UNI_CONTROLLER_CLASS_GAMEPAD;
|
||||
pad.controller.gamepad.buttons = BUTTON_A | BUTTON_SHOULDER_L;
|
||||
// Parser init_report may clear legacy fields; native precision comes from
|
||||
// the independent sensor provider rather than cached controller values.
|
||||
pad.controller.gamepad.accel[1] = 0;
|
||||
pad.controller.gamepad.gyro[2] = 0;
|
||||
pad.controller.battery = 176;
|
||||
platform_on_controller_data(&pad, &pad.controller);
|
||||
}
|
||||
|
||||
Bluepad32NativeGamepadSnapshot bridge_snapshot() {
|
||||
Bluepad32NativeGamepadSnapshot result{};
|
||||
bluepad32_input_backend_native_snapshot(&result);
|
||||
return result;
|
||||
}
|
||||
|
||||
void source_isolation() {
|
||||
start_pairing_backend();
|
||||
auto ordinary = device(0, true, UNI_BT_CONN_PROTOCOL_BR_EDR);
|
||||
ordinary.vendor_id = 0x054c;
|
||||
ordinary.product_id = 0x0ce6;
|
||||
#if !SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
ordinary.gamepad = false;
|
||||
#endif
|
||||
require(platform_on_device_ready(&ordinary) == UNI_ERROR_INVALID_CONTROLLER,
|
||||
"an ineligible controller must not enter dedicated output slots");
|
||||
bluepad32_input_backend_select_native_source(ordinary.conn.btaddr);
|
||||
require(!bridge_snapshot().controller.active, "an ineligible device cannot become the native source");
|
||||
platform_on_device_disconnected(&ordinary);
|
||||
bluepad32_input_backend_select_native_source(nullptr);
|
||||
auto first = dualsense(0);
|
||||
auto second = dualsense(1);
|
||||
require(platform_on_device_ready(&first) == UNI_ERROR_SUCCESS, "first DS5 must connect");
|
||||
now_ms = 100;
|
||||
report_dualsense(first);
|
||||
const auto initial = bridge_snapshot();
|
||||
require(initial.slot == 0 && initial.controller.active && initial.controller.state.button_south &&
|
||||
initial.battery == 176 && initial.accel_valid && initial.gyro_valid &&
|
||||
!initial.requires_stationary_bias && initial.accel_q13[1] == 8193 &&
|
||||
initial.gyro_q10[2] == -123456 && initial.gyro_received_us == 100000,
|
||||
"native snapshot must preserve coherent physical controls and calibrated precision");
|
||||
now_ms = 120;
|
||||
platform_on_controller_data(&first, &first.controller);
|
||||
bluepad32_input_backend_report_sent(0);
|
||||
auto snapshot = bridge_snapshot();
|
||||
require(snapshot.state_generation == initial.state_generation && snapshot.received_us == 100000 &&
|
||||
snapshot.accel_sequence == initial.accel_sequence &&
|
||||
snapshot.gyro_sequence == initial.gyro_sequence,
|
||||
"polling/cached callbacks and USB consumption must not freshen input or motion");
|
||||
report_dualsense(first, false);
|
||||
snapshot = bridge_snapshot();
|
||||
require(snapshot.received_us == 120000 && snapshot.gyro_received_us == 100000 &&
|
||||
!snapshot.accel_valid && !snapshot.gyro_valid,
|
||||
"a controls-only admission must not refresh a duplicate sensor timestamp");
|
||||
sensors[0].valid = false;
|
||||
first.controller.gamepad.buttons = 0;
|
||||
platform_on_controller_data(&first, &first.controller);
|
||||
require(bridge_snapshot().controller.state.button_south,
|
||||
"malformed parser input must not publish an invented button release");
|
||||
uint64_t old_token;
|
||||
require(bluepad32_input_backend_native_sample_request(0, 1, &old_token), "first source cue must queue");
|
||||
require(platform_on_device_ready(&second) == UNI_ERROR_SUCCESS, "Edge must connect");
|
||||
report_dualsense(second);
|
||||
require(!bridge_snapshot().controller.active &&
|
||||
bluepad32_input_backend_native_sample_result(0, old_token) == -1,
|
||||
"auto ambiguity must fail closed and retire source work immediately");
|
||||
platform_on_device_disconnected(&second);
|
||||
platform_on_controller_data(&first, &first.controller);
|
||||
require(!bridge_snapshot().controller.active, "returning to a source cannot resurrect cached state");
|
||||
report_dualsense(first);
|
||||
snapshot = bridge_snapshot();
|
||||
require(snapshot.controller.active && snapshot.controller.connection_generation != initial.controller.connection_generation,
|
||||
"a missed ambiguous interval still needs a new adapter epoch");
|
||||
platform_on_device_connected(&second);
|
||||
require(platform_on_device_ready(&second) == UNI_ERROR_SUCCESS, "Edge reconnect must succeed");
|
||||
bluepad32_input_backend_select_native_source(first.conn.btaddr);
|
||||
report_dualsense(first);
|
||||
report_dualsense(second);
|
||||
require(bridge_snapshot().slot == 0, "explicit source must ignore another live PS5");
|
||||
platform_on_device_disconnected(&first);
|
||||
require(!bridge_snapshot().controller.active, "disconnect must not migrate an explicit source");
|
||||
}
|
||||
|
||||
void stable_logical_slot() {
|
||||
start_pairing_backend();
|
||||
auto unrelated = device(0, true, UNI_BT_CONN_PROTOCOL_BR_EDR);
|
||||
#if !SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
unrelated.gamepad = false;
|
||||
#endif
|
||||
auto pad = dualsense(1);
|
||||
pad.report_parser.set_lightbar_color = set_lightbar;
|
||||
// An earlier, still-unclassified transport connection must not reserve player 1.
|
||||
platform_on_device_connected(&unrelated);
|
||||
platform_on_device_connected(&pad);
|
||||
require(platform_on_device_ready(&pad) == UNI_ERROR_SUCCESS, "DS5 must complete setup");
|
||||
now_ms = 100;
|
||||
report_dualsense(pad);
|
||||
const auto first = bridge_snapshot();
|
||||
const auto color = switch_pro_get_slot_light_color(0);
|
||||
require(first.controller.active && first.slot == 0 &&
|
||||
pad.lightbar_red == color.red && pad.lightbar_green == color.green &&
|
||||
pad.lightbar_blue == color.blue,
|
||||
"a transport-index-1 DualSense must own logical slot 0 and its lightbar color");
|
||||
require(platform_on_device_ready(&unrelated) == UNI_ERROR_INVALID_CONTROLLER,
|
||||
"dedicated DualSense mode must reject an unrelated ready controller");
|
||||
platform_on_device_disconnected(&unrelated);
|
||||
platform_on_device_connected(&unrelated);
|
||||
require(platform_on_device_ready(&unrelated) == UNI_ERROR_INVALID_CONTROLLER,
|
||||
"remembered unrelated reconnects must remain outside logical slots");
|
||||
platform_on_device_disconnected(&unrelated);
|
||||
require(bridge_snapshot().slot == 0 &&
|
||||
bridge_snapshot().controller.connection_generation == first.controller.connection_generation &&
|
||||
pad.lightbar_calls == 1,
|
||||
"unrelated connection churn must not rebind or recolor the active DualSense");
|
||||
|
||||
platform_on_device_disconnected(&pad);
|
||||
auto reconnected = dualsense(2);
|
||||
memcpy(reconnected.conn.btaddr, pad.conn.btaddr, sizeof(pad.conn.btaddr));
|
||||
reconnected.report_parser.set_lightbar_color = set_lightbar;
|
||||
platform_on_device_connected(&reconnected);
|
||||
require(platform_on_device_ready(&reconnected) == UNI_ERROR_SUCCESS, "DS5 reconnect must succeed");
|
||||
now_ms = 200;
|
||||
report_dualsense(reconnected);
|
||||
const auto next = bridge_snapshot();
|
||||
require(next.controller.active && next.slot == 0 &&
|
||||
controller_identity_equal(next.controller.identity, first.controller.identity) &&
|
||||
next.controller.connection_generation != first.controller.connection_generation &&
|
||||
reconnected.lightbar_red == color.red && reconnected.lightbar_green == color.green &&
|
||||
reconnected.lightbar_blue == color.blue,
|
||||
"reusing another Bluetooth index must preserve identity and the first logical slot");
|
||||
auto aborted = dualsense(3);
|
||||
platform_on_device_connected(&aborted);
|
||||
platform_on_device_disconnected(&aborted);
|
||||
require(platform_on_device_ready(&aborted) == UNI_ERROR_NO_SLOTS,
|
||||
"a late ready callback must not resurrect an unassigned disconnected transport");
|
||||
require(bridge_snapshot().controller.active && bridge_snapshot().slot == 0,
|
||||
"an aborted second setup must not disturb the active source");
|
||||
auto pending0 = device(0, true, UNI_BT_CONN_PROTOCOL_BR_EDR);
|
||||
auto pending1 = device(1, true, UNI_BT_CONN_PROTOCOL_BR_EDR);
|
||||
auto pending3 = device(3, true, UNI_BT_CONN_PROTOCOL_BR_EDR);
|
||||
platform_on_device_connected(&pending0);
|
||||
platform_on_device_connected(&pending1);
|
||||
platform_on_device_connected(&pending3);
|
||||
require(!incoming_connections && !scanning_enabled,
|
||||
"unclassified transports still consume physical connection capacity");
|
||||
platform_on_device_disconnected(&pending0);
|
||||
require(incoming_connections && bridge_snapshot().slot == 0 &&
|
||||
bridge_snapshot().controller.connection_generation == next.controller.connection_generation,
|
||||
"freeing pending transport capacity must not move the logical source");
|
||||
platform_on_device_disconnected(&pending1);
|
||||
platform_on_device_disconnected(&pending3);
|
||||
}
|
||||
|
||||
void cue_lifetime() {
|
||||
start_pairing_backend();
|
||||
auto pad = dualsense(0);
|
||||
require(platform_on_device_ready(&pad) == UNI_ERROR_SUCCESS, "DS5 must connect");
|
||||
uint64_t right, left, stop;
|
||||
require(bluepad32_input_backend_native_sample_request(0, 6, &right) &&
|
||||
bluepad32_input_backend_native_sample_request(1, 1, &left) && right != left &&
|
||||
bluepad32_input_backend_native_sample_result(0, right) == 0 && pad.rumble_calls == 0,
|
||||
"independent acceptance is not driver completion");
|
||||
dualsense_transport_available = false;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(bluepad32_input_backend_native_sample_result(0, right) == 0 && pad.rumble_calls == 0,
|
||||
"a busy source driver must not count as dispatch completion");
|
||||
dualsense_transport_available = true;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(pad.last_high == 96 && pad.last_low == 160 && pad.last_rumble_duration_ms == 60 &&
|
||||
bluepad32_input_backend_native_sample_result(0, right) == 1 &&
|
||||
bluepad32_input_backend_native_sample_result(1, right) == -1 &&
|
||||
bluepad32_input_backend_native_sample_result(1, left) == 1,
|
||||
"combined output must route R weak/right and L strong/left with the shortest safe timer");
|
||||
now_ms = 60;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(pad.last_high == 0 && pad.last_low == 160 && pad.last_rumble_duration_ms == 940,
|
||||
"ending right must preserve only the left pulse's original remaining lifetime");
|
||||
require(bluepad32_input_backend_native_sample_request(0, 3, &right), "right can restart independently");
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
now_ms = 85;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(pad.last_high == 0 && pad.last_low == 160, "right gap must not stop the left motor");
|
||||
now_ms = 175;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(pad.last_high == 96 && pad.last_low == 160, "later pulse must resume after its gap");
|
||||
require(bluepad32_input_backend_native_sample_request(1, 0, &stop), "left stop must replace only left");
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(pad.last_high == 96 && pad.last_low == 0 &&
|
||||
bluepad32_input_backend_native_sample_result(1, stop) == 1 &&
|
||||
bluepad32_input_backend_native_sample_result(1, left) == -1,
|
||||
"a side stop needs actual dispatch and cannot stop its sibling");
|
||||
now_ms = 200;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(pad.last_rumble_duration_ms == 0, "final pulse expiry must release both motors");
|
||||
const int stopped = pad.rumble_calls;
|
||||
now_ms = 5000;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(pad.rumble_calls == stopped, "expired pulses must never replay after a stall");
|
||||
require(bluepad32_input_backend_native_sample_request(0, 1, &right), "pending timeout cue must queue");
|
||||
now_ms += 2000;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(bluepad32_input_backend_native_sample_result(0, right) == -1 && pad.rumble_calls == stopped,
|
||||
"an undispatched expired cue must fail without producing a late pulse");
|
||||
}
|
||||
|
||||
void cue_races() {
|
||||
start_pairing_backend();
|
||||
auto pad = dualsense(0);
|
||||
require(platform_on_device_ready(&pad) == UNI_ERROR_SUCCESS, "DS5 must connect");
|
||||
uint64_t token;
|
||||
require(bluepad32_input_backend_native_sample_request(0, 1, &token), "race cue must queue");
|
||||
during_dualsense_dispatch = [] { bluepad32_input_backend_native_sample_cancel(0); };
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
during_dualsense_dispatch = nullptr;
|
||||
require(bluepad32_input_backend_native_sample_result(0, token) == -1,
|
||||
"cancellation during dispatch must defeat a late completion");
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(pad.last_rumble_duration_ms == 0, "in-flight cancellation must retain a bounded stop obligation");
|
||||
require(bluepad32_input_backend_native_sample_request(1, 1, &token), "reselection race must queue");
|
||||
during_dualsense_dispatch = [] { bluepad32_input_backend_select_native_source(nullptr); };
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
during_dualsense_dispatch = nullptr;
|
||||
require(bluepad32_input_backend_native_sample_result(1, token) == -1,
|
||||
"reselection must retire an in-flight token even for the same physical source");
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(pad.last_rumble_duration_ms == 0, "reselection cannot orphan the just-dispatched motor");
|
||||
require(bluepad32_input_backend_native_sample_request(0, 1, &token), "disconnect race must queue");
|
||||
platform_on_device_disconnected(&pad);
|
||||
auto replacement = dualsense(0);
|
||||
require(platform_on_device_ready(&replacement) == UNI_ERROR_SUCCESS, "replacement must connect");
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(bluepad32_input_backend_native_sample_result(0, token) == -1 && replacement.rumble_calls == 0,
|
||||
"old tokens and deferred stops must never enter a replacement connection");
|
||||
}
|
||||
void report_gamepad(uni_hid_device_t& pad) {
|
||||
pad.controller.klass = UNI_CONTROLLER_CLASS_GAMEPAD;
|
||||
platform_on_controller_data(&pad, &pad.controller);
|
||||
}
|
||||
|
||||
SensorFixture& motion_fixture(uni_hid_device_t& pad, bool tracked = true) {
|
||||
SensorFixture& fixture = sensors[pad.idx];
|
||||
fixture = {};
|
||||
fixture.device = &pad;
|
||||
fixture.valid = true;
|
||||
fixture.metadata.report_tracked = tracked;
|
||||
fixture.metadata.report_valid = true;
|
||||
fixture.metadata.report_sequence = 1;
|
||||
fixture.metadata.accel_sequence = 1;
|
||||
fixture.metadata.gyro_sequence = 1;
|
||||
fixture.metadata.accel_valid = true;
|
||||
fixture.metadata.gyro_valid = true;
|
||||
fixture.metadata.accel_q13[1] = 8193;
|
||||
fixture.metadata.gyro_q10[2] = -123456;
|
||||
return fixture;
|
||||
}
|
||||
|
||||
void sensorless_admission() {
|
||||
start_pairing_backend();
|
||||
auto xbox = device(2, true, UNI_BT_CONN_PROTOCOL_BR_EDR);
|
||||
xbox.controller_type = CONTROLLER_TYPE_XBoxOneController;
|
||||
xbox.report_parser.play_dual_rumble = nullptr;
|
||||
platform_on_device_connected(&xbox);
|
||||
require(platform_on_device_ready(&xbox) == UNI_ERROR_SUCCESS,
|
||||
"normal gamepad admission must not require a motion parser");
|
||||
xbox.controller.gamepad.buttons = BUTTON_A;
|
||||
xbox.controller.gamepad.accel[1] = 8192;
|
||||
xbox.controller.gamepad.gyro[0] = 65536;
|
||||
now_ms = 100;
|
||||
report_gamepad(xbox);
|
||||
const auto initial = bridge_snapshot();
|
||||
uint64_t cue = 99;
|
||||
require(initial.controller.active && initial.slot == 0 &&
|
||||
initial.controller.state.button_south && !initial.accel_valid &&
|
||||
!initial.gyro_valid && !initial.requires_stationary_bias &&
|
||||
initial.accel_sequence == 0 && initial.gyro_sequence == 0 &&
|
||||
!bluepad32_input_backend_native_sample_request(0, 1, &cue) && cue == 0,
|
||||
"sensorless controls remain live without invented IMU or rumble capability");
|
||||
auto generic = device(0, true, UNI_BT_CONN_PROTOCOL_BLE);
|
||||
platform_on_device_connected(&generic);
|
||||
require(platform_on_device_ready(&generic) == UNI_ERROR_SUCCESS,
|
||||
"unknown-family normal AIO gamepads must not face a native brand whitelist");
|
||||
report_gamepad(generic);
|
||||
require(!bridge_snapshot().controller.active, "two logical gamepads are ambiguous");
|
||||
bluepad32_input_backend_select_native_source(xbox.conn.btaddr);
|
||||
now_ms = 110;
|
||||
report_gamepad(xbox);
|
||||
require(bridge_snapshot().controller.active && bridge_snapshot().slot == 0 &&
|
||||
controller_identity_equal(bridge_snapshot().controller.identity, initial.controller.identity),
|
||||
"explicit sensorless selection retains its real identity and logical slot");
|
||||
platform_on_device_disconnected(&xbox);
|
||||
report_gamepad(generic);
|
||||
require(!bridge_snapshot().controller.active, "explicit selection cannot migrate on disconnect");
|
||||
bluepad32_input_backend_select_native_source(nullptr);
|
||||
generic.controller.gamepad.buttons = BUTTON_B;
|
||||
report_gamepad(generic);
|
||||
require(bridge_snapshot().controller.active && bridge_snapshot().controller.state.button_east &&
|
||||
!bridge_snapshot().controller.state.button_south,
|
||||
"unique sensorless reselection must publish only the new source's controls");
|
||||
}
|
||||
|
||||
void independent_motion() {
|
||||
start_pairing_backend();
|
||||
auto ds4 = device(0, true, UNI_BT_CONN_PROTOCOL_BR_EDR);
|
||||
ds4.controller_type = CONTROLLER_TYPE_PS4Controller;
|
||||
require(platform_on_device_ready(&ds4) == UNI_ERROR_SUCCESS, "DS4 controls must be admitted");
|
||||
auto& ds = motion_fixture(ds4).metadata;
|
||||
ds4.controller.gamepad.buttons = BUTTON_A;
|
||||
now_ms = 100;
|
||||
report_gamepad(ds4);
|
||||
const auto first = bridge_snapshot();
|
||||
require(first.accel_valid && first.gyro_valid && !first.requires_stationary_bias &&
|
||||
first.accel_q13[1] == 8193 && first.gyro_q10[2] == -123456,
|
||||
"calibrated DS4 motion keeps raw precision without Wii settling");
|
||||
now_ms = 110;
|
||||
++ds.report_sequence;
|
||||
++ds.accel_sequence;
|
||||
report_gamepad(ds4);
|
||||
require(bridge_snapshot().accel_received_us == 110000 &&
|
||||
bridge_snapshot().gyro_received_us == 100000 && bridge_snapshot().gyro_valid,
|
||||
"acceleration ingress must not refresh an unchanged gyro");
|
||||
now_ms = 120;
|
||||
ds.report_valid = false;
|
||||
ds4.controller.gamepad.buttons = 0;
|
||||
report_gamepad(ds4);
|
||||
require(bridge_snapshot().controller.state.button_south &&
|
||||
bridge_snapshot().received_us == 110000,
|
||||
"a tracked partial/malformed report cannot publish phantom releases");
|
||||
ds.report_valid = true;
|
||||
++ds.report_sequence;
|
||||
ds.gyro_valid = false;
|
||||
report_gamepad(ds4);
|
||||
require(!bridge_snapshot().gyro_valid && bridge_snapshot().accel_valid,
|
||||
"gyro capability loss must not suppress working acceleration or controls");
|
||||
bluepad32_input_backend_select_native_source(nullptr);
|
||||
++ds.report_sequence;
|
||||
ds.gyro_valid = true;
|
||||
report_gamepad(ds4);
|
||||
require(bridge_snapshot().controller.active && !bridge_snapshot().accel_valid &&
|
||||
!bridge_snapshot().gyro_valid,
|
||||
"reselection cannot rehabilitate cached sensor sequences");
|
||||
++ds.report_sequence;
|
||||
++ds.gyro_sequence;
|
||||
report_gamepad(ds4);
|
||||
require(bridge_snapshot().gyro_valid && !bridge_snapshot().accel_valid,
|
||||
"each sensor must earn freshness separately in a new source epoch");
|
||||
platform_on_device_disconnected(&ds4);
|
||||
|
||||
auto pro = device(1, true, UNI_BT_CONN_PROTOCOL_BR_EDR);
|
||||
pro.controller_type = CONTROLLER_TYPE_SwitchProController;
|
||||
require(platform_on_device_ready(&pro) == UNI_ERROR_SUCCESS, "Switch Pro controls must be admitted");
|
||||
auto& sw = motion_fixture(pro).metadata;
|
||||
sw.accel_valid = sw.gyro_valid = false;
|
||||
pro.controller.gamepad.buttons = BUTTON_B;
|
||||
report_gamepad(pro);
|
||||
require(bridge_snapshot().controller.state.button_east && !bridge_snapshot().gyro_valid,
|
||||
"Switch controls must not wait for motion calibration");
|
||||
++sw.report_sequence;
|
||||
++sw.accel_sequence;
|
||||
++sw.gyro_sequence;
|
||||
sw.accel_valid = sw.gyro_valid = true;
|
||||
report_gamepad(pro);
|
||||
require(bridge_snapshot().accel_valid && bridge_snapshot().gyro_valid &&
|
||||
!bridge_snapshot().requires_stationary_bias,
|
||||
"validated Switch motion starts without Wii stationary settling");
|
||||
platform_on_device_disconnected(&pro);
|
||||
|
||||
auto remote = wii_device(2);
|
||||
require(platform_on_device_ready(&remote) == UNI_ERROR_SUCCESS, "Wii controls must be admitted");
|
||||
auto& wii = motion_fixture(remote, false).metadata;
|
||||
wii.gyro_valid = false;
|
||||
remote.controller.gamepad.buttons = BUTTON_A;
|
||||
now_ms = 200;
|
||||
report_gamepad(remote);
|
||||
require(bridge_snapshot().controller.state.button_south && bridge_snapshot().accel_valid &&
|
||||
!bridge_snapshot().gyro_valid && bridge_snapshot().requires_stationary_bias,
|
||||
"Wii without MotionPlus remains acceleration-capable, not a fabricated full IMU");
|
||||
now_ms = 210;
|
||||
++wii.gyro_sequence;
|
||||
wii.gyro_valid = true;
|
||||
report_gamepad(remote);
|
||||
require(bridge_snapshot().gyro_valid && bridge_snapshot().gyro_received_us == 210000 &&
|
||||
bridge_snapshot().accel_received_us == 200000 &&
|
||||
bridge_snapshot().requires_stationary_bias,
|
||||
"Wii MotionPlus ingress must retain independent acceleration age and Wii bias policy");
|
||||
}
|
||||
|
||||
void paired_source() {
|
||||
start_pairing_backend();
|
||||
auto left = switch2_device(2, UNI_SW2_JOYCON_L_PID);
|
||||
auto right = switch2_device(1, UNI_SW2_JOYCON_R_PID);
|
||||
ready_switch2(left);
|
||||
ready_switch2(right);
|
||||
auto& r = motion_fixture(right).metadata;
|
||||
auto& l = motion_fixture(left).metadata;
|
||||
right.controller.gamepad.buttons = BUTTON_A;
|
||||
left.controller.gamepad.dpad = DPAD_LEFT;
|
||||
now_ms = 100;
|
||||
report_gamepad(right);
|
||||
const auto initial = bridge_snapshot();
|
||||
require_pair_owner(initial.controller.identity, left, right);
|
||||
require(initial.controller.active && initial.slot == 0 && initial.gyro_valid &&
|
||||
!slot_snapshot(1).active && !slot_snapshot(2).active,
|
||||
"an existing Joy-Con pair is one logical native source, not auto ambiguity");
|
||||
now_ms = 110;
|
||||
l.gyro_q10[2] = 654321;
|
||||
report_gamepad(left);
|
||||
require(bridge_snapshot().controller.state.button_south &&
|
||||
bridge_snapshot().controller.state.dpad_left &&
|
||||
bridge_snapshot().received_us == 110000 &&
|
||||
bridge_snapshot().gyro_received_us == 100000 &&
|
||||
bridge_snapshot().gyro_q10[2] == initial.gyro_q10[2],
|
||||
"left controls merge without refreshing or replacing the right motion owner");
|
||||
bluepad32_input_backend_select_native_source(right.conn.btaddr);
|
||||
++l.report_sequence;
|
||||
report_gamepad(left);
|
||||
require(bridge_snapshot().controller.active && !bridge_snapshot().gyro_valid,
|
||||
"either member address selects the logical pair, not that member's cached IMU");
|
||||
++r.report_sequence;
|
||||
report_gamepad(right);
|
||||
require(!bridge_snapshot().gyro_valid, "reselection cannot freshen the right parser cache");
|
||||
++r.report_sequence;
|
||||
++r.accel_sequence;
|
||||
++r.gyro_sequence;
|
||||
report_gamepad(right);
|
||||
require(bridge_snapshot().gyro_valid, "new right samples restore paired motion");
|
||||
uint64_t rc, lc, stop;
|
||||
require(bluepad32_input_backend_native_sample_request(0, 6, &rc) &&
|
||||
bluepad32_input_backend_native_sample_request(1, 1, &lc),
|
||||
"both real halves expose their own driver capability");
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(right.last_high == 96 && right.last_low == 96 &&
|
||||
left.last_high == 160 && left.last_low == 160 &&
|
||||
bluepad32_input_backend_native_sample_result(0, rc) == 1 &&
|
||||
bluepad32_input_backend_native_sample_result(1, lc) == 1,
|
||||
"paired cues must dispatch only each half's contribution to its real driver");
|
||||
now_ms = 120;
|
||||
require(bluepad32_input_backend_native_sample_request(0, 0, &stop), "right stop must queue");
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(right.last_rumble_duration_ms == 0 && left.last_rumble_duration_ms == 990 &&
|
||||
bluepad32_input_backend_native_sample_result(0, stop) == 1,
|
||||
"stopping one paired side preserves the other side's original finite deadline");
|
||||
bluepad32_input_backend_select_native_source(nullptr);
|
||||
set_runtime_joycon_mode(JoyConMode::kIndividual);
|
||||
require(!bridge_snapshot().controller.active &&
|
||||
bluepad32_input_backend_native_sample_result(1, lc) == -1,
|
||||
"live split retires the pair immediately and fails auto selection closed");
|
||||
bluepad32_input_backend_select_native_source(right.conn.btaddr);
|
||||
++r.report_sequence;
|
||||
++r.accel_sequence;
|
||||
++r.gyro_sequence;
|
||||
report_gamepad(right);
|
||||
require(bridge_snapshot().controller.active &&
|
||||
controller_identity_equal(bridge_snapshot().controller.identity, identity_for_device(&right)),
|
||||
"explicit selection after a split retains the real surviving member identity");
|
||||
}
|
||||
|
||||
void pair_cue_races() {
|
||||
start_pairing_backend();
|
||||
auto left = switch2_device(0, UNI_SW2_JOYCON_L_PID);
|
||||
auto right = switch2_device(1, UNI_SW2_JOYCON_R_PID);
|
||||
ready_switch2(left);
|
||||
ready_switch2(right);
|
||||
left.report_parser.play_dual_rumble = nullptr;
|
||||
uint64_t rc, lc;
|
||||
require(!bluepad32_input_backend_native_sample_request(1, 1, &lc) && lc == 0,
|
||||
"a paired half without a rumble driver cannot borrow its sibling's capability");
|
||||
left.report_parser.play_dual_rumble = play_rumble;
|
||||
right.report_parser.play_dual_rumble = [](
|
||||
uni_hid_device_t* pad, uint16_t delay, uint16_t duration, uint8_t weak, uint8_t strong) {
|
||||
require(state_lock_depth == 0, "paired driver dispatch must not hold the shared lock");
|
||||
play_rumble(pad, delay, duration, weak, strong);
|
||||
bluepad32_input_backend_native_sample_cancel(1);
|
||||
};
|
||||
require(bluepad32_input_backend_native_sample_request(0, 1, &rc) &&
|
||||
bluepad32_input_backend_native_sample_request(1, 1, &lc), "paired race cues must queue");
|
||||
const int before = left.rumble_calls;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(left.rumble_calls == before && right.last_rumble_duration_ms == 1000 &&
|
||||
bluepad32_input_backend_native_sample_result(0, rc) == 1 &&
|
||||
bluepad32_input_backend_native_sample_result(1, lc) == -1,
|
||||
"canceling L during R dispatch must not submit stale L or retire the accepted R cue");
|
||||
bluepad32_input_backend_native_sample_cancel(0);
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
const int before_stall = left.rumble_calls;
|
||||
require(right.last_rumble_duration_ms == 0, "partial pair submission retains a real stop obligation");
|
||||
right.report_parser.play_dual_rumble = [](
|
||||
uni_hid_device_t* pad, uint16_t delay, uint16_t duration, uint8_t weak, uint8_t strong) {
|
||||
play_rumble(pad, delay, duration, weak, strong);
|
||||
now_ms += 2000;
|
||||
bluepad32_input_backend_select_native_source(nullptr);
|
||||
};
|
||||
require(bluepad32_input_backend_native_sample_request(0, 1, &rc) &&
|
||||
bluepad32_input_backend_native_sample_request(1, 1, &lc), "reselection race cues must queue");
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
right.report_parser.play_dual_rumble = play_rumble;
|
||||
require(left.rumble_calls == before_stall &&
|
||||
bluepad32_input_backend_native_sample_result(0, rc) == -1 &&
|
||||
bluepad32_input_backend_native_sample_result(1, lc) == -1,
|
||||
"reselection and a dispatch stall cannot submit the old epoch's remaining half");
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(right.last_rumble_duration_ms == 0, "reselection cannot orphan a partial pair output");
|
||||
}
|
||||
|
||||
uint8_t mono_magnitude = 0;
|
||||
void observe_mono_rumble(uni_hid_device_t* pad, uint16_t delay, uint16_t duration,
|
||||
uint8_t weak, uint8_t strong) {
|
||||
require(state_lock_depth == 0 && delay == 0, "mono driver dispatch is immediate and outside the lock");
|
||||
mono_magnitude = duration == 0 ? 0 : (weak > strong ? weak : strong);
|
||||
play_rumble(pad, delay, duration, mono_magnitude, 0);
|
||||
}
|
||||
|
||||
void mono_rumble() {
|
||||
start_pairing_backend();
|
||||
auto move = device(0, true, UNI_BT_CONN_PROTOCOL_BR_EDR);
|
||||
move.controller_type = CONTROLLER_TYPE_PSMoveController;
|
||||
move.report_parser.play_dual_rumble = observe_mono_rumble;
|
||||
require(platform_on_device_ready(&move) == UNI_ERROR_SUCCESS, "mono-rumble gamepad must connect");
|
||||
uint64_t right, left;
|
||||
require(bluepad32_input_backend_native_sample_request(0, 1, &right) &&
|
||||
bluepad32_input_backend_native_sample_request(1, 7, &left),
|
||||
"mono devices accept independent logical contributions, not separate actuators");
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(mono_magnitude == 220 && move.last_rumble_duration_ms == 120,
|
||||
"one mono actuator combines both contributions with the shortest finite boundary");
|
||||
bluepad32_input_backend_native_sample_cancel(1);
|
||||
now_ms = 40;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(mono_magnitude == 160 && move.last_rumble_duration_ms == 960 &&
|
||||
bluepad32_input_backend_native_sample_result(1, left) == -1,
|
||||
"canceling one mono contribution must preserve the other's remaining pulse");
|
||||
now_ms = 1000;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(mono_magnitude == 0, "the last mono contribution must stop at its original deadline");
|
||||
require(bluepad32_input_backend_native_sample_request(0, 1, &right), "new mono cue must queue");
|
||||
const int calls = move.rumble_calls;
|
||||
now_ms += 2000;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(move.rumble_calls == calls && bluepad32_input_backend_native_sample_result(0, right) == -1,
|
||||
"expired undispatched mono work cannot replay after a timer stall");
|
||||
require(bluepad32_input_backend_native_sample_request(1, 1, &left), "disconnect cue must queue");
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(mono_magnitude == 160, "disconnect regression must own a real active driver pulse");
|
||||
platform_on_device_disconnected(&move);
|
||||
require(mono_magnitude == 0 && move.last_rumble_duration_ms == 0,
|
||||
"disconnect must retire the source driver's timer before parser memory reuse");
|
||||
auto replacement = device(0, true, UNI_BT_CONN_PROTOCOL_BR_EDR);
|
||||
replacement.report_parser.play_dual_rumble = observe_mono_rumble;
|
||||
require(platform_on_device_ready(&replacement) == UNI_ERROR_SUCCESS, "new mono connection must succeed");
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(replacement.rumble_calls == 0 &&
|
||||
bluepad32_input_backend_native_sample_result(1, left) == -1,
|
||||
"retired mono work must never enter a replacement connection");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" void uni_hid_parser_ds5_parse_input_report(uni_hid_device_t*, const uint8_t*, uint16_t) {}
|
||||
extern "C" bool uni_hid_parser_ds5_bridge_rumble(
|
||||
uni_hid_device_t* pad, uint16_t duration, uint8_t right, uint8_t left) {
|
||||
if (!dualsense_transport_available) return false;
|
||||
observe_dualsense_rumble(pad, 0, duration, right, left);
|
||||
return true;
|
||||
}
|
||||
extern "C" bool uni_hid_parser_native_motion_snapshot(
|
||||
uni_hid_device_t* pad, uni_native_motion_snapshot_t* out) {
|
||||
*out = {};
|
||||
for (const auto& fixture : sensors) {
|
||||
if (fixture.device != pad) continue;
|
||||
*out = fixture.metadata;
|
||||
if (!fixture.valid) out->report_valid = false;
|
||||
return fixture.valid;
|
||||
}
|
||||
// DS5 must be guarded even before its first good full report.
|
||||
out->report_tracked =
|
||||
pad->report_parser.parse_input_report == uni_hid_parser_ds5_parse_input_report;
|
||||
return false;
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
require(argc == 2, "scenario required");
|
||||
const std::string scenario = argv[1];
|
||||
if (scenario == "source-isolation") source_isolation();
|
||||
else if (scenario == "cue-lifetime") cue_lifetime();
|
||||
else if (scenario == "cue-races") cue_races();
|
||||
else if (scenario == "stable-logical-slot") stable_logical_slot();
|
||||
else if (scenario == "sensorless-admission") sensorless_admission();
|
||||
else if (scenario == "independent-motion") independent_motion();
|
||||
else if (scenario == "paired-source") paired_source();
|
||||
else if (scenario == "pair-cue-races") pair_cue_races();
|
||||
else if (scenario == "mono-rumble") mono_rumble();
|
||||
else require(false, "unknown native gamepad scenario");
|
||||
return 0;
|
||||
}
|
||||
264
tests/native_gamepad_motion_parser_test.c
Normal file
264
tests/native_gamepad_motion_parser_test.c
Normal file
|
|
@ -0,0 +1,264 @@
|
|||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "parser/uni_hid_parser_ds4.h"
|
||||
#include "parser/uni_hid_parser_psmove.h"
|
||||
#include "parser/uni_hid_parser_switch.h"
|
||||
#include "parser/uni_hid_parser_native_motion.h"
|
||||
#include "uni_hid_device.h"
|
||||
#include "uni_utils.h"
|
||||
|
||||
// Real staged parsers and normalization. Only radio, clock and platform edges
|
||||
// are substituted; fixtures enter public setup/feature/input APIs.
|
||||
static uint8_t command[128];
|
||||
static unsigned command_len, ready_count;
|
||||
void uni_log(const char* fmt, ...) { (void)fmt; }
|
||||
void printf_hexdump(const void* data, int len) { (void)data; (void)len; }
|
||||
void uni_hid_device_send_ctrl_report(uni_hid_device_t* d, const uint8_t* bytes, uint16_t len) {
|
||||
(void)d; assert(len <= sizeof(command)); memcpy(command, bytes, len); command_len = len;
|
||||
}
|
||||
void uni_hid_device_send_intr_report(uni_hid_device_t* d, const uint8_t* bytes, uint16_t len) {
|
||||
uni_hid_device_send_ctrl_report(d, bytes, len);
|
||||
}
|
||||
int l2cap_can_send_packet_now(uint16_t cid) { (void)cid; return 1; }
|
||||
int l2cap_send(uint16_t cid, uint8_t* bytes, uint16_t len) {
|
||||
(void)cid; uni_hid_device_send_intr_report(NULL, bytes, len); return ERROR_CODE_SUCCESS;
|
||||
}
|
||||
uint8_t l2cap_request_can_send_now_event(uint16_t cid) { (void)cid; return 0; }
|
||||
uint32_t btstack_run_loop_get_time_ms(void) { return 0; }
|
||||
void btstack_run_loop_set_timer(btstack_timer_source_t* t, uint32_t ms) { (void)t; (void)ms; }
|
||||
void btstack_run_loop_add_timer(btstack_timer_source_t* t) { (void)t; }
|
||||
bool btstack_run_loop_remove_timer(btstack_timer_source_t* t) { (void)t; return false; }
|
||||
void btstack_run_loop_set_timer_context(btstack_timer_source_t* t, void* p) { t->context = p; }
|
||||
void btstack_run_loop_set_timer_handler(btstack_timer_source_t* t, void (*fn)(btstack_timer_source_t*)) { t->process = fn; }
|
||||
void* btstack_run_loop_get_timer_context(btstack_timer_source_t* t) { return t->context; }
|
||||
bool uni_hid_device_set_ready_complete(uni_hid_device_t* d) { (void)d; ++ready_count; return true; }
|
||||
uni_hid_device_t* uni_hid_device_create_virtual(uni_hid_device_t* d) { (void)d; return NULL; }
|
||||
void uni_hid_device_set_cod(uni_hid_device_t* d, uint32_t cod) { (void)d; (void)cod; }
|
||||
void uni_hid_device_connect(uni_hid_device_t* d) { (void)d; }
|
||||
void uni_hid_device_process_controller(uni_hid_device_t* d) { (void)d; }
|
||||
void uni_hid_device_set_product_id(uni_hid_device_t* d, uint16_t pid) { d->product_id = pid; }
|
||||
void uni_hid_device_set_vendor_id(uni_hid_device_t* d, uint16_t vid) { d->vendor_id = vid; }
|
||||
uint8_t uni_hid_parser_hat_to_dpad(uint8_t hat) { return hat == 0 ? DPAD_UP : 0; }
|
||||
void uni_hid_parser_wii_setup(uni_hid_device_t* d) { (void)d; assert(false); }
|
||||
bool uni_hid_parser_wii_accel_snapshot(uni_hid_device_t* d, int32_t v[3], uint32_t* s) {
|
||||
(void)d; (void)v; (void)s; assert(false); return false;
|
||||
}
|
||||
bool uni_hid_parser_wii_gyro_snapshot(uni_hid_device_t* d, int32_t v[3], uint32_t* s) {
|
||||
(void)d; (void)v; (void)s; assert(false); return false;
|
||||
}
|
||||
static void put16(uint8_t* bytes, int value) {
|
||||
bytes[0] = (uint16_t)value; bytes[1] = (uint16_t)value >> 8;
|
||||
}
|
||||
static uni_native_motion_snapshot_t snapshot(uni_hid_device_t* d) {
|
||||
uni_native_motion_snapshot_t result;
|
||||
assert(uni_hid_parser_native_motion_snapshot(d, &result));
|
||||
return result;
|
||||
}
|
||||
static void ds4_seal(uint8_t bytes[78]) {
|
||||
const uint8_t transaction = 0xa1;
|
||||
uint32_t crc = ~uni_crc32_le(uni_crc32_le(UINT32_MAX, &transaction, 1), bytes, 74);
|
||||
for (unsigned i = 0; i < 4; ++i) bytes[74 + i] = crc >> (8 * i);
|
||||
}
|
||||
static void ds4_input(uni_hid_device_t* d, uint8_t bytes[78], uint16_t tick) {
|
||||
put16(bytes + 12, tick);
|
||||
ds4_seal(bytes);
|
||||
uni_hid_parser_ds4_init_report(d);
|
||||
uni_hid_parser_ds4_parse_input_report(d, bytes, 78);
|
||||
}
|
||||
static void ds4_provenance(void) {
|
||||
uni_hid_device_t d = {0};
|
||||
d.report_parser.setup = uni_hid_parser_ds4_setup;
|
||||
uni_hid_parser_ds4_setup(&d);
|
||||
uint8_t bytes[78] = {0x11};
|
||||
bytes[7] = 0x28; // Cross, neutral hat.
|
||||
put16(bytes + 19, 8192); // Gyro Z.
|
||||
put16(bytes + 23, 8192); // Acceleration Y.
|
||||
ds4_input(&d, bytes, 65530);
|
||||
uni_native_motion_snapshot_t first = snapshot(&d);
|
||||
assert(first.report_valid && !first.accel_valid && !first.gyro_valid);
|
||||
assert(d.controller.gamepad.buttons & BUTTON_A);
|
||||
uint8_t calibration[37] = {2};
|
||||
for (unsigned i = 0; i < 3; ++i) {
|
||||
put16(calibration + 7 + 2 * i, 100);
|
||||
put16(calibration + 13 + 2 * i, -100);
|
||||
put16(calibration + 23 + 4 * i, 8192);
|
||||
put16(calibration + 25 + 4 * i, -8192);
|
||||
}
|
||||
put16(calibration + 19, 100); put16(calibration + 21, 100);
|
||||
uni_hid_parser_ds4_parse_feature_report(&d, calibration, 36);
|
||||
ds4_input(&d, bytes, 65531);
|
||||
assert(!snapshot(&d).accel_valid);
|
||||
uni_hid_parser_ds4_parse_feature_report(&d, calibration, 37);
|
||||
ds4_input(&d, bytes, 65531);
|
||||
assert(!snapshot(&d).accel_valid); // Calibration cannot rejuvenate a cached tick.
|
||||
ds4_input(&d, bytes, 2); // Wrapped sensor clock is forward progress.
|
||||
first = snapshot(&d);
|
||||
assert(first.accel_valid && first.gyro_valid && first.accel_q13[1] == 8192);
|
||||
ds4_input(&d, bytes, 2);
|
||||
assert(snapshot(&d).accel_sequence == first.accel_sequence);
|
||||
ds4_input(&d, bytes, 1);
|
||||
assert(snapshot(&d).gyro_sequence == first.gyro_sequence);
|
||||
bytes[7] ^= 0x20; // Corrupt input must not be admitted as button release.
|
||||
uni_hid_parser_ds4_parse_input_report(&d, bytes, 78);
|
||||
assert(!snapshot(&d).report_valid);
|
||||
assert(snapshot(&d).accel_sequence == first.accel_sequence);
|
||||
uint8_t buttons[10] = {1};
|
||||
uni_hid_parser_ds4_init_report(&d);
|
||||
uni_hid_parser_ds4_parse_input_report(&d, buttons, sizeof(buttons));
|
||||
assert(snapshot(&d).report_valid && snapshot(&d).accel_sequence == first.accel_sequence);
|
||||
// Valid accel calibration with degenerate gyro extrema is accel-only.
|
||||
memset(calibration + 7, 0, 12);
|
||||
uni_hid_parser_ds4_parse_feature_report(&d, calibration, 37);
|
||||
ds4_input(&d, bytes, 3);
|
||||
assert(snapshot(&d).accel_valid && !snapshot(&d).gyro_valid);
|
||||
uni_hid_parser_native_motion_forget(&d);
|
||||
uni_hid_parser_ds4_setup(&d); // Same pointer, new parser lifetime.
|
||||
assert(!snapshot(&d).report_valid && !snapshot(&d).accel_valid);
|
||||
uni_hid_parser_ds4_parse_feature_report(&d, calibration, 37);
|
||||
ds4_input(&d, bytes, 3);
|
||||
assert(snapshot(&d).accel_sequence != first.accel_sequence);
|
||||
uni_hid_parser_native_motion_forget(&d);
|
||||
}
|
||||
|
||||
static void move_calibration(uni_hid_device_t* d, bool zcm2, bool valid) {
|
||||
uint8_t blob[143] = {0};
|
||||
const uint8_t lo1[] = {0x0a, 0x24, 0x14}, hi1[] = {0x16, 0x1e, 0x08};
|
||||
const uint8_t lo2[] = {0x08, 0x16, 0x24}, hi2[] = {0x02, 0x10, 0x1e};
|
||||
const uint8_t bias1[] = {0x2a, 0x2c, 0x2e}, high1[] = {0x46, 0x50, 0x5a};
|
||||
const uint8_t bias2[] = {0x26, 0x28, 0x2a}, high2[] = {0x30, 0x38, 0x40}, low2[] = {0x42, 0x4a, 0x52};
|
||||
for (unsigned i = 0; i < 3; ++i) {
|
||||
int center = zcm2 ? 0 : 0x8000;
|
||||
put16(blob + (zcm2 ? lo2[i] : lo1[i]), center - (valid ? 1000 : 0));
|
||||
put16(blob + (zcm2 ? hi2[i] : hi1[i]), center + (valid ? 1000 : 0));
|
||||
put16(blob + (zcm2 ? bias2[i] : bias1[i]), center);
|
||||
put16(blob + (zcm2 ? high2[i] : high1[i]), center + 1000);
|
||||
if (zcm2) put16(blob + low2[i], -1000);
|
||||
}
|
||||
blob[0] = 0x10;
|
||||
uni_hid_parser_psmove_parse_feature_report(d, blob, 49);
|
||||
uint8_t continuation[49] = {0x10, zcm2 ? 0x81 : 1};
|
||||
memcpy(continuation + 2, blob + 49, 47);
|
||||
uni_hid_parser_psmove_parse_feature_report(d, continuation, 49);
|
||||
if (!zcm2) {
|
||||
continuation[1] = 0x82;
|
||||
memcpy(continuation + 2, blob + 96, 47);
|
||||
uni_hid_parser_psmove_parse_feature_report(d, continuation, 49);
|
||||
}
|
||||
}
|
||||
static void move_provenance(bool zcm2) {
|
||||
uni_hid_device_t d = {0};
|
||||
d.report_parser.setup = uni_hid_parser_psmove_setup;
|
||||
d.product_id = zcm2 ? 0x0c5e : 0x03d5;
|
||||
uni_hid_parser_psmove_setup(&d);
|
||||
uint8_t report[49] = {1};
|
||||
report[2] = 0x40; // Cross.
|
||||
report[43] = 1;
|
||||
for (unsigned i = 0; i < 12; ++i)
|
||||
put16(report + 13 + 2 * i, (zcm2 ? 0 : 0x8000) + 1000);
|
||||
uni_hid_parser_psmove_parse_input_report(&d, report, sizeof(report));
|
||||
assert(snapshot(&d).report_valid && !snapshot(&d).accel_valid);
|
||||
move_calibration(&d, zcm2, false);
|
||||
report[43] = 2;
|
||||
uni_hid_parser_psmove_parse_input_report(&d, report, sizeof(report));
|
||||
assert(!snapshot(&d).accel_valid && !snapshot(&d).gyro_valid);
|
||||
move_calibration(&d, zcm2, true);
|
||||
uni_hid_parser_psmove_parse_input_report(&d, report, sizeof(report));
|
||||
assert(!snapshot(&d).accel_valid);
|
||||
report[43] = 3;
|
||||
uni_hid_parser_psmove_parse_input_report(&d, report, sizeof(report));
|
||||
uni_native_motion_snapshot_t first = snapshot(&d);
|
||||
assert(first.accel_valid && first.gyro_valid && first.accel_q13[0] == 8192);
|
||||
assert(first.gyro_q10[0] == (zcm2 ? 540 : 480) * 1024);
|
||||
uni_hid_parser_psmove_init_report(&d);
|
||||
uni_hid_parser_psmove_parse_input_report(&d, report, sizeof(report));
|
||||
assert(snapshot(&d).gyro_sequence == first.gyro_sequence);
|
||||
report[43] = 2;
|
||||
uni_hid_parser_psmove_parse_input_report(&d, report, sizeof(report));
|
||||
assert(snapshot(&d).accel_sequence == first.accel_sequence);
|
||||
uni_hid_parser_psmove_parse_input_report(&d, report, 43);
|
||||
assert(!snapshot(&d).report_valid && snapshot(&d).gyro_sequence == first.gyro_sequence);
|
||||
uni_hid_parser_native_motion_forget(&d);
|
||||
}
|
||||
|
||||
static void switch_setup(uni_hid_device_t* d, uint8_t type, bool calibrated) {
|
||||
memset(d, 0, sizeof(*d));
|
||||
d->report_parser.setup = uni_hid_parser_switch_setup;
|
||||
d->conn.interrupt_cid = 0x40;
|
||||
unsigned ready_before = ready_count;
|
||||
uni_hid_parser_switch_setup(d);
|
||||
for (unsigned step = 0; ready_count == ready_before && step < 12; ++step) {
|
||||
assert(command_len >= 12);
|
||||
uint8_t reply[49] = {0x21};
|
||||
reply[13] = 0x80;
|
||||
reply[14] = command[11];
|
||||
if (reply[14] == 2) reply[17] = type;
|
||||
if (reply[14] == 0x10) {
|
||||
memcpy(reply + 15, command + 12, 5);
|
||||
// Centered sticks, nonzero spans; no user calibration magic.
|
||||
if (little_endian_read_32(reply, 15) == 0x603d || little_endian_read_32(reply, 15) == 0x6046)
|
||||
memset(reply + 20, 0x80, reply[19]);
|
||||
if (little_endian_read_32(reply, 15) == 0x6020) {
|
||||
for (unsigned i = 0; i < 3; ++i) {
|
||||
put16(reply + 26 + 2 * i, calibrated ? 16384 : 0);
|
||||
put16(reply + 38 + 2 * i, calibrated ? 13371 : 0);
|
||||
}
|
||||
// Truncation with a convincing declared length cannot certify sensors.
|
||||
uni_hid_parser_switch_parse_input_report(d, reply, 22);
|
||||
assert(!snapshot(d).accel_valid);
|
||||
}
|
||||
}
|
||||
uni_hid_parser_switch_parse_input_report(d, reply, sizeof(reply));
|
||||
}
|
||||
assert(ready_count != ready_before);
|
||||
}
|
||||
static void switch_provenance(uint8_t type, bool calibrated) {
|
||||
uni_hid_device_t d;
|
||||
switch_setup(&d, type, calibrated);
|
||||
uint8_t report[49] = {0x30, 254};
|
||||
memset(report + 6, 0x80, 6);
|
||||
put16(report + 37, 4096); // Latest accelerometer sample, native X.
|
||||
put16(report + 43, 1000); // Latest gyro sample, native X.
|
||||
uni_hid_parser_switch_parse_input_report(&d, report, sizeof(report));
|
||||
uni_native_motion_snapshot_t first = snapshot(&d);
|
||||
assert(first.report_tracked && first.report_valid);
|
||||
if (type == 0x0b || !calibrated) {
|
||||
assert(!first.accel_valid && !first.gyro_valid);
|
||||
} else {
|
||||
assert(first.accel_valid && first.gyro_valid && first.accel_q13[2] == -8192);
|
||||
uni_hid_parser_switch_parse_input_report(&d, report, sizeof(report));
|
||||
assert(snapshot(&d).accel_sequence == first.accel_sequence);
|
||||
report[1] = 1;
|
||||
uni_hid_parser_switch_parse_input_report(&d, report, sizeof(report));
|
||||
assert(snapshot(&d).gyro_sequence != first.gyro_sequence);
|
||||
first = snapshot(&d);
|
||||
report[1] = 0;
|
||||
uni_hid_parser_switch_parse_input_report(&d, report, sizeof(report));
|
||||
assert(snapshot(&d).gyro_sequence == first.gyro_sequence);
|
||||
}
|
||||
uni_hid_parser_switch_parse_input_report(&d, report, 48);
|
||||
assert(!snapshot(&d).report_valid && snapshot(&d).accel_sequence == first.accel_sequence);
|
||||
uint8_t buttons[12] = {0x3f};
|
||||
uni_hid_parser_switch_parse_input_report(&d, buttons, sizeof(buttons));
|
||||
assert(snapshot(&d).report_valid && snapshot(&d).accel_sequence == first.accel_sequence);
|
||||
uni_hid_parser_switch_teardown(&d);
|
||||
assert(!uni_hid_parser_native_motion_snapshot(&d, &first));
|
||||
}
|
||||
int main(void) {
|
||||
ds4_provenance();
|
||||
move_provenance(false);
|
||||
move_provenance(true);
|
||||
switch_provenance(3, true);
|
||||
switch_provenance(1, true);
|
||||
switch_provenance(2, true);
|
||||
switch_provenance(3, false);
|
||||
switch_provenance(0x0b, false);
|
||||
uni_hid_device_t sensorless = {0};
|
||||
uni_native_motion_snapshot_t absent;
|
||||
memset(&absent, 0xff, sizeof(absent));
|
||||
assert(!uni_hid_parser_native_motion_snapshot(&sensorless, &absent));
|
||||
assert(!absent.report_tracked && !absent.accel_valid && !absent.gyro_valid && absent.gyro_sequence == 0);
|
||||
puts("Native DS4, Move, Switch motion provenance and sensorless absence passed");
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -13,7 +13,7 @@
|
|||
namespace {
|
||||
uint64_t now_us = 1000000;
|
||||
uint32_t stage;
|
||||
Bluepad32DualSenseBridgeSnapshot source;
|
||||
Bluepad32NativeGamepadSnapshot source;
|
||||
ControllerProfile profile;
|
||||
bool alternating_shortcut;
|
||||
bool shortcut_phase;
|
||||
|
|
@ -32,11 +32,11 @@ void bluepad32_input_backend_start() { stage = 2; }
|
|||
void bluepad32_input_backend_poll() {}
|
||||
void bluepad32_input_backend_diagnostics(Bluepad32BackendDiagnostics* out) { *out = {}; out->initialization_stage = stage; }
|
||||
void bluepad32_input_backend_open_pairing_window() {}
|
||||
void bluepad32_input_backend_select_dualsense_source(const uint8_t*) {}
|
||||
void bluepad32_input_backend_dualsense_snapshot(Bluepad32DualSenseBridgeSnapshot* out) { *out = source; }
|
||||
bool bluepad32_input_backend_dualsense_sample_request(uint8_t, uint8_t, uint64_t*) { return false; }
|
||||
int bluepad32_input_backend_dualsense_sample_result(uint8_t, uint64_t) { return -1; }
|
||||
void bluepad32_input_backend_dualsense_sample_cancel(uint8_t) {}
|
||||
void bluepad32_input_backend_select_native_source(const uint8_t*) {}
|
||||
void bluepad32_input_backend_native_snapshot(Bluepad32NativeGamepadSnapshot* out) { *out = source; }
|
||||
bool bluepad32_input_backend_native_sample_request(uint8_t, uint8_t, uint64_t*) { return false; }
|
||||
int bluepad32_input_backend_native_sample_result(uint8_t, uint64_t) { return -1; }
|
||||
void bluepad32_input_backend_native_sample_cancel(uint8_t) {}
|
||||
void bluepad32_input_backend_queue_profile_feedback(uint8_t, uint32_t, uint8_t, ControllerProfileConfirmationPolicy) {}
|
||||
void controller_profile_runtime_reset() { profile = controller_profile_default(controller_identity_global(), 0); }
|
||||
bool controller_profile_runtime_take_initial_profile_indication(uint8_t, ControllerProfileRuntimeProfileChangeEvent*) { return false; }
|
||||
|
|
@ -94,8 +94,9 @@ void publish(bool motion = true) {
|
|||
source.received_us = time_us_32();
|
||||
++source.state_generation;
|
||||
if (motion) {
|
||||
source.motion_received_us = time_us_32();
|
||||
++source.motion_sequence;
|
||||
source.accel_received_us = source.gyro_received_us = time_us_32();
|
||||
++source.accel_sequence;
|
||||
++source.gyro_sequence;
|
||||
}
|
||||
}
|
||||
uint32_t peek(uint8_t instance) {
|
||||
|
|
@ -210,7 +211,7 @@ void independent_backpressure_and_resets() {
|
|||
}
|
||||
|
||||
void real_motion_admission_and_loss() {
|
||||
source.motion_valid = true;
|
||||
source.accel_valid = source.gyro_valid = true;
|
||||
source.accel_q13[1] = 8192; // SDL face-up gravity -> native +Z, no mouse mounting.
|
||||
// These values have already passed the DS5 factory-calibration path.
|
||||
// Even a controller rotating at connection must not wait for stationary bias estimation.
|
||||
|
|
@ -262,11 +263,11 @@ void real_motion_admission_and_loss() {
|
|||
source.gyro_q10[1] -= 90 * 1024;
|
||||
publish();
|
||||
const uint32_t obsolete = peek(0);
|
||||
source.motion_valid = false;
|
||||
source.accel_valid = source.gyro_valid = false;
|
||||
assert(!probe_controller_input_commit_native_report(0, obsolete));
|
||||
publish(false); pair();
|
||||
assert(controls[0].active && reports[0][2] == 2 && imu_length(0) == 0 && imu_length(1) == 0);
|
||||
source.motion_valid = true;
|
||||
source.accel_valid = source.gyro_valid = true;
|
||||
publish(); pair();
|
||||
assert(imu_length(0) == 30);
|
||||
for (unsigned i = 0; i < 38; ++i) { publish(false); pair(); }
|
||||
|
|
@ -275,7 +276,7 @@ void real_motion_admission_and_loss() {
|
|||
const uint32_t old_right = peek(0), old_left = peek(1);
|
||||
// Even a reconnect whose teardown was missed retires both USB identities.
|
||||
++source.controller.connection_generation;
|
||||
source.motion_valid = false; // The backend withholds motion until a new report in the new epoch.
|
||||
source.accel_valid = source.gyro_valid = false; // New epochs require fresh reports.
|
||||
assert(!probe_controller_input_commit_native_report(0, old_right));
|
||||
assert(!probe_controller_input_commit_native_report(1, old_left));
|
||||
pair();
|
||||
|
|
@ -299,7 +300,7 @@ void selected_motion_target_keeps_both_control_halves() {
|
|||
source.controller.connection_generation = 99;
|
||||
source.controller.state.button_south = true;
|
||||
source.controller.state.dpad_up = true;
|
||||
source.motion_valid = true;
|
||||
source.accel_valid = source.gyro_valid = true;
|
||||
source.accel_q13[1] = 8192;
|
||||
profile = controller_profile_default(controller_identity_global(), 0);
|
||||
calibrate(0, 2048, 2048, 2047, 2047, 2048, 2048);
|
||||
|
|
@ -318,6 +319,43 @@ void selected_motion_target_keeps_both_control_halves() {
|
|||
no_mouse_or_rails();
|
||||
}
|
||||
|
||||
void wii_bias_and_independent_sensor_freshness() {
|
||||
++source.controller.connection_generation;
|
||||
source.requires_stationary_bias = true;
|
||||
source.gyro_q10[1] = 2 * 1024;
|
||||
publish(); pair();
|
||||
assert(controls[0].active && controls[1].active);
|
||||
assert(reports[0][2] == 0x01 && reports[1][2] == 0x08);
|
||||
assert(imu_length(0) == 0 && imu_length(1) == 0);
|
||||
for (unsigned i = 0; i < 400; ++i) { publish(); pair(); }
|
||||
for (uint8_t instance = 0; instance < 2; ++instance) {
|
||||
assert(imu_length(instance) == ((SWITCH2_BRIDGE_IMU_TARGET_MASK & (1u << instance)) ? 30 : 0));
|
||||
}
|
||||
// Accelerometer-only reports must not refresh a stalled MotionPlus stream.
|
||||
for (unsigned i = 0; i < 38; ++i) {
|
||||
publish(false);
|
||||
source.accel_received_us = time_us_32();
|
||||
++source.accel_sequence;
|
||||
pair();
|
||||
}
|
||||
assert(controls[0].active && controls[1].active);
|
||||
assert(imu_length(0) == 0 && imu_length(1) == 0);
|
||||
source.gyro_valid = false;
|
||||
publish(); pair();
|
||||
assert(reports[0][2] == 0x01 && reports[1][2] == 0x08);
|
||||
assert(imu_length(0) == 0 && imu_length(1) == 0);
|
||||
// Returning real Wii sensors must settle again, not reuse the old bias.
|
||||
source.gyro_valid = true;
|
||||
publish(); pair();
|
||||
assert(imu_length(0) == 0 && imu_length(1) == 0);
|
||||
// A factory-calibrated source switching policy must initialize immediately.
|
||||
source.requires_stationary_bias = false;
|
||||
publish(); pair();
|
||||
for (uint8_t instance = 0; instance < 2; ++instance) {
|
||||
assert(imu_length(instance) == ((SWITCH2_BRIDGE_IMU_TARGET_MASK & (1u << instance)) ? 30 : 0));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
|
|
@ -331,5 +369,6 @@ int main() {
|
|||
independent_backpressure_and_resets();
|
||||
if (SWITCH2_BRIDGE_IMU_TARGET_MASK == 3) real_motion_admission_and_loss();
|
||||
selected_motion_target_keeps_both_control_halves();
|
||||
wii_bias_and_independent_sensor_freshness();
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -4,9 +4,19 @@
|
|||
|
||||
#include "protocol_fixture.h"
|
||||
#include "parser/uni_hid_parser_switch2.h"
|
||||
#include "parser/uni_hid_parser_native_motion.h"
|
||||
#include "parser/uni_switch2_pairing.h"
|
||||
#include "sdkconfig.h"
|
||||
|
||||
#if SWITCH2_BRIDGE_FULL_INPUT
|
||||
void uni_hid_parser_wii_setup(uni_hid_device_t* d) { (void)d; assert(false); }
|
||||
bool uni_hid_parser_wii_accel_snapshot(uni_hid_device_t* d, int32_t v[3], uint32_t* s) {
|
||||
(void)d; (void)v; (void)s; assert(false); return false;
|
||||
}
|
||||
bool uni_hid_parser_wii_gyro_snapshot(uni_hid_device_t* d, int32_t v[3], uint32_t* s) {
|
||||
(void)d; (void)v; (void)s; assert(false); return false;
|
||||
}
|
||||
#endif
|
||||
#define PEERS CONFIG_BLUEPAD32_MAX_DEVICES
|
||||
#define SERVICE_START 0x100
|
||||
#define INPUT_HANDLE 0x104
|
||||
|
|
@ -367,7 +377,10 @@ static unsigned response_data(struct fixture_peer* peer, uint8_t* out, bool eras
|
|||
if (address == 0x13000) {
|
||||
little_endian_store_16(out, 16 + 18, UNI_SW2_NINTENDO_VID);
|
||||
little_endian_store_16(out, 16 + 20, peer->device.product_id);
|
||||
} else if (address != 0x13044) {
|
||||
} else if (address == 0x13044) {
|
||||
if (erased)
|
||||
memset(out + 16, 0xff, length);
|
||||
} else {
|
||||
if (erased)
|
||||
memset(out + 16, 0xff, length);
|
||||
else {
|
||||
|
|
@ -657,8 +670,19 @@ static void test_calibration_physical_inputs_and_sensor_units(void) {
|
|||
}
|
||||
assert(gp->accel[0] == 8192 && gp->accel[1] == -8192 && gp->accel[2] == -16384);
|
||||
assert(gp->gyro[0] >= 2041740 && gp->gyro[0] <= 2041750);
|
||||
#if SWITCH2_BRIDGE_FULL_INPUT
|
||||
uni_native_motion_snapshot_t first, duplicate;
|
||||
assert(uni_hid_parser_native_motion_snapshot(&peer->device, &first));
|
||||
assert(first.accel_valid && first.gyro_valid && first.accel_q13[2] == -16384);
|
||||
#endif
|
||||
notify(peer, INPUT_HANDLE, report, sizeof(report)); // Repeated sensor sample must not become zero.
|
||||
#if SWITCH2_BRIDGE_FULL_INPUT
|
||||
assert(uni_hid_parser_native_motion_snapshot(&peer->device, &duplicate));
|
||||
assert(duplicate.accel_sequence == first.accel_sequence && duplicate.gyro_sequence == first.gyro_sequence);
|
||||
assert(duplicate.gyro_q10[0] >= 2041740);
|
||||
#else
|
||||
assert(gp->gyro[0] >= 2041740);
|
||||
#endif
|
||||
reset();
|
||||
peer = connect_peer(UNI_SW2_JOYCON_L_PID, false);
|
||||
discover(peer);
|
||||
|
|
@ -672,6 +696,59 @@ static void test_calibration_physical_inputs_and_sensor_units(void) {
|
|||
assert(uni_hid_parser_switch2_extra_buttons(&peer->device) == (UNI_SW2_BUTTON_LEFT_SL | UNI_SW2_BUTTON_LEFT_SR));
|
||||
}
|
||||
|
||||
#if SWITCH2_BRIDGE_FULL_INPUT
|
||||
static void test_native_independent_motion_and_lifetime(void) {
|
||||
const uint16_t products[] = {UNI_SW2_PRO_PID, UNI_SW2_JOYCON_L_PID, UNI_SW2_JOYCON_R_PID};
|
||||
uint32_t previous_sequence = 0;
|
||||
for (unsigned product = 0; product < 3; ++product) {
|
||||
for (unsigned invalid_bias = 0; invalid_bias < 2; ++invalid_bias) {
|
||||
reset();
|
||||
struct fixture_peer* peer = connect_peer(products[product], false);
|
||||
discover(peer);
|
||||
subscribe_response(peer);
|
||||
for (unsigned step = 0; step < 10; ++step) {
|
||||
if (peer->command[0] == 2 && little_endian_read_32(peer->command, 12) == 0x13044)
|
||||
break;
|
||||
acknowledge(peer, false);
|
||||
}
|
||||
assert(little_endian_read_32(peer->command, 12) == 0x13044);
|
||||
acknowledge(peer, invalid_bias);
|
||||
finish_setup(peer);
|
||||
uni_native_motion_snapshot_t native;
|
||||
assert(uni_hid_parser_native_motion_snapshot(&peer->device, &native));
|
||||
assert(!native.report_valid && !native.accel_valid && !native.gyro_valid);
|
||||
uint8_t report[63] = {0};
|
||||
little_endian_store_16(report, 48, 4096);
|
||||
little_endian_store_16(report, 54, 32767);
|
||||
little_endian_store_32(report, 42, 1000);
|
||||
notify(peer, INPUT_HANDLE, report, sizeof(report));
|
||||
assert(uni_hid_parser_native_motion_snapshot(&peer->device, &native));
|
||||
assert(native.report_valid && native.accel_valid && native.accel_q13[0] == 8192);
|
||||
assert(!native.gyro_valid && native.accel_sequence != previous_sequence);
|
||||
for (unsigned sample = 1; sample < 46; ++sample) {
|
||||
advance(10);
|
||||
little_endian_store_32(report, 42, 1000 + sample * 10000);
|
||||
notify(peer, INPUT_HANDLE, report, sizeof(report));
|
||||
}
|
||||
assert(uni_hid_parser_native_motion_snapshot(&peer->device, &native));
|
||||
assert(native.accel_valid && native.gyro_valid == !invalid_bias);
|
||||
uint32_t accel = native.accel_sequence, gyro = native.gyro_sequence;
|
||||
little_endian_store_32(report, 42, 1000); // Regressed sensor clock.
|
||||
notify(peer, INPUT_HANDLE, report, sizeof(report));
|
||||
assert(uni_hid_parser_native_motion_snapshot(&peer->device, &native));
|
||||
assert(native.accel_sequence == accel && native.gyro_sequence == gyro);
|
||||
uni_hid_parser_switch2_parse_input_report(&peer->device, report, 62);
|
||||
assert(uni_hid_parser_native_motion_snapshot(&peer->device, &native));
|
||||
assert(!native.report_valid && native.accel_sequence == accel);
|
||||
previous_sequence = accel;
|
||||
uni_hid_parser_switch2_teardown(&peer->device);
|
||||
assert(!uni_hid_parser_native_motion_snapshot(&peer->device, &native));
|
||||
assert(!native.accel_valid && !native.gyro_valid);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
static uint64_t rumble_frame(const struct fixture_peer* peer) {
|
||||
uint64_t value = 0;
|
||||
for (unsigned i = 0; i < 5; ++i)
|
||||
|
|
@ -1223,6 +1300,9 @@ static void test_neutral_budget_waits_for_success_and_stale_active_completion(vo
|
|||
}
|
||||
|
||||
int main(void) {
|
||||
#if SWITCH2_BRIDGE_FULL_INPUT
|
||||
test_native_independent_motion_and_lifetime();
|
||||
#endif
|
||||
test_connected_callback_rejection();
|
||||
test_advertisement_bounds_and_admission();
|
||||
test_discovery_metadata_and_rejection_isolation();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#pragma once
|
||||
|
||||
#include <assert.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
|
|
@ -23,6 +24,12 @@ typedef struct btstack_timer_source {
|
|||
#define HID_MESSAGE_TYPE_DATA 0x0a
|
||||
#define HID_REPORT_TYPE_OUTPUT 0x02
|
||||
#define btstack_min(a, b) ((a) < (b) ? (a) : (b))
|
||||
#define btstack_max(a, b) ((a) > (b) ? (a) : (b))
|
||||
|
||||
static inline uint32_t little_endian_read_32(const uint8_t* bytes, int offset) {
|
||||
return (uint32_t)bytes[offset] | ((uint32_t)bytes[offset + 1] << 8) |
|
||||
((uint32_t)bytes[offset + 2] << 16) | ((uint32_t)bytes[offset + 3] << 24);
|
||||
}
|
||||
|
||||
void btstack_run_loop_set_timer(btstack_timer_source_t* timer, uint32_t ms);
|
||||
void btstack_run_loop_add_timer(btstack_timer_source_t* timer);
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ def test_dualsense_parser_native(tmp_path: Path) -> None:
|
|||
"-fdata-sections",
|
||||
"-DENABLE_BLE",
|
||||
"-DENABLE_CLASSIC",
|
||||
"-DSWITCH2_BRIDGE_DUALSENSE_INPUT=1",
|
||||
"-DSWITCH2_BRIDGE_FULL_INPUT=1",
|
||||
"-DHID_MESSAGE_TYPE_GET_REPORT=4",
|
||||
"-DHID_REPORT_TYPE_FEATURE=3",
|
||||
f"-I{root / 'tests' / 'switch_parser_native_stubs'}",
|
||||
|
|
@ -40,6 +40,9 @@ def test_dualsense_parser_native(tmp_path: Path) -> None:
|
|||
f"-I{component / 'include'}",
|
||||
str(root / "tests" / "dualsense_parser_native_test.c"),
|
||||
str(component / "parser" / "uni_hid_parser_ds5.c"),
|
||||
str(
|
||||
root / "bluepad32_config" / "parser" / "uni_hid_parser_native_motion.c"
|
||||
),
|
||||
str(component / "uni_utils.c"),
|
||||
"-Wl,--gc-sections",
|
||||
"-o",
|
||||
|
|
|
|||
|
|
@ -4,15 +4,18 @@ import shutil
|
|||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
def test_dualsense_backend_native(tmp_path: Path) -> None:
|
||||
|
||||
@pytest.mark.parametrize("source", ("GAMEPAD", "DUALSENSE"))
|
||||
def test_native_gamepad_backend_native(tmp_path: Path, source: str) -> None:
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
compiler = shutil.which("c++") or shutil.which("g++")
|
||||
assert compiler is not None, "a host C++ compiler is required"
|
||||
executable = tmp_path / "dualsense_backend_test"
|
||||
executable = tmp_path / "native_gamepad_backend_test"
|
||||
firmware = root / "src" / "firmware"
|
||||
sources = [
|
||||
root / "tests" / "dualsense_backend_test.cpp",
|
||||
root / "tests" / "native_gamepad_backend_test.cpp",
|
||||
firmware / "profile" / "controller_profile.cpp",
|
||||
firmware / "profile" / "controller_profile_transform.cpp",
|
||||
firmware / "profile" / "controller_synthetic_input.cpp",
|
||||
|
|
@ -34,7 +37,8 @@ def test_dualsense_backend_native(tmp_path: Path) -> None:
|
|||
"-DSWITCH_PICO_USB_OUTPUT_MODES=1",
|
||||
"-DSWITCH_PICO_ENABLE_BLE=1",
|
||||
"-DSWITCH_PICO_ENABLE_CLASSIC=1",
|
||||
"-DSWITCH2_BRIDGE_DUALSENSE_INPUT=1",
|
||||
"-DSWITCH2_BRIDGE_FULL_INPUT=1",
|
||||
f"-DSWITCH2_BRIDGE_{source}_INPUT=1",
|
||||
f"-I{root / 'tests' / 'bluepad32_native_stubs'}",
|
||||
f"-I{firmware}",
|
||||
f"-I{root / 'bluepad32_config'}",
|
||||
|
|
@ -52,3 +56,12 @@ def test_dualsense_backend_native(tmp_path: Path) -> None:
|
|||
"cue-races",
|
||||
):
|
||||
subprocess.run([str(executable), scenario], check=True, cwd=root)
|
||||
if source == "GAMEPAD":
|
||||
for scenario in (
|
||||
"sensorless-admission",
|
||||
"independent-motion",
|
||||
"paired-source",
|
||||
"pair-cue-races",
|
||||
"mono-rumble",
|
||||
):
|
||||
subprocess.run([str(executable), scenario], check=True, cwd=root)
|
||||
58
tests/test_native_gamepad_motion_parser_native.py
Normal file
58
tests/test_native_gamepad_motion_parser_native.py
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "tools"))
|
||||
|
||||
from prepare_bluepad32 import prepare_bluepad32
|
||||
|
||||
|
||||
def test_native_gamepad_motion_parser_provenance(tmp_path: Path) -> None:
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
compiler = shutil.which("cc") or shutil.which("gcc")
|
||||
assert compiler is not None, "a host C compiler is required"
|
||||
prepared = prepare_bluepad32(
|
||||
root / "external" / "bluepad32",
|
||||
root / "patches" / "bluepad32-sdl3-imu.patch",
|
||||
tmp_path / "bluepad32-src",
|
||||
)
|
||||
component = prepared / "src" / "components" / "bluepad32"
|
||||
executable = tmp_path / "native_gamepad_motion_parser_test"
|
||||
subprocess.run(
|
||||
[
|
||||
compiler,
|
||||
"-std=gnu11",
|
||||
"-O1",
|
||||
"-Wall",
|
||||
"-Wextra",
|
||||
"-ffunction-sections",
|
||||
"-fdata-sections",
|
||||
"-DENABLE_BLE",
|
||||
"-DENABLE_CLASSIC",
|
||||
"-DSWITCH2_BRIDGE_FULL_INPUT=1",
|
||||
"-DHID_MESSAGE_TYPE_GET_REPORT=4",
|
||||
"-DHID_REPORT_TYPE_FEATURE=3",
|
||||
f"-I{root / 'tests' / 'switch_parser_native_stubs'}",
|
||||
f"-I{root / 'bluepad32_config'}",
|
||||
f"-I{component / 'include'}",
|
||||
str(root / "tests" / "native_gamepad_motion_parser_test.c"),
|
||||
str(
|
||||
root / "bluepad32_config" / "parser" / "uni_hid_parser_native_motion.c"
|
||||
),
|
||||
str(component / "parser" / "uni_hid_parser_ds4.c"),
|
||||
str(component / "parser" / "uni_hid_parser_psmove.c"),
|
||||
str(component / "parser" / "uni_hid_parser_switch.c"),
|
||||
str(component / "uni_circular_buffer.c"),
|
||||
str(component / "uni_utils.c"),
|
||||
str(component / "controller" / "uni_gamepad.c"),
|
||||
"-Wl,--gc-sections",
|
||||
"-o",
|
||||
str(executable),
|
||||
],
|
||||
check=True,
|
||||
cwd=root,
|
||||
)
|
||||
subprocess.run([str(executable)], check=True, cwd=root)
|
||||
|
|
@ -8,14 +8,14 @@ import pytest
|
|||
|
||||
|
||||
@pytest.mark.parametrize("imu_target", [1, 2, 3], ids=["right", "left", "both"])
|
||||
def test_dualsense_native_bridge_mapping_motion_and_backpressure(
|
||||
def test_native_gamepad_bridge_mapping_motion_and_backpressure(
|
||||
tmp_path: Path,
|
||||
imu_target: int,
|
||||
) -> None:
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
compiler = shutil.which("c++") or shutil.which("g++")
|
||||
assert compiler is not None, "a host C++ compiler is required"
|
||||
executable = tmp_path / "switch2_dualsense_bridge_test"
|
||||
executable = tmp_path / "switch2_native_gamepad_bridge_test"
|
||||
subprocess.run(
|
||||
[
|
||||
compiler,
|
||||
|
|
@ -25,7 +25,8 @@ def test_dualsense_native_bridge_mapping_motion_and_backpressure(
|
|||
"-Werror",
|
||||
"-pedantic",
|
||||
"-DSWITCH_PICO_SWITCH2_USB_BRIDGE=1",
|
||||
"-DSWITCH2_BRIDGE_DUALSENSE_INPUT=1",
|
||||
"-DSWITCH2_BRIDGE_GAMEPAD_INPUT=1",
|
||||
"-DSWITCH2_BRIDGE_FULL_INPUT=1",
|
||||
"-DSWITCH2_BRIDGE_SOURCE_AUTO=1",
|
||||
"-DSWITCH2_PROBE_HUB=1",
|
||||
f"-DSWITCH2_BRIDGE_IMU_TARGET_MASK={imu_target}",
|
||||
|
|
@ -35,9 +36,9 @@ def test_dualsense_native_bridge_mapping_motion_and_backpressure(
|
|||
f"-I{root / 'tests' / 'wii_ir_aiming_native_stubs'}",
|
||||
f"-I{root / 'tools' / 'switch2_usb_probe'}",
|
||||
f"-I{root / 'src' / 'firmware'}",
|
||||
str(root / "tests" / "switch2_dualsense_bridge_test.cpp"),
|
||||
str(root / "tests" / "switch2_native_gamepad_bridge_test.cpp"),
|
||||
str(root / "tools" / "switch2_usb_probe" / "controller_input.cpp"),
|
||||
str(root / "tools" / "switch2_usb_probe" / "dualsense_input.cpp"),
|
||||
str(root / "tools" / "switch2_usb_probe" / "native_gamepad_input.cpp"),
|
||||
str(root / "tools" / "switch2_usb_probe" / "native_imu.cpp"),
|
||||
str(root / "src" / "firmware" / "core" / "controller_identity.cpp"),
|
||||
str(root / "src" / "firmware" / "profile" / "controller_profile.cpp"),
|
||||
|
|
@ -13,7 +13,10 @@ sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "tools"))
|
|||
from prepare_bluepad32 import prepare_bluepad32
|
||||
|
||||
|
||||
def test_switch2_parser_protocol_and_lifecycle(tmp_path: Path) -> None:
|
||||
@pytest.mark.parametrize("full_input", [False, True])
|
||||
def test_switch2_parser_protocol_and_lifecycle(
|
||||
tmp_path: Path, full_input: bool
|
||||
) -> None:
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
compiler = shutil.which("cc") or shutil.which("gcc")
|
||||
assert compiler is not None, "a host C compiler is required"
|
||||
|
|
@ -24,12 +27,17 @@ def test_switch2_parser_protocol_and_lifecycle(tmp_path: Path) -> None:
|
|||
if sdk_path := os.environ.get("PICO_SDK_PATH"):
|
||||
sdk_candidates.insert(0, Path(sdk_path))
|
||||
btstack = next(
|
||||
(sdk / "lib" / "btstack" / "src" for sdk in sdk_candidates
|
||||
if (sdk / "lib" / "btstack" / "src" / "ble" / "gatt_client.h").is_file()),
|
||||
(
|
||||
sdk / "lib" / "btstack" / "src"
|
||||
for sdk in sdk_candidates
|
||||
if (sdk / "lib" / "btstack" / "src" / "ble" / "gatt_client.h").is_file()
|
||||
),
|
||||
None,
|
||||
)
|
||||
if btstack is None:
|
||||
pytest.skip("Pico SDK BTstack headers required; configure firmware or set PICO_SDK_PATH")
|
||||
pytest.skip(
|
||||
"Pico SDK BTstack headers required; configure firmware or set PICO_SDK_PATH"
|
||||
)
|
||||
prepared = prepare_bluepad32(
|
||||
root / "external" / "bluepad32",
|
||||
root / "patches" / "bluepad32-sdl3-imu.patch",
|
||||
|
|
@ -48,6 +56,7 @@ def test_switch2_parser_protocol_and_lifecycle(tmp_path: Path) -> None:
|
|||
"-fdata-sections",
|
||||
"-DENABLE_BLE",
|
||||
"-DENABLE_CLASSIC",
|
||||
f"-DSWITCH2_BRIDGE_FULL_INPUT={int(full_input)}",
|
||||
f"-I{root / 'tests' / 'switch2_parser_native_stubs'}",
|
||||
f"-I{root / 'bluepad32_config'}",
|
||||
f"-I{component / 'include'}",
|
||||
|
|
@ -58,6 +67,9 @@ def test_switch2_parser_protocol_and_lifecycle(tmp_path: Path) -> None:
|
|||
str(root / "tests" / "switch2_parser_native_test.c"),
|
||||
str(root / "bluepad32_config" / "parser" / "uni_hid_parser_switch2.c"),
|
||||
str(root / "bluepad32_config" / "parser" / "uni_switch2_haptics.c"),
|
||||
str(
|
||||
root / "bluepad32_config" / "parser" / "uni_hid_parser_native_motion.c"
|
||||
),
|
||||
str(btstack / "btstack_util.c"),
|
||||
"-Wl,--gc-sections",
|
||||
"-o",
|
||||
|
|
|
|||
|
|
@ -5,12 +5,17 @@ import subprocess
|
|||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "tools"))
|
||||
|
||||
from prepare_bluepad32 import prepare_bluepad32
|
||||
|
||||
|
||||
def test_wii_parser_native_motion_and_lifecycle(tmp_path: Path) -> None:
|
||||
@pytest.mark.parametrize("full_input", [False, True])
|
||||
def test_wii_parser_native_motion_and_lifecycle(
|
||||
tmp_path: Path, full_input: bool
|
||||
) -> None:
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
compiler = shutil.which("cc") or shutil.which("gcc")
|
||||
assert compiler is not None, "a host C compiler is required"
|
||||
|
|
@ -32,11 +37,15 @@ def test_wii_parser_native_motion_and_lifecycle(tmp_path: Path) -> None:
|
|||
"-fdata-sections",
|
||||
"-DENABLE_BLE",
|
||||
"-DENABLE_CLASSIC",
|
||||
f"-DSWITCH2_BRIDGE_FULL_INPUT={int(full_input)}",
|
||||
f"-I{root / 'tests' / 'switch_parser_native_stubs'}",
|
||||
f"-I{root / 'bluepad32_config'}",
|
||||
f"-I{component / 'include'}",
|
||||
str(root / "tests" / "wii_parser_native_test.c"),
|
||||
str(component / "parser" / "uni_hid_parser_wii.c"),
|
||||
str(
|
||||
root / "bluepad32_config" / "parser" / "uni_hid_parser_native_motion.c"
|
||||
),
|
||||
str(component / "controller" / "uni_gamepad.c"),
|
||||
"-Wl,--gc-sections",
|
||||
"-o",
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
#include <string.h>
|
||||
|
||||
#include "parser/uni_hid_parser_wii.h"
|
||||
#include "parser/uni_hid_parser_native_motion.h"
|
||||
#include "uni_hid_device.h"
|
||||
|
||||
// The real staged parser and gamepad definitions are linked. Only transport and
|
||||
|
|
@ -501,11 +502,21 @@ static void accelerometer_snapshot_requires_fresh_calibrated_reports(void) {
|
|||
assert(uni_hid_parser_wii_accel_snapshot(&f.device, acceleration, &sequence));
|
||||
expect_vector(acceleration, -8192, 2048, -4096);
|
||||
const uint32_t first = sequence;
|
||||
#if SWITCH2_BRIDGE_FULL_INPUT
|
||||
uni_native_motion_snapshot_t native;
|
||||
assert(uni_hid_parser_native_motion_snapshot(&f.device, &native));
|
||||
assert(native.accel_valid && !native.gyro_valid && !native.report_tracked);
|
||||
expect_vector(native.accel_q13, -8192, 2048, -4096);
|
||||
#endif
|
||||
send_ack(0x16, 0);
|
||||
const uint8_t short_report[5] = {0x31};
|
||||
feed(short_report, sizeof(short_report));
|
||||
assert(uni_hid_parser_wii_accel_snapshot(&f.device, acceleration, &sequence));
|
||||
assert(sequence == first);
|
||||
#if SWITCH2_BRIDGE_FULL_INPUT
|
||||
assert(uni_hid_parser_native_motion_snapshot(&f.device, &native));
|
||||
assert(native.accel_sequence == first && !native.gyro_valid);
|
||||
#endif
|
||||
send_core_and_accel();
|
||||
assert(uni_hid_parser_wii_accel_snapshot(&f.device, acceleration, &sequence));
|
||||
assert(sequence != first); // Identical readings can still be fresh.
|
||||
|
|
@ -528,6 +539,12 @@ static void gyro_snapshot_advances_only_on_calibrated_motionplus_packets(void) {
|
|||
assert(uni_hid_parser_wii_gyro_snapshot(&f.device, gyro, &sequence));
|
||||
expect_vector(gyro, 360 * 1024, -120 * 1024, 40 * 1024);
|
||||
const uint32_t first = sequence;
|
||||
#if SWITCH2_BRIDGE_FULL_INPUT
|
||||
uni_native_motion_snapshot_t native;
|
||||
assert(uni_hid_parser_native_motion_snapshot(&f.device, &native));
|
||||
assert(native.accel_valid && native.gyro_valid);
|
||||
expect_vector(native.gyro_q10, 360 * 1024, -120 * 1024, 40 * 1024);
|
||||
#endif
|
||||
send_nunchuk_controls(true);
|
||||
send_ack(0x16, 0);
|
||||
send_status(true);
|
||||
|
|
@ -537,6 +554,10 @@ static void gyro_snapshot_advances_only_on_calibrated_motionplus_packets(void) {
|
|||
assert(uni_hid_parser_wii_gyro_snapshot(&f.device, gyro, &sequence));
|
||||
assert(sequence == first);
|
||||
expect_vector(gyro, 360 * 1024, -120 * 1024, 40 * 1024);
|
||||
#if SWITCH2_BRIDGE_FULL_INPUT
|
||||
assert(uni_hid_parser_native_motion_snapshot(&f.device, &native));
|
||||
assert(native.gyro_sequence == first);
|
||||
#endif
|
||||
send_motion(7760, 8200, 7560, false, true, false);
|
||||
assert(uni_hid_parser_wii_gyro_snapshot(&f.device, gyro, &sequence));
|
||||
assert(sequence != first); // Equal values do not mean a duplicate packet.
|
||||
|
|
@ -592,10 +613,23 @@ static void gyro_snapshot_invalidates_on_topology_and_teardown(void) {
|
|||
uni_hid_parser_wii_teardown(&f.device);
|
||||
assert(!uni_hid_parser_wii_gyro_snapshot(&f.device, gyro, &sequence));
|
||||
assert(!uni_hid_parser_wii_rumble_ready(&f.device));
|
||||
#if SWITCH2_BRIDGE_FULL_INPUT
|
||||
uni_native_motion_snapshot_t retired;
|
||||
assert(uni_hid_parser_native_motion_snapshot(&f.device, &retired));
|
||||
assert(!retired.accel_valid && !retired.gyro_valid);
|
||||
#endif
|
||||
f.ready_count = 0; // A new parser connection gets its own ready notification.
|
||||
uni_hid_parser_wii_setup(&f.device);
|
||||
finish_setup();
|
||||
assert(!uni_hid_parser_wii_gyro_snapshot(&f.device, gyro, &sequence));
|
||||
#if SWITCH2_BRIDGE_FULL_INPUT
|
||||
send_motion(7760, 8200, 7560, false, true, false);
|
||||
// An already-active MP first resolves its downstream topology after reconnect.
|
||||
finish_setup();
|
||||
send_motion(7760, 8200, 7560, false, true, false);
|
||||
assert(uni_hid_parser_native_motion_snapshot(&f.device, &retired));
|
||||
assert(retired.gyro_valid && retired.gyro_sequence != first);
|
||||
#endif
|
||||
}
|
||||
|
||||
static void setup_read_and_write_errors_leave_buttons_ready(void) {
|
||||
|
|
@ -808,6 +842,11 @@ static void legacy_plain_nunchuk_and_wii_u_pro(void) {
|
|||
assert(f.device.controller.gamepad.misc_buttons == MISC_BUTTON_START);
|
||||
expect_vector(f.device.controller.gamepad.accel, 0, 0, 0);
|
||||
expect_vector(f.device.controller.gamepad.gyro, 0, 0, 0);
|
||||
#if SWITCH2_BRIDGE_FULL_INPUT
|
||||
uni_native_motion_snapshot_t native;
|
||||
assert(uni_hid_parser_native_motion_snapshot(&f.device, &native));
|
||||
assert(!native.accel_valid && !native.gyro_valid);
|
||||
#endif
|
||||
}
|
||||
|
||||
static void plus_selects_vertical_without_disabling_motion(void) {
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
"""Bounded, non-pairing qualification of the switch-pico native Joy-Con USB hub.
|
||||
|
||||
Requires Linux, PyUSB/libusb, and the existing sudo -n setfacl permission policy.
|
||||
Uses the already-paired real R/L donors; it cannot wake or pair them. The JSON
|
||||
capture is created exclusively before USB access and retains partial failures.
|
||||
Uses already-paired R/L donors or one full gamepad; it cannot wake or pair them.
|
||||
The JSON capture is created exclusively before USB access and retains failures.
|
||||
No reset, configuration change, pairing exchange, profile access, flash write,
|
||||
or HID output is sent. Motor sample playback requires --rumble-sample explicitly.
|
||||
"""
|
||||
|
|
@ -37,14 +37,17 @@ MODELS = {
|
|||
}
|
||||
|
||||
|
||||
def model_references(build_dir: Path) -> dict[str, dict[str, Any]]:
|
||||
def model_references(
|
||||
build_dir: Path, *, require_imu: bool = True
|
||||
) -> dict[str, dict[str, Any]]:
|
||||
cache = {}
|
||||
for line in (build_dir / "CMakeCache.txt").read_text().splitlines():
|
||||
if line.startswith("SWITCH2_") and ":" in line and "=" in line:
|
||||
field, value = line.split("=", 1)
|
||||
cache[field.split(":", 1)[0]] = value
|
||||
if (
|
||||
cache.get("SWITCH2_BRIDGE_INPUT") == "DUALSENSE"
|
||||
require_imu
|
||||
and cache.get("SWITCH2_BRIDGE_INPUT") in ("DUALSENSE", "GAMEPAD")
|
||||
and cache.get("SWITCH2_BRIDGE_IMU_TARGET", "BOTH") != "BOTH"
|
||||
):
|
||||
raise ValueError(
|
||||
|
|
@ -145,6 +148,7 @@ class Check:
|
|||
"duration_seconds": args.duration,
|
||||
"usb_timeout_ms": args.usb_timeout_ms,
|
||||
"rumble_sample": args.rumble_sample,
|
||||
"require_imu": not args.input_only,
|
||||
},
|
||||
"safety": {
|
||||
"pairing_writes": False,
|
||||
|
|
@ -170,6 +174,7 @@ class Check:
|
|||
side: {
|
||||
"packets": 0,
|
||||
"valid_native_imu": 0,
|
||||
"valid_native_packets": 0,
|
||||
"imu_counter_changes": 0,
|
||||
"wrong_side": 0,
|
||||
"unexpected_report": 0,
|
||||
|
|
@ -186,6 +191,7 @@ class Check:
|
|||
"first_valid_seconds": None,
|
||||
"last_valid_seconds": None,
|
||||
"last_counter_change_seconds": None,
|
||||
"last_control_change_seconds": None,
|
||||
}
|
||||
for side in SIDES
|
||||
},
|
||||
|
|
@ -697,15 +703,29 @@ class Check:
|
|||
rejection = f"native report is {len(packet)} bytes, expected 64"
|
||||
else:
|
||||
payload = packet[1:]
|
||||
stream["valid_native_packets"] += 1
|
||||
controls = (payload[2:4], payload[5:8])
|
||||
sample["buttons_hex"], sample["stick_hex"] = (
|
||||
part.hex() for part in controls
|
||||
)
|
||||
if any(controls[0]):
|
||||
stream["buttons_nonzero"] += 1
|
||||
if side in self.last_controls and self.last_controls[side] != controls:
|
||||
stream["control_changes"] += 1
|
||||
stream["last_control_change_seconds"] = self.elapsed()
|
||||
self.last_controls[side] = controls
|
||||
length_offset = 14 if side == "L" else 15
|
||||
length = payload[length_offset]
|
||||
key = str(length)
|
||||
stream["imu_lengths"][key] = stream["imu_lengths"].get(key, 0) + 1
|
||||
if length == 0:
|
||||
stream["zero_length_imu"] += 1
|
||||
rejection = (
|
||||
"zero-length IMU: inactive donor/neutral fallback is not live input"
|
||||
)
|
||||
if self.args.input_only:
|
||||
if len(stream["samples"]) < 4:
|
||||
stream["samples"].append(sample)
|
||||
stream["last_sample"] = sample
|
||||
else:
|
||||
rejection = "zero-length IMU: inactive donor/neutral fallback is not live motion"
|
||||
else:
|
||||
try:
|
||||
block = (
|
||||
|
|
@ -743,18 +763,6 @@ class Check:
|
|||
self.last_counter[side] = counter
|
||||
if len(self.imu_evidence[side]) < 512:
|
||||
self.imu_evidence[side].add(block)
|
||||
controls = (payload[2:4], payload[5:8])
|
||||
sample["buttons_hex"], sample["stick_hex"] = (
|
||||
part.hex() for part in controls
|
||||
)
|
||||
if any(controls[0]):
|
||||
stream["buttons_nonzero"] += 1
|
||||
if (
|
||||
side in self.last_controls
|
||||
and self.last_controls[side] != controls
|
||||
):
|
||||
stream["control_changes"] += 1
|
||||
self.last_controls[side] = controls
|
||||
format_key = f"{decoded['format']:02x}"
|
||||
first_format = format_key not in stream["imu_formats"]
|
||||
stream["imu_formats"][format_key] = (
|
||||
|
|
@ -783,7 +791,10 @@ class Check:
|
|||
|
||||
def counts(self) -> dict[str, int]:
|
||||
return {
|
||||
side: self.result["streams"][side]["valid_native_imu"] for side in SIDES
|
||||
side: self.result["streams"][side][
|
||||
"valid_native_packets" if self.args.input_only else "valid_native_imu"
|
||||
]
|
||||
for side in SIDES
|
||||
}
|
||||
|
||||
def donors_ready(self) -> None:
|
||||
|
|
@ -793,23 +804,31 @@ class Check:
|
|||
self.poll_pair(until, bool(iteration % 2))
|
||||
iteration += 1
|
||||
if all(
|
||||
stream["valid_native_imu"] >= 3 and stream["imu_counter_changes"] >= 2
|
||||
(stream["buttons_nonzero"] > 0 and stream["control_changes"] >= 2)
|
||||
if self.args.input_only
|
||||
else (
|
||||
stream["valid_native_imu"] >= 3
|
||||
and stream["imu_counter_changes"] >= 2
|
||||
)
|
||||
for stream in self.result["streams"].values()
|
||||
):
|
||||
return
|
||||
details = "; ".join(
|
||||
f"{side}: valid={stream['valid_native_imu']}, counter_changes={stream['imu_counter_changes']}, zero_imu={stream['zero_length_imu']}, invalid={stream['invalid']}, timeouts={stream['timeouts']}"
|
||||
f"{side}: imu={stream['valid_native_imu']}, counter_changes={stream['imu_counter_changes']}, buttons={stream['buttons_nonzero']}, control_changes={stream['control_changes']}, zero_imu={stream['zero_length_imu']}, invalid={stream['invalid']}, timeouts={stream['timeouts']}"
|
||||
for side, stream in self.result["streams"].items()
|
||||
)
|
||||
raise RuntimeError(
|
||||
f"donors did not produce fresh decodable native IMU during the manual-wake window; {details}; no pairing/wake/reset was attempted"
|
||||
f"sources did not satisfy the requested live-input evidence during the manual-input window; {details}; no pairing/wake/reset was attempted"
|
||||
)
|
||||
|
||||
def active(self) -> None:
|
||||
until = min(self.deadline, time.monotonic() + self.args.duration)
|
||||
initial_counts = self.counts()
|
||||
initial_changes = {
|
||||
side: self.result["streams"][side]["imu_counter_changes"] for side in SIDES
|
||||
side: self.result["streams"][side][
|
||||
"control_changes" if self.args.input_only else "imu_counter_changes"
|
||||
]
|
||||
for side in SIDES
|
||||
}
|
||||
next_query = time.monotonic()
|
||||
round_number = 0
|
||||
|
|
@ -851,10 +870,12 @@ class Check:
|
|||
"no interleaved control/bulk round was bracketed by valid input from both donors"
|
||||
)
|
||||
shared = self.imu_evidence["R"] & self.imu_evidence["L"]
|
||||
shared_source = self.models["R"].get("source_mode") == "DUALSENSE"
|
||||
shared_source = self.models["R"].get("source_mode") in ("DUALSENSE", "GAMEPAD")
|
||||
self.result["imu_isolation"] = {
|
||||
"sample_limit_per_side": 512,
|
||||
"policy": "shared_physical_source"
|
||||
"policy": "not_required_input_only"
|
||||
if self.args.input_only
|
||||
else "shared_physical_source"
|
||||
if shared_source
|
||||
else "independent_physical_sources",
|
||||
"identical_blocks_seen_on_both_sides": len(shared),
|
||||
|
|
@ -865,7 +886,7 @@ class Check:
|
|||
side: len(blocks - shared) for side, blocks in self.imu_evidence.items()
|
||||
},
|
||||
}
|
||||
for side in SIDES:
|
||||
for side in () if self.args.input_only else SIDES:
|
||||
evidence = (
|
||||
self.imu_evidence[side]
|
||||
if shared_source
|
||||
|
|
@ -881,18 +902,26 @@ class Check:
|
|||
for side in SIDES:
|
||||
stream = self.result["streams"][side]
|
||||
if (
|
||||
stream["valid_native_imu"] - initial_counts[side] < 3
|
||||
or stream["imu_counter_changes"] - initial_changes[side] < 2
|
||||
self.counts()[side] - initial_counts[side] < 3
|
||||
or stream[
|
||||
"control_changes" if self.args.input_only else "imu_counter_changes"
|
||||
]
|
||||
- initial_changes[side]
|
||||
< 2
|
||||
):
|
||||
self.error(
|
||||
"insufficient fresh native donor IMU during active control/bulk reads",
|
||||
"insufficient fresh controller transitions"
|
||||
if self.args.input_only
|
||||
else "insufficient fresh native source IMU during active control/bulk reads",
|
||||
side,
|
||||
)
|
||||
last_change = stream["last_counter_change_seconds"]
|
||||
last_change = stream[
|
||||
"last_control_change_seconds"
|
||||
if self.args.input_only
|
||||
else "last_counter_change_seconds"
|
||||
]
|
||||
if last_change is None or self.elapsed() - last_change > 2:
|
||||
self.error(
|
||||
"donor IMU stopped advancing before streaming finished", side
|
||||
)
|
||||
self.error("source stopped advancing before streaming finished", side)
|
||||
if stream["wrong_side"] or stream["unexpected_report"] or stream["invalid"]:
|
||||
self.error(
|
||||
f"rejected wrong-side={stream['wrong_side']}, unexpected={stream['unexpected_report']}, malformed={stream['invalid']} HID packets",
|
||||
|
|
@ -938,7 +967,9 @@ class Check:
|
|||
import usb.util
|
||||
|
||||
with self.stage("references"):
|
||||
self.models = model_references(self.args.build_dir)
|
||||
self.models = model_references(
|
||||
self.args.build_dir, require_imu=not self.args.input_only
|
||||
)
|
||||
self.core, self.util = usb.core, usb.util
|
||||
with self.stage("discovery"):
|
||||
self.discover()
|
||||
|
|
@ -1044,6 +1075,11 @@ def main() -> int:
|
|||
default=500,
|
||||
help="per control/bulk transfer timeout, 20..3000 ms (default: 500)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--input-only",
|
||||
action="store_true",
|
||||
help="qualify controllers without IMU; requires real button presses and continued control changes on both halves",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--rumble-sample",
|
||||
type=int,
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@
|
|||
#include <inttypes.h>
|
||||
extern "C" int probe_debug_printf(const char* format, ...);
|
||||
#endif
|
||||
#if SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
#include "dualsense_input.h"
|
||||
#if SWITCH2_BRIDGE_FULL_INPUT
|
||||
#include "native_gamepad_input.h"
|
||||
#endif
|
||||
#if SWITCH2_BRIDGE_WII_INPUT
|
||||
#include <math.h>
|
||||
|
|
@ -26,9 +26,9 @@ extern "C" int probe_debug_printf(const char* format, ...);
|
|||
|
||||
#if !SWITCH_PICO_SWITCH2_USB_BRIDGE || !SWITCH_PICO_BLUEPAD32
|
||||
#error "The controller bridge requires Bluepad32"
|
||||
#elif SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
#if !SWITCH_PICO_ENABLE_CLASSIC
|
||||
#error "The DualSense bridge requires Classic Bluetooth"
|
||||
#elif SWITCH2_BRIDGE_FULL_INPUT
|
||||
#if SWITCH2_BRIDGE_DUALSENSE_INPUT && !SWITCH_PICO_ENABLE_CLASSIC
|
||||
#error "The DualSense source requires Classic Bluetooth"
|
||||
#endif
|
||||
#elif !SWITCH_PICO_ENABLE_BLE || !SWITCH_PICO_SWITCH2_MOUSE_CAPTURE || \
|
||||
!SWITCH_PICO_SWITCH2_MOUSE_CAPTURE_NATIVE
|
||||
|
|
@ -36,7 +36,7 @@ extern "C" int probe_debug_printf(const char* format, ...);
|
|||
#endif
|
||||
|
||||
namespace {
|
||||
#if !SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
#if !SWITCH2_BRIDGE_FULL_INPUT
|
||||
constexpr uint8_t kSourceAddress[] = {SWITCH2_BRIDGE_SOURCE_ADDRESS_BYTES};
|
||||
static_assert(sizeof(kSourceAddress) == 6, "Select one physical Bluetooth address");
|
||||
#if SWITCH2_PROBE_COMPOSITE || SWITCH2_PROBE_HUB
|
||||
|
|
@ -56,7 +56,7 @@ bool g_start_attempted;
|
|||
bool g_flash_ready;
|
||||
#if SWITCH2_BRIDGE_WII_INPUT
|
||||
probe_controller_input g_input;
|
||||
#elif !SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
#elif !SWITCH2_BRIDGE_FULL_INPUT
|
||||
probe_controller_input g_inputs[PROBE_CONTROLLER_COUNT];
|
||||
uint32_t g_received_times[PROBE_CONTROLLER_COUNT];
|
||||
#endif
|
||||
|
|
@ -382,9 +382,9 @@ extern "C" void probe_controller_input_clock_init(void) {
|
|||
|
||||
extern "C" void probe_controller_input_init(void) {
|
||||
if (g_initialized) return;
|
||||
#if SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
#if SWITCH2_BRIDGE_FULL_INPUT
|
||||
bluepad32_input_backend_init();
|
||||
probe_dualsense_input_init();
|
||||
probe_native_gamepad_input_init();
|
||||
#elif SWITCH2_BRIDGE_WII_INPUT
|
||||
bluepad32_input_backend_init();
|
||||
bluepad32_input_backend_select_wii_source(kSourceAddress);
|
||||
|
|
@ -484,17 +484,17 @@ extern "C" void probe_controller_input_set_native_features(uint8_t features) {
|
|||
}
|
||||
#endif
|
||||
|
||||
#if SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
#if SWITCH2_BRIDGE_FULL_INPUT
|
||||
extern "C" void probe_controller_input_set_full_stick_calibration(
|
||||
uint8_t instance, const uint8_t calibration[9]) {
|
||||
probe_dualsense_input_set_stick_calibration(instance, calibration);
|
||||
probe_native_gamepad_input_set_stick_calibration(instance, calibration);
|
||||
}
|
||||
#endif
|
||||
|
||||
extern "C" void probe_controller_input_set_native_stream(uint8_t instance, bool enabled) {
|
||||
if (instance >= PROBE_CONTROLLER_COUNT) return;
|
||||
#if SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
probe_dualsense_input_set_native_stream(instance, enabled && g_flash_ready);
|
||||
#if SWITCH2_BRIDGE_FULL_INPUT
|
||||
probe_native_gamepad_input_set_native_stream(instance, enabled && g_flash_ready);
|
||||
#elif SWITCH2_BRIDGE_WII_INPUT
|
||||
enabled = enabled && g_flash_ready;
|
||||
if (g_native_stream != enabled || !enabled) discard_wii_output();
|
||||
|
|
@ -508,8 +508,8 @@ extern "C" void probe_controller_input_set_native_stream(uint8_t instance, bool
|
|||
extern "C" uint32_t probe_controller_input_peek_native_report(
|
||||
uint8_t instance, uint32_t now_ms, uint8_t report[63]) {
|
||||
if (instance >= PROBE_CONTROLLER_COUNT || !g_flash_ready) return 0;
|
||||
#if SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
return probe_dualsense_input_peek_native_report(instance, now_ms, report);
|
||||
#if SWITCH2_BRIDGE_FULL_INPUT
|
||||
return probe_native_gamepad_input_peek_native_report(instance, now_ms, report);
|
||||
#elif SWITCH2_BRIDGE_WII_INPUT
|
||||
(void)now_ms;
|
||||
return prepare_wii_report(report);
|
||||
|
|
@ -520,8 +520,8 @@ extern "C" uint32_t probe_controller_input_peek_native_report(
|
|||
|
||||
extern "C" bool probe_controller_input_commit_native_report(uint8_t instance, uint32_t serial) {
|
||||
if (instance >= PROBE_CONTROLLER_COUNT || !g_flash_ready) return false;
|
||||
#if SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
return probe_dualsense_input_commit_native_report(instance, serial);
|
||||
#if SWITCH2_BRIDGE_FULL_INPUT
|
||||
return probe_native_gamepad_input_commit_native_report(instance, serial);
|
||||
#elif SWITCH2_BRIDGE_WII_INPUT
|
||||
if (!g_native_stream || !serial || serial != g_pending_serial ||
|
||||
g_pending_generation != g_wii_generation || !g_wii_active) return false;
|
||||
|
|
@ -543,8 +543,8 @@ extern "C" bool probe_controller_input_play_sample(uint8_t instance, uint8_t sam
|
|||
if (token != nullptr) *token = 0;
|
||||
return false;
|
||||
}
|
||||
#if SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
return bluepad32_input_backend_dualsense_sample_request(instance, sample_id, token);
|
||||
#if SWITCH2_BRIDGE_FULL_INPUT
|
||||
return bluepad32_input_backend_native_sample_request(instance, sample_id, token);
|
||||
#elif SWITCH2_BRIDGE_WII_INPUT
|
||||
return bluepad32_input_backend_wii_sample_request(sample_id, token);
|
||||
#else
|
||||
|
|
@ -555,9 +555,9 @@ extern "C" bool probe_controller_input_play_sample(uint8_t instance, uint8_t sam
|
|||
|
||||
extern "C" int probe_controller_input_sample_result(uint8_t instance, uint64_t token, uint32_t now_ms) {
|
||||
if (instance >= PROBE_CONTROLLER_COUNT || !g_flash_ready) return -1;
|
||||
#if SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
#if SWITCH2_BRIDGE_FULL_INPUT
|
||||
(void)now_ms;
|
||||
return bluepad32_input_backend_dualsense_sample_result(instance, token);
|
||||
return bluepad32_input_backend_native_sample_result(instance, token);
|
||||
#elif SWITCH2_BRIDGE_WII_INPUT
|
||||
(void)now_ms;
|
||||
return bluepad32_input_backend_wii_sample_result(token);
|
||||
|
|
@ -568,8 +568,8 @@ extern "C" int probe_controller_input_sample_result(uint8_t instance, uint64_t t
|
|||
|
||||
extern "C" void probe_controller_input_cancel_sample(uint8_t instance) {
|
||||
if (instance >= PROBE_CONTROLLER_COUNT) return;
|
||||
#if SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
bluepad32_input_backend_dualsense_sample_cancel(instance);
|
||||
#if SWITCH2_BRIDGE_FULL_INPUT
|
||||
bluepad32_input_backend_native_sample_cancel(instance);
|
||||
#elif SWITCH2_BRIDGE_WII_INPUT
|
||||
bluepad32_input_backend_wii_sample_cancel();
|
||||
#else
|
||||
|
|
@ -584,8 +584,8 @@ extern "C" void probe_controller_input_poll(uint8_t instance, uint32_t now_ms,
|
|||
*out = {};
|
||||
return;
|
||||
}
|
||||
#if SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
probe_dualsense_input_poll(instance, now_ms, out);
|
||||
#if SWITCH2_BRIDGE_FULL_INPUT
|
||||
probe_native_gamepad_input_poll(instance, now_ms, out);
|
||||
return;
|
||||
#elif SWITCH2_BRIDGE_WII_INPUT
|
||||
poll_wii_source(now_ms);
|
||||
|
|
@ -618,7 +618,7 @@ extern "C" void probe_controller_input_poll(uint8_t instance, uint32_t now_ms,
|
|||
g_input.mouse_surface = 0;
|
||||
}
|
||||
#endif
|
||||
#if !SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
#if !SWITCH2_BRIDGE_FULL_INPUT
|
||||
*out = g_input;
|
||||
#endif
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ void probe_controller_input_set_stick_calibration(const uint8_t calibration[9]);
|
|||
// Native feature changes are output barriers, not Bluetooth/IMU resets.
|
||||
void probe_controller_input_set_native_features(uint8_t features);
|
||||
#endif
|
||||
#if SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
#if SWITCH2_BRIDGE_FULL_INPUT
|
||||
// Supply each child's advertised, validated nine-byte stick record. Native
|
||||
// output stays unavailable until that child's calibration has been supplied.
|
||||
void probe_controller_input_set_full_stick_calibration(uint8_t instance, const uint8_t calibration[9]);
|
||||
|
|
@ -55,7 +55,7 @@ void probe_controller_input_set_full_stick_calibration(uint8_t instance, const u
|
|||
// Core0 native07/08 output. Disable discards queued/prepared data; repeated
|
||||
// enable preserves it. Joy-Con mode relays its bounded FIFO; right-only Wii
|
||||
// mode synthesizes fresh calibrated sensors and the selected IR pointer.
|
||||
// DualSense mode splits one full controller into independent R/L output streams;
|
||||
// Full-controller mode splits one supported gamepad into independent R/L output streams;
|
||||
// controls remain live while motion is unavailable. Only Wii estimates stationary bias.
|
||||
// No pairing changes.
|
||||
void probe_controller_input_set_native_stream(uint8_t instance, bool enabled);
|
||||
|
|
|
|||
|
|
@ -1,13 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include "controller_input.h"
|
||||
|
||||
#if SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
// Core 0 only. One coherent profile/motion evaluation feeds both native children.
|
||||
void probe_dualsense_input_init();
|
||||
void probe_dualsense_input_set_stick_calibration(uint8_t instance, const uint8_t calibration[9]);
|
||||
void probe_dualsense_input_set_native_stream(uint8_t instance, bool enabled);
|
||||
void probe_dualsense_input_poll(uint8_t instance, uint32_t now_ms, probe_controller_input* out);
|
||||
uint32_t probe_dualsense_input_peek_native_report(uint8_t instance, uint32_t now_ms, uint8_t report[63]);
|
||||
bool probe_dualsense_input_commit_native_report(uint8_t instance, uint32_t token);
|
||||
#endif
|
||||
|
|
@ -319,7 +319,7 @@ static void reset_controller_protocol(uint8_t instance) {
|
|||
#if SWITCH2_BRIDGE_WII_INPUT
|
||||
probe_controller_input_set_stick_calibration(stick_calibration);
|
||||
probe_controller_input_set_native_features(0);
|
||||
#elif SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
#elif SWITCH2_BRIDGE_FULL_INPUT
|
||||
probe_controller_input_set_full_stick_calibration(instance, stick_calibration);
|
||||
#endif
|
||||
protocol->read_memory = read_memory;
|
||||
|
|
@ -487,8 +487,8 @@ static void protocol_task(probe_usb_controller* controller, uint32_t now) {
|
|||
#if SWITCH2_BRIDGE_WII_INPUT
|
||||
probe_debug_printf("[PROBE] Wii cue dispatched itf=%u token=%" PRIu64 "\n",
|
||||
instance, reply->deferred_token);
|
||||
#elif SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
probe_debug_printf("[PROBE] DualSense cue dispatched itf=%u token=%" PRIu64 "\n",
|
||||
#elif SWITCH2_BRIDGE_FULL_INPUT
|
||||
probe_debug_printf("[PROBE] Native gamepad cue dispatched itf=%u token=%" PRIu64 "\n",
|
||||
instance, reply->deferred_token);
|
||||
#else
|
||||
probe_debug_printf("[PROBE] Source sample ACK itf=%u token=%" PRIu64 "\n",
|
||||
|
|
@ -528,7 +528,7 @@ static void protocol_task(probe_usb_controller* controller, uint32_t now) {
|
|||
native_serial = probe_controller_input_peek_native_report(instance, now, input);
|
||||
if (!native_serial) return;
|
||||
length = sizeof(input);
|
||||
#if SWITCH2_BRIDGE_WII_INPUT || SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
#if SWITCH2_BRIDGE_WII_INPUT || SWITCH2_BRIDGE_FULL_INPUT
|
||||
input[8] = (uint8_t)(0x30 | ((protocol->enabled_features & 0x20) ? 8 : 0));
|
||||
#endif
|
||||
probe_protocol_gate_native_report(protocol, input);
|
||||
|
|
@ -792,6 +792,8 @@ int main(void) {
|
|||
probe_controller_input_init();
|
||||
#if SWITCH2_BRIDGE_WII_INPUT
|
||||
probe_debug_printf("[PROBE] UART0 GP0=TX, 115200 8N1; selected Wii IR/MotionPlus source enabled\n");
|
||||
#elif SWITCH2_BRIDGE_FULL_INPUT
|
||||
probe_debug_printf("[PROBE] UART0 GP0=TX, 115200 8N1; one supported gamepad feeds the native R/L pair\n");
|
||||
#else
|
||||
probe_debug_printf("[PROBE] UART0 GP0=TX, 115200 8N1; %u selected Joy-Con Bluetooth source(s)\n",
|
||||
PROBE_CONTROLLER_COUNT);
|
||||
|
|
@ -819,6 +821,10 @@ int main(void) {
|
|||
#if SWITCH2_BRIDGE_WII_INPUT
|
||||
probe_debug_printf("[PROBE] Wii IR drives native mouse movement; buttons retain profile mapping; keep Wii still for MotionPlus calibration\n");
|
||||
probe_debug_printf("[PROBE] Hold BOOTSEL2s for pairing; Wii cue feedback uses bounded ERM patterns, not HD audio waveforms\n");
|
||||
#elif SWITCH2_BRIDGE_FULL_INPUT
|
||||
probe_debug_printf("[PROBE] Full gamepad controls on R/L; IMU mask=%u; only Wii requires settling\n",
|
||||
(unsigned)SWITCH2_BRIDGE_IMU_TARGET_MASK);
|
||||
probe_debug_printf("[PROBE] Hold BOOTSEL 2s for Bluetooth pairing (never clears pairings); cues use source capabilities\n");
|
||||
#else
|
||||
probe_debug_printf("[PROBE] Live Joy-Con buttons/stick/native mouse; hold BOOTSEL 2s for Bluetooth pairing (never clears pairings)\n");
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
#include "dualsense_input.h"
|
||||
#include "native_gamepad_input.h"
|
||||
|
||||
#if SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
#if SWITCH2_BRIDGE_FULL_INPUT
|
||||
#include <limits.h>
|
||||
#include <string.h>
|
||||
|
||||
|
|
@ -11,7 +11,7 @@
|
|||
#include "profile/controller_profile_runtime.h"
|
||||
|
||||
#if !SWITCH2_PROBE_HUB || SWITCH2_BRIDGE_WII_INPUT
|
||||
#error "One DualSense requires the native R/L USB hub source mode"
|
||||
#error "A full gamepad source requires the native R/L USB hub"
|
||||
#endif
|
||||
static_assert(PROBE_CONTROLLER_COUNT == 2);
|
||||
extern "C" int probe_debug_printf(const char* format, ...);
|
||||
|
|
@ -40,15 +40,17 @@ struct Child {
|
|||
uint32_t pending_token = 0;
|
||||
uint32_t pending_us = 0;
|
||||
uint32_t pending_ticks = 0;
|
||||
uint32_t pending_motion_sequence = 0;
|
||||
uint32_t pending_accel_sequence = 0;
|
||||
uint32_t pending_gyro_sequence = 0;
|
||||
bool pending_motion = false;
|
||||
uint8_t pending_report[63]{};
|
||||
bool have_committed_motion = false;
|
||||
uint32_t committed_motion_sequence = 0;
|
||||
uint32_t committed_accel_sequence = 0;
|
||||
uint32_t committed_gyro_sequence = 0;
|
||||
uint32_t committed_ticks = 0;
|
||||
};
|
||||
Child g_children[PROBE_CONTROLLER_COUNT];
|
||||
Bluepad32DualSenseBridgeSnapshot g_source;
|
||||
Bluepad32NativeGamepadSnapshot g_source;
|
||||
ControllerProfileTransformResult g_mapped;
|
||||
ProbeNativeMotion g_motion;
|
||||
bool g_active;
|
||||
|
|
@ -78,8 +80,10 @@ void discard_output(Child& child) {
|
|||
child.have_committed_motion = false;
|
||||
}
|
||||
|
||||
bool sensors_fresh(uint32_t now_us) {
|
||||
return g_source.motion_valid && now_us - g_source.motion_received_us < kSensorDeadlineUs;
|
||||
bool sensors_fresh(const Bluepad32NativeGamepadSnapshot& source, uint32_t now_us) {
|
||||
return source.accel_valid && source.gyro_valid &&
|
||||
now_us - source.accel_received_us < kSensorDeadlineUs &&
|
||||
now_us - source.gyro_received_us < kSensorDeadlineUs;
|
||||
}
|
||||
|
||||
void unpack_stick_pair(const uint8_t* bytes, uint16_t pair[2]) {
|
||||
|
|
@ -147,7 +151,7 @@ void lose_source(uint32_t now_ms) {
|
|||
g_motion.reset();
|
||||
for (uint8_t i = 0; i < PROBE_CONTROLLER_COUNT; ++i) {
|
||||
discard_output(g_children[i]);
|
||||
bluepad32_input_backend_dualsense_sample_cancel(i);
|
||||
bluepad32_input_backend_native_sample_cancel(i);
|
||||
}
|
||||
g_sensor_status = -1;
|
||||
}
|
||||
|
|
@ -156,8 +160,8 @@ void lose_source(uint32_t now_ms) {
|
|||
}
|
||||
|
||||
void refresh(uint32_t now_ms) {
|
||||
Bluepad32DualSenseBridgeSnapshot source;
|
||||
bluepad32_input_backend_dualsense_snapshot(&source);
|
||||
Bluepad32NativeGamepadSnapshot source;
|
||||
bluepad32_input_backend_native_snapshot(&source);
|
||||
// Snapshot first: source receipt timestamps must not be ahead of this clock.
|
||||
const uint32_t now_us = time_us_32();
|
||||
advance_clock(now_us);
|
||||
|
|
@ -174,13 +178,15 @@ void refresh(uint32_t now_ms) {
|
|||
// motion evaluation. A real publication in the same millisecond still wins.
|
||||
if (!changed_connection && g_evaluated && g_evaluated_ms == now_ms &&
|
||||
source.state_generation == g_source.state_generation && source.received_us == g_source.received_us &&
|
||||
source.motion_sequence == g_source.motion_sequence &&
|
||||
source.motion_received_us == g_source.motion_received_us && source.motion_valid == g_source.motion_valid) return;
|
||||
source.accel_sequence == g_source.accel_sequence && source.gyro_sequence == g_source.gyro_sequence &&
|
||||
source.accel_received_us == g_source.accel_received_us && source.gyro_received_us == g_source.gyro_received_us &&
|
||||
source.accel_valid == g_source.accel_valid && source.gyro_valid == g_source.gyro_valid &&
|
||||
source.requires_stationary_bias == g_source.requires_stationary_bias) return;
|
||||
if (changed_connection) {
|
||||
lose_source(now_ms);
|
||||
g_motion.reset();
|
||||
for (Child& child : g_children) discard_output(child);
|
||||
probe_debug_printf("[PROBE] DualSense source active in slot %u; using validated factory IMU calibration\n", source.slot);
|
||||
probe_debug_printf("[PROBE] Native gamepad source active in slot %u\n", source.slot);
|
||||
}
|
||||
g_source = source;
|
||||
g_active = true;
|
||||
|
|
@ -194,9 +200,12 @@ void refresh(uint32_t now_ms) {
|
|||
feedback.active_profile_number, feedback.policy);
|
||||
}
|
||||
ProbeNativeMotionSample sample{};
|
||||
sample.accel_valid = sample.gyro_valid = source.motion_valid;
|
||||
sample.accel_sequence = sample.gyro_sequence = source.motion_sequence;
|
||||
sample.accel_us = sample.gyro_us = source.motion_received_us;
|
||||
sample.accel_valid = source.accel_valid;
|
||||
sample.gyro_valid = source.gyro_valid;
|
||||
sample.accel_sequence = source.accel_sequence;
|
||||
sample.gyro_sequence = source.gyro_sequence;
|
||||
sample.accel_us = source.accel_received_us;
|
||||
sample.gyro_us = source.gyro_received_us;
|
||||
// SDL -> upright native body [X,-Z,Y], the same physical transform used by
|
||||
// the Wii adapter before its mouse-mount rotation. Both halves represent
|
||||
// one rigid, full controller: no solo-Joy-Con or mouse mounting rotation.
|
||||
|
|
@ -206,12 +215,15 @@ void refresh(uint32_t now_ms) {
|
|||
sample.gyro_dps[0] = static_cast<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;
|
||||
g_motion.update(now_us, source.controller.connection_generation, sample,
|
||||
source.requires_stationary_bias ? ProbeNativeMotionBias::kEstimateStationary :
|
||||
ProbeNativeMotionBias::kAlreadyCalibrated);
|
||||
const int status = !sensors_fresh(g_source, now_us) ? 0 : g_motion.ready() ? 2 : 1;
|
||||
if (status != g_sensor_status) {
|
||||
g_sensor_status = status;
|
||||
probe_debug_printf("[PROBE] DualSense native IMU %s\n", status == 2 ? "ready" :
|
||||
status == 1 ? "waiting for a usable acceleration sample" : "waiting for fresh complete sensors");
|
||||
probe_debug_printf("[PROBE] Native gamepad IMU %s\n", status == 2 ? "ready" :
|
||||
status == 1 ? (source.requires_stationary_bias ? "Wii calibrating: keep still" :
|
||||
"waiting for a usable acceleration sample") : "waiting for supported fresh sensors");
|
||||
}
|
||||
for (uint8_t i = 0; i < PROBE_CONTROLLER_COUNT; ++i) {
|
||||
// Latest-only: a blocked endpoint never queues obsolete controls/IMU.
|
||||
|
|
@ -221,15 +233,15 @@ void refresh(uint32_t now_ms) {
|
|||
}
|
||||
} // namespace
|
||||
|
||||
void probe_dualsense_input_init() {
|
||||
void probe_native_gamepad_input_init() {
|
||||
#if SWITCH2_BRIDGE_SOURCE_AUTO
|
||||
bluepad32_input_backend_select_dualsense_source(nullptr);
|
||||
bluepad32_input_backend_select_native_source(nullptr);
|
||||
#else
|
||||
bluepad32_input_backend_select_dualsense_source(kSourceAddress);
|
||||
bluepad32_input_backend_select_native_source(kSourceAddress);
|
||||
#endif
|
||||
}
|
||||
|
||||
void probe_dualsense_input_set_stick_calibration(uint8_t instance, const uint8_t calibration[9]) {
|
||||
void probe_native_gamepad_input_set_stick_calibration(uint8_t instance, const uint8_t calibration[9]) {
|
||||
if (instance >= PROBE_CONTROLLER_COUNT) return;
|
||||
Child& child = g_children[instance];
|
||||
child.calibrated = false;
|
||||
|
|
@ -248,30 +260,31 @@ void probe_dualsense_input_set_stick_calibration(uint8_t instance, const uint8_t
|
|||
pack_controls(instance);
|
||||
}
|
||||
|
||||
void probe_dualsense_input_set_native_stream(uint8_t instance, bool enabled) {
|
||||
void probe_native_gamepad_input_set_native_stream(uint8_t instance, bool enabled) {
|
||||
if (instance >= PROBE_CONTROLLER_COUNT) return;
|
||||
Child& child = g_children[instance];
|
||||
if (child.enabled != enabled || !enabled) discard_output(child);
|
||||
child.enabled = enabled;
|
||||
if (!enabled) bluepad32_input_backend_dualsense_sample_cancel(instance);
|
||||
if (!enabled) bluepad32_input_backend_native_sample_cancel(instance);
|
||||
}
|
||||
|
||||
void probe_dualsense_input_poll(uint8_t instance, uint32_t now_ms, probe_controller_input* out) {
|
||||
void probe_native_gamepad_input_poll(uint8_t instance, uint32_t now_ms, probe_controller_input* out) {
|
||||
if (!out) return;
|
||||
if (instance >= PROBE_CONTROLLER_COUNT) { *out = {}; return; }
|
||||
refresh(now_ms);
|
||||
*out = g_children[instance].input;
|
||||
}
|
||||
|
||||
uint32_t probe_dualsense_input_peek_native_report(uint8_t instance, uint32_t now_ms, uint8_t report[63]) {
|
||||
uint32_t probe_native_gamepad_input_peek_native_report(uint8_t instance, uint32_t now_ms, uint8_t report[63]) {
|
||||
if (instance >= PROBE_CONTROLLER_COUNT || !report) return 0;
|
||||
refresh(now_ms);
|
||||
Child& child = g_children[instance];
|
||||
if (!child.enabled || !child.input.active) return 0;
|
||||
const uint32_t now_us = time_us_32();
|
||||
const bool motion_ready = (SWITCH2_BRIDGE_IMU_TARGET_MASK & (1u << instance)) != 0 &&
|
||||
g_motion.ready() && sensors_fresh(now_us) &&
|
||||
(!child.have_committed_motion || child.committed_motion_sequence != g_source.motion_sequence);
|
||||
g_motion.ready() && sensors_fresh(g_source, now_us) &&
|
||||
(!child.have_committed_motion || child.committed_accel_sequence != g_source.accel_sequence ||
|
||||
child.committed_gyro_sequence != g_source.gyro_sequence);
|
||||
if (child.pending_token && (now_us - child.pending_us >= kOutputDeadlineUs ||
|
||||
child.pending_motion != motion_ready)) child.pending_token = 0;
|
||||
if (!child.pending_token) {
|
||||
|
|
@ -293,7 +306,8 @@ uint32_t probe_dualsense_input_peek_native_report(uint8_t instance, uint32_t now
|
|||
g_motion.quaternion(), g_motion.acceleration(), static_cast<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_accel_sequence = g_source.accel_sequence;
|
||||
child.pending_gyro_sequence = g_source.gyro_sequence;
|
||||
child.pending_us = now_us;
|
||||
child.pending_token = ++g_report_token;
|
||||
}
|
||||
|
|
@ -301,27 +315,28 @@ uint32_t probe_dualsense_input_peek_native_report(uint8_t instance, uint32_t now
|
|||
return child.pending_token;
|
||||
}
|
||||
|
||||
bool probe_dualsense_input_commit_native_report(uint8_t instance, uint32_t token) {
|
||||
bool probe_native_gamepad_input_commit_native_report(uint8_t instance, uint32_t token) {
|
||||
if (instance >= PROBE_CONTROLLER_COUNT || !token) return false;
|
||||
Child& child = g_children[instance];
|
||||
// Check the live source even when the caller did not poll after a disconnect.
|
||||
Bluepad32DualSenseBridgeSnapshot source;
|
||||
bluepad32_input_backend_dualsense_snapshot(&source);
|
||||
Bluepad32NativeGamepadSnapshot source;
|
||||
bluepad32_input_backend_native_snapshot(&source);
|
||||
const uint32_t now_us = time_us_32();
|
||||
if (!child.enabled || !g_active || child.pending_token != token ||
|
||||
!source.controller.active || source.slot != g_source.slot ||
|
||||
source.controller.connection_generation != g_source.controller.connection_generation ||
|
||||
source.state_generation != g_source.state_generation ||
|
||||
source.motion_sequence != g_source.motion_sequence ||
|
||||
source.motion_received_us != g_source.motion_received_us ||
|
||||
source.motion_valid != g_source.motion_valid ||
|
||||
source.accel_sequence != g_source.accel_sequence || source.gyro_sequence != g_source.gyro_sequence ||
|
||||
source.accel_received_us != g_source.accel_received_us || source.gyro_received_us != g_source.gyro_received_us ||
|
||||
source.accel_valid != g_source.accel_valid || source.gyro_valid != g_source.gyro_valid ||
|
||||
source.requires_stationary_bias != g_source.requires_stationary_bias ||
|
||||
now_us - source.received_us >= kInputDeadlineUs || now_us - child.pending_us >= kOutputDeadlineUs ||
|
||||
(child.pending_motion && (!source.motion_valid ||
|
||||
now_us - g_source.motion_received_us >= kSensorDeadlineUs))) return false;
|
||||
(child.pending_motion && !sensors_fresh(source, now_us))) return false;
|
||||
child.pending_token = 0;
|
||||
if (child.pending_motion) {
|
||||
child.have_committed_motion = true;
|
||||
child.committed_motion_sequence = child.pending_motion_sequence;
|
||||
child.committed_accel_sequence = child.pending_accel_sequence;
|
||||
child.committed_gyro_sequence = child.pending_gyro_sequence;
|
||||
child.committed_ticks = child.pending_ticks;
|
||||
}
|
||||
++child.counter;
|
||||
13
tools/switch2_usb_probe/native_gamepad_input.h
Normal file
13
tools/switch2_usb_probe/native_gamepad_input.h
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
#pragma once
|
||||
|
||||
#include "controller_input.h"
|
||||
|
||||
#if SWITCH2_BRIDGE_FULL_INPUT
|
||||
// Core 0 only. One coherent profile/motion evaluation feeds both native children.
|
||||
void probe_native_gamepad_input_init();
|
||||
void probe_native_gamepad_input_set_stick_calibration(uint8_t instance, const uint8_t calibration[9]);
|
||||
void probe_native_gamepad_input_set_native_stream(uint8_t instance, bool enabled);
|
||||
void probe_native_gamepad_input_poll(uint8_t instance, uint32_t now_ms, probe_controller_input* out);
|
||||
uint32_t probe_native_gamepad_input_peek_native_report(uint8_t instance, uint32_t now_ms, uint8_t report[63]);
|
||||
bool probe_native_gamepad_input_commit_native_report(uint8_t instance, uint32_t token);
|
||||
#endif
|
||||
|
|
@ -25,8 +25,8 @@ else()
|
|||
endif()
|
||||
if(SWITCH2_PROBE_COMPOSITE OR SWITCH2_PROBE_HUB)
|
||||
if(NOT SWITCH_PICO_SWITCH2_USB_BRIDGE OR SWITCH2_BRIDGE_WII_INPUT
|
||||
OR (NOT SWITCH2_BRIDGE_INPUT STREQUAL "JOYCON2" AND NOT SWITCH2_BRIDGE_DUALSENSE_INPUT))
|
||||
message(FATAL_ERROR "Native R/L output requires JOYCON2 input, or DUALSENSE with HUB")
|
||||
OR (NOT SWITCH2_BRIDGE_INPUT STREQUAL "JOYCON2" AND NOT SWITCH2_BRIDGE_FULL_INPUT))
|
||||
message(FATAL_ERROR "Native R/L output requires JOYCON2 input or a full-controller HUB source")
|
||||
endif()
|
||||
set(probe_composite 0)
|
||||
if(SWITCH2_PROBE_COMPOSITE)
|
||||
|
|
@ -54,10 +54,10 @@ set(SWITCH2_BRIDGE_SECOND_SOURCE_ADDRESS "" CACHE STRING
|
|||
set(SWITCH2_BRIDGE_SOURCE_AUTO OFF)
|
||||
if(SWITCH_PICO_SWITCH2_USB_BRIDGE OR SWITCH2_PROBE_COMPOSITE)
|
||||
set(probe_source_fields SWITCH2_BRIDGE_SOURCE_ADDRESS)
|
||||
if(SWITCH2_BRIDGE_DUALSENSE_INPUT AND SWITCH2_BRIDGE_SOURCE_ADDRESS STREQUAL "")
|
||||
if(SWITCH2_BRIDGE_FULL_INPUT AND SWITCH2_BRIDGE_SOURCE_ADDRESS STREQUAL "")
|
||||
set(SWITCH2_BRIDGE_SOURCE_AUTO ON)
|
||||
set(probe_source_fields "")
|
||||
elseif((SWITCH2_PROBE_COMPOSITE OR SWITCH2_PROBE_HUB) AND NOT SWITCH2_BRIDGE_DUALSENSE_INPUT)
|
||||
elseif((SWITCH2_PROBE_COMPOSITE OR SWITCH2_PROBE_HUB) AND NOT SWITCH2_BRIDGE_FULL_INPUT)
|
||||
list(APPEND probe_source_fields SWITCH2_BRIDGE_SECOND_SOURCE_ADDRESS)
|
||||
endif()
|
||||
set(probe_source_addresses "")
|
||||
|
|
@ -367,11 +367,11 @@ function(switch2_usb_probe_configure target)
|
|||
else()
|
||||
pico_set_program_name(${target} "Switch 2 USB initialization capture")
|
||||
endif()
|
||||
if(SWITCH2_PROBE_HUB AND SWITCH2_BRIDGE_DUALSENSE_INPUT)
|
||||
if(SWITCH2_PROBE_HUB AND SWITCH2_BRIDGE_FULL_INPUT)
|
||||
if(SWITCH2_PROBE_TRACE_NATIVE_INPUT)
|
||||
pico_set_program_version(${target} "0.69-native-hub-imu-trace")
|
||||
pico_set_program_version(${target} "0.70-native-gamepad-trace")
|
||||
else()
|
||||
pico_set_program_version(${target} "0.69-native-hub-imu")
|
||||
pico_set_program_version(${target} "0.70-native-gamepad")
|
||||
endif()
|
||||
elseif(SWITCH2_PROBE_HUB)
|
||||
pico_set_program_version(${target} "0.67-native-hub-latency")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue