diff --git a/CMakeLists.txt b/CMakeLists.txt index 947132c..92629fa 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -573,6 +573,16 @@ else() target_compile_definitions(switch-pico PRIVATE SWITCH_PICO_HID_INSTANCE_COUNT=${SWITCH_PICO_UART_CONTROLLERS} ) + if(NOT SWITCH_PICO_SWITCH2_USB_BRIDGE) + # INFO + BOOTSEL reboot over EP0 so switch-pico-config can reflash a + # UART Pico plugged into a PC. configuration_storage.cpp provides the + # protocol CRC; no storage is instantiated. + target_compile_definitions(switch-pico PRIVATE SWITCH_PICO_UART_USB_MANAGEMENT=1) + target_sources(switch-pico PRIVATE + ${SWITCH_PICO_SOURCE_DIR}/usb/uart_usb_management.cpp + ${SWITCH_PICO_SOURCE_DIR}/configuration/configuration_storage.cpp + ) + endif() endif() if(NOT SWITCH_PICO_SWITCH2_USB_BRIDGE) diff --git a/README.md b/README.md index f84d942..5da107e 100644 --- a/README.md +++ b/README.md @@ -2649,24 +2649,36 @@ The firmware acknowledges the endpoint-zero request, waits 50 ms, and then calls the Pico ROM `reset_usb_boot()` entry point. Physical BOOTSEL remains the fallback if the firmware or USB management path is unavailable. -The regular UART firmware has the same shortcut over its serial link. The -host sends the command frame `0xAA 0xFE 0x08 0x01 "BOOTSEL" checksum`; the -firmware verifies the checksum and magic, flushes UART TX, and calls -`reset_usb_boot()`. Line noise or a misframed report cannot trigger it. The -Pico can stay plugged into the Switch or a PC; only the UART adapter needs to -be connected to the host: +The regular UART firmware has the same shortcut, reachable two ways: + +- **Over USB** (Pico's USB plugged into the PC, no serial adapter needed): the + firmware answers the same EP0 vendor `INFO` and `BOOTSEL reboot` requests as + the AIO, so `switch-pico-config reboot bootsel` works unchanged. Every other + management operation is stalled; `status`, profiles and configuration are + AIO-only. The Switch never issues vendor requests, so the handler is + invisible to the console. +- **Over UART** (Pico can stay on the Switch or a PC; only the serial adapter + is on the host): the host sends `0xAA 0xFE 0x08 0x01 "BOOTSEL" checksum`; + the firmware verifies checksum and magic, flushes UART TX, and calls + `reset_usb_boot()`. Line noise or a misframed report cannot trigger it. ```sh -# One-off reboot, then flash the published image +# USB side on the PC +uv run switch-pico-config reboot bootsel +picotool load -v -x firmware/switch-pico.uf2 + +# UART adapter on the PC uv run controller-uart-bridge --reboot-bootsel /dev/ttyUSB0 picotool load -v -x firmware/switch-pico.uf2 -# Build, publish, reboot the running Pico and flash in one step +# Build, publish, reboot the running Pico over UART and flash in one step uv run python build.py --uart-port /dev/ttyUSB0 ``` -Firmware from before this command ignores the frame; hold BOOTSEL while -replugging once to install a build that has it. +Either way the RPI-RP2 loader appears on whichever USB host the Pico's own +USB port is connected to, so flashing needs that port on the PC. Firmware from +before these commands ignores both; hold BOOTSEL while replugging once to +install a build that has them. Flash alternatives: bootsel + drag-drop or `picotool load`. Flags: diff --git a/firmware/switch-pico.elf b/firmware/switch-pico.elf index 6b6b9d9..114f962 100755 Binary files a/firmware/switch-pico.elf and b/firmware/switch-pico.elf differ diff --git a/firmware/switch-pico.uf2 b/firmware/switch-pico.uf2 index a27f842..40a226c 100644 Binary files a/firmware/switch-pico.uf2 and b/firmware/switch-pico.uf2 differ diff --git a/src/firmware/main.cpp b/src/firmware/main.cpp index a21858c..d8bd086 100644 --- a/src/firmware/main.cpp +++ b/src/firmware/main.cpp @@ -8,6 +8,9 @@ #ifndef SWITCH_PICO_BLUEPAD32 #include "hardware/uart.h" #include "pico/bootrom.h" +#ifdef SWITCH_PICO_UART_USB_MANAGEMENT +#include "usb/uart_usb_management.h" +#endif #else #include "adapter/adapter_mode_controller.h" #include "input/bluepad32_input_backend.h" @@ -382,6 +385,9 @@ int main() { SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD); (void)usb_output_driver_task(instance); } +#ifdef SWITCH_PICO_UART_USB_MANAGEMENT + uart_usb_management_task(); +#endif #endif log_usb_state(); } diff --git a/src/firmware/usb/uart_usb_management.cpp b/src/firmware/usb/uart_usb_management.cpp new file mode 100644 index 0000000..4aa959e --- /dev/null +++ b/src/firmware/usb/uart_usb_management.cpp @@ -0,0 +1,136 @@ +#include "usb/uart_usb_management.h" + +#include + +#include "pico/bootrom.h" +#include "pico/time.h" + +#include "configuration/configuration_storage.h" +#include "usb/usb_management_protocol.h" +#include "usb/usb_output_driver.h" + +namespace { + +using namespace UsbConfigurationManagement; + +// Let the status ACK reach the host before the USB controller disappears. +constexpr uint32_t kBootselRebootDelayMs = 50; +constexpr uint8_t kFirmwareVersion[3] = {0, 2, 0}; + +uint8_t g_request[kRequestHeaderSize]{}; +uint8_t g_response[kResponseHeaderSize + kInfoPayloadSize]{}; +tusb_control_request_t g_pending_setup{}; +bool g_out_pending = false; +bool g_reboot_requested = false; +absolute_time_t g_reboot_deadline{}; + +void write_u16(uint8_t* output, uint16_t value) { + output[0] = static_cast(value); + output[1] = static_cast(value >> 8); +} + +void write_u32(uint8_t* output, uint32_t value) { + write_u16(output, static_cast(value)); + write_u16(output + 2, static_cast(value >> 16)); +} + +uint16_t read_u16(const uint8_t* input) { + return static_cast(input[0] | (input[1] << 8)); +} + +uint32_t read_u32(const uint8_t* input) { + return static_cast(read_u16(input)) | + (static_cast(read_u16(input + 2)) << 16); +} + +// Same envelope as usb_configuration_management.cpp encode_response. +size_t encode_info() { + uint8_t payload[kInfoPayloadSize] = { + kFirmwareVersion[0], kFirmwareVersion[1], kFirmwareVersion[2], + kBoardPico, + static_cast(usb_output_driver_mode()), + usb_output_driver_capabilities(), + 0, 0, // No configuration storage on this firmware. + }; + memcpy(g_response, "SPMG", 4); + g_response[4] = kProtocolVersion; + g_response[5] = static_cast(Operation::kInfo); + g_response[6] = static_cast(Status::kOk); + g_response[7] = 0; + write_u16(&g_response[8], kInfoPayloadSize); + write_u16(&g_response[10], 0); + write_u32(&g_response[12], 0); + write_u32(&g_response[16], configuration_crc32(payload, sizeof(payload))); + memcpy(&g_response[kResponseHeaderSize], payload, sizeof(payload)); + return sizeof(g_response); +} + +// Same header validation as usb_configuration_management.cpp decode_request, +// restricted to the empty-payload BOOTSEL request. +bool valid_bootsel_request() { + return memcmp(g_request, "SPMG", 4) == 0 && + g_request[4] == kProtocolVersion && + g_request[5] == static_cast(Operation::kBootselReboot) && + g_request[6] == 0 && g_request[7] == 0 && + read_u16(&g_request[8]) == 0 && read_u16(&g_request[10]) == 0 && + read_u32(&g_request[12]) == configuration_crc32(nullptr, 0); +} + +} // namespace + +bool uart_usb_management_vendor_control(uint8_t rhport, uint8_t stage, + const tusb_control_request_t* request) { + if (request == nullptr || + request->bmRequestType_bit.type != TUSB_REQ_TYPE_VENDOR || + request->bmRequestType_bit.recipient != TUSB_REQ_RCPT_DEVICE || + request->wValue != kRequestValue || + request->wIndex != kRequestIndex) { + return false; + } + const Operation operation = static_cast(request->bRequest); + const bool input = request->bmRequestType_bit.direction == TUSB_DIR_IN; + + if (stage == CONTROL_STAGE_SETUP) { + g_out_pending = false; + if (input) { + if (operation != Operation::kInfo) return false; + return tud_control_xfer(rhport, request, g_response, + static_cast(encode_info())); + } + if (operation != Operation::kBootselReboot || + request->wLength != kRequestHeaderSize) { + return false; + } + g_pending_setup = *request; + g_out_pending = tud_control_xfer(rhport, request, g_request, + request->wLength); + return g_out_pending; + } + if (stage == CONTROL_STAGE_DATA) { + return true; + } + if (stage != CONTROL_STAGE_ACK) { + return false; + } + if (input) { + return true; + } + if (!g_out_pending || + memcmp(request, &g_pending_setup, sizeof(*request)) != 0 || + !valid_bootsel_request()) { + g_out_pending = false; + return false; + } + g_out_pending = false; + if (!g_reboot_requested) { + g_reboot_requested = true; + g_reboot_deadline = make_timeout_time_ms(kBootselRebootDelayMs); + } + return true; +} + +void uart_usb_management_task() { + if (g_reboot_requested && time_reached(g_reboot_deadline)) { + reset_usb_boot(0, 0); + } +} diff --git a/src/firmware/usb/uart_usb_management.h b/src/firmware/usb/uart_usb_management.h new file mode 100644 index 0000000..337c210 --- /dev/null +++ b/src/firmware/usb/uart_usb_management.h @@ -0,0 +1,16 @@ +#pragma once + +#include + +#include "tusb.h" + +// Minimal EP0 vendor-request management for the regular UART firmware. It +// answers INFO and accepts BOOTSEL reboot using the same wire protocol as the +// AIO firmware, so `switch-pico-config reboot bootsel` works when the Pico's +// USB side is plugged into a PC. Every other operation is stalled. +bool uart_usb_management_vendor_control(uint8_t rhport, uint8_t stage, + const tusb_control_request_t* request); + +// Main-loop hook: performs a requested BOOTSEL reboot once the USB status +// stage has had time to complete. +void uart_usb_management_task(); diff --git a/src/firmware/usb/usb_configuration_management.h b/src/firmware/usb/usb_configuration_management.h index f7afbd5..4199c7e 100644 --- a/src/firmware/usb/usb_configuration_management.h +++ b/src/firmware/usb/usb_configuration_management.h @@ -8,18 +8,14 @@ #include "input/bluepad32_input_backend.h" #include "configuration/configuration_service.h" #include "profile/profile_service.h" +#include "usb/usb_management_protocol.h" namespace UsbConfigurationManagement { -constexpr uint16_t kRequestValue = 0x5350; -constexpr uint16_t kRequestIndex = 0x0001; -constexpr uint8_t kProtocolVersion = 1; // INFO-only active mode; never persisted as an AdapterRequestedMode. constexpr uint8_t kNativeHubActiveMode = 5; -constexpr size_t kRequestHeaderSize = 16; constexpr uint16_t kSwitch2WakeSchemaVersion = 1; constexpr size_t kSwitch2WakePayloadSize = 20; -constexpr size_t kResponseHeaderSize = 20; constexpr size_t kPairingRecordSize = 8; constexpr size_t kPairingPayloadHeaderSize = 4; constexpr size_t kMaximumRequestSize = 80; @@ -51,56 +47,6 @@ constexpr size_t kMaximumChunkSize = static_assert(kMaximumResponseSize == 837, "profile list no longer fits the EP0 response buffer"); -enum class Operation : uint8_t { - kInfo = 0x01, - kModeSet = 0x02, - kReboot = 0x03, - kBootselReboot = 0x04, - kSwitch2Wake = 0x05, - kConfigurationRead = 0x10, - kConfigurationBegin = 0x11, - kConfigurationChunk = 0x12, - kConfigurationCommit = 0x13, - kConfigurationReset = 0x14, - kTransactionStatus = 0x15, - kPairingRead = 0x20, - kPairingRefresh = 0x21, - kPairingClear = 0x22, - kRuntimeDiagnostics = 0x23, - kProfileList = 0x30, - kProfileSelect = 0x31, - kProfileRead = 0x32, - kProfileBegin = 0x33, - kProfileChunk = 0x34, - kProfileCommit = 0x35, - kProfileReset = 0x36, - kProfileActivate = 0x37, - kProfileTransactionStatus = 0x38, - kProfilePlaytest = 0x39, - kProfileMetadataRead = 0x3a, - kProfileMetadataSet = 0x3b, - kProfileIdentify = 0x3c, - kWiiOrientation = 0x3d, - kHapticsExperiment = 0x40, - kHapticsTransportProbe = 0x41, - kMacroCapture = 0x42, - kNativeSwitchRumble = 0x43, - kSwitch2MouseCapture = 0x44, - kWiiIrGyro = 0x45, -}; - -enum class Status : uint8_t { - kOk = 0, - kPending = 1, - kMalformed = 2, - kUnsupportedSchema = 3, - kTooLarge = 4, - kOutOfOrder = 5, - kBadCrc = 6, - kBusy = 7, - kStorageError = 8, -}; - struct DecodedRequest { Operation operation = Operation::kInfo; const uint8_t* payload = nullptr; diff --git a/src/firmware/usb/usb_management_protocol.h b/src/firmware/usb/usb_management_protocol.h new file mode 100644 index 0000000..f493bb7 --- /dev/null +++ b/src/firmware/usb/usb_management_protocol.h @@ -0,0 +1,72 @@ +#pragma once + +#include +#include + +// EP0 vendor-request wire protocol shared by every firmware variant and +// switch_pico_bridge.config_manager. Keep this header free of adapter, +// Bluetooth or storage dependencies so the UART firmware can speak the +// subset it supports. +namespace UsbConfigurationManagement { + +constexpr uint16_t kRequestValue = 0x5350; +constexpr uint16_t kRequestIndex = 0x0001; +constexpr uint8_t kProtocolVersion = 1; +constexpr size_t kRequestHeaderSize = 16; +constexpr size_t kResponseHeaderSize = 20; +constexpr size_t kInfoPayloadSize = 8; +// Board byte of the INFO payload. +constexpr uint8_t kBoardPico = 1; +constexpr uint8_t kBoardPico2W = 2; + +enum class Operation : uint8_t { + kInfo = 0x01, + kModeSet = 0x02, + kReboot = 0x03, + kBootselReboot = 0x04, + kSwitch2Wake = 0x05, + kConfigurationRead = 0x10, + kConfigurationBegin = 0x11, + kConfigurationChunk = 0x12, + kConfigurationCommit = 0x13, + kConfigurationReset = 0x14, + kTransactionStatus = 0x15, + kPairingRead = 0x20, + kPairingRefresh = 0x21, + kPairingClear = 0x22, + kRuntimeDiagnostics = 0x23, + kProfileList = 0x30, + kProfileSelect = 0x31, + kProfileRead = 0x32, + kProfileBegin = 0x33, + kProfileChunk = 0x34, + kProfileCommit = 0x35, + kProfileReset = 0x36, + kProfileActivate = 0x37, + kProfileTransactionStatus = 0x38, + kProfilePlaytest = 0x39, + kProfileMetadataRead = 0x3a, + kProfileMetadataSet = 0x3b, + kProfileIdentify = 0x3c, + kWiiOrientation = 0x3d, + kHapticsExperiment = 0x40, + kHapticsTransportProbe = 0x41, + kMacroCapture = 0x42, + kNativeSwitchRumble = 0x43, + kSwitch2MouseCapture = 0x44, + kWiiIrGyro = 0x45, +}; + +enum class Status : uint8_t { + kOk = 0, + kPending = 1, + kMalformed = 2, + kUnsupportedSchema = 3, + kTooLarge = 4, + kOutOfOrder = 5, + kBadCrc = 6, + kBusy = 7, + kStorageError = 8, +}; + +} // namespace UsbConfigurationManagement diff --git a/src/firmware/usb/usb_output_driver.cpp b/src/firmware/usb/usb_output_driver.cpp index e560edc..bed5bdb 100644 --- a/src/firmware/usb/usb_output_driver.cpp +++ b/src/firmware/usb/usb_output_driver.cpp @@ -17,6 +17,8 @@ #ifdef SWITCH_PICO_BLUEPAD32 #include "usb/usb_configuration_management.h" +#elif defined(SWITCH_PICO_UART_USB_MANAGEMENT) +#include "usb/uart_usb_management.h" #endif #ifdef SWITCH_PICO_USB_OUTPUT_MODES @@ -390,6 +392,8 @@ extern "C" bool tud_vendor_control_xfer_cb( #ifdef SWITCH_PICO_BLUEPAD32 return usb_configuration_management_vendor_control(rhport, stage, request); +#elif defined(SWITCH_PICO_UART_USB_MANAGEMENT) + return uart_usb_management_vendor_control(rhport, stage, request); #else (void)rhport; (void)stage; diff --git a/src/switch_pico_bridge/config_manager.py b/src/switch_pico_bridge/config_manager.py index c586fb1..28c1d6b 100755 --- a/src/switch_pico_bridge/config_manager.py +++ b/src/switch_pico_bridge/config_manager.py @@ -164,6 +164,8 @@ CAPABILITY_INPUT = 1 << 0 CAPABILITY_RUMBLE = 1 << 1 CAPABILITY_MOTION = 1 << 2 CAPABILITY_MASK = CAPABILITY_INPUT | CAPABILITY_RUMBLE | CAPABILITY_MOTION +# USB management info byte 3. +BOARD_NAMES = {1: "Pico", 2: "Pico 2 W"} PAIRING_RECORD_SIZE = 8 PAIRING_RECORD_CAPACITY = 16 TRANSPORT_UNKNOWN = 0 @@ -5489,7 +5491,7 @@ def main(argv: Sequence[str] | None = None) -> int: configuration = read_configuration(device) version = ".".join(str(part) for part in info.firmware_version) print(f"Firmware: {version}") - print(f"Board: Pico 2 W ({info.board})") + print(f"Board: {BOARD_NAMES.get(info.board, 'unknown')} ({info.board})") print( "Requested USB mode: " f"{REQUESTED_MODE_NAMES[configuration.requested_mode]}" diff --git a/tests/native_stubs/pico/bootrom.h b/tests/native_stubs/pico/bootrom.h new file mode 100644 index 0000000..844f9e6 --- /dev/null +++ b/tests/native_stubs/pico/bootrom.h @@ -0,0 +1,14 @@ +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +void reset_usb_boot(uint32_t usb_activity_gpio_pin_mask, + uint32_t disable_interface_mask); + +#ifdef __cplusplus +} +#endif diff --git a/tests/native_stubs/pico/time.h b/tests/native_stubs/pico/time.h index 7ade2a8..bf54fd1 100644 --- a/tests/native_stubs/pico/time.h +++ b/tests/native_stubs/pico/time.h @@ -1,5 +1,6 @@ #pragma once +#include #include #ifdef __cplusplus @@ -12,6 +13,8 @@ typedef struct { absolute_time_t get_absolute_time(void); uint32_t to_ms_since_boot(absolute_time_t time); +absolute_time_t make_timeout_time_ms(uint32_t milliseconds); +bool time_reached(absolute_time_t time); #ifdef __cplusplus } diff --git a/tests/native_stubs/tusb.h b/tests/native_stubs/tusb.h index 78bc4ef..ec896d7 100644 --- a/tests/native_stubs/tusb.h +++ b/tests/native_stubs/tusb.h @@ -25,6 +25,11 @@ enum { TUSB_DIR_IN = 1, TUSB_DESC_ENDPOINT = 5, TUSB_DESC_STRING = 3, + TUSB_REQ_RCPT_DEVICE = 0, + TUSB_REQ_TYPE_VENDOR = 2, + CONTROL_STAGE_SETUP = 0, + CONTROL_STAGE_DATA = 1, + CONTROL_STAGE_ACK = 2, }; #pragma pack(push, 1) @@ -64,13 +69,23 @@ static inline uint8_t tu_edpt_dir(uint8_t endpoint) { } typedef struct { - uint8_t bmRequestType; + union { + struct { + uint8_t recipient : 5; + uint8_t type : 2; + uint8_t direction : 1; + } bmRequestType_bit; + uint8_t bmRequestType; + }; uint8_t bRequest; uint16_t wValue; uint16_t wIndex; uint16_t wLength; } tusb_control_request_t; +bool tud_control_xfer(uint8_t rhport, tusb_control_request_t const* request, + void* buffer, uint16_t length); + bool tud_hid_n_ready(uint8_t instance); bool tud_hid_n_report(uint8_t instance, uint8_t report_id, const void* report, uint16_t length); diff --git a/tests/test_uart_usb_management_native.py b/tests/test_uart_usb_management_native.py new file mode 100644 index 0000000..04b54ce --- /dev/null +++ b/tests/test_uart_usb_management_native.py @@ -0,0 +1,78 @@ +"""The UART firmware's EP0 management must interoperate with config_manager.""" + +from __future__ import annotations + +import shutil +import subprocess +from pathlib import Path + +import pytest + +from switch_pico_bridge import config_manager + + +@pytest.fixture(scope="module") +def harness(tmp_path_factory: pytest.TempPathFactory) -> Path: + root = Path(__file__).resolve().parents[1] + compiler = shutil.which("c++") or shutil.which("g++") + assert compiler is not None, "a host C++ compiler is required" + executable = tmp_path_factory.mktemp("uart_usb_management") / "harness" + subprocess.run( + [ + compiler, + "-std=c++17", + "-Wall", + "-Wextra", + "-Werror", + "-pedantic", + "-DSWITCH_PICO_HID_INSTANCE_COUNT=4", + f"-I{root / 'tests' / 'native_stubs'}", + f"-I{root / 'src' / 'firmware'}", + str(root / "src" / "firmware" / "usb" / "uart_usb_management.cpp"), + str(root / "src" / "firmware" / "configuration" / "configuration_storage.cpp"), + str(root / "tests" / "uart_usb_management_test.cpp"), + "-o", + str(executable), + ], + check=True, + cwd=root, + ) + return executable + + +def run(harness: Path, *args: str) -> str: + return subprocess.run( + [str(harness), *args], check=True, capture_output=True, text=True + ).stdout.strip() + + +def test_info_response_parses_as_a_regular_pico(harness: Path) -> None: + response = bytes.fromhex(run(harness, "info")) + envelope = config_manager.parse_response(response, config_manager.OP_INFO) + assert envelope.status == config_manager.STATUS_OK + version = tuple(envelope.payload[:3]) + board, active_mode, capabilities = envelope.payload[3:6] + assert version == (0, 2, 0) + assert config_manager.BOARD_NAMES[board] == "Pico" + assert config_manager.ACTIVE_MODE_NAMES[active_mode] == "Switch" + assert capabilities & config_manager.CAPABILITY_INPUT + assert capabilities & ~config_manager.CAPABILITY_MASK == 0 + + +def test_bootsel_request_from_config_manager_reboots_after_status_stage( + harness: Path, +) -> None: + request = config_manager.encode_request(config_manager.OP_BOOTSEL_REBOOT, b"") + assert run(harness, "bootsel", request.hex()) == "accepted=1 early=0 reboot=1" + + +def test_corrupted_bootsel_request_is_rejected_without_rebooting(harness: Path) -> None: + request = bytearray(config_manager.encode_request(config_manager.OP_BOOTSEL_REBOOT, b"")) + request[-1] ^= 0x01 # break the payload CRC + assert run(harness, "bootsel", bytes(request).hex()) == "accepted=0 early=0 reboot=0" + wrong_operation = config_manager.encode_request(config_manager.OP_REBOOT, b"") + assert run(harness, "bootsel", wrong_operation.hex()) == "accepted=0 early=0 reboot=0" + + +def test_unsupported_operations_are_stalled(harness: Path) -> None: + assert run(harness, "other", str(config_manager.OP_CONFIGURATION_READ)) == "accepted=0" diff --git a/tests/uart_usb_management_test.cpp b/tests/uart_usb_management_test.cpp new file mode 100644 index 0000000..5672fcc --- /dev/null +++ b/tests/uart_usb_management_test.cpp @@ -0,0 +1,108 @@ +// Host harness for the UART firmware's EP0 management handler. Drives the +// TinyUSB control stages the way usbd does and reports what reached the +// (stubbed) ROM. Request/response bytes cross stdin/stdout as hex so the +// Python test can build them with config_manager itself. +// +// uart_usb_management_test info -> prints INFO response hex +// uart_usb_management_test bootsel -> prints "accepted=<0|1> reboot=<0|1>" +// uart_usb_management_test other -> prints "accepted=<0|1>" for an IN request +#include +#include +#include +#include +#include + +#include "tusb.h" +#include "pico/bootrom.h" +#include "pico/time.h" +#include "usb/uart_usb_management.h" +#include "usb/usb_output_driver.h" + +namespace { + +void* g_xfer_buffer = nullptr; +uint16_t g_xfer_length = 0; +int g_reboots = 0; +uint64_t g_now_ms = 1000; + +std::vector parse_hex(const char* text) { + std::vector bytes; + for (size_t i = 0; text[i] != '\0' && text[i + 1] != '\0'; i += 2) { + bytes.push_back(static_cast(strtoul(std::string(text + i, 2).c_str(), nullptr, 16))); + } + return bytes; +} + +tusb_control_request_t make_request(uint8_t direction, uint8_t operation, uint16_t length) { + tusb_control_request_t request{}; + request.bmRequestType = static_cast((direction << 7) | (TUSB_REQ_TYPE_VENDOR << 5)); + request.bRequest = operation; + request.wValue = 0x5350; + request.wIndex = 0x0001; + request.wLength = length; + return request; +} + +} // namespace + +extern "C" bool tud_control_xfer(uint8_t, tusb_control_request_t const*, void* buffer, uint16_t length) { + g_xfer_buffer = buffer; + g_xfer_length = length; + return true; +} + +extern "C" void reset_usb_boot(uint32_t, uint32_t) { ++g_reboots; } + +extern "C" absolute_time_t make_timeout_time_ms(uint32_t milliseconds) { + return absolute_time_t{g_now_ms + milliseconds}; +} + +extern "C" bool time_reached(absolute_time_t time) { return g_now_ms >= time.milliseconds; } + +AdapterUsbMode usb_output_driver_mode() { return AdapterUsbMode::kSwitch; } +uint8_t usb_output_driver_capabilities() { + return USB_OUTPUT_CAPABILITY_INPUT | USB_OUTPUT_CAPABILITY_RUMBLE | USB_OUTPUT_CAPABILITY_MOTION; +} + +int main(int argc, char** argv) { + if (argc < 2) return 2; + const std::string mode = argv[1]; + if (mode == "info" || mode == "other") { + const uint8_t operation = mode == "info" ? 0x01 : static_cast(strtoul(argv[2], nullptr, 0)); + const tusb_control_request_t request = make_request(TUSB_DIR_IN, operation, 837); + const bool accepted = uart_usb_management_vendor_control(0, CONTROL_STAGE_SETUP, &request); + if (mode == "other") { + printf("accepted=%d\n", accepted ? 1 : 0); + return 0; + } + if (!accepted) return 1; + uart_usb_management_vendor_control(0, CONTROL_STAGE_ACK, &request); + const uint8_t* bytes = static_cast(g_xfer_buffer); + for (uint16_t i = 0; i < g_xfer_length; ++i) printf("%02x", bytes[i]); + printf("\n"); + return 0; + } + if (mode == "bootsel" && argc >= 3) { + const std::vector payload = parse_hex(argv[2]); + const tusb_control_request_t request = + make_request(TUSB_DIR_OUT, 0x04, static_cast(payload.size())); + bool accepted = uart_usb_management_vendor_control(0, CONTROL_STAGE_SETUP, &request); + if (accepted) { + if (g_xfer_length < payload.size()) return 3; + memcpy(g_xfer_buffer, payload.data(), payload.size()); + accepted = uart_usb_management_vendor_control(0, CONTROL_STAGE_DATA, &request) && + uart_usb_management_vendor_control(0, CONTROL_STAGE_ACK, &request); + } + // The reboot must wait for the status stage, then fire exactly once. + uart_usb_management_task(); + const int early = g_reboots; + g_now_ms += 49; + uart_usb_management_task(); + const int before_deadline = g_reboots; + g_now_ms += 1; + uart_usb_management_task(); + printf("accepted=%d early=%d reboot=%d\n", accepted ? 1 : 0, early + before_deadline, g_reboots); + return 0; + } + return 2; +}