Add stock-USB native Joy-Con R/L hub bridge

This commit is contained in:
Joey Yakimowich-Payne 2026-09-12 15:28:58 -06:00
commit 1748910316
41 changed files with 7101 additions and 909 deletions

View file

@ -1,21 +1,71 @@
#include "bootsel.h"
#include "model.h"
#if SWITCH2_PROBE_HUB
#include <string.h>
#include "pico/bootrom.h"
#include "usb/native_hub/native_hub.h"
#else
#include "adapter/adapter_mode_controller.h"
#endif
#include "usb/usb_configuration_management.h"
namespace {
bool bootsel_accepted;
#if SWITCH2_PROBE_HUB
constexpr uint32_t kBootselRebootDelayMs = 50;
struct BootselTransfer {
uint8_t envelope[UsbConfigurationManagement::kRequestHeaderSize];
bool pending;
bool validated;
};
// Control state is independent even when the two children enumerate together.
BootselTransfer bootsel_transfers[PROBE_CONTROLLER_COUNT + 1];
bool bootsel_delay_started;
uint32_t bootsel_deadline_ms;
#endif
}
bool probe_bootsel_vendor_control(uint8_t rhport, uint8_t stage,
const tusb_control_request_t* request) {
using namespace UsbConfigurationManagement;
#if SWITCH2_PROBE_HUB
if (rhport > PROBE_CONTROLLER_COUNT) return false;
BootselTransfer& transfer = bootsel_transfers[rhport];
if (stage == CONTROL_STAGE_SETUP) {
transfer.pending = false;
transfer.validated = false;
}
#endif
if (request == nullptr || request->bmRequestType != 0x40 ||
request->bRequest != static_cast<uint8_t>(Operation::kBootselReboot) ||
request->wValue != kRequestValue || request->wIndex != kRequestIndex ||
request->wLength != kRequestHeaderSize) {
return false;
}
#if SWITCH2_PROBE_HUB
if (stage == CONTROL_STAGE_SETUP) {
// Any short OUT leaves nonzero reserved/CRC bytes and fails decoding.
memset(transfer.envelope, 0xff, sizeof(transfer.envelope));
transfer.pending = native_hub_control_xfer(
rhport, request, transfer.envelope, sizeof(transfer.envelope));
return transfer.pending;
}
if (stage == CONTROL_STAGE_DATA) {
DecodedRequest decoded{};
transfer.validated = transfer.pending &&
decode_request(Operation::kBootselReboot, transfer.envelope,
sizeof(transfer.envelope), &decoded) &&
decoded.payload_size == 0;
return transfer.validated;
}
if (stage == CONTROL_STAGE_ACK && transfer.pending && transfer.validated) {
transfer.pending = false;
bootsel_accepted = true;
return true;
}
return false;
#else
// The shared handler receives the envelope at SETUP and validates and
// dispatches it only at ACK, after the host's control transfer completes.
const bool accepted =
@ -24,12 +74,24 @@ bool probe_bootsel_vendor_control(uint8_t rhport, uint8_t stage,
bootsel_accepted = true;
}
return accepted;
#endif
}
void probe_bootsel_task(uint32_t now_ms) {
#if SWITCH2_PROBE_HUB
if (!bootsel_accepted) return;
if (!bootsel_delay_started) {
bootsel_delay_started = true;
bootsel_deadline_ms = now_ms + kBootselRebootDelayMs;
} else if (static_cast<int32_t>(now_ms - bootsel_deadline_ms) >= 0) {
bootsel_accepted = false;
reset_usb_boot(0, 0);
}
#else
// The native bridge does not initialize ordinary adapter-mode selection.
// A successful BOOTSEL dispatch guarantees the task takes its reboot path.
if (bootsel_accepted) {
adapter_mode_controller_task(now_ms);
}
#endif
}

View file

@ -1,4 +1,5 @@
#include "controller_input.h"
#include "model.h"
#include <string.h>
@ -8,6 +9,10 @@
#include "platform/pico/system_clock.h"
#include "profile/controller_profile_runtime.h"
#include "pico/stdlib.h"
#if SWITCH2_PROBE_HUB
#include <inttypes.h>
extern "C" int probe_debug_printf(const char* format, ...);
#endif
#if SWITCH2_BRIDGE_WII_INPUT
#include <math.h>
#include "input/wii_ir_pointer.h"
@ -25,17 +30,25 @@ extern "C" int probe_debug_printf(const char* format, ...);
namespace {
constexpr uint8_t kSourceAddress[] = {SWITCH2_BRIDGE_SOURCE_ADDRESS_BYTES};
static_assert(sizeof(kSourceAddress) == 6, "Select one physical Bluetooth address");
#if SWITCH2_PROBE_COMPOSITE || SWITCH2_PROBE_HUB
constexpr uint8_t kSecondSourceAddress[] = {SWITCH2_BRIDGE_SECOND_SOURCE_ADDRESS_BYTES};
static_assert(sizeof(kSecondSourceAddress) == 6, "Select the second physical Bluetooth address");
#endif
constexpr uint32_t kInputDeadlineMs = 500;
#if !SWITCH2_PROBE_HUB
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().
#endif
// Stage 2 publishes flash safety: both cores registered in dedicated-radio
// modes, or Core 0 registered with an SRAM-only/IRQ-disabled Core 1 in hub mode.
constexpr uint32_t kFlashCoordinationStage = 2;
bool g_initialized;
bool g_start_attempted;
bool g_flash_ready;
#if SWITCH2_BRIDGE_WII_INPUT
probe_controller_input g_input;
#if !SWITCH2_BRIDGE_WII_INPUT
uint32_t g_received_ms;
#else
probe_controller_input g_inputs[PROBE_CONTROLLER_COUNT];
uint32_t g_received_times[PROBE_CONTROLLER_COUNT];
#endif
#if SWITCH2_BRIDGE_WII_INPUT
#ifndef SWITCH2_WII_IR_SCREEN_CONFIG
@ -368,8 +381,13 @@ extern "C" void probe_controller_input_init(void) {
if (!g_screen_configured) probe_debug_printf("[PROBE] Invalid native IR viewport configuration\n");
wii_ir_mouse_set_output_enabled(false);
#else
static_assert(SWITCH2_MOUSE_CAPTURE_SOURCE_COUNT == PROBE_CONTROLLER_COUNT,
"Each native controller requires an independent capture channel");
switch2_mouse_capture_init();
switch2_mouse_capture_select_input(kSourceAddress);
switch2_mouse_capture_select_input(0, kSourceAddress, probe_model_pid(0));
#if SWITCH2_PROBE_COMPOSITE || SWITCH2_PROBE_HUB
switch2_mouse_capture_select_input(1, kSecondSourceAddress, probe_model_pid(1));
#endif
bluepad32_input_backend_init();
#endif
controller_profile_runtime_reset();
@ -384,6 +402,14 @@ extern "C" bool probe_controller_input_start(void) {
#endif
g_start_attempted = true;
bluepad32_input_backend_start();
#if SWITCH2_PROBE_HUB
// Initialization is synchronous on Core 0; there is no radio Core 1 to
// wait for. The SDK async context advances radio startup in task().
Bluepad32BackendDiagnostics diagnostics;
bluepad32_input_backend_diagnostics(&diagnostics);
g_flash_ready = diagnostics.initialization_stage >= kFlashCoordinationStage;
return g_flash_ready;
#else
const absolute_time_t deadline = make_timeout_time_ms(kFlashCoordinationTimeoutMs);
do {
Bluepad32BackendDiagnostics diagnostics;
@ -397,6 +423,25 @@ extern "C" bool probe_controller_input_start(void) {
// 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;
#endif
}
extern "C" void probe_controller_input_task(void) {
#if SWITCH2_PROBE_HUB
if (!g_flash_ready) return;
bluepad32_input_backend_poll();
static uint32_t last_diagnostics;
const uint32_t now = to_ms_since_boot(get_absolute_time());
if ((uint32_t)(now - last_diagnostics) >= 1000u) {
last_diagnostics = now;
Bluepad32BackendDiagnostics diagnostics;
bluepad32_input_backend_diagnostics(&diagnostics);
probe_debug_printf("[HUB_RADIO] stage=%" PRIu32 " timers=%" PRIu32 "/%" PRIu32
" reports=%" PRIu32 "\n", diagnostics.initialization_stage,
diagnostics.rumble_timer_ticks, diagnostics.configuration_timer_ticks,
diagnostics.controller_reports);
}
#endif
}
extern "C" bool probe_controller_input_pairing_task(void) {
@ -426,30 +471,31 @@ extern "C" void probe_controller_input_set_native_features(uint8_t features) {
}
#endif
extern "C" void probe_controller_input_set_native_stream(bool enabled) {
extern "C" void probe_controller_input_set_native_stream(uint8_t instance, bool enabled) {
if (instance >= PROBE_CONTROLLER_COUNT) return;
#if SWITCH2_BRIDGE_WII_INPUT
enabled = enabled && g_flash_ready;
if (g_native_stream != enabled || !enabled) discard_wii_output();
g_native_stream = enabled;
update_wii_ir_gate(time_us_32());
#else
switch2_mouse_capture_set_native_stream(g_flash_ready && enabled);
switch2_mouse_capture_set_native_stream(instance, g_flash_ready && enabled);
#endif
}
extern "C" uint32_t probe_controller_input_peek_native_report(
uint32_t now_ms, uint8_t report[63]) {
if (!g_flash_ready) return 0;
uint8_t instance, uint32_t now_ms, uint8_t report[63]) {
if (instance >= PROBE_CONTROLLER_COUNT || !g_flash_ready) return 0;
#if SWITCH2_BRIDGE_WII_INPUT
(void)now_ms;
return prepare_wii_report(report);
#else
return switch2_mouse_capture_peek_native_report(now_ms, report);
return switch2_mouse_capture_peek_native_report(instance, now_ms, report);
#endif
}
extern "C" bool probe_controller_input_commit_native_report(uint32_t serial) {
if (!g_flash_ready) return false;
extern "C" bool probe_controller_input_commit_native_report(uint8_t instance, uint32_t serial) {
if (instance >= PROBE_CONTROLLER_COUNT || !g_flash_ready) return false;
#if SWITCH2_BRIDGE_WII_INPUT
if (!g_native_stream || !serial || serial != g_pending_serial ||
g_pending_generation != g_wii_generation || !g_wii_active) return false;
@ -462,12 +508,12 @@ extern "C" bool probe_controller_input_commit_native_report(uint32_t serial) {
++g_report_counter;
return true;
#else
return switch2_mouse_capture_commit_native_report(serial);
return switch2_mouse_capture_commit_native_report(instance, serial);
#endif
}
extern "C" bool probe_controller_input_play_sample(uint8_t sample_id, uint64_t* token) {
if (!g_flash_ready) {
extern "C" bool probe_controller_input_play_sample(uint8_t instance, uint8_t sample_id, uint64_t* token) {
if (instance >= PROBE_CONTROLLER_COUNT || !g_flash_ready) {
if (token != nullptr) *token = 0;
return false;
}
@ -475,40 +521,43 @@ extern "C" bool probe_controller_input_play_sample(uint8_t sample_id, uint64_t*
return bluepad32_input_backend_wii_sample_request(sample_id, token);
#else
return switch2_mouse_capture_request_sample(
sample_id, to_ms_since_boot(get_absolute_time()), token);
instance, sample_id, to_ms_since_boot(get_absolute_time()), token);
#endif
}
extern "C" int probe_controller_input_sample_result(uint64_t token, uint32_t now_ms) {
if (!g_flash_ready) return -1;
extern "C" int probe_controller_input_sample_result(uint8_t instance, uint64_t token, uint32_t now_ms) {
if (instance >= PROBE_CONTROLLER_COUNT || !g_flash_ready) return -1;
#if SWITCH2_BRIDGE_WII_INPUT
(void)now_ms;
return bluepad32_input_backend_wii_sample_result(token);
#else
return switch2_mouse_capture_sample_result(token, now_ms);
return switch2_mouse_capture_sample_result(instance, token, now_ms);
#endif
}
extern "C" void probe_controller_input_cancel_sample(void) {
extern "C" void probe_controller_input_cancel_sample(uint8_t instance) {
if (instance >= PROBE_CONTROLLER_COUNT) return;
#if SWITCH2_BRIDGE_WII_INPUT
bluepad32_input_backend_wii_sample_cancel();
#else
switch2_mouse_capture_cancel_sample();
switch2_mouse_capture_cancel_sample(instance);
#endif
}
extern "C" void probe_controller_input_poll(uint32_t now_ms,
extern "C" void probe_controller_input_poll(uint8_t instance, uint32_t now_ms,
probe_controller_input* out) {
if (out == nullptr) return;
if (!g_flash_ready) {
if (instance >= PROBE_CONTROLLER_COUNT || !g_flash_ready) {
*out = {};
return;
}
#if SWITCH2_BRIDGE_WII_INPUT
poll_wii_source(now_ms);
#else
probe_controller_input& g_input = g_inputs[instance];
uint32_t& g_received_ms = g_received_times[instance];
Switch2MouseCaptureInput sample;
if (switch2_mouse_capture_latest_input(g_input.serial, &sample)) {
if (switch2_mouse_capture_latest_input(instance, g_input.serial, &sample)) {
g_input.serial = sample.serial;
g_input.active = sample.active;
g_received_ms = sample.received_ms;

View file

@ -12,7 +12,7 @@ typedef struct {
uint32_t serial;
uint8_t buttons[2];
uint8_t stick[3];
// Latest opaque native 08 byte 8.
// Latest opaque native 07/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.
@ -20,7 +20,7 @@ typedef struct {
uint32_t mouse_epoch;
int64_t mouse_total_x;
int64_t mouse_total_y;
// Latest opaque native 08 byte 13.
// Latest opaque native 07/08 byte 13.
uint8_t mouse_surface;
} probe_controller_input;
@ -28,10 +28,15 @@ typedef struct {
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.
// True means flash coordination is ready, not that the radio is ready or a
// controller is connected. Hub mode initializes on Core 0 with Core 1 reserved
// for SRAM-only USB; other modes register both cores and launch the radio there.
// Failure is latched: keep USB and flash-writing protocol operations disabled.
bool probe_controller_input_start(void);
// Core 0 main loop before USB tasks, outside IRQs and application state locks.
// Hub mode cooperatively services CYW43/BTstack, including storage and haptics;
// a no-op before successful start and in dedicated-radio modes.
void probe_controller_input_task(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.
@ -42,29 +47,30 @@ void probe_controller_input_set_stick_calibration(const uint8_t calibration[9]);
// Native feature changes are output barriers, not Bluetooth/IMU resets.
void probe_controller_input_set_native_features(uint8_t features);
#endif
// Core0 native08 output. Disable discards queued/prepared data; repeated enable
// preserves it. Joy-Con mode relays its bounded FIFO; Wii mode synthesizes from
// fresh calibrated sensors and the selected IR pointer. No pairing changes.
void probe_controller_input_set_native_stream(bool enabled);
// Core0 native07/08 output. Disable discards queued/prepared data; repeated
// enable preserves it. Joy-Con mode relays its bounded FIFO; right-only Wii
// mode synthesizes fresh calibrated sensors and the selected IR pointer.
// No pairing changes.
void probe_controller_input_set_native_stream(uint8_t instance, bool enabled);
// Copy one63-byte payload without report ID. Returns a boot-unique token, or0
// without changing output. Nondestructive until successful HID submission and
// commit. now_ms uses the Pico boot-ms clock; unavailable/stale input is rejected.
uint32_t probe_controller_input_peek_native_report(uint32_t now_ms, uint8_t report[63]);
uint32_t probe_controller_input_peek_native_report(uint8_t instance, 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);
bool probe_controller_input_commit_native_report(uint8_t instance, uint32_t serial);
// Built-in vibration samples only; raw HD-rumble output is not forwarded.
// A nonzero token means queued, not completed. Result:0 pending,1 completion,
// -1 failed/stale. Joy-Con completion is its application ACK; Wii completion is
// actual bounded rumble-driver dispatch (not an HD-waveform fidelity claim).
// 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);
bool probe_controller_input_play_sample(uint8_t instance, uint8_t sample_id, uint64_t* token);
int probe_controller_input_sample_result(uint8_t instance, uint64_t token, uint32_t now_ms);
void probe_controller_input_cancel_sample(uint8_t instance);
// Core0 at250Hz; now_ms uses Pico boot milliseconds. Supplies current mapped
// controls for diagnostic reports; the native sender owns motion consumption.
// Inactive controls are zero except serial; USB supplies its calibrated center.
void probe_controller_input_poll(uint32_t now_ms, probe_controller_input* out);
void probe_controller_input_poll(uint8_t instance, uint32_t now_ms, probe_controller_input* out);
#ifdef __cplusplus
}

View file

@ -1,33 +1,66 @@
#pragma once
#include <stdint.h>
#include "model.h"
// Published Joy-Con 2 (R) USB descriptors, reproduced for enumeration capture.
// Published Joy-Con 2 USB descriptors, reproduced for the selected model.
// 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,
};
// Composite retains the primary right PID (0x2066): USB has one device identity,
// not a separate device PID for each left/right function.
#define PROBE_DEVICE_DESCRIPTOR(pid) { \
0x12, 0x01, 0x00, 0x02, 0xef, 0x02, 0x01, 0x40, 0x7e, 0x05, \
(pid) & 0xff, (pid) >> 8, \
0x00, 0x01, 0x01, 0x02, 0x03, 0x01, \
}
static const uint8_t probe_device_descriptor[] = PROBE_DEVICE_DESCRIPTOR(PROBE_JOYCON_PID);
#if SWITCH2_PROBE_HUB
static const uint8_t probe_left_device_descriptor[] = PROBE_DEVICE_DESCRIPTOR(0x2067u);
#endif
#undef PROBE_DEVICE_DESCRIPTOR
static const uint8_t probe_configuration_descriptor[] = {
0x09, 0x02, 0x50, 0x00, 0x02, 0x01, 0x04, 0xc0, 0xfa, 0x08, 0x0b, 0x00,
#if SWITCH2_PROBE_COMPOSITE
0x09, 0x02, 0x97, 0x00, 0x04, 0x01, 0x04, 0xc0, 0xfa,
#else
0x09, 0x02, 0x50, 0x00, 0x02, 0x01, 0x04, 0xc0, 0xfa,
#endif
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,
#if SWITCH2_PROBE_COMPOSITE
0x08, 0x0b, 0x02, 0x01, 0x03, 0x00, 0x00, 0x00,
0x09, 0x04, 0x02, 0x00, 0x02, 0x03, 0x00, 0x00, 0x07,
0x09, 0x21, 0x11, 0x01, 0x00, 0x01, 0x22, 0x64, 0x00,
0x07, 0x05, 0x83, 0x03, 0x40, 0x00, 0x04,
0x07, 0x05, 0x03, 0x03, 0x40, 0x00, 0x04,
0x08, 0x0b, 0x03, 0x01, 0xff, 0x00, 0x00, 0x00,
0x09, 0x04, 0x03, 0x00, 0x02, 0xff, 0x00, 0x00, 0x08,
0x07, 0x05, 0x04, 0x02, 0x40, 0x00, 0x00,
0x07, 0x05, 0x84, 0x02, 0x40, 0x00, 0x00,
#endif
};
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,
#define PROBE_HID_DESCRIPTOR(report_id) { \
0x05, 0x01, 0x09, 0x05, 0xa1, 0x01, 0x85, 0x05, 0x05, 0xff, 0x09, 0x01, \
0x15, 0x00, 0x26, 0xff, 0x00, 0x95, 0x3f, 0x75, 0x08, 0x81, 0x02, 0x85, \
report_id, 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, \
}
static const uint8_t probe_hid_report_descriptors[PROBE_CONTROLLER_COUNT][100] = {
PROBE_HID_DESCRIPTOR(PROBE_NATIVE_REPORT_ID),
#if SWITCH2_PROBE_COMPOSITE || SWITCH2_PROBE_HUB
PROBE_HID_DESCRIPTOR(0x07u),
#endif
};
#undef PROBE_HID_DESCRIPTOR

File diff suppressed because it is too large Load diff

View file

@ -2,21 +2,23 @@
#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");
_Static_assert(sizeof(probe_factory_memories) == PROBE_CONTROLLER_COUNT * 8192u,
"factory capture sizes");
_Static_assert(sizeof(probe_user_calibrations) == PROBE_CONTROLLER_COUNT * 4096u,
"user calibration capture sizes");
bool probe_memory_read(uint32_t address, uint8_t* output, size_t length) {
if (!output) return false;
bool probe_memory_read(uint8_t instance, uint32_t address, uint8_t* output, size_t length) {
if (instance >= PROBE_CONTROLLER_COUNT || !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;
source = probe_factory_memories[instance];
available = sizeof(probe_factory_memories[instance]) - offset;
} else if (address >= 0x1fc000 && address < 0x1fd000) {
offset = address - 0x1fc000;
source = probe_user_calibration;
available = sizeof(probe_user_calibration) - offset;
source = probe_user_calibrations[instance];
available = sizeof(probe_user_calibrations[instance]) - offset;
} else {
return false; // No fabricated erased bytes, pairing keys, or firmware reads.
}
@ -43,12 +45,12 @@ static bool valid_calibration(const uint8_t* data) {
return true;
}
bool probe_memory_right_stick_calibration(uint8_t output[9]) {
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;
bool probe_memory_stick_calibration(uint8_t instance, uint8_t output[9]) {
if (instance >= PROBE_CONTROLLER_COUNT || !output) return false;
// Each Joy-Con's own capture uses the primary calibration record.
// User magic precedes its 9-byte record; factory has no magic.
const uint8_t* selected = probe_factory_memories[instance] + 0xa8;
const uint8_t* user = probe_user_calibrations[instance] + 0x40;
if (user[0] == 0xb2 && user[1] == 0xa1 && valid_calibration(user + 2))
selected = user + 2;
if (!valid_calibration(selected)) return false;

View file

@ -3,6 +3,7 @@
#include <stddef.h>
#include <stdint.h>
bool probe_memory_read(uint32_t address, uint8_t* output, size_t length);
// Invalid instances and unavailable ranges leave output unchanged.
bool probe_memory_read(uint8_t instance, uint32_t address, uint8_t* output, size_t length);
// Packed center, positive travel, negative travel (two12-bit axes each).
bool probe_memory_right_stick_calibration(uint8_t output[9]);
bool probe_memory_stick_calibration(uint8_t instance, uint8_t output[9]);

View file

@ -0,0 +1,94 @@
#pragma once
#include <stdbool.h>
#include <stdint.h>
#ifndef SWITCH2_PROBE_HUB
#define SWITCH2_PROBE_HUB 0
#endif
#if SWITCH2_PROBE_HUB != 0 && SWITCH2_PROBE_HUB != 1
#error "SWITCH2_PROBE_HUB must be 0 or 1"
#endif
#ifndef SWITCH2_PROBE_COMPOSITE
#define SWITCH2_PROBE_COMPOSITE 0
#endif
#if SWITCH2_PROBE_COMPOSITE != 0 && SWITCH2_PROBE_COMPOSITE != 1
#error "SWITCH2_PROBE_COMPOSITE must be 0 or 1"
#endif
#if SWITCH2_PROBE_HUB && SWITCH2_PROBE_COMPOSITE
#error "Native hub and composite USB backends are mutually exclusive"
#endif
#ifndef SWITCH2_PROBE_JOYCON_LEFT
#define SWITCH2_PROBE_JOYCON_LEFT 0
#endif
#if SWITCH2_PROBE_JOYCON_LEFT != 0 && SWITCH2_PROBE_JOYCON_LEFT != 1
#error "SWITCH2_PROBE_JOYCON_LEFT must be 0 or 1"
#endif
#if SWITCH2_PROBE_COMPOSITE || SWITCH2_PROBE_HUB
#if SWITCH2_PROBE_JOYCON_LEFT
#error "Dual-controller primary must be Joy-Con 2 (R)"
#endif
#ifndef PROBE_CONTROLLER_COUNT
#define PROBE_CONTROLLER_COUNT 2
#endif
#if PROBE_CONTROLLER_COUNT != 2
#error "Dual-controller output requires two controller instances"
#endif
#else
#ifndef PROBE_CONTROLLER_COUNT
#define PROBE_CONTROLLER_COUNT 1
#endif
#if PROBE_CONTROLLER_COUNT != 1
#error "Standalone requires one controller instance"
#endif
#endif
#if SWITCH2_PROBE_JOYCON_LEFT
#define PROBE_JOYCON_PID 0x2067u
#define PROBE_JOYCON_PRODUCT "Joy-Con 2 (L)"
#define PROBE_JOYCON_SIDE "left"
#define PROBE_NATIVE_REPORT_ID 0x07u
#define PROBE_IMU_LENGTH_OFFSET 14u
#define PROBE_IMU_DATA_OFFSET 15u
#else
#define PROBE_JOYCON_PID 0x2066u
#define PROBE_JOYCON_PRODUCT "Joy-Con 2 (R)"
#define PROBE_JOYCON_SIDE "right"
#define PROBE_NATIVE_REPORT_ID 0x08u
#define PROBE_IMU_LENGTH_OFFSET 15u
#define PROBE_IMU_DATA_OFFSET 16u
#endif
// Instance zero is the standalone model or the dual-controller right function.
// In composite and native hub modes, instance one is the independent left side.
static inline bool probe_model_is_left(uint8_t instance) {
#if SWITCH2_PROBE_COMPOSITE || SWITCH2_PROBE_HUB
return instance == 1;
#else
(void)instance;
return SWITCH2_PROBE_JOYCON_LEFT != 0;
#endif
}
static inline uint16_t probe_model_pid(uint8_t instance) {
return probe_model_is_left(instance) ? 0x2067u : 0x2066u;
}
static inline uint8_t probe_model_report_id(uint8_t instance) {
return probe_model_is_left(instance) ? 0x07u : 0x08u;
}
static inline uint8_t probe_model_imu_length_offset(uint8_t instance) {
return probe_model_is_left(instance) ? 14u : 15u;
}
static inline uint8_t probe_model_imu_data_offset(uint8_t instance) {
return probe_model_is_left(instance) ? 15u : 16u;
}

View file

@ -3,6 +3,78 @@
set(SWITCH2_USB_PROBE_DIR "${CMAKE_CURRENT_LIST_DIR}")
set(PICO_MBEDTLS_CONFIG_FILE "${SWITCH2_USB_PROBE_DIR}/mbedtls_config.h")
option(SWITCH2_PROBE_COMPOSITE
"Experiment: independent right and left Joy-Con 2 functions on one USB port" OFF)
option(SWITCH2_PROBE_HUB "Native R/L devices on the built-in SIO USB hub" OFF)
if(SWITCH2_PROBE_HUB AND SWITCH2_PROBE_COMPOSITE)
message(FATAL_ERROR "Select native hub or composite, not both")
endif()
option(SWITCH2_PROBE_JOIN_CHORD_GATE
"Experiment: pass L/R shoulder presses only while both physical halves hold them" OFF)
set(SWITCH2_PROBE_SIDE "RIGHT" CACHE STRING "Primary Joy-Con 2 model: LEFT or RIGHT")
set_property(CACHE SWITCH2_PROBE_SIDE PROPERTY STRINGS LEFT RIGHT)
if(SWITCH2_PROBE_SIDE STREQUAL "LEFT")
if(SWITCH2_PROBE_COMPOSITE OR SWITCH2_PROBE_HUB OR SWITCH2_BRIDGE_WII_INPUT)
message(FATAL_ERROR "SWITCH2_PROBE_SIDE=LEFT cannot be combined with composite or Wii input; select RIGHT")
endif()
set(probe_joycon_left 1)
elseif(SWITCH2_PROBE_SIDE STREQUAL "RIGHT")
set(probe_joycon_left 0)
else()
message(FATAL_ERROR "SWITCH2_PROBE_SIDE must be LEFT or RIGHT")
endif()
if(SWITCH2_PROBE_COMPOSITE OR SWITCH2_PROBE_HUB)
if(NOT SWITCH_PICO_SWITCH2_USB_BRIDGE OR SWITCH2_BRIDGE_WII_INPUT
OR NOT SWITCH2_BRIDGE_INPUT STREQUAL "JOYCON2")
message(FATAL_ERROR "Composite Joy-Con 2 requires SWITCH_PICO_SWITCH2_USB_BRIDGE=ON and SWITCH2_BRIDGE_INPUT=JOYCON2")
endif()
set(probe_composite 0)
if(SWITCH2_PROBE_COMPOSITE)
set(probe_composite 1)
endif()
set(probe_controller_count 2)
else()
set(probe_composite 0)
set(probe_controller_count 1)
endif()
if(SWITCH2_PROBE_JOIN_CHORD_GATE AND NOT SWITCH2_PROBE_COMPOSITE)
message(FATAL_ERROR "The L+R shoulder gate requires the composite Joy-Con bridge")
endif()
# Capture, the Bluetooth backend, and TinyUSB must agree before their targets exist.
add_compile_definitions(
SWITCH2_PROBE_JOYCON_LEFT=${probe_joycon_left}
SWITCH2_PROBE_COMPOSITE=${probe_composite}
SWITCH2_PROBE_HUB=$<BOOL:${SWITCH2_PROBE_HUB}>
PROBE_CONTROLLER_COUNT=${probe_controller_count})
set(SWITCH2_BRIDGE_SOURCE_ADDRESS "" CACHE STRING
"Primary physical Bluetooth source address (xx:xx:xx:xx:xx:xx)")
set(SWITCH2_BRIDGE_SECOND_SOURCE_ADDRESS "" CACHE STRING
"Secondary left physical Bluetooth source address (xx:xx:xx:xx:xx:xx)")
if(SWITCH_PICO_SWITCH2_USB_BRIDGE OR SWITCH2_PROBE_COMPOSITE)
set(probe_source_fields SWITCH2_BRIDGE_SOURCE_ADDRESS)
if(SWITCH2_PROBE_COMPOSITE OR SWITCH2_PROBE_HUB)
list(APPEND probe_source_fields SWITCH2_BRIDGE_SECOND_SOURCE_ADDRESS)
endif()
set(probe_source_addresses "")
foreach(field IN LISTS probe_source_fields)
string(TOLOWER "${${field}}" source_address)
string(LENGTH "${source_address}" source_address_length)
if(NOT source_address_length EQUAL 17
OR NOT source_address MATCHES "^([0-9a-f][0-9a-f]:)+[0-9a-f][0-9a-f]$"
OR source_address STREQUAL "00:00:00:00:00:00"
OR source_address STREQUAL "ff:ff:ff:ff:ff")
message(FATAL_ERROR "Provide ${field} as a physical six-byte Bluetooth address")
endif()
if(source_address IN_LIST probe_source_addresses)
message(FATAL_ERROR "Composite physical source addresses must be distinct")
endif()
list(APPEND probe_source_addresses "${source_address}")
string(REPLACE ":" ",0x" ${field}_BYTES "${source_address}")
string(PREPEND ${field}_BYTES "0x")
endforeach()
endif()
function(switch2_usb_probe_configure target)
set(probe_sources
${SWITCH2_USB_PROBE_DIR}/main.c
@ -10,6 +82,9 @@ function(switch2_usb_probe_configure target)
${SWITCH2_USB_PROBE_DIR}/storage.cpp
${SWITCH2_USB_PROBE_DIR}/button_test.c)
target_compile_features(${target} PRIVATE c_std_11 cxx_std_17)
if(SWITCH2_PROBE_JOIN_CHORD_GATE)
target_compile_definitions(${target} PRIVATE SWITCH2_PROBE_JOIN_CHORD_GATE=1)
endif()
target_include_directories(${target} PRIVATE
${SWITCH2_USB_PROBE_DIR}
${SWITCH2_USB_PROBE_DIR}/../../src/firmware
@ -81,64 +156,13 @@ function(switch2_usb_probe_configure target)
target_compile_definitions(${target} PRIVATE SWITCH2_PROBE_ZERO_NATIVE_IMU_PAYLOAD=1)
endif()
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()
foreach(prefix IN ITEMS SWITCH2_PROBE SWITCH2_PROBE_SECOND)
set(${prefix}_IDENTITY_FILE "" CACHE FILEPATH "64-byte matching Joy-Con 2 factory-format identity block")
set(${prefix}_VERSION_FILE "" CACHE FILEPATH "Captured 12-byte matching Joy-Con 2 firmware-version reply")
set(${prefix}_CONTROLLER_ADDRESS "" CACHE STRING "Advertised controller address (captured or distinct virtual identity)")
set(${prefix}_FACTORY_FILE "" CACHE FILEPATH "8192-byte captured factory region with configured virtual identity")
set(${prefix}_USER_CALIBRATION_FILE "" CACHE FILEPATH "4096-byte captured user calibration region")
endforeach()
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)
@ -153,29 +177,130 @@ function(switch2_usb_probe_configure target)
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")
set(probe_capture_prefixes SWITCH2_PROBE)
if(SWITCH2_PROBE_COMPOSITE OR SWITCH2_PROBE_HUB)
list(APPEND probe_capture_prefixes SWITCH2_PROBE_SECOND)
endif()
set(identity_rows "")
set(status_rows "")
set(firmware_rows "")
set(factory_rows "")
set(user_calibration_rows "")
set(controller_addresses "")
foreach(prefix IN LISTS probe_capture_prefixes)
if(probe_joycon_left OR prefix STREQUAL "SWITCH2_PROBE_SECOND")
set(probe_model "Joy-Con 2 (L)")
set(probe_vid_pid "7e056720")
set(probe_firmware_type "00")
else()
set(probe_model "Joy-Con 2 (R)")
set(probe_vid_pid "7e056620")
set(probe_firmware_type "01")
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")
if(SWITCH2_PROBE_COMPOSITE OR SWITCH2_PROBE_HUB)
foreach(field IDENTITY_FILE VERSION_FILE FACTORY_FILE USER_CALIBRATION_FILE CONTROLLER_ADDRESS)
if(NOT ${prefix}_${field})
message(FATAL_ERROR "Composite ${probe_model} requires ${prefix}_${field}")
endif()
endforeach()
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")
if(${prefix}_IDENTITY_FILE)
file(READ "${${prefix}_IDENTITY_FILE}" identity_hex LIMIT 65 HEX)
string(LENGTH "${identity_hex}" identity_length)
if(NOT identity_length EQUAL 128)
message(FATAL_ERROR "${prefix}_IDENTITY_FILE 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 probe_vid_pid)
message(FATAL_ERROR "${prefix}_IDENTITY_FILE must match selected model ${probe_model}")
endif()
string(REGEX REPLACE "([0-9a-f][0-9a-f])" "0x\\1," identity_bytes "${identity_hex}")
string(APPEND identity_rows " {${identity_bytes}},\n")
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${${prefix}_IDENTITY_FILE}")
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}")
if(${prefix}_VERSION_FILE)
if(NOT ${prefix}_IDENTITY_FILE)
message(FATAL_ERROR "${prefix}_VERSION_FILE requires the matching identity capture")
endif()
file(READ "${${prefix}_VERSION_FILE}" version_hex LIMIT 13 HEX)
string(LENGTH "${version_hex}" version_length)
if(NOT version_length EQUAL 24)
message(FATAL_ERROR "${prefix}_VERSION_FILE 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 probe_firmware_type)
message(FATAL_ERROR "${prefix}_VERSION_FILE must describe selected model ${probe_model}")
endif()
string(REPLACE ":" "" address_hex "${${prefix}_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]+$"
OR address_hex STREQUAL "000000000000" OR address_hex STREQUAL "ffffffffffff")
message(FATAL_ERROR "Provide ${prefix}_CONTROLLER_ADDRESS as a six-byte advertised Bluetooth address")
endif()
if(address_hex IN_LIST controller_addresses)
message(FATAL_ERROR "Composite advertised controller addresses must be distinct")
endif()
list(APPEND controller_addresses "${address_hex}")
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}")
string(APPEND status_rows " {${status_bytes}},\n")
string(APPEND firmware_rows " {${firmware_bytes}},\n")
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${${prefix}_VERSION_FILE}")
endif()
if(${prefix}_FACTORY_FILE OR ${prefix}_USER_CALIBRATION_FILE)
if(NOT SWITCH2_PROBE_USB_INIT OR NOT ${prefix}_FACTORY_FILE OR NOT ${prefix}_USER_CALIBRATION_FILE)
message(FATAL_ERROR "${prefix} memory replies require initialized USB and both calibration captures")
endif()
file(READ "${${prefix}_FACTORY_FILE}" factory_hex LIMIT 8193 HEX)
file(READ "${${prefix}_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 "${prefix} factory/user captures must contain exactly 8192/4096 bytes")
endif()
string(TOLOWER "${factory_hex}" factory_hex)
string(TOLOWER "${user_calibration_hex}" user_calibration_hex)
string(SUBSTRING "${factory_hex}" 0 128 factory_identity_hex)
if(NOT factory_identity_hex STREQUAL identity_hex)
message(FATAL_ERROR "${prefix} 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}")
string(APPEND factory_rows " {${factory_bytes}},\n")
string(APPEND user_calibration_rows " {${user_calibration_bytes}},\n")
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS
"${${prefix}_FACTORY_FILE}" "${${prefix}_USER_CALIBRATION_FILE}")
endif()
endforeach()
set(capture_header "// Generated from private, read-only controller captures; do not commit.\n#include <stdint.h>\n#include \"model.h\"\n")
if(SWITCH2_PROBE_IDENTITY_FILE)
file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/probe_identity.h"
"${capture_header}static const uint8_t probe_identity_replies[PROBE_CONTROLLER_COUNT][64] = {\n${identity_rows}};\n")
target_compile_definitions(${target} PRIVATE SWITCH2_PROBE_IDENTITY_REPLY=1)
endif()
if(SWITCH2_PROBE_VERSION_FILE)
file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/probe_version.h"
"${capture_header}static const uint8_t probe_version_replies[PROBE_CONTROLLER_COUNT][16] = {\n${status_rows}};\nstatic const uint8_t probe_firmware_versions[PROBE_CONTROLLER_COUNT][12] = {\n${firmware_rows}};\n")
target_compile_definitions(${target} PRIVATE SWITCH2_PROBE_VERSION_REPLY=1)
endif()
if(SWITCH2_PROBE_FACTORY_FILE)
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}")
"${capture_header}static const uint8_t probe_factory_memories[PROBE_CONTROLLER_COUNT][8192] = {\n${factory_rows}};\nstatic const uint8_t probe_user_calibrations[PROBE_CONTROLLER_COUNT][4096] = {\n${user_calibration_rows}};\n")
list(APPEND probe_sources ${SWITCH2_USB_PROBE_DIR}/memory.c)
target_compile_definitions(${target} PRIVATE SWITCH2_PROBE_MEMORY=1)
endif()
@ -189,17 +314,69 @@ function(switch2_usb_probe_configure target)
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)
hardware_flash pico_flash pico_mbedtls_crypto pico_mbedtls_headers)
if(SWITCH2_PROBE_HUB)
target_sources(${target} PRIVATE
${SWITCH2_USB_PROBE_DIR}/../pico_usb_address_probe/router.c
${SWITCH2_USB_PROBE_DIR}/../../src/firmware/usb/native_hub/native_hub.c)
target_include_directories(${target} PRIVATE
${SWITCH2_USB_PROBE_DIR}/../pico_usb_address_probe
${PICO_SDK_PATH}/lib/tinyusb/src)
target_compile_definitions(${target} PRIVATE CFG_TUSB_MCU=OPT_MCU_RP2040)
target_link_libraries(${target} PRIVATE pico_multicore pico_unique_id hardware_irq hardware_resets)
set_source_files_properties(
${SWITCH2_USB_PROBE_DIR}/../pico_usb_address_probe/router.c
${SWITCH2_USB_PROBE_DIR}/../../src/firmware/usb/native_hub/native_hub.c
PROPERTIES COMPILE_OPTIONS "-O3;-fno-jump-tables;-Wall;-Wextra;-Werror")
# Core0 now owns the Bluetooth call stack. Reserve16KiB from main
# SRAM instead of overflowing the SDK's4KiB scratch stack region.
set(default_linker "${PICO_SDK_PATH}/src/rp2_common/pico_crt0/rp2350/memmap_default.ld")
file(READ "${default_linker}" hub_linker)
string(REPLACE "RAM(rwx) : ORIGIN = 0x20000000, LENGTH = 512k"
"RAM(rwx) : ORIGIN = 0x20000000, LENGTH = 496k\n MAIN_STACK(rwx) : ORIGIN = 0x2007c000, LENGTH = 16k"
hub_linker "${hub_linker}")
string(REPLACE "KEEP(*(.stack*))\n } > SCRATCH_Y"
"KEEP(*(.stack*))\n } > MAIN_STACK" hub_linker "${hub_linker}")
string(REPLACE "__StackTop = ORIGIN(SCRATCH_Y) + LENGTH(SCRATCH_Y);"
"__StackTop = ORIGIN(MAIN_STACK) + LENGTH(MAIN_STACK);" hub_linker "${hub_linker}")
if(NOT hub_linker MATCHES "MAIN_STACK")
message(FATAL_ERROR "SDK linker stack layout changed")
endif()
file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/native_hub_stack.ld" "${hub_linker}")
pico_set_linker_script(${target} "${CMAKE_CURRENT_BINARY_DIR}/native_hub_stack.ld")
else()
target_link_libraries(${target} PRIVATE tinyusb_device)
endif()
pico_enable_stdio_usb(${target} 0)
pico_enable_stdio_uart(${target} 1)
if(SWITCH2_BRIDGE_WII_INPUT)
if(SWITCH2_PROBE_HUB)
pico_set_program_name(${target} "Native Joy-Con 2 R and L stock USB hub bridge")
elseif(SWITCH2_PROBE_COMPOSITE)
pico_set_program_name(${target} "Switch 2 right and left Joy-Con composite bridge")
elseif(SWITCH2_BRIDGE_WII_INPUT)
pico_set_program_name(${target} "Switch 2 Wii IR and native motion bridge")
elseif(SWITCH_PICO_SWITCH2_USB_BRIDGE AND probe_joycon_left)
pico_set_program_name(${target} "Switch 2 left Joy-Con Bluetooth bridge")
elseif(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()
if(SWITCH2_PROBE_OMIT_NATIVE_IMU)
if(SWITCH2_PROBE_HUB)
pico_set_program_version(${target} "0.66-native-hub-input")
elseif(SWITCH2_PROBE_JOIN_CHORD_GATE)
if(SWITCH2_PROBE_TRACE_NATIVE_INPUT)
pico_set_program_version(${target} "0.37-pair-chord-trace")
else()
pico_set_program_version(${target} "0.37-pair-chord")
endif()
elseif(SWITCH2_PROBE_COMPOSITE)
if(SWITCH2_PROBE_TRACE_NATIVE_INPUT)
pico_set_program_version(${target} "0.35-pair-trace")
else()
pico_set_program_version(${target} "0.35-pair")
endif()
elseif(SWITCH2_PROBE_OMIT_NATIVE_IMU)
pico_set_program_version(${target} "0.24-no-imu")
elseif(SWITCH2_PROBE_ZERO_NATIVE_IMU_PAYLOAD)
pico_set_program_version(${target} "0.24-zero-imu-payload")
@ -209,6 +386,12 @@ function(switch2_usb_probe_configure target)
else()
pico_set_program_version(${target} "0.33-wii")
endif()
elseif(SWITCH_PICO_SWITCH2_USB_BRIDGE AND probe_joycon_left)
if(SWITCH2_PROBE_TRACE_NATIVE_INPUT)
pico_set_program_version(${target} "0.34-left-trace")
else()
pico_set_program_version(${target} "0.34-left")
endif()
elseif(SWITCH_PICO_SWITCH2_USB_BRIDGE)
if(SWITCH2_PROBE_TRACE_NATIVE_INPUT)
pico_set_program_version(${target} "0.25-trace")

View file

@ -3,7 +3,7 @@
#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
// 03/0A, 07/01, 09/01-08, 16/01, 15/01-04) and hid_reports.md (05/07/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] = {
@ -71,7 +71,7 @@ static bool finalize_pairing(probe_protocol_state* state, const uint8_t* key) {
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;
if (!state->save_pairing(state->context, 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));
@ -81,11 +81,12 @@ static bool finalize_pairing(probe_protocol_state* state, const uint8_t* key) {
return true;
}
void probe_protocol_reset(probe_protocol_state* state) {
void probe_protocol_reset(probe_protocol_state* state, bool is_left) {
memset(state, 0, sizeof(*state));
state->report_id = 0x08;
state->right_stick_center[1] = 0x08;
state->right_stick_center[2] = 0x80;
state->is_left = is_left;
state->report_id = is_left ? 0x07 : 0x08;
state->stick_center[1] = 0x08;
state->stick_center[2] = 0x80;
}
bool probe_protocol_restore_pairing(probe_protocol_state* state,
@ -226,14 +227,14 @@ size_t probe_protocol_command(probe_protocol_state* state, const uint8_t* comman
if (capacity < reply_length) return 0;
if (vibration_sample) {
uint64_t token = 0;
if (!state->play_sample(command[8], &token) || !token) return 0;
if (!state->play_sample(state->context, 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;
!state->read_memory(state->context, 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) {
@ -255,7 +256,8 @@ size_t probe_protocol_command(probe_protocol_state* state, const uint8_t* comman
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];
if (command[8] == 0x05 || command[8] == (state->is_left ? 0x07 : 0x08))
state->report_id = command[8];
} else if (exchange_addresses) {
if (length != 8) {
clear_pending_pairing(state);
@ -337,34 +339,48 @@ size_t probe_protocol_command(probe_protocol_state* state, const uint8_t* comman
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;
(report_id != 0x05 && report_id != (state->is_left ? 0x07 : 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 buttons1 = state->controller_active && buttons_enabled ?
state->controller_buttons[1] & (state->is_left ? 0xc1 : 0xd1) : 0;
const uint8_t* stick = state->controller_active && (state->enabled_features & 2) ?
state->controller_stick : state->right_stick_center;
if (report_id == 0x08) {
state->controller_stick : state->stick_center;
if (report_id != 0x05) {
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[3] |= 0xc0; // Both models' 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);
if (state->is_left) {
output[5] = (uint8_t)(((buttons0 & 0x40) >> 6) | ((buttons0 & 0x80) >> 4) |
((buttons1 & 0x01) << 5));
output[6] = (uint8_t)((buttons0 & 0x01) | ((buttons0 & 0x06) << 1) |
((buttons0 & 0x08) >> 2) | ((buttons0 & 0x30) << 2) |
((buttons1 & 0xc0) >> 2));
if (state->test_rail_buttons && buttons_enabled)
output[6] |= 0x30; // Common report: left SL + SR.
memcpy(output + 10, stick, 3);
output[14] = 0x08;
output[15] = 0x80;
} else {
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 && buttons_enabled)
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;
@ -372,3 +388,22 @@ size_t probe_protocol_report(const probe_protocol_state* state, uint8_t report_i
}
return PROBE_INPUT_SIZE;
}
void probe_protocol_gate_native_report(const probe_protocol_state* state,
uint8_t input[PROBE_INPUT_SIZE]) {
const uint8_t imu_length_offset = state->is_left ? 14u : 15u;
if (!(state->enabled_features & 1)) memset(input + 2, 0, 2);
if (!(state->enabled_features & 2))
memcpy(input + 5, state->stick_center, sizeof(state->stick_center));
if (!(state->enabled_features & 0x10)) memset(input + 9, 0, 5);
#ifdef SWITCH2_PROBE_OMIT_NATIVE_IMU
// Deliberate A/B fault injection: leave every other field and feature bit intact.
memset(input + imu_length_offset, 0, 41);
#elif defined(SWITCH2_PROBE_ZERO_NATIVE_IMU_PAYLOAD)
if (!(state->enabled_features & 4)) input[imu_length_offset] = 0;
memset(input + imu_length_offset + 1u, 0, 40); // Preserve enabled genuine length.
#else
if (!(state->enabled_features & 4))
memset(input + imu_length_offset, 0, 41);
#endif
}

View file

@ -2,6 +2,7 @@
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include "model.h"
#define PROBE_COMMAND_MAX_SIZE 263u
#define PROBE_REPLY_MAX_SIZE 96u
@ -10,13 +11,15 @@
#define PROBE_INPUT_SIZE 63u
typedef struct {
bool is_left;
void* context; // Caller-owned context shared by this state's callbacks.
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];
uint8_t stick_center[3];
bool controller_active;
uint8_t controller_buttons[2]; // Native right Joy-Con button ordering.
uint8_t controller_buttons[2]; // Selected model's native 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;
@ -39,15 +42,15 @@ typedef struct {
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);
bool (*save_pairing)(void* context, const uint8_t* blob, size_t length);
bool (*read_memory)(void* context, 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);
bool (*play_sample)(void* context, uint8_t sample_id, uint64_t* token);
uint32_t report_counter;
} probe_protocol_state;
void probe_protocol_reset(probe_protocol_state* state);
void probe_protocol_reset(probe_protocol_state* state, bool is_left);
// Blob: own address[6], count[1], zero-padded host addresses[42][6], AES key[16].
// Rejects other identities, invalid counts/padding/lengths without mutation.
// A successful restore replaces the committed record and clears pending state.
@ -66,3 +69,7 @@ size_t probe_protocol_command(probe_protocol_state* state, const uint8_t* comman
// 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);
// Apply virtual feature gates to one complete native payload in place.
// Enabled mouse/motion and all opaque bytes remain unchanged.
void probe_protocol_gate_native_report(const probe_protocol_state* state,
uint8_t input[PROBE_INPUT_SIZE]);

View file

@ -1,4 +1,5 @@
#include "storage.h"
#include "model.h"
#include <string.h>
@ -16,13 +17,16 @@ namespace {
constexpr size_t kSlotCount = 2;
constexpr size_t kMaximumPayloadSize = 512;
constexpr size_t kStorageSize = kSlotCount * FLASH_SECTOR_SIZE;
constexpr size_t kReservedStorageSize = 2 * kStorageSize;
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;
// Keep the original right bank adjacent to profiles; reserve the left bank below.
constexpr uint32_t kRightStorageOffset = kProfileStorageOffset - kStorageSize;
constexpr uint32_t kLeftStorageOffset = kRightStorageOffset - kStorageSize;
constexpr uint32_t kFlashSafeTimeoutMs = 5000;
constexpr uint32_t kFormatVersion = 1;
@ -66,9 +70,11 @@ 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,
kReservedStorageSize,
"pairing storage offset underflows flash");
static_assert(kStorageOffset + kStorageSize == kProfileStorageOffset);
static_assert(kLeftStorageOffset + kStorageSize == kRightStorageOffset);
static_assert(kRightStorageOffset + kStorageSize == kProfileStorageOffset);
static_assert(kLeftStorageOffset + kReservedStorageSize == kProfileStorageOffset);
static_assert(kProfileStorageOffset + PROFILE_STORAGE_TOTAL_SIZE ==
kConfigurationStorageOffset);
static_assert(kConfigurationStorageOffset + kConfigurationStorageSize ==
@ -119,22 +125,23 @@ bool is_erased(const uint8_t *bytes, size_t size) {
return true;
}
bool storage_region_available() {
bool storage_region_available(uint32_t storage_offset) {
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;
binary_end - XIP_BASE <= kLeftStorageOffset &&
storage_offset % FLASH_SECTOR_SIZE == 0 &&
storage_offset <= PICO_FLASH_SIZE_BYTES &&
kStorageSize <= PICO_FLASH_SIZE_BYTES - storage_offset &&
storage_offset >= kLeftStorageOffset &&
storage_offset + kStorageSize <= kProfileStorageOffset;
}
uint32_t slot_offset(size_t slot) {
return static_cast<uint32_t>(kStorageOffset + slot * FLASH_SECTOR_SIZE);
uint32_t slot_offset(uint32_t storage_offset, size_t slot) {
return static_cast<uint32_t>(storage_offset + slot * FLASH_SECTOR_SIZE);
}
const uint8_t *slot_bytes(size_t slot) {
return reinterpret_cast<const uint8_t *>(XIP_BASE + slot_offset(slot));
const uint8_t *slot_bytes(uint32_t storage_offset, size_t slot) {
return reinterpret_cast<const uint8_t *>(XIP_BASE + slot_offset(storage_offset, slot));
}
bool owner_valid(const uint8_t *bytes, uint32_t offset) {
@ -180,10 +187,10 @@ bool commit_valid(const uint8_t *bytes, uint32_t offset) {
FLASH_PAGE_SIZE - kDescriptorSize);
}
Slot inspect_slot(size_t index) {
const uint8_t *bytes = slot_bytes(index);
Slot inspect_slot(uint32_t storage_offset, size_t index) {
const uint8_t *bytes = slot_bytes(storage_offset, index);
Slot slot{SlotKind::Unknown, bytes, 0, 0};
if (!owner_valid(bytes, slot_offset(index))) {
if (!owner_valid(bytes, slot_offset(storage_offset, index))) {
if (is_erased(bytes, FLASH_SECTOR_SIZE)) {
slot.kind = SlotKind::Erased;
}
@ -198,7 +205,7 @@ Slot inspect_slot(size_t index) {
// 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))) {
if (body_valid(bytes) && commit_valid(bytes, slot_offset(storage_offset, index))) {
slot.kind = SlotKind::Committed;
slot.generation = read_u32(bytes + kBodyOffset + 8);
slot.size = read_u32(bytes + kBodyOffset + 16);
@ -245,37 +252,37 @@ void perform_flash_mutation(void *context) {
// 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()) {
bool erase_slot(uint32_t storage_offset, size_t index) {
if (index >= kSlotCount || !storage_region_available(storage_offset)) {
return false;
}
FlashMutation mutation{slot_offset(index), nullptr};
FlashMutation mutation{slot_offset(storage_offset, index), nullptr};
return flash_safe_execute(perform_flash_mutation, &mutation,
kFlashSafeTimeoutMs) == PICO_OK &&
is_erased(slot_bytes(index), FLASH_SECTOR_SIZE);
is_erased(slot_bytes(storage_offset, index), FLASH_SECTOR_SIZE);
}
bool program_page(size_t index, size_t offset, const uint8_t *page) {
bool program_page(uint32_t storage_offset, 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)) {
!storage_region_available(storage_offset) ||
!is_erased(slot_bytes(storage_offset, index) + offset, FLASH_PAGE_SIZE)) {
return false;
}
FlashMutation mutation{
static_cast<uint32_t>(slot_offset(index) + offset), page,
static_cast<uint32_t>(slot_offset(storage_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;
memcmp(slot_bytes(storage_offset, index) + offset, page, FLASH_PAGE_SIZE) == 0;
}
void prepare_record(size_t target, uint32_t generation,
void prepare_record(uint32_t storage_offset, 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 + 20, slot_offset(storage_offset, target));
write_u32(staging + 24, kMaximumPayloadSize);
write_u32(staging + 28, FLASH_PAGE_SIZE);
write_u32(staging + 32, FLASH_SECTOR_SIZE);
@ -300,19 +307,23 @@ void prepare_record(size_t target, uint32_t 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 + 32, slot_offset(storage_offset, target));
write_u32(commit + kDescriptorCrcOffset,
configuration_crc32(commit, kDescriptorCrcOffset));
}
} // namespace
bool probe_storage_load(uint8_t *output, size_t size) {
bool probe_storage_load(uint8_t instance, uint8_t *output, size_t size) {
if (instance >= PROBE_CONTROLLER_COUNT) return false;
const uint32_t storage_offset = probe_storage_offset(instance);
if (output == nullptr || size == 0 || size > kMaximumPayloadSize ||
!storage_region_available()) {
!storage_region_available(storage_offset)) {
return false;
}
const Slot slots[kSlotCount] = {inspect_slot(0), inspect_slot(1)};
const Slot slots[kSlotCount] = {
inspect_slot(storage_offset, 0), inspect_slot(storage_offset, 1),
};
int active;
if (!newest_slot(slots, &active) || active < 0 || slots[active].size != size) {
return false;
@ -321,12 +332,16 @@ bool probe_storage_load(uint8_t *output, size_t size) {
return true;
}
bool probe_storage_save(const uint8_t *data, size_t size) {
bool probe_storage_save(uint8_t instance, const uint8_t *data, size_t size) {
if (instance >= PROBE_CONTROLLER_COUNT) return false;
const uint32_t storage_offset = probe_storage_offset(instance);
if (data == nullptr || size == 0 || size > kMaximumPayloadSize ||
!storage_region_available()) {
!storage_region_available(storage_offset)) {
return false;
}
const Slot slots[kSlotCount] = {inspect_slot(0), inspect_slot(1)};
const Slot slots[kSlotCount] = {
inspect_slot(storage_offset, 0), inspect_slot(storage_offset, 1),
};
if (slots[0].kind == SlotKind::Unknown || slots[1].kind == SlotKind::Unknown) {
return false; // Never erase through an unrecognized region.
}
@ -342,32 +357,33 @@ bool probe_storage_save(const uint8_t *data, size_t size) {
? 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);
prepare_record(storage_offset, target, generation, data, size);
if (slots[target].kind != SlotKind::Erased && !erase_slot(target)) {
if (slots[target].kind != SlotKind::Erased && !erase_slot(storage_offset, target)) {
return false;
}
if (!program_page(target, 0, staging)) {
if (!program_page(storage_offset, 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)) {
!program_page(storage_offset, target, offset, staging + offset)) {
return false;
}
}
if (!body_valid(slot_bytes(target)) ||
!program_page(target, kCommitOffset, staging + kCommitOffset)) {
if (!body_valid(slot_bytes(storage_offset, target)) ||
!program_page(storage_offset, target, kCommitOffset, staging + kCommitOffset)) {
return false;
}
const Slot committed = inspect_slot(target);
const Slot committed = inspect_slot(storage_offset, 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;
uint32_t probe_storage_offset(uint8_t instance) {
if (instance >= PROBE_CONTROLLER_COUNT) return UINT32_MAX;
return probe_model_is_left(instance) ? kLeftStorageOffset : kRightStorageOffset;
}

View file

@ -11,14 +11,18 @@ extern "C" {
// 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);
// Invalid instances fail before reading a bank or writing output.
bool probe_storage_load(uint8_t instance, 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);
bool probe_storage_save(uint8_t instance, const uint8_t *data, size_t size);
// Flash-relative offset of the two sectors immediately below profile storage.
uint32_t probe_storage_offset(void);
// Flash-relative offset of the instance's two-sector pairing bank, or UINT32_MAX
// for an invalid instance. The right bank remains immediately below profile
// storage; the left bank occupies the preceding two sectors. Both are reserved
// in every build, and load/save inspect and mutate only the selected bank.
uint32_t probe_storage_offset(uint8_t instance);
#ifdef __cplusplus
}

View file

@ -0,0 +1,101 @@
#pragma once
#include "model.h"
#include "tusb.h"
#if SWITCH2_PROBE_HUB
#include "usb/native_hub/native_hub.h"
#endif
// Application instances are controllers, never native hub device slots.
// Only control transfers retain the transport's rhport/device-slot argument.
static inline bool probe_transport_mounted(uint8_t instance) {
#if SWITCH2_PROBE_HUB
return native_hub_mounted(instance);
#else
(void)instance;
return tud_mounted();
#endif
}
static inline bool probe_transport_suspended(uint8_t instance) {
#if SWITCH2_PROBE_HUB
return native_hub_suspended(instance);
#else
(void)instance;
return tud_suspended();
#endif
}
static inline bool probe_transport_hid_ready(uint8_t instance) {
#if SWITCH2_PROBE_HUB
return native_hub_hid_ready(instance);
#else
return tud_hid_n_ready(instance);
#endif
}
static inline bool probe_transport_hid_report(uint8_t instance, uint8_t report_id,
const void* data, uint16_t length) {
#if SWITCH2_PROBE_HUB
return native_hub_hid_report(instance, report_id, data, length);
#else
return tud_hid_n_report(instance, report_id, data, length);
#endif
}
static inline uint32_t probe_transport_vendor_write_available(uint8_t instance) {
#if SWITCH2_PROBE_HUB
return native_hub_vendor_write_available(instance);
#else
return tud_vendor_n_write_available(instance);
#endif
}
static inline uint32_t probe_transport_vendor_write(uint8_t instance,
const void* data, uint32_t length) {
#if SWITCH2_PROBE_HUB
return native_hub_vendor_write(instance, data, length);
#else
return tud_vendor_n_write(instance, data, length);
#endif
}
static inline uint32_t probe_transport_vendor_write_flush(uint8_t instance) {
#if SWITCH2_PROBE_HUB
return native_hub_vendor_write_flush(instance);
#else
return tud_vendor_n_write_flush(instance);
#endif
}
static inline void probe_transport_vendor_discard_received(uint8_t instance) {
#if SWITCH2_PROBE_HUB
// Native RX supplies the actual packet once, with no second receive FIFO.
(void)instance;
#else
// The application consumes the raw callback packet, not this duplicate.
uint8_t discarded[64];
while (tud_vendor_n_available(instance)) {
if (!tud_vendor_n_read(instance, discarded, sizeof(discarded))) break;
}
#endif
}
static inline bool probe_transport_control_xfer(uint8_t rhport,
const tusb_control_request_t* request,
void* buffer, uint16_t length) {
#if SWITCH2_PROBE_HUB
return native_hub_control_xfer(rhport, request, buffer, length);
#else
return tud_control_xfer(rhport, request, buffer, length);
#endif
}
static inline bool probe_transport_control_status(uint8_t rhport,
const tusb_control_request_t* request) {
#if SWITCH2_PROBE_HUB
return native_hub_control_status(rhport, request);
#else
return tud_control_status(rhport, request);
#endif
}

View file

@ -1,16 +1,18 @@
#pragma once
#include "model.h"
#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 PROBE_CONTROLLER_COUNT
#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 PROBE_CONTROLLER_COUNT
#define CFG_TUD_VENDOR_EPSIZE 64
#define CFG_TUD_VENDOR_RX_BUFSIZE 256
#define CFG_TUD_VENDOR_TX_BUFSIZE 256