Add persistent USB output mode selection
This commit is contained in:
parent
31a8cfb9d3
commit
ec9359bce8
35 changed files with 4136 additions and 217 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
430
adapter_mode_controller.cpp
Normal 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
34
adapter_mode_controller.h
Normal 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
7
adapter_reboot.h
Normal 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);
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
193
tests/adapter_host_probe_test.cpp
Normal file
193
tests/adapter_host_probe_test.cpp
Normal 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;
|
||||
}
|
||||
612
tests/adapter_mode_controller_test.cpp
Normal file
612
tests/adapter_mode_controller_test.cpp
Normal 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;
|
||||
}
|
||||
|
|
@ -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() {
|
||||
|
|
|
|||
520
tests/configuration_service_test.cpp
Normal file
520
tests/configuration_service_test.cpp
Normal 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;
|
||||
}
|
||||
|
|
@ -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) ==
|
||||
|
|
|
|||
9
tests/mode_native_stubs/hardware/structs/watchdog.h
Normal file
9
tests/mode_native_stubs/hardware/structs/watchdog.h
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
struct watchdog_hw_t {
|
||||
uint32_t scratch[8];
|
||||
};
|
||||
|
||||
extern watchdog_hw_t* watchdog_hw;
|
||||
5
tests/mode_native_stubs/hardware/watchdog.h
Normal file
5
tests/mode_native_stubs/hardware/watchdog.h
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
void watchdog_reboot(uint32_t pc, uint32_t sp, uint32_t delay_ms);
|
||||
12
tests/mode_native_stubs/pico/time.h
Normal file
12
tests/mode_native_stubs/pico/time.h
Normal 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);
|
||||
33
tests/mode_native_stubs/tusb.h
Normal file
33
tests/mode_native_stubs/tusb.h
Normal 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);
|
||||
32
tests/test_adapter_host_probe_native.py
Normal file
32
tests/test_adapter_host_probe_native.py
Normal 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)
|
||||
32
tests/test_adapter_mode_controller_native.py
Normal file
32
tests/test_adapter_mode_controller_native.py
Normal 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)
|
||||
|
|
@ -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'}",
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
36
tests/test_configuration_service_native.py
Normal file
36
tests/test_configuration_service_native.py
Normal 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
|
||||
)
|
||||
|
|
@ -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}",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 ||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue