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

@ -100,6 +100,8 @@ add_executable(switch-pico
if(SWITCH_PICO_INPUT_BACKEND STREQUAL "BLUEPAD32")
target_sources(switch-pico PRIVATE
bluepad32_input_backend.cpp
adapter_host_probe.cpp
adapter_mode_controller.cpp
controller_identity.cpp
controller_profile.cpp
controller_profile_transform.cpp
@ -115,19 +117,12 @@ if(SWITCH_PICO_INPUT_BACKEND STREQUAL "BLUEPAD32")
configuration_service.cpp
pico_configuration_storage.cpp
usb_configuration_management.cpp
xinput_driver.cpp
)
if(SWITCH_PICO_ADAPTER_FEASIBILITY)
target_sources(switch-pico PRIVATE
adapter_host_probe.cpp
xinput_driver.cpp
)
target_compile_definitions(switch-pico PRIVATE
SWITCH_PICO_ADAPTER_FEASIBILITY=1
)
endif()
target_compile_definitions(switch-pico PRIVATE
SWITCH_PICO_BLUEPAD32=1
SWITCH_PICO_HID_INSTANCE_COUNT=4
SWITCH_PICO_USB_OUTPUT_MODES=1
PICO_FLASH_ASSUME_CORE1_SAFE=0
)
else()
@ -162,9 +157,7 @@ if(SWITCH_PICO_INPUT_BACKEND STREQUAL "BLUEPAD32")
pico_multicore
pico_flash
)
if(SWITCH_PICO_ADAPTER_FEASIBILITY)
target_link_libraries(switch-pico hardware_watchdog)
endif()
target_link_libraries(switch-pico hardware_watchdog)
endif()
if (SWITCH_PICO_LOG)

View file

@ -1,43 +1,112 @@
#include "adapter_configuration.h"
namespace {
bool pairing_window_valid(uint16_t pairing_window_seconds) {
return pairing_window_seconds >= ADAPTER_PAIRING_WINDOW_SECONDS_MIN &&
pairing_window_seconds <= ADAPTER_PAIRING_WINDOW_SECONDS_MAX;
}
} // namespace
AdapterConfiguration adapter_configuration_default() {
return {};
}
bool adapter_requested_mode_valid(AdapterRequestedMode requested_mode) {
switch (requested_mode) {
case AdapterRequestedMode::kAuto:
case AdapterRequestedMode::kSwitch:
case AdapterRequestedMode::kXInput:
case AdapterRequestedMode::kDInput:
case AdapterRequestedMode::kMac:
return true;
}
return false;
}
bool adapter_requested_mode_available(
AdapterRequestedMode requested_mode,
const AdapterModeAvailability& availability) {
switch (requested_mode) {
case AdapterRequestedMode::kAuto:
return true;
case AdapterRequestedMode::kSwitch:
return availability.switch_mode;
case AdapterRequestedMode::kXInput:
return availability.xinput_mode;
case AdapterRequestedMode::kDInput:
return availability.dinput_mode;
case AdapterRequestedMode::kMac:
return availability.mac_mode;
}
return false;
}
bool adapter_configuration_encode(const AdapterConfiguration& configuration,
uint8_t* output, size_t output_size) {
if (output == nullptr || output_size < ADAPTER_CONFIGURATION_ENCODED_SIZE ||
configuration.pairing_window_seconds <
ADAPTER_PAIRING_WINDOW_SECONDS_MIN ||
configuration.pairing_window_seconds >
ADAPTER_PAIRING_WINDOW_SECONDS_MAX) {
if (output == nullptr ||
output_size != ADAPTER_CONFIGURATION_ENCODED_SIZE ||
!pairing_window_valid(configuration.pairing_window_seconds) ||
!adapter_requested_mode_valid(configuration.requested_mode)) {
return false;
}
output[0] = static_cast<uint8_t>(configuration.pairing_window_seconds);
output[1] =
static_cast<uint8_t>(configuration.pairing_window_seconds >> 8);
output[2] = 0;
output[2] = static_cast<uint8_t>(configuration.requested_mode);
output[3] = 0;
output[4] = 0;
output[5] = 0;
output[6] = 0;
output[7] = 0;
return true;
}
bool adapter_configuration_decode(uint16_t schema_version,
const uint8_t* payload,
size_t payload_size,
AdapterConfiguration* output) {
if (payload == nullptr || output == nullptr) {
return false;
}
AdapterConfiguration decoded{};
if (schema_version == ADAPTER_CONFIGURATION_LEGACY_SCHEMA_VERSION) {
if (payload_size != ADAPTER_CONFIGURATION_LEGACY_ENCODED_SIZE ||
payload[2] != 0 || payload[3] != 0) {
return false;
}
decoded.requested_mode = AdapterRequestedMode::kAuto;
} else if (schema_version == ADAPTER_CONFIGURATION_SCHEMA_VERSION) {
if (payload_size != ADAPTER_CONFIGURATION_ENCODED_SIZE ||
payload[3] != 0 || payload[4] != 0 || payload[5] != 0 ||
payload[6] != 0 || payload[7] != 0) {
return false;
}
decoded.requested_mode =
static_cast<AdapterRequestedMode>(payload[2]);
if (!adapter_requested_mode_valid(decoded.requested_mode)) {
return false;
}
} else {
return false;
}
decoded.pairing_window_seconds =
static_cast<uint16_t>(payload[0]) |
static_cast<uint16_t>(payload[1] << 8);
if (!pairing_window_valid(decoded.pairing_window_seconds)) {
return false;
}
*output = decoded;
return true;
}
bool adapter_configuration_decode(const uint8_t* payload, size_t payload_size,
AdapterConfiguration* output) {
if (payload == nullptr || output == nullptr ||
payload_size != ADAPTER_CONFIGURATION_ENCODED_SIZE ||
payload[2] != 0 || payload[3] != 0) {
return false;
}
const uint16_t pairing_window_seconds =
static_cast<uint16_t>(payload[0]) |
static_cast<uint16_t>(payload[1] << 8);
if (pairing_window_seconds < ADAPTER_PAIRING_WINDOW_SECONDS_MIN ||
pairing_window_seconds > ADAPTER_PAIRING_WINDOW_SECONDS_MAX) {
return false;
}
output->pairing_window_seconds = pairing_window_seconds;
return true;
return adapter_configuration_decode(ADAPTER_CONFIGURATION_SCHEMA_VERSION,
payload, payload_size, output);
}

View file

@ -3,8 +3,12 @@
#include <stddef.h>
#include <stdint.h>
constexpr uint16_t ADAPTER_CONFIGURATION_SCHEMA_VERSION = 1;
constexpr size_t ADAPTER_CONFIGURATION_ENCODED_SIZE = 4;
#include "adapter_usb_mode.h"
constexpr uint16_t ADAPTER_CONFIGURATION_LEGACY_SCHEMA_VERSION = 1;
constexpr uint16_t ADAPTER_CONFIGURATION_SCHEMA_VERSION = 2;
constexpr size_t ADAPTER_CONFIGURATION_LEGACY_ENCODED_SIZE = 4;
constexpr size_t ADAPTER_CONFIGURATION_ENCODED_SIZE = 8;
constexpr uint16_t ADAPTER_PAIRING_WINDOW_SECONDS_MIN = 10;
constexpr uint16_t ADAPTER_PAIRING_WINDOW_SECONDS_MAX = 300;
constexpr uint16_t ADAPTER_PAIRING_WINDOW_SECONDS_DEFAULT = 60;
@ -12,10 +16,26 @@ constexpr uint16_t ADAPTER_PAIRING_WINDOW_SECONDS_DEFAULT = 60;
struct AdapterConfiguration {
uint16_t pairing_window_seconds =
ADAPTER_PAIRING_WINDOW_SECONDS_DEFAULT;
AdapterRequestedMode requested_mode = AdapterRequestedMode::kAuto;
};
struct AdapterModeAvailability {
bool switch_mode = true;
bool xinput_mode = true;
bool dinput_mode = false;
bool mac_mode = false;
};
AdapterConfiguration adapter_configuration_default();
bool adapter_requested_mode_valid(AdapterRequestedMode requested_mode);
bool adapter_requested_mode_available(
AdapterRequestedMode requested_mode,
const AdapterModeAvailability& availability);
bool adapter_configuration_encode(const AdapterConfiguration& configuration,
uint8_t* output, size_t output_size);
bool adapter_configuration_decode(uint16_t schema_version,
const uint8_t* payload,
size_t payload_size,
AdapterConfiguration* output);
bool adapter_configuration_decode(const uint8_t* payload, size_t payload_size,
AdapterConfiguration* output);

View file

@ -9,6 +9,7 @@
#endif
#include "adapter_host_probe_state.h"
#include "controller_profile_runtime.h"
#include "hardware/structs/watchdog.h"
#include "hardware/watchdog.h"
#include "pico/time.h"
@ -22,7 +23,7 @@ constexpr uint8_t kStatusRequest = 0x21;
constexpr uint16_t kStatusIndex = 0x0005;
uint8_t g_status_response[4]{};
AdapterUsbMode g_mode = AdapterUsbMode::kSwitchProbe;
AdapterUsbMode g_mode = AdapterUsbMode::kSwitch;
AdapterHostProbeState g_probe;
alarm_id_t g_reboot_alarm = 0;
@ -32,6 +33,7 @@ uint32_t now_ms() {
int64_t reboot_to_xinput(alarm_id_t alarm_id, void *user_data) {
(void)alarm_id;
(void)user_data;
controller_profile_runtime_reset();
watchdog_hw->scratch[kModeScratchRegister] = kXInputBootMagic;
watchdog_reboot(0, 0, 0);
return 0;
@ -39,16 +41,39 @@ int64_t reboot_to_xinput(alarm_id_t alarm_id, void *user_data) {
} // namespace
void adapter_host_probe_init() {
if (watchdog_hw->scratch[kModeScratchRegister] == kXInputBootMagic) {
watchdog_hw->scratch[kModeScratchRegister] = 0;
g_mode = AdapterUsbMode::kXInput;
} else {
g_mode = AdapterUsbMode::kSwitchProbe;
void adapter_host_probe_init(AdapterRequestedMode requested_mode) {
const uint32_t scratch = watchdog_hw->scratch[kModeScratchRegister];
// Scratch is a one-boot transition token. Always consume it, including
// stale tokens left behind when a persistent manual mode bypasses auto.
watchdog_hw->scratch[kModeScratchRegister] = 0;
switch (requested_mode) {
case AdapterRequestedMode::kSwitch:
g_mode = AdapterUsbMode::kSwitch;
break;
case AdapterRequestedMode::kXInput:
g_mode = AdapterUsbMode::kXInput;
break;
case AdapterRequestedMode::kAuto:
g_mode = scratch == kXInputBootMagic
? AdapterUsbMode::kXInput
: AdapterUsbMode::kSwitchProbe;
break;
case AdapterRequestedMode::kDInput:
case AdapterRequestedMode::kMac:
// Keep USB usable without treating unavailable explicit choices
// as Auto. Host/controller setters reject these until drivers land.
g_mode = AdapterUsbMode::kSwitch;
break;
}
g_probe = {};
g_reboot_alarm = 0;
PROBE_LOG("[HOST PROBE] boot mode=%s\n",
g_mode == AdapterUsbMode::kXInput ? "XInput" : "Switch probe");
g_mode == AdapterUsbMode::kXInput
? "XInput"
: (g_mode == AdapterUsbMode::kSwitch
? "Switch"
: "Switch probe"));
}
AdapterUsbMode adapter_host_probe_mode() { return g_mode; }
@ -105,8 +130,11 @@ bool adapter_host_probe_vendor_control(uint8_t rhport, uint8_t stage,
return queued;
}
return tud_control_xfer(
rhport, request,
const_cast<uint8_t *>(XInput::kMsCompatIdDescriptor),
sizeof(XInput::kMsCompatIdDescriptor));
if (g_mode == AdapterUsbMode::kXInput) {
return tud_control_xfer(
rhport, request,
const_cast<uint8_t *>(XInput::kMsCompatIdDescriptor),
sizeof(XInput::kMsCompatIdDescriptor));
}
return false;
}

View file

@ -5,8 +5,10 @@
#include "adapter_usb_mode.h"
#include "tusb.h"
void adapter_host_probe_init();
// Consume watchdog scratch and freeze the active mode. Call exactly once
// before usb_output_driver_init() and tusb_init().
void adapter_host_probe_init(AdapterRequestedMode requested_mode);
AdapterUsbMode adapter_host_probe_mode();
void adapter_host_probe_note_string_descriptor(uint8_t index);
bool adapter_host_probe_vendor_control(uint8_t rhport, uint8_t stage,
tusb_control_request_t const *request);

430
adapter_mode_controller.cpp Normal file
View file

@ -0,0 +1,430 @@
#include "adapter_mode_controller.h"
#include "adapter_reboot.h"
#include "adapter_configuration.h"
#include "adapter_host_probe.h"
#include "bluepad32_input_backend.h"
#include "configuration_service.h"
#include "controller_profile.h"
#include "controller_profile_runtime.h"
#include "hardware/watchdog.h"
#include "tusb.h"
#include "usb_output_driver.h"
namespace {
constexpr uint32_t kInternalTransactionBit =
CONFIGURATION_SERVICE_INTERNAL_TRANSACTION_ID_MASK;
constexpr uint32_t kInternalTransactionValueMask =
~kInternalTransactionBit;
constexpr uint32_t kFeedbackPhaseMs = 75;
constexpr uint32_t kFeedbackGuardMs = 75;
static_assert(
ADAPTER_MODE_CHORD_BUTTON_MASK ==
static_cast<uint16_t>(
(1u << static_cast<uint8_t>(
ControllerProfileLogicalButton::kLeftShoulder)) |
(1u << static_cast<uint8_t>(
ControllerProfileLogicalButton::kRightShoulder)) |
(1u << static_cast<uint8_t>(
ControllerProfileLogicalButton::kSelect)) |
(1u << static_cast<uint8_t>(
ControllerProfileLogicalButton::kStart)) |
(1u << static_cast<uint8_t>(
ControllerProfileLogicalButton::kSystem))),
"raw mode chord must track the logical pre-hotkey mask");
struct ModeChordSlot {
uint32_t connection_generation;
uint32_t hold_started_ms;
bool generation_valid;
bool holding;
bool triggered;
};
enum class ModeOperationKind : uint8_t {
kNone,
kChord,
kRecovery,
};
enum class ModeOperationPhase : uint8_t {
kSubmit,
kWaitForCommit,
kAcknowledge,
};
struct ModeOperation {
ModeOperationKind kind;
ModeOperationPhase phase;
uint32_t transaction_id;
AdapterRequestedMode target_mode;
uint32_t feedback_deadline_ms;
uint32_t connection_generation;
uint8_t slot;
};
ModeChordSlot g_chord_slots[ADAPTER_MODE_CONTROLLER_SLOT_COUNT]{};
ModeOperation g_operation{};
uint32_t g_reboot_transaction_id = 0;
bool g_reboot_scheduled = false;
bool g_correlated_reboot_scheduled = false;
AdapterRequestedMode g_requested_mode = AdapterRequestedMode::kAuto;
uint32_t g_next_internal_transaction_value = 1;
bool g_recovery_requested = false;
bool g_recovery_failed = false;
uint32_t g_recovery_clear_pairings_token = 0;
bool deadline_reached(uint32_t now_ms, uint32_t deadline_ms) {
return static_cast<int32_t>(now_ms - deadline_ms) >= 0;
}
bool successful_status(ConfigurationTransactionStatus status) {
return status == ConfigurationTransactionStatus::kCommitted ||
status == ConfigurationTransactionStatus::kUnchanged;
}
bool pending_status(ConfigurationTransactionStatus status) {
return status == ConfigurationTransactionStatus::kReceiving ||
status == ConfigurationTransactionStatus::kPending;
}
uint32_t next_internal_transaction_id() {
const uint32_t transaction_id =
kInternalTransactionBit | g_next_internal_transaction_value;
if (g_next_internal_transaction_value ==
kInternalTransactionValueMask) {
g_next_internal_transaction_value = 1;
} else {
++g_next_internal_transaction_value;
}
return transaction_id;
}
AdapterRequestedMode next_mode(AdapterRequestedMode mode) {
switch (mode) {
case AdapterRequestedMode::kAuto:
return AdapterRequestedMode::kSwitch;
case AdapterRequestedMode::kSwitch:
return AdapterRequestedMode::kXInput;
case AdapterRequestedMode::kXInput:
case AdapterRequestedMode::kDInput:
case AdapterRequestedMode::kMac:
return AdapterRequestedMode::kAuto;
}
return AdapterRequestedMode::kAuto;
}
uint8_t feedback_pulse_count(AdapterRequestedMode mode) {
switch (mode) {
case AdapterRequestedMode::kAuto:
return 1;
case AdapterRequestedMode::kSwitch:
return 2;
case AdapterRequestedMode::kXInput:
return 3;
case AdapterRequestedMode::kDInput:
case AdapterRequestedMode::kMac:
return 1;
}
return 1;
}
void clear_mode_chord(ControllerState* state) {
if (state == nullptr) {
return;
}
state->button_left_shoulder = false;
state->button_right_shoulder = false;
state->button_select = false;
state->button_start = false;
state->button_system = false;
}
void begin_operation(ModeOperationKind kind, AdapterRequestedMode target_mode,
uint8_t slot = 0,
uint32_t connection_generation = 0) {
g_operation = {};
g_operation.kind = kind;
g_operation.phase = ModeOperationPhase::kSubmit;
g_operation.transaction_id = next_internal_transaction_id();
g_operation.target_mode = target_mode;
g_operation.slot = slot;
g_operation.connection_generation = connection_generation;
}
void begin_recovery_operation_if_ready() {
if (!g_recovery_requested || g_recovery_failed ||
g_operation.kind != ModeOperationKind::kNone) {
return;
}
Bluepad32PairingSnapshot pairing{};
bluepad32_input_backend_pairing_snapshot(&pairing);
if (!bluepad32_input_backend_clear_pairings_completed(
pairing, g_recovery_clear_pairings_token)) {
return;
}
begin_operation(ModeOperationKind::kRecovery,
AdapterRequestedMode::kAuto);
}
void reboot_now();
void finish_failed_operation() {
const bool recovery =
g_operation.kind == ModeOperationKind::kRecovery;
g_operation = {};
if (recovery) {
g_recovery_failed = true;
} else {
begin_recovery_operation_if_ready();
}
}
void finish_successful_mode_write(uint32_t now_ms) {
g_requested_mode = g_operation.target_mode;
if (g_recovery_requested ||
g_operation.kind == ModeOperationKind::kRecovery) {
if (g_operation.kind != ModeOperationKind::kRecovery) {
g_operation = {};
begin_recovery_operation_if_ready();
return;
}
if (configuration_service_mode_transaction_reboot_ready(
g_operation.transaction_id)) {
reboot_now();
return;
}
// Reboot only while this remains the latest accepted mode mutation.
// Retry Auto if its correlation was displaced before Core 0 observed
// the terminal result.
begin_operation(ModeOperationKind::kRecovery,
AdapterRequestedMode::kAuto);
return;
}
controller_profile_runtime_reset();
const uint8_t pulses = feedback_pulse_count(g_operation.target_mode);
bluepad32_input_backend_queue_profile_feedback(
g_operation.slot, g_operation.connection_generation, pulses,
ControllerProfileConfirmationPolicy::kRumbleAndLed);
g_operation.feedback_deadline_ms =
now_ms + static_cast<uint32_t>(pulses) * 2u * kFeedbackPhaseMs +
kFeedbackGuardMs;
g_operation.phase = ModeOperationPhase::kAcknowledge;
}
void reboot_now() {
if (g_reboot_scheduled) {
return;
}
g_reboot_scheduled = true;
controller_profile_runtime_reset();
watchdog_reboot(0, 0, 0);
}
void advance_mode_write(uint32_t now_ms) {
if (g_operation.phase == ModeOperationPhase::kSubmit) {
const ConfigurationTransactionStatus status =
configuration_service_set_mode_internal(
g_operation.transaction_id, g_operation.target_mode,
adapter_usb_mode_availability());
if (status == ConfigurationTransactionStatus::kBusy) {
return;
}
if (successful_status(status)) {
finish_successful_mode_write(now_ms);
return;
}
if (pending_status(status)) {
g_operation.phase = ModeOperationPhase::kWaitForCommit;
return;
}
finish_failed_operation();
return;
}
ConfigurationTransactionStatus status =
ConfigurationTransactionStatus::kIdle;
if (!configuration_service_mode_transaction_status(
g_operation.transaction_id, &status)) {
finish_failed_operation();
return;
}
if (successful_status(status)) {
finish_successful_mode_write(now_ms);
} else if (!pending_status(status)) {
finish_failed_operation();
}
}
} // namespace
const AdapterModeAvailability& adapter_usb_mode_availability() {
static constexpr AdapterModeAvailability kAvailability{
true, true, false, false};
return kAvailability;
}
void adapter_mode_controller_initialize_usb() {
for (ModeChordSlot& slot : g_chord_slots) {
slot = {};
}
g_operation = {};
g_reboot_scheduled = false;
g_reboot_transaction_id = 0;
g_next_internal_transaction_value = 1;
g_recovery_requested = false;
g_recovery_failed = false;
g_recovery_clear_pairings_token = 0;
g_correlated_reboot_scheduled = false;
configuration_service_initialize_pre_usb();
ConfigurationServiceSnapshot snapshot{};
configuration_service_snapshot(&snapshot);
g_requested_mode = snapshot.configuration.requested_mode;
adapter_host_probe_init(g_requested_mode);
usb_output_driver_init(adapter_host_probe_mode());
tusb_init();
}
AdapterRequestedMode adapter_mode_controller_requested_mode() {
return g_requested_mode;
}
void adapter_mode_controller_process_input(
uint8_t slot_index, bool active, uint32_t connection_generation,
uint16_t* pre_hotkey_button_mask, uint32_t now_ms,
ControllerState* state) {
if (slot_index >= ADAPTER_MODE_CONTROLLER_SLOT_COUNT) {
return;
}
ModeChordSlot& slot = g_chord_slots[slot_index];
if (!active) {
slot = {};
return;
}
if (!slot.generation_valid ||
slot.connection_generation != connection_generation) {
slot = {};
slot.connection_generation = connection_generation;
slot.generation_valid = true;
}
const uint16_t raw_button_mask =
pre_hotkey_button_mask == nullptr ? 0 : *pre_hotkey_button_mask;
const bool chord_held =
(raw_button_mask & ADAPTER_MODE_CHORD_BUTTON_MASK) ==
ADAPTER_MODE_CHORD_BUTTON_MASK;
if (!chord_held) {
slot.holding = false;
slot.triggered = false;
return;
}
if (pre_hotkey_button_mask != nullptr) {
*pre_hotkey_button_mask = static_cast<uint16_t>(
*pre_hotkey_button_mask &
~ADAPTER_MODE_CHORD_BUTTON_MASK);
}
clear_mode_chord(state);
if (!slot.holding) {
slot.holding = true;
slot.hold_started_ms = now_ms;
return;
}
if (slot.triggered ||
!deadline_reached(now_ms,
slot.hold_started_ms +
ADAPTER_MODE_CHORD_HOLD_MS)) {
return;
}
// Latch once per continuous hold even if another slot or recovery already
// owns the single serialized internal mutation.
slot.triggered = true;
if (g_operation.kind == ModeOperationKind::kNone &&
!g_recovery_requested) {
ConfigurationServiceSnapshot snapshot{};
configuration_service_snapshot(&snapshot);
g_requested_mode = snapshot.configuration.requested_mode;
begin_operation(ModeOperationKind::kChord,
next_mode(g_requested_mode), slot_index,
connection_generation);
}
}
void adapter_mode_controller_task(uint32_t now_ms) {
if (g_reboot_scheduled) {
return;
}
begin_recovery_operation_if_ready();
if (g_operation.kind == ModeOperationKind::kNone) {
return;
}
if (g_operation.phase == ModeOperationPhase::kSubmit ||
g_operation.phase == ModeOperationPhase::kWaitForCommit) {
advance_mode_write(now_ms);
return;
}
if (g_operation.phase == ModeOperationPhase::kAcknowledge) {
if (g_recovery_requested) {
g_operation = {};
begin_recovery_operation_if_ready();
return;
}
if (deadline_reached(now_ms,
g_operation.feedback_deadline_ms)) {
if (configuration_service_mode_transaction_reboot_ready(
g_operation.transaction_id)) {
reboot_now();
} else {
finish_failed_operation();
}
}
}
}
void adapter_mode_controller_begin_recovery() {
if (g_recovery_requested) {
return;
}
configuration_service_reserve_for_recovery();
g_recovery_requested = true;
g_recovery_clear_pairings_token =
bluepad32_input_backend_clear_pairings();
g_recovery_failed =
g_recovery_clear_pairings_token == 0;
g_operation = {};
}
bool adapter_reboot_for_mode_transaction(uint32_t transaction_id) {
if (g_reboot_scheduled) {
return g_correlated_reboot_scheduled &&
g_reboot_transaction_id == transaction_id;
}
if ((transaction_id & kInternalTransactionBit) != 0) {
return false;
}
if (g_recovery_requested) {
return false;
}
if (!configuration_service_mode_transaction_reboot_ready(
transaction_id)) {
return false;
}
g_correlated_reboot_scheduled = true;
g_reboot_transaction_id = transaction_id;
reboot_now();
return true;
}

34
adapter_mode_controller.h Normal file
View file

@ -0,0 +1,34 @@
#pragma once
#include <stdint.h>
#include "adapter_usb_mode.h"
#include "controller_state.h"
constexpr uint8_t ADAPTER_MODE_CONTROLLER_SLOT_COUNT = 4;
constexpr uint32_t ADAPTER_MODE_CHORD_HOLD_MS = 3000;
constexpr uint16_t ADAPTER_MODE_CHORD_BUTTON_MASK =
static_cast<uint16_t>((1u << 4) | (1u << 5) | (1u << 6) |
(1u << 7) | (1u << 8));
// Loads persistent configuration on Core 0, consumes watchdog scratch,
// freezes the output implementation, then starts TinyUSB in that exact order.
void adapter_mode_controller_initialize_usb();
AdapterRequestedMode adapter_mode_controller_requested_mode();
// Observes the physical pre-hotkey mask and removes the mode chord from both
// that mask and the state before profile processing. Slot state is isolated by
// connection generation and all time comparisons are uint32-wrap safe.
void adapter_mode_controller_process_input(
uint8_t slot, bool active, uint32_t connection_generation,
uint16_t* pre_hotkey_button_mask, uint32_t now_ms,
ControllerState* state);
// Advances serialized internal mode writes, acknowledgement, recovery, and
// reboot work. Call once per Core-0 loop after processing every input slot.
void adapter_mode_controller_task(uint32_t now_ms);
// Starts the physical ten-second recovery operation: clear only Bluetooth
// pairings, then restore requested mode Auto as the final persisted mutation
// and reboot immediately after that transaction is still current.
void adapter_mode_controller_begin_recovery();

7
adapter_reboot.h Normal file
View file

@ -0,0 +1,7 @@
#pragma once
#include <stdint.h>
// Correlated normal reboot entry point for management opcode 0x03. Only a
// successful host mode transaction can reset profile runtime and reboot.
bool adapter_reboot_for_mode_transaction(uint32_t transaction_id);

View file

@ -2,9 +2,27 @@
#include <stdint.h>
enum class AdapterUsbMode : uint8_t {
kSwitchProbe,
kXInput,
struct AdapterModeAvailability;
// Persisted user selection. Numeric values are part of adapter configuration
// schema v2 and the USB management protocol.
enum class AdapterRequestedMode : uint8_t {
kAuto = 0,
kSwitch = 1,
kXInput = 2,
kDInput = 3,
kMac = 4,
};
// The immutable USB implementation selected before tusb_init(). Auto is a
// requested mode, not an active USB mode.
enum class AdapterUsbMode : uint8_t {
kSwitch = 0,
kSwitchProbe = 1,
kXInput = 2,
};
// Availability of USB mode implementations in this firmware build. All mode
// selection paths consume this single value.
const AdapterModeAvailability& adapter_usb_mode_availability();
AdapterUsbMode adapter_host_probe_mode();

View file

@ -14,7 +14,7 @@
#include <pico/multicore.h>
#include <pico/stdlib.h>
#include <uni.h>
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
#ifdef SWITCH_PICO_USB_OUTPUT_MODES
#include "adapter_usb_mode.h"
#endif
@ -170,12 +170,14 @@ critical_section_t g_state_lock;
BackendSlot g_slots[kSlotCount];
BleIdentityMapping g_ble_identity_mappings[kSlotCount]{};
// These acknowledgement generations and the pairing request producer are only
// used by Core 0. The request is transferred under the cross-core state lock.
// These acknowledgement generations and request producers are only used by
// Core 0. Requests are transferred under the cross-core state lock.
uint32_t g_consumed_generation[kSlotCount]{};
uint32_t g_last_snapshot_generation[kSlotCount]{};
bool g_pairing_window_requested = false;
bool g_clear_pairings_requested = false;
uint32_t g_clear_pairings_requested_token = 0;
uint32_t g_clear_pairings_in_progress_token = 0;
uint32_t g_next_clear_pairings_request_token = 1;
bool g_pairing_snapshot_requested = false;
bool g_initialized = false;
bool g_started = false;
@ -198,7 +200,7 @@ bool g_status_led_on = false;
Bluepad32PairingSnapshot g_pairing_snapshot{};
uint16_t host_rumble_duration_ms() {
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
#ifdef SWITCH_PICO_USB_OUTPUT_MODES
if (adapter_host_probe_mode() == AdapterUsbMode::kXInput) {
return kXInputHostRumbleDurationMs;
}
@ -1074,6 +1076,8 @@ void refresh_pairing_snapshot() {
critical_section_enter_blocking(&g_state_lock);
snapshot.generation = g_pairing_snapshot.generation + 1;
snapshot.completed_clear_pairings_token =
g_pairing_snapshot.completed_clear_pairings_token;
g_pairing_snapshot = snapshot;
g_pairing_snapshot_requested = false;
critical_section_exit(&g_state_lock);
@ -1093,9 +1097,13 @@ void apply_connection_policy();
void process_clear_pairings(uint32_t now_ms) {
uni_hid_device_t* devices[kSlotCount]{};
critical_section_enter_blocking(&g_state_lock);
const bool requested = g_clear_pairings_requested;
g_clear_pairings_requested = false;
if (requested) {
const uint32_t request_token =
g_clear_pairings_requested_token;
if (request_token != 0) {
g_clear_pairings_requested_token = 0;
g_clear_pairings_in_progress_token = request_token;
}
if (request_token != 0) {
g_pairing_window_requested = false;
for (uint8_t slot_index = 0; slot_index < kSlotCount; ++slot_index) {
BackendSlot& slot = g_slots[slot_index];
@ -1113,7 +1121,7 @@ void process_clear_pairings(uint32_t now_ms) {
}
}
critical_section_exit(&g_state_lock);
if (!requested) {
if (request_token == 0) {
return;
}
for (BleIdentityMapping& mapping : g_ble_identity_mappings) {
@ -1136,6 +1144,11 @@ void process_clear_pairings(uint32_t now_ms) {
g_pairing_reset_feedback_deadline_ms =
now_ms + kPairingResetFeedbackDurationMs;
apply_connection_policy();
critical_section_enter_blocking(&g_state_lock);
g_pairing_snapshot.completed_clear_pairings_token =
request_token;
g_clear_pairings_in_progress_token = 0;
critical_section_exit(&g_state_lock);
}
@ -1734,7 +1747,9 @@ void bluepad32_input_backend_init() {
g_pairing_snapshot = {};
g_pairing_snapshot.status =
Bluepad32PairingSnapshotStatus::kPending;
g_clear_pairings_requested = false;
g_clear_pairings_requested_token = 0;
g_clear_pairings_in_progress_token = 0;
g_next_clear_pairings_request_token = 1;
g_connection_status = ConnectionStatus::Initializing;
g_connection_policy_state = ConnectionPolicyState::Uninitialized;
g_pairing_window_deadline_ms = 0;
@ -1773,16 +1788,26 @@ void bluepad32_input_backend_open_pairing_window() {
g_pairing_window_requested = true;
critical_section_exit(&g_state_lock);
}
void bluepad32_input_backend_clear_pairings() {
uint32_t bluepad32_input_backend_clear_pairings() {
if (!g_initialized) {
bluepad32_input_backend_init();
}
critical_section_enter_blocking(&g_state_lock);
g_clear_pairings_requested = true;
g_pairing_snapshot.status =
Bluepad32PairingSnapshotStatus::kPending;
uint32_t request_token = g_clear_pairings_requested_token;
if (request_token == 0) {
request_token = g_clear_pairings_in_progress_token;
}
if (request_token == 0) {
request_token = g_next_clear_pairings_request_token;
g_next_clear_pairings_request_token =
request_token == UINT32_MAX ? 1 : request_token + 1;
g_clear_pairings_requested_token = request_token;
g_pairing_snapshot.status =
Bluepad32PairingSnapshotStatus::kPending;
}
critical_section_exit(&g_state_lock);
return request_token;
}
void bluepad32_input_backend_request_pairing_snapshot() {

View file

@ -29,11 +29,29 @@ struct Bluepad32PairingRecord {
struct Bluepad32PairingSnapshot {
uint32_t generation;
// Unchanged by ordinary refreshes; published only after all clear work.
uint32_t completed_clear_pairings_token;
Bluepad32PairingSnapshotStatus status;
uint8_t record_count;
bool overflow;
Bluepad32PairingRecord records[BLUEPAD32_PAIRING_RECORD_CAPACITY];
};
// Clear tokens form a bounded serial number space over every nonzero uint32_t.
// A completion at most half that space ahead of a request also acknowledges
// the request, including across the UINT32_MAX-to-1 wrap.
constexpr bool bluepad32_input_backend_clear_pairings_completed(
const Bluepad32PairingSnapshot& snapshot, uint32_t request_token) {
const uint32_t completed_token =
snapshot.completed_clear_pairings_token;
if (request_token == 0 || completed_token == 0) {
return false;
}
const uint32_t forward_distance =
completed_token >= request_token
? completed_token - request_token
: (UINT32_MAX - request_token) + completed_token;
return forward_distance <= UINT32_MAX / 2u;
}
struct Bluepad32SlotSnapshot {
bool active;
uint32_t connection_generation;
@ -49,7 +67,9 @@ struct Bluepad32SlotSnapshot {
void bluepad32_input_backend_init();
void bluepad32_input_backend_start();
void bluepad32_input_backend_open_pairing_window();
void bluepad32_input_backend_clear_pairings();
// Repeated calls coalesce until Core 1 completes the operation and return the
// same nonzero token.
uint32_t bluepad32_input_backend_clear_pairings();
void bluepad32_input_backend_snapshot(uint8_t slot,
Bluepad32SlotSnapshot* out);
void bluepad32_input_backend_request_pairing_snapshot();

View file

@ -9,28 +9,83 @@ namespace {
constexpr uint32_t kMinimumCommitIntervalMs = 1000;
enum class PendingWriteOwner : uint8_t {
kNone,
kHost,
kInternal,
kMigration,
};
critical_section_t g_lock;
bool g_prepared = false;
bool g_pre_usb_initialized = false;
bool g_storage_initialized = false;
bool g_storage_core_adopted = false;
bool g_migration_needed = false;
bool g_migration_pending = false;
ConfigurationStorage g_storage;
ConfigurationTransaction g_transaction;
ConfigurationServiceSnapshot g_snapshot;
ConfigurationModeTransactionSnapshot g_host_mode_transaction;
ConfigurationModeTransactionSnapshot g_internal_mode_transaction;
uint8_t g_internal_payload[ADAPTER_CONFIGURATION_ENCODED_SIZE]{};
uint8_t g_migration_payload[ADAPTER_CONFIGURATION_ENCODED_SIZE]{};
uint32_t g_published_reset_generation = 0;
bool g_has_committed = false;
uint32_t g_last_commit_ms = 0;
uint64_t g_latest_mode_transaction_serial = 0;
bool g_recovery_reserved = false;
void publish_storage_snapshot(ConfigurationServiceState state) {
bool transaction_active(ConfigurationTransactionStatus status) {
return status == ConfigurationTransactionStatus::kReceiving ||
status == ConfigurationTransactionStatus::kPending;
}
bool internal_write_pending() {
return g_migration_pending ||
g_internal_mode_transaction.status ==
ConfigurationTransactionStatus::kPending;
}
uint64_t advance_mode_transaction_serial() {
++g_latest_mode_transaction_serial;
if (g_latest_mode_transaction_serial == 0) {
++g_latest_mode_transaction_serial;
}
return g_latest_mode_transaction_serial;
}
bool decode_storage_configuration(
const ConfigurationStorageSnapshot& stored,
AdapterConfiguration* configuration,
bool* migration_needed) {
*configuration = adapter_configuration_default();
*migration_needed = false;
if (!stored.valid) {
return true;
}
if (!adapter_configuration_decode(stored.schema_version, stored.payload,
stored.payload_size, configuration)) {
return false;
}
*migration_needed =
stored.schema_version == ADAPTER_CONFIGURATION_LEGACY_SCHEMA_VERSION;
return true;
}
void publish_storage_snapshot(bool initialized) {
const ConfigurationStorageSnapshot& stored = g_storage.snapshot();
AdapterConfiguration configuration = adapter_configuration_default();
if (stored.valid &&
(stored.schema_version != ADAPTER_CONFIGURATION_SCHEMA_VERSION ||
!adapter_configuration_decode(stored.payload,
stored.payload_size,
&configuration))) {
state = ConfigurationServiceState::kStorageError;
}
bool migration_needed = false;
const bool decoded = initialized &&
decode_storage_configuration(
stored, &configuration, &migration_needed);
critical_section_enter_blocking(&g_lock);
g_snapshot.state = state;
g_storage_initialized = initialized;
g_migration_needed = decoded && migration_needed;
g_snapshot.state = decoded ? ConfigurationServiceState::kReady
: ConfigurationServiceState::kStorageError;
g_snapshot.configuration = configuration;
g_snapshot.generation = stored.valid ? stored.generation : 0;
g_snapshot.payload_crc = stored.valid ? stored.payload_crc : 0;
@ -38,6 +93,54 @@ void publish_storage_snapshot(ConfigurationServiceState state) {
critical_section_exit(&g_lock);
}
void publish_mode_transaction(
const ConfigurationModeTransactionSnapshot& transaction) {
g_snapshot.mode_transaction = transaction;
}
const ConfigurationModeTransactionSnapshot& mode_transaction_for_id(
uint32_t transaction_id) {
return (transaction_id &
CONFIGURATION_SERVICE_INTERNAL_TRANSACTION_ID_MASK) != 0
? g_internal_mode_transaction
: g_host_mode_transaction;
}
bool mode_transaction_succeeded(
const ConfigurationModeTransactionSnapshot& transaction) {
return transaction.status == ConfigurationTransactionStatus::kCommitted ||
transaction.status == ConfigurationTransactionStatus::kUnchanged;
}
ConfigurationTransactionStatus validate_mode_request(
AdapterRequestedMode requested_mode,
const AdapterModeAvailability& availability) {
if (!adapter_requested_mode_valid(requested_mode)) {
return ConfigurationTransactionStatus::kMalformed;
}
if (!adapter_requested_mode_available(requested_mode, availability)) {
return ConfigurationTransactionStatus::kUnsupportedSchema;
}
return ConfigurationTransactionStatus::kIdle;
}
void update_snapshot_from_storage(ConfigurationStorageResult result) {
const ConfigurationStorageSnapshot& stored = g_storage.snapshot();
AdapterConfiguration configuration = adapter_configuration_default();
bool migration_needed = false;
const bool decoded = decode_storage_configuration(
stored, &configuration, &migration_needed);
g_snapshot.state = decoded ? ConfigurationServiceState::kReady
: ConfigurationServiceState::kStorageError;
if (!stored.valid && result == ConfigurationStorageResult::kIoError) {
g_snapshot.state = ConfigurationServiceState::kStorageError;
}
g_snapshot.configuration = configuration;
g_snapshot.generation = stored.valid ? stored.generation : 0;
g_snapshot.payload_crc = stored.valid ? stored.payload_crc : 0;
g_migration_needed = decoded && migration_needed;
}
} // namespace
void configuration_service_prepare() {
@ -46,87 +149,184 @@ void configuration_service_prepare() {
}
critical_section_init(&g_lock);
g_snapshot = {};
__atomic_store_n(&g_published_reset_generation, 0, __ATOMIC_RELAXED);
g_snapshot.configuration = adapter_configuration_default();
g_transaction.clear();
g_host_mode_transaction = {};
g_internal_mode_transaction = {};
g_latest_mode_transaction_serial = 0;
g_recovery_reserved = false;
__atomic_store_n(&g_published_reset_generation, 0, __ATOMIC_RELAXED);
g_prepared = true;
}
void configuration_service_initialize_pre_usb() {
if (!g_prepared) {
configuration_service_prepare();
}
critical_section_enter_blocking(&g_lock);
const bool already_initialized = g_pre_usb_initialized;
if (!already_initialized) {
g_pre_usb_initialized = true;
}
critical_section_exit(&g_lock);
if (already_initialized) {
return;
}
const bool initialized =
g_storage.initialize(pico_configuration_storage_io());
publish_storage_snapshot(initialized);
}
void configuration_service_initialize_on_storage_core() {
if (!g_prepared) {
configuration_service_prepare();
}
const bool initialized =
g_storage.initialize(pico_configuration_storage_io());
publish_storage_snapshot(initialized
? ConfigurationServiceState::kReady
: ConfigurationServiceState::kStorageError);
critical_section_enter_blocking(&g_lock);
const bool pre_usb_initialized = g_pre_usb_initialized;
critical_section_exit(&g_lock);
if (!pre_usb_initialized) {
configuration_service_initialize_pre_usb();
}
critical_section_enter_blocking(&g_lock);
if (g_storage_core_adopted) {
critical_section_exit(&g_lock);
return;
}
g_storage_core_adopted = g_storage_initialized;
if (g_storage_core_adopted && g_migration_needed) {
const AdapterConfiguration configuration = g_snapshot.configuration;
if (adapter_configuration_encode(configuration, g_migration_payload,
sizeof(g_migration_payload))) {
g_migration_pending = true;
} else {
g_snapshot.state = ConfigurationServiceState::kStorageError;
}
}
critical_section_exit(&g_lock);
}
void configuration_service_task_on_storage_core(uint32_t now_ms) {
uint8_t payload[CONFIGURATION_STORAGE_MAX_PAYLOAD_SIZE]{};
uint16_t payload_size = 0;
uint16_t schema_version = 0;
uint16_t schema_version = ADAPTER_CONFIGURATION_SCHEMA_VERSION;
uint32_t transaction_id = 0;
PendingWriteOwner owner = PendingWriteOwner::kNone;
critical_section_enter_blocking(&g_lock);
const ConfigurationTransactionSnapshot transaction =
g_transaction.snapshot();
if (transaction.status == ConfigurationTransactionStatus::kPending &&
(!g_has_committed ||
static_cast<uint32_t>(now_ms - g_last_commit_ms) >=
kMinimumCommitIntervalMs)) {
payload_size = transaction.expected_size;
schema_version = g_transaction.schema_version();
memcpy(payload, g_transaction.payload(), payload_size);
const bool rate_limited =
g_has_committed &&
static_cast<uint32_t>(now_ms - g_last_commit_ms) <
kMinimumCommitIntervalMs;
if (g_storage_core_adopted && !rate_limited) {
if (g_migration_pending) {
owner = PendingWriteOwner::kMigration;
payload_size = sizeof(g_migration_payload);
memcpy(payload, g_migration_payload, payload_size);
} else if (g_internal_mode_transaction.status ==
ConfigurationTransactionStatus::kPending) {
owner = PendingWriteOwner::kInternal;
transaction_id = g_internal_mode_transaction.transaction_id;
payload_size = sizeof(g_internal_payload);
memcpy(payload, g_internal_payload, payload_size);
} else {
const ConfigurationTransactionSnapshot transaction =
g_transaction.snapshot();
if (transaction.status ==
ConfigurationTransactionStatus::kPending) {
owner = PendingWriteOwner::kHost;
transaction_id = transaction.transaction_id;
payload_size = transaction.expected_size;
schema_version = g_transaction.schema_version();
memcpy(payload, g_transaction.payload(), payload_size);
}
}
}
critical_section_exit(&g_lock);
if (payload_size == 0) {
if (owner == PendingWriteOwner::kNone) {
return;
}
const ConfigurationStorageResult result =
g_storage.commit(schema_version, payload, payload_size);
const ConfigurationStorageSnapshot& stored = g_storage.snapshot();
ConfigurationTransactionStatus transaction_status =
ConfigurationTransactionStatus status =
ConfigurationTransactionStatus::kStorageError;
if (result == ConfigurationStorageResult::kOk) {
transaction_status = ConfigurationTransactionStatus::kCommitted;
status = ConfigurationTransactionStatus::kCommitted;
g_has_committed = true;
g_last_commit_ms = now_ms;
} else if (result == ConfigurationStorageResult::kUnchanged) {
transaction_status = ConfigurationTransactionStatus::kUnchanged;
}
AdapterConfiguration configuration{};
ConfigurationServiceState service_state =
ConfigurationServiceState::kStorageError;
if (stored.valid &&
stored.schema_version == ADAPTER_CONFIGURATION_SCHEMA_VERSION &&
adapter_configuration_decode(stored.payload, stored.payload_size,
&configuration)) {
service_state = ConfigurationServiceState::kReady;
} else if (!stored.valid) {
configuration = adapter_configuration_default();
status = ConfigurationTransactionStatus::kUnchanged;
}
critical_section_enter_blocking(&g_lock);
g_transaction.set_result(transaction_status,
stored.valid ? stored.generation : 0,
stored.valid ? stored.payload_crc : 0);
g_snapshot.state = service_state;
g_snapshot.configuration = configuration;
g_snapshot.generation = stored.valid ? stored.generation : 0;
g_snapshot.payload_crc = stored.valid ? stored.payload_crc : 0;
g_snapshot.transaction = g_transaction.snapshot();
update_snapshot_from_storage(result);
if (owner == PendingWriteOwner::kHost) {
const ConfigurationTransactionSnapshot transaction =
g_transaction.snapshot();
if (transaction.transaction_id == transaction_id &&
transaction.status ==
ConfigurationTransactionStatus::kPending) {
g_transaction.set_result(status, g_snapshot.generation,
g_snapshot.payload_crc);
if (g_host_mode_transaction.transaction_id == transaction_id &&
g_host_mode_transaction.status ==
ConfigurationTransactionStatus::kPending) {
g_host_mode_transaction.status = status;
g_host_mode_transaction.stored_generation =
g_snapshot.generation;
if (!g_snapshot.mode_transaction.internal &&
g_snapshot.mode_transaction.transaction_id ==
transaction_id) {
publish_mode_transaction(g_host_mode_transaction);
}
}
}
g_snapshot.transaction = g_transaction.snapshot();
} else if (owner == PendingWriteOwner::kInternal) {
if (g_internal_mode_transaction.transaction_id == transaction_id &&
g_internal_mode_transaction.status ==
ConfigurationTransactionStatus::kPending) {
g_internal_mode_transaction.status = status;
g_internal_mode_transaction.stored_generation =
g_snapshot.generation;
if (g_snapshot.mode_transaction.internal &&
g_snapshot.mode_transaction.transaction_id ==
transaction_id) {
publish_mode_transaction(g_internal_mode_transaction);
}
}
} else {
g_migration_pending = false;
if (status == ConfigurationTransactionStatus::kCommitted ||
status == ConfigurationTransactionStatus::kUnchanged) {
g_migration_needed = false;
}
}
critical_section_exit(&g_lock);
}
ConfigurationTransactionStatus configuration_service_begin(
uint32_t transaction_id, uint16_t schema_version, size_t payload_size,
uint32_t payload_crc) {
if ((transaction_id &
CONFIGURATION_SERVICE_INTERNAL_TRANSACTION_ID_MASK) != 0) {
return ConfigurationTransactionStatus::kMalformed;
}
critical_section_enter_blocking(&g_lock);
if (!g_storage_core_adopted || g_recovery_reserved ||
internal_write_pending()) {
critical_section_exit(&g_lock);
return ConfigurationTransactionStatus::kBusy;
}
const ConfigurationTransactionStatus status = g_transaction.begin(
transaction_id, schema_version, payload_size, payload_crc);
if (status == ConfigurationTransactionStatus::kReceiving) {
advance_mode_transaction_serial();
}
g_snapshot.transaction = g_transaction.snapshot();
critical_section_exit(&g_lock);
return status;
@ -135,7 +335,16 @@ ConfigurationTransactionStatus configuration_service_begin(
ConfigurationTransactionStatus configuration_service_append(
uint32_t transaction_id, size_t offset, const uint8_t* data,
size_t size) {
if ((transaction_id &
CONFIGURATION_SERVICE_INTERNAL_TRANSACTION_ID_MASK) != 0) {
return ConfigurationTransactionStatus::kMalformed;
}
critical_section_enter_blocking(&g_lock);
if (g_recovery_reserved || internal_write_pending()) {
critical_section_exit(&g_lock);
return ConfigurationTransactionStatus::kBusy;
}
const ConfigurationTransactionStatus status =
g_transaction.append(transaction_id, offset, data, size);
g_snapshot.transaction = g_transaction.snapshot();
@ -145,7 +354,16 @@ ConfigurationTransactionStatus configuration_service_append(
ConfigurationTransactionStatus configuration_service_commit(
uint32_t transaction_id) {
if ((transaction_id &
CONFIGURATION_SERVICE_INTERNAL_TRANSACTION_ID_MASK) != 0) {
return ConfigurationTransactionStatus::kMalformed;
}
critical_section_enter_blocking(&g_lock);
if (g_recovery_reserved || internal_write_pending()) {
critical_section_exit(&g_lock);
return ConfigurationTransactionStatus::kBusy;
}
const ConfigurationTransactionStatus status =
g_transaction.finish(transaction_id);
g_snapshot.transaction = g_transaction.snapshot();
@ -155,6 +373,12 @@ ConfigurationTransactionStatus configuration_service_commit(
ConfigurationTransactionStatus configuration_service_reset(
uint32_t transaction_id) {
if (transaction_id == 0 ||
(transaction_id &
CONFIGURATION_SERVICE_INTERNAL_TRANSACTION_ID_MASK) != 0) {
return ConfigurationTransactionStatus::kMalformed;
}
uint8_t payload[ADAPTER_CONFIGURATION_ENCODED_SIZE]{};
const AdapterConfiguration defaults = adapter_configuration_default();
if (!adapter_configuration_encode(defaults, payload, sizeof(payload))) {
@ -163,6 +387,11 @@ ConfigurationTransactionStatus configuration_service_reset(
const uint32_t crc = configuration_crc32(payload, sizeof(payload));
critical_section_enter_blocking(&g_lock);
if (!g_storage_core_adopted || g_recovery_reserved ||
internal_write_pending()) {
critical_section_exit(&g_lock);
return ConfigurationTransactionStatus::kBusy;
}
ConfigurationTransactionStatus status = g_transaction.begin(
transaction_id, ADAPTER_CONFIGURATION_SCHEMA_VERSION,
sizeof(payload), crc);
@ -173,6 +402,9 @@ ConfigurationTransactionStatus configuration_service_reset(
if (status == ConfigurationTransactionStatus::kReceiving) {
status = g_transaction.finish(transaction_id);
}
if (status == ConfigurationTransactionStatus::kPending) {
advance_mode_transaction_serial();
}
if (status == ConfigurationTransactionStatus::kPending &&
g_snapshot.reset_generation != UINT32_MAX) {
++g_snapshot.reset_generation;
@ -186,6 +418,204 @@ ConfigurationTransactionStatus configuration_service_reset(
return status;
}
ConfigurationTransactionStatus configuration_service_set_mode(
uint32_t transaction_id, AdapterRequestedMode requested_mode,
const AdapterModeAvailability& availability) {
if (transaction_id == 0 ||
(transaction_id &
CONFIGURATION_SERVICE_INTERNAL_TRANSACTION_ID_MASK) != 0) {
return ConfigurationTransactionStatus::kMalformed;
}
critical_section_enter_blocking(&g_lock);
if (g_recovery_reserved) {
critical_section_exit(&g_lock);
return ConfigurationTransactionStatus::kBusy;
}
const ConfigurationTransactionStatus validation =
validate_mode_request(requested_mode, availability);
if (validation != ConfigurationTransactionStatus::kIdle) {
critical_section_exit(&g_lock);
return validation;
}
if (g_host_mode_transaction.transaction_id == transaction_id) {
const ConfigurationTransactionStatus status =
g_host_mode_transaction.requested_mode == requested_mode
? g_host_mode_transaction.status
: ConfigurationTransactionStatus::kMalformed;
critical_section_exit(&g_lock);
return status;
}
if (!g_storage_core_adopted || internal_write_pending() ||
transaction_active(g_transaction.snapshot().status)) {
critical_section_exit(&g_lock);
return ConfigurationTransactionStatus::kBusy;
}
if (g_snapshot.state != ConfigurationServiceState::kReady) {
critical_section_exit(&g_lock);
return ConfigurationTransactionStatus::kStorageError;
}
AdapterConfiguration configuration = g_snapshot.configuration;
configuration.requested_mode = requested_mode;
uint8_t payload[ADAPTER_CONFIGURATION_ENCODED_SIZE]{};
if (!adapter_configuration_encode(configuration, payload,
sizeof(payload))) {
critical_section_exit(&g_lock);
return ConfigurationTransactionStatus::kMalformed;
}
const uint32_t crc = configuration_crc32(payload, sizeof(payload));
ConfigurationTransactionStatus status = g_transaction.begin(
transaction_id, ADAPTER_CONFIGURATION_SCHEMA_VERSION,
sizeof(payload), crc);
if (status == ConfigurationTransactionStatus::kReceiving) {
status = g_transaction.append(transaction_id, 0, payload,
sizeof(payload));
}
if (status == ConfigurationTransactionStatus::kReceiving) {
status = g_transaction.finish(transaction_id);
}
if (status == ConfigurationTransactionStatus::kPending) {
g_host_mode_transaction = {
transaction_id,
requested_mode,
0,
advance_mode_transaction_serial(),
status,
false,
};
publish_mode_transaction(g_host_mode_transaction);
}
g_snapshot.transaction = g_transaction.snapshot();
critical_section_exit(&g_lock);
return status;
}
ConfigurationTransactionStatus configuration_service_set_mode_internal(
uint32_t transaction_id, AdapterRequestedMode requested_mode,
const AdapterModeAvailability& availability) {
if ((transaction_id &
CONFIGURATION_SERVICE_INTERNAL_TRANSACTION_ID_MASK) == 0) {
return ConfigurationTransactionStatus::kMalformed;
}
critical_section_enter_blocking(&g_lock);
if (g_recovery_reserved &&
requested_mode != AdapterRequestedMode::kAuto) {
critical_section_exit(&g_lock);
return ConfigurationTransactionStatus::kBusy;
}
const ConfigurationTransactionStatus validation =
validate_mode_request(requested_mode, availability);
if (validation != ConfigurationTransactionStatus::kIdle) {
critical_section_exit(&g_lock);
return validation;
}
if (g_internal_mode_transaction.transaction_id == transaction_id) {
const ConfigurationTransactionStatus status =
g_internal_mode_transaction.requested_mode == requested_mode
? g_internal_mode_transaction.status
: ConfigurationTransactionStatus::kMalformed;
critical_section_exit(&g_lock);
return status;
}
if (!g_storage_core_adopted || g_migration_pending ||
g_internal_mode_transaction.status ==
ConfigurationTransactionStatus::kPending ||
transaction_active(g_transaction.snapshot().status)) {
critical_section_exit(&g_lock);
return ConfigurationTransactionStatus::kBusy;
}
if (g_snapshot.state != ConfigurationServiceState::kReady) {
critical_section_exit(&g_lock);
return ConfigurationTransactionStatus::kStorageError;
}
AdapterConfiguration configuration = g_snapshot.configuration;
configuration.requested_mode = requested_mode;
if (!adapter_configuration_encode(configuration, g_internal_payload,
sizeof(g_internal_payload))) {
critical_section_exit(&g_lock);
return ConfigurationTransactionStatus::kMalformed;
}
g_internal_mode_transaction = {
transaction_id,
requested_mode,
0,
advance_mode_transaction_serial(),
ConfigurationTransactionStatus::kPending,
true,
};
publish_mode_transaction(g_internal_mode_transaction);
critical_section_exit(&g_lock);
return ConfigurationTransactionStatus::kPending;
}
void configuration_service_reserve_for_recovery() {
critical_section_enter_blocking(&g_lock);
if (!g_recovery_reserved) {
g_recovery_reserved = true;
advance_mode_transaction_serial();
}
const ConfigurationTransactionSnapshot transaction =
g_transaction.snapshot();
if (transaction_active(transaction.status)) {
g_transaction.set_result(ConfigurationTransactionStatus::kBusy, 0, 0);
if (g_host_mode_transaction.transaction_id ==
transaction.transaction_id &&
transaction_active(g_host_mode_transaction.status)) {
g_host_mode_transaction.status =
ConfigurationTransactionStatus::kBusy;
g_host_mode_transaction.stored_generation = 0;
if (!g_snapshot.mode_transaction.internal &&
g_snapshot.mode_transaction.transaction_id ==
transaction.transaction_id) {
publish_mode_transaction(g_host_mode_transaction);
}
}
g_snapshot.transaction = g_transaction.snapshot();
}
critical_section_exit(&g_lock);
}
bool configuration_service_mode_transaction_status(
uint32_t transaction_id, ConfigurationTransactionStatus* output) {
if (transaction_id == 0 || output == nullptr) {
return false;
}
critical_section_enter_blocking(&g_lock);
const ConfigurationModeTransactionSnapshot& transaction =
mode_transaction_for_id(transaction_id);
const bool found = transaction.transaction_id == transaction_id;
if (found) {
*output = transaction.status;
}
critical_section_exit(&g_lock);
return found;
}
bool configuration_service_mode_transaction_reboot_ready(
uint32_t transaction_id) {
if (transaction_id == 0) {
return false;
}
critical_section_enter_blocking(&g_lock);
const ConfigurationModeTransactionSnapshot& transaction =
mode_transaction_for_id(transaction_id);
const bool ready =
transaction.transaction_id == transaction_id &&
mode_transaction_succeeded(transaction) &&
transaction.accepted_serial ==
g_latest_mode_transaction_serial &&
transaction.stored_generation == g_snapshot.generation &&
transaction.requested_mode == g_snapshot.configuration.requested_mode;
critical_section_exit(&g_lock);
return ready;
}
void configuration_service_snapshot(ConfigurationServiceSnapshot* output) {
if (output == nullptr) {
return;

View file

@ -6,12 +6,25 @@
#include "adapter_configuration.h"
#include "configuration_transaction.h"
constexpr uint32_t CONFIGURATION_SERVICE_INTERNAL_TRANSACTION_ID_MASK =
0x80000000u;
enum class ConfigurationServiceState : uint8_t {
kLoading = 0,
kReady = 1,
kStorageError = 2,
};
struct ConfigurationModeTransactionSnapshot {
uint32_t transaction_id = 0;
AdapterRequestedMode requested_mode = AdapterRequestedMode::kAuto;
uint32_t stored_generation = 0;
uint64_t accepted_serial = 0;
ConfigurationTransactionStatus status =
ConfigurationTransactionStatus::kIdle;
bool internal = false;
};
struct ConfigurationServiceSnapshot {
ConfigurationServiceState state = ConfigurationServiceState::kLoading;
AdapterConfiguration configuration{};
@ -19,9 +32,11 @@ struct ConfigurationServiceSnapshot {
uint32_t payload_crc = 0;
uint32_t reset_generation = 0;
ConfigurationTransactionSnapshot transaction{};
ConfigurationModeTransactionSnapshot mode_transaction{};
};
void configuration_service_prepare();
void configuration_service_initialize_pre_usb();
void configuration_service_initialize_on_storage_core();
void configuration_service_task_on_storage_core(uint32_t now_ms);
@ -35,6 +50,19 @@ ConfigurationTransactionStatus configuration_service_commit(
uint32_t transaction_id);
ConfigurationTransactionStatus configuration_service_reset(
uint32_t transaction_id);
ConfigurationTransactionStatus configuration_service_set_mode(
uint32_t transaction_id, AdapterRequestedMode requested_mode,
const AdapterModeAvailability& availability);
ConfigurationTransactionStatus configuration_service_set_mode_internal(
uint32_t transaction_id, AdapterRequestedMode requested_mode,
const AdapterModeAvailability& availability);
// Permanently reserves configuration writes for physical recovery. Active
// host work is canceled; only an internal Auto mode transaction remains legal.
void configuration_service_reserve_for_recovery();
bool configuration_service_mode_transaction_status(
uint32_t transaction_id, ConfigurationTransactionStatus* output);
bool configuration_service_mode_transaction_reboot_ready(
uint32_t transaction_id);
void configuration_service_snapshot(ConfigurationServiceSnapshot* output);
// Lock-free publication for the report path. The value changes as soon as a

View file

@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Manage switch-pico persistent configuration, profiles, and pairings."""
"""Manage switch-pico USB modes, configuration, profiles, and pairings."""
from __future__ import annotations
@ -28,8 +28,11 @@ MAXIMUM_RESPONSE_SIZE = 293
MAXIMUM_CHUNK_SIZE = 40
USB_TIMEOUT_MS = 1000
DEFAULT_OPERATION_TIMEOUT_SECONDS = 15.0
HOST_TRANSACTION_ID_MASK = 0x7FFFFFFF
OP_INFO = 0x01
OP_MODE_SET = 0x02
OP_REBOOT = 0x03
OP_CONFIGURATION_READ = 0x10
OP_CONFIGURATION_BEGIN = 0x11
OP_CONFIGURATION_CHUNK = 0x12
@ -61,10 +64,21 @@ STATUS_NAMES = {
8: "storage failure",
}
CONFIGURATION_SCHEMA_VERSION = 1
CONFIGURATION_SIZE = 4
CONFIGURATION_SCHEMA_VERSION = 2
CONFIGURATION_SIZE = 8
PAIRING_WINDOW_SECONDS_MIN = 10
PAIRING_WINDOW_SECONDS_MAX = 300
REQUESTED_MODE_AUTO = 0
REQUESTED_MODE_SWITCH = 1
REQUESTED_MODE_XINPUT = 2
REQUESTED_MODE_DINPUT = 3
REQUESTED_MODE_MAC = 4
REQUESTED_MODE_NAMES = ("auto", "switch", "xinput", "dinput", "mac")
SELECTABLE_MODE_NAMES = REQUESTED_MODE_NAMES[:3]
ACTIVE_MODE_SWITCH = 0
ACTIVE_MODE_SWITCH_PROBE = 1
ACTIVE_MODE_XINPUT = 2
ACTIVE_MODE_NAMES = ("Switch", "Switch probe", "XInput")
PAIRING_RECORD_SIZE = 8
PAIRING_RECORD_CAPACITY = 16
TRANSPORT_UNKNOWN = 0
@ -123,6 +137,7 @@ class ConfigManagerError(RuntimeError):
class UsbDevice(Protocol):
bus: int | None
address: int | None
port_numbers: tuple[int, ...] | None
def ctrl_transfer(
self,
@ -154,12 +169,21 @@ class DeviceInfo:
active_mode: int
maximum_configuration_size: int
def mode_name(self) -> str:
try:
return ACTIVE_MODE_NAMES[self.active_mode]
except IndexError as exc:
raise ConfigManagerError(
f"unknown active USB mode {self.active_mode}"
) from exc
@dataclass(frozen=True)
class AdapterConfiguration:
pairing_window_seconds: int
generation: int
crc: int
requested_mode: int = REQUESTED_MODE_AUTO
@dataclass(frozen=True)
@ -1168,6 +1192,10 @@ def _crc32(payload: bytes) -> int:
return zlib.crc32(payload) & 0xFFFFFFFF
def _host_transaction_id() -> int:
return (secrets.randbits(31) & HOST_TRANSACTION_ID_MASK) or 1
def encode_request(operation: int, payload: bytes = b"") -> bytes:
if len(payload) + REQUEST_HEADER_SIZE > MAXIMUM_REQUEST_SIZE:
raise ConfigManagerError("management request exceeds EP0 limit")
@ -1267,8 +1295,11 @@ def _control_out(
def read_info(device: UsbDevice) -> DeviceInfo:
envelope = _control_in(device, OP_INFO)
_raise_status(envelope)
if len(envelope.payload) != 8:
if len(envelope.payload) != 8 or envelope.payload[5] != 0:
raise ConfigManagerError("invalid device-info payload")
active_mode = envelope.payload[4]
if active_mode >= len(ACTIVE_MODE_NAMES):
raise ConfigManagerError(f"unknown active USB mode {active_mode}")
return DeviceInfo(
firmware_version=(
envelope.payload[0],
@ -1276,7 +1307,7 @@ def read_info(device: UsbDevice) -> DeviceInfo:
envelope.payload[2],
),
board=envelope.payload[3],
active_mode=envelope.payload[4],
active_mode=active_mode,
maximum_configuration_size=struct.unpack_from(
"<H", envelope.payload, 6
)[0],
@ -1289,20 +1320,26 @@ def read_configuration(device: UsbDevice) -> AdapterConfiguration:
if (
envelope.schema_version != CONFIGURATION_SCHEMA_VERSION
or len(envelope.payload) != CONFIGURATION_SIZE
or envelope.payload[2:] != b"\x00\x00"
or envelope.payload[3:] != bytes(5)
):
raise ConfigManagerError("unsupported configuration object")
pairing_window_seconds = struct.unpack_from("<H", envelope.payload)[0]
requested_mode = envelope.payload[2]
if not (
PAIRING_WINDOW_SECONDS_MIN
<= pairing_window_seconds
<= PAIRING_WINDOW_SECONDS_MAX
):
raise ConfigManagerError("invalid stored pairing-window duration")
if requested_mode >= len(REQUESTED_MODE_NAMES):
raise ConfigManagerError(
f"invalid stored requested USB mode {requested_mode}"
)
return AdapterConfiguration(
pairing_window_seconds=pairing_window_seconds,
generation=envelope.generation,
crc=envelope.payload_crc,
requested_mode=requested_mode,
)
@ -1340,8 +1377,17 @@ def write_configuration(
raise ConfigManagerError(
"pairing window must be between 10 and 300 seconds"
)
payload = struct.pack("<Hxx", configuration.pairing_window_seconds)
transaction_id = secrets.randbits(32) or 1
if (
type(configuration.requested_mode) is not int
or not 0 <= configuration.requested_mode < len(REQUESTED_MODE_NAMES)
):
raise ConfigManagerError("invalid requested USB mode")
payload = struct.pack(
"<HB5x",
configuration.pairing_window_seconds,
configuration.requested_mode,
)
transaction_id = _host_transaction_id()
_control_out(
device,
OP_CONFIGURATION_BEGIN,
@ -1367,13 +1413,48 @@ def write_configuration(
def reset_configuration(device: UsbDevice, timeout: float) -> TransactionStatus:
transaction_id = secrets.randbits(32) or 1
transaction_id = _host_transaction_id()
_control_out(
device, OP_CONFIGURATION_RESET, struct.pack("<I", transaction_id)
)
return _wait_for_transaction(device, transaction_id, timeout)
def set_mode(
device: UsbDevice, requested_mode: int, timeout: float
) -> TransactionStatus:
if type(requested_mode) is not int or requested_mode not in (
REQUESTED_MODE_AUTO,
REQUESTED_MODE_SWITCH,
REQUESTED_MODE_XINPUT,
):
raise ConfigManagerError("requested USB mode is not available")
transaction_id = _host_transaction_id()
_control_out(
device,
OP_MODE_SET,
struct.pack("<IB", transaction_id, requested_mode),
)
return _wait_for_transaction(device, transaction_id, timeout)
def request_reboot(device: UsbDevice, transaction_id: int) -> None:
_require_int(
transaction_id, "transaction ID", 1, HOST_TRANSACTION_ID_MASK
)
_control_out(device, OP_REBOOT, struct.pack("<I", transaction_id))
def _mode_is_active(requested_mode: int, active_mode: int) -> bool:
if requested_mode == REQUESTED_MODE_AUTO:
return active_mode in (ACTIVE_MODE_SWITCH_PROBE, ACTIVE_MODE_XINPUT)
if requested_mode == REQUESTED_MODE_SWITCH:
return active_mode == ACTIVE_MODE_SWITCH
if requested_mode == REQUESTED_MODE_XINPUT:
return active_mode == ACTIVE_MODE_XINPUT
return False
def parse_profile_list(envelope: Envelope) -> tuple[ProfileListEntry, ...]:
_raise_status(envelope)
if envelope.schema_version not in (
@ -1683,6 +1764,220 @@ def find_pico(
raise ConfigManagerError("no USB-connected switch-pico firmware found")
_UsbPhysicalLocation = tuple[int, tuple[int, ...]]
_UsbEnumerationIdentity = tuple[int, int]
@dataclass(frozen=True)
class _ReenumerationSnapshot:
previous_device: UsbDevice
previous_bus: int | None
previous_address: int | None
selected_location: _UsbPhysicalLocation | None
other_locations: frozenset[_UsbPhysicalLocation]
other_enumerations: frozenset[_UsbEnumerationIdentity]
def _physical_location(device: UsbDevice) -> _UsbPhysicalLocation | None:
bus = getattr(device, "bus", None)
try:
port_numbers = getattr(device, "port_numbers", None)
except (AttributeError, NotImplementedError):
return None
if bus is None or port_numbers is None:
return None
ports = tuple(port_numbers)
if not ports:
return None
return bus, ports
def _enumeration_identity(
device: UsbDevice,
) -> _UsbEnumerationIdentity | None:
bus = getattr(device, "bus", None)
address = getattr(device, "address", None)
if bus is None or address is None:
return None
return bus, address
def _capture_reenumeration_snapshot(
previous_device: UsbDevice,
) -> _ReenumerationSnapshot:
previous_bus = getattr(previous_device, "bus", None)
previous_address = getattr(previous_device, "address", None)
previous_enumeration = _enumeration_identity(previous_device)
selected_location = _physical_location(previous_device)
other_locations: set[_UsbPhysicalLocation] = set()
other_enumerations: set[_UsbEnumerationIdentity] = set()
other_count = 0
for device in _candidate_devices():
enumeration = _enumeration_identity(device)
location = _physical_location(device)
if (
device is previous_device
or (
previous_enumeration is not None
and enumeration == previous_enumeration
)
or (
selected_location is not None
and location == selected_location
)
):
continue
other_count += 1
if location is not None:
other_locations.add(location)
if enumeration is not None:
other_enumerations.add(enumeration)
if selected_location is None and other_count:
raise ConfigManagerError(
"USB port topology is unavailable; cannot safely reboot while "
"multiple switch-pico adapters are connected"
)
return _ReenumerationSnapshot(
previous_device,
previous_bus,
previous_address,
selected_location,
frozenset(other_locations),
frozenset(other_enumerations),
)
def _is_previous_enumeration(
device: UsbDevice, snapshot: _ReenumerationSnapshot
) -> bool:
if device is snapshot.previous_device:
return True
identity = _enumeration_identity(device)
return (
identity is not None
and snapshot.previous_bus is not None
and snapshot.previous_address is not None
and identity == (snapshot.previous_bus, snapshot.previous_address)
)
def _is_reenumeration_candidate(
device: UsbDevice, snapshot: _ReenumerationSnapshot
) -> bool:
location = _physical_location(device)
enumeration = _enumeration_identity(device)
if location is not None:
if location in snapshot.other_locations:
return False
elif enumeration is not None and enumeration in snapshot.other_enumerations:
return False
if snapshot.selected_location is not None:
return location == snapshot.selected_location
return (
snapshot.previous_bus is None
or getattr(device, "bus", None) == snapshot.previous_bus
)
def _wait_for_reenumeration(
snapshot: _ReenumerationSnapshot, timeout: float
) -> UsbDevice:
deadline = time.monotonic() + timeout
disappeared = False
failures: list[Exception] = []
while True:
candidates = list(_candidate_devices())
if not disappeared and not any(
_is_previous_enumeration(device, snapshot)
for device in candidates
):
disappeared = True
if disappeared:
matches: list[UsbDevice] = []
for device in candidates:
if not _is_reenumeration_candidate(device, snapshot):
continue
try:
_ = read_info(device)
except (ConfigManagerError, usb.core.USBError) as exc:
failures.append(exc)
continue
matches.append(device)
if len(matches) == 1:
return matches[0]
if len(matches) > 1:
locations = ", ".join(
f"{device.bus}:{device.address}" for device in matches
)
if snapshot.selected_location is None:
raise ConfigManagerError(
"USB port topology is unavailable; multiple "
"switch-pico devices make reboot identity ambiguous "
f"({locations})"
)
raise ConfigManagerError(
"multiple switch-pico devices re-enumerated on the "
f"selected USB port ({locations})"
)
if time.monotonic() >= deadline:
break
time.sleep(0.05)
if not disappeared:
raise ConfigManagerError(
"Pico did not disappear from USB after the reboot request"
)
if failures:
raise ConfigManagerError(
"Pico re-enumerated on the selected USB port, but did not accept "
f"the management request; last error: {failures[-1]}"
) from failures[-1]
if snapshot.selected_location is not None:
raise ConfigManagerError(
"Pico did not re-enumerate on its original physical USB port "
"after reboot"
)
raise ConfigManagerError("Pico did not re-enumerate after reboot")
def configure_mode(
device: UsbDevice, requested_mode: int, timeout: float
) -> tuple[UsbDevice, bool]:
if type(requested_mode) is not int or requested_mode not in (
REQUESTED_MODE_AUTO,
REQUESTED_MODE_SWITCH,
REQUESTED_MODE_XINPUT,
):
raise ConfigManagerError("requested USB mode is not available")
before_info = read_info(device)
before_configuration = read_configuration(device)
if (
before_configuration.requested_mode == requested_mode
and _mode_is_active(requested_mode, before_info.active_mode)
):
return device, False
reenumeration_snapshot = _capture_reenumeration_snapshot(device)
transaction = set_mode(device, requested_mode, timeout)
request_reboot(device, transaction.transaction_id)
reenumerated = _wait_for_reenumeration(
reenumeration_snapshot, timeout
)
after_info = read_info(reenumerated)
after_configuration = read_configuration(reenumerated)
if after_configuration.requested_mode != requested_mode:
raise ConfigManagerError(
"requested USB mode was not stored after reboot"
)
if not _mode_is_active(requested_mode, after_info.active_mode):
raise ConfigManagerError(
f"device activated {after_info.mode_name()} instead of "
f"{REQUESTED_MODE_NAMES[requested_mode]}"
)
return reenumerated, True
def _print_pairings(snapshot: PairingSnapshot) -> None:
if not snapshot.records:
print("No stored pairings.")
@ -1780,8 +2075,8 @@ def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
prog="switch-pico-config",
description=(
"Manage switch-pico persistent configuration, profiles, "
"and pairings."
"Manage switch-pico USB modes, persistent configuration, "
"profiles, and pairings."
),
)
parser.add_argument("--bus", type=int, help="USB bus number")
@ -1797,6 +2092,8 @@ def build_parser() -> argparse.ArgumentParser:
)
commands = parser.add_subparsers(dest="command", required=True)
commands.add_parser("status", help="show firmware and configuration status")
mode = commands.add_parser("mode", help="select the persistent USB mode")
mode.add_argument("mode", choices=SELECTABLE_MODE_NAMES)
config = commands.add_parser("config", help="read or change configuration")
config_commands = config.add_subparsers(dest="config_command", required=True)
@ -1900,22 +2197,36 @@ def main(argv: Sequence[str] | None = None) -> int:
info = read_info(device)
configuration = read_configuration(device)
version = ".".join(str(part) for part in info.firmware_version)
mode = "XInput" if info.active_mode else "Switch"
print(f"Firmware: {version}")
print(f"Board: Pico 2 W ({info.board})")
print(f"Active USB mode: {mode}")
print(
"Requested USB mode: "
f"{REQUESTED_MODE_NAMES[configuration.requested_mode]}"
)
print(f"Active USB mode: {info.mode_name()}")
print(f"Configuration generation: {configuration.generation}")
print(f"Configuration CRC: {configuration.crc:08x}")
print(
"Pairing window: "
f"{configuration.pairing_window_seconds} seconds"
)
elif args.command == "mode":
requested_mode = REQUESTED_MODE_NAMES.index(args.mode)
_, changed = configure_mode(device, requested_mode, args.timeout)
if changed:
print(f"USB mode changed to {args.mode}.")
else:
print(f"USB mode is already {args.mode}.")
elif args.command == "config":
if args.config_command == "show":
configuration = read_configuration(device)
print(
f"pairing_window_seconds={configuration.pairing_window_seconds}"
)
print(
"requested_mode="
f"{REQUESTED_MODE_NAMES[configuration.requested_mode]}"
)
print(f"generation={configuration.generation}")
print(f"crc={configuration.crc:08x}")
elif args.config_command == "set":
@ -1926,6 +2237,7 @@ def main(argv: Sequence[str] | None = None) -> int:
pairing_window_seconds=args.pairing_window_seconds,
generation=before.generation,
crc=before.crc,
requested_mode=before.requested_mode,
),
args.timeout,
)

View file

@ -7,12 +7,10 @@
#ifndef SWITCH_PICO_BLUEPAD32
#include "hardware/uart.h"
#else
#include "adapter_mode_controller.h"
#include "bluepad32_input_backend.h"
#include "controller_profile_runtime.h"
#include "bootsel_pairing_button.h"
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
#include "adapter_host_probe.h"
#endif
#include "controller_profile_runtime.h"
#endif
#ifdef SWITCH_PICO_LOG
@ -191,14 +189,9 @@ static void log_usb_state() {
const bool ready = usb_output_driver_is_ready(instance);
if (ready != g_last_ready[instance]) {
g_last_ready[instance] = ready;
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
LOG_PRINTF("[%s %u] driver %s\n",
usb_output_driver_name(), instance,
ready ? "ready" : "not ready");
#else
LOG_PRINTF("[SWITCH %u] driver %s\n", instance,
ready ? "ready (handshake OK)" : "not ready");
#endif
}
}
#else
@ -218,19 +211,12 @@ int main() {
#ifdef SWITCH_PICO_BLUEPAD32
bluepad32_input_backend_init();
controller_profile_runtime_reset();
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
adapter_host_probe_init();
#endif
adapter_mode_controller_initialize_usb();
#else
init_uart_input();
#endif
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
usb_output_driver_init(adapter_host_probe_mode());
#else
usb_output_driver_init(AdapterUsbMode::kSwitchProbe);
#endif
usb_output_driver_init(AdapterUsbMode::kSwitch);
tusb_init();
#endif
#ifdef SWITCH_PICO_BLUEPAD32
for (uint8_t instance = 0;
instance < BLUEPAD32_INPUT_BACKEND_SLOT_COUNT; ++instance) {
@ -253,12 +239,8 @@ int main() {
#ifdef SWITCH_PICO_BLUEPAD32
bluepad32_input_backend_start();
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
LOG_PRINTF("[BOOT] adapter feasibility mode=%s\n",
LOG_PRINTF("[BOOT] adapter mode=%s\n",
usb_output_driver_mode_name());
#else
LOG_PRINTF("[BOOT] switch-pico starting (Bluepad32 wireless @ 115200)\n");
#endif
#else
LOG_PRINTF("[BOOT] switch-pico starting (UART0 log @ 115200)\n");
LOG_PRINTF("[INFO] UART1 pins TX=%d RX=%d baud=%d\n",
@ -274,7 +256,7 @@ int main() {
bluepad32_input_backend_open_pairing_window();
break;
case BootselPairingButtonEvent::kClearPairings:
bluepad32_input_backend_clear_pairings();
adapter_mode_controller_begin_recovery();
break;
case BootselPairingButtonEvent::kNone:
break;
@ -286,6 +268,11 @@ int main() {
instance < BLUEPAD32_INPUT_BACKEND_SLOT_COUNT; ++instance) {
Bluepad32SlotSnapshot snapshot{};
bluepad32_input_backend_snapshot(instance, &snapshot);
adapter_mode_controller_process_input(
instance, snapshot.active,
snapshot.connection_generation,
&snapshot.pre_hotkey_button_mask, now_ms,
&snapshot.state);
const ControllerProfileTransformResult transformed =
controller_profile_runtime_transform(
instance, snapshot, now_ms, output_mode);
@ -318,6 +305,7 @@ int main() {
bluepad32_input_backend_report_sent(instance);
}
}
adapter_mode_controller_task(now_ms);
#else
bool new_data = poll_uart_frames(); // Pull controller state from UART1
(void)new_data;

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;

View file

@ -3,10 +3,11 @@
#include <string.h>
#include "adapter_configuration.h"
#include "tusb.h"
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
#include "adapter_host_probe.h"
#endif
#include "adapter_reboot.h"
#include "adapter_usb_mode.h"
#include "tusb.h"
#include "usb_output_driver.h"
namespace UsbConfigurationManagement {
namespace {
@ -74,6 +75,10 @@ Status profile_service_status(const ProfileServiceMetadata& metadata) {
bool valid_out_size(Operation operation, size_t size) {
switch (operation) {
case Operation::kModeSet:
return size == kRequestHeaderSize + 5;
case Operation::kReboot:
return size == kRequestHeaderSize + 4;
case Operation::kConfigurationBegin:
return size == kRequestHeaderSize + 12;
case Operation::kProfileBegin:
@ -147,11 +152,7 @@ size_t encode_transaction(uint8_t* output, size_t output_size) {
size_t encode_info(uint8_t* output, size_t output_size) {
uint8_t payload[8] = {
0, 2, 0, 2,
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
adapter_host_probe_mode() == AdapterUsbMode::kXInput ? 1u : 0u,
#else
0,
#endif
static_cast<uint8_t>(usb_output_driver_mode()),
0,
static_cast<uint8_t>(CONFIGURATION_STORAGE_MAX_PAYLOAD_SIZE),
static_cast<uint8_t>(
@ -329,6 +330,41 @@ bool process_out_request() {
const uint8_t* payload = request.payload;
switch (request.operation) {
case Operation::kModeSet: {
const uint32_t transaction_id =
static_cast<uint32_t>(payload[0]) |
(static_cast<uint32_t>(payload[1]) << 8) |
(static_cast<uint32_t>(payload[2]) << 16) |
(static_cast<uint32_t>(payload[3]) << 24);
const AdapterRequestedMode requested_mode =
static_cast<AdapterRequestedMode>(payload[4]);
if (transaction_id == 0 ||
(transaction_id &
CONFIGURATION_SERVICE_INTERNAL_TRANSACTION_ID_MASK) != 0 ||
!adapter_requested_mode_valid(requested_mode)) {
return false;
}
const ConfigurationTransactionStatus status =
configuration_service_set_mode(
transaction_id, requested_mode,
adapter_usb_mode_availability());
return status == ConfigurationTransactionStatus::kPending ||
status == ConfigurationTransactionStatus::kCommitted ||
status == ConfigurationTransactionStatus::kUnchanged;
}
case Operation::kReboot: {
const uint32_t transaction_id =
static_cast<uint32_t>(payload[0]) |
(static_cast<uint32_t>(payload[1]) << 8) |
(static_cast<uint32_t>(payload[2]) << 16) |
(static_cast<uint32_t>(payload[3]) << 24);
if (transaction_id == 0 ||
(transaction_id &
CONFIGURATION_SERVICE_INTERNAL_TRANSACTION_ID_MASK) != 0) {
return false;
}
return adapter_reboot_for_mode_transaction(transaction_id);
}
case Operation::kConfigurationBegin:
configuration_service_begin(
static_cast<uint32_t>(payload[0]) |
@ -482,11 +518,9 @@ bool process_out_request() {
bool usb_configuration_management_vendor_control(
uint8_t rhport, uint8_t stage,
tusb_control_request_t const* request) {
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
if (adapter_host_probe_vendor_control(rhport, stage, request)) {
return true;
}
#endif
using namespace UsbConfigurationManagement;
if (request == nullptr ||
request->bmRequestType_bit.type != TUSB_REQ_TYPE_VENDOR ||

View file

@ -30,6 +30,8 @@ static_assert(kMaximumResponseSize == 293,
enum class Operation : uint8_t {
kInfo = 0x01,
kModeSet = 0x02,
kReboot = 0x03,
kConfigurationRead = 0x10,
kConfigurationBegin = 0x11,
kConfigurationChunk = 0x12,

View file

@ -12,7 +12,7 @@
#include "usb_configuration_management.h"
#endif
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
#ifdef SWITCH_PICO_USB_OUTPUT_MODES
#include "adapter_host_probe.h"
#include "xinput_descriptors.h"
#include "xinput_driver.h"
@ -26,11 +26,11 @@
namespace {
AdapterUsbMode g_mode = AdapterUsbMode::kSwitchProbe;
AdapterUsbMode g_mode = AdapterUsbMode::kSwitch;
uint16_t g_string_descriptor[32]{};
bool xinput_selected() {
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
#ifdef SWITCH_PICO_USB_OUTPUT_MODES
return g_mode == AdapterUsbMode::kXInput;
#else
return false;
@ -40,16 +40,16 @@ bool xinput_selected() {
} // namespace
void usb_output_driver_init(AdapterUsbMode mode) {
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
#ifdef SWITCH_PICO_USB_OUTPUT_MODES
g_mode = mode;
#else
(void)mode;
g_mode = AdapterUsbMode::kSwitchProbe;
g_mode = AdapterUsbMode::kSwitch;
#endif
for (uint8_t instance = 0;
instance < SWITCH_PICO_HID_INSTANCE_COUNT; ++instance) {
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
#ifdef SWITCH_PICO_USB_OUTPUT_MODES
if (xinput_selected()) {
xinput_init(instance);
} else
@ -61,7 +61,15 @@ void usb_output_driver_init(AdapterUsbMode mode) {
}
const char* usb_output_driver_mode_name() {
return xinput_selected() ? "XInput" : "Switch probe";
switch (g_mode) {
case AdapterUsbMode::kSwitch:
return "Switch";
case AdapterUsbMode::kSwitchProbe:
return "Switch probe";
case AdapterUsbMode::kXInput:
return "XInput";
}
return "Switch";
}
AdapterUsbMode usb_output_driver_mode() {
@ -76,7 +84,7 @@ void usb_output_driver_set_input(uint8_t instance,
const ControllerState& state,
uint16_t left_trigger_threshold,
uint16_t right_trigger_threshold) {
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
#ifdef SWITCH_PICO_USB_OUTPUT_MODES
if (xinput_selected()) {
xinput_set_input(instance, state);
return;
@ -87,7 +95,7 @@ void usb_output_driver_set_input(uint8_t instance,
}
bool usb_output_driver_task(uint8_t instance) {
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
#ifdef SWITCH_PICO_USB_OUTPUT_MODES
if (xinput_selected()) {
return xinput_task(instance);
}
@ -96,7 +104,7 @@ bool usb_output_driver_task(uint8_t instance) {
}
bool usb_output_driver_is_ready(uint8_t instance) {
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
#ifdef SWITCH_PICO_USB_OUTPUT_MODES
if (xinput_selected()) {
return xinput_is_ready(instance);
}
@ -106,7 +114,7 @@ bool usb_output_driver_is_ready(uint8_t instance) {
void usb_output_driver_set_rumble_callback(
uint8_t instance, ControllerRumbleCallback callback) {
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
#ifdef SWITCH_PICO_USB_OUTPUT_MODES
if (xinput_selected()) {
xinput_set_rumble_callback(instance, callback);
return;
@ -151,19 +159,20 @@ extern "C" uint8_t const* tud_hid_descriptor_report_cb(uint8_t instance) {
}
extern "C" uint8_t const* tud_descriptor_device_cb() {
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
#ifdef SWITCH_PICO_USB_OUTPUT_MODES
if (xinput_selected()) {
return XInput::kDeviceDescriptor;
}
return XInput::kSwitchProbeDeviceDescriptor;
#else
return switch_pro_device_descriptor;
if (g_mode == AdapterUsbMode::kSwitchProbe) {
return XInput::kSwitchProbeDeviceDescriptor;
}
#endif
return switch_pro_device_descriptor;
}
extern "C" uint8_t const* tud_descriptor_configuration_cb(uint8_t index) {
(void)index;
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
#ifdef SWITCH_PICO_USB_OUTPUT_MODES
if (xinput_selected()) {
return XInput::kConfigurationDescriptor;
}
@ -175,9 +184,9 @@ extern "C" uint16_t const* tud_descriptor_string_cb(uint8_t index,
uint16_t langid) {
(void)langid;
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
#ifdef SWITCH_PICO_USB_OUTPUT_MODES
adapter_host_probe_note_string_descriptor(index);
if (index == 0xee) {
if (index == 0xee && g_mode != AdapterUsbMode::kSwitch) {
static constexpr char kSignature[] = "MSFT100";
for (uint8_t i = 0; i < sizeof(kSignature) - 1; ++i) {
g_string_descriptor[1 + i] = kSignature[i];
@ -195,7 +204,7 @@ extern "C" uint16_t const* tud_descriptor_string_cb(uint8_t index,
character_count = 1;
} else {
const uint8_t* string = nullptr;
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
#ifdef SWITCH_PICO_USB_OUTPUT_MODES
static const uint8_t kManufacturer[] = "Switch Pico";
static const uint8_t kProduct[] = "XInput Feasibility";
static const uint8_t kSerial[] = "XINPUT-PROTOTYPE";
@ -279,7 +288,7 @@ extern "C" usbd_class_driver_t const* usbd_app_driver_get_cb(
if (driver_count == nullptr) {
return nullptr;
}
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
#ifdef SWITCH_PICO_USB_OUTPUT_MODES
if (xinput_selected()) {
*driver_count = 1;
return xinput_class_driver();