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
|
|
@ -28,6 +28,12 @@ option(SWITCH_PICO_WII_IR_MOUSE
|
|||
"Experiment: Wii IR as a composite USB mouse with Switch controllers" OFF)
|
||||
option(SWITCH_PICO_WII_IR_GYRO
|
||||
"Experiment: selectable Wii IR or MotionPlus gyro aiming" OFF)
|
||||
option(SWITCH_PICO_SWITCH2_MOUSE_CAPTURE
|
||||
"Experiment: capture Joy-Con 2 mouse reports through USB management" OFF)
|
||||
option(SWITCH_PICO_SWITCH2_MOUSE_CAPTURE_NATIVE
|
||||
"Capture native Joy-Con mouse reports instead of common controller reports" OFF)
|
||||
option(SWITCH_PICO_SWITCH2_USB_BRIDGE
|
||||
"Experiment: forward a native right Joy-Con 2 through the verified USB probe" OFF)
|
||||
if(SWITCH_PICO_INPUT_BACKEND STREQUAL "BLUEPAD32")
|
||||
set(SWITCH_PICO_NATIVE_DEFAULT ON)
|
||||
set(SWITCH_PICO_CLOCK_DEFAULT 300)
|
||||
|
|
@ -131,6 +137,40 @@ endif()
|
|||
if(SWITCH_PICO_WII_IR_MOUSE)
|
||||
add_compile_definitions(SWITCH_PICO_WII_IR_MOUSE=1)
|
||||
endif()
|
||||
if(SWITCH_PICO_SWITCH2_MOUSE_CAPTURE)
|
||||
if(NOT SWITCH_PICO_INPUT_BACKEND STREQUAL "BLUEPAD32"
|
||||
OR NOT SWITCH_PICO_ENABLE_BLE)
|
||||
message(FATAL_ERROR "Joy-Con mouse capture requires BLUEPAD32 with BLE")
|
||||
endif()
|
||||
add_compile_definitions(
|
||||
SWITCH_PICO_SWITCH2_MOUSE_CAPTURE=1
|
||||
SWITCH_PICO_SWITCH2_MOUSE_CAPTURE_NATIVE=$<BOOL:${SWITCH_PICO_SWITCH2_MOUSE_CAPTURE_NATIVE}>)
|
||||
endif()
|
||||
if(SWITCH_PICO_SWITCH2_USB_BRIDGE)
|
||||
if(NOT SWITCH_PICO_INPUT_BACKEND STREQUAL "BLUEPAD32"
|
||||
OR NOT SWITCH_PICO_ENABLE_BLE
|
||||
OR NOT SWITCH_PICO_SWITCH2_MOUSE_CAPTURE
|
||||
OR NOT SWITCH_PICO_SWITCH2_MOUSE_CAPTURE_NATIVE)
|
||||
message(FATAL_ERROR
|
||||
"Switch 2 USB bridge requires BLUEPAD32 with BLE and native Joy-Con mouse capture")
|
||||
endif()
|
||||
if(SWITCH_PICO_WII_IR_MOUSE OR SWITCH_PICO_WII_IR_GYRO)
|
||||
message(FATAL_ERROR "Switch 2 USB bridge owns USB; Wii IR USB experiments cannot be combined")
|
||||
endif()
|
||||
set(SWITCH2_BRIDGE_SOURCE_ADDRESS "" CACHE STRING
|
||||
"Physical right Joy-Con 2 Bluetooth source address (xx:xx:xx:xx:xx:xx)")
|
||||
string(TOLOWER "${SWITCH2_BRIDGE_SOURCE_ADDRESS}" bridge_source_address)
|
||||
string(LENGTH "${bridge_source_address}" bridge_source_address_length)
|
||||
if(NOT bridge_source_address_length EQUAL 17
|
||||
OR NOT bridge_source_address MATCHES "^([0-9a-f][0-9a-f]:)+[0-9a-f][0-9a-f]$"
|
||||
OR bridge_source_address STREQUAL "00:00:00:00:00:00"
|
||||
OR bridge_source_address STREQUAL "ff:ff:ff:ff:ff:ff")
|
||||
message(FATAL_ERROR "Provide SWITCH2_BRIDGE_SOURCE_ADDRESS as a physical six-byte Bluetooth address")
|
||||
endif()
|
||||
string(REPLACE ":" ",0x" SWITCH2_BRIDGE_SOURCE_ADDRESS_BYTES "${bridge_source_address}")
|
||||
string(PREPEND SWITCH2_BRIDGE_SOURCE_ADDRESS_BYTES "0x")
|
||||
add_compile_definitions(SWITCH_PICO_SWITCH2_USB_BRIDGE=1)
|
||||
endif()
|
||||
set(PICO_BOARD pico CACHE STRING "Board type")
|
||||
if(SWITCH_PICO_INPUT_BACKEND STREQUAL "BLUEPAD32"
|
||||
AND NOT PICO_BOARD STREQUAL "pico2_w")
|
||||
|
|
@ -170,6 +210,9 @@ endif()
|
|||
include(pico_sdk_import.cmake)
|
||||
|
||||
project(switch-pico C CXX ASM)
|
||||
if(SWITCH_PICO_SWITCH2_USB_BRIDGE)
|
||||
include(${CMAKE_CURRENT_LIST_DIR}/tools/switch2_usb_probe/probe_build.cmake)
|
||||
endif()
|
||||
|
||||
# Initialise the Raspberry Pi Pico SDK
|
||||
pico_sdk_init()
|
||||
|
|
@ -281,11 +324,23 @@ endif()
|
|||
set(SWITCH_PICO_SOURCE_DIR ${CMAKE_CURRENT_LIST_DIR}/src/firmware)
|
||||
|
||||
add_executable(switch-pico
|
||||
${SWITCH_PICO_SOURCE_DIR}/main.cpp
|
||||
${SWITCH_PICO_SOURCE_DIR}/usb/usb_output_driver.cpp
|
||||
${SWITCH_PICO_SOURCE_DIR}/usb/switch/switch_pro_driver.cpp
|
||||
${SWITCH_PICO_SOURCE_DIR}/usb/switch/switch_haptics.cpp
|
||||
)
|
||||
if(SWITCH_PICO_SWITCH2_USB_BRIDGE)
|
||||
switch2_usb_probe_configure(switch-pico)
|
||||
if(NOT SWITCH2_PROBE_USB_INIT OR NOT SWITCH2_PROBE_FACTORY_FILE
|
||||
OR NOT SWITCH2_PROBE_USER_CALIBRATION_FILE)
|
||||
message(FATAL_ERROR "Switch 2 USB bridge requires the initialized probe with factory and user calibration captures")
|
||||
endif()
|
||||
target_sources(switch-pico PRIVATE
|
||||
${SWITCH2_USB_PROBE_DIR}/controller_input.cpp)
|
||||
target_compile_definitions(switch-pico PRIVATE
|
||||
SWITCH2_BRIDGE_SOURCE_ADDRESS_BYTES=${SWITCH2_BRIDGE_SOURCE_ADDRESS_BYTES})
|
||||
else()
|
||||
target_sources(switch-pico PRIVATE ${SWITCH_PICO_SOURCE_DIR}/main.cpp)
|
||||
endif()
|
||||
if(SWITCH_PICO_WII_IR_MOUSE OR SWITCH_PICO_WII_IR_GYRO)
|
||||
target_sources(switch-pico PRIVATE
|
||||
${SWITCH_PICO_SOURCE_DIR}/input/wii_ir_pointer.cpp
|
||||
|
|
@ -295,6 +350,10 @@ if(SWITCH_PICO_WII_IR_MOUSE)
|
|||
target_sources(switch-pico PRIVATE
|
||||
${SWITCH_PICO_SOURCE_DIR}/usb/wii_ir_mouse_usb.cpp)
|
||||
endif()
|
||||
if(SWITCH_PICO_SWITCH2_MOUSE_CAPTURE)
|
||||
target_sources(switch-pico PRIVATE
|
||||
${SWITCH_PICO_SOURCE_DIR}/input/switch2_mouse_capture.cpp)
|
||||
endif()
|
||||
if(SWITCH_PICO_INPUT_BACKEND STREQUAL "BLUEPAD32")
|
||||
target_sources(switch-pico PRIVATE
|
||||
${SWITCH_PICO_SOURCE_DIR}/adapter/adapter_host_probe.cpp
|
||||
|
|
@ -391,8 +450,10 @@ else()
|
|||
)
|
||||
endif()
|
||||
|
||||
pico_set_program_name(switch-pico "switch-pico")
|
||||
pico_set_program_version(switch-pico "0.2.0")
|
||||
if(NOT SWITCH_PICO_SWITCH2_USB_BRIDGE)
|
||||
pico_set_program_name(switch-pico "switch-pico")
|
||||
pico_set_program_version(switch-pico "0.2.0")
|
||||
endif()
|
||||
|
||||
# Modify the below lines to enable/disable output over UART/USB
|
||||
# UART0 is enabled for debug logging; USB stdio remains off.
|
||||
|
|
@ -400,7 +461,7 @@ pico_enable_stdio_uart(switch-pico 1)
|
|||
pico_enable_stdio_usb(switch-pico 0)
|
||||
|
||||
# Add the standard library to the build
|
||||
target_link_libraries(switch-pico
|
||||
target_link_libraries(switch-pico PRIVATE
|
||||
pico_stdlib
|
||||
tinyusb_device
|
||||
tinyusb_board
|
||||
|
|
@ -408,7 +469,7 @@ target_link_libraries(switch-pico
|
|||
pico_rand
|
||||
)
|
||||
if(SWITCH_PICO_INPUT_BACKEND STREQUAL "BLUEPAD32")
|
||||
target_link_libraries(switch-pico
|
||||
target_link_libraries(switch-pico PRIVATE
|
||||
bluepad32
|
||||
pico_cyw43_arch_none
|
||||
pico_btstack_ble
|
||||
|
|
@ -417,15 +478,15 @@ if(SWITCH_PICO_INPUT_BACKEND STREQUAL "BLUEPAD32")
|
|||
pico_multicore
|
||||
pico_flash
|
||||
)
|
||||
target_link_libraries(switch-pico hardware_watchdog hardware_adc hardware_vreg hardware_powman)
|
||||
target_link_libraries(switch-pico PRIVATE hardware_watchdog hardware_adc hardware_vreg hardware_powman)
|
||||
endif()
|
||||
|
||||
if (SWITCH_PICO_LOG)
|
||||
target_compile_definitions(switch-pico PRIVATE SWITCH_PICO_LOG=1)
|
||||
endif()
|
||||
|
||||
# Internal includes are rooted at src/firmware. TinyUSB discovers tusb_config.h
|
||||
# through the Pico platform directory.
|
||||
# Internal includes are rooted at src/firmware. Ordinary builds discover
|
||||
# tusb_config.h here; the bridge selects the probe config explicitly.
|
||||
target_include_directories(switch-pico PRIVATE
|
||||
${SWITCH_PICO_SOURCE_DIR}
|
||||
${SWITCH_PICO_SOURCE_DIR}/platform/pico
|
||||
|
|
|
|||
|
|
@ -10,6 +10,9 @@
|
|||
#include <math.h>
|
||||
#include <stdatomic.h>
|
||||
#include <string.h>
|
||||
#if SWITCH_PICO_SWITCH2_MOUSE_CAPTURE
|
||||
#include <stdio.h>
|
||||
#endif
|
||||
|
||||
#include <btstack.h>
|
||||
#include "bt/uni_bt_defines.h"
|
||||
|
|
@ -33,6 +36,12 @@ static const uint8_t sw2_service_uuid[16] = {
|
|||
0xab, 0x7d, 0xe9, 0xbe, 0x89, 0xfe, 0x49, 0xad, 0x82, 0x8f, 0x11, 0x8f, 0x09, 0xdf, 0x7f, 0xd0};
|
||||
static const uint8_t sw2_input_uuid[16] = {
|
||||
0xab, 0x7d, 0xe9, 0xbe, 0x89, 0xfe, 0x49, 0xad, 0x82, 0x8f, 0x11, 0x8f, 0x09, 0xdf, 0x7f, 0xd2};
|
||||
#if SWITCH_PICO_SWITCH2_MOUSE_CAPTURE
|
||||
static const uint8_t sw2_secondary_left_uuid[16] = {
|
||||
0xcc, 0x1b, 0xbb, 0xb5, 0x73, 0x54, 0x4d, 0x32, 0xa7, 0x16, 0xa8, 0x1c, 0xb2, 0x41, 0xa3, 0x2a};
|
||||
static const uint8_t sw2_secondary_right_uuid[16] = {
|
||||
0xd5, 0xa9, 0xe0, 0x1e, 0x2f, 0xfc, 0x4c, 0xca, 0xb2, 0x0c, 0x8b, 0x67, 0x14, 0x2b, 0xf4, 0x42};
|
||||
#endif
|
||||
static const uint8_t sw2_command_uuid[16] = {
|
||||
0x64, 0x9d, 0x4a, 0xc9, 0x8e, 0xb7, 0x4e, 0x6c, 0xaf, 0x44, 0x1e, 0xa5, 0x4f, 0xe5, 0xf0, 0x05};
|
||||
static const uint8_t sw2_response_uuid[16] = {
|
||||
|
|
@ -58,6 +67,9 @@ typedef enum {
|
|||
SW2_RESPONSE_DESCRIPTOR, SW2_INPUT_DESCRIPTOR, SW2_SUBSCRIBE_RESPONSE,
|
||||
SW2_INFO, SW2_PAIR, SW2_CALIBRATION, SW2_GYRO_CALIBRATION,
|
||||
SW2_SUBSCRIBE_INPUT, SW2_FEATURES, SW2_READY,
|
||||
#if SWITCH_PICO_SWITCH2_MOUSE_CAPTURE
|
||||
SW2_SECONDARY_DESCRIPTOR, SW2_SUBSCRIBE_SECONDARY,
|
||||
#endif
|
||||
} sw2_state_t;
|
||||
typedef enum { SW2_QUERY_NONE, SW2_QUERY_DISCOVERY, SW2_QUERY_CCCD, SW2_QUERY_COMMAND, SW2_QUERY_RUMBLE } sw2_query_t;
|
||||
typedef struct {
|
||||
|
|
@ -89,12 +101,21 @@ typedef struct {
|
|||
uint16_t response_cccd, input_cccd;
|
||||
gatt_client_notification_t response_listener, input_listener;
|
||||
bool response_listening, input_listening;
|
||||
#if SWITCH_PICO_SWITCH2_MOUSE_CAPTURE
|
||||
gatt_client_characteristic_t secondary;
|
||||
uint16_t secondary_cccd;
|
||||
gatt_client_notification_t secondary_listener;
|
||||
bool secondary_listening;
|
||||
#endif
|
||||
btstack_timer_source_t timeout_timer, output_timer;
|
||||
bool timeout_active, output_active;
|
||||
// ATT write-request buffers must outlive the asynchronous call.
|
||||
uint8_t command_data[32], rumble_data[33], cccd_data[2];
|
||||
uint8_t command_length;
|
||||
bool command_pending, command_sent, command_acked;
|
||||
#if SWITCH_PICO_SWITCH2_USB_BRIDGE
|
||||
uint64_t sample_token;
|
||||
#endif
|
||||
uint8_t step, calibration_slot;
|
||||
bool factory_calibration, calibration_done;
|
||||
uint32_t memory_address;
|
||||
|
|
@ -138,6 +159,33 @@ static bool sw2_product(uint16_t pid) {
|
|||
return pid == UNI_SW2_PRO_PID || pid == UNI_SW2_JOYCON_L_PID || pid == UNI_SW2_JOYCON_R_PID;
|
||||
}
|
||||
|
||||
#if SWITCH_PICO_SWITCH2_MOUSE_CAPTURE
|
||||
static bool sw2_mouse_capture_enabled(const sw2_instance_t* ins) {
|
||||
return ins->device->product_id == UNI_SW2_JOYCON_L_PID || ins->device->product_id == UNI_SW2_JOYCON_R_PID;
|
||||
}
|
||||
|
||||
static void sw2_capture(sw2_instance_t* ins, uint8_t report_id, const uint8_t* report, uint16_t length) {
|
||||
if (sw2_mouse_capture_enabled(ins))
|
||||
switch_pico_switch2_mouse_report(ins->device->product_id, ins->address, report_id, report,
|
||||
length > 64 ? 64 : length, btstack_run_loop_get_time_ms());
|
||||
}
|
||||
|
||||
static void sw2_log_capture(const char* label, const sw2_instance_t* ins,
|
||||
const uint8_t* data, unsigned length) {
|
||||
if (length > 64 || !sw2_mouse_capture_enabled(ins)) return;
|
||||
static const char hex[] = "0123456789abcdef";
|
||||
char text[129];
|
||||
for (unsigned i = 0; i < length; ++i) {
|
||||
text[2 * i] = hex[data[i] >> 4];
|
||||
text[2 * i + 1] = hex[data[i] & 15];
|
||||
}
|
||||
text[length * 2] = 0;
|
||||
printf("[SW2_%s] pid=%04x address=%02x:%02x:%02x:%02x:%02x:%02x data=%s\n",
|
||||
label, ins->device->product_id, ins->address[0], ins->address[1],
|
||||
ins->address[2], ins->address[3], ins->address[4], ins->address[5], text);
|
||||
}
|
||||
#endif
|
||||
|
||||
bool uni_hid_parser_switch2_is_ble_device(const uni_hid_device_t* d) {
|
||||
return d && d->conn.protocol == UNI_BT_CONN_PROTOCOL_BLE && d->vendor_id == UNI_SW2_NINTENDO_VID &&
|
||||
sw2_product(d->product_id);
|
||||
|
|
@ -168,6 +216,13 @@ void uni_hid_parser_switch2_teardown(uni_hid_device_t* d) {
|
|||
sw2_instance_t* ins = sw2_instance(d);
|
||||
if (!ins)
|
||||
return;
|
||||
#if SWITCH_PICO_SWITCH2_USB_BRIDGE
|
||||
if (ins->sample_token) {
|
||||
switch_pico_switch2_sample_result(d->product_id, ins->address, ins->sample_token,
|
||||
-1, btstack_run_loop_get_time_ms());
|
||||
ins->sample_token = 0;
|
||||
}
|
||||
#endif
|
||||
sw2_disarm_timeout(ins);
|
||||
if (ins->output_active)
|
||||
btstack_run_loop_remove_timer(&ins->output_timer);
|
||||
|
|
@ -177,6 +232,12 @@ void uni_hid_parser_switch2_teardown(uni_hid_device_t* d) {
|
|||
if (ins->input_listening)
|
||||
gatt_client_stop_listening_for_characteristic_value_updates(&ins->input_listener);
|
||||
ins->response_listening = ins->input_listening = false;
|
||||
#if SWITCH_PICO_SWITCH2_MOUSE_CAPTURE
|
||||
if (ins->secondary_listening)
|
||||
gatt_client_stop_listening_for_characteristic_value_updates(&ins->secondary_listener);
|
||||
ins->secondary_listening = false;
|
||||
sw2_capture(ins, 0, NULL, 0);
|
||||
#endif
|
||||
sw2_discard_host(ins);
|
||||
++ins->haptics_epoch;
|
||||
ins->command_pending = ins->rumble_scheduled = false;
|
||||
|
|
@ -254,6 +315,29 @@ static void sw2_subscribe(sw2_instance_t* ins, bool input) {
|
|||
sw2_gatt_handler, ins->handle, input ? ins->input_cccd : ins->response_cccd, 2, ins->cccd_data));
|
||||
}
|
||||
|
||||
#if SWITCH_PICO_SWITCH2_MOUSE_CAPTURE
|
||||
static void sw2_discover_secondary_descriptors(sw2_instance_t* ins) {
|
||||
ins->state = SW2_SECONDARY_DESCRIPTOR;
|
||||
ins->query = SW2_QUERY_DISCOVERY;
|
||||
sw2_arm_timeout(ins);
|
||||
sw2_check_query(ins, gatt_client_discover_characteristic_descriptors(
|
||||
sw2_gatt_handler, ins->handle, &ins->secondary));
|
||||
}
|
||||
|
||||
static void sw2_subscribe_secondary(sw2_instance_t* ins) {
|
||||
ins->state = SW2_SUBSCRIBE_SECONDARY;
|
||||
ins->query = SW2_QUERY_CCCD;
|
||||
ins->cccd_data[0] = 1;
|
||||
ins->cccd_data[1] = 0;
|
||||
gatt_client_listen_for_characteristic_value_updates(&ins->secondary_listener, sw2_gatt_handler,
|
||||
ins->handle, &ins->secondary);
|
||||
ins->secondary_listening = true;
|
||||
sw2_arm_timeout(ins);
|
||||
sw2_check_query(ins, gatt_client_write_characteristic_descriptor_using_descriptor_handle(
|
||||
sw2_gatt_handler, ins->handle, ins->secondary_cccd, 2, ins->cccd_data));
|
||||
}
|
||||
#endif
|
||||
|
||||
static bool sw2_transient_write_error(uint8_t status) {
|
||||
return status == BTSTACK_ACL_BUFFERS_FULL || status == GATT_CLIENT_BUSY ||
|
||||
status == GATT_CLIENT_IN_WRONG_STATE;
|
||||
|
|
@ -262,6 +346,18 @@ static bool sw2_transient_write_error(uint8_t status) {
|
|||
static void sw2_try_command(sw2_instance_t* ins) {
|
||||
if (!ins->command_pending || ins->command_sent || ins->query != SW2_QUERY_NONE)
|
||||
return;
|
||||
#if SWITCH_PICO_SWITCH2_USB_BRIDGE
|
||||
if (ins->sample_token &&
|
||||
!switch_pico_switch2_sample_result(ins->device->product_id, ins->address,
|
||||
ins->sample_token, 0, btstack_run_loop_get_time_ms())) {
|
||||
// Nothing was sent yet. Once sent, keep the old transaction serialized
|
||||
// until its ACK/timeout: the empty response has no request nonce.
|
||||
ins->sample_token = 0;
|
||||
ins->command_pending = false;
|
||||
sw2_disarm_timeout(ins);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
// Re-check immediately before every application-pairing write, including a
|
||||
// deferred write after ACL backpressure. A closed window cannot write MACs.
|
||||
if (ins->command_data[0] == 0x15 && !switch_pico_switch2_pairing_allowed()) {
|
||||
|
|
@ -342,6 +438,12 @@ static bool sw2_calibration(sw2_stick_t* out, const uint8_t* data) {
|
|||
static void sw2_continue(sw2_instance_t* ins) {
|
||||
switch (ins->state) {
|
||||
case SW2_INFO:
|
||||
#if SWITCH_PICO_SWITCH2_MOUSE_CAPTURE
|
||||
if (sw2_mouse_capture_enabled(ins) && ins->step == 1) {
|
||||
sw2_send_command(ins, 0x10, 0x01, NULL, 0);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
sw2_read_memory(ins, 0x13000, 0x40);
|
||||
break;
|
||||
case SW2_PAIR: {
|
||||
|
|
@ -372,7 +474,15 @@ static void sw2_continue(sw2_instance_t* ins) {
|
|||
sw2_read_memory(ins, 0x13044, 12);
|
||||
break;
|
||||
case SW2_FEATURES: {
|
||||
#if SWITCH_PICO_SWITCH2_USB_BRIDGE
|
||||
// Match the console's complete native feature set, including the
|
||||
// status associated with bit 5, rather than a mouse-only capture.
|
||||
const uint8_t features[4] = {sw2_mouse_capture_enabled(ins) ? 0x37 : 0x04, 0, 0, 0};
|
||||
#elif SWITCH_PICO_SWITCH2_MOUSE_CAPTURE
|
||||
const uint8_t features[4] = {sw2_mouse_capture_enabled(ins) ? 0x14 : 0x04, 0, 0, 0};
|
||||
#else
|
||||
const uint8_t features[4] = {0x04, 0, 0, 0};
|
||||
#endif
|
||||
sw2_send_command(ins, 0x0c, ins->step ? 0x04 : 0x02, features, sizeof(features));
|
||||
break;
|
||||
}
|
||||
|
|
@ -393,8 +503,22 @@ static void sw2_complete_command(sw2_instance_t* ins) {
|
|||
return;
|
||||
ins->command_pending = false;
|
||||
sw2_disarm_timeout(ins);
|
||||
#if SWITCH_PICO_SWITCH2_USB_BRIDGE
|
||||
if (ins->sample_token) {
|
||||
switch_pico_switch2_sample_result(ins->device->product_id, ins->address,
|
||||
ins->sample_token, 1, btstack_run_loop_get_time_ms());
|
||||
ins->sample_token = 0;
|
||||
}
|
||||
#endif
|
||||
switch (ins->state) {
|
||||
case SW2_INFO:
|
||||
#if SWITCH_PICO_SWITCH2_MOUSE_CAPTURE
|
||||
if (sw2_mouse_capture_enabled(ins) && ins->step == 0) {
|
||||
ins->step = 1;
|
||||
break;
|
||||
}
|
||||
ins->step = 0;
|
||||
#endif
|
||||
ins->state = ins->needs_pair ? SW2_PAIR : SW2_CALIBRATION;
|
||||
break;
|
||||
case SW2_PAIR:
|
||||
|
|
@ -438,6 +562,19 @@ static void sw2_response(sw2_instance_t* ins, const uint8_t* data, uint16_t leng
|
|||
sw2_fail(ins, "negative application ACK", data[5]);
|
||||
return;
|
||||
}
|
||||
#if SWITCH_PICO_SWITCH2_USB_BRIDGE
|
||||
// 0A/02 has an empty application ACK, not a returned sample ID or status
|
||||
// payload. Never interpret a truncated/extended response as its success.
|
||||
if (ins->sample_token && (length != 8 || data[4] != 0x10 || data[6] || data[7]))
|
||||
return;
|
||||
#endif
|
||||
#if SWITCH_PICO_SWITCH2_MOUSE_CAPTURE
|
||||
if (ins->state == SW2_INFO && ins->step == 1) {
|
||||
const uint8_t expected_type = ins->device->product_id == UNI_SW2_JOYCON_R_PID ? 1 : 0;
|
||||
if (data[0] != 0x10 || length < 20 || data[11] != expected_type) return;
|
||||
sw2_log_capture("VERSION", ins, data + 8, 12);
|
||||
}
|
||||
#endif
|
||||
if (data[0] == 0x02) {
|
||||
if (length < 16 || data[8] != ins->memory_length ||
|
||||
little_endian_read_32(data, 12) != ins->memory_address || length < 16 + ins->memory_length)
|
||||
|
|
@ -449,6 +586,12 @@ static void sw2_response(sw2_instance_t* ins, const uint8_t* data, uint16_t leng
|
|||
sw2_fail(ins, "controller identity mismatch", 0);
|
||||
return;
|
||||
}
|
||||
#if SWITCH_PICO_SWITCH2_MOUSE_CAPTURE
|
||||
// This read is already part of normal setup. Capture its validated
|
||||
// factory identity for USB enumeration research, without new reads
|
||||
// or writes to the controller. Keep donor data out of source files.
|
||||
sw2_log_capture("IDENTITY", ins, value, 64);
|
||||
#endif
|
||||
} else if (ins->state == SW2_CALIBRATION) {
|
||||
// Both solo Joy-Cons store their one stick in calibration slot 1.
|
||||
unsigned stick = ins->device->product_id == UNI_SW2_JOYCON_R_PID ? 1 : ins->calibration_slot;
|
||||
|
|
@ -483,6 +626,10 @@ static void sw2_response(sw2_instance_t* ins, const uint8_t* data, uint16_t leng
|
|||
return;
|
||||
}
|
||||
}
|
||||
#if SWITCH_PICO_SWITCH2_MOUSE_CAPTURE
|
||||
if (ins->state == SW2_FEATURES)
|
||||
sw2_capture(ins, 0xc0, data, length);
|
||||
#endif
|
||||
ins->command_acked = true;
|
||||
sw2_complete_command(ins);
|
||||
}
|
||||
|
|
@ -527,6 +674,12 @@ static void sw2_query_complete(sw2_instance_t* ins, uint8_t status) {
|
|||
sw2_fail(ins, "missing required characteristic", 0);
|
||||
return;
|
||||
}
|
||||
#if SWITCH_PICO_SWITCH2_MOUSE_CAPTURE
|
||||
if (sw2_mouse_capture_enabled(ins) && !ins->secondary.value_handle) {
|
||||
sw2_fail(ins, "missing secondary input characteristic", 0);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
sw2_discover_descriptors(ins, false);
|
||||
break;
|
||||
case SW2_RESPONSE_DESCRIPTOR:
|
||||
|
|
@ -541,13 +694,39 @@ static void sw2_query_complete(sw2_instance_t* ins, uint8_t status) {
|
|||
sw2_fail(ins, "missing input CCCD", 0);
|
||||
return;
|
||||
}
|
||||
#if SWITCH_PICO_SWITCH2_MOUSE_CAPTURE
|
||||
if (sw2_mouse_capture_enabled(ins)) {
|
||||
sw2_discover_secondary_descriptors(ins);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
sw2_subscribe(ins, false);
|
||||
break;
|
||||
#if SWITCH_PICO_SWITCH2_MOUSE_CAPTURE
|
||||
case SW2_SECONDARY_DESCRIPTOR:
|
||||
if (!ins->secondary_cccd) {
|
||||
sw2_fail(ins, "missing secondary input CCCD", 0);
|
||||
return;
|
||||
}
|
||||
sw2_subscribe(ins, false);
|
||||
break;
|
||||
#endif
|
||||
case SW2_SUBSCRIBE_RESPONSE:
|
||||
ins->state = SW2_INFO;
|
||||
sw2_continue(ins);
|
||||
break;
|
||||
case SW2_SUBSCRIBE_INPUT:
|
||||
#if SWITCH_PICO_SWITCH2_MOUSE_CAPTURE
|
||||
case SW2_SUBSCRIBE_SECONDARY:
|
||||
// This Joy-Con emits the last subscribed input format, not both.
|
||||
// Native capture is explicit: its packed input is not yet decoded
|
||||
// by the normal gamepad parser. Common capture preserves controls.
|
||||
if (SWITCH_PICO_SWITCH2_MOUSE_CAPTURE_NATIVE &&
|
||||
ins->state == SW2_SUBSCRIBE_INPUT && sw2_mouse_capture_enabled(ins)) {
|
||||
sw2_subscribe_secondary(ins);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
ins->state = SW2_FEATURES;
|
||||
ins->step = 0;
|
||||
sw2_continue(ins);
|
||||
|
|
@ -576,6 +755,12 @@ static void sw2_characteristic(sw2_instance_t* ins, const gatt_client_characteri
|
|||
target = &ins->rumble;
|
||||
properties = ATT_PROPERTY_WRITE | ATT_PROPERTY_WRITE_WITHOUT_RESPONSE;
|
||||
}
|
||||
#if SWITCH_PICO_SWITCH2_MOUSE_CAPTURE
|
||||
if (!target && sw2_mouse_capture_enabled(ins) &&
|
||||
memcmp(characteristic->uuid128, ins->device->product_id == UNI_SW2_JOYCON_L_PID ?
|
||||
sw2_secondary_left_uuid : sw2_secondary_right_uuid, 16) == 0)
|
||||
target = &ins->secondary;
|
||||
#endif
|
||||
if (!target)
|
||||
return;
|
||||
if (target->value_handle || !(characteristic->properties & properties) ||
|
||||
|
|
@ -632,8 +817,14 @@ static void sw2_gatt_handler(uint8_t packet_type, uint16_t channel, uint8_t* pac
|
|||
break;
|
||||
}
|
||||
case GATT_EVENT_ALL_CHARACTERISTIC_DESCRIPTORS_QUERY_RESULT: {
|
||||
#if SWITCH_PICO_SWITCH2_MOUSE_CAPTURE
|
||||
if (ins->state != SW2_RESPONSE_DESCRIPTOR && ins->state != SW2_INPUT_DESCRIPTOR &&
|
||||
ins->state != SW2_SECONDARY_DESCRIPTOR)
|
||||
return;
|
||||
#else
|
||||
if (ins->state != SW2_RESPONSE_DESCRIPTOR && ins->state != SW2_INPUT_DESCRIPTOR)
|
||||
return;
|
||||
#endif
|
||||
gatt_client_characteristic_descriptor_t descriptor;
|
||||
gatt_event_all_characteristic_descriptors_query_result_get_characteristic_descriptor(packet, &descriptor);
|
||||
if (descriptor.uuid16 != SW2_CCCD_UUID)
|
||||
|
|
@ -641,6 +832,12 @@ static void sw2_gatt_handler(uint8_t packet_type, uint16_t channel, uint8_t* pac
|
|||
bool input = ins->state == SW2_INPUT_DESCRIPTOR;
|
||||
gatt_client_characteristic_t* characteristic = input ? &ins->input : &ins->response;
|
||||
uint16_t* handle = input ? &ins->input_cccd : &ins->response_cccd;
|
||||
#if SWITCH_PICO_SWITCH2_MOUSE_CAPTURE
|
||||
if (ins->state == SW2_SECONDARY_DESCRIPTOR) {
|
||||
characteristic = &ins->secondary;
|
||||
handle = &ins->secondary_cccd;
|
||||
}
|
||||
#endif
|
||||
if (*handle || descriptor.handle <= characteristic->value_handle || descriptor.handle > characteristic->end_handle) {
|
||||
sw2_fail(ins, "invalid or ambiguous CCCD", descriptor.handle);
|
||||
return;
|
||||
|
|
@ -657,6 +854,14 @@ static void sw2_gatt_handler(uint8_t packet_type, uint16_t channel, uint8_t* pac
|
|||
return;
|
||||
uint16_t handle = gatt_event_notification_get_value_handle(packet);
|
||||
const uint8_t* value = gatt_event_notification_get_value(packet);
|
||||
#if SWITCH_PICO_SWITCH2_MOUSE_CAPTURE
|
||||
// BLE omits report IDs. Keep both formats raw and never route the
|
||||
// secondary packed motion data through the common gamepad parser.
|
||||
if (length && ins->input_listening && handle == ins->input.value_handle)
|
||||
sw2_capture(ins, 0x05, value, length);
|
||||
else if (length && ins->secondary_listening && handle == ins->secondary.value_handle)
|
||||
sw2_capture(ins, ins->device->product_id == UNI_SW2_JOYCON_L_PID ? 0x07 : 0x08, value, length);
|
||||
#endif
|
||||
if (ins->response_listening && handle == ins->response.value_handle)
|
||||
sw2_response(ins, value, length);
|
||||
else if (ins->state == SW2_READY && ins->input_listening && handle == ins->input.value_handle && length == SW2_REPORT_SIZE) {
|
||||
|
|
@ -1271,6 +1476,18 @@ static void sw2_output_tick(btstack_timer_source_t* timer) {
|
|||
if (ins->state != SW2_READY)
|
||||
return; // A blocked setup command rescheduled itself, or awaits its ACK.
|
||||
uint32_t now = btstack_run_loop_get_time_ms();
|
||||
#if SWITCH_PICO_SWITCH2_USB_BRIDGE
|
||||
if (!ins->command_pending && ins->query == SW2_QUERY_NONE) {
|
||||
uint8_t sample_id;
|
||||
if (switch_pico_switch2_sample_take(ins->device->product_id, ins->address, now,
|
||||
&sample_id, &ins->sample_token)) {
|
||||
const uint8_t data[4] = {sample_id, 0, 0, 0};
|
||||
sw2_send_command(ins, 0x0a, 0x02, data, sizeof(data));
|
||||
if (!ins->device)
|
||||
return;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
sw2_update_haptics(ins, now);
|
||||
bool sent = sw2_send_rumble(ins, now);
|
||||
if (!ins->device)
|
||||
|
|
|
|||
|
|
@ -29,6 +29,24 @@ enum {
|
|||
// Supplied by the platform's existing bounded pairing-window policy.
|
||||
bool switch_pico_switch2_pairing_allowed(void);
|
||||
|
||||
#if SWITCH_PICO_SWITCH2_MOUSE_CAPTURE
|
||||
// BTstack-core callback: canonical Bluepad32 address, unmodified BLE payload
|
||||
// (without a report-ID prefix), at most 64 bytes. 0xc0 is a validated feature
|
||||
// reply; report_id=0 with length=0 marks teardown and permits a NULL report.
|
||||
void switch_pico_switch2_mouse_report(uint16_t product_id, const uint8_t address[6], uint8_t report_id,
|
||||
const uint8_t* report, uint16_t length, uint32_t received_ms);
|
||||
#endif
|
||||
|
||||
#if SWITCH_PICO_SWITCH2_USB_BRIDGE
|
||||
// BTstack-core mailbox hooks. Take only when READY with no command/query in
|
||||
// flight. Result 0 checks ownership before a deferred write, 1 records verified
|
||||
// command completion, and -1 fails it. False means canceled/stale/wrong source.
|
||||
bool switch_pico_switch2_sample_take(uint16_t product_id, const uint8_t address[6],
|
||||
uint32_t now_ms, uint8_t* sample_id, uint64_t* token);
|
||||
bool switch_pico_switch2_sample_result(uint16_t product_id, const uint8_t address[6],
|
||||
uint64_t token, int result, uint32_t now_ms);
|
||||
#endif
|
||||
|
||||
bool uni_bt_le_switch2_handle_advertisement(const uint8_t* packet, uint16_t size);
|
||||
bool uni_hid_parser_switch2_is_ble_device(const struct uni_hid_device_s* d);
|
||||
void uni_hid_parser_switch2_on_le_connected(struct uni_hid_device_s* d);
|
||||
|
|
|
|||
352
src/firmware/input/switch2_mouse_capture.cpp
Normal file
352
src/firmware/input/switch2_mouse_capture.cpp
Normal file
|
|
@ -0,0 +1,352 @@
|
|||
#include "input/switch2_mouse_capture.h"
|
||||
|
||||
#ifdef SWITCH_PICO_SWITCH2_MOUSE_CAPTURE
|
||||
#include <string.h>
|
||||
|
||||
#include "parser/uni_hid_parser_switch2.h"
|
||||
#include "pico/critical_section.h"
|
||||
|
||||
namespace {
|
||||
critical_section_t g_lock;
|
||||
bool g_initialized;
|
||||
uint8_t g_rows[SWITCH2_MOUSE_CAPTURE_CAPACITY][SWITCH2_MOUSE_CAPTURE_ROW_SIZE];
|
||||
uint8_t g_next;
|
||||
uint8_t g_count;
|
||||
uint32_t g_total_records;
|
||||
bool g_input_selected;
|
||||
uint8_t g_input_address[6];
|
||||
Switch2MouseCaptureInput g_latest_input;
|
||||
#if SWITCH_PICO_SWITCH2_USB_BRIDGE
|
||||
constexpr uint32_t kInputDeadlineMs = 500;
|
||||
constexpr uint32_t kSampleDeadlineMs = 2000;
|
||||
constexpr uint8_t kNativeReportCapacity = 32;
|
||||
struct NativeReport {
|
||||
uint8_t report[SWITCH2_MOUSE_CAPTURE_NATIVE_INPUT_SIZE];
|
||||
uint32_t serial;
|
||||
uint32_t received_ms;
|
||||
};
|
||||
NativeReport g_native_reports[kNativeReportCapacity];
|
||||
uint8_t g_native_head;
|
||||
uint8_t g_native_count;
|
||||
bool g_native_stream;
|
||||
uint64_t g_sample_serial;
|
||||
bool g_source_active;
|
||||
struct {
|
||||
uint64_t token;
|
||||
uint32_t started_ms;
|
||||
uint32_t mouse_epoch;
|
||||
uint8_t sample_id;
|
||||
bool taken;
|
||||
bool acked;
|
||||
} g_sample;
|
||||
|
||||
void clear_native_reports() {
|
||||
g_native_head = 0;
|
||||
g_native_count = 0;
|
||||
}
|
||||
|
||||
bool source_fresh(uint32_t now_ms) {
|
||||
return g_input_selected && g_source_active && g_latest_input.active &&
|
||||
static_cast<int32_t>(now_ms - g_latest_input.received_ms) <
|
||||
static_cast<int32_t>(kInputDeadlineMs);
|
||||
}
|
||||
|
||||
bool sample_current(uint32_t now_ms) {
|
||||
if (g_sample.token &&
|
||||
(!source_fresh(now_ms) || g_sample.mouse_epoch != g_latest_input.mouse_epoch ||
|
||||
static_cast<int32_t>(now_ms - g_sample.started_ms) >=
|
||||
static_cast<int32_t>(kSampleDeadlineMs))) {
|
||||
g_sample = {};
|
||||
}
|
||||
return g_sample.token != 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
void write_u16(uint8_t* output, uint16_t value) {
|
||||
output[0] = static_cast<uint8_t>(value);
|
||||
output[1] = static_cast<uint8_t>(value >> 8);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
int32_t read_i16(const uint8_t* input) {
|
||||
const int32_t value = input[0] | (static_cast<int32_t>(input[1]) << 8);
|
||||
return value >= 0x8000 ? value - 0x10000 : value;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void switch2_mouse_capture_init() {
|
||||
if (g_initialized) return;
|
||||
// The application and TinyUSB already consume all eight exclusive
|
||||
// spinlocks. Capture never nests another striped spinlock, so use the
|
||||
// SDK's shared stripe pool rather than exhausting startup allocation.
|
||||
critical_section_init_with_lock_num(&g_lock, next_striped_spin_lock_num());
|
||||
g_initialized = true;
|
||||
}
|
||||
|
||||
extern "C" void switch_pico_switch2_mouse_report(
|
||||
uint16_t product_id, const uint8_t address[6], uint8_t report_id,
|
||||
const uint8_t* report, uint16_t length, uint32_t received_ms) {
|
||||
if (!g_initialized || address == nullptr ||
|
||||
(product_id != UNI_SW2_JOYCON_L_PID &&
|
||||
product_id != UNI_SW2_JOYCON_R_PID) ||
|
||||
length > SWITCH2_MOUSE_CAPTURE_REPORT_SIZE ||
|
||||
(length != 0 && report == nullptr)) {
|
||||
return;
|
||||
}
|
||||
if (report_id == 0) {
|
||||
if (length != 0) return;
|
||||
} else if (length == 0 ||
|
||||
(report_id != 0x05 && report_id != 0x07 &&
|
||||
report_id != 0x08 && report_id != 0xc0)) {
|
||||
return;
|
||||
}
|
||||
|
||||
critical_section_enter_blocking(&g_lock);
|
||||
#if SWITCH_PICO_SWITCH2_USB_BRIDGE
|
||||
// Teardown must invalidate source ownership even after capture serials are
|
||||
// exhausted; this is independent of whether a ring event can be recorded.
|
||||
if (report_id == 0 && g_input_selected && product_id == UNI_SW2_JOYCON_R_PID &&
|
||||
memcmp(address, g_input_address, sizeof(g_input_address)) == 0) {
|
||||
g_source_active = false;
|
||||
clear_native_reports();
|
||||
g_sample = {};
|
||||
}
|
||||
#endif
|
||||
// Never reuse a serial within one boot, even after UINT32_MAX records.
|
||||
if (g_total_records == UINT32_MAX) {
|
||||
#if SWITCH_PICO_SWITCH2_USB_BRIDGE
|
||||
clear_native_reports();
|
||||
#endif
|
||||
critical_section_exit(&g_lock);
|
||||
return;
|
||||
}
|
||||
uint8_t* row = g_rows[g_next];
|
||||
write_u32(row, ++g_total_records);
|
||||
#if SWITCH_PICO_SWITCH2_USB_BRIDGE
|
||||
// Exhaustion is terminal for the relay, including the last recorded event.
|
||||
if (g_total_records == UINT32_MAX) clear_native_reports();
|
||||
#endif
|
||||
write_u32(row + 4, received_ms);
|
||||
write_u16(row + 8, product_id);
|
||||
row[10] = report_id;
|
||||
row[11] = static_cast<uint8_t>(length);
|
||||
memcpy(row + 12, address, 6);
|
||||
row[18] = row[19] = 0;
|
||||
if (length != 0) memcpy(row + 20, report, length);
|
||||
memset(row + 20 + length, 0, SWITCH2_MOUSE_CAPTURE_REPORT_SIZE - length);
|
||||
g_next = static_cast<uint8_t>((g_next + 1) % SWITCH2_MOUSE_CAPTURE_CAPACITY);
|
||||
if (g_input_selected && product_id == UNI_SW2_JOYCON_R_PID &&
|
||||
memcmp(address, g_input_address, sizeof(g_input_address)) == 0 &&
|
||||
(report_id == 0 ||
|
||||
(report_id == 0x08 && length == SWITCH2_MOUSE_CAPTURE_NATIVE_INPUT_SIZE))) {
|
||||
if (report_id == 0x08) {
|
||||
if (!g_latest_input.active) {
|
||||
g_latest_input.mouse_epoch = g_total_records;
|
||||
g_latest_input.mouse_total_x = 0;
|
||||
g_latest_input.mouse_total_y = 0;
|
||||
}
|
||||
g_latest_input.active = true;
|
||||
#if SWITCH_PICO_SWITCH2_USB_BRIDGE
|
||||
g_source_active = true;
|
||||
if (g_native_stream && g_total_records != UINT32_MAX) {
|
||||
if (g_native_count == kNativeReportCapacity) clear_native_reports();
|
||||
NativeReport& packet =
|
||||
g_native_reports[(g_native_head + g_native_count) % kNativeReportCapacity];
|
||||
memcpy(packet.report, report, sizeof(packet.report));
|
||||
packet.serial = g_total_records;
|
||||
packet.received_ms = received_ms;
|
||||
++g_native_count;
|
||||
}
|
||||
#endif
|
||||
memcpy(g_latest_input.buttons, report + 2, sizeof(g_latest_input.buttons));
|
||||
memcpy(g_latest_input.stick, report + 5, sizeof(g_latest_input.stick));
|
||||
g_latest_input.native_status = report[8];
|
||||
// At most UINT32_MAX signed16 additions per boot: magnitude < 2^47.
|
||||
// Every packet contributes, even when its delta matches the last.
|
||||
g_latest_input.mouse_total_x += read_i16(report + 9);
|
||||
g_latest_input.mouse_total_y += read_i16(report + 11);
|
||||
g_latest_input.mouse_surface = report[13];
|
||||
} else {
|
||||
g_latest_input = {};
|
||||
}
|
||||
g_latest_input.serial = g_total_records;
|
||||
g_latest_input.received_ms = received_ms;
|
||||
}
|
||||
if (g_count < SWITCH2_MOUSE_CAPTURE_CAPACITY) ++g_count;
|
||||
critical_section_exit(&g_lock);
|
||||
}
|
||||
|
||||
size_t switch2_mouse_capture_snapshot(uint8_t* output, size_t capacity,
|
||||
uint32_t* total_records) {
|
||||
if (!g_initialized || output == nullptr || total_records == nullptr) return 0;
|
||||
|
||||
critical_section_enter_blocking(&g_lock);
|
||||
const size_t required = SWITCH2_MOUSE_CAPTURE_HEADER_SIZE +
|
||||
g_count * SWITCH2_MOUSE_CAPTURE_ROW_SIZE;
|
||||
if (capacity < required) {
|
||||
critical_section_exit(&g_lock);
|
||||
return 0;
|
||||
}
|
||||
*total_records = g_total_records;
|
||||
write_u32(output, g_total_records);
|
||||
output[4] = g_count;
|
||||
memset(output + 5, 0, SWITCH2_MOUSE_CAPTURE_HEADER_SIZE - 5);
|
||||
const size_t oldest = g_count == SWITCH2_MOUSE_CAPTURE_CAPACITY ? g_next : 0;
|
||||
const size_t first_count =
|
||||
g_count < SWITCH2_MOUSE_CAPTURE_CAPACITY - oldest
|
||||
? g_count : SWITCH2_MOUSE_CAPTURE_CAPACITY - oldest;
|
||||
memcpy(output + SWITCH2_MOUSE_CAPTURE_HEADER_SIZE, g_rows[oldest],
|
||||
first_count * SWITCH2_MOUSE_CAPTURE_ROW_SIZE);
|
||||
if (first_count != g_count) {
|
||||
memcpy(output + SWITCH2_MOUSE_CAPTURE_HEADER_SIZE +
|
||||
first_count * SWITCH2_MOUSE_CAPTURE_ROW_SIZE,
|
||||
g_rows[0], (g_count - first_count) * SWITCH2_MOUSE_CAPTURE_ROW_SIZE);
|
||||
}
|
||||
critical_section_exit(&g_lock);
|
||||
return required;
|
||||
}
|
||||
|
||||
void switch2_mouse_capture_select_input(const uint8_t address[6]) {
|
||||
if (!g_initialized || address == nullptr) return;
|
||||
critical_section_enter_blocking(&g_lock);
|
||||
#if SWITCH_PICO_SWITCH2_USB_BRIDGE
|
||||
g_source_active = false;
|
||||
g_native_stream = false;
|
||||
clear_native_reports();
|
||||
g_sample = {};
|
||||
#endif
|
||||
memcpy(g_input_address, address, sizeof(g_input_address));
|
||||
g_latest_input = {};
|
||||
g_input_selected = true;
|
||||
critical_section_exit(&g_lock);
|
||||
}
|
||||
|
||||
bool switch2_mouse_capture_latest_input(uint32_t after_serial,
|
||||
Switch2MouseCaptureInput* output) {
|
||||
if (!g_initialized || output == nullptr) return false;
|
||||
critical_section_enter_blocking(&g_lock);
|
||||
const bool fresh = g_latest_input.serial > after_serial;
|
||||
if (fresh) *output = g_latest_input;
|
||||
critical_section_exit(&g_lock);
|
||||
return fresh;
|
||||
}
|
||||
|
||||
#if SWITCH_PICO_SWITCH2_USB_BRIDGE
|
||||
void switch2_mouse_capture_set_native_stream(bool enabled) {
|
||||
if (!g_initialized) return;
|
||||
critical_section_enter_blocking(&g_lock);
|
||||
g_native_stream = enabled;
|
||||
if (!enabled) clear_native_reports();
|
||||
critical_section_exit(&g_lock);
|
||||
}
|
||||
|
||||
uint32_t switch2_mouse_capture_peek_native_report(
|
||||
uint32_t now_ms, uint8_t report[SWITCH2_MOUSE_CAPTURE_NATIVE_INPUT_SIZE]) {
|
||||
if (!g_initialized || report == nullptr) return 0;
|
||||
critical_section_enter_blocking(&g_lock);
|
||||
uint32_t serial = 0;
|
||||
if (!g_native_stream || !source_fresh(now_ms) ||
|
||||
(g_native_count != 0 &&
|
||||
static_cast<int32_t>(now_ms - g_native_reports[g_native_head].received_ms) >=
|
||||
static_cast<int32_t>(kInputDeadlineMs))) {
|
||||
clear_native_reports();
|
||||
} else if (g_native_count != 0) {
|
||||
const NativeReport& packet = g_native_reports[g_native_head];
|
||||
memcpy(report, packet.report, sizeof(packet.report));
|
||||
serial = packet.serial;
|
||||
}
|
||||
critical_section_exit(&g_lock);
|
||||
return serial;
|
||||
}
|
||||
|
||||
bool switch2_mouse_capture_commit_native_report(uint32_t serial) {
|
||||
if (!g_initialized || serial == 0) return false;
|
||||
critical_section_enter_blocking(&g_lock);
|
||||
const bool accepted = g_native_stream && g_native_count != 0 &&
|
||||
g_native_reports[g_native_head].serial == serial;
|
||||
if (accepted) {
|
||||
g_native_head = static_cast<uint8_t>((g_native_head + 1) % kNativeReportCapacity);
|
||||
--g_native_count;
|
||||
}
|
||||
critical_section_exit(&g_lock);
|
||||
return accepted;
|
||||
}
|
||||
|
||||
bool switch2_mouse_capture_request_sample(uint8_t sample_id, uint32_t now_ms,
|
||||
uint64_t* token) {
|
||||
if (token != nullptr) *token = 0;
|
||||
if (!g_initialized || token == nullptr || sample_id > 7) return false;
|
||||
critical_section_enter_blocking(&g_lock);
|
||||
const bool accepted = !sample_current(now_ms) && source_fresh(now_ms) &&
|
||||
g_sample_serial != UINT64_MAX;
|
||||
if (accepted) {
|
||||
g_sample.token = ++g_sample_serial;
|
||||
g_sample.started_ms = now_ms;
|
||||
g_sample.mouse_epoch = g_latest_input.mouse_epoch;
|
||||
g_sample.sample_id = sample_id;
|
||||
*token = g_sample.token;
|
||||
}
|
||||
critical_section_exit(&g_lock);
|
||||
return accepted;
|
||||
}
|
||||
|
||||
int switch2_mouse_capture_sample_result(uint64_t token, uint32_t now_ms) {
|
||||
if (!g_initialized || token == 0) return -1;
|
||||
critical_section_enter_blocking(&g_lock);
|
||||
int result = -1;
|
||||
if (sample_current(now_ms) && g_sample.token == token) {
|
||||
result = g_sample.acked ? 1 : 0;
|
||||
if (result == 1) g_sample = {};
|
||||
}
|
||||
critical_section_exit(&g_lock);
|
||||
return result;
|
||||
}
|
||||
|
||||
void switch2_mouse_capture_cancel_sample() {
|
||||
if (!g_initialized) return;
|
||||
critical_section_enter_blocking(&g_lock);
|
||||
g_sample = {};
|
||||
critical_section_exit(&g_lock);
|
||||
}
|
||||
|
||||
extern "C" bool switch_pico_switch2_sample_take(
|
||||
uint16_t product_id, const uint8_t address[6], uint32_t now_ms,
|
||||
uint8_t* sample_id, uint64_t* token) {
|
||||
if (!g_initialized || address == nullptr || sample_id == nullptr || token == nullptr)
|
||||
return false;
|
||||
critical_section_enter_blocking(&g_lock);
|
||||
const bool accepted = sample_current(now_ms) && !g_sample.taken &&
|
||||
product_id == UNI_SW2_JOYCON_R_PID &&
|
||||
memcmp(address, g_input_address, sizeof(g_input_address)) == 0;
|
||||
if (accepted) {
|
||||
g_sample.taken = true;
|
||||
*sample_id = g_sample.sample_id;
|
||||
*token = g_sample.token;
|
||||
}
|
||||
critical_section_exit(&g_lock);
|
||||
return accepted;
|
||||
}
|
||||
|
||||
extern "C" bool switch_pico_switch2_sample_result(
|
||||
uint16_t product_id, const uint8_t address[6], uint64_t token,
|
||||
int result, uint32_t now_ms) {
|
||||
if (!g_initialized || address == nullptr || token == 0) return false;
|
||||
critical_section_enter_blocking(&g_lock);
|
||||
const bool accepted = sample_current(now_ms) && g_sample.taken &&
|
||||
g_sample.token == token && product_id == UNI_SW2_JOYCON_R_PID &&
|
||||
memcmp(address, g_input_address, sizeof(g_input_address)) == 0;
|
||||
if (accepted) {
|
||||
if (result > 0) g_sample.acked = true;
|
||||
else if (result < 0) g_sample = {};
|
||||
}
|
||||
critical_section_exit(&g_lock);
|
||||
return accepted;
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
93
src/firmware/input/switch2_mouse_capture.h
Normal file
93
src/firmware/input/switch2_mouse_capture.h
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
constexpr size_t SWITCH2_MOUSE_CAPTURE_CAPACITY = 9;
|
||||
constexpr size_t SWITCH2_MOUSE_CAPTURE_REPORT_SIZE = 64;
|
||||
constexpr size_t SWITCH2_MOUSE_CAPTURE_HEADER_SIZE = 16;
|
||||
constexpr size_t SWITCH2_MOUSE_CAPTURE_ROW_SIZE = 20 + SWITCH2_MOUSE_CAPTURE_REPORT_SIZE;
|
||||
constexpr size_t SWITCH2_MOUSE_CAPTURE_MAXIMUM_PAYLOAD_SIZE =
|
||||
SWITCH2_MOUSE_CAPTURE_HEADER_SIZE +
|
||||
SWITCH2_MOUSE_CAPTURE_CAPACITY * SWITCH2_MOUSE_CAPTURE_ROW_SIZE;
|
||||
|
||||
// Core 0 initializes once before starting the Core 1 Bluetooth producer.
|
||||
// Repeated initialization never clears the boot-lifetime sequence or records.
|
||||
void switch2_mouse_capture_init();
|
||||
|
||||
// Nondestructive, oldest-to-newest snapshot. All integers are little-endian.
|
||||
// Header: total_records u32, count u8, eleven zero bytes.
|
||||
// Row: serial u32, received_ms u32, PID u16, report_id u8, length u8,
|
||||
// address[6] in Bluepad32 order, two zero bytes, raw[64] (zero-padded).
|
||||
// Returns zero before initialization or when the complete snapshot will not fit.
|
||||
// total_records is taken under the same lock as the serialized rows.
|
||||
size_t switch2_mouse_capture_snapshot(uint8_t* output, size_t capacity,
|
||||
uint32_t* total_records);
|
||||
|
||||
constexpr size_t SWITCH2_MOUSE_CAPTURE_NATIVE_INPUT_SIZE = 63;
|
||||
|
||||
struct Switch2MouseCaptureInput {
|
||||
uint32_t serial;
|
||||
uint32_t received_ms;
|
||||
bool active;
|
||||
// Unrotated native 08 payload bytes 2..3 and 5..7, without report ID.
|
||||
uint8_t buttons[2];
|
||||
uint8_t stick[3];
|
||||
// Latest opaque native 08 byte 8; preserve without interpretation.
|
||||
uint8_t native_status;
|
||||
// Cumulative relative motion, never consumed by a snapshot. A stream's
|
||||
// epoch is its first native packet's boot-lifetime serial; inactive is 0.
|
||||
uint32_t mouse_epoch;
|
||||
int64_t mouse_total_x;
|
||||
int64_t mouse_total_y;
|
||||
// Latest opaque native 08 byte 13; no interpreted surface semantics.
|
||||
uint8_t mouse_surface;
|
||||
};
|
||||
|
||||
// Select one physical right Joy-Con before launching the Bluetooth producer.
|
||||
// Selection starts empty; it never replays the serialized ring or clears it.
|
||||
// Its latest native 08 input / teardown survives unrelated ring traffic.
|
||||
// Each selection also disables and clears the separate native relay FIFO.
|
||||
void switch2_mouse_capture_select_input(const uint8_t address[6]);
|
||||
|
||||
// Copy only a selected event newer than after_serial. A teardown is an event
|
||||
// with active=false and zeroed fields. False leaves output untouched.
|
||||
// Boot-lifetime serials do not wrap, just as in the serialized capture.
|
||||
// Cached reads do not consume totals; every selected native packet contributes,
|
||||
// including identical consecutive deltas. A new stream receives a new epoch
|
||||
// even when the reader missed its preceding teardown.
|
||||
bool switch2_mouse_capture_latest_input(uint32_t after_serial,
|
||||
Switch2MouseCaptureInput* output);
|
||||
|
||||
#if SWITCH_PICO_SWITCH2_USB_BRIDGE
|
||||
// Core 0 explicitly enables the selected source's ordered native 08 relay.
|
||||
// Starts disabled; false clears the FIFO, repeated true preserves it. Enable
|
||||
// never replays earlier capture-ring/latest-input data. Selection disables it;
|
||||
// teardown and capture-serial exhaustion clear it without changing selection.
|
||||
// Only exact selected right Joy-Con 63-byte native 08 payloads are queued.
|
||||
// The 32-entry FIFO is independent of the raw capture ring; overflow discards
|
||||
// queued history and retains only the arriving packet.
|
||||
void switch2_mouse_capture_set_native_stream(bool enabled);
|
||||
|
||||
// Copy the complete opaque 63-byte payload without its report ID. Returns its
|
||||
// nonzero boot-lifetime capture serial, never reused, or 0 with report untouched.
|
||||
// Peek is nondestructive: retry until commit after successful HID submission.
|
||||
// If the latest selected source or FIFO head is >=500 ms old, discard the FIFO.
|
||||
// Signed elapsed time tolerates a producer clock just ahead and uint32 rollover.
|
||||
uint32_t switch2_mouse_capture_peek_native_report(
|
||||
uint32_t now_ms, uint8_t report[SWITCH2_MOUSE_CAPTURE_NATIVE_INPUT_SIZE]);
|
||||
|
||||
// Remove only the exact current head once. False leaves the FIFO unchanged,
|
||||
// including for tokens invalidated by overflow, disable, teardown or selection.
|
||||
bool switch2_mouse_capture_commit_native_report(uint32_t serial);
|
||||
|
||||
// Core 0: one cue for the selected, active right Joy-Con's native stream.
|
||||
// IDs 0..7 only; input must be newer than 500 ms. Accepted requests receive a
|
||||
// nonzero boot-lifetime token, never reused by reset, selection or cancellation.
|
||||
bool switch2_mouse_capture_request_sample(uint8_t sample_id, uint32_t now_ms,
|
||||
uint64_t* token);
|
||||
// 0=pending, 1=verified source ACK (consumed once), -1=failed/stale/expired.
|
||||
// A request expires 2000 ms after acceptance, including time awaiting dispatch.
|
||||
int switch2_mouse_capture_sample_result(uint64_t token, uint32_t now_ms);
|
||||
void switch2_mouse_capture_cancel_sample();
|
||||
#endif
|
||||
|
|
@ -16,6 +16,9 @@
|
|||
#ifdef SWITCH_PICO_WII_IR_MOUSE
|
||||
#include "usb/wii_ir_mouse_usb.h"
|
||||
#endif
|
||||
#ifdef SWITCH_PICO_SWITCH2_MOUSE_CAPTURE
|
||||
#include "input/switch2_mouse_capture.h"
|
||||
#endif
|
||||
|
||||
#ifdef SWITCH_PICO_LOG
|
||||
#define LOG_PRINTF(...) printf(__VA_ARGS__)
|
||||
|
|
@ -219,6 +222,9 @@ int main() {
|
|||
#endif
|
||||
board_init();
|
||||
stdio_init_all();
|
||||
#ifdef SWITCH_PICO_SWITCH2_MOUSE_CAPTURE
|
||||
switch2_mouse_capture_init();
|
||||
#endif
|
||||
|
||||
#ifdef SWITCH_PICO_BLUEPAD32
|
||||
bluepad32_input_backend_init();
|
||||
|
|
|
|||
13
src/firmware/platform/pico/bootsel_button_sample.h
Normal file
13
src/firmware/platform/pico/bootsel_button_sample.h
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
// Samples BOOTSEL through flash_safe_execute. Returns 1 pressed, 0 released,
|
||||
// or -1 when sampling was unsafe/unavailable. Does not trigger pairing actions.
|
||||
int bootsel_button_sample(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
#include "platform/pico/bootsel_button_sample.h"
|
||||
#include "platform/pico/bootsel_pairing_button.h"
|
||||
#include "hardware/gpio.h"
|
||||
#include "hardware/structs/ioqspi.h"
|
||||
|
|
@ -48,19 +49,19 @@ void __no_inline_not_in_flash_func(read_bootsel_callback)(void* parameter) {
|
|||
IO_QSPI_GPIO_QSPI_SS_CTRL_OEOVER_BITS);
|
||||
}
|
||||
|
||||
BootselPairingButtonSample sample_bootsel() {
|
||||
|
||||
} // namespace
|
||||
|
||||
int bootsel_button_sample(void) {
|
||||
bool pressed = false;
|
||||
const int result = flash_safe_execute(read_bootsel_callback, &pressed,
|
||||
kFlashSafeTimeoutMs);
|
||||
if (result != PICO_OK) {
|
||||
return BootselPairingButtonSample::kUnread;
|
||||
return -1;
|
||||
}
|
||||
return pressed ? BootselPairingButtonSample::kPressed
|
||||
: BootselPairingButtonSample::kReleased;
|
||||
return pressed ? 1 : 0;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
BootselPairingButtonEvent BootselPairingButtonHoldFsm::update(
|
||||
BootselPairingButtonSample sample) {
|
||||
if (sample == BootselPairingButtonSample::kUnread) {
|
||||
|
|
@ -96,5 +97,8 @@ BootselPairingButtonEvent bootsel_pairing_button_task() {
|
|||
}
|
||||
g_last_sample_ms = now_ms;
|
||||
|
||||
return g_hold_fsm.update(sample_bootsel());
|
||||
const int sample = bootsel_button_sample();
|
||||
return g_hold_fsm.update(sample < 0 ? BootselPairingButtonSample::kUnread :
|
||||
sample ? BootselPairingButtonSample::kPressed :
|
||||
BootselPairingButtonSample::kReleased);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,9 @@
|
|||
#ifdef SWITCH_PICO_HAPTICS_EXPERIMENT
|
||||
#include "input/haptics_transport_probe.h"
|
||||
#endif
|
||||
#ifdef SWITCH_PICO_SWITCH2_MOUSE_CAPTURE
|
||||
#include "input/switch2_mouse_capture.h"
|
||||
#endif
|
||||
#ifdef SWITCH_PICO_WII_IR_GYRO
|
||||
#include "input/wii_ir_pointer.h"
|
||||
#endif
|
||||
|
|
@ -399,6 +402,28 @@ size_t encode_native_switch_rumble(uint8_t* output, size_t output_size) {
|
|||
#endif
|
||||
}
|
||||
|
||||
size_t encode_switch2_mouse_capture(uint8_t* output, size_t output_size) {
|
||||
#ifdef SWITCH_PICO_SWITCH2_MOUSE_CAPTURE
|
||||
static_assert(kResponseHeaderSize + SWITCH2_MOUSE_CAPTURE_MAXIMUM_PAYLOAD_SIZE <=
|
||||
kMaximumResponseSize,
|
||||
"Switch 2 mouse capture no longer fits the EP0 response buffer");
|
||||
if (output == nullptr || output_size < kResponseHeaderSize) return 0;
|
||||
uint32_t total_records = 0;
|
||||
uint8_t* payload = output + kResponseHeaderSize;
|
||||
const size_t payload_size = switch2_mouse_capture_snapshot(
|
||||
payload, output_size - kResponseHeaderSize, &total_records);
|
||||
if (payload_size == 0) return 0;
|
||||
return encode_response(
|
||||
Operation::kSwitch2MouseCapture, Status::kOk, 0,
|
||||
kSwitch2MouseCaptureSchemaVersion, total_records,
|
||||
payload, payload_size, output, output_size);
|
||||
#else
|
||||
return encode_response(
|
||||
Operation::kSwitch2MouseCapture, Status::kUnsupportedSchema, 0,
|
||||
kSwitch2MouseCaptureSchemaVersion, 0, nullptr, 0, output, output_size);
|
||||
#endif
|
||||
}
|
||||
|
||||
size_t encode_wii_ir_gyro(uint8_t* output, size_t output_size) {
|
||||
#ifdef SWITCH_PICO_WII_IR_GYRO
|
||||
if (output == nullptr || output_size < kResponseHeaderSize) return 0;
|
||||
|
|
@ -976,6 +1001,7 @@ bool usb_configuration_management_vendor_control(
|
|||
const Operation operation =
|
||||
static_cast<Operation>(request->bRequest);
|
||||
if ((operation == Operation::kHapticsTransportProbe ||
|
||||
operation == Operation::kSwitch2MouseCapture ||
|
||||
operation == Operation::kWiiIrGyro) &&
|
||||
request->bmRequestType_bit.direction != TUSB_DIR_IN) {
|
||||
return false;
|
||||
|
|
@ -1058,6 +1084,9 @@ bool usb_configuration_management_vendor_control(
|
|||
case Operation::kNativeSwitchRumble:
|
||||
response_size = encode_native_switch_rumble(response, sizeof(response));
|
||||
break;
|
||||
case Operation::kSwitch2MouseCapture:
|
||||
response_size = encode_switch2_mouse_capture(response, sizeof(response));
|
||||
break;
|
||||
case Operation::kWiiIrGyro:
|
||||
response_size = encode_wii_ir_gyro(response, sizeof(response));
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ constexpr size_t kMacroCaptureEventSize = 20;
|
|||
constexpr uint16_t kNativeSwitchRumbleSchemaVersion = 2;
|
||||
constexpr size_t kNativeSwitchRumbleRowSize = 80;
|
||||
constexpr size_t kNativeSwitchRumblePayloadSize = 4 * kNativeSwitchRumbleRowSize;
|
||||
constexpr uint16_t kSwitch2MouseCaptureSchemaVersion = 1;
|
||||
constexpr size_t kMaximumResponseSize =
|
||||
kResponseHeaderSize + kProfileListPayloadSize;
|
||||
constexpr size_t kMaximumChunkSize =
|
||||
|
|
@ -79,6 +80,7 @@ enum class Operation : uint8_t {
|
|||
kHapticsTransportProbe = 0x41,
|
||||
kMacroCapture = 0x42,
|
||||
kNativeSwitchRumble = 0x43,
|
||||
kSwitch2MouseCapture = 0x44,
|
||||
kWiiIrGyro = 0x45,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -36,7 +36,9 @@
|
|||
namespace {
|
||||
|
||||
AdapterUsbMode g_mode = AdapterUsbMode::kSwitch;
|
||||
#ifndef SWITCH_PICO_SWITCH2_USB_BRIDGE
|
||||
uint16_t g_string_descriptor[32]{};
|
||||
#endif
|
||||
|
||||
bool xinput_selected() {
|
||||
#ifdef SWITCH_PICO_USB_OUTPUT_MODES
|
||||
|
|
@ -55,9 +57,11 @@ bool generic_selected() {
|
|||
#endif
|
||||
}
|
||||
|
||||
#ifndef SWITCH_PICO_SWITCH2_USB_BRIDGE
|
||||
bool switch_selected() {
|
||||
return !xinput_selected() && !generic_selected();
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace
|
||||
|
||||
|
|
@ -193,6 +197,9 @@ void usb_output_driver_set_rumble_callback(
|
|||
switch_pro_set_rumble_callback(instance, callback);
|
||||
}
|
||||
|
||||
// The bridge's probe main owns every descriptor and TinyUSB callback. Keep the
|
||||
// legacy output APIs above available to the normal backend dependency graph.
|
||||
#ifndef SWITCH_PICO_SWITCH2_USB_BRIDGE
|
||||
extern "C" 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) {
|
||||
|
|
@ -454,3 +461,4 @@ extern "C" usbd_class_driver_t const* usbd_app_driver_get_cb(
|
|||
*driver_count = 0;
|
||||
return nullptr;
|
||||
}
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
#pragma once
|
||||
#include <mutex>
|
||||
|
||||
struct critical_section_t { std::mutex mutex; };
|
||||
inline unsigned next_striped_spin_lock_num() { return 16; }
|
||||
inline void critical_section_init_with_lock_num(critical_section_t*, unsigned) {}
|
||||
inline void critical_section_enter_blocking(critical_section_t* lock) { lock->mutex.lock(); }
|
||||
inline void critical_section_exit(critical_section_t* lock) { lock->mutex.unlock(); }
|
||||
9
tests/switch2_mouse_bridge_native_stubs/pico/stdlib.h
Normal file
9
tests/switch2_mouse_bridge_native_stubs/pico/stdlib.h
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
#pragma once
|
||||
#include <stdint.h>
|
||||
|
||||
using absolute_time_t = uint64_t;
|
||||
absolute_time_t get_absolute_time();
|
||||
uint32_t to_ms_since_boot(absolute_time_t value);
|
||||
absolute_time_t make_timeout_time_ms(uint32_t timeout);
|
||||
bool time_reached(absolute_time_t deadline);
|
||||
void sleep_ms(uint32_t milliseconds);
|
||||
363
tests/switch2_mouse_bridge_test.cpp
Normal file
363
tests/switch2_mouse_bridge_test.cpp
Normal file
|
|
@ -0,0 +1,363 @@
|
|||
#include "controller_input.h"
|
||||
#include "input/bluepad32_input_backend.h"
|
||||
#include "input/switch2_mouse_capture.h"
|
||||
#include "platform/pico/bootsel_pairing_button.h"
|
||||
#include "parser/uni_hid_parser_switch2.h"
|
||||
#include "pico/stdlib.h"
|
||||
#include <array>
|
||||
#include <cassert>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
static uint64_t now;
|
||||
static uint32_t stage;
|
||||
static BootselPairingButtonEvent next_button_event = BootselPairingButtonEvent::kNone;
|
||||
static unsigned pairing_requests, clear_requests, button_polls;
|
||||
BootselPairingButtonEvent bootsel_pairing_button_task() {
|
||||
++button_polls;
|
||||
const auto event = next_button_event;
|
||||
next_button_event = BootselPairingButtonEvent::kNone;
|
||||
return event;
|
||||
}
|
||||
void bluepad32_input_backend_open_pairing_window() { ++pairing_requests; }
|
||||
uint32_t bluepad32_input_backend_clear_pairings() { ++clear_requests; return 1; }
|
||||
static const uint8_t source_address[] = {0x98,0xe2,0x55,7,0xdf,0};
|
||||
static const uint8_t other_address[] = {0x98,0xe2,0x55,7,0xdf,1};
|
||||
void system_clock_initialize() {}
|
||||
void bluepad32_input_backend_init() { stage = 1; }
|
||||
void controller_profile_runtime_reset() {}
|
||||
void bluepad32_input_backend_start() { stage = 2; }
|
||||
void bluepad32_input_backend_diagnostics(Bluepad32BackendDiagnostics* out) {
|
||||
*out = {}; out->initialization_stage = stage;
|
||||
}
|
||||
absolute_time_t make_timeout_time_ms(uint32_t timeout) { return now + timeout; }
|
||||
absolute_time_t get_absolute_time() { return now; }
|
||||
uint32_t to_ms_since_boot(absolute_time_t value) { return static_cast<uint32_t>(value); }
|
||||
bool time_reached(absolute_time_t deadline) { return now >= deadline; }
|
||||
void sleep_ms(uint32_t milliseconds) { now += milliseconds; }
|
||||
|
||||
using NativeReport = std::array<uint8_t, 63>;
|
||||
|
||||
static NativeReport native_report(uint8_t counter, uint8_t motion_length,
|
||||
int16_t x = 1, int16_t y = -2) {
|
||||
NativeReport report{};
|
||||
// Deliberately opaque, nonzero bytes, including NFC and reserved fields.
|
||||
// Byte 15 declares 30/40 packed motion bytes at 16..55; do not decode them
|
||||
// or normalize the unused tail of a 30-byte sample.
|
||||
for (size_t i = 0; i < report.size(); ++i)
|
||||
report[i] = static_cast<uint8_t>((i * 37 + counter) % 255 + 1);
|
||||
report[0] = counter;
|
||||
report[1] = 0x93;
|
||||
report[2] = 0x12; report[3] = 0xd1;
|
||||
report[4] = 0xe7;
|
||||
report[5] = 0x23; report[6] = 0x81; report[7] = 0x45;
|
||||
report[8] = 0x38;
|
||||
report[9] = static_cast<uint8_t>(x);
|
||||
report[10] = static_cast<uint16_t>(x) >> 8;
|
||||
report[11] = static_cast<uint8_t>(y);
|
||||
report[12] = static_cast<uint16_t>(y) >> 8;
|
||||
report[13] = 0x1b;
|
||||
report[15] = motion_length;
|
||||
return report;
|
||||
}
|
||||
|
||||
static void emit(const NativeReport& report, const uint8_t* address = source_address,
|
||||
uint16_t product_id = 0x2066, uint8_t report_id = 8,
|
||||
uint16_t length = 63) {
|
||||
assert(length <= report.size());
|
||||
switch_pico_switch2_mouse_report(product_id, address, report_id, report.data(),
|
||||
length, static_cast<uint32_t>(now));
|
||||
}
|
||||
|
||||
static void disconnect(const uint8_t* address = source_address,
|
||||
uint16_t product_id = 0x2066) {
|
||||
switch_pico_switch2_mouse_report(product_id, address, 0, nullptr, 0,
|
||||
static_cast<uint32_t>(now));
|
||||
}
|
||||
|
||||
static probe_controller_input poll(uint32_t timestamp = static_cast<uint32_t>(now)) {
|
||||
probe_controller_input input{};
|
||||
probe_controller_input_poll(timestamp, &input);
|
||||
return input;
|
||||
}
|
||||
|
||||
static uint32_t expect_report(const NativeReport& expected,
|
||||
uint32_t timestamp = static_cast<uint32_t>(now)) {
|
||||
NativeReport actual;
|
||||
actual.fill(0xa5);
|
||||
const uint32_t serial =
|
||||
probe_controller_input_peek_native_report(timestamp, actual.data());
|
||||
assert(serial != 0 && actual == expected);
|
||||
return serial;
|
||||
}
|
||||
|
||||
static void expect_empty() {
|
||||
NativeReport actual;
|
||||
actual.fill(0xa5);
|
||||
const auto untouched = actual;
|
||||
assert(probe_controller_input_peek_native_report(
|
||||
static_cast<uint32_t>(now), actual.data()) == 0);
|
||||
assert(actual == untouched);
|
||||
}
|
||||
|
||||
static void expect_inactive(const probe_controller_input& input) {
|
||||
assert(!input.active && input.mouse_epoch == 0);
|
||||
assert(input.buttons[0] == 0 && input.buttons[1] == 0);
|
||||
assert(input.stick[0] == 0 && input.stick[1] == 0 && input.stick[2] == 0);
|
||||
assert(input.native_status == 0 && input.mouse_surface == 0);
|
||||
assert(input.mouse_total_x == 0 && input.mouse_total_y == 0);
|
||||
}
|
||||
|
||||
static void test_startup_pairing_and_stream_gate() {
|
||||
next_button_event = BootselPairingButtonEvent::kOpenPairing;
|
||||
assert(!probe_controller_input_pairing_task() && button_polls == 0 && pairing_requests == 0);
|
||||
probe_controller_input_set_native_stream(true);
|
||||
expect_empty();
|
||||
assert(!probe_controller_input_commit_native_report(1));
|
||||
expect_inactive(poll());
|
||||
probe_controller_input_clock_init();
|
||||
probe_controller_input_init();
|
||||
|
||||
// Even an enable request after init must not open the pre-flash-ready gate.
|
||||
probe_controller_input_set_native_stream(true);
|
||||
const auto report = native_report(0x31, 30, -6, 9);
|
||||
emit(report);
|
||||
expect_empty();
|
||||
assert(probe_controller_input_start());
|
||||
expect_empty();
|
||||
assert(probe_controller_input_pairing_task() && pairing_requests == 1);
|
||||
assert(!probe_controller_input_pairing_task());
|
||||
next_button_event = BootselPairingButtonEvent::kClearPairings;
|
||||
assert(!probe_controller_input_pairing_task() && clear_requests == 0 && pairing_requests == 1);
|
||||
next_button_event = BootselPairingButtonEvent::kOpenPairing;
|
||||
assert(probe_controller_input_pairing_task() && pairing_requests == 2 && clear_requests == 0);
|
||||
|
||||
const auto input = poll();
|
||||
assert(input.active && input.buttons[0] == 0x12 && input.buttons[1] == 0xd1);
|
||||
assert(input.stick[0] == 0x23 && input.stick[1] == 0x81 && input.stick[2] == 0x45);
|
||||
assert(input.native_status == 0x38 && input.mouse_surface == 0x1b);
|
||||
assert(input.mouse_total_x == -6 && input.mouse_total_y == 9);
|
||||
probe_controller_input_set_native_stream(true);
|
||||
expect_empty(); // Enabling never replays the latest input or raw ring.
|
||||
emit(report);
|
||||
const uint32_t pending = expect_report(report);
|
||||
probe_controller_input_set_native_stream(false);
|
||||
assert(!probe_controller_input_commit_native_report(pending));
|
||||
emit(report); // Selected input continues updating while native USB is gated.
|
||||
assert(poll().active);
|
||||
expect_empty();
|
||||
probe_controller_input_set_native_stream(true);
|
||||
expect_empty();
|
||||
emit(report);
|
||||
const uint32_t resumed = expect_report(report);
|
||||
assert(resumed > pending);
|
||||
assert(probe_controller_input_commit_native_report(resumed));
|
||||
expect_empty();
|
||||
}
|
||||
|
||||
static void test_opaque_fidelity_order_and_retry() {
|
||||
now = 100;
|
||||
const auto first = native_report(0xfe, 30, -6, 9);
|
||||
const auto repeated = native_report(0xff, 40, -32768, 32767);
|
||||
const auto last = native_report(0x00, 30, 1, -2);
|
||||
emit(first);
|
||||
const uint32_t first_serial = expect_report(first);
|
||||
|
||||
// A failed USB submission simply does not commit. New arrivals must not
|
||||
// overwrite that retry, combine deltas, or collapse identical packets.
|
||||
++now; emit(repeated);
|
||||
++now; emit(repeated);
|
||||
++now; emit(last);
|
||||
const uint32_t last_serial = poll().serial;
|
||||
assert(last_serial > first_serial);
|
||||
assert(!probe_controller_input_commit_native_report(last_serial));
|
||||
assert(!probe_controller_input_commit_native_report(0));
|
||||
probe_controller_input_set_native_stream(true);
|
||||
assert(expect_report(first) == first_serial);
|
||||
assert(expect_report(first) == first_serial);
|
||||
assert(probe_controller_input_commit_native_report(first_serial));
|
||||
assert(!probe_controller_input_commit_native_report(first_serial));
|
||||
|
||||
const uint32_t second_serial = expect_report(repeated);
|
||||
assert(second_serial > first_serial);
|
||||
assert(probe_controller_input_commit_native_report(second_serial));
|
||||
const uint32_t third_serial = expect_report(repeated);
|
||||
assert(third_serial > second_serial);
|
||||
assert(!probe_controller_input_commit_native_report(second_serial));
|
||||
assert(expect_report(repeated) == third_serial);
|
||||
assert(probe_controller_input_commit_native_report(third_serial));
|
||||
assert(expect_report(last) == last_serial);
|
||||
assert(probe_controller_input_commit_native_report(last_serial));
|
||||
expect_empty();
|
||||
assert(!probe_controller_input_commit_native_report(last_serial));
|
||||
expect_empty(); // No cached duplicate report when the source has not advanced.
|
||||
}
|
||||
|
||||
static void test_selected_source_isolation_and_reconnect() {
|
||||
now = 200;
|
||||
const auto first = native_report(0x41, 30, -17, 19);
|
||||
const auto second = native_report(0x42, 40, 31, -37);
|
||||
const auto unrelated = native_report(0x99, 40, 300, 300);
|
||||
emit(first);
|
||||
const auto selected = poll();
|
||||
const uint32_t first_serial = expect_report(first);
|
||||
assert(first_serial == selected.serial);
|
||||
for (unsigned i = 0; i < 30; ++i) emit(unrelated, other_address);
|
||||
emit(unrelated, source_address, 0x2067); // Left Joy-Con at the same address.
|
||||
emit(unrelated, source_address, 0x2066, 5);
|
||||
emit(unrelated, source_address, 0x2066, 0xc0, 12);
|
||||
emit(unrelated, source_address, 0x2066, 8, 62);
|
||||
uint8_t oversized[64];
|
||||
memcpy(oversized, unrelated.data(), unrelated.size());
|
||||
oversized[63] = 0x5a;
|
||||
switch_pico_switch2_mouse_report(0x2066, source_address, 8, oversized,
|
||||
sizeof(oversized), static_cast<uint32_t>(now));
|
||||
disconnect(other_address);
|
||||
disconnect(source_address, 0x2067);
|
||||
const auto isolated = poll();
|
||||
assert(isolated.active && isolated.serial == selected.serial);
|
||||
assert(isolated.mouse_epoch == selected.mouse_epoch);
|
||||
assert(isolated.mouse_total_x == selected.mouse_total_x &&
|
||||
isolated.mouse_total_y == selected.mouse_total_y);
|
||||
assert(expect_report(first) == first_serial);
|
||||
++now; emit(second);
|
||||
const uint32_t second_serial = poll().serial;
|
||||
assert(probe_controller_input_commit_native_report(first_serial));
|
||||
assert(expect_report(second) == second_serial);
|
||||
assert(probe_controller_input_commit_native_report(second_serial));
|
||||
expect_empty(); // Unrelated ring entries neither evict nor enter the FIFO.
|
||||
|
||||
emit(first);
|
||||
const uint32_t disconnected_serial = expect_report(first);
|
||||
disconnect();
|
||||
++now; emit(second); // Disconnect and reconnect both occur between polls.
|
||||
const auto reconnected = poll();
|
||||
assert(reconnected.active && reconnected.mouse_epoch != selected.mouse_epoch);
|
||||
assert(reconnected.mouse_total_x == 31 && reconnected.mouse_total_y == -37);
|
||||
assert(!probe_controller_input_commit_native_report(disconnected_serial));
|
||||
assert(expect_report(second) == reconnected.serial);
|
||||
assert(reconnected.serial > disconnected_serial);
|
||||
assert(probe_controller_input_commit_native_report(reconnected.serial));
|
||||
expect_empty();
|
||||
|
||||
emit(first);
|
||||
const uint32_t pending = expect_report(first);
|
||||
disconnect();
|
||||
for (unsigned i = 0; i < 30; ++i) emit(unrelated, other_address);
|
||||
expect_inactive(poll());
|
||||
expect_empty();
|
||||
assert(!probe_controller_input_commit_native_report(pending));
|
||||
++now; emit(second);
|
||||
const auto resumed = poll();
|
||||
assert(resumed.active && resumed.mouse_epoch != reconnected.mouse_epoch);
|
||||
const uint32_t resumed_serial = expect_report(second);
|
||||
assert(resumed_serial > pending);
|
||||
assert(probe_controller_input_commit_native_report(resumed_serial));
|
||||
|
||||
emit(first);
|
||||
const uint32_t old_source = expect_report(first);
|
||||
emit(unrelated, other_address);
|
||||
switch2_mouse_capture_select_input(other_address);
|
||||
expect_empty();
|
||||
assert(!probe_controller_input_commit_native_report(old_source));
|
||||
probe_controller_input_set_native_stream(true);
|
||||
expect_empty(); // Selection cannot revive the other peer's raw history.
|
||||
emit(first);
|
||||
expect_empty();
|
||||
emit(unrelated, other_address);
|
||||
const uint32_t new_source = expect_report(unrelated);
|
||||
assert(new_source > old_source);
|
||||
switch2_mouse_capture_select_input(source_address);
|
||||
probe_controller_input_set_native_stream(true);
|
||||
expect_empty();
|
||||
assert(!probe_controller_input_commit_native_report(new_source));
|
||||
emit(second);
|
||||
const uint32_t restored = expect_report(second);
|
||||
assert(restored > new_source);
|
||||
assert(probe_controller_input_commit_native_report(restored));
|
||||
}
|
||||
|
||||
static void test_bounded_overflow() {
|
||||
now = 1000;
|
||||
const auto first = native_report(0x50, 30);
|
||||
emit(first);
|
||||
const uint32_t old_serial = expect_report(first);
|
||||
// The 32-entry contract bounds backlog independently of the diagnostic ring.
|
||||
for (unsigned i = 1; i < 32; ++i) {
|
||||
++now;
|
||||
emit(native_report(static_cast<uint8_t>(0x50 + i), 40));
|
||||
}
|
||||
assert(expect_report(first) == old_serial);
|
||||
const auto newest = native_report(0xbb, 30, -101, 103);
|
||||
++now; emit(newest);
|
||||
assert(!probe_controller_input_commit_native_report(old_serial));
|
||||
const uint32_t newest_serial = expect_report(newest);
|
||||
assert(newest_serial > old_serial);
|
||||
const auto following = native_report(0xbc, 40, 107, -109);
|
||||
++now; emit(following);
|
||||
assert(expect_report(newest) == newest_serial);
|
||||
assert(probe_controller_input_commit_native_report(newest_serial));
|
||||
const uint32_t following_serial = expect_report(following);
|
||||
assert(following_serial > newest_serial);
|
||||
assert(probe_controller_input_commit_native_report(following_serial));
|
||||
expect_empty(); // Overflow discarded all prior history, not merely its head.
|
||||
}
|
||||
|
||||
static void test_expiry_and_wrapping_clock() {
|
||||
now = 2000;
|
||||
const auto first = native_report(0x61, 30);
|
||||
const auto fresh = native_report(0x62, 40);
|
||||
emit(first);
|
||||
const uint32_t expired = expect_report(first);
|
||||
now += 499;
|
||||
assert(poll().active && expect_report(first) == expired);
|
||||
++now;
|
||||
emit(fresh, other_address); // Wrong-source traffic cannot refresh the timeout.
|
||||
expect_inactive(poll());
|
||||
expect_empty();
|
||||
assert(!probe_controller_input_commit_native_report(expired));
|
||||
|
||||
++now; emit(first);
|
||||
const uint32_t stale_head = expect_report(first);
|
||||
now += 499; emit(fresh);
|
||||
assert(expect_report(first) == stale_head);
|
||||
++now;
|
||||
assert(poll().active); // Latest source is fresh, but its queued head is not.
|
||||
expect_empty();
|
||||
assert(!probe_controller_input_commit_native_report(stale_head));
|
||||
emit(fresh);
|
||||
const uint32_t resumed = expect_report(fresh);
|
||||
assert(resumed > stale_head);
|
||||
assert(probe_controller_input_commit_native_report(resumed));
|
||||
expect_empty();
|
||||
|
||||
now = static_cast<uint64_t>(UINT32_MAX) - 100;
|
||||
emit(first);
|
||||
const uint32_t wrapped = expect_report(first);
|
||||
// The producer can timestamp input one millisecond after the caller samples
|
||||
// its clock; a signed age must accept this race rather than expire the input.
|
||||
const uint32_t before_capture = static_cast<uint32_t>(now) - 1;
|
||||
assert(poll(before_capture).active);
|
||||
assert(expect_report(first, before_capture) == wrapped);
|
||||
now += 499; // Cross the uint32 millisecond rollover with a fresh packet.
|
||||
assert(poll().active && expect_report(first) == wrapped);
|
||||
++now;
|
||||
expect_inactive(poll());
|
||||
expect_empty();
|
||||
assert(!probe_controller_input_commit_native_report(wrapped));
|
||||
++now; emit(fresh);
|
||||
assert(poll().active);
|
||||
const uint32_t after_wrap = expect_report(fresh);
|
||||
assert(after_wrap > wrapped);
|
||||
assert(probe_controller_input_commit_native_report(after_wrap));
|
||||
expect_empty();
|
||||
}
|
||||
|
||||
int main() {
|
||||
test_startup_pairing_and_stream_gate();
|
||||
test_opaque_fidelity_order_and_retry();
|
||||
test_selected_source_isolation_and_reconnect();
|
||||
test_bounded_overflow();
|
||||
test_expiry_and_wrapping_clock();
|
||||
puts("Native packet fidelity, FIFO retry/order, source barriers, overflow, expiry and pairing passed");
|
||||
}
|
||||
103
tests/switch2_usb_probe_protocol_test.c
Normal file
103
tests/switch2_usb_probe_protocol_test.c
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
#include <assert.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#include "protocol.h"
|
||||
|
||||
static const uint8_t sample_command[] = {
|
||||
0x0a, 0x91, 0, 0x02, 0, 4, 0, 0, 3, 0, 0, 0,
|
||||
};
|
||||
static unsigned source_calls;
|
||||
static uint8_t expected_sample = 3;
|
||||
static bool source_available = true;
|
||||
static uint64_t source_token = UINT64_C(0x1234567800000001);
|
||||
|
||||
static bool play_sample(uint8_t sample_id, uint64_t* token) {
|
||||
++source_calls;
|
||||
assert(sample_id == expected_sample);
|
||||
*token = source_token;
|
||||
return source_available;
|
||||
}
|
||||
|
||||
static void expect_no_dispatch(probe_protocol_state* state, const uint8_t* command,
|
||||
size_t length, size_t capacity) {
|
||||
uint8_t reply[8];
|
||||
uint64_t token = UINT64_MAX;
|
||||
const unsigned calls_before = source_calls;
|
||||
assert(probe_protocol_command(state, command, length, reply, capacity, &token) == 0);
|
||||
assert(token == 0);
|
||||
assert(source_calls == calls_before);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
probe_protocol_state state;
|
||||
probe_protocol_reset(&state);
|
||||
state.play_sample = play_sample;
|
||||
|
||||
// Each transport/header field and reserved payload byte is a dispatch gate.
|
||||
const struct { uint8_t offset; uint8_t value; } invalid[] = {
|
||||
{0, 0x18}, {1, 0x01}, {2, 0x01}, {3, 0x01}, {4, 1}, {5, 3},
|
||||
{6, 1}, {7, 1}, {8, 8}, {9, 1}, {10, 1}, {11, 1},
|
||||
};
|
||||
for (size_t i = 0; i < sizeof(invalid) / sizeof(invalid[0]); ++i) {
|
||||
uint8_t command[sizeof(sample_command)];
|
||||
memcpy(command, sample_command, sizeof(command));
|
||||
command[invalid[i].offset] = invalid[i].value;
|
||||
expect_no_dispatch(&state, command, sizeof(command), 8);
|
||||
}
|
||||
expect_no_dispatch(&state, sample_command, 7, 8);
|
||||
uint8_t resized[13] = {0};
|
||||
memcpy(resized, sample_command, sizeof(sample_command));
|
||||
resized[5] = 3;
|
||||
expect_no_dispatch(&state, resized, 11, 8);
|
||||
resized[5] = 5;
|
||||
expect_no_dispatch(&state, resized, sizeof(resized), 8);
|
||||
expect_no_dispatch(&state, sample_command, sizeof(sample_command), 7);
|
||||
|
||||
// Synchronous-only callers cannot accidentally acknowledge a sample.
|
||||
uint8_t reply[8];
|
||||
const unsigned calls_before = source_calls;
|
||||
assert(probe_protocol_command(&state, sample_command, sizeof(sample_command),
|
||||
reply, sizeof(reply), NULL) == 0);
|
||||
assert(source_calls == calls_before);
|
||||
state.play_sample = NULL;
|
||||
expect_no_dispatch(&state, sample_command, sizeof(sample_command), sizeof(reply));
|
||||
state.play_sample = play_sample;
|
||||
|
||||
// A rejected request must not leak even a token written by the source.
|
||||
uint64_t token = UINT64_MAX;
|
||||
source_available = false;
|
||||
assert(probe_protocol_command(&state, sample_command, sizeof(sample_command),
|
||||
reply, sizeof(reply), &token) == 0);
|
||||
assert(token == 0);
|
||||
source_available = true;
|
||||
source_token = 0;
|
||||
token = UINT64_MAX;
|
||||
assert(probe_protocol_command(&state, sample_command, sizeof(sample_command),
|
||||
reply, sizeof(reply), &token) == 0);
|
||||
assert(token == 0);
|
||||
|
||||
// The observed sample and range boundaries only produce deferred replies.
|
||||
const uint8_t samples[] = {3, 0, 7};
|
||||
const uint8_t sample_ack[] = {0x0a, 0x01, 0, 0x02, 0, 0xf8, 0, 0};
|
||||
source_token = UINT64_C(0x1234567800000001);
|
||||
for (size_t i = 0; i < sizeof(samples); ++i) {
|
||||
uint8_t command[sizeof(sample_command)];
|
||||
memcpy(command, sample_command, sizeof(command));
|
||||
command[8] = expected_sample = samples[i];
|
||||
token = 0;
|
||||
assert(probe_protocol_command(&state, command, sizeof(command), reply,
|
||||
sizeof(reply), &token) == sizeof(sample_ack));
|
||||
assert(token == source_token);
|
||||
assert(memcmp(reply, sample_ack, sizeof(sample_ack)) == 0);
|
||||
++source_token;
|
||||
}
|
||||
|
||||
// Ordinary report selection retains its immediate, empty USB ACK.
|
||||
const uint8_t select_report[] = {0x03, 0x91, 0, 0x0a, 0, 4, 0, 0, 5, 0, 0, 0};
|
||||
const uint8_t select_ack[] = {0x03, 0x01, 0, 0x0a, 0, 0xf8, 0, 0};
|
||||
assert(probe_protocol_command(&state, select_report, sizeof(select_report),
|
||||
reply, sizeof(reply), &token) == sizeof(select_ack));
|
||||
assert(token == 0);
|
||||
assert(memcmp(reply, select_ack, sizeof(select_ack)) == 0);
|
||||
return 0;
|
||||
}
|
||||
26
tests/test_switch2_mouse_bridge_native.py
Normal file
26
tests/test_switch2_mouse_bridge_native.py
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
|
||||
def test_native_packet_fidelity_and_lifecycle(tmp_path: Path) -> None:
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
cxx = shutil.which("c++") or shutil.which("g++")
|
||||
assert cxx is not None, "a host C++ compiler is required"
|
||||
probe = root / "tools" / "switch2_usb_probe"
|
||||
executable = tmp_path / "switch2_mouse_bridge_test"
|
||||
subprocess.run(
|
||||
[cxx, "-std=c++17", "-Wall", "-Wextra", "-Werror", "-pthread",
|
||||
"-DSWITCH_PICO_SWITCH2_USB_BRIDGE=1", "-DSWITCH_PICO_BLUEPAD32=1",
|
||||
"-DSWITCH_PICO_ENABLE_BLE=1", "-DSWITCH_PICO_SWITCH2_MOUSE_CAPTURE=1",
|
||||
"-DSWITCH_PICO_SWITCH2_MOUSE_CAPTURE_NATIVE=1",
|
||||
"-DSWITCH2_BRIDGE_SOURCE_ADDRESS_BYTES=0x98,0xe2,0x55,0x07,0xdf,0x00",
|
||||
f"-I{root / 'tests' / 'switch2_mouse_bridge_native_stubs'}",
|
||||
f"-I{probe}", f"-I{root / 'src' / 'firmware'}", f"-I{root / 'bluepad32_config'}",
|
||||
str(root / "tests" / "switch2_mouse_bridge_test.cpp"),
|
||||
str(probe / "controller_input.cpp"),
|
||||
str(root / "src" / "firmware" / "input" / "switch2_mouse_capture.cpp"),
|
||||
"-o", str(executable)],
|
||||
check=True, cwd=root,
|
||||
)
|
||||
subprocess.run([str(executable)], check=True, cwd=root)
|
||||
343
tests/test_switch2_sample_relay_native.py
Normal file
343
tests/test_switch2_sample_relay_native.py
Normal file
|
|
@ -0,0 +1,343 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "tools"))
|
||||
|
||||
from prepare_bluepad32 import prepare_bluepad32
|
||||
|
||||
|
||||
# Reuse the existing real-BTstack-header radio/run-loop fixture, but drive the
|
||||
# capture-enabled discovery path and link the actual cross-core capture mailbox.
|
||||
SOURCE = r'''
|
||||
#define main parser_fixture_main
|
||||
#include "switch2_parser_native_test.c"
|
||||
#undef main
|
||||
|
||||
void capture_init(void);
|
||||
bool capture_request(uint8_t id, uint64_t* token);
|
||||
int capture_result(uint64_t token);
|
||||
void capture_cancel(void);
|
||||
void capture_exhaust_records(void);
|
||||
|
||||
#define SECONDARY_HANDLE 0x144
|
||||
static const uint8_t secondary_uuid[16] = {
|
||||
0xd5,0xa9,0xe0,0x1e,0x2f,0xfc,0x4c,0xca,0xb2,0x0c,0x8b,0x67,0x14,0x2b,0xf4,0x42};
|
||||
static const uint8_t sample_ack[8] = {0x0a,1,1,2,0x10,0x78,0,0};
|
||||
|
||||
static void native_input(struct fixture_peer* peer) {
|
||||
uint8_t input[63] = {0};
|
||||
notify(peer, SECONDARY_HANDLE, input, sizeof(input));
|
||||
}
|
||||
|
||||
static struct fixture_peer* sample_ready_on_handle(bool requests, uint32_t start,
|
||||
hci_con_handle_t handle) {
|
||||
reset();
|
||||
now_ms = start;
|
||||
capture_init();
|
||||
request_writes = requests;
|
||||
uint8_t advertisement_data[64];
|
||||
size_t advertisement_size = advertisement(advertisement_data, UNI_SW2_JOYCON_R_PID, false);
|
||||
assert(uni_bt_le_switch2_handle_advertisement(advertisement_data, advertisement_size));
|
||||
struct fixture_peer* peer = &peers[0];
|
||||
peer->device.conn.handle = handle;
|
||||
peer->link_alive = true;
|
||||
uni_hid_parser_switch2_on_le_connected(&peer->device);
|
||||
uint8_t service[28] = {GATT_EVENT_SERVICE_QUERY_RESULT};
|
||||
little_endian_store_16(service, 8, SERVICE_START);
|
||||
little_endian_store_16(service, 10, SERVICE_START + 0x60);
|
||||
reverse_128(service_uuid, service + 12);
|
||||
event(peer, service, sizeof(service));
|
||||
query_done(peer, 0);
|
||||
const unsigned write = requests ? ATT_PROPERTY_WRITE : ATT_PROPERTY_WRITE_WITHOUT_RESPONSE;
|
||||
characteristic(peer, INPUT_HANDLE, input_uuid, ATT_PROPERTY_NOTIFY);
|
||||
characteristic(peer, RESPONSE_HANDLE, response_uuid, ATT_PROPERTY_NOTIFY);
|
||||
characteristic(peer, COMMAND_HANDLE, command_uuid, write);
|
||||
characteristic(peer, RUMBLE_HANDLE, rumble_uuids[2], write);
|
||||
characteristic(peer, SECONDARY_HANDLE, secondary_uuid, ATT_PROPERTY_NOTIFY);
|
||||
query_done(peer, 0);
|
||||
descriptor(peer, RESPONSE_HANDLE + 2);
|
||||
query_done(peer, 0);
|
||||
descriptor(peer, INPUT_HANDLE + 2);
|
||||
query_done(peer, 0);
|
||||
descriptor(peer, SECONDARY_HANDLE + 2);
|
||||
query_done(peer, 0);
|
||||
query_done(peer, 0);
|
||||
for (unsigned i = 0; i < 24 && !ready && !disconnected; ++i) {
|
||||
if (peer->query == QUERY_CCCD) {
|
||||
query_done(peer, 0);
|
||||
} else if (peer->command[0] == 0x10) {
|
||||
uint8_t version[20] = {0x10,1,1,1,0x10,0x78,0,0};
|
||||
version[11] = 1;
|
||||
if (peer->query == QUERY_WRITE) query_done(peer, 0);
|
||||
notify(peer, RESPONSE_HANDLE, version, sizeof(version));
|
||||
} else {
|
||||
acknowledge(peer, false);
|
||||
}
|
||||
}
|
||||
assert(ready == 1 && !disconnected);
|
||||
// Let the unchanged neutral-rumble budget quiesce before requesting a cue.
|
||||
for (unsigned i = 0; i < 6; ++i) {
|
||||
advance(13);
|
||||
if (peer->query == QUERY_WRITE) query_done(peer, 0);
|
||||
}
|
||||
native_input(peer);
|
||||
return peer;
|
||||
}
|
||||
|
||||
static struct fixture_peer* sample_ready(bool requests, uint32_t start) {
|
||||
return sample_ready_on_handle(requests, start, 0);
|
||||
}
|
||||
|
||||
static uint64_t request_sample(struct fixture_peer* peer, uint8_t id) {
|
||||
uint64_t token = 0;
|
||||
assert(capture_request(id, &token) && token);
|
||||
assert(capture_result(token) == 0);
|
||||
unsigned commands = peer->commands;
|
||||
advance(13);
|
||||
const uint8_t expected[12] = {0x0a,0x91,1,2,0,4,0,0,id,0,0,0};
|
||||
assert(peer->commands == commands + 1 && peer->command_length == sizeof(expected));
|
||||
assert(memcmp(peer->command, expected, sizeof(expected)) == 0);
|
||||
assert(capture_result(token) == 0);
|
||||
return token;
|
||||
}
|
||||
|
||||
static void successful_ack(struct fixture_peer* peer, uint64_t token) {
|
||||
if (peer->query == QUERY_WRITE) query_done(peer, 0);
|
||||
assert(capture_result(token) == 0);
|
||||
notify(peer, RESPONSE_HANDLE, sample_ack, sizeof(sample_ack));
|
||||
assert(capture_result(token) == 1);
|
||||
assert(capture_result(token) == -1);
|
||||
}
|
||||
|
||||
static void test_ack_order_and_source_matching(void) {
|
||||
struct fixture_peer* peer = sample_ready(true, 0);
|
||||
uint64_t token = request_sample(peer, 3), refused = 99;
|
||||
assert(!capture_request(4, &refused) && refused == 0);
|
||||
uint8_t malformed[9] = {0x0a,1,1,2,0x10,0x78,0,0,0};
|
||||
notify(peer, RESPONSE_HANDLE, malformed, sizeof(malformed));
|
||||
malformed[3] = 1;
|
||||
notify(peer, RESPONSE_HANDLE, malformed, 8);
|
||||
assert(capture_result(token) == 0);
|
||||
// Deliver a genuine-shaped ACK from a different Bluetooth handle.
|
||||
uint8_t wrong_peer[20] = {GATT_EVENT_NOTIFICATION,18};
|
||||
little_endian_store_16(wrong_peer, 2, 99);
|
||||
little_endian_store_16(wrong_peer, 8, RESPONSE_HANDLE);
|
||||
little_endian_store_16(wrong_peer, 10, sizeof(sample_ack));
|
||||
memcpy(wrong_peer + 12, sample_ack, sizeof(sample_ack));
|
||||
peer->callback(HCI_EVENT_PACKET, 0, wrong_peer, sizeof(wrong_peer));
|
||||
assert(capture_result(token) == 0);
|
||||
notify(peer, RESPONSE_HANDLE, sample_ack, sizeof(sample_ack));
|
||||
assert(capture_result(token) == 0); // Application ACK cannot beat ATT completion.
|
||||
query_done(peer, 0);
|
||||
assert(capture_result(token) == 1 && capture_result(token) == -1);
|
||||
for (uint8_t id = 0; id < 8; ++id) {
|
||||
uint64_t next = request_sample(peer, id);
|
||||
assert(next > token);
|
||||
token = next;
|
||||
successful_ack(peer, token); // ATT success alone is not application success.
|
||||
}
|
||||
assert(!capture_request(8, &refused) && !capture_request(3, NULL));
|
||||
}
|
||||
|
||||
static void test_cancel_does_not_transfer_old_ack(void) {
|
||||
struct fixture_peer* peer = sample_ready(true, 0);
|
||||
uint64_t old = request_sample(peer, 3), next;
|
||||
capture_cancel();
|
||||
assert(capture_result(old) == -1);
|
||||
assert(capture_request(4, &next) && next > old);
|
||||
unsigned commands = peer->commands;
|
||||
advance(13);
|
||||
assert(peer->commands == commands); // Old untagged ACK must drain first.
|
||||
notify(peer, RESPONSE_HANDLE, sample_ack, sizeof(sample_ack));
|
||||
query_done(peer, 0);
|
||||
assert(capture_result(old) == -1 && capture_result(next) == 0);
|
||||
advance(13);
|
||||
assert(peer->commands == commands + 1 && peer->command[8] == 4);
|
||||
successful_ack(peer, next);
|
||||
|
||||
peer = sample_ready(false, 0);
|
||||
commands = peer->commands;
|
||||
assert(capture_request(3, &old));
|
||||
next_write_error = GATT_CLIENT_BUSY;
|
||||
advance(13);
|
||||
assert(peer->commands == commands);
|
||||
capture_cancel();
|
||||
assert(capture_request(4, &next) && next > old);
|
||||
advance(13);
|
||||
assert(peer->commands == commands + 1 && peer->command[8] == 4);
|
||||
successful_ack(peer, next);
|
||||
}
|
||||
|
||||
static void test_rejection_disconnect_and_late_link_events(void) {
|
||||
struct fixture_peer* peer = sample_ready(true, 0);
|
||||
uint64_t old = request_sample(peer, 3);
|
||||
notify(peer, RESPONSE_HANDLE, sample_ack, sizeof(sample_ack));
|
||||
query_done(peer, 0x0e);
|
||||
assert(disconnected == 1 && capture_result(old) == -1);
|
||||
|
||||
peer = sample_ready(false, 0);
|
||||
old = request_sample(peer, 3);
|
||||
uint8_t rejected[8];
|
||||
memcpy(rejected, sample_ack, sizeof(rejected));
|
||||
rejected[5] = 0x81;
|
||||
notify(peer, RESPONSE_HANDLE, rejected, sizeof(rejected));
|
||||
assert(disconnected == 1 && capture_result(old) == -1);
|
||||
|
||||
peer = sample_ready(false, 0);
|
||||
old = request_sample(peer, 3);
|
||||
btstack_packet_handler_t retired_callback = peer->callback;
|
||||
uni_hid_device_disconnect(&peer->device);
|
||||
assert(capture_result(old) == -1);
|
||||
uint64_t next = 0;
|
||||
assert(!capture_request(3, &next));
|
||||
peer = sample_ready_on_handle(false, 0, 1);
|
||||
next = request_sample(peer, 4);
|
||||
assert(next > old);
|
||||
// The retired link's callback still carries its old, now absent handle.
|
||||
uint8_t late[20] = {GATT_EVENT_NOTIFICATION,18};
|
||||
little_endian_store_16(late, 2, 0);
|
||||
little_endian_store_16(late, 8, RESPONSE_HANDLE);
|
||||
little_endian_store_16(late, 10, sizeof(sample_ack));
|
||||
memcpy(late + 12, sample_ack, sizeof(sample_ack));
|
||||
retired_callback(HCI_EVENT_PACKET, 0, late, sizeof(late));
|
||||
assert(capture_result(next) == 0 && capture_result(old) == -1);
|
||||
successful_ack(peer, next);
|
||||
|
||||
old = request_sample(peer, 3);
|
||||
notify(peer, RESPONSE_HANDLE, sample_ack, sizeof(sample_ack));
|
||||
uni_hid_device_disconnect(&peer->device);
|
||||
assert(capture_result(old) == -1); // Disconnect revokes even unconsumed success.
|
||||
}
|
||||
|
||||
static void test_freshness_timeout_and_clock_wrap(void) {
|
||||
capture_init();
|
||||
uint64_t token = 0;
|
||||
assert(!capture_request(3, &token));
|
||||
uint8_t input[63] = {0};
|
||||
uint8_t other[6];
|
||||
memcpy(other, controller_address, sizeof(other));
|
||||
++other[5];
|
||||
switch_pico_switch2_mouse_report(UNI_SW2_JOYCON_L_PID, controller_address, 8, input, 63, now_ms);
|
||||
switch_pico_switch2_mouse_report(UNI_SW2_JOYCON_R_PID, other, 8, input, 63, now_ms);
|
||||
assert(!capture_request(3, &token));
|
||||
|
||||
struct fixture_peer* peer = sample_ready(false, 0);
|
||||
token = request_sample(peer, 3);
|
||||
advance(500);
|
||||
assert(capture_result(token) == -1 && !capture_request(3, &token));
|
||||
|
||||
peer = sample_ready(false, UINT32_MAX - 200);
|
||||
uint32_t started = now_ms;
|
||||
token = request_sample(peer, 3);
|
||||
while ((uint32_t)(now_ms - started) < 1900) {
|
||||
advance(100);
|
||||
native_input(peer);
|
||||
}
|
||||
advance(1999 - (uint32_t)(now_ms - started));
|
||||
native_input(peer);
|
||||
assert(capture_result(token) == 0);
|
||||
advance(1);
|
||||
assert(capture_result(token) == -1);
|
||||
notify(peer, RESPONSE_HANDLE, sample_ack, sizeof(sample_ack));
|
||||
assert(capture_result(token) == -1);
|
||||
|
||||
peer = sample_ready(false, 0);
|
||||
token = request_sample(peer, 3);
|
||||
for (unsigned i = 0; i < 21 && !disconnected; ++i) {
|
||||
advance(100);
|
||||
if (!disconnected) native_input(peer);
|
||||
}
|
||||
assert(disconnected == 1 && capture_result(token) == -1);
|
||||
}
|
||||
|
||||
static void test_teardown_survives_capture_exhaustion(void) {
|
||||
struct fixture_peer* peer = sample_ready(false, 0);
|
||||
uint64_t token, next;
|
||||
assert(capture_request(3, &token));
|
||||
// The parser has not taken this request, so only source teardown can fail
|
||||
// the mailbox when the serialized capture cannot record another event.
|
||||
capture_exhaust_records();
|
||||
uni_hid_device_disconnect(&peer->device);
|
||||
assert(capture_result(token) == -1 && !capture_request(3, &next));
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
test_ack_order_and_source_matching();
|
||||
test_cancel_does_not_transfer_old_ack();
|
||||
test_rejection_disconnect_and_late_link_events();
|
||||
test_freshness_timeout_and_clock_wrap();
|
||||
test_teardown_survives_capture_exhaustion();
|
||||
reset();
|
||||
puts("Source sample relay ACK ordering, ownership, rejection and lifetime passed");
|
||||
return 0;
|
||||
}
|
||||
'''
|
||||
|
||||
CAPTURE = r'''
|
||||
#include "input/switch2_mouse_capture.cpp"
|
||||
extern "C" uint32_t btstack_run_loop_get_time_ms(void);
|
||||
extern "C" void capture_init(void) {
|
||||
const uint8_t address[6] = {0xc0,0x22,0x33,0x44,0x55,0x66};
|
||||
switch2_mouse_capture_init();
|
||||
switch2_mouse_capture_select_input(address);
|
||||
}
|
||||
extern "C" bool capture_request(uint8_t id, uint64_t* token) {
|
||||
return switch2_mouse_capture_request_sample(id, btstack_run_loop_get_time_ms(), token);
|
||||
}
|
||||
extern "C" int capture_result(uint64_t token) {
|
||||
return switch2_mouse_capture_sample_result(token, btstack_run_loop_get_time_ms());
|
||||
}
|
||||
extern "C" void capture_cancel(void) { switch2_mouse_capture_cancel_sample(); }
|
||||
// Simulate the boot-lifetime counter boundary without billions of reports.
|
||||
extern "C" void capture_exhaust_records(void) { g_total_records = UINT32_MAX; }
|
||||
'''
|
||||
|
||||
|
||||
def test_source_sample_requires_its_real_bluetooth_ack(tmp_path: Path) -> None:
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
cc = shutil.which("cc") or shutil.which("gcc")
|
||||
cxx = shutil.which("c++") or shutil.which("g++")
|
||||
assert cc is not None and cxx is not None, "host C and C++ compilers are required"
|
||||
sdks = [root / "build" / "_deps" / "pico_sdk-src", root / "external" / "pico-sdk"]
|
||||
if sdk := os.environ.get("PICO_SDK_PATH"):
|
||||
sdks.insert(0, Path(sdk))
|
||||
btstack = next((sdk / "lib" / "btstack" / "src" for sdk in sdks
|
||||
if (sdk / "lib" / "btstack" / "src" / "ble" / "gatt_client.h").is_file()), None)
|
||||
if btstack is None:
|
||||
pytest.skip("Pico SDK BTstack headers required; configure firmware or set PICO_SDK_PATH")
|
||||
prepared = prepare_bluepad32(root / "external" / "bluepad32",
|
||||
root / "patches" / "bluepad32-sdl3-imu.patch",
|
||||
tmp_path / "bluepad32-src")
|
||||
common = ["-O1", "-Wall", "-Wextra", "-ffunction-sections", "-fdata-sections",
|
||||
"-DSWITCH_PICO_SWITCH2_USB_BRIDGE=1", "-DSWITCH_PICO_SWITCH2_MOUSE_CAPTURE=1",
|
||||
"-DSWITCH_PICO_SWITCH2_MOUSE_CAPTURE_NATIVE=1", f"-I{root / 'bluepad32_config'}"]
|
||||
cflags = [*common, "-std=gnu11", "-DENABLE_BLE", "-DENABLE_CLASSIC",
|
||||
f"-I{root / 'tests'}", f"-I{root / 'tests' / 'switch2_parser_native_stubs'}",
|
||||
f"-I{prepared / 'src' / 'components' / 'bluepad32' / 'include'}", f"-I{btstack}",
|
||||
f"-I{btstack.parent / '3rd-party' / 'bluedroid' / 'encoder' / 'include'}",
|
||||
f"-I{btstack.parent / '3rd-party' / 'bluedroid' / 'decoder' / 'include'}",
|
||||
f"-I{btstack.parent / '3rd-party' / 'yxml'}"]
|
||||
source = tmp_path / "sample_relay.c"
|
||||
source.write_text(SOURCE)
|
||||
capture = tmp_path / "capture.cpp"
|
||||
capture.write_text(CAPTURE)
|
||||
objects = []
|
||||
for index, path in enumerate((source, root / "bluepad32_config" / "parser" / "uni_hid_parser_switch2.c",
|
||||
root / "bluepad32_config" / "parser" / "uni_switch2_haptics.c",
|
||||
btstack / "btstack_util.c")):
|
||||
obj = tmp_path / f"source{index}.o"
|
||||
subprocess.run([cc, *cflags, "-c", str(path), "-o", str(obj)], check=True, cwd=root)
|
||||
objects.append(str(obj))
|
||||
executable = tmp_path / "sample_relay"
|
||||
subprocess.run([cxx, *common, "-std=c++17", "-pthread",
|
||||
f"-I{root / 'tests' / 'switch2_mouse_bridge_native_stubs'}",
|
||||
f"-I{root / 'src' / 'firmware'}", str(capture), *objects,
|
||||
"-Wl,--gc-sections", "-o", str(executable)], check=True, cwd=root)
|
||||
subprocess.run([str(executable)], check=True, cwd=root)
|
||||
51
tests/test_switch2_usb_probe_protocol_native.py
Normal file
51
tests/test_switch2_usb_probe_protocol_native.py
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def test_switch2_usb_probe_deferred_sample(tmp_path: Path) -> None:
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
compiler = shutil.which("cc") or shutil.which("gcc")
|
||||
assert compiler is not None, "a host C compiler is required"
|
||||
sdk_candidates = [
|
||||
root / "build" / "_deps" / "pico_sdk-src",
|
||||
root / "external" / "pico-sdk",
|
||||
]
|
||||
if sdk_path := os.environ.get("PICO_SDK_PATH"):
|
||||
sdk_candidates.insert(0, Path(sdk_path))
|
||||
mbedtls = next(
|
||||
(sdk / "lib" / "mbedtls" for sdk in sdk_candidates
|
||||
if (sdk / "lib" / "mbedtls" / "library" / "aes.c").is_file()),
|
||||
None,
|
||||
)
|
||||
if mbedtls is None:
|
||||
pytest.skip("Pico SDK mbedTLS required; configure firmware or set PICO_SDK_PATH")
|
||||
probe = root / "tools" / "switch2_usb_probe"
|
||||
executable = tmp_path / "switch2_usb_probe_protocol"
|
||||
subprocess.run(
|
||||
[
|
||||
compiler,
|
||||
"-std=c11",
|
||||
"-Wall",
|
||||
"-Wextra",
|
||||
"-Werror",
|
||||
"-pedantic",
|
||||
f'-DMBEDTLS_CONFIG_FILE="{probe / "mbedtls_config.h"}"',
|
||||
f"-I{probe}",
|
||||
f"-I{mbedtls / 'include'}",
|
||||
str(probe / "protocol.c"),
|
||||
str(mbedtls / "library" / "aes.c"),
|
||||
str(mbedtls / "library" / "platform_util.c"),
|
||||
str(root / "tests" / "switch2_usb_probe_protocol_test.c"),
|
||||
"-o",
|
||||
str(executable),
|
||||
],
|
||||
check=True,
|
||||
cwd=root,
|
||||
)
|
||||
subprocess.run([str(executable)], check=True, cwd=root)
|
||||
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