Add versioned persistent configuration protocol
This commit is contained in:
parent
b11ae076a8
commit
7afc9981fe
36 changed files with 2784 additions and 756 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
20
README.md
20
README.md
|
|
@ -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
43
adapter_configuration.cpp
Normal 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
21
adapter_configuration.h
Normal 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);
|
||||
|
|
@ -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
186
configuration_service.cpp
Normal 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
37
configuration_service.h
Normal 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
197
configuration_storage.cpp
Normal 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
63
configuration_storage.h
Normal 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;
|
||||
};
|
||||
108
configuration_transaction.cpp
Normal file
108
configuration_transaction.cpp
Normal 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_;
|
||||
}
|
||||
57
configuration_transaction.h
Normal file
57
configuration_transaction.h
Normal 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.
Binary file not shown.
Binary file not shown.
111
pico_configuration_storage.cpp
Normal file
111
pico_configuration_storage.cpp
Normal 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,
|
||||
};
|
||||
}
|
||||
5
pico_configuration_storage.h
Normal file
5
pico_configuration_storage.h
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
#pragma once
|
||||
|
||||
#include "configuration_storage.h"
|
||||
|
||||
ConfigurationStorageIo pico_configuration_storage_io();
|
||||
|
|
@ -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"}
|
||||
|
|
|
|||
600
src/switch_pico_bridge/config_manager.py
Executable file
600
src/switch_pico_bridge/config_manager.py
Executable 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())
|
||||
|
|
@ -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())
|
||||
|
|
@ -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() {
|
||||
|
|
|
|||
232
tests/configuration_storage_test.cpp
Normal file
232
tests/configuration_storage_test.cpp
Normal 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;
|
||||
}
|
||||
244
tests/test_config_manager.py
Normal file
244
tests/test_config_manager.py
Normal 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
|
||||
31
tests/test_configuration_storage_native.py
Normal file
31
tests/test_configuration_storage_native.py
Normal 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)
|
||||
|
|
@ -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
|
||||
|
|
@ -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),
|
||||
],
|
||||
301
tests/usb_configuration_management_test.cpp
Normal file
301
tests/usb_configuration_management_test.cpp
Normal 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;
|
||||
}
|
||||
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
377
usb_configuration_management.cpp
Normal file
377
usb_configuration_management.cpp
Normal 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));
|
||||
}
|
||||
65
usb_configuration_management.h
Normal file
65
usb_configuration_management.h
Normal 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
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
Loading…
Add table
Add a link
Reference in a new issue