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