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.
1064 lines
47 KiB
C
1064 lines
47 KiB
C
// Joy-Con 2 USB instrument and optional Bluetooth controller/mouse bridge.
|
|
// Only documented/observed transactions are implemented. Built-in vibration
|
|
// cues wait for the source ACK; native sensor packets are relayed without decoding.
|
|
// Only virtual pairing storage is writable here.
|
|
#include <inttypes.h>
|
|
#include <stdarg.h>
|
|
#include <stdbool.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include "model.h"
|
|
|
|
#if defined(SWITCH_PICO_SWITCH2_USB_BRIDGE) || SWITCH2_PROBE_NEUTRAL_INPUT
|
|
#include "bootsel.h"
|
|
#endif
|
|
#ifdef SWITCH_PICO_SWITCH2_USB_BRIDGE
|
|
#include "controller_input.h"
|
|
#elif !SWITCH2_PROBE_NEUTRAL_INPUT
|
|
#include "platform/pico/bootsel_button_sample.h"
|
|
#include "button_test.h"
|
|
#endif
|
|
#if SWITCH2_PROBE_NEUTRAL_INPUT
|
|
#include "platform/pico/system_clock.h"
|
|
#if !defined(SWITCH2_PROBE_USB_INIT) || !defined(SWITCH2_PROBE_MEMORY)
|
|
#error "Neutral hub requires native USB protocol initialization and captured stick calibration"
|
|
#endif
|
|
#endif
|
|
#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"
|
|
#include "storage.h"
|
|
#include "transport.h"
|
|
#ifdef SWITCH2_PROBE_MEMORY
|
|
#include "memory.h"
|
|
#endif
|
|
#ifdef SWITCH2_PROBE_IDENTITY_REPLY
|
|
#include "probe_identity.h"
|
|
_Static_assert(sizeof(probe_identity_replies[0]) == 64, "factory identity response size");
|
|
_Static_assert(sizeof(probe_identity_replies) / sizeof(probe_identity_replies[0]) ==
|
|
PROBE_CONTROLLER_COUNT, "one captured identity per controller");
|
|
#endif
|
|
#ifdef SWITCH2_PROBE_VERSION_REPLY
|
|
#include "probe_version.h"
|
|
_Static_assert(sizeof(probe_version_replies[0]) == 16, "version/address response size");
|
|
_Static_assert(sizeof(probe_version_replies) / sizeof(probe_version_replies[0]) ==
|
|
PROBE_CONTROLLER_COUNT, "one captured version/address per controller");
|
|
#endif
|
|
|
|
#define LOG_CAPACITY 8192u
|
|
static char log_bytes[LOG_CAPACITY];
|
|
static uint32_t log_written, log_read, log_dropped;
|
|
static uint32_t bulk_packets, hid_packets;
|
|
static uint32_t identity_requests, version_requests, setup_completions;
|
|
static uint16_t string_descriptors[PROBE_CONTROLLER_COUNT][64];
|
|
static uint32_t input_reports, command_drops;
|
|
#ifdef SWITCH2_PROBE_USB_INIT
|
|
#define REPLY_CAPACITY 4u
|
|
typedef struct {
|
|
uint8_t data[PROBE_REPLY_MAX_SIZE];
|
|
uint8_t length;
|
|
uint64_t deferred_token;
|
|
} queued_reply;
|
|
typedef struct {
|
|
uint8_t instance;
|
|
probe_protocol_state protocol;
|
|
queued_reply replies[REPLY_CAPACITY];
|
|
uint8_t reply_head, reply_count;
|
|
bool reply_inflight;
|
|
uint16_t reply_remaining;
|
|
uint8_t command_frame[PROBE_COMMAND_MAX_SIZE];
|
|
uint16_t command_used, command_expected;
|
|
uint32_t last_input_ms, input_reports;
|
|
uint32_t command_drops;
|
|
#ifdef SWITCH_PICO_SWITCH2_USB_BRIDGE
|
|
uint32_t last_controller_poll_ms;
|
|
uint16_t last_delivered_buttons;
|
|
bool native_stream_ready;
|
|
uint32_t last_hid_complete_ms;
|
|
bool hid_completion_seen;
|
|
#if SWITCH2_BRIDGE_FULL_INPUT
|
|
bool gameplay_rumble_seen;
|
|
#endif
|
|
uint32_t mouse_delivered_reports, mouse_logged_reports;
|
|
int64_t mouse_delivered_x, mouse_delivered_y;
|
|
#ifdef SWITCH2_PROBE_TRACE_NATIVE_INPUT
|
|
uint32_t last_native_trace_ms;
|
|
#endif
|
|
#elif !SWITCH2_PROBE_NEUTRAL_INPUT
|
|
probe_button_state button_test;
|
|
uint32_t last_button_ms;
|
|
bool button_sample_error;
|
|
#endif
|
|
uint8_t last_delivered_rails;
|
|
} probe_usb_controller;
|
|
static probe_usb_controller controllers[PROBE_CONTROLLER_COUNT];
|
|
#endif
|
|
|
|
_Static_assert(sizeof(probe_device_descriptor) == 18, "device descriptor size");
|
|
_Static_assert(sizeof(probe_configuration_descriptor) ==
|
|
(SWITCH2_PROBE_COMPOSITE ? 151 : 80), "configuration descriptor size");
|
|
_Static_assert(sizeof(probe_hid_report_descriptors[0]) == 100, "HID descriptor size");
|
|
|
|
#if defined(SWITCH2_PROBE_JOIN_CHORD_GATE) && !SWITCH2_PROBE_HUB
|
|
_Static_assert(PROBE_CONTROLLER_COUNT == 2, "L+R gate requires both native functions");
|
|
static uint8_t last_join_shoulder_mask;
|
|
|
|
static uint8_t join_shoulder_mask(void) {
|
|
uint8_t mask = 0;
|
|
for (uint8_t instance = 0; instance < PROBE_CONTROLLER_COUNT; ++instance) {
|
|
const probe_protocol_state* state = &controllers[instance].protocol;
|
|
if (state->controller_active && (state->controller_buttons[0] & 0x10))
|
|
mask |= (uint8_t)(1u << instance);
|
|
}
|
|
return mask;
|
|
}
|
|
|
|
static void gate_join_shoulders(uint8_t instance, uint8_t report_id,
|
|
uint8_t input[PROBE_INPUT_SIZE]) {
|
|
if (join_shoulder_mask() == 3) return;
|
|
if (report_id == probe_model_report_id(instance))
|
|
input[2] &= (uint8_t)~0x10u;
|
|
else if (report_id == 0x05)
|
|
input[probe_model_is_left(instance) ? 6 : 4] &= (uint8_t)~0x40u;
|
|
// Only suppress a real shoulder bit; never insert a counterpart press.
|
|
}
|
|
#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.
|
|
// Neither USB IRQ nor the Core1 observer accesses this ring. Masking IRQs
|
|
// across a message copy prevents completion service and can lose the next
|
|
// address's token; do not turn a diagnostic into USB backpressure.
|
|
hard_assert(get_core_num() == 0 && __get_current_exception() == 0);
|
|
#endif
|
|
char message[512];
|
|
va_list args;
|
|
va_start(args, format);
|
|
const int result = vsnprintf(message, sizeof(message), format, args);
|
|
va_end(args);
|
|
if (result <= 0) return result;
|
|
const size_t size = (size_t)result < sizeof(message) ? (size_t)result : sizeof(message) - 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
|
|
const uint32_t interrupts = save_and_disable_interrupts();
|
|
#endif
|
|
if (log_read == log_written) {
|
|
#if !SWITCH2_PROBE_HUB
|
|
restore_interrupts(interrupts);
|
|
#endif
|
|
break;
|
|
}
|
|
const char value = log_bytes[log_read++ % LOG_CAPACITY];
|
|
#if !SWITCH2_PROBE_HUB
|
|
restore_interrupts(interrupts);
|
|
#endif
|
|
uart_putc_raw(uart0, value);
|
|
}
|
|
}
|
|
|
|
static void log_packet(const char* kind, uint8_t instance, uint8_t report_id,
|
|
const uint8_t* data, uint16_t length) {
|
|
if (length >= 4 && data[0] == 0x15 && (data[3] == 0x02 || data[3] == 0x04)) {
|
|
probe_debug_printf("[PROBE] %s itf=%u command=15/%02x len=%u [pairing payload redacted]\n",
|
|
kind, instance, data[3], length);
|
|
return;
|
|
}
|
|
static const char hex[] = "0123456789abcdef";
|
|
char message[288];
|
|
size_t at = (size_t)snprintf(message, sizeof(message),
|
|
"[PROBE %" PRIu32 "] %s itf=%u report=%02x len=%u:",
|
|
to_ms_since_boot(get_absolute_time()), kind, instance, report_id, length);
|
|
const uint16_t count = length < 64 ? length : 64;
|
|
for (uint16_t i = 0; i < count && at + 4 < sizeof(message); ++i) {
|
|
message[at++] = ' ';
|
|
message[at++] = hex[data[i] >> 4];
|
|
message[at++] = hex[data[i] & 15];
|
|
}
|
|
message[at] = 0;
|
|
probe_debug_printf("%s%s\n", message, length > count ? " [truncated]" : "");
|
|
}
|
|
|
|
uint8_t const* tud_descriptor_device_cb(void) {
|
|
probe_debug_printf("[PROBE] DEVICE_DESCRIPTOR 057e:%04x\n", (unsigned)PROBE_JOYCON_PID);
|
|
return probe_device_descriptor;
|
|
}
|
|
|
|
uint8_t const* tud_descriptor_configuration_cb(uint8_t index) {
|
|
probe_debug_printf("[PROBE] CONFIG_DESCRIPTOR index=%u\n", index);
|
|
return index == 0 ? probe_configuration_descriptor : NULL;
|
|
}
|
|
|
|
uint8_t const* tud_hid_descriptor_report_cb(uint8_t instance) {
|
|
probe_debug_printf("[PROBE] HID_DESCRIPTOR itf=%u\n", instance);
|
|
return instance < PROBE_CONTROLLER_COUNT ? probe_hid_report_descriptors[instance] : NULL;
|
|
}
|
|
|
|
static uint16_t const* controller_string_descriptor(uint8_t instance, uint8_t index,
|
|
uint16_t langid) {
|
|
// Manufacturer/product/serial match the published reference. Remaining
|
|
// descriptor labels describe the probe; their genuine strings are unknown.
|
|
static const char* const strings[] = {
|
|
"", "Nintendo", PROBE_JOYCON_PRODUCT, "00", "USB configuration", "HID", "Commands",
|
|
"Left HID", "Left commands"};
|
|
if (instance >= PROBE_CONTROLLER_COUNT) return NULL;
|
|
uint16_t* descriptor = string_descriptors[instance];
|
|
probe_debug_printf("[PROBE] STRING_DESCRIPTOR itf=%u index=%u lang=%04x\n",
|
|
instance, index, langid);
|
|
if (index == 0) {
|
|
descriptor[0] = (TUSB_DESC_STRING << 8) | 4;
|
|
descriptor[1] = 0x0409;
|
|
return descriptor;
|
|
}
|
|
if (index >= sizeof(strings) / sizeof(strings[0])) return NULL;
|
|
const char* text = index == 2 && probe_model_is_left(instance) ?
|
|
"Joy-Con 2 (L)" : strings[index];
|
|
size_t count = strlen(text);
|
|
if (count > 63) count = 63;
|
|
descriptor[0] = (uint16_t)((TUSB_DESC_STRING << 8) | (2 + count * 2));
|
|
for (size_t i = 0; i < count; ++i)
|
|
descriptor[i + 1] = (uint8_t)text[i];
|
|
return descriptor;
|
|
}
|
|
|
|
uint16_t const* tud_descriptor_string_cb(uint8_t index, uint16_t langid) {
|
|
return controller_string_descriptor(0, index, langid);
|
|
}
|
|
|
|
#if SWITCH2_PROBE_HUB
|
|
const uint8_t* native_joycon_device_descriptor(uint8_t instance) {
|
|
if (instance >= PROBE_CONTROLLER_COUNT) return NULL;
|
|
probe_debug_printf("[PROBE] DEVICE_DESCRIPTOR itf=%u 057e:%04x\n",
|
|
instance, probe_model_pid(instance));
|
|
return probe_model_is_left(instance) ? probe_left_device_descriptor : probe_device_descriptor;
|
|
}
|
|
|
|
const uint8_t* native_joycon_configuration_descriptor(uint8_t instance) {
|
|
// HUB is mutually exclusive with COMPOSITE: each address uses native EP1/2.
|
|
return instance < PROBE_CONTROLLER_COUNT ? probe_configuration_descriptor : NULL;
|
|
}
|
|
|
|
const uint16_t* native_joycon_string_descriptor(uint8_t instance, uint8_t index,
|
|
uint16_t language_id) {
|
|
if (index > 6) return NULL;
|
|
return controller_string_descriptor(instance, index, language_id);
|
|
}
|
|
#endif
|
|
|
|
uint16_t tud_hid_get_report_cb(uint8_t instance, uint8_t report_id,
|
|
hid_report_type_t report_type, uint8_t* buffer,
|
|
uint16_t requested_length) {
|
|
#ifdef SWITCH2_PROBE_USB_INIT
|
|
uint8_t input[PROBE_INPUT_SIZE];
|
|
if (instance < PROBE_CONTROLLER_COUNT && report_type == HID_REPORT_TYPE_INPUT &&
|
|
probe_protocol_report(&controllers[instance].protocol, report_id, input, sizeof(input))) {
|
|
const uint16_t size = requested_length < sizeof(input) ? requested_length : sizeof(input);
|
|
#if defined(SWITCH2_PROBE_JOIN_CHORD_GATE) && !SWITCH2_PROBE_HUB
|
|
gate_join_shoulders(instance, report_id, input);
|
|
#endif
|
|
memcpy(buffer, input, size);
|
|
probe_debug_printf("[PROBE] GET_REPORT itf=%u id=%02x diagnostic length=%u\n",
|
|
instance, report_id, size);
|
|
return size;
|
|
}
|
|
#else
|
|
(void)buffer;
|
|
#endif
|
|
probe_debug_printf("[PROBE] GET_REPORT itf=%u report=%02x type=%u length=%u -> STALL\n",
|
|
instance, report_id, report_type, requested_length);
|
|
return 0;
|
|
}
|
|
|
|
void tud_hid_set_report_cb(uint8_t instance, uint8_t report_id,
|
|
hid_report_type_t report_type, const uint8_t* buffer,
|
|
uint16_t length) {
|
|
++hid_packets;
|
|
#if defined(SWITCH2_PROBE_USB_INIT) && SWITCH2_BRIDGE_FULL_INPUT
|
|
if (instance < PROBE_CONTROLLER_COUNT && report_type == HID_REPORT_TYPE_OUTPUT &&
|
|
controllers[instance].protocol.initialized &&
|
|
probe_transport_mounted(instance) && !probe_transport_suspended(instance)) {
|
|
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.sample_count && probe_controller_input_submit_rumble(instance, &frame))
|
|
controllers[instance].gameplay_rumble_seen = true;
|
|
return;
|
|
}
|
|
}
|
|
#endif
|
|
probe_debug_printf("[PROBE] HID_REPORT_TYPE=%u\n", report_type);
|
|
log_packet("HID_OUT", instance, report_id, buffer, length);
|
|
}
|
|
|
|
#ifdef SWITCH2_PROBE_USB_INIT
|
|
static bool save_pairing(void* context, const uint8_t* data, size_t length) {
|
|
const probe_usb_controller* controller = context;
|
|
const bool saved = probe_storage_save(controller->instance, data, length);
|
|
probe_debug_printf("[PROBE] Virtual pairing persistence itf=%u %s\n",
|
|
controller->instance, saved ? "verified" : "failed");
|
|
return saved;
|
|
}
|
|
|
|
#ifdef SWITCH2_PROBE_MEMORY
|
|
static bool read_memory(void* context, uint32_t address, uint8_t* data, size_t length) {
|
|
const probe_usb_controller* controller = context;
|
|
return probe_memory_read(controller->instance, address, data, length);
|
|
}
|
|
#endif
|
|
|
|
#ifdef SWITCH_PICO_SWITCH2_USB_BRIDGE
|
|
static bool play_sample(void* context, uint8_t sample_id, uint64_t* token) {
|
|
const probe_usb_controller* controller = context;
|
|
return probe_controller_input_play_sample(controller->instance, sample_id, token);
|
|
}
|
|
#endif
|
|
|
|
static void reset_controller_protocol(uint8_t instance) {
|
|
probe_usb_controller* controller = &controllers[instance];
|
|
#ifdef SWITCH_PICO_SWITCH2_USB_BRIDGE
|
|
probe_controller_input_cancel_sample(instance);
|
|
#if SWITCH2_BRIDGE_FULL_INPUT
|
|
probe_controller_input_cancel_rumble(instance);
|
|
#endif
|
|
probe_controller_input_set_native_stream(instance, false);
|
|
#endif
|
|
memset(controller, 0, sizeof(*controller));
|
|
controller->instance = instance;
|
|
controller->command_expected = 8;
|
|
probe_protocol_state* protocol = &controller->protocol;
|
|
probe_protocol_reset(protocol, probe_model_is_left(instance));
|
|
protocol->context = controller;
|
|
memcpy(protocol->controller_address, probe_version_replies[instance] + 10,
|
|
sizeof(protocol->controller_address));
|
|
protocol->firmware_version = probe_firmware_versions[instance];
|
|
protocol->save_pairing = save_pairing;
|
|
#ifdef SWITCH_PICO_SWITCH2_USB_BRIDGE
|
|
protocol->play_sample = play_sample;
|
|
#endif
|
|
#ifdef SWITCH2_PROBE_MEMORY
|
|
uint8_t stick_calibration[9];
|
|
if (!probe_memory_stick_calibration(instance, stick_calibration))
|
|
panic("Invalid captured Joy-Con stick calibration");
|
|
memcpy(protocol->stick_center, stick_calibration, sizeof(protocol->stick_center));
|
|
#if SWITCH2_BRIDGE_WII_INPUT
|
|
probe_controller_input_set_stick_calibration(stick_calibration);
|
|
probe_controller_input_set_native_features(0);
|
|
#elif SWITCH2_BRIDGE_FULL_INPUT
|
|
probe_controller_input_set_full_stick_calibration(instance, stick_calibration);
|
|
#endif
|
|
protocol->read_memory = read_memory;
|
|
#endif
|
|
uint8_t pairing[PROBE_PAIRING_BLOB_SIZE];
|
|
if (probe_storage_load(instance, pairing, sizeof(pairing)) &&
|
|
probe_protocol_restore_pairing(protocol, pairing, sizeof(pairing))) {
|
|
probe_debug_printf("[PROBE] Restored own virtual pairing record itf=%u\n", instance);
|
|
}
|
|
}
|
|
|
|
static void reset_protocol(void) {
|
|
#if defined(SWITCH2_PROBE_JOIN_CHORD_GATE) && !SWITCH2_PROBE_HUB
|
|
last_join_shoulder_mask = 0;
|
|
#endif
|
|
for (uint8_t instance = 0; instance < PROBE_CONTROLLER_COUNT; ++instance)
|
|
reset_controller_protocol(instance);
|
|
}
|
|
|
|
static void complete_command(probe_usb_controller* controller) {
|
|
const uint8_t instance = controller->instance;
|
|
probe_protocol_state* protocol = &controller->protocol;
|
|
const uint8_t* command = controller->command_frame;
|
|
// Log complete frames rather than fragments so key material can be redacted.
|
|
log_packet("BULK_OUT", instance, 0, command, controller->command_used);
|
|
if (controller->reply_count == REPLY_CAPACITY) {
|
|
++command_drops;
|
|
++controller->command_drops;
|
|
probe_debug_printf("[PROBE] Command not executed itf=%u: reply queue full\n", instance);
|
|
return;
|
|
}
|
|
queued_reply* reply = &controller->replies[
|
|
(controller->reply_head + controller->reply_count) % REPLY_CAPACITY];
|
|
#ifdef SWITCH_PICO_SWITCH2_USB_BRIDGE
|
|
const uint8_t previous_report_id = protocol->report_id;
|
|
const uint8_t previous_features = protocol->enabled_features;
|
|
#endif
|
|
const size_t length = probe_protocol_command(
|
|
protocol, command, controller->command_used, reply->data, sizeof(reply->data),
|
|
&reply->deferred_token);
|
|
if (length) {
|
|
#ifdef SWITCH_PICO_SWITCH2_USB_BRIDGE
|
|
if (previous_report_id != protocol->report_id ||
|
|
previous_features != protocol->enabled_features) {
|
|
probe_controller_input_set_native_stream(instance, false);
|
|
controller->native_stream_ready = false;
|
|
#if SWITCH2_BRIDGE_WII_INPUT
|
|
probe_controller_input_set_native_features(protocol->enabled_features);
|
|
#endif
|
|
}
|
|
#endif
|
|
reply->length = (uint8_t)length;
|
|
++controller->reply_count;
|
|
if (reply->deferred_token) {
|
|
probe_debug_printf("[PROBE] Sample %u itf=%u awaiting source token=%" PRIu64 "\n",
|
|
command[8], instance, reply->deferred_token);
|
|
} else {
|
|
log_packet("BULK_REPLY_QUEUED", instance, 0, reply->data, reply->length);
|
|
}
|
|
if (command[0] == 0x09)
|
|
probe_debug_printf("[PROBE] Virtual player LEDs itf=%u mask=%x flashing=%u\n",
|
|
instance, protocol->player_leds, protocol->player_leds_flashing);
|
|
if (command[0] == 0x0c)
|
|
probe_debug_printf("[PROBE] Virtual features itf=%u mask=%02x enabled=%02x\n",
|
|
instance, protocol->feature_mask, protocol->enabled_features);
|
|
if (command[0] == 0x0a && command[3] == 8)
|
|
probe_debug_printf("[PROBE] Virtual vibration parameters itf=%u stored; no motor output\n", instance);
|
|
if (command[0] == 0x03 && command[3] == 0x0c)
|
|
probe_debug_printf("[PROBE] Runtime 03/0C itf=%u value=%u\n", instance, protocol->runtime03_0c);
|
|
} else {
|
|
probe_debug_printf("[PROBE] Command itf=%u %02x/%02x length=%u capture-only or malformed\n",
|
|
instance, command[0], command[3], controller->command_used);
|
|
}
|
|
}
|
|
|
|
static void consume_bulk_packet(probe_usb_controller* controller, const uint8_t* buffer,
|
|
uint16_t length) {
|
|
for (uint16_t i = 0; i < length; ++i) {
|
|
controller->command_frame[controller->command_used++] = buffer[i];
|
|
if (controller->command_used == 8)
|
|
controller->command_expected = (uint16_t)(8 + controller->command_frame[5]);
|
|
if (controller->command_used == controller->command_expected) {
|
|
complete_command(controller);
|
|
controller->command_used = 0;
|
|
controller->command_expected = 8;
|
|
}
|
|
}
|
|
// A short packet/ZLP ends this interface's OUT transfer, not its sibling's.
|
|
if (length < CFG_TUD_VENDOR_EPSIZE && controller->command_used) {
|
|
probe_debug_printf("[PROBE] Truncated command discarded itf=%u received=%u expected=%u\n",
|
|
controller->instance, controller->command_used, controller->command_expected);
|
|
controller->command_used = 0;
|
|
controller->command_expected = 8;
|
|
}
|
|
}
|
|
|
|
#if !defined(SWITCH_PICO_SWITCH2_USB_BRIDGE) && !SWITCH2_PROBE_NEUTRAL_INPUT
|
|
static void button_test_task(probe_usb_controller* controller, uint32_t now) {
|
|
probe_protocol_state* protocol = &controller->protocol;
|
|
const bool ready = probe_transport_mounted(controller->instance) &&
|
|
!probe_transport_suspended(controller->instance) &&
|
|
protocol->initialized && (protocol->enabled_features & 1);
|
|
bool pressed;
|
|
if (!ready) {
|
|
pressed = probe_button_update(&controller->button_test, -1, false);
|
|
controller->last_button_ms = now;
|
|
} else {
|
|
if (now - controller->last_button_ms < 10) return;
|
|
controller->last_button_ms = now;
|
|
const int sample = bootsel_button_sample();
|
|
if (sample < 0 && !controller->button_sample_error)
|
|
probe_debug_printf("[PROBE] BOOTSEL sampling unavailable; test buttons released and disarmed\n");
|
|
controller->button_sample_error = sample < 0;
|
|
pressed = probe_button_update(&controller->button_test, sample, true);
|
|
}
|
|
if (protocol->test_rail_buttons != pressed) {
|
|
protocol->test_rail_buttons = pressed;
|
|
probe_debug_printf("[PROBE] TEST_BUTTON SL+SR %s\n", pressed ? "pressed" : "released");
|
|
}
|
|
}
|
|
#endif
|
|
|
|
#ifdef SWITCH_PICO_SWITCH2_USB_BRIDGE
|
|
static void controller_input_task(probe_usb_controller* controller, uint32_t now) {
|
|
if (now - controller->last_controller_poll_ms < 4) return;
|
|
controller->last_controller_poll_ms = now;
|
|
const uint8_t instance = controller->instance;
|
|
probe_protocol_state* protocol = &controller->protocol;
|
|
probe_controller_input source = {0};
|
|
probe_controller_input_poll(instance, now, &source);
|
|
const bool usb_active = probe_transport_mounted(instance) &&
|
|
!probe_transport_suspended(instance);
|
|
const bool output_active = source.active && usb_active;
|
|
#if SWITCH2_BRIDGE_FULL_INPUT
|
|
if (!usb_active) probe_controller_input_cancel_sample(instance);
|
|
if (controller->gameplay_rumble_seen && (!output_active || !protocol->initialized)) {
|
|
probe_controller_input_cancel_rumble(instance);
|
|
controller->gameplay_rumble_seen = false;
|
|
}
|
|
#endif
|
|
if (protocol->controller_active != output_active)
|
|
probe_debug_printf("[PROBE] Controller input itf=%u %s\n",
|
|
instance, output_active ? "active" : "neutral (disconnected/stale)");
|
|
protocol->controller_active = output_active;
|
|
if (output_active) {
|
|
memcpy(protocol->controller_buttons, source.buttons, sizeof(source.buttons));
|
|
memcpy(protocol->controller_stick, source.stick, sizeof(source.stick));
|
|
} else {
|
|
memset(protocol->controller_buttons, 0, sizeof(protocol->controller_buttons));
|
|
}
|
|
// Each HID endpoint must make its own progress before consuming donor packets.
|
|
controller->native_stream_ready = probe_transport_mounted(instance) &&
|
|
!probe_transport_suspended(instance) &&
|
|
protocol->initialized && protocol->report_id == probe_model_report_id(instance) &&
|
|
controller->hid_completion_seen &&
|
|
(uint32_t)(now - controller->last_hid_complete_ms) < 500;
|
|
probe_controller_input_set_native_stream(instance, controller->native_stream_ready);
|
|
}
|
|
|
|
#endif
|
|
|
|
static void protocol_task(probe_usb_controller* controller, uint32_t now) {
|
|
const uint8_t instance = controller->instance;
|
|
if (!probe_transport_mounted(instance) || probe_transport_suspended(instance)) return;
|
|
probe_protocol_state* protocol = &controller->protocol;
|
|
if (controller->reply_count && !controller->reply_inflight) {
|
|
queued_reply* reply = &controller->replies[controller->reply_head];
|
|
bool ready = reply->deferred_token == 0;
|
|
#ifdef SWITCH_PICO_SWITCH2_USB_BRIDGE
|
|
if (!ready) {
|
|
const int result = probe_controller_input_sample_result(instance, reply->deferred_token, now);
|
|
if (result > 0) {
|
|
#if SWITCH2_BRIDGE_WII_INPUT
|
|
probe_debug_printf("[PROBE] Wii cue dispatched itf=%u token=%" PRIu64 "\n",
|
|
instance, reply->deferred_token);
|
|
#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",
|
|
instance, reply->deferred_token);
|
|
#endif
|
|
reply->deferred_token = 0;
|
|
ready = true;
|
|
log_packet("BULK_REPLY_QUEUED", instance, 0, reply->data, reply->length);
|
|
} else if (result < 0) {
|
|
probe_debug_printf("[PROBE] Source sample failed itf=%u token=%" PRIu64 "; no USB ACK\n",
|
|
instance, reply->deferred_token);
|
|
controller->reply_head = (uint8_t)((controller->reply_head + 1) % REPLY_CAPACITY);
|
|
--controller->reply_count;
|
|
++controller->command_drops;
|
|
++command_drops;
|
|
}
|
|
}
|
|
#endif
|
|
if (ready && probe_transport_vendor_write_available(instance) >= reply->length) {
|
|
if (probe_transport_vendor_write(instance, reply->data, reply->length) == reply->length) {
|
|
controller->reply_inflight = true;
|
|
controller->reply_remaining = reply->length;
|
|
probe_transport_vendor_write_flush(instance);
|
|
controller->reply_head = (uint8_t)((controller->reply_head + 1) % REPLY_CAPACITY);
|
|
--controller->reply_count;
|
|
}
|
|
}
|
|
}
|
|
if (protocol->initialized && now - controller->last_input_ms >= 4 &&
|
|
probe_transport_hid_ready(instance)) {
|
|
uint8_t input[PROBE_INPUT_SIZE];
|
|
size_t length = 0;
|
|
#ifdef SWITCH_PICO_SWITCH2_USB_BRIDGE
|
|
uint32_t native_serial = 0;
|
|
if (protocol->report_id == probe_model_report_id(instance) &&
|
|
protocol->controller_active && controller->native_stream_ready) {
|
|
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_FULL_INPUT
|
|
input[8] = (uint8_t)(0x30 | ((protocol->enabled_features & 0x20) ? 8 : 0));
|
|
#endif
|
|
probe_protocol_gate_native_report(protocol, input);
|
|
}
|
|
#endif
|
|
if (!length)
|
|
length = probe_protocol_report(protocol, protocol->report_id, input, sizeof(input));
|
|
#if defined(SWITCH2_PROBE_JOIN_CHORD_GATE) && !SWITCH2_PROBE_HUB
|
|
if (length) gate_join_shoulders(instance, protocol->report_id, input);
|
|
#endif
|
|
if (length && probe_transport_hid_report(instance, protocol->report_id, input, (uint16_t)length)) {
|
|
#ifdef SWITCH_PICO_SWITCH2_USB_BRIDGE
|
|
if (native_serial) {
|
|
if (!probe_controller_input_commit_native_report(instance, native_serial))
|
|
probe_debug_printf("[PROBE] Native stream changed during HID submission itf=%u\n", instance);
|
|
protocol->report_counter = input[0];
|
|
}
|
|
#endif
|
|
++protocol->report_counter;
|
|
++controller->input_reports;
|
|
++input_reports;
|
|
controller->last_input_ms = now;
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
#if SWITCH2_PROBE_HUB
|
|
void native_joycon_usb_reset(uint8_t instance) {
|
|
if (instance >= PROBE_CONTROLLER_COUNT) return;
|
|
#ifdef SWITCH2_PROBE_USB_INIT
|
|
reset_controller_protocol(instance);
|
|
#endif
|
|
probe_debug_printf("[PROBE] USB_RESET itf=%u\n", instance);
|
|
}
|
|
#endif
|
|
|
|
#ifdef SWITCH_PICO_SWITCH2_USB_BRIDGE
|
|
static int32_t signed_mouse_delta(const uint8_t* data) {
|
|
const uint16_t raw = (uint16_t)(data[0] | ((uint16_t)data[1] << 8));
|
|
return raw >= 0x8000 ? (int32_t)raw - 0x10000 : raw;
|
|
}
|
|
|
|
void tud_hid_report_failed_cb(uint8_t instance, hid_report_type_t report_type,
|
|
const uint8_t* report, uint16_t length) {
|
|
(void)report;
|
|
(void)length;
|
|
if (instance < PROBE_CONTROLLER_COUNT && report_type == HID_REPORT_TYPE_INPUT) {
|
|
probe_controller_input_set_native_stream(instance, false);
|
|
controllers[instance].native_stream_ready = false;
|
|
controllers[instance].hid_completion_seen = false;
|
|
probe_debug_printf("[PROBE] HID input transfer failed itf=%u; pending native packets discarded\n", instance);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
void tud_hid_report_complete_cb(uint8_t instance, const uint8_t* report, uint16_t length) {
|
|
#ifdef SWITCH2_PROBE_USB_INIT
|
|
if (instance >= PROBE_CONTROLLER_COUNT || length != PROBE_INPUT_SIZE + 1) return;
|
|
probe_usb_controller* controller = &controllers[instance];
|
|
const uint8_t native_id = probe_model_report_id(instance);
|
|
const uint8_t common_buttons_offset = probe_model_is_left(instance) ? 7 : 5;
|
|
uint8_t rails;
|
|
if (report[0] == native_id) rails = (report[4] >> 6) & 3;
|
|
else if (report[0] == 0x05) rails = (report[common_buttons_offset] >> 4) & 3;
|
|
else return;
|
|
#ifdef SWITCH_PICO_SWITCH2_USB_BRIDGE
|
|
controller->last_hid_complete_ms = to_ms_since_boot(get_absolute_time());
|
|
controller->hid_completion_seen = true;
|
|
if (report[0] == native_id) {
|
|
const int32_t dx = signed_mouse_delta(report + 10);
|
|
const int32_t dy = signed_mouse_delta(report + 12);
|
|
#ifdef SWITCH2_PROBE_TRACE_NATIVE_INPUT
|
|
if ((uint32_t)(controller->last_hid_complete_ms - controller->last_native_trace_ms) >= 1000) {
|
|
controller->last_native_trace_ms = controller->last_hid_complete_ms;
|
|
log_packet("NATIVE_INPUT_DELIVERED", instance, report[0], report, length);
|
|
}
|
|
#endif
|
|
if (dx || dy) {
|
|
++controller->mouse_delivered_reports;
|
|
controller->mouse_delivered_x += dx;
|
|
controller->mouse_delivered_y += dy;
|
|
}
|
|
}
|
|
const uint16_t buttons = report[0] == native_id ?
|
|
(uint16_t)(report[3] | ((uint16_t)report[4] << 8)) :
|
|
(uint16_t)(report[common_buttons_offset] | ((uint16_t)report[6] << 8));
|
|
if (buttons != controller->last_delivered_buttons) {
|
|
controller->last_delivered_buttons = buttons;
|
|
probe_debug_printf("[PROBE] CONTROLLER_REPORT delivered itf=%u id=%02x buttons=%04x\n",
|
|
instance, report[0], buttons);
|
|
}
|
|
#endif
|
|
if (rails != controller->last_delivered_rails) {
|
|
controller->last_delivered_rails = rails;
|
|
probe_debug_printf("[PROBE] TEST_REPORT delivered itf=%u id=%02x SL=%u SR=%u\n",
|
|
instance, report[0], (rails >> 1) & 1, rails & 1);
|
|
}
|
|
#else
|
|
(void)instance;
|
|
(void)report;
|
|
(void)length;
|
|
#endif
|
|
}
|
|
|
|
void tud_vendor_rx_cb(uint8_t instance, const uint8_t* buffer, uint16_t length) {
|
|
++bulk_packets;
|
|
if (instance >= PROBE_CONTROLLER_COUNT) return;
|
|
#ifdef SWITCH2_PROBE_USB_INIT
|
|
consume_bulk_packet(&controllers[instance], buffer, length);
|
|
#else
|
|
log_packet("BULK_OUT", instance, 0, buffer, length);
|
|
#endif
|
|
probe_transport_vendor_discard_received(instance);
|
|
}
|
|
|
|
void tud_vendor_tx_cb(uint8_t instance, uint32_t length) {
|
|
#ifdef SWITCH2_PROBE_USB_INIT
|
|
if (instance >= PROBE_CONTROLLER_COUNT) return;
|
|
probe_usb_controller* controller = &controllers[instance];
|
|
if (controller->reply_inflight) {
|
|
if (length <= controller->reply_remaining) {
|
|
controller->reply_remaining -= (uint16_t)length;
|
|
// Exact packet multiples finish only after the transport's ZLP.
|
|
if (controller->reply_remaining == 0 && length < CFG_TUD_VENDOR_EPSIZE)
|
|
controller->reply_inflight = false;
|
|
} else {
|
|
probe_debug_printf("[PROBE] Unexpected bulk completion itf=%u pending=%u\n",
|
|
instance, controller->reply_remaining);
|
|
}
|
|
}
|
|
#endif
|
|
probe_debug_printf("[PROBE] BULK_TX_COMPLETE itf=%u length=%" PRIu32 "\n", instance, length);
|
|
}
|
|
|
|
bool tud_vendor_control_xfer_cb(uint8_t rhport, uint8_t stage,
|
|
const tusb_control_request_t* request) {
|
|
if (stage == CONTROL_STAGE_SETUP)
|
|
log_packet("VENDOR_CONTROL", rhport, 0, (const uint8_t*)request, sizeof(*request));
|
|
#if defined(SWITCH_PICO_SWITCH2_USB_BRIDGE) || SWITCH2_PROBE_NEUTRAL_INPUT
|
|
if (probe_management_vendor_control(rhport, stage, request))
|
|
return true;
|
|
#endif
|
|
#if SWITCH2_PROBE_HUB
|
|
// Each USB address owns native interfaces 0/1; rhport is its device slot.
|
|
if (rhport < 1 || rhport > PROBE_CONTROLLER_COUNT || request->wIndex >= 2) return false;
|
|
const uint8_t instance = (uint8_t)(rhport - 1);
|
|
#else
|
|
// Device-level index zero names the primary identity. Explicit interface
|
|
// indexes can select the sibling; never infer an identity from timing.
|
|
if (request->wIndex >= 2 * PROBE_CONTROLLER_COUNT) return false;
|
|
const uint8_t instance = (uint8_t)(request->wIndex / 2);
|
|
#endif
|
|
#ifdef SWITCH2_PROBE_IDENTITY_REPLY
|
|
if ((request->bmRequestType == 0xc0 || request->bmRequestType == 0xc1) &&
|
|
request->bRequest == 0x03 && request->wValue == 0) {
|
|
if (stage == CONTROL_STAGE_SETUP) {
|
|
++identity_requests;
|
|
log_packet("IDENTITY_REPLY", instance, 3, probe_identity_replies[instance],
|
|
sizeof(probe_identity_replies[instance]));
|
|
return probe_transport_control_xfer(rhport, request, (void*)probe_identity_replies[instance],
|
|
sizeof(probe_identity_replies[instance]));
|
|
}
|
|
return true;
|
|
}
|
|
#endif
|
|
#ifdef SWITCH2_PROBE_VERSION_REPLY
|
|
if ((request->bmRequestType == 0xc0 || request->bmRequestType == 0xc1) &&
|
|
request->bRequest == 0x02 && request->wValue == 0) {
|
|
if (stage == CONTROL_STAGE_SETUP) {
|
|
++version_requests;
|
|
log_packet("VERSION_REPLY", instance, 2, probe_version_replies[instance],
|
|
sizeof(probe_version_replies[instance]));
|
|
return probe_transport_control_xfer(rhport, request, (void*)probe_version_replies[instance],
|
|
sizeof(probe_version_replies[instance]));
|
|
}
|
|
return true;
|
|
}
|
|
#endif
|
|
#ifdef SWITCH2_PROBE_ACK_SETUP04
|
|
// Preserve the observed setup payload; its parameter semantics are unknown.
|
|
if ((request->bmRequestType == 0x40 || request->bmRequestType == 0x41) &&
|
|
request->bRequest == 0x04 && request->wValue == 0x0276 && request->wLength == 0) {
|
|
if (stage == CONTROL_STAGE_SETUP)
|
|
return probe_transport_control_status(rhport, request);
|
|
if (stage == CONTROL_STAGE_ACK) {
|
|
++setup_completions;
|
|
probe_debug_printf("[PROBE] SETUP04 itf=%u acknowledged value=%04x\n",
|
|
instance, request->wValue);
|
|
}
|
|
return true;
|
|
}
|
|
#endif
|
|
return false;
|
|
}
|
|
|
|
#if !SWITCH2_PROBE_HUB
|
|
void tud_mount_cb(void) {
|
|
#ifdef SWITCH2_PROBE_USB_INIT
|
|
reset_protocol();
|
|
#endif
|
|
probe_debug_printf("[PROBE] MOUNT\n");
|
|
}
|
|
void tud_umount_cb(void) {
|
|
#ifdef SWITCH2_PROBE_USB_INIT
|
|
reset_protocol();
|
|
#endif
|
|
probe_debug_printf("[PROBE] UNMOUNT\n");
|
|
}
|
|
void tud_suspend_cb(bool remote_wakeup_en) {
|
|
probe_debug_printf("[PROBE] SUSPEND wake=%u\n", remote_wakeup_en);
|
|
#ifdef SWITCH2_PROBE_USB_INIT
|
|
for (uint8_t instance = 0; instance < PROBE_CONTROLLER_COUNT; ++instance) {
|
|
probe_usb_controller* controller = &controllers[instance];
|
|
#ifndef SWITCH_PICO_SWITCH2_USB_BRIDGE
|
|
(void)probe_button_update(&controller->button_test, -1, false);
|
|
#endif
|
|
controller->protocol.test_rail_buttons = false;
|
|
controller->protocol.controller_active = false;
|
|
#ifdef SWITCH_PICO_SWITCH2_USB_BRIDGE
|
|
probe_controller_input_set_native_stream(instance, false);
|
|
#if SWITCH2_BRIDGE_FULL_INPUT
|
|
probe_controller_input_cancel_sample(instance);
|
|
probe_controller_input_cancel_rumble(instance);
|
|
controller->gameplay_rumble_seen = false;
|
|
#endif
|
|
controller->native_stream_ready = false;
|
|
controller->hid_completion_seen = false;
|
|
#endif
|
|
}
|
|
#endif
|
|
}
|
|
void tud_resume_cb(void) { probe_debug_printf("[PROBE] RESUME\n"); }
|
|
#endif
|
|
|
|
int main(void) {
|
|
#if SWITCH2_PROBE_HUB
|
|
native_hub_startup_guard();
|
|
#endif
|
|
#ifdef SWITCH_PICO_SWITCH2_USB_BRIDGE
|
|
probe_controller_input_clock_init();
|
|
#elif SWITCH2_PROBE_NEUTRAL_INPUT
|
|
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
|
|
probe_debug_printf("\n[PROBE] Neutral native USB hub transport-only experiment: %u pair(s), %u children\n",
|
|
PROBE_CONTROLLER_COUNT / 2, PROBE_CONTROLLER_COUNT);
|
|
#else
|
|
probe_debug_printf("\n[PROBE] " PROBE_JOYCON_PRODUCT " USB enumeration recorder\n");
|
|
#endif
|
|
#if SWITCH2_PROBE_HUB
|
|
#if PROBE_CONTROLLER_COUNT == 4
|
|
probe_debug_printf("[PROBE] NATIVE_HUB: two pairs in A_R/A_L/B_R/B_L order; each HID0/vendor1 EP1/2; no shoulder gate\n");
|
|
for (uint8_t instance = 0; instance < PROBE_CONTROLLER_COUNT; ++instance)
|
|
probe_debug_printf("[PROBE] CHILD slot=%u pair=%c side=%c pid=%04x report=%02x\n",
|
|
instance + 1, 'A' + instance / 2,
|
|
probe_model_is_left(instance) ? 'L' : 'R',
|
|
probe_model_pid(instance), probe_model_report_id(instance));
|
|
#else
|
|
probe_debug_printf("[PROBE] NATIVE_HUB: device1 right PID2066, device2 left PID2067; each HID0/vendor1 EP1/2; no shoulder gate\n");
|
|
#endif
|
|
#endif
|
|
#if SWITCH2_PROBE_COMPOSITE
|
|
#ifdef SWITCH2_PROBE_JOIN_CHORD_GATE
|
|
probe_debug_printf("[PROBE] JOIN_CHORD_GATE enabled: physical L+R required; no synthesized presses or USB initialization\n");
|
|
#endif
|
|
probe_debug_printf("[PROBE] COMPOSITE: right HID0/vendor1, left HID2/vendor3; shared USB PID2066\n");
|
|
#endif
|
|
#ifdef SWITCH2_PROBE_OMIT_NATIVE_IMU
|
|
probe_debug_printf("[PROBE] ACTIVATION_TEST=no-imu: native%02x bytes %u..%u omitted; features, power, status, counters and cadence unchanged\n",
|
|
PROBE_NATIVE_REPORT_ID, PROBE_IMU_LENGTH_OFFSET, PROBE_IMU_DATA_OFFSET + 39u);
|
|
#elif defined(SWITCH2_PROBE_ZERO_NATIVE_IMU_PAYLOAD)
|
|
probe_debug_printf("[PROBE] ACTIVATION_TEST=zero-imu-payload: native%02x bytes %u..%u zeroed; length, features and all other fields unchanged\n",
|
|
PROBE_NATIVE_REPORT_ID, PROBE_IMU_DATA_OFFSET, PROBE_IMU_DATA_OFFSET + 39u);
|
|
#endif
|
|
#ifdef SWITCH_PICO_SWITCH2_USB_BRIDGE
|
|
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; up to %u supported gamepad(s) feed %u native R/L pair(s)\n",
|
|
PROBE_CONTROLLER_COUNT / 2, PROBE_CONTROLLER_COUNT / 2);
|
|
#else
|
|
probe_debug_printf("[PROBE] UART0 GP0=TX, 115200 8N1; %u selected Joy-Con Bluetooth source(s)\n",
|
|
PROBE_CONTROLLER_COUNT);
|
|
#endif
|
|
#else
|
|
probe_debug_printf("[PROBE] UART0 GP0=TX, 115200 8N1; Bluetooth disabled\n");
|
|
#endif
|
|
#ifdef SWITCH2_PROBE_IDENTITY_REPLY
|
|
probe_debug_printf("[PROBE] Configured factory-format identity reply enabled for vendor read 03\n");
|
|
#else
|
|
probe_debug_printf("[PROBE] Factory identity reply disabled\n");
|
|
#endif
|
|
#ifdef SWITCH2_PROBE_VERSION_REPLY
|
|
probe_debug_printf("[PROBE] Captured firmware version with configured controller address enabled\n");
|
|
#endif
|
|
#ifdef SWITCH2_PROBE_ACK_SETUP04
|
|
probe_debug_printf("[PROBE] Observed vendor-04 setup acknowledgement enabled\n");
|
|
#endif
|
|
#ifdef SWITCH2_PROBE_USB_INIT
|
|
reset_protocol();
|
|
for (uint8_t instance = 0; instance < PROBE_CONTROLLER_COUNT; ++instance)
|
|
probe_debug_printf("[PROBE] Own virtual pairing itf=%u offset=%08" PRIx32 "\n",
|
|
instance, probe_storage_offset(instance));
|
|
#ifdef SWITCH_PICO_SWITCH2_USB_BRIDGE
|
|
#if SWITCH2_BRIDGE_WII_INPUT
|
|
probe_debug_printf("[PROBE] Wii IR drives native mouse movement; buttons retain profile mapping; MotionPlus bias learns in background\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 %u pair(s); IMU side mask=%u; Wii bias learns without startup settling\n",
|
|
PROBE_CONTROLLER_COUNT / 2, (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
|
|
#elif SWITCH2_PROBE_NEUTRAL_INPUT
|
|
probe_debug_printf("[PROBE] Neutral captured-calibration reports only; physical BOOTSEL input disabled; no Bluetooth, mouse/IMU samples or motor cue acknowledgements\n");
|
|
probe_debug_printf("[PROBE] Private software BOOTSEL on root/children enabled; profile/configuration management disabled\n");
|
|
#else
|
|
probe_debug_printf("[PROBE] Manual input test: hold BOOTSEL for SL+SR, release for neutral; no controller forwarding\n");
|
|
#endif
|
|
#else
|
|
probe_debug_printf("[PROBE] Bulk commands are capture-only; no input reports\n");
|
|
#endif
|
|
#ifdef SWITCH_PICO_SWITCH2_USB_BRIDGE
|
|
if (!probe_controller_input_start())
|
|
panic("Bluetooth source startup failed");
|
|
#endif
|
|
#if SWITCH2_PROBE_HUB
|
|
if (!native_hub_init())
|
|
panic("Native hub startup failed");
|
|
#else
|
|
tud_init(0);
|
|
#endif
|
|
uint32_t last_heartbeat = 0;
|
|
while (true) {
|
|
#if SWITCH2_PROBE_HUB
|
|
#ifdef SWITCH_PICO_SWITCH2_USB_BRIDGE
|
|
#if defined(SWITCH2_PROBE_TRACE_NATIVE_INPUT)
|
|
native_hub_trace_phase(NATIVE_HUB_TRACE_PHASE_RADIO_POLL);
|
|
#endif
|
|
probe_controller_input_task();
|
|
#endif
|
|
#if defined(SWITCH2_PROBE_TRACE_NATIVE_INPUT)
|
|
native_hub_trace_phase(NATIVE_HUB_TRACE_PHASE_USB_TASK);
|
|
#endif
|
|
native_hub_task();
|
|
#else
|
|
tud_task();
|
|
#endif
|
|
#if SWITCH2_PROBE_HUB && defined(SWITCH2_PROBE_TRACE_NATIVE_INPUT)
|
|
native_hub_trace_phase(NATIVE_HUB_TRACE_PHASE_LOG_DRAIN);
|
|
#endif
|
|
drain_log();
|
|
#if SWITCH2_PROBE_HUB && defined(SWITCH2_PROBE_TRACE_NATIVE_INPUT)
|
|
native_hub_trace_phase(NATIVE_HUB_TRACE_PHASE_PROTOCOL);
|
|
#endif
|
|
const uint32_t now = to_ms_since_boot(get_absolute_time());
|
|
#if defined(SWITCH_PICO_SWITCH2_USB_BRIDGE) || SWITCH2_PROBE_NEUTRAL_INPUT
|
|
probe_bootsel_task(now);
|
|
#endif
|
|
#ifdef SWITCH2_PROBE_USB_INIT
|
|
#ifdef SWITCH_PICO_SWITCH2_USB_BRIDGE
|
|
if (probe_controller_input_pairing_task())
|
|
probe_debug_printf("[PROBE] Bluetooth pairing window requested; put the selected source in SYNC pairing mode\n");
|
|
#endif
|
|
for (uint8_t instance = 0; instance < PROBE_CONTROLLER_COUNT; ++instance) {
|
|
#ifdef SWITCH_PICO_SWITCH2_USB_BRIDGE
|
|
controller_input_task(&controllers[instance], now);
|
|
#elif !SWITCH2_PROBE_NEUTRAL_INPUT
|
|
button_test_task(&controllers[instance], now);
|
|
#endif
|
|
#if !defined(SWITCH2_PROBE_JOIN_CHORD_GATE) || SWITCH2_PROBE_HUB
|
|
protocol_task(&controllers[instance], now);
|
|
#endif
|
|
}
|
|
#if defined(SWITCH2_PROBE_JOIN_CHORD_GATE) && !SWITCH2_PROBE_HUB
|
|
const uint8_t shoulder_mask = join_shoulder_mask();
|
|
if (shoulder_mask != last_join_shoulder_mask) {
|
|
last_join_shoulder_mask = shoulder_mask;
|
|
probe_debug_printf("[PROBE %" PRIu32 "] JOIN_CHORD right=%u left=%u open=%u initialized=%u/%u\n",
|
|
now, (shoulder_mask & 1) != 0, (shoulder_mask & 2) != 0,
|
|
shoulder_mask == 3, controllers[0].protocol.initialized,
|
|
controllers[1].protocol.initialized);
|
|
}
|
|
// Poll both sources before either USB submission observes the chord.
|
|
for (uint8_t instance = 0; instance < PROBE_CONTROLLER_COUNT; ++instance)
|
|
protocol_task(&controllers[instance], now);
|
|
#endif
|
|
#endif
|
|
if (now - last_heartbeat >= 1000) {
|
|
last_heartbeat = now;
|
|
probe_debug_printf("[PROBE %" PRIu32 "] alive mounted=%u bulk=%" PRIu32
|
|
" hid=%" PRIu32 " identity=%" PRIu32
|
|
" version=%" PRIu32 " setup=%" PRIu32
|
|
" inputs=%" PRIu32 " command_drops=%" PRIu32
|
|
" log_dropped_bytes=%" PRIu32 "\n",
|
|
now, probe_transport_mounted(0), bulk_packets, hid_packets,
|
|
identity_requests, version_requests, setup_completions,
|
|
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];
|
|
probe_debug_printf("[PROBE] CHANNEL itf=%u mounted=%u suspended=%u initialized=%u active=%u report=%02x LEDs=%x inputs=%" PRIu32
|
|
" drops=%" PRIu32 "\n", instance, probe_transport_mounted(instance),
|
|
probe_transport_suspended(instance), controller->protocol.initialized,
|
|
controller->protocol.controller_active, controller->protocol.report_id,
|
|
controller->protocol.player_leds, controller->input_reports,
|
|
controller->command_drops);
|
|
#ifdef SWITCH_PICO_SWITCH2_USB_BRIDGE
|
|
if (controller->mouse_delivered_reports != controller->mouse_logged_reports) {
|
|
controller->mouse_logged_reports = controller->mouse_delivered_reports;
|
|
probe_debug_printf("[PROBE] MOUSE_REPORT itf=%u packets=%" PRIu32
|
|
" total_x=%" PRId64 " total_y=%" PRId64 "\n", instance,
|
|
controller->mouse_delivered_reports,
|
|
controller->mouse_delivered_x, controller->mouse_delivered_y);
|
|
}
|
|
#endif
|
|
}
|
|
#endif
|
|
}
|
|
#if SWITCH2_PROBE_HUB && defined(SWITCH2_PROBE_TRACE_NATIVE_INPUT)
|
|
native_hub_trace_phase(NATIVE_HUB_TRACE_PHASE_SLEEP);
|
|
#endif
|
|
sleep_us(100);
|
|
}
|
|
}
|