Add persistent USB output mode selection

This commit is contained in:
Joey Yakimowich-Payne 2026-09-03 07:26:56 -06:00
commit ec9359bce8
35 changed files with 4136 additions and 217 deletions

View file

@ -0,0 +1,193 @@
#include "adapter_host_probe.h"
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include "hardware/structs/watchdog.h"
#include "pico/time.h"
#include "xinput_descriptors.h"
namespace {
constexpr uint32_t kXInputBootMagic = 0x58494e50u;
watchdog_hw_t watchdog_registers{};
uint64_t now_ms = 0;
alarm_callback_t pending_alarm = nullptr;
void* pending_alarm_user_data = nullptr;
int64_t pending_alarm_delay_ms = 0;
int alarm_count = 0;
int reset_count = 0;
int reboot_count = 0;
int call_sequence = 0;
int reset_sequence = 0;
int reboot_sequence = 0;
bool control_result = true;
int control_count = 0;
const void* control_buffer = nullptr;
uint16_t control_length = 0;
void require(bool condition, const char* message) {
if (!condition) {
std::cerr << "FAIL: " << message << '\n';
std::exit(1);
}
}
void reset_harness(uint32_t scratch) {
watchdog_registers = {};
watchdog_registers.scratch[0] = scratch;
now_ms = 0;
pending_alarm = nullptr;
pending_alarm_user_data = nullptr;
pending_alarm_delay_ms = 0;
alarm_count = 0;
reset_count = 0;
reboot_count = 0;
call_sequence = 0;
reset_sequence = 0;
reboot_sequence = 0;
control_result = true;
control_count = 0;
control_buffer = nullptr;
control_length = 0;
}
tusb_control_request_t microsoft_request() {
tusb_control_request_t request{};
request.bmRequestType_bit.direction = TUSB_DIR_IN;
request.bmRequestType_bit.type = TUSB_REQ_TYPE_VENDOR;
request.bmRequestType_bit.recipient = TUSB_REQ_RCPT_DEVICE;
request.bRequest = XInput::kMsVendorRequest;
request.wIndex = XInput::kMsCompatIdIndex;
return request;
}
void test_auto_scratch_lifecycle() {
reset_harness(0);
adapter_host_probe_init(AdapterRequestedMode::kAuto);
require(adapter_host_probe_mode() == AdapterUsbMode::kSwitchProbe &&
watchdog_registers.scratch[0] == 0,
"auto cold boot did not select Switch probe");
reset_harness(kXInputBootMagic);
adapter_host_probe_init(AdapterRequestedMode::kAuto);
require(adapter_host_probe_mode() == AdapterUsbMode::kXInput &&
watchdog_registers.scratch[0] == 0,
"auto transition token did not select one XInput boot");
adapter_host_probe_init(AdapterRequestedMode::kAuto);
require(adapter_host_probe_mode() == AdapterUsbMode::kSwitchProbe,
"consumed scratch token caused a reboot loop");
reset_harness(0xa5a55a5au);
adapter_host_probe_init(AdapterRequestedMode::kAuto);
require(adapter_host_probe_mode() == AdapterUsbMode::kSwitchProbe &&
watchdog_registers.scratch[0] == 0,
"stale non-token scratch was not consumed safely");
}
void test_manual_modes_bypass_and_consume_probe_state() {
tusb_control_request_t request = microsoft_request();
reset_harness(kXInputBootMagic);
adapter_host_probe_init(AdapterRequestedMode::kSwitch);
require(adapter_host_probe_mode() == AdapterUsbMode::kSwitch &&
watchdog_registers.scratch[0] == 0,
"manual Switch honored stale auto scratch");
adapter_host_probe_note_string_descriptor(0xee);
require(!adapter_host_probe_vendor_control(
0, CONTROL_STAGE_SETUP, &request) &&
control_count == 0 && alarm_count == 0,
"manual Switch entered the host probe path");
reset_harness(0xdeadbeefu);
adapter_host_probe_init(AdapterRequestedMode::kXInput);
require(adapter_host_probe_mode() == AdapterUsbMode::kXInput &&
watchdog_registers.scratch[0] == 0,
"manual XInput honored stale scratch");
require(adapter_host_probe_vendor_control(
0, CONTROL_STAGE_SETUP, &request) &&
control_count == 1 &&
control_length == sizeof(XInput::kMsCompatIdDescriptor) &&
std::memcmp(control_buffer, XInput::kMsCompatIdDescriptor,
control_length) == 0 &&
alarm_count == 0,
"manual XInput did not serve XInput descriptors without probing");
reset_harness(kXInputBootMagic);
adapter_host_probe_init(AdapterRequestedMode::kDInput);
require(adapter_host_probe_mode() == AdapterUsbMode::kSwitch &&
watchdog_registers.scratch[0] == 0 && alarm_count == 0,
"unavailable DInput was selected or treated as Auto");
}
void test_probe_transition_resets_before_watchdog() {
reset_harness(0);
adapter_host_probe_init(AdapterRequestedMode::kAuto);
adapter_host_probe_note_string_descriptor(0xee);
tusb_control_request_t request = microsoft_request();
require(adapter_host_probe_vendor_control(
0, CONTROL_STAGE_SETUP, &request) &&
control_length ==
sizeof(XInput::kProbeMsCompatIdDescriptor) &&
std::memcmp(control_buffer,
XInput::kProbeMsCompatIdDescriptor,
control_length) == 0 &&
alarm_count == 1 && pending_alarm_delay_ms == 100 &&
reboot_count == 0,
"auto probe did not queue the exact delayed transition");
pending_alarm(1, pending_alarm_user_data);
require(reset_count == 1 && reboot_count == 1 &&
reset_sequence < reboot_sequence &&
watchdog_registers.scratch[0] == kXInputBootMagic,
"probe reboot did not reset synthetic state before scratch reset");
adapter_host_probe_init(AdapterRequestedMode::kAuto);
require(adapter_host_probe_mode() == AdapterUsbMode::kXInput &&
watchdog_registers.scratch[0] == 0,
"probe transition was not consumed on the next boot");
}
} // namespace
watchdog_hw_t* watchdog_hw = &watchdog_registers;
absolute_time_t get_absolute_time() { return now_ms; }
uint64_t to_ms_since_boot(absolute_time_t time) { return time; }
alarm_id_t add_alarm_in_ms(int64_t delay_ms, alarm_callback_t callback,
void* user_data, bool) {
++alarm_count;
pending_alarm_delay_ms = delay_ms;
pending_alarm = callback;
pending_alarm_user_data = user_data;
return alarm_count;
}
bool tud_control_xfer(uint8_t, const tusb_control_request_t*, void* buffer,
uint16_t length) {
++control_count;
control_buffer = buffer;
control_length = length;
return control_result;
}
void controller_profile_runtime_reset() {
++reset_count;
reset_sequence = ++call_sequence;
}
void watchdog_reboot(uint32_t, uint32_t, uint32_t) {
++reboot_count;
reboot_sequence = ++call_sequence;
}
int main() {
test_auto_scratch_lifecycle();
test_manual_modes_bypass_and_consume_probe_state();
test_probe_transition_resets_before_watchdog();
return 0;
}

View file

@ -0,0 +1,612 @@
#include "adapter_mode_controller.h"
#include <algorithm>
#include <cstdint>
#include <cstdlib>
#include <iostream>
#include <vector>
#include "adapter_configuration.h"
#include "adapter_reboot.h"
#include "bluepad32_input_backend.h"
#include "configuration_service.h"
#include "controller_profile.h"
namespace {
enum class Call : uint8_t {
kPreload,
kSnapshot,
kProbeInit,
kOutputInit,
kTinyUsbInit,
kSetMode,
kQueryMode,
kRebootReady,
kRuntimeReset,
kFeedback,
kReserveRecovery,
kClearPairings,
kPairingSnapshot,
kWatchdogReboot,
};
std::vector<Call> calls;
AdapterRequestedMode stored_mode = AdapterRequestedMode::kAuto;
AdapterRequestedMode probed_requested_mode = AdapterRequestedMode::kAuto;
AdapterUsbMode probed_active_mode = AdapterUsbMode::kSwitchProbe;
AdapterUsbMode initialized_output_mode = AdapterUsbMode::kSwitchProbe;
std::vector<ConfigurationTransactionStatus> set_results;
size_t next_set_result = 0;
std::vector<uint32_t> set_transaction_ids;
std::vector<AdapterRequestedMode> set_modes;
bool query_matches = true;
ConfigurationTransactionStatus query_status =
ConfigurationTransactionStatus::kPending;
uint32_t last_query_transaction_id = 0;
uint32_t last_reboot_ready_transaction_id = 0;
bool reboot_ready = true;
std::vector<bool> reboot_ready_results;
size_t next_reboot_ready_result = 0;
Bluepad32PairingSnapshotStatus pairing_status =
Bluepad32PairingSnapshotStatus::kPending;
uint32_t pairing_generation = 0;
uint32_t pairing_completed_clear_token = 0;
uint32_t clear_pairings_result_token = 1;
uint8_t feedback_slot = 0;
uint32_t feedback_generation = 0;
uint8_t feedback_pulses = 0;
ControllerProfileConfirmationPolicy feedback_policy =
ControllerProfileConfirmationPolicy::kNone;
int reboot_count = 0;
int runtime_reset_count = 0;
int clear_pairings_count = 0;
bool recovery_reserved = false;
bool abandoned_host_receive = false;
bool abandoned_host_receive_canceled = false;
void require(bool condition, const char* message) {
if (!condition) {
std::cerr << "FAIL: " << message << '\n';
std::exit(1);
}
}
void reset_harness(AdapterRequestedMode requested_mode =
AdapterRequestedMode::kAuto) {
calls.clear();
stored_mode = requested_mode;
probed_requested_mode = AdapterRequestedMode::kAuto;
probed_active_mode = AdapterUsbMode::kSwitchProbe;
initialized_output_mode = AdapterUsbMode::kSwitchProbe;
set_results.clear();
next_set_result = 0;
set_transaction_ids.clear();
set_modes.clear();
query_matches = true;
query_status = ConfigurationTransactionStatus::kPending;
last_query_transaction_id = 0;
last_reboot_ready_transaction_id = 0;
reboot_ready = true;
reboot_ready_results.clear();
next_reboot_ready_result = 0;
pairing_status = Bluepad32PairingSnapshotStatus::kPending;
pairing_generation = 0;
pairing_completed_clear_token = 0;
clear_pairings_result_token = 1;
feedback_slot = 0;
feedback_generation = 0;
feedback_pulses = 0;
feedback_policy = ControllerProfileConfirmationPolicy::kNone;
reboot_count = 0;
runtime_reset_count = 0;
clear_pairings_count = 0;
recovery_reserved = false;
abandoned_host_receive = false;
abandoned_host_receive_canceled = false;
adapter_mode_controller_initialize_usb();
calls.clear();
}
ControllerState held_state() {
ControllerState state{};
state.button_left_shoulder = true;
state.button_right_shoulder = true;
state.button_select = true;
state.button_start = true;
state.button_system = true;
return state;
}
uint16_t sample(uint8_t slot, uint32_t generation, uint16_t mask,
uint32_t now_ms, ControllerState* state) {
adapter_mode_controller_process_input(slot, true, generation, &mask,
now_ms, state);
return mask;
}
void begin_hold(uint8_t slot, uint32_t generation, uint32_t start_ms) {
ControllerState state = held_state();
sample(slot, generation, ADAPTER_MODE_CHORD_BUTTON_MASK, start_ms,
&state);
}
void finish_hold(uint8_t slot, uint32_t generation, uint32_t start_ms) {
ControllerState state = held_state();
sample(slot, generation, ADAPTER_MODE_CHORD_BUTTON_MASK,
start_ms + ADAPTER_MODE_CHORD_HOLD_MS, &state);
}
void test_pre_tusb_ordering_and_configured_selection() {
calls.clear();
stored_mode = AdapterRequestedMode::kXInput;
probed_active_mode = AdapterUsbMode::kXInput;
adapter_mode_controller_initialize_usb();
require(calls == std::vector<Call>({
Call::kPreload, Call::kSnapshot,
Call::kProbeInit, Call::kOutputInit,
Call::kTinyUsbInit}),
"persistent mode was not consumed before output init and tusb");
require(probed_requested_mode == AdapterRequestedMode::kXInput &&
initialized_output_mode == AdapterUsbMode::kXInput &&
adapter_mode_controller_requested_mode() ==
AdapterRequestedMode::kXInput,
"preloaded configured mode did not reach the frozen driver");
}
void test_mode_availability_has_one_stable_value() {
const AdapterModeAvailability& first =
adapter_usb_mode_availability();
const AdapterModeAvailability& second =
adapter_usb_mode_availability();
require(&first == &second && first.switch_mode &&
first.xinput_mode && !first.dinput_mode &&
!first.mac_mode,
"mode availability was not the shared implemented-mode value");
}
void test_exact_hold_release_wrap_and_consumption() {
reset_harness();
ControllerState state = held_state();
state.button_south = true;
const uint16_t consumed_mask = sample(
0, 7, ADAPTER_MODE_CHORD_BUTTON_MASK, 100, &state);
require(consumed_mask == 0 &&
!state.button_left_shoulder &&
!state.button_right_shoulder && !state.button_select &&
!state.button_start && !state.button_system &&
state.button_south,
"held raw mode chord was not consumed before profile processing");
state = held_state();
sample(0, 7, ADAPTER_MODE_CHORD_BUTTON_MASK, 3099, &state);
adapter_mode_controller_task(3099);
require(set_transaction_ids.empty(),
"mode chord fired before exactly three seconds");
state = {};
sample(0, 7, 0, 3100, &state);
begin_hold(0, 7, 4000);
finish_hold(0, 7, 4000);
set_results = {ConfigurationTransactionStatus::kBusy};
adapter_mode_controller_task(7000);
require(set_transaction_ids.size() == 1,
"release did not rearm a full three-second hold");
reset_harness();
constexpr uint32_t kWrapStart = UINT32_MAX - 999u;
begin_hold(0, 9, kWrapStart);
ControllerState wrap_state = held_state();
sample(0, 9, ADAPTER_MODE_CHORD_BUTTON_MASK, 1999, &wrap_state);
adapter_mode_controller_task(1999);
require(set_transaction_ids.empty(),
"wrap-safe hold fired one millisecond early");
wrap_state = held_state();
sample(0, 9, ADAPTER_MODE_CHORD_BUTTON_MASK, 2000, &wrap_state);
set_results = {ConfigurationTransactionStatus::kBusy};
adapter_mode_controller_task(2000);
require(set_transaction_ids.size() == 1,
"three-second hold failed across uint32 wrap");
}
AdapterRequestedMode triggered_target(AdapterRequestedMode initial) {
reset_harness(initial);
begin_hold(0, 1, 0);
finish_hold(0, 1, 0);
set_results = {ConfigurationTransactionStatus::kBusy};
adapter_mode_controller_task(ADAPTER_MODE_CHORD_HOLD_MS);
require(set_modes.size() == 1, "cycle did not submit a mode request");
return set_modes[0];
}
void test_cycle_and_slot_isolation() {
require(triggered_target(AdapterRequestedMode::kAuto) ==
AdapterRequestedMode::kSwitch,
"Auto did not cycle to Switch");
require(triggered_target(AdapterRequestedMode::kSwitch) ==
AdapterRequestedMode::kXInput,
"Switch did not cycle to XInput");
require(triggered_target(AdapterRequestedMode::kXInput) ==
AdapterRequestedMode::kAuto,
"XInput did not cycle to Auto");
require(triggered_target(AdapterRequestedMode::kDInput) ==
AdapterRequestedMode::kAuto,
"unimplemented configured value entered the chord cycle");
reset_harness();
begin_hold(0, 10, 0);
begin_hold(1, 20, 1000);
ControllerState released{};
sample(0, 10, 0, 2999, &released);
finish_hold(1, 20, 1000);
set_results = {ConfigurationTransactionStatus::kBusy};
adapter_mode_controller_task(4000);
require(set_transaction_ids.size() == 1,
"one slot's release reset another slot's hold");
reset_harness();
begin_hold(2, 30, 0);
ControllerState state = held_state();
sample(2, 31, ADAPTER_MODE_CHORD_BUTTON_MASK, 3000, &state);
adapter_mode_controller_task(3000);
require(set_transaction_ids.empty(),
"connection generation change inherited a prior hold");
uint16_t inactive_mask = ADAPTER_MODE_CHORD_BUTTON_MASK;
adapter_mode_controller_process_input(
2, false, 31, &inactive_mask, 6000, &state);
sample(2, 31, ADAPTER_MODE_CHORD_BUTTON_MASK, 6001, &state);
adapter_mode_controller_task(6001);
require(set_transaction_ids.empty(),
"inactive slot retained mode chord state");
}
void test_busy_retry_and_one_shot() {
reset_harness();
begin_hold(0, 1, 0);
finish_hold(0, 1, 0);
set_results = {
ConfigurationTransactionStatus::kBusy,
ConfigurationTransactionStatus::kBusy,
ConfigurationTransactionStatus::kPending,
};
adapter_mode_controller_task(3000);
adapter_mode_controller_task(3001);
adapter_mode_controller_task(3002);
require(set_transaction_ids.size() == 3 &&
set_transaction_ids[0] == set_transaction_ids[1] &&
set_transaction_ids[1] == set_transaction_ids[2] &&
(set_transaction_ids[0] & 0x80000000u) != 0,
"busy retry did not preserve one high-bit transaction ID");
query_status = ConfigurationTransactionStatus::kStorageError;
adapter_mode_controller_task(3003);
require(last_query_transaction_id == set_transaction_ids[0],
"commit polling lost internal transaction correlation");
ControllerState state = held_state();
sample(0, 1, ADAPTER_MODE_CHORD_BUTTON_MASK, 9000, &state);
adapter_mode_controller_task(9000);
require(set_transaction_ids.size() == 3 && reboot_count == 0,
"continuous hold retriggered after a failed transaction");
state = {};
sample(0, 1, 0, 9001, &state);
begin_hold(0, 1, 10000);
finish_hold(0, 1, 10000);
set_results.push_back(ConfigurationTransactionStatus::kStorageError);
adapter_mode_controller_task(13000);
require(set_transaction_ids.size() == 4 &&
set_transaction_ids[3] != set_transaction_ids[0],
"release did not create exactly one new internal transaction");
}
void test_commit_feedback_then_reboot() {
reset_harness(AdapterRequestedMode::kAuto);
begin_hold(3, 44, 0);
finish_hold(3, 44, 0);
set_results = {ConfigurationTransactionStatus::kPending};
adapter_mode_controller_task(3000);
query_status = ConfigurationTransactionStatus::kCommitted;
adapter_mode_controller_task(3001);
require(runtime_reset_count == 1 && feedback_slot == 3 &&
feedback_generation == 44 && feedback_pulses == 2 &&
feedback_policy ==
ControllerProfileConfirmationPolicy::kRumbleAndLed &&
reboot_count == 0 &&
calls[calls.size() - 2] == Call::kRuntimeReset &&
calls.back() == Call::kFeedback,
"commit did not cancel synthetic state then acknowledge Switch");
adapter_mode_controller_task(3375);
require(reboot_count == 0,
"reboot occurred before bounded mode feedback completed");
adapter_mode_controller_task(3376);
require(reboot_count == 1 && runtime_reset_count == 2 &&
calls[calls.size() - 2] == Call::kRuntimeReset &&
calls.back() == Call::kWatchdogReboot,
"committed mode did not reset synthetic state at watchdog reboot");
adapter_mode_controller_task(4000);
require(reboot_count == 1 && runtime_reset_count == 2,
"scheduled watchdog reboot looped");
}
void test_configuration_failure_and_correlated_reboot() {
reset_harness();
begin_hold(0, 1, 0);
finish_hold(0, 1, 0);
set_results = {ConfigurationTransactionStatus::kStorageError};
adapter_mode_controller_task(3000);
require(runtime_reset_count == 0 && feedback_pulses == 0 &&
reboot_count == 0,
"failed internal mode write rebooted or acknowledged");
reset_harness();
reboot_ready = false;
require(!adapter_reboot_for_mode_transaction(12) &&
runtime_reset_count == 0 && reboot_count == 0,
"non-current host mode transaction rebooted");
reboot_ready = true;
require(!adapter_reboot_for_mode_transaction(0x80000001u) &&
reboot_count == 0,
"internal transaction used the management reboot API");
require(adapter_reboot_for_mode_transaction(12) &&
last_reboot_ready_transaction_id == 12 &&
runtime_reset_count == 1 && reboot_count == 1 &&
calls[calls.size() - 2] == Call::kRuntimeReset &&
calls.back() == Call::kWatchdogReboot,
"current committed host transaction did not reset then reboot");
require(adapter_reboot_for_mode_transaction(12) &&
reboot_count == 1 && runtime_reset_count == 1,
"duplicate normal reboot was not latched");
}
void test_recovery_auto_wins_host_mode_interleaving() {
reset_harness(AdapterRequestedMode::kXInput);
begin_hold(0, 5, 0);
finish_hold(0, 5, 0);
calls.clear();
pairing_status = Bluepad32PairingSnapshotStatus::kReady;
pairing_generation = 41;
clear_pairings_result_token = 17;
abandoned_host_receive = true;
adapter_mode_controller_begin_recovery();
adapter_mode_controller_begin_recovery();
require(clear_pairings_count == 1 && recovery_reserved &&
abandoned_host_receive_canceled &&
!abandoned_host_receive && calls.size() >= 2 &&
calls[0] == Call::kReserveRecovery &&
calls[1] == Call::kClearPairings,
"BOOTSEL recovery must reserve and cancel abandoned host receive "
"before requesting one pairing clear");
require(!adapter_reboot_for_mode_transaction(77) &&
last_reboot_ready_transaction_id == 0 &&
reboot_count == 0,
"host mode traffic rebooted during active recovery");
set_results = {
ConfigurationTransactionStatus::kBusy,
ConfigurationTransactionStatus::kPending,
ConfigurationTransactionStatus::kPending,
};
adapter_mode_controller_task(0);
pairing_status = Bluepad32PairingSnapshotStatus::kReady;
pairing_generation = 42;
adapter_mode_controller_task(1);
require(set_modes.empty(),
"unrelated pairing refresh advanced recovery");
pairing_completed_clear_token = clear_pairings_result_token - 1;
adapter_mode_controller_task(2);
require(set_modes.empty(),
"pre-request pairing clear completion advanced recovery");
pairing_status = Bluepad32PairingSnapshotStatus::kPending;
adapter_mode_controller_task(3);
require(set_modes.empty(),
"pending recovery pairing clear advanced recovery");
// Recovery clear N completed, then host clear N+1 was accepted and
// completed before recovery observed N. N+1 must still acknowledge N.
pairing_status = Bluepad32PairingSnapshotStatus::kReady;
pairing_completed_clear_token = clear_pairings_result_token + 1;
adapter_mode_controller_task(4);
adapter_mode_controller_task(5);
require(set_modes.size() == 2 &&
set_modes[0] == AdapterRequestedMode::kAuto &&
set_modes[1] == AdapterRequestedMode::kAuto &&
set_transaction_ids[0] == set_transaction_ids[1],
"later clear completion did not busy-retry recovery Auto");
const size_t first_set_call = static_cast<size_t>(
std::find(calls.begin(), calls.end(), Call::kSetMode) -
calls.begin());
const size_t clear_call = static_cast<size_t>(
std::find(calls.begin(), calls.end(), Call::kClearPairings) -
calls.begin());
require(clear_call < first_set_call,
"recovery submitted Auto before requesting bond clear");
// Model a stale mode commit result. Reboot readiness rejects it, so
// recovery submits a new correlated Auto transaction.
reboot_ready_results = {false, true};
query_status = ConfigurationTransactionStatus::kCommitted;
adapter_mode_controller_task(6);
require(reboot_count == 0 && set_transaction_ids.size() == 2,
"superseded recovery Auto rebooted");
adapter_mode_controller_task(7);
require(set_transaction_ids.size() == 3 &&
set_modes[2] == AdapterRequestedMode::kAuto &&
set_transaction_ids[2] != set_transaction_ids[1] &&
reboot_count == 0,
"superseded Auto did not yield to a fresh recovery transaction");
adapter_mode_controller_task(8);
require(last_reboot_ready_transaction_id ==
set_transaction_ids[2] &&
adapter_mode_controller_requested_mode() ==
AdapterRequestedMode::kAuto &&
feedback_pulses == 0 && runtime_reset_count == 1 &&
reboot_count == 1 &&
calls[calls.size() - 2] == Call::kRuntimeReset &&
calls.back() == Call::kWatchdogReboot,
"recovery did not make Auto final and reboot immediately");
}
void test_failed_recovery_never_reboots() {
reset_harness(AdapterRequestedMode::kXInput);
clear_pairings_result_token = 0;
abandoned_host_receive = true;
adapter_mode_controller_begin_recovery();
pairing_status = Bluepad32PairingSnapshotStatus::kReady;
pairing_completed_clear_token = 1;
adapter_mode_controller_task(0);
require(recovery_reserved && abandoned_host_receive_canceled &&
clear_pairings_count == 1 && set_modes.empty() &&
!adapter_reboot_for_mode_transaction(91) &&
last_reboot_ready_transaction_id == 0 &&
runtime_reset_count == 0 && reboot_count == 0,
"failed pairing clear released recovery ownership or rebooted");
reset_harness(AdapterRequestedMode::kXInput);
clear_pairings_result_token = UINT32_MAX;
adapter_mode_controller_begin_recovery();
pairing_status = Bluepad32PairingSnapshotStatus::kReady;
pairing_completed_clear_token = 1;
set_results = {ConfigurationTransactionStatus::kStorageError};
adapter_mode_controller_task(1);
adapter_mode_controller_task(2);
require(set_modes.size() == 1 &&
set_modes[0] == AdapterRequestedMode::kAuto &&
!adapter_reboot_for_mode_transaction(92) &&
last_reboot_ready_transaction_id == 0 &&
runtime_reset_count == 0 && feedback_pulses == 0 &&
reboot_count == 0,
"wrapped clear completion did not start exactly one recovery Auto, "
"or failed recovery acknowledged, released ownership, or retried");
}
} // namespace
void configuration_service_initialize_pre_usb() {
calls.push_back(Call::kPreload);
}
void configuration_service_snapshot(ConfigurationServiceSnapshot* output) {
calls.push_back(Call::kSnapshot);
*output = {};
output->state = ConfigurationServiceState::kReady;
output->configuration.requested_mode = stored_mode;
}
void configuration_service_reserve_for_recovery() {
calls.push_back(Call::kReserveRecovery);
recovery_reserved = true;
if (abandoned_host_receive) {
abandoned_host_receive = false;
abandoned_host_receive_canceled = true;
}
}
ConfigurationTransactionStatus configuration_service_set_mode_internal(
uint32_t transaction_id, AdapterRequestedMode requested_mode,
const AdapterModeAvailability& availability) {
require(!recovery_reserved ||
requested_mode == AdapterRequestedMode::kAuto,
"recovery reservation admitted a non-Auto internal mode");
calls.push_back(Call::kSetMode);
require(availability.switch_mode && availability.xinput_mode &&
!availability.dinput_mode && !availability.mac_mode,
"mode controller advertised unavailable drivers");
set_transaction_ids.push_back(transaction_id);
set_modes.push_back(requested_mode);
if (next_set_result >= set_results.size()) {
return ConfigurationTransactionStatus::kBusy;
}
return set_results[next_set_result++];
}
bool configuration_service_mode_transaction_status(
uint32_t transaction_id, ConfigurationTransactionStatus* output) {
calls.push_back(Call::kQueryMode);
last_query_transaction_id = transaction_id;
if (!query_matches) {
return false;
}
*output = query_status;
return true;
}
bool configuration_service_mode_transaction_reboot_ready(
uint32_t transaction_id) {
calls.push_back(Call::kRebootReady);
last_reboot_ready_transaction_id = transaction_id;
if (next_reboot_ready_result < reboot_ready_results.size()) {
return reboot_ready_results[next_reboot_ready_result++];
}
return reboot_ready;
}
void adapter_host_probe_init(AdapterRequestedMode requested_mode) {
calls.push_back(Call::kProbeInit);
probed_requested_mode = requested_mode;
}
AdapterUsbMode adapter_host_probe_mode() { return probed_active_mode; }
void tusb_init() {
calls.push_back(Call::kTinyUsbInit);
}
void usb_output_driver_init(AdapterUsbMode mode) {
calls.push_back(Call::kOutputInit);
initialized_output_mode = mode;
}
void controller_profile_runtime_reset() {
calls.push_back(Call::kRuntimeReset);
++runtime_reset_count;
}
void bluepad32_input_backend_queue_profile_feedback(
uint8_t slot, uint32_t connection_generation,
uint8_t active_profile_number,
ControllerProfileConfirmationPolicy policy) {
calls.push_back(Call::kFeedback);
feedback_slot = slot;
feedback_generation = connection_generation;
feedback_pulses = active_profile_number;
feedback_policy = policy;
}
uint32_t bluepad32_input_backend_clear_pairings() {
calls.push_back(Call::kClearPairings);
require(recovery_reserved,
"pairing clear was requested before recovery reservation");
++clear_pairings_count;
pairing_status = Bluepad32PairingSnapshotStatus::kPending;
return clear_pairings_result_token;
}
void bluepad32_input_backend_pairing_snapshot(
Bluepad32PairingSnapshot* output) {
calls.push_back(Call::kPairingSnapshot);
*output = {};
output->status = pairing_status;
output->generation = pairing_generation;
output->completed_clear_pairings_token =
pairing_completed_clear_token;
}
void watchdog_reboot(uint32_t, uint32_t, uint32_t) {
calls.push_back(Call::kWatchdogReboot);
++reboot_count;
}
int main() {
test_pre_tusb_ordering_and_configured_selection();
test_mode_availability_has_one_stable_value();
test_exact_hold_release_wrap_and_consumption();
test_cycle_and_slot_isolation();
test_busy_retry_and_one_shot();
test_commit_feedback_then_reboot();
test_configuration_failure_and_correlated_reboot();
test_recovery_auto_wins_host_mode_interleaving();
test_failed_recovery_never_reboots();
return 0;
}

View file

@ -48,6 +48,12 @@ int device_disconnect_calls = 0;
uni_hid_device_t* last_disconnected_device = nullptr;
uni_hid_device_t* lookup_devices[8]{};
size_t lookup_device_count = 0;
uint32_t expected_pending_clear_token = 0;
uint32_t repeated_in_progress_clear_token = 0;
bool repeat_clear_during_disconnect = false;
void require_clear_completion_pending();
void require_clear_snapshot_published();
void request_repeated_clear_during_disconnect();
gap_connection_type_t gap_connection_types[256]{};
struct CoreStopped {};
@ -129,8 +135,9 @@ uni_hid_device_t* uni_hid_device_get_instance_for_connection_handle(
}
return nullptr;
}
void uni_hid_device_disconnect(uni_hid_device_t* device) {
require_clear_completion_pending();
request_repeated_clear_during_disconnect();
++device_disconnect_calls;
last_disconnected_device = device;
}
@ -165,6 +172,8 @@ void uni_bt_le_scan_stop() {
}
void uni_bt_start_scanning_and_autoconnect_unsafe() {
require_clear_completion_pending();
require_clear_snapshot_published();
uni_bt_bredr_scan_start();
uni_bt_le_scan_start();
}
@ -174,6 +183,7 @@ void uni_bt_stop_scanning_unsafe() {
uni_bt_le_scan_stop();
}
void uni_bt_del_keys_unsafe() {
require_clear_completion_pending();
++delete_key_calls;
classic_bond_count = 0;
ble_bond_count = 0;
@ -434,6 +444,30 @@ uint32_t btstack_run_loop_get_time_ms() {
#include "../controller_identity.cpp"
#include "../bluepad32_input_backend.cpp"
namespace {
void require_clear_completion_pending() {
if (expected_pending_clear_token != 0) {
require(!bluepad32_input_backend_clear_pairings_completed(
g_pairing_snapshot, expected_pending_clear_token),
"pairing clear token completed before Core1 work finished");
}
}
void require_clear_snapshot_published() {
if (expected_pending_clear_token != 0) {
require(g_pairing_snapshot.status ==
Bluepad32PairingSnapshotStatus::kReady &&
g_pairing_snapshot.record_count == 0,
"pairing clear policy ran before empty snapshot publication");
}
}
void request_repeated_clear_during_disconnect() {
if (repeat_clear_during_disconnect) {
repeat_clear_during_disconnect = false;
repeated_in_progress_clear_token =
bluepad32_input_backend_clear_pairings();
}
}
} // namespace
ControllerIdentity observed_profile_identities[8]{};
size_t observed_profile_identity_count = 0;
void configuration_service_prepare() {}
@ -470,7 +504,7 @@ void configuration_service_snapshot(ConfigurationServiceSnapshot* output) {
output->configuration.pairing_window_seconds =
ADAPTER_PAIRING_WINDOW_SECONDS_DEFAULT;
}
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
#ifdef SWITCH_PICO_USB_OUTPUT_MODES
AdapterUsbMode test_adapter_mode = AdapterUsbMode::kXInput;
AdapterUsbMode adapter_host_probe_mode() {
return test_adapter_mode;
@ -1811,7 +1845,7 @@ void test_stateful_host_rumble_restore() {
generations[slot] = snapshot.connection_generation;
}
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
#ifdef SWITCH_PICO_USB_OUTPUT_MODES
test_adapter_mode = AdapterUsbMode::kXInput;
const ControllerRumbleOutput desired[kSlotCount] = {
{0x11, 0x21}, {0x12, 0x22}, {0x13, 0x23}, {0x14, 0x24}};
@ -2075,7 +2109,7 @@ void test_host_rumble_mode_duration() {
require(platform_on_device_ready(&controller) == UNI_ERROR_SUCCESS,
"rumble-mode controller did not become ready");
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
#ifdef SWITCH_PICO_USB_OUTPUT_MODES
test_adapter_mode = AdapterUsbMode::kSwitchProbe;
#endif
bluepad32_input_backend_queue_rumble(
@ -2085,7 +2119,7 @@ void test_host_rumble_mode_duration() {
kSwitchHostRumbleDurationMs,
"Switch mode did not use bounded host rumble");
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
#ifdef SWITCH_PICO_USB_OUTPUT_MODES
test_adapter_mode = AdapterUsbMode::kXInput;
bluepad32_input_backend_queue_rumble(
0, ControllerRumbleOutput{102, 103});
@ -2111,8 +2145,12 @@ void test_clear_pairings() {
g_pairing_snapshot.records[1].transport ==
Bluepad32PairingTransport::kBle,
"initial pairing snapshot must enumerate Classic and BLE bonds");
require(!bluepad32_input_backend_clear_pairings_completed(
g_pairing_snapshot, 0),
"zero pairing-clear token must never be a valid completion query");
const uint32_t snapshot_generation =
g_pairing_snapshot.generation;
g_next_clear_pairings_request_token = UINT32_MAX;
uni_hid_device_t devices[2] = {device(0), device(1)};
for (uni_hid_device_t& controller : devices) {
require(platform_on_device_ready(&controller) == UNI_ERROR_SUCCESS,
@ -2121,20 +2159,41 @@ void test_clear_pairings() {
bluepad32_input_backend_queue_rumble(
0, ControllerRumbleOutput{100, 101});
bluepad32_input_backend_clear_pairings();
require(g_clear_pairings_requested && delete_key_calls == 0 &&
device_disconnect_calls == 0,
"Core0 pairing reset request must wait for Core1");
const uint32_t clear_token =
bluepad32_input_backend_clear_pairings();
const uint32_t repeated_token =
bluepad32_input_backend_clear_pairings();
expected_pending_clear_token = clear_token;
repeat_clear_during_disconnect = true;
g_connection_policy_state =
ConnectionPolicyState::Uninitialized;
require(clear_token == UINT32_MAX &&
repeated_token == clear_token &&
g_clear_pairings_requested_token == clear_token &&
g_pairing_snapshot.completed_clear_pairings_token == 0 &&
!bluepad32_input_backend_clear_pairings_completed(
g_pairing_snapshot, clear_token) &&
delete_key_calls == 0 && device_disconnect_calls == 0,
"Core0 repeated pending clear calls must share one nonzero token, "
"remain incomplete, and defer the operation to Core1");
process_rumble_timer(&g_rumble_timer);
require(repeated_in_progress_clear_token == clear_token,
"clear repeated during Core1 work did not share its token");
require(delete_key_calls == 1 && device_disconnect_calls == 2,
"pairing reset must delete bonds and disconnect every session");
require(g_pairing_snapshot.status ==
Bluepad32PairingSnapshotStatus::kReady &&
g_pairing_snapshot.record_count == 0 &&
g_pairing_snapshot.generation ==
snapshot_generation + 1,
"pairing reset must publish an empty refreshed snapshot");
snapshot_generation + 1 &&
g_pairing_snapshot.completed_clear_pairings_token ==
clear_token &&
bluepad32_input_backend_clear_pairings_completed(
g_pairing_snapshot, clear_token),
"pairing reset must publish its completion token with an empty "
"refreshed snapshot after policy update");
expected_pending_clear_token = 0;
for (const BackendSlot& slot : g_slots) {
require(slot.device == nullptr && !slot.active &&
!slot.rumble_pending && !slot.feedback_pending &&
@ -2156,8 +2215,56 @@ void test_clear_pairings() {
require(!observed_status_led_on,
"pairing reset confirmation must use the rapid blink pattern");
process_rumble_timer(&g_rumble_timer);
require(delete_key_calls == 1 && device_disconnect_calls == 2,
require(delete_key_calls == 1 && device_disconnect_calls == 2 &&
g_pairing_snapshot.completed_clear_pairings_token ==
clear_token,
"pairing reset request must execute only once");
const uint32_t completed_generation =
g_pairing_snapshot.generation;
bluepad32_input_backend_request_pairing_snapshot();
require(g_pairing_snapshot.status ==
Bluepad32PairingSnapshotStatus::kPending &&
bluepad32_input_backend_clear_pairings_completed(
g_pairing_snapshot, clear_token),
"unrelated refresh revoked an already completed clear");
process_rumble_timer(&g_rumble_timer);
require(g_pairing_snapshot.generation ==
completed_generation + 1 &&
g_pairing_snapshot.completed_clear_pairings_token ==
clear_token &&
bluepad32_input_backend_clear_pairings_completed(
g_pairing_snapshot, clear_token),
"ordinary pairing refresh fabricated or revoked clear completion");
const uint32_t refreshed_generation =
g_pairing_snapshot.generation;
const uint32_t wrapped_token =
bluepad32_input_backend_clear_pairings();
require(wrapped_token == 1 &&
g_pairing_snapshot.status ==
Bluepad32PairingSnapshotStatus::kPending &&
g_pairing_snapshot.completed_clear_pairings_token ==
clear_token &&
!bluepad32_input_backend_clear_pairings_completed(
g_pairing_snapshot, wrapped_token) &&
bluepad32_input_backend_clear_pairings_completed(
g_pairing_snapshot, clear_token),
"UINT32_MAX-to-1 wrap reused the prior completion or revoked it");
expected_pending_clear_token = wrapped_token;
process_rumble_timer(&g_rumble_timer);
expected_pending_clear_token = 0;
require(delete_key_calls == 2 && device_disconnect_calls == 2 &&
g_pairing_snapshot.generation ==
refreshed_generation + 1 &&
g_pairing_snapshot.completed_clear_pairings_token ==
wrapped_token &&
bluepad32_input_backend_clear_pairings_completed(
g_pairing_snapshot, wrapped_token) &&
bluepad32_input_backend_clear_pairings_completed(
g_pairing_snapshot, clear_token),
"wrapped clear completion did not acknowledge both itself and "
"the earlier pre-wrap request exactly once");
}
void test_configuration_timer_rearms_before_storage_work() {

View file

@ -0,0 +1,520 @@
#include "adapter_configuration.h"
#include "configuration_service.h"
#include "configuration_storage.h"
#include "pico_configuration_storage.h"
#include <cstdlib>
#include <cstring>
#include <iostream>
namespace {
constexpr size_t kSectorSize = 4096;
constexpr size_t kPageSize = 256;
struct FakeFlash {
uint8_t bytes[CONFIGURATION_STORAGE_COPY_COUNT][kSectorSize];
int read_count = 0;
int erase_count = 0;
int program_count = 0;
bool fail_program = false;
FakeFlash() { memset(bytes, 0xff, sizeof(bytes)); }
};
FakeFlash g_flash;
bool g_reserve_recovery_during_program = false;
void require(bool condition, const char* message) {
if (!condition) {
std::cerr << message << '\n';
std::exit(1);
}
}
bool fake_read(void* context, uint8_t copy, size_t offset,
uint8_t* output, size_t size) {
auto* flash = static_cast<FakeFlash*>(context);
if (copy >= CONFIGURATION_STORAGE_COPY_COUNT ||
offset + size > kSectorSize) {
return false;
}
memcpy(output, &flash->bytes[copy][offset], size);
++flash->read_count;
return true;
}
bool fake_erase(void* context, uint8_t copy) {
auto* flash = static_cast<FakeFlash*>(context);
if (copy >= CONFIGURATION_STORAGE_COPY_COUNT) {
return false;
}
memset(flash->bytes[copy], 0xff, kSectorSize);
++flash->erase_count;
return true;
}
bool fake_program(void* context, uint8_t copy, size_t offset,
const uint8_t* data, size_t size) {
auto* flash = static_cast<FakeFlash*>(context);
if (copy >= CONFIGURATION_STORAGE_COPY_COUNT || size != kPageSize ||
offset + size > kSectorSize || flash->fail_program) {
return false;
}
if (g_reserve_recovery_during_program) {
g_reserve_recovery_during_program = false;
configuration_service_reserve_for_recovery();
}
for (size_t index = 0; index < size; ++index) {
flash->bytes[copy][offset + index] &= data[index];
}
++flash->program_count;
return true;
}
ConfigurationStorageIo fake_io() {
return {
&g_flash,
kSectorSize,
kPageSize,
fake_read,
fake_erase,
fake_program,
};
}
void require_mode_status(uint32_t transaction_id,
ConfigurationTransactionStatus expected,
const char* message) {
ConfigurationTransactionStatus actual =
ConfigurationTransactionStatus::kIdle;
require(configuration_service_mode_transaction_status(transaction_id,
&actual) &&
actual == expected,
message);
}
} // namespace
ConfigurationStorageIo pico_configuration_storage_io() {
return fake_io();
}
namespace {
void seed_legacy_configuration() {
ConfigurationStorage seed;
const uint8_t legacy[] = {90, 0, 0, 0};
require(seed.initialize(fake_io()), "legacy seed storage init failed");
require(seed.commit(ADAPTER_CONFIGURATION_LEGACY_SCHEMA_VERSION,
legacy, sizeof(legacy)) ==
ConfigurationStorageResult::kOk,
"legacy seed commit failed");
}
void test_service_lifecycle_and_mutations() {
seed_legacy_configuration();
const int programs_after_seed = g_flash.program_count;
const int erases_after_seed = g_flash.erase_count;
configuration_service_prepare();
configuration_service_initialize_pre_usb();
ConfigurationServiceSnapshot snapshot{};
configuration_service_snapshot(&snapshot);
require(snapshot.state == ConfigurationServiceState::kReady &&
snapshot.configuration.pairing_window_seconds == 90 &&
snapshot.configuration.requested_mode ==
AdapterRequestedMode::kAuto,
"Core 0 did not decode and publish the v1 configuration");
require(g_flash.program_count == programs_after_seed &&
g_flash.erase_count == erases_after_seed,
"pre-USB initialization wrote flash");
const AdapterModeAvailability implemented{};
require(configuration_service_set_mode(
1, AdapterRequestedMode::kSwitch, implemented) ==
ConfigurationTransactionStatus::kBusy,
"host mutation was accepted before storage-core adoption");
const int reads_before_adoption = g_flash.read_count;
configuration_service_initialize_on_storage_core();
require(g_flash.read_count == reads_before_adoption &&
g_flash.program_count == programs_after_seed &&
g_flash.erase_count == erases_after_seed,
"Core 1 reread or wrote instead of adopting Core 0 state");
require(configuration_service_set_mode(
2, AdapterRequestedMode::kSwitch, implemented) ==
ConfigurationTransactionStatus::kBusy,
"host mutation displaced the pending v1 migration");
configuration_service_task_on_storage_core(0);
configuration_service_snapshot(&snapshot);
require(snapshot.state == ConfigurationServiceState::kReady &&
snapshot.configuration.requested_mode ==
AdapterRequestedMode::kAuto &&
snapshot.transaction.status ==
ConfigurationTransactionStatus::kIdle,
"v1 migration changed configuration or host transaction state");
ConfigurationStorage after_migration;
require(after_migration.initialize(fake_io()) &&
after_migration.snapshot().valid &&
after_migration.snapshot().schema_version ==
ADAPTER_CONFIGURATION_SCHEMA_VERSION &&
after_migration.snapshot().payload_size ==
ADAPTER_CONFIGURATION_ENCODED_SIZE,
"power cycle did not observe the migrated v2 record");
AdapterConfiguration migrated{};
require(adapter_configuration_decode(
after_migration.snapshot().schema_version,
after_migration.snapshot().payload,
after_migration.snapshot().payload_size, &migrated) &&
migrated.pairing_window_seconds == 90 &&
migrated.requested_mode == AdapterRequestedMode::kAuto,
"migrated v2 bytes did not preserve v1 configuration");
require(configuration_service_set_mode(
10, AdapterRequestedMode::kSwitch, implemented) ==
ConfigurationTransactionStatus::kPending,
"host mode transaction was not queued");
require_mode_status(10, ConfigurationTransactionStatus::kPending,
"host mode correlation was not pending");
require(configuration_service_set_mode_internal(
0x80000001u, AdapterRequestedMode::kXInput, implemented) ==
ConfigurationTransactionStatus::kBusy,
"internal mode displaced a pending host mode");
configuration_service_task_on_storage_core(999);
require_mode_status(10, ConfigurationTransactionStatus::kPending,
"write-rate guard committed host mode too early");
configuration_service_task_on_storage_core(1000);
require_mode_status(10, ConfigurationTransactionStatus::kCommitted,
"host mode transaction did not commit");
configuration_service_snapshot(&snapshot);
require(snapshot.configuration.pairing_window_seconds == 90 &&
snapshot.configuration.requested_mode ==
AdapterRequestedMode::kSwitch &&
snapshot.transaction.transaction_id == 10 &&
snapshot.transaction.status ==
ConfigurationTransactionStatus::kCommitted,
"mode-only commit changed pairing state or host status");
const int programs_before_rejections = g_flash.program_count;
require(configuration_service_set_mode(
11, AdapterRequestedMode::kDInput, implemented) ==
ConfigurationTransactionStatus::kUnsupportedSchema &&
configuration_service_set_mode(
11, static_cast<AdapterRequestedMode>(5), implemented) ==
ConfigurationTransactionStatus::kMalformed &&
configuration_service_set_mode(
0x80000011u, AdapterRequestedMode::kXInput,
implemented) ==
ConfigurationTransactionStatus::kMalformed &&
configuration_service_begin(
0x80000012u, ADAPTER_CONFIGURATION_SCHEMA_VERSION,
ADAPTER_CONFIGURATION_ENCODED_SIZE, 0) ==
ConfigurationTransactionStatus::kMalformed &&
configuration_service_reset(0x80000013u) ==
ConfigurationTransactionStatus::kMalformed,
"unavailable, invalid, or internal-range host request was accepted");
configuration_service_snapshot(&snapshot);
require(snapshot.configuration.requested_mode ==
AdapterRequestedMode::kSwitch &&
snapshot.transaction.transaction_id == 10 &&
g_flash.program_count == programs_before_rejections,
"rejected mode request corrupted persisted or host-visible state");
require(configuration_service_set_mode(
12, AdapterRequestedMode::kSwitch, implemented) ==
ConfigurationTransactionStatus::kPending,
"unchanged host mode was not serialized");
const int programs_before_unchanged = g_flash.program_count;
configuration_service_task_on_storage_core(2000);
require_mode_status(12, ConfigurationTransactionStatus::kUnchanged,
"unchanged host mode did not terminate unchanged");
require(g_flash.program_count == programs_before_unchanged,
"unchanged mode consumed a flash program");
require(configuration_service_mode_transaction_reboot_ready(12),
"current unchanged host mode could not authorize reboot");
constexpr uint32_t kInternalXInput = 0x80000020u;
require(configuration_service_set_mode_internal(
kInternalXInput, AdapterRequestedMode::kXInput,
implemented) == ConfigurationTransactionStatus::kPending &&
configuration_service_set_mode_internal(
kInternalXInput, AdapterRequestedMode::kXInput,
implemented) == ConfigurationTransactionStatus::kPending,
"accepted internal mode was not idempotently pending");
require(!configuration_service_mode_transaction_reboot_ready(12),
"accepted internal mode did not immediately supersede host reboot");
configuration_service_snapshot(&snapshot);
const ConfigurationTransactionSnapshot preserved_host =
snapshot.transaction;
require(configuration_service_begin(
20, ADAPTER_CONFIGURATION_SCHEMA_VERSION,
ADAPTER_CONFIGURATION_ENCODED_SIZE, 0) ==
ConfigurationTransactionStatus::kBusy &&
configuration_service_reset(21) ==
ConfigurationTransactionStatus::kBusy,
"host mutation was not blocked by pending internal mode");
configuration_service_task_on_storage_core(2000);
require_mode_status(12, ConfigurationTransactionStatus::kUnchanged,
"internal commit overwrote retained host status");
require_mode_status(kInternalXInput,
ConfigurationTransactionStatus::kCommitted,
"internal mode transaction did not commit");
require(!configuration_service_mode_transaction_reboot_ready(12) &&
configuration_service_mode_transaction_reboot_ready(
kInternalXInput),
"stale host generation authorized reboot after internal commit");
configuration_service_snapshot(&snapshot);
require(snapshot.configuration.pairing_window_seconds == 90 &&
snapshot.configuration.requested_mode ==
AdapterRequestedMode::kXInput &&
snapshot.transaction.transaction_id ==
preserved_host.transaction_id &&
snapshot.transaction.status == preserved_host.status &&
snapshot.mode_transaction.transaction_id == kInternalXInput &&
snapshot.mode_transaction.stored_generation ==
snapshot.generation,
"internal mode changed host status or was not published");
require(configuration_service_set_mode(
13, AdapterRequestedMode::kAuto, implemented) ==
ConfigurationTransactionStatus::kPending,
"host mode did not serialize after internal mode");
require(!configuration_service_mode_transaction_reboot_ready(
kInternalXInput),
"accepted host mode did not immediately supersede internal reboot");
configuration_service_task_on_storage_core(3000);
require_mode_status(kInternalXInput,
ConfigurationTransactionStatus::kCommitted,
"host commit overwrote retained internal status");
require_mode_status(13, ConfigurationTransactionStatus::kCommitted,
"interleaved host mode transaction did not commit");
require(!configuration_service_mode_transaction_reboot_ready(
kInternalXInput) &&
configuration_service_mode_transaction_reboot_ready(13),
"stale internal generation authorized reboot after host commit");
configuration_service_snapshot(&snapshot);
require(snapshot.configuration.requested_mode ==
AdapterRequestedMode::kAuto &&
snapshot.mode_transaction.transaction_id == 13 &&
!snapshot.mode_transaction.internal &&
snapshot.mode_transaction.stored_generation ==
snapshot.generation,
"latest host mode transaction was not published");
AdapterConfiguration generic = snapshot.configuration;
generic.pairing_window_seconds = 120;
uint8_t generic_payload[ADAPTER_CONFIGURATION_ENCODED_SIZE]{};
require(adapter_configuration_encode(generic, generic_payload,
sizeof(generic_payload)),
"generic test configuration did not encode");
const uint32_t generic_crc =
configuration_crc32(generic_payload, sizeof(generic_payload));
require(configuration_service_begin(
30, ADAPTER_CONFIGURATION_SCHEMA_VERSION,
sizeof(generic_payload), generic_crc) ==
ConfigurationTransactionStatus::kReceiving,
"generic host transaction did not begin");
require(!configuration_service_mode_transaction_reboot_ready(13),
"accepted generic configuration did not invalidate mode reboot");
require(configuration_service_set_mode_internal(
0x80000021u, AdapterRequestedMode::kAuto, implemented) ==
ConfigurationTransactionStatus::kBusy,
"internal mode displaced receiving host configuration");
require(configuration_service_append(
30, 0, generic_payload, sizeof(generic_payload)) ==
ConfigurationTransactionStatus::kReceiving &&
configuration_service_commit(30) ==
ConfigurationTransactionStatus::kPending,
"generic host configuration did not reach pending");
configuration_service_task_on_storage_core(4000);
configuration_service_snapshot(&snapshot);
require(snapshot.configuration.pairing_window_seconds == 120 &&
snapshot.configuration.requested_mode ==
AdapterRequestedMode::kAuto &&
snapshot.transaction.transaction_id == 30 &&
snapshot.transaction.status ==
ConfigurationTransactionStatus::kCommitted,
"generic host commit did not preserve requested mode");
require_mode_status(13, ConfigurationTransactionStatus::kCommitted,
"generic commit erased retained host mode status");
require(!configuration_service_mode_transaction_reboot_ready(13),
"host mode authorized reboot for a stale stored generation");
ConfigurationTransactionStatus ignored{};
require(!configuration_service_mode_transaction_status(30, &ignored),
"generic transaction was misidentified as a mode transaction");
constexpr uint32_t kFailingInternal = 0x80000022u;
require(configuration_service_set_mode_internal(
kFailingInternal, AdapterRequestedMode::kXInput,
implemented) == ConfigurationTransactionStatus::kPending,
"failing internal mode was not queued");
g_flash.fail_program = true;
configuration_service_task_on_storage_core(5000);
g_flash.fail_program = false;
require_mode_status(kFailingInternal,
ConfigurationTransactionStatus::kStorageError,
"failed internal mode did not report storage error");
configuration_service_snapshot(&snapshot);
require(snapshot.state == ConfigurationServiceState::kReady &&
snapshot.configuration.pairing_window_seconds == 120 &&
snapshot.configuration.requested_mode ==
AdapterRequestedMode::kAuto &&
snapshot.transaction.transaction_id == 30 &&
snapshot.transaction.status ==
ConfigurationTransactionStatus::kCommitted &&
!configuration_service_mode_transaction_reboot_ready(
kFailingInternal),
"failed internal mode did not roll back coherently");
const uint32_t reset_generation_before =
configuration_service_reset_generation();
require(configuration_service_reset(40) ==
ConfigurationTransactionStatus::kPending &&
configuration_service_reset_generation() ==
reset_generation_before + 1,
"configuration reset acceptance semantics changed");
configuration_service_snapshot(&snapshot);
require(snapshot.configuration.pairing_window_seconds == 120 &&
snapshot.configuration.requested_mode ==
AdapterRequestedMode::kAuto,
"reset published defaults before durable commit");
configuration_service_task_on_storage_core(6000);
configuration_service_snapshot(&snapshot);
require(snapshot.configuration.pairing_window_seconds ==
ADAPTER_PAIRING_WINDOW_SECONDS_DEFAULT &&
snapshot.configuration.requested_mode ==
AdapterRequestedMode::kAuto &&
snapshot.transaction.transaction_id == 40 &&
snapshot.transaction.status ==
ConfigurationTransactionStatus::kCommitted &&
snapshot.reset_generation == reset_generation_before + 1,
"configuration reset did not durably restore v2 defaults");
constexpr uint32_t kCapturedHostMode = 50;
require(configuration_service_set_mode(
kCapturedHostMode, AdapterRequestedMode::kSwitch,
implemented) == ConfigurationTransactionStatus::kPending,
"host mode was not queued for the storage completion race");
g_reserve_recovery_during_program = true;
configuration_service_task_on_storage_core(7000);
require_mode_status(
kCapturedHostMode, ConfigurationTransactionStatus::kBusy,
"in-flight host mode completion revived its canceled status");
configuration_service_snapshot(&snapshot);
require(snapshot.transaction.transaction_id == kCapturedHostMode &&
snapshot.transaction.status ==
ConfigurationTransactionStatus::kBusy &&
snapshot.configuration.requested_mode ==
AdapterRequestedMode::kSwitch &&
!configuration_service_mode_transaction_reboot_ready(
kCapturedHostMode),
"captured host completion escaped recovery ownership");
const uint8_t blocked_payload = 0;
require(configuration_service_append(
kCapturedHostMode, 0, &blocked_payload,
sizeof(blocked_payload)) ==
ConfigurationTransactionStatus::kBusy &&
configuration_service_commit(kCapturedHostMode) ==
ConfigurationTransactionStatus::kBusy &&
configuration_service_begin(
51, ADAPTER_CONFIGURATION_SCHEMA_VERSION,
ADAPTER_CONFIGURATION_ENCODED_SIZE, 0) ==
ConfigurationTransactionStatus::kBusy &&
configuration_service_reset(52) ==
ConfigurationTransactionStatus::kBusy &&
configuration_service_set_mode(
53, AdapterRequestedMode::kSwitch, implemented) ==
ConfigurationTransactionStatus::kBusy,
"host mutation escaped recovery reservation");
require(configuration_service_set_mode_internal(
0x80000030u, AdapterRequestedMode::kSwitch,
implemented) == ConfigurationTransactionStatus::kBusy,
"non-Auto internal mode escaped recovery reservation");
constexpr uint32_t kRecoveryAuto = 0x80000031u;
require(configuration_service_set_mode_internal(
kRecoveryAuto, AdapterRequestedMode::kAuto,
implemented) == ConfigurationTransactionStatus::kPending &&
!configuration_service_mode_transaction_reboot_ready(
kRecoveryAuto),
"recovery Auto was blocked or authorized before persistence");
configuration_service_task_on_storage_core(8000);
require_mode_status(kRecoveryAuto,
ConfigurationTransactionStatus::kCommitted,
"recovery Auto did not overwrite canceled host mode");
require(configuration_service_mode_transaction_reboot_ready(
kRecoveryAuto),
"latest recovery Auto did not authorize reboot");
constexpr uint32_t kLatestRecoveryAuto = 0x80000032u;
require(configuration_service_set_mode_internal(
kLatestRecoveryAuto, AdapterRequestedMode::kAuto,
implemented) == ConfigurationTransactionStatus::kPending &&
!configuration_service_mode_transaction_reboot_ready(
kRecoveryAuto),
"newer recovery Auto did not immediately supersede reboot");
configuration_service_task_on_storage_core(9000);
require(!configuration_service_mode_transaction_reboot_ready(
kRecoveryAuto) &&
configuration_service_mode_transaction_reboot_ready(
kLatestRecoveryAuto),
"recovery reboot authorization did not remain latest-only");
}
void test_abandoned_host_receive_does_not_block_recovery() {
configuration_service_prepare();
configuration_service_initialize_pre_usb();
configuration_service_initialize_on_storage_core();
ConfigurationServiceSnapshot snapshot{};
configuration_service_snapshot(&snapshot);
AdapterConfiguration abandoned = snapshot.configuration;
abandoned.pairing_window_seconds = 180;
uint8_t payload[ADAPTER_CONFIGURATION_ENCODED_SIZE]{};
require(adapter_configuration_encode(abandoned, payload,
sizeof(payload)),
"abandoned recovery-race configuration did not encode");
const uint32_t crc = configuration_crc32(payload, sizeof(payload));
require(configuration_service_begin(
60, ADAPTER_CONFIGURATION_SCHEMA_VERSION,
sizeof(payload), crc) ==
ConfigurationTransactionStatus::kReceiving,
"abandoned host configuration did not begin receiving");
configuration_service_reserve_for_recovery();
configuration_service_reserve_for_recovery();
configuration_service_snapshot(&snapshot);
require(snapshot.transaction.transaction_id == 60 &&
snapshot.transaction.status ==
ConfigurationTransactionStatus::kBusy,
"recovery reservation did not terminally cancel host receive");
const AdapterModeAvailability implemented{};
constexpr uint32_t kRecoveryAuto = 0x80000040u;
require(configuration_service_set_mode_internal(
kRecoveryAuto, AdapterRequestedMode::kAuto,
implemented) == ConfigurationTransactionStatus::kPending,
"abandoned host receive blocked recovery Auto");
configuration_service_task_on_storage_core(0);
require_mode_status(kRecoveryAuto,
ConfigurationTransactionStatus::kCommitted,
"recovery Auto did not commit after abandoned receive");
require(configuration_service_mode_transaction_reboot_ready(
kRecoveryAuto),
"recovery Auto did not become latest reboot authority");
}
} // namespace
int main(int argc, char**) {
if (argc > 1) {
test_abandoned_host_receive_does_not_block_recovery();
} else {
test_service_lifecycle_and_mutations();
}
return 0;
}

View file

@ -78,23 +78,124 @@ ConfigurationStorageIo fake_io(FakeFlash* flash) {
void test_schema_encoding() {
AdapterConfiguration configuration{};
configuration.pairing_window_seconds = 90;
configuration.requested_mode = AdapterRequestedMode::kXInput;
uint8_t payload[ADAPTER_CONFIGURATION_ENCODED_SIZE]{};
require(adapter_configuration_encode(configuration, payload,
sizeof(payload)),
"valid configuration did not encode");
"valid v2 configuration did not encode");
const uint8_t expected[] = {
90,
0,
static_cast<uint8_t>(AdapterRequestedMode::kXInput),
0,
0,
0,
0,
0,
};
require(memcmp(payload, expected, sizeof(expected)) == 0,
"v2 configuration bytes are not canonical");
AdapterConfiguration decoded{};
require(adapter_configuration_decode(payload, sizeof(payload),
&decoded) &&
decoded.pairing_window_seconds == 90,
"configuration did not round trip");
payload[2] = 1;
require(!adapter_configuration_decode(payload, sizeof(payload),
&decoded),
"nonzero reserved configuration byte was accepted");
require(adapter_configuration_decode(
ADAPTER_CONFIGURATION_SCHEMA_VERSION, payload,
sizeof(payload), &decoded) &&
decoded.pairing_window_seconds == 90 &&
decoded.requested_mode == AdapterRequestedMode::kXInput,
"v2 configuration did not round trip");
const AdapterRequestedMode valid_modes[] = {
AdapterRequestedMode::kAuto,
AdapterRequestedMode::kSwitch,
AdapterRequestedMode::kXInput,
AdapterRequestedMode::kDInput,
AdapterRequestedMode::kMac,
};
for (AdapterRequestedMode mode : valid_modes) {
configuration.requested_mode = mode;
require(adapter_configuration_encode(configuration, payload,
sizeof(payload)) &&
adapter_configuration_decode(
ADAPTER_CONFIGURATION_SCHEMA_VERSION, payload,
sizeof(payload), &decoded) &&
decoded.requested_mode == mode,
"valid requested mode did not round trip");
}
AdapterModeAvailability availability{};
require(!adapter_requested_mode_available(
AdapterRequestedMode::kDInput, availability) &&
!adapter_requested_mode_available(
AdapterRequestedMode::kMac, availability),
"future modes were available by default");
availability.dinput_mode = true;
availability.mac_mode = true;
require(adapter_requested_mode_available(
AdapterRequestedMode::kDInput, availability) &&
adapter_requested_mode_available(
AdapterRequestedMode::kMac, availability),
"availability API could not enable future modes");
const uint8_t legacy[] = {120, 0, 0, 0};
require(adapter_configuration_decode(
ADAPTER_CONFIGURATION_LEGACY_SCHEMA_VERSION, legacy,
sizeof(legacy), &decoded) &&
decoded.pairing_window_seconds == 120 &&
decoded.requested_mode == AdapterRequestedMode::kAuto,
"v1 configuration did not migrate to auto");
uint8_t malformed_legacy[sizeof(legacy)];
memcpy(malformed_legacy, legacy, sizeof(legacy));
malformed_legacy[3] = 1;
require(!adapter_configuration_decode(
ADAPTER_CONFIGURATION_LEGACY_SCHEMA_VERSION,
malformed_legacy, sizeof(malformed_legacy), &decoded),
"v1 nonzero reserved byte was accepted");
require(!adapter_configuration_decode(
ADAPTER_CONFIGURATION_LEGACY_SCHEMA_VERSION, legacy,
sizeof(legacy) - 1, &decoded),
"v1 record with wrong size was accepted");
for (size_t index = 3; index < sizeof(payload); ++index) {
uint8_t malformed[sizeof(payload)];
memcpy(malformed, payload, sizeof(payload));
malformed[index] = 1;
require(!adapter_configuration_decode(
ADAPTER_CONFIGURATION_SCHEMA_VERSION, malformed,
sizeof(malformed), &decoded),
"v2 nonzero reserved byte was accepted");
}
uint8_t invalid_mode[sizeof(payload)];
memcpy(invalid_mode, payload, sizeof(payload));
invalid_mode[2] = 5;
require(!adapter_configuration_decode(
ADAPTER_CONFIGURATION_SCHEMA_VERSION, invalid_mode,
sizeof(invalid_mode), &decoded),
"out-of-range requested mode was accepted");
require(!adapter_configuration_decode(
ADAPTER_CONFIGURATION_SCHEMA_VERSION, payload,
sizeof(payload) - 1, &decoded),
"short v2 record was accepted");
uint8_t oversized[ADAPTER_CONFIGURATION_ENCODED_SIZE + 1]{};
memcpy(oversized, payload, sizeof(payload));
require(!adapter_configuration_decode(
ADAPTER_CONFIGURATION_SCHEMA_VERSION, oversized,
sizeof(oversized), &decoded),
"oversized v2 record was accepted");
configuration.pairing_window_seconds = 9;
require(!adapter_configuration_encode(configuration, payload,
sizeof(payload)),
"out-of-range pairing window was accepted");
configuration.pairing_window_seconds = 90;
configuration.requested_mode =
static_cast<AdapterRequestedMode>(5);
require(!adapter_configuration_encode(configuration, payload,
sizeof(payload)),
"out-of-range requested mode encoded");
configuration.requested_mode = AdapterRequestedMode::kAuto;
require(!adapter_configuration_encode(configuration, oversized,
sizeof(oversized)),
"v2 encoder accepted a noncanonical output size");
}
void test_two_copy_recovery() {
@ -189,7 +290,8 @@ void test_transaction_validation() {
"replacement transaction did not begin");
require(transaction.append(8, 0, payload, 2) ==
ConfigurationTransactionStatus::kReceiving &&
transaction.append(8, 2, &payload[2], 2) ==
transaction.append(8, 2, &payload[2],
sizeof(payload) - 2) ==
ConfigurationTransactionStatus::kReceiving,
"ordered chunks were rejected");
require(transaction.finish(8) ==

View file

@ -0,0 +1,9 @@
#pragma once
#include <stdint.h>
struct watchdog_hw_t {
uint32_t scratch[8];
};
extern watchdog_hw_t* watchdog_hw;

View file

@ -0,0 +1,5 @@
#pragma once
#include <stdint.h>
void watchdog_reboot(uint32_t pc, uint32_t sp, uint32_t delay_ms);

View file

@ -0,0 +1,12 @@
#pragma once
#include <stdint.h>
typedef int32_t alarm_id_t;
typedef uint64_t absolute_time_t;
typedef int64_t (*alarm_callback_t)(alarm_id_t alarm_id, void* user_data);
absolute_time_t get_absolute_time();
uint64_t to_ms_since_boot(absolute_time_t time);
alarm_id_t add_alarm_in_ms(int64_t delay_ms, alarm_callback_t callback,
void* user_data, bool fire_if_past);

View file

@ -0,0 +1,33 @@
#pragma once
#include <stdbool.h>
#include <stdint.h>
enum {
CONTROL_STAGE_SETUP = 0,
CONTROL_STAGE_DATA = 1,
CONTROL_STAGE_ACK = 2,
TUSB_REQ_RCPT_DEVICE = 0,
TUSB_DIR_OUT = 0,
TUSB_REQ_TYPE_VENDOR = 2,
TUSB_DIR_IN = 1,
};
struct tusb_request_type_bits_t {
uint8_t recipient;
uint8_t type;
uint8_t direction;
};
struct tusb_control_request_t {
tusb_request_type_bits_t bmRequestType_bit;
uint8_t bRequest;
uint16_t wValue;
uint16_t wIndex;
uint16_t wLength;
};
void tusb_init();
bool tud_control_xfer(uint8_t rhport,
const tusb_control_request_t* request,
void* buffer, uint16_t length);

View file

@ -0,0 +1,32 @@
from __future__ import annotations
import shutil
import subprocess
from pathlib import Path
def test_adapter_host_probe_native(tmp_path: Path) -> None:
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 / "adapter_host_probe_test"
subprocess.run(
[
compiler,
"-std=c++17",
"-Wall",
"-Wextra",
"-Werror",
"-pedantic",
"-DSWITCH_PICO_HID_INSTANCE_COUNT=4",
f"-I{root / 'tests' / 'mode_native_stubs'}",
f"-I{root}",
str(root / "adapter_host_probe.cpp"),
str(root / "tests" / "adapter_host_probe_test.cpp"),
"-o",
str(executable),
],
check=True,
cwd=root,
)
subprocess.run([str(executable)], check=True, cwd=root)

View file

@ -0,0 +1,32 @@
from __future__ import annotations
import shutil
import subprocess
from pathlib import Path
def test_adapter_mode_controller_native(tmp_path: Path) -> None:
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 / "adapter_mode_controller_test"
subprocess.run(
[
compiler,
"-std=c++17",
"-Wall",
"-Wextra",
"-Werror",
"-pedantic",
"-DSWITCH_PICO_HID_INSTANCE_COUNT=4",
f"-I{root / 'tests' / 'mode_native_stubs'}",
f"-I{root}",
str(root / "adapter_mode_controller.cpp"),
str(root / "tests" / "adapter_mode_controller_test.cpp"),
"-o",
str(executable),
],
check=True,
cwd=root,
)
subprocess.run([str(executable)], check=True, cwd=root)

View file

@ -23,7 +23,7 @@ def test_bluepad32_backend_lifecycle_native(tmp_path: Path) -> None:
"-DSWITCH_PICO_HID_INSTANCE_COUNT=4",
]
if adapter_feasibility:
command.append("-DSWITCH_PICO_ADAPTER_FEASIBILITY=1")
command.append("-DSWITCH_PICO_USB_OUTPUT_MODES=1")
command.extend(
[
f"-I{root / 'tests' / 'bluepad32_native_stubs'}",

View file

@ -36,15 +36,23 @@ def make_response(
class FakeDevice:
bus = 1
address = 7
port_numbers = (1,)
def __init__(self) -> None:
self.configuration = struct.pack("<Hxx", 60)
self.configuration = struct.pack(
"<HB5x", 60, config_manager.REQUESTED_MODE_AUTO
)
self.configuration_generation = 3
self.active_mode = config_manager.ACTIVE_MODE_SWITCH_PROBE
self.transaction_id = 0
self.transaction_payload = bytearray()
self.transaction_expected_size = 0
self.transaction_expected_crc = 0
self.transaction_status = config_manager.STATUS_OK
self.pending_requested_mode: int | None = None
self.mode_pending_reads = 0
self.fail_mode_status: int | None = None
self.reboot_transaction_ids: list[int] = []
self.records = [
(
config_manager.TRANSPORT_CLASSIC,
@ -93,6 +101,7 @@ class FakeDevice:
self.fail_profile_commit_status: int | None = None
self.bad_profile_response_crc = False
self.requests: list[int] = []
self.out_requests: list[tuple[int, bytes, bytes]] = []
self.profile_chunk_sizes: list[int] = []
self.pending_profile_mutation: tuple[int, bytes, int] | None = None
self.profile_transaction_pending_reads = 0
@ -198,7 +207,19 @@ class FakeDevice:
if bm_request_type == 0xC0:
if request == config_manager.OP_INFO:
return make_response(
request, bytes([0, 2, 0, 2, 0, 0, 0, 2])
request,
bytes(
[
0,
2,
0,
2,
self.active_mode,
0,
0,
2,
]
),
)
if request == config_manager.OP_CONFIGURATION_READ:
return make_response(
@ -208,6 +229,29 @@ class FakeDevice:
generation=self.configuration_generation,
)
if request == config_manager.OP_TRANSACTION_STATUS:
if (
self.transaction_status == config_manager.STATUS_PENDING
and self.pending_requested_mode is not None
):
if self.mode_pending_reads:
self.mode_pending_reads -= 1
else:
self.transaction_status = (
self.fail_mode_status
if self.fail_mode_status is not None
else config_manager.STATUS_OK
)
if self.transaction_status == config_manager.STATUS_OK:
pairing_window = struct.unpack_from(
"<H", self.configuration
)[0]
self.configuration = struct.pack(
"<HB5x",
pairing_window,
self.pending_requested_mode,
)
self.configuration_generation += 1
self.pending_requested_mode = None
return make_response(
request,
self._transaction_payload(),
@ -270,6 +314,7 @@ class FakeDevice:
assert struct.unpack_from("<I", encoded, 12)[0] == (
zlib.crc32(payload) & 0xFFFFFFFF
)
self.out_requests.append((request, payload, encoded))
if request == config_manager.OP_CONFIGURATION_BEGIN:
(
self.transaction_id,
@ -277,6 +322,9 @@ class FakeDevice:
self.transaction_expected_size,
self.transaction_expected_crc,
) = struct.unpack("<IHHI", payload)
assert 0 < self.transaction_id <= (
config_manager.HOST_TRANSACTION_ID_MASK
)
self.transaction_payload = bytearray()
self.transaction_status = config_manager.STATUS_PENDING
elif request == config_manager.OP_CONFIGURATION_CHUNK:
@ -297,7 +345,12 @@ class FakeDevice:
self.transaction_status = config_manager.STATUS_OK
elif request == config_manager.OP_CONFIGURATION_RESET:
self.transaction_id = struct.unpack("<I", payload)[0]
self.configuration = struct.pack("<Hxx", 60)
assert 0 < self.transaction_id <= (
config_manager.HOST_TRANSACTION_ID_MASK
)
self.configuration = struct.pack(
"<HB5x", 60, config_manager.REQUESTED_MODE_AUTO
)
self.configuration_generation += 1
self.transaction_payload = bytearray(self.configuration)
self.transaction_expected_size = len(self.configuration)
@ -305,6 +358,27 @@ class FakeDevice:
zlib.crc32(self.configuration) & 0xFFFFFFFF
)
self.transaction_status = config_manager.STATUS_OK
elif request == config_manager.OP_MODE_SET:
assert len(payload) == 5
self.transaction_id, requested_mode = struct.unpack("<IB", payload)
assert 0 < self.transaction_id <= 0x7FFFFFFF
assert requested_mode in (
config_manager.REQUESTED_MODE_AUTO,
config_manager.REQUESTED_MODE_SWITCH,
config_manager.REQUESTED_MODE_XINPUT,
)
self.transaction_payload = bytearray()
self.transaction_expected_size = 0
self.transaction_expected_crc = 0
self.transaction_status = config_manager.STATUS_PENDING
self.pending_requested_mode = requested_mode
self.mode_pending_reads = 1
elif request == config_manager.OP_REBOOT:
assert len(payload) == 4
reboot_transaction_id = struct.unpack("<I", payload)[0]
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_PAIRING_REFRESH:
self.pairing_generation += 1
elif request == config_manager.OP_PAIRING_CLEAR:
@ -465,16 +539,431 @@ def test_configuration_transaction_and_reset() -> None:
device = FakeDevice()
before = config_manager.read_configuration(device)
assert before.pairing_window_seconds == 60
assert before.requested_mode == config_manager.REQUESTED_MODE_AUTO
status = config_manager.write_configuration(
device,
config_manager.AdapterConfiguration(90, before.generation, before.crc),
config_manager.AdapterConfiguration(
90,
before.generation,
before.crc,
config_manager.REQUESTED_MODE_XINPUT,
),
1.0,
)
assert status.stored_generation == 4
assert config_manager.read_configuration(device).pairing_window_seconds == 90
stored = config_manager.read_configuration(device)
assert stored.pairing_window_seconds == 90
assert stored.requested_mode == config_manager.REQUESTED_MODE_XINPUT
assert device.configuration == struct.pack(
"<HB5x", 90, config_manager.REQUESTED_MODE_XINPUT
)
reset = config_manager.reset_configuration(device, 1.0)
assert reset.stored_generation == 5
assert config_manager.read_configuration(device).pairing_window_seconds == 60
reset_configuration = config_manager.read_configuration(device)
assert reset_configuration.pairing_window_seconds == 60
assert (
reset_configuration.requested_mode
== config_manager.REQUESTED_MODE_AUTO
)
def test_configuration_transaction_ids_stay_in_host_range(
monkeypatch: pytest.MonkeyPatch,
) -> None:
device = FakeDevice()
generated_values = iter((0xFFFFFFFF, 0x80000000, 0xFEDCBA98))
requested_bits: list[int] = []
def randbits(bits: int) -> int:
requested_bits.append(bits)
return next(generated_values)
monkeypatch.setattr(config_manager.secrets, "randbits", randbits)
monkeypatch.setattr(config_manager.time, "sleep", lambda _seconds: None)
before = config_manager.read_configuration(device)
config_manager.write_configuration(
device,
config_manager.AdapterConfiguration(
90,
before.generation,
before.crc,
config_manager.REQUESTED_MODE_AUTO,
),
1.0,
)
config_manager.reset_configuration(device, 1.0)
config_manager.set_mode(
device, config_manager.REQUESTED_MODE_SWITCH, 1.0
)
transaction_ids = [
struct.unpack_from("<I", payload)[0]
for operation, payload, _ in device.out_requests
if operation
in (
config_manager.OP_CONFIGURATION_BEGIN,
config_manager.OP_CONFIGURATION_RESET,
config_manager.OP_MODE_SET,
)
]
assert requested_bits == [31, 31, 31]
assert transaction_ids == [0x7FFFFFFF, 1, 0x7EDCBA98]
assert all(
transaction_id & ~config_manager.HOST_TRANSACTION_ID_MASK == 0
for transaction_id in transaction_ids
)
def test_mode_envelopes_and_host_side_validation(
monkeypatch: pytest.MonkeyPatch,
) -> None:
device = FakeDevice()
monkeypatch.setattr(config_manager.time, "sleep", lambda _seconds: None)
monkeypatch.setattr(
config_manager.secrets, "randbits", lambda _bits: 0x12345678
)
status = config_manager.set_mode(
device, config_manager.REQUESTED_MODE_XINPUT, 1.0
)
mode_payload = struct.pack(
"<IB", 0x12345678, config_manager.REQUESTED_MODE_XINPUT
)
operation, payload, encoded = device.out_requests[0]
assert operation == config_manager.OP_MODE_SET
assert payload == mode_payload
assert encoded == config_manager.encode_request(
config_manager.OP_MODE_SET, mode_payload
)
assert status.transaction_id == 0x12345678
config_manager.request_reboot(device, status.transaction_id)
operation, payload, encoded = device.out_requests[-1]
reboot_payload = struct.pack("<I", 0x12345678)
assert operation == config_manager.OP_REBOOT
assert payload == reboot_payload
assert encoded == config_manager.encode_request(
config_manager.OP_REBOOT, reboot_payload
)
for transaction_id in (0, 0x80000000, True):
with pytest.raises(config_manager.ConfigManagerError):
config_manager.request_reboot(device, transaction_id)
for mode in (
config_manager.REQUESTED_MODE_DINPUT,
config_manager.REQUESTED_MODE_MAC,
0xFF,
True,
):
with pytest.raises(
config_manager.ConfigManagerError, match="not available"
):
config_manager.set_mode(device, mode, 1.0)
@pytest.mark.parametrize(
("failure_status", "message"),
(
(7, "device busy"),
(8, "storage failure"),
),
)
def test_mode_set_propagates_busy_and_commit_errors_without_reboot(
monkeypatch: pytest.MonkeyPatch,
failure_status: int,
message: str,
) -> None:
device = FakeDevice()
device.fail_mode_status = failure_status
monkeypatch.setattr(config_manager.time, "sleep", lambda _seconds: None)
with pytest.raises(config_manager.ConfigManagerError, match=message):
config_manager.configure_mode(
device, config_manager.REQUESTED_MODE_SWITCH, 1.0
)
assert config_manager.OP_REBOOT not in device.requests
def test_mode_transaction_must_correlate_before_reboot(
monkeypatch: pytest.MonkeyPatch,
) -> None:
class WrongTransactionDevice(FakeDevice):
def _transaction_payload(self) -> bytes:
payload = bytearray(super()._transaction_payload())
struct.pack_into("<I", payload, 0, self.transaction_id + 1)
return bytes(payload)
device = WrongTransactionDevice()
monkeypatch.setattr(config_manager.time, "sleep", lambda _seconds: None)
with pytest.raises(
config_manager.ConfigManagerError,
match="different transaction",
):
config_manager.configure_mode(
device, config_manager.REQUESTED_MODE_SWITCH, 1.0
)
assert config_manager.OP_REBOOT not in device.requests
@pytest.mark.parametrize(
("requested_mode", "active_mode"),
(
(
config_manager.REQUESTED_MODE_AUTO,
config_manager.ACTIVE_MODE_SWITCH_PROBE,
),
(
config_manager.REQUESTED_MODE_AUTO,
config_manager.ACTIVE_MODE_XINPUT,
),
(
config_manager.REQUESTED_MODE_SWITCH,
config_manager.ACTIVE_MODE_SWITCH,
),
(
config_manager.REQUESTED_MODE_XINPUT,
config_manager.ACTIVE_MODE_XINPUT,
),
),
)
def test_mode_noop_accepts_only_mode_appropriate_active_state(
requested_mode: int, active_mode: int
) -> None:
device = FakeDevice()
device.configuration = struct.pack("<HB5x", 60, requested_mode)
device.active_mode = active_mode
same_device, changed = config_manager.configure_mode(
device, requested_mode, 1.0
)
assert same_device is device
assert not changed
assert device.out_requests == []
@pytest.mark.parametrize(
("requested_mode", "active_mode"),
(
(
config_manager.REQUESTED_MODE_AUTO,
config_manager.ACTIVE_MODE_SWITCH_PROBE,
),
(
config_manager.REQUESTED_MODE_AUTO,
config_manager.ACTIVE_MODE_XINPUT,
),
(
config_manager.REQUESTED_MODE_SWITCH,
config_manager.ACTIVE_MODE_SWITCH,
),
(
config_manager.REQUESTED_MODE_XINPUT,
config_manager.ACTIVE_MODE_XINPUT,
),
),
)
def test_mode_change_waits_for_disappearance_and_reenumeration(
monkeypatch: pytest.MonkeyPatch,
requested_mode: int,
active_mode: int,
) -> None:
previous = FakeDevice()
if requested_mode == config_manager.REQUESTED_MODE_AUTO:
previous.configuration = struct.pack(
"<HB5x", 60, config_manager.REQUESTED_MODE_SWITCH
)
previous.active_mode = config_manager.ACTIVE_MODE_SWITCH
reenumerated = FakeDevice()
reenumerated.address = 8
reenumerated.configuration = struct.pack("<HB5x", 60, requested_mode)
reenumerated.active_mode = active_mode
scans = iter(((previous,), (), (reenumerated,)))
monkeypatch.setattr(
config_manager,
"_candidate_devices",
lambda: next(scans, (reenumerated,)),
)
monkeypatch.setattr(config_manager.time, "sleep", lambda _seconds: None)
result, changed = config_manager.configure_mode(
previous, requested_mode, 1.0
)
assert result is reenumerated
assert changed
assert previous.reboot_transaction_ids == [previous.transaction_id]
assert [
operation
for operation, _, _ in previous.out_requests
if operation in (config_manager.OP_MODE_SET, config_manager.OP_REBOOT)
] == [config_manager.OP_MODE_SET, config_manager.OP_REBOOT]
def test_mode_reenumeration_tracks_same_port_among_adapters_on_one_bus(
monkeypatch: pytest.MonkeyPatch,
) -> None:
previous = FakeDevice()
previous.port_numbers = (2, 1)
other = FakeDevice()
other.address = 8
other.port_numbers = (2, 2)
replacement = FakeDevice()
replacement.address = 9
replacement.port_numbers = previous.port_numbers
replacement.configuration = struct.pack(
"<HB5x", 60, config_manager.REQUESTED_MODE_SWITCH
)
replacement.active_mode = config_manager.ACTIVE_MODE_SWITCH
scans = iter(
(
(previous, other),
(other,),
(other, replacement),
)
)
monkeypatch.setattr(
config_manager,
"_candidate_devices",
lambda: next(scans, (other, replacement)),
)
monkeypatch.setattr(config_manager.time, "sleep", lambda _seconds: None)
result, changed = config_manager.configure_mode(
previous, config_manager.REQUESTED_MODE_SWITCH, 1.0
)
assert result is replacement
assert changed
assert replacement.address != previous.address
assert replacement.port_numbers == previous.port_numbers
assert other.requests == []
def test_mode_reboot_fails_when_missing_topology_is_ambiguous(
monkeypatch: pytest.MonkeyPatch,
) -> None:
previous = FakeDevice()
previous.port_numbers = None
other = FakeDevice()
other.address = 8
other.port_numbers = None
monkeypatch.setattr(
config_manager,
"_candidate_devices",
lambda: (previous, other),
)
with pytest.raises(
config_manager.ConfigManagerError,
match="topology is unavailable.*multiple",
):
config_manager.configure_mode(
previous, config_manager.REQUESTED_MODE_SWITCH, 1.0
)
assert previous.out_requests == []
assert other.out_requests == []
@pytest.mark.parametrize(
("stored_mode", "active_mode", "message"),
(
(
config_manager.REQUESTED_MODE_SWITCH,
config_manager.ACTIVE_MODE_SWITCH_PROBE,
"activated",
),
(
config_manager.REQUESTED_MODE_AUTO,
config_manager.ACTIVE_MODE_SWITCH,
"was not stored",
),
),
)
def test_mode_verifies_requested_and_active_state_after_reenumeration(
monkeypatch: pytest.MonkeyPatch,
stored_mode: int,
active_mode: int,
message: str,
) -> None:
previous = FakeDevice()
reenumerated = FakeDevice()
reenumerated.address = 8
reenumerated.configuration = struct.pack("<HB5x", 60, stored_mode)
reenumerated.active_mode = active_mode
scans = iter(((previous,), (), (reenumerated,)))
monkeypatch.setattr(
config_manager,
"_candidate_devices",
lambda: next(scans, (reenumerated,)),
)
monkeypatch.setattr(config_manager.time, "sleep", lambda _seconds: None)
with pytest.raises(config_manager.ConfigManagerError, match=message):
config_manager.configure_mode(
previous, config_manager.REQUESTED_MODE_SWITCH, 1.0
)
def test_reenumeration_reports_missing_disappearance_and_return(
monkeypatch: pytest.MonkeyPatch,
) -> None:
device = FakeDevice()
monkeypatch.setattr(
config_manager, "_candidate_devices", lambda: (device,)
)
with pytest.raises(
config_manager.ConfigManagerError, match="did not disappear"
):
snapshot = config_manager._capture_reenumeration_snapshot(device)
config_manager._wait_for_reenumeration(snapshot, 0)
monkeypatch.setattr(config_manager, "_candidate_devices", lambda: ())
with pytest.raises(
config_manager.ConfigManagerError, match="did not re-enumerate"
):
snapshot = config_manager._capture_reenumeration_snapshot(device)
config_manager._wait_for_reenumeration(snapshot, 0)
def test_requested_and_active_mode_response_validation() -> None:
device = FakeDevice()
device.configuration = struct.pack(
"<HB5x", 60, config_manager.REQUESTED_MODE_DINPUT
)
assert (
config_manager.read_configuration(device).requested_mode
== config_manager.REQUESTED_MODE_DINPUT
)
assert (
config_manager.read_info(device).active_mode
== config_manager.ACTIVE_MODE_SWITCH_PROBE
)
device.configuration = struct.pack("<HB5x", 60, 0xFF)
with pytest.raises(
config_manager.ConfigManagerError, match="invalid stored requested"
):
config_manager.read_configuration(device)
device.configuration = (
struct.pack("<HB5x", 60, config_manager.REQUESTED_MODE_AUTO)[:-1]
+ b"\x01"
)
with pytest.raises(
config_manager.ConfigManagerError,
match="unsupported configuration object",
):
config_manager.read_configuration(device)
device.active_mode = 0xFF
with pytest.raises(
config_manager.ConfigManagerError, match="unknown active USB mode"
):
config_manager.read_info(device)
def test_identity_and_profile_binary_json_round_trip() -> None:
@ -711,6 +1200,7 @@ def test_profile_reset_and_activate_wait_for_correlated_transactions(
reset = config_manager.reset_profile(device, identity, 2, 1.0)
assert reset.transaction_id == device.profile_transaction_id == 1
assert reset.status == config_manager.STATUS_OK
assert reset.stored_generation == 8
@ -981,6 +1471,8 @@ def test_status_and_pairing_commands(
output = capsys.readouterr().out
assert "Firmware: 0.2.0" in output
assert "Pairing window: 60 seconds" in output
assert "Requested USB mode: auto" in output
assert "Active USB mode: Switch probe" in output
assert config_manager.main(["pairings", "list"]) == 0
output = capsys.readouterr().out
@ -993,6 +1485,95 @@ def test_status_and_pairing_commands(
assert capsys.readouterr().out == "Cleared 2 stored pairing(s).\n"
def test_config_cli_preserves_requested_mode(
monkeypatch: pytest.MonkeyPatch,
capsys: pytest.CaptureFixture[str],
) -> None:
device = FakeDevice()
device.configuration = struct.pack(
"<HB5x", 60, config_manager.REQUESTED_MODE_XINPUT
)
device.active_mode = config_manager.ACTIVE_MODE_XINPUT
monkeypatch.setattr(
config_manager, "_candidate_devices", lambda: (device,)
)
assert (
config_manager.main(
["config", "set", "--pairing-window-seconds", "90"]
)
== 0
)
assert struct.unpack("<HB5x", device.configuration) == (
90,
config_manager.REQUESTED_MODE_XINPUT,
)
assert "Stored configuration generation" in capsys.readouterr().out
assert config_manager.main(["config", "show"]) == 0
output = capsys.readouterr().out
assert "pairing_window_seconds=90" in output
assert "requested_mode=xinput" in output
def test_mode_cli_changes_then_noops(
monkeypatch: pytest.MonkeyPatch,
capsys: pytest.CaptureFixture[str],
) -> None:
previous = FakeDevice()
reenumerated = FakeDevice()
reenumerated.address = 8
reenumerated.configuration = struct.pack(
"<HB5x", 60, config_manager.REQUESTED_MODE_XINPUT
)
reenumerated.active_mode = config_manager.ACTIVE_MODE_XINPUT
scans = iter(
((previous,), (previous,), (), (reenumerated,))
)
monkeypatch.setattr(
config_manager,
"_candidate_devices",
lambda: next(scans, (reenumerated,)),
)
monkeypatch.setattr(config_manager.time, "sleep", lambda _seconds: None)
assert config_manager.main(["mode", "xinput"]) == 0
assert capsys.readouterr().out == "USB mode changed to xinput.\n"
assert previous.reboot_transaction_ids == [previous.transaction_id]
monkeypatch.setattr(
config_manager, "_candidate_devices", lambda: (reenumerated,)
)
assert config_manager.main(["mode", "xinput"]) == 0
assert capsys.readouterr().out == "USB mode is already xinput.\n"
assert reenumerated.out_requests == []
def test_mode_parser_rejects_unimplemented_modes() -> None:
for mode in ("dinput", "mac"):
with pytest.raises(SystemExit):
config_manager.build_parser().parse_args(["mode", mode])
def test_candidate_discovery_checks_switch_and_xinput_identities(
monkeypatch: pytest.MonkeyPatch,
) -> None:
lookups: list[tuple[int, int]] = []
def find(**arguments: object) -> tuple[object, ...]:
lookups.append(
(
int(arguments["idVendor"]),
int(arguments["idProduct"]),
)
)
return ()
monkeypatch.setattr(config_manager.usb.core, "find", find)
assert list(config_manager._candidate_devices()) == []
assert tuple(lookups) == config_manager.USB_IDENTITIES
def test_find_requires_selector_for_multiple_picos(
monkeypatch: pytest.MonkeyPatch,
) -> None:

View file

@ -0,0 +1,36 @@
import shutil
import subprocess
from pathlib import Path
def test_configuration_service_native(tmp_path: Path) -> None:
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 / "configuration_service_test"
subprocess.run(
[
compiler,
"-std=c++17",
"-Wall",
"-Wextra",
"-Werror",
"-pedantic",
f"-I{root / 'tests' / 'bluepad32_native_stubs'}",
f"-I{root}",
str(root / "tests" / "configuration_service_test.cpp"),
str(root / "adapter_configuration.cpp"),
str(root / "configuration_service.cpp"),
str(root / "configuration_storage.cpp"),
str(root / "configuration_transaction.cpp"),
"-o",
str(executable),
],
check=True,
cwd=root,
)
subprocess.run([str(executable)], check=True, cwd=root)
subprocess.run(
[str(executable), "abandoned-receive"], check=True, cwd=root
)

View file

@ -33,7 +33,7 @@ def test_usb_output_driver_contracts(tmp_path: Path) -> None:
"-Werror",
"-pedantic",
f"-DSWITCH_PICO_HID_INSTANCE_COUNT={instance_count}",
"-DSWITCH_PICO_ADAPTER_FEASIBILITY=1",
"-DSWITCH_PICO_USB_OUTPUT_MODES=1",
*backend_definitions,
f"-I{root / 'tests' / 'native_stubs'}",
f"-I{root}",

View file

@ -14,6 +14,18 @@ ConfigurationServiceSnapshot current_configuration{};
ProfileServiceListSnapshot current_profile_list{};
ProfileServiceSelectedSnapshot current_profile_selected{};
ProfileServiceTransactionSnapshot current_profile_transaction{};
AdapterUsbMode current_active_mode = AdapterUsbMode::kSwitchProbe;
ConfigurationTransactionStatus mode_set_result =
ConfigurationTransactionStatus::kPending;
uint32_t mode_set_transaction_id = 0;
AdapterRequestedMode mode_set_requested_mode = AdapterRequestedMode::kAuto;
AdapterModeAvailability mode_set_availability{};
AdapterModeAvailability runtime_mode_availability{};
uint32_t mode_availability_query_count = 0;
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 refresh_requested = false;
bool clear_requested = false;
std::vector<uint8_t> control_payload;
@ -233,6 +245,142 @@ void test_vendor_requests() {
"request with invalid magic was accepted");
}
void test_mode_vendor_requests() {
using namespace UsbConfigurationManagement;
current_configuration.configuration.requested_mode =
AdapterRequestedMode::kXInput;
current_active_mode = AdapterUsbMode::kSwitchProbe;
tusb_control_request_t request =
setup_request(Operation::kInfo, TUSB_DIR_IN, kMaximumResponseSize);
require(usb_configuration_management_vendor_control(
0, CONTROL_STAGE_SETUP, &request) &&
control_payload[kResponseHeaderSize + 4] ==
static_cast<uint8_t>(AdapterUsbMode::kSwitchProbe),
"info response did not report the active USB mode");
request = setup_request(
Operation::kConfigurationRead, TUSB_DIR_IN,
kMaximumResponseSize);
require(usb_configuration_management_vendor_control(
0, CONTROL_STAGE_SETUP, &request) &&
control_payload.size() ==
kResponseHeaderSize +
ADAPTER_CONFIGURATION_ENCODED_SIZE &&
control_payload[10] ==
ADAPTER_CONFIGURATION_SCHEMA_VERSION &&
control_payload[kResponseHeaderSize + 2] ==
static_cast<uint8_t>(AdapterRequestedMode::kXInput),
"configuration response did not keep requested mode separate");
std::vector<uint8_t> mode_set(5);
write_u32(&mode_set, 0, 0x12345678);
mode_set[4] =
static_cast<uint8_t>(AdapterRequestedMode::kXInput);
const std::vector<uint8_t> encoded =
make_request(Operation::kModeSet, mode_set);
require(encoded.size() == kRequestHeaderSize + 5 &&
encoded[5] ==
static_cast<uint8_t>(Operation::kModeSet) &&
encoded[8] == 5 &&
read_u32(encoded, kRequestHeaderSize) == 0x12345678 &&
encoded[kRequestHeaderSize + 4] ==
static_cast<uint8_t>(
AdapterRequestedMode::kXInput),
"mode-set request envelope does not match the protocol");
mode_set_result = ConfigurationTransactionStatus::kPending;
perform_out(Operation::kModeSet, mode_set);
require(mode_set_transaction_id == 0x12345678 &&
mode_set_requested_mode ==
AdapterRequestedMode::kXInput &&
mode_set_availability.switch_mode &&
mode_set_availability.xinput_mode &&
!mode_set_availability.dinput_mode &&
!mode_set_availability.mac_mode &&
mode_availability_query_count == 1,
"XInput mode set did not use runtime availability");
write_u32(&mode_set, 0, 0x12345679);
mode_set[4] = static_cast<uint8_t>(AdapterRequestedMode::kSwitch);
perform_out(Operation::kModeSet, mode_set);
require(mode_set_requested_mode == AdapterRequestedMode::kSwitch &&
mode_availability_query_count == 2,
"Switch mode set did not use runtime availability");
mode_set_result = ConfigurationTransactionStatus::kBusy;
write_u32(&mode_set, 0, 0x1234567a);
perform_out(Operation::kModeSet, mode_set, false);
mode_set_result = ConfigurationTransactionStatus::kStorageError;
write_u32(&mode_set, 0, 0x1234567b);
perform_out(Operation::kModeSet, mode_set, false);
const uint32_t calls_before_invalid = mode_set_call_count;
const uint32_t availability_queries_before_invalid =
mode_availability_query_count;
for (const uint32_t transaction_id : {0u, 0x80000000u}) {
write_u32(&mode_set, 0, transaction_id);
perform_out(Operation::kModeSet, mode_set, false);
}
write_u32(&mode_set, 0, 0x1234567c);
mode_set[4] = 0xff;
perform_out(Operation::kModeSet, mode_set, false);
require(mode_set_call_count == calls_before_invalid &&
mode_availability_query_count ==
availability_queries_before_invalid,
"malformed mode request reached runtime mode selection");
mode_set_result = ConfigurationTransactionStatus::kPending;
for (const AdapterRequestedMode unavailable : {
AdapterRequestedMode::kDInput,
AdapterRequestedMode::kMac,
}) {
mode_set[4] = static_cast<uint8_t>(unavailable);
perform_out(Operation::kModeSet, mode_set, false);
}
require(mode_set_call_count == calls_before_invalid + 2 &&
mode_availability_query_count ==
availability_queries_before_invalid + 2,
"unsupported mode did not use runtime availability");
request = setup_request(
Operation::kModeSet, TUSB_DIR_OUT, kRequestHeaderSize + 4);
require(!usb_configuration_management_vendor_control(
0, CONTROL_STAGE_SETUP, &request),
"short mode-set request was accepted");
std::vector<uint8_t> reboot(4);
write_u32(&reboot, 0, 0x12345678);
const std::vector<uint8_t> encoded_reboot =
make_request(Operation::kReboot, reboot);
require(encoded_reboot.size() == kRequestHeaderSize + 4 &&
encoded_reboot[5] ==
static_cast<uint8_t>(Operation::kReboot) &&
encoded_reboot[8] == 4 &&
read_u32(encoded_reboot, kRequestHeaderSize) ==
0x12345678,
"reboot request envelope does not match the protocol");
correlated_reboot_transaction_id = 0x12345678;
perform_out(Operation::kReboot, reboot);
require(reboot_transaction_id == correlated_reboot_transaction_id,
"correlated reboot request was not dispatched");
write_u32(&reboot, 0, 0x12345679);
perform_out(Operation::kReboot, reboot, false);
const uint32_t reboot_calls_before_invalid = reboot_call_count;
for (const uint32_t transaction_id : {0u, 0x80000000u}) {
write_u32(&reboot, 0, transaction_id);
perform_out(Operation::kReboot, reboot, false);
}
request = setup_request(
Operation::kReboot, TUSB_DIR_OUT, kRequestHeaderSize + 5);
require(!usb_configuration_management_vendor_control(
0, CONTROL_STAGE_SETUP, &request),
"oversized reboot request was accepted");
require(reboot_call_count == reboot_calls_before_invalid,
"malformed reboot transaction reached the helper");
}
void test_profile_vendor_requests() {
using namespace UsbConfigurationManagement;
ControllerIdentity expected_identity{};
@ -448,6 +596,32 @@ ConfigurationTransactionStatus configuration_service_reset(uint32_t) {
return ConfigurationTransactionStatus::kPending;
}
const AdapterModeAvailability& adapter_usb_mode_availability() {
++mode_availability_query_count;
return runtime_mode_availability;
}
ConfigurationTransactionStatus configuration_service_set_mode(
uint32_t transaction_id, AdapterRequestedMode requested_mode,
const AdapterModeAvailability& availability) {
mode_set_transaction_id = transaction_id;
mode_set_requested_mode = requested_mode;
mode_set_availability = availability;
++mode_set_call_count;
if (!adapter_requested_mode_available(requested_mode, availability)) {
return ConfigurationTransactionStatus::kUnsupportedSchema;
}
return mode_set_result;
}
AdapterUsbMode usb_output_driver_mode() { return current_active_mode; }
bool adapter_reboot_for_mode_transaction(uint32_t transaction_id) {
reboot_transaction_id = transaction_id;
++reboot_call_count;
return transaction_id == correlated_reboot_transaction_id;
}
ConfigurationTransactionStatus profile_service_select(
const ControllerIdentity& identity, uint8_t selected_profile) {
profile_identity = identity;
@ -521,8 +695,9 @@ void bluepad32_input_backend_request_pairing_snapshot() {
refresh_requested = true;
}
void bluepad32_input_backend_clear_pairings() {
uint32_t bluepad32_input_backend_clear_pairings() {
clear_requested = true;
return 1;
}
void bluepad32_input_backend_pairing_snapshot(
@ -530,6 +705,11 @@ void bluepad32_input_backend_pairing_snapshot(
*out = current_pairings;
}
bool adapter_host_probe_vendor_control(
uint8_t, uint8_t, const tusb_control_request_t*) {
return false;
}
bool tud_control_xfer(uint8_t, const tusb_control_request_t* request,
void* buffer, uint16_t length) {
if (request->bmRequestType_bit.direction == TUSB_DIR_OUT) {
@ -560,6 +740,7 @@ int main() {
test_envelope_encoding();
test_pairing_encoding();
test_vendor_requests();
test_mode_vendor_requests();
test_profile_vendor_requests();
return 0;
}

View file

@ -289,6 +289,7 @@ void expect_usb_string(uint8_t index, const char* expected,
}
void test_switch_boundary_dispatch() {
reset_usb_harness();
usb_output_driver_init(AdapterUsbMode::kSwitchProbe);
expect(usb_output_driver_mode() == AdapterUsbMode::kSwitchProbe &&
@ -373,6 +374,19 @@ void test_switch_boundary_dispatch() {
expect(!tud_control_request_cb(0, nullptr),
"generic control routing claimed an unhandled request");
}
void test_manual_switch_selection() {
reset_usb_harness();
usb_output_driver_init(AdapterUsbMode::kSwitch);
expect(usb_output_driver_mode() == AdapterUsbMode::kSwitch &&
std::strcmp(usb_output_driver_mode_name(), "Switch") == 0,
"manual Switch mode was not frozen separately from probe mode");
expect(std::memcmp(tud_descriptor_device_cb(),
switch_pro_device_descriptor,
sizeof(switch_pro_device_descriptor)) == 0,
"manual Switch did not use the production Switch descriptor");
expect(tud_descriptor_string_cb(0xee, 0x0409) == nullptr,
"manual Switch exposed the automatic Windows probe string");
}
void open_xinput_interfaces(usbd_class_driver_t const* driver) {
endpoint_harness = {};
@ -671,6 +685,7 @@ int main() {
test_rumble_report();
test_host_probe_sequence();
test_switch_boundary_dispatch();
test_manual_switch_selection();
test_xinput_boundary_dispatch();
test_vendor_control_boundary();
return failures == 0 ? 0 : 1;