Add DualSense native pair and Wii-only IMU settling
This commit is contained in:
parent
1748910316
commit
ab70536fdf
31 changed files with 2617 additions and 77 deletions
|
|
@ -43,6 +43,14 @@ def model_references(build_dir: Path) -> dict[str, dict[str, Any]]:
|
|||
if line.startswith("SWITCH2_") and ":" in line and "=" in line:
|
||||
field, value = line.split("=", 1)
|
||||
cache[field.split(":", 1)[0]] = value
|
||||
if (
|
||||
cache.get("SWITCH2_BRIDGE_INPUT") == "DUALSENSE"
|
||||
and cache.get("SWITCH2_BRIDGE_IMU_TARGET", "BOTH") != "BOTH"
|
||||
):
|
||||
raise ValueError(
|
||||
"Full dual-IMU qualification requires SWITCH2_BRIDGE_IMU_TARGET=BOTH; "
|
||||
"use the USB-completion UART trace for LEFT/RIGHT routing comparisons"
|
||||
)
|
||||
models = {}
|
||||
for side, constants in MODELS.items():
|
||||
prefix = "SWITCH2_PROBE" if side == "R" else "SWITCH2_PROBE_SECOND"
|
||||
|
|
@ -68,6 +76,7 @@ def model_references(build_dir: Path) -> dict[str, dict[str, Any]]:
|
|||
"version": version.hex(),
|
||||
"factory_extension": factory[64:81].hex(),
|
||||
"mac_wire": address[::-1].hex(),
|
||||
"source_mode": cache.get("SWITCH2_BRIDGE_INPUT", "JOYCON2"),
|
||||
}
|
||||
return models
|
||||
|
||||
|
|
@ -842,8 +851,12 @@ class Check:
|
|||
"no interleaved control/bulk round was bracketed by valid input from both donors"
|
||||
)
|
||||
shared = self.imu_evidence["R"] & self.imu_evidence["L"]
|
||||
shared_source = self.models["R"].get("source_mode") == "DUALSENSE"
|
||||
self.result["imu_isolation"] = {
|
||||
"sample_limit_per_side": 512,
|
||||
"policy": "shared_physical_source"
|
||||
if shared_source
|
||||
else "independent_physical_sources",
|
||||
"identical_blocks_seen_on_both_sides": len(shared),
|
||||
"unique_blocks": {
|
||||
side: len(blocks) for side, blocks in self.imu_evidence.items()
|
||||
|
|
@ -853,9 +866,16 @@ class Check:
|
|||
},
|
||||
}
|
||||
for side in SIDES:
|
||||
if len(self.imu_evidence[side] - shared) < 2:
|
||||
evidence = (
|
||||
self.imu_evidence[side]
|
||||
if shared_source
|
||||
else self.imu_evidence[side] - shared
|
||||
)
|
||||
if len(evidence) < 2:
|
||||
self.error(
|
||||
"donor IMU evidence is frozen or duplicated across child devices",
|
||||
"IMU evidence is frozen"
|
||||
if shared_source
|
||||
else "donor IMU evidence is frozen or duplicated across child devices",
|
||||
side,
|
||||
)
|
||||
for side in SIDES:
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@
|
|||
#include <inttypes.h>
|
||||
extern "C" int probe_debug_printf(const char* format, ...);
|
||||
#endif
|
||||
#if SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
#include "dualsense_input.h"
|
||||
#endif
|
||||
#if SWITCH2_BRIDGE_WII_INPUT
|
||||
#include <math.h>
|
||||
#include "input/wii_ir_pointer.h"
|
||||
|
|
@ -21,13 +24,19 @@ extern "C" int probe_debug_printf(const char* format, ...);
|
|||
extern "C" int probe_debug_printf(const char* format, ...);
|
||||
#endif
|
||||
|
||||
#if !SWITCH_PICO_SWITCH2_USB_BRIDGE || !SWITCH_PICO_BLUEPAD32 || \
|
||||
!SWITCH_PICO_ENABLE_BLE || !SWITCH_PICO_SWITCH2_MOUSE_CAPTURE || \
|
||||
#if !SWITCH_PICO_SWITCH2_USB_BRIDGE || !SWITCH_PICO_BLUEPAD32
|
||||
#error "The controller bridge requires Bluepad32"
|
||||
#elif SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
#if !SWITCH_PICO_ENABLE_CLASSIC
|
||||
#error "The DualSense bridge requires Classic Bluetooth"
|
||||
#endif
|
||||
#elif !SWITCH_PICO_ENABLE_BLE || !SWITCH_PICO_SWITCH2_MOUSE_CAPTURE || \
|
||||
!SWITCH_PICO_SWITCH2_MOUSE_CAPTURE_NATIVE
|
||||
#error "The controller bridge requires Bluepad32 BLE and native Switch 2 capture"
|
||||
#error "The Joy-Con/Wii bridge requires Bluepad32 BLE and native Switch 2 capture"
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
#if !SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
constexpr uint8_t kSourceAddress[] = {SWITCH2_BRIDGE_SOURCE_ADDRESS_BYTES};
|
||||
static_assert(sizeof(kSourceAddress) == 6, "Select one physical Bluetooth address");
|
||||
#if SWITCH2_PROBE_COMPOSITE || SWITCH2_PROBE_HUB
|
||||
|
|
@ -35,6 +44,7 @@ constexpr uint8_t kSecondSourceAddress[] = {SWITCH2_BRIDGE_SECOND_SOURCE_ADDRESS
|
|||
static_assert(sizeof(kSecondSourceAddress) == 6, "Select the second physical Bluetooth address");
|
||||
#endif
|
||||
constexpr uint32_t kInputDeadlineMs = 500;
|
||||
#endif
|
||||
#if !SWITCH2_PROBE_HUB
|
||||
constexpr uint32_t kFlashCoordinationTimeoutMs = 1000;
|
||||
#endif
|
||||
|
|
@ -46,7 +56,7 @@ bool g_start_attempted;
|
|||
bool g_flash_ready;
|
||||
#if SWITCH2_BRIDGE_WII_INPUT
|
||||
probe_controller_input g_input;
|
||||
#else
|
||||
#elif !SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
probe_controller_input g_inputs[PROBE_CONTROLLER_COUNT];
|
||||
uint32_t g_received_times[PROBE_CONTROLLER_COUNT];
|
||||
#endif
|
||||
|
|
@ -280,7 +290,7 @@ void poll_wii_source(uint32_t now_ms) {
|
|||
motion.gyro_dps[0] = static_cast<float>(g_wii.gyro_q10[1]) / 1024.0f;
|
||||
motion.gyro_dps[1] = -static_cast<float>(g_wii.gyro_q10[2]) / 1024.0f;
|
||||
motion.gyro_dps[2] = -static_cast<float>(g_wii.gyro_q10[0]) / 1024.0f;
|
||||
g_motion.update(now_us, g_wii_generation, motion);
|
||||
g_motion.update(now_us, g_wii_generation, motion, ProbeNativeMotionBias::kEstimateStationary);
|
||||
WiiIrMouseReport optical{};
|
||||
(void)wii_ir_mouse_peek(&optical, 0);
|
||||
// Core 1 may publish during the peek. Read the clock after the snapshot.
|
||||
|
|
@ -372,7 +382,10 @@ extern "C" void probe_controller_input_clock_init(void) {
|
|||
|
||||
extern "C" void probe_controller_input_init(void) {
|
||||
if (g_initialized) return;
|
||||
#if SWITCH2_BRIDGE_WII_INPUT
|
||||
#if SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
bluepad32_input_backend_init();
|
||||
probe_dualsense_input_init();
|
||||
#elif SWITCH2_BRIDGE_WII_INPUT
|
||||
bluepad32_input_backend_init();
|
||||
bluepad32_input_backend_select_wii_source(kSourceAddress);
|
||||
g_screen_configured = wii_ir_pointer_configure_screen(
|
||||
|
|
@ -471,9 +484,18 @@ extern "C" void probe_controller_input_set_native_features(uint8_t features) {
|
|||
}
|
||||
#endif
|
||||
|
||||
#if SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
extern "C" void probe_controller_input_set_full_stick_calibration(
|
||||
uint8_t instance, const uint8_t calibration[9]) {
|
||||
probe_dualsense_input_set_stick_calibration(instance, calibration);
|
||||
}
|
||||
#endif
|
||||
|
||||
extern "C" void probe_controller_input_set_native_stream(uint8_t instance, bool enabled) {
|
||||
if (instance >= PROBE_CONTROLLER_COUNT) return;
|
||||
#if SWITCH2_BRIDGE_WII_INPUT
|
||||
#if SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
probe_dualsense_input_set_native_stream(instance, enabled && g_flash_ready);
|
||||
#elif SWITCH2_BRIDGE_WII_INPUT
|
||||
enabled = enabled && g_flash_ready;
|
||||
if (g_native_stream != enabled || !enabled) discard_wii_output();
|
||||
g_native_stream = enabled;
|
||||
|
|
@ -486,7 +508,9 @@ extern "C" void probe_controller_input_set_native_stream(uint8_t instance, bool
|
|||
extern "C" uint32_t probe_controller_input_peek_native_report(
|
||||
uint8_t instance, uint32_t now_ms, uint8_t report[63]) {
|
||||
if (instance >= PROBE_CONTROLLER_COUNT || !g_flash_ready) return 0;
|
||||
#if SWITCH2_BRIDGE_WII_INPUT
|
||||
#if SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
return probe_dualsense_input_peek_native_report(instance, now_ms, report);
|
||||
#elif SWITCH2_BRIDGE_WII_INPUT
|
||||
(void)now_ms;
|
||||
return prepare_wii_report(report);
|
||||
#else
|
||||
|
|
@ -496,7 +520,9 @@ extern "C" uint32_t probe_controller_input_peek_native_report(
|
|||
|
||||
extern "C" bool probe_controller_input_commit_native_report(uint8_t instance, uint32_t serial) {
|
||||
if (instance >= PROBE_CONTROLLER_COUNT || !g_flash_ready) return false;
|
||||
#if SWITCH2_BRIDGE_WII_INPUT
|
||||
#if SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
return probe_dualsense_input_commit_native_report(instance, serial);
|
||||
#elif SWITCH2_BRIDGE_WII_INPUT
|
||||
if (!g_native_stream || !serial || serial != g_pending_serial ||
|
||||
g_pending_generation != g_wii_generation || !g_wii_active) return false;
|
||||
wii_ir_mouse_commit(g_pending_pointer);
|
||||
|
|
@ -517,7 +543,9 @@ extern "C" bool probe_controller_input_play_sample(uint8_t instance, uint8_t sam
|
|||
if (token != nullptr) *token = 0;
|
||||
return false;
|
||||
}
|
||||
#if SWITCH2_BRIDGE_WII_INPUT
|
||||
#if SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
return bluepad32_input_backend_dualsense_sample_request(instance, sample_id, token);
|
||||
#elif SWITCH2_BRIDGE_WII_INPUT
|
||||
return bluepad32_input_backend_wii_sample_request(sample_id, token);
|
||||
#else
|
||||
return switch2_mouse_capture_request_sample(
|
||||
|
|
@ -527,7 +555,10 @@ extern "C" bool probe_controller_input_play_sample(uint8_t instance, uint8_t sam
|
|||
|
||||
extern "C" int probe_controller_input_sample_result(uint8_t instance, uint64_t token, uint32_t now_ms) {
|
||||
if (instance >= PROBE_CONTROLLER_COUNT || !g_flash_ready) return -1;
|
||||
#if SWITCH2_BRIDGE_WII_INPUT
|
||||
#if SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
(void)now_ms;
|
||||
return bluepad32_input_backend_dualsense_sample_result(instance, token);
|
||||
#elif SWITCH2_BRIDGE_WII_INPUT
|
||||
(void)now_ms;
|
||||
return bluepad32_input_backend_wii_sample_result(token);
|
||||
#else
|
||||
|
|
@ -537,7 +568,9 @@ extern "C" int probe_controller_input_sample_result(uint8_t instance, uint64_t t
|
|||
|
||||
extern "C" void probe_controller_input_cancel_sample(uint8_t instance) {
|
||||
if (instance >= PROBE_CONTROLLER_COUNT) return;
|
||||
#if SWITCH2_BRIDGE_WII_INPUT
|
||||
#if SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
bluepad32_input_backend_dualsense_sample_cancel(instance);
|
||||
#elif SWITCH2_BRIDGE_WII_INPUT
|
||||
bluepad32_input_backend_wii_sample_cancel();
|
||||
#else
|
||||
switch2_mouse_capture_cancel_sample(instance);
|
||||
|
|
@ -551,7 +584,10 @@ extern "C" void probe_controller_input_poll(uint8_t instance, uint32_t now_ms,
|
|||
*out = {};
|
||||
return;
|
||||
}
|
||||
#if SWITCH2_BRIDGE_WII_INPUT
|
||||
#if SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
probe_dualsense_input_poll(instance, now_ms, out);
|
||||
return;
|
||||
#elif SWITCH2_BRIDGE_WII_INPUT
|
||||
poll_wii_source(now_ms);
|
||||
#else
|
||||
probe_controller_input& g_input = g_inputs[instance];
|
||||
|
|
@ -582,5 +618,7 @@ extern "C" void probe_controller_input_poll(uint8_t instance, uint32_t now_ms,
|
|||
g_input.mouse_surface = 0;
|
||||
}
|
||||
#endif
|
||||
#if !SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
*out = g_input;
|
||||
#endif
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,9 +47,16 @@ void probe_controller_input_set_stick_calibration(const uint8_t calibration[9]);
|
|||
// Native feature changes are output barriers, not Bluetooth/IMU resets.
|
||||
void probe_controller_input_set_native_features(uint8_t features);
|
||||
#endif
|
||||
#if SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
// Supply each child's advertised, validated nine-byte stick record. Native
|
||||
// output stays unavailable until that child's calibration has been supplied.
|
||||
void probe_controller_input_set_full_stick_calibration(uint8_t instance, const uint8_t calibration[9]);
|
||||
#endif
|
||||
// Core0 native07/08 output. Disable discards queued/prepared data; repeated
|
||||
// enable preserves it. Joy-Con mode relays its bounded FIFO; right-only Wii
|
||||
// mode synthesizes fresh calibrated sensors and the selected IR pointer.
|
||||
// DualSense mode splits one full controller into independent R/L output streams;
|
||||
// controls remain live while motion is unavailable. Only Wii estimates stationary bias.
|
||||
// No pairing changes.
|
||||
void probe_controller_input_set_native_stream(uint8_t instance, bool enabled);
|
||||
// Copy one63-byte payload without report ID. Returns a boot-unique token, or0
|
||||
|
|
@ -61,8 +68,9 @@ uint32_t probe_controller_input_peek_native_report(uint8_t instance, uint32_t no
|
|||
bool probe_controller_input_commit_native_report(uint8_t instance, uint32_t serial);
|
||||
// Built-in vibration samples only; raw HD-rumble output is not forwarded.
|
||||
// A nonzero token means queued, not completed. Result:0 pending,1 completion,
|
||||
// -1 failed/stale. Joy-Con completion is its application ACK; Wii completion is
|
||||
// actual bounded rumble-driver dispatch (not an HD-waveform fidelity claim).
|
||||
// -1 failed/stale. Joy-Con completion is its application ACK; Wii/DualSense
|
||||
// completion is actual bounded rumble-driver dispatch, not a source application
|
||||
// ACK or an HD-waveform fidelity claim.
|
||||
// Reset cancels the request, never stored pairing.
|
||||
bool probe_controller_input_play_sample(uint8_t instance, uint8_t sample_id, uint64_t* token);
|
||||
int probe_controller_input_sample_result(uint8_t instance, uint64_t token, uint32_t now_ms);
|
||||
|
|
|
|||
330
tools/switch2_usb_probe/dualsense_input.cpp
Normal file
330
tools/switch2_usb_probe/dualsense_input.cpp
Normal file
|
|
@ -0,0 +1,330 @@
|
|||
#include "dualsense_input.h"
|
||||
|
||||
#if SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
#include <limits.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "input/bluepad32_input_backend.h"
|
||||
#include "model.h"
|
||||
#include "native_imu.h"
|
||||
#include "pico/time.h"
|
||||
#include "profile/controller_profile_runtime.h"
|
||||
|
||||
#if !SWITCH2_PROBE_HUB || SWITCH2_BRIDGE_WII_INPUT
|
||||
#error "One DualSense requires the native R/L USB hub source mode"
|
||||
#endif
|
||||
static_assert(PROBE_CONTROLLER_COUNT == 2);
|
||||
extern "C" int probe_debug_printf(const char* format, ...);
|
||||
#ifndef SWITCH2_BRIDGE_IMU_TARGET_MASK
|
||||
#define SWITCH2_BRIDGE_IMU_TARGET_MASK 3
|
||||
#endif
|
||||
static_assert(SWITCH2_BRIDGE_IMU_TARGET_MASK >= 1 && SWITCH2_BRIDGE_IMU_TARGET_MASK <= 3);
|
||||
|
||||
namespace {
|
||||
constexpr uint32_t kInputDeadlineUs = 500000;
|
||||
constexpr uint32_t kSensorDeadlineUs = 150000;
|
||||
constexpr uint32_t kOutputDeadlineUs = 100000;
|
||||
#if !SWITCH2_BRIDGE_SOURCE_AUTO
|
||||
constexpr uint8_t kSourceAddress[] = {SWITCH2_BRIDGE_SOURCE_ADDRESS_BYTES};
|
||||
static_assert(sizeof(kSourceAddress) == 6);
|
||||
#endif
|
||||
|
||||
struct Child {
|
||||
bool enabled = false;
|
||||
bool calibrated = false;
|
||||
uint16_t center[2]{};
|
||||
uint16_t positive[2]{};
|
||||
uint16_t negative[2]{};
|
||||
probe_controller_input input{};
|
||||
uint8_t counter = 0;
|
||||
uint32_t pending_token = 0;
|
||||
uint32_t pending_us = 0;
|
||||
uint32_t pending_ticks = 0;
|
||||
uint32_t pending_motion_sequence = 0;
|
||||
bool pending_motion = false;
|
||||
uint8_t pending_report[63]{};
|
||||
bool have_committed_motion = false;
|
||||
uint32_t committed_motion_sequence = 0;
|
||||
uint32_t committed_ticks = 0;
|
||||
};
|
||||
Child g_children[PROBE_CONTROLLER_COUNT];
|
||||
Bluepad32DualSenseBridgeSnapshot g_source;
|
||||
ControllerProfileTransformResult g_mapped;
|
||||
ProbeNativeMotion g_motion;
|
||||
bool g_active;
|
||||
bool g_evaluated;
|
||||
uint32_t g_evaluated_ms;
|
||||
uint32_t g_report_token;
|
||||
int g_sensor_status = -1;
|
||||
bool g_clock_started;
|
||||
uint32_t g_clock_us;
|
||||
uint32_t g_clock_ticks;
|
||||
uint32_t g_clock_fraction;
|
||||
|
||||
void advance_clock(uint32_t now_us) {
|
||||
if (!g_clock_started) {
|
||||
g_clock_started = true;
|
||||
g_clock_us = now_us;
|
||||
return;
|
||||
}
|
||||
const uint64_t scaled = static_cast<uint64_t>(now_us - g_clock_us) * 960u + g_clock_fraction;
|
||||
g_clock_us = now_us;
|
||||
g_clock_ticks += static_cast<uint32_t>(scaled / 1000000u);
|
||||
g_clock_fraction = static_cast<uint32_t>(scaled % 1000000u);
|
||||
}
|
||||
|
||||
void discard_output(Child& child) {
|
||||
child.pending_token = 0;
|
||||
child.have_committed_motion = false;
|
||||
}
|
||||
|
||||
bool sensors_fresh(uint32_t now_us) {
|
||||
return g_source.motion_valid && now_us - g_source.motion_received_us < kSensorDeadlineUs;
|
||||
}
|
||||
|
||||
void unpack_stick_pair(const uint8_t* bytes, uint16_t pair[2]) {
|
||||
pair[0] = bytes[0] | (static_cast<uint16_t>(bytes[1] & 15) << 8);
|
||||
pair[1] = (bytes[1] >> 4) | (static_cast<uint16_t>(bytes[2]) << 4);
|
||||
}
|
||||
|
||||
uint16_t calibrated_axis(const Child& child, int16_t value, unsigned axis, bool invert) {
|
||||
// Same signed endpoint/rounding convention as the native Wii adapter.
|
||||
const int32_t input = value;
|
||||
const bool input_positive = input >= 0;
|
||||
const bool output_positive = input_positive != invert;
|
||||
const int32_t magnitude = input_positive ? input : -input;
|
||||
const int32_t denominator = input_positive ? INT16_MAX : 32768;
|
||||
const int32_t travel = output_positive ? child.positive[axis] : child.negative[axis];
|
||||
const int32_t displacement = (magnitude * travel + denominator / 2) / denominator;
|
||||
return static_cast<uint16_t>(child.center[axis] +
|
||||
(output_positive ? displacement : -displacement));
|
||||
}
|
||||
|
||||
void pack_controls(uint8_t instance) {
|
||||
Child& child = g_children[instance];
|
||||
child.input = {};
|
||||
child.input.serial = g_source.state_generation;
|
||||
if (!g_active || !child.calibrated) return;
|
||||
child.input.active = true;
|
||||
child.input.native_status = 0x30; // Host feature status is gated per model in main.
|
||||
child.input.mouse_surface = 0xff; // No optical sensor, clicks, or invented movement.
|
||||
const ControllerState& state = g_mapped.state;
|
||||
const bool left = probe_model_is_left(instance);
|
||||
if (left) {
|
||||
child.input.buttons[0] = static_cast<uint8_t>(
|
||||
(state.dpad_down ? 0x01 : 0) | (state.dpad_right ? 0x02 : 0) |
|
||||
(state.dpad_left ? 0x04 : 0) | (state.dpad_up ? 0x08 : 0) |
|
||||
(state.button_left_shoulder ? 0x10 : 0) |
|
||||
(state.left_trigger != 0 && state.left_trigger >= g_mapped.left_trigger_digital_threshold ? 0x20 : 0) |
|
||||
(state.button_select ? 0x40 : 0) | (state.button_left_stick ? 0x80 : 0));
|
||||
child.input.buttons[1] = static_cast<uint8_t>(
|
||||
(state.button_capture ? 0x01 : 0) |
|
||||
((state.extra_buttons & (1u << 3)) ? 0x80 : 0) |
|
||||
((state.extra_buttons & (1u << 4)) ? 0x40 : 0));
|
||||
} else {
|
||||
child.input.buttons[0] = static_cast<uint8_t>(
|
||||
(state.button_south ? 0x01 : 0) | (state.button_east ? 0x02 : 0) |
|
||||
(state.button_west ? 0x04 : 0) | (state.button_north ? 0x08 : 0) |
|
||||
(state.button_right_shoulder ? 0x10 : 0) |
|
||||
(state.right_trigger != 0 && state.right_trigger >= g_mapped.right_trigger_digital_threshold ? 0x20 : 0) |
|
||||
(state.button_start ? 0x40 : 0) | (state.button_right_stick ? 0x80 : 0));
|
||||
child.input.buttons[1] = static_cast<uint8_t>(
|
||||
(state.button_system ? 0x01 : 0) | ((state.extra_buttons & 1) ? 0x10 : 0) |
|
||||
((state.extra_buttons & (1u << 5)) ? 0x80 : 0) |
|
||||
((state.extra_buttons & (1u << 6)) ? 0x40 : 0));
|
||||
}
|
||||
const uint16_t x = calibrated_axis(child, left ? state.left_stick_x : state.right_stick_x, 0, false);
|
||||
const uint16_t y = calibrated_axis(child, left ? state.left_stick_y : state.right_stick_y, 1, true);
|
||||
child.input.stick[0] = static_cast<uint8_t>(x);
|
||||
child.input.stick[1] = static_cast<uint8_t>((x >> 8) | (y << 4));
|
||||
child.input.stick[2] = static_cast<uint8_t>(y >> 4);
|
||||
}
|
||||
|
||||
void lose_source(uint32_t now_ms) {
|
||||
if (g_active) {
|
||||
Bluepad32SlotSnapshot inactive{};
|
||||
(void)controller_profile_runtime_transform(g_source.slot, inactive, now_ms, AdapterUsbMode::kSwitch);
|
||||
g_motion.reset();
|
||||
for (uint8_t i = 0; i < PROBE_CONTROLLER_COUNT; ++i) {
|
||||
discard_output(g_children[i]);
|
||||
bluepad32_input_backend_dualsense_sample_cancel(i);
|
||||
}
|
||||
g_sensor_status = -1;
|
||||
}
|
||||
g_active = false;
|
||||
for (Child& child : g_children) child.input = {};
|
||||
}
|
||||
|
||||
void refresh(uint32_t now_ms) {
|
||||
Bluepad32DualSenseBridgeSnapshot source;
|
||||
bluepad32_input_backend_dualsense_snapshot(&source);
|
||||
// Snapshot first: source receipt timestamps must not be ahead of this clock.
|
||||
const uint32_t now_us = time_us_32();
|
||||
advance_clock(now_us);
|
||||
if (!source.controller.active || source.slot >= BLUEPAD32_INPUT_BACKEND_SLOT_COUNT ||
|
||||
now_us - source.received_us >= kInputDeadlineUs) {
|
||||
lose_source(now_ms);
|
||||
g_source = source;
|
||||
g_evaluated = false;
|
||||
return;
|
||||
}
|
||||
const bool changed_connection = !g_active || source.slot != g_source.slot ||
|
||||
source.controller.connection_generation != g_source.controller.connection_generation;
|
||||
// Both polls and both peeks in a paired output round share one profile and
|
||||
// motion evaluation. A real publication in the same millisecond still wins.
|
||||
if (!changed_connection && g_evaluated && g_evaluated_ms == now_ms &&
|
||||
source.state_generation == g_source.state_generation && source.received_us == g_source.received_us &&
|
||||
source.motion_sequence == g_source.motion_sequence &&
|
||||
source.motion_received_us == g_source.motion_received_us && source.motion_valid == g_source.motion_valid) return;
|
||||
if (changed_connection) {
|
||||
lose_source(now_ms);
|
||||
g_motion.reset();
|
||||
for (Child& child : g_children) discard_output(child);
|
||||
probe_debug_printf("[PROBE] DualSense source active in slot %u; using validated factory IMU calibration\n", source.slot);
|
||||
}
|
||||
g_source = source;
|
||||
g_active = true;
|
||||
g_evaluated = true;
|
||||
g_evaluated_ms = now_ms;
|
||||
g_mapped = controller_profile_runtime_transform(source.slot, source.controller, now_ms, AdapterUsbMode::kSwitch);
|
||||
ControllerProfileRuntimeProfileChangeEvent feedback{};
|
||||
if (controller_profile_runtime_take_initial_profile_indication(source.slot, &feedback) ||
|
||||
controller_profile_runtime_take_profile_change(source.slot, &feedback)) {
|
||||
bluepad32_input_backend_queue_profile_feedback(source.slot, feedback.connection_generation,
|
||||
feedback.active_profile_number, feedback.policy);
|
||||
}
|
||||
ProbeNativeMotionSample sample{};
|
||||
sample.accel_valid = sample.gyro_valid = source.motion_valid;
|
||||
sample.accel_sequence = sample.gyro_sequence = source.motion_sequence;
|
||||
sample.accel_us = sample.gyro_us = source.motion_received_us;
|
||||
// SDL -> upright native body [X,-Z,Y], the same physical transform used by
|
||||
// the Wii adapter before its mouse-mount rotation. Both halves represent
|
||||
// one rigid, full controller: no solo-Joy-Con or mouse mounting rotation.
|
||||
sample.accel_g[0] = static_cast<float>(source.accel_q13[0]) / 8192.0f;
|
||||
sample.accel_g[1] = -static_cast<float>(source.accel_q13[2]) / 8192.0f;
|
||||
sample.accel_g[2] = static_cast<float>(source.accel_q13[1]) / 8192.0f;
|
||||
sample.gyro_dps[0] = static_cast<float>(source.gyro_q10[0]) / 1024.0f;
|
||||
sample.gyro_dps[1] = -static_cast<float>(source.gyro_q10[2]) / 1024.0f;
|
||||
sample.gyro_dps[2] = static_cast<float>(source.gyro_q10[1]) / 1024.0f;
|
||||
g_motion.update(now_us, source.controller.connection_generation, sample);
|
||||
const int status = !sensors_fresh(now_us) ? 0 : g_motion.ready() ? 2 : 1;
|
||||
if (status != g_sensor_status) {
|
||||
g_sensor_status = status;
|
||||
probe_debug_printf("[PROBE] DualSense native IMU %s\n", status == 2 ? "ready" :
|
||||
status == 1 ? "waiting for a usable acceleration sample" : "waiting for fresh complete sensors");
|
||||
}
|
||||
for (uint8_t i = 0; i < PROBE_CONTROLLER_COUNT; ++i) {
|
||||
// Latest-only: a blocked endpoint never queues obsolete controls/IMU.
|
||||
g_children[i].pending_token = 0;
|
||||
pack_controls(i);
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void probe_dualsense_input_init() {
|
||||
#if SWITCH2_BRIDGE_SOURCE_AUTO
|
||||
bluepad32_input_backend_select_dualsense_source(nullptr);
|
||||
#else
|
||||
bluepad32_input_backend_select_dualsense_source(kSourceAddress);
|
||||
#endif
|
||||
}
|
||||
|
||||
void probe_dualsense_input_set_stick_calibration(uint8_t instance, const uint8_t calibration[9]) {
|
||||
if (instance >= PROBE_CONTROLLER_COUNT) return;
|
||||
Child& child = g_children[instance];
|
||||
child.calibrated = false;
|
||||
discard_output(child);
|
||||
if (calibration) {
|
||||
unpack_stick_pair(calibration, child.center);
|
||||
unpack_stick_pair(calibration + 3, child.positive);
|
||||
unpack_stick_pair(calibration + 6, child.negative);
|
||||
child.calibrated = true;
|
||||
for (unsigned axis = 0; axis < 2; ++axis) {
|
||||
if (!child.positive[axis] || !child.negative[axis] ||
|
||||
child.center[axis] + child.positive[axis] > 4095 ||
|
||||
child.negative[axis] > child.center[axis]) child.calibrated = false;
|
||||
}
|
||||
}
|
||||
pack_controls(instance);
|
||||
}
|
||||
|
||||
void probe_dualsense_input_set_native_stream(uint8_t instance, bool enabled) {
|
||||
if (instance >= PROBE_CONTROLLER_COUNT) return;
|
||||
Child& child = g_children[instance];
|
||||
if (child.enabled != enabled || !enabled) discard_output(child);
|
||||
child.enabled = enabled;
|
||||
if (!enabled) bluepad32_input_backend_dualsense_sample_cancel(instance);
|
||||
}
|
||||
|
||||
void probe_dualsense_input_poll(uint8_t instance, uint32_t now_ms, probe_controller_input* out) {
|
||||
if (!out) return;
|
||||
if (instance >= PROBE_CONTROLLER_COUNT) { *out = {}; return; }
|
||||
refresh(now_ms);
|
||||
*out = g_children[instance].input;
|
||||
}
|
||||
|
||||
uint32_t probe_dualsense_input_peek_native_report(uint8_t instance, uint32_t now_ms, uint8_t report[63]) {
|
||||
if (instance >= PROBE_CONTROLLER_COUNT || !report) return 0;
|
||||
refresh(now_ms);
|
||||
Child& child = g_children[instance];
|
||||
if (!child.enabled || !child.input.active) return 0;
|
||||
const uint32_t now_us = time_us_32();
|
||||
const bool motion_ready = (SWITCH2_BRIDGE_IMU_TARGET_MASK & (1u << instance)) != 0 &&
|
||||
g_motion.ready() && sensors_fresh(now_us) &&
|
||||
(!child.have_committed_motion || child.committed_motion_sequence != g_source.motion_sequence);
|
||||
if (child.pending_token && (now_us - child.pending_us >= kOutputDeadlineUs ||
|
||||
child.pending_motion != motion_ready)) child.pending_token = 0;
|
||||
if (!child.pending_token) {
|
||||
if (g_report_token == UINT32_MAX) return 0; // Boot-unique, including across children/resets.
|
||||
memset(child.pending_report, 0, sizeof(child.pending_report));
|
||||
child.pending_report[0] = child.counter;
|
||||
// Bluepad32 zero denotes unknown; do not manufacture a full battery.
|
||||
const unsigned battery_level = (static_cast<unsigned>(g_source.battery) * 9u + 127u) / 255u;
|
||||
child.pending_report[1] = static_cast<uint8_t>(battery_level << 2);
|
||||
memcpy(child.pending_report + 2, child.input.buttons, sizeof(child.input.buttons));
|
||||
child.pending_report[4] = 7;
|
||||
memcpy(child.pending_report + 5, child.input.stick, sizeof(child.input.stick));
|
||||
child.pending_report[8] = child.input.native_status;
|
||||
child.pending_report[13] = 0xff;
|
||||
child.pending_ticks = g_clock_ticks;
|
||||
const uint32_t elapsed = child.have_committed_motion ? child.pending_ticks - child.committed_ticks : 1;
|
||||
const uint16_t wire_elapsed = static_cast<uint16_t>(elapsed <= 0xfff ? elapsed : 1);
|
||||
child.pending_motion = motion_ready && probe_native_imu_pack(
|
||||
g_motion.quaternion(), g_motion.acceleration(), static_cast<uint16_t>(child.pending_ticks & 0xfff),
|
||||
wire_elapsed, 0, child.pending_report + probe_model_imu_data_offset(instance));
|
||||
if (child.pending_motion) child.pending_report[probe_model_imu_length_offset(instance)] = 30;
|
||||
child.pending_motion_sequence = g_source.motion_sequence;
|
||||
child.pending_us = now_us;
|
||||
child.pending_token = ++g_report_token;
|
||||
}
|
||||
memcpy(report, child.pending_report, sizeof(child.pending_report));
|
||||
return child.pending_token;
|
||||
}
|
||||
|
||||
bool probe_dualsense_input_commit_native_report(uint8_t instance, uint32_t token) {
|
||||
if (instance >= PROBE_CONTROLLER_COUNT || !token) return false;
|
||||
Child& child = g_children[instance];
|
||||
// Check the live source even when the caller did not poll after a disconnect.
|
||||
Bluepad32DualSenseBridgeSnapshot source;
|
||||
bluepad32_input_backend_dualsense_snapshot(&source);
|
||||
const uint32_t now_us = time_us_32();
|
||||
if (!child.enabled || !g_active || child.pending_token != token ||
|
||||
!source.controller.active || source.slot != g_source.slot ||
|
||||
source.controller.connection_generation != g_source.controller.connection_generation ||
|
||||
source.state_generation != g_source.state_generation ||
|
||||
source.motion_sequence != g_source.motion_sequence ||
|
||||
source.motion_received_us != g_source.motion_received_us ||
|
||||
source.motion_valid != g_source.motion_valid ||
|
||||
now_us - source.received_us >= kInputDeadlineUs || now_us - child.pending_us >= kOutputDeadlineUs ||
|
||||
(child.pending_motion && (!source.motion_valid ||
|
||||
now_us - g_source.motion_received_us >= kSensorDeadlineUs))) return false;
|
||||
child.pending_token = 0;
|
||||
if (child.pending_motion) {
|
||||
child.have_committed_motion = true;
|
||||
child.committed_motion_sequence = child.pending_motion_sequence;
|
||||
child.committed_ticks = child.pending_ticks;
|
||||
}
|
||||
++child.counter;
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
13
tools/switch2_usb_probe/dualsense_input.h
Normal file
13
tools/switch2_usb_probe/dualsense_input.h
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
#pragma once
|
||||
|
||||
#include "controller_input.h"
|
||||
|
||||
#if SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
// Core 0 only. One coherent profile/motion evaluation feeds both native children.
|
||||
void probe_dualsense_input_init();
|
||||
void probe_dualsense_input_set_stick_calibration(uint8_t instance, const uint8_t calibration[9]);
|
||||
void probe_dualsense_input_set_native_stream(uint8_t instance, bool enabled);
|
||||
void probe_dualsense_input_poll(uint8_t instance, uint32_t now_ms, probe_controller_input* out);
|
||||
uint32_t probe_dualsense_input_peek_native_report(uint8_t instance, uint32_t now_ms, uint8_t report[63]);
|
||||
bool probe_dualsense_input_commit_native_report(uint8_t instance, uint32_t token);
|
||||
#endif
|
||||
|
|
@ -319,6 +319,8 @@ static void reset_controller_protocol(uint8_t instance) {
|
|||
#if SWITCH2_BRIDGE_WII_INPUT
|
||||
probe_controller_input_set_stick_calibration(stick_calibration);
|
||||
probe_controller_input_set_native_features(0);
|
||||
#elif SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
probe_controller_input_set_full_stick_calibration(instance, stick_calibration);
|
||||
#endif
|
||||
protocol->read_memory = read_memory;
|
||||
#endif
|
||||
|
|
@ -485,6 +487,9 @@ static void protocol_task(probe_usb_controller* controller, uint32_t now) {
|
|||
#if SWITCH2_BRIDGE_WII_INPUT
|
||||
probe_debug_printf("[PROBE] Wii cue dispatched itf=%u token=%" PRIu64 "\n",
|
||||
instance, reply->deferred_token);
|
||||
#elif SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
probe_debug_printf("[PROBE] DualSense cue dispatched itf=%u token=%" PRIu64 "\n",
|
||||
instance, reply->deferred_token);
|
||||
#else
|
||||
probe_debug_printf("[PROBE] Source sample ACK itf=%u token=%" PRIu64 "\n",
|
||||
instance, reply->deferred_token);
|
||||
|
|
@ -523,7 +528,7 @@ static void protocol_task(probe_usb_controller* controller, uint32_t now) {
|
|||
native_serial = probe_controller_input_peek_native_report(instance, now, input);
|
||||
if (!native_serial) return;
|
||||
length = sizeof(input);
|
||||
#if SWITCH2_BRIDGE_WII_INPUT
|
||||
#if SWITCH2_BRIDGE_WII_INPUT || SWITCH2_BRIDGE_DUALSENSE_INPUT
|
||||
input[8] = (uint8_t)(0x30 | ((protocol->enabled_features & 0x20) ? 8 : 0));
|
||||
#endif
|
||||
probe_protocol_gate_native_report(protocol, input);
|
||||
|
|
|
|||
|
|
@ -319,11 +319,12 @@ void ProbeNativeMotion::observe_optical_heading(uint32_t now_us, uint32_t refere
|
|||
}
|
||||
|
||||
void ProbeNativeMotion::update(uint32_t now_us, uint32_t connection_generation,
|
||||
const ProbeNativeMotionSample& sample) {
|
||||
if (!have_generation_ || connection_generation != connection_generation_) {
|
||||
const ProbeNativeMotionSample& sample, ProbeNativeMotionBias bias_mode) {
|
||||
if (!have_generation_ || connection_generation != connection_generation_ || bias_mode != bias_mode_) {
|
||||
reset();
|
||||
have_generation_ = true;
|
||||
connection_generation_ = connection_generation;
|
||||
bias_mode_ = bias_mode;
|
||||
}
|
||||
const uint32_t elapsed_us = have_update_ ? now_us - update_us_ : 0;
|
||||
update_us_ = now_us;
|
||||
|
|
@ -385,6 +386,14 @@ void ProbeNativeMotion::update(uint32_t now_us, uint32_t connection_generation,
|
|||
return;
|
||||
}
|
||||
|
||||
if (bias_mode_ == ProbeNativeMotionBias::kAlreadyCalibrated) {
|
||||
// Trust only the caller's validated calibrated samples, not an estimated
|
||||
// stationary bias. The first acceleration fixes a relative gravity frame.
|
||||
for (unsigned i = 0; i < 3; ++i) mean_accel_[i] = acceleration_[i];
|
||||
ready_ = initialize_orientation();
|
||||
return;
|
||||
}
|
||||
|
||||
const float acceleration_norm_squared = squared_norm(acceleration_);
|
||||
if (acceleration_norm_squared < 0.85f * 0.85f ||
|
||||
acceleration_norm_squared > 1.15f * 1.15f) {
|
||||
|
|
|
|||
|
|
@ -29,18 +29,25 @@ struct ProbeNativeMotionSample {
|
|||
float gyro_dps[3]{};
|
||||
};
|
||||
|
||||
enum class ProbeNativeMotionBias : uint8_t {
|
||||
kAlreadyCalibrated,
|
||||
kEstimateStationary,
|
||||
};
|
||||
|
||||
// Core 0 only. Call update even when sensors are unavailable, and consult ready
|
||||
// before using the orientation. Sequence identities, not polling, admit samples;
|
||||
// repeated sequences cannot refresh timestamps or contribute to calibration.
|
||||
// Startup requires the user to rest the controller: constant rotation about
|
||||
// gravity is indistinguishable from an unknown gyro bias without another sensor.
|
||||
// Fresh near-1g acceleration corrects tilt drift after startup. Heading remains
|
||||
// gyro-derived unless a reliable optical heading observation is supplied.
|
||||
// Already-calibrated sources initialize from the first usable fresh sensor pair.
|
||||
// Wii explicitly requests stationary residual-bias estimation: constant rotation
|
||||
// about gravity is indistinguishable from an unknown bias without another sensor.
|
||||
// Fresh near-1g acceleration corrects tilt drift. Heading remains gyro-derived
|
||||
// unless a reliable optical heading observation is supplied.
|
||||
class ProbeNativeMotion {
|
||||
public:
|
||||
void reset();
|
||||
void update(uint32_t now_us, uint32_t connection_generation,
|
||||
const ProbeNativeMotionSample& sample);
|
||||
const ProbeNativeMotionSample& sample,
|
||||
ProbeNativeMotionBias bias_mode = ProbeNativeMotionBias::kAlreadyCalibrated);
|
||||
// Call after update, using a full observed sensor-bar pair (never inferred).
|
||||
// Positive optical yaw means aim-right, the negative reference-world turn.
|
||||
// The first sample in each optical generation anchors the current heading;
|
||||
|
|
@ -62,6 +69,7 @@ private:
|
|||
bool normalize_orientation();
|
||||
|
||||
bool have_generation_ = false;
|
||||
ProbeNativeMotionBias bias_mode_ = ProbeNativeMotionBias::kAlreadyCalibrated;
|
||||
bool have_update_ = false;
|
||||
bool seen_accel_sequence_ = false;
|
||||
bool seen_gyro_sequence_ = false;
|
||||
|
|
|
|||
|
|
@ -25,8 +25,8 @@ else()
|
|||
endif()
|
||||
if(SWITCH2_PROBE_COMPOSITE OR SWITCH2_PROBE_HUB)
|
||||
if(NOT SWITCH_PICO_SWITCH2_USB_BRIDGE OR SWITCH2_BRIDGE_WII_INPUT
|
||||
OR NOT SWITCH2_BRIDGE_INPUT STREQUAL "JOYCON2")
|
||||
message(FATAL_ERROR "Composite Joy-Con 2 requires SWITCH_PICO_SWITCH2_USB_BRIDGE=ON and SWITCH2_BRIDGE_INPUT=JOYCON2")
|
||||
OR (NOT SWITCH2_BRIDGE_INPUT STREQUAL "JOYCON2" AND NOT SWITCH2_BRIDGE_DUALSENSE_INPUT))
|
||||
message(FATAL_ERROR "Native R/L output requires JOYCON2 input, or DUALSENSE with HUB")
|
||||
endif()
|
||||
set(probe_composite 0)
|
||||
if(SWITCH2_PROBE_COMPOSITE)
|
||||
|
|
@ -51,9 +51,13 @@ set(SWITCH2_BRIDGE_SOURCE_ADDRESS "" CACHE STRING
|
|||
"Primary physical Bluetooth source address (xx:xx:xx:xx:xx:xx)")
|
||||
set(SWITCH2_BRIDGE_SECOND_SOURCE_ADDRESS "" CACHE STRING
|
||||
"Secondary left physical Bluetooth source address (xx:xx:xx:xx:xx:xx)")
|
||||
set(SWITCH2_BRIDGE_SOURCE_AUTO OFF)
|
||||
if(SWITCH_PICO_SWITCH2_USB_BRIDGE OR SWITCH2_PROBE_COMPOSITE)
|
||||
set(probe_source_fields SWITCH2_BRIDGE_SOURCE_ADDRESS)
|
||||
if(SWITCH2_PROBE_COMPOSITE OR SWITCH2_PROBE_HUB)
|
||||
if(SWITCH2_BRIDGE_DUALSENSE_INPUT AND SWITCH2_BRIDGE_SOURCE_ADDRESS STREQUAL "")
|
||||
set(SWITCH2_BRIDGE_SOURCE_AUTO ON)
|
||||
set(probe_source_fields "")
|
||||
elseif((SWITCH2_PROBE_COMPOSITE OR SWITCH2_PROBE_HUB) AND NOT SWITCH2_BRIDGE_DUALSENSE_INPUT)
|
||||
list(APPEND probe_source_fields SWITCH2_BRIDGE_SECOND_SOURCE_ADDRESS)
|
||||
endif()
|
||||
set(probe_source_addresses "")
|
||||
|
|
@ -74,6 +78,7 @@ if(SWITCH_PICO_SWITCH2_USB_BRIDGE OR SWITCH2_PROBE_COMPOSITE)
|
|||
string(PREPEND ${field}_BYTES "0x")
|
||||
endforeach()
|
||||
endif()
|
||||
add_compile_definitions(SWITCH2_BRIDGE_SOURCE_AUTO=$<BOOL:${SWITCH2_BRIDGE_SOURCE_AUTO}>)
|
||||
|
||||
function(switch2_usb_probe_configure target)
|
||||
set(probe_sources
|
||||
|
|
@ -362,8 +367,14 @@ function(switch2_usb_probe_configure target)
|
|||
else()
|
||||
pico_set_program_name(${target} "Switch 2 USB initialization capture")
|
||||
endif()
|
||||
if(SWITCH2_PROBE_HUB)
|
||||
pico_set_program_version(${target} "0.66-native-hub-input")
|
||||
if(SWITCH2_PROBE_HUB AND SWITCH2_BRIDGE_DUALSENSE_INPUT)
|
||||
if(SWITCH2_PROBE_TRACE_NATIVE_INPUT)
|
||||
pico_set_program_version(${target} "0.69-native-hub-imu-trace")
|
||||
else()
|
||||
pico_set_program_version(${target} "0.69-native-hub-imu")
|
||||
endif()
|
||||
elseif(SWITCH2_PROBE_HUB)
|
||||
pico_set_program_version(${target} "0.67-native-hub-latency")
|
||||
elseif(SWITCH2_PROBE_JOIN_CHORD_GATE)
|
||||
if(SWITCH2_PROBE_TRACE_NATIVE_INPUT)
|
||||
pico_set_program_version(${target} "0.37-pair-chord-trace")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue