Add managed reboot into USB BOOTSEL

This commit is contained in:
Joey Yakimowich-Payne 2026-09-03 14:15:31 -06:00
commit 4626b13bd8
13 changed files with 153 additions and 0 deletions

View file

@ -29,6 +29,7 @@ enum class Call : uint8_t {
kClearPairings,
kPairingSnapshot,
kWatchdogReboot,
kBootselReboot,
};
std::vector<Call> calls;
@ -61,6 +62,9 @@ ControllerProfileConfirmationPolicy feedback_policy =
int reboot_count = 0;
int runtime_reset_count = 0;
int clear_pairings_count = 0;
int bootsel_reboot_count = 0;
uint32_t bootsel_gpio_mask = UINT32_MAX;
uint32_t bootsel_disable_mask = UINT32_MAX;
bool recovery_reserved = false;
bool abandoned_host_receive = false;
bool abandoned_host_receive_canceled = false;
@ -101,6 +105,9 @@ void reset_harness(AdapterRequestedMode requested_mode =
reboot_count = 0;
runtime_reset_count = 0;
clear_pairings_count = 0;
bootsel_reboot_count = 0;
bootsel_gpio_mask = UINT32_MAX;
bootsel_disable_mask = UINT32_MAX;
recovery_reserved = false;
abandoned_host_receive = false;
abandoned_host_receive_canceled = false;
@ -553,6 +560,29 @@ void test_auto_xinput_disconnect_reboots_to_probe() {
"Auto Switch probe rebooted after USB unmount");
}
void test_bootsel_reboot_is_delayed_and_idempotent() {
reset_harness(AdapterRequestedMode::kAuto);
require(adapter_reboot_to_bootsel() &&
adapter_reboot_to_bootsel() &&
runtime_reset_count == 1 &&
bootsel_reboot_count == 0 && reboot_count == 0,
"BOOTSEL request was not accepted exactly once");
adapter_mode_controller_task(UINT32_MAX - 25u);
adapter_mode_controller_task(23);
require(bootsel_reboot_count == 0,
"BOOTSEL reboot occurred before the control-transfer guard");
adapter_mode_controller_task(24);
adapter_mode_controller_task(25);
require(bootsel_reboot_count == 1 &&
bootsel_gpio_mask == 0 &&
bootsel_disable_mask == 0 &&
reboot_count == 0 &&
!adapter_reboot_for_mode_transaction(91),
"BOOTSEL reboot was not delayed, one-shot, or isolated from "
"the correlated watchdog path");
}
} // namespace
void configuration_service_initialize_pre_usb() {
@ -669,6 +699,14 @@ void watchdog_reboot(uint32_t, uint32_t, uint32_t) {
++reboot_count;
}
void reset_usb_boot(uint32_t usb_activity_gpio_pin_mask,
uint32_t disable_interface_mask) {
calls.push_back(Call::kBootselReboot);
++bootsel_reboot_count;
bootsel_gpio_mask = usb_activity_gpio_pin_mask;
bootsel_disable_mask = disable_interface_mask;
}
int main() {
test_pre_tusb_ordering_and_configured_selection();
test_mode_availability_has_one_stable_value();
@ -681,5 +719,6 @@ int main() {
test_recovery_auto_wins_host_mode_interleaving();
test_failed_recovery_never_reboots();
test_auto_xinput_disconnect_reboots_to_probe();
test_bootsel_reboot_is_delayed_and_idempotent();
return 0;
}

View file

@ -0,0 +1,6 @@
#pragma once
#include <stdint.h>
void reset_usb_boot(uint32_t usb_activity_gpio_pin_mask,
uint32_t disable_interface_mask);

View file

@ -58,6 +58,7 @@ class FakeDevice:
self.mode_pending_reads = 0
self.fail_mode_status: int | None = None
self.reboot_transaction_ids: list[int] = []
self.bootsel_reboot_requested = False
self.records = [
(
config_manager.TRANSPORT_CLASSIC,
@ -392,6 +393,9 @@ class FakeDevice:
assert reboot_transaction_id == self.transaction_id
assert self.transaction_status == config_manager.STATUS_OK
self.reboot_transaction_ids.append(reboot_transaction_id)
elif request == config_manager.OP_BOOTSEL_REBOOT:
assert payload == b""
self.bootsel_reboot_requested = True
elif request == config_manager.OP_PAIRING_REFRESH:
self.pairing_generation += 1
elif request == config_manager.OP_PAIRING_CLEAR:
@ -660,6 +664,15 @@ def test_mode_envelopes_and_host_side_validation(
config_manager.OP_REBOOT, reboot_payload
)
config_manager.request_bootsel_reboot(device)
operation, payload, encoded = device.out_requests[-1]
assert operation == config_manager.OP_BOOTSEL_REBOOT
assert payload == b""
assert encoded == config_manager.encode_request(
config_manager.OP_BOOTSEL_REBOOT
)
assert device.bootsel_reboot_requested
for transaction_id in (0, 0x80000000, True):
with pytest.raises(config_manager.ConfigManagerError):
config_manager.request_reboot(device, transaction_id)
@ -1687,6 +1700,20 @@ def test_mode_cli_changes_then_noops(
assert reenumerated.out_requests == []
def test_bootsel_reboot_cli(
monkeypatch: pytest.MonkeyPatch,
capsys: pytest.CaptureFixture[str],
) -> None:
device = FakeDevice()
monkeypatch.setattr(
config_manager, "_candidate_devices", lambda: (device,)
)
assert config_manager.main(["reboot", "bootsel"]) == 0
assert capsys.readouterr().out == "Rebooting into USB BOOTSEL mode.\n"
assert device.bootsel_reboot_requested
def test_mode_parser_accepts_all_implemented_modes() -> None:
for mode in config_manager.REQUESTED_MODE_NAMES:
args = config_manager.build_parser().parse_args(["mode", mode])

View file

@ -30,6 +30,7 @@ uint32_t mode_set_call_count = 0;
uint32_t correlated_reboot_transaction_id = 0;
uint32_t reboot_transaction_id = 0;
uint32_t reboot_call_count = 0;
bool bootsel_reboot_requested = false;
bool refresh_requested = false;
bool clear_requested = false;
Bluepad32BackendDiagnostics current_diagnostics{};
@ -221,6 +222,9 @@ void test_vendor_requests() {
perform_out(Operation::kPairingRefresh, {});
require(refresh_requested,
"pairing refresh was not dispatched");
perform_out(Operation::kBootselReboot, {});
require(bootsel_reboot_requested,
"BOOTSEL reboot request was not dispatched");
perform_out(Operation::kPairingClear, {});
require(clear_requested, "pairing clear was not dispatched");
@ -756,6 +760,11 @@ void bluepad32_input_backend_diagnostics(
*out = current_diagnostics;
}
bool adapter_reboot_to_bootsel() {
bootsel_reboot_requested = true;
return true;
}
bool adapter_host_probe_vendor_control(
uint8_t, uint8_t, const tusb_control_request_t*) {
return false;