feat(native-usb): add 32-frame HD gameplay rumble and USB flight diagnostics

Preserve native frequency/amplitude timelines for DualSense PCM output,
remove the unsupported 64-frame path, and retain compatibility rumble for
other controllers. Use the validated 300 MHz sampling phase and restrict
non-bondable Classic discovery autoconnect to previously paired peers.

Buffer native-hub UART stdout and release USB IRQs around port-reset
callbacks. Add observer liveness and pre-SETUP root-response observations
without changing recovery behavior. Cover transport and haptics boundaries.

Record the user-accepted 0.108 trial: controls remained responsive and
rumble felt fine. Instrumentation changes timing; the disconnect root
cause and long-term reliability remain unqualified.

Validation: 624 tests, 11 affected firmware/probe builds, and on-device
concurrent USB and HD-auto-start checks. Private captures, generated
images, and unrelated working-tree files are intentionally excluded.
This commit is contained in:
Joey Yakimowich-Payne 2026-09-19 17:03:46 -06:00
commit 80b788099b
46 changed files with 2873 additions and 412 deletions

View file

@ -491,9 +491,9 @@ extern "C" void probe_controller_input_set_full_stick_calibration(
}
extern "C" bool probe_controller_input_submit_rumble(
uint8_t instance, const uint8_t* magnitudes, uint8_t count) {
uint8_t instance, const NativeHapticsActuatorFrame* frame) {
return g_flash_ready && instance < PROBE_CONTROLLER_COUNT &&
bluepad32_input_backend_native_rumble_submit(instance, magnitudes, count);
bluepad32_input_backend_native_rumble_submit(instance, frame);
}
extern "C" void probe_controller_input_cancel_rumble(uint8_t instance) {

View file

@ -2,6 +2,7 @@
#include <stdbool.h>
#include <stdint.h>
#include "core/native_haptics.h"
#ifdef __cplusplus
extern "C" {
@ -51,10 +52,10 @@ void probe_controller_input_set_native_features(uint8_t features);
// 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]);
// Native single-actuator gameplay envelope for this virtual half. Backend owns
// binding/generation checks, profile gain, finite playback and stale-stream stop.
bool probe_controller_input_submit_rumble(uint8_t instance, const uint8_t* magnitudes,
uint8_t count);
// Lossless native waveform for this virtual half. Backend owns source binding,
// profile gain, selected HD output and bounded conventional fallback.
bool probe_controller_input_submit_rumble(uint8_t instance,
const NativeHapticsActuatorFrame* frame);
void probe_controller_input_cancel_rumble(uint8_t instance);
#endif
// Core0 native07/08 output. Disable discards queued/prepared data; repeated

View file

@ -27,6 +27,10 @@
#include "pico/stdlib.h"
#include "hardware/sync.h"
#include "hardware/uart.h"
#if SWITCH2_PROBE_HUB
#include "pico/stdio/driver.h"
#include "pico/stdio_uart.h"
#endif
#include "tusb.h"
#include "descriptors.h"
#include "protocol.h"
@ -127,6 +131,40 @@ static void gate_join_shoulders(uint8_t instance, uint8_t report_id,
}
#endif
static void count_dropped_log_bytes(uint32_t count) {
if (!count) return;
#if SWITCH2_PROBE_HUB
__atomic_add_fetch(&log_dropped, count, __ATOMIC_RELAXED);
#else
log_dropped += count;
#endif
}
static bool queue_log_bytes(const char* message, size_t size, uint32_t requested) {
#if !SWITCH2_PROBE_HUB
const uint32_t interrupts = save_and_disable_interrupts();
#elif defined(SWITCH2_PROBE_TRACE_NATIVE_INPUT)
const uint32_t trace_parent = native_hub_trace_phase(NATIVE_HUB_TRACE_PHASE_LOG_COPY);
#endif
const bool queued = LOG_CAPACITY - (log_written - log_read) >= size;
if (queued) {
const size_t offset = log_written % LOG_CAPACITY;
const size_t first = size < LOG_CAPACITY - offset ? size : LOG_CAPACITY - offset;
memcpy(log_bytes + offset, message, first);
memcpy(log_bytes, message + first, size - first);
log_written += (uint32_t)size;
count_dropped_log_bytes(requested - (uint32_t)size);
} else {
count_dropped_log_bytes(requested);
}
#if !SWITCH2_PROBE_HUB
restore_interrupts(interrupts);
#elif defined(SWITCH2_PROBE_TRACE_NATIVE_INPUT)
native_hub_trace_phase(trace_parent);
#endif
return queued;
}
int probe_debug_printf(const char* format, ...) {
#if SWITCH2_PROBE_HUB
// Native-hub producers and the UART consumer all run on Core0 foreground.
@ -142,30 +180,30 @@ int probe_debug_printf(const char* format, ...) {
va_end(args);
if (result <= 0) return result;
const size_t size = (size_t)result < sizeof(message) ? (size_t)result : sizeof(message) - 1;
#if !SWITCH2_PROBE_HUB
const uint32_t interrupts = save_and_disable_interrupts();
#elif defined(SWITCH2_PROBE_TRACE_NATIVE_INPUT)
const uint32_t trace_parent = native_hub_trace_phase(NATIVE_HUB_TRACE_PHASE_LOG_COPY);
#endif
const bool queued = LOG_CAPACITY - (log_written - log_read) >= size;
if (queued) {
const size_t offset = log_written % LOG_CAPACITY;
const size_t first = size < LOG_CAPACITY - offset ? size : LOG_CAPACITY - offset;
memcpy(log_bytes + offset, message, first);
memcpy(log_bytes, message + first, size - first);
log_written += (uint32_t)size;
log_dropped += (uint32_t)result - (uint32_t)size;
} else {
log_dropped += (uint32_t)result;
}
#if !SWITCH2_PROBE_HUB
restore_interrupts(interrupts);
#elif defined(SWITCH2_PROBE_TRACE_NATIVE_INPUT)
native_hub_trace_phase(trace_parent);
#endif
return queued ? result : -1;
return queue_log_bytes(message, size, (uint32_t)result) ? result : -1;
}
#if SWITCH2_PROBE_HUB
static void buffered_stdio_out(const char* bytes, int length) {
if (length <= 0) return;
// Stdio can also be called by panic/IRQ paths. Never recursively assert,
// race the foreground ring, or wait for UART from those contexts.
if (get_core_num() != 0 || __get_current_exception() != 0) {
count_dropped_log_bytes((uint32_t)length);
return;
}
queue_log_bytes(bytes, (size_t)length, (uint32_t)length);
}
static void buffer_uart_stdio(void) {
// Install before Bluetooth starts. Bluepad32's vfprintf/printf output
// must share the ordered queue rather than blocking RADIO_POLL on UART.
// Keep the SDK's UART initialization, stdin and availability callbacks.
stdio_uart.out_chars = buffered_stdio_out;
stdio_uart.out_flush = NULL; // Foreground drain_log owns physical output.
}
#endif
static void drain_log(void) {
while (uart_is_writable(uart0)) {
#if !SWITCH2_PROBE_HUB
@ -305,12 +343,11 @@ void tud_hid_set_report_cb(uint8_t instance, uint8_t report_id,
if (instance < PROBE_CONTROLLER_COUNT && report_type == HID_REPORT_TYPE_OUTPUT &&
controllers[instance].protocol.initialized &&
probe_transport_mounted(instance) && !probe_transport_suspended(instance)) {
probe_rumble_frame frame;
NativeHapticsActuatorFrame frame;
if (probe_protocol_decode_rumble(report_id, buffer, length, &frame)) {
// Count-zero HOLD leaves both motor state and watchdog untouched.
// Valid gameplay traffic must not fill the slow UART log ring.
if (frame.count && probe_controller_input_submit_rumble(
instance, frame.magnitude, frame.count))
if (frame.sample_count && probe_controller_input_submit_rumble(instance, &frame))
controllers[instance].gameplay_rumble_seen = true;
return;
}
@ -836,6 +873,9 @@ int main(void) {
system_clock_initialize();
#endif
stdio_init_all();
#if SWITCH2_PROBE_HUB
buffer_uart_stdio();
#endif
#ifdef SWITCH_PICO_SWITCH2_USB_BRIDGE
probe_debug_printf("\n[PROBE] " PROBE_JOYCON_PRODUCT " Bluetooth-to-USB controller/native mouse bridge\n");
#elif SWITCH2_PROBE_NEUTRAL_INPUT
@ -994,7 +1034,7 @@ int main(void) {
" log_dropped_bytes=%" PRIu32 "\n",
now, probe_transport_mounted(0), bulk_packets, hid_packets,
identity_requests, version_requests, setup_completions,
input_reports, command_drops, log_dropped);
input_reports, command_drops, __atomic_load_n(&log_dropped, __ATOMIC_RELAXED));
#ifdef SWITCH2_PROBE_USB_INIT
for (uint8_t instance = 0; instance < PROBE_CONTROLLER_COUNT; ++instance) {
probe_usb_controller* controller = &controllers[instance];

View file

@ -417,27 +417,27 @@ function(switch2_usb_probe_configure target)
endif()
if(SWITCH2_PROBE_NEUTRAL_INPUT)
if(SWITCH2_PROBE_TRACE_NATIVE_INPUT)
pico_set_program_version(${target} "0.102-neutral-hub-${SWITCH2_PROBE_PAIR_COUNT}pair-trace")
pico_set_program_version(${target} "0.108-neutral-hub-${SWITCH2_PROBE_PAIR_COUNT}pair-trace")
else()
pico_set_program_version(${target} "0.102-neutral-hub-${SWITCH2_PROBE_PAIR_COUNT}pair")
pico_set_program_version(${target} "0.108-neutral-hub-${SWITCH2_PROBE_PAIR_COUNT}pair")
endif()
elseif(SWITCH2_PROBE_HUB AND SWITCH2_BRIDGE_FULL_INPUT)
if(probe_controller_count GREATER 2)
if(SWITCH2_PROBE_TRACE_NATIVE_INPUT)
pico_set_program_version(${target} "0.102-live-two-pair-trace")
pico_set_program_version(${target} "0.108-live-two-pair-trace")
else()
pico_set_program_version(${target} "0.102-live-two-pair")
pico_set_program_version(${target} "0.108-live-two-pair")
endif()
elseif(SWITCH2_PROBE_TRACE_NATIVE_INPUT)
pico_set_program_version(${target} "0.102-native-digital-stick-trace")
pico_set_program_version(${target} "0.108-native-digital-stick-trace")
else()
pico_set_program_version(${target} "0.102-native-digital-stick")
pico_set_program_version(${target} "0.108-native-digital-stick")
endif()
elseif(SWITCH2_PROBE_HUB)
if(SWITCH2_PROBE_TRACE_NATIVE_INPUT)
pico_set_program_version(${target} "0.102-native-hub-profiles-trace")
pico_set_program_version(${target} "0.108-native-hub-profiles-trace")
else()
pico_set_program_version(${target} "0.102-native-hub-profiles")
pico_set_program_version(${target} "0.108-native-hub-profiles")
endif()
elseif(SWITCH2_PROBE_JOIN_CHORD_GATE)
if(SWITCH2_PROBE_TRACE_NATIVE_INPUT)

View file

@ -409,7 +409,7 @@ void probe_protocol_gate_native_report(const probe_protocol_state* state,
}
bool probe_protocol_decode_rumble(uint8_t report_id, const uint8_t* data,
size_t length, probe_rumble_frame* output) {
size_t length, NativeHapticsActuatorFrame* output) {
if (!data || !output) return false;
if (report_id == 0) {
if (length < 17 || length > 64 || data[0] != 0x01) return false;
@ -424,16 +424,14 @@ bool probe_protocol_decode_rumble(uint8_t report_id, const uint8_t* data,
// packs frequency/amplitude/frequency/amplitude as four 10-bit LE fields.
// Header: format 01 [7:6], sample count [5:4], sequence [3:0].
// Sequence is informational; unused sample bytes and USB padding may be stale.
probe_rumble_frame decoded = {.count = (data[0] >> 4) & 3u};
for (unsigned i = 0; i < decoded.count; ++i) {
NativeHapticsActuatorFrame decoded = {.sample_count = (data[0] >> 4) & 3u};
for (unsigned i = 0; i < decoded.sample_count; ++i) {
const uint8_t* sample = data + 1u + 5u * i;
const unsigned first = (sample[1] >> 2) | ((sample[2] & 0x0fu) << 6);
const unsigned second = (sample[3] >> 6) | ((unsigned)sample[4] << 2);
const unsigned amplitude = first > second ? first : second;
// ERM compatibility, not HD waveform reproduction: ignore frequencies
// and round max(amplitudes) across the full 10-bit range to 0..255.
// SDL's conservative outbound clamp is not an inbound validity limit.
decoded.magnitude[i] = (uint8_t)((amplitude * 255u + 511u) / 1023u);
NativeHapticsSample* out = &decoded.samples[i];
out->low_frequency_code = sample[0] | ((uint16_t)(sample[1] & 3u) << 8);
out->low_amplitude = (sample[1] >> 2) | ((uint16_t)(sample[2] & 15u) << 6);
out->high_frequency_code = (sample[2] >> 4) | ((uint16_t)(sample[3] & 63u) << 4);
out->high_amplitude = (sample[3] >> 6) | ((uint16_t)sample[4] << 2);
}
*output = decoded;
return true;

View file

@ -3,6 +3,7 @@
#include <stddef.h>
#include <stdint.h>
#include "model.h"
#include "core/native_haptics.h"
#define PROBE_COMMAND_MAX_SIZE 263u
#define PROBE_REPLY_MAX_SIZE 96u
@ -50,17 +51,14 @@ typedef struct {
uint32_t report_counter;
} probe_protocol_state;
typedef struct {
uint8_t count;
uint8_t magnitude[3];
} probe_rumble_frame;
// Stateless Output 01 compatibility rumble: report_id 0 includes the leading
// Stateless Output 01 waveform decoding: report_id 0 includes the leading
// wire ID (17..64 bytes); report_id 1 omits it (16..63 bytes).
// Count zero means HOLD/no update, not stop or watchdog refresh. A nonempty
// zero-amplitude sample is stop. Malformed input leaves output unchanged.
// Both 10-bit frequency and amplitude fields survive decoding; each physical
// output backend chooses its own supported frequency range and rendering.
bool probe_protocol_decode_rumble(uint8_t report_id, const uint8_t* data,
size_t length, probe_rumble_frame* output);
size_t length, NativeHapticsActuatorFrame* output);
void probe_protocol_reset(probe_protocol_state* state, bool is_left);
// Blob: own address[6], count[1], zero-padded host addresses[42][6], AES key[16].