723 lines
28 KiB
C++
723 lines
28 KiB
C++
#include "adapter/adapter_mode_controller.h"
|
|
#include <algorithm>
|
|
#include <cstdint>
|
|
#include <cstdlib>
|
|
#include <iostream>
|
|
#include <vector>
|
|
|
|
#include "configuration/adapter_configuration.h"
|
|
#include "adapter/adapter_reboot.h"
|
|
#include "input/bluepad32_input_backend.h"
|
|
#include "configuration/configuration_service.h"
|
|
#include "profile/controller_profile.h"
|
|
|
|
namespace {
|
|
|
|
enum class Call : uint8_t {
|
|
kPreload,
|
|
kSnapshot,
|
|
kProbeInit,
|
|
kOutputInit,
|
|
kTinyUsbInit,
|
|
kSetMode,
|
|
kQueryMode,
|
|
kRebootReady,
|
|
kRuntimeReset,
|
|
kFeedback,
|
|
kReserveRecovery,
|
|
kClearPairings,
|
|
kPairingSnapshot,
|
|
kWatchdogReboot,
|
|
kBootselReboot,
|
|
};
|
|
|
|
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;
|
|
int bootsel_reboot_count = 0;
|
|
uint32_t bootsel_gpio_mask = UINT32_MAX;
|
|
uint32_t bootsel_disable_mask = UINT32_MAX;
|
|
bool recovery_reserved = false;
|
|
bool abandoned_host_receive = false;
|
|
bool abandoned_host_receive_canceled = false;
|
|
|
|
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;
|
|
bootsel_reboot_count = 0;
|
|
bootsel_gpio_mask = UINT32_MAX;
|
|
bootsel_disable_mask = UINT32_MAX;
|
|
recovery_reserved = false;
|
|
abandoned_host_receive = false;
|
|
abandoned_host_receive_canceled = false;
|
|
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 five-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::kDInput,
|
|
"XInput did not cycle to DInput");
|
|
require(triggered_target(AdapterRequestedMode::kDInput) ==
|
|
AdapterRequestedMode::kMac,
|
|
"DInput did not cycle to Mac");
|
|
require(triggered_target(AdapterRequestedMode::kMac) ==
|
|
AdapterRequestedMode::kAuto,
|
|
"Mac did not cycle to Auto");
|
|
|
|
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_feedback_distinguishes_all_modes() {
|
|
struct FeedbackCase {
|
|
AdapterRequestedMode initial_mode;
|
|
AdapterRequestedMode target_mode;
|
|
uint8_t pulses;
|
|
ControllerProfileConfirmationPolicy policy;
|
|
};
|
|
constexpr FeedbackCase cases[] = {
|
|
{AdapterRequestedMode::kAuto, AdapterRequestedMode::kSwitch, 2,
|
|
ControllerProfileConfirmationPolicy::kRumbleAndLed},
|
|
{AdapterRequestedMode::kSwitch, AdapterRequestedMode::kXInput, 3,
|
|
ControllerProfileConfirmationPolicy::kRumbleAndLed},
|
|
{AdapterRequestedMode::kXInput, AdapterRequestedMode::kDInput, 4,
|
|
ControllerProfileConfirmationPolicy::kRumbleAndLed},
|
|
{AdapterRequestedMode::kDInput, AdapterRequestedMode::kMac, 4,
|
|
ControllerProfileConfirmationPolicy::kLed},
|
|
{AdapterRequestedMode::kMac, AdapterRequestedMode::kAuto, 1,
|
|
ControllerProfileConfirmationPolicy::kRumbleAndLed},
|
|
};
|
|
|
|
for (const FeedbackCase& expected : cases) {
|
|
reset_harness(expected.initial_mode);
|
|
begin_hold(1, 23, 0);
|
|
finish_hold(1, 23, 0);
|
|
set_results = {ConfigurationTransactionStatus::kCommitted};
|
|
adapter_mode_controller_task(ADAPTER_MODE_CHORD_HOLD_MS);
|
|
require(set_modes.size() == 1 &&
|
|
set_modes[0] == expected.target_mode &&
|
|
feedback_slot == 1 && feedback_generation == 23 &&
|
|
feedback_pulses == expected.pulses &&
|
|
feedback_policy == expected.policy &&
|
|
reboot_count == 0,
|
|
"mode feedback tuple did not uniquely identify its target");
|
|
|
|
const uint32_t feedback_duration =
|
|
static_cast<uint32_t>(expected.pulses) * 2u * 75u + 75u;
|
|
adapter_mode_controller_task(
|
|
ADAPTER_MODE_CHORD_HOLD_MS + feedback_duration - 1u);
|
|
require(reboot_count == 0,
|
|
"mode feedback deadline was shorter than its bounded pulse sequence");
|
|
adapter_mode_controller_task(
|
|
ADAPTER_MODE_CHORD_HOLD_MS + feedback_duration);
|
|
require(reboot_count == 1,
|
|
"mode feedback did not reboot at its bounded deadline");
|
|
}
|
|
}
|
|
|
|
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");
|
|
}
|
|
|
|
void test_auto_xinput_disconnect_reboots_to_probe() {
|
|
reset_harness(AdapterRequestedMode::kAuto);
|
|
probed_active_mode = AdapterUsbMode::kXInput;
|
|
adapter_mode_controller_on_usb_unmounted();
|
|
adapter_mode_controller_on_usb_unmounted();
|
|
require(runtime_reset_count == 1 && reboot_count == 1,
|
|
"Auto XInput unmount did not schedule exactly one probe reboot");
|
|
|
|
reset_harness(AdapterRequestedMode::kXInput);
|
|
probed_active_mode = AdapterUsbMode::kXInput;
|
|
adapter_mode_controller_on_usb_unmounted();
|
|
require(runtime_reset_count == 0 && reboot_count == 0,
|
|
"manual XInput mode rebooted after USB unmount");
|
|
|
|
reset_harness(AdapterRequestedMode::kAuto);
|
|
probed_active_mode = AdapterUsbMode::kSwitchProbe;
|
|
adapter_mode_controller_on_usb_unmounted();
|
|
require(runtime_reset_count == 0 && reboot_count == 0,
|
|
"Auto Switch probe rebooted after USB unmount");
|
|
}
|
|
|
|
void test_bootsel_reboot_is_delayed_and_idempotent() {
|
|
reset_harness(AdapterRequestedMode::kAuto);
|
|
require(adapter_reboot_to_bootsel() &&
|
|
adapter_reboot_to_bootsel() &&
|
|
runtime_reset_count == 1 &&
|
|
bootsel_reboot_count == 0 && reboot_count == 0,
|
|
"BOOTSEL request was not accepted exactly once");
|
|
|
|
adapter_mode_controller_task(UINT32_MAX - 25u);
|
|
adapter_mode_controller_task(23);
|
|
require(bootsel_reboot_count == 0,
|
|
"BOOTSEL reboot occurred before the control-transfer guard");
|
|
adapter_mode_controller_task(24);
|
|
adapter_mode_controller_task(25);
|
|
require(bootsel_reboot_count == 1 &&
|
|
bootsel_gpio_mask == 0 &&
|
|
bootsel_disable_mask == 0 &&
|
|
reboot_count == 0 &&
|
|
!adapter_reboot_for_mode_transaction(91),
|
|
"BOOTSEL reboot was not delayed, one-shot, or isolated from "
|
|
"the correlated watchdog path");
|
|
}
|
|
|
|
} // namespace
|
|
|
|
void configuration_service_initialize_pre_usb() {
|
|
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 did not advertise all compiled 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;
|
|
}
|
|
|
|
void reset_usb_boot(uint32_t usb_activity_gpio_pin_mask,
|
|
uint32_t disable_interface_mask) {
|
|
calls.push_back(Call::kBootselReboot);
|
|
++bootsel_reboot_count;
|
|
bootsel_gpio_mask = usb_activity_gpio_pin_mask;
|
|
bootsel_disable_mask = disable_interface_mask;
|
|
}
|
|
|
|
int main() {
|
|
test_pre_tusb_ordering_and_configured_selection();
|
|
test_mode_availability_has_one_stable_value();
|
|
test_exact_hold_release_wrap_and_consumption();
|
|
test_cycle_and_slot_isolation();
|
|
test_busy_retry_and_one_shot();
|
|
test_commit_feedback_then_reboot();
|
|
test_feedback_distinguishes_all_modes();
|
|
test_configuration_failure_and_correlated_reboot();
|
|
test_recovery_auto_wins_host_mode_interleaving();
|
|
test_failed_recovery_never_reboots();
|
|
test_auto_xinput_disconnect_reboots_to_probe();
|
|
test_bootsel_reboot_is_delayed_and_idempotent();
|
|
return 0;
|
|
}
|