Support Bluepad32 gamepads as native Joy-Con pair sources

This commit is contained in:
Joey Yakimowich-Payne 2026-09-12 23:03:15 -06:00
commit a17ca603aa
33 changed files with 2529 additions and 851 deletions

View file

@ -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,

View file

@ -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
}

View file

@ -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);

View file

@ -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

View file

@ -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

View file

@ -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;

View 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

View file

@ -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")