Accept USB BOOTSEL reboot on the UART firmware

The regular firmware now answers the EP0 vendor INFO and BOOTSEL reboot
requests with the AIO wire protocol, so switch-pico-config reboot bootsel
works when the Pico's USB side is on a PC. Wire constants move to
usb_management_protocol.h; other operations stall.
This commit is contained in:
Joey Yakimowich-Payne 2026-09-22 10:59:29 -06:00
commit 8db720fb9a
16 changed files with 489 additions and 67 deletions

View file

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

View file

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

Binary file not shown.

Binary file not shown.

View file

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

View file

@ -0,0 +1,136 @@
#include "usb/uart_usb_management.h"
#include <string.h>
#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<uint8_t>(value);
output[1] = static_cast<uint8_t>(value >> 8);
}
void write_u32(uint8_t* output, uint32_t value) {
write_u16(output, static_cast<uint16_t>(value));
write_u16(output + 2, static_cast<uint16_t>(value >> 16));
}
uint16_t read_u16(const uint8_t* input) {
return static_cast<uint16_t>(input[0] | (input[1] << 8));
}
uint32_t read_u32(const uint8_t* input) {
return static_cast<uint32_t>(read_u16(input)) |
(static_cast<uint32_t>(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<uint8_t>(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<uint8_t>(Operation::kInfo);
g_response[6] = static_cast<uint8_t>(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<uint8_t>(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<Operation>(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<uint16_t>(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);
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -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]}"

View file

@ -0,0 +1,14 @@
#pragma once
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
void reset_usb_boot(uint32_t usb_activity_gpio_pin_mask,
uint32_t disable_interface_mask);
#ifdef __cplusplus
}
#endif

View file

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

View file

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

View file

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

View file

@ -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 <hex> -> prints "accepted=<0|1> reboot=<0|1>"
// uart_usb_management_test other <op> -> prints "accepted=<0|1>" for an IN request
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <vector>
#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<uint8_t> parse_hex(const char* text) {
std::vector<uint8_t> bytes;
for (size_t i = 0; text[i] != '\0' && text[i + 1] != '\0'; i += 2) {
bytes.push_back(static_cast<uint8_t>(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<uint8_t>((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<uint8_t>(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<const uint8_t*>(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<uint8_t> payload = parse_hex(argv[2]);
const tusb_control_request_t request =
make_request(TUSB_DIR_OUT, 0x04, static_cast<uint16_t>(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;
}