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
15
tools/switch2_usb_probe/CMakeLists.txt
Normal file
15
tools/switch2_usb_probe/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
cmake_minimum_required(VERSION 3.13)
|
||||
set(PICO_BOARD pico2_w CACHE STRING "Target board")
|
||||
include(${CMAKE_CURRENT_LIST_DIR}/../../pico_sdk_import.cmake)
|
||||
project(switch2_usb_probe C CXX ASM)
|
||||
set(CMAKE_C_STANDARD 11)
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
include(${CMAKE_CURRENT_LIST_DIR}/probe_build.cmake)
|
||||
pico_sdk_init()
|
||||
add_executable(switch2-usb-probe
|
||||
../../src/firmware/configuration/configuration_storage.cpp
|
||||
../../src/firmware/platform/pico/bootsel_pairing_button.cpp)
|
||||
target_compile_definitions(switch2-usb-probe PRIVATE
|
||||
PICO_FLASH_ASSUME_CORE1_SAFE=1 PICO_FLASH_ASSERT_ON_UNSAFE=0)
|
||||
switch2_usb_probe_configure(switch2-usb-probe)
|
||||
pico_add_extra_outputs(switch2-usb-probe)
|
||||
17
tools/switch2_usb_probe/button_test.c
Normal file
17
tools/switch2_usb_probe/button_test.c
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
#include "button_test.h"
|
||||
|
||||
bool probe_button_update(probe_button_state* state, int sample, bool ready) {
|
||||
if (!ready || (sample != 0 && sample != 1)) {
|
||||
state->armed = false;
|
||||
state->pressed_samples = 0;
|
||||
return false;
|
||||
}
|
||||
if (sample == 0) {
|
||||
state->armed = true;
|
||||
state->pressed_samples = 0;
|
||||
return false;
|
||||
}
|
||||
if (!state->armed) return false;
|
||||
if (state->pressed_samples < 2) ++state->pressed_samples;
|
||||
return state->pressed_samples == 2;
|
||||
}
|
||||
12
tools/switch2_usb_probe/button_test.h
Normal file
12
tools/switch2_usb_probe/button_test.h
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
#pragma once
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
typedef struct {
|
||||
bool armed;
|
||||
uint8_t pressed_samples;
|
||||
} probe_button_state;
|
||||
|
||||
// Sample at 10 ms intervals. Require a release after reset/unavailability,
|
||||
// then two pressed samples. Release/error/not-ready clears output immediately.
|
||||
bool probe_button_update(probe_button_state* state, int sample, bool ready);
|
||||
149
tools/switch2_usb_probe/controller_input.cpp
Normal file
149
tools/switch2_usb_probe/controller_input.cpp
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
#include "controller_input.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include "input/bluepad32_input_backend.h"
|
||||
#include "input/switch2_mouse_capture.h"
|
||||
#include "platform/pico/bootsel_pairing_button.h"
|
||||
#include "platform/pico/system_clock.h"
|
||||
#include "profile/controller_profile_runtime.h"
|
||||
#include "pico/stdlib.h"
|
||||
|
||||
#if !SWITCH_PICO_SWITCH2_USB_BRIDGE || !SWITCH_PICO_BLUEPAD32 || \
|
||||
!SWITCH_PICO_ENABLE_BLE || !SWITCH_PICO_SWITCH2_MOUSE_CAPTURE || \
|
||||
!SWITCH_PICO_SWITCH2_MOUSE_CAPTURE_NATIVE
|
||||
#error "The controller bridge requires Bluepad32 BLE and native Switch 2 capture"
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
constexpr uint8_t kSourceAddress[] = {SWITCH2_BRIDGE_SOURCE_ADDRESS_BYTES};
|
||||
static_assert(sizeof(kSourceAddress) == 6, "Select one physical Bluetooth address");
|
||||
constexpr uint32_t kInputDeadlineMs = 500;
|
||||
constexpr uint32_t kFlashCoordinationTimeoutMs = 1000;
|
||||
// The backend publishes stage 2 only after Core 1's flash-safe registration;
|
||||
// reaching Core 1 already required successful Core 0 registration in start().
|
||||
constexpr uint32_t kFlashCoordinationStage = 2;
|
||||
bool g_initialized;
|
||||
bool g_start_attempted;
|
||||
bool g_flash_ready;
|
||||
probe_controller_input g_input;
|
||||
uint32_t g_received_ms;
|
||||
} // namespace
|
||||
|
||||
extern "C" void probe_controller_input_clock_init(void) {
|
||||
system_clock_initialize();
|
||||
}
|
||||
|
||||
extern "C" void probe_controller_input_init(void) {
|
||||
if (g_initialized) return;
|
||||
switch2_mouse_capture_init();
|
||||
switch2_mouse_capture_select_input(kSourceAddress);
|
||||
// Prepare the existing storage services without initializing legacy USB.
|
||||
// Core 1 loads their persisted state during the normal backend startup.
|
||||
bluepad32_input_backend_init();
|
||||
controller_profile_runtime_reset();
|
||||
g_initialized = true;
|
||||
}
|
||||
|
||||
extern "C" bool probe_controller_input_start(void) {
|
||||
if (!g_initialized) probe_controller_input_init();
|
||||
if (g_start_attempted) return g_flash_ready;
|
||||
g_start_attempted = true;
|
||||
bluepad32_input_backend_start();
|
||||
const absolute_time_t deadline = make_timeout_time_ms(kFlashCoordinationTimeoutMs);
|
||||
do {
|
||||
Bluepad32BackendDiagnostics diagnostics;
|
||||
bluepad32_input_backend_diagnostics(&diagnostics);
|
||||
if (diagnostics.initialization_stage >= kFlashCoordinationStage) {
|
||||
g_flash_ready = true;
|
||||
return true;
|
||||
}
|
||||
sleep_ms(1);
|
||||
} while (!time_reached(deadline));
|
||||
// Do not reset Core 1 or retry a partially launched backend. It may still
|
||||
// be running; a false return keeps USB and its flash writes fail-closed.
|
||||
return false;
|
||||
}
|
||||
|
||||
extern "C" bool probe_controller_input_pairing_task(void) {
|
||||
if (!g_flash_ready) return false;
|
||||
// Use the shared sampler/hold policy, but deliberately do not route its
|
||||
// kClearPairings event to recovery or any storage-clearing operation.
|
||||
if (bootsel_pairing_button_task() != BootselPairingButtonEvent::kOpenPairing)
|
||||
return false;
|
||||
bluepad32_input_backend_open_pairing_window();
|
||||
return true;
|
||||
}
|
||||
|
||||
extern "C" void probe_controller_input_set_native_stream(bool enabled) {
|
||||
switch2_mouse_capture_set_native_stream(g_flash_ready && enabled);
|
||||
}
|
||||
|
||||
extern "C" uint32_t probe_controller_input_peek_native_report(
|
||||
uint32_t now_ms, uint8_t report[63]) {
|
||||
if (!g_flash_ready) return 0;
|
||||
return switch2_mouse_capture_peek_native_report(now_ms, report);
|
||||
}
|
||||
|
||||
extern "C" bool probe_controller_input_commit_native_report(uint32_t serial) {
|
||||
if (!g_flash_ready) return false;
|
||||
return switch2_mouse_capture_commit_native_report(serial);
|
||||
}
|
||||
|
||||
extern "C" bool probe_controller_input_play_sample(uint8_t sample_id, uint64_t* token) {
|
||||
if (!g_flash_ready) {
|
||||
if (token != nullptr) *token = 0;
|
||||
return false;
|
||||
}
|
||||
return switch2_mouse_capture_request_sample(
|
||||
sample_id, to_ms_since_boot(get_absolute_time()), token);
|
||||
}
|
||||
|
||||
extern "C" int probe_controller_input_sample_result(uint64_t token, uint32_t now_ms) {
|
||||
if (!g_flash_ready) return -1;
|
||||
return switch2_mouse_capture_sample_result(token, now_ms);
|
||||
}
|
||||
|
||||
extern "C" void probe_controller_input_cancel_sample(void) {
|
||||
switch2_mouse_capture_cancel_sample();
|
||||
}
|
||||
|
||||
extern "C" void probe_controller_input_poll(uint32_t now_ms,
|
||||
probe_controller_input* out) {
|
||||
if (out == nullptr) return;
|
||||
if (!g_flash_ready) {
|
||||
*out = {};
|
||||
return;
|
||||
}
|
||||
Switch2MouseCaptureInput sample;
|
||||
if (switch2_mouse_capture_latest_input(g_input.serial, &sample)) {
|
||||
g_input.serial = sample.serial;
|
||||
g_input.active = sample.active;
|
||||
g_received_ms = sample.received_ms;
|
||||
// Preserve physical byte meaning: never route through the generic
|
||||
// solo Joy-Con rotation/mapping. Teardown fields are already zeroed.
|
||||
memcpy(g_input.buttons, sample.buttons, sizeof(g_input.buttons));
|
||||
memcpy(g_input.stick, sample.stick, sizeof(g_input.stick));
|
||||
g_input.native_status = sample.native_status;
|
||||
g_input.mouse_epoch = sample.mouse_epoch;
|
||||
g_input.mouse_total_x = sample.mouse_total_x;
|
||||
g_input.mouse_total_y = sample.mouse_total_y;
|
||||
g_input.mouse_surface = sample.mouse_surface;
|
||||
}
|
||||
// The producer can be a millisecond ahead of the caller's pre-poll clock.
|
||||
// Signed elapsed time tolerates that race and ordinary uint32_t rollover.
|
||||
// Expiration latches inactive until a newer capture serial arrives.
|
||||
if (g_input.active &&
|
||||
static_cast<int32_t>(now_ms - g_received_ms) >=
|
||||
static_cast<int32_t>(kInputDeadlineMs)) {
|
||||
g_input.active = false;
|
||||
memset(g_input.buttons, 0, sizeof(g_input.buttons));
|
||||
memset(g_input.stick, 0, sizeof(g_input.stick));
|
||||
g_input.native_status = 0;
|
||||
g_input.mouse_epoch = 0;
|
||||
g_input.mouse_total_x = 0;
|
||||
g_input.mouse_total_y = 0;
|
||||
g_input.mouse_surface = 0;
|
||||
}
|
||||
*out = g_input;
|
||||
}
|
||||
67
tools/switch2_usb_probe/controller_input.h
Normal file
67
tools/switch2_usb_probe/controller_input.h
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct {
|
||||
bool active;
|
||||
uint32_t serial;
|
||||
uint8_t buttons[2];
|
||||
uint8_t stick[3];
|
||||
// Latest opaque native 08 byte 8.
|
||||
uint8_t native_status;
|
||||
// Cumulative signed relative totals within mouse_epoch, not per-poll
|
||||
// deltas. Cached polls repeat these totals without consuming motion.
|
||||
// Epoch changes on reconnect, including a teardown missed between polls.
|
||||
uint32_t mouse_epoch;
|
||||
int64_t mouse_total_x;
|
||||
int64_t mouse_total_y;
|
||||
// Latest opaque native 08 byte 13.
|
||||
uint8_t mouse_surface;
|
||||
} probe_controller_input;
|
||||
|
||||
// Core 0, before stdio/peripheral initialization.
|
||||
void probe_controller_input_clock_init(void);
|
||||
// Core 0, after stdio and before protocol reset or USB startup.
|
||||
void probe_controller_input_init(void);
|
||||
// True means both cores are registered for flash coordination, not that the
|
||||
// radio is ready or a controller is connected. Failure is latched: keep USB
|
||||
// and flash-writing protocol operations disabled rather than retrying startup.
|
||||
bool probe_controller_input_start(void);
|
||||
// Core 0 after start(): polls the existing two-second BOOTSEL hold gesture.
|
||||
// True means a Bluetooth pairing-window request was queued. Long holds NEVER
|
||||
// clear pairings in this bridge, and this does not inject USB controller input.
|
||||
bool probe_controller_input_pairing_task(void);
|
||||
// Core 0 native 08 relay: disabled until explicitly enabled after flash-ready
|
||||
// startup. Disable clears queued data, repeated enable preserves it. Resets
|
||||
// must disable the stream; this never changes Bluetooth bonds or pairing.
|
||||
// Only subsequent selected-source packets enter the separate 32-entry FIFO.
|
||||
// Overflow drops queued history and retains only the arriving packet.
|
||||
void probe_controller_input_set_native_stream(bool enabled);
|
||||
// Copy a full opaque 63-byte payload (without report ID), oldest first. Returns
|
||||
// its never-reused boot-lifetime serial; 0 leaves report untouched. Latest source
|
||||
// or head >=500 ms old discards the FIFO; now_ms is the Pico boot-ms clock.
|
||||
// Nondestructive until successful HID submission followed by commit.
|
||||
uint32_t probe_controller_input_peek_native_report(uint32_t now_ms, uint8_t report[63]);
|
||||
// Remove only the exact current head once. A stale/replaced token cannot pop a
|
||||
// new stream's packet. Before flash-ready startup peek/commit return 0/false.
|
||||
bool probe_controller_input_commit_native_report(uint32_t serial);
|
||||
// Built-in vibration samples only; raw HD-rumble output is not forwarded.
|
||||
// A nonzero token means queued, not acknowledged. Result: 0 pending, 1 real
|
||||
// source ACK, -1 failed/stale. Reset cancels the request, never stored pairing.
|
||||
bool probe_controller_input_play_sample(uint8_t sample_id, uint64_t* token);
|
||||
int probe_controller_input_sample_result(uint64_t token, uint32_t now_ms);
|
||||
void probe_controller_input_cancel_sample(void);
|
||||
// Core 0 at 250 Hz; now_ms uses the Pico boot-millisecond clock. Only fresh
|
||||
// native 08 buttons/stick and cumulative mouse totals are exposed. Inactive
|
||||
// fields are zero except serial; the USB protocol must supply its calibrated
|
||||
// stick center rather than forwarding inactive stick bytes.
|
||||
void probe_controller_input_poll(uint32_t now_ms, probe_controller_input* out);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
33
tools/switch2_usb_probe/descriptors.h
Normal file
33
tools/switch2_usb_probe/descriptors.h
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
// Published Joy-Con 2 (R) USB descriptors, reproduced for enumeration capture.
|
||||
// https://github.com/ndeadly/switch2_controller_research/blob/master/descriptors.md
|
||||
|
||||
static const uint8_t probe_device_descriptor[] = {
|
||||
0x12, 0x01, 0x00, 0x02, 0xef, 0x02, 0x01, 0x40, 0x7e, 0x05, 0x66, 0x20,
|
||||
0x00, 0x01, 0x01, 0x02, 0x03, 0x01,
|
||||
};
|
||||
|
||||
static const uint8_t probe_configuration_descriptor[] = {
|
||||
0x09, 0x02, 0x50, 0x00, 0x02, 0x01, 0x04, 0xc0, 0xfa, 0x08, 0x0b, 0x00,
|
||||
0x01, 0x03, 0x00, 0x00, 0x00, 0x09, 0x04, 0x00, 0x00, 0x02, 0x03, 0x00,
|
||||
0x00, 0x05, 0x09, 0x21, 0x11, 0x01, 0x00, 0x01, 0x22, 0x64, 0x00, 0x07,
|
||||
0x05, 0x81, 0x03, 0x40, 0x00, 0x04, 0x07, 0x05, 0x01, 0x03, 0x40, 0x00,
|
||||
0x04, 0x08, 0x0b, 0x01, 0x01, 0xff, 0x00, 0x00, 0x00, 0x09, 0x04, 0x01,
|
||||
0x00, 0x02, 0xff, 0x00, 0x00, 0x06, 0x07, 0x05, 0x02, 0x02, 0x40, 0x00,
|
||||
0x00, 0x07, 0x05, 0x82, 0x02, 0x40, 0x00, 0x00,
|
||||
};
|
||||
|
||||
static const uint8_t probe_hid_report_descriptor[] = {
|
||||
0x05, 0x01, 0x09, 0x05, 0xa1, 0x01, 0x85, 0x05, 0x05, 0xff, 0x09, 0x01,
|
||||
0x15, 0x00, 0x26, 0xff, 0x00, 0x95, 0x3f, 0x75, 0x08, 0x81, 0x02, 0x85,
|
||||
0x08, 0x09, 0x01, 0x95, 0x02, 0x81, 0x02, 0x05, 0x09, 0x19, 0x01, 0x29,
|
||||
0x10, 0x25, 0x01, 0x95, 0x10, 0x75, 0x01, 0x81, 0x02, 0x05, 0xff, 0x09,
|
||||
0x01, 0x26, 0xff, 0x00, 0x95, 0x01, 0x75, 0x08, 0x81, 0x02, 0x05, 0x01,
|
||||
0x09, 0x01, 0xa1, 0x00, 0x09, 0x30, 0x09, 0x31, 0x26, 0xff, 0x0f, 0x95,
|
||||
0x02, 0x75, 0x0c, 0x81, 0x02, 0xc0, 0x05, 0xff, 0x09, 0x02, 0x26, 0xff,
|
||||
0x00, 0x95, 0x37, 0x75, 0x08, 0x81, 0x02, 0x85, 0x01, 0x09, 0x01, 0x95,
|
||||
0x3f, 0x91, 0x02, 0xc0,
|
||||
};
|
||||
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);
|
||||
}
|
||||
}
|
||||
5
tools/switch2_usb_probe/mbedtls_config.h
Normal file
5
tools/switch2_usb_probe/mbedtls_config.h
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
#pragma once
|
||||
|
||||
// Pairing uses one AES-128 ECB block; no TLS, entropy service, or heap-backed cipher API.
|
||||
#define MBEDTLS_AES_C
|
||||
#define MBEDTLS_AES_ROM_TABLES
|
||||
57
tools/switch2_usb_probe/memory.c
Normal file
57
tools/switch2_usb_probe/memory.c
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
#include "memory.h"
|
||||
#include "probe_memory_data.h"
|
||||
#include <string.h>
|
||||
|
||||
_Static_assert(sizeof(probe_factory_memory) == 8192, "factory capture size");
|
||||
_Static_assert(sizeof(probe_user_calibration) == 4096, "user calibration capture size");
|
||||
|
||||
bool probe_memory_read(uint32_t address, uint8_t* output, size_t length) {
|
||||
if (!output) return false;
|
||||
const uint8_t* source;
|
||||
size_t offset, available;
|
||||
if (address >= 0x13000 && address < 0x15000) {
|
||||
offset = address - 0x13000;
|
||||
source = probe_factory_memory;
|
||||
available = sizeof(probe_factory_memory) - offset;
|
||||
} else if (address >= 0x1fc000 && address < 0x1fd000) {
|
||||
offset = address - 0x1fc000;
|
||||
source = probe_user_calibration;
|
||||
available = sizeof(probe_user_calibration) - offset;
|
||||
} else {
|
||||
return false; // No fabricated erased bytes, pairing keys, or firmware reads.
|
||||
}
|
||||
if (length > available) return false;
|
||||
memcpy(output, source + offset, length);
|
||||
return true;
|
||||
}
|
||||
|
||||
static void unpack_pair(const uint8_t* data, uint16_t values[2]) {
|
||||
values[0] = data[0] | ((uint16_t)(data[1] & 15) << 8);
|
||||
values[1] = (data[1] >> 4) | ((uint16_t)data[2] << 4);
|
||||
}
|
||||
|
||||
static bool valid_calibration(const uint8_t* data) {
|
||||
uint16_t center[2], positive[2], negative[2];
|
||||
unpack_pair(data, center);
|
||||
unpack_pair(data + 3, positive);
|
||||
unpack_pair(data + 6, negative);
|
||||
// Same bounds as the existing Bluepad32 Switch 2 calibration decoder.
|
||||
for (unsigned i = 0; i < 2; ++i) {
|
||||
if (!center[i] || center[i] == 4095 || !positive[i] || !negative[i] ||
|
||||
positive[i] > 4095 - center[i] || negative[i] > center[i]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool probe_memory_right_stick_center(uint8_t output[3]) {
|
||||
if (!output) return false;
|
||||
// A solo Joy-Con uses the primary calibration record, even for the right
|
||||
// controller. User magic precedes its 9-byte record; factory has no magic.
|
||||
const uint8_t* selected = probe_factory_memory + 0xa8;
|
||||
const uint8_t* user = probe_user_calibration + 0x40;
|
||||
if (user[0] == 0xb2 && user[1] == 0xa1 && valid_calibration(user + 2))
|
||||
selected = user + 2;
|
||||
if (!valid_calibration(selected)) return false;
|
||||
memcpy(output, selected, 3);
|
||||
return true;
|
||||
}
|
||||
7
tools/switch2_usb_probe/memory.h
Normal file
7
tools/switch2_usb_probe/memory.h
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
#pragma once
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
bool probe_memory_read(uint32_t address, uint8_t* output, size_t length);
|
||||
bool probe_memory_right_stick_center(uint8_t output[3]);
|
||||
139
tools/switch2_usb_probe/probe_build.cmake
Normal file
139
tools/switch2_usb_probe/probe_build.cmake
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
# Shared by the standalone USB probe and the Bluetooth-backed root target.
|
||||
# Include before pico_sdk_init() so every mbedTLS source uses the same config.
|
||||
set(SWITCH2_USB_PROBE_DIR "${CMAKE_CURRENT_LIST_DIR}")
|
||||
set(PICO_MBEDTLS_CONFIG_FILE "${SWITCH2_USB_PROBE_DIR}/mbedtls_config.h")
|
||||
|
||||
function(switch2_usb_probe_configure target)
|
||||
set(probe_sources
|
||||
${SWITCH2_USB_PROBE_DIR}/main.c
|
||||
${SWITCH2_USB_PROBE_DIR}/protocol.c
|
||||
${SWITCH2_USB_PROBE_DIR}/storage.cpp
|
||||
${SWITCH2_USB_PROBE_DIR}/button_test.c)
|
||||
target_compile_features(${target} PRIVATE c_std_11 cxx_std_17)
|
||||
target_include_directories(${target} PRIVATE
|
||||
${SWITCH2_USB_PROBE_DIR}
|
||||
${SWITCH2_USB_PROBE_DIR}/../../src/firmware
|
||||
${CMAKE_CURRENT_BINARY_DIR}
|
||||
${PICO_SDK_PATH}/src/rp2_common/pico_btstack/include
|
||||
${PICO_SDK_PATH}/lib/btstack/platform/embedded)
|
||||
target_compile_definitions(${target} PRIVATE
|
||||
CFG_TUSB_CONFIG_FILE="${SWITCH2_USB_PROBE_DIR}/tusb_config.h")
|
||||
|
||||
set(SWITCH2_PROBE_IDENTITY_FILE "" CACHE FILEPATH "64-byte Joy-Con 2 (R) factory-format identity block")
|
||||
if(SWITCH2_PROBE_IDENTITY_FILE)
|
||||
file(READ "${SWITCH2_PROBE_IDENTITY_FILE}" identity_hex LIMIT 65 HEX)
|
||||
string(LENGTH "${identity_hex}" identity_length)
|
||||
if(NOT identity_length EQUAL 128)
|
||||
message(FATAL_ERROR "Identity capture must contain exactly 64 bytes")
|
||||
endif()
|
||||
string(TOLOWER "${identity_hex}" identity_hex)
|
||||
string(SUBSTRING "${identity_hex}" 36 8 identity_vid_pid)
|
||||
if(NOT identity_vid_pid STREQUAL "7e056620")
|
||||
message(FATAL_ERROR "Identity capture must match Joy-Con 2 (R), 057e:2066")
|
||||
endif()
|
||||
string(REGEX REPLACE "([0-9a-f][0-9a-f])" "0x\\1," identity_bytes "${identity_hex}")
|
||||
file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/probe_identity.h"
|
||||
"// Generated from a private, read-only controller capture; do not commit.\n#include <stdint.h>\nstatic const uint8_t probe_identity_reply[64] = {${identity_bytes}};\n")
|
||||
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${SWITCH2_PROBE_IDENTITY_FILE}")
|
||||
target_compile_definitions(${target} PRIVATE SWITCH2_PROBE_IDENTITY_REPLY=1)
|
||||
endif()
|
||||
set(SWITCH2_PROBE_VERSION_FILE "" CACHE FILEPATH "Captured 12-byte Joy-Con 2 (R) firmware-version reply")
|
||||
set(SWITCH2_PROBE_CONTROLLER_ADDRESS "" CACHE STRING "Advertised controller address (captured or distinct virtual identity)")
|
||||
if(SWITCH2_PROBE_VERSION_FILE)
|
||||
if(NOT SWITCH2_PROBE_IDENTITY_FILE)
|
||||
message(FATAL_ERROR "Version response requires the matching identity capture")
|
||||
endif()
|
||||
file(READ "${SWITCH2_PROBE_VERSION_FILE}" version_hex LIMIT 13 HEX)
|
||||
string(LENGTH "${version_hex}" version_length)
|
||||
if(NOT version_length EQUAL 24)
|
||||
message(FATAL_ERROR "Firmware version capture must contain exactly 12 bytes")
|
||||
endif()
|
||||
string(TOLOWER "${version_hex}" version_hex)
|
||||
string(SUBSTRING "${version_hex}" 6 2 firmware_type)
|
||||
if(NOT firmware_type STREQUAL "01")
|
||||
message(FATAL_ERROR "Firmware version capture must describe Joy-Con 2 (R)")
|
||||
endif()
|
||||
string(REPLACE ":" "" address_hex "${SWITCH2_PROBE_CONTROLLER_ADDRESS}")
|
||||
string(TOLOWER "${address_hex}" address_hex)
|
||||
string(LENGTH "${address_hex}" address_length)
|
||||
if(NOT address_length EQUAL 12 OR NOT address_hex MATCHES "^[0-9a-f]+$")
|
||||
message(FATAL_ERROR "Provide a six-byte advertised controller Bluetooth address")
|
||||
endif()
|
||||
set(address_reversed "")
|
||||
foreach(byte RANGE 0 5)
|
||||
math(EXPR position "10 - 2 * ${byte}")
|
||||
string(SUBSTRING "${address_hex}" ${position} 2 octet)
|
||||
string(APPEND address_reversed "${octet}")
|
||||
endforeach()
|
||||
string(SUBSTRING "${version_hex}" 0 6 main_version)
|
||||
string(SUBSTRING "${version_hex}" 8 6 bluetooth_version)
|
||||
# Layout corroborated against two genuine USB vendor-02 responses and their
|
||||
# matching command-10 version and command-15 address responses.
|
||||
set(status_hex "${main_version}000000${bluetooth_version}00${address_reversed}")
|
||||
string(REGEX REPLACE "([0-9a-f][0-9a-f])" "0x\\1," status_bytes "${status_hex}")
|
||||
string(REGEX REPLACE "([0-9a-f][0-9a-f])" "0x\\1," firmware_bytes "${version_hex}")
|
||||
file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/probe_version.h"
|
||||
"// Generated from private controller captures; do not commit.\n#include <stdint.h>\nstatic const uint8_t probe_version_reply[16] = {${status_bytes}};\nstatic const uint8_t probe_firmware_version[12] = {${firmware_bytes}};\n")
|
||||
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${SWITCH2_PROBE_VERSION_FILE}")
|
||||
target_compile_definitions(${target} PRIVATE SWITCH2_PROBE_VERSION_REPLY=1)
|
||||
endif()
|
||||
option(SWITCH2_PROBE_ACK_SETUP04 "Acknowledge the observed vendor-04 setup transaction" OFF)
|
||||
if(SWITCH2_PROBE_ACK_SETUP04)
|
||||
if(NOT SWITCH2_PROBE_IDENTITY_FILE OR NOT SWITCH2_PROBE_VERSION_FILE)
|
||||
message(FATAL_ERROR "Setup acknowledgement requires both verified controller replies")
|
||||
endif()
|
||||
target_compile_definitions(${target} PRIVATE SWITCH2_PROBE_ACK_SETUP04=1)
|
||||
endif()
|
||||
option(SWITCH2_PROBE_USB_INIT "Implement documented USB init and stream neutral diagnostic reports" OFF)
|
||||
if(SWITCH2_PROBE_USB_INIT)
|
||||
if(NOT SWITCH2_PROBE_ACK_SETUP04)
|
||||
message(FATAL_ERROR "USB initialization probe requires the verified setup acknowledgement")
|
||||
endif()
|
||||
target_compile_definitions(${target} PRIVATE SWITCH2_PROBE_USB_INIT=1)
|
||||
endif()
|
||||
set(SWITCH2_PROBE_FACTORY_FILE "" CACHE FILEPATH "8192-byte captured factory region with configured virtual identity")
|
||||
set(SWITCH2_PROBE_USER_CALIBRATION_FILE "" CACHE FILEPATH "4096-byte captured user calibration region")
|
||||
if(SWITCH2_PROBE_FACTORY_FILE OR SWITCH2_PROBE_USER_CALIBRATION_FILE)
|
||||
if(NOT SWITCH2_PROBE_USB_INIT OR NOT SWITCH2_PROBE_FACTORY_FILE OR NOT SWITCH2_PROBE_USER_CALIBRATION_FILE)
|
||||
message(FATAL_ERROR "Memory replies require initialized USB and both calibration captures")
|
||||
endif()
|
||||
file(READ "${SWITCH2_PROBE_FACTORY_FILE}" factory_hex LIMIT 8193 HEX)
|
||||
file(READ "${SWITCH2_PROBE_USER_CALIBRATION_FILE}" user_calibration_hex LIMIT 4097 HEX)
|
||||
string(LENGTH "${factory_hex}" factory_length)
|
||||
string(LENGTH "${user_calibration_hex}" user_calibration_length)
|
||||
if(NOT factory_length EQUAL 16384 OR NOT user_calibration_length EQUAL 8192)
|
||||
message(FATAL_ERROR "Factory/user captures must contain exactly 8192/4096 bytes")
|
||||
endif()
|
||||
string(SUBSTRING "${factory_hex}" 0 128 factory_identity_hex)
|
||||
if(NOT factory_identity_hex STREQUAL identity_hex)
|
||||
message(FATAL_ERROR "Factory memory identity must match the vendor-control identity")
|
||||
endif()
|
||||
string(REGEX REPLACE "([0-9a-f][0-9a-f])" "0x\\1," factory_bytes "${factory_hex}")
|
||||
string(REGEX REPLACE "([0-9a-f][0-9a-f])" "0x\\1," user_calibration_bytes "${user_calibration_hex}")
|
||||
file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/probe_memory_data.h"
|
||||
"// Generated from private calibration captures; do not commit.\n#include <stdint.h>\nstatic const uint8_t probe_factory_memory[8192] = {${factory_bytes}};\nstatic const uint8_t probe_user_calibration[4096] = {${user_calibration_bytes}};\n")
|
||||
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS
|
||||
"${SWITCH2_PROBE_FACTORY_FILE}" "${SWITCH2_PROBE_USER_CALIBRATION_FILE}")
|
||||
list(APPEND probe_sources ${SWITCH2_USB_PROBE_DIR}/memory.c)
|
||||
target_compile_definitions(${target} PRIVATE SWITCH2_PROBE_MEMORY=1)
|
||||
endif()
|
||||
target_sources(${target} PRIVATE ${probe_sources})
|
||||
set(probe_compile_options -Wall -Wextra -Werror)
|
||||
if(SWITCH_PICO_SWITCH2_USB_BRIDGE)
|
||||
# Preserve the root firmware's warning policy outside the probe sources.
|
||||
set_source_files_properties(${probe_sources} PROPERTIES
|
||||
COMPILE_OPTIONS "${probe_compile_options}")
|
||||
else()
|
||||
target_compile_options(${target} PRIVATE ${probe_compile_options})
|
||||
endif()
|
||||
target_link_libraries(${target} PRIVATE pico_stdlib hardware_uart hardware_sync
|
||||
hardware_flash pico_flash pico_mbedtls_crypto pico_mbedtls_headers tinyusb_device)
|
||||
pico_enable_stdio_usb(${target} 0)
|
||||
pico_enable_stdio_uart(${target} 1)
|
||||
if(SWITCH_PICO_SWITCH2_USB_BRIDGE)
|
||||
pico_set_program_name(${target} "Switch 2 right Joy-Con Bluetooth bridge")
|
||||
else()
|
||||
pico_set_program_name(${target} "Switch 2 USB initialization capture")
|
||||
endif()
|
||||
pico_set_program_version(${target} "0.24")
|
||||
endfunction()
|
||||
374
tools/switch2_usb_probe/protocol.c
Normal file
374
tools/switch2_usb_probe/protocol.c
Normal file
|
|
@ -0,0 +1,374 @@
|
|||
#include "protocol.h"
|
||||
#include "mbedtls/aes.h"
|
||||
#include <string.h>
|
||||
|
||||
// Wire contracts: ndeadly/switch2_controller_research commands.md (03/0D,
|
||||
// 03/0A, 07/01, 09/01-08, 16/01, 15/01-04) and hid_reports.md (05/08). USB reply headers
|
||||
// and status payloads match captures/usb/rumble-procon-gccon.pcapng.gz.
|
||||
// This public component is not a pairing key. The host supplies the other half.
|
||||
static const uint8_t device_key_component[16] = {
|
||||
0x5c, 0xf6, 0xee, 0x79, 0x2c, 0xdf, 0x05, 0xe1,
|
||||
0xba, 0x2b, 0x63, 0x25, 0xc4, 0x1a, 0x5f, 0x10,
|
||||
};
|
||||
|
||||
// Identical 11/03 payload in five genuine Joy-Con BLE captures; also present
|
||||
// in the GameCube USB capture. Full semantics remain undocumented.
|
||||
static const uint8_t joycon_info11_03[] = {
|
||||
0x01, 0x20, 0x03, 0x00, 0x00, 0x0a, 0xe8, 0x1c,
|
||||
0x3b, 0x79, 0x7d, 0x8b, 0x3a, 0x0a, 0xe8, 0x9c,
|
||||
0x42, 0x58, 0xa0, 0x0b, 0x42, 0x0a, 0xe8, 0x9c,
|
||||
0x41, 0x58, 0xa0, 0x0b, 0x41,
|
||||
};
|
||||
|
||||
static void clear_pending_pairing(probe_protocol_state* state) {
|
||||
state->pending_host_count = 0;
|
||||
memset(state->pending_host_addresses, 0, sizeof(state->pending_host_addresses));
|
||||
state->pending_key_valid = false;
|
||||
memset(state->pending_key, 0, sizeof(state->pending_key));
|
||||
state->challenge_confirmed = false;
|
||||
}
|
||||
|
||||
static const uint8_t* pairing_key_for_context(const probe_protocol_state* state) {
|
||||
if (!state->pending_host_count) return NULL;
|
||||
if (state->pending_key_valid) return state->pending_key;
|
||||
// Host addresses are key associations: order and subset size may change,
|
||||
// but no new host may borrow the committed key without its own exchange.
|
||||
for (unsigned i = 0; i < state->pending_host_count; ++i) {
|
||||
bool stored = false;
|
||||
for (unsigned j = 0; j < state->committed_host_count; ++j) {
|
||||
if (memcmp(state->pending_host_addresses[i],
|
||||
state->committed_host_addresses[j], 6) == 0) {
|
||||
stored = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!stored) return NULL;
|
||||
}
|
||||
return state->committed_key;
|
||||
}
|
||||
|
||||
static bool challenge_response(const uint8_t* key, const uint8_t* wire_challenge,
|
||||
uint8_t* response) {
|
||||
uint8_t challenge[16];
|
||||
for (unsigned i = 0; i < sizeof(challenge); ++i) {
|
||||
challenge[i] = wire_challenge[sizeof(challenge) - 1u - i];
|
||||
}
|
||||
mbedtls_aes_context aes;
|
||||
mbedtls_aes_init(&aes);
|
||||
int result = mbedtls_aes_setkey_enc(&aes, key, 128);
|
||||
if (result == 0) {
|
||||
// Genuine challenge capture: reverse key/input, but NOT ciphertext.
|
||||
result = mbedtls_aes_crypt_ecb(&aes, MBEDTLS_AES_ENCRYPT, challenge, response);
|
||||
}
|
||||
mbedtls_aes_free(&aes);
|
||||
return result == 0;
|
||||
}
|
||||
|
||||
static bool finalize_pairing(probe_protocol_state* state, const uint8_t* key) {
|
||||
uint8_t blob[PROBE_PAIRING_BLOB_SIZE] = {0};
|
||||
memcpy(blob, state->controller_address, sizeof(state->controller_address));
|
||||
blob[6] = state->pending_host_count;
|
||||
memcpy(blob + 7, state->pending_host_addresses, 6u * state->pending_host_count);
|
||||
memcpy(blob + sizeof(blob) - 16u, key, 16);
|
||||
// Preserve both the old committed key and pending retry on any save failure.
|
||||
if (!state->save_pairing(blob, sizeof(blob))) return false;
|
||||
state->committed_host_count = blob[6];
|
||||
memcpy(state->committed_host_addresses, blob + 7, sizeof(state->committed_host_addresses));
|
||||
memcpy(state->committed_key, blob + sizeof(blob) - 16u, sizeof(state->committed_key));
|
||||
state->pending_key_valid = false;
|
||||
memset(state->pending_key, 0, sizeof(state->pending_key));
|
||||
// Keep this context confirmed so a repeated finalize can safely retry its ACK.
|
||||
return true;
|
||||
}
|
||||
|
||||
void probe_protocol_reset(probe_protocol_state* state) {
|
||||
memset(state, 0, sizeof(*state));
|
||||
state->report_id = 0x08;
|
||||
state->right_stick_center[1] = 0x08;
|
||||
state->right_stick_center[2] = 0x80;
|
||||
}
|
||||
|
||||
bool probe_protocol_restore_pairing(probe_protocol_state* state,
|
||||
const uint8_t* blob, size_t length) {
|
||||
if (!state || !blob || length != PROBE_PAIRING_BLOB_SIZE ||
|
||||
memcmp(state->controller_address, blob, sizeof(state->controller_address)) != 0 ||
|
||||
blob[6] == 0 || blob[6] > PROBE_HOST_MAX_ADDRESSES) return false;
|
||||
for (size_t i = 7u + 6u * blob[6]; i < length - 16u; ++i) {
|
||||
if (blob[i] != 0) return false;
|
||||
}
|
||||
// Validate the whole record before changing either committed or pending state.
|
||||
state->committed_host_count = blob[6];
|
||||
memcpy(state->committed_host_addresses, blob + 7, sizeof(state->committed_host_addresses));
|
||||
memcpy(state->committed_key, blob + length - 16u, sizeof(state->committed_key));
|
||||
clear_pending_pairing(state);
|
||||
return true;
|
||||
}
|
||||
|
||||
size_t probe_protocol_command(probe_protocol_state* state, const uint8_t* command,
|
||||
size_t length, uint8_t* reply, size_t capacity,
|
||||
uint64_t* deferred_token) {
|
||||
if (deferred_token) *deferred_token = 0;
|
||||
if (!state || !command || !reply || length < 8 ||
|
||||
command[1] != 0x91 || command[2] != 0 ||
|
||||
command[4] != 0 || command[6] != 0 || command[7] != 0 ||
|
||||
length != 8u + command[5]) return 0;
|
||||
const bool initialize = command[0] == 0x03 && command[3] == 0x0d;
|
||||
const bool select_report = command[0] == 0x03 && command[3] == 0x0a;
|
||||
const bool status_query = (command[0] == 0x07 || command[0] == 0x16) && command[3] == 1;
|
||||
const bool exchange_addresses = command[0] == 0x15 && command[3] == 1;
|
||||
const bool confirm_key = command[0] == 0x15 && command[3] == 2;
|
||||
const bool finalize = command[0] == 0x15 && command[3] == 3;
|
||||
const bool exchange_keys = command[0] == 0x15 && command[3] == 4;
|
||||
const bool power07 = command[0] == 0x0b && command[3] == 0x07;
|
||||
const bool player_leds = command[0] == 0x09 && command[3] >= 1 && command[3] <= 8;
|
||||
const bool features = command[0] == 0x0c && command[3] >= 1 && command[3] <= 5;
|
||||
const bool memory_read = command[0] == 0x02 && (command[3] == 1 || command[3] == 4);
|
||||
const bool info11_03 = command[0] == 0x11 && command[3] == 3;
|
||||
const bool info11_01 = command[0] == 0x11 && command[3] == 1;
|
||||
const bool vibration_setup = command[0] == 0x0a && command[3] == 8;
|
||||
const bool vibration_sample = command[0] == 0x0a && command[3] == 2;
|
||||
const bool joycon_query = command[0] == 0x13 && command[3] >= 1 && command[3] <= 3;
|
||||
const bool firmware_info = command[0] == 0x10 && command[3] == 1;
|
||||
const bool nfc_info = command[0] == 0x01 && command[3] == 0x0c;
|
||||
const bool runtime_toggle = command[0] == 0x03 && command[3] == 0x0c;
|
||||
uint32_t memory_address = 0;
|
||||
uint8_t memory_length = 0;
|
||||
size_t reply_length;
|
||||
const uint8_t* pairing_key = NULL;
|
||||
if (initialize) {
|
||||
if (length != 16 || command[8] != 1 || command[9] != 0) return 0;
|
||||
reply_length = 12;
|
||||
} else if (select_report) {
|
||||
if (length != 12) return 0;
|
||||
reply_length = 8;
|
||||
} else if (status_query) {
|
||||
if (length != 8) return 0;
|
||||
reply_length = command[0] == 0x07 ? 9 : 32;
|
||||
} else if (exchange_addresses) {
|
||||
if (length != 8 &&
|
||||
(length < 10 || command[8] != 0 ||
|
||||
command[9] > PROBE_HOST_MAX_ADDRESSES ||
|
||||
length != 10u + 6u * command[9])) return 0;
|
||||
reply_length = 17;
|
||||
} else if (exchange_keys) {
|
||||
if (length != 25 || command[8] != 0 || !state->pending_host_count) return 0;
|
||||
reply_length = 25;
|
||||
} else if (confirm_key) {
|
||||
if (length != 25 || command[8] != 0) return 0;
|
||||
pairing_key = pairing_key_for_context(state);
|
||||
if (!pairing_key) return 0;
|
||||
reply_length = 25;
|
||||
} else if (finalize) {
|
||||
if (length != 9 || command[8] != 0 || !state->challenge_confirmed ||
|
||||
!state->save_pairing) return 0;
|
||||
pairing_key = pairing_key_for_context(state);
|
||||
if (!pairing_key) return 0;
|
||||
reply_length = 9;
|
||||
} else if (power07) {
|
||||
// Donor BLE capture: 0b 01 01 07 10 78 00 00. Apply the previously
|
||||
// corroborated USB header mapping. Semantics remain unknown: only the
|
||||
// console's observed four-zero argument has been queried on the donor.
|
||||
if (length != 12 || command[8] || command[9] || command[10] || command[11]) return 0;
|
||||
reply_length = 8;
|
||||
} else if (player_leds) {
|
||||
if (command[3] <= 6) {
|
||||
if (length != 8) return 0;
|
||||
} else {
|
||||
// Current console sends four payload bytes; published captures use
|
||||
// eight. Only the first byte carries the mask/flashing setting.
|
||||
if (length != 12 && length != 16) return 0;
|
||||
if (command[3] == 8 && command[8] > 1) return 0;
|
||||
}
|
||||
reply_length = 8;
|
||||
} else if (features) {
|
||||
if (length != 12) return 0;
|
||||
reply_length = command[3] == 1 ? 20 : 12;
|
||||
} else if (memory_read) {
|
||||
if (length != 16 || !state->read_memory || command[10] || command[11]) return 0;
|
||||
if (command[3] == 1) {
|
||||
if (command[8] || command[9]) return 0;
|
||||
memory_length = 64;
|
||||
} else {
|
||||
if (command[9] != 0x7e || command[8] > 80) return 0;
|
||||
memory_length = command[8];
|
||||
}
|
||||
for (unsigned i = 0; i < 4; ++i)
|
||||
memory_address |= (uint32_t)command[12 + i] << (8 * i);
|
||||
reply_length = 16u + memory_length;
|
||||
} else if (info11_03 || info11_01) {
|
||||
if (length != 8) return 0;
|
||||
reply_length = info11_03 ? 8 + sizeof(joycon_info11_03) : 12;
|
||||
} else if (vibration_setup) {
|
||||
// Five genuine Joy-Con traces acknowledge this 20-byte parameter block
|
||||
// with no response payload. Its first byte is consistently 1.
|
||||
if (length != 28 || command[8] != 1) return 0;
|
||||
reply_length = 8;
|
||||
} else if (vibration_sample) {
|
||||
if (length != 12 || command[8] > 7 ||
|
||||
command[9] || command[10] || command[11] ||
|
||||
!state->play_sample || !deferred_token) return 0;
|
||||
reply_length = 8;
|
||||
} else if (joycon_query) {
|
||||
if (length != 8) return 0;
|
||||
reply_length = command[3] == 1 ? 12 : 16;
|
||||
} else if (firmware_info) {
|
||||
if (length != 8 || !state->firmware_version) return 0;
|
||||
reply_length = 20;
|
||||
} else if (nfc_info) {
|
||||
if (length != 8) return 0;
|
||||
reply_length = 12;
|
||||
} else if (runtime_toggle) {
|
||||
if (length != 12 || command[8] > 1 || command[9] || command[10] || command[11]) return 0;
|
||||
reply_length = 8;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
if (capacity < reply_length) return 0;
|
||||
if (vibration_sample) {
|
||||
uint64_t token = 0;
|
||||
if (!state->play_sample(command[8], &token) || !token) return 0;
|
||||
*deferred_token = token;
|
||||
}
|
||||
uint8_t encrypted_challenge[16];
|
||||
if (confirm_key && !challenge_response(pairing_key, command + 9, encrypted_challenge)) return 0;
|
||||
if (finalize && !finalize_pairing(state, pairing_key)) return 0;
|
||||
if (memory_read &&
|
||||
!state->read_memory(memory_address, reply + 16, memory_length)) return 0;
|
||||
const uint8_t header[] = {command[0], 0x01, 0, command[3], 0, 0xf8, 0, 0};
|
||||
memcpy(reply, header, sizeof(header));
|
||||
if (info11_03) {
|
||||
memcpy(reply + 8, joycon_info11_03, sizeof(joycon_info11_03));
|
||||
} else if (firmware_info) {
|
||||
memcpy(reply + 8, state->firmware_version, 12);
|
||||
} else if (nfc_info) {
|
||||
// Identical in five Joy-Con captures; Pro's last byte differs.
|
||||
const uint8_t info[] = {0x61, 0x12, 0x50, 0x0d};
|
||||
memcpy(reply + 8, info, sizeof(info));
|
||||
} else {
|
||||
// Memory payload was supplied directly into the reply; initialize only its metadata.
|
||||
memset(reply + 8, 0, memory_read ? 8 : reply_length - 8);
|
||||
}
|
||||
if (initialize) {
|
||||
memcpy(state->host_address, command + 10, sizeof(state->host_address));
|
||||
state->initialized = true;
|
||||
// Retransmission is idempotent: do not reset an already-running counter.
|
||||
reply[8] = 1;
|
||||
} else if (select_report) {
|
||||
// The real controller acknowledges but ignores unsupported report IDs.
|
||||
if (command[8] == 0x05 || command[8] == 0x08) state->report_id = command[8];
|
||||
} else if (exchange_addresses) {
|
||||
if (length != 8) {
|
||||
clear_pending_pairing(state);
|
||||
state->pending_host_count = command[9];
|
||||
memcpy(state->pending_host_addresses, command + 10, 6u * command[9]);
|
||||
}
|
||||
reply[8] = 1;
|
||||
reply[9] = 4; // Observed address-response field; semantics unresolved.
|
||||
reply[10] = 1;
|
||||
memcpy(reply + 11, state->controller_address, sizeof(state->controller_address));
|
||||
} else if (exchange_keys) {
|
||||
for (unsigned i = 0; i < sizeof(state->pending_key); ++i) {
|
||||
const unsigned wire_index = sizeof(state->pending_key) - 1u - i;
|
||||
state->pending_key[i] = command[9 + wire_index] ^ device_key_component[wire_index];
|
||||
}
|
||||
state->pending_key_valid = true;
|
||||
state->challenge_confirmed = false;
|
||||
reply[8] = 1;
|
||||
memcpy(reply + 9, device_key_component, sizeof(device_key_component));
|
||||
} else if (confirm_key) {
|
||||
reply[8] = 1;
|
||||
memcpy(reply + 9, encrypted_challenge, sizeof(encrypted_challenge));
|
||||
state->challenge_confirmed = true;
|
||||
} else if (finalize) {
|
||||
reply[8] = 1;
|
||||
} else if (player_leds) {
|
||||
if (command[3] <= 4) {
|
||||
state->player_leds = (uint8_t)(1u << (command[3] - 1));
|
||||
} else if (command[3] == 5) {
|
||||
state->player_leds = 0x0f;
|
||||
} else if (command[3] == 6) {
|
||||
state->player_leds = 0;
|
||||
} else if (command[3] == 7) {
|
||||
state->player_leds = command[8] & 0x0f;
|
||||
} else {
|
||||
state->player_leds_flashing = command[8] != 0;
|
||||
}
|
||||
} else if (features) {
|
||||
const uint8_t flags = command[8] & 0xb7; // Bits 3 and 6 are unused.
|
||||
if (command[3] == 1) {
|
||||
// Published Joy-Con-specific feature-info encoding; first four
|
||||
// response bytes and final two feature-info bytes remain zero.
|
||||
reply[12] = flags & 0x01 ? 7 : 0;
|
||||
reply[13] = flags & 0x02 ? 7 : 0;
|
||||
reply[14] = flags & 0x04 ? 3 : 0;
|
||||
reply[15] = flags & 0x80 ? 3 : 0;
|
||||
reply[16] = flags & 0x10 ? 3 : 0;
|
||||
reply[17] = flags & 0x20 ? 3 : 0;
|
||||
} else if (command[3] == 2) {
|
||||
state->feature_mask = flags;
|
||||
state->enabled_features &= flags;
|
||||
} else if (command[3] == 3) {
|
||||
state->feature_mask = state->enabled_features = 0;
|
||||
} else if (command[3] == 4) {
|
||||
state->enabled_features |= flags & state->feature_mask;
|
||||
} else {
|
||||
state->enabled_features &= (uint8_t)~(flags & state->feature_mask);
|
||||
}
|
||||
} else if (memory_read) {
|
||||
reply[8] = memory_length;
|
||||
for (unsigned i = 0; i < 4; ++i)
|
||||
reply[12 + i] = (uint8_t)(memory_address >> (8 * i));
|
||||
} else if (vibration_setup) {
|
||||
memcpy(state->vibration_parameters, command + 8, sizeof(state->vibration_parameters));
|
||||
state->vibration_parameters_set = true;
|
||||
} else if (info11_01) {
|
||||
// Identical 01 00 00 00 payload in all five genuine Joy-Con captures.
|
||||
reply[8] = 1;
|
||||
} else if (joycon_query) {
|
||||
// Published 13/01-03 replies: leading 1, then reserved zero bytes.
|
||||
// These queries' full semantics are still undocumented.
|
||||
reply[8] = 1;
|
||||
} else if (runtime_toggle) {
|
||||
state->runtime03_0c = command[8] != 0;
|
||||
}
|
||||
return reply_length;
|
||||
}
|
||||
|
||||
size_t probe_protocol_report(const probe_protocol_state* state, uint8_t report_id,
|
||||
uint8_t* output, size_t capacity) {
|
||||
if (!state || !state->initialized || !output || capacity < PROBE_INPUT_SIZE ||
|
||||
(report_id != 0x05 && report_id != 0x08)) return 0;
|
||||
memset(output, 0, PROBE_INPUT_SIZE);
|
||||
const bool buttons_enabled = (state->enabled_features & 1) != 0;
|
||||
const uint8_t buttons0 = state->controller_active && buttons_enabled ? state->controller_buttons[0] : 0;
|
||||
const uint8_t buttons1 = state->controller_active && buttons_enabled ? state->controller_buttons[1] & 0xd1 : 0;
|
||||
const uint8_t* stick = state->controller_active && (state->enabled_features & 2) ?
|
||||
state->controller_stick : state->right_stick_center;
|
||||
if (report_id == 0x08) {
|
||||
output[0] = (uint8_t)state->report_counter;
|
||||
output[1] = 0x25; // Virtual full battery, external USB power.
|
||||
output[2] = buttons0;
|
||||
output[3] = buttons1;
|
||||
if (state->test_rail_buttons && (state->enabled_features & 1))
|
||||
output[3] |= 0xc0; // Joy-Con R native SL + SR.
|
||||
output[4] = 0x07;
|
||||
memcpy(output + 5, stick, 3);
|
||||
// Diagnostic snapshot only; complete live native packets bypass this generator.
|
||||
} else {
|
||||
for (unsigned i = 0; i < 4; ++i) output[i] = (uint8_t)(state->report_counter >> (8 * i));
|
||||
output[4] = (uint8_t)(((buttons0 & 0x03) << 2) | ((buttons0 & 0x0c) >> 2) |
|
||||
((buttons0 & 0x30) << 2) | ((buttons1 & 0xc0) >> 2));
|
||||
output[5] = (uint8_t)(((buttons0 & 0xc0) >> 5) | ((buttons1 & 0x01) << 4) |
|
||||
((buttons1 & 0x10) << 2));
|
||||
if (state->test_rail_buttons && (state->enabled_features & 1))
|
||||
output[4] |= 0x30; // Common report: right SL + SR.
|
||||
output[11] = 0x08;
|
||||
output[12] = 0x80;
|
||||
memcpy(output + 13, stick, 3);
|
||||
output[31] = 0xa0;
|
||||
output[32] = 0x0f; // Virtual battery voltage 4000mV.
|
||||
output[33] = 0x20;
|
||||
output[41] = 1;
|
||||
}
|
||||
return PROBE_INPUT_SIZE;
|
||||
}
|
||||
68
tools/switch2_usb_probe/protocol.h
Normal file
68
tools/switch2_usb_probe/protocol.h
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
#pragma once
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#define PROBE_COMMAND_MAX_SIZE 263u
|
||||
#define PROBE_REPLY_MAX_SIZE 96u
|
||||
#define PROBE_HOST_MAX_ADDRESSES ((255u - 2u) / 6u)
|
||||
#define PROBE_PAIRING_BLOB_SIZE (6u + 1u + PROBE_HOST_MAX_ADDRESSES * 6u + 16u)
|
||||
#define PROBE_INPUT_SIZE 63u
|
||||
|
||||
typedef struct {
|
||||
bool initialized;
|
||||
uint8_t report_id;
|
||||
bool test_rail_buttons;
|
||||
bool runtime03_0c; // Observed USB toggle; full semantics remain unknown.
|
||||
uint8_t right_stick_center[3];
|
||||
bool controller_active;
|
||||
uint8_t controller_buttons[2]; // Native right Joy-Con button ordering.
|
||||
uint8_t controller_stick[3]; // Raw packed 12-bit axes from the selected donor.
|
||||
uint8_t player_leds; // Virtual four-LED mask, exposed through UART diagnostics.
|
||||
bool player_leds_flashing;
|
||||
// Negotiated virtual features; relative mouse events belong to the USB sender.
|
||||
uint8_t feature_mask;
|
||||
uint8_t enabled_features;
|
||||
bool vibration_parameters_set;
|
||||
uint8_t vibration_parameters[20]; // Captured 0A/08 format; no motor output.
|
||||
uint8_t host_address[6];
|
||||
uint8_t controller_address[6]; // Own virtual identity, wire byte order (LE).
|
||||
const uint8_t* firmware_version; // Twelve captured bytes; caller retains their lifetime.
|
||||
// Every payload-bearing address exchange starts a fresh pending context.
|
||||
uint8_t pending_host_count;
|
||||
uint8_t pending_host_addresses[PROBE_HOST_MAX_ADDRESSES][6];
|
||||
bool pending_key_valid;
|
||||
uint8_t pending_key[16]; // Standard AES byte order, not wire byte order.
|
||||
bool challenge_confirmed;
|
||||
// A zero count means no committed pairing record.
|
||||
uint8_t committed_host_count;
|
||||
uint8_t committed_host_addresses[PROBE_HOST_MAX_ADDRESSES][6];
|
||||
uint8_t committed_key[16]; // Standard AES byte order.
|
||||
// Synchronous durable save; NULL disables successful finalization.
|
||||
bool (*save_pairing)(const uint8_t* blob, size_t length);
|
||||
bool (*read_memory)(uint32_t address, uint8_t* output, size_t length);
|
||||
// Queue a physical sample, returning true only with a nonzero completion token.
|
||||
// Acceptance is not a Bluetooth application ACK.
|
||||
bool (*play_sample)(uint8_t sample_id, uint64_t* token);
|
||||
uint32_t report_counter;
|
||||
} probe_protocol_state;
|
||||
|
||||
void probe_protocol_reset(probe_protocol_state* state);
|
||||
// Blob: own address[6], count[1], zero-padded host addresses[42][6], AES key[16].
|
||||
// Rejects other identities, invalid counts/padding/lengths without mutation.
|
||||
// A successful restore replaces the committed record and clears pending state.
|
||||
bool probe_protocol_restore_pairing(probe_protocol_state* state,
|
||||
const uint8_t* blob, size_t length);
|
||||
// Complete command frames only. Unsupported/malformed commands return zero
|
||||
// and do not mutate state. Pairing finalization requires a successful save.
|
||||
// When non-NULL, deferred_token is cleared before validation. Synchronous replies
|
||||
// leave it zero. Sample 0A/02 requires this output and play_sample; acceptance
|
||||
// prepares an 8-byte reply and returns its nonzero source completion token.
|
||||
// Those bytes MUST NOT be transmitted until that token has a genuine positive
|
||||
// Bluetooth application ACK. Failure, cancellation or expiry must discard them.
|
||||
size_t probe_protocol_command(probe_protocol_state* state, const uint8_t* command,
|
||||
size_t length, uint8_t* reply, size_t capacity,
|
||||
uint64_t* deferred_token);
|
||||
// Button/stick snapshot without relative mouse events; safe for GET_REPORT.
|
||||
size_t probe_protocol_report(const probe_protocol_state* state, uint8_t report_id,
|
||||
uint8_t* output, size_t capacity);
|
||||
373
tools/switch2_usb_probe/storage.cpp
Normal file
373
tools/switch2_usb_probe/storage.cpp
Normal file
|
|
@ -0,0 +1,373 @@
|
|||
#include "storage.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include "configuration/configuration_storage.h"
|
||||
#include "hardware/flash.h"
|
||||
#include "pico/btstack_flash_bank.h"
|
||||
#include "pico/flash.h"
|
||||
#include "pico/platform.h"
|
||||
#include "profile/profile_storage.h"
|
||||
|
||||
extern "C" char __flash_binary_end;
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr size_t kSlotCount = 2;
|
||||
constexpr size_t kMaximumPayloadSize = 512;
|
||||
constexpr size_t kStorageSize = kSlotCount * FLASH_SECTOR_SIZE;
|
||||
constexpr size_t kConfigurationStorageSize =
|
||||
CONFIGURATION_STORAGE_COPY_COUNT * FLASH_SECTOR_SIZE;
|
||||
constexpr size_t kConfigurationStorageOffset =
|
||||
PICO_FLASH_BANK_STORAGE_OFFSET - kConfigurationStorageSize;
|
||||
constexpr size_t kProfileStorageOffset =
|
||||
kConfigurationStorageOffset - PROFILE_STORAGE_TOTAL_SIZE;
|
||||
constexpr uint32_t kStorageOffset = kProfileStorageOffset - kStorageSize;
|
||||
constexpr uint32_t kFlashSafeTimeoutMs = 5000;
|
||||
constexpr uint32_t kFormatVersion = 1;
|
||||
|
||||
// Each sector owns one record. Integers are little-endian; all padding is ff.
|
||||
// Page 0: magic[16], version:u32, absolute sector offset:u32, maximum payload:u32,
|
||||
// page size:u32, sector size:u32, CRC32(bytes 0..35):u32, padding.
|
||||
// Page 1 starts the body: magic[8], generation:u32, ~generation:u32,
|
||||
// length:u32, payload CRC32:u32, header size:u32, header CRC32:u32,
|
||||
// payload[length], padding to the fixed body-area boundary.
|
||||
// The separate commit page is programmed LAST: magic[16], generation:u32,
|
||||
// header CRC32:u32, payload CRC32:u32, length:u32, sector offset:u32,
|
||||
// CRC32(bytes 0..35):u32, padding. The rest of the sector stays erased.
|
||||
// CRC32 is the project's IEEE CRC32 (also compatible with zlib.crc32).
|
||||
constexpr uint8_t kOwnerMagic[16] = {
|
||||
'S', '2', 'P', 'R', 'O', 'B', 'E', '-',
|
||||
'P', 'A', 'I', 'R', 'I', 'N', 'G', 0,
|
||||
};
|
||||
constexpr uint8_t kBodyMagic[8] = {'S', '2', 'P', 'A', 'I', 'R', '0', '1'};
|
||||
constexpr uint8_t kCommitMagic[16] = {
|
||||
'S', '2', 'P', 'A', 'I', 'R', '-', 'C',
|
||||
'O', 'M', 'M', 'I', 'T', 'T', 'E', 'D',
|
||||
};
|
||||
constexpr size_t kDescriptorCrcOffset = 36;
|
||||
constexpr size_t kDescriptorSize = kDescriptorCrcOffset + sizeof(uint32_t);
|
||||
constexpr size_t kBodyOffset = FLASH_PAGE_SIZE;
|
||||
constexpr size_t kBodyHeaderCrcOffset = 28;
|
||||
constexpr size_t kBodyHeaderSize = kBodyHeaderCrcOffset + sizeof(uint32_t);
|
||||
constexpr size_t kBodySize =
|
||||
((kBodyHeaderSize + kMaximumPayloadSize + FLASH_PAGE_SIZE - 1) /
|
||||
FLASH_PAGE_SIZE) * FLASH_PAGE_SIZE;
|
||||
constexpr size_t kPayloadOffset = kBodyOffset + kBodyHeaderSize;
|
||||
constexpr size_t kCommitOffset = kBodyOffset + kBodySize;
|
||||
constexpr size_t kRecordFootprint = kCommitOffset + FLASH_PAGE_SIZE;
|
||||
|
||||
static_assert(FLASH_SECTOR_SIZE == PROFILE_STORAGE_SECTOR_SIZE);
|
||||
static_assert(FLASH_PAGE_SIZE == PROFILE_STORAGE_PAGE_SIZE);
|
||||
static_assert(FLASH_SECTOR_SIZE % FLASH_PAGE_SIZE == 0);
|
||||
static_assert(kDescriptorSize <= FLASH_PAGE_SIZE);
|
||||
static_assert(kRecordFootprint <= FLASH_SECTOR_SIZE);
|
||||
static_assert(PROFILE_STORAGE_TOTAL_SIZE % FLASH_SECTOR_SIZE == 0);
|
||||
static_assert(PICO_FLASH_BANK_STORAGE_OFFSET % FLASH_SECTOR_SIZE == 0);
|
||||
static_assert(PICO_FLASH_BANK_STORAGE_OFFSET >=
|
||||
kConfigurationStorageSize + PROFILE_STORAGE_TOTAL_SIZE +
|
||||
kStorageSize,
|
||||
"pairing storage offset underflows flash");
|
||||
static_assert(kStorageOffset + kStorageSize == kProfileStorageOffset);
|
||||
static_assert(kProfileStorageOffset + PROFILE_STORAGE_TOTAL_SIZE ==
|
||||
kConfigurationStorageOffset);
|
||||
static_assert(kConfigurationStorageOffset + kConfigurationStorageSize ==
|
||||
PICO_FLASH_BANK_STORAGE_OFFSET);
|
||||
static_assert(PICO_FLASH_BANK_STORAGE_OFFSET <= PICO_FLASH_SIZE_BYTES);
|
||||
static_assert(PICO_FLASH_BANK_TOTAL_SIZE <=
|
||||
PICO_FLASH_SIZE_BYTES - PICO_FLASH_BANK_STORAGE_OFFSET,
|
||||
"BTstack storage exceeds flash");
|
||||
|
||||
// Avoid a large USB callback stack frame. All program sources, including any
|
||||
// input originally backed by XIP, are staged before the first flash mutation.
|
||||
alignas(FLASH_PAGE_SIZE) uint8_t staging[kRecordFootprint];
|
||||
|
||||
enum class SlotKind { Erased, Unknown, OwnedIncomplete, Committed };
|
||||
|
||||
struct Slot {
|
||||
SlotKind kind;
|
||||
const uint8_t *bytes;
|
||||
uint32_t generation;
|
||||
size_t size;
|
||||
};
|
||||
|
||||
struct FlashMutation {
|
||||
uint32_t offset;
|
||||
const uint8_t *page; // Null means erase one sector.
|
||||
};
|
||||
|
||||
uint32_t read_u32(const uint8_t *input) {
|
||||
return static_cast<uint32_t>(input[0]) |
|
||||
(static_cast<uint32_t>(input[1]) << 8) |
|
||||
(static_cast<uint32_t>(input[2]) << 16) |
|
||||
(static_cast<uint32_t>(input[3]) << 24);
|
||||
}
|
||||
|
||||
void write_u32(uint8_t *output, uint32_t value) {
|
||||
output[0] = static_cast<uint8_t>(value);
|
||||
output[1] = static_cast<uint8_t>(value >> 8);
|
||||
output[2] = static_cast<uint8_t>(value >> 16);
|
||||
output[3] = static_cast<uint8_t>(value >> 24);
|
||||
}
|
||||
|
||||
bool is_erased(const uint8_t *bytes, size_t size) {
|
||||
for (size_t index = 0; index < size; ++index) {
|
||||
if (bytes[index] != 0xff) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool storage_region_available() {
|
||||
const uintptr_t binary_end = reinterpret_cast<uintptr_t>(&__flash_binary_end);
|
||||
return binary_end >= XIP_BASE &&
|
||||
binary_end - XIP_BASE <= kStorageOffset &&
|
||||
kStorageOffset % FLASH_SECTOR_SIZE == 0 &&
|
||||
kStorageOffset <= PICO_FLASH_SIZE_BYTES &&
|
||||
kStorageSize <= PICO_FLASH_SIZE_BYTES - kStorageOffset &&
|
||||
kStorageOffset + kStorageSize == kProfileStorageOffset;
|
||||
}
|
||||
|
||||
uint32_t slot_offset(size_t slot) {
|
||||
return static_cast<uint32_t>(kStorageOffset + slot * FLASH_SECTOR_SIZE);
|
||||
}
|
||||
|
||||
const uint8_t *slot_bytes(size_t slot) {
|
||||
return reinterpret_cast<const uint8_t *>(XIP_BASE + slot_offset(slot));
|
||||
}
|
||||
|
||||
bool owner_valid(const uint8_t *bytes, uint32_t offset) {
|
||||
return memcmp(bytes, kOwnerMagic, sizeof(kOwnerMagic)) == 0 &&
|
||||
read_u32(bytes + 16) == kFormatVersion &&
|
||||
read_u32(bytes + 20) == offset &&
|
||||
read_u32(bytes + 24) == kMaximumPayloadSize &&
|
||||
read_u32(bytes + 28) == FLASH_PAGE_SIZE &&
|
||||
read_u32(bytes + 32) == FLASH_SECTOR_SIZE &&
|
||||
read_u32(bytes + kDescriptorCrcOffset) ==
|
||||
configuration_crc32(bytes, kDescriptorCrcOffset) &&
|
||||
is_erased(bytes + kDescriptorSize,
|
||||
FLASH_PAGE_SIZE - kDescriptorSize);
|
||||
}
|
||||
|
||||
bool body_valid(const uint8_t *bytes) {
|
||||
const uint8_t *body = bytes + kBodyOffset;
|
||||
const size_t size = read_u32(body + 16);
|
||||
return memcmp(body, kBodyMagic, sizeof(kBodyMagic)) == 0 &&
|
||||
read_u32(body + 12) == ~read_u32(body + 8) &&
|
||||
size != 0 && size <= kMaximumPayloadSize &&
|
||||
read_u32(body + 24) == kBodyHeaderSize &&
|
||||
read_u32(body + kBodyHeaderCrcOffset) ==
|
||||
configuration_crc32(body, kBodyHeaderCrcOffset) &&
|
||||
read_u32(body + 20) ==
|
||||
configuration_crc32(bytes + kPayloadOffset, size) &&
|
||||
is_erased(bytes + kPayloadOffset + size,
|
||||
kBodySize - kBodyHeaderSize - size);
|
||||
}
|
||||
|
||||
bool commit_valid(const uint8_t *bytes, uint32_t offset) {
|
||||
const uint8_t *body = bytes + kBodyOffset;
|
||||
const uint8_t *commit = bytes + kCommitOffset;
|
||||
return memcmp(commit, kCommitMagic, sizeof(kCommitMagic)) == 0 &&
|
||||
read_u32(commit + 16) == read_u32(body + 8) &&
|
||||
read_u32(commit + 20) == read_u32(body + kBodyHeaderCrcOffset) &&
|
||||
read_u32(commit + 24) == read_u32(body + 20) &&
|
||||
read_u32(commit + 28) == read_u32(body + 16) &&
|
||||
read_u32(commit + 32) == offset &&
|
||||
read_u32(commit + kDescriptorCrcOffset) ==
|
||||
configuration_crc32(commit, kDescriptorCrcOffset) &&
|
||||
is_erased(commit + kDescriptorSize,
|
||||
FLASH_PAGE_SIZE - kDescriptorSize);
|
||||
}
|
||||
|
||||
Slot inspect_slot(size_t index) {
|
||||
const uint8_t *bytes = slot_bytes(index);
|
||||
Slot slot{SlotKind::Unknown, bytes, 0, 0};
|
||||
if (!owner_valid(bytes, slot_offset(index))) {
|
||||
if (is_erased(bytes, FLASH_SECTOR_SIZE)) {
|
||||
slot.kind = SlotKind::Erased;
|
||||
}
|
||||
// A torn ownership page or erase is deliberately NOT guessed to be
|
||||
// ours. Recovery may need an externally verified backup in that case.
|
||||
return slot;
|
||||
}
|
||||
if (!is_erased(bytes + kRecordFootprint,
|
||||
FLASH_SECTOR_SIZE - kRecordFootprint)) {
|
||||
return slot;
|
||||
}
|
||||
// A complete ownership page plus an erased tail proves ownership of the
|
||||
// bounded body/commit area, even if either subsequent write was interrupted.
|
||||
slot.kind = SlotKind::OwnedIncomplete;
|
||||
if (body_valid(bytes) && commit_valid(bytes, slot_offset(index))) {
|
||||
slot.kind = SlotKind::Committed;
|
||||
slot.generation = read_u32(bytes + kBodyOffset + 8);
|
||||
slot.size = read_u32(bytes + kBodyOffset + 16);
|
||||
}
|
||||
return slot;
|
||||
}
|
||||
|
||||
bool newest_slot(const Slot (&slots)[kSlotCount], int *index) {
|
||||
*index = -1;
|
||||
for (size_t candidate = 0; candidate < kSlotCount; ++candidate) {
|
||||
if (slots[candidate].kind != SlotKind::Committed) {
|
||||
continue;
|
||||
}
|
||||
if (*index < 0) {
|
||||
*index = static_cast<int>(candidate);
|
||||
continue;
|
||||
}
|
||||
const Slot ¤t = slots[*index];
|
||||
const Slot &next = slots[candidate];
|
||||
const uint32_t difference = next.generation - current.generation;
|
||||
if (difference == 0) {
|
||||
if (next.size != current.size ||
|
||||
memcmp(next.bytes + kPayloadOffset,
|
||||
current.bytes + kPayloadOffset, next.size) != 0) {
|
||||
return false; // Conflicting records with no ordering.
|
||||
}
|
||||
} else if (difference == 0x80000000u) {
|
||||
return false; // Exactly half a generation cycle is ambiguous.
|
||||
} else if (difference < 0x80000000u) {
|
||||
*index = static_cast<int>(candidate);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void perform_flash_mutation(void *context) {
|
||||
const auto *mutation = static_cast<const FlashMutation *>(context);
|
||||
if (mutation->page == nullptr) {
|
||||
flash_range_erase(mutation->offset, FLASH_SECTOR_SIZE);
|
||||
} else {
|
||||
flash_range_program(mutation->offset, mutation->page, FLASH_PAGE_SIZE);
|
||||
}
|
||||
}
|
||||
|
||||
// The caller has classified BOTH sectors before permitting any erase. Only
|
||||
// the inactive, explicitly owned sector is passed here; the active one survives.
|
||||
bool erase_slot(size_t index) {
|
||||
if (index >= kSlotCount || !storage_region_available()) {
|
||||
return false;
|
||||
}
|
||||
FlashMutation mutation{slot_offset(index), nullptr};
|
||||
return flash_safe_execute(perform_flash_mutation, &mutation,
|
||||
kFlashSafeTimeoutMs) == PICO_OK &&
|
||||
is_erased(slot_bytes(index), FLASH_SECTOR_SIZE);
|
||||
}
|
||||
|
||||
bool program_page(size_t index, size_t offset, const uint8_t *page) {
|
||||
if (index >= kSlotCount || offset % FLASH_PAGE_SIZE != 0 ||
|
||||
offset > kRecordFootprint - FLASH_PAGE_SIZE ||
|
||||
!storage_region_available() ||
|
||||
!is_erased(slot_bytes(index) + offset, FLASH_PAGE_SIZE)) {
|
||||
return false;
|
||||
}
|
||||
FlashMutation mutation{
|
||||
static_cast<uint32_t>(slot_offset(index) + offset), page,
|
||||
};
|
||||
return flash_safe_execute(perform_flash_mutation, &mutation,
|
||||
kFlashSafeTimeoutMs) == PICO_OK &&
|
||||
memcmp(slot_bytes(index) + offset, page, FLASH_PAGE_SIZE) == 0;
|
||||
}
|
||||
|
||||
void prepare_record(size_t target, uint32_t generation,
|
||||
const uint8_t *data, size_t size) {
|
||||
memset(staging, 0xff, sizeof(staging));
|
||||
memcpy(staging, kOwnerMagic, sizeof(kOwnerMagic));
|
||||
write_u32(staging + 16, kFormatVersion);
|
||||
write_u32(staging + 20, slot_offset(target));
|
||||
write_u32(staging + 24, kMaximumPayloadSize);
|
||||
write_u32(staging + 28, FLASH_PAGE_SIZE);
|
||||
write_u32(staging + 32, FLASH_SECTOR_SIZE);
|
||||
write_u32(staging + kDescriptorCrcOffset,
|
||||
configuration_crc32(staging, kDescriptorCrcOffset));
|
||||
|
||||
uint8_t *body = staging + kBodyOffset;
|
||||
memcpy(body, kBodyMagic, sizeof(kBodyMagic));
|
||||
write_u32(body + 8, generation);
|
||||
write_u32(body + 12, ~generation);
|
||||
write_u32(body + 16, static_cast<uint32_t>(size));
|
||||
memcpy(staging + kPayloadOffset, data, size);
|
||||
const uint32_t payload_crc = configuration_crc32(staging + kPayloadOffset, size);
|
||||
write_u32(body + 20, payload_crc);
|
||||
write_u32(body + 24, kBodyHeaderSize);
|
||||
const uint32_t header_crc = configuration_crc32(body, kBodyHeaderCrcOffset);
|
||||
write_u32(body + kBodyHeaderCrcOffset, header_crc);
|
||||
|
||||
uint8_t *commit = staging + kCommitOffset;
|
||||
memcpy(commit, kCommitMagic, sizeof(kCommitMagic));
|
||||
write_u32(commit + 16, generation);
|
||||
write_u32(commit + 20, header_crc);
|
||||
write_u32(commit + 24, payload_crc);
|
||||
write_u32(commit + 28, static_cast<uint32_t>(size));
|
||||
write_u32(commit + 32, slot_offset(target));
|
||||
write_u32(commit + kDescriptorCrcOffset,
|
||||
configuration_crc32(commit, kDescriptorCrcOffset));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool probe_storage_load(uint8_t *output, size_t size) {
|
||||
if (output == nullptr || size == 0 || size > kMaximumPayloadSize ||
|
||||
!storage_region_available()) {
|
||||
return false;
|
||||
}
|
||||
const Slot slots[kSlotCount] = {inspect_slot(0), inspect_slot(1)};
|
||||
int active;
|
||||
if (!newest_slot(slots, &active) || active < 0 || slots[active].size != size) {
|
||||
return false;
|
||||
}
|
||||
memcpy(output, slots[active].bytes + kPayloadOffset, size);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool probe_storage_save(const uint8_t *data, size_t size) {
|
||||
if (data == nullptr || size == 0 || size > kMaximumPayloadSize ||
|
||||
!storage_region_available()) {
|
||||
return false;
|
||||
}
|
||||
const Slot slots[kSlotCount] = {inspect_slot(0), inspect_slot(1)};
|
||||
if (slots[0].kind == SlotKind::Unknown || slots[1].kind == SlotKind::Unknown) {
|
||||
return false; // Never erase through an unrecognized region.
|
||||
}
|
||||
int active;
|
||||
if (!newest_slot(slots, &active)) {
|
||||
return false;
|
||||
}
|
||||
if (active >= 0 && slots[active].size == size &&
|
||||
memcmp(slots[active].bytes + kPayloadOffset, data, size) == 0) {
|
||||
return true;
|
||||
}
|
||||
const size_t target = active >= 0
|
||||
? static_cast<size_t>(active) ^ 1u
|
||||
: (slots[0].kind == SlotKind::Erased ? 0u : 1u);
|
||||
const uint32_t generation = active >= 0 ? slots[active].generation + 1u : 1u;
|
||||
prepare_record(target, generation, data, size);
|
||||
|
||||
if (slots[target].kind != SlotKind::Erased && !erase_slot(target)) {
|
||||
return false;
|
||||
}
|
||||
if (!program_page(target, 0, staging)) {
|
||||
return false;
|
||||
}
|
||||
for (size_t offset = kBodyOffset; offset < kCommitOffset;
|
||||
offset += FLASH_PAGE_SIZE) {
|
||||
if (!is_erased(staging + offset, FLASH_PAGE_SIZE) &&
|
||||
!program_page(target, offset, staging + offset)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (!body_valid(slot_bytes(target)) ||
|
||||
!program_page(target, kCommitOffset, staging + kCommitOffset)) {
|
||||
return false;
|
||||
}
|
||||
const Slot committed = inspect_slot(target);
|
||||
return committed.kind == SlotKind::Committed &&
|
||||
committed.generation == generation && committed.size == size &&
|
||||
memcmp(committed.bytes + kPayloadOffset,
|
||||
staging + kPayloadOffset, size) == 0;
|
||||
}
|
||||
|
||||
uint32_t probe_storage_offset(void) {
|
||||
return kStorageOffset;
|
||||
}
|
||||
25
tools/switch2_usb_probe/storage.h
Normal file
25
tools/switch2_usb_probe/storage.h
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
// Synchronous, main-loop-only API for the single-core probe. Serialize calls.
|
||||
// Blobs are opaque, nonempty, and at most 512 bytes. Load requires an exact
|
||||
// length match and leaves output unchanged on failure; it never writes flash.
|
||||
bool probe_storage_load(uint8_t *output, size_t size);
|
||||
|
||||
// Success means an identical blob was already committed, or a replacement was
|
||||
// committed and read back. Failure never authorizes a protocol acknowledgement.
|
||||
bool probe_storage_save(const uint8_t *data, size_t size);
|
||||
|
||||
// Flash-relative offset of the two sectors immediately below profile storage.
|
||||
uint32_t probe_storage_offset(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
31
tools/switch2_usb_probe/tusb_config.h
Normal file
31
tools/switch2_usb_probe/tusb_config.h
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
#pragma once
|
||||
|
||||
#define CFG_TUSB_RHPORT0_MODE (OPT_MODE_DEVICE | OPT_MODE_FULL_SPEED)
|
||||
#ifndef CFG_TUSB_OS
|
||||
#define CFG_TUSB_OS OPT_OS_NONE
|
||||
#endif
|
||||
#define CFG_TUD_ENDPOINT0_SIZE 64
|
||||
#define CFG_TUD_HID 1
|
||||
#define CFG_TUD_HID_EP_BUFSIZE 64
|
||||
#define CFG_TUD_CDC 0
|
||||
#define CFG_TUD_MSC 0
|
||||
#define CFG_TUD_MIDI 0
|
||||
#define CFG_TUD_VENDOR 1
|
||||
#define CFG_TUD_VENDOR_EPSIZE 64
|
||||
#define CFG_TUD_VENDOR_RX_BUFSIZE 256
|
||||
#define CFG_TUD_VENDOR_TX_BUFSIZE 256
|
||||
#ifdef CFG_TUSB_DEBUG
|
||||
#undef CFG_TUSB_DEBUG
|
||||
#endif
|
||||
// Packet-level SDK logging would overflow 115200 baud while streaming input.
|
||||
// Requests, command payloads, errors and aggregate counters are logged explicitly.
|
||||
#define CFG_TUSB_DEBUG 1
|
||||
#define CFG_TUSB_DEBUG_PRINTF probe_debug_printf
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
int probe_debug_printf(const char* format, ...);
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
Loading…
Add table
Add a link
Reference in a new issue