Add verified Joy-Con 2 USB bridge with native mouse support
Implement the standalone USB protocol probe and Bluetooth-backed right Joy-Con bridge with its own persistent virtual pairing identity. Preserve complete ordered native reports, including opaque motion data, and match the console feature set. Relay built-in vibration cues only after genuine source acknowledgement and expose safe BOOTSEL pairing control. Include native capture diagnostics and focused protocol, packet-lifecycle, and cue regressions. Native mouse operation confirmed on Switch with bridge 0.24; private captures and firmware backups remain outside the commit.
This commit is contained in:
parent
485ad39709
commit
3040c9d294
34 changed files with 3777 additions and 15 deletions
684
tools/switch2_usb_probe/main.c
Normal file
684
tools/switch2_usb_probe/main.c
Normal file
|
|
@ -0,0 +1,684 @@
|
|||
// Joy-Con 2 (R) 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>
|
||||
|
||||
#ifdef SWITCH_PICO_SWITCH2_USB_BRIDGE
|
||||
#include "controller_input.h"
|
||||
#else
|
||||
#include "platform/pico/bootsel_button_sample.h"
|
||||
#include "button_test.h"
|
||||
#endif
|
||||
#include "pico/stdlib.h"
|
||||
#include "hardware/sync.h"
|
||||
#include "hardware/uart.h"
|
||||
#include "tusb.h"
|
||||
#include "descriptors.h"
|
||||
#include "protocol.h"
|
||||
#include "storage.h"
|
||||
#ifdef SWITCH2_PROBE_MEMORY
|
||||
#include "memory.h"
|
||||
#endif
|
||||
#ifdef SWITCH2_PROBE_IDENTITY_REPLY
|
||||
#include "probe_identity.h"
|
||||
_Static_assert(sizeof(probe_identity_reply) == 64, "factory identity response size");
|
||||
#endif
|
||||
#ifdef SWITCH2_PROBE_VERSION_REPLY
|
||||
#include "probe_version.h"
|
||||
_Static_assert(sizeof(probe_version_reply) == 16, "version/address response size");
|
||||
#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_descriptor[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;
|
||||
static probe_protocol_state protocol;
|
||||
static queued_reply replies[REPLY_CAPACITY];
|
||||
static uint8_t reply_head, reply_count;
|
||||
static bool reply_inflight;
|
||||
static uint16_t reply_remaining;
|
||||
static uint8_t command_frame[PROBE_COMMAND_MAX_SIZE];
|
||||
static uint16_t command_used, command_expected = 8;
|
||||
static uint32_t last_input_ms;
|
||||
#ifdef SWITCH_PICO_SWITCH2_USB_BRIDGE
|
||||
static uint32_t last_controller_poll_ms;
|
||||
static uint16_t last_delivered_buttons;
|
||||
static bool native_stream_ready;
|
||||
static uint32_t last_hid_complete_ms;
|
||||
static bool hid_completion_seen;
|
||||
static uint32_t mouse_delivered_reports, mouse_logged_reports;
|
||||
static int64_t mouse_delivered_x, mouse_delivered_y;
|
||||
#endif
|
||||
#ifndef SWITCH_PICO_SWITCH2_USB_BRIDGE
|
||||
static probe_button_state button_test;
|
||||
static uint32_t last_button_ms;
|
||||
static bool button_sample_error;
|
||||
#endif
|
||||
static uint8_t last_delivered_rails;
|
||||
#endif
|
||||
|
||||
_Static_assert(sizeof(probe_device_descriptor) == 18, "device descriptor size");
|
||||
_Static_assert(sizeof(probe_configuration_descriptor) == 80, "configuration descriptor size");
|
||||
_Static_assert(sizeof(probe_hid_report_descriptor) == 100, "HID descriptor size");
|
||||
|
||||
int probe_debug_printf(const char* format, ...) {
|
||||
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;
|
||||
const uint32_t interrupts = save_and_disable_interrupts();
|
||||
if (LOG_CAPACITY - (log_written - log_read) >= size) {
|
||||
for (size_t i = 0; i < size; ++i)
|
||||
log_bytes[(log_written + i) % LOG_CAPACITY] = message[i];
|
||||
log_written += (uint32_t)size;
|
||||
log_dropped += (uint32_t)result - (uint32_t)size;
|
||||
} else {
|
||||
log_dropped += (uint32_t)result;
|
||||
}
|
||||
restore_interrupts(interrupts);
|
||||
return result;
|
||||
}
|
||||
|
||||
static void drain_log(void) {
|
||||
while (uart_is_writable(uart0)) {
|
||||
const uint32_t interrupts = save_and_disable_interrupts();
|
||||
if (log_read == log_written) {
|
||||
restore_interrupts(interrupts);
|
||||
break;
|
||||
}
|
||||
const char value = log_bytes[log_read++ % LOG_CAPACITY];
|
||||
restore_interrupts(interrupts);
|
||||
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:2066\n");
|
||||
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 == 0 ? probe_hid_report_descriptor : NULL;
|
||||
}
|
||||
|
||||
uint16_t const* tud_descriptor_string_cb(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", "Joy-Con 2 (R)", "00", "USB configuration", "HID", "Commands"};
|
||||
probe_debug_printf("[PROBE] STRING_DESCRIPTOR index=%u lang=%04x\n", index, langid);
|
||||
if (index == 0) {
|
||||
string_descriptor[0] = (TUSB_DESC_STRING << 8) | 4;
|
||||
string_descriptor[1] = 0x0409;
|
||||
return string_descriptor;
|
||||
}
|
||||
if (index >= sizeof(strings) / sizeof(strings[0])) return NULL;
|
||||
size_t count = strlen(strings[index]);
|
||||
if (count > 63) count = 63;
|
||||
string_descriptor[0] = (uint16_t)((TUSB_DESC_STRING << 8) | (2 + count * 2));
|
||||
for (size_t i = 0; i < count; ++i)
|
||||
string_descriptor[i + 1] = (uint8_t)strings[index][i];
|
||||
return string_descriptor;
|
||||
}
|
||||
|
||||
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 == 0 && report_type == HID_REPORT_TYPE_INPUT &&
|
||||
probe_protocol_report(&protocol, report_id, input, sizeof(input))) {
|
||||
const uint16_t size = requested_length < sizeof(input) ? requested_length : sizeof(input);
|
||||
memcpy(buffer, input, size);
|
||||
probe_debug_printf("[PROBE] GET_REPORT id=%02x diagnostic length=%u\n", 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;
|
||||
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(const uint8_t* data, size_t length) {
|
||||
const bool saved = probe_storage_save(data, length);
|
||||
probe_debug_printf("[PROBE] Virtual pairing persistence %s\n", saved ? "verified" : "failed");
|
||||
return saved;
|
||||
}
|
||||
|
||||
static void reset_protocol(void) {
|
||||
#ifdef SWITCH_PICO_SWITCH2_USB_BRIDGE
|
||||
probe_controller_input_cancel_sample();
|
||||
#endif
|
||||
probe_protocol_reset(&protocol);
|
||||
memcpy(protocol.controller_address, probe_version_reply + 10, sizeof(protocol.controller_address));
|
||||
protocol.firmware_version = probe_firmware_version;
|
||||
protocol.save_pairing = save_pairing;
|
||||
#ifdef SWITCH_PICO_SWITCH2_USB_BRIDGE
|
||||
protocol.play_sample = probe_controller_input_play_sample;
|
||||
#endif
|
||||
#ifdef SWITCH2_PROBE_MEMORY
|
||||
if (!probe_memory_right_stick_center(protocol.right_stick_center))
|
||||
panic("Invalid captured Joy-Con stick calibration");
|
||||
protocol.read_memory = probe_memory_read;
|
||||
#endif
|
||||
uint8_t pairing[PROBE_PAIRING_BLOB_SIZE];
|
||||
if (probe_storage_load(pairing, sizeof(pairing)) &&
|
||||
probe_protocol_restore_pairing(&protocol, pairing, sizeof(pairing))) {
|
||||
probe_debug_printf("[PROBE] Restored own virtual pairing record\n");
|
||||
}
|
||||
reply_head = reply_count = 0;
|
||||
reply_inflight = false;
|
||||
reply_remaining = 0;
|
||||
command_used = 0;
|
||||
command_expected = 8;
|
||||
last_input_ms = 0;
|
||||
#ifdef SWITCH_PICO_SWITCH2_USB_BRIDGE
|
||||
last_controller_poll_ms = 0;
|
||||
last_delivered_buttons = 0;
|
||||
probe_controller_input_set_native_stream(false);
|
||||
native_stream_ready = false;
|
||||
last_hid_complete_ms = 0;
|
||||
hid_completion_seen = false;
|
||||
#endif
|
||||
#ifndef SWITCH_PICO_SWITCH2_USB_BRIDGE
|
||||
(void)probe_button_update(&button_test, -1, false);
|
||||
last_button_ms = 0;
|
||||
button_sample_error = false;
|
||||
#endif
|
||||
last_delivered_rails = 0;
|
||||
}
|
||||
|
||||
static void complete_command(void) {
|
||||
// Log complete frames rather than fragments so key material can be redacted.
|
||||
log_packet("BULK_OUT", 0, 0, command_frame, command_used);
|
||||
if (reply_count == REPLY_CAPACITY) {
|
||||
++command_drops;
|
||||
probe_debug_printf("[PROBE] Command captured but not executed: reply queue full\n");
|
||||
return;
|
||||
}
|
||||
queued_reply* reply = &replies[(reply_head + 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_frame, 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(false);
|
||||
native_stream_ready = false;
|
||||
}
|
||||
#endif
|
||||
reply->length = (uint8_t)length;
|
||||
++reply_count;
|
||||
if (reply->deferred_token) {
|
||||
probe_debug_printf("[PROBE] Sample %u awaiting source ACK token=%" PRIu64 "\n",
|
||||
command_frame[8], reply->deferred_token);
|
||||
} else {
|
||||
log_packet("BULK_REPLY_QUEUED", 0, 0, reply->data, reply->length);
|
||||
}
|
||||
if (command_frame[0] == 0x09) {
|
||||
probe_debug_printf("[PROBE] Virtual player LEDs mask=%x flashing=%u\n",
|
||||
protocol.player_leds, protocol.player_leds_flashing);
|
||||
}
|
||||
if (command_frame[0] == 0x0c) {
|
||||
probe_debug_printf("[PROBE] Virtual features mask=%02x enabled=%02x\n",
|
||||
protocol.feature_mask, protocol.enabled_features);
|
||||
}
|
||||
if (command_frame[0] == 0x0a && command_frame[3] == 8)
|
||||
probe_debug_printf("[PROBE] Virtual vibration parameters stored; no motor output\n");
|
||||
if (command_frame[0] == 0x03 && command_frame[3] == 0x0c)
|
||||
probe_debug_printf("[PROBE] Runtime 03/0C value=%u\n", protocol.runtime03_0c);
|
||||
} else {
|
||||
probe_debug_printf("[PROBE] Command %02x/%02x length=%u capture-only or malformed\n",
|
||||
command_frame[0], command_frame[3], command_used);
|
||||
}
|
||||
}
|
||||
|
||||
static void consume_bulk_packet(const uint8_t* buffer, uint16_t length) {
|
||||
for (uint16_t i = 0; i < length; ++i) {
|
||||
command_frame[command_used++] = buffer[i];
|
||||
if (command_used == 8) command_expected = (uint16_t)(8 + command_frame[5]);
|
||||
if (command_used == command_expected) {
|
||||
complete_command();
|
||||
command_used = 0;
|
||||
command_expected = 8;
|
||||
}
|
||||
}
|
||||
// A short USB packet/ZLP ends an OUT transfer. Never let a malformed
|
||||
// truncated frame consume a subsequent independent host command.
|
||||
if (length < CFG_TUD_VENDOR_EPSIZE && command_used) {
|
||||
probe_debug_printf("[PROBE] Truncated command discarded: received=%u expected=%u\n",
|
||||
command_used, command_expected);
|
||||
command_used = 0;
|
||||
command_expected = 8;
|
||||
}
|
||||
}
|
||||
|
||||
#ifndef SWITCH_PICO_SWITCH2_USB_BRIDGE
|
||||
static void button_test_task(uint32_t now) {
|
||||
const bool ready = tud_mounted() && !tud_suspended() &&
|
||||
protocol.initialized && (protocol.enabled_features & 1);
|
||||
bool pressed;
|
||||
if (!ready) {
|
||||
pressed = probe_button_update(&button_test, -1, false);
|
||||
last_button_ms = now;
|
||||
} else {
|
||||
if (now - last_button_ms < 10) return;
|
||||
last_button_ms = now;
|
||||
const int sample = bootsel_button_sample();
|
||||
if (sample < 0 && !button_sample_error)
|
||||
probe_debug_printf("[PROBE] BOOTSEL sampling unavailable; test buttons released and disarmed\n");
|
||||
button_sample_error = sample < 0;
|
||||
pressed = probe_button_update(&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(uint32_t now) {
|
||||
if (now - last_controller_poll_ms < 4) return;
|
||||
last_controller_poll_ms = now;
|
||||
probe_controller_input source = {0};
|
||||
probe_controller_input_poll(now, &source);
|
||||
const bool output_active = source.active && tud_mounted() && !tud_suspended();
|
||||
if (protocol.controller_active != output_active)
|
||||
probe_debug_printf("[PROBE] Controller input %s\n", 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));
|
||||
}
|
||||
// Bootstrap with diagnostic reports until the host polls HID. Then consume
|
||||
// complete source packets once, including opaque packed motion samples.
|
||||
native_stream_ready = tud_mounted() && !tud_suspended() && protocol.initialized &&
|
||||
protocol.report_id == 0x08 && hid_completion_seen &&
|
||||
(uint32_t)(now - last_hid_complete_ms) < 500;
|
||||
probe_controller_input_set_native_stream(native_stream_ready);
|
||||
}
|
||||
|
||||
static void gate_native_report(uint8_t input[PROBE_INPUT_SIZE]) {
|
||||
if (!(protocol.enabled_features & 1)) memset(input + 2, 0, 2);
|
||||
if (!(protocol.enabled_features & 2))
|
||||
memcpy(input + 5, protocol.right_stick_center, sizeof(protocol.right_stick_center));
|
||||
if (!(protocol.enabled_features & 0x10)) memset(input + 9, 0, 5);
|
||||
if (!(protocol.enabled_features & 4)) memset(input + 15, 0, 41);
|
||||
}
|
||||
#endif
|
||||
|
||||
static void protocol_task(uint32_t now) {
|
||||
if (!tud_mounted() || tud_suspended()) return;
|
||||
if (reply_count && !reply_inflight) {
|
||||
queued_reply* reply = &replies[reply_head];
|
||||
bool ready = reply->deferred_token == 0;
|
||||
#ifdef SWITCH_PICO_SWITCH2_USB_BRIDGE
|
||||
if (!ready) {
|
||||
const int result = probe_controller_input_sample_result(reply->deferred_token, now);
|
||||
if (result > 0) {
|
||||
probe_debug_printf("[PROBE] Source sample ACK token=%" PRIu64 "\n",
|
||||
reply->deferred_token);
|
||||
reply->deferred_token = 0;
|
||||
ready = true;
|
||||
log_packet("BULK_REPLY_QUEUED", 0, 0, reply->data, reply->length);
|
||||
} else if (result < 0) {
|
||||
probe_debug_printf("[PROBE] Source sample failed or expired token=%" PRIu64
|
||||
"; no USB ACK\n", reply->deferred_token);
|
||||
reply_head = (uint8_t)((reply_head + 1) % REPLY_CAPACITY);
|
||||
--reply_count;
|
||||
++command_drops;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if (ready && tud_vendor_n_write_available(0) >= reply->length) {
|
||||
if (tud_vendor_n_write(0, reply->data, reply->length) == reply->length) {
|
||||
reply_inflight = true;
|
||||
reply_remaining = reply->length;
|
||||
tud_vendor_n_write_flush(0);
|
||||
reply_head = (uint8_t)((reply_head + 1) % REPLY_CAPACITY);
|
||||
--reply_count;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (protocol.initialized && now - last_input_ms >= 4 && tud_hid_n_ready(0)) {
|
||||
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 == 0x08 && protocol.controller_active && native_stream_ready) {
|
||||
native_serial = probe_controller_input_peek_native_report(now, input);
|
||||
if (!native_serial) return; // Never replay a packet's mouse or motion samples.
|
||||
length = sizeof(input);
|
||||
gate_native_report(input);
|
||||
}
|
||||
#endif
|
||||
if (!length) {
|
||||
length = probe_protocol_report(&protocol, protocol.report_id, input, sizeof(input));
|
||||
}
|
||||
if (length && tud_hid_n_report(0, protocol.report_id, input, (uint16_t)length)) {
|
||||
#ifdef SWITCH_PICO_SWITCH2_USB_BRIDGE
|
||||
if (native_serial) {
|
||||
if (!probe_controller_input_commit_native_report(native_serial))
|
||||
probe_debug_printf("[PROBE] Native stream changed during HID submission\n");
|
||||
protocol.report_counter = input[0];
|
||||
}
|
||||
#endif
|
||||
++protocol.report_counter;
|
||||
++input_reports;
|
||||
last_input_ms = now;
|
||||
}
|
||||
}
|
||||
}
|
||||
#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 == 0 && report_type == HID_REPORT_TYPE_INPUT) {
|
||||
probe_controller_input_set_native_stream(false);
|
||||
native_stream_ready = false;
|
||||
hid_completion_seen = false;
|
||||
probe_debug_printf("[PROBE] HID input transfer failed; pending native packets discarded\n");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
void tud_hid_report_complete_cb(uint8_t instance, const uint8_t* report, uint16_t length) {
|
||||
#ifdef SWITCH2_PROBE_USB_INIT
|
||||
if (instance != 0 || length != PROBE_INPUT_SIZE + 1) return;
|
||||
uint8_t rails;
|
||||
if (report[0] == 0x08) rails = (report[4] >> 6) & 3;
|
||||
else if (report[0] == 0x05) rails = (report[5] >> 4) & 3;
|
||||
else return;
|
||||
#ifdef SWITCH_PICO_SWITCH2_USB_BRIDGE
|
||||
last_hid_complete_ms = to_ms_since_boot(get_absolute_time());
|
||||
hid_completion_seen = true;
|
||||
if (report[0] == 0x08) {
|
||||
const int32_t dx = signed_mouse_delta(report + 10);
|
||||
const int32_t dy = signed_mouse_delta(report + 12);
|
||||
if (dx || dy) {
|
||||
++mouse_delivered_reports;
|
||||
mouse_delivered_x += dx;
|
||||
mouse_delivered_y += dy;
|
||||
}
|
||||
}
|
||||
const uint16_t buttons = report[0] == 0x08 ?
|
||||
(uint16_t)(report[3] | ((uint16_t)report[4] << 8)) :
|
||||
(uint16_t)(report[5] | ((uint16_t)report[6] << 8));
|
||||
if (buttons != last_delivered_buttons) {
|
||||
last_delivered_buttons = buttons;
|
||||
probe_debug_printf("[PROBE] CONTROLLER_REPORT delivered id=%02x buttons=%04x\n", report[0], buttons);
|
||||
}
|
||||
#endif
|
||||
if (rails != last_delivered_rails) {
|
||||
last_delivered_rails = rails;
|
||||
probe_debug_printf("[PROBE] TEST_REPORT delivered id=%02x SL=%u SR=%u\n",
|
||||
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;
|
||||
#ifdef SWITCH2_PROBE_USB_INIT
|
||||
if (instance == 0) consume_bulk_packet(buffer, length);
|
||||
#else
|
||||
log_packet("BULK_OUT", instance, 0, buffer, length);
|
||||
#endif
|
||||
// Drain TinyUSB's receive FIFO; raw packet data above is consumed once.
|
||||
uint8_t discarded[64];
|
||||
while (tud_vendor_n_available(instance)) {
|
||||
if (!tud_vendor_n_read(instance, discarded, sizeof(discarded))) break;
|
||||
}
|
||||
}
|
||||
|
||||
void tud_vendor_tx_cb(uint8_t instance, uint32_t length) {
|
||||
#ifdef SWITCH2_PROBE_USB_INIT
|
||||
if (instance == 0 && reply_inflight) {
|
||||
if (length <= reply_remaining) {
|
||||
reply_remaining -= (uint16_t)length;
|
||||
// TinyUSB calls this per packet. Exact packet multiples finish only
|
||||
// after its automatic ZLP, not after the last full-size packet.
|
||||
if (reply_remaining == 0 && length < CFG_TUD_VENDOR_EPSIZE)
|
||||
reply_inflight = false;
|
||||
} else {
|
||||
probe_debug_printf("[PROBE] Unexpected bulk completion; pending=%u\n", 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));
|
||||
#ifdef SWITCH2_PROBE_IDENTITY_REPLY
|
||||
if (request->bmRequestType == 0xc0 && request->bRequest == 0x03 &&
|
||||
request->wValue == 0 && request->wIndex == 0) {
|
||||
if (stage == CONTROL_STAGE_SETUP) {
|
||||
++identity_requests;
|
||||
log_packet("IDENTITY_REPLY", 0, 3, probe_identity_reply, sizeof(probe_identity_reply));
|
||||
return tud_control_xfer(rhport, request, (void*)probe_identity_reply,
|
||||
sizeof(probe_identity_reply));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
#ifdef SWITCH2_PROBE_VERSION_REPLY
|
||||
if (request->bmRequestType == 0xc0 && request->bRequest == 0x02 &&
|
||||
request->wValue == 0 && request->wIndex == 0) {
|
||||
if (stage == CONTROL_STAGE_SETUP) {
|
||||
++version_requests;
|
||||
log_packet("VERSION_REPLY", 0, 2, probe_version_reply, sizeof(probe_version_reply));
|
||||
return tud_control_xfer(rhport, request, (void*)probe_version_reply,
|
||||
sizeof(probe_version_reply));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
#ifdef SWITCH2_PROBE_ACK_SETUP04
|
||||
// Exact control-transfer contract observed on the console and on two
|
||||
// genuine controllers. The meaning of 0x0276 is unresolved: do not infer
|
||||
// UART baud, USB speed, or any flash operation from it.
|
||||
if (request->bmRequestType == 0x40 && request->bRequest == 0x04 &&
|
||||
request->wValue == 0x0276 && request->wIndex == 0 && request->wLength == 0) {
|
||||
if (stage == CONTROL_STAGE_SETUP)
|
||||
return tud_control_status(rhport, request);
|
||||
if (stage == CONTROL_STAGE_ACK) {
|
||||
++setup_completions;
|
||||
probe_debug_printf("[PROBE] SETUP04 acknowledged value=%04x (parameter semantics unresolved)\n",
|
||||
request->wValue);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
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
|
||||
#ifndef SWITCH_PICO_SWITCH2_USB_BRIDGE
|
||||
(void)probe_button_update(&button_test, -1, false);
|
||||
#endif
|
||||
protocol.test_rail_buttons = false;
|
||||
protocol.controller_active = false;
|
||||
#ifdef SWITCH_PICO_SWITCH2_USB_BRIDGE
|
||||
probe_controller_input_set_native_stream(false);
|
||||
native_stream_ready = false;
|
||||
hid_completion_seen = false;
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
void tud_resume_cb(void) { probe_debug_printf("[PROBE] RESUME\n"); }
|
||||
|
||||
int main(void) {
|
||||
#ifdef SWITCH_PICO_SWITCH2_USB_BRIDGE
|
||||
probe_controller_input_clock_init();
|
||||
#endif
|
||||
stdio_init_all();
|
||||
#ifdef SWITCH_PICO_SWITCH2_USB_BRIDGE
|
||||
probe_debug_printf("\n[PROBE] Joy-Con 2 (R) Bluetooth-to-USB controller/native mouse bridge\n");
|
||||
#else
|
||||
probe_debug_printf("\n[PROBE] Joy-Con 2 (R) USB enumeration recorder\n");
|
||||
#endif
|
||||
#ifdef SWITCH_PICO_SWITCH2_USB_BRIDGE
|
||||
probe_controller_input_init();
|
||||
probe_debug_printf("[PROBE] UART0 GP0=TX, 115200 8N1; selected right Joy-Con Bluetooth source enabled\n");
|
||||
#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();
|
||||
probe_debug_printf("[PROBE] Own virtual pairing storage offset=%08" PRIx32 "\n",
|
||||
probe_storage_offset());
|
||||
#ifdef SWITCH_PICO_SWITCH2_USB_BRIDGE
|
||||
probe_debug_printf("[PROBE] Live right Joy-Con buttons/stick/native mouse; hold BOOTSEL 2s for Bluetooth pairing (never clears pairings)\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 could not establish multicore flash coordination");
|
||||
#endif
|
||||
tud_init(0);
|
||||
uint32_t last_heartbeat = 0;
|
||||
while (true) {
|
||||
tud_task();
|
||||
drain_log();
|
||||
const uint32_t now = to_ms_since_boot(get_absolute_time());
|
||||
#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 right Joy-Con in SYNC pairing mode\n");
|
||||
controller_input_task(now);
|
||||
#else
|
||||
button_test_task(now);
|
||||
#endif
|
||||
protocol_task(now);
|
||||
#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, tud_mounted(), bulk_packets, hid_packets,
|
||||
identity_requests, version_requests, setup_completions,
|
||||
input_reports, command_drops, log_dropped);
|
||||
#ifdef SWITCH_PICO_SWITCH2_USB_BRIDGE
|
||||
if (mouse_delivered_reports != mouse_logged_reports) {
|
||||
mouse_logged_reports = mouse_delivered_reports;
|
||||
probe_debug_printf("[PROBE] MOUSE_REPORT delivered packets=%" PRIu32
|
||||
" total_x=%" PRId64 " total_y=%" PRId64 "\n",
|
||||
mouse_delivered_reports, mouse_delivered_x, mouse_delivered_y);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
sleep_us(100);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue