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

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

View file

@ -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=$<BOOL:${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
$<$<NOT:$<OR:$<BOOL:${SWITCH_PICO_WII_IR_MOUSE}>,$<BOOL:${SWITCH_PICO_WII_IR_GYRO}>>>:SWITCH_PICO_USB_OUTPUT_MODES=1>
PICO_FLASH_ASSUME_CORE1_SAFE=0
PICO_STACK_SIZE=4096
PICO_FLASH_ASSUME_CORE1_SAFE=$<BOOL:${SWITCH2_PROBE_HUB}>
PICO_STACK_SIZE=$<IF:$<BOOL:${SWITCH2_PROBE_HUB}>,16384,4096>
PICO_CORE1_STACK_SIZE=4096
PICO_BTSTACK_CYW43_MAX_HCI_PROCESS_LOOP_COUNT=$<IF:$<BOOL:${SWITCH_PICO_HAPTICS_EXPERIMENT}>,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

188
README.md
View file

@ -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_<address>`. 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.

View file

@ -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) {

View file

@ -23,10 +23,15 @@
#include <string.h>
#include <btstack_run_loop.h>
#if SWITCH2_PROBE_HUB
#include <pico/async_context.h>
#endif
#include <pico/critical_section.h>
#include <pico/cyw43_arch.h>
#include <pico/flash.h>
#if !SWITCH2_PROBE_HUB
#include <pico/multicore.h>
#endif
#include <pico/stdlib.h>
#include <uni.h>
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() {

View file

@ -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);

View file

@ -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<int32_t>(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<int32_t>(now_ms - source.latest_input.received_ms) <
static_cast<int32_t>(kInputDeadlineMs);
}
bool sample_current(uint32_t now_ms) {
if (g_sample.token &&
(!source_fresh(now_ms) || g_sample.mouse_epoch != g_latest_input.mouse_epoch ||
static_cast<int32_t>(now_ms - g_sample.started_ms) >=
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<int32_t>(now_ms - source.sample.started_ms) >=
static_cast<int32_t>(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<uint8_t>((g_next + 1) % SWITCH2_MOUSE_CAPTURE_CAPACITY);
if (g_input_selected && product_id == UNI_SW2_JOYCON_R_PID &&
memcmp(address, g_input_address, sizeof(g_input_address)) == 0 &&
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<int32_t>(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<int32_t>(now_ms - source.native_reports[source.native_head].received_ms) >=
static_cast<int32_t>(kInputDeadlineMs))) {
clear_native_reports();
} else if (g_native_count != 0) {
const NativeReport& packet = g_native_reports[g_native_head];
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<uint8_t>((g_native_head + 1) % kNativeReportCapacity);
--g_native_count;
source.native_head = static_cast<uint8_t>((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;

View file

@ -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

View file

@ -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 =

View file

@ -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 <inttypes.h>
#include <stddef.h>
#include <stdio.h>
#include <string.h>
#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);
}

View file

@ -0,0 +1,44 @@
#pragma once
#include <stdbool.h>
#include <stdint.h>
#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

View file

@ -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<uint8_t>((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<uint8_t>(y);
report[12] = static_cast<uint16_t>(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<uint32_t>(now));
}
static probe_controller_input poll(uint32_t timestamp = static_cast<uint32_t>(now)) {
static probe_controller_input poll(uint32_t timestamp = static_cast<uint32_t>(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<uint32_t>(now)) {
uint32_t timestamp = static_cast<uint32_t>(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<uint32_t>(now), actual.data()) == 0);
assert(probe_controller_input_peek_native_report(instance, static_cast<uint32_t>(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<uint32_t>(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<uint64_t>(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");
}

View file

@ -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;

View file

@ -2,35 +2,286 @@
#include <stdint.h>
#include <string.h>
#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;
}

View file

@ -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;
}

View file

@ -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)

View file

@ -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, &ltoken) && 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, &ltoken));
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)

View file

@ -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"),

1062
tools/native_joycon_hub_check.py Executable file

File diff suppressed because it is too large Load diff

View file

@ -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=$<BOOL:${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)

View file

@ -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("<HH", data, 8)
if (vendor, product) != (VID, expected_pid):
raise RuntimeError(
f"address {device.address} returned wrong identity {vendor:04x}:{product:04x}"
)
bus, address = device_location(device)
return {"bus": bus, "address": address, "vid": int(vendor), "pid": int(product)}
def delta(after: dict[str, int], before: dict[str, int], field: str) -> 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())

View file

@ -0,0 +1,62 @@
#include <inttypes.h>
#include <stdio.h>
#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);
}
}

View file

@ -0,0 +1,738 @@
#include "router.h"
#include <string.h>
#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
}
}

View file

@ -0,0 +1,48 @@
#pragma once
#include <stdbool.h>
#include <stdint.h>
#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);

View file

@ -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

View file

@ -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 <inttypes.h>
#include <stdio.h>
#include <string.h>
#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);
}

View file

@ -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);

View file

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

View file

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

View file

@ -12,7 +12,7 @@ typedef struct {
uint32_t serial;
uint8_t buttons[2];
uint8_t stick[3];
// Latest opaque native 08 byte 8.
// Latest opaque native 07/08 byte 8.
uint8_t native_status;
// Cumulative signed relative totals within mouse_epoch, not per-poll
// deltas. Cached polls repeat these totals without consuming motion.
@ -20,7 +20,7 @@ typedef struct {
uint32_t mouse_epoch;
int64_t mouse_total_x;
int64_t mouse_total_y;
// Latest opaque native 08 byte 13.
// Latest opaque native 07/08 byte 13.
uint8_t mouse_surface;
} probe_controller_input;
@ -28,10 +28,15 @@ typedef struct {
void probe_controller_input_clock_init(void);
// Core 0, after stdio and before protocol reset or USB startup.
void probe_controller_input_init(void);
// True means both cores are registered for flash coordination, not that the
// radio is ready or a controller is connected. Failure is latched: keep USB
// and flash-writing protocol operations disabled rather than retrying startup.
// True means flash coordination is ready, not that the radio is ready or a
// controller is connected. Hub mode initializes on Core 0 with Core 1 reserved
// for SRAM-only USB; other modes register both cores and launch the radio there.
// Failure is latched: keep USB and flash-writing protocol operations disabled.
bool probe_controller_input_start(void);
// Core 0 main loop before USB tasks, outside IRQs and application state locks.
// Hub mode cooperatively services CYW43/BTstack, including storage and haptics;
// a no-op before successful start and in dedicated-radio modes.
void probe_controller_input_task(void);
// Core 0 after start(): polls the existing two-second BOOTSEL hold gesture.
// True means a Bluetooth pairing-window request was queued. Long holds NEVER
// clear pairings in this bridge, and this does not inject USB controller input.
@ -42,29 +47,30 @@ void probe_controller_input_set_stick_calibration(const uint8_t calibration[9]);
// Native feature changes are output barriers, not Bluetooth/IMU resets.
void probe_controller_input_set_native_features(uint8_t features);
#endif
// Core0 native08 output. Disable discards queued/prepared data; repeated enable
// preserves it. Joy-Con mode relays its bounded FIFO; Wii mode synthesizes from
// fresh calibrated sensors and the selected IR pointer. No pairing changes.
void probe_controller_input_set_native_stream(bool enabled);
// Core0 native07/08 output. Disable discards queued/prepared data; repeated
// enable preserves it. Joy-Con mode relays its bounded FIFO; right-only Wii
// mode synthesizes fresh calibrated sensors and the selected IR pointer.
// No pairing changes.
void probe_controller_input_set_native_stream(uint8_t instance, bool enabled);
// Copy one63-byte payload without report ID. Returns a boot-unique token, or0
// without changing output. Nondestructive until successful HID submission and
// commit. now_ms uses the Pico boot-ms clock; unavailable/stale input is rejected.
uint32_t probe_controller_input_peek_native_report(uint32_t now_ms, uint8_t report[63]);
uint32_t probe_controller_input_peek_native_report(uint8_t instance, uint32_t now_ms, uint8_t report[63]);
// Remove only the exact current head once. A stale/replaced token cannot pop a
// new stream's packet. Before flash-ready startup peek/commit return 0/false.
bool probe_controller_input_commit_native_report(uint32_t serial);
bool probe_controller_input_commit_native_report(uint8_t instance, uint32_t serial);
// Built-in vibration samples only; raw HD-rumble output is not forwarded.
// A nonzero token means queued, not completed. Result:0 pending,1 completion,
// -1 failed/stale. Joy-Con completion is its application ACK; Wii completion is
// actual bounded rumble-driver dispatch (not an HD-waveform fidelity claim).
// Reset cancels the request, never stored pairing.
bool probe_controller_input_play_sample(uint8_t sample_id, uint64_t* token);
int probe_controller_input_sample_result(uint64_t token, uint32_t now_ms);
void probe_controller_input_cancel_sample(void);
bool probe_controller_input_play_sample(uint8_t instance, uint8_t sample_id, uint64_t* token);
int probe_controller_input_sample_result(uint8_t instance, uint64_t token, uint32_t now_ms);
void probe_controller_input_cancel_sample(uint8_t instance);
// Core0 at250Hz; now_ms uses Pico boot milliseconds. Supplies current mapped
// controls for diagnostic reports; the native sender owns motion consumption.
// Inactive controls are zero except serial; USB supplies its calibrated center.
void probe_controller_input_poll(uint32_t now_ms, probe_controller_input* out);
void probe_controller_input_poll(uint8_t instance, uint32_t now_ms, probe_controller_input* out);
#ifdef __cplusplus
}

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

View file

@ -3,7 +3,7 @@
#include <string.h>
// Wire contracts: ndeadly/switch2_controller_research commands.md (03/0D,
// 03/0A, 07/01, 09/01-08, 16/01, 15/01-04) and hid_reports.md (05/08). USB reply headers
// 03/0A, 07/01, 09/01-08, 16/01, 15/01-04) and hid_reports.md (05/07/08). USB reply headers
// and status payloads match captures/usb/rumble-procon-gccon.pcapng.gz.
// This public component is not a pairing key. The host supplies the other half.
static const uint8_t device_key_component[16] = {
@ -71,7 +71,7 @@ static bool finalize_pairing(probe_protocol_state* state, const uint8_t* key) {
memcpy(blob + 7, state->pending_host_addresses, 6u * state->pending_host_count);
memcpy(blob + sizeof(blob) - 16u, key, 16);
// Preserve both the old committed key and pending retry on any save failure.
if (!state->save_pairing(blob, sizeof(blob))) return false;
if (!state->save_pairing(state->context, blob, sizeof(blob))) return false;
state->committed_host_count = blob[6];
memcpy(state->committed_host_addresses, blob + 7, sizeof(state->committed_host_addresses));
memcpy(state->committed_key, blob + sizeof(blob) - 16u, sizeof(state->committed_key));
@ -81,11 +81,12 @@ static bool finalize_pairing(probe_protocol_state* state, const uint8_t* key) {
return true;
}
void probe_protocol_reset(probe_protocol_state* state) {
void probe_protocol_reset(probe_protocol_state* state, bool is_left) {
memset(state, 0, sizeof(*state));
state->report_id = 0x08;
state->right_stick_center[1] = 0x08;
state->right_stick_center[2] = 0x80;
state->is_left = is_left;
state->report_id = is_left ? 0x07 : 0x08;
state->stick_center[1] = 0x08;
state->stick_center[2] = 0x80;
}
bool probe_protocol_restore_pairing(probe_protocol_state* state,
@ -226,14 +227,14 @@ size_t probe_protocol_command(probe_protocol_state* state, const uint8_t* comman
if (capacity < reply_length) return 0;
if (vibration_sample) {
uint64_t token = 0;
if (!state->play_sample(command[8], &token) || !token) return 0;
if (!state->play_sample(state->context, command[8], &token) || !token) return 0;
*deferred_token = token;
}
uint8_t encrypted_challenge[16];
if (confirm_key && !challenge_response(pairing_key, command + 9, encrypted_challenge)) return 0;
if (finalize && !finalize_pairing(state, pairing_key)) return 0;
if (memory_read &&
!state->read_memory(memory_address, reply + 16, memory_length)) return 0;
!state->read_memory(state->context, memory_address, reply + 16, memory_length)) return 0;
const uint8_t header[] = {command[0], 0x01, 0, command[3], 0, 0xf8, 0, 0};
memcpy(reply, header, sizeof(header));
if (info11_03) {
@ -255,7 +256,8 @@ size_t probe_protocol_command(probe_protocol_state* state, const uint8_t* comman
reply[8] = 1;
} else if (select_report) {
// The real controller acknowledges but ignores unsupported report IDs.
if (command[8] == 0x05 || command[8] == 0x08) state->report_id = command[8];
if (command[8] == 0x05 || command[8] == (state->is_left ? 0x07 : 0x08))
state->report_id = command[8];
} else if (exchange_addresses) {
if (length != 8) {
clear_pending_pairing(state);
@ -337,34 +339,48 @@ size_t probe_protocol_command(probe_protocol_state* state, const uint8_t* comman
size_t probe_protocol_report(const probe_protocol_state* state, uint8_t report_id,
uint8_t* output, size_t capacity) {
if (!state || !state->initialized || !output || capacity < PROBE_INPUT_SIZE ||
(report_id != 0x05 && report_id != 0x08)) return 0;
(report_id != 0x05 && report_id != (state->is_left ? 0x07 : 0x08))) return 0;
memset(output, 0, PROBE_INPUT_SIZE);
const bool buttons_enabled = (state->enabled_features & 1) != 0;
const uint8_t buttons0 = state->controller_active && buttons_enabled ? state->controller_buttons[0] : 0;
const uint8_t buttons1 = state->controller_active && buttons_enabled ? state->controller_buttons[1] & 0xd1 : 0;
const uint8_t buttons1 = state->controller_active && buttons_enabled ?
state->controller_buttons[1] & (state->is_left ? 0xc1 : 0xd1) : 0;
const uint8_t* stick = state->controller_active && (state->enabled_features & 2) ?
state->controller_stick : state->right_stick_center;
if (report_id == 0x08) {
state->controller_stick : state->stick_center;
if (report_id != 0x05) {
output[0] = (uint8_t)state->report_counter;
output[1] = 0x25; // Virtual full battery, external USB power.
output[2] = buttons0;
output[3] = buttons1;
if (state->test_rail_buttons && (state->enabled_features & 1))
output[3] |= 0xc0; // Joy-Con R native SL + SR.
output[3] |= 0xc0; // Both models' native SL + SR.
output[4] = 0x07;
memcpy(output + 5, stick, 3);
// Diagnostic snapshot only; complete live native packets bypass this generator.
} else {
for (unsigned i = 0; i < 4; ++i) output[i] = (uint8_t)(state->report_counter >> (8 * i));
output[4] = (uint8_t)(((buttons0 & 0x03) << 2) | ((buttons0 & 0x0c) >> 2) |
((buttons0 & 0x30) << 2) | ((buttons1 & 0xc0) >> 2));
output[5] = (uint8_t)(((buttons0 & 0xc0) >> 5) | ((buttons1 & 0x01) << 4) |
((buttons1 & 0x10) << 2));
if (state->test_rail_buttons && (state->enabled_features & 1))
output[4] |= 0x30; // Common report: right SL + SR.
output[11] = 0x08;
output[12] = 0x80;
memcpy(output + 13, stick, 3);
if (state->is_left) {
output[5] = (uint8_t)(((buttons0 & 0x40) >> 6) | ((buttons0 & 0x80) >> 4) |
((buttons1 & 0x01) << 5));
output[6] = (uint8_t)((buttons0 & 0x01) | ((buttons0 & 0x06) << 1) |
((buttons0 & 0x08) >> 2) | ((buttons0 & 0x30) << 2) |
((buttons1 & 0xc0) >> 2));
if (state->test_rail_buttons && buttons_enabled)
output[6] |= 0x30; // Common report: left SL + SR.
memcpy(output + 10, stick, 3);
output[14] = 0x08;
output[15] = 0x80;
} else {
output[4] = (uint8_t)(((buttons0 & 0x03) << 2) | ((buttons0 & 0x0c) >> 2) |
((buttons0 & 0x30) << 2) | ((buttons1 & 0xc0) >> 2));
output[5] = (uint8_t)(((buttons0 & 0xc0) >> 5) | ((buttons1 & 0x01) << 4) |
((buttons1 & 0x10) << 2));
if (state->test_rail_buttons && buttons_enabled)
output[4] |= 0x30; // Common report: right SL + SR.
output[11] = 0x08;
output[12] = 0x80;
memcpy(output + 13, stick, 3);
}
output[31] = 0xa0;
output[32] = 0x0f; // Virtual battery voltage 4000mV.
output[33] = 0x20;
@ -372,3 +388,22 @@ size_t probe_protocol_report(const probe_protocol_state* state, uint8_t report_i
}
return PROBE_INPUT_SIZE;
}
void probe_protocol_gate_native_report(const probe_protocol_state* state,
uint8_t input[PROBE_INPUT_SIZE]) {
const uint8_t imu_length_offset = state->is_left ? 14u : 15u;
if (!(state->enabled_features & 1)) memset(input + 2, 0, 2);
if (!(state->enabled_features & 2))
memcpy(input + 5, state->stick_center, sizeof(state->stick_center));
if (!(state->enabled_features & 0x10)) memset(input + 9, 0, 5);
#ifdef SWITCH2_PROBE_OMIT_NATIVE_IMU
// Deliberate A/B fault injection: leave every other field and feature bit intact.
memset(input + imu_length_offset, 0, 41);
#elif defined(SWITCH2_PROBE_ZERO_NATIVE_IMU_PAYLOAD)
if (!(state->enabled_features & 4)) input[imu_length_offset] = 0;
memset(input + imu_length_offset + 1u, 0, 40); // Preserve enabled genuine length.
#else
if (!(state->enabled_features & 4))
memset(input + imu_length_offset, 0, 41);
#endif
}

View file

@ -2,6 +2,7 @@
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include "model.h"
#define PROBE_COMMAND_MAX_SIZE 263u
#define PROBE_REPLY_MAX_SIZE 96u
@ -10,13 +11,15 @@
#define PROBE_INPUT_SIZE 63u
typedef struct {
bool is_left;
void* context; // Caller-owned context shared by this state's callbacks.
bool initialized;
uint8_t report_id;
bool test_rail_buttons;
bool runtime03_0c; // Observed USB toggle; full semantics remain unknown.
uint8_t right_stick_center[3];
uint8_t stick_center[3];
bool controller_active;
uint8_t controller_buttons[2]; // Native right Joy-Con button ordering.
uint8_t controller_buttons[2]; // Selected model's native Joy-Con button ordering.
uint8_t controller_stick[3]; // Raw packed 12-bit axes from the selected donor.
uint8_t player_leds; // Virtual four-LED mask, exposed through UART diagnostics.
bool player_leds_flashing;
@ -39,15 +42,15 @@ typedef struct {
uint8_t committed_host_addresses[PROBE_HOST_MAX_ADDRESSES][6];
uint8_t committed_key[16]; // Standard AES byte order.
// Synchronous durable save; NULL disables successful finalization.
bool (*save_pairing)(const uint8_t* blob, size_t length);
bool (*read_memory)(uint32_t address, uint8_t* output, size_t length);
bool (*save_pairing)(void* context, const uint8_t* blob, size_t length);
bool (*read_memory)(void* context, uint32_t address, uint8_t* output, size_t length);
// Queue a physical sample, returning true only with a nonzero completion token.
// Acceptance is not a Bluetooth application ACK.
bool (*play_sample)(uint8_t sample_id, uint64_t* token);
bool (*play_sample)(void* context, uint8_t sample_id, uint64_t* token);
uint32_t report_counter;
} probe_protocol_state;
void probe_protocol_reset(probe_protocol_state* state);
void probe_protocol_reset(probe_protocol_state* state, bool is_left);
// Blob: own address[6], count[1], zero-padded host addresses[42][6], AES key[16].
// Rejects other identities, invalid counts/padding/lengths without mutation.
// A successful restore replaces the committed record and clears pending state.
@ -66,3 +69,7 @@ size_t probe_protocol_command(probe_protocol_state* state, const uint8_t* comman
// Button/stick snapshot without relative mouse events; safe for GET_REPORT.
size_t probe_protocol_report(const probe_protocol_state* state, uint8_t report_id,
uint8_t* output, size_t capacity);
// Apply virtual feature gates to one complete native payload in place.
// Enabled mouse/motion and all opaque bytes remain unchanged.
void probe_protocol_gate_native_report(const probe_protocol_state* state,
uint8_t input[PROBE_INPUT_SIZE]);

View file

@ -1,4 +1,5 @@
#include "storage.h"
#include "model.h"
#include <string.h>
@ -16,13 +17,16 @@ namespace {
constexpr size_t kSlotCount = 2;
constexpr size_t kMaximumPayloadSize = 512;
constexpr size_t kStorageSize = kSlotCount * FLASH_SECTOR_SIZE;
constexpr size_t kReservedStorageSize = 2 * kStorageSize;
constexpr size_t kConfigurationStorageSize =
CONFIGURATION_STORAGE_COPY_COUNT * FLASH_SECTOR_SIZE;
constexpr size_t kConfigurationStorageOffset =
PICO_FLASH_BANK_STORAGE_OFFSET - kConfigurationStorageSize;
constexpr size_t kProfileStorageOffset =
kConfigurationStorageOffset - PROFILE_STORAGE_TOTAL_SIZE;
constexpr uint32_t kStorageOffset = kProfileStorageOffset - kStorageSize;
// Keep the original right bank adjacent to profiles; reserve the left bank below.
constexpr uint32_t kRightStorageOffset = kProfileStorageOffset - kStorageSize;
constexpr uint32_t kLeftStorageOffset = kRightStorageOffset - kStorageSize;
constexpr uint32_t kFlashSafeTimeoutMs = 5000;
constexpr uint32_t kFormatVersion = 1;
@ -66,9 +70,11 @@ static_assert(PROFILE_STORAGE_TOTAL_SIZE % FLASH_SECTOR_SIZE == 0);
static_assert(PICO_FLASH_BANK_STORAGE_OFFSET % FLASH_SECTOR_SIZE == 0);
static_assert(PICO_FLASH_BANK_STORAGE_OFFSET >=
kConfigurationStorageSize + PROFILE_STORAGE_TOTAL_SIZE +
kStorageSize,
kReservedStorageSize,
"pairing storage offset underflows flash");
static_assert(kStorageOffset + kStorageSize == kProfileStorageOffset);
static_assert(kLeftStorageOffset + kStorageSize == kRightStorageOffset);
static_assert(kRightStorageOffset + kStorageSize == kProfileStorageOffset);
static_assert(kLeftStorageOffset + kReservedStorageSize == kProfileStorageOffset);
static_assert(kProfileStorageOffset + PROFILE_STORAGE_TOTAL_SIZE ==
kConfigurationStorageOffset);
static_assert(kConfigurationStorageOffset + kConfigurationStorageSize ==
@ -119,22 +125,23 @@ bool is_erased(const uint8_t *bytes, size_t size) {
return true;
}
bool storage_region_available() {
bool storage_region_available(uint32_t storage_offset) {
const uintptr_t binary_end = reinterpret_cast<uintptr_t>(&__flash_binary_end);
return binary_end >= XIP_BASE &&
binary_end - XIP_BASE <= kStorageOffset &&
kStorageOffset % FLASH_SECTOR_SIZE == 0 &&
kStorageOffset <= PICO_FLASH_SIZE_BYTES &&
kStorageSize <= PICO_FLASH_SIZE_BYTES - kStorageOffset &&
kStorageOffset + kStorageSize == kProfileStorageOffset;
binary_end - XIP_BASE <= kLeftStorageOffset &&
storage_offset % FLASH_SECTOR_SIZE == 0 &&
storage_offset <= PICO_FLASH_SIZE_BYTES &&
kStorageSize <= PICO_FLASH_SIZE_BYTES - storage_offset &&
storage_offset >= kLeftStorageOffset &&
storage_offset + kStorageSize <= kProfileStorageOffset;
}
uint32_t slot_offset(size_t slot) {
return static_cast<uint32_t>(kStorageOffset + slot * FLASH_SECTOR_SIZE);
uint32_t slot_offset(uint32_t storage_offset, size_t slot) {
return static_cast<uint32_t>(storage_offset + slot * FLASH_SECTOR_SIZE);
}
const uint8_t *slot_bytes(size_t slot) {
return reinterpret_cast<const uint8_t *>(XIP_BASE + slot_offset(slot));
const uint8_t *slot_bytes(uint32_t storage_offset, size_t slot) {
return reinterpret_cast<const uint8_t *>(XIP_BASE + slot_offset(storage_offset, slot));
}
bool owner_valid(const uint8_t *bytes, uint32_t offset) {
@ -180,10 +187,10 @@ bool commit_valid(const uint8_t *bytes, uint32_t offset) {
FLASH_PAGE_SIZE - kDescriptorSize);
}
Slot inspect_slot(size_t index) {
const uint8_t *bytes = slot_bytes(index);
Slot inspect_slot(uint32_t storage_offset, size_t index) {
const uint8_t *bytes = slot_bytes(storage_offset, index);
Slot slot{SlotKind::Unknown, bytes, 0, 0};
if (!owner_valid(bytes, slot_offset(index))) {
if (!owner_valid(bytes, slot_offset(storage_offset, index))) {
if (is_erased(bytes, FLASH_SECTOR_SIZE)) {
slot.kind = SlotKind::Erased;
}
@ -198,7 +205,7 @@ Slot inspect_slot(size_t index) {
// A complete ownership page plus an erased tail proves ownership of the
// bounded body/commit area, even if either subsequent write was interrupted.
slot.kind = SlotKind::OwnedIncomplete;
if (body_valid(bytes) && commit_valid(bytes, slot_offset(index))) {
if (body_valid(bytes) && commit_valid(bytes, slot_offset(storage_offset, index))) {
slot.kind = SlotKind::Committed;
slot.generation = read_u32(bytes + kBodyOffset + 8);
slot.size = read_u32(bytes + kBodyOffset + 16);
@ -245,37 +252,37 @@ void perform_flash_mutation(void *context) {
// The caller has classified BOTH sectors before permitting any erase. Only
// the inactive, explicitly owned sector is passed here; the active one survives.
bool erase_slot(size_t index) {
if (index >= kSlotCount || !storage_region_available()) {
bool erase_slot(uint32_t storage_offset, size_t index) {
if (index >= kSlotCount || !storage_region_available(storage_offset)) {
return false;
}
FlashMutation mutation{slot_offset(index), nullptr};
FlashMutation mutation{slot_offset(storage_offset, index), nullptr};
return flash_safe_execute(perform_flash_mutation, &mutation,
kFlashSafeTimeoutMs) == PICO_OK &&
is_erased(slot_bytes(index), FLASH_SECTOR_SIZE);
is_erased(slot_bytes(storage_offset, index), FLASH_SECTOR_SIZE);
}
bool program_page(size_t index, size_t offset, const uint8_t *page) {
bool program_page(uint32_t storage_offset, size_t index, size_t offset, const uint8_t *page) {
if (index >= kSlotCount || offset % FLASH_PAGE_SIZE != 0 ||
offset > kRecordFootprint - FLASH_PAGE_SIZE ||
!storage_region_available() ||
!is_erased(slot_bytes(index) + offset, FLASH_PAGE_SIZE)) {
!storage_region_available(storage_offset) ||
!is_erased(slot_bytes(storage_offset, index) + offset, FLASH_PAGE_SIZE)) {
return false;
}
FlashMutation mutation{
static_cast<uint32_t>(slot_offset(index) + offset), page,
static_cast<uint32_t>(slot_offset(storage_offset, index) + offset), page,
};
return flash_safe_execute(perform_flash_mutation, &mutation,
kFlashSafeTimeoutMs) == PICO_OK &&
memcmp(slot_bytes(index) + offset, page, FLASH_PAGE_SIZE) == 0;
memcmp(slot_bytes(storage_offset, index) + offset, page, FLASH_PAGE_SIZE) == 0;
}
void prepare_record(size_t target, uint32_t generation,
void prepare_record(uint32_t storage_offset, size_t target, uint32_t generation,
const uint8_t *data, size_t size) {
memset(staging, 0xff, sizeof(staging));
memcpy(staging, kOwnerMagic, sizeof(kOwnerMagic));
write_u32(staging + 16, kFormatVersion);
write_u32(staging + 20, slot_offset(target));
write_u32(staging + 20, slot_offset(storage_offset, target));
write_u32(staging + 24, kMaximumPayloadSize);
write_u32(staging + 28, FLASH_PAGE_SIZE);
write_u32(staging + 32, FLASH_SECTOR_SIZE);
@ -300,19 +307,23 @@ void prepare_record(size_t target, uint32_t generation,
write_u32(commit + 20, header_crc);
write_u32(commit + 24, payload_crc);
write_u32(commit + 28, static_cast<uint32_t>(size));
write_u32(commit + 32, slot_offset(target));
write_u32(commit + 32, slot_offset(storage_offset, target));
write_u32(commit + kDescriptorCrcOffset,
configuration_crc32(commit, kDescriptorCrcOffset));
}
} // namespace
bool probe_storage_load(uint8_t *output, size_t size) {
bool probe_storage_load(uint8_t instance, uint8_t *output, size_t size) {
if (instance >= PROBE_CONTROLLER_COUNT) return false;
const uint32_t storage_offset = probe_storage_offset(instance);
if (output == nullptr || size == 0 || size > kMaximumPayloadSize ||
!storage_region_available()) {
!storage_region_available(storage_offset)) {
return false;
}
const Slot slots[kSlotCount] = {inspect_slot(0), inspect_slot(1)};
const Slot slots[kSlotCount] = {
inspect_slot(storage_offset, 0), inspect_slot(storage_offset, 1),
};
int active;
if (!newest_slot(slots, &active) || active < 0 || slots[active].size != size) {
return false;
@ -321,12 +332,16 @@ bool probe_storage_load(uint8_t *output, size_t size) {
return true;
}
bool probe_storage_save(const uint8_t *data, size_t size) {
bool probe_storage_save(uint8_t instance, const uint8_t *data, size_t size) {
if (instance >= PROBE_CONTROLLER_COUNT) return false;
const uint32_t storage_offset = probe_storage_offset(instance);
if (data == nullptr || size == 0 || size > kMaximumPayloadSize ||
!storage_region_available()) {
!storage_region_available(storage_offset)) {
return false;
}
const Slot slots[kSlotCount] = {inspect_slot(0), inspect_slot(1)};
const Slot slots[kSlotCount] = {
inspect_slot(storage_offset, 0), inspect_slot(storage_offset, 1),
};
if (slots[0].kind == SlotKind::Unknown || slots[1].kind == SlotKind::Unknown) {
return false; // Never erase through an unrecognized region.
}
@ -342,32 +357,33 @@ bool probe_storage_save(const uint8_t *data, size_t size) {
? static_cast<size_t>(active) ^ 1u
: (slots[0].kind == SlotKind::Erased ? 0u : 1u);
const uint32_t generation = active >= 0 ? slots[active].generation + 1u : 1u;
prepare_record(target, generation, data, size);
prepare_record(storage_offset, target, generation, data, size);
if (slots[target].kind != SlotKind::Erased && !erase_slot(target)) {
if (slots[target].kind != SlotKind::Erased && !erase_slot(storage_offset, target)) {
return false;
}
if (!program_page(target, 0, staging)) {
if (!program_page(storage_offset, target, 0, staging)) {
return false;
}
for (size_t offset = kBodyOffset; offset < kCommitOffset;
offset += FLASH_PAGE_SIZE) {
if (!is_erased(staging + offset, FLASH_PAGE_SIZE) &&
!program_page(target, offset, staging + offset)) {
!program_page(storage_offset, target, offset, staging + offset)) {
return false;
}
}
if (!body_valid(slot_bytes(target)) ||
!program_page(target, kCommitOffset, staging + kCommitOffset)) {
if (!body_valid(slot_bytes(storage_offset, target)) ||
!program_page(storage_offset, target, kCommitOffset, staging + kCommitOffset)) {
return false;
}
const Slot committed = inspect_slot(target);
const Slot committed = inspect_slot(storage_offset, target);
return committed.kind == SlotKind::Committed &&
committed.generation == generation && committed.size == size &&
memcmp(committed.bytes + kPayloadOffset,
staging + kPayloadOffset, size) == 0;
}
uint32_t probe_storage_offset(void) {
return kStorageOffset;
uint32_t probe_storage_offset(uint8_t instance) {
if (instance >= PROBE_CONTROLLER_COUNT) return UINT32_MAX;
return probe_model_is_left(instance) ? kLeftStorageOffset : kRightStorageOffset;
}

View file

@ -11,14 +11,18 @@ extern "C" {
// Synchronous, main-loop-only API for the single-core probe. Serialize calls.
// Blobs are opaque, nonempty, and at most 512 bytes. Load requires an exact
// length match and leaves output unchanged on failure; it never writes flash.
bool probe_storage_load(uint8_t *output, size_t size);
// Invalid instances fail before reading a bank or writing output.
bool probe_storage_load(uint8_t instance, uint8_t *output, size_t size);
// Success means an identical blob was already committed, or a replacement was
// committed and read back. Failure never authorizes a protocol acknowledgement.
bool probe_storage_save(const uint8_t *data, size_t size);
bool probe_storage_save(uint8_t instance, const uint8_t *data, size_t size);
// Flash-relative offset of the two sectors immediately below profile storage.
uint32_t probe_storage_offset(void);
// Flash-relative offset of the instance's two-sector pairing bank, or UINT32_MAX
// for an invalid instance. The right bank remains immediately below profile
// storage; the left bank occupies the preceding two sectors. Both are reserved
// in every build, and load/save inspect and mutate only the selected bank.
uint32_t probe_storage_offset(uint8_t instance);
#ifdef __cplusplus
}

View file

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

View file

@ -1,16 +1,18 @@
#pragma once
#include "model.h"
#define CFG_TUSB_RHPORT0_MODE (OPT_MODE_DEVICE | OPT_MODE_FULL_SPEED)
#ifndef CFG_TUSB_OS
#define CFG_TUSB_OS OPT_OS_NONE
#endif
#define CFG_TUD_ENDPOINT0_SIZE 64
#define CFG_TUD_HID 1
#define CFG_TUD_HID PROBE_CONTROLLER_COUNT
#define CFG_TUD_HID_EP_BUFSIZE 64
#define CFG_TUD_CDC 0
#define CFG_TUD_MSC 0
#define CFG_TUD_MIDI 0
#define CFG_TUD_VENDOR 1
#define CFG_TUD_VENDOR PROBE_CONTROLLER_COUNT
#define CFG_TUD_VENDOR_EPSIZE 64
#define CFG_TUD_VENDOR_RX_BUFSIZE 256
#define CFG_TUD_VENDOR_TX_BUFSIZE 256