Add stock-USB native Joy-Con R/L hub bridge
This commit is contained in:
parent
9f6dddb790
commit
1748910316
41 changed files with 7101 additions and 909 deletions
|
|
@ -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
188
README.md
|
|
@ -483,10 +483,12 @@ Protocol references: [WiiBrew Wiimote](https://wiibrew.org/wiki/Wiimote), [Motio
|
|||
|
||||
`SWITCH_PICO_SWITCH2_USB_BRIDGE=ON` selects the separate USB protocol probe in
|
||||
`tools/switch2_usb_probe`, not the ordinary four-Pro-controller AIO output.
|
||||
`SWITCH2_BRIDGE_INPUT=JOYCON2` preserves complete packets from one selected right
|
||||
Joy-Con 2; `SWITCH2_BRIDGE_INPUT=WII` generates native right-Joy-Con reports from
|
||||
Wii input. Select the physical Bluetooth address with
|
||||
`SWITCH2_BRIDGE_SOURCE_ADDRESS`.
|
||||
`SWITCH2_BRIDGE_INPUT=JOYCON2` preserves complete packets from one selected
|
||||
Joy-Con 2. Choose `SWITCH2_PROBE_SIDE=LEFT` or `RIGHT` (default), with matching
|
||||
identity, firmware and calibration captures, and select the physical Bluetooth
|
||||
address with `SWITCH2_BRIDGE_SOURCE_ADDRESS`. Left uses USB PID `2067`/report
|
||||
`07`; right uses PID `2066`/report `08`. `SWITCH2_BRIDGE_INPUT=WII` generates
|
||||
native right-Joy-Con reports and rejects `LEFT`.
|
||||
|
||||
The Wii source requires Pico 2 W, the Bluepad32 backend, Bluetooth `MIXED` mode,
|
||||
and the bridge's native capture prerequisites
|
||||
|
|
@ -502,6 +504,107 @@ factory-memory and user-calibration inputs, a distinct virtual controller addres
|
|||
pairing records and firmware backups are not bundled with the source.
|
||||
Keep a known-good UF2 and use a separate build directory for experiments.
|
||||
|
||||
For read-only **donor capture**, use a separate ordinary Bluepad32 build with
|
||||
`SWITCH_PICO_SWITCH2_USB_BRIDGE=OFF`, `SWITCH_PICO_LOG=ON`,
|
||||
`SWITCH_PICO_SWITCH2_MOUSE_CAPTURE=ON`, and
|
||||
`SWITCH_PICO_SWITCH2_MEMORY_CAPTURE=ON`. After normal pairing/calibration,
|
||||
each connected Joy-Con reads 192 acknowledged 64-byte pages covering factory
|
||||
`0x13000..0x14fff` and user calibration `0x1fc000..0x1fcfff`, logged as
|
||||
`SW2_MEMORY_<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.
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
|
|
|||
906
src/firmware/usb/native_hub/native_hub.c
Normal file
906
src/firmware/usb/native_hub/native_hub.c
Normal 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);
|
||||
}
|
||||
44
src/firmware/usb/native_hub/native_hub.h
Normal file
44
src/firmware/usb/native_hub/native_hub.h
Normal 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
|
||||
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
|
@ -12,23 +12,30 @@ sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "tools"))
|
|||
|
||||
from prepare_bluepad32 import prepare_bluepad32
|
||||
|
||||
|
||||
# Reuse the existing real-BTstack-header radio/run-loop fixture, but drive the
|
||||
# capture-enabled discovery path and link the actual cross-core capture mailbox.
|
||||
SOURCE = r'''
|
||||
SOURCE = r"""
|
||||
#define main parser_fixture_main
|
||||
#include "switch2_parser_native_test.c"
|
||||
#undef main
|
||||
#include "model.h"
|
||||
|
||||
void capture_init(void);
|
||||
bool capture_request(uint8_t id, uint64_t* token);
|
||||
int capture_result(uint64_t token);
|
||||
void capture_cancel(void);
|
||||
void capture_select(uint8_t instance, const uint8_t address[6], uint16_t product_id);
|
||||
void capture_native_stream(uint8_t instance, bool enabled);
|
||||
uint32_t capture_native_peek(uint8_t instance, uint8_t report[63]);
|
||||
bool capture_native_commit(uint8_t instance, uint32_t serial);
|
||||
bool capture_request(uint8_t instance, uint8_t id, uint64_t* token);
|
||||
int capture_result(uint8_t instance, uint64_t token);
|
||||
void capture_cancel(uint8_t instance);
|
||||
void capture_exhaust_records(void);
|
||||
|
||||
#define SECONDARY_HANDLE 0x144
|
||||
static const uint8_t secondary_uuid[16] = {
|
||||
0xd5,0xa9,0xe0,0x1e,0x2f,0xfc,0x4c,0xca,0xb2,0x0c,0x8b,0x67,0x14,0x2b,0xf4,0x42};
|
||||
static const uint8_t secondary_uuids[2][16] = {
|
||||
{0xd5,0xa9,0xe0,0x1e,0x2f,0xfc,0x4c,0xca,0xb2,0x0c,0x8b,0x67,0x14,0x2b,0xf4,0x42},
|
||||
{0xcc,0x1b,0xbb,0xb5,0x73,0x54,0x4d,0x32,0xa7,0x16,0xa8,0x1c,0xb2,0x41,0xa3,0x2a},
|
||||
};
|
||||
#define OTHER_PRODUCT_ID (SWITCH2_PROBE_JOYCON_LEFT ? UNI_SW2_JOYCON_R_PID : UNI_SW2_JOYCON_L_PID)
|
||||
static const uint8_t sample_ack[8] = {0x0a,1,1,2,0x10,0x78,0,0};
|
||||
|
||||
static void native_input(struct fixture_peer* peer) {
|
||||
|
|
@ -36,16 +43,16 @@ static void native_input(struct fixture_peer* peer) {
|
|||
notify(peer, SECONDARY_HANDLE, input, sizeof(input));
|
||||
}
|
||||
|
||||
static struct fixture_peer* sample_ready_on_handle(bool requests, uint32_t start,
|
||||
hci_con_handle_t handle) {
|
||||
reset();
|
||||
now_ms = start;
|
||||
capture_init();
|
||||
request_writes = requests;
|
||||
static struct fixture_peer* connect_sample_source(uint8_t instance,
|
||||
const uint8_t address[6],
|
||||
hci_con_handle_t handle) {
|
||||
const bool left = probe_model_is_left(instance);
|
||||
const unsigned previous_ready = ready;
|
||||
uint8_t advertisement_data[64];
|
||||
size_t advertisement_size = advertisement(advertisement_data, UNI_SW2_JOYCON_R_PID, false);
|
||||
size_t advertisement_size = advertisement(advertisement_data, probe_model_pid(instance), false);
|
||||
reverse_bytes(address, advertisement_data + 4, 6);
|
||||
assert(uni_bt_le_switch2_handle_advertisement(advertisement_data, advertisement_size));
|
||||
struct fixture_peer* peer = &peers[0];
|
||||
struct fixture_peer* peer = &peers[instance];
|
||||
peer->device.conn.handle = handle;
|
||||
peer->link_alive = true;
|
||||
uni_hid_parser_switch2_on_le_connected(&peer->device);
|
||||
|
|
@ -55,12 +62,12 @@ static struct fixture_peer* sample_ready_on_handle(bool requests, uint32_t start
|
|||
reverse_128(service_uuid, service + 12);
|
||||
event(peer, service, sizeof(service));
|
||||
query_done(peer, 0);
|
||||
const unsigned write = requests ? ATT_PROPERTY_WRITE : ATT_PROPERTY_WRITE_WITHOUT_RESPONSE;
|
||||
const unsigned write = request_writes ? ATT_PROPERTY_WRITE : ATT_PROPERTY_WRITE_WITHOUT_RESPONSE;
|
||||
characteristic(peer, INPUT_HANDLE, input_uuid, ATT_PROPERTY_NOTIFY);
|
||||
characteristic(peer, RESPONSE_HANDLE, response_uuid, ATT_PROPERTY_NOTIFY);
|
||||
characteristic(peer, COMMAND_HANDLE, command_uuid, write);
|
||||
characteristic(peer, RUMBLE_HANDLE, rumble_uuids[2], write);
|
||||
characteristic(peer, SECONDARY_HANDLE, secondary_uuid, ATT_PROPERTY_NOTIFY);
|
||||
characteristic(peer, RUMBLE_HANDLE, rumble_uuids[left ? 1 : 2], write);
|
||||
characteristic(peer, SECONDARY_HANDLE, secondary_uuids[left ? 1 : 0], ATT_PROPERTY_NOTIFY);
|
||||
query_done(peer, 0);
|
||||
descriptor(peer, RESPONSE_HANDLE + 2);
|
||||
query_done(peer, 0);
|
||||
|
|
@ -69,19 +76,19 @@ static struct fixture_peer* sample_ready_on_handle(bool requests, uint32_t start
|
|||
descriptor(peer, SECONDARY_HANDLE + 2);
|
||||
query_done(peer, 0);
|
||||
query_done(peer, 0);
|
||||
for (unsigned i = 0; i < 24 && !ready && !disconnected; ++i) {
|
||||
for (unsigned i = 0; i < 24 && ready == previous_ready && !disconnected; ++i) {
|
||||
if (peer->query == QUERY_CCCD) {
|
||||
query_done(peer, 0);
|
||||
} else if (peer->command[0] == 0x10) {
|
||||
uint8_t version[20] = {0x10,1,1,1,0x10,0x78,0,0};
|
||||
version[11] = 1;
|
||||
version[11] = left ? 0 : 1;
|
||||
if (peer->query == QUERY_WRITE) query_done(peer, 0);
|
||||
notify(peer, RESPONSE_HANDLE, version, sizeof(version));
|
||||
} else {
|
||||
acknowledge(peer, false);
|
||||
}
|
||||
}
|
||||
assert(ready == 1 && !disconnected);
|
||||
assert(ready == previous_ready + 1 && !disconnected);
|
||||
// Let the unchanged neutral-rumble budget quiesce before requesting a cue.
|
||||
for (unsigned i = 0; i < 6; ++i) {
|
||||
advance(13);
|
||||
|
|
@ -90,6 +97,15 @@ static struct fixture_peer* sample_ready_on_handle(bool requests, uint32_t start
|
|||
native_input(peer);
|
||||
return peer;
|
||||
}
|
||||
static struct fixture_peer* sample_ready_on_handle(bool requests, uint32_t start,
|
||||
hci_con_handle_t handle) {
|
||||
reset();
|
||||
now_ms = start;
|
||||
capture_init();
|
||||
request_writes = requests;
|
||||
return connect_sample_source(0, controller_address, handle);
|
||||
}
|
||||
|
||||
|
||||
static struct fixture_peer* sample_ready(bool requests, uint32_t start) {
|
||||
return sample_ready_on_handle(requests, start, 0);
|
||||
|
|
@ -97,34 +113,34 @@ static struct fixture_peer* sample_ready(bool requests, uint32_t start) {
|
|||
|
||||
static uint64_t request_sample(struct fixture_peer* peer, uint8_t id) {
|
||||
uint64_t token = 0;
|
||||
assert(capture_request(id, &token) && token);
|
||||
assert(capture_result(token) == 0);
|
||||
assert(capture_request(0, id, &token) && token);
|
||||
assert(capture_result(0, token) == 0);
|
||||
unsigned commands = peer->commands;
|
||||
advance(13);
|
||||
const uint8_t expected[12] = {0x0a,0x91,1,2,0,4,0,0,id,0,0,0};
|
||||
assert(peer->commands == commands + 1 && peer->command_length == sizeof(expected));
|
||||
assert(memcmp(peer->command, expected, sizeof(expected)) == 0);
|
||||
assert(capture_result(token) == 0);
|
||||
assert(capture_result(0, token) == 0);
|
||||
return token;
|
||||
}
|
||||
|
||||
static void successful_ack(struct fixture_peer* peer, uint64_t token) {
|
||||
if (peer->query == QUERY_WRITE) query_done(peer, 0);
|
||||
assert(capture_result(token) == 0);
|
||||
assert(capture_result(0, token) == 0);
|
||||
notify(peer, RESPONSE_HANDLE, sample_ack, sizeof(sample_ack));
|
||||
assert(capture_result(token) == 1);
|
||||
assert(capture_result(token) == -1);
|
||||
assert(capture_result(0, token) == 1);
|
||||
assert(capture_result(0, token) == -1);
|
||||
}
|
||||
|
||||
static void test_ack_order_and_source_matching(void) {
|
||||
struct fixture_peer* peer = sample_ready(true, 0);
|
||||
uint64_t token = request_sample(peer, 3), refused = 99;
|
||||
assert(!capture_request(4, &refused) && refused == 0);
|
||||
assert(!capture_request(0, 4, &refused) && refused == 0);
|
||||
uint8_t malformed[9] = {0x0a,1,1,2,0x10,0x78,0,0,0};
|
||||
notify(peer, RESPONSE_HANDLE, malformed, sizeof(malformed));
|
||||
malformed[3] = 1;
|
||||
notify(peer, RESPONSE_HANDLE, malformed, 8);
|
||||
assert(capture_result(token) == 0);
|
||||
assert(capture_result(0, token) == 0);
|
||||
// Deliver a genuine-shaped ACK from a different Bluetooth handle.
|
||||
uint8_t wrong_peer[20] = {GATT_EVENT_NOTIFICATION,18};
|
||||
little_endian_store_16(wrong_peer, 2, 99);
|
||||
|
|
@ -132,44 +148,91 @@ static void test_ack_order_and_source_matching(void) {
|
|||
little_endian_store_16(wrong_peer, 10, sizeof(sample_ack));
|
||||
memcpy(wrong_peer + 12, sample_ack, sizeof(sample_ack));
|
||||
peer->callback(HCI_EVENT_PACKET, 0, wrong_peer, sizeof(wrong_peer));
|
||||
assert(capture_result(token) == 0);
|
||||
assert(capture_result(0, token) == 0);
|
||||
notify(peer, RESPONSE_HANDLE, sample_ack, sizeof(sample_ack));
|
||||
assert(capture_result(token) == 0); // Application ACK cannot beat ATT completion.
|
||||
assert(capture_result(0, token) == 0); // Application ACK cannot beat ATT completion.
|
||||
query_done(peer, 0);
|
||||
assert(capture_result(token) == 1 && capture_result(token) == -1);
|
||||
assert(capture_result(0, token) == 1 && capture_result(0, token) == -1);
|
||||
for (uint8_t id = 0; id < 8; ++id) {
|
||||
uint64_t next = request_sample(peer, id);
|
||||
assert(next > token);
|
||||
token = next;
|
||||
successful_ack(peer, token); // ATT success alone is not application success.
|
||||
}
|
||||
assert(!capture_request(8, &refused) && !capture_request(3, NULL));
|
||||
assert(!capture_request(0, 8, &refused) && !capture_request(0, 3, NULL));
|
||||
}
|
||||
|
||||
static void test_native_notification_bounds_and_fidelity(void) {
|
||||
struct fixture_peer* peer = sample_ready(false, 0);
|
||||
capture_native_stream(0, true);
|
||||
uint8_t input[100], actual[63];
|
||||
for (unsigned i = 0; i < sizeof(input); ++i)
|
||||
input[i] = (uint8_t)(i * 37 + 11);
|
||||
input[PROBE_IMU_LENGTH_OFFSET] = 40;
|
||||
memset(actual, 0xa5, sizeof(actual));
|
||||
notify(peer, SECONDARY_HANDLE, input, 62);
|
||||
notify(peer, SECONDARY_HANDLE, input, 64);
|
||||
notify(peer, SECONDARY_HANDLE, input, 100);
|
||||
assert(capture_native_peek(0, actual) == 0 && actual[0] == 0xa5);
|
||||
notify(peer, SECONDARY_HANDLE, input, 63);
|
||||
uint32_t serial = capture_native_peek(0, actual);
|
||||
assert(serial && memcmp(actual, input, sizeof(actual)) == 0);
|
||||
assert(capture_native_peek(0, actual) == serial); // Failed USB submission retries intact.
|
||||
assert(capture_native_commit(0, serial) && !capture_native_commit(0, serial));
|
||||
assert(capture_native_peek(0, actual) == 0);
|
||||
}
|
||||
|
||||
static void test_selection_cannot_transfer_pending_ack(void) {
|
||||
struct fixture_peer* peer = sample_ready(true, 0);
|
||||
uint64_t old = request_sample(peer, 3), next = 0;
|
||||
uint8_t other[6];
|
||||
memcpy(other, controller_address, sizeof(other));
|
||||
++other[5];
|
||||
capture_select(0, other, PROBE_JOYCON_PID);
|
||||
assert(capture_result(0, old) == -1);
|
||||
native_input(peer);
|
||||
assert(!capture_request(0, 4, &next)); // Old address cannot activate new source.
|
||||
capture_select(0, controller_address, OTHER_PRODUCT_ID);
|
||||
native_input(peer);
|
||||
assert(!capture_request(0, 4, &next)); // Same address, wrong side still cannot.
|
||||
capture_select(0, controller_address, PROBE_JOYCON_PID);
|
||||
native_input(peer);
|
||||
assert(capture_request(0, 4, &next) && next > old);
|
||||
unsigned commands = peer->commands;
|
||||
advance(13);
|
||||
assert(peer->commands == commands);
|
||||
notify(peer, RESPONSE_HANDLE, sample_ack, sizeof(sample_ack));
|
||||
query_done(peer, 0); // Retired transaction drains without owning the new cue.
|
||||
assert(capture_result(0, old) == -1 && capture_result(0, next) == 0);
|
||||
advance(13);
|
||||
assert(peer->commands == commands + 1 && peer->command[8] == 4);
|
||||
successful_ack(peer, next);
|
||||
}
|
||||
|
||||
static void test_cancel_does_not_transfer_old_ack(void) {
|
||||
struct fixture_peer* peer = sample_ready(true, 0);
|
||||
uint64_t old = request_sample(peer, 3), next;
|
||||
capture_cancel();
|
||||
assert(capture_result(old) == -1);
|
||||
assert(capture_request(4, &next) && next > old);
|
||||
capture_cancel(0);
|
||||
assert(capture_result(0, old) == -1);
|
||||
assert(capture_request(0, 4, &next) && next > old);
|
||||
unsigned commands = peer->commands;
|
||||
advance(13);
|
||||
assert(peer->commands == commands); // Old untagged ACK must drain first.
|
||||
notify(peer, RESPONSE_HANDLE, sample_ack, sizeof(sample_ack));
|
||||
query_done(peer, 0);
|
||||
assert(capture_result(old) == -1 && capture_result(next) == 0);
|
||||
assert(capture_result(0, old) == -1 && capture_result(0, next) == 0);
|
||||
advance(13);
|
||||
assert(peer->commands == commands + 1 && peer->command[8] == 4);
|
||||
successful_ack(peer, next);
|
||||
|
||||
peer = sample_ready(false, 0);
|
||||
commands = peer->commands;
|
||||
assert(capture_request(3, &old));
|
||||
assert(capture_request(0, 3, &old));
|
||||
next_write_error = GATT_CLIENT_BUSY;
|
||||
advance(13);
|
||||
assert(peer->commands == commands);
|
||||
capture_cancel();
|
||||
assert(capture_request(4, &next) && next > old);
|
||||
capture_cancel(0);
|
||||
assert(capture_request(0, 4, &next) && next > old);
|
||||
advance(13);
|
||||
assert(peer->commands == commands + 1 && peer->command[8] == 4);
|
||||
successful_ack(peer, next);
|
||||
|
|
@ -180,7 +243,7 @@ static void test_rejection_disconnect_and_late_link_events(void) {
|
|||
uint64_t old = request_sample(peer, 3);
|
||||
notify(peer, RESPONSE_HANDLE, sample_ack, sizeof(sample_ack));
|
||||
query_done(peer, 0x0e);
|
||||
assert(disconnected == 1 && capture_result(old) == -1);
|
||||
assert(disconnected == 1 && capture_result(0, old) == -1);
|
||||
|
||||
peer = sample_ready(false, 0);
|
||||
old = request_sample(peer, 3);
|
||||
|
|
@ -188,15 +251,15 @@ static void test_rejection_disconnect_and_late_link_events(void) {
|
|||
memcpy(rejected, sample_ack, sizeof(rejected));
|
||||
rejected[5] = 0x81;
|
||||
notify(peer, RESPONSE_HANDLE, rejected, sizeof(rejected));
|
||||
assert(disconnected == 1 && capture_result(old) == -1);
|
||||
assert(disconnected == 1 && capture_result(0, old) == -1);
|
||||
|
||||
peer = sample_ready(false, 0);
|
||||
old = request_sample(peer, 3);
|
||||
btstack_packet_handler_t retired_callback = peer->callback;
|
||||
uni_hid_device_disconnect(&peer->device);
|
||||
assert(capture_result(old) == -1);
|
||||
assert(capture_result(0, old) == -1);
|
||||
uint64_t next = 0;
|
||||
assert(!capture_request(3, &next));
|
||||
assert(!capture_request(0, 3, &next));
|
||||
peer = sample_ready_on_handle(false, 0, 1);
|
||||
next = request_sample(peer, 4);
|
||||
assert(next > old);
|
||||
|
|
@ -207,31 +270,33 @@ static void test_rejection_disconnect_and_late_link_events(void) {
|
|||
little_endian_store_16(late, 10, sizeof(sample_ack));
|
||||
memcpy(late + 12, sample_ack, sizeof(sample_ack));
|
||||
retired_callback(HCI_EVENT_PACKET, 0, late, sizeof(late));
|
||||
assert(capture_result(next) == 0 && capture_result(old) == -1);
|
||||
assert(capture_result(0, next) == 0 && capture_result(0, old) == -1);
|
||||
successful_ack(peer, next);
|
||||
|
||||
old = request_sample(peer, 3);
|
||||
notify(peer, RESPONSE_HANDLE, sample_ack, sizeof(sample_ack));
|
||||
uni_hid_device_disconnect(&peer->device);
|
||||
assert(capture_result(old) == -1); // Disconnect revokes even unconsumed success.
|
||||
assert(capture_result(0, old) == -1); // Disconnect revokes even unconsumed success.
|
||||
}
|
||||
|
||||
static void test_freshness_timeout_and_clock_wrap(void) {
|
||||
capture_init();
|
||||
uint64_t token = 0;
|
||||
assert(!capture_request(3, &token));
|
||||
assert(!capture_request(0, 3, &token));
|
||||
uint8_t input[63] = {0};
|
||||
uint8_t other[6];
|
||||
memcpy(other, controller_address, sizeof(other));
|
||||
++other[5];
|
||||
switch_pico_switch2_mouse_report(UNI_SW2_JOYCON_L_PID, controller_address, 8, input, 63, now_ms);
|
||||
switch_pico_switch2_mouse_report(UNI_SW2_JOYCON_R_PID, other, 8, input, 63, now_ms);
|
||||
assert(!capture_request(3, &token));
|
||||
switch_pico_switch2_mouse_report(OTHER_PRODUCT_ID, controller_address,
|
||||
PROBE_NATIVE_REPORT_ID, input, 63, now_ms);
|
||||
switch_pico_switch2_mouse_report(PROBE_JOYCON_PID, other,
|
||||
PROBE_NATIVE_REPORT_ID, input, 63, now_ms);
|
||||
assert(!capture_request(0, 3, &token));
|
||||
|
||||
struct fixture_peer* peer = sample_ready(false, 0);
|
||||
token = request_sample(peer, 3);
|
||||
advance(500);
|
||||
assert(capture_result(token) == -1 && !capture_request(3, &token));
|
||||
assert(capture_result(0, token) == -1 && !capture_request(0, 3, &token));
|
||||
|
||||
peer = sample_ready(false, UINT32_MAX - 200);
|
||||
uint32_t started = now_ms;
|
||||
|
|
@ -242,11 +307,11 @@ static void test_freshness_timeout_and_clock_wrap(void) {
|
|||
}
|
||||
advance(1999 - (uint32_t)(now_ms - started));
|
||||
native_input(peer);
|
||||
assert(capture_result(token) == 0);
|
||||
assert(capture_result(0, token) == 0);
|
||||
advance(1);
|
||||
assert(capture_result(token) == -1);
|
||||
assert(capture_result(0, token) == -1);
|
||||
notify(peer, RESPONSE_HANDLE, sample_ack, sizeof(sample_ack));
|
||||
assert(capture_result(token) == -1);
|
||||
assert(capture_result(0, token) == -1);
|
||||
|
||||
peer = sample_ready(false, 0);
|
||||
token = request_sample(peer, 3);
|
||||
|
|
@ -254,53 +319,157 @@ static void test_freshness_timeout_and_clock_wrap(void) {
|
|||
advance(100);
|
||||
if (!disconnected) native_input(peer);
|
||||
}
|
||||
assert(disconnected == 1 && capture_result(token) == -1);
|
||||
assert(disconnected == 1 && capture_result(0, token) == -1);
|
||||
}
|
||||
|
||||
#if SWITCH2_PROBE_COMPOSITE || SWITCH2_PROBE_HUB
|
||||
static void test_simultaneous_native_sources_and_real_acks(void) {
|
||||
const uint8_t left_address[6] = {0xc0,0x22,0x33,0x44,0x55,0x67};
|
||||
struct fixture_peer* right = sample_ready(true, 0);
|
||||
struct fixture_peer* left = connect_sample_source(1, left_address, 1);
|
||||
assert(ready == 2 && right->link_alive && left->link_alive);
|
||||
capture_native_stream(0, true);
|
||||
capture_native_stream(1, true);
|
||||
uint8_t rinput[63], linput[63], actual[63];
|
||||
for (unsigned i = 0; i < sizeof(rinput); ++i) {
|
||||
rinput[i] = (uint8_t)(i * 17 + 3);
|
||||
linput[i] = (uint8_t)(i * 29 + 9);
|
||||
}
|
||||
rinput[probe_model_imu_length_offset(0)] = 30;
|
||||
linput[probe_model_imu_length_offset(1)] = 40;
|
||||
notify(right, SECONDARY_HANDLE, rinput, sizeof(rinput));
|
||||
uint32_t rserial = capture_native_peek(0, actual);
|
||||
assert(rserial && memcmp(actual, rinput, sizeof(actual)) == 0);
|
||||
notify(left, SECONDARY_HANDLE, linput, sizeof(linput));
|
||||
uint32_t lserial = capture_native_peek(1, actual);
|
||||
assert(lserial > rserial && memcmp(actual, linput, sizeof(actual)) == 0);
|
||||
assert(!capture_native_commit(0, lserial) && !capture_native_commit(1, rserial));
|
||||
assert(capture_native_commit(1, lserial));
|
||||
assert(capture_native_peek(1, actual) == 0);
|
||||
assert(capture_native_peek(0, actual) == rserial);
|
||||
assert(memcmp(actual, rinput, sizeof(actual)) == 0);
|
||||
|
||||
uint64_t rtoken, ltoken;
|
||||
assert(capture_request(0, 3, &rtoken));
|
||||
assert(capture_request(1, 6, <oken) && ltoken > rtoken);
|
||||
const unsigned rcommands = right->commands, lcommands = left->commands;
|
||||
advance(13);
|
||||
assert(right->commands == rcommands + 1 && right->command[0] == 0x0a && right->command[8] == 3);
|
||||
assert(left->commands == lcommands + 1 && left->command[0] == 0x0a && left->command[8] == 6);
|
||||
assert(capture_result(0, ltoken) == -1 && capture_result(1, rtoken) == -1);
|
||||
assert(capture_result(0, rtoken) == 0 && capture_result(1, ltoken) == 0);
|
||||
|
||||
// Opposite ATT/application ordering on live links: neither acknowledges its mate.
|
||||
notify(left, RESPONSE_HANDLE, sample_ack, sizeof(sample_ack));
|
||||
assert(capture_result(0, rtoken) == 0 && capture_result(1, ltoken) == 0);
|
||||
query_done(right, 0);
|
||||
assert(capture_result(0, rtoken) == 0 && capture_result(1, ltoken) == 0);
|
||||
notify(right, RESPONSE_HANDLE, sample_ack, sizeof(sample_ack));
|
||||
assert(capture_result(0, rtoken) == 1 && capture_result(0, rtoken) == -1);
|
||||
assert(capture_result(1, ltoken) == 0);
|
||||
query_done(left, 0);
|
||||
assert(capture_result(1, ltoken) == 1 && capture_result(1, ltoken) == -1);
|
||||
|
||||
assert(capture_request(0, 4, &rtoken));
|
||||
assert(capture_request(1, 7, <oken));
|
||||
advance(13);
|
||||
assert(right->command[8] == 4 && left->command[8] == 7);
|
||||
notify(left, SECONDARY_HANDLE, linput, sizeof(linput));
|
||||
lserial = capture_native_peek(1, actual);
|
||||
assert(lserial > rserial);
|
||||
uni_hid_device_disconnect(&right->device);
|
||||
assert(capture_result(0, rtoken) == -1 && capture_result(1, ltoken) == 0);
|
||||
assert(capture_native_peek(0, actual) == 0);
|
||||
assert(!capture_native_commit(0, rserial));
|
||||
assert(capture_native_peek(1, actual) == lserial);
|
||||
assert(memcmp(actual, linput, sizeof(actual)) == 0);
|
||||
query_done(left, 0);
|
||||
assert(capture_result(1, ltoken) == 0);
|
||||
notify(left, RESPONSE_HANDLE, sample_ack, sizeof(sample_ack));
|
||||
assert(capture_result(1, ltoken) == 1 && capture_result(1, ltoken) == -1);
|
||||
assert(capture_native_commit(1, lserial));
|
||||
assert(capture_native_peek(1, actual) == 0);
|
||||
}
|
||||
#endif
|
||||
|
||||
static void test_teardown_survives_capture_exhaustion(void) {
|
||||
struct fixture_peer* peer = sample_ready(false, 0);
|
||||
uint64_t token, next;
|
||||
assert(capture_request(3, &token));
|
||||
assert(capture_request(0, 3, &token));
|
||||
// The parser has not taken this request, so only source teardown can fail
|
||||
// the mailbox when the serialized capture cannot record another event.
|
||||
capture_exhaust_records();
|
||||
uni_hid_device_disconnect(&peer->device);
|
||||
assert(capture_result(token) == -1 && !capture_request(3, &next));
|
||||
assert(capture_result(0, token) == -1 && !capture_request(0, 3, &next));
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
test_ack_order_and_source_matching();
|
||||
test_native_notification_bounds_and_fidelity();
|
||||
test_selection_cannot_transfer_pending_ack();
|
||||
test_cancel_does_not_transfer_old_ack();
|
||||
test_rejection_disconnect_and_late_link_events();
|
||||
test_freshness_timeout_and_clock_wrap();
|
||||
#if SWITCH2_PROBE_COMPOSITE || SWITCH2_PROBE_HUB
|
||||
test_simultaneous_native_sources_and_real_acks();
|
||||
#endif
|
||||
test_teardown_survives_capture_exhaustion();
|
||||
reset();
|
||||
puts("Source sample relay ACK ordering, ownership, rejection and lifetime passed");
|
||||
return 0;
|
||||
}
|
||||
'''
|
||||
"""
|
||||
|
||||
CAPTURE = r'''
|
||||
CAPTURE = r"""
|
||||
#include "input/switch2_mouse_capture.cpp"
|
||||
#include "model.h"
|
||||
extern "C" uint32_t btstack_run_loop_get_time_ms(void);
|
||||
extern "C" void capture_init(void) {
|
||||
const uint8_t address[6] = {0xc0,0x22,0x33,0x44,0x55,0x66};
|
||||
switch2_mouse_capture_init();
|
||||
switch2_mouse_capture_select_input(address);
|
||||
switch2_mouse_capture_select_input(0, address, probe_model_pid(0));
|
||||
#if SWITCH2_PROBE_COMPOSITE || SWITCH2_PROBE_HUB
|
||||
const uint8_t second[6] = {0xc0,0x22,0x33,0x44,0x55,0x67};
|
||||
switch2_mouse_capture_select_input(1, second, probe_model_pid(1));
|
||||
#endif
|
||||
}
|
||||
extern "C" bool capture_request(uint8_t id, uint64_t* token) {
|
||||
return switch2_mouse_capture_request_sample(id, btstack_run_loop_get_time_ms(), token);
|
||||
extern "C" void capture_select(uint8_t instance, const uint8_t address[6], uint16_t product_id) {
|
||||
switch2_mouse_capture_select_input(instance, address, product_id);
|
||||
}
|
||||
extern "C" int capture_result(uint64_t token) {
|
||||
return switch2_mouse_capture_sample_result(token, btstack_run_loop_get_time_ms());
|
||||
extern "C" void capture_native_stream(uint8_t instance, bool enabled) {
|
||||
switch2_mouse_capture_set_native_stream(instance, enabled);
|
||||
}
|
||||
extern "C" void capture_cancel(void) { switch2_mouse_capture_cancel_sample(); }
|
||||
extern "C" uint32_t capture_native_peek(uint8_t instance, uint8_t report[63]) {
|
||||
return switch2_mouse_capture_peek_native_report(instance, btstack_run_loop_get_time_ms(), report);
|
||||
}
|
||||
extern "C" bool capture_native_commit(uint8_t instance, uint32_t serial) {
|
||||
return switch2_mouse_capture_commit_native_report(instance, serial);
|
||||
}
|
||||
extern "C" bool capture_request(uint8_t instance, uint8_t id, uint64_t* token) {
|
||||
return switch2_mouse_capture_request_sample(instance, id, btstack_run_loop_get_time_ms(), token);
|
||||
}
|
||||
extern "C" int capture_result(uint8_t instance, uint64_t token) {
|
||||
return switch2_mouse_capture_sample_result(instance, token, btstack_run_loop_get_time_ms());
|
||||
}
|
||||
extern "C" void capture_cancel(uint8_t instance) { switch2_mouse_capture_cancel_sample(instance); }
|
||||
// Simulate the boot-lifetime counter boundary without billions of reports.
|
||||
extern "C" void capture_exhaust_records(void) { g_total_records = UINT32_MAX; }
|
||||
'''
|
||||
"""
|
||||
|
||||
|
||||
def test_source_sample_requires_its_real_bluetooth_ack(tmp_path: Path) -> None:
|
||||
@pytest.mark.parametrize(
|
||||
("left", "composite", "hub"),
|
||||
[
|
||||
(False, False, False),
|
||||
(True, False, False),
|
||||
(False, True, False),
|
||||
(False, False, True),
|
||||
],
|
||||
ids=["right", "left", "composite", "hub"],
|
||||
)
|
||||
def test_source_sample_requires_its_real_bluetooth_ack(
|
||||
tmp_path: Path, left: bool, composite: bool, hub: bool
|
||||
) -> None:
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
cc = shutil.which("cc") or shutil.which("gcc")
|
||||
cxx = shutil.which("c++") or shutil.which("g++")
|
||||
|
|
@ -308,36 +477,85 @@ def test_source_sample_requires_its_real_bluetooth_ack(tmp_path: Path) -> None:
|
|||
sdks = [root / "build" / "_deps" / "pico_sdk-src", root / "external" / "pico-sdk"]
|
||||
if sdk := os.environ.get("PICO_SDK_PATH"):
|
||||
sdks.insert(0, Path(sdk))
|
||||
btstack = next((sdk / "lib" / "btstack" / "src" for sdk in sdks
|
||||
if (sdk / "lib" / "btstack" / "src" / "ble" / "gatt_client.h").is_file()), None)
|
||||
btstack = next(
|
||||
(
|
||||
sdk / "lib" / "btstack" / "src"
|
||||
for sdk in sdks
|
||||
if (sdk / "lib" / "btstack" / "src" / "ble" / "gatt_client.h").is_file()
|
||||
),
|
||||
None,
|
||||
)
|
||||
if btstack is None:
|
||||
pytest.skip("Pico SDK BTstack headers required; configure firmware or set PICO_SDK_PATH")
|
||||
prepared = prepare_bluepad32(root / "external" / "bluepad32",
|
||||
root / "patches" / "bluepad32-sdl3-imu.patch",
|
||||
tmp_path / "bluepad32-src")
|
||||
common = ["-O1", "-Wall", "-Wextra", "-ffunction-sections", "-fdata-sections",
|
||||
"-DSWITCH_PICO_SWITCH2_USB_BRIDGE=1", "-DSWITCH_PICO_SWITCH2_MOUSE_CAPTURE=1",
|
||||
"-DSWITCH_PICO_SWITCH2_MOUSE_CAPTURE_NATIVE=1", f"-I{root / 'bluepad32_config'}"]
|
||||
cflags = [*common, "-std=gnu11", "-DENABLE_BLE", "-DENABLE_CLASSIC",
|
||||
f"-I{root / 'tests'}", f"-I{root / 'tests' / 'switch2_parser_native_stubs'}",
|
||||
f"-I{prepared / 'src' / 'components' / 'bluepad32' / 'include'}", f"-I{btstack}",
|
||||
f"-I{btstack.parent / '3rd-party' / 'bluedroid' / 'encoder' / 'include'}",
|
||||
f"-I{btstack.parent / '3rd-party' / 'bluedroid' / 'decoder' / 'include'}",
|
||||
f"-I{btstack.parent / '3rd-party' / 'yxml'}"]
|
||||
pytest.skip(
|
||||
"Pico SDK BTstack headers required; configure firmware or set PICO_SDK_PATH"
|
||||
)
|
||||
prepared = prepare_bluepad32(
|
||||
root / "external" / "bluepad32",
|
||||
root / "patches" / "bluepad32-sdl3-imu.patch",
|
||||
tmp_path / "bluepad32-src",
|
||||
)
|
||||
common = [
|
||||
"-O1",
|
||||
"-Wall",
|
||||
"-Wextra",
|
||||
"-ffunction-sections",
|
||||
"-fdata-sections",
|
||||
"-DSWITCH_PICO_SWITCH2_USB_BRIDGE=1",
|
||||
"-DSWITCH_PICO_SWITCH2_MOUSE_CAPTURE=1",
|
||||
"-DSWITCH_PICO_SWITCH2_MOUSE_CAPTURE_NATIVE=1",
|
||||
f"-DSWITCH2_PROBE_JOYCON_LEFT={int(left)}",
|
||||
f"-DSWITCH2_PROBE_COMPOSITE={int(composite)}",
|
||||
f"-DSWITCH2_PROBE_HUB={int(hub)}",
|
||||
f"-I{root / 'tools' / 'switch2_usb_probe'}",
|
||||
f"-I{root / 'bluepad32_config'}",
|
||||
]
|
||||
cflags = [
|
||||
*common,
|
||||
"-std=gnu11",
|
||||
"-DENABLE_BLE",
|
||||
"-DENABLE_CLASSIC",
|
||||
f"-I{root / 'tests'}",
|
||||
f"-I{root / 'tests' / 'switch2_parser_native_stubs'}",
|
||||
f"-I{prepared / 'src' / 'components' / 'bluepad32' / 'include'}",
|
||||
f"-I{btstack}",
|
||||
f"-I{btstack.parent / '3rd-party' / 'bluedroid' / 'encoder' / 'include'}",
|
||||
f"-I{btstack.parent / '3rd-party' / 'bluedroid' / 'decoder' / 'include'}",
|
||||
f"-I{btstack.parent / '3rd-party' / 'yxml'}",
|
||||
]
|
||||
source = tmp_path / "sample_relay.c"
|
||||
source.write_text(SOURCE)
|
||||
capture = tmp_path / "capture.cpp"
|
||||
capture.write_text(CAPTURE)
|
||||
objects = []
|
||||
for index, path in enumerate((source, root / "bluepad32_config" / "parser" / "uni_hid_parser_switch2.c",
|
||||
root / "bluepad32_config" / "parser" / "uni_switch2_haptics.c",
|
||||
btstack / "btstack_util.c")):
|
||||
for index, path in enumerate(
|
||||
(
|
||||
source,
|
||||
root / "bluepad32_config" / "parser" / "uni_hid_parser_switch2.c",
|
||||
root / "bluepad32_config" / "parser" / "uni_switch2_haptics.c",
|
||||
btstack / "btstack_util.c",
|
||||
)
|
||||
):
|
||||
obj = tmp_path / f"source{index}.o"
|
||||
subprocess.run([cc, *cflags, "-c", str(path), "-o", str(obj)], check=True, cwd=root)
|
||||
subprocess.run(
|
||||
[cc, *cflags, "-c", str(path), "-o", str(obj)], check=True, cwd=root
|
||||
)
|
||||
objects.append(str(obj))
|
||||
executable = tmp_path / "sample_relay"
|
||||
subprocess.run([cxx, *common, "-std=c++17", "-pthread",
|
||||
f"-I{root / 'tests' / 'switch2_mouse_bridge_native_stubs'}",
|
||||
f"-I{root / 'src' / 'firmware'}", str(capture), *objects,
|
||||
"-Wl,--gc-sections", "-o", str(executable)], check=True, cwd=root)
|
||||
subprocess.run(
|
||||
[
|
||||
cxx,
|
||||
*common,
|
||||
"-std=c++17",
|
||||
"-pthread",
|
||||
f"-I{root / 'tests' / 'switch2_mouse_bridge_native_stubs'}",
|
||||
f"-I{root / 'src' / 'firmware'}",
|
||||
str(capture),
|
||||
*objects,
|
||||
"-Wl,--gc-sections",
|
||||
"-o",
|
||||
str(executable),
|
||||
],
|
||||
check=True,
|
||||
cwd=root,
|
||||
)
|
||||
subprocess.run([str(executable)], check=True, cwd=root)
|
||||
|
|
|
|||
|
|
@ -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
1062
tools/native_joycon_hub_check.py
Executable file
File diff suppressed because it is too large
Load diff
45
tools/pico_usb_address_probe/CMakeLists.txt
Normal file
45
tools/pico_usb_address_probe/CMakeLists.txt
Normal 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)
|
||||
288
tools/pico_usb_address_probe/host_probe.py
Executable file
288
tools/pico_usb_address_probe/host_probe.py
Executable 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())
|
||||
62
tools/pico_usb_address_probe/main.c
Normal file
62
tools/pico_usb_address_probe/main.c
Normal 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);
|
||||
}
|
||||
}
|
||||
738
tools/pico_usb_address_probe/router.c
Normal file
738
tools/pico_usb_address_probe/router.c
Normal 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
|
||||
}
|
||||
}
|
||||
48
tools/pico_usb_address_probe/router.h
Normal file
48
tools/pico_usb_address_probe/router.h
Normal 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);
|
||||
13
tools/pico_usb_address_probe/tusb_config.h
Normal file
13
tools/pico_usb_address_probe/tusb_config.h
Normal 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
|
||||
839
tools/pico_usb_address_probe/usb_probe.c
Normal file
839
tools/pico_usb_address_probe/usb_probe.c
Normal 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);
|
||||
}
|
||||
5
tools/pico_usb_address_probe/usb_probe.h
Normal file
5
tools/pico_usb_address_probe/usb_probe.h
Normal 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);
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
|
|
|
|||
94
tools/switch2_usb_probe/model.h
Normal file
94
tools/switch2_usb_probe/model.h
Normal 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;
|
||||
}
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
101
tools/switch2_usb_probe/transport.h
Normal file
101
tools/switch2_usb_probe/transport.h
Normal 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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue