diff --git a/CMakeLists.txt b/CMakeLists.txt index 096269e..5023f22 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -32,8 +32,23 @@ 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_MEMORY_CAPTURE + "Research: log read-only Joy-Con factory/user calibration banks during setup" OFF) option(SWITCH_PICO_SWITCH2_USB_BRIDGE - "Experiment: forward a native right Joy-Con 2 through the verified USB probe" OFF) + "Experiment: forward a selected native Joy-Con 2 through the verified USB probe" OFF) +option(SWITCH2_PROBE_COMPOSITE + "Experiment: independent right and left Joy-Con 2 functions on one USB port" OFF) +option(SWITCH2_PROBE_HUB + "Native Joy-Con 2 R/L devices behind a stock-socket SIO USB hub" OFF) +if(SWITCH2_PROBE_HUB AND (NOT SWITCH_PICO_SWITCH2_USB_BRIDGE OR SWITCH2_PROBE_COMPOSITE)) + message(FATAL_ERROR "Native hub requires the Switch2 bridge and excludes composite mode") +endif() +if(SWITCH2_PROBE_COMPOSITE AND NOT SWITCH_PICO_SWITCH2_USB_BRIDGE) + message(FATAL_ERROR "The composite Joy-Con 2 experiment requires SWITCH_PICO_SWITCH2_USB_BRIDGE") +endif() +if(NOT SWITCH_PICO_SWITCH2_USB_BRIDGE) + add_compile_definitions(SWITCH2_PROBE_COMPOSITE=0 PROBE_CONTROLLER_COUNT=1) +endif() if(SWITCH_PICO_INPUT_BACKEND STREQUAL "BLUEPAD32") set(SWITCH_PICO_NATIVE_DEFAULT ON) set(SWITCH_PICO_CLOCK_DEFAULT 300) @@ -49,11 +64,17 @@ option(SWITCH_PICO_HAPTICS_EXPERIMENT_RAM "Execute the native haptics hot path from SRAM" ON) option(SWITCH_PICO_HD_RUMBLE "Auto-arm the first eligible DualSense native haptics stream" ${SWITCH_PICO_NATIVE_DEFAULT}) +if(SWITCH2_PROBE_HUB) + set(SWITCH_PICO_CLOCK_DEFAULT 240) +endif() set(SWITCH_PICO_SYS_CLOCK_MHZ "${SWITCH_PICO_CLOCK_DEFAULT}" CACHE STRING "Pico 2 W CPU clock: 300 MHz default, 150 stock or 400 opt-in") -set_property(CACHE SWITCH_PICO_SYS_CLOCK_MHZ PROPERTY STRINGS 150 300 400) -if(NOT SWITCH_PICO_SYS_CLOCK_MHZ MATCHES "^(150|300|400)$") - message(FATAL_ERROR "SWITCH_PICO_SYS_CLOCK_MHZ must be 150, 300, or 400") +set_property(CACHE SWITCH_PICO_SYS_CLOCK_MHZ PROPERTY STRINGS 150 240 300 400) +if(NOT SWITCH_PICO_SYS_CLOCK_MHZ MATCHES "^(150|240|300|400)$") + message(FATAL_ERROR "SWITCH_PICO_SYS_CLOCK_MHZ must be 150, 240, 300, or 400") +endif() +if(SWITCH2_PROBE_HUB AND NOT SWITCH_PICO_SYS_CLOCK_MHZ STREQUAL "240") + message(FATAL_ERROR "Native SIO hub requires SWITCH_PICO_SYS_CLOCK_MHZ=240") endif() set(SWITCH_PICO_OVERCLOCK_MV "1300" CACHE STRING "Experimental core voltage: 1300 mV, or explicit 1400 mV at 400 MHz") @@ -146,6 +167,14 @@ if(SWITCH_PICO_SWITCH2_MOUSE_CAPTURE) SWITCH_PICO_SWITCH2_MOUSE_CAPTURE=1 SWITCH_PICO_SWITCH2_MOUSE_CAPTURE_NATIVE=$) endif() +if(SWITCH_PICO_SWITCH2_MEMORY_CAPTURE) + if(NOT SWITCH_PICO_SWITCH2_MOUSE_CAPTURE OR NOT SWITCH_PICO_LOG + OR SWITCH_PICO_SWITCH2_USB_BRIDGE) + message(FATAL_ERROR + "Joy-Con memory capture requires a logging capture build, not the USB bridge") + endif() + add_compile_definitions(SWITCH_PICO_SWITCH2_MEMORY_CAPTURE=1) +endif() set(SWITCH2_BRIDGE_WII_INPUT OFF) if(SWITCH_PICO_SWITCH2_USB_BRIDGE) if(NOT SWITCH_PICO_INPUT_BACKEND STREQUAL "BLUEPAD32" @@ -169,18 +198,6 @@ if(SWITCH_PICO_SWITCH2_USB_BRIDGE) elseif(NOT SWITCH2_BRIDGE_INPUT STREQUAL "JOYCON2") message(FATAL_ERROR "SWITCH2_BRIDGE_INPUT must be JOYCON2 or WII") endif() - set(SWITCH2_BRIDGE_SOURCE_ADDRESS "" CACHE STRING - "Physical Bluetooth controller 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") @@ -326,6 +343,19 @@ if(SWITCH_PICO_INPUT_BACKEND STREQUAL "BLUEPAD32") ${BLUEPAD32_ROOT}/src/components/bluepad32 ${CMAKE_CURRENT_BINARY_DIR}/libbluepad32 ) + if(SWITCH2_PROBE_HUB) + # Upstream Bluepad32 also links arch_none (background IRQ execution). + # Replace that transitive selection, not just the executable's link. + foreach(property LINK_LIBRARIES INTERFACE_LINK_LIBRARIES) + get_target_property(bluepad_links bluepad32 ${property}) + if(bluepad_links) + list(REMOVE_ITEM bluepad_links pico_cyw43_arch_none) + list(APPEND bluepad_links pico_cyw43_arch_poll) + set_property(TARGET bluepad32 PROPERTY ${property} "${bluepad_links}") + endif() + endforeach() + target_compile_definitions(bluepad32 PUBLIC CYW43_LWIP=0) + endif() target_sources(bluepad32 PRIVATE ${CMAKE_CURRENT_LIST_DIR}/bluepad32_config/parser/uni_hid_parser_switch2.c ${CMAKE_CURRENT_LIST_DIR}/bluepad32_config/parser/uni_switch2_pairing.c @@ -374,6 +404,10 @@ if(SWITCH_PICO_SWITCH2_USB_BRIDGE) endif() target_compile_definitions(switch-pico PRIVATE SWITCH2_BRIDGE_SOURCE_ADDRESS_BYTES=${SWITCH2_BRIDGE_SOURCE_ADDRESS_BYTES}) + if(SWITCH2_PROBE_COMPOSITE OR SWITCH2_PROBE_HUB) + target_compile_definitions(switch-pico PRIVATE + SWITCH2_BRIDGE_SECOND_SOURCE_ADDRESS_BYTES=${SWITCH2_BRIDGE_SECOND_SOURCE_ADDRESS_BYTES}) + endif() else() target_sources(switch-pico PRIVATE ${SWITCH_PICO_SOURCE_DIR}/main.cpp) endif() @@ -438,8 +472,9 @@ if(SWITCH_PICO_INPUT_BACKEND STREQUAL "BLUEPAD32") SWITCH_PICO_NATIVE_SWITCH_RUMBLE=1 SWITCH_PICO_HID_INSTANCE_COUNT=4 $<$,$>>:SWITCH_PICO_USB_OUTPUT_MODES=1> - PICO_FLASH_ASSUME_CORE1_SAFE=0 - PICO_STACK_SIZE=4096 + PICO_FLASH_ASSUME_CORE1_SAFE=$ + PICO_STACK_SIZE=$,16384,4096> + PICO_CORE1_STACK_SIZE=4096 PICO_BTSTACK_CYW43_MAX_HCI_PROCESS_LOOP_COUNT=$,1,16> SWITCH2_WAKE_CONFIGURED=${SWITCH2_WAKE_CONFIGURED_VALUE} ) @@ -500,15 +535,21 @@ pico_enable_stdio_usb(switch-pico 0) # Add the standard library to the build target_link_libraries(switch-pico PRIVATE pico_stdlib - tinyusb_device - tinyusb_board hardware_uart pico_rand ) +if(NOT SWITCH2_PROBE_HUB) + target_link_libraries(switch-pico PRIVATE tinyusb_device tinyusb_board) +endif() if(SWITCH_PICO_INPUT_BACKEND STREQUAL "BLUEPAD32") + if(SWITCH2_PROBE_HUB) + target_link_libraries(switch-pico PRIVATE pico_cyw43_arch_poll) + target_compile_definitions(switch-pico PRIVATE CYW43_LWIP=0) + else() + target_link_libraries(switch-pico PRIVATE pico_cyw43_arch_none) + endif() target_link_libraries(switch-pico PRIVATE bluepad32 - pico_cyw43_arch_none pico_btstack_ble pico_btstack_classic pico_btstack_cyw43 diff --git a/README.md b/README.md index 52bc07b..2e48a67 100644 --- a/README.md +++ b/README.md @@ -483,10 +483,12 @@ Protocol references: [WiiBrew Wiimote](https://wiibrew.org/wiki/Wiimote), [Motio `SWITCH_PICO_SWITCH2_USB_BRIDGE=ON` selects the separate USB protocol probe in `tools/switch2_usb_probe`, not the ordinary four-Pro-controller AIO output. -`SWITCH2_BRIDGE_INPUT=JOYCON2` preserves complete packets from one selected right -Joy-Con 2; `SWITCH2_BRIDGE_INPUT=WII` generates native right-Joy-Con reports from -Wii input. Select the physical Bluetooth address with -`SWITCH2_BRIDGE_SOURCE_ADDRESS`. +`SWITCH2_BRIDGE_INPUT=JOYCON2` preserves complete packets from one selected +Joy-Con 2. Choose `SWITCH2_PROBE_SIDE=LEFT` or `RIGHT` (default), with matching +identity, firmware and calibration captures, and select the physical Bluetooth +address with `SWITCH2_BRIDGE_SOURCE_ADDRESS`. Left uses USB PID `2067`/report +`07`; right uses PID `2066`/report `08`. `SWITCH2_BRIDGE_INPUT=WII` generates +native right-Joy-Con reports and rejects `LEFT`. The Wii source requires Pico 2 W, the Bluepad32 backend, Bluetooth `MIXED` mode, and the bridge's native capture prerequisites @@ -502,6 +504,107 @@ factory-memory and user-calibration inputs, a distinct virtual controller addres pairing records and firmware backups are not bundled with the source. Keep a known-good UF2 and use a separate build directory for experiments. +For read-only **donor capture**, use a separate ordinary Bluepad32 build with +`SWITCH_PICO_SWITCH2_USB_BRIDGE=OFF`, `SWITCH_PICO_LOG=ON`, +`SWITCH_PICO_SWITCH2_MOUSE_CAPTURE=ON`, and +`SWITCH_PICO_SWITCH2_MEMORY_CAPTURE=ON`. After normal pairing/calibration, +each connected Joy-Con reads 192 acknowledged 64-byte pages covering factory +`0x13000..0x14fff` and user calibration `0x1fc000..0x1fcfff`, logged as +`SW2_MEMORY_
`. Setup identity/version logs identify the donor. +The capture adds no memory writes or erases; normal pairing rules still apply. +Keep these private captures out of commits. Add +`SWITCH_PICO_SWITCH2_MOUSE_CAPTURE_NATIVE=ON` to capture raw left `07` or right +`08` input through USB management after setup. This is capture firmware, not +left-side or dual-Joy-Con USB emulation; composite L/R acceptance is unverified. + +**Left-only passthrough:** firmware `0.34-left-trace` has enumerated as a left +Joy-Con on Linux. A live USB check verified all 192 factory/user memory pages +and received 402 native `07` packets, including 400 motion-bearing packets whose +IMU blocks decoded and reconstructed exactly. The donor's separate stationary +capture also reconstructed all 539 blocks and measured approximately 0.995 g; +left directional axes and console gameplay remain unqualified. Fifteen targeted +tests pass, and left, right and Wii variants build. +The Switch subsequently completed left-side pairing and activation (runtime +`03/0C=1`, player LED mask 1), received motion-bearing `07` reports, and requested +its connection vibration cue, acknowledged by the physical donor. All 54 sampled +console motion blocks reconstructed exactly. The user confirmed menu navigation +with the left stick. Perceived vibration and directional IMU behavior remain +unconfirmed. + +Left and right native USB pairing records use independent two-sector banks. +On the 4 MiB Pico 2 W, left occupies flash offsets `0x3b7000..0x3b8fff`; the +existing right bank stays at `0x3b9000..0x3bafff`. Profiles, configuration and +Bluetooth storage do not move. Host fault-injection checks verified old-right +record recovery, opposite-bank preservation, torn-write recovery and refusal +of foreign sector ownership. + +**Simultaneous L/R experiment:** `SWITCH2_PROBE_COMPOSITE=ON` builds +`0.35-pair[-trace]` with two live native donor paths. It requires +`SWITCH2_PROBE_SIDE=RIGHT`, `SWITCH2_BRIDGE_INPUT=JOYCON2`, distinct physical +`SWITCH2_BRIDGE_SOURCE_ADDRESS` / `SWITCH2_BRIDGE_SECOND_SOURCE_ADDRESS`, and +distinct advertised controller addresses. The existing capture inputs describe +R; `SWITCH2_PROBE_SECOND_IDENTITY_FILE`, `SWITCH2_PROBE_SECOND_VERSION_FILE`, +`SWITCH2_PROBE_SECOND_FACTORY_FILE`, `SWITCH2_PROBE_SECOND_USER_CALIBRATION_FILE` +and `SWITCH2_PROBE_SECOND_CONTROLLER_ADDRESS` describe L. + +The 151-byte USB configuration exposes R HID/vendor interfaces 0/1 and L +interfaces 2/3, using endpoint pairs 1/2 and 3/4 respectively. Both functions +have independent protocol state, native report consumption, feature gates, +command/reply queues, cue tokens and pairing records. Device-level VID/PID +remains `057e:2066`; explicit control indexes 2/3 address L, while index 0 +continues to identify R. No identity is inferred from request timing. +Composite discovery uses the native device class `EF/02/01` and interface +associations. Single-side builds retain their published 80-byte configuration. +The flat per-interface trial described below was reverted in source. + +Seventeen targeted tests pass across single and composite modes. A host smoke +through the actual USB callbacks exercised simultaneous report delivery, +cross-interface backpressure, deferred cue replies, fragmented commands, +indexed identities and disconnect/reset boundaries. Indexed storage +fault-injection and right, left, Wii, donor-capture, standalone-probe and +composite builds also pass. These checks do not establish Switch acceptance +of both functions; that requires the console enumeration trial. + +Full-controller input splitting and continuous USB HD-rumble forwarding are +not implemented yet. This experiment relays two genuine Joy-Cons, including +their opaque motion/mouse packets and acknowledged built-in vibration cues. + +The composite image has now been flashed with both pairing banks and all other +persistent storage verified unchanged. Linux enumerates all four interfaces; +indexed R/L identity reads and independent initialization succeed. A live check +received 250 reports from each function: L carried 248 motion blocks, while R +correctly remained neutral because its physical donor was not connected. +Both saved virtual pairing records restored. After reconnecting R, a simultaneous +live USB check received 376 reports from each donor, all 752 carrying motion +blocks that decoded and reconstructed exactly. On Switch, however, 0.35 only +initialized and displayed R: L was Bluetooth-active but USB-uninitialized, with +no commands or reports on its function. Connection order is not an adequate +explanation for the missing USB initialization. + +Firmware 0.36 tested device class `00/00/00` without association descriptors. +Its interfaces, endpoints, reports, identities and protocol behavior were +unchanged. Binary comparison, 17 targeted tests, USB callback smoke and Linux +descriptor checks passed, but the user reported neither controller appearing on +Switch. R completed bulk initialization yet its input count stayed at one; +L remained USB-uninitialized, despite both Bluetooth sources being active. +The source was restored to 0.35 and rebuilt byte-identically to its saved image. +A later capture included USB restart and grip-screen activity: R resumed reports +and player assignment, but L remained uninitialized and its L press was not +detected by the console. This does not establish that opening the grip screen +alone caused R to recover. + +The user-requested `SWITCH2_PROBE_JOIN_CHORD_GATE=ON` experiment builds +`0.37-pair-chord[-trace]` on the 0.35 native layout. It requires composite output +and suppresses each real L/R shoulder bit until both active physical sources +hold their shoulders. Release or stale/disconnected input closes the gate. +Both sources are polled before USB submissions. Native and common GET_REPORT +paths are gated; other buttons and opaque motion bytes are retained. The gate +does not synthesize presses, force initialization or make two USB device PIDs. +`JOIN_CHORD` traces record raw shoulder states and initialization status. +Host smoke checks covered these boundaries; the ungated firmware remained +byte-identical to 0.35. Firmware 0.37 was flashed with persistent storage +unchanged and both pairing records restored; its console chord test is pending. + - **Motion:** factory-calibrated Wii acceleration and MotionPlus gyro have independent freshness counters. Keep the Remote still at startup for at least 1.5 seconds and 64 fresh gyro samples to estimate residual bias. The encoder @@ -615,7 +718,7 @@ protocol adaptations belong in the separate adapter. Both native bridge sources support the existing software **BOOTSEL reboot** without erasing pairings, profiles or configuration. The standalone USB diagnostic -probe does not. Connect the bridge to a PC and disconnect any genuine USB right +probe does not. Connect the bridge to a PC and disconnect any genuine USB Joy-Con 2 before running this from the repository: ```sh @@ -623,9 +726,10 @@ uv run python - <<'PY' import usb.core from switch_pico_bridge.config_manager import request_bootsel_reboot -devices = list(usb.core.find(find_all=True, idVendor=0x057e, idProduct=0x2066)) +product_id = 0x2066 # Use 0x2067 for a LEFT bridge build. +devices = list(usb.core.find(find_all=True, idVendor=0x057e, idProduct=product_id)) if len(devices) != 1: - raise SystemExit("Connect exactly one native bridge (057e:2066).") + raise SystemExit(f"Connect exactly one native bridge (057e:{product_id:04x}).") request_bootsel_reboot(devices[0]) print("Rebooting into USB BOOTSEL mode.") PY @@ -646,6 +750,76 @@ separate request `0x04`, value `0x0276`, index `0`, length `0` remains an ordina setup acknowledgement. No configuration writes or extra management capabilities are enabled in native mode. +### Experimental stock-socket native Joy-Con 2 hub + +`SWITCH2_PROBE_HUB=ON` exposes a `057e:2068` hub with separate right +`057e:2066` and left `057e:2067` devices through the unchanged Pico 2 W USB +socket. It uses the native USB PHY/SIE and a Core 1 SIO observer, not USB +wiring on GPIO pins. Each child retains its own native HID/vendor interfaces, +EP1/EP2 state, identity, protocol state and pairing bank. + +This mode requires `SWITCH_PICO_SWITCH2_USB_BRIDGE=ON`, +`SWITCH2_BRIDGE_INPUT=JOYCON2`, `SWITCH2_PROBE_SIDE=RIGHT`, +`SWITCH2_PROBE_COMPOSITE=OFF`, and `SWITCH_PICO_SYS_CLOCK_MHZ=240`. +Configure both private donor captures and source addresses as for the paired +native probe. Bluetooth runs cooperatively on Core 0; Core 1 is reserved for +USB observation. Receive PID state is selected before accepting OUT traffic. +Transmit payloads are prepared outside the bank lock and published by Core 0; +unavailable IN buffers NAK rather than expose another device's packet. + +**Qualification history:** the earlier RAM-only +probe established three-address EP0 routing, not Joy-Con output. The +`0.65-native-hub-ready` bridge subsequently passed interleaved native descriptor, +identity and short control reads, both initialization sequences and bulk +isolation. Two consecutive 60-second captures received 7,504 and 7,496 native +HID packets, with correct R/L report IDs and lengths and no USB protocol error +or hub reset. All packets lacked live donor IMU, so both captures correctly +failed the live-input requirement. + +An awake-controller trial exposed a separate hub-mode bug: the input capture +mailbox still allocated one channel unless composite mode was enabled, silently +rejecting L registration. Firmware `0.66-native-hub-input` enables both capture +channels for hub mode and checks that their count matches the controller models. +The dual-source BLE/capture regression failed on L packet delivery before this +fix; its new hub case and all 16 focused regression cases now pass. + +Live PC qualification then passed with 575 R and 703 L decoded IMU reports and +changing sensor counters. A follow-up run received 587 R and 588 L live IMU +reports while completing 37 interleaved read-isolation rounds and matching the +Bluetooth-backed built-in motor-sample-0 acknowledgement independently on each +side. Neither run reported malformed or wrong-side packets or qualification +errors. These captures did not exercise deliberate button presses or establish +physical motor feel. The user subsequently confirmed that 0.66 works on Switch, +with some noticeable input lag. This is console smoke-test evidence, not a +latency measurement or exhaustive compatibility test. This mode does not add +arbitrary full-controller splitting or continuous USB HD-rumble forwarding. + +With the existing private build configured, qualify on a PC using: + +```sh +uv run python tools/native_joycon_hub_check.py \ + --build-dir build-switch2-native-hub \ + --output build-switch2-native-hub/qualification.json +``` + +Wake both physical Joy-Cons and move them during the manual-wake window. +The checker rejects neutral/zero-length IMU reports and requires fresh, +decodable motion with changing counters from both devices. It does not pair, +reset, change profiles or write flash. `--rumble-sample 0` is an explicit +optional motor-cue test, not a continuous HD-rumble test. Captures and flash +backups contain private device data and must remain untracked. + +`HUB_RADIO reports` counts normal parsed gamepad callbacks, which native packed +input bypasses. Zero is not evidence that a native donor is asleep or inactive; +use per-source activation and the host's fresh native IMU results instead. + +The hardware trials verified the complete persistent region +`0x103b7000..0x10400000` unchanged before and after application-only flashing. +Software BOOTSEL recovery uses the existing helper above on the verified +`057e:2068` root, not either child. UART remains available during qualification. +Watchdog recovery and failure to configure the initial root hub enter BOOTSEL +without erasing storage; neither mechanism proves successful controller output. + ### Switch 2 controller input The AIO firmware implements the proprietary BLE protocol for Nintendo `057E:2069` (Pro), `057E:2067` (left Joy-Con 2), and `057E:2066` (right Joy-Con 2). This is controller **input** support, distinct from the existing Switch 2 console-wake feature and from emulating a native Switch 2 USB controller. diff --git a/bluepad32_config/parser/uni_hid_parser_switch2.c b/bluepad32_config/parser/uni_hid_parser_switch2.c index 4ba668a..042c8af 100644 --- a/bluepad32_config/parser/uni_hid_parser_switch2.c +++ b/bluepad32_config/parser/uni_hid_parser_switch2.c @@ -70,6 +70,9 @@ typedef enum { #if SWITCH_PICO_SWITCH2_MOUSE_CAPTURE SW2_SECONDARY_DESCRIPTOR, SW2_SUBSCRIBE_SECONDARY, #endif +#if SWITCH_PICO_SWITCH2_MEMORY_CAPTURE + SW2_CAPTURE_MEMORY, +#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 { @@ -473,6 +476,11 @@ static void sw2_continue(sw2_instance_t* ins) { case SW2_GYRO_CALIBRATION: sw2_read_memory(ins, 0x13044, 12); break; +#if SWITCH_PICO_SWITCH2_MEMORY_CAPTURE + case SW2_CAPTURE_MEMORY: + sw2_read_memory(ins, ins->memory_address, 64); + break; +#endif case SW2_FEATURES: { #if SWITCH_PICO_SWITCH2_USB_BRIDGE // Match the console's complete native feature set, including the @@ -537,8 +545,28 @@ static void sw2_complete_command(sw2_instance_t* ins) { ins->state = SW2_GYRO_CALIBRATION; break; case SW2_GYRO_CALIBRATION: +#if SWITCH_PICO_SWITCH2_MEMORY_CAPTURE + if (sw2_mouse_capture_enabled(ins)) { + ins->state = SW2_CAPTURE_MEMORY; + ins->memory_address = 0x13000; + break; + } +#endif sw2_subscribe(ins, true); return; +#if SWITCH_PICO_SWITCH2_MEMORY_CAPTURE + case SW2_CAPTURE_MEMORY: + // Only factory/user calibration banks; never pairing keys or + // write/erase commands. Each page requires its matching ACK. + ins->memory_address += 64; + if (ins->memory_address == 0x15000) + ins->memory_address = 0x1fc000; + if (ins->memory_address == 0x1fd000) { + sw2_subscribe(ins, true); + return; + } + break; +#endif case SW2_FEATURES: if (++ins->step == 2) { ins->state = SW2_READY; @@ -580,6 +608,13 @@ static void sw2_response(sw2_instance_t* ins, const uint8_t* data, uint16_t leng little_endian_read_32(data, 12) != ins->memory_address || length < 16 + ins->memory_length) return; // Includes a stale memory response with the same cmd/subcmd. const uint8_t* value = data + 16; +#if SWITCH_PICO_SWITCH2_MEMORY_CAPTURE + if (ins->state == SW2_CAPTURE_MEMORY) { + char label[24]; + snprintf(label, sizeof(label), "MEMORY_%08lx", (unsigned long)ins->memory_address); + sw2_log_capture(label, ins, value, ins->memory_length); + } +#endif if (ins->state == SW2_INFO) { if (little_endian_read_16(value, 18) != UNI_SW2_NINTENDO_VID || little_endian_read_16(value, 20) != ins->device->product_id) { diff --git a/src/firmware/input/bluepad32_input_backend.cpp b/src/firmware/input/bluepad32_input_backend.cpp index 407a86a..9e5eef4 100644 --- a/src/firmware/input/bluepad32_input_backend.cpp +++ b/src/firmware/input/bluepad32_input_backend.cpp @@ -23,10 +23,15 @@ #include #include +#if SWITCH2_PROBE_HUB +#include +#endif #include #include #include +#if !SWITCH2_PROBE_HUB #include +#endif #include #include extern "C" { @@ -39,6 +44,13 @@ extern "C" { #include "adapter/adapter_usb_mode.h" #endif +#if SWITCH2_PROBE_HUB && !PICO_CYW43_ARCH_POLL +#error "Native hub Bluetooth requires pico_cyw43_arch_poll on Core 0" +#endif +#if SWITCH2_PROBE_HUB && !PICO_FLASH_ASSUME_CORE1_SAFE +#error "Native hub flash writes require its IRQ-disabled SRAM-only Core 1 transport" +#endif + namespace { constexpr int32_t kAxisMinimum = -512; @@ -77,7 +89,7 @@ constexpr uint32_t kWiiAimChordFreshUs = 150000; constexpr uint16_t kWiiAimChordButtons = 0x0002 | 0x0001; #endif // One initial indication can be followed by one committed switch before the -// Core 1 timer drains the queue. Profile commits are rate-limited well beyond +// BTstack timer drains the queue. Profile commits are rate-limited well beyond // the longest feedback sequence. constexpr uint8_t kProfileFeedbackQueueCapacity = 2; constexpr SwitchRgbColor kProfileLightbarPalette[CONTROLLER_PROFILE_COUNT] = { @@ -301,9 +313,13 @@ critical_section_t g_state_lock; uni_hid_device_t* g_retired_devices[kSlotCount]{}; BackendSlot g_slots[kSlotCount]; ControllerMacroCapture g_macro_capture; +#if !SWITCH2_PROBE_HUB // Catalog migration/compaction needs more than the 4 KiB scratch bank. // Supply a dedicated static stack in main SRAM rather than overflowing it. alignas(8) uint32_t g_core1_stack[4096]; +#else +bool g_poll_ready = false; +#endif BleIdentityMapping g_ble_identity_mappings[kSlotCount]{}; // These acknowledgement generations and request producers are only used by @@ -362,7 +378,7 @@ void retire_wii_slot(uint8_t slot_index) { } #endif -// These fields are only read or written by Core 1 / BTstack. +// These fields are only read or written by the BTstack execution context. btstack_timer_source_t g_rumble_timer{}; btstack_timer_source_t g_configuration_timer{}; ConnectionStatus g_connection_status = ConnectionStatus::Initializing; @@ -398,7 +414,7 @@ struct JoyConConnectionOverride { }; JoyConConnectionOverride g_joycon_overrides[kSlotCount]{}; -// Core 1 physical-link state survives logical slot moves. Raw reports stay in +// BTstack physical-link state survives logical slot moves. Raw reports stay in // BackendSlot; only the derived logical view consumes the reserved buttons. struct JoyConGesture { uni_hid_device_t* device = nullptr; @@ -728,7 +744,7 @@ void stop_background_scan() { g_background_scan_active = false; } } -// Core 1 only. Reconcile every ready physical Switch 2 link to the fast interval, +// BTstack only. Reconcile every ready physical Switch 2 link to the fast interval, // independently of player grouping, controller count, or Classic connections. void apply_radio_connection_policy() { if (!SWITCH_PICO_ENABLE_BLE) { @@ -776,7 +792,7 @@ void apply_radio_connection_policy() { } -// Caller holds the cross-core state lock. Only Core 1 resets parser state. +// Caller holds the state lock. Only the BTstack context resets parser state. void clear_switch2_ingress(BackendSlot& slot) { Switch2Ingress& ingress = slot.switch2_ingress; __atomic_add_fetch(&g_switch2_ingress_drops, ingress.count, __ATOMIC_RELAXED); @@ -3014,7 +3030,7 @@ void stop_joycon_output(uni_hid_device_t* device) { } } -// Core 1 only; shared by ready admission, saved defaults and explicit gestures. +// BTstack only; shared by ready admission, saved defaults and explicit gestures. // Pair enrollment is atomic and idempotent, and always precedes topology // changes with no cross-core input lock held during storage I/O. bool merge_joycon_slots(int owner_index, int joining_index, @@ -3728,17 +3744,17 @@ uni_platform* get_platform() { return &platform; } +#if !SWITCH2_PROBE_HUB [[noreturn]] void halt_wireless_backend() { publish_all_neutral(); while (true) { tight_loop_contents(); } } +#endif -[[noreturn]] void core1_main() { - if (!flash_safe_execute_core_init()) { - halt_wireless_backend(); - } +// Called on the Bluetooth/storage owner after flash-safe registration. +bool initialize_wireless_backend() { __atomic_store_n(&g_initialization_stage, 2, __ATOMIC_RELEASE); configuration_service_initialize_on_storage_core(); profile_service_initialize_on_storage_core(); @@ -3750,7 +3766,7 @@ uni_platform* get_platform() { } __atomic_store_n(&g_initialization_stage, 3, __ATOMIC_RELEASE); if (cyw43_arch_init() != 0) { - halt_wireless_backend(); + return false; } __atomic_store_n(&g_initialization_stage, 4, __ATOMIC_RELEASE); cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, true); @@ -3758,15 +3774,24 @@ uni_platform* get_platform() { uni_platform_set_custom(get_platform()); if (uni_init(0, nullptr) != 0) { - halt_wireless_backend(); + return false; } __atomic_store_n(&g_initialization_stage, 5, __ATOMIC_RELEASE); + return true; +} + +#if !SWITCH2_PROBE_HUB +[[noreturn]] void core1_main() { + if (!flash_safe_execute_core_init() || !initialize_wireless_backend()) { + halt_wireless_backend(); + } btstack_run_loop_execute(); while (true) { tight_loop_contents(); } } +#endif } // namespace @@ -3920,23 +3945,47 @@ void bluepad32_input_backend_init() { } void bluepad32_input_backend_start() { +#if SWITCH2_PROBE_HUB + if (get_core_num() != 0) { + panic("native hub Bluetooth must start on Core 0"); + } +#endif if (!g_initialized) { bluepad32_input_backend_init(); } if (g_started) { return; } - // Core 0 services USB from flash while Core 1 owns BTstack. Register both - // cores before either side can initiate a flash-backed BTstack TLV write. + // Non-hub builds register both cores before BTstack can write flash. + // Hub builds keep Core 1 in IRQ-disabled SRAM code, so the SDK's + // PICO_FLASH_ASSUME_CORE1_SAFE path only disables Core 0 interrupts. if (!flash_safe_execute_core_init()) { g_connection_policy_state = ConnectionPolicyState::FailedClosed; return; } - g_started = true; +#if SWITCH2_PROBE_HUB + g_poll_ready = initialize_wireless_backend(); + if (!g_poll_ready) { + g_connection_policy_state = ConnectionPolicyState::FailedClosed; + publish_all_neutral(); + } +#else multicore_launch_core1_with_stack( core1_main, g_core1_stack, sizeof(g_core1_stack)); +#endif +} + +void bluepad32_input_backend_poll() { +#if SWITCH2_PROBE_HUB + if (!g_poll_ready) return; + async_context_t* context = cyw43_arch_async_context(); + // The SDK checks both the owning core and non-IRQ context. Polling invokes + // the existing BTstack workers/timers; no second scheduler or wait loop. + async_context_lock_check(context); + async_context_poll(context); +#endif } void bluepad32_input_backend_open_pairing_window() { diff --git a/src/firmware/input/bluepad32_input_backend.h b/src/firmware/input/bluepad32_input_backend.h index aeec139..aa85b9d 100644 --- a/src/firmware/input/bluepad32_input_backend.h +++ b/src/firmware/input/bluepad32_input_backend.h @@ -141,13 +141,19 @@ struct Bluepad32BackendDiagnostics { +// Core 0 startup. Normally start launches a dedicated Core 1 BTstack/storage +// owner; SWITCH2_PROBE_HUB keeps that owner on Core 0 and leaves Core 1 to USB. void bluepad32_input_backend_init(); void bluepad32_input_backend_start(); +// Hub only: call on Core 0 outside IRQs, without any application state lock +// held, once per main-loop iteration. Services the existing SDK CYW43 async +// context without waiting. Safe before start; a no-op in dedicated-Core 1 modes. +void bluepad32_input_backend_poll(); void bluepad32_input_backend_open_pairing_window(); -// Core 1 parser admission gate for fresh proprietary Switch 2 pairing; this +// BTstack parser admission gate for fresh proprietary Switch 2 pairing; this // never opens a pairing window or changes the bounded connection policy. extern "C" bool switch_pico_switch2_pairing_allowed(void); -// Repeated calls coalesce until Core 1 completes the operation and return the +// Repeated calls coalesce until BTstack completes the operation and return the // same nonzero token. uint32_t bluepad32_input_backend_clear_pairings(); void bluepad32_input_backend_snapshot(uint8_t slot, @@ -197,8 +203,8 @@ struct Bluepad32CaptureSnapshot { CaptureEvent events[BLUEPAD32_CAPTURE_PAGE_EVENTS]{}; }; -// Core 0 management operations; recording itself observes Core 1 input before -// profile transforms. All recorder access uses the existing slot-state lock. +// Core 0 management operations; recording observes BTstack input before profile +// transforms. All recorder access uses the existing slot-state lock. bool bluepad32_input_backend_capture_start( uint8_t slot, uint32_t connection_generation, const CaptureOptions& options); bool bluepad32_input_backend_capture_stop(uint32_t run_id); diff --git a/src/firmware/input/switch2_mouse_capture.cpp b/src/firmware/input/switch2_mouse_capture.cpp index 61a2333..2408389 100644 --- a/src/firmware/input/switch2_mouse_capture.cpp +++ b/src/firmware/input/switch2_mouse_capture.cpp @@ -13,9 +13,6 @@ 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; @@ -25,40 +22,62 @@ struct NativeReport { 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 { +struct Sample { uint64_t token; uint32_t started_ms; uint32_t mouse_epoch; uint8_t sample_id; bool taken; bool acked; -} g_sample; +}; +#endif -void clear_native_reports() { - g_native_head = 0; - g_native_count = 0; +struct Source { + bool input_selected; + uint8_t input_address[6]; + uint16_t input_product_id; + Switch2MouseCaptureInput latest_input; +#if SWITCH_PICO_SWITCH2_USB_BRIDGE + NativeReport native_reports[kNativeReportCapacity]; + uint8_t native_head; + uint8_t native_count; + bool native_stream; + bool source_active; + Sample sample; +#endif +}; +Source g_sources[SWITCH2_MOUSE_CAPTURE_SOURCE_COUNT]; + +Source* selected_source(uint16_t product_id, const uint8_t address[6]) { + for (Source& source : g_sources) { + if (source.input_selected && product_id == source.input_product_id && + memcmp(address, source.input_address, sizeof(source.input_address)) == 0) + return &source; + } + return nullptr; } -bool source_fresh(uint32_t now_ms) { - return g_input_selected && g_source_active && g_latest_input.active && - static_cast(now_ms - g_latest_input.received_ms) < +#if SWITCH_PICO_SWITCH2_USB_BRIDGE +void clear_native_reports(Source& source) { + source.native_head = 0; + source.native_count = 0; +} + +bool source_fresh(const Source& source, uint32_t now_ms) { + return source.input_selected && source.source_active && source.latest_input.active && + static_cast(now_ms - source.latest_input.received_ms) < static_cast(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(now_ms - g_sample.started_ms) >= +bool sample_current(Source& source, uint32_t now_ms) { + if (source.sample.token && + (!source_fresh(source, now_ms) || source.sample.mouse_epoch != source.latest_input.mouse_epoch || + static_cast(now_ms - source.sample.started_ms) >= static_cast(kSampleDeadlineMs))) { - g_sample = {}; + source.sample = {}; } - return g_sample.token != 0; + return source.sample.token != 0; } #endif @@ -108,20 +127,20 @@ extern "C" void switch_pico_switch2_mouse_report( } critical_section_enter_blocking(&g_lock); + Source* selected = selected_source(product_id, address); #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 = {}; + if (report_id == 0 && selected != nullptr) { + selected->source_active = false; + clear_native_reports(*selected); + selected->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(); + for (Source& source : g_sources) clear_native_reports(source); #endif critical_section_exit(&g_lock); return; @@ -130,7 +149,9 @@ extern "C" void switch_pico_switch2_mouse_report( 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(); + if (g_total_records == UINT32_MAX) { + for (Source& source : g_sources) clear_native_reports(source); + } #endif write_u32(row + 4, received_ms); write_u16(row + 8, product_id); @@ -141,42 +162,43 @@ extern "C" void switch_pico_switch2_mouse_report( if (length != 0) memcpy(row + 20, report, length); memset(row + 20 + length, 0, SWITCH2_MOUSE_CAPTURE_REPORT_SIZE - length); g_next = static_cast((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 && + const uint8_t native_report_id = product_id == UNI_SW2_JOYCON_L_PID ? 0x07 : 0x08; + if (selected != nullptr && (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; + (report_id == native_report_id && length == SWITCH2_MOUSE_CAPTURE_NATIVE_INPUT_SIZE))) { + Source& source = *selected; + if (report_id != 0) { + if (!source.latest_input.active) { + source.latest_input.mouse_epoch = g_total_records; + source.latest_input.mouse_total_x = 0; + source.latest_input.mouse_total_y = 0; } - g_latest_input.active = true; + source.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(); + source.source_active = true; + if (source.native_stream && g_total_records != UINT32_MAX) { + if (source.native_count == kNativeReportCapacity) clear_native_reports(source); NativeReport& packet = - g_native_reports[(g_native_head + g_native_count) % kNativeReportCapacity]; + source.native_reports[(source.native_head + source.native_count) % kNativeReportCapacity]; memcpy(packet.report, report, sizeof(packet.report)); packet.serial = g_total_records; packet.received_ms = received_ms; - ++g_native_count; + ++source.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]; + memcpy(source.latest_input.buttons, report + 2, sizeof(source.latest_input.buttons)); + memcpy(source.latest_input.stick, report + 5, sizeof(source.latest_input.stick)); + source.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]; + source.latest_input.mouse_total_x += read_i16(report + 9); + source.latest_input.mouse_total_y += read_i16(report + 11); + source.latest_input.mouse_surface = report[13]; } else { - g_latest_input = {}; + source.latest_input = {}; } - g_latest_input.serial = g_total_records; - g_latest_input.received_ms = received_ms; + source.latest_input.serial = g_total_records; + source.latest_input.received_ms = received_ms; } if (g_count < SWITCH2_MOUSE_CAPTURE_CAPACITY) ++g_count; critical_section_exit(&g_lock); @@ -212,52 +234,65 @@ size_t switch2_mouse_capture_snapshot(uint8_t* output, size_t capacity, return required; } -void switch2_mouse_capture_select_input(const uint8_t address[6]) { - if (!g_initialized || address == nullptr) return; +void switch2_mouse_capture_select_input(uint8_t instance, const uint8_t address[6], uint16_t product_id) { + if (!g_initialized || instance >= SWITCH2_MOUSE_CAPTURE_SOURCE_COUNT || address == nullptr || + (product_id != UNI_SW2_JOYCON_L_PID && product_id != UNI_SW2_JOYCON_R_PID)) return; critical_section_enter_blocking(&g_lock); + Source& source = g_sources[instance]; + // A physical source has one owner; never duplicate its packets into both FIFOs. + Source* existing = selected_source(product_id, address); + if (existing != nullptr && existing != &source) { + critical_section_exit(&g_lock); + return; + } #if SWITCH_PICO_SWITCH2_USB_BRIDGE - g_source_active = false; - g_native_stream = false; - clear_native_reports(); - g_sample = {}; + source.source_active = false; + source.native_stream = false; + clear_native_reports(source); + source.sample = {}; #endif - memcpy(g_input_address, address, sizeof(g_input_address)); - g_latest_input = {}; - g_input_selected = true; + memcpy(source.input_address, address, sizeof(source.input_address)); + source.input_product_id = product_id; + source.latest_input = {}; + source.input_selected = true; critical_section_exit(&g_lock); } -bool switch2_mouse_capture_latest_input(uint32_t after_serial, +bool switch2_mouse_capture_latest_input(uint8_t instance, uint32_t after_serial, Switch2MouseCaptureInput* output) { - if (!g_initialized || output == nullptr) return false; + if (!g_initialized || instance >= SWITCH2_MOUSE_CAPTURE_SOURCE_COUNT || 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; + Source& source = g_sources[instance]; + const bool fresh = source.latest_input.serial > after_serial || + (source.latest_input.serial == 0 && after_serial != 0); + if (fresh) *output = source.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; +void switch2_mouse_capture_set_native_stream(uint8_t instance, bool enabled) { + if (!g_initialized || instance >= SWITCH2_MOUSE_CAPTURE_SOURCE_COUNT) return; critical_section_enter_blocking(&g_lock); - g_native_stream = enabled; - if (!enabled) clear_native_reports(); + Source& source = g_sources[instance]; + source.native_stream = enabled; + if (!enabled) clear_native_reports(source); 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; + uint8_t instance, uint32_t now_ms, uint8_t report[SWITCH2_MOUSE_CAPTURE_NATIVE_INPUT_SIZE]) { + if (!g_initialized || instance >= SWITCH2_MOUSE_CAPTURE_SOURCE_COUNT || report == nullptr) return 0; critical_section_enter_blocking(&g_lock); + Source& source = g_sources[instance]; uint32_t serial = 0; - if (!g_native_stream || !source_fresh(now_ms) || - (g_native_count != 0 && - static_cast(now_ms - g_native_reports[g_native_head].received_ms) >= + if (!source.native_stream || !source_fresh(source, now_ms) || + (source.native_count != 0 && + static_cast(now_ms - source.native_reports[source.native_head].received_ms) >= static_cast(kInputDeadlineMs))) { - clear_native_reports(); - } else if (g_native_count != 0) { - const NativeReport& packet = g_native_reports[g_native_head]; + clear_native_reports(source); + } else if (source.native_count != 0) { + const NativeReport& packet = source.native_reports[source.native_head]; memcpy(report, packet.report, sizeof(packet.report)); serial = packet.serial; } @@ -265,53 +300,57 @@ uint32_t switch2_mouse_capture_peek_native_report( return serial; } -bool switch2_mouse_capture_commit_native_report(uint32_t serial) { - if (!g_initialized || serial == 0) return false; +bool switch2_mouse_capture_commit_native_report(uint8_t instance, uint32_t serial) { + if (!g_initialized || instance >= SWITCH2_MOUSE_CAPTURE_SOURCE_COUNT || 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; + Source& source = g_sources[instance]; + const bool accepted = source.native_stream && source.native_count != 0 && + source.native_reports[source.native_head].serial == serial; if (accepted) { - g_native_head = static_cast((g_native_head + 1) % kNativeReportCapacity); - --g_native_count; + source.native_head = static_cast((source.native_head + 1) % kNativeReportCapacity); + --source.native_count; } critical_section_exit(&g_lock); return accepted; } -bool switch2_mouse_capture_request_sample(uint8_t sample_id, uint32_t now_ms, +bool switch2_mouse_capture_request_sample(uint8_t instance, 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; + if (!g_initialized || instance >= SWITCH2_MOUSE_CAPTURE_SOURCE_COUNT || token == nullptr || sample_id > 7) return false; critical_section_enter_blocking(&g_lock); - const bool accepted = !sample_current(now_ms) && source_fresh(now_ms) && + Source& source = g_sources[instance]; + const bool accepted = !sample_current(source, now_ms) && source_fresh(source, 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; + source.sample.token = ++g_sample_serial; + source.sample.started_ms = now_ms; + source.sample.mouse_epoch = source.latest_input.mouse_epoch; + source.sample.sample_id = sample_id; + *token = source.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; +int switch2_mouse_capture_sample_result(uint8_t instance, uint64_t token, uint32_t now_ms) { + if (!g_initialized || instance >= SWITCH2_MOUSE_CAPTURE_SOURCE_COUNT || token == 0) return -1; critical_section_enter_blocking(&g_lock); + Source& source = g_sources[instance]; int result = -1; - if (sample_current(now_ms) && g_sample.token == token) { - result = g_sample.acked ? 1 : 0; - if (result == 1) g_sample = {}; + if (source.sample.token == token && sample_current(source, now_ms)) { + result = source.sample.acked ? 1 : 0; + if (result == 1) source.sample = {}; } critical_section_exit(&g_lock); return result; } -void switch2_mouse_capture_cancel_sample() { - if (!g_initialized) return; +void switch2_mouse_capture_cancel_sample(uint8_t instance) { + if (!g_initialized || instance >= SWITCH2_MOUSE_CAPTURE_SOURCE_COUNT) return; critical_section_enter_blocking(&g_lock); - g_sample = {}; + Source& source = g_sources[instance]; + source.sample = {}; critical_section_exit(&g_lock); } @@ -321,13 +360,13 @@ extern "C" bool switch_pico_switch2_sample_take( 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; + Source* source = selected_source(product_id, address); + const bool accepted = source != nullptr && + sample_current(*source, now_ms) && !source->sample.taken; if (accepted) { - g_sample.taken = true; - *sample_id = g_sample.sample_id; - *token = g_sample.token; + source->sample.taken = true; + *sample_id = source->sample.sample_id; + *token = source->sample.token; } critical_section_exit(&g_lock); return accepted; @@ -338,12 +377,13 @@ extern "C" bool switch_pico_switch2_sample_result( 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; + Source* source = selected_source(product_id, address); + const bool accepted = source != nullptr && + source->sample.token == token && source->sample.taken && + sample_current(*source, now_ms); if (accepted) { - if (result > 0) g_sample.acked = true; - else if (result < 0) g_sample = {}; + if (result > 0) source->sample.acked = true; + else if (result < 0) source->sample = {}; } critical_section_exit(&g_lock); return accepted; diff --git a/src/firmware/input/switch2_mouse_capture.h b/src/firmware/input/switch2_mouse_capture.h index 6928537..47bf6bf 100644 --- a/src/firmware/input/switch2_mouse_capture.h +++ b/src/firmware/input/switch2_mouse_capture.h @@ -11,7 +11,7 @@ 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. +// Initialize once before starting the Bluetooth producer on its owning core. // Repeated initialization never clears the boot-lifetime sequence or records. void switch2_mouse_capture_init(); @@ -26,48 +26,58 @@ size_t switch2_mouse_capture_snapshot(uint8_t* output, size_t capacity, constexpr size_t SWITCH2_MOUSE_CAPTURE_NATIVE_INPUT_SIZE = 63; +#if SWITCH2_PROBE_COMPOSITE || SWITCH2_PROBE_HUB +constexpr uint8_t SWITCH2_MOUSE_CAPTURE_SOURCE_COUNT = 2; +#else +constexpr uint8_t SWITCH2_MOUSE_CAPTURE_SOURCE_COUNT = 1; +#endif + struct Switch2MouseCaptureInput { uint32_t serial; uint32_t received_ms; bool active; - // Unrotated native 08 payload bytes 2..3 and 5..7, without report ID. + // Unrotated native 07/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. + // Latest opaque native 07/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. + // Latest opaque native 07/08 byte 13; no interpreted surface semantics. uint8_t mouse_surface; }; -// Select one physical right Joy-Con before launching the Bluetooth producer. +// Select one physical Joy-Con per instance by address and PID (2067 L, 2066 R). +// Invalid instance/address/PID or a source owned by another instance leaves the +// active selection unchanged. A physical source can never feed both instances. // 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]); +// Its latest native 07/08 input / teardown survives unrelated ring traffic. +// Each selection also disables and clears the native FIFO and pending sample. +void switch2_mouse_capture_select_input(uint8_t instance, const uint8_t address[6], uint16_t product_id); -// 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. +// Copy a selected event newer than after_serial. A teardown is an event +// with active=false and zeroed fields. A new, still-empty selection returns a +// zero-serial inactive barrier to a reader with after_serial != 0. +// Otherwise false leaves output untouched. Nonzero 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, +bool switch2_mouse_capture_latest_input(uint8_t instance, uint32_t after_serial, Switch2MouseCaptureInput* output); #if SWITCH_PICO_SWITCH2_USB_BRIDGE -// Core 0 explicitly enables the selected source's ordered native 08 relay. +// Core 0 explicitly enables the selected source's ordered native 07/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. +// Only exact selected Joy-Con 63-byte native payloads (07 left, 08 right) queue. // 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); +void switch2_mouse_capture_set_native_stream(uint8_t instance, 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. @@ -75,19 +85,19 @@ void switch2_mouse_capture_set_native_stream(bool enabled); // 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]); + uint8_t instance, 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); +bool switch2_mouse_capture_commit_native_report(uint8_t instance, uint32_t serial); -// Core 0: one cue for the selected, active right Joy-Con's native stream. +// Core 0: one cue for the selected, active 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, +bool switch2_mouse_capture_request_sample(uint8_t instance, 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(); +int switch2_mouse_capture_sample_result(uint8_t instance, uint64_t token, uint32_t now_ms); +void switch2_mouse_capture_cancel_sample(uint8_t instance); #endif diff --git a/src/firmware/platform/pico/system_clock.cpp b/src/firmware/platform/pico/system_clock.cpp index 6c9dce6..6deaee1 100644 --- a/src/firmware/platform/pico/system_clock.cpp +++ b/src/firmware/platform/pico/system_clock.cpp @@ -15,8 +15,13 @@ SystemClockStatus g_status{}; void system_clock_initialize() { static_assert(SWITCH_PICO_SYS_CLOCK_MHZ == 150 || + SWITCH_PICO_SYS_CLOCK_MHZ == 240 || SWITCH_PICO_SYS_CLOCK_MHZ == 300 || SWITCH_PICO_SYS_CLOCK_MHZ == 400); +#if SWITCH2_PROBE_HUB + static_assert(SWITCH_PICO_SYS_CLOCK_MHZ == 240, + "Native SIO hub requires a 240 MHz system clock"); +#endif // Flash timing was established by boot stage 2. Do not raise clk_sys if // another boot configuration failed to provide the required divider. const uint32_t flash_divider = diff --git a/src/firmware/usb/native_hub/native_hub.c b/src/firmware/usb/native_hub/native_hub.c new file mode 100644 index 0000000..96f7ba4 --- /dev/null +++ b/src/firmware/usb/native_hub/native_hub.c @@ -0,0 +1,906 @@ +// Native RP2350 SIE transport for an embedded hub and two Joy-Con devices. +// Core1 selects address, endpoint controls and receive buffers. Core0 publishes +// transmit buffers and owns protocols/IRQ completions; IN endpoints NAK until ready. +#include "native_hub.h" +#include "router.h" +#include +#include +#include +#include +#include "hardware/clocks.h" +#include "hardware/irq.h" +#include "hardware/resets.h" +#include "hardware/structs/sio.h" +#include "hardware/structs/usb.h" +#include "hardware/structs/usb_dpram.h" +#include "hardware/sync.h" +#include "hardware/watchdog.h" +#include "pico/bootrom.h" +#include "pico/multicore.h" +#include "pico/stdlib.h" +#include "pico/unique_id.h" + +#ifndef NATIVE_HUB_SAMPLE_PHASE +#define NATIVE_HUB_SAMPLE_PHASE 4u +#endif +#define DEVICES 3u +#define CHANNELS 6u +#define PACKET 64u +#define EVENTS 64u +#define NONE 255u +#define CONNECT 1u +#define ENABLE 2u +#define SUSPEND 4u +#define RESET 16u +#define POWER 256u +#define C_CONNECT 1u +#define C_ENABLE 2u +#define C_SUSPEND 4u +#define C_RESET 16u + +typedef enum { IDLE, DATA_IN, DATA_OUT, STATUS_IN, STATUS_OUT, STALLED } stage_t; +typedef enum { NO_ACTION, ADDRESS, CONFIGURE, PORT_SET, PORT_CLEAR, HID_SET_REPORT, + HID_IDLE, HID_PROTOCOL, ENDPOINT_HALT, ENDPOINT_CLEAR } action_t; +typedef struct { + uint8_t data[128]; + uint16_t length, sent, packet_length; + bool busy, flush, zlp; + uint8_t next_pid; + bool halted; +} endpoint_t; +typedef struct { + tusb_control_request_t request; + uint8_t data[1024]; + uint8_t* external; + uint16_t length, position, packet_length; + stage_t stage; + action_t action; + bool zlp, vendor; + uint32_t generation; +} control_t; +typedef struct { + uint32_t buffers[CHANNELS]; + uint32_t endpoint_controls[4]; + uint32_t ep0_image[16]; + endpoint_t ep[CHANNELS]; + control_t control; + uint32_t generation; + // Control SETUP aborts only EP0, never unrelated HID/vendor completions. + uint32_t endpoint_generation[CHANNELS]; + uint8_t configuration, idle_rate, protocol; +} device_t; +typedef struct { uint16_t status, change; uint32_t deadline; } port_t; +typedef struct { + uint8_t device, channel, kind; + uint16_t length; + uint32_t generation; + uint8_t data[64]; +} event_t; + +static device_t devices[DEVICES]; +static port_t ports[2]; +static uint8_t addresses[DEVICES]; +static uint8_t default_device; +static volatile uint8_t active_device; +static volatile bool bank_restore_pending; +static spin_lock_t* bank_lock; +static event_t events[EVENTS]; +static volatile uint32_t event_head, event_tail; +static volatile bool failed; +static volatile bool bus_suspended; +static uint32_t startup_time; +static bool root_configured_once; +static uint32_t hub_endpoint_control; +static bool started; +static uint32_t setup_count[DEVICES], input_count[DEVICES], output_count[DEVICES]; +static uint32_t switches, missed_switches, slow_switches; +static uint32_t minimum_lateness = UINT32_MAX, maximum_lateness; +static uint32_t token_hits[DEVICES], missed_lock, blocked_buffers, blocked_sie, root_naks; +static uint16_t root_string[64]; +static char root_serial[48]; + +static const uint8_t hub_device[] = { + 18,1,0x10,1,9,0,0,64,0x7e,5,0x68,0x20,0,1,1,2,3,1 +}; +static const uint8_t hub_configuration[] = { + // Grip-style self-powered topology; the Bluetooth children use their own + // batteries. Do not advertise unimplemented high-speed TT/remote wake. + 9,2,25,0,1,1,0,0xc0,250, 9,4,0,0,1,9,0,0,0, 7,5,0x8f,3,1,0,12 +}; +static const uint8_t hub_descriptor[] = {9,0x29,2,0x11,0,5,100,6,255}; + +static inline volatile uint32_t* buffer_regs(void) { + return (volatile uint32_t*)&usb_dpram->ep_buf_ctrl[0]; +} +static inline volatile uint32_t* endpoint_regs(void) { + return (volatile uint32_t*)&usb_dpram->ep_ctrl[0]; +} +static inline uint32_t data_offset(uint8_t device, uint8_t channel) { + return 0x180u + ((uint32_t)device * 4u + channel - 2u) * PACKET; +} +static inline unsigned physical_channel(uint8_t slot, uint8_t channel) { + return slot == 0 && channel == 2 ? 30u : channel; +} +static inline unsigned logical_channel(uint8_t slot, uint16_t endpoint) { + if (slot == 0 && endpoint == 0x8f) return 2; + return (endpoint & 15u)*2u + ((endpoint & 0x80u) ? 0u : 1u); +} +static inline uint8_t* packet_buffer(uint8_t device, uint8_t channel) { + return channel < 2 ? usb_dpram->ep0_buf_a : + (uint8_t*)USBCTRL_DPRAM_BASE + data_offset(device, channel); +} +static __force_inline void copy_from_usb(uint8_t* to, const volatile uint8_t* from, uint16_t length) { + // Every DPRAM packet starts on a word boundary. Keep only the tail bytewise: + // unrestricted memcpy may generate an unaligned access for a short tail. + const volatile uint32_t* words = (const volatile uint32_t*)from; + while (length >= 4) { + uint32_t word = *words++; + memcpy(to,&word,4); + to += 4; + length -= 4; + } + from = (const volatile uint8_t*)words; + while (length--) *to++ = *from++; +} +static __force_inline void copy_to_usb(volatile uint8_t* to, const uint8_t* from, uint16_t length) { + volatile uint32_t* words = (volatile uint32_t*)to; + while (length >= 4) { + uint32_t word; + memcpy(&word,from,4); + *words++ = word; + from += 4; + length -= 4; + } + to = (volatile uint8_t*)words; + while (length--) *to++ = *from++; +} +static __force_inline void buffer_settle(void) { + // Same minimum metadata-to-AVAIL interval as the Pico TinyUSB DCD. + __asm volatile (".rept 12\n nop\n .endr" ::: "memory"); +} +static __force_inline void set_buffer(uint8_t device, uint8_t channel, uint32_t value) { + devices[device].buffers[channel] = value; + __dmb(); + if ((active_device == device && (!bank_restore_pending || (channel & 1u))) || + (device == 0 && channel == 2)) { + unsigned physical = physical_channel(device,channel); + buffer_regs()[physical] = value & ~USB_BUF_CTRL_AVAIL; + if (value & USB_BUF_CTRL_AVAIL) buffer_settle(); + buffer_regs()[physical] = value; + } +} + +// SRAM receiver only. No bank changes while a hardware completion is pending. +bool __not_in_flash_func(native_hub_select_device)(uint8_t address, uint8_t owner, uint32_t cutoff) { + if (owner >= DEVICES || bank_lock == NULL) return false; + ++token_hits[owner]; + if (active_device == owner && usb_hw->dev_addr_ctrl == address) return true; + if (!spin_try_lock_unsafe(bank_lock)) { ++missed_switches; ++missed_lock; return false; } + __dmb(); + if ((usb_hw->sie_status & USB_SIE_STATUS_SETUP_REC_BITS) || usb_hw->buf_status || + (int32_t)(sio_hw->mtime - cutoff) >= 0) { + ++missed_switches; + blocked_buffers = usb_hw->buf_status; + blocked_sie = usb_hw->sie_status; + spin_unlock_unsafe(bank_lock); + return false; + } + if (active_device != owner) { + const device_t* restrict incoming = &devices[owner]; + volatile uint32_t* buffers = (volatile uint32_t*)&usb_dpram->ep_buf_ctrl[0]; + volatile uint32_t* controls = (volatile uint32_t*)&usb_dpram->ep_ctrl[0]; + // Receive PID/availability must be selected before accepting an OUT token. + // Transmit buffers can safely NAK until Core0 publishes their contents. + for (unsigned i = 0; i < CHANNELS; ++i) { + uint32_t value = owner == 0 && i >= 2 ? 0 : incoming->buffers[i]; + buffers[i] = value & ~USB_BUF_CTRL_AVAIL; + } + usb_dpram->ep_ctrl[14].in = owner == 0 ? hub_endpoint_control : 0; + for (unsigned i = 0; i < 4; ++i) controls[i] = incoming->endpoint_controls[i]; + buffer_settle(); + for (unsigned i = 1; i < CHANNELS; i += 2) + buffers[i] = owner == 0 && i >= 2 ? 0 : incoming->buffers[i]; + __dmb(); + usb_hw->dev_addr_ctrl = address; + bank_restore_pending = true; + active_device = owner; + } else { + usb_hw->dev_addr_ctrl = address; + } + __dmb(); + ++switches; + int32_t lateness = (int32_t)(sio_hw->mtime - cutoff); + if (lateness >= 0) { + ++slow_switches; + if ((uint32_t)lateness < minimum_lateness) minimum_lateness = (uint32_t)lateness; + if ((uint32_t)lateness > maximum_lateness) maximum_lateness = (uint32_t)lateness; + } + spin_unlock_unsafe(bank_lock); + return true; +} +static void __not_in_flash_func(restore_selected_bank)(void) { + if (!bank_restore_pending) return; + uint32_t flags = spin_lock_blocking(bank_lock); + if (!bank_restore_pending || (usb_hw->sie_status & USB_SIE_STATUS_SETUP_REC_BITS) || + (usb_hw->buf_status & 0x3fu)) { + spin_unlock(bank_lock,flags); + return; + } + uint8_t owner = active_device; + const device_t* incoming = &devices[owner]; + volatile uint32_t* buffers = buffer_regs(); + if ((incoming->buffers[0] & USB_BUF_CTRL_FULL) && + (incoming->buffers[0] & USB_BUF_CTRL_LEN_MASK)) { + volatile uint32_t* to = (volatile uint32_t*)usb_dpram->ep0_buf_a; + const uint32_t* from = incoming->ep0_image; + unsigned words = ((incoming->buffers[0] & USB_BUF_CTRL_LEN_MASK) + 3u) / 4u; + for (unsigned i = 0; i < words; ++i) to[i] = from[i]; + } + for (unsigned i = 0; i < CHANNELS; i += 2) { + uint32_t value = owner == 0 && i >= 2 ? 0 : incoming->buffers[i]; + buffers[i] = value & ~USB_BUF_CTRL_AVAIL; + } + usb_hw->ep_stall_arm = ((incoming->buffers[0] & USB_BUF_CTRL_STALL) ? 1u : 0u) | + ((incoming->buffers[1] & USB_BUF_CTRL_STALL) ? 2u : 0u); + buffer_settle(); + for (unsigned i = 0; i < CHANNELS; i += 2) + buffers[i] = owner == 0 && i >= 2 ? 0 : incoming->buffers[i]; + bank_restore_pending = false; + spin_unlock(bank_lock,flags); +} + + +static void publish_addresses(void) { probe_router_publish(addresses, default_device); } +static uint8_t hardware_owner(void) { + uint8_t address = usb_hw->dev_addr_ctrl & 127u; + if (address == 0) return default_device; + for (uint8_t i = 0; i < DEVICES; ++i) if (addresses[i] == address) return i; + return NONE; +} +static __force_inline bool push_event(uint8_t device, uint8_t channel, uint8_t kind, uint16_t length, + const uint8_t* data) { + uint32_t next = (event_head + 1u) % EVENTS; + if (next == event_tail || length > 64) { failed = true; return false; } + event_t* event = &events[event_head]; + event->device = device; event->channel = channel; event->kind = kind; + event->length = length; + event->generation = device < DEVICES ? (channel < 2 ? devices[device].generation : + devices[device].endpoint_generation[channel]) : 0; + if (kind == 2 && (channel & 1u) && channel != 1) copy_from_usb(event->data,data,length); + else if (length) memcpy(event->data,data,length); + __dmb(); event_head = next; + return true; +} + +static void __not_in_flash_func(usb_interrupt)(void) { + uint32_t flags = spin_lock_blocking(bank_lock); + uint32_t status = usb_hw->ints; + if (status & USB_INTS_BUS_RESET_BITS) { + // Reset wins over stale transfers and setup snapshots. + for (uint8_t i = 0; i < DEVICES; ++i) { + ++devices[i].generation; + for (unsigned ch = 2; ch < CHANNELS; ++ch) ++devices[i].endpoint_generation[ch]; + } + for (unsigned i = 0; i < CHANNELS; ++i) buffer_regs()[i] = 0; + hw_clear_bits(&usb_hw->buf_status,usb_hw->buf_status); + hw_clear_bits(&usb_hw->sie_status,USB_SIE_STATUS_BUS_RESET_BITS | USB_SIE_STATUS_SETUP_REC_BITS); + push_event(0,0,3,0,NULL); + spin_unlock(bank_lock, flags); + return; + } + uint8_t owner = active_device; + if (owner >= DEVICES) { + failed = true; + usb_hw->inte = 0; + spin_unlock(bank_lock,flags); + return; + } + uint32_t pending = usb_hw->buf_status; + while (pending) { + unsigned physical = (unsigned)__builtin_ctz(pending); + uint32_t mask = 1u << physical; + unsigned channel = physical == 30 ? 2u : physical; + uint8_t completed_owner = physical == 30 ? 0u : owner; + if (channel >= CHANNELS) { failed = true; hw_clear_bits(&usb_hw->buf_status,mask); pending &= ~mask; continue; } + uint32_t value = buffer_regs()[physical]; + uint16_t length = value & USB_BUF_CTRL_LEN_MASK; + if (length > PACKET) { failed = true; length = 0; } + const uint8_t* data = (channel & 1u) ? packet_buffer(completed_owner,channel) : + devices[completed_owner].ep[channel].data; + uint32_t ep0_snapshot[PACKET / sizeof(uint32_t)]; + if (channel == 1 && length) { + // EP0 storage is shared; other packet buffers belong to one device. + copy_from_usb((uint8_t*)ep0_snapshot,data,length); + data = (const uint8_t*)ep0_snapshot; + } + devices[completed_owner].ep[channel].next_pid ^= 1u; + devices[completed_owner].buffers[channel] = 0; + buffer_regs()[physical] = 0; + hw_clear_bits(&usb_hw->buf_status,mask); + pending &= ~mask; + // Unarmed device-specific buffers and the TX shadow cannot be reused + // until Core0 consumes this event. Copy them without blocking routing. + spin_unlock(bank_lock,flags); + push_event(completed_owner,(uint8_t)channel,2,length,data); + if (!pending && !(status & (USB_INTS_SETUP_REQ_BITS | USB_INTS_DEV_SUSPEND_BITS | + USB_INTS_DEV_RESUME_FROM_HOST_BITS))) return; + flags = spin_lock_blocking(bank_lock); + } + if (status & USB_INTS_SETUP_REQ_BITS) { + uint8_t actual_owner = hardware_owner(); + uint8_t setup[8]; + copy_from_usb(setup, usb_dpram->setup_packet, sizeof(setup)); + if (actual_owner < DEVICES && actual_owner == owner) { + ++devices[owner].generation; + devices[owner].buffers[0] = devices[owner].buffers[1] = 0; + buffer_regs()[0] = buffer_regs()[1] = 0; + devices[owner].ep[0].next_pid = devices[owner].ep[1].next_pid = 1; + push_event(owner,0,1,sizeof(setup),setup); + } else { + // No logical owner: never reinterpret it as another controller. + hw_set_bits(&usb_hw->ep_stall_arm,3u); + buffer_regs()[0] = buffer_regs()[1] = USB_BUF_CTRL_STALL; + } + hw_clear_bits(&usb_hw->sie_status,USB_SIE_STATUS_SETUP_REC_BITS); + } + if (status & USB_INTS_DEV_SUSPEND_BITS) { + bus_suspended = true; hw_clear_bits(&usb_hw->sie_status,USB_SIE_STATUS_SUSPENDED_BITS); + } + if (status & USB_INTS_DEV_RESUME_FROM_HOST_BITS) { + bus_suspended = false; hw_clear_bits(&usb_hw->sie_status,USB_SIE_STATUS_RESUME_BITS); + } + spin_unlock(bank_lock, flags); +} + +static void stall(uint8_t slot) { + uint32_t flags = spin_lock_blocking(bank_lock); + if (devices[slot].control.generation != devices[slot].generation) { + spin_unlock(bank_lock,flags); + return; + } + devices[slot].control.stage = STALLED; + devices[slot].control.action = NO_ACTION; + if (active_device == slot) hw_set_bits(&usb_hw->ep_stall_arm,3u); + set_buffer(slot,0,USB_BUF_CTRL_STALL); set_buffer(slot,1,USB_BUF_CTRL_STALL); + spin_unlock(bank_lock, flags); +} +static void __not_in_flash_func(arm_packet)(uint8_t slot, uint8_t channel, const uint8_t* data, uint16_t length) { + endpoint_t* ep = &devices[slot].ep[channel]; + uint32_t generation = channel < 2 ? devices[slot].control.generation : + devices[slot].endpoint_generation[channel]; + if (!(channel & 1u) && length) { + // Private packet storage is unarmed until the metadata below is published. + memcpy(ep->data,data,length); + if (channel == 0) memcpy(devices[slot].ep0_image,data,length); + else copy_to_usb(packet_buffer(slot,channel),data,length); + } + uint32_t flags = spin_lock_blocking(bank_lock); + if (generation != (channel < 2 ? devices[slot].generation : + devices[slot].endpoint_generation[channel])) { + spin_unlock(bank_lock,flags); + return; + } + ep->packet_length = length; + uint32_t value = USB_BUF_CTRL_AVAIL | USB_BUF_CTRL_LAST | USB_BUF_CTRL_SEL | + (ep->next_pid ? USB_BUF_CTRL_DATA1_PID : 0); + if (!(channel & 1u)) { + if (channel == 0 && active_device == slot && !bank_restore_pending) + copy_to_usb(packet_buffer(slot,channel),data,length); + value |= USB_BUF_CTRL_FULL | length; + } else if (channel == 1) { + control_t* c = &devices[slot].control; + uint16_t remaining = c->stage == DATA_OUT ? c->length - c->position : 0; + value |= remaining > PACKET ? PACKET : remaining; + } else { + value |= PACKET; + } + set_buffer(slot,channel,value); + spin_unlock(bank_lock, flags); +} +static void control_next(uint8_t slot) { + control_t* c = &devices[slot].control; + uint16_t left = c->length - c->position; + c->packet_length = left > PACKET ? PACKET : left; + if (!left) c->zlp = false; + c->stage = DATA_IN; + if (slot == 0) { + uint8_t preview[4] = {0}; + uint16_t preview_length = c->packet_length < 4 ? c->packet_length : 4; + if (preview_length) memcpy(preview,c->data+c->position,preview_length); + probe_debug_printf("[HUB_CTRL] arm g=%" PRIu32 " len=%u pid=%u data=%02x%02x%02x%02x\n", + c->generation, c->packet_length, devices[slot].ep[0].next_pid, + preview[0],preview[1],preview[2],preview[3]); + } + arm_packet(slot,0,c->data + c->position,c->packet_length); +} +static void reply(uint8_t slot, const void* data, uint16_t length) { + control_t* c = &devices[slot].control; + if (length > sizeof(c->data)) { stall(slot); return; } + if (length && data != c->data) memcpy(c->data,data,length); + c->length = length < c->request.wLength ? length : c->request.wLength; + c->position = 0; + c->zlp = length < c->request.wLength && length % PACKET == 0; + if (!c->request.wLength) { c->stage = STATUS_OUT; arm_packet(slot,1,NULL,0); } + else control_next(slot); +} +static void status_in(uint8_t slot, action_t action) { + control_t* c = &devices[slot].control; + c->action = action; c->stage = STATUS_IN; + arm_packet(slot,0,NULL,0); +} +bool native_hub_control_xfer(uint8_t slot, const tusb_control_request_t* request, + void* buffer, uint16_t length) { + if (slot >= DEVICES || request == NULL || (length && buffer == NULL)) return false; + control_t* c = &devices[slot].control; + if (memcmp(request,&c->request,sizeof(*request)) != 0) return false; + if (request->bmRequestType & 0x80) reply(slot,buffer,length); + else if (!request->wLength) status_in(slot,NO_ACTION); + else { + if (length < request->wLength || request->wLength > sizeof(c->data)) return false; + c->external = buffer; c->length = request->wLength; c->position = 0; + c->stage = DATA_OUT; arm_packet(slot,1,NULL,0); + } + return c->stage != STALLED; +} +bool native_hub_control_status(uint8_t slot, const tusb_control_request_t* request) { + if (slot >= DEVICES || request == NULL || request->wLength) return false; + if (request->bmRequestType & 0x80) { + devices[slot].control.stage = STATUS_OUT; arm_packet(slot,1,NULL,0); + } else status_in(slot,NO_ACTION); + return true; +} + +static void reset_device(uint8_t slot) { + uint32_t flags = spin_lock_blocking(bank_lock); + ++devices[slot].generation; + for (unsigned ch = 2; ch < CHANNELS; ++ch) ++devices[slot].endpoint_generation[ch]; + memset(devices[slot].buffers,0,sizeof(devices[slot].buffers)); + memset(devices[slot].endpoint_controls,0,sizeof(devices[slot].endpoint_controls)); + memset(devices[slot].ep,0,sizeof(devices[slot].ep)); + memset(&devices[slot].control,0,sizeof(devices[slot].control)); + devices[slot].configuration = 0; devices[slot].protocol = 1; + if (slot == 0) { + hub_endpoint_control = 0; + usb_dpram->ep_ctrl[14].in = 0; + buffer_regs()[30] = 0; + } + if (active_device == slot) { + for (unsigned i = 0; i < CHANNELS; ++i) buffer_regs()[i] = 0; + for (unsigned i = 0; i < 4; ++i) endpoint_regs()[i] = 0; + } + spin_unlock(bank_lock, flags); + if (slot) native_joycon_usb_reset(slot-1); +} +static void forget_port(unsigned port) { + uint8_t slot = port + 1; + addresses[slot] = NONE; + if (default_device == slot) default_device = NONE; + reset_device(slot); + publish_addresses(); +} +static void reset_bus(void) { + probe_router_enable(false); + uint32_t flags = spin_lock_blocking(bank_lock); + active_device = 0; usb_hw->dev_addr_ctrl = 0; + memset(ports,0,sizeof(ports)); + addresses[0] = 0; addresses[1] = addresses[2] = NONE; default_device = 0; + bus_suspended = false; + spin_unlock(bank_lock, flags); + for (uint8_t slot = 0; slot < DEVICES; ++slot) reset_device(slot); + publish_addresses(); probe_router_enable(true); +} +static void configure_device(uint8_t slot, uint8_t configuration) { + uint32_t flags = spin_lock_blocking(bank_lock); + device_t* d = &devices[slot]; + d->configuration = configuration; + for (unsigned ch = 2; ch < CHANNELS; ++ch) ++d->endpoint_generation[ch]; + if (slot == 0 && configuration != 0) root_configured_once = true; + for (unsigned ch = 2; ch < CHANNELS; ++ch) { + bool use = configuration && slot != 0; + uint8_t type = slot && ch >= 4 ? TUSB_XFER_BULK : TUSB_XFER_INTERRUPT; + d->endpoint_controls[ch-2] = use ? EP_CTRL_ENABLE_BITS | EP_CTRL_INTERRUPT_PER_BUFFER | + ((uint32_t)type << EP_CTRL_BUFFER_TYPE_LSB) | data_offset(slot,ch) | + (ch == 2 ? EP_CTRL_INTERRUPT_ON_NAK : 0) : 0; + d->buffers[ch] = 0; memset(&d->ep[ch],0,sizeof(d->ep[ch])); + if (active_device == slot) { + endpoint_regs()[ch-2] = d->endpoint_controls[ch-2]; buffer_regs()[ch] = 0; + } + } + if (slot == 0) { + hub_endpoint_control = configuration ? EP_CTRL_ENABLE_BITS | EP_CTRL_INTERRUPT_PER_BUFFER | EP_CTRL_INTERRUPT_ON_NAK | + ((uint32_t)TUSB_XFER_INTERRUPT << EP_CTRL_BUFFER_TYPE_LSB) | data_offset(0,2) : 0; + usb_dpram->ep_ctrl[14].in = active_device == 0 ? hub_endpoint_control : 0; + buffer_regs()[30] = 0; + } + spin_unlock(bank_lock, flags); + if (slot) { + native_joycon_usb_reset(slot-1); + if (configuration) { arm_packet(slot,3,NULL,0); arm_packet(slot,5,NULL,0); } + } else if (!configuration) { + for (unsigned p = 0; p < 2; ++p) { ports[p].status = ports[p].change = 0; forget_port(p); } + } +} +static const uint16_t* hub_string(uint8_t index) { + if (!index) { root_string[0] = 0x0304; root_string[1] = 0x0409; return root_string; } + const char* text = index == 1 ? "Nintendo Co., Ltd." : + index == 2 ? "Joy-Con 2 Charging Grip" : index == 3 ? root_serial : NULL; + if (!text) return NULL; + size_t size = strlen(text); if (size > 63) size = 63; + root_string[0] = (uint16_t)(0x0300u | (2u + 2u*size)); + for (size_t n = 0; n < size; ++n) root_string[n+1] = (uint8_t)text[n]; + return root_string; +} +static uint16_t get16(const uint8_t* p) { return (uint16_t)(p[0] | ((uint16_t)p[1]<<8)); } +static void word_reply(uint8_t slot, uint16_t value, uint16_t length) { + uint8_t data[2] = {(uint8_t)value,(uint8_t)(value>>8)}; reply(slot,data,length); +} +static bool standard_request(uint8_t slot) { + control_t* c = &devices[slot].control; + const tusb_control_request_t* r = &c->request; + uint8_t recipient = r->bmRequestType & 31u; + if (r->bRequest == TUSB_REQ_GET_DESCRIPTOR && (r->bmRequestType & 0x80)) { + uint8_t type = r->wValue >> 8, index = r->wValue; + const uint8_t* data = NULL; uint16_t size = 0; + if (type == TUSB_DESC_DEVICE && !index && !r->wIndex && recipient == 0) { + data = slot ? native_joycon_device_descriptor(slot-1) : hub_device; size = 18; + } else if (type == TUSB_DESC_CONFIGURATION && !index && !r->wIndex && recipient == 0) { + data = slot ? native_joycon_configuration_descriptor(slot-1) : hub_configuration; + size = get16(data+2); + } else if (type == TUSB_DESC_STRING && recipient == 0) { + const uint16_t* text = slot ? native_joycon_string_descriptor(slot-1,index,r->wIndex) : hub_string(index); + if (text) { data = (const uint8_t*)text; size = text[0] & 255u; } + } else if (slot && recipient == 1 && !r->wIndex && !index && type == 0x22) { + data = tud_hid_descriptor_report_cb(slot-1); size = 100; + } else if (slot && recipient == 1 && !r->wIndex && !index && type == 0x21) { + data = native_joycon_configuration_descriptor(slot-1)+26; size = 9; + } + if (!data) return false; + reply(slot,data,size); return true; + } + if (recipient == 0) { + if (r->bRequest == TUSB_REQ_SET_ADDRESS && r->bmRequestType == 0 && !r->wLength && + !r->wIndex && r->wValue <= 127 && !devices[slot].configuration) { + for (unsigned i = 0; i < DEVICES; ++i) if (i != slot && addresses[i] == r->wValue) return false; + status_in(slot,ADDRESS); return true; + } + if (r->bRequest == TUSB_REQ_SET_CONFIGURATION && r->bmRequestType == 0 && !r->wLength && + !r->wIndex && r->wValue <= 1) { status_in(slot,CONFIGURE); return true; } + if (r->bRequest == TUSB_REQ_GET_CONFIGURATION && r->bmRequestType == 0x80 && + !r->wValue && !r->wIndex && r->wLength == 1) { word_reply(slot,devices[slot].configuration,1); return true; } + if (r->bRequest == TUSB_REQ_GET_STATUS && r->bmRequestType == 0x80 && + !r->wValue && !r->wIndex && r->wLength == 2) { word_reply(slot,1,2); return true; } + } + if (recipient == 1 && r->wIndex < (slot ? 2 : 1)) { + if (r->bRequest == TUSB_REQ_GET_STATUS && r->bmRequestType == 0x81 && !r->wValue && r->wLength == 2) { word_reply(slot,0,2); return true; } + if (r->bRequest == TUSB_REQ_GET_INTERFACE && r->bmRequestType == 0x81 && !r->wValue && r->wLength == 1) { word_reply(slot,0,1); return true; } + if (r->bRequest == TUSB_REQ_SET_INTERFACE && r->bmRequestType == 1 && !r->wValue && !r->wLength) { status_in(slot,NO_ACTION); return true; } + } + if (recipient == 2 && !(r->wIndex & 0xff70u)) { + unsigned ep = r->wIndex & 15u; + unsigned channel = logical_channel(slot,r->wIndex); + if (channel >= CHANNELS || (ep && !(slot == 0 && r->wIndex == 0x8f ? + hub_endpoint_control : devices[slot].endpoint_controls[channel-2]))) return false; + if (r->bRequest == TUSB_REQ_GET_STATUS && r->bmRequestType == 0x82 && !r->wValue && r->wLength == 2) { word_reply(slot,devices[slot].ep[channel].halted,2); return true; } + if (ep && !r->wValue && !r->wLength && r->bmRequestType == 2 && + (r->bRequest == TUSB_REQ_SET_FEATURE || r->bRequest == TUSB_REQ_CLEAR_FEATURE)) { + status_in(slot,r->bRequest == TUSB_REQ_SET_FEATURE ? ENDPOINT_HALT : ENDPOINT_CLEAR); return true; + } + } + return false; +} +static bool class_request(uint8_t slot) { + control_t* c = &devices[slot].control; const tusb_control_request_t* r = &c->request; + if (!slot) { + if (r->bmRequestType == 0xa0 && r->bRequest == 6 && r->wValue == 0x2900 && !r->wIndex) { reply(slot,hub_descriptor,sizeof(hub_descriptor)); return true; } + if (r->bmRequestType == 0xa0 && r->bRequest == 0 && !r->wValue && !r->wIndex && r->wLength == 4) { uint32_t zero=0; reply(slot,&zero,4); return true; } + if (r->wIndex < 1 || r->wIndex > 2) return false; + port_t* p = &ports[r->wIndex-1]; + if (r->bmRequestType == 0xa3 && !r->bRequest && !r->wValue && r->wLength == 4) { + uint8_t status[4]={(uint8_t)p->status,(uint8_t)(p->status>>8),(uint8_t)p->change,(uint8_t)(p->change>>8)}; + reply(slot,status,4); return true; + } + if (r->bmRequestType != 0x23 || r->wLength || (r->bRequest != 1 && r->bRequest != 3)) return false; + bool set = r->bRequest == 3; + if (set && r->wValue != 8 && r->wValue != 4 && r->wValue != 2) return false; + if (!set && r->wValue != 8 && r->wValue != 1 && r->wValue != 2 && + (r->wValue < 16 || r->wValue > 20)) return false; + if (set && r->wValue == 4 && (!(p->status & POWER) || + (default_device != NONE && default_device != r->wIndex))) return false; + status_in(slot,set ? PORT_SET : PORT_CLEAR); return true; + } + if (r->wIndex != 0) return false; + if (r->bmRequestType == 0xa1 && r->bRequest == 1) { + uint8_t id = r->wValue, type = r->wValue >> 8; + uint16_t limit = r->wLength < 64 ? r->wLength : 64; + uint16_t prefix = id && limit > 1 ? 1 : 0; + if (prefix) c->data[0] = id; + uint16_t size = tud_hid_get_report_cb(slot-1,id,(hid_report_type_t)type,c->data+prefix,limit-prefix); + if (!size || size > limit-prefix) return false; + reply(slot,c->data,size+prefix); return true; + } + if (r->bmRequestType == 0x21 && r->bRequest == 9 && r->wLength <= 64) { + c->action = HID_SET_REPORT; c->external = c->data; c->length = r->wLength; c->position = 0; + if (r->wLength) { c->stage = DATA_OUT; arm_packet(slot,1,NULL,0); } + else status_in(slot,HID_SET_REPORT); + return true; + } + if (r->bmRequestType == 0x21 && r->bRequest == 10 && !r->wLength) { status_in(slot,HID_IDLE); return true; } + if (r->bmRequestType == 0xa1 && r->bRequest == 2 && r->wLength == 1) { word_reply(slot,devices[slot].idle_rate,1); return true; } + if (r->bmRequestType == 0x21 && r->bRequest == 11 && !r->wLength && r->wValue <= 1) { status_in(slot,HID_PROTOCOL); return true; } + if (r->bmRequestType == 0xa1 && r->bRequest == 3 && !r->wValue && r->wLength == 1) { word_reply(slot,devices[slot].protocol,1); return true; } + return false; +} +static void setup_request(const event_t* event) { + uint8_t slot = event->device; device_t* d = &devices[slot]; + if (event->generation != d->generation) return; + memset(&d->control,0,sizeof(d->control)); + control_t* c = &d->control; memcpy(&c->request,event->data,8); + c->generation = event->generation; + if (slot == 0) + probe_debug_printf("[HUB_CTRL] setup g=%" PRIu32 " req=%02x/%02x v=%04x i=%04x n=%u\n", + c->generation, c->request.bmRequestType, c->request.bRequest, + c->request.wValue, c->request.wIndex, c->request.wLength); + ++setup_count[slot]; + uint8_t type = c->request.bmRequestType & 0x60; + bool supported; + if (type == 0) supported = standard_request(slot); + else if (type == 0x20) supported = class_request(slot); + else if (type == 0x40) { + c->vendor = true; + supported = tud_vendor_control_xfer_cb(slot,CONTROL_STAGE_SETUP,&c->request); + } else supported = false; + if (!supported) stall(slot); +} +static void control_complete(uint8_t slot) { + control_t* c = &devices[slot].control; + c->stage = IDLE; + if (c->vendor) { tud_vendor_control_xfer_cb(slot,CONTROL_STAGE_ACK,&c->request); return; } + switch (c->action) { + case ADDRESS: { + uint32_t flags = spin_lock_blocking(bank_lock); + addresses[slot] = (uint8_t)c->request.wValue; + if (!addresses[slot]) default_device = slot; + else if (default_device == slot) default_device = NONE; + if (active_device == slot) usb_hw->dev_addr_ctrl = addresses[slot]; + spin_unlock(bank_lock,flags); + publish_addresses(); break; + } + case CONFIGURE: configure_device(slot,(uint8_t)c->request.wValue); break; + case HID_IDLE: devices[slot].idle_rate = c->request.wValue >> 8; break; + case HID_PROTOCOL: devices[slot].protocol = c->request.wValue; break; + case HID_SET_REPORT: { + uint8_t id = c->request.wValue; const uint8_t* data = c->data; uint16_t length = c->position; + if (id && length > 1 && data[0] == id) { ++data; --length; } + tud_hid_set_report_cb(slot-1,id,(hid_report_type_t)(c->request.wValue>>8),data,length); break; + } + case ENDPOINT_HALT: + case ENDPOINT_CLEAR: { + uint8_t channel = (uint8_t)logical_channel(slot,c->request.wIndex); + uint32_t flags = spin_lock_blocking(bank_lock); + ++devices[slot].endpoint_generation[channel]; + endpoint_t* ep = &devices[slot].ep[channel]; ep->halted = c->action == ENDPOINT_HALT; + ep->busy = ep->flush = ep->zlp = false; ep->next_pid = 0; + set_buffer(slot,channel,ep->halted ? USB_BUF_CTRL_STALL : 0); + spin_unlock(bank_lock,flags); + if (!ep->halted && (channel & 1u)) arm_packet(slot,channel,NULL,0); + break; + } + case PORT_SET: + case PORT_CLEAR: { + unsigned index = c->request.wIndex - 1; port_t* p = &ports[index]; bool set = c->action == PORT_SET; + switch (c->request.wValue) { + case 8: + if (set) { p->status |= POWER | CONNECT; p->change |= C_CONNECT; } + else { p->status = 0; p->change |= C_CONNECT; forget_port(index); } + break; + case 4: + forget_port(index); p->status = (p->status | RESET) & ~(ENABLE | SUSPEND); + p->deadline = time_us_32()+10000u; break; + case 1: p->status &= ~ENABLE; forget_port(index); break; + case 2: + if (set) p->status |= SUSPEND; + else { p->status &= ~SUSPEND; p->change |= C_SUSPEND; } + break; + default: p->change &= ~(1u << (c->request.wValue-16)); break; + } + break; + } + default: break; + } +} +static void transmit_next(uint8_t slot, uint8_t channel) { + endpoint_t* ep = &devices[slot].ep[channel]; + uint16_t remaining = ep->length - ep->sent; + uint16_t size = remaining > 64 ? 64 : remaining; + if (!remaining) ep->zlp = false; + // arm_packet snapshots this packet for actual completion callbacks. + uint8_t packet[64]; if (size) memcpy(packet,ep->data+ep->sent,size); + arm_packet(slot,channel,packet,size); +} +static void transfer_complete(const event_t* event) { + uint8_t slot = event->device, channel = event->channel; + device_t* d = &devices[slot]; + if (channel >= 2 && event->generation != d->endpoint_generation[channel]) return; + if (channel < 2) { + control_t* c = &d->control; + if (slot == 0) + probe_debug_printf("[HUB_CTRL] complete g=%" PRIu32 "/%" PRIu32 " ch=%u len=%u expected=%u state=%u\n", + event->generation, c->generation, channel, event->length, + c->packet_length, (unsigned)c->stage); + if (event->generation != c->generation) return; + if ((c->stage == STATUS_IN && channel == 0) || (c->stage == STATUS_OUT && channel == 1)) { + if (event->length) stall(slot); else control_complete(slot); + } else if (c->stage == DATA_IN && channel == 0) { + if (event->generation != d->generation) return; + if (event->length != c->packet_length) { stall(slot); return; } + c->position += event->length; + if (c->position < c->length || c->zlp) control_next(slot); + else { + if (c->vendor && !tud_vendor_control_xfer_cb(slot,CONTROL_STAGE_DATA,&c->request)) { stall(slot); return; } + c->stage = STATUS_OUT; arm_packet(slot,1,NULL,0); + } + } else if (c->stage == DATA_OUT && channel == 1) { + if (event->generation != d->generation) return; + if (event->length > c->length-c->position) { stall(slot); return; } + if (event->length) memcpy(c->external+c->position,event->data,event->length); + c->position += event->length; + if (c->position == c->length || event->length < PACKET) { + if (c->position != c->length || (c->vendor && !tud_vendor_control_xfer_cb(slot,CONTROL_STAGE_DATA,&c->request))) { stall(slot); return; } + status_in(slot,c->action); + } else arm_packet(slot,1,NULL,0); + } + return; + } + if (!d->configuration) return; + endpoint_t* ep = &d->ep[channel]; + if (channel & 1u) { + ++output_count[slot]; + if (slot && channel == 5) tud_vendor_rx_cb(slot-1,event->data,event->length); + else if (slot && channel == 3) tud_hid_set_report_cb(slot-1,0,HID_REPORT_TYPE_OUTPUT,event->data,event->length); + if (!ep->halted) arm_packet(slot,channel,NULL,0); + } else { + if (!ep->busy || event->length != ep->packet_length) { failed = true; return; } + ep->sent += event->length; + bool done = ep->sent == ep->length && !ep->zlp; + if (done) { ep->busy = false; ep->flush = false; } + else transmit_next(slot,channel); + ++input_count[slot]; + if (slot && channel == 2) tud_hid_report_complete_cb(slot-1,event->data,event->length); + else if (slot && channel == 4) tud_vendor_tx_cb(slot-1,event->length); + } +} + +bool native_hub_mounted(uint8_t instance) { return instance < 2 && devices[instance+1].configuration != 0; } +bool native_hub_suspended(uint8_t instance) { return instance >= 2 || bus_suspended || (ports[instance].status & SUSPEND); } +bool native_hub_hid_ready(uint8_t instance) { + return native_hub_mounted(instance) && !native_hub_suspended(instance) && + !devices[instance+1].ep[2].busy && !devices[instance+1].ep[2].halted; +} +bool native_hub_hid_report(uint8_t instance, uint8_t report_id, const void* data, uint16_t length) { + if (!native_hub_hid_ready(instance) || length > 63 || (length && !data)) return false; + endpoint_t* ep = &devices[instance+1].ep[2]; + ep->data[0] = report_id; if (length) memcpy(ep->data+1,data,length); + ep->length = length+1; ep->sent = 0; ep->busy = ep->flush = true; ep->zlp = false; + transmit_next(instance+1,2); return true; +} +uint32_t native_hub_vendor_write_available(uint8_t instance) { + if (!native_hub_mounted(instance) || native_hub_suspended(instance)) return 0; + endpoint_t* ep = &devices[instance+1].ep[4]; + return ep->busy || ep->halted ? 0 : sizeof(ep->data); +} +uint32_t native_hub_vendor_write(uint8_t instance, const void* data, uint32_t length) { + if (!length || length > native_hub_vendor_write_available(instance) || !data) return 0; + endpoint_t* ep = &devices[instance+1].ep[4]; + memcpy(ep->data,data,length); ep->length = length; ep->sent = 0; + ep->busy = true; ep->flush = false; ep->zlp = length % 64 == 0; + return length; +} +uint32_t native_hub_vendor_write_flush(uint8_t instance) { + if (instance >= 2) return 0; + endpoint_t* ep = &devices[instance+1].ep[4]; + if (!ep->busy || ep->flush) return 0; + ep->flush = true; transmit_next(instance+1,4); return ep->length; +} + +void native_hub_startup_guard(void) { + if (watchdog_enable_caused_reboot()) { + stdio_init_all(); + printf("[NATIVE_HUB] watchdog timeout recovery -> BOOTSEL; storage retained\n"); + sleep_ms(20); + reset_usb_boot(0,0); + } + watchdog_enable(8000,false); +} + +bool native_hub_init(void) { + if (started || clock_get_hz(clk_sys) != 240000000u) return false; + bank_lock = spin_lock_instance(spin_lock_claim_unused(true)); + memset(devices,0,sizeof(devices)); memset(ports,0,sizeof(ports)); + snprintf(root_serial,sizeof(root_serial),"switch-pico-"); + pico_get_unique_board_id_string(root_serial+12,sizeof(root_serial)-12); + reset_block(RESETS_RESET_USBCTRL_BITS); unreset_block_wait(RESETS_RESET_USBCTRL_BITS); + memset(usb_dpram,0,USB_DPRAM_SIZE); + active_device = 0; addresses[0] = 0; addresses[1] = addresses[2] = NONE; default_device = 0; + usb_hw->muxing = USB_USB_MUXING_TO_PHY_BITS | USB_USB_MUXING_SOFTCON_BITS | USB_USB_MUXING_USBPHY_AS_GPIO_BITS; + sio_hw->gpio_hi_oe_clr = SIO_GPIO_HI_IN_USB_DP_BITS | SIO_GPIO_HI_IN_USB_DM_BITS; + hw_set_bits(&usb_hw->phy_direct,USB_USBPHY_DIRECT_DP_PULLUP_EN_BITS); + hw_set_bits(&usb_hw->phy_direct_override,USB_USBPHY_DIRECT_OVERRIDE_DP_PULLUP_EN_OVERRIDE_EN_BITS); + usb_hw->pwr = USB_USB_PWR_VBUS_DETECT_BITS | USB_USB_PWR_VBUS_DETECT_OVERRIDE_EN_BITS; + usb_hw->main_ctrl = USB_MAIN_CTRL_CONTROLLER_EN_BITS; + usb_hw->sie_ctrl = USB_SIE_CTRL_EP0_INT_1BUF_BITS; + usb_hw->inte = USB_INTS_BUFF_STATUS_BITS | USB_INTS_BUS_RESET_BITS | USB_INTS_SETUP_REQ_BITS | + USB_INTS_DEV_SUSPEND_BITS | USB_INTS_DEV_RESUME_FROM_HOST_BITS; + probe_router_init(clock_get_hz(clk_sys)); probe_router_set_phase(NATIVE_HUB_SAMPLE_PHASE); + multicore_launch_core1(probe_router_core1); + uint32_t deadline = time_us_32()+100000; + probe_router_stats observer; + do { probe_router_snapshot(&observer); if (observer.ready) break; tight_loop_contents(); } + while ((int32_t)(time_us_32()-deadline) < 0); + if (!observer.ready) return false; + publish_addresses(); probe_router_enable(true); + irq_set_exclusive_handler(USBCTRL_IRQ,usb_interrupt); + irq_set_priority(USBCTRL_IRQ,0); + irq_set_enabled(USBCTRL_IRQ,true); + hw_set_bits(&usb_hw->sie_ctrl,USB_SIE_CTRL_PULLUP_EN_BITS); + watchdog_enable(8000,false); started = true; startup_time = time_us_32(); + probe_debug_printf("[NATIVE_HUB] stock USB, SIO phase=%u, 240MHz; hub2068 R2066 L2067; isolated EP0/1/2 banks\n",NATIVE_HUB_SAMPLE_PHASE); + return true; +} +void native_hub_task(void) { + if (!started) return; + if (failed) { + printf("[NATIVE_HUB] transport failed closed; entering BOOTSEL without erasing storage\n"); + reset_usb_boot(0,0); + return; + } + while (event_tail != event_head) { + event_t event = events[event_tail]; + __dmb(); event_tail = (event_tail+1u)%EVENTS; + if (event.kind == 3) reset_bus(); + else if (event.device < DEVICES && event.kind == 1) setup_request(&event); + else if (event.device < DEVICES && event.kind == 2) transfer_complete(&event); + } + restore_selected_bank(); + uint32_t now = time_us_32(); + if (usb_hw->ep_nak_stall_status & (1u << 30)) { + ++root_naks; + hw_clear_bits(&usb_hw->ep_nak_stall_status,1u << 30); + } + for (unsigned p = 0; p < 2; ++p) { + if ((ports[p].status & RESET) && (int32_t)(now-ports[p].deadline) >= 0) { + if (default_device != NONE && default_device != p+1) { failed = true; return; } + ports[p].status = (ports[p].status & ~RESET) | ENABLE; + ports[p].change |= C_RESET; addresses[p+1] = 0; default_device = p+1; publish_addresses(); + } + } + if (devices[0].configuration && !devices[0].ep[2].busy && !devices[0].ep[2].halted) { + uint8_t changed = (ports[0].change ? 2u : 0u) | (ports[1].change ? 4u : 0u); + if (changed) { + endpoint_t* ep = &devices[0].ep[2]; ep->data[0] = changed; + ep->length = 1; ep->sent = 0; ep->busy = ep->flush = true; ep->zlp = false; + transmit_next(0,2); + } + } + static uint32_t last_log; + if ((uint32_t)(now-last_log) >= 1000000u) { + last_log = now; + probe_debug_printf("[NATIVE_HUB] addr=%u/%u/%u cfg=%u/%u/%u setup=%"PRIu32"/%"PRIu32"/%"PRIu32 + " in=%"PRIu32"/%"PRIu32" out=%"PRIu32"/%"PRIu32" switch=%"PRIu32" busy=%"PRIu32" slow=%"PRIu32" late=%"PRIu32"/%"PRIu32"\n", + addresses[0],addresses[1],addresses[2],devices[0].configuration,devices[1].configuration,devices[2].configuration, + setup_count[0],setup_count[1],setup_count[2],input_count[1],input_count[2],output_count[1],output_count[2],switches,missed_switches,slow_switches,minimum_lateness,maximum_lateness); + probe_debug_printf("[HUB_SIE] owner=%u sie=%08"PRIx32" nak=%08"PRIx32 + " txerr=%08"PRIx32" rxerr=%08"PRIx32" ep1=%08"PRIx32"/%08"PRIx32" hidbusy=%u/%u\n", + active_device,usb_hw->sie_status,usb_hw->ep_nak_stall_status, + usb_hw->ep_tx_error,usb_hw->ep_rx_error,endpoint_regs()[0],buffer_regs()[2], + devices[1].ep[2].busy,devices[2].ep[2].busy); + probe_debug_printf("[HUB_ROUTE] hits=%"PRIu32"/%"PRIu32"/%"PRIu32 + " lock=%"PRIu32" blocked=%08"PRIx32"/%08"PRIx32" rootnak=%"PRIu32"\n", + token_hits[0],token_hits[1],token_hits[2],missed_lock, + blocked_buffers,blocked_sie,root_naks); + } + watchdog_update(); + // This qualification firmware must remain recoverable if the hub never + // enumerates. A normal reset after successful enumeration is unaffected. + if (!root_configured_once && (uint32_t)(time_us_32()-startup_time) >= 15000000u) + reset_usb_boot(0,0); +} diff --git a/src/firmware/usb/native_hub/native_hub.h b/src/firmware/usb/native_hub/native_hub.h new file mode 100644 index 0000000..f2fdd78 --- /dev/null +++ b/src/firmware/usb/native_hub/native_hub.h @@ -0,0 +1,44 @@ +#pragma once + +#include +#include +#include "tusb.h" + +#ifdef __cplusplus +extern "C" { +#endif + +// Native SIE hub: device slot 0 is the hub; controller instances 0/1 map to +// device slots 1/2 (right/left). The caller owns Bluetooth on Core 0; this +// transport owns Core 1. No external USB wiring is used. +// Recover a timed-out test firmware to BOOTSEL instead of rebooting forever. +void native_hub_startup_guard(void); +bool native_hub_init(void); +void native_hub_task(void); +bool native_hub_mounted(uint8_t instance); +bool native_hub_suspended(uint8_t instance); +bool native_hub_hid_ready(uint8_t instance); +bool native_hub_hid_report(uint8_t instance, uint8_t report_id, + const void* data, uint16_t length); +uint32_t native_hub_vendor_write_available(uint8_t instance); +uint32_t native_hub_vendor_write(uint8_t instance, const void* data, uint32_t length); +uint32_t native_hub_vendor_write_flush(uint8_t instance); +// OUT packets are delivered directly and once through tud_vendor_rx_cb; +// there is no second receive FIFO to drain in this backend. +bool native_hub_control_xfer(uint8_t device_slot, + const tusb_control_request_t* request, + void* buffer, uint16_t length); +bool native_hub_control_status(uint8_t device_slot, + const tusb_control_request_t* request); + +// Supplied by the existing native Joy-Con protocol engine. Each returned +// descriptor is the standalone model, with interfaces 0/1 and EPs 1/2. +const uint8_t* native_joycon_device_descriptor(uint8_t instance); +const uint8_t* native_joycon_configuration_descriptor(uint8_t instance); +const uint16_t* native_joycon_string_descriptor(uint8_t instance, uint8_t index, + uint16_t language_id); +void native_joycon_usb_reset(uint8_t instance); + +#ifdef __cplusplus +} +#endif diff --git a/tests/switch2_mouse_bridge_test.cpp b/tests/switch2_mouse_bridge_test.cpp index 8ccc6be..7bbc496 100644 --- a/tests/switch2_mouse_bridge_test.cpp +++ b/tests/switch2_mouse_bridge_test.cpp @@ -1,4 +1,5 @@ #include "controller_input.h" +#include "model.h" #include "input/bluepad32_input_backend.h" #include "input/switch2_mouse_capture.h" #include "platform/pico/bootsel_pairing_button.h" @@ -23,6 +24,8 @@ 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}; +static constexpr uint16_t other_product_id = SWITCH2_PROBE_JOYCON_LEFT ? 0x2066 : 0x2067; +static constexpr uint8_t other_report_id = SWITCH2_PROBE_JOYCON_LEFT ? 8 : 7; void system_clock_initialize() {} void bluepad32_input_backend_init() { stage = 1; } void controller_profile_runtime_reset() {} @@ -42,8 +45,8 @@ 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. + // The model-specific length declares 30/40 packed motion bytes; 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((i * 37 + counter) % 255 + 1); report[0] = counter; @@ -57,12 +60,13 @@ static NativeReport native_report(uint8_t counter, uint8_t motion_length, report[11] = static_cast(y); report[12] = static_cast(y) >> 8; report[13] = 0x1b; - report[15] = motion_length; + report[PROBE_IMU_LENGTH_OFFSET] = 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 product_id = PROBE_JOYCON_PID, + uint8_t report_id = PROBE_NATIVE_REPORT_ID, uint16_t length = 63) { assert(length <= report.size()); switch_pico_switch2_mouse_report(product_id, address, report_id, report.data(), @@ -70,33 +74,34 @@ static void emit(const NativeReport& report, const uint8_t* address = source_add } static void disconnect(const uint8_t* address = source_address, - uint16_t product_id = 0x2066) { + uint16_t product_id = PROBE_JOYCON_PID) { switch_pico_switch2_mouse_report(product_id, address, 0, nullptr, 0, static_cast(now)); } -static probe_controller_input poll(uint32_t timestamp = static_cast(now)) { +static probe_controller_input poll(uint32_t timestamp = static_cast(now), + uint8_t instance = 0) { probe_controller_input input{}; - probe_controller_input_poll(timestamp, &input); + probe_controller_input_poll(instance, timestamp, &input); return input; } static uint32_t expect_report(const NativeReport& expected, - uint32_t timestamp = static_cast(now)) { + uint32_t timestamp = static_cast(now), + uint8_t instance = 0) { NativeReport actual; actual.fill(0xa5); const uint32_t serial = - probe_controller_input_peek_native_report(timestamp, actual.data()); + probe_controller_input_peek_native_report(instance, timestamp, actual.data()); assert(serial != 0 && actual == expected); return serial; } -static void expect_empty() { +static void expect_empty(uint8_t instance = 0) { NativeReport actual; actual.fill(0xa5); const auto untouched = actual; - assert(probe_controller_input_peek_native_report( - static_cast(now), actual.data()) == 0); + assert(probe_controller_input_peek_native_report(instance, static_cast(now), actual.data()) == 0); assert(actual == untouched); } @@ -111,15 +116,15 @@ static void expect_inactive(const probe_controller_input& input) { 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); + probe_controller_input_set_native_stream(0, true); expect_empty(); - assert(!probe_controller_input_commit_native_report(1)); + assert(!probe_controller_input_commit_native_report(0, 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); + probe_controller_input_set_native_stream(0, true); const auto report = native_report(0x31, 30, -6, 9); emit(report); expect_empty(); @@ -137,21 +142,21 @@ static void test_startup_pairing_and_stream_gate() { 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); + probe_controller_input_set_native_stream(0, 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)); + probe_controller_input_set_native_stream(0, false); + assert(!probe_controller_input_commit_native_report(0, pending)); emit(report); // Selected input continues updating while native USB is gated. assert(poll().active); expect_empty(); - probe_controller_input_set_native_stream(true); + probe_controller_input_set_native_stream(0, true); expect_empty(); emit(report); const uint32_t resumed = expect_report(report); assert(resumed > pending); - assert(probe_controller_input_commit_native_report(resumed)); + assert(probe_controller_input_commit_native_report(0, resumed)); expect_empty(); } @@ -170,26 +175,26 @@ static void test_opaque_fidelity_order_and_retry() { ++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(!probe_controller_input_commit_native_report(0, last_serial)); + assert(!probe_controller_input_commit_native_report(0, 0)); + probe_controller_input_set_native_stream(0, 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)); + assert(probe_controller_input_commit_native_report(0, first_serial)); + assert(!probe_controller_input_commit_native_report(0, first_serial)); const uint32_t second_serial = expect_report(repeated); assert(second_serial > first_serial); - assert(probe_controller_input_commit_native_report(second_serial)); + assert(probe_controller_input_commit_native_report(0, 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(!probe_controller_input_commit_native_report(0, second_serial)); assert(expect_report(repeated) == third_serial); - assert(probe_controller_input_commit_native_report(third_serial)); + assert(probe_controller_input_commit_native_report(0, third_serial)); assert(expect_report(last) == last_serial); - assert(probe_controller_input_commit_native_report(last_serial)); + assert(probe_controller_input_commit_native_report(0, last_serial)); expect_empty(); - assert(!probe_controller_input_commit_native_report(last_serial)); + assert(!probe_controller_input_commit_native_report(0, last_serial)); expect_empty(); // No cached duplicate report when the source has not advanced. } @@ -203,17 +208,20 @@ static void test_selected_source_isolation_and_reconnect() { 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); + emit(unrelated, source_address, other_product_id, other_report_id); + emit(unrelated, source_address, PROBE_JOYCON_PID, other_report_id); + emit(unrelated, source_address, PROBE_JOYCON_PID, 5); + emit(unrelated, source_address, PROBE_JOYCON_PID, 0xc0, 12); + emit(unrelated, source_address, PROBE_JOYCON_PID, PROBE_NATIVE_REPORT_ID, 62); + emit(unrelated, source_address, PROBE_JOYCON_PID, 0, 1); // Not a teardown. uint8_t oversized[64]; memcpy(oversized, unrelated.data(), unrelated.size()); oversized[63] = 0x5a; - switch_pico_switch2_mouse_report(0x2066, source_address, 8, oversized, + switch_pico_switch2_mouse_report(PROBE_JOYCON_PID, source_address, + PROBE_NATIVE_REPORT_ID, oversized, sizeof(oversized), static_cast(now)); disconnect(other_address); - disconnect(source_address, 0x2067); + disconnect(source_address, other_product_id); const auto isolated = poll(); assert(isolated.active && isolated.serial == selected.serial); assert(isolated.mouse_epoch == selected.mouse_epoch); @@ -222,9 +230,9 @@ static void test_selected_source_isolation_and_reconnect() { 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(probe_controller_input_commit_native_report(0, first_serial)); assert(expect_report(second) == second_serial); - assert(probe_controller_input_commit_native_report(second_serial)); + assert(probe_controller_input_commit_native_report(0, second_serial)); expect_empty(); // Unrelated ring entries neither evict nor enter the FIFO. emit(first); @@ -234,10 +242,10 @@ static void test_selected_source_isolation_and_reconnect() { 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(!probe_controller_input_commit_native_report(0, disconnected_serial)); assert(expect_report(second) == reconnected.serial); assert(reconnected.serial > disconnected_serial); - assert(probe_controller_input_commit_native_report(reconnected.serial)); + assert(probe_controller_input_commit_native_report(0, reconnected.serial)); expect_empty(); emit(first); @@ -246,35 +254,133 @@ static void test_selected_source_isolation_and_reconnect() { for (unsigned i = 0; i < 30; ++i) emit(unrelated, other_address); expect_inactive(poll()); expect_empty(); - assert(!probe_controller_input_commit_native_report(pending)); + assert(!probe_controller_input_commit_native_report(0, 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)); + assert(probe_controller_input_commit_native_report(0, resumed_serial)); emit(first); const uint32_t old_source = expect_report(first); emit(unrelated, other_address); - switch2_mouse_capture_select_input(other_address); + switch2_mouse_capture_select_input(0, other_address, PROBE_JOYCON_PID); + expect_inactive(poll()); expect_empty(); - assert(!probe_controller_input_commit_native_report(old_source)); - probe_controller_input_set_native_stream(true); + assert(!probe_controller_input_commit_native_report(0, old_source)); + probe_controller_input_set_native_stream(0, 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); + switch2_mouse_capture_select_input(0, source_address, PROBE_JOYCON_PID); + expect_inactive(poll()); + probe_controller_input_set_native_stream(0, true); expect_empty(); - assert(!probe_controller_input_commit_native_report(new_source)); + assert(!probe_controller_input_commit_native_report(0, new_source)); emit(second); const uint32_t restored = expect_report(second); assert(restored > new_source); - assert(probe_controller_input_commit_native_report(restored)); + assert(probe_controller_input_commit_native_report(0, restored)); +} + +static void test_side_switch_and_sample_ownership() { + now = 500; + const auto first = native_report(0x71, 30); + auto opposite = native_report(0x72, 40); + opposite[SWITCH2_PROBE_JOYCON_LEFT ? 15 : 14] = 40; + emit(first); + const uint32_t old_packet = expect_report(first); + assert(poll().active); + uint64_t old_cue = 0; + assert(probe_controller_input_play_sample(0, 3, &old_cue) && old_cue != 0); + uint64_t taken = 0; + uint8_t sample = 0; + // Unrelated callers cannot take a cue, even with a timestamp that would + // otherwise expire it. Ownership is checked before mutating its lifetime. + assert(!switch_pico_switch2_sample_take(other_product_id, source_address, + now + 2000, &sample, &taken)); + assert(!switch_pico_switch2_sample_take(PROBE_JOYCON_PID, other_address, + now + 2000, &sample, &taken)); + assert(probe_controller_input_sample_result(0, old_cue, now) == 0); + assert(!switch_pico_switch2_sample_result(PROBE_JOYCON_PID, source_address, + old_cue, 1, now)); // Not dispatched. + assert(switch_pico_switch2_sample_take(PROBE_JOYCON_PID, source_address, + now, &sample, &taken)); + assert(taken == old_cue && sample == 3); + switch2_mouse_capture_select_input(0, source_address, 0x2069); // Invalid PID. + switch2_mouse_capture_select_input(0, nullptr, PROBE_JOYCON_PID); + assert(expect_report(first) == old_packet && poll().active); + assert(probe_controller_input_sample_result(0, old_cue, now) == 0); + + // Same address, different side is still a new source. Native and cue + // tokens from the prior selection cannot acknowledge or consume it. + switch2_mouse_capture_select_input(0, source_address, other_product_id); + expect_empty(); + expect_inactive(poll()); + assert(!probe_controller_input_commit_native_report(0, old_packet)); + assert(probe_controller_input_sample_result(0, old_cue, now) == -1); + uint64_t new_cue = 0; + assert(!probe_controller_input_play_sample(0, 4, &new_cue)); + emit(first); + emit(opposite, source_address, other_product_id, PROBE_NATIVE_REPORT_ID); + expect_inactive(poll()); + emit(opposite, source_address, other_product_id, other_report_id); + assert(poll().active); + expect_empty(); // Selection disabled native output even for valid input. + probe_controller_input_set_native_stream(0, true); + expect_empty(); + emit(opposite, source_address, other_product_id, other_report_id); + const uint32_t new_packet = expect_report(opposite); + assert(new_packet > old_packet); + assert(probe_controller_input_play_sample(0, 4, &new_cue) && new_cue > old_cue); + assert(!switch_pico_switch2_sample_result(PROBE_JOYCON_PID, source_address, + old_cue, 1, now + 2000)); + assert(switch_pico_switch2_sample_take(other_product_id, source_address, + now, &sample, &taken)); + assert(sample == 4 && taken == new_cue); + assert(!switch_pico_switch2_sample_result(PROBE_JOYCON_PID, source_address, + new_cue, 1, now + 2000)); + assert(!switch_pico_switch2_sample_result(other_product_id, other_address, + new_cue, -1, now + 2000)); + assert(!switch_pico_switch2_sample_result(other_product_id, source_address, + old_cue, 1, now + 2000)); + assert(probe_controller_input_sample_result(0, old_cue, now + 2000) == -1); + disconnect(source_address); + assert(poll().active && expect_report(opposite) == new_packet); + assert(probe_controller_input_sample_result(0, new_cue, now) == 0); + assert(switch_pico_switch2_sample_result(other_product_id, source_address, + new_cue, 1, now)); + disconnect(source_address, other_product_id); + expect_inactive(poll()); + expect_empty(); + assert(!probe_controller_input_commit_native_report(0, new_packet)); + assert(probe_controller_input_sample_result(0, new_cue, now) == -1); + + emit(opposite, source_address, other_product_id, other_report_id); + assert(probe_controller_input_play_sample(0, 5, &new_cue) && new_cue > old_cue); + old_cue = new_cue; + switch2_mouse_capture_select_input(0, other_address, other_product_id); + expect_inactive(poll()); + assert(probe_controller_input_sample_result(0, old_cue, now) == -1); + assert(!probe_controller_input_play_sample(0, 6, &new_cue)); + emit(opposite, other_address, other_product_id, other_report_id); + assert(probe_controller_input_play_sample(0, 6, &new_cue) && new_cue > old_cue); + assert(switch_pico_switch2_sample_take(other_product_id, other_address, + now, &sample, &taken)); + assert(taken == new_cue && sample == 6); + assert(!switch_pico_switch2_sample_result(other_product_id, source_address, + old_cue, 1, now)); + assert(switch_pico_switch2_sample_result(other_product_id, other_address, + new_cue, 1, now)); + assert(probe_controller_input_sample_result(0, new_cue, now) == 1); + assert(probe_controller_input_sample_result(0, new_cue, now) == -1); + switch2_mouse_capture_select_input(0, source_address, PROBE_JOYCON_PID); + probe_controller_input_set_native_stream(0, true); + expect_empty(); } static void test_bounded_overflow() { @@ -290,16 +396,16 @@ static void test_bounded_overflow() { 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)); + assert(!probe_controller_input_commit_native_report(0, 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)); + assert(probe_controller_input_commit_native_report(0, newest_serial)); const uint32_t following_serial = expect_report(following); assert(following_serial > newest_serial); - assert(probe_controller_input_commit_native_report(following_serial)); + assert(probe_controller_input_commit_native_report(0, following_serial)); expect_empty(); // Overflow discarded all prior history, not merely its head. } @@ -315,7 +421,7 @@ static void test_expiry_and_wrapping_clock() { emit(fresh, other_address); // Wrong-source traffic cannot refresh the timeout. expect_inactive(poll()); expect_empty(); - assert(!probe_controller_input_commit_native_report(expired)); + assert(!probe_controller_input_commit_native_report(0, expired)); ++now; emit(first); const uint32_t stale_head = expect_report(first); @@ -324,11 +430,11 @@ static void test_expiry_and_wrapping_clock() { ++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)); + assert(!probe_controller_input_commit_native_report(0, stale_head)); emit(fresh); const uint32_t resumed = expect_report(fresh); assert(resumed > stale_head); - assert(probe_controller_input_commit_native_report(resumed)); + assert(probe_controller_input_commit_native_report(0, resumed)); expect_empty(); now = static_cast(UINT32_MAX) - 100; @@ -344,20 +450,139 @@ static void test_expiry_and_wrapping_clock() { ++now; expect_inactive(poll()); expect_empty(); - assert(!probe_controller_input_commit_native_report(wrapped)); + assert(!probe_controller_input_commit_native_report(0, 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)); + assert(probe_controller_input_commit_native_report(0, after_wrap)); expect_empty(); } +#if SWITCH2_PROBE_COMPOSITE +static void test_simultaneous_sources() { + const uint8_t left_address[] = {0x98,0xe2,0x55,7,0xe9,0xd3}; + now = 10000; + disconnect(); + const auto right = native_report(0x31, 30, 7, -9); + auto left = native_report(0x62, 40, -13, 17); + left[probe_model_imu_length_offset(1)] = 40; + probe_controller_input_set_native_stream(0, true); + probe_controller_input_set_native_stream(1, true); + expect_empty(0); + expect_empty(1); + emit(right); + const uint32_t r0 = expect_report(right); + emit(left, left_address, probe_model_pid(1), probe_model_report_id(1)); + const uint32_t l0 = expect_report(left, now, 1); + emit(right); + const uint32_t r1 = poll().serial; + emit(left, left_address, probe_model_pid(1), probe_model_report_id(1)); + const auto li = poll(now, 1); + const auto ri = poll(); + assert(r0 < l0 && l0 < r1 && r1 < li.serial); + assert(ri.mouse_epoch == r0 && li.mouse_epoch == l0); + assert(ri.mouse_total_x == 14 && ri.mouse_total_y == -18); + assert(li.mouse_total_x == -26 && li.mouse_total_y == 34); + // R is already owned: selecting it for L must not duplicate or steal it. + switch2_mouse_capture_select_input(1, source_address, probe_model_pid(0)); + assert(expect_report(right) == r0); + assert(expect_report(left, now, 1) == l0); + assert(!probe_controller_input_commit_native_report(1, r0)); + assert(!probe_controller_input_commit_native_report(0, l0)); + assert(probe_controller_input_commit_native_report(1, l0)); + assert(expect_report(left, now, 1) == li.serial); + assert(probe_controller_input_commit_native_report(1, li.serial)); + expect_empty(1); + assert(expect_report(right) == r0); // L consumption never moves stalled R. + + // R overflow drops only its own backlog; L's retry remains byte-identical. + emit(left, left_address, probe_model_pid(1), probe_model_report_id(1)); + const uint32_t left_retry = expect_report(left, now, 1); + for (unsigned i = 0; i < 31; ++i) emit(right); + assert(!probe_controller_input_commit_native_report(0, r0)); + assert(expect_report(left, now, 1) == left_retry); + const uint32_t r2 = expect_report(right); + assert(r2 > left_retry); + + uint64_t rcue, lcue, taken; + uint8_t sample; + assert(probe_controller_input_play_sample(0, 3, &rcue)); + assert(probe_controller_input_play_sample(1, 5, &lcue) && lcue > rcue); + assert(switch_pico_switch2_sample_take(probe_model_pid(0), source_address, now, &sample, &taken)); + assert(sample == 3 && taken == rcue); + assert(switch_pico_switch2_sample_take(probe_model_pid(1), left_address, now, &sample, &taken)); + assert(sample == 5 && taken == lcue); + assert(!switch_pico_switch2_sample_result(probe_model_pid(1), left_address, rcue, 1, now + 2000)); + assert(!switch_pico_switch2_sample_result(probe_model_pid(0), source_address, lcue, -1, now + 2000)); + assert(probe_controller_input_sample_result(0, lcue, now + 2000) == -1); + assert(probe_controller_input_sample_result(1, rcue, now + 2000) == -1); + assert(probe_controller_input_sample_result(0, rcue, now) == 0); + assert(probe_controller_input_sample_result(1, lcue, now) == 0); + + probe_controller_input_set_native_stream(0, false); + probe_controller_input_cancel_sample(0); + assert(probe_controller_input_sample_result(0, rcue, now) == -1); + assert(probe_controller_input_sample_result(1, lcue, now) == 0); + assert(expect_report(left, now, 1) == left_retry); + assert(switch_pico_switch2_sample_result(probe_model_pid(1), left_address, lcue, 1, now)); + assert(probe_controller_input_sample_result(1, lcue, now) == 1); + assert(probe_controller_input_sample_result(1, lcue, now) == -1); + + // R selection/disconnect cannot revoke L's pending packet or cue. + assert(probe_controller_input_play_sample(1, 6, &lcue)); + switch2_mouse_capture_select_input(0, other_address, probe_model_pid(0)); + disconnect(source_address, probe_model_pid(0)); + assert(expect_report(left, now, 1) == left_retry); + assert(probe_controller_input_sample_result(1, lcue, now) == 0); + assert(poll(now, 1).mouse_epoch == l0); + switch2_mouse_capture_select_input(0, source_address, probe_model_pid(0)); + probe_controller_input_set_native_stream(0, true); + emit(right); + const uint32_t right_retry = expect_report(right); + assert(probe_controller_input_play_sample(0, 7, &rcue) && rcue > lcue); + disconnect(left_address, probe_model_pid(1)); + expect_empty(1); + expect_inactive(poll(now, 1)); + assert(probe_controller_input_sample_result(1, lcue, now) == -1); + assert(expect_report(right) == right_retry); + assert(probe_controller_input_sample_result(0, rcue, now) == 0); + assert(switch_pico_switch2_sample_take(probe_model_pid(0), source_address, now, &sample, &taken)); + assert(sample == 7 && taken == rcue); + assert(switch_pico_switch2_sample_result(probe_model_pid(0), source_address, rcue, 1, now)); + assert(probe_controller_input_sample_result(0, rcue, now) == 1); + + // Reconnection creates a distinct L epoch and does not replay its old queue. + emit(left, left_address, probe_model_pid(1), probe_model_report_id(1)); + const auto resumed_left = poll(now, 1); + assert(resumed_left.mouse_epoch != l0 && resumed_left.mouse_total_x == -13); + assert(!probe_controller_input_commit_native_report(1, left_retry)); + assert(probe_controller_input_commit_native_report(1, expect_report(left, now, 1))); + assert(probe_controller_input_commit_native_report(0, right_retry)); + expect_empty(0); + expect_empty(1); + + // A refreshed L packet cannot refresh R's independent source deadline. + now += 499; + emit(left, left_address, probe_model_pid(1), probe_model_report_id(1)); + ++now; + expect_inactive(poll()); + assert(poll(now, 1).active); + assert(probe_controller_input_commit_native_report(1, expect_report(left, now, 1))); + expect_empty(0); + expect_empty(1); +} +#endif + int main() { test_startup_pairing_and_stream_gate(); test_opaque_fidelity_order_and_retry(); test_selected_source_isolation_and_reconnect(); + test_side_switch_and_sample_ownership(); test_bounded_overflow(); test_expiry_and_wrapping_clock(); +#if SWITCH2_PROBE_COMPOSITE + test_simultaneous_sources(); +#endif puts("Native packet fidelity, FIFO retry/order, source barriers, overflow, expiry and pairing passed"); } diff --git a/tests/switch2_parser_native_test.c b/tests/switch2_parser_native_test.c index 764fffe..0f6260b 100644 --- a/tests/switch2_parser_native_test.c +++ b/tests/switch2_parser_native_test.c @@ -167,7 +167,7 @@ void uni_hid_device_set_ready(uni_hid_device_t* d) { uni_hid_parser_switch2_setup(d); } bool uni_hid_device_set_ready_complete(uni_hid_device_t* d) { - assert(d->conn.connected && connected_events == 1); + assert(d->conn.connected && connected_events > ready); ++ready; d->conn.state = UNI_BT_CONN_STATE_DEVICE_READY; return true; diff --git a/tests/switch2_usb_probe_protocol_test.c b/tests/switch2_usb_probe_protocol_test.c index d8643fa..312e900 100644 --- a/tests/switch2_usb_probe_protocol_test.c +++ b/tests/switch2_usb_probe_protocol_test.c @@ -2,35 +2,286 @@ #include #include #include "protocol.h" +#include "descriptors.h" +#include "memory.h" + +static const uint32_t common_button_bits[2][16] = { + { + 0x000004, 0x000008, 0x000001, 0x000002, 0x000040, 0x000080, 0x000200, 0x000400, + 0x001000, 0, 0, 0, 0x004000, 0, 0x000010, 0x000020, + }, + { + 0x010000, 0x040000, 0x080000, 0x020000, 0x400000, 0x800000, 0x000100, 0x000800, + 0x002000, 0, 0, 0, 0, 0, 0x100000, 0x200000, + }, +}; + +static void initialize(probe_protocol_state* state) { + static const uint8_t command[] = { + 0x03, 0x91, 0, 0x0d, 0, 8, 0, 0, 1, 0, 1, 2, 3, 4, 5, 6, + }; + uint8_t reply[12]; + assert(probe_protocol_command(state, command, sizeof(command), reply, sizeof(reply), NULL) == 12); +} + +static void set_features(probe_protocol_state* state, uint8_t subcommand, uint8_t flags) { + const uint8_t command[] = {0x0c, 0x91, 0, subcommand, 0, 4, 0, 0, flags, 0, 0, 0}; + uint8_t reply[12]; + assert(probe_protocol_command(state, command, sizeof(command), reply, sizeof(reply), NULL) == 12); +} + +static void select_report(probe_protocol_state* state, uint8_t report_id) { + const uint8_t command[] = {0x03, 0x91, 0, 0x0a, 0, 4, 0, 0, report_id, 0, 0, 0}; + uint8_t reply[8]; + assert(probe_protocol_command(state, command, sizeof(command), reply, sizeof(reply), NULL) == 8); +} + +static void test_descriptors(void) { + const uint16_t product_id = probe_device_descriptor[10] | + ((uint16_t)probe_device_descriptor[11] << 8); + assert(product_id == (SWITCH2_PROBE_JOYCON_LEFT ? 0x2067 : 0x2066)); + assert(probe_configuration_descriptor[2] == sizeof(probe_configuration_descriptor)); + assert(probe_configuration_descriptor[4] == 2 * PROBE_CONTROLLER_COUNT); + unsigned interface_count = 0, endpoint_count = 0; + unsigned interface = 0, seen_endpoints = 0; + for (size_t offset = 9; offset < sizeof(probe_configuration_descriptor);) { + const uint8_t* descriptor = probe_configuration_descriptor + offset; + assert(descriptor[0] >= 2); + assert(offset + descriptor[0] <= sizeof(probe_configuration_descriptor)); + if (descriptor[1] == 4) { + assert(descriptor[0] == 9); + interface = descriptor[2]; + assert(interface == interface_count++); + assert(descriptor[4] == 2); + assert(descriptor[5] == (interface % 2 ? 0xff : 3)); + assert(descriptor[8] == 5 + interface); + } else if (descriptor[1] == 5) { + assert(descriptor[0] == 7); + const unsigned endpoint = descriptor[2] & 0x0f; + assert(endpoint == interface + 1); + const unsigned bit = endpoint + ((descriptor[2] & 0x80) ? 8 : 0); + assert(!(seen_endpoints & (1u << bit))); + seen_endpoints |= 1u << bit; + assert(descriptor[3] == (interface % 2 ? 2 : 3)); + ++endpoint_count; + } + offset += descriptor[0]; + } + assert(interface_count == 2 * PROBE_CONTROLLER_COUNT); + assert(endpoint_count == 4 * PROBE_CONTROLLER_COUNT); + // Read HID short items as a host would: each function advertises only its + // own native report plus common 05, with sizes matching report generation. + for (uint8_t instance = 0; instance < PROBE_CONTROLLER_COUNT; ++instance) { + const uint8_t* descriptor = probe_hid_report_descriptors[instance]; + unsigned input_bits[256] = {0}, output_bits[256] = {0}; + unsigned report_id = 0, report_size = 0, report_count = 0; + for (size_t offset = 0; offset < sizeof(probe_hid_report_descriptors[instance]);) { + const uint8_t prefix = descriptor[offset++]; + assert(prefix != 0xfe); + const unsigned size = (prefix & 3) == 3 ? 4 : prefix & 3; + assert(offset + size <= sizeof(probe_hid_report_descriptors[instance])); + uint32_t value = 0; + for (unsigned i = 0; i < size; ++i) + value |= (uint32_t)descriptor[offset++] << (8 * i); + switch (prefix & 0xfc) { + case 0x74: report_size = value; break; + case 0x94: report_count = value; break; + case 0x84: report_id = value; assert(report_id < 256); break; + case 0x80: input_bits[report_id] += report_size * report_count; break; + case 0x90: output_bits[report_id] += report_size * report_count; break; + } + } + const bool is_left = SWITCH2_PROBE_COMPOSITE ? instance == 1 : SWITCH2_PROBE_JOYCON_LEFT; + probe_protocol_state state; + probe_protocol_reset(&state, is_left); + initialize(&state); + uint8_t report[PROBE_INPUT_SIZE]; + for (unsigned id = 0; id < 256; ++id) { + const size_t expected = (id == 5 || id == (is_left ? 7u : 8u)) ? sizeof(report) : 0; + assert(input_bits[id] == expected * 8u); + assert(probe_protocol_report(&state, (uint8_t)id, report, sizeof(report)) == expected); + assert(output_bits[id] == (id == 1 ? 63u * 8u : 0)); + } + } +} + +static void test_report_selection_and_reset(bool is_left) { + const uint8_t native_id = is_left ? 7 : 8, opposite_id = is_left ? 8 : 7; + probe_protocol_state state; + probe_protocol_reset(&state, is_left); + uint8_t report[PROBE_INPUT_SIZE]; + assert(probe_protocol_report(&state, native_id, report, sizeof(report)) == 0); + initialize(&state); + assert(state.report_id == native_id); + assert(probe_protocol_report(&state, state.report_id, report, sizeof(report)) == sizeof(report)); + select_report(&state, 5); + assert(state.report_id == 5); + select_report(&state, opposite_id); + assert(state.report_id == 5); // Unsupported IDs are ACKed but ignored. + memset(report, 0xa5, sizeof(report)); + assert(probe_protocol_report(&state, opposite_id, report, sizeof(report)) == 0); + for (size_t i = 0; i < sizeof(report); ++i) assert(report[i] == 0xa5); + select_report(&state, native_id); + assert(state.report_id == native_id); + assert(probe_protocol_report(&state, native_id, report, sizeof(report) - 1) == 0); + state.report_counter = 0x12345678; + initialize(&state); // Repeated USB initialization must not rewind a live stream. + assert(probe_protocol_report(&state, native_id, report, sizeof(report)) == sizeof(report)); + assert(report[0] == 0x78); + select_report(&state, 5); + probe_protocol_reset(&state, is_left); + assert(probe_protocol_report(&state, native_id, report, sizeof(report)) == 0); + initialize(&state); + assert(state.report_id == native_id); + assert(probe_protocol_report(&state, state.report_id, report, sizeof(report)) == sizeof(report)); + assert(report[0] == 0); +} + +static void test_buttons_stick_and_feature_control(bool is_left) { + const uint8_t native_id = is_left ? 7 : 8; + const unsigned common_stick_offset = is_left ? 10 : 13; + const unsigned absent_stick_offset = is_left ? 13 : 10; + const unsigned common_rail_offset = is_left ? 6 : 4; + probe_protocol_state state; + probe_protocol_reset(&state, is_left); + initialize(&state); + set_features(&state, 2, 3); + set_features(&state, 4, 3); + state.controller_active = true; + const uint8_t stick[] = {0x23, 0x61, 0x45}; + const uint8_t calibrated_center[] = {0xff, 0x47, 0x81}; + const uint8_t neutral[] = {0, 8, 0x80}; + memcpy(state.controller_stick, stick, sizeof(stick)); + memcpy(state.stick_center, calibrated_center, sizeof(calibrated_center)); + uint8_t native[PROBE_INPUT_SIZE], common[PROBE_INPUT_SIZE]; + for (unsigned bit = 0; bit < 16; ++bit) { + memset(state.controller_buttons, 0, sizeof(state.controller_buttons)); + state.controller_buttons[bit / 8] = (uint8_t)(1u << (bit % 8)); + assert(probe_protocol_report(&state, native_id, native, sizeof(native)) == sizeof(native)); + assert(probe_protocol_report(&state, 5, common, sizeof(common)) == sizeof(common)); + const uint16_t expected_native = common_button_bits[is_left][bit] ? (uint16_t)(1u << bit) : 0; + assert((uint16_t)(native[2] | ((uint16_t)native[3] << 8)) == expected_native); + for (unsigned byte = 0; byte < 4; ++byte) + assert(common[4 + byte] == (uint8_t)(common_button_bits[is_left][bit] >> (8 * byte))); + assert(memcmp(native + 5, stick, sizeof(stick)) == 0); + assert(memcmp(common + common_stick_offset, stick, sizeof(stick)) == 0); + assert(memcmp(common + absent_stick_offset, neutral, sizeof(neutral)) == 0); + } + // Host feature disable gates the live controls without losing calibration. + set_features(&state, 5, 3); + assert(probe_protocol_report(&state, native_id, native, sizeof(native)) == sizeof(native)); + assert(native[2] == 0 && native[3] == 0); + assert(memcmp(native + 5, calibrated_center, sizeof(calibrated_center)) == 0); + assert(probe_protocol_report(&state, 5, common, sizeof(common)) == sizeof(common)); + assert(common[4] == 0 && common[5] == 0 && common[6] == 0 && common[7] == 0); + assert(memcmp(common + common_stick_offset, calibrated_center, sizeof(calibrated_center)) == 0); + set_features(&state, 4, 3); + state.controller_active = false; + assert(probe_protocol_report(&state, native_id, native, sizeof(native)) == sizeof(native)); + assert(native[2] == 0 && native[3] == 0); + assert(memcmp(native + 5, calibrated_center, sizeof(calibrated_center)) == 0); + state.test_rail_buttons = true; + assert(probe_protocol_report(&state, native_id, native, sizeof(native)) == sizeof(native)); + assert(native[2] == 0 && native[3] == 0xc0); + assert(probe_protocol_report(&state, 5, common, sizeof(common)) == sizeof(common)); + assert(common[common_rail_offset] == 0x30); + set_features(&state, 5, 1); + assert(probe_protocol_report(&state, native_id, native, sizeof(native)) == sizeof(native)); + assert(native[3] == 0); +} + +static void test_opaque_native_feature_gates(bool is_left) { + const unsigned imu_length_offset = is_left ? 14 : 15; +#ifdef SWITCH2_PROBE_ZERO_NATIVE_IMU_PAYLOAD + const unsigned imu_data_offset = imu_length_offset + 1; +#endif + probe_protocol_state state; + probe_protocol_reset(&state, is_left); + set_features(&state, 2, 0x17); + set_features(&state, 4, 0x17); + uint8_t source[PROBE_INPUT_SIZE], actual[PROBE_INPUT_SIZE], expected[PROBE_INPUT_SIZE]; + for (size_t i = 0; i < sizeof(source); ++i) source[i] = (uint8_t)(i * 3 + 1); + source[imu_length_offset] = 30; + memcpy(expected, source, sizeof(expected)); +#ifdef SWITCH2_PROBE_OMIT_NATIVE_IMU + memset(expected + imu_length_offset, 0, 41); +#elif defined(SWITCH2_PROBE_ZERO_NATIVE_IMU_PAYLOAD) + memset(expected + imu_data_offset, 0, 40); +#endif + memcpy(actual, source, sizeof(actual)); + probe_protocol_gate_native_report(&state, actual); + assert(memcmp(actual, expected, sizeof(actual)) == 0); + // IMU disable must leave mouse, NFC (R), and reserved tail bytes untouched. + set_features(&state, 5, 4); + memcpy(actual, source, sizeof(actual)); + memset(expected + imu_length_offset, 0, 41); + probe_protocol_gate_native_report(&state, actual); + assert(memcmp(actual, expected, sizeof(actual)) == 0); + set_features(&state, 4, 4); + set_features(&state, 5, 0x13); + memcpy(actual, source, sizeof(actual)); + memcpy(expected, source, sizeof(expected)); + memset(expected + 2, 0, 2); + memcpy(expected + 5, state.stick_center, sizeof(state.stick_center)); + memset(expected + 9, 0, 5); +#ifdef SWITCH2_PROBE_OMIT_NATIVE_IMU + memset(expected + imu_length_offset, 0, 41); +#elif defined(SWITCH2_PROBE_ZERO_NATIVE_IMU_PAYLOAD) + memset(expected + imu_data_offset, 0, 40); +#endif + probe_protocol_gate_native_report(&state, actual); + assert(memcmp(actual, expected, sizeof(actual)) == 0); +} 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); +typedef struct { + unsigned source_calls; + uint8_t expected_sample; + bool source_available; + uint64_t source_token; + bool storage_available; + unsigned saves; + uint8_t pairing_blob[PROBE_PAIRING_BLOB_SIZE]; +} controller_context; -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 bool play_sample(void* context, uint8_t sample_id, uint64_t* token) { + controller_context* controller = context; + ++controller->source_calls; + assert(sample_id == controller->expected_sample); + *token = controller->source_token; + return controller->source_available; +} + +static bool save_pairing(void* context, const uint8_t* blob, size_t size) { + controller_context* controller = context; + assert(size == sizeof(controller->pairing_blob)); + if (!controller->storage_available) return false; + memcpy(controller->pairing_blob, blob, size); + ++controller->saves; + return true; } 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; + const controller_context* controller = state->context; + const unsigned calls_before = controller->source_calls; assert(probe_protocol_command(state, command, length, reply, capacity, &token) == 0); assert(token == 0); - assert(source_calls == calls_before); + assert(controller->source_calls == calls_before); } -int main(void) { +static void test_sample_dispatch(void) { + controller_context controller = { + .expected_sample = 3, .source_available = true, + .source_token = UINT64_C(0x1234567800000001), + }; probe_protocol_state state; - probe_protocol_reset(&state); + probe_protocol_reset(&state, false); + state.context = &controller; state.play_sample = play_sample; // Each transport/header field and reserved payload byte is a dispatch gate. @@ -55,22 +306,22 @@ int main(void) { // Synchronous-only callers cannot accidentally acknowledge a sample. uint8_t reply[8]; - const unsigned calls_before = source_calls; + const unsigned calls_before = controller.source_calls; assert(probe_protocol_command(&state, sample_command, sizeof(sample_command), reply, sizeof(reply), NULL) == 0); - assert(source_calls == calls_before); + assert(controller.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; + controller.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; + controller.source_available = true; + controller.source_token = 0; token = UINT64_MAX; assert(probe_protocol_command(&state, sample_command, sizeof(sample_command), reply, sizeof(reply), &token) == 0); @@ -79,17 +330,17 @@ int main(void) { // 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); + controller.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]; + command[8] = controller.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(token == controller.source_token); assert(memcmp(reply, sample_ack, sizeof(sample_ack)) == 0); - ++source_token; + ++controller.source_token; } // Ordinary report selection retains its immediate, empty USB ACK. @@ -99,5 +350,265 @@ int main(void) { reply, sizeof(reply), &token) == sizeof(select_ack)); assert(token == 0); assert(memcmp(reply, select_ack, sizeof(select_ack)) == 0); +} + +static void test_interleaved_reports_and_features(void) { + probe_protocol_state right, left; + probe_protocol_reset(&right, false); + probe_protocol_reset(&left, true); + uint8_t reports[2][PROBE_INPUT_SIZE]; + initialize(&right); + assert(probe_protocol_report(&right, 8, reports[0], sizeof(reports[0])) == PROBE_INPUT_SIZE); + assert(probe_protocol_report(&left, 7, reports[1], sizeof(reports[1])) == 0); + initialize(&left); + select_report(&right, 5); + select_report(&left, 8); + select_report(&right, 7); + assert(right.report_id == 5 && left.report_id == 7); + set_features(&right, 2, 0x17); + set_features(&right, 4, 0x17); + set_features(&left, 2, 0x03); + set_features(&left, 4, 0x17); + right.controller_active = left.controller_active = true; + right.controller_buttons[0] = 0x84; // A + Plus. + left.controller_buttons[0] = 0x41; // Down + Minus. + const uint8_t sticks[2][3] = {{0x11, 0x22, 0x33}, {0x44, 0x55, 0x66}}; + const uint8_t neutral[] = {0, 8, 0x80}; + memcpy(right.controller_stick, sticks[0], 3); + memcpy(left.controller_stick, sticks[1], 3); + assert(probe_protocol_report(&right, right.report_id, reports[0], sizeof(reports[0])) == PROBE_INPUT_SIZE); + assert(probe_protocol_report(&left, left.report_id, reports[1], sizeof(reports[1])) == PROBE_INPUT_SIZE); + assert(reports[0][4] == 1 && reports[0][5] == 4 && reports[0][6] == 0); + assert(memcmp(reports[0] + 10, neutral, 3) == 0); + assert(memcmp(reports[0] + 13, sticks[0], 3) == 0); + assert(reports[1][2] == 0x41 && reports[1][3] == 0); + assert(memcmp(reports[1] + 5, sticks[1], 3) == 0); + select_report(&left, 5); + assert(probe_protocol_report(&left, left.report_id, reports[1], sizeof(reports[1])) == PROBE_INPUT_SIZE); + assert(reports[1][4] == 0 && reports[1][5] == 1 && reports[1][6] == 1); + assert(memcmp(reports[1] + 10, sticks[1], 3) == 0); + assert(memcmp(reports[1] + 13, neutral, 3) == 0); + + uint8_t source[PROBE_INPUT_SIZE], expected[2][PROBE_INPUT_SIZE]; + for (size_t i = 0; i < sizeof(source); ++i) source[i] = (uint8_t)(i * 3 + 1); + memcpy(expected[0], source, sizeof(source)); +#ifdef SWITCH2_PROBE_OMIT_NATIVE_IMU + memset(expected[0] + 15, 0, 41); +#elif defined(SWITCH2_PROBE_ZERO_NATIVE_IMU_PAYLOAD) + memset(expected[0] + 16, 0, 40); +#endif + memcpy(expected[1], source, sizeof(source)); + memset(expected[1] + 9, 0, 5); + memset(expected[1] + 14, 0, 41); + memcpy(reports[0], source, sizeof(source)); + memcpy(reports[1], source, sizeof(source)); + probe_protocol_gate_native_report(&left, reports[1]); + probe_protocol_gate_native_report(&right, reports[0]); + assert(memcmp(reports, expected, sizeof(reports)) == 0); + + // Reverse the negotiated gates without changing either donor's opaque bytes. + set_features(&right, 5, 0x15); + set_features(&left, 2, 0x17); + set_features(&left, 4, 0x17); + memcpy(expected[0], source, sizeof(source)); + memset(expected[0] + 2, 0, 2); + memset(expected[0] + 9, 0, 5); + memset(expected[0] + 15, 0, 41); + memcpy(expected[1], source, sizeof(source)); +#ifdef SWITCH2_PROBE_OMIT_NATIVE_IMU + memset(expected[1] + 14, 0, 41); +#elif defined(SWITCH2_PROBE_ZERO_NATIVE_IMU_PAYLOAD) + memset(expected[1] + 15, 0, 40); +#endif + memcpy(reports[0], source, sizeof(source)); + memcpy(reports[1], source, sizeof(source)); + probe_protocol_gate_native_report(&right, reports[0]); + probe_protocol_gate_native_report(&left, reports[1]); + assert(memcmp(reports, expected, sizeof(reports)) == 0); + probe_protocol_reset(&right, false); + assert(probe_protocol_report(&right, 8, reports[0], sizeof(reports[0])) == 0); + assert(probe_protocol_report(&left, 5, reports[1], sizeof(reports[1])) == PROBE_INPUT_SIZE); + assert(reports[1][5] == 1 && reports[1][6] == 1); + memcpy(reports[1], source, sizeof(source)); + probe_protocol_gate_native_report(&left, reports[1]); + assert(memcmp(reports[1], expected[1], sizeof(reports[1])) == 0); +} + +static void test_interleaved_callbacks_and_pairing(void) { + const uint8_t addresses[2][6] = { + {0x64, 0xf9, 0xd8, 0x93, 0x05, 0xa2}, + {0x65, 0xf9, 0xd8, 0x93, 0x05, 0xa2}, + }; + controller_context controllers[2] = { + {.expected_sample = 3, .source_available = true, + .source_token = UINT64_C(0x100000001), .storage_available = true}, + {.expected_sample = 3, .source_available = false, + .source_token = UINT64_C(0x200000001), .storage_available = false}, + }; + probe_protocol_state states[2]; + for (unsigned side = 0; side < 2; ++side) { + probe_protocol_reset(&states[side], side != 0); + states[side].context = &controllers[side]; + states[side].play_sample = play_sample; + states[side].save_pairing = save_pairing; + memcpy(states[side].controller_address, addresses[side], 6); + } + uint8_t reply[PROBE_REPLY_MAX_SIZE]; + uint8_t cue_replies[2][8]; + uint64_t tokens[2] = {0, UINT64_MAX}; + assert(probe_protocol_command(&states[0], sample_command, sizeof(sample_command), + cue_replies[0], 8, &tokens[0]) == 8); + assert(probe_protocol_command(&states[1], sample_command, sizeof(sample_command), + cue_replies[1], 8, &tokens[1]) == 0); + assert(tokens[0] == UINT64_C(0x100000001) && tokens[1] == 0); + controllers[1].source_available = true; + assert(probe_protocol_command(&states[1], sample_command, sizeof(sample_command), + cue_replies[1], 8, &tokens[1]) == 8); + assert(tokens[0] == UINT64_C(0x100000001) && tokens[1] == UINT64_C(0x200000001)); + const uint8_t cue_ack[] = {0x0a, 1, 0, 2, 0, 0xf8, 0, 0}; + assert(memcmp(cue_replies[0], cue_ack, 8) == 0); + assert(memcmp(cue_replies[1], cue_ack, 8) == 0); + + const uint8_t hosts[2][16] = { + {0x15, 0x91, 0, 1, 0, 8, 0, 0, 0, 1, 1, 2, 3, 4, 5, 6}, + {0x15, 0x91, 0, 1, 0, 8, 0, 0, 0, 1, 7, 8, 9, 10, 11, 12}, + }; + const uint8_t device_component[] = { + 0x5c, 0xf6, 0xee, 0x79, 0x2c, 0xdf, 0x05, 0xe1, + 0xba, 0x2b, 0x63, 0x25, 0xc4, 0x1a, 0x5f, 0x10, + }; + const uint8_t ciphertexts[2][16] = { + {0x69, 0xc4, 0xe0, 0xd8, 0x6a, 0x7b, 0x04, 0x30, + 0xd8, 0xcd, 0xb7, 0x80, 0x70, 0xb4, 0xc5, 0x5a}, + {0x66, 0xe9, 0x4b, 0xd4, 0xef, 0x8a, 0x2c, 0x3b, + 0x88, 0x4c, 0xfa, 0x59, 0xca, 0x34, 0x2b, 0x2e}, + }; + uint8_t challenges[2][25] = { + {0x15, 0x91, 0, 2, 0, 17, 0, 0, 0}, + {0x15, 0x91, 0, 2, 0, 17, 0, 0, 0}, + }; + const uint8_t finalize[] = {0x15, 0x91, 0, 3, 0, 1, 0, 0, 0}; + for (unsigned side = 0; side < 2; ++side) { + assert(probe_protocol_command(&states[side], hosts[side], sizeof(hosts[side]), + reply, sizeof(reply), NULL) == 17); + assert(memcmp(reply + 11, addresses[side], 6) == 0); + } + for (unsigned side = 0; side < 2; ++side) { + uint8_t key[] = {0x15, 0x91, 0, 4, 0, 17, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + for (unsigned i = 0; i < 16; ++i) { + // R uses AES's 000102...0f / 001122...ff vector; L uses all zeros. + key[9 + i] = device_component[i] ^ (side ? 0 : 15u - i); + challenges[side][9 + i] = side ? 0 : (uint8_t)((15u - i) * 0x11u); + } + assert(probe_protocol_command(&states[side], key, sizeof(key), + reply, sizeof(reply), NULL) == 25); + } + assert(probe_protocol_command(&states[0], challenges[0], sizeof(challenges[0]), + reply, sizeof(reply), NULL) == 25); + assert(memcmp(reply + 9, ciphertexts[0], 16) == 0); + // Right confirmation cannot authorize the left's finalize. + assert(probe_protocol_command(&states[1], finalize, sizeof(finalize), + reply, sizeof(reply), NULL) == 0); + assert(controllers[0].saves == 0 && controllers[1].saves == 0); + assert(probe_protocol_command(&states[1], challenges[1], sizeof(challenges[1]), + reply, sizeof(reply), NULL) == 25); + assert(memcmp(reply + 9, ciphertexts[1], 16) == 0); + assert(probe_protocol_command(&states[0], finalize, sizeof(finalize), + reply, sizeof(reply), NULL) == 9); + assert(reply[8] == 1); + assert(probe_protocol_command(&states[1], finalize, sizeof(finalize), + reply, sizeof(reply), NULL) == 0); + assert(controllers[0].saves == 1 && controllers[1].saves == 0); + controllers[1].storage_available = true; + assert(probe_protocol_command(&states[1], finalize, sizeof(finalize), + reply, sizeof(reply), NULL) == 9); + assert(reply[8] == 1); + assert(controllers[0].saves == 1 && controllers[1].saves == 1); + + // After independent resets, each durable record must resume only its own + // challenge association; swapping the two contexts' records is rejected. + for (unsigned side = 0; side < 2; ++side) { + probe_protocol_reset(&states[side], side != 0); + memcpy(states[side].controller_address, addresses[side], 6); + assert(!probe_protocol_restore_pairing(&states[side], controllers[1 - side].pairing_blob, + PROBE_PAIRING_BLOB_SIZE)); + assert(probe_protocol_restore_pairing(&states[side], controllers[side].pairing_blob, + PROBE_PAIRING_BLOB_SIZE)); + } + for (unsigned side = 0; side < 2; ++side) { + assert(probe_protocol_command(&states[side], hosts[1 - side], sizeof(hosts[0]), + reply, sizeof(reply), NULL) == 17); + assert(probe_protocol_command(&states[side], challenges[side], sizeof(challenges[side]), + reply, sizeof(reply), NULL) == 0); + assert(probe_protocol_command(&states[side], hosts[side], sizeof(hosts[side]), + reply, sizeof(reply), NULL) == 17); + assert(probe_protocol_command(&states[side], challenges[side], sizeof(challenges[side]), + reply, sizeof(reply), NULL) == 25); + assert(memcmp(reply + 9, ciphertexts[side], 16) == 0); + } +} + +static bool read_memory(void* context, uint32_t address, uint8_t* output, size_t length) { + return probe_memory_read(*(const uint8_t*)context, address, output, length); +} + +static void test_indexed_memory(void) { + probe_protocol_state states[PROBE_CONTROLLER_COUNT]; + uint8_t instances[PROBE_CONTROLLER_COUNT]; + for (uint8_t instance = 0; instance < PROBE_CONTROLLER_COUNT; ++instance) { + instances[instance] = instance; + probe_protocol_reset(&states[instance], probe_model_is_left(instance)); + states[instance].context = &instances[instance]; + states[instance].read_memory = read_memory; + } + const uint8_t calibrations[2][9] = { + {0x10, 0x08, 0x81, 0, 3, 0x30, 0, 4, 0x40}, // Valid user override. + {0, 0x09, 0x90, 0, 3, 0x30, 0, 4, 0x40}, // Invalid user, factory fallback. + }; + const uint8_t command[] = { + 0x02, 0x91, 0, 4, 0, 8, 0, 0, 9, 0x7e, 0, 0, 0xa8, 0x30, 1, 0, + }; + for (unsigned remaining = PROBE_CONTROLLER_COUNT; remaining; --remaining) { + const uint8_t instance = (uint8_t)(remaining - 1); + const bool is_left = SWITCH2_PROBE_COMPOSITE ? instance == 1 : SWITCH2_PROBE_JOYCON_LEFT; + uint8_t reply[PROBE_REPLY_MAX_SIZE], calibration[9]; + assert(probe_memory_stick_calibration(instance, calibration)); + assert(memcmp(calibration, calibrations[is_left], sizeof(calibration)) == 0); + assert(probe_protocol_command(&states[instance], command, sizeof(command), + reply, sizeof(reply), NULL) == 25); + const uint8_t factory[] = {0, is_left ? 9 : 8, is_left ? 0x90 : 0x80, 0, 3, 0x30, 0, 4, 0x40}; + assert(memcmp(reply + 16, factory, sizeof(factory)) == 0); + const uint32_t ends[] = {0x14fff, 0x1fcfff}; + for (unsigned region = 0; region < 2; ++region) { + uint8_t output[2] = {0xa5, 0xa5}; + assert(!probe_memory_read(instance, ends[region], output, sizeof(output))); + assert(output[0] == 0xa5 && output[1] == 0xa5); + assert(probe_memory_read(instance, ends[region], output, 1)); + assert(output[0] == (uint8_t)((region ? 0xf1 : 0xe1) + is_left)); + assert(output[1] == 0xa5); + } + } + uint8_t output[9]; + memset(output, 0xa5, sizeof(output)); + const uint8_t invalid[] = {PROBE_CONTROLLER_COUNT, UINT8_MAX}; + for (size_t i = 0; i < sizeof(invalid); ++i) { + assert(!probe_memory_read(invalid[i], 0x130a8, output, sizeof(output))); + assert(!probe_memory_stick_calibration(invalid[i], output)); + for (size_t byte = 0; byte < sizeof(output); ++byte) assert(output[byte] == 0xa5); + } +} + +int main(void) { + test_indexed_memory(); + test_descriptors(); + for (unsigned side = 0; side < 2; ++side) { + test_report_selection_and_reset(side != 0); + test_buttons_stick_and_feature_control(side != 0); + test_opaque_native_feature_gates(side != 0); + } + test_sample_dispatch(); + test_interleaved_reports_and_features(); + test_interleaved_callbacks_and_pairing(); return 0; } diff --git a/tests/switch2_wii_bridge_test.cpp b/tests/switch2_wii_bridge_test.cpp index 6ae3f60..760f156 100644 --- a/tests/switch2_wii_bridge_test.cpp +++ b/tests/switch2_wii_bridge_test.cpp @@ -119,9 +119,9 @@ static void publish(bool gyro_fresh = true) { static uint32_t poll(bool commit = true, bool gyro_fresh = true) { now_us += 4000; publish(gyro_fresh); - probe_controller_input_poll(to_ms_since_boot(now_us), &controls); - const uint32_t token = probe_controller_input_peek_native_report(to_ms_since_boot(now_us), packet); - if (commit && token) assert(probe_controller_input_commit_native_report(token)); + probe_controller_input_poll(0, to_ms_since_boot(now_us), &controls); + const uint32_t token = probe_controller_input_peek_native_report(0, to_ms_since_boot(now_us), packet); + if (commit && token) assert(probe_controller_input_commit_native_report(0, token)); return token; } @@ -134,7 +134,7 @@ int main() { put_pair(calibration+6, 1600, 1700); probe_controller_input_set_stick_calibration(calibration); probe_controller_input_set_native_features(0x37); - probe_controller_input_set_native_stream(true); + probe_controller_input_set_native_stream(0, true); source.slot = 0; source.controller.active = true; source.controller.connection_generation = 7; @@ -179,10 +179,10 @@ int main() { ir_x[0] += 8; ir_x[1] += 8; const uint32_t ticket = poll(false); uint8_t retry[63]; - assert(ticket && probe_controller_input_peek_native_report(to_ms_since_boot(now_us), retry) == ticket); + assert(ticket && probe_controller_input_peek_native_report(0, to_ms_since_boot(now_us), retry) == ticket); assert(memcmp(packet, retry, sizeof(packet)) == 0); - assert(probe_controller_input_commit_native_report(ticket)); - assert(!probe_controller_input_commit_native_report(ticket)); + assert(probe_controller_input_commit_native_report(0, ticket)); + assert(!probe_controller_input_commit_native_report(0, ticket)); // Tilting the Wii up moves camera spots down. Native Joy-Con Y must // reverse the desktop-pointer convention without changing consumption. @@ -247,17 +247,17 @@ int main() { const uint32_t obsolete = poll(false, false); ++source.controller.connection_generation; assert(poll(false)); - assert(!probe_controller_input_commit_native_report(obsolete)); - probe_controller_input_set_native_stream(false); - assert(probe_controller_input_peek_native_report(to_ms_since_boot(now_us), retry) == 0); - probe_controller_input_set_native_stream(true); + assert(!probe_controller_input_commit_native_report(0, obsolete)); + probe_controller_input_set_native_stream(0, false); + assert(probe_controller_input_peek_native_report(0, to_ms_since_boot(now_us), retry) == 0); + probe_controller_input_set_native_stream(0, true); publish_during_snapshot = true; for (unsigned i = 0; i < 450; ++i) assert(poll()); assert(packet[15] == 30); source.controller.active = false; now_us += 4000; - probe_controller_input_poll(to_ms_since_boot(now_us), &controls); + probe_controller_input_poll(0, to_ms_since_boot(now_us), &controls); assert(!controls.active); - assert(probe_controller_input_peek_native_report(to_ms_since_boot(now_us), retry) == 0); + assert(probe_controller_input_peek_native_report(0, to_ms_since_boot(now_us), retry) == 0); return 0; } diff --git a/tests/test_switch2_mouse_bridge_native.py b/tests/test_switch2_mouse_bridge_native.py index 8b8a900..82af2cf 100644 --- a/tests/test_switch2_mouse_bridge_native.py +++ b/tests/test_switch2_mouse_bridge_native.py @@ -1,26 +1,51 @@ -from pathlib import Path import shutil import subprocess +from pathlib import Path + +import pytest -def test_native_packet_fidelity_and_lifecycle(tmp_path: Path) -> None: +@pytest.mark.parametrize( + ("left", "composite"), + [(False, False), (True, False), (False, True)], + ids=["right", "left", "composite"], +) +def test_native_packet_fidelity_and_lifecycle( + tmp_path: Path, left: bool, composite: bool +) -> 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, + [ + 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", + f"-DSWITCH2_PROBE_JOYCON_LEFT={int(left)}", + f"-DSWITCH2_PROBE_COMPOSITE={int(composite)}", + "-DSWITCH2_BRIDGE_SOURCE_ADDRESS_BYTES=0x98,0xe2,0x55,0x07,0xdf,0x00", + "-DSWITCH2_BRIDGE_SECOND_SOURCE_ADDRESS_BYTES=0x98,0xe2,0x55,0x07,0xe9,0xd3", + 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) diff --git a/tests/test_switch2_sample_relay_native.py b/tests/test_switch2_sample_relay_native.py index b55ef5a..a25268a 100644 --- a/tests/test_switch2_sample_relay_native.py +++ b/tests/test_switch2_sample_relay_native.py @@ -1,10 +1,10 @@ from __future__ import annotations import os -from pathlib import Path import shutil import subprocess import sys +from pathlib import Path import pytest @@ -12,23 +12,30 @@ 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''' +SOURCE = r""" #define main parser_fixture_main #include "switch2_parser_native_test.c" #undef main +#include "model.h" 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_select(uint8_t instance, const uint8_t address[6], uint16_t product_id); +void capture_native_stream(uint8_t instance, bool enabled); +uint32_t capture_native_peek(uint8_t instance, uint8_t report[63]); +bool capture_native_commit(uint8_t instance, uint32_t serial); +bool capture_request(uint8_t instance, uint8_t id, uint64_t* token); +int capture_result(uint8_t instance, uint64_t token); +void capture_cancel(uint8_t instance); 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 secondary_uuids[2][16] = { + {0xd5,0xa9,0xe0,0x1e,0x2f,0xfc,0x4c,0xca,0xb2,0x0c,0x8b,0x67,0x14,0x2b,0xf4,0x42}, + {0xcc,0x1b,0xbb,0xb5,0x73,0x54,0x4d,0x32,0xa7,0x16,0xa8,0x1c,0xb2,0x41,0xa3,0x2a}, +}; +#define OTHER_PRODUCT_ID (SWITCH2_PROBE_JOYCON_LEFT ? UNI_SW2_JOYCON_R_PID : UNI_SW2_JOYCON_L_PID) static const uint8_t sample_ack[8] = {0x0a,1,1,2,0x10,0x78,0,0}; static void native_input(struct fixture_peer* peer) { @@ -36,16 +43,16 @@ static void native_input(struct fixture_peer* peer) { 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; +static struct fixture_peer* connect_sample_source(uint8_t instance, + const uint8_t address[6], + hci_con_handle_t handle) { + const bool left = probe_model_is_left(instance); + const unsigned previous_ready = ready; uint8_t advertisement_data[64]; - size_t advertisement_size = advertisement(advertisement_data, UNI_SW2_JOYCON_R_PID, false); + size_t advertisement_size = advertisement(advertisement_data, probe_model_pid(instance), false); + reverse_bytes(address, advertisement_data + 4, 6); assert(uni_bt_le_switch2_handle_advertisement(advertisement_data, advertisement_size)); - struct fixture_peer* peer = &peers[0]; + struct fixture_peer* peer = &peers[instance]; peer->device.conn.handle = handle; peer->link_alive = true; uni_hid_parser_switch2_on_le_connected(&peer->device); @@ -55,12 +62,12 @@ static struct fixture_peer* sample_ready_on_handle(bool requests, uint32_t start 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; + const unsigned write = request_writes ? 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); + characteristic(peer, RUMBLE_HANDLE, rumble_uuids[left ? 1 : 2], write); + characteristic(peer, SECONDARY_HANDLE, secondary_uuids[left ? 1 : 0], ATT_PROPERTY_NOTIFY); query_done(peer, 0); descriptor(peer, RESPONSE_HANDLE + 2); query_done(peer, 0); @@ -69,19 +76,19 @@ static struct fixture_peer* sample_ready_on_handle(bool requests, uint32_t start descriptor(peer, SECONDARY_HANDLE + 2); query_done(peer, 0); query_done(peer, 0); - for (unsigned i = 0; i < 24 && !ready && !disconnected; ++i) { + for (unsigned i = 0; i < 24 && ready == previous_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; + version[11] = left ? 0 : 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); + assert(ready == previous_ready + 1 && !disconnected); // Let the unchanged neutral-rumble budget quiesce before requesting a cue. for (unsigned i = 0; i < 6; ++i) { advance(13); @@ -90,6 +97,15 @@ static struct fixture_peer* sample_ready_on_handle(bool requests, uint32_t start native_input(peer); return peer; } +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; + return connect_sample_source(0, controller_address, handle); +} + static struct fixture_peer* sample_ready(bool requests, uint32_t start) { return sample_ready_on_handle(requests, start, 0); @@ -97,34 +113,34 @@ static struct fixture_peer* sample_ready(bool requests, uint32_t start) { 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); + assert(capture_request(0, id, &token) && token); + assert(capture_result(0, 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); + assert(capture_result(0, 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); + assert(capture_result(0, token) == 0); notify(peer, RESPONSE_HANDLE, sample_ack, sizeof(sample_ack)); - assert(capture_result(token) == 1); - assert(capture_result(token) == -1); + assert(capture_result(0, token) == 1); + assert(capture_result(0, 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); + assert(!capture_request(0, 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); + assert(capture_result(0, 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); @@ -132,44 +148,91 @@ static void test_ack_order_and_source_matching(void) { 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); + assert(capture_result(0, token) == 0); notify(peer, RESPONSE_HANDLE, sample_ack, sizeof(sample_ack)); - assert(capture_result(token) == 0); // Application ACK cannot beat ATT completion. + assert(capture_result(0, token) == 0); // Application ACK cannot beat ATT completion. query_done(peer, 0); - assert(capture_result(token) == 1 && capture_result(token) == -1); + assert(capture_result(0, token) == 1 && capture_result(0, 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)); + assert(!capture_request(0, 8, &refused) && !capture_request(0, 3, NULL)); +} + +static void test_native_notification_bounds_and_fidelity(void) { + struct fixture_peer* peer = sample_ready(false, 0); + capture_native_stream(0, true); + uint8_t input[100], actual[63]; + for (unsigned i = 0; i < sizeof(input); ++i) + input[i] = (uint8_t)(i * 37 + 11); + input[PROBE_IMU_LENGTH_OFFSET] = 40; + memset(actual, 0xa5, sizeof(actual)); + notify(peer, SECONDARY_HANDLE, input, 62); + notify(peer, SECONDARY_HANDLE, input, 64); + notify(peer, SECONDARY_HANDLE, input, 100); + assert(capture_native_peek(0, actual) == 0 && actual[0] == 0xa5); + notify(peer, SECONDARY_HANDLE, input, 63); + uint32_t serial = capture_native_peek(0, actual); + assert(serial && memcmp(actual, input, sizeof(actual)) == 0); + assert(capture_native_peek(0, actual) == serial); // Failed USB submission retries intact. + assert(capture_native_commit(0, serial) && !capture_native_commit(0, serial)); + assert(capture_native_peek(0, actual) == 0); +} + +static void test_selection_cannot_transfer_pending_ack(void) { + struct fixture_peer* peer = sample_ready(true, 0); + uint64_t old = request_sample(peer, 3), next = 0; + uint8_t other[6]; + memcpy(other, controller_address, sizeof(other)); + ++other[5]; + capture_select(0, other, PROBE_JOYCON_PID); + assert(capture_result(0, old) == -1); + native_input(peer); + assert(!capture_request(0, 4, &next)); // Old address cannot activate new source. + capture_select(0, controller_address, OTHER_PRODUCT_ID); + native_input(peer); + assert(!capture_request(0, 4, &next)); // Same address, wrong side still cannot. + capture_select(0, controller_address, PROBE_JOYCON_PID); + native_input(peer); + assert(capture_request(0, 4, &next) && next > old); + unsigned commands = peer->commands; + advance(13); + assert(peer->commands == commands); + notify(peer, RESPONSE_HANDLE, sample_ack, sizeof(sample_ack)); + query_done(peer, 0); // Retired transaction drains without owning the new cue. + assert(capture_result(0, old) == -1 && capture_result(0, next) == 0); + advance(13); + assert(peer->commands == commands + 1 && peer->command[8] == 4); + successful_ack(peer, next); } 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); + capture_cancel(0); + assert(capture_result(0, old) == -1); + assert(capture_request(0, 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); + assert(capture_result(0, old) == -1 && capture_result(0, 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)); + assert(capture_request(0, 3, &old)); next_write_error = GATT_CLIENT_BUSY; advance(13); assert(peer->commands == commands); - capture_cancel(); - assert(capture_request(4, &next) && next > old); + capture_cancel(0); + assert(capture_request(0, 4, &next) && next > old); advance(13); assert(peer->commands == commands + 1 && peer->command[8] == 4); successful_ack(peer, next); @@ -180,7 +243,7 @@ static void test_rejection_disconnect_and_late_link_events(void) { 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); + assert(disconnected == 1 && capture_result(0, old) == -1); peer = sample_ready(false, 0); old = request_sample(peer, 3); @@ -188,15 +251,15 @@ static void test_rejection_disconnect_and_late_link_events(void) { memcpy(rejected, sample_ack, sizeof(rejected)); rejected[5] = 0x81; notify(peer, RESPONSE_HANDLE, rejected, sizeof(rejected)); - assert(disconnected == 1 && capture_result(old) == -1); + assert(disconnected == 1 && capture_result(0, 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); + assert(capture_result(0, old) == -1); uint64_t next = 0; - assert(!capture_request(3, &next)); + assert(!capture_request(0, 3, &next)); peer = sample_ready_on_handle(false, 0, 1); next = request_sample(peer, 4); assert(next > old); @@ -207,31 +270,33 @@ static void test_rejection_disconnect_and_late_link_events(void) { 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); + assert(capture_result(0, next) == 0 && capture_result(0, 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. + assert(capture_result(0, 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)); + assert(!capture_request(0, 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)); + switch_pico_switch2_mouse_report(OTHER_PRODUCT_ID, controller_address, + PROBE_NATIVE_REPORT_ID, input, 63, now_ms); + switch_pico_switch2_mouse_report(PROBE_JOYCON_PID, other, + PROBE_NATIVE_REPORT_ID, input, 63, now_ms); + assert(!capture_request(0, 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)); + assert(capture_result(0, token) == -1 && !capture_request(0, 3, &token)); peer = sample_ready(false, UINT32_MAX - 200); uint32_t started = now_ms; @@ -242,11 +307,11 @@ static void test_freshness_timeout_and_clock_wrap(void) { } advance(1999 - (uint32_t)(now_ms - started)); native_input(peer); - assert(capture_result(token) == 0); + assert(capture_result(0, token) == 0); advance(1); - assert(capture_result(token) == -1); + assert(capture_result(0, token) == -1); notify(peer, RESPONSE_HANDLE, sample_ack, sizeof(sample_ack)); - assert(capture_result(token) == -1); + assert(capture_result(0, token) == -1); peer = sample_ready(false, 0); token = request_sample(peer, 3); @@ -254,53 +319,157 @@ static void test_freshness_timeout_and_clock_wrap(void) { advance(100); if (!disconnected) native_input(peer); } - assert(disconnected == 1 && capture_result(token) == -1); + assert(disconnected == 1 && capture_result(0, token) == -1); } +#if SWITCH2_PROBE_COMPOSITE || SWITCH2_PROBE_HUB +static void test_simultaneous_native_sources_and_real_acks(void) { + const uint8_t left_address[6] = {0xc0,0x22,0x33,0x44,0x55,0x67}; + struct fixture_peer* right = sample_ready(true, 0); + struct fixture_peer* left = connect_sample_source(1, left_address, 1); + assert(ready == 2 && right->link_alive && left->link_alive); + capture_native_stream(0, true); + capture_native_stream(1, true); + uint8_t rinput[63], linput[63], actual[63]; + for (unsigned i = 0; i < sizeof(rinput); ++i) { + rinput[i] = (uint8_t)(i * 17 + 3); + linput[i] = (uint8_t)(i * 29 + 9); + } + rinput[probe_model_imu_length_offset(0)] = 30; + linput[probe_model_imu_length_offset(1)] = 40; + notify(right, SECONDARY_HANDLE, rinput, sizeof(rinput)); + uint32_t rserial = capture_native_peek(0, actual); + assert(rserial && memcmp(actual, rinput, sizeof(actual)) == 0); + notify(left, SECONDARY_HANDLE, linput, sizeof(linput)); + uint32_t lserial = capture_native_peek(1, actual); + assert(lserial > rserial && memcmp(actual, linput, sizeof(actual)) == 0); + assert(!capture_native_commit(0, lserial) && !capture_native_commit(1, rserial)); + assert(capture_native_commit(1, lserial)); + assert(capture_native_peek(1, actual) == 0); + assert(capture_native_peek(0, actual) == rserial); + assert(memcmp(actual, rinput, sizeof(actual)) == 0); + + uint64_t rtoken, ltoken; + assert(capture_request(0, 3, &rtoken)); + assert(capture_request(1, 6, <oken) && ltoken > rtoken); + const unsigned rcommands = right->commands, lcommands = left->commands; + advance(13); + assert(right->commands == rcommands + 1 && right->command[0] == 0x0a && right->command[8] == 3); + assert(left->commands == lcommands + 1 && left->command[0] == 0x0a && left->command[8] == 6); + assert(capture_result(0, ltoken) == -1 && capture_result(1, rtoken) == -1); + assert(capture_result(0, rtoken) == 0 && capture_result(1, ltoken) == 0); + + // Opposite ATT/application ordering on live links: neither acknowledges its mate. + notify(left, RESPONSE_HANDLE, sample_ack, sizeof(sample_ack)); + assert(capture_result(0, rtoken) == 0 && capture_result(1, ltoken) == 0); + query_done(right, 0); + assert(capture_result(0, rtoken) == 0 && capture_result(1, ltoken) == 0); + notify(right, RESPONSE_HANDLE, sample_ack, sizeof(sample_ack)); + assert(capture_result(0, rtoken) == 1 && capture_result(0, rtoken) == -1); + assert(capture_result(1, ltoken) == 0); + query_done(left, 0); + assert(capture_result(1, ltoken) == 1 && capture_result(1, ltoken) == -1); + + assert(capture_request(0, 4, &rtoken)); + assert(capture_request(1, 7, <oken)); + advance(13); + assert(right->command[8] == 4 && left->command[8] == 7); + notify(left, SECONDARY_HANDLE, linput, sizeof(linput)); + lserial = capture_native_peek(1, actual); + assert(lserial > rserial); + uni_hid_device_disconnect(&right->device); + assert(capture_result(0, rtoken) == -1 && capture_result(1, ltoken) == 0); + assert(capture_native_peek(0, actual) == 0); + assert(!capture_native_commit(0, rserial)); + assert(capture_native_peek(1, actual) == lserial); + assert(memcmp(actual, linput, sizeof(actual)) == 0); + query_done(left, 0); + assert(capture_result(1, ltoken) == 0); + notify(left, RESPONSE_HANDLE, sample_ack, sizeof(sample_ack)); + assert(capture_result(1, ltoken) == 1 && capture_result(1, ltoken) == -1); + assert(capture_native_commit(1, lserial)); + assert(capture_native_peek(1, actual) == 0); +} +#endif + 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)); + assert(capture_request(0, 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)); + assert(capture_result(0, token) == -1 && !capture_request(0, 3, &next)); } int main(void) { test_ack_order_and_source_matching(); + test_native_notification_bounds_and_fidelity(); + test_selection_cannot_transfer_pending_ack(); test_cancel_does_not_transfer_old_ack(); test_rejection_disconnect_and_late_link_events(); test_freshness_timeout_and_clock_wrap(); +#if SWITCH2_PROBE_COMPOSITE || SWITCH2_PROBE_HUB + test_simultaneous_native_sources_and_real_acks(); +#endif test_teardown_survives_capture_exhaustion(); reset(); puts("Source sample relay ACK ordering, ownership, rejection and lifetime passed"); return 0; } -''' +""" -CAPTURE = r''' +CAPTURE = r""" #include "input/switch2_mouse_capture.cpp" +#include "model.h" 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); + switch2_mouse_capture_select_input(0, address, probe_model_pid(0)); +#if SWITCH2_PROBE_COMPOSITE || SWITCH2_PROBE_HUB + const uint8_t second[6] = {0xc0,0x22,0x33,0x44,0x55,0x67}; + switch2_mouse_capture_select_input(1, second, probe_model_pid(1)); +#endif } -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" void capture_select(uint8_t instance, const uint8_t address[6], uint16_t product_id) { + switch2_mouse_capture_select_input(instance, address, product_id); } -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_native_stream(uint8_t instance, bool enabled) { + switch2_mouse_capture_set_native_stream(instance, enabled); } -extern "C" void capture_cancel(void) { switch2_mouse_capture_cancel_sample(); } +extern "C" uint32_t capture_native_peek(uint8_t instance, uint8_t report[63]) { + return switch2_mouse_capture_peek_native_report(instance, btstack_run_loop_get_time_ms(), report); +} +extern "C" bool capture_native_commit(uint8_t instance, uint32_t serial) { + return switch2_mouse_capture_commit_native_report(instance, serial); +} +extern "C" bool capture_request(uint8_t instance, uint8_t id, uint64_t* token) { + return switch2_mouse_capture_request_sample(instance, id, btstack_run_loop_get_time_ms(), token); +} +extern "C" int capture_result(uint8_t instance, uint64_t token) { + return switch2_mouse_capture_sample_result(instance, token, btstack_run_loop_get_time_ms()); +} +extern "C" void capture_cancel(uint8_t instance) { switch2_mouse_capture_cancel_sample(instance); } // 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: +@pytest.mark.parametrize( + ("left", "composite", "hub"), + [ + (False, False, False), + (True, False, False), + (False, True, False), + (False, False, True), + ], + ids=["right", "left", "composite", "hub"], +) +def test_source_sample_requires_its_real_bluetooth_ack( + tmp_path: Path, left: bool, composite: bool, hub: bool +) -> None: root = Path(__file__).resolve().parents[1] cc = shutil.which("cc") or shutil.which("gcc") cxx = shutil.which("c++") or shutil.which("g++") @@ -308,36 +477,85 @@ def test_source_sample_requires_its_real_bluetooth_ack(tmp_path: Path) -> None: 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) + 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'}"] + 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"-DSWITCH2_PROBE_JOYCON_LEFT={int(left)}", + f"-DSWITCH2_PROBE_COMPOSITE={int(composite)}", + f"-DSWITCH2_PROBE_HUB={int(hub)}", + f"-I{root / 'tools' / 'switch2_usb_probe'}", + 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")): + 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) + 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( + [ + 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) diff --git a/tests/test_switch2_usb_probe_protocol_native.py b/tests/test_switch2_usb_probe_protocol_native.py index 3335367..78b9e6a 100644 --- a/tests/test_switch2_usb_probe_protocol_native.py +++ b/tests/test_switch2_usb_probe_protocol_native.py @@ -8,7 +8,17 @@ from pathlib import Path import pytest -def test_switch2_usb_probe_deferred_sample(tmp_path: Path) -> None: +@pytest.mark.parametrize( + ("left", "composite"), + [(False, False), (True, False), (False, True)], + ids=["right", "left", "composite"], +) +@pytest.mark.parametrize( + "imu_mode", [None, "OMIT_NATIVE_IMU", "ZERO_NATIVE_IMU_PAYLOAD"] +) +def test_switch2_usb_probe_protocol( + tmp_path: Path, left: bool, composite: bool, imu_mode: str | None +) -> 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" @@ -19,13 +29,42 @@ def test_switch2_usb_probe_deferred_sample(tmp_path: Path) -> None: 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()), + ( + 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") + pytest.skip( + "Pico SDK mbedTLS required; configure firmware or set PICO_SDK_PATH" + ) probe = root / "tools" / "switch2_usb_probe" + sides = [False, True] if composite else [left] + factory_rows = [] + user_rows = [] + for is_left in sides: + factory_center = "0x00, 0x09, 0x90" if is_left else "0x00, 0x08, 0x80" + factory_rows.append( + f"{{[0xa8] = {factory_center}, 0, 3, 0x30, 0, 4, 0x40," + f" [8191] = {0xE2 if is_left else 0xE1}}}" + ) + # L deliberately has invalid user calibration despite valid magic. + user_center = "0, 0, 0" if is_left else "0x10, 0x08, 0x81" + user_rows.append( + f"{{[0x40] = 0xb2, 0xa1, {user_center}, 0, 3, 0x30, 0, 4, 0x40," + f" [4095] = {0xF2 if is_left else 0xF1}}}" + ) + (tmp_path / "probe_memory_data.h").write_text( + '#include "model.h"\n' + "static const uint8_t probe_factory_memories[PROBE_CONTROLLER_COUNT][8192] = {\n" + + ",\n".join(factory_rows) + + "\n};\n" + "static const uint8_t probe_user_calibrations[PROBE_CONTROLLER_COUNT][4096] = {\n" + + ",\n".join(user_rows) + + "\n};\n" + ) executable = tmp_path / "switch2_usb_probe_protocol" subprocess.run( [ @@ -36,9 +75,14 @@ def test_switch2_usb_probe_deferred_sample(tmp_path: Path) -> None: "-Werror", "-pedantic", f'-DMBEDTLS_CONFIG_FILE="{probe / "mbedtls_config.h"}"', + f"-DSWITCH2_PROBE_JOYCON_LEFT={int(left)}", + f"-DSWITCH2_PROBE_COMPOSITE={int(composite)}", + *([f"-DSWITCH2_PROBE_{imu_mode}=1"] if imu_mode else []), f"-I{probe}", + f"-I{tmp_path}", f"-I{mbedtls / 'include'}", str(probe / "protocol.c"), + str(probe / "memory.c"), str(mbedtls / "library" / "aes.c"), str(mbedtls / "library" / "platform_util.c"), str(root / "tests" / "switch2_usb_probe_protocol_test.c"), diff --git a/tools/native_joycon_hub_check.py b/tools/native_joycon_hub_check.py new file mode 100755 index 0000000..73dbc23 --- /dev/null +++ b/tools/native_joycon_hub_check.py @@ -0,0 +1,1062 @@ +#!/usr/bin/env python3 +"""Bounded, non-pairing qualification of the switch-pico native Joy-Con USB hub. + +Requires Linux, PyUSB/libusb, and the existing sudo -n setfacl permission policy. +Uses the already-paired real R/L donors; it cannot wake or pair them. The JSON +capture is created exclusively before USB access and retains partial failures. +No reset, configuration change, pairing exchange, profile access, flash write, +or HID output is sent. Motor sample playback requires --rumble-sample explicitly. +""" + +from __future__ import annotations + +import argparse +import json +import math +import os +import signal +import struct +import subprocess +import time +from contextlib import contextmanager +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + +from switch2_native_imu import decode_block, native_block + +VID = 0x057E +ROOT_PID = 0x2068 +SERIAL_PREFIX = "switch-pico-" +SIDES = ("R", "L") +# Only protocol constants live in source. Device-specific factory/calibration +# captures stay in the private build paths configured by CMake. +MODELS = { + "R": {"pid": 0x2066, "port": 1, "report": 0x08, "diagnostic_host": "020000000001"}, + "L": {"pid": 0x2067, "port": 2, "report": 0x07, "diagnostic_host": "020000000002"}, +} + + +def model_references(build_dir: Path) -> dict[str, dict[str, Any]]: + cache = {} + for line in (build_dir / "CMakeCache.txt").read_text().splitlines(): + if line.startswith("SWITCH2_") and ":" in line and "=" in line: + field, value = line.split("=", 1) + cache[field.split(":", 1)[0]] = value + models = {} + for side, constants in MODELS.items(): + prefix = "SWITCH2_PROBE" if side == "R" else "SWITCH2_PROBE_SECOND" + identity = Path(cache[prefix + "_IDENTITY_FILE"]).read_bytes() + version = Path(cache[prefix + "_VERSION_FILE"]).read_bytes() + factory = Path(cache[prefix + "_FACTORY_FILE"]).read_bytes() + address = bytes.fromhex(cache[prefix + "_CONTROLLER_ADDRESS"].replace(":", "")) + if ( + len(identity) != 64 + or len(version) != 12 + or len(factory) != 8192 + or len(address) != 6 + ): + raise ValueError(f"{side} build references have invalid native lengths") + if factory[:64] != identity or struct.unpack_from(" bytes: + version = bytes.fromhex(model["version"]) + return ( + version[:3] + bytes(3) + version[4:7] + b"\0" + bytes.fromhex(model["mac_wire"]) + ) + + +def location(device: Any) -> dict[str, Any]: + if device.bus is None or device.address is None or not device.port_numbers: + raise RuntimeError("USB device has no usable physical bus/address/port path") + return { + "bus": int(device.bus), + "address": int(device.address), + "ports": list(device.port_numbers), + "vid": int(device.idVendor), + "pid": int(device.idProduct), + } + + +def command(group: int, subcommand: int, payload: bytes = b"") -> bytes: + # An explicit allowlist, not a general-purpose command injection interface. + if (group, subcommand) not in { + (0x03, 0x0D), + (0x03, 0x0A), + (0x0C, 0x02), + (0x0C, 0x04), + (0x02, 0x04), + (0x10, 0x01), + (0x0A, 0x02), + }: + raise ValueError("command is outside the qualification allowlist") + return bytes((group, 0x91, 0, subcommand, 0, len(payload), 0, 0)) + payload + + +def reply(request: bytes, payload: bytes = b"") -> bytes: + return bytes((request[0], 1, 0, request[3], 0, 0xF8, 0, 0)) + payload + + +class Check: + def __init__(self, args: argparse.Namespace, capture: Any) -> None: + self.args = args + self.models = {side: model.copy() for side, model in MODELS.items()} + self.capture = capture + self.started = time.monotonic() + self.deadline = self.started + args.timeout + self.core: Any = None + self.util: Any = None + self.devices: dict[str, Any] = {} + self.claimed: list[tuple[str, int]] = [] + self.detached: list[tuple[str, int]] = [] + self.current_stage = "dependencies" + self.last_counter: dict[str, int] = {} + self.last_controls: dict[str, tuple[bytes, bytes]] = {} + self.imu_evidence: dict[str, set[bytes]] = {side: set() for side in SIDES} + self.result: dict[str, Any] = { + "schema_version": 1, + "success": False, + "exit_code": 2, + "started_utc": datetime.now(timezone.utc).isoformat(), + "parameters": { + "timeout_seconds": args.timeout, + "duration_seconds": args.duration, + "usb_timeout_ms": args.usb_timeout_ms, + "rumble_sample": args.rumble_sample, + }, + "safety": { + "pairing_writes": False, + "profile_access": False, + "flash_writes": False, + "usb_reset": False, + "motor_requested": args.rumble_sample is not None, + }, + "scope": "USB identity/protocol/input isolation, not console or motor-feel qualification", + "imu_decode_note": "Existing candidate codec; left uses its documented one-byte-earlier IMU boundary. Physical scales are not calibrated by this check.", + "stages": [], + "errors": [], + "seen_roots": [], + "seen_children": [], + "devices": {}, + "acl": [], + "interfaces": [], + "controls": [], + "bulk": [], + "active_rounds": [], + "cleanup": [], + "streams": { + side: { + "packets": 0, + "valid_native_imu": 0, + "imu_counter_changes": 0, + "wrong_side": 0, + "unexpected_report": 0, + "invalid": 0, + "zero_length_imu": 0, + "timeouts": 0, + "report_ids": {}, + "imu_lengths": {}, + "imu_formats": {}, + "buttons_nonzero": 0, + "control_changes": 0, + "samples": [], + "rejected_samples": [], + "first_valid_seconds": None, + "last_valid_seconds": None, + "last_counter_change_seconds": None, + } + for side in SIDES + }, + } + self.checkpoint() + + def elapsed(self) -> float: + return round(time.monotonic() - self.started, 6) + + def checkpoint(self) -> None: + self.result["elapsed_seconds"] = self.elapsed() + self.capture.seek(0) + json.dump(self.result, self.capture, indent=2, allow_nan=False) + self.capture.write("\n") + self.capture.truncate() + self.capture.flush() + + def error(self, message: str, side: str | None = None) -> None: + self.result["errors"].append( + { + "stage": self.current_stage, + "side": side, + "at_seconds": self.elapsed(), + "message": message, + } + ) + + @contextmanager + def stage(self, name: str): + self.current_stage = name + entry = {"name": name, "status": "running", "started_seconds": self.elapsed()} + self.result["stages"].append(entry) + self.checkpoint() + print(f"[NATIVEHUB] stage={name}", flush=True) + previous_errors = len(self.result["errors"]) + try: + yield + except BaseException as error: + entry["status"] = "failed" + self.error(f"{type(error).__name__}: {error}") + raise + else: + entry["status"] = ( + "passed" if len(self.result["errors"]) == previous_errors else "failed" + ) + finally: + entry["finished_seconds"] = self.elapsed() + self.checkpoint() + + def timeout_ms(self, maximum: int | None = None, until: float | None = None) -> int: + remaining = ( + min(self.deadline, until if until is not None else self.deadline) + - time.monotonic() + ) + if remaining <= 0: + raise TimeoutError(f"bounded deadline reached during {self.current_stage}") + return max( + 1, + min( + self.args.usb_timeout_ms if maximum is None else maximum, + int(remaining * 1000), + ), + ) + + def control( + self, + owner: str, + name: str, + request_type: int, + request: int, + value: int, + index: int, + length: int, + ) -> bytes: + entry = { + "stage": self.current_stage, + "side": owner, + "name": name, + "setup": [request_type, request, value, index, length], + "at_seconds": self.elapsed(), + } + self.result["controls"].append(entry) + try: + data = bytes( + self.devices[owner].ctrl_transfer( + request_type, + request, + value, + index, + length, + timeout=self.timeout_ms(), + ) + ) + entry["response_hex"] = data.hex() + entry["length"] = len(data) + return data + except Exception as error: + entry["error"] = str(error) + raise + + def discover(self) -> None: + until = min(self.deadline, self.started + 30) + while time.monotonic() < until: + devices = list(self.core.find(find_all=True) or []) + roots = [] + for device in devices: + if (device.idVendor, device.idProduct) != (VID, ROOT_PID): + continue + seen = location(device) + # Read kernel-cached serials before granting access, so genuine + # Nintendo hubs and unrelated devices receive no USB requests/ACL. + sysname = f"{seen['bus']}-" + ".".join(map(str, seen["ports"])) + try: + serial = ( + (Path("/sys/bus/usb/devices") / sysname / "serial") + .read_text() + .strip() + ) + except OSError as error: + seen["serial_error"] = str(error) + serial = "" + seen["serial"] = serial + if seen not in self.result["seen_roots"]: + self.result["seen_roots"].append(seen) + if serial.startswith(SERIAL_PREFIX): + roots.append((device, seen)) + if len(roots) > 1: + raise RuntimeError( + "multiple switch-pico 057e:2068 hubs; refusing ambiguous target" + ) + if roots: + root, root_info = roots[0] + selected = {} + direct_children = [] + for device in devices: + ports = tuple(device.port_numbers or ()) + if device.bus != root.bus or ports[:-1] != tuple(root.port_numbers): + continue + seen = location(device) + direct_children.append(seen) + if seen not in self.result["seen_children"]: + self.result["seen_children"].append(seen) + for side in SIDES: + model = self.models[side] + if (device.idVendor, device.idProduct, ports[-1]) == ( + VID, + model["pid"], + model["port"], + ): + if side in selected: + raise RuntimeError( + f"duplicate {side} child on the expected hub port" + ) + selected[side] = device + if len(selected) == 2: + if len(direct_children) != 2: + raise RuntimeError( + "target hub has unexpected additional direct children" + ) + self.devices = {"root": root, **selected} + if len({device.address for device in self.devices.values()}) != 3: + raise RuntimeError( + "root/R/L do not have three distinct USB addresses" + ) + self.result["devices"] = { + "root": root_info, + **{side: location(selected[side]) for side in SIDES}, + } + return + time.sleep(min(0.1, max(0, until - time.monotonic()))) + raise TimeoutError( + "discovery: expected one switch-pico 057e:2068 with R 2066 at port 1 and L 2067 at port 2 on the same path; inspect seen_roots/seen_children" + ) + + def permissions(self) -> None: + for owner, device in self.devices.items(): + node = f"/dev/bus/usb/{device.bus:03d}/{device.address:03d}" + entry = {"owner": owner, "node": node, "granted": False} + self.result["acl"].append(entry) + try: + subprocess.run( + ["sudo", "-n", "setfacl", "-m", f"u:{os.getuid()}:rw", node], + check=True, + capture_output=True, + text=True, + timeout=self.timeout_ms(3000) / 1000, + ) + entry["granted"] = True + except subprocess.CalledProcessError as error: + entry["error"] = error.stderr.strip() + raise RuntimeError( + f"cannot grant access to {owner} {node}: {error.stderr.strip()}" + ) from error + data = self.control("root", "device_descriptor", 0x80, 6, 0x0100, 0, 18) + if ( + len(data) != 18 + or data[:2] != b"\x12\x01" + or data[4] != 9 + or struct.unpack_from(" None: + for side in SIDES: + # GET_CONFIGURATION only: do not reset USB or set a configuration. + if self.control(side, "active_configuration", 0x80, 8, 0, 0, 1) != b"\x01": + raise RuntimeError( + f"{side} is not already configured as configuration 1" + ) + device = self.devices[side] + for interface in (0, 1): + entry = { + "side": side, + "interface": interface, + "detached": False, + "claimed": False, + } + self.result["interfaces"].append(entry) + self.timeout_ms() + if device.is_kernel_driver_active(interface): + device.detach_kernel_driver(interface) + self.detached.append((side, interface)) + entry["detached"] = True + self.util.claim_interface(device, interface) + self.claimed.append((side, interface)) + entry["claimed"] = True + + def descriptors(self, side: str, *, report: bool = True) -> None: + model = self.models[side] + device = self.control(side, "device_descriptor", 0x80, 6, 0x0100, 0, 18) + if ( + len(device) != 18 + or device[:2] != b"\x12\x01" + or device[7] != 64 + or device[17] != 1 + or struct.unpack_from(" len(config): + raise RuntimeError(f"{side} malformed configuration item at {offset}") + item = config[offset : offset + size] + if item[1] == 4: + if size != 9 or item[3] != 0 or item[2] in interfaces: + raise RuntimeError(f"{side} duplicate or non-native interface") + current = item[2] + interfaces[current] = (item[5], item[4]) + endpoints[current] = [] + elif item[1] == 5: + if size != 7 or current is None: + raise RuntimeError(f"{side} malformed endpoint") + endpoints[current].append( + (item[2], item[3], int.from_bytes(item[4:6], "little")) + ) + elif item[1] == 0x21: + if size != 9 or current != 0 or item[5:7] != b"\x01\x22": + raise RuntimeError(f"{side} malformed HID descriptor") + hid_length = int.from_bytes(item[7:9], "little") + offset += size + expected_endpoints = { + 0: [(0x81, 3, 64), (0x01, 3, 64)], + 1: [(0x02, 2, 64), (0x82, 2, 64)], + } + if ( + interfaces != {0: (3, 2), 1: (255, 2)} + or endpoints != expected_endpoints + or hid_length != 100 + ): + raise RuntimeError(f"{side} is not HID0/vendor1 with native EP81/01/82/02") + if not report: + return + hid = self.control( + side, "hid_report_descriptor", 0x81, 6, 0x2200, 0, hid_length + ) + if len(hid) != hid_length: + raise RuntimeError(f"{side} truncated HID report descriptor") + inputs: dict[int, int] = {} + outputs: dict[int, int] = {} + report_id = report_size = report_count = 0 + offset = 0 + while offset < len(hid): + prefix = hid[offset] + offset += 1 + size = 4 if prefix & 3 == 3 else prefix & 3 + if prefix == 0xFE or offset + size > len(hid): + raise RuntimeError(f"{side} malformed/unsupported HID item") + value = int.from_bytes(hid[offset : offset + size], "little") + offset += size + kind = prefix & 0xFC + if kind == 0x74: + report_size = value + elif kind == 0x94: + report_count = value + elif kind == 0x84: + report_id = value + elif kind in (0x80, 0x90): + target = inputs if kind == 0x80 else outputs + target[report_id] = ( + target.get(report_id, 0) + report_size * report_count + ) + if inputs != {5: 504, model["report"]: 504} or outputs != {1: 504}: + raise RuntimeError( + f"{side} HID advertises the wrong side/report lengths: {inputs}, {outputs}" + ) + + def identities(self, side: str, round_number: int = 0) -> None: + # Alternate device and explicit interface recipients; both child-local + # indexes must resolve to this USB address, never to the sibling. + request_type, index = (0xC0, 0) if round_number % 2 == 0 else (0xC1, 1) + identity = self.control( + side, "vendor_identity03", request_type, 3, 0, index, 64 + ) + status = self.control(side, "vendor_version02", request_type, 2, 0, index, 64) + self.result["devices"][side]["identity03_hex"] = identity.hex() + self.result["devices"][side]["version02_hex"] = status.hex() + if identity != bytes.fromhex(self.models[side]["identity"]): + raise RuntimeError( + f"{side} vendor 03 does not match its distinct captured factory identity" + ) + if status != expected_status(self.models[side]): + raise RuntimeError( + f"{side} vendor 02 must be 16 bytes with wire MAC tail {self.models[side]['mac_wire']}" + ) + # Short-then-full EP0 reads also check transfer length/context teardown. + short_length = (1, 7, 15)[round_number % 3] + short = self.control( + side, "vendor_version02_short", request_type, 2, 0, index, short_length + ) + if short != status[:short_length]: + raise RuntimeError(f"{side} short vendor read leaked/truncated incorrectly") + + def exchange_pair( + self, requests: dict[str, tuple[bytes, bytes]], round_number: int = 0 + ) -> None: + order = SIDES if round_number % 2 == 0 else tuple(reversed(SIDES)) + entries = {} + # Both devices have pending, identical-form commands before either IN is + # consumed. Reverse completion order to expose global reply-buffer reuse. + for side in order: + request, expected = requests[side] + if request[:4] == b"\x0a\x91\x00\x02" and self.args.rumble_sample is None: + raise RuntimeError("motor command requires explicit --rumble-sample") + entry = { + "stage": self.current_stage, + "side": side, + "at_seconds": self.elapsed(), + "request_hex": request.hex(), + "expected_hex": expected.hex(), + "segments_hex": [], + "response_hex": "", + "matched": False, + } + self.result["bulk"].append(entry) + entries[side] = entry + written = self.devices[side].write(0x02, request, timeout=self.timeout_ms()) + entry["written"] = int(written) + if written != len(request): + raise RuntimeError( + f"{side} short native bulk OUT ({written}/{len(request)})" + ) + for side in reversed(order): + expected = requests[side][1] + actual = bytearray() + entry = entries[side] + while len(actual) < len(expected): + segment = bytes( + self.devices[side].read(0x82, 64, timeout=self.timeout_ms()) + ) + entry["segments_hex"].append(segment.hex()) + remaining = len(expected) - len(actual) + actual.extend(segment) + entry["response_hex"] = actual.hex() + if len(segment) != min(64, remaining): + raise RuntimeError( + f"{side} bulk segment length {len(segment)}, expected {min(64, remaining)}" + ) + if actual != expected: + raise RuntimeError( + f"{side} bulk header/length/content mismatch; possible cross-device reply leakage" + ) + entry["matched"] = True + + def initialize(self) -> None: + for operation in ( + "initialize", + "select_report", + "feature_mask", + "feature_enable", + ): + requests = {} + for side in SIDES: + if operation == "initialize": + request = command( + 3, + 0x0D, + b"\x01\x00" + + bytes.fromhex(self.models[side]["diagnostic_host"]), + ) + response = reply(request, b"\x01\x00\x00\x00") + elif operation == "select_report": + request = command( + 3, 0x0A, bytes((self.models[side]["report"], 0, 0, 0)) + ) + response = reply(request) + else: + # 0x01 buttons, 0x02 sticks, 0x04 IMU; not vibration/mouse/NFC. + request = command( + 0x0C, + 2 if operation == "feature_mask" else 4, + b"\x07\x00\x00\x00", + ) + response = reply(request, bytes(4)) + requests[side] = (request, response) + self.exchange_pair(requests) + + def queries(self, round_number: int) -> None: + request = command(0x10, 1) + self.exchange_pair( + { + side: ( + request, + reply(request, bytes.fromhex(self.models[side]["version"])), + ) + for side in SIDES + }, + round_number, + ) + requests = {} + for side_index, side in enumerate(SIDES): + offset = (round_number + side_index) % 2 + address = 0x13000 + offset + request = command(2, 4, b"\x50\x7e\x00\x00" + struct.pack(" None: + stream = self.result["streams"][side] + remaining = min(self.deadline, until) - time.monotonic() + if remaining <= 0: + return + try: + packet = bytes( + self.devices[side].read( + 0x81, 64, timeout=max(1, min(10, int(remaining * 1000))) + ) + ) + except self.core.USBTimeoutError: + stream["timeouts"] += 1 + return + stream["packets"] += 1 + report_id = packet[0] if packet else -1 + report_key = f"{report_id:02x}" if report_id >= 0 else "empty" + stream["report_ids"][report_key] = stream["report_ids"].get(report_key, 0) + 1 + sample = { + "at_seconds": self.elapsed(), + "stage": self.current_stage, + "packet_hex": packet.hex(), + } + rejection = None + if report_id == self.models["L" if side == "R" else "R"]["report"]: + stream["wrong_side"] += 1 + rejection = "wrong-side native report on this device's HID pipe" + elif report_id != self.models[side]["report"]: + stream["unexpected_report"] += 1 + rejection = ( + "unexpected report ID; only this side's native 07/08 is accepted" + ) + elif len(packet) != 64: + stream["invalid"] += 1 + rejection = f"native report is {len(packet)} bytes, expected 64" + else: + payload = packet[1:] + length_offset = 14 if side == "L" else 15 + length = payload[length_offset] + key = str(length) + stream["imu_lengths"][key] = stream["imu_lengths"].get(key, 0) + 1 + if length == 0: + stream["zero_length_imu"] += 1 + rejection = ( + "zero-length IMU: inactive donor/neutral fallback is not live input" + ) + else: + try: + block = ( + native_block({"native_hex": packet.hex()}) + if side == "R" + else payload[length_offset + 1 : length_offset + 1 + length] + ) + decoded = decode_block(block) + if not any( + value + for vector in decoded["accelerations"] + for value in vector["raw"] + ): + raise ValueError( + "all-zero acceleration is not donor IMU evidence" + ) + if not all( + math.isfinite(value) for value in decoded["quaternion_wire"] + ): + raise ValueError("non-finite decoded quaternion") + sample["imu"] = decoded + except (ValueError, AssertionError) as error: + stream["invalid"] += 1 + rejection = f"native IMU decode failed: {error}" + else: + now = self.elapsed() + stream["valid_native_imu"] += 1 + stream["last_valid_seconds"] = now + if stream["first_valid_seconds"] is None: + stream["first_valid_seconds"] = now + counter = decoded["counter_ticks"] + if side in self.last_counter and self.last_counter[side] != counter: + stream["imu_counter_changes"] += 1 + stream["last_counter_change_seconds"] = now + self.last_counter[side] = counter + if len(self.imu_evidence[side]) < 512: + self.imu_evidence[side].add(block) + controls = (payload[2:4], payload[5:8]) + sample["buttons_hex"], sample["stick_hex"] = ( + part.hex() for part in controls + ) + if any(controls[0]): + stream["buttons_nonzero"] += 1 + if ( + side in self.last_controls + and self.last_controls[side] != controls + ): + stream["control_changes"] += 1 + self.last_controls[side] = controls + format_key = f"{decoded['format']:02x}" + first_format = format_key not in stream["imu_formats"] + stream["imu_formats"][format_key] = ( + stream["imu_formats"].get(format_key, 0) + 1 + ) + samples = stream["samples"] + if ( + len(samples) < 4 + or first_format + or ( + len(samples) < 32 and now - samples[-1]["at_seconds"] >= 0.5 + ) + ): + samples.append(sample) + stream["last_sample"] = sample + if rejection: + sample["reason"] = rejection + if len(stream["rejected_samples"]) < 8: + stream["rejected_samples"].append(sample) + + def poll_pair(self, until: float, reverse: bool = False) -> None: + for side in reversed(SIDES) if reverse else SIDES: + if time.monotonic() >= until: + break + self.poll(side, until) + + def counts(self) -> dict[str, int]: + return { + side: self.result["streams"][side]["valid_native_imu"] for side in SIDES + } + + def donors_ready(self) -> None: + until = min(self.deadline, time.monotonic() + 60) + iteration = 0 + while time.monotonic() < until: + self.poll_pair(until, bool(iteration % 2)) + iteration += 1 + if all( + stream["valid_native_imu"] >= 3 and stream["imu_counter_changes"] >= 2 + for stream in self.result["streams"].values() + ): + return + details = "; ".join( + f"{side}: valid={stream['valid_native_imu']}, counter_changes={stream['imu_counter_changes']}, zero_imu={stream['zero_length_imu']}, invalid={stream['invalid']}, timeouts={stream['timeouts']}" + for side, stream in self.result["streams"].items() + ) + raise RuntimeError( + f"donors did not produce fresh decodable native IMU during the manual-wake window; {details}; no pairing/wake/reset was attempted" + ) + + def active(self) -> None: + until = min(self.deadline, time.monotonic() + self.args.duration) + initial_counts = self.counts() + initial_changes = { + side: self.result["streams"][side]["imu_counter_changes"] for side in SIDES + } + next_query = time.monotonic() + round_number = 0 + pending = None + while time.monotonic() < until: + if time.monotonic() >= next_query and until - time.monotonic() >= 0.25: + if pending is not None: + pending["after"] = self.counts() + pending = { + "round": round_number, + "before": self.counts(), + "started_seconds": self.elapsed(), + } + self.result["active_rounds"].append(pending) + for side in SIDES if round_number % 2 == 0 else reversed(SIDES): + self.descriptors(side, report=round_number % 2 == 0) + self.identities(side, round_number) + self.queries(round_number) + pending["reads_matched"] = True + next_query = time.monotonic() + 0.25 + round_number += 1 + self.checkpoint() + self.poll_pair(until, bool(round_number % 2)) + if pending is not None: + pending["after"] = self.counts() + if round_number < 2: + self.error( + "fewer than two interleaved control/bulk rounds completed during streaming" + ) + if not any( + all( + entry.get("after", {}).get(side, 0) > entry["before"][side] + for side in SIDES + ) + for entry in self.result["active_rounds"] + if entry.get("reads_matched") + ): + self.error( + "no interleaved control/bulk round was bracketed by valid input from both donors" + ) + shared = self.imu_evidence["R"] & self.imu_evidence["L"] + self.result["imu_isolation"] = { + "sample_limit_per_side": 512, + "identical_blocks_seen_on_both_sides": len(shared), + "unique_blocks": { + side: len(blocks) for side, blocks in self.imu_evidence.items() + }, + "side_exclusive_blocks": { + side: len(blocks - shared) for side, blocks in self.imu_evidence.items() + }, + } + for side in SIDES: + if len(self.imu_evidence[side] - shared) < 2: + self.error( + "donor IMU evidence is frozen or duplicated across child devices", + side, + ) + for side in SIDES: + stream = self.result["streams"][side] + if ( + stream["valid_native_imu"] - initial_counts[side] < 3 + or stream["imu_counter_changes"] - initial_changes[side] < 2 + ): + self.error( + "insufficient fresh native donor IMU during active control/bulk reads", + side, + ) + last_change = stream["last_counter_change_seconds"] + if last_change is None or self.elapsed() - last_change > 2: + self.error( + "donor IMU stopped advancing before streaming finished", side + ) + if stream["wrong_side"] or stream["unexpected_report"] or stream["invalid"]: + self.error( + f"rejected wrong-side={stream['wrong_side']}, unexpected={stream['unexpected_report']}, malformed={stream['invalid']} HID packets", + side, + ) + if time.monotonic() >= self.deadline: + raise TimeoutError( + "overall timeout interrupted the required streaming duration" + ) + + def cleanup(self) -> None: + self.current_stage = "cleanup" + for action, tracked in (("release", self.claimed), ("reattach", self.detached)): + for side, interface in reversed(tracked): + entry = { + "action": action, + "side": side, + "interface": interface, + "success": False, + } + self.result["cleanup"].append(entry) + try: + if action == "release": + self.util.release_interface(self.devices[side], interface) + else: + self.devices[side].attach_kernel_driver(interface) + entry["success"] = True + except (OSError, RuntimeError, NotImplementedError) as error: + entry["error"] = str(error) + self.error(f"{action} interface {interface}: {error}", side) + if self.util is not None: + for owner, device in self.devices.items(): + try: + self.util.dispose_resources(device) + except (OSError, RuntimeError, NotImplementedError) as error: + self.error(f"dispose USB resources: {error}", owner) + + def run(self) -> int: + completed = False + try: + with self.stage("dependencies"): + import usb.core + import usb.util + + with self.stage("references"): + self.models = model_references(self.args.build_dir) + self.core, self.util = usb.core, usb.util + with self.stage("discovery"): + self.discover() + with self.stage("permissions_and_root_identity"): + self.permissions() + with self.stage("claim_child_interfaces"): + self.claim() + with self.stage("descriptor_and_ep0_isolation"): + for round_number in range(20): + for side in SIDES if round_number % 2 == 0 else reversed(SIDES): + self.descriptors(side, report=round_number in (0, 19)) + self.identities(side, round_number) + with self.stage("native_initialization"): + self.initialize() + with self.stage("bulk_isolation"): + for round_number in range(2): + self.queries(round_number) + with self.stage("donor_startup"): + self.donors_ready() + with self.stage("active_input_and_read_isolation"): + self.active() + if self.args.rumble_sample is not None and not self.result["errors"]: + with self.stage("explicit_motor_sample_ack"): + request = command( + 0x0A, 2, bytes((self.args.rumble_sample, 0, 0, 0)) + ) + self.exchange_pair( + {side: (request, reply(request)) for side in SIDES} + ) + self.result["motor_result"] = ( + "native sample ACK received for each side; physical sensation is not measured" + ) + else: + self.result["stages"].append( + {"name": "explicit_motor_sample_ack", "status": "skipped"} + ) + completed = True + except KeyboardInterrupt: + self.result["interrupted"] = True + except ( + OSError, + RuntimeError, + ValueError, + TypeError, + ImportError, + subprocess.SubprocessError, + struct.error, + ) as error: + # The stage records partial responses; retain the terminal cause too. + self.result["failure"] = str(error) + finally: + try: + with self.stage("cleanup"): + self.cleanup() + finally: + self.result["success"] = completed and not self.result["errors"] + self.result["exit_code"] = 0 if self.result["success"] else 2 + failed = sorted({entry["stage"] for entry in self.result["errors"]}) + streams = self.result["streams"] + self.result["summary"] = ( + f"{'PASS' if self.result['success'] else 'FAIL'} " + f"R={streams['R']['valid_native_imu']} L={streams['L']['valid_native_imu']} " + f"R_counter_changes={streams['R']['imu_counter_changes']} " + f"L_counter_changes={streams['L']['imu_counter_changes']} " + f"active_rounds={len(self.result['active_rounds'])} " + f"errors={len(self.result['errors'])} failed_stages={','.join(failed) or 'none'}" + ) + self.checkpoint() + print(f"[NATIVEHUB] {self.result['summary']}", flush=True) + print(f"[NATIVEHUB] capture={self.args.output}", flush=True) + return self.result["exit_code"] + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--build-dir", + type=Path, + default=Path(__file__).resolve().parents[1] / "build-switch2-native-hub", + help="configured build containing private capture reference paths", + ) + parser.add_argument( + "--output", + required=True, + type=Path, + help="new JSON capture path; existing files are never overwritten", + ) + parser.add_argument( + "--timeout", + type=float, + default=90, + help="overall USB scenario deadline in seconds, at most 600 (default: 90)", + ) + parser.add_argument( + "--duration", + type=float, + default=10, + help="active input/read overlap duration, 2..120 seconds (default: 10)", + ) + parser.add_argument( + "--usb-timeout-ms", + type=int, + default=500, + help="per control/bulk transfer timeout, 20..3000 ms (default: 500)", + ) + parser.add_argument( + "--rumble-sample", + type=int, + choices=range(8), + help="OPT-IN: play native motor sample 0..7 once on each donor and require its ACK", + ) + args = parser.parse_args() + if not math.isfinite(args.timeout) or not 0 < args.timeout <= 600: + parser.error("timeout must be finite and in (0,600]") + if not math.isfinite(args.duration) or not 2 <= args.duration <= 120: + parser.error("duration must be finite and in [2,120]") + if args.timeout < args.duration + 10: + parser.error( + "timeout must allow at least duration + 10 seconds for discovery/initialization" + ) + if not 20 <= args.usb_timeout_ms <= 3000: + parser.error("usb-timeout-ms must be in [20,3000]") + try: + args.output.parent.mkdir(parents=True, exist_ok=True) + capture = args.output.open("x+", encoding="utf-8") + except OSError as error: + parser.error(f"cannot create a new capture: {error}") + + def interrupted(signum: int, _frame: Any) -> None: + raise KeyboardInterrupt(f"received signal {signum}") + + previous = signal.signal(signal.SIGTERM, interrupted) + try: + with capture: + return Check(args, capture).run() + finally: + signal.signal(signal.SIGTERM, previous) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/pico_usb_address_probe/CMakeLists.txt b/tools/pico_usb_address_probe/CMakeLists.txt new file mode 100644 index 0000000..aee90f3 --- /dev/null +++ b/tools/pico_usb_address_probe/CMakeLists.txt @@ -0,0 +1,45 @@ +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(pico_usb_address_probe C CXX ASM) +set(CMAKE_C_STANDARD 11) +set(CMAKE_CXX_STANDARD 17) +pico_sdk_init() + +if(NOT PICO_PLATFORM STREQUAL "rp2350-arm-s") + message(FATAL_ERROR "The native PHY timing probe requires the RP2350 ARM platform") +endif() + +option(PROBE_USB_GPIO_MODE "Enable native-pad SIO observation with SIO outputs disabled" ON) +set(TINYUSB_DIR ${PICO_SDK_PATH}/lib/tinyusb/src) +set(RP_USB_DIR ${TINYUSB_DIR}/portable/raspberrypi/rp2040) +add_executable(native_usb_address_probe + main.c + router.c + usb_probe.c + ${RP_USB_DIR}/dcd_rp2040.c + ${RP_USB_DIR}/rp2040_usb.c + ${TINYUSB_DIR}/common/tusb_fifo.c +) +target_include_directories(native_usb_address_probe PRIVATE + ${CMAKE_CURRENT_LIST_DIR} + ${TINYUSB_DIR} + ${RP_USB_DIR} +) +target_compile_definitions(native_usb_address_probe PRIVATE + CFG_TUSB_CONFIG_FILE="${CMAKE_CURRENT_LIST_DIR}/tusb_config.h" + CFG_TUSB_MCU=OPT_MCU_RP2040 + RP2040_USB_DEVICE_MODE=1 + PROBE_USB_GPIO_MODE=$ +) +target_compile_options(native_usb_address_probe PRIVATE -O3 -Wall -Wextra) +target_link_libraries(native_usb_address_probe PRIVATE + pico_stdlib pico_multicore hardware_structs hardware_irq hardware_resets + hardware_sync hardware_timer hardware_clocks hardware_vreg hardware_watchdog +) +pico_set_binary_type(native_usb_address_probe no_flash) +pico_enable_stdio_usb(native_usb_address_probe 0) +pico_enable_stdio_uart(native_usb_address_probe 1) +pico_set_program_name(native_usb_address_probe "RAM-only native USB address capability probe") +pico_set_program_version(native_usb_address_probe "0.5-native-sio-hub-probe") +pico_add_extra_outputs(native_usb_address_probe) diff --git a/tools/pico_usb_address_probe/host_probe.py b/tools/pico_usb_address_probe/host_probe.py new file mode 100755 index 0000000..1914ef1 --- /dev/null +++ b/tools/pico_usb_address_probe/host_probe.py @@ -0,0 +1,288 @@ +#!/usr/bin/env python3 +"""Exercise the RAM-only native USB address probe; never flash firmware.""" + +from __future__ import annotations + +import argparse +import json +import os +import struct +import subprocess +import time +from collections.abc import Iterable +from pathlib import Path +from typing import Any, cast + +import usb.core +import usb.util + +VID = 0x1209 +HUB_PID = 0x0001 +CHILD_PIDS = (0x0002, 0x0003) +FIELDS: tuple[str, ...] = ( + "magic", + "version", + "system_hz", + "routing_enabled", + "hub_address", + "child1_address", + "child2_address", + "default_slot", + "hub_setups", + "child1_setups", + "child2_setups", + "bad_setup_owner", + "observer_ready", + "sops", + "sync_ok", + "valid_tokens", + "valid_setups", + "crc_errors", + "late_samples", + "retargets", + "hub_tokens", + "child1_tokens", + "child2_tokens", + "cycles_per_bit", + "raw0", + "raw1", + "raw2", + "raw_count", + "raw_eop", + "raw_late", + "live_phy", + "correlated_setups", +) + + +def probe_devices(product_id: int) -> list[usb.core.Device]: + found = usb.core.find(find_all=True, idVendor=VID, idProduct=product_id) + return list(cast(Iterable[usb.core.Device], found)) if found is not None else [] + + +def device_location(device: usb.core.Device) -> tuple[int, int]: + bus, address = device.bus, device.address + if not isinstance(bus, int) or not isinstance(address, int): + raise TypeError("USB device has no usable bus/address") + return bus, address + + +def grant_access(device: usb.core.Device) -> None: + bus, address = device_location(device) + node = f"/dev/bus/usb/{bus:03d}/{address:03d}" + subprocess.run( + ["sudo", "-n", "setfacl", "-m", f"u:{os.getuid()}:rw", node], + check=True, + capture_output=True, + text=True, + timeout=3, + ) + + +def stats(device: usb.core.Device) -> dict[str, int]: + packet = bytes(device.ctrl_transfer(0xC0, 0x5A, 0, 0, 128, timeout=400)) + if len(packet) != 128: + raise RuntimeError(f"statistics length {len(packet)} != 128") + result = { + key: int(value) + for key, value in zip(FIELDS, struct.unpack("<32I", packet), strict=True) + } + if result["magic"] != 0x42554850 or result["version"] != 3: + raise RuntimeError("device did not return the address-probe signature") + return result + + +def descriptor(device: usb.core.Device, expected_pid: int) -> dict[str, int]: + data = bytes(device.ctrl_transfer(0x80, 6, 0x0100, 0, 18, timeout=400)) + if len(data) != 18 or data[0:2] != b"\x12\x01": + raise RuntimeError("invalid device descriptor") + vendor, product = struct.unpack_from(" int: + return (after[field] - before[field]) & 0xFFFFFFFF + + +def measure_phase(device: usb.core.Device, phase: int) -> dict[str, Any]: + device.ctrl_transfer(0x40, 0x5D, phase, 0, b"", timeout=400) + before = stats(device) + after = before + for _ in range(24): + after = stats(device) + time.sleep(0.002) + hardware = delta(after, before, "hub_setups") + confirmed = delta(after, before, "correlated_setups") + hits = delta(after, before, "hub_tokens") + # Qualify observed headers against real, CRC-accepted hardware SETUP IRQs. + # Full software CRC capture can overrun after routing work and is diagnostic. + credible = hardware >= 20 and hardware * 0.8 <= confirmed <= hardware * 1.5 + return { + "phase": phase, + "hardware_setups": hardware, + "correlated_setups": confirmed, + "crc_verified_setups": delta(after, before, "valid_setups"), + "matched_hub_tokens": hits, + "credible": credible, + "crc_errors": delta(after, before, "crc_errors"), + "late_samples": delta(after, before, "late_samples"), + "before": before, + "after": after, + } + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--wait-seconds", type=float, default=30) + parser.add_argument( + "--arm", + action="store_true", + help="attempt address routing only after credible passive capture", + ) + args = parser.parse_args() + if args.output.exists(): + parser.error("output already exists; choose a new capture filename") + if not 0 < args.wait_seconds <= 120: + parser.error("wait-seconds must be in (0,120]") + result: dict[str, Any] = { + "success": False, + "armed": False, + "phases": [], + "return_request_sent": False, + } + hub: usb.core.Device | None = None + print("[HOSTPROBE] waiting for RAM hub probe", flush=True) + try: + deadline = time.monotonic() + args.wait_seconds + while time.monotonic() < deadline: + devices = probe_devices(HUB_PID) + if len(devices) > 1: + raise RuntimeError( + "multiple matching hub probes; refusing ambiguous target" + ) + if devices: + hub = devices[0] + break + time.sleep(0.05) + if hub is None: + raise RuntimeError("RAM hub did not enumerate before timeout") + grant_access(hub) + result["hub"] = descriptor(hub, HUB_PID) + result["initial_stats"] = stats(hub) + print( + f"[HOSTPROBE] hub address={hub.address}, observer={result['initial_stats']['observer_ready']}", + flush=True, + ) + if result["initial_stats"]["observer_ready"] != 1: + result["failure"] = ( + "cycle-timed observer did not initialize; no address routing attempted" + ) + return 2 + phases = result["initial_stats"]["cycles_per_bit"] + if not 1 <= phases <= 64: + raise RuntimeError(f"invalid cycles-per-bit {phases}") + for phase in range(phases): + measured = measure_phase(hub, phase) + result["phases"].append(measured) + print( + f"[HOSTPROBE] phase={phase} confirmed={measured['correlated_setups']}/{measured['hardware_setups']} hits={measured['matched_hub_tokens']} late={measured['late_samples']} raw={measured['after']['raw0']:08x}/{measured['after']['raw1']:08x} n={measured['after']['raw_count']} eop={measured['after']['raw_eop']}", + flush=True, + ) + candidates = [item for item in result["phases"] if item["credible"]] + if not candidates: + result["failure"] = ( + "no sampling phase reliably observed native USB SETUP tokens; retargeting was not armed" + ) + return 2 + best = min( + candidates, + key=lambda item: ( + abs(item["correlated_setups"] - item["hardware_setups"]), + item["crc_errors"], + item["late_samples"], + ), + ) + hub.ctrl_transfer(0x40, 0x5D, best["phase"], 0, b"", timeout=400) + result["selected_phase"] = best["phase"] + if not args.arm: + result["passive_capture_verified"] = True + return 0 + hub.ctrl_transfer(0x40, 0x5B, 1, 0, b"", timeout=400) + result["armed"] = True + print( + "[HOSTPROBE] address retargeting armed; waiting for real downstream enumeration", + flush=True, + ) + children = {} + deadline = time.monotonic() + 5 + # Let the kernel finish downstream enumeration without injecting root + # control transfers into the probe's still-shared physical EP0 context. + # Five seconds is below the ACK-fed watchdog's eight-second deadline. + while time.monotonic() < deadline and len(children) != 2: + for port, pid in enumerate(CHILD_PIDS, 1): + found = probe_devices(pid) + if ( + len(found) == 1 + and found[0].bus == hub.bus + and hub.port_numbers is not None + and found[0].port_numbers == (*hub.port_numbers, port) + ): + children[pid] = found[0] + time.sleep(0.05) + if len(children) != 2: + result["failure"] = ( + "hub did not enumerate both separately addressed children" + ) + result["children_seen"] = [hex(pid) for pid in children] + return 2 + ordered = [hub, children[CHILD_PIDS[0]], children[CHILD_PIDS[1]]] + if len({device.address for device in ordered}) != 3: + raise RuntimeError("host did not assign three distinct USB addresses") + for child in ordered[1:]: + grant_access(child) + result["devices"] = [] + for _ in range(20): + for device, pid in zip(ordered, (HUB_PID, *CHILD_PIDS), strict=True): + identity = descriptor(device, pid) + result["devices"].append(identity) + result["latest_stats"] = stats(hub) + result["success"] = True + print( + "[HOSTPROBE] PASS: three actual addresses, each repeatedly returned its own descriptor", + flush=True, + ) + return 0 + except ( + usb.core.USBError, + RuntimeError, + TypeError, + subprocess.SubprocessError, + OSError, + ) as error: + result["failure"] = str(error) + print(f"[HOSTPROBE] failure: {error}", flush=True) + return 2 + finally: + if hub is not None: + try: + hub.ctrl_transfer(0x40, 0x5C, 0, 0, b"", timeout=400) + result["return_request_sent"] = True + except usb.core.USBError as error: + result["return_request_error"] = str(error) + usb.util.dispose_resources(hub) + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(json.dumps(result, indent=2) + "\n") + print( + f"[HOSTPROBE] saved {args.output}; RAM probe has an 8-second watchdog fallback", + flush=True, + ) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/pico_usb_address_probe/main.c b/tools/pico_usb_address_probe/main.c new file mode 100644 index 0000000..f3361ee --- /dev/null +++ b/tools/pico_usb_address_probe/main.c @@ -0,0 +1,62 @@ +#include +#include + +#include "hardware/clocks.h" +#include "hardware/structs/sio.h" +#include "hardware/structs/usb.h" +#include "hardware/vreg.h" +#include "hardware/watchdog.h" +#include "pico/multicore.h" +#include "pico/stdlib.h" +#include "router.h" +#include "usb_probe.h" + +#if !PICO_NO_FLASH +#error "The native USB address probe must run from RAM, never replace flash firmware" +#endif +#if !PICO_RP2350 +#error "The native USB address probe requires RP2350" +#endif + +int main(void) { + // A failed USB experiment must not strand the board in this RAM program. + // Explicit host GET_STATS requests are the only keepalive after startup. + watchdog_enable(8000, false); + vreg_set_voltage(VREG_VOLTAGE_1_30); + sleep_ms(10); + set_sys_clock_khz(240000, true); + stdio_init_all(); + printf("\n[HUBPROBE] RAM-only built-in USB address experiment, clock=%" PRIu32 " Hz\n", + clock_get_hz(clk_sys)); + printf("[HUBPROBE] No GPIO data wiring, Bluetooth, or flash writes; watchdog returns to stored firmware\n"); + + probe_router_init(clock_get_hz(clk_sys)); + multicore_launch_core1(probe_router_core1); + const uint32_t start = time_us_32(); + probe_router_stats observer = {0}; + do { + probe_router_snapshot(&observer); + if (observer.ready) break; + sleep_us(10); + } while ((uint32_t)(time_us_32() - start) < 100000); + printf("[HUBPROBE] Observer ready=%" PRIu32 " cycles/bit=%" PRIu32 "\n", + observer.ready, observer.cycles_per_bit); + probe_hub_init(); +#if PROBE_USB_GPIO_MODE + // With TO_PHY retained and SIO outputs disabled, hardware measurements + // showed both live SIO inputs and successful native-controller enumeration. + // Keep only the internal full-speed attachment resistor; do not drive data. + sio_hw->gpio_hi_oe_clr = SIO_GPIO_HI_IN_USB_DP_BITS | SIO_GPIO_HI_IN_USB_DM_BITS; + hw_set_bits(&usb_hw->phy_direct, USB_USBPHY_DIRECT_DP_PULLUP_EN_BITS); + hw_set_bits(&usb_hw->phy_direct_override, + USB_USBPHY_DIRECT_OVERRIDE_DP_PULLUP_EN_OVERRIDE_EN_BITS); + hw_set_bits(&usb_hw->muxing, USB_USB_MUXING_USBPHY_AS_GPIO_BITS); + printf("[HUBPROBE] USBPHY_AS_GPIO plus TO_PHY; SIO outputs disabled, internal pull-up retained\n"); +#endif + printf("[HUBPROBE] RX=SIO GPIO_HI_IN[25:24], mux=%08" PRIx32 "; SIO=%08" PRIx32 + " PHY=%08" PRIx32 "\n", usb_hw->muxing, sio_hw->gpio_hi_in, usb_hw->phy_direct); + while (true) { + probe_hub_task(); + sleep_us(100); + } +} diff --git a/tools/pico_usb_address_probe/router.c b/tools/pico_usb_address_probe/router.c new file mode 100644 index 0000000..debfce4 --- /dev/null +++ b/tools/pico_usb_address_probe/router.c @@ -0,0 +1,738 @@ +#include "router.h" + +#include + +#include "pico.h" +#include "hardware/structs/sio.h" +#include "hardware/structs/usb.h" +#include "hardware/sync.h" + +#if defined(SWITCH2_PROBE_HUB) && SWITCH2_PROBE_HUB +extern bool native_hub_select_device(uint8_t address, uint8_t owner, uint32_t cutoff); +#endif + +#if !PICO_RP2350 || defined(__riscv) +#error "The native PHY observer requires an RP2350 Arm core" +#endif + +// This sampler does not drive USB data. Main enables the native-pad input +// mux and attachment pull-up; the native SIE remains the USB transmitter. +// This isolated probe owns SIO MTIME, usable by a Secure Arm core. FULLSPEED +// makes it a zero-wait-state cycle counter next to the GPIO inputs, avoiding +// SysTick's PPB accesses and 24-bit down-counter arithmetic in every sample. +// Deadlines use modular 32-bit arithmetic for intervals below 2^31 cycles. +#define FS_CLOCK_HZ 240000000u +#define FS_BIT_CYCLES 20u +#define LINE_SE0 0u +#define LINE_J 1u +#define LINE_K 2u +#define LINE_SE1 3u +#define PID_OUT 0xe1u +#define PID_IN 0x69u +#define PID_SETUP 0x2du +#define NO_READER 2u +#define SETUP_SEQUENCE_MASK 0x3fffffffu +#define SETUP_SLOT_SHIFT 30u +#define SETUP_INVALID (3u << SETUP_SLOT_SHIFT) +#define RAW_BITS 40u + +_Static_assert(SIO_GPIO_HI_IN_USB_DP_BITS == (1u << 24), "SIO USB DP layout"); +_Static_assert(SIO_GPIO_HI_IN_USB_DM_BITS == (1u << 25), "SIO USB DM layout"); +_Static_assert(PROBE_ROUTER_SLOTS == 3u, "Packed setup owner has three slots"); + +typedef struct { + uint8_t owner[128]; +#if defined(SWITCH2_PROBE_HUB) && SWITCH2_PROBE_HUB + uint8_t early_address[2][16]; +#endif +} routing_table; + +// Complete physical NRZI SYNC+PID signatures. The PID's two distinguishing +// symbols index this table, but the entire signature must match. +static uint32_t token_words[16]; + +typedef struct { + uint32_t words[3]; + uint32_t count; + uint32_t retargets; + bool eop; + bool late; + bool sop; + bool resync; +} raw_packet; + +static routing_table tables[2]; +static probe_router_stats counters; +static uint32_t published_generation; +static uint32_t reader_index; +static uint32_t enabled; +static uint32_t phase_cycles; +static uint32_t setup_publication; +static uint32_t fatal_fault; +static bool valid_clock; +static uint8_t address_decoder[2][256]; +static bool address_decoder_ready; + +static __force_inline uint32_t atomic_read(const uint32_t* value) { + return __atomic_load_n(value, __ATOMIC_RELAXED); +} + +static __force_inline void atomic_write(uint32_t* value, uint32_t next) { + __atomic_store_n(value, next, __ATOMIC_RELAXED); +} + +// These counters have one writer (Core 1); only loads/stores, not exclusive +// read-modify-write loops, are needed. They are updated outside sample windows. +static __force_inline void count_one(uint32_t* counter) { + atomic_write(counter, atomic_read(counter) + 1u); +} + +static __force_inline void invalidate_setup(void) { + const uint32_t previous = atomic_read(&setup_publication); + __atomic_store_n(&setup_publication, + (previous & SETUP_SEQUENCE_MASK) | SETUP_INVALID, + __ATOMIC_RELEASE); +} + +static __force_inline void publish_setup(uint8_t slot) { + const uint32_t sequence = (atomic_read(&setup_publication) + 1u) & SETUP_SEQUENCE_MASK; + const uint32_t owner = slot < PROBE_ROUTER_SLOTS ? slot : 3u; + __atomic_store_n(&setup_publication, sequence | (owner << SETUP_SLOT_SHIFT), + __ATOMIC_RELEASE); +} + + +static void build_address_decoder(void) { + if (address_decoder_ready) return; + // C0 initializes once. Eight observed D+ symbols cover all seven address + // bits plus at most one stuffed bit. All three token PIDs end in K. + for (unsigned kind = 0; kind < 2; ++kind) { + for (unsigned wire = 0; wire < 256; ++wire) { + unsigned previous = 0, ones = kind ? 3u : 0u, bits = 0, address = 0; + bool valid = true; + for (unsigned n = 0; n < 8 && bits < 7; ++n) { + const unsigned line = (wire >> n) & 1u; + const unsigned bit = line == previous; + previous = line; + if (ones == 6u) { + if (bit != 0u) valid = false; + ones = 0; + continue; + } + address |= bit << bits++; + ones = bit ? ones + 1u : 0u; + } + address_decoder[kind][wire] = valid && bits == 7 ? + (uint8_t)address : PROBE_ROUTER_UNASSIGNED; + } + } + address_decoder_ready = true; +} + +static void build_table(routing_table* table, + const uint8_t addresses[PROBE_ROUTER_SLOTS], uint8_t default_slot) { + build_address_decoder(); + memset(table->owner, PROBE_ROUTER_UNASSIGNED, sizeof(table->owner)); + if (default_slot < PROBE_ROUTER_SLOTS) + table->owner[0] = default_slot; + for (uint8_t slot = 0; slot < PROBE_ROUTER_SLOTS; ++slot) { + const uint8_t address = addresses[slot]; + if (address == 0 || address >= 128) continue; + bool unique = true; + for (uint8_t other = 0; other < PROBE_ROUTER_SLOTS; ++other) { + if (other != slot && addresses[other] == address) + unique = false; + } + if (unique) + table->owner[address] = slot; + } +#if defined(SWITCH2_PROBE_HUB) && SWITCH2_PROBE_HUB + // A unique observed prefix can preselect the SIE sooner. It still compares + // the complete hardware address and CRC before accepting the transaction. + for (unsigned kind = 0; kind < 2; ++kind) { + for (unsigned prefix = 0; prefix < 16; ++prefix) { + uint8_t candidate = PROBE_ROUTER_UNASSIGNED; + for (unsigned suffix = 0; suffix < 16; ++suffix) { + uint8_t address = address_decoder[kind][prefix | (suffix << 4)]; + if (address >= 128 || table->owner[address] >= PROBE_ROUTER_SLOTS) continue; + if (candidate != PROBE_ROUTER_UNASSIGNED && candidate != address) { + candidate = PROBE_ROUTER_UNASSIGNED; + break; + } + candidate = address; + } + table->early_address[kind][prefix] = candidate; + } + } +#endif +} + +void probe_router_init(uint32_t system_clock_hz) { + // Explicit SRAM data: Core1 must never fetch flash during durable saves. + token_words[6] = 0xaa66a666u; + token_words[10] = 0x95a6a666u; + token_words[5] = 0x9a56a666u; + const uint8_t addresses[PROBE_ROUTER_SLOTS] = {0u, PROBE_ROUTER_UNASSIGNED, + PROBE_ROUTER_UNASSIGNED}; + memset(&counters, 0, sizeof(counters)); + published_generation = 0u; + reader_index = NO_READER; + enabled = 0u; + phase_cycles = 0u; + setup_publication = SETUP_INVALID; + fatal_fault = 0u; + valid_clock = system_clock_hz == FS_CLOCK_HZ; + counters.cycles_per_bit = system_clock_hz / 12000000u; + build_table(&tables[0], addresses, 0u); +} + +void probe_router_publish(const uint8_t addresses[PROBE_ROUTER_SLOTS], uint8_t default_slot) { + const uint32_t generation = atomic_read(&published_generation); + const uint32_t next_index = (generation + 1u) & 1u; + // A pointer swap alone is NOT safe double buffering: a second publication + // could overwrite the table still in use by a packet. The reader's hazard + // index protects that table until decoding finishes. Core 1 never waits. + // Bound the writer's wait as well: an unexpectedly stopped observer must + // not trap Core 0 or prevent the watchdog/reboot control path from running. + uint32_t remaining = 1000000u; + while (__atomic_load_n(&reader_index, __ATOMIC_SEQ_CST) == next_index) { + if (--remaining == 0u) { + atomic_write(&enabled, 0u); + atomic_write(&fatal_fault, 1u); + atomic_write(&counters.ready, 0u); + return; + } + } + build_table(&tables[next_index], addresses, default_slot); + __atomic_store_n(&published_generation, generation + 1u, __ATOMIC_SEQ_CST); +} + +void probe_router_enable(bool enable) { + // ARM qualification (observed hub tokens/SETUPs) belongs to the control + // request handler. This additionally prevents enabling a failed observer. + __atomic_store_n(&enabled, enable && atomic_read(&counters.ready) != 0u && + atomic_read(&fatal_fault) == 0u, __ATOMIC_RELEASE); +} + +bool probe_router_set_phase(uint32_t cycles) { + if (cycles >= atomic_read(&counters.cycles_per_bit) || atomic_read(&enabled) != 0u) + return false; + __atomic_store_n(&phase_cycles, cycles, __ATOMIC_RELEASE); + return true; +} + +void probe_router_snapshot(probe_router_stats* out) { +#define SNAPSHOT(member) out->member = atomic_read(&counters.member) + SNAPSHOT(ready); + SNAPSHOT(sops); + SNAPSHOT(sync_ok); + SNAPSHOT(valid_tokens); + SNAPSHOT(valid_setups); + SNAPSHOT(crc_errors); + SNAPSHOT(late_samples); + SNAPSHOT(retargets); + for (uint32_t slot = 0u; slot < PROBE_ROUTER_SLOTS; ++slot) + out->address_hits[slot] = atomic_read(&counters.address_hits[slot]); + SNAPSHOT(cycles_per_bit); + SNAPSHOT(last_pid); + SNAPSHOT(last_address); + for (uint32_t i = 0; i < 3; ++i) + out->last_raw[i] = atomic_read(&counters.last_raw[i]); + SNAPSHOT(last_raw_count); + SNAPSHOT(last_raw_eop); + SNAPSHOT(last_raw_late); +#undef SNAPSHOT + const uint32_t setup = __atomic_load_n(&setup_publication, __ATOMIC_ACQUIRE); + out->last_setup_sequence = setup & SETUP_SEQUENCE_MASK; + const uint32_t slot = setup >> SETUP_SLOT_SHIFT; + out->last_setup_slot = slot < PROBE_ROUTER_SLOTS ? slot : PROBE_ROUTER_UNASSIGNED; +} + +uint8_t probe_router_setup_slot(uint32_t* sequence) { + const uint32_t setup = __atomic_load_n(&setup_publication, __ATOMIC_ACQUIRE); + *sequence = setup & SETUP_SEQUENCE_MASK; + const uint32_t slot = setup >> SETUP_SLOT_SHIFT; + return slot < PROBE_ROUTER_SLOTS ? (uint8_t)slot : PROBE_ROUTER_UNASSIGNED; +} + +static __force_inline uint32_t cycles_now(void) { + return sio_hw->mtime; +} + +static __force_inline int32_t cycles_after(uint32_t now, uint32_t deadline) { + return (int32_t)(now - deadline); +} + +static __force_inline uint32_t receive_line(void) { + // Native-mode measurements returned zero here while PHY_DIRECT saw traffic. + // Main can select USBPHY_AS_GPIO to test the separate native-pad SIO path. + return (sio_hw->gpio_hi_in >> 24) & 3u; +} + +static __force_inline bool sample_line(uint32_t* deadline, uint32_t* line) { + uint32_t now; + do { + now = cycles_now(); + } while (cycles_after(now, *deadline) < 0); + // Reuse the wait-loop timestamp instead of a second timer access per bit. + // A full-bit overrun is definitely a missed sample. Edge-poll timing still + // needs calibration: the host correlates sampled headers with actual + // hardware-accepted SETUP requests before enabling address writes. + if (cycles_after(now, *deadline) >= (int32_t)FS_BIT_CYCLES) + return false; + *line = receive_line(); + *deadline += FS_BIT_CYCLES; + // Keep one rolling deadline. GCC's unrolled affine expansion otherwise + // retains SOP/phase and spills/rebuilds per-bit deadlines in the hot path. + __asm volatile ("" : "+r"(*deadline)); + return true; +} + +static __force_inline void route_header(const routing_table* table, uint32_t address, + bool setup, uint32_t initial_address, + uint32_t cutoff, raw_packet* packet) { + // TinyUSB clears SETUP_REC only AFTER copying the hardware-validated SETUP + // into its event callback. Until then, preserve both address and owner. + if (usb_hw->sie_status & USB_SIE_STATUS_SETUP_REC_BITS) + return; + invalidate_setup(); + if (address >= 128u || table->owner[address] >= PROBE_ROUTER_SLOTS) + return; +#if defined(SWITCH2_PROBE_HUB) && SWITCH2_PROBE_HUB + if (atomic_read(&enabled) != 0u) { + if (!native_hub_select_device((uint8_t)address, table->owner[address], cutoff)) + return; + if (initial_address != address) ++packet->retargets; + } +#else + if (initial_address != address && atomic_read(&enabled) != 0u) { + if (cycles_after(cycles_now(), cutoff) >= 0) { + packet->late = true; + return; + } + __dmb(); + usb_hw->dev_addr_ctrl = address; + ++packet->retargets; + } +#endif + // Candidate observations qualify calibration only. Runtime ownership + // comes from the hardware address frozen by SETUP_REC. A missed software + // candidate must not reject a correctly addressed, hardware-accepted SETUP. + if (setup) + publish_setup(table->owner[address]); +} + +// The timing-critical path samples the complete address before selecting the +// native SIE. Hardware SETUP acceptance qualifies the candidate; opportunistic +// full-token CRC decoding below is diagnostic, not an ownership authority. +static bool observe_idle_j(void); + +// Prepare before waiting for EOP: an ACK can be followed immediately by a poll. +#if defined(SWITCH2_PROBE_HUB) && SWITCH2_PROBE_HUB +static raw_packet __no_inline_not_in_flash_func(capture_packet)( + uint32_t phase, const routing_table* table, bool draining) { +prepare_capture:; +#else +static raw_packet __no_inline_not_in_flash_func(capture_packet)( + uint32_t phase, const routing_table* table) { +#endif + raw_packet result = {0}; + uint32_t word0 = LINE_K, word1 = 0u, word2 = 0u; + uint32_t address_wire = 0u; + const uint8_t* decoder = NULL; + uint32_t expected_word = 0u; + const uint32_t initial_address = usb_hw->dev_addr_ctrl; + #if defined(SWITCH2_PROBE_HUB) && SWITCH2_PROBE_HUB + const uint8_t* early_decoder = NULL; + #endif + // Complete capture preparation before looking for the edge. The first + // hardware traces showed that preparing this state after SOP lost bit 1. + // Later zero-valued accumulators must remain constants until first use; + // forcing them into live registers adds spills and unnecessary ORs. + __asm volatile ("" : "+r"(word0), "+m"(result) : : "memory"); + #if defined(SWITCH2_PROBE_HUB) && SWITCH2_PROBE_HUB + if (draining) { + const uint32_t stop = cycles_now() + FS_CLOCK_HZ / 10000u; + bool saw_se0 = false; + uint32_t se0_since = 0; + for (;;) { + uint32_t line = receive_line(), now = cycles_now(); + if (cycles_after(now,stop) >= 0) { result.resync = true; return result; } + if (line == LINE_SE0) { + if (!saw_se0) se0_since = now; + saw_se0 = true; + } else { + // Half a bit rejects pad skew while allowing late ACK EOP entry. + if (line == LINE_J && saw_se0 && + cycles_after(now,se0_since) >= (int32_t)(FS_BIT_CYCLES / 2u)) break; + if (line == LINE_J && observe_idle_j()) break; + saw_se0 = false; + } + } + } + #endif + uint32_t line = receive_line(); + if (line != LINE_J) { + result.resync = true; + return result; + } + // A falling D+ leaves full-speed idle. Inspect the complete captured pair + // before accepting K; defer normalization until after the polling loop. + // Eight straight polls amortize loop bookkeeping and reduce edge jitter. + uint32_t pins; +#define POLL_IDLE() do { \ + pins = sio_hw->gpio_hi_in; \ + if ((pins & SIO_GPIO_HI_IN_USB_DP_BITS) == 0u) goto edge; \ + } while (0) + #if defined(SWITCH2_PROBE_HUB) && SWITCH2_PROBE_HUB + for (;;) { + POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); + POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); + POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); + POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); + POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); + POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); + POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); + POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); + // Keep the prepared frame while idle; leave only for a table update/fault. + if ((atomic_read(&published_generation) & 1u) != atomic_read(&reader_index) || + atomic_read(&fatal_fault) != 0u) return result; + } + #else + for (unsigned poll = 0; poll < 512u; ++poll) { + POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); + POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); POLL_IDLE(); + } + #endif +#undef POLL_IDLE + return result; +edge: + line = (pins >> 24) & 3u; + if (line != LINE_K) { + result.resync = true; + return result; + } + const uint32_t sop_time = cycles_now(); + // This timestamp follows the PHY read and edge-detection instructions. + // Captures showed an extra full-bit delay skipped SYNC's second symbol. + // Sweep the next sample relative to read completion, then keep 20-cycle + // spacing; every stored line symbol is still physically observed. + uint32_t deadline = sop_time + phase; + result.sop = true; + // The first stored K is the observed SOP above, not an invented SYNC bit. +#if defined(SWITCH2_PROBE_HUB) && SWITCH2_PROBE_HUB +#define SET_EARLY_DECODER(kind) (early_decoder = table->early_address[kind]) +#define DISCARD_NON_TOKEN() do { draining = true; goto prepare_capture; } while (0) +#define ROUTE_EARLY(bit, base) do { \ + if ((base) + (bit) == 19u && decoder != NULL) { \ + uint8_t candidate = early_decoder[address_wire]; \ + if (candidate < 128u) \ + route_header(table, candidate, word0 == 0x9a56a666u, initial_address, \ + deadline + 11u * FS_BIT_CYCLES, &result); \ + } \ + } while (0) +#else +#define SET_EARLY_DECODER(kind) ((void)0) +#define DISCARD_NON_TOKEN() ((void)0) +#define ROUTE_EARLY(bit, base) ((void)0) +#endif +#define ROUTE_BITS(word, bit, base) do { \ + if ((base) + (bit) == 11u) { \ + const uint32_t index = (word0 >> 20) & 15u; \ + expected_word = token_words[index]; \ + decoder = address_decoder[index == 6u]; \ + SET_EARLY_DECODER(index == 6u); \ + } \ + if ((base) + (bit) == 15u && word0 != expected_word) { \ + decoder = NULL; \ + DISCARD_NON_TOKEN(); \ + } \ + if ((base) + (bit) >= 16u && (base) + (bit) <= 23u) \ + address_wire |= (line & 1u) << (bit); \ + ROUTE_EARLY(bit, base); \ + if ((base) + (bit) == 23u && decoder != NULL) { \ + route_header(table, decoder[address_wire], word0 == 0x9a56a666u, \ + initial_address, deadline + 7u * FS_BIT_CYCLES, &result); \ + if (result.late) { result.count = (base) + (bit) + 1u; goto done; } \ + } \ + } while (0) +#define CAPTURE(word, bit, base) do { \ + if (!sample_line(&deadline, &line)) { \ + result.count = (base) + (bit); goto late; \ + } \ + if (line == LINE_SE0) { result.count = (base) + (bit); goto eop; } \ + (word) |= line << (2u * (bit)); \ + ROUTE_BITS(word, bit, base); \ + } while (0) +#define CAPTURE_16(word, base) \ + CAPTURE(word, 0u, base); CAPTURE(word, 1u, base); \ + CAPTURE(word, 2u, base); CAPTURE(word, 3u, base); \ + CAPTURE(word, 4u, base); CAPTURE(word, 5u, base); \ + CAPTURE(word, 6u, base); CAPTURE(word, 7u, base); \ + CAPTURE(word, 8u, base); CAPTURE(word, 9u, base); \ + CAPTURE(word, 10u, base); CAPTURE(word, 11u, base); \ + CAPTURE(word, 12u, base); CAPTURE(word, 13u, base); \ + CAPTURE(word, 14u, base); CAPTURE(word, 15u, base) + CAPTURE(word0, 1u, 0u); CAPTURE(word0, 2u, 0u); + CAPTURE(word0, 3u, 0u); CAPTURE(word0, 4u, 0u); + CAPTURE(word0, 5u, 0u); CAPTURE(word0, 6u, 0u); + CAPTURE(word0, 7u, 0u); CAPTURE(word0, 8u, 0u); + CAPTURE(word0, 9u, 0u); CAPTURE(word0, 10u, 0u); + CAPTURE(word0, 11u, 0u); CAPTURE(word0, 12u, 0u); + CAPTURE(word0, 13u, 0u); CAPTURE(word0, 14u, 0u); + CAPTURE(word0, 15u, 0u); + CAPTURE_16(word1, 16u); + CAPTURE(word2, 0u, 32u); CAPTURE(word2, 1u, 32u); + CAPTURE(word2, 2u, 32u); CAPTURE(word2, 3u, 32u); + CAPTURE(word2, 4u, 32u); CAPTURE(word2, 5u, 32u); + CAPTURE(word2, 6u, 32u); CAPTURE(word2, 7u, 32u); +#undef CAPTURE_16 +#undef CAPTURE +#undef ROUTE_EARLY +#undef SET_EARLY_DECODER +#undef DISCARD_NON_TOKEN + result.count = RAW_BITS; + goto done; +eop: + // Full-speed EOP is two bit times of SE0 followed by one J bit. A reset, + // truncated packet, or SE1 is not a token. Check all three samples. + if (!sample_line(&deadline, &line)) + goto late; + if (line != LINE_SE0) + goto done; + if (!sample_line(&deadline, &line)) + goto late; + result.eop = line == LINE_J; + goto done; +late: + result.late = true; +done: + result.words[0] = word0; + result.words[1] = word1; + result.words[2] = word2; + return result; +} + + +static bool __not_in_flash_func(observe_idle_j)(void) { + // Stuffing prohibits eight consecutive J bit times inside a packet. + // Use tight PHY polling, not sparse timer-paced reads that could miss K. + const uint32_t start = cycles_now(); + for (uint32_t i = 0; i < 64u; ++i) { + if (receive_line() != LINE_J) + return false; + } + return cycles_after(cycles_now(), start) >= (int32_t)(8u * FS_BIT_CYCLES); +} + +#if !defined(SWITCH2_PROBE_HUB) || !SWITCH2_PROBE_HUB +static __force_inline uint32_t raw_line(const raw_packet* packet, uint32_t bit) { + return (packet->words[bit >> 4] >> ((bit & 15u) * 2u)) & 3u; +} + +static void __not_in_flash_func(decode_packet)(const raw_packet* packet, const routing_table* table) { + // With LSB-first two-bit line samples, K J K J K J K K is 0xa666. + if (packet->count < 8u || (packet->words[0] & 0xffffu) != 0xa666u) { + return; + } + count_one(&counters.sync_ok); + uint32_t previous = LINE_K; + uint32_t ones = 1u; // Final decoded SYNC bit is one. + uint32_t decoded = 0u; + uint32_t value = 0u; + uint32_t pid = 0u; + bool token = false; + for (uint32_t wire_bit = 8u; wire_bit < packet->count; ++wire_bit) { + const uint32_t line = raw_line(packet, wire_bit); + if (line != LINE_J && line != LINE_K) { + return; + } + const uint32_t bit = line == previous; + previous = line; + if (ones == 6u) { + if (bit != 0u) { + return; + } + ones = 0u; + continue; + } + ones = bit != 0u ? ones + 1u : 0u; + if (decoded < 8u) { + pid |= bit << decoded; + ++decoded; + if (decoded == 8u) { + if ((((pid >> 4) ^ pid) & 15u) != 15u) { + return; + } + atomic_write(&counters.last_pid, pid); + token = pid == PID_IN || pid == PID_OUT || pid == PID_SETUP; + if (!token) + return; // Do not parse device data, SOFs, or handshakes. + } + } else { + if (decoded == 24u) { + return; + } + value |= bit << (decoded - 8u); + ++decoded; + } + } + if (!token || decoded != 24u || ones == 6u || !packet->eop || packet->late) { + return; + } + uint32_t crc = 0x1fu; + for (uint32_t bit = 0u; bit < 11u; ++bit) { + const uint32_t feedback = (crc ^ (value >> bit)) & 1u; + crc >>= 1; + if (feedback != 0u) + crc ^= 0x14u; // Reflected x^5 + x^2 + 1. + } + if (((crc ^ 0x1fu) & 0x1fu) != (value >> 11)) { + count_one(&counters.crc_errors); + return; + } + const uint32_t address = value & 0x7fu; + const uint8_t owner = table->owner[address]; + atomic_write(&counters.last_address, address); + count_one(&counters.valid_tokens); + if (owner < PROBE_ROUTER_SLOTS) + count_one(&counters.address_hits[owner]); + if (pid == PID_SETUP) { + count_one(&counters.valid_setups); + } +} + +#endif + +static const routing_table* __not_in_flash_func(acquire_table)(uint32_t* generation) { + for (;;) { + const uint32_t selected = __atomic_load_n(&published_generation, __ATOMIC_SEQ_CST); + __atomic_store_n(&reader_index, selected & 1u, __ATOMIC_SEQ_CST); + if (__atomic_load_n(&published_generation, __ATOMIC_SEQ_CST) == selected) { + *generation = selected; + return &tables[selected & 1u]; + } + } +} + +static void __not_in_flash_func(observer_failed)(void) { + atomic_write(&enabled, 0u); + atomic_write(&counters.ready, 0u); + invalidate_setup(); + __atomic_store_n(&reader_index, NO_READER, __ATOMIC_SEQ_CST); + for (;;) + __wfe(); +} + +void __not_in_flash_func(probe_router_core1)(void) { + (void)save_and_disable_interrupts(); + if (!valid_clock || atomic_read(&fatal_fault) != 0u) + observer_failed(); + + sio_hw->mtime_ctrl = 0u; + sio_hw->mtimecmp = UINT32_MAX; + sio_hw->mtimecmph = UINT32_MAX; + sio_hw->mtime = 0u; + sio_hw->mtimeh = 0u; + sio_hw->mtime_ctrl = SIO_MTIME_CTRL_EN_BITS | SIO_MTIME_CTRL_FULLSPEED_BITS; + __dsb(); + __isb(); + bool timer_running = false; + uint32_t previous_timer = cycles_now(); + for (uint32_t attempt = 0u; attempt < 256u; ++attempt) { + const uint32_t now = cycles_now(); + const int32_t elapsed = cycles_after(now, previous_timer); + if (elapsed > 0 && elapsed < 1024) { + timer_running = true; + break; + } + previous_timer = now; + } + if (!timer_running) + observer_failed(); + atomic_write(&counters.ready, 1u); + + uint32_t generation; + const routing_table* table = acquire_table(&generation); + // Resynchronize at qualified EOP or a long idle J, never an arbitrary + // data transition. An idle gap must not cost the next control's SETUP. + bool draining = true; + #if !defined(SWITCH2_PROBE_HUB) || !SWITCH2_PROBE_HUB + bool saw_se0 = false; + uint32_t se0_since = 0u; + #endif + for (;;) { + if (atomic_read(&fatal_fault) != 0u) + observer_failed(); + const uint32_t phase = atomic_read(&phase_cycles); + for (;;) { + if (atomic_read(&published_generation) != generation) + table = acquire_table(&generation); + #if !defined(SWITCH2_PROBE_HUB) || !SWITCH2_PROBE_HUB + if (draining) { + const uint32_t line = receive_line(); + const uint32_t now = cycles_now(); + if (line == LINE_SE0) { + if (!saw_se0) se0_since = now; + saw_se0 = true; + } else { + // Reject momentary pad skew as EOP. A real SE0 persists + // across at least one complete bit before returning to J. + if (line == LINE_J && saw_se0 && + cycles_after(now, se0_since) >= (int32_t)FS_BIT_CYCLES) + draining = false; + else if (line == LINE_J && observe_idle_j()) + draining = false; + saw_se0 = false; + } + if (draining) continue; + break; + } + #endif + break; + } + // Phase is relative to the observed J->K edge, not a promised physical + // edge timestamp. The host sweeps 0..19 cycles and correlates sampled + // headers with the native DCD's CRC-accepted SETUP interrupts. A successful + // passive phase still does NOT prove when the SIE latches its address. + // Calibrate the real routing instruction path, not a lighter sampler + // whose phase/register allocation changes when routing is enabled. + // The independent enabled flag still forbids every dry-run USB write. + #if defined(SWITCH2_PROBE_HUB) && SWITCH2_PROBE_HUB + const raw_packet packet = capture_packet(phase, table, draining); + #else + const raw_packet packet = capture_packet(phase, table); + #endif + if (!packet.sop) { + if (packet.resync) { + draining = true; + #if !defined(SWITCH2_PROBE_HUB) || !SWITCH2_PROBE_HUB + saw_se0 = false; + #endif + } + continue; + } + #if !defined(SWITCH2_PROBE_HUB) || !SWITCH2_PROBE_HUB + for (uint32_t i = 0; i < 3; ++i) + atomic_write(&counters.last_raw[i], packet.words[i]); + atomic_write(&counters.last_raw_count, packet.count); + atomic_write(&counters.last_raw_eop, packet.eop); + atomic_write(&counters.last_raw_late, packet.late); + count_one(&counters.sops); + atomic_write(&counters.retargets, atomic_read(&counters.retargets) + packet.retargets); + if (packet.late) + count_one(&counters.late_samples); + decode_packet(&packet, table); + // Decoding can outlast the minimum interpacket gap. Qualify another + // EOP or a long idle J before accepting a new SOP; an arbitrary J->K + // inside a packet is not a start. Missing traffic is preferable to + // manufacturing a SETUP owner from a payload transition. + draining = true; + saw_se0 = false; + #else + // A response may start during the return/preparation path even if the + // preceding EOP was sampled. Requalify from the prepared capture frame. + draining = true; + #endif + } +} diff --git a/tools/pico_usb_address_probe/router.h b/tools/pico_usb_address_probe/router.h new file mode 100644 index 0000000..d68903d --- /dev/null +++ b/tools/pico_usb_address_probe/router.h @@ -0,0 +1,48 @@ +#pragma once + +#include +#include + +#define PROBE_ROUTER_SLOTS 3u +#define PROBE_ROUTER_UNASSIGNED 0xffu + +typedef struct { + uint32_t ready; + uint32_t sops; + uint32_t sync_ok; + uint32_t valid_tokens; + uint32_t valid_setups; + uint32_t crc_errors; + uint32_t late_samples; + uint32_t retargets; + uint32_t address_hits[PROBE_ROUTER_SLOTS]; + uint32_t cycles_per_bit; + uint32_t last_pid; + uint32_t last_address; + uint32_t last_setup_sequence; + uint32_t last_setup_slot; + uint32_t last_raw[3]; + uint32_t last_raw_count; + uint32_t last_raw_eop; + uint32_t last_raw_late; +} probe_router_stats; + +// Core 0 initializes before launching Core 1. The native SIE drives USB; +// Core 1 observes the existing socket and selects known device addresses. +void probe_router_init(uint32_t system_clock_hz); +void probe_router_core1(void); + +// Core 0 publishes assigned addresses (0xff means unassigned). Address zero +// belongs only to default_slot, or nobody when default_slot is 0xff. + +void probe_router_publish(const uint8_t addresses[PROBE_ROUTER_SLOTS], uint8_t default_slot); +void probe_router_enable(bool enabled); +// Sampling phase within one USB bit, for passive timing calibration only. +// Reject out-of-range values and changes after address retargeting is enabled. +bool probe_router_set_phase(uint32_t cycles); + +// Diagnostic snapshots. Counters are individually atomic, not a transaction. +void probe_router_snapshot(probe_router_stats* out); +// Last sampled SETUP-header candidate, used for calibration correlation only. +// Runtime control ownership comes from the SIE address at its SETUP interrupt. +uint8_t probe_router_setup_slot(uint32_t* sequence); diff --git a/tools/pico_usb_address_probe/tusb_config.h b/tools/pico_usb_address_probe/tusb_config.h new file mode 100644 index 0000000..7a19662 --- /dev/null +++ b/tools/pico_usb_address_probe/tusb_config.h @@ -0,0 +1,13 @@ +#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_TUSB_DEBUG 0 +#define CFG_TUD_ENDPOINT0_SIZE 64 +#define CFG_TUD_HID 0 +#define CFG_TUD_CDC 0 +#define CFG_TUD_MSC 0 +#define CFG_TUD_MIDI 0 +#define CFG_TUD_VENDOR 0 diff --git a/tools/pico_usb_address_probe/usb_probe.c b/tools/pico_usb_address_probe/usb_probe.c new file mode 100644 index 0000000..10d004a --- /dev/null +++ b/tools/pico_usb_address_probe/usb_probe.c @@ -0,0 +1,839 @@ +// RAM-only native-SIE address-retargeting experiment. These are vendor test +// devices, not controllers. No usbd/tud global-device state is linked here. +#include "usb_probe.h" +#include "router.h" + +#include +#include +#include + +#include "device/dcd.h" +#include "hardware/clocks.h" +#include "hardware/structs/usb.h" +#include "hardware/sync.h" +#include "hardware/uart.h" +#include "hardware/watchdog.h" +#include "pico/stdlib.h" + +#define RHPORT 0u +#define EP0_OUT 0x00u +#define EP0_IN 0x80u +#define HUB_EP 0x81u +#define EP0_SIZE 64u +#define EVENT_CAPACITY 32u +#define PORT_COUNT 2u + +#define PORT_CONNECTION 0x0001u +#define PORT_ENABLE 0x0002u +#define PORT_SUSPEND 0x0004u +#define PORT_RESET 0x0010u +#define PORT_POWER 0x0100u +#define C_CONNECTION 0x0001u +#define C_ENABLE 0x0002u +#define C_SUSPEND 0x0004u +#define C_RESET 0x0010u + +enum { + FEATURE_PORT_ENABLE = 1, + FEATURE_PORT_SUSPEND = 2, + FEATURE_PORT_RESET = 4, + FEATURE_PORT_POWER = 8, + FEATURE_C_CONNECTION = 16, + FEATURE_C_ENABLE = 17, + FEATURE_C_SUSPEND = 18, + FEATURE_C_OVERCURRENT = 19, + FEATURE_C_RESET = 20, +}; + +typedef enum { + CTRL_IDLE, + CTRL_DATA_IN, + CTRL_STATUS_IN, + CTRL_STATUS_OUT, + CTRL_STALLED, +} control_stage; + +typedef enum { + ACTION_NONE, + ACTION_ADDRESS, + ACTION_CONFIGURATION, + ACTION_INTERFACE, + ACTION_HALT, + ACTION_CLEAR_HALT, + ACTION_PORT_SET, + ACTION_PORT_CLEAR, + ACTION_KEEPALIVE, + ACTION_ARM, + ACTION_REBOOT, +} control_action; + +typedef struct { + uint16_t status; + uint16_t change; + uint32_t reset_deadline; + uint32_t resume_deadline; + bool resetting; + bool resuming; +} hub_port; + +typedef struct { + dcd_event_t event; + uint32_t generation; + uint32_t endpoint_epoch; + uint8_t setup_slot; +} queued_event; + +typedef struct { + tusb_control_request_t request; + uint32_t generation; + uint16_t length; + uint16_t sent; + uint16_t packet_length; + uint8_t owner; + control_stage stage; + control_action action; + bool need_zlp; +} control_transfer; + +static uint8_t addresses[PROBE_ROUTER_SLOTS]; +static uint8_t configurations[PROBE_ROUTER_SLOTS]; +static uint8_t default_slot; +static bool routing_enabled; +static hub_port ports[PORT_COUNT]; +static control_transfer control; +static uint8_t control_data[128] TU_ATTR_ALIGNED(4); +// Even a malformed nonempty status OUT cannot make the DCD copy into NULL. +static uint8_t control_out[EP0_SIZE] TU_ATTR_ALIGNED(4); +static uint32_t setup_count[PROBE_ROUTER_SLOTS]; +static uint32_t bad_setup_owner; +static uint32_t correlated_setups; +static uint32_t system_clock_hz; +static bool interrupt_open; +static bool interrupt_pending; +static bool interrupt_halted; +static uint8_t interrupt_bitmap; +static uint32_t endpoint_epoch; +static bool reboot_pending; +static bool failed; + +// Only the DCD IRQ produces; only Core 0's task consumes. All task-side DCD +// operations run with USB IRQ disabled. The IRQ never rearms a transfer: this +// SDK resets its transfer state *after* invoking dcd_event_handler(). +static queued_event events[EVENT_CAPACITY]; +static volatile uint32_t event_head; +static volatile uint32_t event_tail; +static volatile uint32_t event_generation; +static volatile bool event_overflow; +static uint32_t observed_setup_sequence; + +static const uint8_t hub_configuration[] = { + 9, 2, 25, 0, 1, 1, 0, 0x80, 50, + 9, 4, 0, 0, 1, 9, 0, 0, 0, + 7, 5, HUB_EP, 3, 1, 0, 12, +}; +static const uint8_t child_configuration[] = { + 9, 2, 18, 0, 1, 1, 0, 0x80, 0, + 9, 4, 0, 0, 0, 0xff, 0, 0, 0, +}; +static const uint8_t hub_descriptor[] = { + // Individual logical port power, no overcurrent sensing, 10ms power-good. + // Both embedded vendor children are non-removable; USB 1.1 full-speed hub. + 9, 0x29, PORT_COUNT, 0x11, 0, 5, 100, 0x06, 0xff, +}; +static const tusb_desc_endpoint_t hub_endpoint = { + .bLength = 7, + .bDescriptorType = TUSB_DESC_ENDPOINT, + .bEndpointAddress = HUB_EP, + .bmAttributes = { .xfer = TUSB_XFER_INTERRUPT }, + .wMaxPacketSize = 1, + .bInterval = 12, +}; + +static void put16(uint8_t* out, uint16_t value) { + out[0] = (uint8_t)value; + out[1] = (uint8_t)(value >> 8); +} + +static void put32(uint8_t* out, uint32_t value) { + put16(out, (uint16_t)value); + put16(out + 2, (uint16_t)(value >> 16)); +} + +static void publish_addresses(void) { + probe_router_publish(addresses, default_slot); +} + +static void stall_control(void) { + control.stage = CTRL_STALLED; + control.action = ACTION_NONE; + dcd_edpt_stall(RHPORT, EP0_OUT); + dcd_edpt_stall(RHPORT, EP0_IN); +} + +static bool queue_control(uint8_t endpoint, uint8_t* data, uint16_t length) { + if (dcd_edpt_xfer(RHPORT, endpoint, data, length)) return true; + stall_control(); + return false; +} + +static void status_in(control_action action) { + control.action = action; + control.stage = CTRL_STATUS_IN; + queue_control(EP0_IN, control_out, 0); +} + +static void next_control_packet(void) { + uint16_t remaining = (uint16_t)(control.length - control.sent); + control.packet_length = remaining > EP0_SIZE ? EP0_SIZE : remaining; + if (remaining == 0) control.need_zlp = false; + control.stage = CTRL_DATA_IN; + queue_control(EP0_IN, control_data + control.sent, control.packet_length); +} + +static void reply_data(uint16_t length) { + control.length = length < control.request.wLength ? length : control.request.wLength; + control.sent = 0; + control.need_zlp = length < control.request.wLength && (length % EP0_SIZE) == 0; + if (control.request.wLength == 0) { + control.stage = CTRL_STATUS_OUT; + queue_control(EP0_OUT, control_out, 0); + } else { + next_control_packet(); + } +} + +static void reply_copy(const uint8_t* data, uint16_t length) { + memcpy(control_data, data, length); + reply_data(length); +} + +static void reply_word(uint16_t value, uint16_t length) { + put16(control_data, value); + reply_data(length); +} + +static uint8_t changed_ports(void) { + uint8_t bitmap = 0; + for (unsigned i = 0; i < PORT_COUNT; ++i) { + if (ports[i].change) bitmap |= (uint8_t)(1u << (i + 1)); + } + return bitmap; +} + +static void arm_interrupt(void) { + if (!interrupt_open || interrupt_pending || interrupt_halted) return; + interrupt_bitmap = changed_ports(); + if (!interrupt_bitmap) return; // NAK until a hub/port change exists. + interrupt_pending = dcd_edpt_xfer(RHPORT, HUB_EP, &interrupt_bitmap, 1); + if (!interrupt_pending) failed = true; +} + +static void close_interrupt(void) { + ++endpoint_epoch; + dcd_edpt_close_all(RHPORT); + interrupt_open = false; + interrupt_pending = false; + interrupt_halted = false; +} + +static void open_interrupt(void) { + close_interrupt(); + interrupt_open = dcd_edpt_open(RHPORT, &hub_endpoint); + if (!interrupt_open) failed = true; +} + +static void forget_child(unsigned port) { + uint8_t slot = (uint8_t)(port + 1); + addresses[slot] = PROBE_ROUTER_UNASSIGNED; + configurations[slot] = 0; + if (default_slot == slot) default_slot = PROBE_ROUTER_UNASSIGNED; +} + +static void reset_bus_state(void) { + probe_router_enable(false); + routing_enabled = false; + correlated_setups = 0; + addresses[0] = 0; + addresses[1] = PROBE_ROUTER_UNASSIGNED; + addresses[2] = PROBE_ROUTER_UNASSIGNED; + default_slot = 0; + memset(configurations, 0, sizeof(configurations)); + memset(ports, 0, sizeof(ports)); + memset(&control, 0, sizeof(control)); + interrupt_open = false; + interrupt_pending = false; + interrupt_halted = false; + ++endpoint_epoch; + publish_addresses(); + usb_hw->dev_addr_ctrl = 0; +} + +static void fill_stats(void) { + probe_router_stats router; + probe_router_snapshot(&router); + const uint32_t words[32] = { + 0x42554850u, 3u, system_clock_hz, routing_enabled, + addresses[0], addresses[1], addresses[2], default_slot, + setup_count[0], setup_count[1], setup_count[2], bad_setup_owner, + router.ready, router.sops, router.sync_ok, router.valid_tokens, + router.valid_setups, router.crc_errors, router.late_samples, + router.retargets, router.address_hits[0], router.address_hits[1], + router.address_hits[2], router.cycles_per_bit, + router.last_raw[0], router.last_raw[1], router.last_raw[2], + router.last_raw_count, router.last_raw_eop, router.last_raw_late, + usb_hw->phy_direct, correlated_setups, + }; + for (unsigned i = 0; i < 32; ++i) put32(control_data + 4 * i, words[i]); +} + +static bool get_descriptor(void) { + const tusb_control_request_t* request = &control.request; + uint8_t type = (uint8_t)(request->wValue >> 8); + uint8_t index = (uint8_t)request->wValue; + if (type == TUSB_DESC_DEVICE && index == 0 && request->wIndex == 0) { + uint8_t descriptor[] = { + 18, 1, 0x10, 0x01, 0, 0, 0, EP0_SIZE, + 0x09, 0x12, 0, 0, 0x00, 0x01, 1, 2, 3, 1, + }; + descriptor[4] = control.owner == 0 ? 9 : 0; + descriptor[10] = (uint8_t)(control.owner + 1); + reply_copy(descriptor, sizeof(descriptor)); + return true; + } + if (type == TUSB_DESC_CONFIGURATION && index == 0 && request->wIndex == 0) { + if (control.owner == 0) reply_copy(hub_configuration, sizeof(hub_configuration)); + else reply_copy(child_configuration, sizeof(child_configuration)); + return true; + } + if (type != TUSB_DESC_STRING) return false; + if (index == 0 && request->wIndex == 0) { + static const uint8_t languages[] = {4, 3, 0x09, 0x04}; + reply_copy(languages, sizeof(languages)); + return true; + } + if (request->wIndex != 0x0409) return false; + const char* text; + if (index == 1) text = "Native USB capability probe"; + else if (index == 2) { + static const char* const products[] = { + "RP2350 native hub probe", + "RP2350 vendor probe child 1", + "RP2350 vendor probe child 2", + }; + text = products[control.owner]; + } else if (index == 3) { + static const char* const serials[] = {"PHUB-ROOT", "PHUB-CHILD1", "PHUB-CHILD2"}; + text = serials[control.owner]; + } else return false; + uint16_t length = (uint16_t)strlen(text); + control_data[0] = (uint8_t)(2 + 2 * length); + control_data[1] = TUSB_DESC_STRING; + for (uint16_t i = 0; i < length; ++i) put16(control_data + 2 + 2 * i, (uint8_t)text[i]); + reply_data((uint16_t)(2 + 2 * length)); + return true; +} + +static bool endpoint_exists(uint16_t index) { + return index == EP0_OUT || index == EP0_IN || + (index == HUB_EP && control.owner == 0 && configurations[0] == 1); +} + +static bool standard_request(void) { + const tusb_control_request_t* request = &control.request; + uint8_t slot = control.owner; + switch (request->bRequest) { + case TUSB_REQ_GET_DESCRIPTOR: + return request->bmRequestType == 0x80 && get_descriptor(); + case TUSB_REQ_SET_ADDRESS: + if (request->bmRequestType != 0 || request->wValue > 127 || + request->wIndex || request->wLength || configurations[slot]) return false; + if (request->wValue == 0 && default_slot != PROBE_ROUTER_UNASSIGNED && default_slot != slot) + return false; + for (unsigned i = 0; i < PROBE_ROUTER_SLOTS; ++i) { + if (i != slot && addresses[i] == request->wValue) return false; + } + status_in(ACTION_ADDRESS); + return true; + case TUSB_REQ_GET_CONFIGURATION: + if (request->bmRequestType != 0x80 || request->wValue || request->wIndex || request->wLength != 1) + return false; + reply_word(configurations[slot], 1); + return true; + case TUSB_REQ_SET_CONFIGURATION: + if (request->bmRequestType != 0 || request->wValue > 1 || request->wIndex || request->wLength || + addresses[slot] == 0 || addresses[slot] == PROBE_ROUTER_UNASSIGNED) return false; + status_in(ACTION_CONFIGURATION); + return true; + case TUSB_REQ_GET_STATUS: + if (request->wValue || request->wLength != 2) return false; + if (request->bmRequestType == 0x80 && request->wIndex == 0) { + reply_word(0, 2); // Bus powered; no remote wakeup capability. + return true; + } + if (request->bmRequestType == 0x81 && request->wIndex == 0 && configurations[slot]) { + reply_word(0, 2); + return true; + } + if (request->bmRequestType == 0x82 && endpoint_exists(request->wIndex)) { + reply_word(request->wIndex == HUB_EP && interrupt_halted ? 1 : 0, 2); + return true; + } + return false; + case TUSB_REQ_CLEAR_FEATURE: + case TUSB_REQ_SET_FEATURE: + if (request->bmRequestType != 0x02 || request->wValue != 0 || request->wIndex != HUB_EP || + request->wLength || slot != 0 || !configurations[0]) return false; + status_in(request->bRequest == TUSB_REQ_SET_FEATURE ? ACTION_HALT : ACTION_CLEAR_HALT); + return true; + case TUSB_REQ_GET_INTERFACE: + if (request->bmRequestType != 0x81 || request->wValue || request->wIndex || + request->wLength != 1 || !configurations[slot]) return false; + reply_word(0, 1); + return true; + case TUSB_REQ_SET_INTERFACE: + if (request->bmRequestType != 0x01 || request->wValue || request->wIndex || + request->wLength || !configurations[slot]) return false; + status_in(ACTION_INTERFACE); + return true; + default: + return false; + } +} + +static bool hub_request(void) { + const tusb_control_request_t* request = &control.request; + if (control.owner != 0) return false; + if (request->bmRequestType == 0xa0 && request->bRequest == TUSB_REQ_GET_DESCRIPTOR && + request->wValue == 0x2900 && request->wIndex == 0) { + reply_copy(hub_descriptor, sizeof(hub_descriptor)); + return true; + } + if (!configurations[0]) return false; + if (request->bmRequestType == 0xa0 && request->bRequest == TUSB_REQ_GET_STATUS && + request->wValue == 0 && request->wIndex == 0 && request->wLength == 4) { + put32(control_data, 0); // No local-power loss or overcurrent changes. + reply_data(4); + return true; + } + if (request->bmRequestType == 0x20 && request->bRequest == TUSB_REQ_CLEAR_FEATURE && + request->wValue <= 1 && request->wIndex == 0 && request->wLength == 0) { + status_in(ACTION_NONE); // Both supported hub change flags are already clear. + return true; + } + if (request->wIndex < 1 || request->wIndex > PORT_COUNT) return false; + hub_port* port = &ports[request->wIndex - 1]; + if (request->bmRequestType == 0xa3 && request->bRequest == TUSB_REQ_GET_STATUS && + request->wValue == 0 && request->wLength == 4) { + put16(control_data, port->status); + put16(control_data + 2, port->change); + reply_data(4); + return true; + } + if (request->bmRequestType != 0x23 || request->wLength) return false; + if (request->bRequest == TUSB_REQ_SET_FEATURE) { + switch (request->wValue) { + case FEATURE_PORT_POWER: + break; + case FEATURE_PORT_RESET: + if (!routing_enabled || (port->status & (PORT_CONNECTION | PORT_POWER)) != + (PORT_CONNECTION | PORT_POWER)) return false; + // The one physical SIE cannot own two simultaneous default addresses. + if (default_slot != PROBE_ROUTER_UNASSIGNED && default_slot != request->wIndex) return false; + break; + case FEATURE_PORT_SUSPEND: + if ((port->status & (PORT_CONNECTION | PORT_ENABLE | PORT_POWER | PORT_RESET)) != + (PORT_CONNECTION | PORT_ENABLE | PORT_POWER)) return false; + break; + default: + return false; + } + status_in(ACTION_PORT_SET); + return true; + } + if (request->bRequest == TUSB_REQ_CLEAR_FEATURE) { + switch (request->wValue) { + case FEATURE_PORT_POWER: + case FEATURE_PORT_ENABLE: + case FEATURE_PORT_SUSPEND: + case FEATURE_C_CONNECTION: + case FEATURE_C_ENABLE: + case FEATURE_C_SUSPEND: + case FEATURE_C_OVERCURRENT: + case FEATURE_C_RESET: + status_in(ACTION_PORT_CLEAR); + return true; + default: + return false; + } + } + return false; +} + +static bool vendor_request(void) { + const tusb_control_request_t* request = &control.request; + if (request->wIndex) return false; + if (request->bmRequestType == 0xc0 && request->bRequest == 0x5a && + request->wValue == 0 && request->wLength == 128) { + fill_stats(); + control.action = ACTION_KEEPALIVE; + reply_data(128); + return true; + } + if (request->bmRequestType != 0x40 || request->wLength) return false; + if (request->bRequest == 0x5b && request->wValue == 1 && control.owner == 0) { + probe_router_stats router; + probe_router_snapshot(&router); + if (!router.ready || correlated_setups < 20) return false; + status_in(ACTION_ARM); + return true; + } + if (request->bRequest == 0x5c && request->wValue == 0) { + status_in(ACTION_REBOOT); + return true; + } + if (request->bRequest == 0x5d && !routing_enabled && + probe_router_set_phase(request->wValue)) { + status_in(ACTION_NONE); + return true; + } + return false; +} + +static void handle_setup(const queued_event* queued) { + // A newer SETUP has already aborted this one's hardware transfer. + if (queued->generation != event_generation) return; + memset(&control, 0, sizeof(control)); + control.request = queued->event.setup_received; + control.generation = queued->generation; + control.owner = routing_enabled ? queued->setup_slot : 0; + if (routing_enabled && (control.owner >= PROBE_ROUTER_SLOTS || + (addresses[control.owner] == PROBE_ROUTER_UNASSIGNED && default_slot != control.owner))) { + ++bad_setup_owner; + stall_control(); + return; + } + ++setup_count[control.owner]; + uint8_t type = control.request.bmRequestType & 0x60; + bool supported = type == 0 ? standard_request() : + type == 0x20 ? hub_request() : type == 0x40 ? vendor_request() : false; + if (!supported) stall_control(); +} + +static void apply_port_feature(bool set) { + unsigned index = control.request.wIndex - 1; + hub_port* port = &ports[index]; + uint16_t feature = control.request.wValue; + uint32_t now = time_us_32(); + if (set) { + if (feature == FEATURE_PORT_POWER) { + port->status |= PORT_POWER; + if (routing_enabled && !(port->status & PORT_CONNECTION)) { + port->status |= PORT_CONNECTION; + port->change |= C_CONNECTION; + } + } else if (feature == FEATURE_PORT_RESET) { + forget_child(index); + port->status = (uint16_t)((port->status | PORT_RESET) & ~(PORT_ENABLE | PORT_SUSPEND)); + port->resetting = true; + port->resuming = false; + port->reset_deadline = now + 10000u; + publish_addresses(); + } else if (feature == FEATURE_PORT_SUSPEND) { + port->status |= PORT_SUSPEND; + port->resuming = false; + } + return; + } + if (feature >= FEATURE_C_CONNECTION && feature <= FEATURE_C_RESET) { + port->change &= (uint16_t)~(1u << (feature - FEATURE_C_CONNECTION)); + } else if (feature == FEATURE_PORT_ENABLE) { + port->status &= (uint16_t)~(PORT_ENABLE | PORT_SUSPEND | PORT_RESET); + port->resetting = false; + port->resuming = false; + forget_child(index); + publish_addresses(); + } else if (feature == FEATURE_PORT_POWER) { + if (port->status & PORT_CONNECTION) port->change |= C_CONNECTION; + port->status = 0; + port->resetting = false; + port->resuming = false; + forget_child(index); + publish_addresses(); + } else if (feature == FEATURE_PORT_SUSPEND && (port->status & PORT_SUSPEND)) { + port->resuming = true; + port->resume_deadline = now + 20000u; + } +} + +static void complete_control(void) { + uint8_t owner = control.owner; + control_action action = control.action; + control.stage = CTRL_IDLE; + control.action = ACTION_NONE; + switch (action) { + case ACTION_ADDRESS: + addresses[owner] = (uint8_t)control.request.wValue; + if (addresses[owner] == 0) default_slot = owner; + else if (default_slot == owner) default_slot = PROBE_ROUTER_UNASSIGNED; + publish_addresses(); + // Once routing is active, C1 is the sole address-register writer. + // It selects the logical address from each token, after this ACK. + // This prevents a C0 SET_ADDRESS completion changing the register + // between another token's acceptance and its SETUP interrupt. + if (!routing_enabled) + dcd_edpt0_status_complete(RHPORT, &control.request); + break; + case ACTION_CONFIGURATION: + configurations[owner] = (uint8_t)control.request.wValue; + if (owner == 0) { + if (configurations[0]) open_interrupt(); + else { + close_interrupt(); + probe_router_enable(false); + routing_enabled = false; + memset(ports, 0, sizeof(ports)); + forget_child(0); + forget_child(1); + publish_addresses(); + usb_hw->dev_addr_ctrl = addresses[0]; + } + } + break; + case ACTION_INTERFACE: + if (owner == 0) open_interrupt(); + break; + case ACTION_HALT: + ++endpoint_epoch; + interrupt_halted = true; + interrupt_pending = false; + dcd_edpt_stall(RHPORT, HUB_EP); + break; + case ACTION_CLEAR_HALT: + ++endpoint_epoch; + interrupt_pending = false; + interrupt_halted = false; + // Reopening also cancels a previously queued interrupt safely and + // resets DATA0; no child has a noncontrol endpoint to disturb. + open_interrupt(); + break; + case ACTION_PORT_SET: + apply_port_feature(true); + break; + case ACTION_PORT_CLEAR: + apply_port_feature(false); + break; + case ACTION_KEEPALIVE: + watchdog_update(); + break; + case ACTION_ARM: + if (!routing_enabled) { + routing_enabled = true; + publish_addresses(); + probe_router_enable(true); + for (unsigned i = 0; i < PORT_COUNT; ++i) { + ports[i].status |= PORT_CONNECTION; + ports[i].change |= C_CONNECTION; + } + } + break; + case ACTION_REBOOT: + reboot_pending = true; + break; + case ACTION_NONE: + break; + } +} + +static void handle_transfer(const queued_event* queued) { + const dcd_event_t* event = &queued->event; + uint8_t endpoint = event->xfer_complete.ep_addr; + if (endpoint == HUB_EP) { + if (queued->endpoint_epoch != endpoint_epoch) return; + interrupt_pending = false; + if (event->xfer_complete.result != XFER_RESULT_SUCCESS || event->xfer_complete.len != 1) failed = true; + return; + } + if ((endpoint != EP0_IN && endpoint != EP0_OUT) || queued->generation != control.generation || + control.stage == CTRL_IDLE || control.stage == CTRL_STALLED) return; + if (event->xfer_complete.result != XFER_RESULT_SUCCESS) { + stall_control(); + return; + } + if ((control.stage == CTRL_STATUS_IN && endpoint == EP0_IN) || + (control.stage == CTRL_STATUS_OUT && endpoint == EP0_OUT)) { + if (event->xfer_complete.len == 0) complete_control(); + else stall_control(); + return; + } + if (queued->generation != event_generation) return; + if (control.stage != CTRL_DATA_IN || endpoint != EP0_IN || + event->xfer_complete.len != control.packet_length) { + stall_control(); + return; + } + control.sent = (uint16_t)(control.sent + control.packet_length); + if (control.sent < control.length || control.need_zlp) next_control_packet(); + else { + control.stage = CTRL_STATUS_OUT; + queue_control(EP0_OUT, control_out, 0); + } +} + +void dcd_event_handler(dcd_event_t const* event, bool in_isr) { + (void)in_isr; + if (event->rhport != RHPORT) return; + if (event->event_id != DCD_EVENT_SETUP_RECEIVED && event->event_id != DCD_EVENT_XFER_COMPLETE && + event->event_id != DCD_EVENT_BUS_RESET && event->event_id != DCD_EVENT_UNPLUGGED) return; + if (event->event_id == DCD_EVENT_SETUP_RECEIVED || event->event_id == DCD_EVENT_BUS_RESET || + event->event_id == DCD_EVENT_UNPLUGGED) ++event_generation; + uint32_t head = event_head; + uint32_t next = (head + 1u) % EVENT_CAPACITY; + if (next == event_tail) { + event_overflow = true; + return; + } + queued_event* queued = &events[head]; + queued->event = *event; + queued->generation = event_generation; + queued->endpoint_epoch = endpoint_epoch; + queued->setup_slot = PROBE_ROUTER_UNASSIGNED; + if (event->event_id == DCD_EVENT_SETUP_RECEIVED) { + // C1 does not change the address while SETUP_REC is pending; C0 does + // not write it in routed mode. This is the hardware-accepted address, + // not a fallback inferred from whichever header we last sampled. + const uint8_t hw_address = usb_hw->dev_addr_ctrl & 0x7fu; + if (hw_address == 0) { + queued->setup_slot = default_slot; + } else { + for (uint8_t slot = 0; slot < PROBE_ROUTER_SLOTS; ++slot) { + if (addresses[slot] == hw_address) { + queued->setup_slot = slot; + break; + } + } + } + uint32_t sequence; + const uint8_t candidate = probe_router_setup_slot(&sequence); + if (candidate < PROBE_ROUTER_SLOTS && candidate == queued->setup_slot && + sequence != observed_setup_sequence) ++correlated_setups; + observed_setup_sequence = sequence; + } + __dmb(); + event_head = next; +} + +static void port_task(uint32_t now) { + for (unsigned i = 0; i < PORT_COUNT; ++i) { + hub_port* port = &ports[i]; + if (port->resetting && (int32_t)(now - port->reset_deadline) >= 0) { + port->resetting = false; + port->status &= (uint16_t)~PORT_RESET; + if (default_slot != PROBE_ROUTER_UNASSIGNED && default_slot != i + 1) { + // Concurrent default-address resets cannot be represented honestly. + failed = true; + return; + } + port->status |= PORT_ENABLE; + port->change |= C_RESET; + addresses[i + 1] = 0; + default_slot = (uint8_t)(i + 1); + publish_addresses(); + } + if (port->resuming && (int32_t)(now - port->resume_deadline) >= 0) { + port->resuming = false; + port->status &= (uint16_t)~PORT_SUSPEND; + port->change |= C_SUSPEND; + } + } +} + +static void diagnostic_task(uint32_t now) { + static uint32_t last_report; + static char line[384]; + static uint16_t length; + static uint16_t sent; + if ((uint32_t)(now - last_report) >= 1000000u && sent == length) { + last_report = now; + probe_router_stats router; + probe_router_snapshot(&router); + int count = snprintf(line, sizeof(line), + "[PHUB] route=%u addr=%u,%u,%u default=%u setup=%" PRIu32 ",%" PRIu32 ",%" PRIu32 + " bad=%" PRIu32 " ready=%" PRIu32 " sop=%" PRIu32 " sync=%" PRIu32 + " token=%" PRIu32 " crc=%" PRIu32 " late=%" PRIu32 " retarget=%" PRIu32 + " hits=%" PRIu32 ",%" PRIu32 ",%" PRIu32 " overflow=%u failed=%u" + " raw=%08" PRIx32 "/%08" PRIx32 " n=%" PRIu32 " eop=%" PRIu32 "\r\n", + routing_enabled, addresses[0], addresses[1], addresses[2], default_slot, + setup_count[0], setup_count[1], setup_count[2], bad_setup_owner, + router.ready, router.sops, router.sync_ok, router.valid_tokens, router.crc_errors, + router.late_samples, router.retargets, router.address_hits[0], router.address_hits[1], + router.address_hits[2], event_overflow, failed, + router.last_raw[0], router.last_raw[1], router.last_raw_count, router.last_raw_eop); + length = count < 0 ? 0 : (uint16_t)((unsigned)count < sizeof(line) ? (unsigned)count : sizeof(line) - 1); + sent = 0; + } + // No blocking stdio writes: fill only available UART FIFO positions. USB + // event service continues while the 115200-baud diagnostic line drains. + for (unsigned budget = 0; sent < length && budget < 32 && uart_is_writable(uart_default); ++budget) + uart_get_hw(uart_default)->dr = (uint8_t)line[sent++]; +} + +void probe_hub_init(void) { + system_clock_hz = clock_get_hz(clk_sys); + reset_bus_state(); + // Enabling, bus resets, ordinary enumeration, and UART never feed this. + watchdog_enable(8000, false); + const tusb_rhport_init_t init = { .role = TUSB_ROLE_DEVICE, .speed = TUSB_SPEED_FULL }; + if (!dcd_init(RHPORT, &init)) failed = true; + dcd_int_enable(RHPORT); +} + +void probe_hub_task(void) { + if (!failed) { + for (unsigned count = 0; count < EVENT_CAPACITY; ++count) { + dcd_int_disable(RHPORT); + if (event_overflow) failed = true; + if (failed || event_tail == event_head) { + dcd_int_enable(RHPORT); + break; + } + __dmb(); + queued_event queued = events[event_tail]; + event_tail = (event_tail + 1u) % EVENT_CAPACITY; + switch (queued.event.event_id) { + case DCD_EVENT_BUS_RESET: + case DCD_EVENT_UNPLUGGED: + reset_bus_state(); + break; + case DCD_EVENT_SETUP_RECEIVED: + handle_setup(&queued); + break; + case DCD_EVENT_XFER_COMPLETE: + handle_transfer(&queued); + break; + default: + break; + } + dcd_int_enable(RHPORT); + if (failed || reboot_pending) break; + } + } + uint32_t now = time_us_32(); + dcd_int_disable(RHPORT); + if (!failed && !reboot_pending) { + port_task(now); + if (!failed) arm_interrupt(); + } + if (failed) { + probe_router_enable(false); + routing_enabled = false; + dcd_disconnect(RHPORT); + } + dcd_int_enable(RHPORT); + if (reboot_pending) { + // This is reached only after the REBOOT request's status IN was ACKed. + watchdog_reboot(0, 0, 10); + reboot_pending = false; + failed = true; + } + diagnostic_task(now); +} diff --git a/tools/pico_usb_address_probe/usb_probe.h b/tools/pico_usb_address_probe/usb_probe.h new file mode 100644 index 0000000..b32455b --- /dev/null +++ b/tools/pico_usb_address_probe/usb_probe.h @@ -0,0 +1,5 @@ +#pragma once + +// Core 0 only. Main initializes the router/Core 1 before attaching USB here. +void probe_hub_init(void); +void probe_hub_task(void); diff --git a/tools/switch2_usb_probe/bootsel.cpp b/tools/switch2_usb_probe/bootsel.cpp index ef5a972..a5e4ce1 100644 --- a/tools/switch2_usb_probe/bootsel.cpp +++ b/tools/switch2_usb_probe/bootsel.cpp @@ -1,21 +1,71 @@ #include "bootsel.h" +#include "model.h" +#if SWITCH2_PROBE_HUB +#include +#include "pico/bootrom.h" +#include "usb/native_hub/native_hub.h" +#else #include "adapter/adapter_mode_controller.h" +#endif #include "usb/usb_configuration_management.h" namespace { bool bootsel_accepted; +#if SWITCH2_PROBE_HUB +constexpr uint32_t kBootselRebootDelayMs = 50; +struct BootselTransfer { + uint8_t envelope[UsbConfigurationManagement::kRequestHeaderSize]; + bool pending; + bool validated; +}; +// Control state is independent even when the two children enumerate together. +BootselTransfer bootsel_transfers[PROBE_CONTROLLER_COUNT + 1]; +bool bootsel_delay_started; +uint32_t bootsel_deadline_ms; +#endif } bool probe_bootsel_vendor_control(uint8_t rhport, uint8_t stage, const tusb_control_request_t* request) { using namespace UsbConfigurationManagement; +#if SWITCH2_PROBE_HUB + if (rhport > PROBE_CONTROLLER_COUNT) return false; + BootselTransfer& transfer = bootsel_transfers[rhport]; + if (stage == CONTROL_STAGE_SETUP) { + transfer.pending = false; + transfer.validated = false; + } +#endif if (request == nullptr || request->bmRequestType != 0x40 || request->bRequest != static_cast(Operation::kBootselReboot) || request->wValue != kRequestValue || request->wIndex != kRequestIndex || request->wLength != kRequestHeaderSize) { return false; } +#if SWITCH2_PROBE_HUB + if (stage == CONTROL_STAGE_SETUP) { + // Any short OUT leaves nonzero reserved/CRC bytes and fails decoding. + memset(transfer.envelope, 0xff, sizeof(transfer.envelope)); + transfer.pending = native_hub_control_xfer( + rhport, request, transfer.envelope, sizeof(transfer.envelope)); + return transfer.pending; + } + if (stage == CONTROL_STAGE_DATA) { + DecodedRequest decoded{}; + transfer.validated = transfer.pending && + decode_request(Operation::kBootselReboot, transfer.envelope, + sizeof(transfer.envelope), &decoded) && + decoded.payload_size == 0; + return transfer.validated; + } + if (stage == CONTROL_STAGE_ACK && transfer.pending && transfer.validated) { + transfer.pending = false; + bootsel_accepted = true; + return true; + } + return false; +#else // The shared handler receives the envelope at SETUP and validates and // dispatches it only at ACK, after the host's control transfer completes. const bool accepted = @@ -24,12 +74,24 @@ bool probe_bootsel_vendor_control(uint8_t rhport, uint8_t stage, bootsel_accepted = true; } return accepted; +#endif } void probe_bootsel_task(uint32_t now_ms) { +#if SWITCH2_PROBE_HUB + if (!bootsel_accepted) return; + if (!bootsel_delay_started) { + bootsel_delay_started = true; + bootsel_deadline_ms = now_ms + kBootselRebootDelayMs; + } else if (static_cast(now_ms - bootsel_deadline_ms) >= 0) { + bootsel_accepted = false; + reset_usb_boot(0, 0); + } +#else // The native bridge does not initialize ordinary adapter-mode selection. // A successful BOOTSEL dispatch guarantees the task takes its reboot path. if (bootsel_accepted) { adapter_mode_controller_task(now_ms); } +#endif } diff --git a/tools/switch2_usb_probe/controller_input.cpp b/tools/switch2_usb_probe/controller_input.cpp index 189079d..8b4c5b5 100644 --- a/tools/switch2_usb_probe/controller_input.cpp +++ b/tools/switch2_usb_probe/controller_input.cpp @@ -1,4 +1,5 @@ #include "controller_input.h" +#include "model.h" #include @@ -8,6 +9,10 @@ #include "platform/pico/system_clock.h" #include "profile/controller_profile_runtime.h" #include "pico/stdlib.h" +#if SWITCH2_PROBE_HUB +#include +extern "C" int probe_debug_printf(const char* format, ...); +#endif #if SWITCH2_BRIDGE_WII_INPUT #include #include "input/wii_ir_pointer.h" @@ -25,17 +30,25 @@ extern "C" int probe_debug_printf(const char* format, ...); namespace { constexpr uint8_t kSourceAddress[] = {SWITCH2_BRIDGE_SOURCE_ADDRESS_BYTES}; static_assert(sizeof(kSourceAddress) == 6, "Select one physical Bluetooth address"); +#if SWITCH2_PROBE_COMPOSITE || SWITCH2_PROBE_HUB +constexpr uint8_t kSecondSourceAddress[] = {SWITCH2_BRIDGE_SECOND_SOURCE_ADDRESS_BYTES}; +static_assert(sizeof(kSecondSourceAddress) == 6, "Select the second physical Bluetooth address"); +#endif constexpr uint32_t kInputDeadlineMs = 500; +#if !SWITCH2_PROBE_HUB constexpr uint32_t kFlashCoordinationTimeoutMs = 1000; -// The backend publishes stage 2 only after Core 1's flash-safe registration; -// reaching Core 1 already required successful Core 0 registration in start(). +#endif +// Stage 2 publishes flash safety: both cores registered in dedicated-radio +// modes, or Core 0 registered with an SRAM-only/IRQ-disabled Core 1 in hub mode. constexpr uint32_t kFlashCoordinationStage = 2; bool g_initialized; bool g_start_attempted; bool g_flash_ready; +#if SWITCH2_BRIDGE_WII_INPUT probe_controller_input g_input; -#if !SWITCH2_BRIDGE_WII_INPUT -uint32_t g_received_ms; +#else +probe_controller_input g_inputs[PROBE_CONTROLLER_COUNT]; +uint32_t g_received_times[PROBE_CONTROLLER_COUNT]; #endif #if SWITCH2_BRIDGE_WII_INPUT #ifndef SWITCH2_WII_IR_SCREEN_CONFIG @@ -368,8 +381,13 @@ extern "C" void probe_controller_input_init(void) { if (!g_screen_configured) probe_debug_printf("[PROBE] Invalid native IR viewport configuration\n"); wii_ir_mouse_set_output_enabled(false); #else + static_assert(SWITCH2_MOUSE_CAPTURE_SOURCE_COUNT == PROBE_CONTROLLER_COUNT, + "Each native controller requires an independent capture channel"); switch2_mouse_capture_init(); - switch2_mouse_capture_select_input(kSourceAddress); + switch2_mouse_capture_select_input(0, kSourceAddress, probe_model_pid(0)); +#if SWITCH2_PROBE_COMPOSITE || SWITCH2_PROBE_HUB + switch2_mouse_capture_select_input(1, kSecondSourceAddress, probe_model_pid(1)); +#endif bluepad32_input_backend_init(); #endif controller_profile_runtime_reset(); @@ -384,6 +402,14 @@ extern "C" bool probe_controller_input_start(void) { #endif g_start_attempted = true; bluepad32_input_backend_start(); +#if SWITCH2_PROBE_HUB + // Initialization is synchronous on Core 0; there is no radio Core 1 to + // wait for. The SDK async context advances radio startup in task(). + Bluepad32BackendDiagnostics diagnostics; + bluepad32_input_backend_diagnostics(&diagnostics); + g_flash_ready = diagnostics.initialization_stage >= kFlashCoordinationStage; + return g_flash_ready; +#else const absolute_time_t deadline = make_timeout_time_ms(kFlashCoordinationTimeoutMs); do { Bluepad32BackendDiagnostics diagnostics; @@ -397,6 +423,25 @@ extern "C" bool probe_controller_input_start(void) { // Do not reset Core 1 or retry a partially launched backend. It may still // be running; a false return keeps USB and its flash writes fail-closed. return false; +#endif +} + +extern "C" void probe_controller_input_task(void) { +#if SWITCH2_PROBE_HUB + if (!g_flash_ready) return; + bluepad32_input_backend_poll(); + static uint32_t last_diagnostics; + const uint32_t now = to_ms_since_boot(get_absolute_time()); + if ((uint32_t)(now - last_diagnostics) >= 1000u) { + last_diagnostics = now; + Bluepad32BackendDiagnostics diagnostics; + bluepad32_input_backend_diagnostics(&diagnostics); + probe_debug_printf("[HUB_RADIO] stage=%" PRIu32 " timers=%" PRIu32 "/%" PRIu32 + " reports=%" PRIu32 "\n", diagnostics.initialization_stage, + diagnostics.rumble_timer_ticks, diagnostics.configuration_timer_ticks, + diagnostics.controller_reports); + } +#endif } extern "C" bool probe_controller_input_pairing_task(void) { @@ -426,30 +471,31 @@ extern "C" void probe_controller_input_set_native_features(uint8_t features) { } #endif -extern "C" void probe_controller_input_set_native_stream(bool enabled) { +extern "C" void probe_controller_input_set_native_stream(uint8_t instance, bool enabled) { + if (instance >= PROBE_CONTROLLER_COUNT) return; #if SWITCH2_BRIDGE_WII_INPUT enabled = enabled && g_flash_ready; if (g_native_stream != enabled || !enabled) discard_wii_output(); g_native_stream = enabled; update_wii_ir_gate(time_us_32()); #else - switch2_mouse_capture_set_native_stream(g_flash_ready && enabled); + switch2_mouse_capture_set_native_stream(instance, g_flash_ready && enabled); #endif } extern "C" uint32_t probe_controller_input_peek_native_report( - uint32_t now_ms, uint8_t report[63]) { - if (!g_flash_ready) return 0; + uint8_t instance, uint32_t now_ms, uint8_t report[63]) { + if (instance >= PROBE_CONTROLLER_COUNT || !g_flash_ready) return 0; #if SWITCH2_BRIDGE_WII_INPUT (void)now_ms; return prepare_wii_report(report); #else - return switch2_mouse_capture_peek_native_report(now_ms, report); + return switch2_mouse_capture_peek_native_report(instance, now_ms, report); #endif } -extern "C" bool probe_controller_input_commit_native_report(uint32_t serial) { - if (!g_flash_ready) return false; +extern "C" bool probe_controller_input_commit_native_report(uint8_t instance, uint32_t serial) { + if (instance >= PROBE_CONTROLLER_COUNT || !g_flash_ready) return false; #if SWITCH2_BRIDGE_WII_INPUT if (!g_native_stream || !serial || serial != g_pending_serial || g_pending_generation != g_wii_generation || !g_wii_active) return false; @@ -462,12 +508,12 @@ extern "C" bool probe_controller_input_commit_native_report(uint32_t serial) { ++g_report_counter; return true; #else - return switch2_mouse_capture_commit_native_report(serial); + return switch2_mouse_capture_commit_native_report(instance, serial); #endif } -extern "C" bool probe_controller_input_play_sample(uint8_t sample_id, uint64_t* token) { - if (!g_flash_ready) { +extern "C" bool probe_controller_input_play_sample(uint8_t instance, uint8_t sample_id, uint64_t* token) { + if (instance >= PROBE_CONTROLLER_COUNT || !g_flash_ready) { if (token != nullptr) *token = 0; return false; } @@ -475,40 +521,43 @@ extern "C" bool probe_controller_input_play_sample(uint8_t sample_id, uint64_t* return bluepad32_input_backend_wii_sample_request(sample_id, token); #else return switch2_mouse_capture_request_sample( - sample_id, to_ms_since_boot(get_absolute_time()), token); + instance, sample_id, to_ms_since_boot(get_absolute_time()), token); #endif } -extern "C" int probe_controller_input_sample_result(uint64_t token, uint32_t now_ms) { - if (!g_flash_ready) return -1; +extern "C" int probe_controller_input_sample_result(uint8_t instance, uint64_t token, uint32_t now_ms) { + if (instance >= PROBE_CONTROLLER_COUNT || !g_flash_ready) return -1; #if SWITCH2_BRIDGE_WII_INPUT (void)now_ms; return bluepad32_input_backend_wii_sample_result(token); #else - return switch2_mouse_capture_sample_result(token, now_ms); + return switch2_mouse_capture_sample_result(instance, token, now_ms); #endif } -extern "C" void probe_controller_input_cancel_sample(void) { +extern "C" void probe_controller_input_cancel_sample(uint8_t instance) { + if (instance >= PROBE_CONTROLLER_COUNT) return; #if SWITCH2_BRIDGE_WII_INPUT bluepad32_input_backend_wii_sample_cancel(); #else - switch2_mouse_capture_cancel_sample(); + switch2_mouse_capture_cancel_sample(instance); #endif } -extern "C" void probe_controller_input_poll(uint32_t now_ms, +extern "C" void probe_controller_input_poll(uint8_t instance, uint32_t now_ms, probe_controller_input* out) { if (out == nullptr) return; - if (!g_flash_ready) { + if (instance >= PROBE_CONTROLLER_COUNT || !g_flash_ready) { *out = {}; return; } #if SWITCH2_BRIDGE_WII_INPUT poll_wii_source(now_ms); #else + probe_controller_input& g_input = g_inputs[instance]; + uint32_t& g_received_ms = g_received_times[instance]; Switch2MouseCaptureInput sample; - if (switch2_mouse_capture_latest_input(g_input.serial, &sample)) { + if (switch2_mouse_capture_latest_input(instance, g_input.serial, &sample)) { g_input.serial = sample.serial; g_input.active = sample.active; g_received_ms = sample.received_ms; diff --git a/tools/switch2_usb_probe/controller_input.h b/tools/switch2_usb_probe/controller_input.h index ae344f0..14b723c 100644 --- a/tools/switch2_usb_probe/controller_input.h +++ b/tools/switch2_usb_probe/controller_input.h @@ -12,7 +12,7 @@ typedef struct { uint32_t serial; uint8_t buttons[2]; uint8_t stick[3]; - // Latest opaque native 08 byte 8. + // Latest opaque native 07/08 byte 8. uint8_t native_status; // Cumulative signed relative totals within mouse_epoch, not per-poll // deltas. Cached polls repeat these totals without consuming motion. @@ -20,7 +20,7 @@ typedef struct { uint32_t mouse_epoch; int64_t mouse_total_x; int64_t mouse_total_y; - // Latest opaque native 08 byte 13. + // Latest opaque native 07/08 byte 13. uint8_t mouse_surface; } probe_controller_input; @@ -28,10 +28,15 @@ typedef struct { void probe_controller_input_clock_init(void); // Core 0, after stdio and before protocol reset or USB startup. void probe_controller_input_init(void); -// True means both cores are registered for flash coordination, not that the -// radio is ready or a controller is connected. Failure is latched: keep USB -// and flash-writing protocol operations disabled rather than retrying startup. +// True means flash coordination is ready, not that the radio is ready or a +// controller is connected. Hub mode initializes on Core 0 with Core 1 reserved +// for SRAM-only USB; other modes register both cores and launch the radio there. +// Failure is latched: keep USB and flash-writing protocol operations disabled. bool probe_controller_input_start(void); +// Core 0 main loop before USB tasks, outside IRQs and application state locks. +// Hub mode cooperatively services CYW43/BTstack, including storage and haptics; +// a no-op before successful start and in dedicated-radio modes. +void probe_controller_input_task(void); // Core 0 after start(): polls the existing two-second BOOTSEL hold gesture. // True means a Bluetooth pairing-window request was queued. Long holds NEVER // clear pairings in this bridge, and this does not inject USB controller input. @@ -42,29 +47,30 @@ void probe_controller_input_set_stick_calibration(const uint8_t calibration[9]); // Native feature changes are output barriers, not Bluetooth/IMU resets. void probe_controller_input_set_native_features(uint8_t features); #endif -// Core0 native08 output. Disable discards queued/prepared data; repeated enable -// preserves it. Joy-Con mode relays its bounded FIFO; Wii mode synthesizes from -// fresh calibrated sensors and the selected IR pointer. No pairing changes. -void probe_controller_input_set_native_stream(bool enabled); +// Core0 native07/08 output. Disable discards queued/prepared data; repeated +// enable preserves it. Joy-Con mode relays its bounded FIFO; right-only Wii +// mode synthesizes fresh calibrated sensors and the selected IR pointer. +// No pairing changes. +void probe_controller_input_set_native_stream(uint8_t instance, bool enabled); // Copy one63-byte payload without report ID. Returns a boot-unique token, or0 // without changing output. Nondestructive until successful HID submission and // commit. now_ms uses the Pico boot-ms clock; unavailable/stale input is rejected. -uint32_t probe_controller_input_peek_native_report(uint32_t now_ms, uint8_t report[63]); +uint32_t probe_controller_input_peek_native_report(uint8_t instance, uint32_t now_ms, uint8_t report[63]); // Remove only the exact current head once. A stale/replaced token cannot pop a // new stream's packet. Before flash-ready startup peek/commit return 0/false. -bool probe_controller_input_commit_native_report(uint32_t serial); +bool probe_controller_input_commit_native_report(uint8_t instance, uint32_t serial); // Built-in vibration samples only; raw HD-rumble output is not forwarded. // A nonzero token means queued, not completed. Result:0 pending,1 completion, // -1 failed/stale. Joy-Con completion is its application ACK; Wii completion is // actual bounded rumble-driver dispatch (not an HD-waveform fidelity claim). // Reset cancels the request, never stored pairing. -bool probe_controller_input_play_sample(uint8_t sample_id, uint64_t* token); -int probe_controller_input_sample_result(uint64_t token, uint32_t now_ms); -void probe_controller_input_cancel_sample(void); +bool probe_controller_input_play_sample(uint8_t instance, uint8_t sample_id, uint64_t* token); +int probe_controller_input_sample_result(uint8_t instance, uint64_t token, uint32_t now_ms); +void probe_controller_input_cancel_sample(uint8_t instance); // Core0 at250Hz; now_ms uses Pico boot milliseconds. Supplies current mapped // controls for diagnostic reports; the native sender owns motion consumption. // Inactive controls are zero except serial; USB supplies its calibrated center. -void probe_controller_input_poll(uint32_t now_ms, probe_controller_input* out); +void probe_controller_input_poll(uint8_t instance, uint32_t now_ms, probe_controller_input* out); #ifdef __cplusplus } diff --git a/tools/switch2_usb_probe/descriptors.h b/tools/switch2_usb_probe/descriptors.h index c2a11ea..1196ae4 100644 --- a/tools/switch2_usb_probe/descriptors.h +++ b/tools/switch2_usb_probe/descriptors.h @@ -1,33 +1,66 @@ #pragma once #include +#include "model.h" -// Published Joy-Con 2 (R) USB descriptors, reproduced for enumeration capture. +// Published Joy-Con 2 USB descriptors, reproduced for the selected model. // https://github.com/ndeadly/switch2_controller_research/blob/master/descriptors.md -static const uint8_t probe_device_descriptor[] = { - 0x12, 0x01, 0x00, 0x02, 0xef, 0x02, 0x01, 0x40, 0x7e, 0x05, 0x66, 0x20, - 0x00, 0x01, 0x01, 0x02, 0x03, 0x01, -}; +// Composite retains the primary right PID (0x2066): USB has one device identity, +// not a separate device PID for each left/right function. +#define PROBE_DEVICE_DESCRIPTOR(pid) { \ + 0x12, 0x01, 0x00, 0x02, 0xef, 0x02, 0x01, 0x40, 0x7e, 0x05, \ + (pid) & 0xff, (pid) >> 8, \ + 0x00, 0x01, 0x01, 0x02, 0x03, 0x01, \ +} +static const uint8_t probe_device_descriptor[] = PROBE_DEVICE_DESCRIPTOR(PROBE_JOYCON_PID); +#if SWITCH2_PROBE_HUB +static const uint8_t probe_left_device_descriptor[] = PROBE_DEVICE_DESCRIPTOR(0x2067u); +#endif +#undef PROBE_DEVICE_DESCRIPTOR static const uint8_t probe_configuration_descriptor[] = { - 0x09, 0x02, 0x50, 0x00, 0x02, 0x01, 0x04, 0xc0, 0xfa, 0x08, 0x0b, 0x00, +#if SWITCH2_PROBE_COMPOSITE + 0x09, 0x02, 0x97, 0x00, 0x04, 0x01, 0x04, 0xc0, 0xfa, +#else + 0x09, 0x02, 0x50, 0x00, 0x02, 0x01, 0x04, 0xc0, 0xfa, +#endif + 0x08, 0x0b, 0x00, 0x01, 0x03, 0x00, 0x00, 0x00, 0x09, 0x04, 0x00, 0x00, 0x02, 0x03, 0x00, 0x00, 0x05, 0x09, 0x21, 0x11, 0x01, 0x00, 0x01, 0x22, 0x64, 0x00, 0x07, 0x05, 0x81, 0x03, 0x40, 0x00, 0x04, 0x07, 0x05, 0x01, 0x03, 0x40, 0x00, 0x04, 0x08, 0x0b, 0x01, 0x01, 0xff, 0x00, 0x00, 0x00, 0x09, 0x04, 0x01, 0x00, 0x02, 0xff, 0x00, 0x00, 0x06, 0x07, 0x05, 0x02, 0x02, 0x40, 0x00, 0x00, 0x07, 0x05, 0x82, 0x02, 0x40, 0x00, 0x00, +#if SWITCH2_PROBE_COMPOSITE + 0x08, 0x0b, 0x02, 0x01, 0x03, 0x00, 0x00, 0x00, + 0x09, 0x04, 0x02, 0x00, 0x02, 0x03, 0x00, 0x00, 0x07, + 0x09, 0x21, 0x11, 0x01, 0x00, 0x01, 0x22, 0x64, 0x00, + 0x07, 0x05, 0x83, 0x03, 0x40, 0x00, 0x04, + 0x07, 0x05, 0x03, 0x03, 0x40, 0x00, 0x04, + 0x08, 0x0b, 0x03, 0x01, 0xff, 0x00, 0x00, 0x00, + 0x09, 0x04, 0x03, 0x00, 0x02, 0xff, 0x00, 0x00, 0x08, + 0x07, 0x05, 0x04, 0x02, 0x40, 0x00, 0x00, + 0x07, 0x05, 0x84, 0x02, 0x40, 0x00, 0x00, +#endif }; -static const uint8_t probe_hid_report_descriptor[] = { - 0x05, 0x01, 0x09, 0x05, 0xa1, 0x01, 0x85, 0x05, 0x05, 0xff, 0x09, 0x01, - 0x15, 0x00, 0x26, 0xff, 0x00, 0x95, 0x3f, 0x75, 0x08, 0x81, 0x02, 0x85, - 0x08, 0x09, 0x01, 0x95, 0x02, 0x81, 0x02, 0x05, 0x09, 0x19, 0x01, 0x29, - 0x10, 0x25, 0x01, 0x95, 0x10, 0x75, 0x01, 0x81, 0x02, 0x05, 0xff, 0x09, - 0x01, 0x26, 0xff, 0x00, 0x95, 0x01, 0x75, 0x08, 0x81, 0x02, 0x05, 0x01, - 0x09, 0x01, 0xa1, 0x00, 0x09, 0x30, 0x09, 0x31, 0x26, 0xff, 0x0f, 0x95, - 0x02, 0x75, 0x0c, 0x81, 0x02, 0xc0, 0x05, 0xff, 0x09, 0x02, 0x26, 0xff, - 0x00, 0x95, 0x37, 0x75, 0x08, 0x81, 0x02, 0x85, 0x01, 0x09, 0x01, 0x95, - 0x3f, 0x91, 0x02, 0xc0, +#define PROBE_HID_DESCRIPTOR(report_id) { \ + 0x05, 0x01, 0x09, 0x05, 0xa1, 0x01, 0x85, 0x05, 0x05, 0xff, 0x09, 0x01, \ + 0x15, 0x00, 0x26, 0xff, 0x00, 0x95, 0x3f, 0x75, 0x08, 0x81, 0x02, 0x85, \ + report_id, 0x09, 0x01, 0x95, 0x02, 0x81, 0x02, 0x05, 0x09, 0x19, 0x01, 0x29, \ + 0x10, 0x25, 0x01, 0x95, 0x10, 0x75, 0x01, 0x81, 0x02, 0x05, 0xff, 0x09, \ + 0x01, 0x26, 0xff, 0x00, 0x95, 0x01, 0x75, 0x08, 0x81, 0x02, 0x05, 0x01, \ + 0x09, 0x01, 0xa1, 0x00, 0x09, 0x30, 0x09, 0x31, 0x26, 0xff, 0x0f, 0x95, \ + 0x02, 0x75, 0x0c, 0x81, 0x02, 0xc0, 0x05, 0xff, 0x09, 0x02, 0x26, 0xff, \ + 0x00, 0x95, 0x37, 0x75, 0x08, 0x81, 0x02, 0x85, 0x01, 0x09, 0x01, 0x95, \ + 0x3f, 0x91, 0x02, 0xc0, \ +} + +static const uint8_t probe_hid_report_descriptors[PROBE_CONTROLLER_COUNT][100] = { + PROBE_HID_DESCRIPTOR(PROBE_NATIVE_REPORT_ID), +#if SWITCH2_PROBE_COMPOSITE || SWITCH2_PROBE_HUB + PROBE_HID_DESCRIPTOR(0x07u), +#endif }; +#undef PROBE_HID_DESCRIPTOR diff --git a/tools/switch2_usb_probe/main.c b/tools/switch2_usb_probe/main.c index 6727aaa..e93ca3c 100644 --- a/tools/switch2_usb_probe/main.c +++ b/tools/switch2_usb_probe/main.c @@ -1,4 +1,4 @@ -// Joy-Con 2 (R) USB instrument and optional Bluetooth controller/mouse bridge. +// Joy-Con 2 USB instrument and optional Bluetooth controller/mouse bridge. // Only documented/observed transactions are implemented. Built-in vibration // cues wait for the source ACK; native sensor packets are relayed without decoding. // Only virtual pairing storage is writable here. @@ -22,16 +22,21 @@ #include "descriptors.h" #include "protocol.h" #include "storage.h" +#include "transport.h" #ifdef SWITCH2_PROBE_MEMORY #include "memory.h" #endif #ifdef SWITCH2_PROBE_IDENTITY_REPLY #include "probe_identity.h" -_Static_assert(sizeof(probe_identity_reply) == 64, "factory identity response size"); +_Static_assert(sizeof(probe_identity_replies[0]) == 64, "factory identity response size"); +_Static_assert(sizeof(probe_identity_replies) / sizeof(probe_identity_replies[0]) == + PROBE_CONTROLLER_COUNT, "one captured identity per controller"); #endif #ifdef SWITCH2_PROBE_VERSION_REPLY #include "probe_version.h" -_Static_assert(sizeof(probe_version_reply) == 16, "version/address response size"); +_Static_assert(sizeof(probe_version_replies[0]) == 16, "version/address response size"); +_Static_assert(sizeof(probe_version_replies) / sizeof(probe_version_replies[0]) == + PROBE_CONTROLLER_COUNT, "one captured version/address per controller"); #endif #define LOG_CAPACITY 8192u @@ -39,7 +44,7 @@ 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 uint16_t string_descriptors[PROBE_CONTROLLER_COUNT][64]; static uint32_t input_reports, command_drops; #ifdef SWITCH2_PROBE_USB_INIT #define REPLY_CAPACITY 4u @@ -48,37 +53,67 @@ typedef struct { 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; +typedef struct { + uint8_t instance; + probe_protocol_state protocol; + queued_reply replies[REPLY_CAPACITY]; + uint8_t reply_head, reply_count; + bool reply_inflight; + uint16_t reply_remaining; + uint8_t command_frame[PROBE_COMMAND_MAX_SIZE]; + uint16_t command_used, command_expected; + uint32_t last_input_ms, input_reports; + uint32_t command_drops; #ifdef SWITCH_PICO_SWITCH2_USB_BRIDGE -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; + uint32_t last_controller_poll_ms; + uint16_t last_delivered_buttons; + bool native_stream_ready; + uint32_t last_hid_complete_ms; + bool hid_completion_seen; + uint32_t mouse_delivered_reports, mouse_logged_reports; + int64_t mouse_delivered_x, mouse_delivered_y; #ifdef SWITCH2_PROBE_TRACE_NATIVE_INPUT -static uint32_t last_native_trace_ms; + uint32_t last_native_trace_ms; #endif +#else + probe_button_state button_test; + uint32_t last_button_ms; + bool button_sample_error; #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; + uint8_t last_delivered_rails; +} probe_usb_controller; +static probe_usb_controller controllers[PROBE_CONTROLLER_COUNT]; #endif _Static_assert(sizeof(probe_device_descriptor) == 18, "device descriptor size"); -_Static_assert(sizeof(probe_configuration_descriptor) == 80, "configuration descriptor size"); -_Static_assert(sizeof(probe_hid_report_descriptor) == 100, "HID descriptor size"); +_Static_assert(sizeof(probe_configuration_descriptor) == + (SWITCH2_PROBE_COMPOSITE ? 151 : 80), "configuration descriptor size"); +_Static_assert(sizeof(probe_hid_report_descriptors[0]) == 100, "HID descriptor size"); + +#if defined(SWITCH2_PROBE_JOIN_CHORD_GATE) && !SWITCH2_PROBE_HUB +_Static_assert(PROBE_CONTROLLER_COUNT == 2, "L+R gate requires both native functions"); +static uint8_t last_join_shoulder_mask; + +static uint8_t join_shoulder_mask(void) { + uint8_t mask = 0; + for (uint8_t instance = 0; instance < PROBE_CONTROLLER_COUNT; ++instance) { + const probe_protocol_state* state = &controllers[instance].protocol; + if (state->controller_active && (state->controller_buttons[0] & 0x10)) + mask |= (uint8_t)(1u << instance); + } + return mask; +} + +static void gate_join_shoulders(uint8_t instance, uint8_t report_id, + uint8_t input[PROBE_INPUT_SIZE]) { + if (join_shoulder_mask() == 3) return; + if (report_id == probe_model_report_id(instance)) + input[2] &= (uint8_t)~0x10u; + else if (report_id == 0x05) + input[probe_model_is_left(instance) ? 6 : 4] &= (uint8_t)~0x40u; + // Only suppress a real shoulder bit; never insert a counterpart press. +} +#endif int probe_debug_printf(const char* format, ...) { char message[512]; @@ -137,7 +172,7 @@ static void log_packet(const char* kind, uint8_t instance, uint8_t report_id, } uint8_t const* tud_descriptor_device_cb(void) { - probe_debug_printf("[PROBE] DEVICE_DESCRIPTOR 057e:2066\n"); + probe_debug_printf("[PROBE] DEVICE_DESCRIPTOR 057e:%04x\n", (unsigned)PROBE_JOYCON_PID); return probe_device_descriptor; } @@ -148,39 +183,74 @@ uint8_t const* tud_descriptor_configuration_cb(uint8_t index) { 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; + return instance < PROBE_CONTROLLER_COUNT ? probe_hid_report_descriptors[instance] : NULL; } -uint16_t const* tud_descriptor_string_cb(uint8_t index, uint16_t langid) { +static uint16_t const* controller_string_descriptor(uint8_t instance, uint8_t index, + uint16_t langid) { // Manufacturer/product/serial match the published reference. Remaining // descriptor labels describe the probe; their genuine strings are unknown. static const char* const strings[] = { - "", "Nintendo", "Joy-Con 2 (R)", "00", "USB configuration", "HID", "Commands"}; - probe_debug_printf("[PROBE] STRING_DESCRIPTOR index=%u lang=%04x\n", index, langid); + "", "Nintendo", PROBE_JOYCON_PRODUCT, "00", "USB configuration", "HID", "Commands", + "Left HID", "Left commands"}; + if (instance >= PROBE_CONTROLLER_COUNT) return NULL; + uint16_t* descriptor = string_descriptors[instance]; + probe_debug_printf("[PROBE] STRING_DESCRIPTOR itf=%u index=%u lang=%04x\n", + instance, index, langid); if (index == 0) { - string_descriptor[0] = (TUSB_DESC_STRING << 8) | 4; - string_descriptor[1] = 0x0409; - return string_descriptor; + descriptor[0] = (TUSB_DESC_STRING << 8) | 4; + descriptor[1] = 0x0409; + return descriptor; } if (index >= sizeof(strings) / sizeof(strings[0])) return NULL; - size_t count = strlen(strings[index]); + const char* text = index == 2 && probe_model_is_left(instance) ? + "Joy-Con 2 (L)" : strings[index]; + size_t count = strlen(text); if (count > 63) count = 63; - string_descriptor[0] = (uint16_t)((TUSB_DESC_STRING << 8) | (2 + count * 2)); + 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; + descriptor[i + 1] = (uint8_t)text[i]; + return descriptor; } +uint16_t const* tud_descriptor_string_cb(uint8_t index, uint16_t langid) { + return controller_string_descriptor(0, index, langid); +} + +#if SWITCH2_PROBE_HUB +const uint8_t* native_joycon_device_descriptor(uint8_t instance) { + if (instance >= PROBE_CONTROLLER_COUNT) return NULL; + probe_debug_printf("[PROBE] DEVICE_DESCRIPTOR itf=%u 057e:%04x\n", + instance, probe_model_pid(instance)); + return probe_model_is_left(instance) ? probe_left_device_descriptor : probe_device_descriptor; +} + +const uint8_t* native_joycon_configuration_descriptor(uint8_t instance) { + // HUB is mutually exclusive with COMPOSITE: each address uses native EP1/2. + return instance < PROBE_CONTROLLER_COUNT ? probe_configuration_descriptor : NULL; +} + +const uint16_t* native_joycon_string_descriptor(uint8_t instance, uint8_t index, + uint16_t language_id) { + if (index > 6) return NULL; + return controller_string_descriptor(instance, index, language_id); +} +#endif + uint16_t tud_hid_get_report_cb(uint8_t instance, uint8_t report_id, hid_report_type_t report_type, uint8_t* buffer, uint16_t requested_length) { #ifdef SWITCH2_PROBE_USB_INIT uint8_t input[PROBE_INPUT_SIZE]; - if (instance == 0 && report_type == HID_REPORT_TYPE_INPUT && - probe_protocol_report(&protocol, report_id, input, sizeof(input))) { + if (instance < PROBE_CONTROLLER_COUNT && report_type == HID_REPORT_TYPE_INPUT && + probe_protocol_report(&controllers[instance].protocol, report_id, input, sizeof(input))) { const uint16_t size = requested_length < sizeof(input) ? requested_length : sizeof(input); +#if defined(SWITCH2_PROBE_JOIN_CHORD_GATE) && !SWITCH2_PROBE_HUB + gate_join_shoulders(instance, report_id, input); +#endif memcpy(buffer, input, size); - probe_debug_printf("[PROBE] GET_REPORT id=%02x diagnostic length=%u\n", report_id, size); + probe_debug_printf("[PROBE] GET_REPORT itf=%u id=%02x diagnostic length=%u\n", + instance, report_id, size); return size; } #else @@ -200,277 +270,297 @@ void tud_hid_set_report_cb(uint8_t instance, uint8_t report_id, } #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"); +static bool save_pairing(void* context, const uint8_t* data, size_t length) { + const probe_usb_controller* controller = context; + const bool saved = probe_storage_save(controller->instance, data, length); + probe_debug_printf("[PROBE] Virtual pairing persistence itf=%u %s\n", + controller->instance, saved ? "verified" : "failed"); return saved; } -static void reset_protocol(void) { -#ifdef SWITCH_PICO_SWITCH2_USB_BRIDGE - probe_controller_input_cancel_sample(); +#ifdef SWITCH2_PROBE_MEMORY +static bool read_memory(void* context, uint32_t address, uint8_t* data, size_t length) { + const probe_usb_controller* controller = context; + return probe_memory_read(controller->instance, address, data, length); +} #endif - 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; +static bool play_sample(void* context, uint8_t sample_id, uint64_t* token) { + const probe_usb_controller* controller = context; + return probe_controller_input_play_sample(controller->instance, sample_id, token); +} +#endif + +static void reset_controller_protocol(uint8_t instance) { + probe_usb_controller* controller = &controllers[instance]; +#ifdef SWITCH_PICO_SWITCH2_USB_BRIDGE + probe_controller_input_cancel_sample(instance); + probe_controller_input_set_native_stream(instance, false); +#endif + memset(controller, 0, sizeof(*controller)); + controller->instance = instance; + controller->command_expected = 8; + probe_protocol_state* protocol = &controller->protocol; + probe_protocol_reset(protocol, probe_model_is_left(instance)); + protocol->context = controller; + memcpy(protocol->controller_address, probe_version_replies[instance] + 10, + sizeof(protocol->controller_address)); + protocol->firmware_version = probe_firmware_versions[instance]; + protocol->save_pairing = save_pairing; +#ifdef SWITCH_PICO_SWITCH2_USB_BRIDGE + protocol->play_sample = play_sample; #endif #ifdef SWITCH2_PROBE_MEMORY uint8_t stick_calibration[9]; - if (!probe_memory_right_stick_calibration(stick_calibration)) + if (!probe_memory_stick_calibration(instance, stick_calibration)) panic("Invalid captured Joy-Con stick calibration"); - memcpy(protocol.right_stick_center, stick_calibration, sizeof(protocol.right_stick_center)); + memcpy(protocol->stick_center, stick_calibration, sizeof(protocol->stick_center)); #if SWITCH2_BRIDGE_WII_INPUT probe_controller_input_set_stick_calibration(stick_calibration); -#endif - 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 -#if SWITCH2_BRIDGE_WII_INPUT probe_controller_input_set_native_features(0); #endif - 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; -#ifdef SWITCH2_PROBE_TRACE_NATIVE_INPUT - last_native_trace_ms = 0; + protocol->read_memory = read_memory; #endif -#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; + uint8_t pairing[PROBE_PAIRING_BLOB_SIZE]; + if (probe_storage_load(instance, pairing, sizeof(pairing)) && + probe_protocol_restore_pairing(protocol, pairing, sizeof(pairing))) { + probe_debug_printf("[PROBE] Restored own virtual pairing record itf=%u\n", instance); + } } -static void complete_command(void) { +static void reset_protocol(void) { +#if defined(SWITCH2_PROBE_JOIN_CHORD_GATE) && !SWITCH2_PROBE_HUB + last_join_shoulder_mask = 0; +#endif + for (uint8_t instance = 0; instance < PROBE_CONTROLLER_COUNT; ++instance) + reset_controller_protocol(instance); +} + +static void complete_command(probe_usb_controller* controller) { + const uint8_t instance = controller->instance; + probe_protocol_state* protocol = &controller->protocol; + const uint8_t* command = controller->command_frame; // Log complete frames rather than fragments so key material can be redacted. - log_packet("BULK_OUT", 0, 0, command_frame, command_used); - if (reply_count == REPLY_CAPACITY) { + log_packet("BULK_OUT", instance, 0, command, controller->command_used); + if (controller->reply_count == REPLY_CAPACITY) { ++command_drops; - probe_debug_printf("[PROBE] Command captured but not executed: reply queue full\n"); + ++controller->command_drops; + probe_debug_printf("[PROBE] Command not executed itf=%u: reply queue full\n", instance); return; } - queued_reply* reply = &replies[(reply_head + reply_count) % REPLY_CAPACITY]; + queued_reply* reply = &controller->replies[ + (controller->reply_head + controller->reply_count) % REPLY_CAPACITY]; #ifdef SWITCH_PICO_SWITCH2_USB_BRIDGE - const uint8_t previous_report_id = protocol.report_id; - const uint8_t previous_features = protocol.enabled_features; + 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), + protocol, command, controller->command_used, reply->data, sizeof(reply->data), &reply->deferred_token); if (length) { #ifdef SWITCH_PICO_SWITCH2_USB_BRIDGE - if (previous_report_id != protocol.report_id || - previous_features != protocol.enabled_features) { - probe_controller_input_set_native_stream(false); - native_stream_ready = false; + if (previous_report_id != protocol->report_id || + previous_features != protocol->enabled_features) { + probe_controller_input_set_native_stream(instance, false); + controller->native_stream_ready = false; #if SWITCH2_BRIDGE_WII_INPUT - probe_controller_input_set_native_features(protocol.enabled_features); + probe_controller_input_set_native_features(protocol->enabled_features); #endif } #endif reply->length = (uint8_t)length; - ++reply_count; + ++controller->reply_count; if (reply->deferred_token) { - probe_debug_printf("[PROBE] Sample %u awaiting source completion token=%" PRIu64 "\n", - command_frame[8], reply->deferred_token); + probe_debug_printf("[PROBE] Sample %u itf=%u awaiting source token=%" PRIu64 "\n", + command[8], instance, reply->deferred_token); } else { - log_packet("BULK_REPLY_QUEUED", 0, 0, reply->data, reply->length); + log_packet("BULK_REPLY_QUEUED", instance, 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); + if (command[0] == 0x09) + probe_debug_printf("[PROBE] Virtual player LEDs itf=%u mask=%x flashing=%u\n", + instance, protocol->player_leds, protocol->player_leds_flashing); + if (command[0] == 0x0c) + probe_debug_printf("[PROBE] Virtual features itf=%u mask=%02x enabled=%02x\n", + instance, protocol->feature_mask, protocol->enabled_features); + if (command[0] == 0x0a && command[3] == 8) + probe_debug_printf("[PROBE] Virtual vibration parameters itf=%u stored; no motor output\n", instance); + if (command[0] == 0x03 && command[3] == 0x0c) + probe_debug_printf("[PROBE] Runtime 03/0C itf=%u value=%u\n", instance, protocol->runtime03_0c); } else { - probe_debug_printf("[PROBE] Command %02x/%02x length=%u capture-only or malformed\n", - command_frame[0], command_frame[3], command_used); + probe_debug_printf("[PROBE] Command itf=%u %02x/%02x length=%u capture-only or malformed\n", + instance, command[0], command[3], controller->command_used); } } -static void consume_bulk_packet(const uint8_t* buffer, uint16_t length) { +static void consume_bulk_packet(probe_usb_controller* controller, 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; + controller->command_frame[controller->command_used++] = buffer[i]; + if (controller->command_used == 8) + controller->command_expected = (uint16_t)(8 + controller->command_frame[5]); + if (controller->command_used == controller->command_expected) { + complete_command(controller); + controller->command_used = 0; + controller->command_expected = 8; } } - // A short 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; + // A short packet/ZLP ends this interface's OUT transfer, not its sibling's. + if (length < CFG_TUD_VENDOR_EPSIZE && controller->command_used) { + probe_debug_printf("[PROBE] Truncated command discarded itf=%u received=%u expected=%u\n", + controller->instance, controller->command_used, controller->command_expected); + controller->command_used = 0; + controller->command_expected = 8; } } #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); +static void button_test_task(probe_usb_controller* controller, uint32_t now) { + probe_protocol_state* protocol = &controller->protocol; + const bool ready = probe_transport_mounted(controller->instance) && + !probe_transport_suspended(controller->instance) && + protocol->initialized && (protocol->enabled_features & 1); bool pressed; if (!ready) { - pressed = probe_button_update(&button_test, -1, false); - last_button_ms = now; + pressed = probe_button_update(&controller->button_test, -1, false); + controller->last_button_ms = now; } else { - if (now - last_button_ms < 10) return; - last_button_ms = now; + if (now - controller->last_button_ms < 10) return; + controller->last_button_ms = now; const int sample = bootsel_button_sample(); - if (sample < 0 && !button_sample_error) + if (sample < 0 && !controller->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); + controller->button_sample_error = sample < 0; + pressed = probe_button_update(&controller->button_test, sample, true); } - if (protocol.test_rail_buttons != pressed) { - protocol.test_rail_buttons = pressed; + 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; +static void controller_input_task(probe_usb_controller* controller, uint32_t now) { + if (now - controller->last_controller_poll_ms < 4) return; + controller->last_controller_poll_ms = now; + const uint8_t instance = controller->instance; + probe_protocol_state* protocol = &controller->protocol; probe_controller_input source = {0}; - probe_controller_input_poll(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; + probe_controller_input_poll(instance, now, &source); + const bool output_active = source.active && probe_transport_mounted(instance) && + !probe_transport_suspended(instance); + if (protocol->controller_active != output_active) + probe_debug_printf("[PROBE] Controller input itf=%u %s\n", + instance, output_active ? "active" : "neutral (disconnected/stale)"); + protocol->controller_active = output_active; if (output_active) { - memcpy(protocol.controller_buttons, source.buttons, sizeof(source.buttons)); - memcpy(protocol.controller_stick, source.stick, sizeof(source.stick)); + 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)); + 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); + // Each HID endpoint must make its own progress before consuming donor packets. + controller->native_stream_ready = probe_transport_mounted(instance) && + !probe_transport_suspended(instance) && + protocol->initialized && protocol->report_id == probe_model_report_id(instance) && + controller->hid_completion_seen && + (uint32_t)(now - controller->last_hid_complete_ms) < 500; + probe_controller_input_set_native_stream(instance, controller->native_stream_ready); } -static void gate_native_report(uint8_t input[PROBE_INPUT_SIZE]) { -#if SWITCH2_BRIDGE_WII_INPUT - // Generated status follows virtual feature state, not a donor snapshot. - input[8] = (uint8_t)(0x30 | ((protocol.enabled_features & 0x20) ? 8 : 0)); -#endif - 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); -#ifdef SWITCH2_PROBE_OMIT_NATIVE_IMU - // Deliberate A/B fault injection: leave every other field and feature bit intact. - memset(input + 15, 0, 41); -#elif defined(SWITCH2_PROBE_ZERO_NATIVE_IMU_PAYLOAD) - if (!(protocol.enabled_features & 4)) input[15] = 0; - memset(input + 16, 0, 40); // Otherwise preserve the genuine length byte. -#else - if (!(protocol.enabled_features & 4)) memset(input + 15, 0, 41); -#endif -} #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]; +static void protocol_task(probe_usb_controller* controller, uint32_t now) { + const uint8_t instance = controller->instance; + if (!probe_transport_mounted(instance) || probe_transport_suspended(instance)) return; + probe_protocol_state* protocol = &controller->protocol; + if (controller->reply_count && !controller->reply_inflight) { + queued_reply* reply = &controller->replies[controller->reply_head]; bool ready = reply->deferred_token == 0; #ifdef SWITCH_PICO_SWITCH2_USB_BRIDGE if (!ready) { - const int result = probe_controller_input_sample_result(reply->deferred_token, now); + const int result = probe_controller_input_sample_result(instance, reply->deferred_token, now); if (result > 0) { #if SWITCH2_BRIDGE_WII_INPUT - probe_debug_printf("[PROBE] Wii cue dispatched token=%" PRIu64 "\n", - reply->deferred_token); + probe_debug_printf("[PROBE] Wii cue dispatched itf=%u token=%" PRIu64 "\n", + instance, reply->deferred_token); #else - probe_debug_printf("[PROBE] Source sample ACK token=%" PRIu64 "\n", - reply->deferred_token); + probe_debug_printf("[PROBE] Source sample ACK itf=%u token=%" PRIu64 "\n", + instance, reply->deferred_token); #endif reply->deferred_token = 0; ready = true; - log_packet("BULK_REPLY_QUEUED", 0, 0, reply->data, reply->length); + log_packet("BULK_REPLY_QUEUED", instance, 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; + probe_debug_printf("[PROBE] Source sample failed itf=%u token=%" PRIu64 "; no USB ACK\n", + instance, reply->deferred_token); + controller->reply_head = (uint8_t)((controller->reply_head + 1) % REPLY_CAPACITY); + --controller->reply_count; + ++controller->command_drops; ++command_drops; } } #endif - if (ready && 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 (ready && probe_transport_vendor_write_available(instance) >= reply->length) { + if (probe_transport_vendor_write(instance, reply->data, reply->length) == reply->length) { + controller->reply_inflight = true; + controller->reply_remaining = reply->length; + probe_transport_vendor_write_flush(instance); + controller->reply_head = (uint8_t)((controller->reply_head + 1) % REPLY_CAPACITY); + --controller->reply_count; } } } - if (protocol.initialized && now - last_input_ms >= 4 && tud_hid_n_ready(0)) { + if (protocol->initialized && now - controller->last_input_ms >= 4 && + probe_transport_hid_ready(instance)) { uint8_t input[PROBE_INPUT_SIZE]; size_t length = 0; #ifdef SWITCH_PICO_SWITCH2_USB_BRIDGE uint32_t native_serial = 0; - if (protocol.report_id == 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. + if (protocol->report_id == probe_model_report_id(instance) && + protocol->controller_active && controller->native_stream_ready) { + native_serial = probe_controller_input_peek_native_report(instance, now, input); + if (!native_serial) return; length = sizeof(input); - gate_native_report(input); +#if SWITCH2_BRIDGE_WII_INPUT + input[8] = (uint8_t)(0x30 | ((protocol->enabled_features & 0x20) ? 8 : 0)); +#endif + probe_protocol_gate_native_report(protocol, input); } #endif - if (!length) { - length = probe_protocol_report(&protocol, protocol.report_id, input, sizeof(input)); - } - if (length && tud_hid_n_report(0, protocol.report_id, input, (uint16_t)length)) { + if (!length) + length = probe_protocol_report(protocol, protocol->report_id, input, sizeof(input)); +#if defined(SWITCH2_PROBE_JOIN_CHORD_GATE) && !SWITCH2_PROBE_HUB + if (length) gate_join_shoulders(instance, protocol->report_id, input); +#endif + if (length && probe_transport_hid_report(instance, protocol->report_id, input, (uint16_t)length)) { #ifdef SWITCH_PICO_SWITCH2_USB_BRIDGE if (native_serial) { - if (!probe_controller_input_commit_native_report(native_serial)) - probe_debug_printf("[PROBE] Native stream changed during HID submission\n"); - protocol.report_counter = input[0]; + if (!probe_controller_input_commit_native_report(instance, native_serial)) + probe_debug_printf("[PROBE] Native stream changed during HID submission itf=%u\n", instance); + protocol->report_counter = input[0]; } #endif - ++protocol.report_counter; + ++protocol->report_counter; + ++controller->input_reports; ++input_reports; - last_input_ms = now; + controller->last_input_ms = now; } } } #endif +#if SWITCH2_PROBE_HUB +void native_joycon_usb_reset(uint8_t instance) { + if (instance >= PROBE_CONTROLLER_COUNT) return; +#ifdef SWITCH2_PROBE_USB_INIT + reset_controller_protocol(instance); +#endif + probe_debug_printf("[PROBE] USB_RESET itf=%u\n", instance); +} +#endif + #ifdef SWITCH_PICO_SWITCH2_USB_BRIDGE static int32_t signed_mouse_delta(const uint8_t* data) { const uint16_t raw = (uint16_t)(data[0] | ((uint16_t)data[1] << 8)); @@ -481,53 +571,56 @@ 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"); + if (instance < PROBE_CONTROLLER_COUNT && report_type == HID_REPORT_TYPE_INPUT) { + probe_controller_input_set_native_stream(instance, false); + controllers[instance].native_stream_ready = false; + controllers[instance].hid_completion_seen = false; + probe_debug_printf("[PROBE] HID input transfer failed itf=%u; pending native packets discarded\n", instance); } } #endif void tud_hid_report_complete_cb(uint8_t instance, const uint8_t* report, uint16_t length) { #ifdef SWITCH2_PROBE_USB_INIT - if (instance != 0 || length != PROBE_INPUT_SIZE + 1) return; + if (instance >= PROBE_CONTROLLER_COUNT || length != PROBE_INPUT_SIZE + 1) return; + probe_usb_controller* controller = &controllers[instance]; + const uint8_t native_id = probe_model_report_id(instance); + const uint8_t common_buttons_offset = probe_model_is_left(instance) ? 7 : 5; uint8_t rails; - if (report[0] == 0x08) rails = (report[4] >> 6) & 3; - else if (report[0] == 0x05) rails = (report[5] >> 4) & 3; + if (report[0] == native_id) rails = (report[4] >> 6) & 3; + else if (report[0] == 0x05) rails = (report[common_buttons_offset] >> 4) & 3; else return; #ifdef SWITCH_PICO_SWITCH2_USB_BRIDGE - last_hid_complete_ms = to_ms_since_boot(get_absolute_time()); - hid_completion_seen = true; - if (report[0] == 0x08) { + controller->last_hid_complete_ms = to_ms_since_boot(get_absolute_time()); + controller->hid_completion_seen = true; + if (report[0] == native_id) { const int32_t dx = signed_mouse_delta(report + 10); const int32_t dy = signed_mouse_delta(report + 12); #ifdef SWITCH2_PROBE_TRACE_NATIVE_INPUT - if ((uint32_t)(last_hid_complete_ms - last_native_trace_ms) >= 1000) { - last_native_trace_ms = last_hid_complete_ms; - // Observe the completed transfer, not a proposed or ungated report. + if ((uint32_t)(controller->last_hid_complete_ms - controller->last_native_trace_ms) >= 1000) { + controller->last_native_trace_ms = controller->last_hid_complete_ms; log_packet("NATIVE_INPUT_DELIVERED", instance, report[0], report, length); } #endif if (dx || dy) { - ++mouse_delivered_reports; - mouse_delivered_x += dx; - mouse_delivered_y += dy; + ++controller->mouse_delivered_reports; + controller->mouse_delivered_x += dx; + controller->mouse_delivered_y += dy; } } - const uint16_t buttons = report[0] == 0x08 ? + const uint16_t buttons = report[0] == native_id ? (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); + (uint16_t)(report[common_buttons_offset] | ((uint16_t)report[6] << 8)); + if (buttons != controller->last_delivered_buttons) { + controller->last_delivered_buttons = buttons; + probe_debug_printf("[PROBE] CONTROLLER_REPORT delivered itf=%u id=%02x buttons=%04x\n", + instance, report[0], buttons); } #endif - if (rails != 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); + if (rails != controller->last_delivered_rails) { + controller->last_delivered_rails = rails; + probe_debug_printf("[PROBE] TEST_REPORT delivered itf=%u id=%02x SL=%u SR=%u\n", + instance, report[0], (rails >> 1) & 1, rails & 1); } #else (void)instance; @@ -538,29 +631,28 @@ void tud_hid_report_complete_cb(uint8_t instance, const uint8_t* report, uint16_ void tud_vendor_rx_cb(uint8_t instance, const uint8_t* buffer, uint16_t length) { ++bulk_packets; + if (instance >= PROBE_CONTROLLER_COUNT) return; #ifdef SWITCH2_PROBE_USB_INIT - if (instance == 0) consume_bulk_packet(buffer, length); + consume_bulk_packet(&controllers[instance], 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; - } + probe_transport_vendor_discard_received(instance); } 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; + if (instance >= PROBE_CONTROLLER_COUNT) return; + probe_usb_controller* controller = &controllers[instance]; + if (controller->reply_inflight) { + if (length <= controller->reply_remaining) { + controller->reply_remaining -= (uint16_t)length; + // Exact packet multiples finish only after the transport's ZLP. + if (controller->reply_remaining == 0 && length < CFG_TUD_VENDOR_EPSIZE) + controller->reply_inflight = false; } else { - probe_debug_printf("[PROBE] Unexpected bulk completion; pending=%u\n", reply_remaining); + probe_debug_printf("[PROBE] Unexpected bulk completion itf=%u pending=%u\n", + instance, controller->reply_remaining); } } #endif @@ -575,42 +667,52 @@ bool tud_vendor_control_xfer_cb(uint8_t rhport, uint8_t stage, if (probe_bootsel_vendor_control(rhport, stage, request)) return true; #endif +#if SWITCH2_PROBE_HUB + // Each USB address owns native interfaces 0/1; rhport is its device slot. + if (rhport < 1 || rhport > PROBE_CONTROLLER_COUNT || request->wIndex >= 2) return false; + const uint8_t instance = (uint8_t)(rhport - 1); +#else + // Device-level index zero names the primary identity. Explicit interface + // indexes can select the sibling; never infer an identity from timing. + if (request->wIndex >= 2 * PROBE_CONTROLLER_COUNT) return false; + const uint8_t instance = (uint8_t)(request->wIndex / 2); +#endif #ifdef SWITCH2_PROBE_IDENTITY_REPLY - if (request->bmRequestType == 0xc0 && request->bRequest == 0x03 && - request->wValue == 0 && request->wIndex == 0) { + if ((request->bmRequestType == 0xc0 || request->bmRequestType == 0xc1) && + request->bRequest == 0x03 && request->wValue == 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)); + log_packet("IDENTITY_REPLY", instance, 3, probe_identity_replies[instance], + sizeof(probe_identity_replies[instance])); + return probe_transport_control_xfer(rhport, request, (void*)probe_identity_replies[instance], + sizeof(probe_identity_replies[instance])); } return true; } #endif #ifdef SWITCH2_PROBE_VERSION_REPLY - if (request->bmRequestType == 0xc0 && request->bRequest == 0x02 && - request->wValue == 0 && request->wIndex == 0) { + if ((request->bmRequestType == 0xc0 || request->bmRequestType == 0xc1) && + request->bRequest == 0x02 && request->wValue == 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)); + log_packet("VERSION_REPLY", instance, 2, probe_version_replies[instance], + sizeof(probe_version_replies[instance])); + return probe_transport_control_xfer(rhport, request, (void*)probe_version_replies[instance], + sizeof(probe_version_replies[instance])); } return true; } #endif #ifdef SWITCH2_PROBE_ACK_SETUP04 - // 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) { + // Preserve the observed setup payload; its parameter semantics are unknown. + if ((request->bmRequestType == 0x40 || request->bmRequestType == 0x41) && + request->bRequest == 0x04 && request->wValue == 0x0276 && request->wLength == 0) { if (stage == CONTROL_STAGE_SETUP) - return tud_control_status(rhport, request); + return probe_transport_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); + probe_debug_printf("[PROBE] SETUP04 itf=%u acknowledged value=%04x\n", + instance, request->wValue); } return true; } @@ -618,6 +720,7 @@ bool tud_vendor_control_xfer_cb(uint8_t rhport, uint8_t stage, return false; } +#if !SWITCH2_PROBE_HUB void tud_mount_cb(void) { #ifdef SWITCH2_PROBE_USB_INIT reset_protocol(); @@ -633,41 +736,60 @@ void tud_umount_cb(void) { void tud_suspend_cb(bool remote_wakeup_en) { probe_debug_printf("[PROBE] SUSPEND wake=%u\n", remote_wakeup_en); #ifdef SWITCH2_PROBE_USB_INIT + for (uint8_t instance = 0; instance < PROBE_CONTROLLER_COUNT; ++instance) { + probe_usb_controller* controller = &controllers[instance]; #ifndef SWITCH_PICO_SWITCH2_USB_BRIDGE - (void)probe_button_update(&button_test, -1, false); + (void)probe_button_update(&controller->button_test, -1, false); #endif - protocol.test_rail_buttons = false; - protocol.controller_active = false; + controller->protocol.test_rail_buttons = false; + controller->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; + probe_controller_input_set_native_stream(instance, false); + controller->native_stream_ready = false; + controller->hid_completion_seen = false; #endif + } #endif } void tud_resume_cb(void) { probe_debug_printf("[PROBE] RESUME\n"); } +#endif int main(void) { + #if SWITCH2_PROBE_HUB + native_hub_startup_guard(); + #endif #ifdef SWITCH_PICO_SWITCH2_USB_BRIDGE probe_controller_input_clock_init(); #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"); + probe_debug_printf("\n[PROBE] " PROBE_JOYCON_PRODUCT " Bluetooth-to-USB controller/native mouse bridge\n"); #else - probe_debug_printf("\n[PROBE] Joy-Con 2 (R) USB enumeration recorder\n"); + probe_debug_printf("\n[PROBE] " PROBE_JOYCON_PRODUCT " USB enumeration recorder\n"); +#endif +#if SWITCH2_PROBE_HUB + probe_debug_printf("[PROBE] NATIVE_HUB: device1 right PID2066, device2 left PID2067; each HID0/vendor1 EP1/2; no shoulder gate\n"); +#endif +#if SWITCH2_PROBE_COMPOSITE +#ifdef SWITCH2_PROBE_JOIN_CHORD_GATE + probe_debug_printf("[PROBE] JOIN_CHORD_GATE enabled: physical L+R required; no synthesized presses or USB initialization\n"); +#endif + probe_debug_printf("[PROBE] COMPOSITE: right HID0/vendor1, left HID2/vendor3; shared USB PID2066\n"); #endif #ifdef SWITCH2_PROBE_OMIT_NATIVE_IMU - probe_debug_printf("[PROBE] ACTIVATION_TEST=no-imu: native08 bytes 15..55 omitted; features, power, status, counters and cadence unchanged\n"); + probe_debug_printf("[PROBE] ACTIVATION_TEST=no-imu: native%02x bytes %u..%u omitted; features, power, status, counters and cadence unchanged\n", + PROBE_NATIVE_REPORT_ID, PROBE_IMU_LENGTH_OFFSET, PROBE_IMU_DATA_OFFSET + 39u); #elif defined(SWITCH2_PROBE_ZERO_NATIVE_IMU_PAYLOAD) - probe_debug_printf("[PROBE] ACTIVATION_TEST=zero-imu-payload: native08 bytes 16..55 zeroed; length, features and all other fields unchanged\n"); + probe_debug_printf("[PROBE] ACTIVATION_TEST=zero-imu-payload: native%02x bytes %u..%u zeroed; length, features and all other fields unchanged\n", + PROBE_NATIVE_REPORT_ID, PROBE_IMU_DATA_OFFSET, PROBE_IMU_DATA_OFFSET + 39u); #endif #ifdef SWITCH_PICO_SWITCH2_USB_BRIDGE probe_controller_input_init(); #if SWITCH2_BRIDGE_WII_INPUT probe_debug_printf("[PROBE] UART0 GP0=TX, 115200 8N1; selected Wii IR/MotionPlus source enabled\n"); #else - probe_debug_printf("[PROBE] UART0 GP0=TX, 115200 8N1; selected right Joy-Con Bluetooth source enabled\n"); + probe_debug_printf("[PROBE] UART0 GP0=TX, 115200 8N1; %u selected Joy-Con Bluetooth source(s)\n", + PROBE_CONTROLLER_COUNT); #endif #else probe_debug_printf("[PROBE] UART0 GP0=TX, 115200 8N1; Bluetooth disabled\n"); @@ -685,14 +807,15 @@ int main(void) { #endif #ifdef SWITCH2_PROBE_USB_INIT reset_protocol(); - probe_debug_printf("[PROBE] Own virtual pairing storage offset=%08" PRIx32 "\n", - probe_storage_offset()); + for (uint8_t instance = 0; instance < PROBE_CONTROLLER_COUNT; ++instance) + probe_debug_printf("[PROBE] Own virtual pairing itf=%u offset=%08" PRIx32 "\n", + instance, probe_storage_offset(instance)); #ifdef SWITCH_PICO_SWITCH2_USB_BRIDGE #if SWITCH2_BRIDGE_WII_INPUT probe_debug_printf("[PROBE] Wii IR drives native mouse movement; buttons retain profile mapping; keep Wii still for MotionPlus calibration\n"); probe_debug_printf("[PROBE] Hold BOOTSEL2s for pairing; Wii cue feedback uses bounded ERM patterns, not HD audio waveforms\n"); #else - probe_debug_printf("[PROBE] Live right Joy-Con buttons/stick/native mouse; hold BOOTSEL 2s for Bluetooth pairing (never clears pairings)\n"); + probe_debug_printf("[PROBE] Live Joy-Con buttons/stick/native mouse; hold BOOTSEL 2s for Bluetooth pairing (never clears pairings)\n"); #endif #else probe_debug_printf("[PROBE] Manual input test: hold BOOTSEL for SL+SR, release for neutral; no controller forwarding\n"); @@ -702,12 +825,22 @@ int main(void) { #endif #ifdef SWITCH_PICO_SWITCH2_USB_BRIDGE if (!probe_controller_input_start()) - panic("Bluetooth source could not establish multicore flash coordination"); + panic("Bluetooth source startup failed"); #endif +#if SWITCH2_PROBE_HUB + if (!native_hub_init()) + panic("Native hub startup failed"); +#else tud_init(0); +#endif uint32_t last_heartbeat = 0; while (true) { +#if SWITCH2_PROBE_HUB + probe_controller_input_task(); + native_hub_task(); +#else tud_task(); +#endif drain_log(); const uint32_t now = to_ms_since_boot(get_absolute_time()); #ifdef SWITCH_PICO_SWITCH2_USB_BRIDGE @@ -716,12 +849,31 @@ int main(void) { #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); + probe_debug_printf("[PROBE] Bluetooth pairing window requested; put the selected source in SYNC pairing mode\n"); +#endif + for (uint8_t instance = 0; instance < PROBE_CONTROLLER_COUNT; ++instance) { +#ifdef SWITCH_PICO_SWITCH2_USB_BRIDGE + controller_input_task(&controllers[instance], now); +#else + button_test_task(&controllers[instance], now); +#endif +#if !defined(SWITCH2_PROBE_JOIN_CHORD_GATE) || SWITCH2_PROBE_HUB + protocol_task(&controllers[instance], now); +#endif + } +#if defined(SWITCH2_PROBE_JOIN_CHORD_GATE) && !SWITCH2_PROBE_HUB + const uint8_t shoulder_mask = join_shoulder_mask(); + if (shoulder_mask != last_join_shoulder_mask) { + last_join_shoulder_mask = shoulder_mask; + probe_debug_printf("[PROBE %" PRIu32 "] JOIN_CHORD right=%u left=%u open=%u initialized=%u/%u\n", + now, (shoulder_mask & 1) != 0, (shoulder_mask & 2) != 0, + shoulder_mask == 3, controllers[0].protocol.initialized, + controllers[1].protocol.initialized); + } + // Poll both sources before either USB submission observes the chord. + for (uint8_t instance = 0; instance < PROBE_CONTROLLER_COUNT; ++instance) + protocol_task(&controllers[instance], now); #endif - protocol_task(now); #endif if (now - last_heartbeat >= 1000) { last_heartbeat = now; @@ -730,15 +882,27 @@ int main(void) { " version=%" PRIu32 " setup=%" PRIu32 " inputs=%" PRIu32 " command_drops=%" PRIu32 " log_dropped_bytes=%" PRIu32 "\n", - now, tud_mounted(), bulk_packets, hid_packets, + now, probe_transport_mounted(0), bulk_packets, hid_packets, identity_requests, version_requests, setup_completions, input_reports, command_drops, log_dropped); +#ifdef SWITCH2_PROBE_USB_INIT + for (uint8_t instance = 0; instance < PROBE_CONTROLLER_COUNT; ++instance) { + probe_usb_controller* controller = &controllers[instance]; + probe_debug_printf("[PROBE] CHANNEL itf=%u mounted=%u suspended=%u initialized=%u active=%u report=%02x LEDs=%x inputs=%" PRIu32 + " drops=%" PRIu32 "\n", instance, probe_transport_mounted(instance), + probe_transport_suspended(instance), controller->protocol.initialized, + controller->protocol.controller_active, controller->protocol.report_id, + controller->protocol.player_leds, controller->input_reports, + controller->command_drops); #ifdef SWITCH_PICO_SWITCH2_USB_BRIDGE - if (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); + if (controller->mouse_delivered_reports != controller->mouse_logged_reports) { + controller->mouse_logged_reports = controller->mouse_delivered_reports; + probe_debug_printf("[PROBE] MOUSE_REPORT itf=%u packets=%" PRIu32 + " total_x=%" PRId64 " total_y=%" PRId64 "\n", instance, + controller->mouse_delivered_reports, + controller->mouse_delivered_x, controller->mouse_delivered_y); + } +#endif } #endif } diff --git a/tools/switch2_usb_probe/memory.c b/tools/switch2_usb_probe/memory.c index 9c82609..1ba221b 100644 --- a/tools/switch2_usb_probe/memory.c +++ b/tools/switch2_usb_probe/memory.c @@ -2,21 +2,23 @@ #include "probe_memory_data.h" #include -_Static_assert(sizeof(probe_factory_memory) == 8192, "factory capture size"); -_Static_assert(sizeof(probe_user_calibration) == 4096, "user calibration capture size"); +_Static_assert(sizeof(probe_factory_memories) == PROBE_CONTROLLER_COUNT * 8192u, + "factory capture sizes"); +_Static_assert(sizeof(probe_user_calibrations) == PROBE_CONTROLLER_COUNT * 4096u, + "user calibration capture sizes"); -bool probe_memory_read(uint32_t address, uint8_t* output, size_t length) { - if (!output) return false; +bool probe_memory_read(uint8_t instance, uint32_t address, uint8_t* output, size_t length) { + if (instance >= PROBE_CONTROLLER_COUNT || !output) return false; const uint8_t* source; size_t offset, available; if (address >= 0x13000 && address < 0x15000) { offset = address - 0x13000; - source = probe_factory_memory; - available = sizeof(probe_factory_memory) - offset; + source = probe_factory_memories[instance]; + available = sizeof(probe_factory_memories[instance]) - offset; } else if (address >= 0x1fc000 && address < 0x1fd000) { offset = address - 0x1fc000; - source = probe_user_calibration; - available = sizeof(probe_user_calibration) - offset; + source = probe_user_calibrations[instance]; + available = sizeof(probe_user_calibrations[instance]) - offset; } else { return false; // No fabricated erased bytes, pairing keys, or firmware reads. } @@ -43,12 +45,12 @@ static bool valid_calibration(const uint8_t* data) { return true; } -bool probe_memory_right_stick_calibration(uint8_t output[9]) { - if (!output) return false; - // A solo Joy-Con uses the primary calibration record, even for the right - // controller. User magic precedes its 9-byte record; factory has no magic. - const uint8_t* selected = probe_factory_memory + 0xa8; - const uint8_t* user = probe_user_calibration + 0x40; +bool probe_memory_stick_calibration(uint8_t instance, uint8_t output[9]) { + if (instance >= PROBE_CONTROLLER_COUNT || !output) return false; + // Each Joy-Con's own capture uses the primary calibration record. + // User magic precedes its 9-byte record; factory has no magic. + const uint8_t* selected = probe_factory_memories[instance] + 0xa8; + const uint8_t* user = probe_user_calibrations[instance] + 0x40; if (user[0] == 0xb2 && user[1] == 0xa1 && valid_calibration(user + 2)) selected = user + 2; if (!valid_calibration(selected)) return false; diff --git a/tools/switch2_usb_probe/memory.h b/tools/switch2_usb_probe/memory.h index 2bc4327..72ddb3b 100644 --- a/tools/switch2_usb_probe/memory.h +++ b/tools/switch2_usb_probe/memory.h @@ -3,6 +3,7 @@ #include #include -bool probe_memory_read(uint32_t address, uint8_t* output, size_t length); +// Invalid instances and unavailable ranges leave output unchanged. +bool probe_memory_read(uint8_t instance, uint32_t address, uint8_t* output, size_t length); // Packed center, positive travel, negative travel (two12-bit axes each). -bool probe_memory_right_stick_calibration(uint8_t output[9]); +bool probe_memory_stick_calibration(uint8_t instance, uint8_t output[9]); diff --git a/tools/switch2_usb_probe/model.h b/tools/switch2_usb_probe/model.h new file mode 100644 index 0000000..77dd152 --- /dev/null +++ b/tools/switch2_usb_probe/model.h @@ -0,0 +1,94 @@ +#pragma once + +#include +#include + +#ifndef SWITCH2_PROBE_HUB +#define SWITCH2_PROBE_HUB 0 +#endif + +#if SWITCH2_PROBE_HUB != 0 && SWITCH2_PROBE_HUB != 1 +#error "SWITCH2_PROBE_HUB must be 0 or 1" +#endif + +#ifndef SWITCH2_PROBE_COMPOSITE +#define SWITCH2_PROBE_COMPOSITE 0 +#endif + +#if SWITCH2_PROBE_COMPOSITE != 0 && SWITCH2_PROBE_COMPOSITE != 1 +#error "SWITCH2_PROBE_COMPOSITE must be 0 or 1" +#endif + +#if SWITCH2_PROBE_HUB && SWITCH2_PROBE_COMPOSITE +#error "Native hub and composite USB backends are mutually exclusive" +#endif + +#ifndef SWITCH2_PROBE_JOYCON_LEFT +#define SWITCH2_PROBE_JOYCON_LEFT 0 +#endif + +#if SWITCH2_PROBE_JOYCON_LEFT != 0 && SWITCH2_PROBE_JOYCON_LEFT != 1 +#error "SWITCH2_PROBE_JOYCON_LEFT must be 0 or 1" +#endif + +#if SWITCH2_PROBE_COMPOSITE || SWITCH2_PROBE_HUB +#if SWITCH2_PROBE_JOYCON_LEFT +#error "Dual-controller primary must be Joy-Con 2 (R)" +#endif +#ifndef PROBE_CONTROLLER_COUNT +#define PROBE_CONTROLLER_COUNT 2 +#endif +#if PROBE_CONTROLLER_COUNT != 2 +#error "Dual-controller output requires two controller instances" +#endif +#else +#ifndef PROBE_CONTROLLER_COUNT +#define PROBE_CONTROLLER_COUNT 1 +#endif +#if PROBE_CONTROLLER_COUNT != 1 +#error "Standalone requires one controller instance" +#endif +#endif + +#if SWITCH2_PROBE_JOYCON_LEFT +#define PROBE_JOYCON_PID 0x2067u +#define PROBE_JOYCON_PRODUCT "Joy-Con 2 (L)" +#define PROBE_JOYCON_SIDE "left" +#define PROBE_NATIVE_REPORT_ID 0x07u +#define PROBE_IMU_LENGTH_OFFSET 14u +#define PROBE_IMU_DATA_OFFSET 15u +#else +#define PROBE_JOYCON_PID 0x2066u +#define PROBE_JOYCON_PRODUCT "Joy-Con 2 (R)" +#define PROBE_JOYCON_SIDE "right" +#define PROBE_NATIVE_REPORT_ID 0x08u +#define PROBE_IMU_LENGTH_OFFSET 15u +#define PROBE_IMU_DATA_OFFSET 16u +#endif + +// Instance zero is the standalone model or the dual-controller right function. +// In composite and native hub modes, instance one is the independent left side. +static inline bool probe_model_is_left(uint8_t instance) { +#if SWITCH2_PROBE_COMPOSITE || SWITCH2_PROBE_HUB + return instance == 1; +#else + (void)instance; + return SWITCH2_PROBE_JOYCON_LEFT != 0; +#endif +} + +static inline uint16_t probe_model_pid(uint8_t instance) { + return probe_model_is_left(instance) ? 0x2067u : 0x2066u; +} + +static inline uint8_t probe_model_report_id(uint8_t instance) { + return probe_model_is_left(instance) ? 0x07u : 0x08u; +} + +static inline uint8_t probe_model_imu_length_offset(uint8_t instance) { + return probe_model_is_left(instance) ? 14u : 15u; +} + +static inline uint8_t probe_model_imu_data_offset(uint8_t instance) { + return probe_model_is_left(instance) ? 15u : 16u; +} diff --git a/tools/switch2_usb_probe/probe_build.cmake b/tools/switch2_usb_probe/probe_build.cmake index 66bae3d..51d07ff 100644 --- a/tools/switch2_usb_probe/probe_build.cmake +++ b/tools/switch2_usb_probe/probe_build.cmake @@ -3,6 +3,78 @@ set(SWITCH2_USB_PROBE_DIR "${CMAKE_CURRENT_LIST_DIR}") set(PICO_MBEDTLS_CONFIG_FILE "${SWITCH2_USB_PROBE_DIR}/mbedtls_config.h") +option(SWITCH2_PROBE_COMPOSITE + "Experiment: independent right and left Joy-Con 2 functions on one USB port" OFF) +option(SWITCH2_PROBE_HUB "Native R/L devices on the built-in SIO USB hub" OFF) +if(SWITCH2_PROBE_HUB AND SWITCH2_PROBE_COMPOSITE) + message(FATAL_ERROR "Select native hub or composite, not both") +endif() +option(SWITCH2_PROBE_JOIN_CHORD_GATE + "Experiment: pass L/R shoulder presses only while both physical halves hold them" OFF) +set(SWITCH2_PROBE_SIDE "RIGHT" CACHE STRING "Primary Joy-Con 2 model: LEFT or RIGHT") +set_property(CACHE SWITCH2_PROBE_SIDE PROPERTY STRINGS LEFT RIGHT) +if(SWITCH2_PROBE_SIDE STREQUAL "LEFT") + if(SWITCH2_PROBE_COMPOSITE OR SWITCH2_PROBE_HUB OR SWITCH2_BRIDGE_WII_INPUT) + message(FATAL_ERROR "SWITCH2_PROBE_SIDE=LEFT cannot be combined with composite or Wii input; select RIGHT") + endif() + set(probe_joycon_left 1) +elseif(SWITCH2_PROBE_SIDE STREQUAL "RIGHT") + set(probe_joycon_left 0) +else() + message(FATAL_ERROR "SWITCH2_PROBE_SIDE must be LEFT or RIGHT") +endif() +if(SWITCH2_PROBE_COMPOSITE OR SWITCH2_PROBE_HUB) + if(NOT SWITCH_PICO_SWITCH2_USB_BRIDGE OR SWITCH2_BRIDGE_WII_INPUT + OR NOT SWITCH2_BRIDGE_INPUT STREQUAL "JOYCON2") + message(FATAL_ERROR "Composite Joy-Con 2 requires SWITCH_PICO_SWITCH2_USB_BRIDGE=ON and SWITCH2_BRIDGE_INPUT=JOYCON2") + endif() + set(probe_composite 0) + if(SWITCH2_PROBE_COMPOSITE) + set(probe_composite 1) + endif() + set(probe_controller_count 2) +else() + set(probe_composite 0) + set(probe_controller_count 1) +endif() +if(SWITCH2_PROBE_JOIN_CHORD_GATE AND NOT SWITCH2_PROBE_COMPOSITE) + message(FATAL_ERROR "The L+R shoulder gate requires the composite Joy-Con bridge") +endif() +# Capture, the Bluetooth backend, and TinyUSB must agree before their targets exist. +add_compile_definitions( + SWITCH2_PROBE_JOYCON_LEFT=${probe_joycon_left} + SWITCH2_PROBE_COMPOSITE=${probe_composite} + SWITCH2_PROBE_HUB=$ + PROBE_CONTROLLER_COUNT=${probe_controller_count}) + +set(SWITCH2_BRIDGE_SOURCE_ADDRESS "" CACHE STRING + "Primary physical Bluetooth source address (xx:xx:xx:xx:xx:xx)") +set(SWITCH2_BRIDGE_SECOND_SOURCE_ADDRESS "" CACHE STRING + "Secondary left physical Bluetooth source address (xx:xx:xx:xx:xx:xx)") +if(SWITCH_PICO_SWITCH2_USB_BRIDGE OR SWITCH2_PROBE_COMPOSITE) + set(probe_source_fields SWITCH2_BRIDGE_SOURCE_ADDRESS) + if(SWITCH2_PROBE_COMPOSITE OR SWITCH2_PROBE_HUB) + list(APPEND probe_source_fields SWITCH2_BRIDGE_SECOND_SOURCE_ADDRESS) + endif() + set(probe_source_addresses "") + foreach(field IN LISTS probe_source_fields) + string(TOLOWER "${${field}}" source_address) + string(LENGTH "${source_address}" source_address_length) + if(NOT source_address_length EQUAL 17 + OR NOT source_address MATCHES "^([0-9a-f][0-9a-f]:)+[0-9a-f][0-9a-f]$" + OR source_address STREQUAL "00:00:00:00:00:00" + OR source_address STREQUAL "ff:ff:ff:ff:ff") + message(FATAL_ERROR "Provide ${field} as a physical six-byte Bluetooth address") + endif() + if(source_address IN_LIST probe_source_addresses) + message(FATAL_ERROR "Composite physical source addresses must be distinct") + endif() + list(APPEND probe_source_addresses "${source_address}") + string(REPLACE ":" ",0x" ${field}_BYTES "${source_address}") + string(PREPEND ${field}_BYTES "0x") + endforeach() +endif() + function(switch2_usb_probe_configure target) set(probe_sources ${SWITCH2_USB_PROBE_DIR}/main.c @@ -10,6 +82,9 @@ function(switch2_usb_probe_configure target) ${SWITCH2_USB_PROBE_DIR}/storage.cpp ${SWITCH2_USB_PROBE_DIR}/button_test.c) target_compile_features(${target} PRIVATE c_std_11 cxx_std_17) + if(SWITCH2_PROBE_JOIN_CHORD_GATE) + target_compile_definitions(${target} PRIVATE SWITCH2_PROBE_JOIN_CHORD_GATE=1) + endif() target_include_directories(${target} PRIVATE ${SWITCH2_USB_PROBE_DIR} ${SWITCH2_USB_PROBE_DIR}/../../src/firmware @@ -81,64 +156,13 @@ function(switch2_usb_probe_configure target) target_compile_definitions(${target} PRIVATE SWITCH2_PROBE_ZERO_NATIVE_IMU_PAYLOAD=1) endif() - set(SWITCH2_PROBE_IDENTITY_FILE "" CACHE FILEPATH "64-byte Joy-Con 2 (R) factory-format identity block") - if(SWITCH2_PROBE_IDENTITY_FILE) - file(READ "${SWITCH2_PROBE_IDENTITY_FILE}" identity_hex LIMIT 65 HEX) - string(LENGTH "${identity_hex}" identity_length) - if(NOT identity_length EQUAL 128) - message(FATAL_ERROR "Identity capture must contain exactly 64 bytes") - endif() - string(TOLOWER "${identity_hex}" identity_hex) - string(SUBSTRING "${identity_hex}" 36 8 identity_vid_pid) - if(NOT identity_vid_pid STREQUAL "7e056620") - message(FATAL_ERROR "Identity capture must match Joy-Con 2 (R), 057e:2066") - endif() - string(REGEX REPLACE "([0-9a-f][0-9a-f])" "0x\\1," identity_bytes "${identity_hex}") - file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/probe_identity.h" - "// Generated from a private, read-only controller capture; do not commit.\n#include \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 \nstatic const uint8_t probe_version_reply[16] = {${status_bytes}};\nstatic const uint8_t probe_firmware_version[12] = {${firmware_bytes}};\n") - set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${SWITCH2_PROBE_VERSION_FILE}") - target_compile_definitions(${target} PRIVATE SWITCH2_PROBE_VERSION_REPLY=1) - endif() + foreach(prefix IN ITEMS SWITCH2_PROBE SWITCH2_PROBE_SECOND) + set(${prefix}_IDENTITY_FILE "" CACHE FILEPATH "64-byte matching Joy-Con 2 factory-format identity block") + set(${prefix}_VERSION_FILE "" CACHE FILEPATH "Captured 12-byte matching Joy-Con 2 firmware-version reply") + set(${prefix}_CONTROLLER_ADDRESS "" CACHE STRING "Advertised controller address (captured or distinct virtual identity)") + set(${prefix}_FACTORY_FILE "" CACHE FILEPATH "8192-byte captured factory region with configured virtual identity") + set(${prefix}_USER_CALIBRATION_FILE "" CACHE FILEPATH "4096-byte captured user calibration region") + endforeach() option(SWITCH2_PROBE_ACK_SETUP04 "Acknowledge the observed vendor-04 setup transaction" OFF) if(SWITCH2_PROBE_ACK_SETUP04) if(NOT SWITCH2_PROBE_IDENTITY_FILE OR NOT SWITCH2_PROBE_VERSION_FILE) @@ -153,29 +177,130 @@ function(switch2_usb_probe_configure target) endif() target_compile_definitions(${target} PRIVATE SWITCH2_PROBE_USB_INIT=1) endif() - set(SWITCH2_PROBE_FACTORY_FILE "" CACHE FILEPATH "8192-byte captured factory region with configured virtual identity") - set(SWITCH2_PROBE_USER_CALIBRATION_FILE "" CACHE FILEPATH "4096-byte captured user calibration region") - if(SWITCH2_PROBE_FACTORY_FILE OR SWITCH2_PROBE_USER_CALIBRATION_FILE) - if(NOT SWITCH2_PROBE_USB_INIT OR NOT SWITCH2_PROBE_FACTORY_FILE OR NOT SWITCH2_PROBE_USER_CALIBRATION_FILE) - message(FATAL_ERROR "Memory replies require initialized USB and both calibration captures") + + set(probe_capture_prefixes SWITCH2_PROBE) + if(SWITCH2_PROBE_COMPOSITE OR SWITCH2_PROBE_HUB) + list(APPEND probe_capture_prefixes SWITCH2_PROBE_SECOND) + endif() + set(identity_rows "") + set(status_rows "") + set(firmware_rows "") + set(factory_rows "") + set(user_calibration_rows "") + set(controller_addresses "") + foreach(prefix IN LISTS probe_capture_prefixes) + if(probe_joycon_left OR prefix STREQUAL "SWITCH2_PROBE_SECOND") + set(probe_model "Joy-Con 2 (L)") + set(probe_vid_pid "7e056720") + set(probe_firmware_type "00") + else() + set(probe_model "Joy-Con 2 (R)") + set(probe_vid_pid "7e056620") + set(probe_firmware_type "01") endif() - file(READ "${SWITCH2_PROBE_FACTORY_FILE}" factory_hex LIMIT 8193 HEX) - file(READ "${SWITCH2_PROBE_USER_CALIBRATION_FILE}" user_calibration_hex LIMIT 4097 HEX) - string(LENGTH "${factory_hex}" factory_length) - string(LENGTH "${user_calibration_hex}" user_calibration_length) - if(NOT factory_length EQUAL 16384 OR NOT user_calibration_length EQUAL 8192) - message(FATAL_ERROR "Factory/user captures must contain exactly 8192/4096 bytes") + if(SWITCH2_PROBE_COMPOSITE OR SWITCH2_PROBE_HUB) + foreach(field IDENTITY_FILE VERSION_FILE FACTORY_FILE USER_CALIBRATION_FILE CONTROLLER_ADDRESS) + if(NOT ${prefix}_${field}) + message(FATAL_ERROR "Composite ${probe_model} requires ${prefix}_${field}") + endif() + endforeach() endif() - string(SUBSTRING "${factory_hex}" 0 128 factory_identity_hex) - if(NOT factory_identity_hex STREQUAL identity_hex) - message(FATAL_ERROR "Factory memory identity must match the vendor-control identity") + if(${prefix}_IDENTITY_FILE) + file(READ "${${prefix}_IDENTITY_FILE}" identity_hex LIMIT 65 HEX) + string(LENGTH "${identity_hex}" identity_length) + if(NOT identity_length EQUAL 128) + message(FATAL_ERROR "${prefix}_IDENTITY_FILE must contain exactly 64 bytes") + endif() + string(TOLOWER "${identity_hex}" identity_hex) + string(SUBSTRING "${identity_hex}" 36 8 identity_vid_pid) + if(NOT identity_vid_pid STREQUAL probe_vid_pid) + message(FATAL_ERROR "${prefix}_IDENTITY_FILE must match selected model ${probe_model}") + endif() + string(REGEX REPLACE "([0-9a-f][0-9a-f])" "0x\\1," identity_bytes "${identity_hex}") + string(APPEND identity_rows " {${identity_bytes}},\n") + set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${${prefix}_IDENTITY_FILE}") endif() - string(REGEX REPLACE "([0-9a-f][0-9a-f])" "0x\\1," factory_bytes "${factory_hex}") - string(REGEX REPLACE "([0-9a-f][0-9a-f])" "0x\\1," user_calibration_bytes "${user_calibration_hex}") + if(${prefix}_VERSION_FILE) + if(NOT ${prefix}_IDENTITY_FILE) + message(FATAL_ERROR "${prefix}_VERSION_FILE requires the matching identity capture") + endif() + file(READ "${${prefix}_VERSION_FILE}" version_hex LIMIT 13 HEX) + string(LENGTH "${version_hex}" version_length) + if(NOT version_length EQUAL 24) + message(FATAL_ERROR "${prefix}_VERSION_FILE must contain exactly 12 bytes") + endif() + string(TOLOWER "${version_hex}" version_hex) + string(SUBSTRING "${version_hex}" 6 2 firmware_type) + if(NOT firmware_type STREQUAL probe_firmware_type) + message(FATAL_ERROR "${prefix}_VERSION_FILE must describe selected model ${probe_model}") + endif() + string(REPLACE ":" "" address_hex "${${prefix}_CONTROLLER_ADDRESS}") + string(TOLOWER "${address_hex}" address_hex) + string(LENGTH "${address_hex}" address_length) + if(NOT address_length EQUAL 12 OR NOT address_hex MATCHES "^[0-9a-f]+$" + OR address_hex STREQUAL "000000000000" OR address_hex STREQUAL "ffffffffffff") + message(FATAL_ERROR "Provide ${prefix}_CONTROLLER_ADDRESS as a six-byte advertised Bluetooth address") + endif() + if(address_hex IN_LIST controller_addresses) + message(FATAL_ERROR "Composite advertised controller addresses must be distinct") + endif() + list(APPEND controller_addresses "${address_hex}") + set(address_reversed "") + foreach(byte RANGE 0 5) + math(EXPR position "10 - 2 * ${byte}") + string(SUBSTRING "${address_hex}" ${position} 2 octet) + string(APPEND address_reversed "${octet}") + endforeach() + string(SUBSTRING "${version_hex}" 0 6 main_version) + string(SUBSTRING "${version_hex}" 8 6 bluetooth_version) + # Layout corroborated against two genuine USB vendor-02 responses and their + # matching command-10 version and command-15 address responses. + set(status_hex "${main_version}000000${bluetooth_version}00${address_reversed}") + string(REGEX REPLACE "([0-9a-f][0-9a-f])" "0x\\1," status_bytes "${status_hex}") + string(REGEX REPLACE "([0-9a-f][0-9a-f])" "0x\\1," firmware_bytes "${version_hex}") + string(APPEND status_rows " {${status_bytes}},\n") + string(APPEND firmware_rows " {${firmware_bytes}},\n") + set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${${prefix}_VERSION_FILE}") + endif() + if(${prefix}_FACTORY_FILE OR ${prefix}_USER_CALIBRATION_FILE) + if(NOT SWITCH2_PROBE_USB_INIT OR NOT ${prefix}_FACTORY_FILE OR NOT ${prefix}_USER_CALIBRATION_FILE) + message(FATAL_ERROR "${prefix} memory replies require initialized USB and both calibration captures") + endif() + file(READ "${${prefix}_FACTORY_FILE}" factory_hex LIMIT 8193 HEX) + file(READ "${${prefix}_USER_CALIBRATION_FILE}" user_calibration_hex LIMIT 4097 HEX) + string(LENGTH "${factory_hex}" factory_length) + string(LENGTH "${user_calibration_hex}" user_calibration_length) + if(NOT factory_length EQUAL 16384 OR NOT user_calibration_length EQUAL 8192) + message(FATAL_ERROR "${prefix} factory/user captures must contain exactly 8192/4096 bytes") + endif() + string(TOLOWER "${factory_hex}" factory_hex) + string(TOLOWER "${user_calibration_hex}" user_calibration_hex) + string(SUBSTRING "${factory_hex}" 0 128 factory_identity_hex) + if(NOT factory_identity_hex STREQUAL identity_hex) + message(FATAL_ERROR "${prefix} factory memory identity must match the vendor-control identity") + endif() + string(REGEX REPLACE "([0-9a-f][0-9a-f])" "0x\\1," factory_bytes "${factory_hex}") + string(REGEX REPLACE "([0-9a-f][0-9a-f])" "0x\\1," user_calibration_bytes "${user_calibration_hex}") + string(APPEND factory_rows " {${factory_bytes}},\n") + string(APPEND user_calibration_rows " {${user_calibration_bytes}},\n") + set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS + "${${prefix}_FACTORY_FILE}" "${${prefix}_USER_CALIBRATION_FILE}") + endif() + endforeach() + set(capture_header "// Generated from private, read-only controller captures; do not commit.\n#include \n#include \"model.h\"\n") + if(SWITCH2_PROBE_IDENTITY_FILE) + file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/probe_identity.h" + "${capture_header}static const uint8_t probe_identity_replies[PROBE_CONTROLLER_COUNT][64] = {\n${identity_rows}};\n") + target_compile_definitions(${target} PRIVATE SWITCH2_PROBE_IDENTITY_REPLY=1) + endif() + if(SWITCH2_PROBE_VERSION_FILE) + file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/probe_version.h" + "${capture_header}static const uint8_t probe_version_replies[PROBE_CONTROLLER_COUNT][16] = {\n${status_rows}};\nstatic const uint8_t probe_firmware_versions[PROBE_CONTROLLER_COUNT][12] = {\n${firmware_rows}};\n") + target_compile_definitions(${target} PRIVATE SWITCH2_PROBE_VERSION_REPLY=1) + endif() + if(SWITCH2_PROBE_FACTORY_FILE) file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/probe_memory_data.h" - "// Generated from private calibration captures; do not commit.\n#include \nstatic const uint8_t probe_factory_memory[8192] = {${factory_bytes}};\nstatic const uint8_t probe_user_calibration[4096] = {${user_calibration_bytes}};\n") - set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS - "${SWITCH2_PROBE_FACTORY_FILE}" "${SWITCH2_PROBE_USER_CALIBRATION_FILE}") + "${capture_header}static const uint8_t probe_factory_memories[PROBE_CONTROLLER_COUNT][8192] = {\n${factory_rows}};\nstatic const uint8_t probe_user_calibrations[PROBE_CONTROLLER_COUNT][4096] = {\n${user_calibration_rows}};\n") list(APPEND probe_sources ${SWITCH2_USB_PROBE_DIR}/memory.c) target_compile_definitions(${target} PRIVATE SWITCH2_PROBE_MEMORY=1) endif() @@ -189,17 +314,69 @@ function(switch2_usb_probe_configure target) target_compile_options(${target} PRIVATE ${probe_compile_options}) endif() target_link_libraries(${target} PRIVATE pico_stdlib hardware_uart hardware_sync - hardware_flash pico_flash pico_mbedtls_crypto pico_mbedtls_headers tinyusb_device) + hardware_flash pico_flash pico_mbedtls_crypto pico_mbedtls_headers) + if(SWITCH2_PROBE_HUB) + target_sources(${target} PRIVATE + ${SWITCH2_USB_PROBE_DIR}/../pico_usb_address_probe/router.c + ${SWITCH2_USB_PROBE_DIR}/../../src/firmware/usb/native_hub/native_hub.c) + target_include_directories(${target} PRIVATE + ${SWITCH2_USB_PROBE_DIR}/../pico_usb_address_probe + ${PICO_SDK_PATH}/lib/tinyusb/src) + target_compile_definitions(${target} PRIVATE CFG_TUSB_MCU=OPT_MCU_RP2040) + target_link_libraries(${target} PRIVATE pico_multicore pico_unique_id hardware_irq hardware_resets) + set_source_files_properties( + ${SWITCH2_USB_PROBE_DIR}/../pico_usb_address_probe/router.c + ${SWITCH2_USB_PROBE_DIR}/../../src/firmware/usb/native_hub/native_hub.c + PROPERTIES COMPILE_OPTIONS "-O3;-fno-jump-tables;-Wall;-Wextra;-Werror") + # Core0 now owns the Bluetooth call stack. Reserve16KiB from main + # SRAM instead of overflowing the SDK's4KiB scratch stack region. + set(default_linker "${PICO_SDK_PATH}/src/rp2_common/pico_crt0/rp2350/memmap_default.ld") + file(READ "${default_linker}" hub_linker) + string(REPLACE "RAM(rwx) : ORIGIN = 0x20000000, LENGTH = 512k" + "RAM(rwx) : ORIGIN = 0x20000000, LENGTH = 496k\n MAIN_STACK(rwx) : ORIGIN = 0x2007c000, LENGTH = 16k" + hub_linker "${hub_linker}") + string(REPLACE "KEEP(*(.stack*))\n } > SCRATCH_Y" + "KEEP(*(.stack*))\n } > MAIN_STACK" hub_linker "${hub_linker}") + string(REPLACE "__StackTop = ORIGIN(SCRATCH_Y) + LENGTH(SCRATCH_Y);" + "__StackTop = ORIGIN(MAIN_STACK) + LENGTH(MAIN_STACK);" hub_linker "${hub_linker}") + if(NOT hub_linker MATCHES "MAIN_STACK") + message(FATAL_ERROR "SDK linker stack layout changed") + endif() + file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/native_hub_stack.ld" "${hub_linker}") + pico_set_linker_script(${target} "${CMAKE_CURRENT_BINARY_DIR}/native_hub_stack.ld") + else() + target_link_libraries(${target} PRIVATE tinyusb_device) + endif() pico_enable_stdio_usb(${target} 0) pico_enable_stdio_uart(${target} 1) - if(SWITCH2_BRIDGE_WII_INPUT) + if(SWITCH2_PROBE_HUB) + pico_set_program_name(${target} "Native Joy-Con 2 R and L stock USB hub bridge") + elseif(SWITCH2_PROBE_COMPOSITE) + pico_set_program_name(${target} "Switch 2 right and left Joy-Con composite bridge") + elseif(SWITCH2_BRIDGE_WII_INPUT) pico_set_program_name(${target} "Switch 2 Wii IR and native motion bridge") + elseif(SWITCH_PICO_SWITCH2_USB_BRIDGE AND probe_joycon_left) + pico_set_program_name(${target} "Switch 2 left Joy-Con Bluetooth bridge") elseif(SWITCH_PICO_SWITCH2_USB_BRIDGE) pico_set_program_name(${target} "Switch 2 right Joy-Con Bluetooth bridge") else() pico_set_program_name(${target} "Switch 2 USB initialization capture") endif() - if(SWITCH2_PROBE_OMIT_NATIVE_IMU) + if(SWITCH2_PROBE_HUB) + pico_set_program_version(${target} "0.66-native-hub-input") + elseif(SWITCH2_PROBE_JOIN_CHORD_GATE) + if(SWITCH2_PROBE_TRACE_NATIVE_INPUT) + pico_set_program_version(${target} "0.37-pair-chord-trace") + else() + pico_set_program_version(${target} "0.37-pair-chord") + endif() + elseif(SWITCH2_PROBE_COMPOSITE) + if(SWITCH2_PROBE_TRACE_NATIVE_INPUT) + pico_set_program_version(${target} "0.35-pair-trace") + else() + pico_set_program_version(${target} "0.35-pair") + endif() + elseif(SWITCH2_PROBE_OMIT_NATIVE_IMU) pico_set_program_version(${target} "0.24-no-imu") elseif(SWITCH2_PROBE_ZERO_NATIVE_IMU_PAYLOAD) pico_set_program_version(${target} "0.24-zero-imu-payload") @@ -209,6 +386,12 @@ function(switch2_usb_probe_configure target) else() pico_set_program_version(${target} "0.33-wii") endif() + elseif(SWITCH_PICO_SWITCH2_USB_BRIDGE AND probe_joycon_left) + if(SWITCH2_PROBE_TRACE_NATIVE_INPUT) + pico_set_program_version(${target} "0.34-left-trace") + else() + pico_set_program_version(${target} "0.34-left") + endif() elseif(SWITCH_PICO_SWITCH2_USB_BRIDGE) if(SWITCH2_PROBE_TRACE_NATIVE_INPUT) pico_set_program_version(${target} "0.25-trace") diff --git a/tools/switch2_usb_probe/protocol.c b/tools/switch2_usb_probe/protocol.c index 211804d..59864b3 100644 --- a/tools/switch2_usb_probe/protocol.c +++ b/tools/switch2_usb_probe/protocol.c @@ -3,7 +3,7 @@ #include // Wire contracts: ndeadly/switch2_controller_research commands.md (03/0D, -// 03/0A, 07/01, 09/01-08, 16/01, 15/01-04) and hid_reports.md (05/08). USB reply headers +// 03/0A, 07/01, 09/01-08, 16/01, 15/01-04) and hid_reports.md (05/07/08). USB reply headers // and status payloads match captures/usb/rumble-procon-gccon.pcapng.gz. // This public component is not a pairing key. The host supplies the other half. static const uint8_t device_key_component[16] = { @@ -71,7 +71,7 @@ static bool finalize_pairing(probe_protocol_state* state, const uint8_t* key) { memcpy(blob + 7, state->pending_host_addresses, 6u * state->pending_host_count); memcpy(blob + sizeof(blob) - 16u, key, 16); // Preserve both the old committed key and pending retry on any save failure. - if (!state->save_pairing(blob, sizeof(blob))) return false; + if (!state->save_pairing(state->context, blob, sizeof(blob))) return false; state->committed_host_count = blob[6]; memcpy(state->committed_host_addresses, blob + 7, sizeof(state->committed_host_addresses)); memcpy(state->committed_key, blob + sizeof(blob) - 16u, sizeof(state->committed_key)); @@ -81,11 +81,12 @@ static bool finalize_pairing(probe_protocol_state* state, const uint8_t* key) { return true; } -void probe_protocol_reset(probe_protocol_state* state) { +void probe_protocol_reset(probe_protocol_state* state, bool is_left) { memset(state, 0, sizeof(*state)); - state->report_id = 0x08; - state->right_stick_center[1] = 0x08; - state->right_stick_center[2] = 0x80; + state->is_left = is_left; + state->report_id = is_left ? 0x07 : 0x08; + state->stick_center[1] = 0x08; + state->stick_center[2] = 0x80; } bool probe_protocol_restore_pairing(probe_protocol_state* state, @@ -226,14 +227,14 @@ size_t probe_protocol_command(probe_protocol_state* state, const uint8_t* comman if (capacity < reply_length) return 0; if (vibration_sample) { uint64_t token = 0; - if (!state->play_sample(command[8], &token) || !token) return 0; + if (!state->play_sample(state->context, command[8], &token) || !token) return 0; *deferred_token = token; } uint8_t encrypted_challenge[16]; if (confirm_key && !challenge_response(pairing_key, command + 9, encrypted_challenge)) return 0; if (finalize && !finalize_pairing(state, pairing_key)) return 0; if (memory_read && - !state->read_memory(memory_address, reply + 16, memory_length)) return 0; + !state->read_memory(state->context, memory_address, reply + 16, memory_length)) return 0; const uint8_t header[] = {command[0], 0x01, 0, command[3], 0, 0xf8, 0, 0}; memcpy(reply, header, sizeof(header)); if (info11_03) { @@ -255,7 +256,8 @@ size_t probe_protocol_command(probe_protocol_state* state, const uint8_t* comman reply[8] = 1; } else if (select_report) { // The real controller acknowledges but ignores unsupported report IDs. - if (command[8] == 0x05 || command[8] == 0x08) state->report_id = command[8]; + if (command[8] == 0x05 || command[8] == (state->is_left ? 0x07 : 0x08)) + state->report_id = command[8]; } else if (exchange_addresses) { if (length != 8) { clear_pending_pairing(state); @@ -337,34 +339,48 @@ size_t probe_protocol_command(probe_protocol_state* state, const uint8_t* comman size_t probe_protocol_report(const probe_protocol_state* state, uint8_t report_id, uint8_t* output, size_t capacity) { if (!state || !state->initialized || !output || capacity < PROBE_INPUT_SIZE || - (report_id != 0x05 && report_id != 0x08)) return 0; + (report_id != 0x05 && report_id != (state->is_left ? 0x07 : 0x08))) return 0; memset(output, 0, PROBE_INPUT_SIZE); const bool buttons_enabled = (state->enabled_features & 1) != 0; const uint8_t buttons0 = state->controller_active && buttons_enabled ? state->controller_buttons[0] : 0; - const uint8_t buttons1 = state->controller_active && buttons_enabled ? state->controller_buttons[1] & 0xd1 : 0; + const uint8_t buttons1 = state->controller_active && buttons_enabled ? + state->controller_buttons[1] & (state->is_left ? 0xc1 : 0xd1) : 0; const uint8_t* stick = state->controller_active && (state->enabled_features & 2) ? - state->controller_stick : state->right_stick_center; - if (report_id == 0x08) { + state->controller_stick : state->stick_center; + if (report_id != 0x05) { output[0] = (uint8_t)state->report_counter; output[1] = 0x25; // Virtual full battery, external USB power. output[2] = buttons0; output[3] = buttons1; if (state->test_rail_buttons && (state->enabled_features & 1)) - output[3] |= 0xc0; // Joy-Con R native SL + SR. + output[3] |= 0xc0; // Both models' native SL + SR. output[4] = 0x07; memcpy(output + 5, stick, 3); // Diagnostic snapshot only; complete live native packets bypass this generator. } else { for (unsigned i = 0; i < 4; ++i) output[i] = (uint8_t)(state->report_counter >> (8 * i)); - output[4] = (uint8_t)(((buttons0 & 0x03) << 2) | ((buttons0 & 0x0c) >> 2) | - ((buttons0 & 0x30) << 2) | ((buttons1 & 0xc0) >> 2)); - output[5] = (uint8_t)(((buttons0 & 0xc0) >> 5) | ((buttons1 & 0x01) << 4) | - ((buttons1 & 0x10) << 2)); - if (state->test_rail_buttons && (state->enabled_features & 1)) - output[4] |= 0x30; // Common report: right SL + SR. - output[11] = 0x08; - output[12] = 0x80; - memcpy(output + 13, stick, 3); + if (state->is_left) { + output[5] = (uint8_t)(((buttons0 & 0x40) >> 6) | ((buttons0 & 0x80) >> 4) | + ((buttons1 & 0x01) << 5)); + output[6] = (uint8_t)((buttons0 & 0x01) | ((buttons0 & 0x06) << 1) | + ((buttons0 & 0x08) >> 2) | ((buttons0 & 0x30) << 2) | + ((buttons1 & 0xc0) >> 2)); + if (state->test_rail_buttons && buttons_enabled) + output[6] |= 0x30; // Common report: left SL + SR. + memcpy(output + 10, stick, 3); + output[14] = 0x08; + output[15] = 0x80; + } else { + output[4] = (uint8_t)(((buttons0 & 0x03) << 2) | ((buttons0 & 0x0c) >> 2) | + ((buttons0 & 0x30) << 2) | ((buttons1 & 0xc0) >> 2)); + output[5] = (uint8_t)(((buttons0 & 0xc0) >> 5) | ((buttons1 & 0x01) << 4) | + ((buttons1 & 0x10) << 2)); + if (state->test_rail_buttons && buttons_enabled) + output[4] |= 0x30; // Common report: right SL + SR. + output[11] = 0x08; + output[12] = 0x80; + memcpy(output + 13, stick, 3); + } output[31] = 0xa0; output[32] = 0x0f; // Virtual battery voltage 4000mV. output[33] = 0x20; @@ -372,3 +388,22 @@ size_t probe_protocol_report(const probe_protocol_state* state, uint8_t report_i } return PROBE_INPUT_SIZE; } + +void probe_protocol_gate_native_report(const probe_protocol_state* state, + uint8_t input[PROBE_INPUT_SIZE]) { + const uint8_t imu_length_offset = state->is_left ? 14u : 15u; + if (!(state->enabled_features & 1)) memset(input + 2, 0, 2); + if (!(state->enabled_features & 2)) + memcpy(input + 5, state->stick_center, sizeof(state->stick_center)); + if (!(state->enabled_features & 0x10)) memset(input + 9, 0, 5); +#ifdef SWITCH2_PROBE_OMIT_NATIVE_IMU + // Deliberate A/B fault injection: leave every other field and feature bit intact. + memset(input + imu_length_offset, 0, 41); +#elif defined(SWITCH2_PROBE_ZERO_NATIVE_IMU_PAYLOAD) + if (!(state->enabled_features & 4)) input[imu_length_offset] = 0; + memset(input + imu_length_offset + 1u, 0, 40); // Preserve enabled genuine length. +#else + if (!(state->enabled_features & 4)) + memset(input + imu_length_offset, 0, 41); +#endif +} diff --git a/tools/switch2_usb_probe/protocol.h b/tools/switch2_usb_probe/protocol.h index a0f5b0f..15ea8d1 100644 --- a/tools/switch2_usb_probe/protocol.h +++ b/tools/switch2_usb_probe/protocol.h @@ -2,6 +2,7 @@ #include #include #include +#include "model.h" #define PROBE_COMMAND_MAX_SIZE 263u #define PROBE_REPLY_MAX_SIZE 96u @@ -10,13 +11,15 @@ #define PROBE_INPUT_SIZE 63u typedef struct { + bool is_left; + void* context; // Caller-owned context shared by this state's callbacks. bool initialized; uint8_t report_id; bool test_rail_buttons; bool runtime03_0c; // Observed USB toggle; full semantics remain unknown. - uint8_t right_stick_center[3]; + uint8_t stick_center[3]; bool controller_active; - uint8_t controller_buttons[2]; // Native right Joy-Con button ordering. + uint8_t controller_buttons[2]; // Selected model's native Joy-Con button ordering. uint8_t controller_stick[3]; // Raw packed 12-bit axes from the selected donor. uint8_t player_leds; // Virtual four-LED mask, exposed through UART diagnostics. bool player_leds_flashing; @@ -39,15 +42,15 @@ typedef struct { uint8_t committed_host_addresses[PROBE_HOST_MAX_ADDRESSES][6]; uint8_t committed_key[16]; // Standard AES byte order. // Synchronous durable save; NULL disables successful finalization. - bool (*save_pairing)(const uint8_t* blob, size_t length); - bool (*read_memory)(uint32_t address, uint8_t* output, size_t length); + bool (*save_pairing)(void* context, const uint8_t* blob, size_t length); + bool (*read_memory)(void* context, uint32_t address, uint8_t* output, size_t length); // Queue a physical sample, returning true only with a nonzero completion token. // Acceptance is not a Bluetooth application ACK. - bool (*play_sample)(uint8_t sample_id, uint64_t* token); + bool (*play_sample)(void* context, uint8_t sample_id, uint64_t* token); uint32_t report_counter; } probe_protocol_state; -void probe_protocol_reset(probe_protocol_state* state); +void probe_protocol_reset(probe_protocol_state* state, bool is_left); // Blob: own address[6], count[1], zero-padded host addresses[42][6], AES key[16]. // Rejects other identities, invalid counts/padding/lengths without mutation. // A successful restore replaces the committed record and clears pending state. @@ -66,3 +69,7 @@ size_t probe_protocol_command(probe_protocol_state* state, const uint8_t* comman // Button/stick snapshot without relative mouse events; safe for GET_REPORT. size_t probe_protocol_report(const probe_protocol_state* state, uint8_t report_id, uint8_t* output, size_t capacity); +// Apply virtual feature gates to one complete native payload in place. +// Enabled mouse/motion and all opaque bytes remain unchanged. +void probe_protocol_gate_native_report(const probe_protocol_state* state, + uint8_t input[PROBE_INPUT_SIZE]); diff --git a/tools/switch2_usb_probe/storage.cpp b/tools/switch2_usb_probe/storage.cpp index 117298a..a8a1b24 100644 --- a/tools/switch2_usb_probe/storage.cpp +++ b/tools/switch2_usb_probe/storage.cpp @@ -1,4 +1,5 @@ #include "storage.h" +#include "model.h" #include @@ -16,13 +17,16 @@ namespace { constexpr size_t kSlotCount = 2; constexpr size_t kMaximumPayloadSize = 512; constexpr size_t kStorageSize = kSlotCount * FLASH_SECTOR_SIZE; +constexpr size_t kReservedStorageSize = 2 * kStorageSize; constexpr size_t kConfigurationStorageSize = CONFIGURATION_STORAGE_COPY_COUNT * FLASH_SECTOR_SIZE; constexpr size_t kConfigurationStorageOffset = PICO_FLASH_BANK_STORAGE_OFFSET - kConfigurationStorageSize; constexpr size_t kProfileStorageOffset = kConfigurationStorageOffset - PROFILE_STORAGE_TOTAL_SIZE; -constexpr uint32_t kStorageOffset = kProfileStorageOffset - kStorageSize; +// Keep the original right bank adjacent to profiles; reserve the left bank below. +constexpr uint32_t kRightStorageOffset = kProfileStorageOffset - kStorageSize; +constexpr uint32_t kLeftStorageOffset = kRightStorageOffset - kStorageSize; constexpr uint32_t kFlashSafeTimeoutMs = 5000; constexpr uint32_t kFormatVersion = 1; @@ -66,9 +70,11 @@ static_assert(PROFILE_STORAGE_TOTAL_SIZE % FLASH_SECTOR_SIZE == 0); static_assert(PICO_FLASH_BANK_STORAGE_OFFSET % FLASH_SECTOR_SIZE == 0); static_assert(PICO_FLASH_BANK_STORAGE_OFFSET >= kConfigurationStorageSize + PROFILE_STORAGE_TOTAL_SIZE + - kStorageSize, + kReservedStorageSize, "pairing storage offset underflows flash"); -static_assert(kStorageOffset + kStorageSize == kProfileStorageOffset); +static_assert(kLeftStorageOffset + kStorageSize == kRightStorageOffset); +static_assert(kRightStorageOffset + kStorageSize == kProfileStorageOffset); +static_assert(kLeftStorageOffset + kReservedStorageSize == kProfileStorageOffset); static_assert(kProfileStorageOffset + PROFILE_STORAGE_TOTAL_SIZE == kConfigurationStorageOffset); static_assert(kConfigurationStorageOffset + kConfigurationStorageSize == @@ -119,22 +125,23 @@ bool is_erased(const uint8_t *bytes, size_t size) { return true; } -bool storage_region_available() { +bool storage_region_available(uint32_t storage_offset) { const uintptr_t binary_end = reinterpret_cast(&__flash_binary_end); return binary_end >= XIP_BASE && - binary_end - XIP_BASE <= kStorageOffset && - kStorageOffset % FLASH_SECTOR_SIZE == 0 && - kStorageOffset <= PICO_FLASH_SIZE_BYTES && - kStorageSize <= PICO_FLASH_SIZE_BYTES - kStorageOffset && - kStorageOffset + kStorageSize == kProfileStorageOffset; + binary_end - XIP_BASE <= kLeftStorageOffset && + storage_offset % FLASH_SECTOR_SIZE == 0 && + storage_offset <= PICO_FLASH_SIZE_BYTES && + kStorageSize <= PICO_FLASH_SIZE_BYTES - storage_offset && + storage_offset >= kLeftStorageOffset && + storage_offset + kStorageSize <= kProfileStorageOffset; } -uint32_t slot_offset(size_t slot) { - return static_cast(kStorageOffset + slot * FLASH_SECTOR_SIZE); +uint32_t slot_offset(uint32_t storage_offset, size_t slot) { + return static_cast(storage_offset + slot * FLASH_SECTOR_SIZE); } -const uint8_t *slot_bytes(size_t slot) { - return reinterpret_cast(XIP_BASE + slot_offset(slot)); +const uint8_t *slot_bytes(uint32_t storage_offset, size_t slot) { + return reinterpret_cast(XIP_BASE + slot_offset(storage_offset, slot)); } bool owner_valid(const uint8_t *bytes, uint32_t offset) { @@ -180,10 +187,10 @@ bool commit_valid(const uint8_t *bytes, uint32_t offset) { FLASH_PAGE_SIZE - kDescriptorSize); } -Slot inspect_slot(size_t index) { - const uint8_t *bytes = slot_bytes(index); +Slot inspect_slot(uint32_t storage_offset, size_t index) { + const uint8_t *bytes = slot_bytes(storage_offset, index); Slot slot{SlotKind::Unknown, bytes, 0, 0}; - if (!owner_valid(bytes, slot_offset(index))) { + if (!owner_valid(bytes, slot_offset(storage_offset, index))) { if (is_erased(bytes, FLASH_SECTOR_SIZE)) { slot.kind = SlotKind::Erased; } @@ -198,7 +205,7 @@ Slot inspect_slot(size_t index) { // A complete ownership page plus an erased tail proves ownership of the // bounded body/commit area, even if either subsequent write was interrupted. slot.kind = SlotKind::OwnedIncomplete; - if (body_valid(bytes) && commit_valid(bytes, slot_offset(index))) { + if (body_valid(bytes) && commit_valid(bytes, slot_offset(storage_offset, index))) { slot.kind = SlotKind::Committed; slot.generation = read_u32(bytes + kBodyOffset + 8); slot.size = read_u32(bytes + kBodyOffset + 16); @@ -245,37 +252,37 @@ void perform_flash_mutation(void *context) { // The caller has classified BOTH sectors before permitting any erase. Only // the inactive, explicitly owned sector is passed here; the active one survives. -bool erase_slot(size_t index) { - if (index >= kSlotCount || !storage_region_available()) { +bool erase_slot(uint32_t storage_offset, size_t index) { + if (index >= kSlotCount || !storage_region_available(storage_offset)) { return false; } - FlashMutation mutation{slot_offset(index), nullptr}; + FlashMutation mutation{slot_offset(storage_offset, index), nullptr}; return flash_safe_execute(perform_flash_mutation, &mutation, kFlashSafeTimeoutMs) == PICO_OK && - is_erased(slot_bytes(index), FLASH_SECTOR_SIZE); + is_erased(slot_bytes(storage_offset, index), FLASH_SECTOR_SIZE); } -bool program_page(size_t index, size_t offset, const uint8_t *page) { +bool program_page(uint32_t storage_offset, size_t index, size_t offset, const uint8_t *page) { if (index >= kSlotCount || offset % FLASH_PAGE_SIZE != 0 || offset > kRecordFootprint - FLASH_PAGE_SIZE || - !storage_region_available() || - !is_erased(slot_bytes(index) + offset, FLASH_PAGE_SIZE)) { + !storage_region_available(storage_offset) || + !is_erased(slot_bytes(storage_offset, index) + offset, FLASH_PAGE_SIZE)) { return false; } FlashMutation mutation{ - static_cast(slot_offset(index) + offset), page, + static_cast(slot_offset(storage_offset, index) + offset), page, }; return flash_safe_execute(perform_flash_mutation, &mutation, kFlashSafeTimeoutMs) == PICO_OK && - memcmp(slot_bytes(index) + offset, page, FLASH_PAGE_SIZE) == 0; + memcmp(slot_bytes(storage_offset, index) + offset, page, FLASH_PAGE_SIZE) == 0; } -void prepare_record(size_t target, uint32_t generation, +void prepare_record(uint32_t storage_offset, size_t target, uint32_t generation, const uint8_t *data, size_t size) { memset(staging, 0xff, sizeof(staging)); memcpy(staging, kOwnerMagic, sizeof(kOwnerMagic)); write_u32(staging + 16, kFormatVersion); - write_u32(staging + 20, slot_offset(target)); + write_u32(staging + 20, slot_offset(storage_offset, target)); write_u32(staging + 24, kMaximumPayloadSize); write_u32(staging + 28, FLASH_PAGE_SIZE); write_u32(staging + 32, FLASH_SECTOR_SIZE); @@ -300,19 +307,23 @@ void prepare_record(size_t target, uint32_t generation, write_u32(commit + 20, header_crc); write_u32(commit + 24, payload_crc); write_u32(commit + 28, static_cast(size)); - write_u32(commit + 32, slot_offset(target)); + write_u32(commit + 32, slot_offset(storage_offset, target)); write_u32(commit + kDescriptorCrcOffset, configuration_crc32(commit, kDescriptorCrcOffset)); } } // namespace -bool probe_storage_load(uint8_t *output, size_t size) { +bool probe_storage_load(uint8_t instance, uint8_t *output, size_t size) { + if (instance >= PROBE_CONTROLLER_COUNT) return false; + const uint32_t storage_offset = probe_storage_offset(instance); if (output == nullptr || size == 0 || size > kMaximumPayloadSize || - !storage_region_available()) { + !storage_region_available(storage_offset)) { return false; } - const Slot slots[kSlotCount] = {inspect_slot(0), inspect_slot(1)}; + const Slot slots[kSlotCount] = { + inspect_slot(storage_offset, 0), inspect_slot(storage_offset, 1), + }; int active; if (!newest_slot(slots, &active) || active < 0 || slots[active].size != size) { return false; @@ -321,12 +332,16 @@ bool probe_storage_load(uint8_t *output, size_t size) { return true; } -bool probe_storage_save(const uint8_t *data, size_t size) { +bool probe_storage_save(uint8_t instance, const uint8_t *data, size_t size) { + if (instance >= PROBE_CONTROLLER_COUNT) return false; + const uint32_t storage_offset = probe_storage_offset(instance); if (data == nullptr || size == 0 || size > kMaximumPayloadSize || - !storage_region_available()) { + !storage_region_available(storage_offset)) { return false; } - const Slot slots[kSlotCount] = {inspect_slot(0), inspect_slot(1)}; + const Slot slots[kSlotCount] = { + inspect_slot(storage_offset, 0), inspect_slot(storage_offset, 1), + }; if (slots[0].kind == SlotKind::Unknown || slots[1].kind == SlotKind::Unknown) { return false; // Never erase through an unrecognized region. } @@ -342,32 +357,33 @@ bool probe_storage_save(const uint8_t *data, size_t size) { ? static_cast(active) ^ 1u : (slots[0].kind == SlotKind::Erased ? 0u : 1u); const uint32_t generation = active >= 0 ? slots[active].generation + 1u : 1u; - prepare_record(target, generation, data, size); + prepare_record(storage_offset, target, generation, data, size); - if (slots[target].kind != SlotKind::Erased && !erase_slot(target)) { + if (slots[target].kind != SlotKind::Erased && !erase_slot(storage_offset, target)) { return false; } - if (!program_page(target, 0, staging)) { + if (!program_page(storage_offset, target, 0, staging)) { return false; } for (size_t offset = kBodyOffset; offset < kCommitOffset; offset += FLASH_PAGE_SIZE) { if (!is_erased(staging + offset, FLASH_PAGE_SIZE) && - !program_page(target, offset, staging + offset)) { + !program_page(storage_offset, target, offset, staging + offset)) { return false; } } - if (!body_valid(slot_bytes(target)) || - !program_page(target, kCommitOffset, staging + kCommitOffset)) { + if (!body_valid(slot_bytes(storage_offset, target)) || + !program_page(storage_offset, target, kCommitOffset, staging + kCommitOffset)) { return false; } - const Slot committed = inspect_slot(target); + const Slot committed = inspect_slot(storage_offset, target); return committed.kind == SlotKind::Committed && committed.generation == generation && committed.size == size && memcmp(committed.bytes + kPayloadOffset, staging + kPayloadOffset, size) == 0; } -uint32_t probe_storage_offset(void) { - return kStorageOffset; +uint32_t probe_storage_offset(uint8_t instance) { + if (instance >= PROBE_CONTROLLER_COUNT) return UINT32_MAX; + return probe_model_is_left(instance) ? kLeftStorageOffset : kRightStorageOffset; } diff --git a/tools/switch2_usb_probe/storage.h b/tools/switch2_usb_probe/storage.h index f33422b..c3e335a 100644 --- a/tools/switch2_usb_probe/storage.h +++ b/tools/switch2_usb_probe/storage.h @@ -11,14 +11,18 @@ extern "C" { // Synchronous, main-loop-only API for the single-core probe. Serialize calls. // Blobs are opaque, nonempty, and at most 512 bytes. Load requires an exact // length match and leaves output unchanged on failure; it never writes flash. -bool probe_storage_load(uint8_t *output, size_t size); +// Invalid instances fail before reading a bank or writing output. +bool probe_storage_load(uint8_t instance, uint8_t *output, size_t size); // Success means an identical blob was already committed, or a replacement was // committed and read back. Failure never authorizes a protocol acknowledgement. -bool probe_storage_save(const uint8_t *data, size_t size); +bool probe_storage_save(uint8_t instance, const uint8_t *data, size_t size); -// Flash-relative offset of the two sectors immediately below profile storage. -uint32_t probe_storage_offset(void); +// Flash-relative offset of the instance's two-sector pairing bank, or UINT32_MAX +// for an invalid instance. The right bank remains immediately below profile +// storage; the left bank occupies the preceding two sectors. Both are reserved +// in every build, and load/save inspect and mutate only the selected bank. +uint32_t probe_storage_offset(uint8_t instance); #ifdef __cplusplus } diff --git a/tools/switch2_usb_probe/transport.h b/tools/switch2_usb_probe/transport.h new file mode 100644 index 0000000..3487ed3 --- /dev/null +++ b/tools/switch2_usb_probe/transport.h @@ -0,0 +1,101 @@ +#pragma once + +#include "model.h" +#include "tusb.h" +#if SWITCH2_PROBE_HUB +#include "usb/native_hub/native_hub.h" +#endif + +// Application instances are controllers, never native hub device slots. +// Only control transfers retain the transport's rhport/device-slot argument. +static inline bool probe_transport_mounted(uint8_t instance) { +#if SWITCH2_PROBE_HUB + return native_hub_mounted(instance); +#else + (void)instance; + return tud_mounted(); +#endif +} + +static inline bool probe_transport_suspended(uint8_t instance) { +#if SWITCH2_PROBE_HUB + return native_hub_suspended(instance); +#else + (void)instance; + return tud_suspended(); +#endif +} + +static inline bool probe_transport_hid_ready(uint8_t instance) { +#if SWITCH2_PROBE_HUB + return native_hub_hid_ready(instance); +#else + return tud_hid_n_ready(instance); +#endif +} + +static inline bool probe_transport_hid_report(uint8_t instance, uint8_t report_id, + const void* data, uint16_t length) { +#if SWITCH2_PROBE_HUB + return native_hub_hid_report(instance, report_id, data, length); +#else + return tud_hid_n_report(instance, report_id, data, length); +#endif +} + +static inline uint32_t probe_transport_vendor_write_available(uint8_t instance) { +#if SWITCH2_PROBE_HUB + return native_hub_vendor_write_available(instance); +#else + return tud_vendor_n_write_available(instance); +#endif +} + +static inline uint32_t probe_transport_vendor_write(uint8_t instance, + const void* data, uint32_t length) { +#if SWITCH2_PROBE_HUB + return native_hub_vendor_write(instance, data, length); +#else + return tud_vendor_n_write(instance, data, length); +#endif +} + +static inline uint32_t probe_transport_vendor_write_flush(uint8_t instance) { +#if SWITCH2_PROBE_HUB + return native_hub_vendor_write_flush(instance); +#else + return tud_vendor_n_write_flush(instance); +#endif +} + +static inline void probe_transport_vendor_discard_received(uint8_t instance) { +#if SWITCH2_PROBE_HUB + // Native RX supplies the actual packet once, with no second receive FIFO. + (void)instance; +#else + // The application consumes the raw callback packet, not this duplicate. + uint8_t discarded[64]; + while (tud_vendor_n_available(instance)) { + if (!tud_vendor_n_read(instance, discarded, sizeof(discarded))) break; + } +#endif +} + +static inline bool probe_transport_control_xfer(uint8_t rhport, + const tusb_control_request_t* request, + void* buffer, uint16_t length) { +#if SWITCH2_PROBE_HUB + return native_hub_control_xfer(rhport, request, buffer, length); +#else + return tud_control_xfer(rhport, request, buffer, length); +#endif +} + +static inline bool probe_transport_control_status(uint8_t rhport, + const tusb_control_request_t* request) { +#if SWITCH2_PROBE_HUB + return native_hub_control_status(rhport, request); +#else + return tud_control_status(rhport, request); +#endif +} diff --git a/tools/switch2_usb_probe/tusb_config.h b/tools/switch2_usb_probe/tusb_config.h index e087f20..066137c 100644 --- a/tools/switch2_usb_probe/tusb_config.h +++ b/tools/switch2_usb_probe/tusb_config.h @@ -1,16 +1,18 @@ #pragma once +#include "model.h" + #define CFG_TUSB_RHPORT0_MODE (OPT_MODE_DEVICE | OPT_MODE_FULL_SPEED) #ifndef CFG_TUSB_OS #define CFG_TUSB_OS OPT_OS_NONE #endif #define CFG_TUD_ENDPOINT0_SIZE 64 -#define CFG_TUD_HID 1 +#define CFG_TUD_HID PROBE_CONTROLLER_COUNT #define CFG_TUD_HID_EP_BUFSIZE 64 #define CFG_TUD_CDC 0 #define CFG_TUD_MSC 0 #define CFG_TUD_MIDI 0 -#define CFG_TUD_VENDOR 1 +#define CFG_TUD_VENDOR PROBE_CONTROLLER_COUNT #define CFG_TUD_VENDOR_EPSIZE 64 #define CFG_TUD_VENDOR_RX_BUFSIZE 256 #define CFG_TUD_VENDOR_TX_BUFSIZE 256