Add versioned persistent configuration protocol

This commit is contained in:
Joey Yakimowich-Payne 2026-09-02 15:24:35 -06:00
commit 7afc9981fe
36 changed files with 2784 additions and 756 deletions

View file

@ -47,7 +47,7 @@ The AIO firmware currently has:
- generation-tagged state snapshots and rumble mailboxes across cores
- persistent Classic and BLE bonding
- a physical BOOTSEL pairing window and pairing reset
- endpoint-zero PC pairing management
- versioned endpoint-zero configuration and pairing management
- Switch Pro input, motion, colors, and rumble per slot
- per-controller ABXY and motion hotkeys
@ -65,7 +65,7 @@ The Bluetooth, UART, Switch, and XInput paths now share `ControllerState`:
| Capability | Status | Evidence or remaining work |
|---|---|---|
| Bluetooth Classic and BLE | Complete | Bluepad32 supports both transports; bonds persist across reboot. |
| Pairing gate, reconnect, list, and clear | Complete | Physical BOOTSEL flow and `switch-pico-pairings` are implemented. |
| Pairing gate, reconnect, list, and clear | Complete | Physical BOOTSEL flow and `switch-pico-config pairings` use the versioned management protocol. |
| Four concurrent controllers | Complete for Switch mode | Four independent USB interfaces and Bluetooth slots are implemented. |
| Switch input | Complete | Buttons, sticks, lifecycle, colors, and per-slot isolation are hardware-tested. |
| Switch motion | Complete for supported parsers | DualSense, Switch-family, Wii accelerometer, PS Move, and compatible 8BitDo modes are normalized. |
@ -75,6 +75,7 @@ The Bluetooth, UART, Switch, and XInput paths now share `ControllerState`:
| Automatic Windows/Switch selection | Feasibility complete | Windows enumeration fix is in `db4a860`; real Windows transition and rumble were reported working. |
| Windows feasibility test | Complete | `tools/Test-AdapterFeasibility.ps1` checks transition, PnP health, four XInput slots, controls, and rumble isolation. |
| Protocol-neutral controller state | Complete | `ControllerState` is shared by Bluetooth, UART, Switch, and XInput paths; analog trigger precision is retained. |
| Persistent configuration protocol | Core complete | Two-copy CRC-validated flash storage, chunked transactions, generation recovery, and PC read/write/reset are hardware-tested. |
| Production USB VID/PID | Missing | Prototype uses `CAFE:4010`; obtain an appropriate project VID/PID and repeat Windows binding tests. |
| DInput output | Missing | Add generic HID descriptor and report driver. |
| Mac output mode | Missing | Capture/define compatible descriptor and report semantics. |
@ -246,10 +247,16 @@ Completion evidence:
- Switch buttons, sticks, ZL/ZR, motion, and rumble matched the fixed master baseline
- the scan-latency regression was bisected, fixed, documented, and retested before acceptance
### Phase 2 — Persistent configuration protocol
### Phase 2 — Persistent configuration protocol — Core complete
Generalize endpoint-zero management beyond pairing while keeping Switch USB enumeration unchanged.
The core delivery implements firmware/board/active-mode status, one
versioned adapter configuration object, transactional writes, and pairing
management. Connected-controller metadata and profile operations are added
with the controller identity and profile schemas in Phases 3 and 5. Reboot
operations remain with the guided updater in Phase 6.
Operations:
- firmware and board version
@ -278,16 +285,15 @@ Storage requirements:
- separate flash region from Bluepad32 bond storage
- bounded write frequency
Host tooling:
Current host tooling:
```text
switch-pico-config status
switch-pico-config mode xinput
switch-pico-config profiles list
switch-pico-config profiles export profile.json
switch-pico-config profiles import profile.json
switch-pico-config profiles activate 2
switch-pico-config reboot --bootsel
switch-pico-config config show
switch-pico-config config set --pairing-window-seconds 90
switch-pico-config config reset --yes
switch-pico-config pairings list
switch-pico-config pairings clear --yes
```
Acceptance:
@ -297,6 +303,25 @@ Acceptance:
- Pairing management migrates to the versioned protocol in the same cutover.
- Configuration survives power cycling on hardware.
Core completion evidence:
- the configuration record has version, size, generation, payload CRC, and
header CRC fields with a fixed 512-byte payload ceiling
- two dedicated Pico flash sectors sit immediately before, and cannot overlap,
BTstack's two-sector bond store
- writes target the inactive copy, verify after programming, preserve the old
copy until validation, skip identical values, and allow at most one changed
commit per second
- native tests reject malformed, truncated, out-of-order, oversized,
unsupported-schema, and bad-CRC data and recover from corrupt or interrupted
writes
- pairing list/refresh/clear moved from the old pairing-only requests into the
versioned envelope and passed on hardware with two stored Classic bonds
- 47 tests passed; UART, AIO, and feasibility firmware built
- generation 1 with a 90-second pairing window survived a physical power cycle
and feasibility firmware reflash, after which reset stored the 60-second
default as generation 2
### Phase 3 — Mapping, tuning, profiles, and macros
Use four profile slots per stable controller identity. Resolve identity from Bluetooth transport, identity address, VID, and PID; use a global default when stable identity is unavailable.
@ -511,17 +536,8 @@ Do not mark a host/controller combination complete from descriptor inspection or
## Next action
Begin Phase 2: generalize endpoint-zero management into a versioned persistent
configuration protocol while preserving pairing commands and Switch
enumeration.
The first Phase 2 delivery should remain narrow:
1. define version, size, CRC, generation, and atomic-commit invariants
2. reserve storage that cannot overlap Bluepad32 bonds
3. add read/write/reset operations for one small configuration object
4. migrate pairing management into the versioned envelope
5. verify malformed requests, interrupted writes, rollback, and power-cycle persistence
Do not begin profiles, macros, or tuning transforms until the storage and USB
transaction boundary is proven.
Begin Phase 3 now that the storage and USB transaction boundary is proven.
Define the fixed-capacity profile schema first, then add mapping and tuning
transforms before macros or Turbo. Wire profile list/import/export/activate
commands through the Phase 2 transaction protocol only after each profile
field has runtime semantics and deterministic tests.

View file

@ -100,7 +100,12 @@ if(SWITCH_PICO_INPUT_BACKEND STREQUAL "BLUEPAD32")
target_sources(switch-pico PRIVATE
bluepad32_input_backend.cpp
bootsel_pairing_button.cpp
usb_pairing_management.cpp
adapter_configuration.cpp
configuration_storage.cpp
configuration_transaction.cpp
configuration_service.cpp
pico_configuration_storage.cpp
usb_configuration_management.cpp
)
if(SWITCH_PICO_ADAPTER_FEASIBILITY)
target_sources(switch-pico PRIVATE
@ -123,7 +128,7 @@ else()
endif()
pico_set_program_name(switch-pico "switch-pico")
pico_set_program_version(switch-pico "0.1")
pico_set_program_version(switch-pico "0.2.0")
# Modify the below lines to enable/disable output over UART/USB
# UART0 is enabled for debug logging; USB stdio remains off.

View file

@ -61,7 +61,7 @@ Both `build.py --aio` and direct AIO CMake configuration apply `patches/bluepad3
Pairing order determines the initial USB slot assignment. Up to four controllers map 1:1 to the four emulated Switch Pro Controller interfaces.
While a slot is free, the Pico continuously runs Bluepad32's normal Bluetooth discovery and autoconnect path. Pairing keys persist across Pico power cycles, so reconnect a previously paired controller by pressing its normal Home, PS, or Xbox power button; BOOTSEL is not required. Outside the BOOTSEL window, BTstack remains non-bondable, rejects new Classic SSP or legacy PIN authentication, and disables every BLE STK generation method. A controller in explicit pairing mode therefore cannot create a new Classic or BLE bond while the window is closed.
With no active controller, the Pico runs Bluepad32 discovery and autoconnect. After any controller becomes active, active discovery pauses to protect input, motion, and rumble latency; bonded controllers may still initiate incoming reconnects. Pairing keys persist across Pico power cycles, so reconnect a previously paired controller by pressing its normal Home, PS, or Xbox power button. Hold BOOTSEL for the bounded pairing window before pairing a new controller or a controller that requires host-side discovery. Outside that window, BTstack remains non-bondable and rejects new Classic and BLE authentication.
To clear every stored Classic and BLE pairing without a PC, hold BOOTSEL continuously for 10 seconds. The normal pairing window opens after two seconds; continuing to hold until the LED changes to a rapid blink clears all bonds, disconnects active controllers, publishes neutral state to every slot, and closes new authentication. Release BOOTSEL, open a new pairing window, and pair controllers again.
@ -82,19 +82,25 @@ The Pico 2 W onboard LED reports the overall Bluetooth state:
- **Reconnect a paired controller**: power it on normally with its Home, PS, or Xbox button.
- **8BitDo Ultimate Bluetooth reconnect**: leave its selector in Bluetooth mode, press Home once, then shake it. After an abrupt controller power-off, the Pico can remain solid for up to four seconds while Bluetooth link supervision confirms the disconnect; scanning restarts immediately afterward.
- **Pair a new controller**: hold BOOTSEL until the LED double-blinks, then put the controller into its explicit Bluetooth pairing mode.
- **Pairing window expires**: new authentication is disabled; discovery and remembered-controller autoconnect continue while a slot is free.
- **Pairing window expires**: new authentication and active discovery stop while a controller is active; remembered controllers may still initiate reconnects.
- **Clear all pairings**: hold BOOTSEL continuously for 10 seconds, through the initial double blink, until the rapid confirmation blink starts. All controllers are disconnected and must be paired again.
### Managing pairings from a PC
### Managing configuration and pairings from a PC
Connect the Pico 2 W to the PC while the AIO firmware is running normally; do not enter the ROM BOOTSEL drive. The management command uses private vendor requests on USB endpoint 0, so it does not add an interface or depend on Linux `hidraw` nodes.
Connect the Pico 2 W to the PC while the AIO firmware is running normally; do not enter the ROM BOOTSEL drive. `switch-pico-config` uses versioned private vendor requests on USB endpoint 0, so it does not add an interface or depend on Linux `hidraw` nodes.
```sh
uv run switch-pico-pairings list
uv run switch-pico-pairings clear --yes
uv run switch-pico-config status
uv run switch-pico-config config show
uv run switch-pico-config config set --pairing-window-seconds 90
uv run switch-pico-config config reset --yes
uv run switch-pico-config pairings list
uv run switch-pico-config pairings clear --yes
```
`list` refreshes and prints stored Bluetooth Classic and BLE addresses. `clear --yes` deletes all bonds, disconnects active controllers, closes new authentication, and leaves autoconnect scanning active. The destructive command requires `--yes`. If multiple compatible Picos are attached, select one with `--bus N --address N`; the error lists their locations. USB access errors require permission to the matching `/dev/bus/usb` device.
Configuration records use version, size, generation, and CRC fields. Writes are chunked and atomically committed between two dedicated flash sectors; interrupted or corrupt writes fall back to the previous valid generation. Those sectors do not overlap Bluepad32's separate bond store. Rewriting identical configuration does not consume a flash write, and successful writes are rate-limited.
`pairings list` refreshes and prints stored Bluetooth Classic and BLE addresses. `pairings clear --yes` deletes all bonds, disconnects active controllers, closes new authentication, and resumes discovery because no controllers remain. Destructive commands require `--yes`. If multiple compatible Picos are attached, select one with `--bus N --address N`; the error lists their locations. USB access errors require permission to the matching `/dev/bus/usb` device.
### Per-controller ABXY layout

43
adapter_configuration.cpp Normal file
View file

@ -0,0 +1,43 @@
#include "adapter_configuration.h"
AdapterConfiguration adapter_configuration_default() {
return {};
}
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) {
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[3] = 0;
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;
}

21
adapter_configuration.h Normal file
View file

@ -0,0 +1,21 @@
#pragma once
#include <stddef.h>
#include <stdint.h>
constexpr uint16_t ADAPTER_CONFIGURATION_SCHEMA_VERSION = 1;
constexpr size_t ADAPTER_CONFIGURATION_ENCODED_SIZE = 4;
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;
struct AdapterConfiguration {
uint16_t pairing_window_seconds =
ADAPTER_PAIRING_WINDOW_SECONDS_DEFAULT;
};
AdapterConfiguration adapter_configuration_default();
bool adapter_configuration_encode(const AdapterConfiguration& configuration,
uint8_t* output, size_t output_size);
bool adapter_configuration_decode(const uint8_t* payload, size_t payload_size,
AdapterConfiguration* output);

View file

@ -1,5 +1,6 @@
#include "bluepad32_input_backend.h"
#include "controller_hotkey_config.h"
#include "configuration_service.h"
#include <limits.h>
#include <stddef.h>
@ -28,8 +29,10 @@ constexpr uint16_t kSwitchHostRumbleDurationMs = 50;
constexpr uint16_t kXInputHostRumbleDurationMs = UINT16_MAX;
#endif
constexpr uint32_t kRumblePollIntervalMs = 5;
constexpr uint32_t kConfigurationPollIntervalMs = 50;
constexpr uint8_t kSlotCount = BLUEPAD32_INPUT_BACKEND_SLOT_COUNT;
constexpr uint32_t kPairingWindowDurationMs = 60000;
constexpr uint32_t kDefaultPairingWindowDurationMs =
ADAPTER_PAIRING_WINDOW_SECONDS_DEFAULT * 1000u;
constexpr uint32_t kPairingResetFeedbackDurationMs = 2000;
// Bluetooth Classic units are 0.625 ms: 0x1900 = 4 seconds.
constexpr uint16_t kClassicLinkSupervisionTimeout = 0x1900;
@ -142,11 +145,14 @@ bool g_started = false;
// These fields are only read or written by Core 1 / BTstack.
btstack_timer_source_t g_rumble_timer{};
btstack_timer_source_t g_configuration_timer{};
ConnectionStatus g_connection_status = ConnectionStatus::Initializing;
btstack_packet_callback_registration_t g_pairing_event_callback{};
ConnectionPolicyState g_connection_policy_state =
ConnectionPolicyState::Uninitialized;
uint32_t g_pairing_window_deadline_ms = 0;
uint32_t g_pairing_window_duration_ms =
kDefaultPairingWindowDurationMs;
uint32_t g_pairing_reset_feedback_deadline_ms = 0;
uint16_t g_status_led_tick = 0;
bool g_pairing_window_open = false;
@ -531,8 +537,15 @@ bool update_pairing_window(uint32_t now_ms) {
critical_section_exit(&g_state_lock);
if (requested) {
ConfigurationServiceSnapshot configuration{};
configuration_service_snapshot(&configuration);
g_pairing_window_duration_ms =
static_cast<uint32_t>(
configuration.configuration.pairing_window_seconds) *
1000u;
g_pairing_window_open = true;
g_pairing_window_deadline_ms = now_ms + kPairingWindowDurationMs;
g_pairing_window_deadline_ms =
now_ms + g_pairing_window_duration_ms;
gap_set_bondable_mode(true);
sm_set_accepted_stk_generation_methods(kAllBlePairingMethods);
g_status_led_tick = 0;
@ -719,8 +732,16 @@ void update_status_led() {
}
}
void process_configuration_timer(btstack_timer_source_t* timer) {
configuration_service_task_on_storage_core(
btstack_run_loop_get_time_ms());
btstack_run_loop_set_timer(timer, kConfigurationPollIntervalMs);
btstack_run_loop_add_timer(timer);
}
void process_rumble_timer(btstack_timer_source_t* timer) {
const uint32_t now_ms = btstack_run_loop_get_time_ms();
process_clear_pairings(now_ms);
process_pairing_snapshot_request();
if (update_pairing_window(now_ms)) {
@ -814,6 +835,11 @@ void platform_on_init_complete() {
btstack_run_loop_set_timer_handler(&g_rumble_timer, process_rumble_timer);
btstack_run_loop_set_timer(&g_rumble_timer, kRumblePollIntervalMs);
btstack_run_loop_add_timer(&g_rumble_timer);
btstack_run_loop_set_timer_handler(
&g_configuration_timer, process_configuration_timer);
btstack_run_loop_set_timer(
&g_configuration_timer, kConfigurationPollIntervalMs);
btstack_run_loop_add_timer(&g_configuration_timer);
recompute_connection_status();
}
@ -994,6 +1020,7 @@ uni_platform* get_platform() {
if (!flash_safe_execute_core_init()) {
halt_wireless_backend();
}
configuration_service_initialize_on_storage_core();
if (cyw43_arch_init() != 0) {
halt_wireless_backend();
}
@ -1019,6 +1046,7 @@ void bluepad32_input_backend_init() {
}
critical_section_init(&g_state_lock);
configuration_service_prepare();
for (uint8_t slot_index = 0; slot_index < kSlotCount; ++slot_index) {
BackendSlot& slot = g_slots[slot_index];
slot = {};
@ -1037,6 +1065,8 @@ void bluepad32_input_backend_init() {
g_connection_status = ConnectionStatus::Initializing;
g_connection_policy_state = ConnectionPolicyState::Uninitialized;
g_pairing_window_deadline_ms = 0;
g_pairing_window_duration_ms =
kDefaultPairingWindowDurationMs;
g_pairing_reset_feedback_deadline_ms = 0;
g_pairing_window_open = false;
g_initialized = true;

186
configuration_service.cpp Normal file
View file

@ -0,0 +1,186 @@
#include "configuration_service.h"
#include <string.h>
#include "pico/critical_section.h"
#include "pico_configuration_storage.h"
namespace {
constexpr uint32_t kMinimumCommitIntervalMs = 1000;
critical_section_t g_lock;
bool g_prepared = false;
ConfigurationStorage g_storage;
ConfigurationTransaction g_transaction;
ConfigurationServiceSnapshot g_snapshot;
bool g_has_committed = false;
uint32_t g_last_commit_ms = 0;
void publish_storage_snapshot(ConfigurationServiceState state) {
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;
}
critical_section_enter_blocking(&g_lock);
g_snapshot.state = 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();
critical_section_exit(&g_lock);
}
} // namespace
void configuration_service_prepare() {
if (g_prepared) {
return;
}
critical_section_init(&g_lock);
g_snapshot = {};
g_snapshot.configuration = adapter_configuration_default();
g_transaction.clear();
g_prepared = true;
}
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);
}
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;
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);
}
critical_section_exit(&g_lock);
if (payload_size == 0) {
return;
}
const ConfigurationStorageResult result =
g_storage.commit(schema_version, payload, payload_size);
const ConfigurationStorageSnapshot& stored = g_storage.snapshot();
ConfigurationTransactionStatus transaction_status =
ConfigurationTransactionStatus::kStorageError;
if (result == ConfigurationStorageResult::kOk) {
transaction_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();
}
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();
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) {
critical_section_enter_blocking(&g_lock);
const ConfigurationTransactionStatus status = g_transaction.begin(
transaction_id, schema_version, payload_size, payload_crc);
g_snapshot.transaction = g_transaction.snapshot();
critical_section_exit(&g_lock);
return status;
}
ConfigurationTransactionStatus configuration_service_append(
uint32_t transaction_id, size_t offset, const uint8_t* data,
size_t size) {
critical_section_enter_blocking(&g_lock);
const ConfigurationTransactionStatus status =
g_transaction.append(transaction_id, offset, data, size);
g_snapshot.transaction = g_transaction.snapshot();
critical_section_exit(&g_lock);
return status;
}
ConfigurationTransactionStatus configuration_service_commit(
uint32_t transaction_id) {
critical_section_enter_blocking(&g_lock);
const ConfigurationTransactionStatus status =
g_transaction.finish(transaction_id);
g_snapshot.transaction = g_transaction.snapshot();
critical_section_exit(&g_lock);
return status;
}
ConfigurationTransactionStatus configuration_service_reset(
uint32_t transaction_id) {
uint8_t payload[ADAPTER_CONFIGURATION_ENCODED_SIZE]{};
const AdapterConfiguration defaults = adapter_configuration_default();
if (!adapter_configuration_encode(defaults, payload, sizeof(payload))) {
return ConfigurationTransactionStatus::kMalformed;
}
const uint32_t crc = configuration_crc32(payload, sizeof(payload));
critical_section_enter_blocking(&g_lock);
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);
}
g_snapshot.transaction = g_transaction.snapshot();
critical_section_exit(&g_lock);
return status;
}
void configuration_service_snapshot(ConfigurationServiceSnapshot* output) {
if (output == nullptr) {
return;
}
critical_section_enter_blocking(&g_lock);
*output = g_snapshot;
critical_section_exit(&g_lock);
}

37
configuration_service.h Normal file
View file

@ -0,0 +1,37 @@
#pragma once
#include <stddef.h>
#include <stdint.h>
#include "adapter_configuration.h"
#include "configuration_transaction.h"
enum class ConfigurationServiceState : uint8_t {
kLoading = 0,
kReady = 1,
kStorageError = 2,
};
struct ConfigurationServiceSnapshot {
ConfigurationServiceState state = ConfigurationServiceState::kLoading;
AdapterConfiguration configuration{};
uint32_t generation = 0;
uint32_t payload_crc = 0;
ConfigurationTransactionSnapshot transaction{};
};
void configuration_service_prepare();
void configuration_service_initialize_on_storage_core();
void configuration_service_task_on_storage_core(uint32_t now_ms);
ConfigurationTransactionStatus configuration_service_begin(
uint32_t transaction_id, uint16_t schema_version, size_t payload_size,
uint32_t payload_crc);
ConfigurationTransactionStatus configuration_service_append(
uint32_t transaction_id, size_t offset, const uint8_t* data,
size_t size);
ConfigurationTransactionStatus configuration_service_commit(
uint32_t transaction_id);
ConfigurationTransactionStatus configuration_service_reset(
uint32_t transaction_id);
void configuration_service_snapshot(ConfigurationServiceSnapshot* output);

197
configuration_storage.cpp Normal file
View file

@ -0,0 +1,197 @@
#include "configuration_storage.h"
#include <string.h>
namespace {
constexpr uint8_t kRecordMagic[4] = {'S', 'P', 'C', 'F'};
constexpr uint16_t kRecordFormatVersion = 1;
constexpr size_t kHeaderCrcOffset = 20;
uint16_t read_u16(const uint8_t* input) {
return static_cast<uint16_t>(input[0]) |
static_cast<uint16_t>(input[1] << 8);
}
uint32_t read_u32(const uint8_t* input) {
return static_cast<uint32_t>(input[0]) |
(static_cast<uint32_t>(input[1]) << 8) |
(static_cast<uint32_t>(input[2]) << 16) |
(static_cast<uint32_t>(input[3]) << 24);
}
void write_u16(uint8_t* output, uint16_t value) {
output[0] = static_cast<uint8_t>(value);
output[1] = static_cast<uint8_t>(value >> 8);
}
void write_u32(uint8_t* output, uint32_t value) {
output[0] = static_cast<uint8_t>(value);
output[1] = static_cast<uint8_t>(value >> 8);
output[2] = static_cast<uint8_t>(value >> 16);
output[3] = static_cast<uint8_t>(value >> 24);
}
bool generation_is_newer(uint32_t candidate, uint32_t current) {
const uint32_t difference = candidate - current;
return difference != 0 && difference < 0x80000000u;
}
} // namespace
uint32_t configuration_crc32(const uint8_t* data, size_t size) {
uint32_t crc = 0xffffffffu;
for (size_t index = 0; index < size; ++index) {
crc ^= data[index];
for (uint8_t bit = 0; bit < 8; ++bit) {
const uint32_t mask = 0u - (crc & 1u);
crc = (crc >> 1) ^ (0xedb88320u & mask);
}
}
return ~crc;
}
bool ConfigurationStorage::initialize(const ConfigurationStorageIo& io) {
io_ = io;
snapshot_ = {};
active_copy_ = 0;
initialized_ = io_.read != nullptr && io_.erase != nullptr &&
io_.program != nullptr && io_.page_size != 0 &&
io_.page_size <= CONFIGURATION_STORAGE_MAX_PAGE_SIZE &&
io_.sector_size >= CONFIGURATION_STORAGE_MAX_RECORD_SIZE &&
io_.sector_size % io_.page_size == 0;
if (!initialized_) {
return false;
}
ConfigurationStorageSnapshot copies[CONFIGURATION_STORAGE_COPY_COUNT]{};
const bool first_valid = read_copy(0, &copies[0]);
const bool second_valid = read_copy(1, &copies[1]);
if (first_valid && second_valid) {
active_copy_ = generation_is_newer(copies[1].generation,
copies[0].generation)
? 1
: 0;
snapshot_ = copies[active_copy_];
} else if (first_valid) {
active_copy_ = 0;
snapshot_ = copies[0];
} else if (second_valid) {
active_copy_ = 1;
snapshot_ = copies[1];
}
return true;
}
const ConfigurationStorageSnapshot& ConfigurationStorage::snapshot() const {
return snapshot_;
}
bool ConfigurationStorage::read_copy(
uint8_t copy, ConfigurationStorageSnapshot* output) const {
uint8_t header[CONFIGURATION_STORAGE_RECORD_HEADER_SIZE]{};
if (output == nullptr ||
!io_.read(io_.context, copy, 0, header, sizeof(header)) ||
memcmp(header, kRecordMagic, sizeof(kRecordMagic)) != 0 ||
read_u16(&header[4]) != kRecordFormatVersion ||
read_u16(&header[6]) != CONFIGURATION_STORAGE_RECORD_HEADER_SIZE ||
configuration_crc32(header, kHeaderCrcOffset) !=
read_u32(&header[kHeaderCrcOffset])) {
return false;
}
const uint16_t payload_size = read_u16(&header[8]);
const uint16_t schema_version = read_u16(&header[10]);
if (payload_size == 0 ||
payload_size > CONFIGURATION_STORAGE_MAX_PAYLOAD_SIZE ||
schema_version == 0 ||
CONFIGURATION_STORAGE_RECORD_HEADER_SIZE + payload_size >
io_.sector_size) {
return false;
}
ConfigurationStorageSnapshot candidate{};
if (!io_.read(io_.context, copy,
CONFIGURATION_STORAGE_RECORD_HEADER_SIZE,
candidate.payload, payload_size)) {
return false;
}
candidate.payload_crc = read_u32(&header[16]);
if (configuration_crc32(candidate.payload, payload_size) !=
candidate.payload_crc) {
return false;
}
candidate.valid = true;
candidate.payload_size = payload_size;
candidate.schema_version = schema_version;
candidate.generation = read_u32(&header[12]);
*output = candidate;
return true;
}
ConfigurationStorageResult ConfigurationStorage::commit(
uint16_t schema_version, const uint8_t* payload, size_t payload_size) {
if (!initialized_ || schema_version == 0 || payload == nullptr ||
payload_size == 0 ||
payload_size > CONFIGURATION_STORAGE_MAX_PAYLOAD_SIZE) {
return ConfigurationStorageResult::kInvalidArgument;
}
const uint32_t payload_crc = configuration_crc32(payload, payload_size);
if (snapshot_.valid && snapshot_.schema_version == schema_version &&
snapshot_.payload_size == payload_size &&
snapshot_.payload_crc == payload_crc &&
memcmp(snapshot_.payload, payload, payload_size) == 0) {
return ConfigurationStorageResult::kUnchanged;
}
const size_t program_size =
((CONFIGURATION_STORAGE_RECORD_HEADER_SIZE + payload_size +
io_.page_size - 1) /
io_.page_size) *
io_.page_size;
if (program_size > CONFIGURATION_STORAGE_MAX_PROGRAM_SIZE ||
program_size > io_.sector_size) {
return ConfigurationStorageResult::kInvalidArgument;
}
uint8_t record[CONFIGURATION_STORAGE_MAX_PROGRAM_SIZE];
memset(record, 0xff, sizeof(record));
memcpy(record, kRecordMagic, sizeof(kRecordMagic));
write_u16(&record[4], kRecordFormatVersion);
write_u16(&record[6], CONFIGURATION_STORAGE_RECORD_HEADER_SIZE);
write_u16(&record[8], static_cast<uint16_t>(payload_size));
write_u16(&record[10], schema_version);
const uint32_t generation = snapshot_.valid
? snapshot_.generation + 1u
: 1u;
write_u32(&record[12], generation);
write_u32(&record[16], payload_crc);
write_u32(&record[kHeaderCrcOffset],
configuration_crc32(record, kHeaderCrcOffset));
memcpy(&record[CONFIGURATION_STORAGE_RECORD_HEADER_SIZE], payload,
payload_size);
const uint8_t target_copy = snapshot_.valid ? active_copy_ ^ 1u : 0u;
if (!io_.erase(io_.context, target_copy)) {
return ConfigurationStorageResult::kIoError;
}
for (size_t offset = 0; offset < program_size;
offset += io_.page_size) {
if (!io_.program(io_.context, target_copy, offset,
&record[offset], io_.page_size)) {
return ConfigurationStorageResult::kIoError;
}
}
ConfigurationStorageSnapshot committed{};
if (!read_copy(target_copy, &committed) ||
committed.generation != generation ||
committed.payload_crc != payload_crc) {
return ConfigurationStorageResult::kIoError;
}
active_copy_ = target_copy;
snapshot_ = committed;
return ConfigurationStorageResult::kOk;
}

63
configuration_storage.h Normal file
View file

@ -0,0 +1,63 @@
#pragma once
#include <stddef.h>
#include <stdint.h>
constexpr size_t CONFIGURATION_STORAGE_COPY_COUNT = 2;
constexpr size_t CONFIGURATION_STORAGE_MAX_PAYLOAD_SIZE = 512;
constexpr size_t CONFIGURATION_STORAGE_RECORD_HEADER_SIZE = 24;
constexpr size_t CONFIGURATION_STORAGE_MAX_PAGE_SIZE = 256;
constexpr size_t CONFIGURATION_STORAGE_MAX_RECORD_SIZE =
CONFIGURATION_STORAGE_RECORD_HEADER_SIZE +
CONFIGURATION_STORAGE_MAX_PAYLOAD_SIZE;
constexpr size_t CONFIGURATION_STORAGE_MAX_PROGRAM_SIZE =
((CONFIGURATION_STORAGE_MAX_RECORD_SIZE +
CONFIGURATION_STORAGE_MAX_PAGE_SIZE - 1) /
CONFIGURATION_STORAGE_MAX_PAGE_SIZE) *
CONFIGURATION_STORAGE_MAX_PAGE_SIZE;
uint32_t configuration_crc32(const uint8_t* data, size_t size);
enum class ConfigurationStorageResult : uint8_t {
kOk,
kUnchanged,
kInvalidArgument,
kIoError,
};
struct ConfigurationStorageIo {
void* context = nullptr;
size_t sector_size = 0;
size_t page_size = 0;
bool (*read)(void* context, uint8_t copy, size_t offset,
uint8_t* output, size_t size) = nullptr;
bool (*erase)(void* context, uint8_t copy) = nullptr;
bool (*program)(void* context, uint8_t copy, size_t offset,
const uint8_t* data, size_t size) = nullptr;
};
struct ConfigurationStorageSnapshot {
bool valid = false;
uint16_t schema_version = 0;
uint16_t payload_size = 0;
uint32_t generation = 0;
uint32_t payload_crc = 0;
uint8_t payload[CONFIGURATION_STORAGE_MAX_PAYLOAD_SIZE]{};
};
class ConfigurationStorage {
public:
bool initialize(const ConfigurationStorageIo& io);
const ConfigurationStorageSnapshot& snapshot() const;
ConfigurationStorageResult commit(uint16_t schema_version,
const uint8_t* payload,
size_t payload_size);
private:
bool read_copy(uint8_t copy, ConfigurationStorageSnapshot* output) const;
ConfigurationStorageIo io_{};
ConfigurationStorageSnapshot snapshot_{};
uint8_t active_copy_ = 0;
bool initialized_ = false;
};

View file

@ -0,0 +1,108 @@
#include "configuration_transaction.h"
#include <string.h>
#include "adapter_configuration.h"
ConfigurationTransactionStatus ConfigurationTransaction::begin(
uint32_t transaction_id, uint16_t schema_version, size_t payload_size,
uint32_t payload_crc) {
if (snapshot_.status == ConfigurationTransactionStatus::kReceiving ||
snapshot_.status == ConfigurationTransactionStatus::kPending) {
return ConfigurationTransactionStatus::kBusy;
}
snapshot_ = {};
snapshot_.transaction_id = transaction_id;
if (transaction_id == 0 || payload_size == 0) {
snapshot_.status = ConfigurationTransactionStatus::kMalformed;
return snapshot_.status;
}
if (schema_version != ADAPTER_CONFIGURATION_SCHEMA_VERSION) {
snapshot_.status =
ConfigurationTransactionStatus::kUnsupportedSchema;
return snapshot_.status;
}
if (payload_size > CONFIGURATION_STORAGE_MAX_PAYLOAD_SIZE) {
snapshot_.status = ConfigurationTransactionStatus::kTooLarge;
return snapshot_.status;
}
snapshot_.expected_size = static_cast<uint16_t>(payload_size);
snapshot_.expected_crc = payload_crc;
snapshot_.status = ConfigurationTransactionStatus::kReceiving;
schema_version_ = schema_version;
return snapshot_.status;
}
ConfigurationTransactionStatus ConfigurationTransaction::append(
uint32_t transaction_id, size_t offset, const uint8_t* data,
size_t size) {
if (snapshot_.status != ConfigurationTransactionStatus::kReceiving) {
return ConfigurationTransactionStatus::kBusy;
}
if (transaction_id != snapshot_.transaction_id || data == nullptr ||
size == 0 || offset != snapshot_.received_size ||
offset + size > snapshot_.expected_size) {
snapshot_.status = ConfigurationTransactionStatus::kOutOfOrder;
return snapshot_.status;
}
memcpy(&payload_[offset], data, size);
snapshot_.received_size =
static_cast<uint16_t>(snapshot_.received_size + size);
return snapshot_.status;
}
ConfigurationTransactionStatus ConfigurationTransaction::finish(
uint32_t transaction_id) {
if (snapshot_.status != ConfigurationTransactionStatus::kReceiving ||
transaction_id != snapshot_.transaction_id) {
snapshot_.status = ConfigurationTransactionStatus::kOutOfOrder;
return snapshot_.status;
}
if (snapshot_.received_size != snapshot_.expected_size) {
snapshot_.status = ConfigurationTransactionStatus::kOutOfOrder;
return snapshot_.status;
}
if (configuration_crc32(payload_, snapshot_.expected_size) !=
snapshot_.expected_crc) {
snapshot_.status = ConfigurationTransactionStatus::kBadCrc;
return snapshot_.status;
}
AdapterConfiguration configuration{};
if (!adapter_configuration_decode(payload_, snapshot_.expected_size,
&configuration)) {
snapshot_.status = ConfigurationTransactionStatus::kMalformed;
return snapshot_.status;
}
snapshot_.status = ConfigurationTransactionStatus::kPending;
return snapshot_.status;
}
void ConfigurationTransaction::set_result(
ConfigurationTransactionStatus status, uint32_t stored_generation,
uint32_t stored_crc) {
snapshot_.status = status;
snapshot_.stored_generation = stored_generation;
snapshot_.stored_crc = stored_crc;
}
void ConfigurationTransaction::clear() {
snapshot_ = {};
schema_version_ = 0;
}
const ConfigurationTransactionSnapshot&
ConfigurationTransaction::snapshot() const {
return snapshot_;
}
uint16_t ConfigurationTransaction::schema_version() const {
return schema_version_;
}
const uint8_t* ConfigurationTransaction::payload() const {
return payload_;
}

View file

@ -0,0 +1,57 @@
#pragma once
#include <stddef.h>
#include <stdint.h>
#include "configuration_storage.h"
enum class ConfigurationTransactionStatus : uint8_t {
kIdle = 0,
kReceiving = 1,
kPending = 2,
kCommitted = 3,
kUnchanged = 4,
kMalformed = 5,
kUnsupportedSchema = 6,
kTooLarge = 7,
kOutOfOrder = 8,
kBadCrc = 9,
kBusy = 10,
kStorageError = 11,
};
struct ConfigurationTransactionSnapshot {
uint32_t transaction_id = 0;
uint16_t received_size = 0;
uint16_t expected_size = 0;
uint32_t expected_crc = 0;
uint32_t stored_generation = 0;
uint32_t stored_crc = 0;
ConfigurationTransactionStatus status =
ConfigurationTransactionStatus::kIdle;
};
class ConfigurationTransaction {
public:
ConfigurationTransactionStatus begin(uint32_t transaction_id,
uint16_t schema_version,
size_t payload_size,
uint32_t payload_crc);
ConfigurationTransactionStatus append(uint32_t transaction_id,
size_t offset,
const uint8_t* data,
size_t size);
ConfigurationTransactionStatus finish(uint32_t transaction_id);
void set_result(ConfigurationTransactionStatus status,
uint32_t stored_generation, uint32_t stored_crc);
void clear();
const ConfigurationTransactionSnapshot& snapshot() const;
uint16_t schema_version() const;
const uint8_t* payload() const;
private:
ConfigurationTransactionSnapshot snapshot_{};
uint16_t schema_version_ = 0;
uint8_t payload_[CONFIGURATION_STORAGE_MAX_PAYLOAD_SIZE]{};
};

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,111 @@
#include "pico_configuration_storage.h"
#include <string.h>
#include "hardware/flash.h"
#include "pico/btstack_flash_bank.h"
#include "pico/flash.h"
#include "pico/platform.h"
extern "C" char __flash_binary_end;
namespace {
constexpr size_t kConfigurationStorageSize =
CONFIGURATION_STORAGE_COPY_COUNT * FLASH_SECTOR_SIZE;
constexpr uint32_t kConfigurationStorageOffset =
PICO_FLASH_BANK_STORAGE_OFFSET - kConfigurationStorageSize;
static_assert(PICO_FLASH_BANK_STORAGE_OFFSET >= kConfigurationStorageSize,
"configuration storage offset underflows flash");
static_assert(kConfigurationStorageOffset + kConfigurationStorageSize <=
PICO_FLASH_BANK_STORAGE_OFFSET,
"configuration storage overlaps BTstack bonds");
static_assert(PICO_FLASH_BANK_STORAGE_OFFSET +
PICO_FLASH_BANK_TOTAL_SIZE <=
PICO_FLASH_SIZE_BYTES,
"BTstack storage exceeds flash");
static_assert(CONFIGURATION_STORAGE_MAX_RECORD_SIZE <= FLASH_SECTOR_SIZE,
"configuration record does not fit one flash sector");
static_assert(CONFIGURATION_STORAGE_MAX_PAGE_SIZE == FLASH_PAGE_SIZE,
"configuration page buffer does not match Pico flash");
struct FlashMutation {
bool erase;
uint32_t offset;
const uint8_t* data;
};
void perform_flash_mutation(void* context) {
const auto* mutation = static_cast<const FlashMutation*>(context);
if (mutation->erase) {
flash_range_erase(mutation->offset, FLASH_SECTOR_SIZE);
} else {
flash_range_program(mutation->offset, mutation->data,
FLASH_PAGE_SIZE);
}
}
bool storage_region_available() {
const uintptr_t binary_end =
reinterpret_cast<uintptr_t>(&__flash_binary_end) - XIP_BASE;
return binary_end <= kConfigurationStorageOffset;
}
bool read_storage(void*, uint8_t copy, size_t offset, uint8_t* output,
size_t size) {
if (copy >= CONFIGURATION_STORAGE_COPY_COUNT || output == nullptr ||
offset + size > FLASH_SECTOR_SIZE || !storage_region_available()) {
return false;
}
const uintptr_t address = XIP_BASE + kConfigurationStorageOffset +
copy * FLASH_SECTOR_SIZE + offset;
memcpy(output, reinterpret_cast<const void*>(address), size);
return true;
}
bool erase_storage(void*, uint8_t copy) {
if (copy >= CONFIGURATION_STORAGE_COPY_COUNT ||
!storage_region_available()) {
return false;
}
FlashMutation mutation{
true,
static_cast<uint32_t>(kConfigurationStorageOffset +
copy * FLASH_SECTOR_SIZE),
nullptr,
};
return flash_safe_execute(perform_flash_mutation, &mutation,
UINT32_MAX) == PICO_OK;
}
bool program_storage(void*, uint8_t copy, size_t offset,
const uint8_t* data, size_t size) {
if (copy >= CONFIGURATION_STORAGE_COPY_COUNT || data == nullptr ||
size != FLASH_PAGE_SIZE || offset % FLASH_PAGE_SIZE != 0 ||
offset + size > FLASH_SECTOR_SIZE || !storage_region_available()) {
return false;
}
FlashMutation mutation{
false,
static_cast<uint32_t>(kConfigurationStorageOffset +
copy * FLASH_SECTOR_SIZE + offset),
data,
};
return flash_safe_execute(perform_flash_mutation, &mutation,
UINT32_MAX) == PICO_OK;
}
} // namespace
ConfigurationStorageIo pico_configuration_storage_io() {
return {
nullptr,
FLASH_SECTOR_SIZE,
FLASH_PAGE_SIZE,
read_storage,
erase_storage,
program_storage,
};
}

View file

@ -0,0 +1,5 @@
#pragma once
#include "configuration_storage.h"
ConfigurationStorageIo pico_configuration_storage_io();

View file

@ -20,7 +20,7 @@ dependencies = [
[project.scripts]
controller-uart-bridge = "switch_pico_bridge.controller_uart_bridge:main"
host-uart-logger = "switch_pico_bridge.host_uart_logger:main"
switch-pico-pairings = "switch_pico_bridge.pairing_manager:main"
switch-pico-config = "switch_pico_bridge.config_manager:main"
[tool.setuptools]
package-dir = {"" = "src"}

View file

@ -0,0 +1,600 @@
#!/usr/bin/env python3
"""Manage switch-pico persistent configuration and pairings over USB EP0."""
from __future__ import annotations
import argparse
import secrets
import struct
import sys
import time
import zlib
from collections.abc import Iterable, Sequence
from dataclasses import dataclass
from typing import Any, Protocol, cast
import usb.core
USB_IDENTITIES = ((0x057E, 0x2009), (0xCAFE, 0x4010))
REQUEST_VALUE = 0x5350
REQUEST_INDEX = 0x0001
PROTOCOL_VERSION = 1
REQUEST_HEADER_SIZE = 16
RESPONSE_HEADER_SIZE = 20
MAXIMUM_REQUEST_SIZE = 64
MAXIMUM_RESPONSE_SIZE = 152
MAXIMUM_CHUNK_SIZE = 40
USB_TIMEOUT_MS = 1000
OP_INFO = 0x01
OP_CONFIGURATION_READ = 0x10
OP_CONFIGURATION_BEGIN = 0x11
OP_CONFIGURATION_CHUNK = 0x12
OP_CONFIGURATION_COMMIT = 0x13
OP_CONFIGURATION_RESET = 0x14
OP_TRANSACTION_STATUS = 0x15
OP_PAIRING_READ = 0x20
OP_PAIRING_REFRESH = 0x21
OP_PAIRING_CLEAR = 0x22
STATUS_OK = 0
STATUS_PENDING = 1
STATUS_NAMES = {
2: "malformed request",
3: "unsupported schema",
4: "value too large",
5: "out-of-order transaction",
6: "CRC mismatch",
7: "device busy",
8: "storage failure",
}
CONFIGURATION_SCHEMA_VERSION = 1
CONFIGURATION_SIZE = 4
PAIRING_WINDOW_SECONDS_MIN = 10
PAIRING_WINDOW_SECONDS_MAX = 300
PAIRING_RECORD_SIZE = 8
PAIRING_RECORD_CAPACITY = 16
TRANSPORT_CLASSIC = 1
TRANSPORT_BLE = 2
class ConfigManagerError(RuntimeError):
"""Expected discovery, USB transport, or management protocol failure."""
class UsbDevice(Protocol):
bus: int | None
address: int | None
def ctrl_transfer(
self,
bm_request_type: int,
request: int,
value: int = 0,
index: int = 0,
data_or_w_length: Any = None,
timeout: int | None = None,
) -> Any:
...
@dataclass(frozen=True)
class Envelope:
operation: int
status: int
flags: int
schema_version: int
generation: int
payload_crc: int
payload: bytes
@dataclass(frozen=True)
class DeviceInfo:
firmware_version: tuple[int, int, int]
board: int
active_mode: int
maximum_configuration_size: int
@dataclass(frozen=True)
class AdapterConfiguration:
pairing_window_seconds: int
generation: int
crc: int
@dataclass(frozen=True)
class TransactionStatus:
transaction_id: int
received_size: int
expected_size: int
expected_crc: int
stored_generation: int
stored_crc: int
status: int
@dataclass(frozen=True)
class PairingRecord:
transport: int
address_type: int
address: bytes
@property
def address_text(self) -> str:
return ":".join(f"{octet:02X}" for octet in self.address)
@property
def transport_text(self) -> str:
if self.transport == TRANSPORT_CLASSIC:
return "Classic"
if self.transport == TRANSPORT_BLE:
address_types = {
0: "public",
1: "random",
2: "public identity",
3: "random identity",
}
suffix = address_types.get(
self.address_type, f"type {self.address_type}"
)
return f"BLE ({suffix})"
return f"unknown transport {self.transport}"
@dataclass(frozen=True)
class PairingSnapshot:
generation: int
pending: bool
overflow: bool
records: tuple[PairingRecord, ...]
def _crc32(payload: bytes) -> int:
return zlib.crc32(payload) & 0xFFFFFFFF
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")
return struct.pack(
"<4sBBBBHHI",
b"SPMG",
PROTOCOL_VERSION,
operation,
0,
0,
len(payload),
0,
_crc32(payload),
) + payload
def parse_response(payload: bytes, expected_operation: int) -> Envelope:
if len(payload) < RESPONSE_HEADER_SIZE:
raise ConfigManagerError("short management response")
(
magic,
version,
operation,
status,
flags,
payload_size,
schema_version,
generation,
payload_crc,
) = struct.unpack_from("<4sBBBBHHII", payload)
if magic != b"SPMG":
raise ConfigManagerError("device does not implement switch-pico management")
if version != PROTOCOL_VERSION:
raise ConfigManagerError(
f"unsupported management protocol version {version}"
)
if operation != expected_operation:
raise ConfigManagerError(
f"unexpected management operation 0x{operation:02x}"
)
if len(payload) != RESPONSE_HEADER_SIZE + payload_size:
raise ConfigManagerError("invalid management payload size")
body = bytes(payload[RESPONSE_HEADER_SIZE:])
if _crc32(body) != payload_crc:
raise ConfigManagerError("management response CRC mismatch")
return Envelope(
operation=operation,
status=status,
flags=flags,
schema_version=schema_version,
generation=generation,
payload_crc=payload_crc,
payload=body,
)
def _raise_status(envelope: Envelope, *, pending_ok: bool = False) -> None:
if envelope.status == STATUS_OK:
return
if envelope.status == STATUS_PENDING and pending_ok:
return
if envelope.status == STATUS_PENDING:
raise ConfigManagerError("device operation is still pending")
raise ConfigManagerError(
STATUS_NAMES.get(
envelope.status, f"unknown device status {envelope.status}"
)
)
def _control_in(device: UsbDevice, operation: int) -> Envelope:
payload = device.ctrl_transfer(
0xC0,
operation,
REQUEST_VALUE,
REQUEST_INDEX,
MAXIMUM_RESPONSE_SIZE,
timeout=USB_TIMEOUT_MS,
)
return parse_response(bytes(payload), operation)
def _control_out(
device: UsbDevice, operation: int, payload: bytes = b""
) -> None:
request = encode_request(operation, payload)
device.ctrl_transfer(
0x40,
operation,
REQUEST_VALUE,
REQUEST_INDEX,
request,
timeout=USB_TIMEOUT_MS,
)
def read_info(device: UsbDevice) -> DeviceInfo:
envelope = _control_in(device, OP_INFO)
_raise_status(envelope)
if len(envelope.payload) != 8:
raise ConfigManagerError("invalid device-info payload")
return DeviceInfo(
firmware_version=(
envelope.payload[0],
envelope.payload[1],
envelope.payload[2],
),
board=envelope.payload[3],
active_mode=envelope.payload[4],
maximum_configuration_size=struct.unpack_from(
"<H", envelope.payload, 6
)[0],
)
def read_configuration(device: UsbDevice) -> AdapterConfiguration:
envelope = _control_in(device, OP_CONFIGURATION_READ)
_raise_status(envelope)
if (
envelope.schema_version != CONFIGURATION_SCHEMA_VERSION
or len(envelope.payload) != CONFIGURATION_SIZE
or envelope.payload[2:] != b"\x00\x00"
):
raise ConfigManagerError("unsupported configuration object")
pairing_window_seconds = struct.unpack_from("<H", envelope.payload)[0]
if not (
PAIRING_WINDOW_SECONDS_MIN
<= pairing_window_seconds
<= PAIRING_WINDOW_SECONDS_MAX
):
raise ConfigManagerError("invalid stored pairing-window duration")
return AdapterConfiguration(
pairing_window_seconds=pairing_window_seconds,
generation=envelope.generation,
crc=envelope.payload_crc,
)
def read_transaction_status(device: UsbDevice) -> TransactionStatus:
envelope = _control_in(device, OP_TRANSACTION_STATUS)
_raise_status(envelope, pending_ok=True)
if len(envelope.payload) != 20:
raise ConfigManagerError("invalid transaction-status payload")
values = struct.unpack("<IHHIII", envelope.payload)
return TransactionStatus(*values, status=envelope.status)
def _wait_for_transaction(
device: UsbDevice, transaction_id: int, timeout: float
) -> TransactionStatus:
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
status = read_transaction_status(device)
if status.transaction_id != transaction_id:
raise ConfigManagerError("device reported a different transaction")
if status.status == STATUS_OK:
return status
time.sleep(0.05)
raise ConfigManagerError("configuration commit did not finish")
def write_configuration(
device: UsbDevice, configuration: AdapterConfiguration, timeout: float
) -> TransactionStatus:
if not (
PAIRING_WINDOW_SECONDS_MIN
<= configuration.pairing_window_seconds
<= PAIRING_WINDOW_SECONDS_MAX
):
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
_control_out(
device,
OP_CONFIGURATION_BEGIN,
struct.pack(
"<IHHI",
transaction_id,
CONFIGURATION_SCHEMA_VERSION,
len(payload),
_crc32(payload),
),
)
for offset in range(0, len(payload), MAXIMUM_CHUNK_SIZE):
chunk = payload[offset : offset + MAXIMUM_CHUNK_SIZE]
_control_out(
device,
OP_CONFIGURATION_CHUNK,
struct.pack("<IHH", transaction_id, offset, len(chunk)) + chunk,
)
_control_out(
device, OP_CONFIGURATION_COMMIT, struct.pack("<I", transaction_id)
)
return _wait_for_transaction(device, transaction_id, timeout)
def reset_configuration(device: UsbDevice, timeout: float) -> TransactionStatus:
transaction_id = secrets.randbits(32) or 1
_control_out(
device, OP_CONFIGURATION_RESET, struct.pack("<I", transaction_id)
)
return _wait_for_transaction(device, transaction_id, timeout)
def parse_pairing_snapshot(envelope: Envelope) -> PairingSnapshot:
_raise_status(envelope, pending_ok=True)
if len(envelope.payload) < 4:
raise ConfigManagerError("short pairing snapshot")
record_count = envelope.payload[0]
required = 4 + record_count * PAIRING_RECORD_SIZE
if (
record_count > PAIRING_RECORD_CAPACITY
or len(envelope.payload) != required
):
raise ConfigManagerError("invalid pairing record count")
records: list[PairingRecord] = []
offset = 4
for _ in range(record_count):
records.append(
PairingRecord(
transport=envelope.payload[offset],
address_type=envelope.payload[offset + 1],
address=envelope.payload[offset + 2 : offset + 8],
)
)
offset += PAIRING_RECORD_SIZE
return PairingSnapshot(
generation=envelope.generation,
pending=envelope.status == STATUS_PENDING,
overflow=bool(envelope.flags & 1 or envelope.payload[1] & 1),
records=tuple(records),
)
def read_pairings(device: UsbDevice) -> PairingSnapshot:
return parse_pairing_snapshot(_control_in(device, OP_PAIRING_READ))
def _wait_for_pairings(
device: UsbDevice, previous_generation: int, timeout: float
) -> PairingSnapshot:
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
snapshot = read_pairings(device)
if not snapshot.pending and snapshot.generation != previous_generation:
return snapshot
time.sleep(0.05)
raise ConfigManagerError("Pico did not finish the pairing operation")
def refresh_pairings(device: UsbDevice, timeout: float) -> PairingSnapshot:
initial = read_pairings(device)
_control_out(device, OP_PAIRING_REFRESH)
return _wait_for_pairings(device, initial.generation, timeout)
def clear_pairings(device: UsbDevice, timeout: float) -> PairingSnapshot:
initial = read_pairings(device)
_control_out(device, OP_PAIRING_CLEAR)
snapshot = _wait_for_pairings(device, initial.generation, timeout)
if snapshot.records:
raise ConfigManagerError("Pico reported pairings after clear completed")
return snapshot
def _candidate_devices() -> Iterable[UsbDevice]:
for vendor_id, product_id in USB_IDENTITIES:
devices = cast(
Iterable[UsbDevice] | None,
usb.core.find(
find_all=True, idVendor=vendor_id, idProduct=product_id
),
)
if devices is not None:
yield from devices
def find_pico(
bus: int | None, address: int | None, timeout: float = 3.0
) -> UsbDevice:
deadline = time.monotonic() + timeout
failures: list[Exception] = []
while True:
matches: list[UsbDevice] = []
for device in _candidate_devices():
if bus is not None and getattr(device, "bus", None) != bus:
continue
if address is not None and getattr(device, "address", None) != address:
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
)
raise ConfigManagerError(
f"multiple switch-pico devices found ({locations}); "
"select one with --bus and --address"
)
if time.monotonic() >= deadline:
break
time.sleep(0.05)
if failures:
raise ConfigManagerError(
"matching USB devices were found, but none accepted the "
f"management request; last error: {failures[-1]}"
) from failures[-1]
raise ConfigManagerError("no USB-connected switch-pico firmware found")
def _print_pairings(snapshot: PairingSnapshot) -> None:
if not snapshot.records:
print("No stored pairings.")
return
for index, record in enumerate(snapshot.records, start=1):
print(f"{index}: {record.transport_text} {record.address_text}")
if snapshot.overflow:
print("Warning: additional pairings did not fit in the response.")
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
prog="switch-pico-config",
description="Manage switch-pico persistent configuration and pairings.",
)
parser.add_argument("--bus", type=int, help="USB bus number")
parser.add_argument("--address", type=int, help="USB device address")
parser.add_argument(
"--timeout",
type=float,
default=3.0,
help="operation timeout in seconds (default: 3)",
)
commands = parser.add_subparsers(dest="command", required=True)
commands.add_parser("status", help="show firmware and configuration status")
config = commands.add_parser("config", help="read or change configuration")
config_commands = config.add_subparsers(dest="config_command", required=True)
config_commands.add_parser("show", help="show persistent configuration")
config_set = config_commands.add_parser("set", help="write configuration")
config_set.add_argument(
"--pairing-window-seconds",
required=True,
type=int,
metavar="SECONDS",
)
config_reset = config_commands.add_parser("reset", help="restore defaults")
config_reset.add_argument("--yes", action="store_true")
pairings = commands.add_parser("pairings", help="list or clear pairings")
pairing_commands = pairings.add_subparsers(
dest="pairing_command", required=True
)
pairing_commands.add_parser("list", help="list stored pairings")
pairing_clear = pairing_commands.add_parser("clear", help="clear pairings")
pairing_clear.add_argument("--yes", action="store_true")
return parser
def main(argv: Sequence[str] | None = None) -> int:
args = build_parser().parse_args(argv)
if args.timeout <= 0:
print("error: --timeout must be positive", file=sys.stderr)
return 2
if args.command == "config" and args.config_command == "reset" and not args.yes:
print("error: config reset requires --yes", file=sys.stderr)
return 2
if args.command == "pairings" and args.pairing_command == "clear" and not args.yes:
print("error: pairings clear requires --yes", file=sys.stderr)
return 2
try:
device = find_pico(args.bus, args.address, args.timeout)
if args.command == "status":
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(f"Configuration generation: {configuration.generation}")
print(f"Configuration CRC: {configuration.crc:08x}")
print(
"Pairing window: "
f"{configuration.pairing_window_seconds} seconds"
)
elif args.command == "config":
if args.config_command == "show":
configuration = read_configuration(device)
print(
f"pairing_window_seconds={configuration.pairing_window_seconds}"
)
print(f"generation={configuration.generation}")
print(f"crc={configuration.crc:08x}")
elif args.config_command == "set":
before = read_configuration(device)
status = write_configuration(
device,
AdapterConfiguration(
pairing_window_seconds=args.pairing_window_seconds,
generation=before.generation,
crc=before.crc,
),
args.timeout,
)
print(
"Stored configuration generation "
f"{status.stored_generation} "
f"(CRC {status.stored_crc:08x})."
)
else:
status = reset_configuration(device, args.timeout)
print(
"Reset configuration at generation "
f"{status.stored_generation}."
)
elif args.pairing_command == "list":
_print_pairings(refresh_pairings(device, args.timeout))
else:
before = refresh_pairings(device, args.timeout)
clear_pairings(device, args.timeout)
print(f"Cleared {len(before.records)} stored pairing(s).")
except ConfigManagerError as exc:
print(f"error: {exc}", file=sys.stderr)
return 1
except usb.core.USBError as exc:
print(f"error: USB access failed: {exc}", file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())

View file

@ -1,292 +0,0 @@
#!/usr/bin/env python3
"""Manage Pico 2 W Bluetooth pairings over vendor requests on USB EP0."""
from __future__ import annotations
import argparse
import struct
import sys
import time
from dataclasses import dataclass
from collections.abc import Iterable, Sequence
from typing import Any, Protocol
import usb.core
USB_VENDOR_ID = 0x057E
USB_PRODUCT_ID = 0x2009
REQUEST_CLEAR = 0x50
REQUEST_GET = 0x51
REQUEST_REFRESH = 0x52
REQUEST_VALUE = 0x5350
REQUEST_INDEX = 0x4D47
PROTOCOL_VERSION = 1
RESPONSE_HEADER_SIZE = 12
RECORD_SIZE = 8
RECORD_CAPACITY = 16
MAXIMUM_RESPONSE_SIZE = RESPONSE_HEADER_SIZE + RECORD_CAPACITY * RECORD_SIZE
STATUS_READY = 0
STATUS_PENDING = 1
TRANSPORT_CLASSIC = 1
TRANSPORT_BLE = 2
USB_TIMEOUT_MS = 1000
class PairingManagerError(RuntimeError):
"""Expected discovery, USB transport, or protocol failure."""
@dataclass(frozen=True)
class PairingRecord:
transport: int
address_type: int
address: bytes
@property
def address_text(self) -> str:
return ":".join(f"{octet:02X}" for octet in self.address)
@property
def transport_text(self) -> str:
if self.transport == TRANSPORT_CLASSIC:
return "Classic"
if self.transport == TRANSPORT_BLE:
address_types = {
0: "public",
1: "random",
2: "public identity",
3: "random identity",
}
suffix = address_types.get(
self.address_type, f"type {self.address_type}"
)
return f"BLE ({suffix})"
return f"unknown transport {self.transport}"
class UsbDevice(Protocol):
bus: int | None
address: int | None
def ctrl_transfer(
self,
bm_request_type: int,
request: int,
value: int = 0,
index: int = 0,
data_or_w_length: Any = None,
timeout: int | None = None,
) -> Any:
...
@dataclass(frozen=True)
class PairingSnapshot:
generation: int
status: int
overflow: bool
records: tuple[PairingRecord, ...]
def parse_snapshot(payload: bytes) -> PairingSnapshot:
if len(payload) < RESPONSE_HEADER_SIZE:
raise PairingManagerError("short pairing-management response")
if payload[:4] != b"SPPM":
raise PairingManagerError("device does not implement pairing management")
if payload[4] != PROTOCOL_VERSION:
raise PairingManagerError(
f"unsupported pairing protocol version {payload[4]}"
)
status = payload[5]
record_count = payload[6]
required = RESPONSE_HEADER_SIZE + record_count * RECORD_SIZE
if record_count > RECORD_CAPACITY or len(payload) < required:
raise PairingManagerError("invalid pairing record count")
generation = int(struct.unpack_from("<I", payload, 8)[0])
records: list[PairingRecord] = []
offset = RESPONSE_HEADER_SIZE
for _ in range(record_count):
records.append(
PairingRecord(
transport=payload[offset],
address_type=payload[offset + 1],
address=bytes(payload[offset + 2 : offset + 8]),
)
)
offset += RECORD_SIZE
return PairingSnapshot(
generation=generation,
status=status,
overflow=bool(payload[7] & 1),
records=tuple(records),
)
def _control_in(device: UsbDevice) -> bytes:
payload = device.ctrl_transfer(
0xC0,
REQUEST_GET,
REQUEST_VALUE,
REQUEST_INDEX,
MAXIMUM_RESPONSE_SIZE,
timeout=USB_TIMEOUT_MS,
)
return bytes(payload)
def _control_out(device: UsbDevice, request: int) -> None:
device.ctrl_transfer(
0x40,
request,
REQUEST_VALUE,
REQUEST_INDEX,
None,
timeout=USB_TIMEOUT_MS,
)
def read_snapshot(device: UsbDevice) -> PairingSnapshot:
return parse_snapshot(_control_in(device))
def wait_for_snapshot(
device: UsbDevice, previous_generation: int, timeout: float
) -> PairingSnapshot:
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
snapshot = read_snapshot(device)
if (
snapshot.status == STATUS_READY
and snapshot.generation != previous_generation
):
return snapshot
time.sleep(0.05)
raise PairingManagerError("Pico did not finish the pairing operation")
def refresh_snapshot(device: UsbDevice, timeout: float) -> PairingSnapshot:
initial = read_snapshot(device)
_control_out(device, REQUEST_REFRESH)
return wait_for_snapshot(device, initial.generation, timeout)
def clear_pairings(device: UsbDevice, timeout: float) -> PairingSnapshot:
initial = read_snapshot(device)
_control_out(device, REQUEST_CLEAR)
snapshot = wait_for_snapshot(device, initial.generation, timeout)
if snapshot.records:
raise PairingManagerError("Pico reported pairings after clear completed")
return snapshot
def _candidate_devices() -> Iterable[UsbDevice]:
devices = usb.core.find(
find_all=True,
idVendor=USB_VENDOR_ID,
idProduct=USB_PRODUCT_ID,
)
return () if devices is None else devices
def find_pico(
bus: int | None, address: int | None, timeout: float = 3.0
) -> UsbDevice:
deadline = time.monotonic() + timeout
failures: list[Exception] = []
while True:
matches: list[UsbDevice] = []
for device in _candidate_devices():
if bus is not None and getattr(device, "bus", None) != bus:
continue
if address is not None and getattr(device, "address", None) != address:
continue
try:
_ = read_snapshot(device)
except (PairingManagerError, 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
)
raise PairingManagerError(
f"multiple switch-pico devices found ({locations}); "
"select one with --bus and --address"
)
if time.monotonic() >= deadline:
break
time.sleep(0.05)
if failures:
raise PairingManagerError(
"matching USB devices were found, but none accepted the "
f"management request; last error: {failures[-1]}"
) from failures[-1]
raise PairingManagerError("no USB-connected switch-pico AIO firmware found")
def _print_snapshot(snapshot: PairingSnapshot) -> None:
if not snapshot.records:
print("No stored pairings.")
return
for index, record in enumerate(snapshot.records, start=1):
print(f"{index}: {record.transport_text} {record.address_text}")
if snapshot.overflow:
print("Warning: additional pairings did not fit in the response.")
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
prog="switch-pico-pairings",
description="List or clear switch-pico AIO Bluetooth pairings.",
)
parser.add_argument("--bus", type=int, help="USB bus number")
parser.add_argument("--address", type=int, help="USB device address")
parser.add_argument(
"--timeout", type=float, default=3.0,
help="operation timeout in seconds (default: 3)",
)
subparsers = parser.add_subparsers(dest="command", required=True)
subparsers.add_parser("list", help="list stored Classic and BLE pairings")
clear_parser = subparsers.add_parser("clear", help="clear all pairings")
clear_parser.add_argument(
"--yes", action="store_true",
help="confirm destructive clearing without prompting",
)
return parser
def main(argv: Sequence[str] | None = None) -> int:
args = build_parser().parse_args(argv)
if args.timeout <= 0:
print("error: --timeout must be positive", file=sys.stderr)
return 2
if args.command == "clear" and not args.yes:
print("error: clear requires --yes", file=sys.stderr)
return 2
try:
device = find_pico(args.bus, args.address, args.timeout)
if args.command == "list":
_print_snapshot(refresh_snapshot(device, args.timeout))
else:
before = refresh_snapshot(device, args.timeout)
clear_pairings(device, args.timeout)
print(f"Cleared {len(before.records)} stored pairing(s).")
except PairingManagerError as exc:
print(f"error: {exc}", file=sys.stderr)
return 1
except usb.core.USBError as exc:
print(f"error: USB access failed: {exc}", file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())

View file

@ -285,6 +285,15 @@ uint32_t btstack_run_loop_get_time_ms() {
#include "../bluepad32_input_backend.cpp"
void configuration_service_prepare() {}
void configuration_service_initialize_on_storage_core() {}
void configuration_service_task_on_storage_core(uint32_t) {}
void configuration_service_snapshot(ConfigurationServiceSnapshot* output) {
*output = {};
output->state = ConfigurationServiceState::kReady;
output->configuration.pairing_window_seconds =
ADAPTER_PAIRING_WINDOW_SECONDS_DEFAULT;
}
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
AdapterUsbMode test_adapter_mode = AdapterUsbMode::kXInput;
AdapterUsbMode adapter_host_probe_mode() {

View file

@ -0,0 +1,232 @@
#include "adapter_configuration.h"
#include "configuration_storage.h"
#include "configuration_transaction.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 successful_programs = 0;
int fail_after_programs = -1;
FakeFlash() { memset(bytes, 0xff, sizeof(bytes)); }
};
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);
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);
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) {
return false;
}
if (flash->fail_after_programs >= 0 &&
flash->successful_programs >= flash->fail_after_programs) {
return false;
}
for (size_t index = 0; index < size; ++index) {
flash->bytes[copy][offset + index] &= data[index];
}
++flash->successful_programs;
return true;
}
ConfigurationStorageIo fake_io(FakeFlash* flash) {
return {
flash,
kSectorSize,
kPageSize,
fake_read,
fake_erase,
fake_program,
};
}
void test_schema_encoding() {
AdapterConfiguration configuration{};
configuration.pairing_window_seconds = 90;
uint8_t payload[ADAPTER_CONFIGURATION_ENCODED_SIZE]{};
require(adapter_configuration_encode(configuration, payload,
sizeof(payload)),
"valid configuration did not encode");
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");
configuration.pairing_window_seconds = 9;
require(!adapter_configuration_encode(configuration, payload,
sizeof(payload)),
"out-of-range pairing window was accepted");
}
void test_two_copy_recovery() {
FakeFlash flash;
ConfigurationStorage store;
require(store.initialize(fake_io(&flash)),
"storage did not initialize");
require(!store.snapshot().valid,
"erased storage appeared valid");
const uint8_t first[] = {1, 2, 3, 4};
require(store.commit(1, first, sizeof(first)) ==
ConfigurationStorageResult::kOk &&
store.snapshot().generation == 1,
"first generation did not commit");
const int programs_after_first = flash.successful_programs;
require(store.commit(1, first, sizeof(first)) ==
ConfigurationStorageResult::kUnchanged &&
flash.successful_programs == programs_after_first,
"identical configuration consumed a flash write");
const uint8_t second[] = {5, 6, 7, 8};
require(store.commit(1, second, sizeof(second)) ==
ConfigurationStorageResult::kOk &&
store.snapshot().generation == 2,
"second generation did not commit");
ConfigurationStorage after_power_cycle;
require(after_power_cycle.initialize(fake_io(&flash)) &&
after_power_cycle.snapshot().generation == 2 &&
memcmp(after_power_cycle.snapshot().payload, second,
sizeof(second)) == 0,
"latest generation did not survive reinitialization");
flash.bytes[1][CONFIGURATION_STORAGE_RECORD_HEADER_SIZE] ^= 0x01;
ConfigurationStorage after_corruption;
require(after_corruption.initialize(fake_io(&flash)) &&
after_corruption.snapshot().generation == 1 &&
memcmp(after_corruption.snapshot().payload, first,
sizeof(first)) == 0,
"corrupt newest generation did not roll back");
}
void test_interrupted_write_retains_previous_generation() {
FakeFlash flash;
ConfigurationStorage store;
require(store.initialize(fake_io(&flash)),
"storage did not initialize for interruption test");
const uint8_t first[] = {9, 8, 7, 6};
require(store.commit(1, first, sizeof(first)) ==
ConfigurationStorageResult::kOk,
"baseline generation did not commit");
uint8_t large[300];
memset(large, 0x5a, sizeof(large));
flash.fail_after_programs = flash.successful_programs + 1;
require(store.commit(1, large, sizeof(large)) ==
ConfigurationStorageResult::kIoError,
"interrupted multi-page write reported success");
flash.fail_after_programs = -1;
ConfigurationStorage recovered;
require(recovered.initialize(fake_io(&flash)) &&
recovered.snapshot().generation == 1 &&
recovered.snapshot().payload_size == sizeof(first) &&
memcmp(recovered.snapshot().payload, first,
sizeof(first)) == 0,
"interrupted write replaced the previous generation");
}
void test_transaction_validation() {
AdapterConfiguration configuration{};
configuration.pairing_window_seconds = 120;
uint8_t payload[ADAPTER_CONFIGURATION_ENCODED_SIZE]{};
require(adapter_configuration_encode(configuration, payload,
sizeof(payload)),
"transaction payload did not encode");
const uint32_t crc = configuration_crc32(payload, sizeof(payload));
ConfigurationTransaction transaction;
require(transaction.begin(7, ADAPTER_CONFIGURATION_SCHEMA_VERSION,
sizeof(payload), crc) ==
ConfigurationTransactionStatus::kReceiving,
"valid transaction did not begin");
require(transaction.append(7, 1, payload, 1) ==
ConfigurationTransactionStatus::kOutOfOrder,
"out-of-order chunk was accepted");
require(transaction.begin(8, ADAPTER_CONFIGURATION_SCHEMA_VERSION,
sizeof(payload), crc ^ 1u) ==
ConfigurationTransactionStatus::kReceiving,
"replacement transaction did not begin");
require(transaction.append(8, 0, payload, 2) ==
ConfigurationTransactionStatus::kReceiving &&
transaction.append(8, 2, &payload[2], 2) ==
ConfigurationTransactionStatus::kReceiving,
"ordered chunks were rejected");
require(transaction.finish(8) ==
ConfigurationTransactionStatus::kBadCrc,
"bad transaction CRC was accepted");
require(transaction.begin(9, ADAPTER_CONFIGURATION_SCHEMA_VERSION,
sizeof(payload), crc) ==
ConfigurationTransactionStatus::kReceiving &&
transaction.append(9, 0, payload, sizeof(payload)) ==
ConfigurationTransactionStatus::kReceiving &&
transaction.finish(9) ==
ConfigurationTransactionStatus::kPending,
"valid transaction did not reach pending commit");
require(transaction.begin(10, ADAPTER_CONFIGURATION_SCHEMA_VERSION,
sizeof(payload), crc) ==
ConfigurationTransactionStatus::kBusy,
"pending transaction was replaced");
transaction.set_result(ConfigurationTransactionStatus::kCommitted,
4, crc);
require(transaction.begin(11, 99, sizeof(payload), crc) ==
ConfigurationTransactionStatus::kUnsupportedSchema,
"unsupported schema was accepted");
require(transaction.begin(
12, ADAPTER_CONFIGURATION_SCHEMA_VERSION,
CONFIGURATION_STORAGE_MAX_PAYLOAD_SIZE + 1, crc) ==
ConfigurationTransactionStatus::kTooLarge,
"oversized transaction was accepted");
}
} // namespace
int main() {
test_schema_encoding();
test_two_copy_recovery();
test_interrupted_write_retains_previous_generation();
test_transaction_validation();
return 0;
}

View file

@ -0,0 +1,244 @@
from __future__ import annotations
import struct
import zlib
import pytest
import switch_pico_bridge.config_manager as config_manager
def make_response(
operation: int,
payload: bytes = b"",
*,
status: int = config_manager.STATUS_OK,
flags: int = 0,
schema: int = 0,
generation: int = 0,
) -> bytes:
return struct.pack(
"<4sBBBBHHII",
b"SPMG",
config_manager.PROTOCOL_VERSION,
operation,
status,
flags,
len(payload),
schema,
generation,
zlib.crc32(payload) & 0xFFFFFFFF,
) + payload
class FakeDevice:
bus = 1
address = 7
def __init__(self) -> None:
self.configuration = struct.pack("<Hxx", 60)
self.configuration_generation = 3
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.records = [
(
config_manager.TRANSPORT_CLASSIC,
0xFE,
bytes.fromhex("010203040506"),
),
(
config_manager.TRANSPORT_BLE,
2,
bytes.fromhex("A1A2A3A4A5A6"),
),
]
self.pairing_generation = 4
self.requests: list[int] = []
def _pairing_payload(self) -> bytes:
payload = bytearray([len(self.records), 0, 0, 0])
for transport, address_type, address in self.records:
payload.extend([transport, address_type])
payload.extend(address)
return bytes(payload)
def _transaction_payload(self) -> bytes:
stored_crc = zlib.crc32(self.configuration) & 0xFFFFFFFF
return struct.pack(
"<IHHIII",
self.transaction_id,
len(self.transaction_payload),
self.transaction_expected_size,
self.transaction_expected_crc,
self.configuration_generation,
stored_crc,
)
def ctrl_transfer(
self,
bm_request_type: int,
request: int,
value: int,
index: int,
data_or_w_length: object,
timeout: int,
) -> bytes | int:
assert value == config_manager.REQUEST_VALUE
assert index == config_manager.REQUEST_INDEX
assert timeout == config_manager.USB_TIMEOUT_MS
self.requests.append(request)
if bm_request_type == 0xC0:
if request == config_manager.OP_INFO:
return make_response(request, bytes([0, 2, 0, 2, 0, 0, 0, 2]))
if request == config_manager.OP_CONFIGURATION_READ:
return make_response(
request,
self.configuration,
schema=config_manager.CONFIGURATION_SCHEMA_VERSION,
generation=self.configuration_generation,
)
if request == config_manager.OP_TRANSACTION_STATUS:
return make_response(
request,
self._transaction_payload(),
status=self.transaction_status,
schema=config_manager.CONFIGURATION_SCHEMA_VERSION,
generation=self.configuration_generation,
)
if request == config_manager.OP_PAIRING_READ:
return make_response(
request,
self._pairing_payload(),
generation=self.pairing_generation,
)
raise AssertionError(f"unexpected IN request {request}")
assert bm_request_type == 0x40
encoded = bytes(data_or_w_length)
assert encoded[:4] == b"SPMG"
payload_size = struct.unpack_from("<H", encoded, 8)[0]
payload = encoded[config_manager.REQUEST_HEADER_SIZE :]
assert payload_size == len(payload)
assert struct.unpack_from("<I", encoded, 12)[0] == (
zlib.crc32(payload) & 0xFFFFFFFF
)
if request == config_manager.OP_CONFIGURATION_BEGIN:
(
self.transaction_id,
_schema,
self.transaction_expected_size,
self.transaction_expected_crc,
) = struct.unpack("<IHHI", payload)
self.transaction_payload = bytearray()
self.transaction_status = config_manager.STATUS_PENDING
elif request == config_manager.OP_CONFIGURATION_CHUNK:
transaction_id, offset, chunk_size = struct.unpack_from(
"<IHH", payload
)
assert transaction_id == self.transaction_id
assert offset == len(self.transaction_payload)
self.transaction_payload.extend(payload[8 : 8 + chunk_size])
elif request == config_manager.OP_CONFIGURATION_COMMIT:
assert struct.unpack("<I", payload)[0] == self.transaction_id
assert len(self.transaction_payload) == self.transaction_expected_size
assert (
zlib.crc32(self.transaction_payload) & 0xFFFFFFFF
) == self.transaction_expected_crc
self.configuration = bytes(self.transaction_payload)
self.configuration_generation += 1
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)
self.configuration_generation += 1
self.transaction_payload = bytearray(self.configuration)
self.transaction_expected_size = len(self.configuration)
self.transaction_expected_crc = (
zlib.crc32(self.configuration) & 0xFFFFFFFF
)
self.transaction_status = config_manager.STATUS_OK
elif request == config_manager.OP_PAIRING_REFRESH:
self.pairing_generation += 1
elif request == config_manager.OP_PAIRING_CLEAR:
self.records = []
self.pairing_generation += 1
else:
raise AssertionError(f"unexpected OUT request {request}")
return len(encoded)
def test_response_validation() -> None:
payload = make_response(config_manager.OP_INFO, b"12345678")
envelope = config_manager.parse_response(payload, config_manager.OP_INFO)
assert envelope.payload == b"12345678"
malformed = [
b"",
b"NOPE" + bytes(config_manager.RESPONSE_HEADER_SIZE - 4),
make_response(config_manager.OP_INFO, b"12345678")[:-1],
make_response(config_manager.OP_INFO, b"12345678") + b"x",
]
bad_crc = bytearray(make_response(config_manager.OP_INFO, b"12345678"))
bad_crc[-1] ^= 1
malformed.append(bytes(bad_crc))
for response in malformed:
with pytest.raises(config_manager.ConfigManagerError):
config_manager.parse_response(response, config_manager.OP_INFO)
def test_configuration_transaction_and_reset() -> None:
device = FakeDevice()
before = config_manager.read_configuration(device)
assert before.pairing_window_seconds == 60
status = config_manager.write_configuration(
device,
config_manager.AdapterConfiguration(90, before.generation, before.crc),
1.0,
)
assert status.stored_generation == 4
assert config_manager.read_configuration(device).pairing_window_seconds == 90
reset = config_manager.reset_configuration(device, 1.0)
assert reset.stored_generation == 5
assert config_manager.read_configuration(device).pairing_window_seconds == 60
def test_status_and_pairing_commands(
monkeypatch: pytest.MonkeyPatch,
capsys: pytest.CaptureFixture[str],
) -> None:
device = FakeDevice()
monkeypatch.setattr(config_manager, "_candidate_devices", lambda: [device])
assert config_manager.main(["status"]) == 0
output = capsys.readouterr().out
assert "Firmware: 0.2.0" in output
assert "Pairing window: 60 seconds" in output
assert config_manager.main(["pairings", "list"]) == 0
output = capsys.readouterr().out
assert "Classic 01:02:03:04:05:06" in output
assert "BLE (public identity) A1:A2:A3:A4:A5:A6" in output
assert config_manager.main(["pairings", "clear"]) == 2
assert "requires --yes" in capsys.readouterr().err
assert config_manager.main(["pairings", "clear", "--yes"]) == 0
assert capsys.readouterr().out == "Cleared 2 stored pairing(s).\n"
def test_find_requires_selector_for_multiple_picos(
monkeypatch: pytest.MonkeyPatch,
) -> None:
first = FakeDevice()
second = FakeDevice()
second.address = 8
monkeypatch.setattr(
config_manager, "_candidate_devices", lambda: [first, second]
)
with pytest.raises(
config_manager.ConfigManagerError,
match="multiple switch-pico devices",
):
config_manager.find_pico(None, None)
assert config_manager.find_pico(1, 8) is second

View file

@ -0,0 +1,31 @@
import shutil
import subprocess
from pathlib import Path
def test_configuration_storage_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_storage_test"
subprocess.run(
[
compiler,
"-std=c++17",
"-Wall",
"-Wextra",
"-Werror",
"-pedantic",
f"-I{root}",
str(root / "tests" / "configuration_storage_test.cpp"),
str(root / "adapter_configuration.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)

View file

@ -1,156 +0,0 @@
from __future__ import annotations
import struct
import pytest
import switch_pico_bridge.pairing_manager as pairing_manager
def make_payload(
generation: int,
records: list[tuple[int, int, bytes]],
*,
status: int = pairing_manager.STATUS_READY,
overflow: bool = False,
) -> bytes:
payload = bytearray(b"SPPM")
payload.extend(
[
pairing_manager.PROTOCOL_VERSION,
status,
len(records),
int(overflow),
]
)
payload.extend(struct.pack("<I", generation))
for transport, address_type, address in records:
payload.extend([transport, address_type])
payload.extend(address)
return bytes(payload)
class FakeDevice:
bus = 1
address = 7
def __init__(self) -> None:
self.generation = 3
self.records = [
(
pairing_manager.TRANSPORT_CLASSIC,
0xFE,
bytes.fromhex("010203040506"),
),
(
pairing_manager.TRANSPORT_BLE,
2,
bytes.fromhex("A1A2A3A4A5A6"),
),
]
self.requests: list[int] = []
def ctrl_transfer(
self,
bm_request_type: int,
request: int,
value: int,
index: int,
data_or_w_length: object,
timeout: int,
) -> bytes | int:
assert value == pairing_manager.REQUEST_VALUE
assert index == pairing_manager.REQUEST_INDEX
assert timeout == pairing_manager.USB_TIMEOUT_MS
self.requests.append(request)
if bm_request_type == 0xC0:
assert request == pairing_manager.REQUEST_GET
return make_payload(self.generation, self.records)
assert bm_request_type == 0x40
if request == pairing_manager.REQUEST_REFRESH:
self.generation += 1
elif request == pairing_manager.REQUEST_CLEAR:
self.records = []
self.generation += 1
else:
raise AssertionError(f"unexpected request {request}")
return 0
def test_parse_snapshot() -> None:
snapshot = pairing_manager.parse_snapshot(
make_payload(
0x78563412,
[
(
pairing_manager.TRANSPORT_CLASSIC,
0xFE,
bytes.fromhex("010203040506"),
),
(
pairing_manager.TRANSPORT_BLE,
3,
bytes.fromhex("A1A2A3A4A5A6"),
),
],
overflow=True,
)
)
assert snapshot.generation == 0x78563412
assert snapshot.overflow
assert snapshot.records[0].transport_text == "Classic"
assert snapshot.records[0].address_text == "01:02:03:04:05:06"
assert snapshot.records[1].transport_text == "BLE (random identity)"
@pytest.mark.parametrize(
"payload",
[
b"",
b"NOPE" + bytes(8),
b"SPPM\x02" + bytes(7),
b"SPPM\x01\x00\x11\x00" + bytes(4),
],
)
def test_parse_rejects_invalid_payload(payload: bytes) -> None:
with pytest.raises(pairing_manager.PairingManagerError):
pairing_manager.parse_snapshot(payload)
def test_list_and_clear_commands(
monkeypatch: pytest.MonkeyPatch,
capsys: pytest.CaptureFixture[str],
) -> None:
device = FakeDevice()
monkeypatch.setattr(pairing_manager, "_candidate_devices", lambda: [device])
assert pairing_manager.main(["list"]) == 0
output = capsys.readouterr().out
assert "Classic 01:02:03:04:05:06" in output
assert "BLE (public identity) A1:A2:A3:A4:A5:A6" in output
assert pairing_manager.main(["clear"]) == 2
assert "requires --yes" in capsys.readouterr().err
assert pairing_manager.main(["clear", "--yes"]) == 0
assert capsys.readouterr().out == "Cleared 2 stored pairing(s).\n"
assert device.records == []
assert pairing_manager.REQUEST_REFRESH in device.requests
assert pairing_manager.REQUEST_CLEAR in device.requests
def test_find_requires_selector_for_multiple_picos(
monkeypatch: pytest.MonkeyPatch,
) -> None:
first = FakeDevice()
second = FakeDevice()
second.address = 8
monkeypatch.setattr(
pairing_manager, "_candidate_devices", lambda: [first, second]
)
with pytest.raises(
pairing_manager.PairingManagerError,
match="multiple switch-pico devices",
):
pairing_manager.find_pico(None, None)
assert pairing_manager.find_pico(1, 8) is second

View file

@ -4,12 +4,12 @@ from pathlib import Path
def test_usb_pairing_management_native(tmp_path: Path) -> None:
def test_usb_configuration_management_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 / "usb_pairing_management_test"
executable = tmp_path / "usb_configuration_management_test"
subprocess.run(
[
compiler,
@ -20,7 +20,7 @@ def test_usb_pairing_management_native(tmp_path: Path) -> None:
"-pedantic",
f"-I{root / 'tests' / 'usb_management_native_stubs'}",
f"-I{root}",
str(root / "tests" / "usb_pairing_management_test.cpp"),
str(root / "tests" / "usb_configuration_management_test.cpp"),
"-o",
str(executable),
],

View file

@ -0,0 +1,301 @@
#include "usb_configuration_management.h"
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <vector>
#include <tusb.h>
namespace {
Bluepad32PairingSnapshot current_pairings{};
ConfigurationServiceSnapshot current_configuration{};
bool refresh_requested = false;
bool clear_requested = false;
std::vector<uint8_t> control_payload;
std::vector<uint8_t> next_out_payload;
uint32_t begin_transaction_id = 0;
uint32_t append_transaction_id = 0;
uint32_t commit_transaction_id = 0;
size_t append_offset = 0;
std::vector<uint8_t> appended_bytes;
void require(bool condition, const char* message) {
if (!condition) {
std::cerr << message << '\n';
std::exit(1);
}
}
void write_u16(std::vector<uint8_t>* output, size_t offset,
uint16_t value) {
(*output)[offset] = static_cast<uint8_t>(value);
(*output)[offset + 1] = static_cast<uint8_t>(value >> 8);
}
void write_u32(std::vector<uint8_t>* output, size_t offset,
uint32_t value) {
(*output)[offset] = static_cast<uint8_t>(value);
(*output)[offset + 1] = static_cast<uint8_t>(value >> 8);
(*output)[offset + 2] = static_cast<uint8_t>(value >> 16);
(*output)[offset + 3] = static_cast<uint8_t>(value >> 24);
}
std::vector<uint8_t> make_request(
UsbConfigurationManagement::Operation operation,
const std::vector<uint8_t>& payload) {
using namespace UsbConfigurationManagement;
std::vector<uint8_t> request(kRequestHeaderSize + payload.size());
memcpy(request.data(), "SPMG", 4);
request[4] = kProtocolVersion;
request[5] = static_cast<uint8_t>(operation);
write_u16(&request, 8, static_cast<uint16_t>(payload.size()));
write_u32(&request, 12,
configuration_crc32(payload.data(), payload.size()));
memcpy(request.data() + kRequestHeaderSize, payload.data(),
payload.size());
return request;
}
tusb_control_request_t setup_request(
UsbConfigurationManagement::Operation operation, uint8_t direction,
uint16_t length) {
tusb_control_request_t request{};
request.bmRequestType_bit.recipient = TUSB_REQ_RCPT_DEVICE;
request.bmRequestType_bit.type = TUSB_REQ_TYPE_VENDOR;
request.bmRequestType_bit.direction = direction;
request.bRequest = static_cast<uint8_t>(operation);
request.wValue = UsbConfigurationManagement::kRequestValue;
request.wIndex = UsbConfigurationManagement::kRequestIndex;
request.wLength = length;
return request;
}
void test_envelope_encoding() {
using namespace UsbConfigurationManagement;
const uint8_t payload[] = {1, 2, 3};
uint8_t encoded[32]{};
const size_t size = encode_response(
Operation::kConfigurationRead, Status::kOk, 5, 1,
0x78563412, payload, sizeof(payload), encoded, sizeof(encoded));
require(size == kResponseHeaderSize + sizeof(payload) &&
memcmp(encoded, "SPMG", 4) == 0 &&
encoded[4] == kProtocolVersion &&
encoded[5] ==
static_cast<uint8_t>(Operation::kConfigurationRead) &&
encoded[6] == static_cast<uint8_t>(Status::kOk) &&
encoded[7] == 5 && encoded[8] == 3 &&
encoded[10] == 1 && encoded[12] == 0x12 &&
encoded[15] == 0x78 &&
memcmp(&encoded[kResponseHeaderSize], payload,
sizeof(payload)) == 0,
"versioned response envelope encoded incorrectly");
require(encode_response(
Operation::kConfigurationRead, Status::kOk, 0, 1, 0,
payload, sizeof(payload), encoded, size - 1) == 0,
"response encoder accepted a short destination");
}
void test_pairing_encoding() {
using namespace UsbConfigurationManagement;
Bluepad32PairingSnapshot snapshot{};
snapshot.generation = 0x78563412;
snapshot.status = Bluepad32PairingSnapshotStatus::kReady;
snapshot.record_count = 2;
snapshot.overflow = true;
snapshot.records[0].transport =
Bluepad32PairingTransport::kClassic;
snapshot.records[0].address_type = 0xfe;
const uint8_t classic_address[6] = {1, 2, 3, 4, 5, 6};
memcpy(snapshot.records[0].address, classic_address, 6);
snapshot.records[1].transport = Bluepad32PairingTransport::kBle;
snapshot.records[1].address_type = 2;
const uint8_t ble_address[6] = {6, 5, 4, 3, 2, 1};
memcpy(snapshot.records[1].address, ble_address, 6);
uint8_t payload[kMaximumResponseSize]{};
const size_t size =
encode_pairing_snapshot(snapshot, payload, sizeof(payload));
require(size == kResponseHeaderSize + kPairingPayloadHeaderSize +
2 * kPairingRecordSize &&
payload[5] ==
static_cast<uint8_t>(Operation::kPairingRead) &&
payload[7] == 1 && payload[12] == 0x12 &&
payload[kResponseHeaderSize] == 2 &&
payload[kResponseHeaderSize + 1] == 1 &&
payload[kResponseHeaderSize + 4] == 1 &&
payload[kResponseHeaderSize + 5] == 0xfe &&
memcmp(&payload[kResponseHeaderSize + 6],
classic_address, 6) == 0,
"pairings were not migrated into the versioned envelope");
}
void perform_out(UsbConfigurationManagement::Operation operation,
const std::vector<uint8_t>& payload,
bool expected_ack = true) {
next_out_payload = make_request(operation, payload);
tusb_control_request_t request = setup_request(
operation, TUSB_DIR_OUT,
static_cast<uint16_t>(next_out_payload.size()));
require(tud_vendor_control_xfer_cb(
0, CONTROL_STAGE_SETUP, &request),
"valid OUT setup was rejected");
require(tud_vendor_control_xfer_cb(
0, CONTROL_STAGE_ACK, &request) == expected_ack,
"OUT acknowledgement result was incorrect");
}
void test_vendor_requests() {
using namespace UsbConfigurationManagement;
current_pairings = {};
current_pairings.generation = 7;
current_pairings.status = Bluepad32PairingSnapshotStatus::kReady;
current_pairings.record_count = 1;
current_pairings.records[0].transport =
Bluepad32PairingTransport::kClassic;
tusb_control_request_t request = setup_request(
Operation::kPairingRead, TUSB_DIR_IN, kMaximumResponseSize);
require(tud_vendor_control_xfer_cb(
0, CONTROL_STAGE_SETUP, &request) &&
control_payload[5] ==
static_cast<uint8_t>(Operation::kPairingRead) &&
control_payload[12] == 7,
"pairing read did not use the versioned envelope");
perform_out(Operation::kPairingRefresh, {});
require(refresh_requested,
"pairing refresh was not dispatched");
perform_out(Operation::kPairingClear, {});
require(clear_requested, "pairing clear was not dispatched");
std::vector<uint8_t> begin(12);
write_u32(&begin, 0, 0x11223344);
write_u16(&begin, 4, ADAPTER_CONFIGURATION_SCHEMA_VERSION);
write_u16(&begin, 6, ADAPTER_CONFIGURATION_ENCODED_SIZE);
write_u32(&begin, 8, 0xaabbccdd);
perform_out(Operation::kConfigurationBegin, begin);
require(begin_transaction_id == 0x11223344,
"configuration begin was not dispatched");
std::vector<uint8_t> chunk(12);
write_u32(&chunk, 0, 0x11223344);
write_u16(&chunk, 4, 0);
write_u16(&chunk, 6, 4);
chunk[8] = 60;
perform_out(Operation::kConfigurationChunk, chunk);
require(append_transaction_id == 0x11223344 &&
append_offset == 0 && appended_bytes.size() == 4,
"configuration chunk was not dispatched");
std::vector<uint8_t> commit(4);
write_u32(&commit, 0, 0x11223344);
perform_out(Operation::kConfigurationCommit, commit);
require(commit_transaction_id == 0x11223344,
"configuration commit was not dispatched");
next_out_payload =
make_request(Operation::kPairingRefresh, {});
next_out_payload[12] ^= 1;
request = setup_request(
Operation::kPairingRefresh, TUSB_DIR_OUT,
static_cast<uint16_t>(next_out_payload.size()));
require(tud_vendor_control_xfer_cb(
0, CONTROL_STAGE_SETUP, &request) &&
!tud_vendor_control_xfer_cb(
0, CONTROL_STAGE_ACK, &request),
"bad request CRC was accepted");
request.wValue = 0;
require(!tud_vendor_control_xfer_cb(
0, CONTROL_STAGE_SETUP, &request),
"request with invalid magic was accepted");
}
} // namespace
uint32_t configuration_crc32(const uint8_t* data, size_t size) {
uint32_t crc = 0xffffffffu;
for (size_t index = 0; index < size; ++index) {
crc ^= data[index];
for (uint8_t bit = 0; bit < 8; ++bit) {
const uint32_t mask = 0u - (crc & 1u);
crc = (crc >> 1) ^ (0xedb88320u & mask);
}
}
return ~crc;
}
void configuration_service_snapshot(ConfigurationServiceSnapshot* output) {
*output = current_configuration;
}
ConfigurationTransactionStatus configuration_service_begin(
uint32_t transaction_id, uint16_t, size_t, uint32_t) {
begin_transaction_id = transaction_id;
return ConfigurationTransactionStatus::kReceiving;
}
ConfigurationTransactionStatus configuration_service_append(
uint32_t transaction_id, size_t offset, const uint8_t* data,
size_t size) {
append_transaction_id = transaction_id;
append_offset = offset;
appended_bytes.assign(data, data + size);
return ConfigurationTransactionStatus::kReceiving;
}
ConfigurationTransactionStatus configuration_service_commit(
uint32_t transaction_id) {
commit_transaction_id = transaction_id;
return ConfigurationTransactionStatus::kPending;
}
ConfigurationTransactionStatus configuration_service_reset(uint32_t) {
return ConfigurationTransactionStatus::kPending;
}
void bluepad32_input_backend_request_pairing_snapshot() {
refresh_requested = true;
}
void bluepad32_input_backend_clear_pairings() {
clear_requested = true;
}
void bluepad32_input_backend_pairing_snapshot(
Bluepad32PairingSnapshot* out) {
*out = current_pairings;
}
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) {
if (next_out_payload.size() != length) {
return false;
}
memcpy(buffer, next_out_payload.data(), length);
} else {
const auto* bytes = static_cast<const uint8_t*>(buffer);
control_payload.assign(bytes, bytes + length);
}
return true;
}
bool tud_control_status(uint8_t, const tusb_control_request_t*) {
return true;
}
#include "../adapter_configuration.cpp"
#include "../usb_configuration_management.cpp"
int main() {
current_configuration.state = ConfigurationServiceState::kReady;
current_configuration.configuration =
adapter_configuration_default();
test_envelope_encoding();
test_pairing_encoding();
test_vendor_requests();
return 0;
}

View file

@ -9,6 +9,7 @@ enum {
CONTROL_STAGE_ACK = 2,
TUSB_REQ_RCPT_DEVICE = 0,
TUSB_DIR_OUT = 0,
TUSB_REQ_TYPE_VENDOR = 2,
TUSB_DIR_IN = 1,
};

View file

@ -1,144 +0,0 @@
#include "usb_pairing_management.h"
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <vector>
#include <tusb.h>
namespace {
Bluepad32PairingSnapshot current_snapshot{};
bool refresh_requested = false;
bool clear_requested = false;
bool control_status_sent = false;
std::vector<uint8_t> control_payload;
void require(bool condition, const char* message) {
if (!condition) {
std::cerr << message << '\n';
std::exit(1);
}
}
void test_encoding() {
Bluepad32PairingSnapshot snapshot{};
snapshot.generation = 0x78563412;
snapshot.status = Bluepad32PairingSnapshotStatus::kReady;
snapshot.record_count = 2;
snapshot.overflow = true;
snapshot.records[0].transport =
Bluepad32PairingTransport::kClassic;
snapshot.records[0].address_type = 0xfe;
const uint8_t classic_address[6] = {1, 2, 3, 4, 5, 6};
memcpy(snapshot.records[0].address, classic_address, 6);
snapshot.records[1].transport = Bluepad32PairingTransport::kBle;
snapshot.records[1].address_type = 2;
const uint8_t ble_address[6] = {6, 5, 4, 3, 2, 1};
memcpy(snapshot.records[1].address, ble_address, 6);
uint8_t payload[UsbPairingManagement::kMaximumResponseSize]{};
const size_t size = UsbPairingManagement::encode_snapshot(
snapshot, payload, sizeof(payload));
require(size == UsbPairingManagement::kResponseHeaderSize +
2 * UsbPairingManagement::kRecordSize,
"snapshot encoded with the wrong size");
require(memcmp(payload, "SPPM", 4) == 0 &&
payload[4] == UsbPairingManagement::kProtocolVersion &&
payload[5] == 0 && payload[6] == 2 && payload[7] == 1,
"snapshot header encoding is invalid");
require(payload[8] == 0x12 && payload[9] == 0x34 &&
payload[10] == 0x56 && payload[11] == 0x78,
"snapshot generation is not little endian");
require(payload[12] == 1 && payload[13] == 0xfe &&
memcmp(&payload[14], classic_address, 6) == 0 &&
payload[20] == 2 && payload[21] == 2 &&
memcmp(&payload[22], ble_address, 6) == 0,
"pairing records are encoded incorrectly");
require(UsbPairingManagement::encode_snapshot(
snapshot, payload, size - 1) == 0,
"encoder accepted a short destination buffer");
}
void test_vendor_requests() {
current_snapshot = {};
current_snapshot.generation = 7;
current_snapshot.status = Bluepad32PairingSnapshotStatus::kReady;
current_snapshot.record_count = 1;
current_snapshot.records[0].transport =
Bluepad32PairingTransport::kClassic;
tusb_control_request_t request{};
request.bmRequestType_bit.recipient = TUSB_REQ_RCPT_DEVICE;
request.bmRequestType_bit.direction = TUSB_DIR_IN;
request.bRequest = UsbPairingManagement::kRequestGet;
request.wValue = UsbPairingManagement::kRequestValue;
request.wIndex = UsbPairingManagement::kRequestIndex;
request.wLength = UsbPairingManagement::kMaximumResponseSize;
require(tud_vendor_control_xfer_cb(
0, CONTROL_STAGE_SETUP, &request) &&
control_payload.size() ==
UsbPairingManagement::kResponseHeaderSize +
UsbPairingManagement::kRecordSize &&
control_payload[8] == 7,
"GET request did not return the current pairing snapshot");
request.bmRequestType_bit.direction = TUSB_DIR_OUT;
request.wLength = 0;
request.bRequest = UsbPairingManagement::kRequestRefresh;
require(tud_vendor_control_xfer_cb(
0, CONTROL_STAGE_SETUP, &request) &&
refresh_requested && control_status_sent,
"REFRESH request was not acknowledged and queued");
control_status_sent = false;
request.bRequest = UsbPairingManagement::kRequestClear;
require(tud_vendor_control_xfer_cb(
0, CONTROL_STAGE_SETUP, &request) &&
clear_requested && control_status_sent,
"CLEAR request was not acknowledged and queued");
request.wValue = 0;
require(!tud_vendor_control_xfer_cb(
0, CONTROL_STAGE_SETUP, &request),
"request with invalid magic was accepted");
require(tud_vendor_control_xfer_cb(
0, CONTROL_STAGE_ACK, &request),
"non-setup control stage was rejected");
}
} // namespace
void bluepad32_input_backend_request_pairing_snapshot() {
refresh_requested = true;
}
void bluepad32_input_backend_clear_pairings() {
clear_requested = true;
}
void bluepad32_input_backend_pairing_snapshot(
Bluepad32PairingSnapshot* out) {
*out = current_snapshot;
}
bool tud_control_xfer(uint8_t, const tusb_control_request_t*,
void* buffer, uint16_t length) {
const auto* bytes = static_cast<const uint8_t*>(buffer);
control_payload.assign(bytes, bytes + length);
return true;
}
bool tud_control_status(uint8_t, const tusb_control_request_t*) {
control_status_sent = true;
return true;
}
#include "../usb_pairing_management.cpp"
int main() {
test_encoding();
test_vendor_requests();
return 0;
}

View file

@ -0,0 +1,377 @@
#include "usb_configuration_management.h"
#include <string.h>
#include "adapter_configuration.h"
#include "tusb.h"
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
#include "adapter_host_probe.h"
#endif
namespace UsbConfigurationManagement {
namespace {
uint16_t read_u16(const uint8_t* input) {
return static_cast<uint16_t>(input[0]) |
static_cast<uint16_t>(input[1] << 8);
}
uint32_t read_u32(const uint8_t* input) {
return static_cast<uint32_t>(input[0]) |
(static_cast<uint32_t>(input[1]) << 8) |
(static_cast<uint32_t>(input[2]) << 16) |
(static_cast<uint32_t>(input[3]) << 24);
}
void write_u16(uint8_t* output, uint16_t value) {
output[0] = static_cast<uint8_t>(value);
output[1] = static_cast<uint8_t>(value >> 8);
}
void write_u32(uint8_t* output, uint32_t value) {
output[0] = static_cast<uint8_t>(value);
output[1] = static_cast<uint8_t>(value >> 8);
output[2] = static_cast<uint8_t>(value >> 16);
output[3] = static_cast<uint8_t>(value >> 24);
}
Status transaction_status(ConfigurationTransactionStatus status) {
switch (status) {
case ConfigurationTransactionStatus::kIdle:
case ConfigurationTransactionStatus::kCommitted:
case ConfigurationTransactionStatus::kUnchanged:
return Status::kOk;
case ConfigurationTransactionStatus::kReceiving:
case ConfigurationTransactionStatus::kPending:
return Status::kPending;
case ConfigurationTransactionStatus::kMalformed:
return Status::kMalformed;
case ConfigurationTransactionStatus::kUnsupportedSchema:
return Status::kUnsupportedSchema;
case ConfigurationTransactionStatus::kTooLarge:
return Status::kTooLarge;
case ConfigurationTransactionStatus::kOutOfOrder:
return Status::kOutOfOrder;
case ConfigurationTransactionStatus::kBadCrc:
return Status::kBadCrc;
case ConfigurationTransactionStatus::kBusy:
return Status::kBusy;
case ConfigurationTransactionStatus::kStorageError:
return Status::kStorageError;
}
return Status::kStorageError;
}
bool valid_out_size(Operation operation, size_t size) {
switch (operation) {
case Operation::kConfigurationBegin:
return size == kRequestHeaderSize + 12;
case Operation::kConfigurationChunk:
return size > kRequestHeaderSize + 8 &&
size <= kMaximumRequestSize;
case Operation::kConfigurationCommit:
case Operation::kConfigurationReset:
return size == kRequestHeaderSize + 4;
case Operation::kPairingRefresh:
case Operation::kPairingClear:
return size == kRequestHeaderSize;
default:
return false;
}
}
size_t encode_configuration(uint8_t* output, size_t output_size) {
ConfigurationServiceSnapshot snapshot{};
configuration_service_snapshot(&snapshot);
uint8_t payload[ADAPTER_CONFIGURATION_ENCODED_SIZE]{};
const bool encoded = adapter_configuration_encode(
snapshot.configuration, payload, sizeof(payload));
Status status = Status::kOk;
if (snapshot.state == ConfigurationServiceState::kLoading) {
status = Status::kPending;
} else if (snapshot.state ==
ConfigurationServiceState::kStorageError) {
status = Status::kStorageError;
}
return encoded
? encode_response(
Operation::kConfigurationRead, status, 0,
ADAPTER_CONFIGURATION_SCHEMA_VERSION,
snapshot.generation, payload, sizeof(payload),
output, output_size)
: 0;
}
size_t encode_transaction(uint8_t* output, size_t output_size) {
ConfigurationServiceSnapshot snapshot{};
configuration_service_snapshot(&snapshot);
const ConfigurationTransactionSnapshot& transaction =
snapshot.transaction;
uint8_t payload[20]{};
write_u32(&payload[0], transaction.transaction_id);
write_u16(&payload[4], transaction.received_size);
write_u16(&payload[6], transaction.expected_size);
write_u32(&payload[8], transaction.expected_crc);
write_u32(&payload[12], transaction.stored_generation);
write_u32(&payload[16], transaction.stored_crc);
return encode_response(
Operation::kTransactionStatus,
transaction_status(transaction.status), 0,
ADAPTER_CONFIGURATION_SCHEMA_VERSION,
transaction.stored_generation, payload, sizeof(payload),
output, 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
0,
static_cast<uint8_t>(CONFIGURATION_STORAGE_MAX_PAYLOAD_SIZE),
static_cast<uint8_t>(
CONFIGURATION_STORAGE_MAX_PAYLOAD_SIZE >> 8),
};
return encode_response(Operation::kInfo, Status::kOk, 0, 0, 0,
payload, sizeof(payload), output, output_size);
}
} // namespace
bool decode_request(Operation setup_operation, const uint8_t* input,
size_t input_size, DecodedRequest* output) {
if (input == nullptr || output == nullptr ||
input_size < kRequestHeaderSize ||
memcmp(input, "SPMG", 4) != 0 ||
input[4] != kProtocolVersion ||
input[5] != static_cast<uint8_t>(setup_operation) ||
input[6] != 0 || input[7] != 0 ||
read_u16(&input[10]) != 0) {
return false;
}
const uint16_t payload_size = read_u16(&input[8]);
if (input_size != kRequestHeaderSize + payload_size ||
configuration_crc32(&input[kRequestHeaderSize], payload_size) !=
read_u32(&input[12])) {
return false;
}
output->operation = setup_operation;
output->payload = &input[kRequestHeaderSize];
output->payload_size = payload_size;
return true;
}
size_t encode_response(Operation operation, Status status, uint8_t flags,
uint16_t schema_version, uint32_t generation,
const uint8_t* payload, size_t payload_size,
uint8_t* output, size_t output_size) {
const size_t required = kResponseHeaderSize + payload_size;
if (output == nullptr || output_size < required ||
payload_size > UINT16_MAX ||
(payload_size != 0 && payload == nullptr)) {
return 0;
}
memcpy(output, "SPMG", 4);
output[4] = kProtocolVersion;
output[5] = static_cast<uint8_t>(operation);
output[6] = static_cast<uint8_t>(status);
output[7] = flags;
write_u16(&output[8], static_cast<uint16_t>(payload_size));
write_u16(&output[10], schema_version);
write_u32(&output[12], generation);
write_u32(&output[16],
configuration_crc32(payload, payload_size));
if (payload_size != 0) {
memcpy(&output[kResponseHeaderSize], payload, payload_size);
}
return required;
}
size_t encode_pairing_snapshot(const Bluepad32PairingSnapshot& snapshot,
uint8_t* output, size_t output_size) {
if (snapshot.record_count > BLUEPAD32_PAIRING_RECORD_CAPACITY) {
return 0;
}
uint8_t payload[kPairingPayloadHeaderSize +
BLUEPAD32_PAIRING_RECORD_CAPACITY *
kPairingRecordSize]{};
payload[0] = snapshot.record_count;
payload[1] = snapshot.overflow ? 1 : 0;
size_t offset = kPairingPayloadHeaderSize;
for (uint8_t index = 0; index < snapshot.record_count; ++index) {
const Bluepad32PairingRecord& record = snapshot.records[index];
payload[offset] = static_cast<uint8_t>(record.transport);
payload[offset + 1] = record.address_type;
memcpy(&payload[offset + 2], record.address,
sizeof(record.address));
offset += kPairingRecordSize;
}
return encode_response(
Operation::kPairingRead,
snapshot.status == Bluepad32PairingSnapshotStatus::kReady
? Status::kOk
: Status::kPending,
snapshot.overflow ? 1 : 0, 0, snapshot.generation,
payload, offset, output, output_size);
}
} // namespace UsbConfigurationManagement
namespace {
uint8_t g_request_buffer[
UsbConfigurationManagement::kMaximumRequestSize]{};
UsbConfigurationManagement::Operation g_pending_operation =
UsbConfigurationManagement::Operation::kInfo;
bool g_out_pending = false;
size_t g_pending_request_size = 0;
bool process_out_request() {
using namespace UsbConfigurationManagement;
DecodedRequest request{};
if (!decode_request(g_pending_operation, g_request_buffer,
g_pending_request_size, &request)) {
return false;
}
const uint8_t* payload = request.payload;
switch (request.operation) {
case Operation::kConfigurationBegin:
configuration_service_begin(
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),
static_cast<uint16_t>(payload[4] |
(payload[5] << 8)),
static_cast<uint16_t>(payload[6] |
(payload[7] << 8)),
static_cast<uint32_t>(payload[8]) |
(static_cast<uint32_t>(payload[9]) << 8) |
(static_cast<uint32_t>(payload[10]) << 16) |
(static_cast<uint32_t>(payload[11]) << 24));
return true;
case Operation::kConfigurationChunk: {
const uint16_t chunk_size =
static_cast<uint16_t>(payload[6] |
(payload[7] << 8));
if (request.payload_size != 8 + chunk_size ||
chunk_size > kMaximumChunkSize) {
return false;
}
configuration_service_append(
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),
static_cast<uint16_t>(payload[4] |
(payload[5] << 8)),
&payload[8], chunk_size);
return true;
}
case Operation::kConfigurationCommit:
configuration_service_commit(
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));
return true;
case Operation::kConfigurationReset:
configuration_service_reset(
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));
return true;
case Operation::kPairingRefresh:
bluepad32_input_backend_request_pairing_snapshot();
return true;
case Operation::kPairingClear:
bluepad32_input_backend_clear_pairings();
return true;
default:
return false;
}
}
} // namespace
extern "C" bool tud_vendor_control_xfer_cb(
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 ||
request->bmRequestType_bit.recipient != TUSB_REQ_RCPT_DEVICE ||
request->wValue != kRequestValue ||
request->wIndex != kRequestIndex) {
return false;
}
const Operation operation =
static_cast<Operation>(request->bRequest);
if (stage == CONTROL_STAGE_ACK) {
if (request->bmRequestType_bit.direction == TUSB_DIR_IN) {
return true;
}
if (!g_out_pending || operation != g_pending_operation) {
return false;
}
g_out_pending = false;
return process_out_request();
}
if (stage == CONTROL_STAGE_DATA) {
return true;
}
if (stage != CONTROL_STAGE_SETUP) {
return false;
}
if (request->bmRequestType_bit.direction == TUSB_DIR_OUT) {
if (!valid_out_size(operation, request->wLength)) {
return false;
}
g_pending_operation = operation;
g_pending_request_size = request->wLength;
g_out_pending = true;
return tud_control_xfer(rhport, request, g_request_buffer,
request->wLength);
}
static uint8_t response[kMaximumResponseSize]{};
size_t response_size = 0;
switch (operation) {
case Operation::kInfo:
response_size = encode_info(response, sizeof(response));
break;
case Operation::kConfigurationRead:
response_size =
encode_configuration(response, sizeof(response));
break;
case Operation::kTransactionStatus:
response_size =
encode_transaction(response, sizeof(response));
break;
case Operation::kPairingRead: {
Bluepad32PairingSnapshot snapshot{};
bluepad32_input_backend_pairing_snapshot(&snapshot);
response_size = encode_pairing_snapshot(
snapshot, response, sizeof(response));
break;
}
default:
return false;
}
return response_size != 0 &&
tud_control_xfer(
rhport, request, response,
static_cast<uint16_t>(response_size));
}

View file

@ -0,0 +1,65 @@
#pragma once
#include <stddef.h>
#include <stdint.h>
#include "bluepad32_input_backend.h"
#include "configuration_service.h"
namespace UsbConfigurationManagement {
constexpr uint16_t kRequestValue = 0x5350;
constexpr uint16_t kRequestIndex = 0x0001;
constexpr uint8_t kProtocolVersion = 1;
constexpr size_t kRequestHeaderSize = 16;
constexpr size_t kResponseHeaderSize = 20;
constexpr size_t kPairingRecordSize = 8;
constexpr size_t kPairingPayloadHeaderSize = 4;
constexpr size_t kMaximumRequestSize = 64;
constexpr size_t kMaximumResponseSize =
kResponseHeaderSize + kPairingPayloadHeaderSize +
BLUEPAD32_PAIRING_RECORD_CAPACITY * kPairingRecordSize;
constexpr size_t kMaximumChunkSize =
kMaximumRequestSize - kRequestHeaderSize - 8;
enum class Operation : uint8_t {
kInfo = 0x01,
kConfigurationRead = 0x10,
kConfigurationBegin = 0x11,
kConfigurationChunk = 0x12,
kConfigurationCommit = 0x13,
kConfigurationReset = 0x14,
kTransactionStatus = 0x15,
kPairingRead = 0x20,
kPairingRefresh = 0x21,
kPairingClear = 0x22,
};
enum class Status : uint8_t {
kOk = 0,
kPending = 1,
kMalformed = 2,
kUnsupportedSchema = 3,
kTooLarge = 4,
kOutOfOrder = 5,
kBadCrc = 6,
kBusy = 7,
kStorageError = 8,
};
struct DecodedRequest {
Operation operation = Operation::kInfo;
const uint8_t* payload = nullptr;
uint16_t payload_size = 0;
};
bool decode_request(Operation setup_operation, const uint8_t* input,
size_t input_size, DecodedRequest* output);
size_t encode_response(Operation operation, Status status, uint8_t flags,
uint16_t schema_version, uint32_t generation,
const uint8_t* payload, size_t payload_size,
uint8_t* output, size_t output_size);
size_t encode_pairing_snapshot(const Bluepad32PairingSnapshot& snapshot,
uint8_t* output, size_t output_size);
} // namespace UsbConfigurationManagement

View file

@ -1,100 +0,0 @@
#include "usb_pairing_management.h"
#include <string.h>
#include "tusb.h"
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
#include "adapter_host_probe.h"
#endif
namespace UsbPairingManagement {
size_t encode_snapshot(const Bluepad32PairingSnapshot& snapshot,
uint8_t* output, size_t output_size) {
const size_t required =
kResponseHeaderSize + snapshot.record_count * kRecordSize;
if (output == nullptr || output_size < required ||
snapshot.record_count > BLUEPAD32_PAIRING_RECORD_CAPACITY) {
return 0;
}
output[0] = 'S';
output[1] = 'P';
output[2] = 'P';
output[3] = 'M';
output[4] = kProtocolVersion;
output[5] = static_cast<uint8_t>(snapshot.status);
output[6] = snapshot.record_count;
output[7] = snapshot.overflow ? 1 : 0;
output[8] = static_cast<uint8_t>(snapshot.generation);
output[9] = static_cast<uint8_t>(snapshot.generation >> 8);
output[10] = static_cast<uint8_t>(snapshot.generation >> 16);
output[11] = static_cast<uint8_t>(snapshot.generation >> 24);
size_t offset = kResponseHeaderSize;
for (uint8_t index = 0; index < snapshot.record_count; ++index) {
const Bluepad32PairingRecord& record = snapshot.records[index];
output[offset] = static_cast<uint8_t>(record.transport);
output[offset + 1] = record.address_type;
memcpy(&output[offset + 2], record.address,
sizeof(record.address));
offset += kRecordSize;
}
return required;
}
} // namespace UsbPairingManagement
extern "C" bool tud_vendor_control_xfer_cb(
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
if (stage != CONTROL_STAGE_SETUP) {
return true;
}
if (request == nullptr ||
request->bmRequestType_bit.recipient != TUSB_REQ_RCPT_DEVICE ||
request->wValue != UsbPairingManagement::kRequestValue ||
request->wIndex != UsbPairingManagement::kRequestIndex) {
return false;
}
switch (request->bRequest) {
case UsbPairingManagement::kRequestGet: {
if (request->bmRequestType_bit.direction != TUSB_DIR_IN) {
return false;
}
static uint8_t response[
UsbPairingManagement::kMaximumResponseSize];
Bluepad32PairingSnapshot snapshot{};
bluepad32_input_backend_pairing_snapshot(&snapshot);
const size_t response_size =
UsbPairingManagement::encode_snapshot(
snapshot, response, sizeof(response));
return response_size != 0 &&
tud_control_xfer(
rhport, request, response,
static_cast<uint16_t>(response_size));
}
case UsbPairingManagement::kRequestRefresh:
if (request->bmRequestType_bit.direction != TUSB_DIR_OUT ||
request->wLength != 0) {
return false;
}
bluepad32_input_backend_request_pairing_snapshot();
return tud_control_status(rhport, request);
case UsbPairingManagement::kRequestClear:
if (request->bmRequestType_bit.direction != TUSB_DIR_OUT ||
request->wLength != 0) {
return false;
}
bluepad32_input_backend_clear_pairings();
return tud_control_status(rhport, request);
default:
return false;
}
}

View file

@ -1,25 +0,0 @@
#pragma once
#include <stddef.h>
#include <stdint.h>
#include "bluepad32_input_backend.h"
namespace UsbPairingManagement {
constexpr uint8_t kRequestClear = 0x50;
constexpr uint8_t kRequestGet = 0x51;
constexpr uint8_t kRequestRefresh = 0x52;
constexpr uint16_t kRequestValue = 0x5350;
constexpr uint16_t kRequestIndex = 0x4d47;
constexpr uint8_t kProtocolVersion = 1;
constexpr size_t kResponseHeaderSize = 12;
constexpr size_t kRecordSize = 8;
constexpr size_t kMaximumResponseSize =
kResponseHeaderSize +
BLUEPAD32_PAIRING_RECORD_CAPACITY * kRecordSize;
size_t encode_snapshot(const Bluepad32PairingSnapshot& snapshot,
uint8_t* output, size_t output_size);
} // namespace UsbPairingManagement