Add persistent controller profile foundation
This commit is contained in:
parent
7afc9981fe
commit
3f90d04a50
29 changed files with 6157 additions and 70 deletions
|
|
@ -99,6 +99,11 @@ add_executable(switch-pico
|
|||
if(SWITCH_PICO_INPUT_BACKEND STREQUAL "BLUEPAD32")
|
||||
target_sources(switch-pico PRIVATE
|
||||
bluepad32_input_backend.cpp
|
||||
controller_identity.cpp
|
||||
controller_profile.cpp
|
||||
profile_storage.cpp
|
||||
profile_service.cpp
|
||||
pico_profile_storage.cpp
|
||||
bootsel_pairing_button.cpp
|
||||
adapter_configuration.cpp
|
||||
configuration_storage.cpp
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
#include "bluepad32_input_backend.h"
|
||||
#include "controller_hotkey_config.h"
|
||||
#include "configuration_service.h"
|
||||
#include "profile_service.h"
|
||||
|
||||
#include <limits.h>
|
||||
#include <stddef.h>
|
||||
|
|
@ -111,9 +112,21 @@ struct FeedbackEnvelope {
|
|||
uint8_t strong_magnitude;
|
||||
};
|
||||
|
||||
// Security Manager identity events arrive before Bluepad32 publishes a ready
|
||||
// device. Retain only the four live handle/address associations so a BLE RPA
|
||||
// is never promoted to a stable identity on its own.
|
||||
struct BleIdentityMapping {
|
||||
bool used;
|
||||
hci_con_handle_t connection_handle;
|
||||
bd_addr_t connection_address;
|
||||
uint8_t identity_address_type;
|
||||
bd_addr_t identity_address;
|
||||
};
|
||||
|
||||
|
||||
struct BackendSlot {
|
||||
ControllerState state;
|
||||
ControllerIdentity identity;
|
||||
// Non-null with active=false is a connected device still becoming ready.
|
||||
uni_hid_device_t* device;
|
||||
uint32_t state_generation;
|
||||
|
|
@ -132,6 +145,7 @@ struct BackendSlot {
|
|||
|
||||
critical_section_t g_state_lock;
|
||||
BackendSlot g_slots[kSlotCount];
|
||||
BleIdentityMapping g_ble_identity_mappings[kSlotCount]{};
|
||||
|
||||
// These acknowledgement generations and the pairing request producer are only
|
||||
// used by Core 0. The request is transferred under the cross-core state lock.
|
||||
|
|
@ -148,6 +162,7 @@ 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{};
|
||||
btstack_packet_callback_registration_t g_identity_event_callback{};
|
||||
ConnectionPolicyState g_connection_policy_state =
|
||||
ConnectionPolicyState::Uninitialized;
|
||||
uint32_t g_pairing_window_deadline_ms = 0;
|
||||
|
|
@ -203,6 +218,186 @@ int slot_for_device(const uni_hid_device_t* device) {
|
|||
const int slot = uni_hid_device_get_idx_for_instance(device);
|
||||
return slot >= 0 && slot < kSlotCount ? slot : -1;
|
||||
}
|
||||
|
||||
bool addresses_equal(const bd_addr_t first, const bd_addr_t second) {
|
||||
return memcmp(first, second, sizeof(bd_addr_t)) == 0;
|
||||
}
|
||||
|
||||
BleIdentityMapping* find_ble_identity_mapping(
|
||||
hci_con_handle_t connection_handle,
|
||||
const bd_addr_t connection_address) {
|
||||
for (BleIdentityMapping& mapping : g_ble_identity_mappings) {
|
||||
if (mapping.used &&
|
||||
mapping.connection_handle == connection_handle &&
|
||||
addresses_equal(mapping.connection_address,
|
||||
connection_address)) {
|
||||
return &mapping;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
BleIdentityMapping* find_ble_identity_mapping_for_handle(
|
||||
hci_con_handle_t connection_handle) {
|
||||
for (BleIdentityMapping& mapping : g_ble_identity_mappings) {
|
||||
if (mapping.used &&
|
||||
mapping.connection_handle == connection_handle) {
|
||||
return &mapping;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
BleIdentityMapping* reserve_ble_identity_mapping(
|
||||
hci_con_handle_t connection_handle) {
|
||||
BleIdentityMapping* available = nullptr;
|
||||
for (BleIdentityMapping& mapping : g_ble_identity_mappings) {
|
||||
if (mapping.used &&
|
||||
mapping.connection_handle == connection_handle) {
|
||||
return &mapping;
|
||||
}
|
||||
if (!mapping.used && available == nullptr) {
|
||||
available = &mapping;
|
||||
}
|
||||
}
|
||||
return available;
|
||||
}
|
||||
|
||||
ControllerIdentity make_ble_identity(
|
||||
const BleIdentityMapping& mapping, const uni_hid_device_t* device) {
|
||||
ControllerIdentity identity{};
|
||||
identity.stable = true;
|
||||
identity.transport = ControllerTransport::kBle;
|
||||
identity.address_type = mapping.identity_address_type;
|
||||
memcpy(identity.address, mapping.identity_address,
|
||||
sizeof(identity.address));
|
||||
identity.vendor_id = device->vendor_id;
|
||||
identity.product_id = device->product_id;
|
||||
return identity;
|
||||
}
|
||||
|
||||
ControllerIdentity identity_for_device(const uni_hid_device_t* device) {
|
||||
if (device == nullptr) {
|
||||
return controller_identity_global();
|
||||
}
|
||||
if (device->conn.protocol == UNI_BT_CONN_PROTOCOL_BR_EDR) {
|
||||
ControllerIdentity identity{};
|
||||
identity.stable = true;
|
||||
identity.transport = ControllerTransport::kClassic;
|
||||
identity.address_type = BD_ADDR_TYPE_UNKNOWN;
|
||||
memcpy(identity.address, device->conn.btaddr,
|
||||
sizeof(identity.address));
|
||||
identity.vendor_id = device->vendor_id;
|
||||
identity.product_id = device->product_id;
|
||||
return identity;
|
||||
}
|
||||
if (device->conn.protocol == UNI_BT_CONN_PROTOCOL_BLE) {
|
||||
const BleIdentityMapping* mapping = find_ble_identity_mapping(
|
||||
device->conn.handle, device->conn.btaddr);
|
||||
if (mapping != nullptr) {
|
||||
return make_ble_identity(*mapping, device);
|
||||
}
|
||||
}
|
||||
return controller_identity_global();
|
||||
}
|
||||
|
||||
void publish_ble_identity(const BleIdentityMapping& mapping) {
|
||||
ControllerIdentity observed_identity{};
|
||||
bool observe_identity = false;
|
||||
critical_section_enter_blocking(&g_state_lock);
|
||||
for (BackendSlot& slot : g_slots) {
|
||||
if (slot.device != nullptr &&
|
||||
slot.device->conn.protocol == UNI_BT_CONN_PROTOCOL_BLE &&
|
||||
slot.device->conn.handle == mapping.connection_handle &&
|
||||
addresses_equal(slot.device->conn.btaddr,
|
||||
mapping.connection_address)) {
|
||||
slot.identity = make_ble_identity(mapping, slot.device);
|
||||
if (slot.active) {
|
||||
observed_identity = slot.identity;
|
||||
observe_identity = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
critical_section_exit(&g_state_lock);
|
||||
if (observe_identity) {
|
||||
profile_service_observe_identity_on_storage_core(
|
||||
observed_identity);
|
||||
}
|
||||
}
|
||||
|
||||
void record_ble_identity(hci_con_handle_t connection_handle,
|
||||
const bd_addr_t connection_address,
|
||||
uint8_t identity_address_type,
|
||||
const bd_addr_t identity_address) {
|
||||
BleIdentityMapping* mapping =
|
||||
reserve_ble_identity_mapping(connection_handle);
|
||||
if (mapping == nullptr) {
|
||||
return;
|
||||
}
|
||||
*mapping = {};
|
||||
mapping->used = true;
|
||||
mapping->connection_handle = connection_handle;
|
||||
memcpy(mapping->connection_address, connection_address,
|
||||
sizeof(mapping->connection_address));
|
||||
mapping->identity_address_type = identity_address_type;
|
||||
memcpy(mapping->identity_address, identity_address,
|
||||
sizeof(mapping->identity_address));
|
||||
publish_ble_identity(*mapping);
|
||||
}
|
||||
|
||||
void clear_ble_identity_for_handle(hci_con_handle_t connection_handle) {
|
||||
for (BleIdentityMapping& mapping : g_ble_identity_mappings) {
|
||||
if (mapping.used &&
|
||||
mapping.connection_handle == connection_handle) {
|
||||
mapping = {};
|
||||
}
|
||||
}
|
||||
|
||||
critical_section_enter_blocking(&g_state_lock);
|
||||
for (BackendSlot& slot : g_slots) {
|
||||
if (slot.device != nullptr &&
|
||||
slot.device->conn.protocol == UNI_BT_CONN_PROTOCOL_BLE &&
|
||||
slot.device->conn.handle == connection_handle) {
|
||||
slot.identity = controller_identity_global();
|
||||
}
|
||||
}
|
||||
critical_section_exit(&g_state_lock);
|
||||
}
|
||||
|
||||
void clear_ble_identity_for_device(const uni_hid_device_t* device) {
|
||||
if (device == nullptr ||
|
||||
device->conn.protocol != UNI_BT_CONN_PROTOCOL_BLE) {
|
||||
return;
|
||||
}
|
||||
for (BleIdentityMapping& mapping : g_ble_identity_mappings) {
|
||||
if (mapping.used &&
|
||||
mapping.connection_handle == device->conn.handle &&
|
||||
addresses_equal(mapping.connection_address,
|
||||
device->conn.btaddr)) {
|
||||
mapping = {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void connection_address_for_handle(hci_con_handle_t connection_handle,
|
||||
const bd_addr_t fallback,
|
||||
bd_addr_t output) {
|
||||
const uni_hid_device_t* device =
|
||||
uni_hid_device_get_instance_for_connection_handle(
|
||||
connection_handle);
|
||||
if (device != nullptr) {
|
||||
memcpy(output, device->conn.btaddr, sizeof(bd_addr_t));
|
||||
return;
|
||||
}
|
||||
const BleIdentityMapping* mapping =
|
||||
find_ble_identity_mapping_for_handle(connection_handle);
|
||||
if (mapping != nullptr) {
|
||||
memcpy(output, mapping->connection_address, sizeof(bd_addr_t));
|
||||
return;
|
||||
}
|
||||
memcpy(output, fallback, sizeof(bd_addr_t));
|
||||
}
|
||||
|
||||
void apply_slot_lighting(uint8_t slot_index, uni_hid_device_t* device) {
|
||||
const SwitchRgbColor color =
|
||||
switch_pro_get_slot_light_color(slot_index);
|
||||
|
|
@ -249,6 +444,7 @@ void publish_all_neutral() {
|
|||
critical_section_enter_blocking(&g_state_lock);
|
||||
for (BackendSlot& slot : g_slots) {
|
||||
slot.state = make_neutral_state();
|
||||
slot.identity = controller_identity_global();
|
||||
slot.device = nullptr;
|
||||
slot.active = false;
|
||||
slot.rumble_pending = false;
|
||||
|
|
@ -256,6 +452,9 @@ void publish_all_neutral() {
|
|||
++slot.connection_generation;
|
||||
}
|
||||
critical_section_exit(&g_state_lock);
|
||||
for (BleIdentityMapping& mapping : g_ble_identity_mappings) {
|
||||
mapping = {};
|
||||
}
|
||||
g_connection_status = ConnectionStatus::Initializing;
|
||||
g_connection_policy_state = ConnectionPolicyState::FailedClosed;
|
||||
g_pairing_window_open = false;
|
||||
|
|
@ -498,18 +697,101 @@ bool pairing_window_active_at(uint32_t now_ms) {
|
|||
static_cast<int32_t>(now_ms - g_pairing_window_deadline_ms) < 0;
|
||||
}
|
||||
|
||||
void handle_pairing_hci_event(uint8_t packet_type, uint16_t channel,
|
||||
uint8_t* packet, uint16_t size) {
|
||||
void handle_btstack_event(uint8_t packet_type, uint16_t channel,
|
||||
uint8_t* packet, uint16_t size) {
|
||||
(void)channel;
|
||||
if (packet_type != HCI_EVENT_PACKET || packet == nullptr || size < 8) {
|
||||
if (packet_type != HCI_EVENT_PACKET || packet == nullptr || size < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
bd_addr_t address{};
|
||||
bd_addr_t identity_address{};
|
||||
bd_addr_t connection_address{};
|
||||
hci_con_handle_t connection_handle = 0;
|
||||
const bool pairing_open =
|
||||
pairing_window_active_at(btstack_run_loop_get_time_ms());
|
||||
switch (hci_event_packet_get_type(packet)) {
|
||||
case SM_EVENT_IDENTITY_RESOLVING_STARTED:
|
||||
if (size >= 11) {
|
||||
clear_ble_identity_for_handle(
|
||||
sm_event_identity_resolving_started_get_handle(packet));
|
||||
}
|
||||
break;
|
||||
case SM_EVENT_IDENTITY_RESOLVING_FAILED:
|
||||
if (size >= 11) {
|
||||
clear_ble_identity_for_handle(
|
||||
sm_event_identity_resolving_failed_get_handle(packet));
|
||||
}
|
||||
break;
|
||||
case SM_EVENT_IDENTITY_RESOLVING_SUCCEEDED:
|
||||
if (size >= 20) {
|
||||
connection_handle =
|
||||
sm_event_identity_resolving_succeeded_get_handle(packet);
|
||||
sm_event_identity_resolving_succeeded_get_address(
|
||||
packet, connection_address);
|
||||
sm_event_identity_resolving_succeeded_get_identity_address(
|
||||
packet, identity_address);
|
||||
record_ble_identity(
|
||||
connection_handle, connection_address,
|
||||
sm_event_identity_resolving_succeeded_get_identity_addr_type(
|
||||
packet),
|
||||
identity_address);
|
||||
}
|
||||
break;
|
||||
case SM_EVENT_IDENTITY_CREATED:
|
||||
if (size >= 20) {
|
||||
connection_handle =
|
||||
sm_event_identity_created_get_handle(packet);
|
||||
sm_event_identity_created_get_address(packet, address);
|
||||
sm_event_identity_created_get_identity_address(
|
||||
packet, identity_address);
|
||||
connection_address_for_handle(
|
||||
connection_handle, address, connection_address);
|
||||
record_ble_identity(
|
||||
connection_handle, connection_address,
|
||||
sm_event_identity_created_get_identity_addr_type(packet),
|
||||
identity_address);
|
||||
}
|
||||
break;
|
||||
case SM_EVENT_REENCRYPTION_STARTED:
|
||||
if (size >= 11) {
|
||||
connection_handle =
|
||||
sm_event_reencryption_started_get_handle(packet);
|
||||
sm_event_reencryption_started_get_address(
|
||||
packet, identity_address);
|
||||
connection_address_for_handle(
|
||||
connection_handle, identity_address,
|
||||
connection_address);
|
||||
record_ble_identity(
|
||||
connection_handle, connection_address,
|
||||
sm_event_reencryption_started_get_addr_type(packet),
|
||||
identity_address);
|
||||
}
|
||||
break;
|
||||
case SM_EVENT_REENCRYPTION_COMPLETE:
|
||||
if (size >= 12) {
|
||||
connection_handle =
|
||||
sm_event_reencryption_complete_get_handle(packet);
|
||||
if (sm_event_reencryption_complete_get_status(packet) ==
|
||||
ERROR_CODE_SUCCESS) {
|
||||
sm_event_reencryption_complete_get_address(
|
||||
packet, identity_address);
|
||||
connection_address_for_handle(
|
||||
connection_handle, identity_address,
|
||||
connection_address);
|
||||
record_ble_identity(
|
||||
connection_handle, connection_address,
|
||||
sm_event_reencryption_complete_get_addr_type(packet),
|
||||
identity_address);
|
||||
} else {
|
||||
clear_ble_identity_for_handle(connection_handle);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case HCI_EVENT_USER_CONFIRMATION_REQUEST:
|
||||
if (size < 8) {
|
||||
break;
|
||||
}
|
||||
hci_event_user_confirmation_request_get_bd_addr(packet, address);
|
||||
if (pairing_open) {
|
||||
gap_ssp_confirmation_response(address);
|
||||
|
|
@ -518,6 +800,9 @@ void handle_pairing_hci_event(uint8_t packet_type, uint16_t channel,
|
|||
}
|
||||
break;
|
||||
case HCI_EVENT_USER_PASSKEY_REQUEST:
|
||||
if (size < 8) {
|
||||
break;
|
||||
}
|
||||
hci_event_user_passkey_request_get_bd_addr(packet, address);
|
||||
if (pairing_open) {
|
||||
gap_ssp_passkey_response(address, 0);
|
||||
|
|
@ -634,6 +919,7 @@ void process_clear_pairings(uint32_t now_ms) {
|
|||
BackendSlot& slot = g_slots[slot_index];
|
||||
devices[slot_index] = slot.device;
|
||||
slot.state = make_neutral_state();
|
||||
slot.identity = controller_identity_global();
|
||||
slot.device = nullptr;
|
||||
slot.active = false;
|
||||
slot.rumble_pending = false;
|
||||
|
|
@ -648,6 +934,9 @@ void process_clear_pairings(uint32_t now_ms) {
|
|||
if (!requested) {
|
||||
return;
|
||||
}
|
||||
for (BleIdentityMapping& mapping : g_ble_identity_mappings) {
|
||||
mapping = {};
|
||||
}
|
||||
|
||||
g_pairing_window_open = false;
|
||||
gap_set_bondable_mode(false);
|
||||
|
|
@ -733,8 +1022,9 @@ 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());
|
||||
const uint32_t now_ms = btstack_run_loop_get_time_ms();
|
||||
configuration_service_task_on_storage_core(now_ms);
|
||||
profile_service_task_on_storage_core(now_ms);
|
||||
btstack_run_loop_set_timer(timer, kConfigurationPollIntervalMs);
|
||||
btstack_run_loop_add_timer(timer);
|
||||
}
|
||||
|
|
@ -828,7 +1118,9 @@ void platform_on_init_complete() {
|
|||
gap_set_bondable_mode(false);
|
||||
sm_set_accepted_stk_generation_methods(0);
|
||||
gap_ssp_set_auto_accept(false);
|
||||
g_pairing_event_callback.callback = handle_pairing_hci_event;
|
||||
g_pairing_event_callback.callback = handle_btstack_event;
|
||||
g_identity_event_callback.callback = handle_btstack_event;
|
||||
sm_add_event_handler(&g_identity_event_callback);
|
||||
hci_add_event_handler(&g_pairing_event_callback);
|
||||
refresh_pairing_snapshot();
|
||||
// Keep Bluepad32 autoconnect active whenever at least one slot is free.
|
||||
|
|
@ -869,6 +1161,8 @@ void platform_on_device_connected(uni_hid_device_t* device) {
|
|||
if (slot_index < 0) {
|
||||
return;
|
||||
}
|
||||
const ControllerIdentity connection_identity =
|
||||
identity_for_device(device);
|
||||
|
||||
bool tracked_connection = false;
|
||||
critical_section_enter_blocking(&g_state_lock);
|
||||
|
|
@ -881,6 +1175,9 @@ void platform_on_device_connected(uni_hid_device_t* device) {
|
|||
} else {
|
||||
tracked_connection = slot.device == device;
|
||||
}
|
||||
if (tracked_connection) {
|
||||
slot.identity = connection_identity;
|
||||
}
|
||||
critical_section_exit(&g_state_lock);
|
||||
|
||||
if (tracked_connection) {
|
||||
|
|
@ -898,18 +1195,18 @@ void platform_on_device_disconnected(uni_hid_device_t* device) {
|
|||
critical_section_enter_blocking(&g_state_lock);
|
||||
BackendSlot& slot = g_slots[slot_index];
|
||||
if (slot.device == device) {
|
||||
if (slot.active) {
|
||||
slot.state = make_neutral_state();
|
||||
++slot.state_generation;
|
||||
}
|
||||
slot.state = make_neutral_state();
|
||||
slot.identity = controller_identity_global();
|
||||
slot.device = nullptr;
|
||||
slot.active = false;
|
||||
slot.rumble_pending = false;
|
||||
reset_slot_hotkeys(slot);
|
||||
++slot.state_generation;
|
||||
++slot.connection_generation;
|
||||
disconnected_tracked_device = true;
|
||||
}
|
||||
critical_section_exit(&g_state_lock);
|
||||
clear_ble_identity_for_device(device);
|
||||
|
||||
if (disconnected_tracked_device) {
|
||||
// Re-evaluate from scratch: resume discovery only after the final
|
||||
|
|
@ -929,6 +1226,8 @@ uni_error_t platform_on_device_ready(uni_hid_device_t* device) {
|
|||
if (slot_index < 0) {
|
||||
return UNI_ERROR_NO_SLOTS;
|
||||
}
|
||||
const ControllerIdentity connection_identity =
|
||||
identity_for_device(device);
|
||||
|
||||
bool occupied_mismatch = false;
|
||||
bool became_active = false;
|
||||
|
|
@ -936,6 +1235,7 @@ uni_error_t platform_on_device_ready(uni_hid_device_t* device) {
|
|||
BackendSlot& slot = g_slots[slot_index];
|
||||
occupied_mismatch = slot.device != nullptr && slot.device != device;
|
||||
if (!occupied_mismatch) {
|
||||
slot.identity = connection_identity;
|
||||
slot.device = device;
|
||||
if (!slot.active) {
|
||||
slot.state = make_neutral_state();
|
||||
|
|
@ -953,6 +1253,10 @@ uni_error_t platform_on_device_ready(uni_hid_device_t* device) {
|
|||
}
|
||||
if (became_active) {
|
||||
apply_slot_lighting(static_cast<uint8_t>(slot_index), device);
|
||||
if (connection_identity.stable) {
|
||||
profile_service_observe_identity_on_storage_core(
|
||||
connection_identity);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -1021,6 +1325,7 @@ uni_platform* get_platform() {
|
|||
halt_wireless_backend();
|
||||
}
|
||||
configuration_service_initialize_on_storage_core();
|
||||
profile_service_initialize_on_storage_core();
|
||||
if (cyw43_arch_init() != 0) {
|
||||
halt_wireless_backend();
|
||||
}
|
||||
|
|
@ -1047,14 +1352,17 @@ void bluepad32_input_backend_init() {
|
|||
|
||||
critical_section_init(&g_state_lock);
|
||||
configuration_service_prepare();
|
||||
profile_service_prepare();
|
||||
for (uint8_t slot_index = 0; slot_index < kSlotCount; ++slot_index) {
|
||||
BackendSlot& slot = g_slots[slot_index];
|
||||
slot = {};
|
||||
slot.state = make_neutral_state();
|
||||
slot.identity = controller_identity_global();
|
||||
slot.pending_rumble.slot = slot_index;
|
||||
reset_slot_hotkeys(slot);
|
||||
g_consumed_generation[slot_index] = 0;
|
||||
g_last_snapshot_generation[slot_index] = 0;
|
||||
g_ble_identity_mappings[slot_index] = {};
|
||||
}
|
||||
g_pairing_window_requested = false;
|
||||
g_pairing_snapshot_requested = false;
|
||||
|
|
@ -1139,27 +1447,29 @@ void bluepad32_input_backend_pairing_snapshot(
|
|||
}
|
||||
|
||||
|
||||
bool bluepad32_input_backend_snapshot(uint8_t slot_index,
|
||||
ControllerState* out) {
|
||||
if (out == nullptr || !valid_slot(slot_index)) {
|
||||
return false;
|
||||
void bluepad32_input_backend_snapshot(uint8_t slot_index,
|
||||
Bluepad32SlotSnapshot* out) {
|
||||
if (out == nullptr) {
|
||||
return;
|
||||
}
|
||||
if (!g_initialized) {
|
||||
*out = make_neutral_state();
|
||||
return false;
|
||||
*out = {};
|
||||
if (!valid_slot(slot_index) || !g_initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
critical_section_enter_blocking(&g_state_lock);
|
||||
*out = g_slots[slot_index].state;
|
||||
const bool controller_active = g_slots[slot_index].active;
|
||||
const uint32_t generation = g_slots[slot_index].state_generation;
|
||||
const BackendSlot& slot = g_slots[slot_index];
|
||||
out->active = slot.active;
|
||||
out->connection_generation = slot.connection_generation;
|
||||
out->identity = slot.identity;
|
||||
out->state = slot.state;
|
||||
const uint32_t state_generation = slot.state_generation;
|
||||
critical_section_exit(&g_state_lock);
|
||||
|
||||
if (generation == g_consumed_generation[slot_index]) {
|
||||
out->motion_sample_count = 0;
|
||||
if (state_generation == g_consumed_generation[slot_index]) {
|
||||
out->state.motion_sample_count = 0;
|
||||
}
|
||||
g_last_snapshot_generation[slot_index] = generation;
|
||||
return controller_active;
|
||||
g_last_snapshot_generation[slot_index] = state_generation;
|
||||
}
|
||||
|
||||
void bluepad32_input_backend_report_sent(uint8_t slot_index) {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
#include <stdint.h>
|
||||
|
||||
#include "controller_color.h"
|
||||
#include "controller_identity.h"
|
||||
#include "controller_state.h"
|
||||
#include "switch_haptics.h"
|
||||
|
||||
|
|
@ -32,13 +33,21 @@ struct Bluepad32PairingSnapshot {
|
|||
bool overflow;
|
||||
Bluepad32PairingRecord records[BLUEPAD32_PAIRING_RECORD_CAPACITY];
|
||||
};
|
||||
struct Bluepad32SlotSnapshot {
|
||||
bool active;
|
||||
uint32_t connection_generation;
|
||||
ControllerIdentity identity;
|
||||
ControllerState state;
|
||||
};
|
||||
|
||||
|
||||
|
||||
void bluepad32_input_backend_init();
|
||||
void bluepad32_input_backend_start();
|
||||
void bluepad32_input_backend_open_pairing_window();
|
||||
void bluepad32_input_backend_clear_pairings();
|
||||
bool bluepad32_input_backend_snapshot(uint8_t slot, ControllerState* out);
|
||||
void bluepad32_input_backend_snapshot(uint8_t slot,
|
||||
Bluepad32SlotSnapshot* out);
|
||||
void bluepad32_input_backend_request_pairing_snapshot();
|
||||
void bluepad32_input_backend_pairing_snapshot(
|
||||
Bluepad32PairingSnapshot* out);
|
||||
|
|
|
|||
|
|
@ -171,6 +171,10 @@ ConfigurationTransactionStatus configuration_service_reset(
|
|||
if (status == ConfigurationTransactionStatus::kReceiving) {
|
||||
status = g_transaction.finish(transaction_id);
|
||||
}
|
||||
if (status == ConfigurationTransactionStatus::kPending &&
|
||||
g_snapshot.reset_generation != UINT32_MAX) {
|
||||
++g_snapshot.reset_generation;
|
||||
}
|
||||
g_snapshot.transaction = g_transaction.snapshot();
|
||||
critical_section_exit(&g_lock);
|
||||
return status;
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ struct ConfigurationServiceSnapshot {
|
|||
AdapterConfiguration configuration{};
|
||||
uint32_t generation = 0;
|
||||
uint32_t payload_crc = 0;
|
||||
uint32_t reset_generation = 0;
|
||||
ConfigurationTransactionSnapshot transaction{};
|
||||
};
|
||||
|
||||
|
|
|
|||
82
controller_identity.cpp
Normal file
82
controller_identity.cpp
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
#include "controller_identity.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
namespace {
|
||||
|
||||
bool controller_identity_valid(const ControllerIdentity& identity) {
|
||||
const uint8_t transport = static_cast<uint8_t>(identity.transport);
|
||||
if (transport > static_cast<uint8_t>(ControllerTransport::kBle)) {
|
||||
return false;
|
||||
}
|
||||
if (identity.stable) {
|
||||
return identity.transport != ControllerTransport::kUnknown;
|
||||
}
|
||||
return controller_identity_is_global(identity);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ControllerIdentity controller_identity_global() {
|
||||
return {};
|
||||
}
|
||||
|
||||
bool controller_identity_is_global(const ControllerIdentity& identity) {
|
||||
const ControllerIdentity global{};
|
||||
return controller_identity_equal(identity, global);
|
||||
}
|
||||
|
||||
bool controller_identity_equal(const ControllerIdentity& first,
|
||||
const ControllerIdentity& second) {
|
||||
return first.stable == second.stable &&
|
||||
first.transport == second.transport &&
|
||||
first.address_type == second.address_type &&
|
||||
memcmp(first.address, second.address, sizeof(first.address)) == 0 &&
|
||||
first.vendor_id == second.vendor_id &&
|
||||
first.product_id == second.product_id;
|
||||
}
|
||||
|
||||
bool controller_identity_encode(const ControllerIdentity& identity,
|
||||
uint8_t* output, size_t output_size) {
|
||||
if (output == nullptr || output_size < CONTROLLER_IDENTITY_ENCODED_SIZE ||
|
||||
!controller_identity_valid(identity)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
output[0] = identity.stable ? 1 : 0;
|
||||
output[1] = static_cast<uint8_t>(identity.transport);
|
||||
output[2] = identity.address_type;
|
||||
output[3] = 0;
|
||||
memcpy(&output[4], identity.address, sizeof(identity.address));
|
||||
output[10] = static_cast<uint8_t>(identity.vendor_id);
|
||||
output[11] = static_cast<uint8_t>(identity.vendor_id >> 8);
|
||||
output[12] = static_cast<uint8_t>(identity.product_id);
|
||||
output[13] = static_cast<uint8_t>(identity.product_id >> 8);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool controller_identity_decode(const uint8_t* input, size_t input_size,
|
||||
ControllerIdentity* output) {
|
||||
if (input == nullptr || output == nullptr ||
|
||||
input_size != CONTROLLER_IDENTITY_ENCODED_SIZE || input[0] > 1 ||
|
||||
input[1] > static_cast<uint8_t>(ControllerTransport::kBle) ||
|
||||
input[3] != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ControllerIdentity decoded{};
|
||||
decoded.stable = input[0] != 0;
|
||||
decoded.transport = static_cast<ControllerTransport>(input[1]);
|
||||
decoded.address_type = input[2];
|
||||
memcpy(decoded.address, &input[4], sizeof(decoded.address));
|
||||
decoded.vendor_id = static_cast<uint16_t>(input[10]) |
|
||||
static_cast<uint16_t>(input[11] << 8);
|
||||
decoded.product_id = static_cast<uint16_t>(input[12]) |
|
||||
static_cast<uint16_t>(input[13] << 8);
|
||||
if (!controller_identity_valid(decoded)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
*output = decoded;
|
||||
return true;
|
||||
}
|
||||
30
controller_identity.h
Normal file
30
controller_identity.h
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
constexpr size_t CONTROLLER_IDENTITY_ENCODED_SIZE = 14;
|
||||
|
||||
enum class ControllerTransport : uint8_t {
|
||||
kUnknown = 0,
|
||||
kClassic = 1,
|
||||
kBle = 2,
|
||||
};
|
||||
|
||||
struct ControllerIdentity {
|
||||
bool stable = false;
|
||||
ControllerTransport transport = ControllerTransport::kUnknown;
|
||||
uint8_t address_type = 0;
|
||||
uint8_t address[6]{};
|
||||
uint16_t vendor_id = 0;
|
||||
uint16_t product_id = 0;
|
||||
};
|
||||
|
||||
ControllerIdentity controller_identity_global();
|
||||
bool controller_identity_is_global(const ControllerIdentity& identity);
|
||||
bool controller_identity_equal(const ControllerIdentity& first,
|
||||
const ControllerIdentity& second);
|
||||
bool controller_identity_encode(const ControllerIdentity& identity,
|
||||
uint8_t* output, size_t output_size);
|
||||
bool controller_identity_decode(const uint8_t* input, size_t input_size,
|
||||
ControllerIdentity* output);
|
||||
725
controller_profile.cpp
Normal file
725
controller_profile.cpp
Normal file
|
|
@ -0,0 +1,725 @@
|
|||
#include "controller_profile.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr uint8_t kDatabaseMagic[4] = {'S', 'P', 'D', 'B'};
|
||||
constexpr size_t kFallbackOffset = CONTROLLER_PROFILE_DATABASE_HEADER_SIZE;
|
||||
constexpr size_t kEntriesOffset =
|
||||
kFallbackOffset + CONTROLLER_PROFILE_COUNT *
|
||||
CONTROLLER_PROFILE_ENCODED_SIZE;
|
||||
constexpr uint8_t kStickInvertX = 1u << 0;
|
||||
constexpr uint8_t kStickInvertY = 1u << 1;
|
||||
constexpr uint8_t kMacroOverrideMask =
|
||||
kControllerProfileOverrideButtons |
|
||||
kControllerProfileOverrideLeftStick |
|
||||
kControllerProfileOverrideRightStick |
|
||||
kControllerProfileOverrideLeftTrigger |
|
||||
kControllerProfileOverrideRightTrigger;
|
||||
|
||||
uint16_t profile_read_u16(const uint8_t* input) {
|
||||
return static_cast<uint16_t>(input[0]) |
|
||||
(static_cast<uint16_t>(input[1]) << 8);
|
||||
}
|
||||
|
||||
int16_t profile_read_i16(const uint8_t* input) {
|
||||
return static_cast<int16_t>(profile_read_u16(input));
|
||||
}
|
||||
|
||||
void profile_write_u16(uint8_t* output, uint16_t value) {
|
||||
output[0] = static_cast<uint8_t>(value);
|
||||
output[1] = static_cast<uint8_t>(value >> 8);
|
||||
}
|
||||
|
||||
void profile_write_i16(uint8_t* output, int16_t value) {
|
||||
profile_write_u16(output, static_cast<uint16_t>(value));
|
||||
}
|
||||
|
||||
bool profile_bytes_are_zero(const uint8_t* data, size_t size) {
|
||||
for (size_t index = 0; index < size; ++index) {
|
||||
if (data[index] != 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool valid_button(uint8_t button) {
|
||||
return button < CONTROLLER_PROFILE_LOGICAL_BUTTON_COUNT ||
|
||||
button == CONTROLLER_PROFILE_NO_BUTTON;
|
||||
}
|
||||
|
||||
bool valid_macro_step(const ControllerProfileMacroStep& step,
|
||||
bool must_end) {
|
||||
if (must_end) {
|
||||
return step.type == ControllerProfileMacroStepType::kEnd &&
|
||||
step.override_flags == 0 && step.duration_ms == 0 &&
|
||||
step.output_button_mask == 0 && step.left_stick_x == 0 &&
|
||||
step.left_stick_y == 0 && step.right_stick_x == 0 &&
|
||||
step.right_stick_y == 0 && step.left_trigger == 0 &&
|
||||
step.right_trigger == 0;
|
||||
}
|
||||
if (step.type != ControllerProfileMacroStepType::kState ||
|
||||
(step.override_flags & ~kMacroOverrideMask) != 0 ||
|
||||
step.duration_ms > CONTROLLER_PROFILE_MAX_WAIT_MS) {
|
||||
return false;
|
||||
}
|
||||
if ((step.override_flags & kControllerProfileOverrideButtons) == 0 &&
|
||||
step.output_button_mask != 0) {
|
||||
return false;
|
||||
}
|
||||
if ((step.override_flags & kControllerProfileOverrideLeftStick) == 0 &&
|
||||
(step.left_stick_x != 0 || step.left_stick_y != 0)) {
|
||||
return false;
|
||||
}
|
||||
if ((step.override_flags & kControllerProfileOverrideRightStick) == 0 &&
|
||||
(step.right_stick_x != 0 || step.right_stick_y != 0)) {
|
||||
return false;
|
||||
}
|
||||
if ((step.override_flags & kControllerProfileOverrideLeftTrigger) == 0 &&
|
||||
step.left_trigger != 0) {
|
||||
return false;
|
||||
}
|
||||
if ((step.override_flags & kControllerProfileOverrideRightTrigger) == 0 &&
|
||||
step.right_trigger != 0) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void copy_overlap(size_t range_offset, uint8_t* output,
|
||||
size_t output_size, size_t field_offset,
|
||||
const uint8_t* field, size_t field_size) {
|
||||
const size_t range_end = range_offset + output_size;
|
||||
const size_t field_end = field_offset + field_size;
|
||||
if (range_offset >= field_end || field_offset >= range_end) {
|
||||
return;
|
||||
}
|
||||
const size_t start = range_offset > field_offset
|
||||
? range_offset
|
||||
: field_offset;
|
||||
const size_t end = range_end < field_end ? range_end : field_end;
|
||||
memcpy(&output[start - range_offset], &field[start - field_offset],
|
||||
end - start);
|
||||
}
|
||||
|
||||
bool read_zero_region(ControllerProfileDatabaseRead read, void* context,
|
||||
size_t offset, size_t size) {
|
||||
uint8_t buffer[CONTROLLER_PROFILE_ENCODED_SIZE]{};
|
||||
while (size != 0) {
|
||||
const size_t chunk = size < sizeof(buffer) ? size : sizeof(buffer);
|
||||
if (!read(context, offset, buffer, chunk) ||
|
||||
!profile_bytes_are_zero(buffer, chunk)) {
|
||||
return false;
|
||||
}
|
||||
offset += chunk;
|
||||
size -= chunk;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ControllerProfile controller_profile_default(const ControllerIdentity& identity,
|
||||
uint8_t profile_index) {
|
||||
(void)identity;
|
||||
(void)profile_index;
|
||||
ControllerProfile profile{};
|
||||
for (uint8_t index = 0;
|
||||
index < CONTROLLER_PROFILE_LOGICAL_BUTTON_COUNT; ++index) {
|
||||
profile.button_map[index] = index;
|
||||
profile.turbo_modes[index] = ControllerProfileTurboMode::kOff;
|
||||
}
|
||||
for (ControllerProfileStickConfiguration& stick : profile.sticks) {
|
||||
stick.center_x = 0;
|
||||
stick.center_y = 0;
|
||||
stick.inner_deadzone = 0;
|
||||
stick.outer_saturation = 32767;
|
||||
stick.curve_q8_8 = 256;
|
||||
stick.invert_x = false;
|
||||
stick.invert_y = false;
|
||||
}
|
||||
for (ControllerProfileTriggerConfiguration& trigger : profile.triggers) {
|
||||
trigger.lower_deadzone = 0;
|
||||
trigger.upper_saturation = UINT16_MAX;
|
||||
trigger.curve_q8_8 = 256;
|
||||
trigger.digital_threshold = 0x8000;
|
||||
}
|
||||
profile.weak_rumble_scale = UINT8_MAX;
|
||||
profile.strong_rumble_scale = UINT8_MAX;
|
||||
profile.confirmation_policy =
|
||||
ControllerProfileConfirmationPolicy::kRumbleAndLed;
|
||||
profile.switching_chord = 0;
|
||||
profile.macro_trigger = CONTROLLER_PROFILE_NO_BUTTON;
|
||||
profile.macro_cancel = CONTROLLER_PROFILE_NO_BUTTON;
|
||||
profile.macro_step_count = 1;
|
||||
for (ControllerProfileMacroStep& step : profile.macro_steps) {
|
||||
step = {};
|
||||
step.type = ControllerProfileMacroStepType::kEnd;
|
||||
}
|
||||
return profile;
|
||||
}
|
||||
|
||||
bool controller_profile_validate(const ControllerProfile& profile) {
|
||||
for (uint8_t output : profile.button_map) {
|
||||
if (!valid_button(output)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (const ControllerProfileStickConfiguration& stick : profile.sticks) {
|
||||
if (stick.inner_deadzone >= stick.outer_saturation ||
|
||||
stick.outer_saturation > 32767 || stick.curve_q8_8 == 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (const ControllerProfileTriggerConfiguration& trigger :
|
||||
profile.triggers) {
|
||||
if (trigger.lower_deadzone >= trigger.upper_saturation ||
|
||||
trigger.curve_q8_8 == 0 ||
|
||||
trigger.digital_threshold < trigger.lower_deadzone ||
|
||||
trigger.digital_threshold > trigger.upper_saturation) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (static_cast<uint8_t>(profile.confirmation_policy) >
|
||||
static_cast<uint8_t>(
|
||||
ControllerProfileConfirmationPolicy::kRumbleAndLed) ||
|
||||
!valid_button(profile.macro_trigger) ||
|
||||
!valid_button(profile.macro_cancel) ||
|
||||
profile.macro_step_count == 0 ||
|
||||
profile.macro_step_count > CONTROLLER_PROFILE_MACRO_STEP_CAPACITY) {
|
||||
return false;
|
||||
}
|
||||
for (ControllerProfileTurboMode mode : profile.turbo_modes) {
|
||||
if (static_cast<uint8_t>(mode) >
|
||||
static_cast<uint8_t>(ControllerProfileTurboMode::kAutoBurst)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (uint8_t index = 0;
|
||||
index < CONTROLLER_PROFILE_MACRO_STEP_CAPACITY; ++index) {
|
||||
const bool must_end = index >= profile.macro_step_count - 1;
|
||||
if (!valid_macro_step(profile.macro_steps[index], must_end)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool controller_profile_encode(const ControllerProfile& profile,
|
||||
uint8_t* output, size_t output_size) {
|
||||
if (output == nullptr || output_size != CONTROLLER_PROFILE_ENCODED_SIZE ||
|
||||
!controller_profile_validate(profile)) {
|
||||
return false;
|
||||
}
|
||||
memset(output, 0, output_size);
|
||||
profile_write_u16(&output[0], CONTROLLER_PROFILE_SCHEMA_VERSION);
|
||||
profile_write_u16(&output[2], CONTROLLER_PROFILE_ENCODED_SIZE);
|
||||
memcpy(&output[4], profile.button_map,
|
||||
CONTROLLER_PROFILE_LOGICAL_BUTTON_COUNT);
|
||||
for (uint8_t index = 0; index < 2; ++index) {
|
||||
const ControllerProfileStickConfiguration& stick =
|
||||
profile.sticks[index];
|
||||
uint8_t* encoded = &output[20 + index * 16];
|
||||
profile_write_i16(&encoded[0], stick.center_x);
|
||||
profile_write_i16(&encoded[2], stick.center_y);
|
||||
profile_write_u16(&encoded[4], stick.inner_deadzone);
|
||||
profile_write_u16(&encoded[6], stick.outer_saturation);
|
||||
profile_write_u16(&encoded[8], stick.curve_q8_8);
|
||||
encoded[10] = (stick.invert_x ? kStickInvertX : 0) |
|
||||
(stick.invert_y ? kStickInvertY : 0);
|
||||
}
|
||||
for (uint8_t index = 0; index < 2; ++index) {
|
||||
const ControllerProfileTriggerConfiguration& trigger =
|
||||
profile.triggers[index];
|
||||
uint8_t* encoded = &output[52 + index * 10];
|
||||
profile_write_u16(&encoded[0], trigger.lower_deadzone);
|
||||
profile_write_u16(&encoded[2], trigger.upper_saturation);
|
||||
profile_write_u16(&encoded[4], trigger.curve_q8_8);
|
||||
profile_write_u16(&encoded[6], trigger.digital_threshold);
|
||||
}
|
||||
output[72] = profile.weak_rumble_scale;
|
||||
output[73] = profile.strong_rumble_scale;
|
||||
output[74] = static_cast<uint8_t>(profile.confirmation_policy);
|
||||
profile_write_u16(&output[76], profile.switching_chord);
|
||||
output[78] = profile.macro_trigger;
|
||||
output[79] = profile.macro_cancel;
|
||||
output[80] = profile.macro_step_count;
|
||||
for (uint8_t index = 0;
|
||||
index < CONTROLLER_PROFILE_LOGICAL_BUTTON_COUNT; ++index) {
|
||||
output[82 + index] =
|
||||
static_cast<uint8_t>(profile.turbo_modes[index]);
|
||||
}
|
||||
for (uint8_t index = 0;
|
||||
index < CONTROLLER_PROFILE_MACRO_STEP_CAPACITY; ++index) {
|
||||
const ControllerProfileMacroStep& step = profile.macro_steps[index];
|
||||
uint8_t* encoded = &output[100 + index * 19];
|
||||
encoded[0] = static_cast<uint8_t>(step.type);
|
||||
encoded[1] = step.override_flags;
|
||||
profile_write_u16(&encoded[2], step.duration_ms);
|
||||
profile_write_u16(&encoded[4], step.output_button_mask);
|
||||
profile_write_i16(&encoded[6], step.left_stick_x);
|
||||
profile_write_i16(&encoded[8], step.left_stick_y);
|
||||
profile_write_i16(&encoded[10], step.right_stick_x);
|
||||
profile_write_i16(&encoded[12], step.right_stick_y);
|
||||
profile_write_u16(&encoded[14], step.left_trigger);
|
||||
profile_write_u16(&encoded[16], step.right_trigger);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool controller_profile_decode(const uint8_t* input, size_t input_size,
|
||||
ControllerProfile* output) {
|
||||
if (input == nullptr || output == nullptr ||
|
||||
input_size != CONTROLLER_PROFILE_ENCODED_SIZE ||
|
||||
profile_read_u16(&input[0]) != CONTROLLER_PROFILE_SCHEMA_VERSION ||
|
||||
profile_read_u16(&input[2]) != CONTROLLER_PROFILE_ENCODED_SIZE ||
|
||||
!profile_bytes_are_zero(&input[31], 5) || !profile_bytes_are_zero(&input[47], 5) ||
|
||||
!profile_bytes_are_zero(&input[60], 2) || !profile_bytes_are_zero(&input[70], 2) ||
|
||||
input[75] != 0 || input[81] != 0 ||
|
||||
!profile_bytes_are_zero(&input[98], 2) || !profile_bytes_are_zero(&input[252], 4)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ControllerProfile profile{};
|
||||
memcpy(profile.button_map, &input[4],
|
||||
CONTROLLER_PROFILE_LOGICAL_BUTTON_COUNT);
|
||||
for (uint8_t index = 0; index < 2; ++index) {
|
||||
ControllerProfileStickConfiguration& stick = profile.sticks[index];
|
||||
const uint8_t* encoded = &input[20 + index * 16];
|
||||
if ((encoded[10] & ~(kStickInvertX | kStickInvertY)) != 0) {
|
||||
return false;
|
||||
}
|
||||
stick.center_x = profile_read_i16(&encoded[0]);
|
||||
stick.center_y = profile_read_i16(&encoded[2]);
|
||||
stick.inner_deadzone = profile_read_u16(&encoded[4]);
|
||||
stick.outer_saturation = profile_read_u16(&encoded[6]);
|
||||
stick.curve_q8_8 = profile_read_u16(&encoded[8]);
|
||||
stick.invert_x = (encoded[10] & kStickInvertX) != 0;
|
||||
stick.invert_y = (encoded[10] & kStickInvertY) != 0;
|
||||
}
|
||||
for (uint8_t index = 0; index < 2; ++index) {
|
||||
ControllerProfileTriggerConfiguration& trigger =
|
||||
profile.triggers[index];
|
||||
const uint8_t* encoded = &input[52 + index * 10];
|
||||
trigger.lower_deadzone = profile_read_u16(&encoded[0]);
|
||||
trigger.upper_saturation = profile_read_u16(&encoded[2]);
|
||||
trigger.curve_q8_8 = profile_read_u16(&encoded[4]);
|
||||
trigger.digital_threshold = profile_read_u16(&encoded[6]);
|
||||
}
|
||||
profile.weak_rumble_scale = input[72];
|
||||
profile.strong_rumble_scale = input[73];
|
||||
profile.confirmation_policy =
|
||||
static_cast<ControllerProfileConfirmationPolicy>(input[74]);
|
||||
profile.switching_chord = profile_read_u16(&input[76]);
|
||||
profile.macro_trigger = input[78];
|
||||
profile.macro_cancel = input[79];
|
||||
profile.macro_step_count = input[80];
|
||||
for (uint8_t index = 0;
|
||||
index < CONTROLLER_PROFILE_LOGICAL_BUTTON_COUNT; ++index) {
|
||||
profile.turbo_modes[index] =
|
||||
static_cast<ControllerProfileTurboMode>(input[82 + index]);
|
||||
}
|
||||
for (uint8_t index = 0;
|
||||
index < CONTROLLER_PROFILE_MACRO_STEP_CAPACITY; ++index) {
|
||||
ControllerProfileMacroStep& step = profile.macro_steps[index];
|
||||
const uint8_t* encoded = &input[100 + index * 19];
|
||||
if (encoded[18] != 0) {
|
||||
return false;
|
||||
}
|
||||
step.type = static_cast<ControllerProfileMacroStepType>(encoded[0]);
|
||||
step.override_flags = encoded[1];
|
||||
step.duration_ms = profile_read_u16(&encoded[2]);
|
||||
step.output_button_mask = profile_read_u16(&encoded[4]);
|
||||
step.left_stick_x = profile_read_i16(&encoded[6]);
|
||||
step.left_stick_y = profile_read_i16(&encoded[8]);
|
||||
step.right_stick_x = profile_read_i16(&encoded[10]);
|
||||
step.right_stick_y = profile_read_i16(&encoded[12]);
|
||||
step.left_trigger = profile_read_u16(&encoded[14]);
|
||||
step.right_trigger = profile_read_u16(&encoded[16]);
|
||||
}
|
||||
if (!controller_profile_validate(profile)) {
|
||||
return false;
|
||||
}
|
||||
*output = profile;
|
||||
return true;
|
||||
}
|
||||
|
||||
void controller_profile_database_default(ControllerProfileDatabase* database) {
|
||||
if (database == nullptr) {
|
||||
return;
|
||||
}
|
||||
database->fallback_active_profile = 0;
|
||||
const ControllerIdentity global = controller_identity_global();
|
||||
for (ControllerProfileDatabaseEntry& entry : database->entries) {
|
||||
entry.used = false;
|
||||
entry.identity = global;
|
||||
entry.active_profile = 0;
|
||||
}
|
||||
for (uint8_t profile_index = 0;
|
||||
profile_index < CONTROLLER_PROFILE_COUNT; ++profile_index) {
|
||||
database->fallback_profiles[profile_index] =
|
||||
controller_profile_default(global, profile_index);
|
||||
}
|
||||
}
|
||||
|
||||
bool controller_profile_database_validate(
|
||||
const ControllerProfileDatabase& database) {
|
||||
if (database.fallback_active_profile >= CONTROLLER_PROFILE_COUNT) {
|
||||
return false;
|
||||
}
|
||||
for (const ControllerProfile& profile : database.fallback_profiles) {
|
||||
if (!controller_profile_validate(profile)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (uint8_t index = 0;
|
||||
index < CONTROLLER_PROFILE_STABLE_IDENTITY_CAPACITY; ++index) {
|
||||
const ControllerProfileDatabaseEntry& entry = database.entries[index];
|
||||
if (!entry.used) {
|
||||
continue;
|
||||
}
|
||||
if (!entry.identity.stable ||
|
||||
controller_identity_is_global(entry.identity) ||
|
||||
entry.active_profile >= CONTROLLER_PROFILE_COUNT) {
|
||||
return false;
|
||||
}
|
||||
for (uint8_t prior = 0; prior < index; ++prior) {
|
||||
if (database.entries[prior].used &&
|
||||
controller_identity_equal(database.entries[prior].identity,
|
||||
entry.identity)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (const ControllerProfile& profile : entry.profiles) {
|
||||
if (!controller_profile_validate(profile)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool controller_profile_database_encode_range(
|
||||
const ControllerProfileDatabase& database, size_t offset,
|
||||
uint8_t* output, size_t size) {
|
||||
if (output == nullptr || offset > CONTROLLER_PROFILE_DATABASE_ENCODED_SIZE ||
|
||||
size > CONTROLLER_PROFILE_DATABASE_ENCODED_SIZE - offset ||
|
||||
!controller_profile_database_validate(database)) {
|
||||
return false;
|
||||
}
|
||||
memset(output, 0, size);
|
||||
uint8_t header[CONTROLLER_PROFILE_DATABASE_HEADER_SIZE]{};
|
||||
memcpy(header, kDatabaseMagic, sizeof(kDatabaseMagic));
|
||||
profile_write_u16(&header[4],
|
||||
CONTROLLER_PROFILE_DATABASE_SCHEMA_VERSION);
|
||||
profile_write_u16(&header[6], CONTROLLER_PROFILE_DATABASE_ENCODED_SIZE);
|
||||
header[8] = CONTROLLER_PROFILE_STABLE_IDENTITY_CAPACITY;
|
||||
header[9] = CONTROLLER_PROFILE_COUNT;
|
||||
header[10] = database.fallback_active_profile;
|
||||
uint8_t used_count = 0;
|
||||
for (const ControllerProfileDatabaseEntry& entry : database.entries) {
|
||||
used_count += entry.used ? 1 : 0;
|
||||
}
|
||||
header[11] = used_count;
|
||||
copy_overlap(offset, output, size, 0, header, sizeof(header));
|
||||
|
||||
uint8_t encoded_profile[CONTROLLER_PROFILE_ENCODED_SIZE]{};
|
||||
for (uint8_t profile_index = 0;
|
||||
profile_index < CONTROLLER_PROFILE_COUNT; ++profile_index) {
|
||||
const size_t profile_offset =
|
||||
kFallbackOffset +
|
||||
profile_index * CONTROLLER_PROFILE_ENCODED_SIZE;
|
||||
if (offset < profile_offset + CONTROLLER_PROFILE_ENCODED_SIZE &&
|
||||
offset + size > profile_offset) {
|
||||
if (!controller_profile_encode(
|
||||
database.fallback_profiles[profile_index],
|
||||
encoded_profile, sizeof(encoded_profile))) {
|
||||
return false;
|
||||
}
|
||||
copy_overlap(offset, output, size, profile_offset,
|
||||
encoded_profile, sizeof(encoded_profile));
|
||||
}
|
||||
}
|
||||
|
||||
for (uint8_t entry_index = 0;
|
||||
entry_index < CONTROLLER_PROFILE_STABLE_IDENTITY_CAPACITY;
|
||||
++entry_index) {
|
||||
const ControllerProfileDatabaseEntry& entry =
|
||||
database.entries[entry_index];
|
||||
if (!entry.used) {
|
||||
continue;
|
||||
}
|
||||
const size_t entry_offset =
|
||||
kEntriesOffset + entry_index * CONTROLLER_PROFILE_DATABASE_ENTRY_SIZE;
|
||||
uint8_t entry_header[CONTROLLER_PROFILE_DATABASE_ENTRY_HEADER_SIZE]{};
|
||||
if (!controller_identity_encode(entry.identity, entry_header,
|
||||
CONTROLLER_IDENTITY_ENCODED_SIZE)) {
|
||||
return false;
|
||||
}
|
||||
entry_header[14] = entry.active_profile;
|
||||
entry_header[15] = 1;
|
||||
copy_overlap(offset, output, size, entry_offset, entry_header,
|
||||
sizeof(entry_header));
|
||||
for (uint8_t profile_index = 0;
|
||||
profile_index < CONTROLLER_PROFILE_COUNT; ++profile_index) {
|
||||
const size_t profile_offset =
|
||||
entry_offset + CONTROLLER_PROFILE_DATABASE_ENTRY_HEADER_SIZE +
|
||||
profile_index * CONTROLLER_PROFILE_ENCODED_SIZE;
|
||||
if (offset < profile_offset + CONTROLLER_PROFILE_ENCODED_SIZE &&
|
||||
offset + size > profile_offset) {
|
||||
if (!controller_profile_encode(entry.profiles[profile_index],
|
||||
encoded_profile,
|
||||
sizeof(encoded_profile))) {
|
||||
return false;
|
||||
}
|
||||
copy_overlap(offset, output, size, profile_offset,
|
||||
encoded_profile, sizeof(encoded_profile));
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool controller_profile_database_decode(
|
||||
ControllerProfileDatabaseRead read, void* context,
|
||||
ControllerProfileDatabase* output) {
|
||||
if (read == nullptr || output == nullptr) {
|
||||
return false;
|
||||
}
|
||||
uint8_t header[CONTROLLER_PROFILE_DATABASE_HEADER_SIZE]{};
|
||||
if (!read(context, 0, header, sizeof(header)) ||
|
||||
memcmp(header, kDatabaseMagic, sizeof(kDatabaseMagic)) != 0 ||
|
||||
profile_read_u16(&header[4]) !=
|
||||
CONTROLLER_PROFILE_DATABASE_SCHEMA_VERSION ||
|
||||
profile_read_u16(&header[6]) != CONTROLLER_PROFILE_DATABASE_ENCODED_SIZE ||
|
||||
header[8] != CONTROLLER_PROFILE_STABLE_IDENTITY_CAPACITY ||
|
||||
header[9] != CONTROLLER_PROFILE_COUNT ||
|
||||
header[10] >= CONTROLLER_PROFILE_COUNT ||
|
||||
header[11] > CONTROLLER_PROFILE_STABLE_IDENTITY_CAPACITY ||
|
||||
!profile_bytes_are_zero(&header[12], sizeof(header) - 12)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
controller_profile_database_default(output);
|
||||
output->fallback_active_profile = header[10];
|
||||
uint8_t encoded_profile[CONTROLLER_PROFILE_ENCODED_SIZE]{};
|
||||
for (uint8_t profile_index = 0;
|
||||
profile_index < CONTROLLER_PROFILE_COUNT; ++profile_index) {
|
||||
const size_t profile_offset =
|
||||
kFallbackOffset +
|
||||
profile_index * CONTROLLER_PROFILE_ENCODED_SIZE;
|
||||
if (!read(context, profile_offset, encoded_profile,
|
||||
sizeof(encoded_profile)) ||
|
||||
!controller_profile_decode(
|
||||
encoded_profile, sizeof(encoded_profile),
|
||||
&output->fallback_profiles[profile_index])) {
|
||||
controller_profile_database_default(output);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t decoded_used_count = 0;
|
||||
for (uint8_t entry_index = 0;
|
||||
entry_index < CONTROLLER_PROFILE_STABLE_IDENTITY_CAPACITY;
|
||||
++entry_index) {
|
||||
const size_t entry_offset =
|
||||
kEntriesOffset + entry_index * CONTROLLER_PROFILE_DATABASE_ENTRY_SIZE;
|
||||
uint8_t entry_header[CONTROLLER_PROFILE_DATABASE_ENTRY_HEADER_SIZE]{};
|
||||
if (!read(context, entry_offset, entry_header,
|
||||
sizeof(entry_header))) {
|
||||
controller_profile_database_default(output);
|
||||
return false;
|
||||
}
|
||||
if (entry_header[15] == 0) {
|
||||
if (!profile_bytes_are_zero(entry_header, sizeof(entry_header)) ||
|
||||
!read_zero_region(
|
||||
read, context,
|
||||
entry_offset + CONTROLLER_PROFILE_DATABASE_ENTRY_HEADER_SIZE,
|
||||
CONTROLLER_PROFILE_COUNT *
|
||||
CONTROLLER_PROFILE_ENCODED_SIZE)) {
|
||||
controller_profile_database_default(output);
|
||||
return false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (entry_header[15] != 1 ||
|
||||
entry_header[14] >= CONTROLLER_PROFILE_COUNT) {
|
||||
controller_profile_database_default(output);
|
||||
return false;
|
||||
}
|
||||
ControllerProfileDatabaseEntry& entry = output->entries[entry_index];
|
||||
if (!controller_identity_decode(entry_header,
|
||||
CONTROLLER_IDENTITY_ENCODED_SIZE,
|
||||
&entry.identity) ||
|
||||
!entry.identity.stable ||
|
||||
controller_identity_is_global(entry.identity)) {
|
||||
controller_profile_database_default(output);
|
||||
return false;
|
||||
}
|
||||
entry.used = true;
|
||||
entry.active_profile = entry_header[14];
|
||||
++decoded_used_count;
|
||||
for (uint8_t profile_index = 0;
|
||||
profile_index < CONTROLLER_PROFILE_COUNT; ++profile_index) {
|
||||
const size_t profile_offset =
|
||||
entry_offset + CONTROLLER_PROFILE_DATABASE_ENTRY_HEADER_SIZE +
|
||||
profile_index * CONTROLLER_PROFILE_ENCODED_SIZE;
|
||||
if (!read(context, profile_offset, encoded_profile,
|
||||
sizeof(encoded_profile)) ||
|
||||
!controller_profile_decode(encoded_profile,
|
||||
sizeof(encoded_profile),
|
||||
&entry.profiles[profile_index])) {
|
||||
controller_profile_database_default(output);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (decoded_used_count != header[11] ||
|
||||
!controller_profile_database_validate(*output)) {
|
||||
controller_profile_database_default(output);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const ControllerProfileDatabaseEntry* controller_profile_database_find(
|
||||
const ControllerProfileDatabase& database,
|
||||
const ControllerIdentity& identity) {
|
||||
if (!identity.stable || controller_identity_is_global(identity)) {
|
||||
return nullptr;
|
||||
}
|
||||
for (const ControllerProfileDatabaseEntry& entry : database.entries) {
|
||||
if (entry.used &&
|
||||
controller_identity_equal(entry.identity, identity)) {
|
||||
return &entry;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
ControllerProfileDatabaseEntry* controller_profile_database_find(
|
||||
ControllerProfileDatabase* database,
|
||||
const ControllerIdentity& identity) {
|
||||
if (database == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
return const_cast<ControllerProfileDatabaseEntry*>(
|
||||
controller_profile_database_find(*database, identity));
|
||||
}
|
||||
|
||||
ControllerProfileDatabaseResult controller_profile_database_ensure(
|
||||
ControllerProfileDatabase* database, const ControllerIdentity& identity,
|
||||
ControllerProfileDatabaseEntry** output) {
|
||||
if (database == nullptr || output == nullptr || !identity.stable ||
|
||||
controller_identity_is_global(identity)) {
|
||||
return ControllerProfileDatabaseResult::kInvalidArgument;
|
||||
}
|
||||
if (ControllerProfileDatabaseEntry* found =
|
||||
controller_profile_database_find(database, identity)) {
|
||||
*output = found;
|
||||
return ControllerProfileDatabaseResult::kOk;
|
||||
}
|
||||
for (ControllerProfileDatabaseEntry& entry : database->entries) {
|
||||
if (!entry.used) {
|
||||
entry.active_profile = 0;
|
||||
entry.used = true;
|
||||
entry.identity = identity;
|
||||
for (uint8_t profile_index = 0;
|
||||
profile_index < CONTROLLER_PROFILE_COUNT; ++profile_index) {
|
||||
entry.profiles[profile_index] =
|
||||
controller_profile_default(identity, profile_index);
|
||||
}
|
||||
*output = &entry;
|
||||
return ControllerProfileDatabaseResult::kOk;
|
||||
}
|
||||
}
|
||||
return ControllerProfileDatabaseResult::kFull;
|
||||
}
|
||||
|
||||
const ControllerProfile* controller_profile_database_get(
|
||||
const ControllerProfileDatabase& database,
|
||||
const ControllerIdentity& identity, uint8_t profile_index) {
|
||||
if (profile_index >= CONTROLLER_PROFILE_COUNT) {
|
||||
return nullptr;
|
||||
}
|
||||
if (controller_identity_is_global(identity)) {
|
||||
return &database.fallback_profiles[profile_index];
|
||||
}
|
||||
const ControllerProfileDatabaseEntry* entry =
|
||||
controller_profile_database_find(database, identity);
|
||||
return entry == nullptr ? nullptr : &entry->profiles[profile_index];
|
||||
}
|
||||
|
||||
ControllerProfileDatabaseResult controller_profile_database_set(
|
||||
ControllerProfileDatabase* database, const ControllerIdentity& identity,
|
||||
uint8_t profile_index, const ControllerProfile& profile) {
|
||||
if (database == nullptr || profile_index >= CONTROLLER_PROFILE_COUNT ||
|
||||
!controller_profile_validate(profile)) {
|
||||
return ControllerProfileDatabaseResult::kInvalidArgument;
|
||||
}
|
||||
if (controller_identity_is_global(identity)) {
|
||||
database->fallback_profiles[profile_index] = profile;
|
||||
return ControllerProfileDatabaseResult::kOk;
|
||||
}
|
||||
ControllerProfileDatabaseEntry* entry = nullptr;
|
||||
const ControllerProfileDatabaseResult result =
|
||||
controller_profile_database_ensure(database, identity, &entry);
|
||||
if (result == ControllerProfileDatabaseResult::kOk) {
|
||||
entry->profiles[profile_index] = profile;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
ControllerProfileDatabaseResult controller_profile_database_reset(
|
||||
ControllerProfileDatabase* database, const ControllerIdentity& identity,
|
||||
uint8_t profile_index) {
|
||||
if (database == nullptr ||
|
||||
(profile_index != CONTROLLER_PROFILE_ALL &&
|
||||
profile_index >= CONTROLLER_PROFILE_COUNT)) {
|
||||
return ControllerProfileDatabaseResult::kInvalidArgument;
|
||||
}
|
||||
if (controller_identity_is_global(identity)) {
|
||||
for (uint8_t index = 0; index < CONTROLLER_PROFILE_COUNT; ++index) {
|
||||
if (profile_index == CONTROLLER_PROFILE_ALL ||
|
||||
profile_index == index) {
|
||||
database->fallback_profiles[index] =
|
||||
controller_profile_default(identity, index);
|
||||
}
|
||||
}
|
||||
return ControllerProfileDatabaseResult::kOk;
|
||||
}
|
||||
ControllerProfileDatabaseEntry* entry = nullptr;
|
||||
const ControllerProfileDatabaseResult result =
|
||||
controller_profile_database_ensure(database, identity, &entry);
|
||||
if (result != ControllerProfileDatabaseResult::kOk) {
|
||||
return result;
|
||||
}
|
||||
for (uint8_t index = 0; index < CONTROLLER_PROFILE_COUNT; ++index) {
|
||||
if (profile_index == CONTROLLER_PROFILE_ALL ||
|
||||
profile_index == index) {
|
||||
entry->profiles[index] = controller_profile_default(identity, index);
|
||||
}
|
||||
}
|
||||
return ControllerProfileDatabaseResult::kOk;
|
||||
}
|
||||
|
||||
ControllerProfileDatabaseResult controller_profile_database_activate(
|
||||
ControllerProfileDatabase* database, const ControllerIdentity& identity,
|
||||
uint8_t profile_index) {
|
||||
if (database == nullptr || profile_index >= CONTROLLER_PROFILE_COUNT) {
|
||||
return ControllerProfileDatabaseResult::kInvalidArgument;
|
||||
}
|
||||
if (controller_identity_is_global(identity)) {
|
||||
database->fallback_active_profile = profile_index;
|
||||
return ControllerProfileDatabaseResult::kOk;
|
||||
}
|
||||
ControllerProfileDatabaseEntry* entry = nullptr;
|
||||
const ControllerProfileDatabaseResult result =
|
||||
controller_profile_database_ensure(database, identity, &entry);
|
||||
if (result == ControllerProfileDatabaseResult::kOk) {
|
||||
entry->active_profile = profile_index;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
191
controller_profile.h
Normal file
191
controller_profile.h
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "controller_identity.h"
|
||||
|
||||
constexpr uint16_t CONTROLLER_PROFILE_SCHEMA_VERSION = 1;
|
||||
constexpr size_t CONTROLLER_PROFILE_ENCODED_SIZE = 256;
|
||||
constexpr uint8_t CONTROLLER_PROFILE_COUNT = 4;
|
||||
constexpr uint8_t CONTROLLER_PROFILE_LOGICAL_BUTTON_COUNT = 16;
|
||||
constexpr uint8_t CONTROLLER_PROFILE_MACRO_STEP_CAPACITY = 8;
|
||||
constexpr uint16_t CONTROLLER_PROFILE_MAX_WAIT_MS = 10000;
|
||||
constexpr uint8_t CONTROLLER_PROFILE_NO_BUTTON = 0xff;
|
||||
constexpr uint8_t CONTROLLER_PROFILE_ALL = 0xff;
|
||||
|
||||
constexpr uint8_t CONTROLLER_PROFILE_STABLE_IDENTITY_CAPACITY = 16;
|
||||
constexpr uint16_t CONTROLLER_PROFILE_DATABASE_SCHEMA_VERSION = 1;
|
||||
constexpr size_t CONTROLLER_PROFILE_DATABASE_HEADER_SIZE = 32;
|
||||
constexpr size_t CONTROLLER_PROFILE_DATABASE_ENTRY_HEADER_SIZE = 16;
|
||||
constexpr size_t CONTROLLER_PROFILE_DATABASE_ENTRY_SIZE =
|
||||
CONTROLLER_PROFILE_DATABASE_ENTRY_HEADER_SIZE +
|
||||
CONTROLLER_PROFILE_COUNT * CONTROLLER_PROFILE_ENCODED_SIZE;
|
||||
constexpr size_t CONTROLLER_PROFILE_DATABASE_ENCODED_SIZE =
|
||||
CONTROLLER_PROFILE_DATABASE_HEADER_SIZE +
|
||||
CONTROLLER_PROFILE_COUNT * CONTROLLER_PROFILE_ENCODED_SIZE +
|
||||
CONTROLLER_PROFILE_STABLE_IDENTITY_CAPACITY *
|
||||
CONTROLLER_PROFILE_DATABASE_ENTRY_SIZE;
|
||||
|
||||
static_assert(CONTROLLER_PROFILE_DATABASE_ENCODED_SIZE == 17696,
|
||||
"profile database wire size changed");
|
||||
enum class ControllerProfileLogicalButton : uint8_t {
|
||||
kSouth = 0,
|
||||
kEast = 1,
|
||||
kWest = 2,
|
||||
kNorth = 3,
|
||||
kLeftShoulder = 4,
|
||||
kRightShoulder = 5,
|
||||
kSelect = 6,
|
||||
kStart = 7,
|
||||
kSystem = 8,
|
||||
kCapture = 9,
|
||||
kLeftStick = 10,
|
||||
kRightStick = 11,
|
||||
kDpadUp = 12,
|
||||
kDpadDown = 13,
|
||||
kDpadLeft = 14,
|
||||
kDpadRight = 15,
|
||||
};
|
||||
static_assert(
|
||||
static_cast<uint8_t>(ControllerProfileLogicalButton::kDpadRight) + 1 ==
|
||||
CONTROLLER_PROFILE_LOGICAL_BUTTON_COUNT);
|
||||
|
||||
|
||||
enum class ControllerProfileConfirmationPolicy : uint8_t {
|
||||
kNone = 0,
|
||||
kRumble = 1,
|
||||
kLed = 2,
|
||||
kRumbleAndLed = 3,
|
||||
};
|
||||
|
||||
enum class ControllerProfileTurboMode : uint8_t {
|
||||
kOff = 0,
|
||||
kTurbo = 1,
|
||||
kAutoBurst = 2,
|
||||
};
|
||||
|
||||
enum class ControllerProfileMacroStepType : uint8_t {
|
||||
kState = 0,
|
||||
kEnd = 1,
|
||||
};
|
||||
|
||||
enum ControllerProfileMacroOverride : uint8_t {
|
||||
kControllerProfileOverrideButtons = 1u << 0,
|
||||
kControllerProfileOverrideLeftStick = 1u << 1,
|
||||
kControllerProfileOverrideRightStick = 1u << 2,
|
||||
kControllerProfileOverrideLeftTrigger = 1u << 3,
|
||||
kControllerProfileOverrideRightTrigger = 1u << 4,
|
||||
};
|
||||
|
||||
struct ControllerProfileStickConfiguration {
|
||||
int16_t center_x = 0;
|
||||
int16_t center_y = 0;
|
||||
uint16_t inner_deadzone = 0;
|
||||
uint16_t outer_saturation = 32767;
|
||||
uint16_t curve_q8_8 = 256;
|
||||
bool invert_x = false;
|
||||
bool invert_y = false;
|
||||
};
|
||||
|
||||
struct ControllerProfileTriggerConfiguration {
|
||||
uint16_t lower_deadzone = 0;
|
||||
uint16_t upper_saturation = UINT16_MAX;
|
||||
uint16_t curve_q8_8 = 256;
|
||||
uint16_t digital_threshold = 0x8000;
|
||||
};
|
||||
|
||||
struct ControllerProfileMacroStep {
|
||||
ControllerProfileMacroStepType type =
|
||||
ControllerProfileMacroStepType::kEnd;
|
||||
uint8_t override_flags = 0;
|
||||
uint16_t duration_ms = 0;
|
||||
uint16_t output_button_mask = 0;
|
||||
int16_t left_stick_x = 0;
|
||||
int16_t left_stick_y = 0;
|
||||
int16_t right_stick_x = 0;
|
||||
int16_t right_stick_y = 0;
|
||||
uint16_t left_trigger = 0;
|
||||
uint16_t right_trigger = 0;
|
||||
};
|
||||
|
||||
struct ControllerProfile {
|
||||
uint8_t button_map[CONTROLLER_PROFILE_LOGICAL_BUTTON_COUNT]{};
|
||||
ControllerProfileStickConfiguration sticks[2]{};
|
||||
ControllerProfileTriggerConfiguration triggers[2]{};
|
||||
uint8_t weak_rumble_scale = UINT8_MAX;
|
||||
uint8_t strong_rumble_scale = UINT8_MAX;
|
||||
ControllerProfileConfirmationPolicy confirmation_policy =
|
||||
ControllerProfileConfirmationPolicy::kRumbleAndLed;
|
||||
uint16_t switching_chord = 0;
|
||||
uint8_t macro_trigger = CONTROLLER_PROFILE_NO_BUTTON;
|
||||
uint8_t macro_cancel = CONTROLLER_PROFILE_NO_BUTTON;
|
||||
uint8_t macro_step_count = 1;
|
||||
ControllerProfileTurboMode
|
||||
turbo_modes[CONTROLLER_PROFILE_LOGICAL_BUTTON_COUNT]{};
|
||||
ControllerProfileMacroStep
|
||||
macro_steps[CONTROLLER_PROFILE_MACRO_STEP_CAPACITY]{};
|
||||
};
|
||||
|
||||
struct ControllerProfileDatabaseEntry {
|
||||
bool used = false;
|
||||
ControllerIdentity identity{};
|
||||
uint8_t active_profile = 0;
|
||||
ControllerProfile profiles[CONTROLLER_PROFILE_COUNT]{};
|
||||
};
|
||||
|
||||
struct ControllerProfileDatabase {
|
||||
uint8_t fallback_active_profile = 0;
|
||||
ControllerProfile fallback_profiles[CONTROLLER_PROFILE_COUNT]{};
|
||||
ControllerProfileDatabaseEntry
|
||||
entries[CONTROLLER_PROFILE_STABLE_IDENTITY_CAPACITY]{};
|
||||
};
|
||||
|
||||
enum class ControllerProfileDatabaseResult : uint8_t {
|
||||
kOk = 0,
|
||||
kInvalidArgument = 1,
|
||||
kFull = 2,
|
||||
};
|
||||
|
||||
using ControllerProfileDatabaseRead = bool (*)(
|
||||
void* context, size_t offset, uint8_t* output, size_t size);
|
||||
|
||||
ControllerProfile controller_profile_default(const ControllerIdentity& identity,
|
||||
uint8_t profile_index);
|
||||
bool controller_profile_validate(const ControllerProfile& profile);
|
||||
bool controller_profile_encode(const ControllerProfile& profile,
|
||||
uint8_t* output, size_t output_size);
|
||||
bool controller_profile_decode(const uint8_t* input, size_t input_size,
|
||||
ControllerProfile* output);
|
||||
|
||||
void controller_profile_database_default(ControllerProfileDatabase* database);
|
||||
bool controller_profile_database_validate(
|
||||
const ControllerProfileDatabase& database);
|
||||
bool controller_profile_database_encode_range(
|
||||
const ControllerProfileDatabase& database, size_t offset,
|
||||
uint8_t* output, size_t size);
|
||||
bool controller_profile_database_decode(
|
||||
ControllerProfileDatabaseRead read, void* context,
|
||||
ControllerProfileDatabase* output);
|
||||
|
||||
const ControllerProfileDatabaseEntry* controller_profile_database_find(
|
||||
const ControllerProfileDatabase& database,
|
||||
const ControllerIdentity& identity);
|
||||
ControllerProfileDatabaseEntry* controller_profile_database_find(
|
||||
ControllerProfileDatabase* database,
|
||||
const ControllerIdentity& identity);
|
||||
ControllerProfileDatabaseResult controller_profile_database_ensure(
|
||||
ControllerProfileDatabase* database, const ControllerIdentity& identity,
|
||||
ControllerProfileDatabaseEntry** output);
|
||||
const ControllerProfile* controller_profile_database_get(
|
||||
const ControllerProfileDatabase& database,
|
||||
const ControllerIdentity& identity, uint8_t profile_index);
|
||||
ControllerProfileDatabaseResult controller_profile_database_set(
|
||||
ControllerProfileDatabase* database, const ControllerIdentity& identity,
|
||||
uint8_t profile_index, const ControllerProfile& profile);
|
||||
ControllerProfileDatabaseResult controller_profile_database_reset(
|
||||
ControllerProfileDatabase* database, const ControllerIdentity& identity,
|
||||
uint8_t profile_index);
|
||||
ControllerProfileDatabaseResult controller_profile_database_activate(
|
||||
ControllerProfileDatabase* database, const ControllerIdentity& identity,
|
||||
uint8_t profile_index);
|
||||
126
pico_profile_storage.cpp
Normal file
126
pico_profile_storage.cpp
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
#include "pico_profile_storage.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include "configuration_storage.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;
|
||||
constexpr uint32_t kProfileStorageOffset =
|
||||
kConfigurationStorageOffset - PROFILE_STORAGE_TOTAL_SIZE;
|
||||
|
||||
static_assert(FLASH_SECTOR_SIZE == PROFILE_STORAGE_SECTOR_SIZE,
|
||||
"profile storage sector size does not match Pico flash");
|
||||
static_assert(FLASH_PAGE_SIZE == PROFILE_STORAGE_PAGE_SIZE,
|
||||
"profile storage page size does not match Pico flash");
|
||||
static_assert(PICO_FLASH_BANK_STORAGE_OFFSET >=
|
||||
kConfigurationStorageSize + PROFILE_STORAGE_TOTAL_SIZE,
|
||||
"profile storage offset underflows flash");
|
||||
static_assert(kProfileStorageOffset + PROFILE_STORAGE_TOTAL_SIZE <=
|
||||
kConfigurationStorageOffset,
|
||||
"profile storage overlaps adapter configuration storage");
|
||||
static_assert(kConfigurationStorageOffset + kConfigurationStorageSize <=
|
||||
PICO_FLASH_BANK_STORAGE_OFFSET,
|
||||
"adapter 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");
|
||||
|
||||
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 <= kProfileStorageOffset;
|
||||
}
|
||||
|
||||
bool read_storage(void*, uint8_t bank, size_t offset, uint8_t* output,
|
||||
size_t size) {
|
||||
if (bank >= PROFILE_STORAGE_BANK_COUNT || output == nullptr ||
|
||||
offset > PROFILE_STORAGE_BANK_SIZE ||
|
||||
size > PROFILE_STORAGE_BANK_SIZE - offset ||
|
||||
!storage_region_available()) {
|
||||
return false;
|
||||
}
|
||||
const uintptr_t address =
|
||||
XIP_BASE + kProfileStorageOffset +
|
||||
bank * PROFILE_STORAGE_BANK_SIZE + offset;
|
||||
memcpy(output, reinterpret_cast<const void*>(address), size);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool erase_storage_sector(void*, uint8_t bank, size_t offset) {
|
||||
if (bank >= PROFILE_STORAGE_BANK_COUNT ||
|
||||
offset % FLASH_SECTOR_SIZE != 0 ||
|
||||
offset > PROFILE_STORAGE_BANK_SIZE ||
|
||||
FLASH_SECTOR_SIZE > PROFILE_STORAGE_BANK_SIZE - offset ||
|
||||
!storage_region_available()) {
|
||||
return false;
|
||||
}
|
||||
FlashMutation mutation{
|
||||
true,
|
||||
static_cast<uint32_t>(
|
||||
kProfileStorageOffset + bank * PROFILE_STORAGE_BANK_SIZE + offset),
|
||||
nullptr,
|
||||
};
|
||||
return flash_safe_execute(perform_flash_mutation, &mutation,
|
||||
UINT32_MAX) == PICO_OK;
|
||||
}
|
||||
|
||||
bool program_storage(void*, uint8_t bank, size_t offset,
|
||||
const uint8_t* data, size_t size) {
|
||||
if (bank >= PROFILE_STORAGE_BANK_COUNT || data == nullptr ||
|
||||
size != FLASH_PAGE_SIZE || offset % FLASH_PAGE_SIZE != 0 ||
|
||||
offset > PROFILE_STORAGE_BANK_SIZE ||
|
||||
size > PROFILE_STORAGE_BANK_SIZE - offset ||
|
||||
!storage_region_available()) {
|
||||
return false;
|
||||
}
|
||||
FlashMutation mutation{
|
||||
false,
|
||||
static_cast<uint32_t>(
|
||||
kProfileStorageOffset + bank * PROFILE_STORAGE_BANK_SIZE + offset),
|
||||
data,
|
||||
};
|
||||
return flash_safe_execute(perform_flash_mutation, &mutation,
|
||||
UINT32_MAX) == PICO_OK;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ProfileStorageIo pico_profile_storage_io() {
|
||||
return {
|
||||
nullptr,
|
||||
PROFILE_STORAGE_BANK_SIZE,
|
||||
FLASH_SECTOR_SIZE,
|
||||
FLASH_PAGE_SIZE,
|
||||
read_storage,
|
||||
erase_storage_sector,
|
||||
program_storage,
|
||||
};
|
||||
}
|
||||
5
pico_profile_storage.h
Normal file
5
pico_profile_storage.h
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
#pragma once
|
||||
|
||||
#include "profile_storage.h"
|
||||
|
||||
ProfileStorageIo pico_profile_storage_io();
|
||||
548
profile_service.cpp
Normal file
548
profile_service.cpp
Normal file
|
|
@ -0,0 +1,548 @@
|
|||
#include "profile_service.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include "pico/critical_section.h"
|
||||
#include "pico_profile_storage.h"
|
||||
#include "profile_storage.h"
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr uint32_t kMinimumCommitIntervalMs = 1000;
|
||||
|
||||
enum class PendingCommandType : uint8_t {
|
||||
kNone = 0,
|
||||
kReset = 1,
|
||||
kActivate = 2,
|
||||
};
|
||||
|
||||
struct PendingCommand {
|
||||
PendingCommandType type = PendingCommandType::kNone;
|
||||
ControllerIdentity identity{};
|
||||
uint8_t profile_index = 0;
|
||||
};
|
||||
|
||||
struct ProfileTransaction {
|
||||
ControllerIdentity identity{};
|
||||
uint8_t profile_index = 0;
|
||||
ConfigurationTransactionSnapshot snapshot{};
|
||||
uint8_t payload[CONTROLLER_PROFILE_ENCODED_SIZE]{};
|
||||
};
|
||||
|
||||
critical_section_t g_lock;
|
||||
bool g_prepared = false;
|
||||
ProfileStorage g_storage;
|
||||
ControllerProfileDatabase g_database;
|
||||
ProfileServiceMetadata g_metadata;
|
||||
ProfileServiceListSnapshot g_list;
|
||||
ProfileServiceSelectedSnapshot g_selected;
|
||||
ProfileTransaction g_transaction;
|
||||
PendingCommand g_command;
|
||||
bool g_identity_dirty = false;
|
||||
bool g_has_committed = false;
|
||||
uint32_t g_last_commit_ms = 0;
|
||||
|
||||
bool valid_identity(const ControllerIdentity& identity) {
|
||||
uint8_t encoded[CONTROLLER_IDENTITY_ENCODED_SIZE]{};
|
||||
return (controller_identity_is_global(identity) || identity.stable) &&
|
||||
controller_identity_encode(identity, encoded, sizeof(encoded));
|
||||
}
|
||||
|
||||
void refresh_list_locked() {
|
||||
g_list = {};
|
||||
g_list.metadata = g_metadata;
|
||||
g_list.count = 1;
|
||||
g_list.rows[0].identity = controller_identity_global();
|
||||
g_list.rows[0].active_profile =
|
||||
g_database.fallback_active_profile;
|
||||
for (const ControllerProfileDatabaseEntry& entry : g_database.entries) {
|
||||
if (!entry.used || g_list.count >= PROFILE_SERVICE_LIST_CAPACITY) {
|
||||
continue;
|
||||
}
|
||||
ProfileServiceListRow& row = g_list.rows[g_list.count++];
|
||||
row.identity = entry.identity;
|
||||
row.active_profile = entry.active_profile;
|
||||
}
|
||||
}
|
||||
|
||||
void refresh_selected_locked() {
|
||||
g_selected.metadata = g_metadata;
|
||||
const ControllerProfile* profile = controller_profile_database_get(
|
||||
g_database, g_selected.identity, g_selected.profile_index);
|
||||
if (profile == nullptr) {
|
||||
g_selected.valid = false;
|
||||
g_selected.status = ConfigurationTransactionStatus::kMalformed;
|
||||
return;
|
||||
}
|
||||
g_selected.profile = *profile;
|
||||
g_selected.valid = true;
|
||||
g_selected.status = ConfigurationTransactionStatus::kCommitted;
|
||||
}
|
||||
|
||||
void refresh_metadata_locked(ProfileServiceState state) {
|
||||
const ProfileStorageSnapshot& stored = g_storage.snapshot();
|
||||
g_metadata.state = state;
|
||||
g_metadata.generation = stored.valid ? stored.generation : 0;
|
||||
g_metadata.payload_crc = stored.valid ? stored.payload_crc : 0;
|
||||
refresh_list_locked();
|
||||
refresh_selected_locked();
|
||||
}
|
||||
|
||||
ConfigurationTransactionStatus database_result_status(
|
||||
ControllerProfileDatabaseResult result) {
|
||||
switch (result) {
|
||||
case ControllerProfileDatabaseResult::kOk:
|
||||
return ConfigurationTransactionStatus::kPending;
|
||||
case ControllerProfileDatabaseResult::kFull:
|
||||
return ConfigurationTransactionStatus::kTooLarge;
|
||||
case ControllerProfileDatabaseResult::kInvalidArgument:
|
||||
return ConfigurationTransactionStatus::kMalformed;
|
||||
}
|
||||
return ConfigurationTransactionStatus::kStorageError;
|
||||
}
|
||||
|
||||
bool mutation_ready(uint32_t now_ms) {
|
||||
return !g_has_committed ||
|
||||
static_cast<uint32_t>(now_ms - g_last_commit_ms) >=
|
||||
kMinimumCommitIntervalMs;
|
||||
}
|
||||
|
||||
void finish_mutation(ConfigurationTransactionStatus status,
|
||||
bool clear_command) {
|
||||
const ProfileStorageSnapshot& stored = g_storage.snapshot();
|
||||
critical_section_enter_blocking(&g_lock);
|
||||
g_transaction.snapshot.status = status;
|
||||
g_transaction.snapshot.stored_generation =
|
||||
stored.valid ? stored.generation : 0;
|
||||
g_transaction.snapshot.stored_crc =
|
||||
stored.valid ? stored.payload_crc : 0;
|
||||
if (clear_command) {
|
||||
g_command = {};
|
||||
}
|
||||
refresh_metadata_locked(
|
||||
status == ConfigurationTransactionStatus::kStorageError
|
||||
? ProfileServiceState::kStorageError
|
||||
: ProfileServiceState::kReady);
|
||||
critical_section_exit(&g_lock);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void profile_service_prepare() {
|
||||
if (g_prepared) {
|
||||
return;
|
||||
}
|
||||
critical_section_init(&g_lock);
|
||||
g_metadata = {};
|
||||
g_list = {};
|
||||
g_selected = {};
|
||||
g_selected.identity = controller_identity_global();
|
||||
g_selected.profile_index = 0;
|
||||
g_transaction = {};
|
||||
g_command = {};
|
||||
g_identity_dirty = false;
|
||||
g_has_committed = false;
|
||||
g_last_commit_ms = 0;
|
||||
g_prepared = true;
|
||||
}
|
||||
|
||||
void profile_service_initialize_on_storage_core() {
|
||||
if (!g_prepared) {
|
||||
profile_service_prepare();
|
||||
}
|
||||
const bool initialized =
|
||||
g_storage.initialize(pico_profile_storage_io(), &g_database);
|
||||
critical_section_enter_blocking(&g_lock);
|
||||
refresh_metadata_locked(initialized ? ProfileServiceState::kReady
|
||||
: ProfileServiceState::kStorageError);
|
||||
critical_section_exit(&g_lock);
|
||||
}
|
||||
|
||||
bool profile_service_observe_identity_on_storage_core(
|
||||
const ControllerIdentity& identity) {
|
||||
if (!g_prepared || !identity.stable ||
|
||||
controller_identity_is_global(identity) ||
|
||||
!valid_identity(identity)) {
|
||||
return false;
|
||||
}
|
||||
critical_section_enter_blocking(&g_lock);
|
||||
if (g_metadata.state != ProfileServiceState::kReady) {
|
||||
critical_section_exit(&g_lock);
|
||||
return false;
|
||||
}
|
||||
ControllerProfileDatabaseEntry* entry =
|
||||
controller_profile_database_find(&g_database, identity);
|
||||
if (entry != nullptr) {
|
||||
critical_section_exit(&g_lock);
|
||||
return true;
|
||||
}
|
||||
const ControllerProfileDatabaseResult result =
|
||||
controller_profile_database_ensure(&g_database, identity, &entry);
|
||||
if (result == ControllerProfileDatabaseResult::kOk) {
|
||||
g_identity_dirty = true;
|
||||
refresh_list_locked();
|
||||
}
|
||||
critical_section_exit(&g_lock);
|
||||
return result == ControllerProfileDatabaseResult::kOk;
|
||||
}
|
||||
|
||||
|
||||
void profile_service_task_on_storage_core(uint32_t now_ms) {
|
||||
PendingCommand command{};
|
||||
bool process_write = false;
|
||||
bool process_identity = false;
|
||||
ControllerIdentity write_identity{};
|
||||
uint8_t write_profile_index = 0;
|
||||
uint8_t write_payload[CONTROLLER_PROFILE_ENCODED_SIZE]{};
|
||||
|
||||
critical_section_enter_blocking(&g_lock);
|
||||
if (mutation_ready(now_ms)) {
|
||||
if (g_command.type != PendingCommandType::kNone) {
|
||||
command = g_command;
|
||||
} else if (g_transaction.snapshot.status ==
|
||||
ConfigurationTransactionStatus::kPending) {
|
||||
process_write = true;
|
||||
write_identity = g_transaction.identity;
|
||||
write_profile_index = g_transaction.profile_index;
|
||||
memcpy(write_payload, g_transaction.payload,
|
||||
sizeof(write_payload));
|
||||
} else if (g_identity_dirty) {
|
||||
process_identity = true;
|
||||
}
|
||||
}
|
||||
critical_section_exit(&g_lock);
|
||||
|
||||
if (!process_write && !process_identity &&
|
||||
command.type == PendingCommandType::kNone) {
|
||||
return;
|
||||
}
|
||||
|
||||
ControllerProfileDatabaseResult database_result =
|
||||
ControllerProfileDatabaseResult::kInvalidArgument;
|
||||
if (process_identity) {
|
||||
database_result = ControllerProfileDatabaseResult::kOk;
|
||||
} else if (process_write) {
|
||||
ControllerProfile profile{};
|
||||
if (controller_profile_decode(write_payload, sizeof(write_payload),
|
||||
&profile)) {
|
||||
critical_section_enter_blocking(&g_lock);
|
||||
database_result = controller_profile_database_set(
|
||||
&g_database, write_identity, write_profile_index, profile);
|
||||
critical_section_exit(&g_lock);
|
||||
}
|
||||
} else if (command.type == PendingCommandType::kReset) {
|
||||
critical_section_enter_blocking(&g_lock);
|
||||
database_result = controller_profile_database_reset(
|
||||
&g_database, command.identity, command.profile_index);
|
||||
critical_section_exit(&g_lock);
|
||||
} else if (command.type == PendingCommandType::kActivate) {
|
||||
critical_section_enter_blocking(&g_lock);
|
||||
database_result = controller_profile_database_activate(
|
||||
&g_database, command.identity, command.profile_index);
|
||||
critical_section_exit(&g_lock);
|
||||
}
|
||||
|
||||
if (database_result != ControllerProfileDatabaseResult::kOk) {
|
||||
finish_mutation(database_result_status(database_result),
|
||||
!process_write);
|
||||
return;
|
||||
}
|
||||
|
||||
const ProfileStorageResult storage_result = g_storage.commit(g_database);
|
||||
ConfigurationTransactionStatus status =
|
||||
ConfigurationTransactionStatus::kStorageError;
|
||||
if (storage_result == ProfileStorageResult::kOk) {
|
||||
status = ConfigurationTransactionStatus::kCommitted;
|
||||
g_has_committed = true;
|
||||
g_last_commit_ms = now_ms;
|
||||
} else if (storage_result == ProfileStorageResult::kUnchanged) {
|
||||
status = ConfigurationTransactionStatus::kUnchanged;
|
||||
} else {
|
||||
critical_section_enter_blocking(&g_lock);
|
||||
const bool restored =
|
||||
g_storage.initialize(pico_profile_storage_io(), &g_database);
|
||||
critical_section_exit(&g_lock);
|
||||
if (!restored) {
|
||||
status = ConfigurationTransactionStatus::kStorageError;
|
||||
}
|
||||
}
|
||||
if (process_identity) {
|
||||
critical_section_enter_blocking(&g_lock);
|
||||
g_identity_dirty = false;
|
||||
refresh_metadata_locked(
|
||||
status == ConfigurationTransactionStatus::kStorageError
|
||||
? ProfileServiceState::kStorageError
|
||||
: ProfileServiceState::kReady);
|
||||
critical_section_exit(&g_lock);
|
||||
return;
|
||||
}
|
||||
critical_section_enter_blocking(&g_lock);
|
||||
g_identity_dirty = false;
|
||||
critical_section_exit(&g_lock);
|
||||
finish_mutation(status, !process_write);
|
||||
}
|
||||
|
||||
ConfigurationTransactionStatus profile_service_select(
|
||||
const ControllerIdentity& identity, uint8_t profile_index) {
|
||||
if (!g_prepared) {
|
||||
profile_service_prepare();
|
||||
}
|
||||
critical_section_enter_blocking(&g_lock);
|
||||
ConfigurationTransactionStatus status =
|
||||
ConfigurationTransactionStatus::kCommitted;
|
||||
if (!valid_identity(identity) ||
|
||||
profile_index >= CONTROLLER_PROFILE_COUNT) {
|
||||
status = ConfigurationTransactionStatus::kMalformed;
|
||||
g_selected.metadata = g_metadata;
|
||||
g_selected.identity = identity;
|
||||
g_selected.profile_index = profile_index;
|
||||
g_selected.valid = false;
|
||||
g_selected.status = status;
|
||||
} else if (g_metadata.state != ProfileServiceState::kReady) {
|
||||
status = g_metadata.state == ProfileServiceState::kLoading
|
||||
? ConfigurationTransactionStatus::kPending
|
||||
: ConfigurationTransactionStatus::kStorageError;
|
||||
g_selected.metadata = g_metadata;
|
||||
g_selected.identity = identity;
|
||||
g_selected.profile_index = profile_index;
|
||||
g_selected.valid = false;
|
||||
g_selected.status = status;
|
||||
} else {
|
||||
g_selected.identity = identity;
|
||||
g_selected.profile_index = profile_index;
|
||||
refresh_selected_locked();
|
||||
status = g_selected.status;
|
||||
}
|
||||
critical_section_exit(&g_lock);
|
||||
return status;
|
||||
}
|
||||
|
||||
ConfigurationTransactionStatus profile_service_begin(
|
||||
uint32_t transaction_id, const ControllerIdentity& identity,
|
||||
uint8_t profile_index, uint16_t schema_version, size_t payload_size,
|
||||
uint32_t payload_crc) {
|
||||
if (!g_prepared) {
|
||||
profile_service_prepare();
|
||||
}
|
||||
critical_section_enter_blocking(&g_lock);
|
||||
if (g_command.type != PendingCommandType::kNone ||
|
||||
g_transaction.snapshot.status ==
|
||||
ConfigurationTransactionStatus::kReceiving ||
|
||||
g_transaction.snapshot.status ==
|
||||
ConfigurationTransactionStatus::kPending) {
|
||||
critical_section_exit(&g_lock);
|
||||
return ConfigurationTransactionStatus::kBusy;
|
||||
}
|
||||
g_transaction = {};
|
||||
g_transaction.snapshot.transaction_id = transaction_id;
|
||||
g_transaction.identity = identity;
|
||||
g_transaction.profile_index = profile_index;
|
||||
if (transaction_id == 0 || !valid_identity(identity) ||
|
||||
profile_index >= CONTROLLER_PROFILE_COUNT || payload_size == 0) {
|
||||
g_transaction.snapshot.status =
|
||||
ConfigurationTransactionStatus::kMalformed;
|
||||
} else if (schema_version != CONTROLLER_PROFILE_SCHEMA_VERSION) {
|
||||
g_transaction.snapshot.status =
|
||||
ConfigurationTransactionStatus::kUnsupportedSchema;
|
||||
} else if (payload_size > CONTROLLER_PROFILE_ENCODED_SIZE) {
|
||||
g_transaction.snapshot.status =
|
||||
ConfigurationTransactionStatus::kTooLarge;
|
||||
} else if (payload_size != CONTROLLER_PROFILE_ENCODED_SIZE) {
|
||||
g_transaction.snapshot.status =
|
||||
ConfigurationTransactionStatus::kMalformed;
|
||||
} else {
|
||||
g_transaction.snapshot.expected_size =
|
||||
static_cast<uint16_t>(payload_size);
|
||||
g_transaction.snapshot.expected_crc = payload_crc;
|
||||
g_transaction.snapshot.status =
|
||||
ConfigurationTransactionStatus::kReceiving;
|
||||
}
|
||||
const ConfigurationTransactionStatus status =
|
||||
g_transaction.snapshot.status;
|
||||
critical_section_exit(&g_lock);
|
||||
return status;
|
||||
}
|
||||
|
||||
ConfigurationTransactionStatus profile_service_append(
|
||||
uint32_t transaction_id, size_t offset, const uint8_t* data,
|
||||
size_t size) {
|
||||
critical_section_enter_blocking(&g_lock);
|
||||
if (g_transaction.snapshot.status !=
|
||||
ConfigurationTransactionStatus::kReceiving) {
|
||||
critical_section_exit(&g_lock);
|
||||
return ConfigurationTransactionStatus::kBusy;
|
||||
}
|
||||
if (transaction_id != g_transaction.snapshot.transaction_id ||
|
||||
data == nullptr || size == 0 ||
|
||||
offset != g_transaction.snapshot.received_size ||
|
||||
offset > g_transaction.snapshot.expected_size ||
|
||||
size > g_transaction.snapshot.expected_size - offset) {
|
||||
g_transaction.snapshot.status =
|
||||
ConfigurationTransactionStatus::kOutOfOrder;
|
||||
} else {
|
||||
memcpy(&g_transaction.payload[offset], data, size);
|
||||
g_transaction.snapshot.received_size =
|
||||
static_cast<uint16_t>(offset + size);
|
||||
}
|
||||
const ConfigurationTransactionStatus status =
|
||||
g_transaction.snapshot.status;
|
||||
critical_section_exit(&g_lock);
|
||||
return status;
|
||||
}
|
||||
|
||||
ConfigurationTransactionStatus profile_service_commit(
|
||||
uint32_t transaction_id) {
|
||||
critical_section_enter_blocking(&g_lock);
|
||||
if (g_transaction.snapshot.status !=
|
||||
ConfigurationTransactionStatus::kReceiving ||
|
||||
transaction_id != g_transaction.snapshot.transaction_id ||
|
||||
g_transaction.snapshot.received_size !=
|
||||
g_transaction.snapshot.expected_size) {
|
||||
g_transaction.snapshot.status =
|
||||
ConfigurationTransactionStatus::kOutOfOrder;
|
||||
} else if (profile_storage_crc32(
|
||||
g_transaction.payload,
|
||||
g_transaction.snapshot.expected_size) !=
|
||||
g_transaction.snapshot.expected_crc) {
|
||||
g_transaction.snapshot.status =
|
||||
ConfigurationTransactionStatus::kBadCrc;
|
||||
} else {
|
||||
ControllerProfile profile{};
|
||||
g_transaction.snapshot.status =
|
||||
controller_profile_decode(
|
||||
g_transaction.payload,
|
||||
g_transaction.snapshot.expected_size, &profile)
|
||||
? ConfigurationTransactionStatus::kPending
|
||||
: ConfigurationTransactionStatus::kMalformed;
|
||||
}
|
||||
const ConfigurationTransactionStatus status =
|
||||
g_transaction.snapshot.status;
|
||||
critical_section_exit(&g_lock);
|
||||
return status;
|
||||
}
|
||||
|
||||
ConfigurationTransactionStatus profile_service_reset(
|
||||
uint32_t transaction_id, const ControllerIdentity& identity,
|
||||
uint8_t profile_index) {
|
||||
if (!g_prepared) {
|
||||
profile_service_prepare();
|
||||
}
|
||||
critical_section_enter_blocking(&g_lock);
|
||||
if (g_command.type != PendingCommandType::kNone ||
|
||||
g_transaction.snapshot.status ==
|
||||
ConfigurationTransactionStatus::kReceiving ||
|
||||
g_transaction.snapshot.status ==
|
||||
ConfigurationTransactionStatus::kPending) {
|
||||
critical_section_exit(&g_lock);
|
||||
return ConfigurationTransactionStatus::kBusy;
|
||||
}
|
||||
g_transaction = {};
|
||||
g_transaction.snapshot.transaction_id = transaction_id;
|
||||
g_transaction.identity = identity;
|
||||
g_transaction.profile_index = profile_index;
|
||||
if (transaction_id == 0 || !valid_identity(identity) ||
|
||||
(profile_index != CONTROLLER_PROFILE_ALL &&
|
||||
profile_index >= CONTROLLER_PROFILE_COUNT)) {
|
||||
g_transaction.snapshot.status =
|
||||
ConfigurationTransactionStatus::kMalformed;
|
||||
critical_section_exit(&g_lock);
|
||||
return ConfigurationTransactionStatus::kMalformed;
|
||||
}
|
||||
g_transaction.snapshot.status = ConfigurationTransactionStatus::kPending;
|
||||
g_command.type = PendingCommandType::kReset;
|
||||
g_command.identity = identity;
|
||||
g_command.profile_index = profile_index;
|
||||
critical_section_exit(&g_lock);
|
||||
return ConfigurationTransactionStatus::kPending;
|
||||
}
|
||||
|
||||
ConfigurationTransactionStatus profile_service_activate(
|
||||
uint32_t transaction_id, const ControllerIdentity& identity,
|
||||
uint8_t profile_index) {
|
||||
if (!g_prepared) {
|
||||
profile_service_prepare();
|
||||
}
|
||||
critical_section_enter_blocking(&g_lock);
|
||||
if (g_command.type != PendingCommandType::kNone ||
|
||||
g_transaction.snapshot.status ==
|
||||
ConfigurationTransactionStatus::kReceiving ||
|
||||
g_transaction.snapshot.status ==
|
||||
ConfigurationTransactionStatus::kPending) {
|
||||
critical_section_exit(&g_lock);
|
||||
return ConfigurationTransactionStatus::kBusy;
|
||||
}
|
||||
g_transaction = {};
|
||||
g_transaction.snapshot.transaction_id = transaction_id;
|
||||
g_transaction.identity = identity;
|
||||
g_transaction.profile_index = profile_index;
|
||||
if (transaction_id == 0 || !valid_identity(identity) ||
|
||||
profile_index >= CONTROLLER_PROFILE_COUNT) {
|
||||
g_transaction.snapshot.status =
|
||||
ConfigurationTransactionStatus::kMalformed;
|
||||
critical_section_exit(&g_lock);
|
||||
return ConfigurationTransactionStatus::kMalformed;
|
||||
}
|
||||
g_transaction.snapshot.status = ConfigurationTransactionStatus::kPending;
|
||||
g_command.type = PendingCommandType::kActivate;
|
||||
g_command.identity = identity;
|
||||
g_command.profile_index = profile_index;
|
||||
critical_section_exit(&g_lock);
|
||||
return ConfigurationTransactionStatus::kPending;
|
||||
}
|
||||
|
||||
void profile_service_list_snapshot(ProfileServiceListSnapshot* output) {
|
||||
if (output == nullptr) {
|
||||
return;
|
||||
}
|
||||
critical_section_enter_blocking(&g_lock);
|
||||
*output = g_list;
|
||||
critical_section_exit(&g_lock);
|
||||
}
|
||||
|
||||
void profile_service_selected_snapshot(
|
||||
ProfileServiceSelectedSnapshot* output) {
|
||||
if (output == nullptr) {
|
||||
return;
|
||||
}
|
||||
critical_section_enter_blocking(&g_lock);
|
||||
*output = g_selected;
|
||||
critical_section_exit(&g_lock);
|
||||
}
|
||||
|
||||
void profile_service_transaction_snapshot(
|
||||
ProfileServiceTransactionSnapshot* output) {
|
||||
if (output == nullptr) {
|
||||
return;
|
||||
}
|
||||
critical_section_enter_blocking(&g_lock);
|
||||
output->metadata = g_metadata;
|
||||
output->identity = g_transaction.identity;
|
||||
output->profile_index = g_transaction.profile_index;
|
||||
output->transaction = g_transaction.snapshot;
|
||||
critical_section_exit(&g_lock);
|
||||
}
|
||||
|
||||
bool profile_service_active_profile(const ControllerIdentity& identity,
|
||||
ControllerProfile* output,
|
||||
uint8_t* profile_index) {
|
||||
if (output == nullptr || profile_index == nullptr ||
|
||||
!valid_identity(identity)) {
|
||||
return false;
|
||||
}
|
||||
critical_section_enter_blocking(&g_lock);
|
||||
if (g_metadata.state != ProfileServiceState::kReady) {
|
||||
critical_section_exit(&g_lock);
|
||||
return false;
|
||||
}
|
||||
const ControllerProfileDatabaseEntry* entry =
|
||||
controller_profile_database_find(g_database, identity);
|
||||
if (entry != nullptr) {
|
||||
*profile_index = entry->active_profile;
|
||||
*output = entry->profiles[entry->active_profile];
|
||||
} else {
|
||||
*profile_index = g_database.fallback_active_profile;
|
||||
*output = g_database.fallback_profiles[*profile_index];
|
||||
}
|
||||
critical_section_exit(&g_lock);
|
||||
return true;
|
||||
}
|
||||
83
profile_service.h
Normal file
83
profile_service.h
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "configuration_transaction.h"
|
||||
#include "controller_profile.h"
|
||||
|
||||
constexpr uint8_t PROFILE_SERVICE_LIST_CAPACITY =
|
||||
CONTROLLER_PROFILE_STABLE_IDENTITY_CAPACITY + 1;
|
||||
|
||||
enum class ProfileServiceState : uint8_t {
|
||||
kLoading = 0,
|
||||
kReady = 1,
|
||||
kStorageError = 2,
|
||||
};
|
||||
|
||||
struct ProfileServiceMetadata {
|
||||
ProfileServiceState state = ProfileServiceState::kLoading;
|
||||
uint32_t generation = 0;
|
||||
uint32_t payload_crc = 0;
|
||||
};
|
||||
|
||||
struct ProfileServiceListRow {
|
||||
ControllerIdentity identity{};
|
||||
uint8_t active_profile = 0;
|
||||
};
|
||||
|
||||
struct ProfileServiceListSnapshot {
|
||||
ProfileServiceMetadata metadata{};
|
||||
uint8_t count = 0;
|
||||
ProfileServiceListRow rows[PROFILE_SERVICE_LIST_CAPACITY]{};
|
||||
};
|
||||
|
||||
struct ProfileServiceSelectedSnapshot {
|
||||
ProfileServiceMetadata metadata{};
|
||||
bool valid = false;
|
||||
ConfigurationTransactionStatus status =
|
||||
ConfigurationTransactionStatus::kIdle;
|
||||
ControllerIdentity identity{};
|
||||
uint8_t profile_index = 0;
|
||||
ControllerProfile profile{};
|
||||
};
|
||||
|
||||
struct ProfileServiceTransactionSnapshot {
|
||||
ProfileServiceMetadata metadata{};
|
||||
ControllerIdentity identity{};
|
||||
uint8_t profile_index = 0;
|
||||
ConfigurationTransactionSnapshot transaction{};
|
||||
};
|
||||
|
||||
void profile_service_prepare();
|
||||
void profile_service_initialize_on_storage_core();
|
||||
void profile_service_task_on_storage_core(uint32_t now_ms);
|
||||
bool profile_service_observe_identity_on_storage_core(
|
||||
const ControllerIdentity& identity);
|
||||
|
||||
ConfigurationTransactionStatus profile_service_select(
|
||||
const ControllerIdentity& identity, uint8_t profile_index);
|
||||
ConfigurationTransactionStatus profile_service_begin(
|
||||
uint32_t transaction_id, const ControllerIdentity& identity,
|
||||
uint8_t profile_index, uint16_t schema_version, size_t payload_size,
|
||||
uint32_t payload_crc);
|
||||
ConfigurationTransactionStatus profile_service_append(
|
||||
uint32_t transaction_id, size_t offset, const uint8_t* data,
|
||||
size_t size);
|
||||
ConfigurationTransactionStatus profile_service_commit(
|
||||
uint32_t transaction_id);
|
||||
ConfigurationTransactionStatus profile_service_reset(
|
||||
uint32_t transaction_id, const ControllerIdentity& identity,
|
||||
uint8_t profile_index);
|
||||
ConfigurationTransactionStatus profile_service_activate(
|
||||
uint32_t transaction_id, const ControllerIdentity& identity,
|
||||
uint8_t profile_index);
|
||||
|
||||
void profile_service_list_snapshot(ProfileServiceListSnapshot* output);
|
||||
void profile_service_selected_snapshot(
|
||||
ProfileServiceSelectedSnapshot* output);
|
||||
void profile_service_transaction_snapshot(
|
||||
ProfileServiceTransactionSnapshot* output);
|
||||
bool profile_service_active_profile(const ControllerIdentity& identity,
|
||||
ControllerProfile* output,
|
||||
uint8_t* profile_index);
|
||||
301
profile_storage.cpp
Normal file
301
profile_storage.cpp
Normal file
|
|
@ -0,0 +1,301 @@
|
|||
#include "profile_storage.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr uint8_t kRecordMagic[4] = {'S', 'P', 'P', 'F'};
|
||||
constexpr uint16_t kRecordFormatVersion = 1;
|
||||
constexpr size_t kHeaderFieldsSize = 24;
|
||||
constexpr size_t kHeaderCrcOffset = 20;
|
||||
|
||||
uint16_t storage_read_u16(const uint8_t* input) {
|
||||
return static_cast<uint16_t>(input[0]) |
|
||||
(static_cast<uint16_t>(input[1]) << 8);
|
||||
}
|
||||
|
||||
uint32_t storage_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 storage_write_u16(uint8_t* output, uint16_t value) {
|
||||
output[0] = static_cast<uint8_t>(value);
|
||||
output[1] = static_cast<uint8_t>(value >> 8);
|
||||
}
|
||||
|
||||
void storage_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);
|
||||
}
|
||||
|
||||
uint32_t crc32_update(uint32_t crc, const uint8_t* data, size_t size) {
|
||||
for (size_t index = 0; index < size; ++index) {
|
||||
crc ^= data[index];
|
||||
for (uint8_t bit = 0; bit < 8; ++bit) {
|
||||
crc = (crc >> 1) ^
|
||||
(0xedb88320u &
|
||||
static_cast<uint32_t>(
|
||||
-static_cast<int32_t>(crc & 1u)));
|
||||
}
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
|
||||
bool generation_is_newer(uint32_t candidate, uint32_t current) {
|
||||
return static_cast<int32_t>(candidate - current) > 0;
|
||||
}
|
||||
|
||||
struct DatabaseReadContext {
|
||||
const ProfileStorageIo* io;
|
||||
uint8_t bank;
|
||||
};
|
||||
|
||||
bool read_database(void* context, size_t offset, uint8_t* output,
|
||||
size_t size) {
|
||||
const auto* read_context =
|
||||
static_cast<const DatabaseReadContext*>(context);
|
||||
return read_context->io->read(
|
||||
read_context->io->context, read_context->bank,
|
||||
PROFILE_STORAGE_RECORD_HEADER_SIZE + offset, output, size);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
uint32_t profile_storage_crc32(const uint8_t* data, size_t size) {
|
||||
if (data == nullptr && size != 0) {
|
||||
return 0;
|
||||
}
|
||||
return ~crc32_update(0xffffffffu, data, size);
|
||||
}
|
||||
|
||||
bool ProfileStorage::initialize(const ProfileStorageIo& io,
|
||||
ControllerProfileDatabase* database) {
|
||||
io_ = io;
|
||||
snapshot_ = {};
|
||||
initialized_ = database != nullptr && io_.read != nullptr &&
|
||||
io_.erase_sector != nullptr && io_.program != nullptr &&
|
||||
io_.bank_size >= PROFILE_STORAGE_BANK_SIZE &&
|
||||
io_.sector_size == PROFILE_STORAGE_SECTOR_SIZE &&
|
||||
io_.page_size == PROFILE_STORAGE_PAGE_SIZE &&
|
||||
io_.bank_size % io_.sector_size == 0 &&
|
||||
io_.sector_size % io_.page_size == 0;
|
||||
if (!initialized_) {
|
||||
return false;
|
||||
}
|
||||
|
||||
controller_profile_database_default(database);
|
||||
BankHeader headers[PROFILE_STORAGE_BANK_COUNT]{};
|
||||
bool valid[PROFILE_STORAGE_BANK_COUNT]{};
|
||||
for (uint8_t bank = 0; bank < PROFILE_STORAGE_BANK_COUNT; ++bank) {
|
||||
valid[bank] = read_header(bank, &headers[bank]) &&
|
||||
validate_payload(bank, headers[bank].payload_crc);
|
||||
}
|
||||
|
||||
uint8_t first = 0;
|
||||
uint8_t second = 1;
|
||||
if (valid[1] &&
|
||||
(!valid[0] || generation_is_newer(headers[1].generation,
|
||||
headers[0].generation))) {
|
||||
first = 1;
|
||||
second = 0;
|
||||
}
|
||||
const uint8_t order[PROFILE_STORAGE_BANK_COUNT] = {first, second};
|
||||
for (uint8_t candidate : order) {
|
||||
if (!valid[candidate] || !decode_bank(candidate, database)) {
|
||||
continue;
|
||||
}
|
||||
snapshot_.valid = true;
|
||||
snapshot_.generation = headers[candidate].generation;
|
||||
snapshot_.payload_crc = headers[candidate].payload_crc;
|
||||
snapshot_.active_bank = candidate;
|
||||
return true;
|
||||
}
|
||||
controller_profile_database_default(database);
|
||||
return true;
|
||||
}
|
||||
|
||||
ProfileStorageResult ProfileStorage::commit(
|
||||
const ControllerProfileDatabase& database) {
|
||||
if (!initialized_ || !controller_profile_database_validate(database)) {
|
||||
return ProfileStorageResult::kInvalidArgument;
|
||||
}
|
||||
if (snapshot_.valid && payload_matches(snapshot_.active_bank, database)) {
|
||||
return ProfileStorageResult::kUnchanged;
|
||||
}
|
||||
|
||||
uint8_t page[PROFILE_STORAGE_PAGE_SIZE]{};
|
||||
uint32_t crc = 0xffffffffu;
|
||||
for (size_t offset = 0;
|
||||
offset < CONTROLLER_PROFILE_DATABASE_ENCODED_SIZE;
|
||||
offset += sizeof(page)) {
|
||||
const size_t size =
|
||||
CONTROLLER_PROFILE_DATABASE_ENCODED_SIZE - offset < sizeof(page)
|
||||
? CONTROLLER_PROFILE_DATABASE_ENCODED_SIZE - offset
|
||||
: sizeof(page);
|
||||
if (!controller_profile_database_encode_range(database, offset,
|
||||
page, size)) {
|
||||
return ProfileStorageResult::kInvalidArgument;
|
||||
}
|
||||
crc = crc32_update(crc, page, size);
|
||||
}
|
||||
crc = ~crc;
|
||||
|
||||
const uint8_t target_bank = snapshot_.valid
|
||||
? snapshot_.active_bank ^ 1u
|
||||
: 0;
|
||||
for (size_t offset = 0; offset < PROFILE_STORAGE_BANK_SIZE;
|
||||
offset += PROFILE_STORAGE_SECTOR_SIZE) {
|
||||
if (!io_.erase_sector(io_.context, target_bank, offset)) {
|
||||
return ProfileStorageResult::kIoError;
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t offset = 0;
|
||||
offset < CONTROLLER_PROFILE_DATABASE_ENCODED_SIZE;
|
||||
offset += sizeof(page)) {
|
||||
memset(page, 0, sizeof(page));
|
||||
const size_t size =
|
||||
CONTROLLER_PROFILE_DATABASE_ENCODED_SIZE - offset < sizeof(page)
|
||||
? CONTROLLER_PROFILE_DATABASE_ENCODED_SIZE - offset
|
||||
: sizeof(page);
|
||||
if (!controller_profile_database_encode_range(database, offset,
|
||||
page, size) ||
|
||||
!io_.program(io_.context, target_bank,
|
||||
PROFILE_STORAGE_RECORD_HEADER_SIZE + offset,
|
||||
page, sizeof(page))) {
|
||||
return ProfileStorageResult::kIoError;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t stored_page[PROFILE_STORAGE_PAGE_SIZE]{};
|
||||
uint32_t stored_crc = 0xffffffffu;
|
||||
bool stored_payload_matches = true;
|
||||
for (size_t offset = 0;
|
||||
offset < CONTROLLER_PROFILE_DATABASE_ENCODED_SIZE;
|
||||
offset += sizeof(stored_page)) {
|
||||
const size_t size =
|
||||
CONTROLLER_PROFILE_DATABASE_ENCODED_SIZE - offset <
|
||||
sizeof(stored_page)
|
||||
? CONTROLLER_PROFILE_DATABASE_ENCODED_SIZE - offset
|
||||
: sizeof(stored_page);
|
||||
if (!io_.read(io_.context, target_bank,
|
||||
PROFILE_STORAGE_RECORD_HEADER_SIZE + offset,
|
||||
stored_page, size)) {
|
||||
return ProfileStorageResult::kIoError;
|
||||
}
|
||||
if (!controller_profile_database_encode_range(database, offset,
|
||||
page, size)) {
|
||||
return ProfileStorageResult::kInvalidArgument;
|
||||
}
|
||||
for (size_t index = 0; index < size; ++index) {
|
||||
stored_payload_matches &=
|
||||
stored_page[index] == page[index];
|
||||
}
|
||||
stored_crc = crc32_update(stored_crc, stored_page, size);
|
||||
}
|
||||
if (!stored_payload_matches || ~stored_crc != crc) {
|
||||
return ProfileStorageResult::kIoError;
|
||||
}
|
||||
|
||||
memset(page, 0, sizeof(page));
|
||||
memcpy(page, kRecordMagic, sizeof(kRecordMagic));
|
||||
storage_write_u16(&page[4], kRecordFormatVersion);
|
||||
storage_write_u16(&page[6],
|
||||
CONTROLLER_PROFILE_DATABASE_SCHEMA_VERSION);
|
||||
const uint32_t generation =
|
||||
snapshot_.valid ? snapshot_.generation + 1u : 1u;
|
||||
storage_write_u32(&page[8], generation);
|
||||
storage_write_u32(&page[12],
|
||||
CONTROLLER_PROFILE_DATABASE_ENCODED_SIZE);
|
||||
storage_write_u32(&page[16], crc);
|
||||
storage_write_u32(&page[kHeaderCrcOffset],
|
||||
profile_storage_crc32(page, kHeaderCrcOffset));
|
||||
if (!io_.program(io_.context, target_bank, 0, page, sizeof(page))) {
|
||||
return ProfileStorageResult::kIoError;
|
||||
}
|
||||
|
||||
snapshot_.valid = true;
|
||||
snapshot_.generation = generation;
|
||||
snapshot_.payload_crc = crc;
|
||||
snapshot_.active_bank = target_bank;
|
||||
return ProfileStorageResult::kOk;
|
||||
}
|
||||
|
||||
const ProfileStorageSnapshot& ProfileStorage::snapshot() const {
|
||||
return snapshot_;
|
||||
}
|
||||
|
||||
bool ProfileStorage::read_header(uint8_t bank, BankHeader* output) const {
|
||||
uint8_t header[kHeaderFieldsSize]{};
|
||||
if (bank >= PROFILE_STORAGE_BANK_COUNT || output == nullptr ||
|
||||
!io_.read(io_.context, bank, 0, header, sizeof(header)) ||
|
||||
memcmp(header, kRecordMagic, sizeof(kRecordMagic)) != 0 ||
|
||||
storage_read_u16(&header[4]) != kRecordFormatVersion ||
|
||||
storage_read_u16(&header[6]) !=
|
||||
CONTROLLER_PROFILE_DATABASE_SCHEMA_VERSION ||
|
||||
storage_read_u32(&header[12]) !=
|
||||
CONTROLLER_PROFILE_DATABASE_ENCODED_SIZE ||
|
||||
profile_storage_crc32(header, kHeaderCrcOffset) !=
|
||||
storage_read_u32(&header[kHeaderCrcOffset])) {
|
||||
return false;
|
||||
}
|
||||
output->generation = storage_read_u32(&header[8]);
|
||||
output->payload_crc = storage_read_u32(&header[16]);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ProfileStorage::validate_payload(uint8_t bank,
|
||||
uint32_t expected_crc) const {
|
||||
uint8_t page[PROFILE_STORAGE_PAGE_SIZE]{};
|
||||
uint32_t crc = 0xffffffffu;
|
||||
for (size_t offset = 0;
|
||||
offset < CONTROLLER_PROFILE_DATABASE_ENCODED_SIZE;
|
||||
offset += sizeof(page)) {
|
||||
const size_t size =
|
||||
CONTROLLER_PROFILE_DATABASE_ENCODED_SIZE - offset < sizeof(page)
|
||||
? CONTROLLER_PROFILE_DATABASE_ENCODED_SIZE - offset
|
||||
: sizeof(page);
|
||||
if (!io_.read(io_.context, bank,
|
||||
PROFILE_STORAGE_RECORD_HEADER_SIZE + offset,
|
||||
page, size)) {
|
||||
return false;
|
||||
}
|
||||
crc = crc32_update(crc, page, size);
|
||||
}
|
||||
return ~crc == expected_crc;
|
||||
}
|
||||
|
||||
bool ProfileStorage::decode_bank(
|
||||
uint8_t bank, ControllerProfileDatabase* database) const {
|
||||
DatabaseReadContext context{&io_, bank};
|
||||
return controller_profile_database_decode(read_database, &context,
|
||||
database);
|
||||
}
|
||||
|
||||
bool ProfileStorage::payload_matches(
|
||||
uint8_t bank, const ControllerProfileDatabase& database) const {
|
||||
uint8_t stored[PROFILE_STORAGE_PAGE_SIZE]{};
|
||||
uint8_t encoded[PROFILE_STORAGE_PAGE_SIZE]{};
|
||||
for (size_t offset = 0;
|
||||
offset < CONTROLLER_PROFILE_DATABASE_ENCODED_SIZE;
|
||||
offset += sizeof(stored)) {
|
||||
const size_t size =
|
||||
CONTROLLER_PROFILE_DATABASE_ENCODED_SIZE - offset < sizeof(stored)
|
||||
? CONTROLLER_PROFILE_DATABASE_ENCODED_SIZE - offset
|
||||
: sizeof(stored);
|
||||
if (!io_.read(io_.context, bank,
|
||||
PROFILE_STORAGE_RECORD_HEADER_SIZE + offset,
|
||||
stored, size) ||
|
||||
!controller_profile_database_encode_range(database, offset,
|
||||
encoded, size) ||
|
||||
memcmp(stored, encoded, size) != 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
78
profile_storage.h
Normal file
78
profile_storage.h
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "controller_profile.h"
|
||||
|
||||
constexpr uint8_t PROFILE_STORAGE_BANK_COUNT = 2;
|
||||
constexpr size_t PROFILE_STORAGE_SECTOR_SIZE = 4096;
|
||||
constexpr size_t PROFILE_STORAGE_SECTORS_PER_BANK = 5;
|
||||
constexpr size_t PROFILE_STORAGE_BANK_SIZE =
|
||||
PROFILE_STORAGE_SECTOR_SIZE * PROFILE_STORAGE_SECTORS_PER_BANK;
|
||||
constexpr size_t PROFILE_STORAGE_TOTAL_SIZE =
|
||||
PROFILE_STORAGE_BANK_COUNT * PROFILE_STORAGE_BANK_SIZE;
|
||||
constexpr size_t PROFILE_STORAGE_PAGE_SIZE = 256;
|
||||
constexpr size_t PROFILE_STORAGE_RECORD_HEADER_SIZE =
|
||||
PROFILE_STORAGE_PAGE_SIZE;
|
||||
|
||||
static_assert(PROFILE_STORAGE_BANK_SIZE == 20 * 1024);
|
||||
static_assert(PROFILE_STORAGE_TOTAL_SIZE == 40 * 1024);
|
||||
static_assert(PROFILE_STORAGE_RECORD_HEADER_SIZE +
|
||||
CONTROLLER_PROFILE_DATABASE_ENCODED_SIZE <=
|
||||
PROFILE_STORAGE_BANK_SIZE,
|
||||
"profile database does not fit a storage bank");
|
||||
|
||||
enum class ProfileStorageResult : uint8_t {
|
||||
kOk = 0,
|
||||
kUnchanged = 1,
|
||||
kInvalidArgument = 2,
|
||||
kIoError = 3,
|
||||
};
|
||||
|
||||
struct ProfileStorageIo {
|
||||
void* context = nullptr;
|
||||
size_t bank_size = 0;
|
||||
size_t sector_size = 0;
|
||||
size_t page_size = 0;
|
||||
bool (*read)(void* context, uint8_t bank, size_t offset,
|
||||
uint8_t* output, size_t size) = nullptr;
|
||||
bool (*erase_sector)(void* context, uint8_t bank,
|
||||
size_t offset) = nullptr;
|
||||
bool (*program)(void* context, uint8_t bank, size_t offset,
|
||||
const uint8_t* data, size_t size) = nullptr;
|
||||
};
|
||||
|
||||
struct ProfileStorageSnapshot {
|
||||
bool valid = false;
|
||||
uint32_t generation = 0;
|
||||
uint32_t payload_crc = 0;
|
||||
uint8_t active_bank = 0;
|
||||
};
|
||||
|
||||
uint32_t profile_storage_crc32(const uint8_t* data, size_t size);
|
||||
|
||||
class ProfileStorage {
|
||||
public:
|
||||
bool initialize(const ProfileStorageIo& io,
|
||||
ControllerProfileDatabase* database);
|
||||
ProfileStorageResult commit(const ControllerProfileDatabase& database);
|
||||
const ProfileStorageSnapshot& snapshot() const;
|
||||
|
||||
private:
|
||||
struct BankHeader {
|
||||
uint32_t generation = 0;
|
||||
uint32_t payload_crc = 0;
|
||||
};
|
||||
|
||||
bool read_header(uint8_t bank, BankHeader* output) const;
|
||||
bool validate_payload(uint8_t bank, uint32_t expected_crc) const;
|
||||
bool decode_bank(uint8_t bank,
|
||||
ControllerProfileDatabase* database) const;
|
||||
bool payload_matches(uint8_t bank,
|
||||
const ControllerProfileDatabase& database) const;
|
||||
|
||||
ProfileStorageIo io_{};
|
||||
ProfileStorageSnapshot snapshot_{};
|
||||
bool initialized_ = false;
|
||||
};
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -290,8 +290,10 @@ int main() {
|
|||
}
|
||||
for (uint8_t instance = 0;
|
||||
instance < BLUEPAD32_INPUT_BACKEND_SLOT_COUNT; ++instance) {
|
||||
bluepad32_input_backend_snapshot(instance,
|
||||
&g_user_states[instance]);
|
||||
Bluepad32SlotSnapshot snapshot{};
|
||||
bluepad32_input_backend_snapshot(instance, &snapshot);
|
||||
g_user_states[instance] =
|
||||
snapshot.active ? snapshot.state : controller_neutral_state();
|
||||
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
|
||||
bool sent = false;
|
||||
if (adapter_host_probe_mode() == AdapterUsbMode::kXInput) {
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ bool ssp_auto_accept = true;
|
|||
uint8_t accepted_stk_methods = 0xff;
|
||||
uint16_t link_supervision_timeout = 0;
|
||||
btstack_packet_handler_t pairing_event_handler = nullptr;
|
||||
btstack_packet_handler_t identity_event_handler = nullptr;
|
||||
int confirmation_accepts = 0;
|
||||
int confirmation_rejections = 0;
|
||||
int passkey_accepts = 0;
|
||||
|
|
@ -43,6 +44,8 @@ int cyw43_init_calls = 0;
|
|||
int uni_init_calls = 0;
|
||||
int device_disconnect_calls = 0;
|
||||
uni_hid_device_t* last_disconnected_device = nullptr;
|
||||
uni_hid_device_t* lookup_devices[8]{};
|
||||
size_t lookup_device_count = 0;
|
||||
|
||||
struct CoreStopped {};
|
||||
|
||||
|
|
@ -82,11 +85,21 @@ uni_hid_device_t device(
|
|||
result.idx = idx;
|
||||
result.gamepad = gamepad;
|
||||
result.conn.protocol = protocol;
|
||||
result.conn.handle = static_cast<hci_con_handle_t>(0x40 + idx);
|
||||
result.conn.btaddr[5] = static_cast<uint8_t>(idx + 1);
|
||||
result.vendor_id = static_cast<uint16_t>(0x1000 + idx);
|
||||
result.product_id = static_cast<uint16_t>(0x2000 + idx);
|
||||
result.report_parser.play_dual_rumble = play_rumble;
|
||||
return result;
|
||||
}
|
||||
|
||||
void register_lookup_device(uni_hid_device_t* candidate) {
|
||||
require(lookup_device_count <
|
||||
sizeof(lookup_devices) / sizeof(lookup_devices[0]),
|
||||
"test BLE lookup registry overflow");
|
||||
lookup_devices[lookup_device_count++] = candidate;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
|
||||
|
|
@ -98,6 +111,16 @@ int uni_hid_device_get_idx_for_instance(const uni_hid_device_t* device) {
|
|||
return device == nullptr ? -1 : device->idx;
|
||||
}
|
||||
|
||||
uni_hid_device_t* uni_hid_device_get_instance_for_connection_handle(
|
||||
hci_con_handle_t handle) {
|
||||
for (size_t index = 0; index < lookup_device_count; ++index) {
|
||||
if (lookup_devices[index]->conn.handle == handle) {
|
||||
return lookup_devices[index];
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void uni_hid_device_disconnect(uni_hid_device_t* device) {
|
||||
++device_disconnect_calls;
|
||||
last_disconnected_device = device;
|
||||
|
|
@ -226,6 +249,11 @@ void hci_add_event_handler(
|
|||
pairing_event_handler = callback_handler->callback;
|
||||
}
|
||||
|
||||
void sm_add_event_handler(
|
||||
btstack_packet_callback_registration_t* callback_handler) {
|
||||
identity_event_handler = callback_handler->callback;
|
||||
}
|
||||
|
||||
uint8_t hci_event_packet_get_type(const uint8_t* packet) {
|
||||
return packet[0];
|
||||
}
|
||||
|
|
@ -246,6 +274,108 @@ void hci_event_user_passkey_request_get_bd_addr(
|
|||
copy_event_address(packet, address);
|
||||
}
|
||||
|
||||
hci_con_handle_t sm_event_handle(const uint8_t* packet) {
|
||||
return static_cast<hci_con_handle_t>(packet[2]) |
|
||||
static_cast<hci_con_handle_t>(packet[3] << 8);
|
||||
}
|
||||
|
||||
void copy_sm_event_address(const uint8_t* packet, size_t offset,
|
||||
bd_addr_t address) {
|
||||
for (size_t index = 0; index < sizeof(bd_addr_t); ++index) {
|
||||
address[index] = packet[offset + sizeof(bd_addr_t) - 1 - index];
|
||||
}
|
||||
}
|
||||
|
||||
hci_con_handle_t sm_event_identity_resolving_started_get_handle(
|
||||
const uint8_t* packet) {
|
||||
return sm_event_handle(packet);
|
||||
}
|
||||
|
||||
hci_con_handle_t sm_event_identity_resolving_failed_get_handle(
|
||||
const uint8_t* packet) {
|
||||
return sm_event_handle(packet);
|
||||
}
|
||||
|
||||
hci_con_handle_t sm_event_identity_resolving_succeeded_get_handle(
|
||||
const uint8_t* packet) {
|
||||
return sm_event_handle(packet);
|
||||
}
|
||||
|
||||
uint8_t sm_event_identity_resolving_succeeded_get_addr_type(
|
||||
const uint8_t* packet) {
|
||||
return packet[4];
|
||||
}
|
||||
|
||||
void sm_event_identity_resolving_succeeded_get_address(
|
||||
const uint8_t* packet, bd_addr_t address) {
|
||||
copy_sm_event_address(packet, 5, address);
|
||||
}
|
||||
|
||||
uint8_t sm_event_identity_resolving_succeeded_get_identity_addr_type(
|
||||
const uint8_t* packet) {
|
||||
return packet[11];
|
||||
}
|
||||
|
||||
void sm_event_identity_resolving_succeeded_get_identity_address(
|
||||
const uint8_t* packet, bd_addr_t address) {
|
||||
copy_sm_event_address(packet, 12, address);
|
||||
}
|
||||
|
||||
hci_con_handle_t sm_event_identity_created_get_handle(
|
||||
const uint8_t* packet) {
|
||||
return sm_event_handle(packet);
|
||||
}
|
||||
|
||||
void sm_event_identity_created_get_address(
|
||||
const uint8_t* packet, bd_addr_t address) {
|
||||
copy_sm_event_address(packet, 5, address);
|
||||
}
|
||||
|
||||
uint8_t sm_event_identity_created_get_identity_addr_type(
|
||||
const uint8_t* packet) {
|
||||
return packet[11];
|
||||
}
|
||||
|
||||
void sm_event_identity_created_get_identity_address(
|
||||
const uint8_t* packet, bd_addr_t address) {
|
||||
copy_sm_event_address(packet, 12, address);
|
||||
}
|
||||
|
||||
hci_con_handle_t sm_event_reencryption_started_get_handle(
|
||||
const uint8_t* packet) {
|
||||
return sm_event_handle(packet);
|
||||
}
|
||||
|
||||
uint8_t sm_event_reencryption_started_get_addr_type(
|
||||
const uint8_t* packet) {
|
||||
return packet[4];
|
||||
}
|
||||
|
||||
void sm_event_reencryption_started_get_address(
|
||||
const uint8_t* packet, bd_addr_t address) {
|
||||
copy_sm_event_address(packet, 5, address);
|
||||
}
|
||||
|
||||
hci_con_handle_t sm_event_reencryption_complete_get_handle(
|
||||
const uint8_t* packet) {
|
||||
return sm_event_handle(packet);
|
||||
}
|
||||
|
||||
uint8_t sm_event_reencryption_complete_get_addr_type(
|
||||
const uint8_t* packet) {
|
||||
return packet[4];
|
||||
}
|
||||
|
||||
void sm_event_reencryption_complete_get_address(
|
||||
const uint8_t* packet, bd_addr_t address) {
|
||||
copy_sm_event_address(packet, 5, address);
|
||||
}
|
||||
|
||||
uint8_t sm_event_reencryption_complete_get_status(
|
||||
const uint8_t* packet) {
|
||||
return packet[11];
|
||||
}
|
||||
|
||||
|
||||
void uni_platform_set_custom(uni_platform* platform) {
|
||||
installed_platform = platform;
|
||||
|
|
@ -284,10 +414,26 @@ uint32_t btstack_run_loop_get_time_ms() {
|
|||
}
|
||||
|
||||
|
||||
#include "../controller_identity.cpp"
|
||||
#include "../bluepad32_input_backend.cpp"
|
||||
ControllerIdentity observed_profile_identities[8]{};
|
||||
size_t observed_profile_identity_count = 0;
|
||||
void configuration_service_prepare() {}
|
||||
void configuration_service_initialize_on_storage_core() {}
|
||||
void configuration_service_task_on_storage_core(uint32_t) {}
|
||||
void profile_service_prepare() {}
|
||||
void profile_service_initialize_on_storage_core() {}
|
||||
void profile_service_task_on_storage_core(uint32_t) {}
|
||||
bool profile_service_observe_identity_on_storage_core(
|
||||
const ControllerIdentity& identity) {
|
||||
require(observed_profile_identity_count <
|
||||
sizeof(observed_profile_identities) /
|
||||
sizeof(observed_profile_identities[0]),
|
||||
"profile identity observation fixture overflow");
|
||||
observed_profile_identities[observed_profile_identity_count++] =
|
||||
identity;
|
||||
return true;
|
||||
}
|
||||
void configuration_service_snapshot(ConfigurationServiceSnapshot* output) {
|
||||
*output = {};
|
||||
output->state = ConfigurationServiceState::kReady;
|
||||
|
|
@ -319,6 +465,15 @@ SwitchRgbColor switch_pro_get_slot_light_color(uint8_t instance) {
|
|||
|
||||
namespace {
|
||||
|
||||
bool read_controller_state(uint8_t slot, ControllerState* output) {
|
||||
Bluepad32SlotSnapshot snapshot{};
|
||||
bluepad32_input_backend_snapshot(slot, &snapshot);
|
||||
if (output != nullptr) {
|
||||
*output = snapshot.state;
|
||||
}
|
||||
return snapshot.active;
|
||||
}
|
||||
|
||||
void start_backend() {
|
||||
bluepad32_input_backend_init();
|
||||
platform_on_init_complete();
|
||||
|
|
@ -327,8 +482,9 @@ void start_backend() {
|
|||
link_supervision_timeout ==
|
||||
kClassicLinkSupervisionTimeout &&
|
||||
!bondable && accepted_stk_methods == 0 &&
|
||||
!ssp_auto_accept && pairing_event_handler != nullptr,
|
||||
"initialization must configure liveness and pairing policy");
|
||||
!ssp_auto_accept && pairing_event_handler != nullptr &&
|
||||
identity_event_handler != nullptr,
|
||||
"initialization must register Classic and BLE identity policy");
|
||||
}
|
||||
void start_pairing_backend() {
|
||||
start_backend();
|
||||
|
|
@ -344,6 +500,114 @@ void dispatch_pairing_event(uint8_t event_type) {
|
|||
pairing_event_handler(HCI_EVENT_PACKET, 0, packet, sizeof(packet));
|
||||
}
|
||||
|
||||
void write_event_address(uint8_t* packet, size_t offset,
|
||||
const bd_addr_t address) {
|
||||
for (size_t index = 0; index < sizeof(bd_addr_t); ++index) {
|
||||
packet[offset + index] =
|
||||
address[sizeof(bd_addr_t) - 1 - index];
|
||||
}
|
||||
}
|
||||
|
||||
void dispatch_identity_event(uint8_t event_type,
|
||||
const uni_hid_device_t& controller,
|
||||
uint8_t identity_address_type,
|
||||
const bd_addr_t identity_address,
|
||||
uint8_t status = ERROR_CODE_SUCCESS) {
|
||||
uint8_t packet[20]{};
|
||||
size_t packet_size = 0;
|
||||
packet[0] = event_type;
|
||||
packet[2] = static_cast<uint8_t>(controller.conn.handle);
|
||||
packet[3] = static_cast<uint8_t>(controller.conn.handle >> 8);
|
||||
switch (event_type) {
|
||||
case SM_EVENT_IDENTITY_RESOLVING_SUCCEEDED:
|
||||
packet_size = sizeof(packet);
|
||||
packet[4] = BD_ADDR_TYPE_LE_RANDOM;
|
||||
write_event_address(packet, 5, controller.conn.btaddr);
|
||||
packet[11] = identity_address_type;
|
||||
write_event_address(packet, 12, identity_address);
|
||||
break;
|
||||
case SM_EVENT_IDENTITY_CREATED:
|
||||
packet_size = sizeof(packet);
|
||||
packet[4] = identity_address_type;
|
||||
write_event_address(packet, 5, identity_address);
|
||||
packet[11] = identity_address_type;
|
||||
write_event_address(packet, 12, identity_address);
|
||||
break;
|
||||
case SM_EVENT_REENCRYPTION_STARTED:
|
||||
packet_size = 11;
|
||||
packet[4] = identity_address_type;
|
||||
write_event_address(packet, 5, identity_address);
|
||||
break;
|
||||
case SM_EVENT_REENCRYPTION_COMPLETE:
|
||||
packet_size = 12;
|
||||
packet[4] = identity_address_type;
|
||||
write_event_address(packet, 5, identity_address);
|
||||
packet[11] = status;
|
||||
break;
|
||||
default:
|
||||
require(false, "unsupported identity event fixture");
|
||||
}
|
||||
packet[1] = static_cast<uint8_t>(packet_size - 2);
|
||||
identity_event_handler(
|
||||
HCI_EVENT_PACKET, 0, packet,
|
||||
static_cast<uint16_t>(packet_size));
|
||||
}
|
||||
|
||||
void require_identity(const ControllerIdentity& actual, bool stable,
|
||||
ControllerTransport transport, uint8_t address_type,
|
||||
const bd_addr_t address, uint16_t vendor_id,
|
||||
uint16_t product_id, const char* message) {
|
||||
ControllerIdentity expected{};
|
||||
expected.stable = stable;
|
||||
expected.transport = transport;
|
||||
expected.address_type = address_type;
|
||||
memcpy(expected.address, address, sizeof(expected.address));
|
||||
expected.vendor_id = vendor_id;
|
||||
expected.product_id = product_id;
|
||||
require(controller_identity_equal(actual, expected), message);
|
||||
}
|
||||
|
||||
void test_identity_encoding_contract() {
|
||||
ControllerIdentity identity{};
|
||||
identity.stable = true;
|
||||
identity.transport = ControllerTransport::kBle;
|
||||
identity.address_type = BD_ADDR_TYPE_LE_RANDOM_IDENTITY;
|
||||
const bd_addr_t address = {0x10, 0x11, 0x12, 0x13, 0x14, 0x15};
|
||||
memcpy(identity.address, address, sizeof(address));
|
||||
identity.vendor_id = 0x1234;
|
||||
identity.product_id = 0xabcd;
|
||||
|
||||
uint8_t encoded[CONTROLLER_IDENTITY_ENCODED_SIZE]{};
|
||||
const uint8_t expected[CONTROLLER_IDENTITY_ENCODED_SIZE] = {
|
||||
1, 2, 3, 0, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15,
|
||||
0x34, 0x12, 0xcd, 0xab};
|
||||
require(controller_identity_encode(identity, encoded, sizeof(encoded)) &&
|
||||
memcmp(encoded, expected, sizeof(expected)) == 0,
|
||||
"controller identity wire encoding changed");
|
||||
|
||||
ControllerIdentity decoded{};
|
||||
require(controller_identity_decode(encoded, sizeof(encoded), &decoded) &&
|
||||
controller_identity_equal(identity, decoded),
|
||||
"controller identity wire round trip failed");
|
||||
decoded.product_id ^= 1;
|
||||
require(!controller_identity_equal(identity, decoded),
|
||||
"controller identity equality must include every field");
|
||||
encoded[3] = 1;
|
||||
require(!controller_identity_decode(encoded, sizeof(encoded), &decoded),
|
||||
"controller identity decoder must reject a nonzero reserved byte");
|
||||
|
||||
const ControllerIdentity global = controller_identity_global();
|
||||
memset(encoded, 0xff, sizeof(encoded));
|
||||
require(controller_identity_is_global(global) &&
|
||||
controller_identity_encode(global, encoded,
|
||||
sizeof(encoded)),
|
||||
"global identity helper must produce an encodable fallback");
|
||||
for (uint8_t byte : encoded) {
|
||||
require(byte == 0,
|
||||
"global fallback identity must encode as all zeroes");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -376,7 +640,7 @@ void test_ready_order(bool reverse) {
|
|||
for (int ready = 0; ready <= position; ++ready) {
|
||||
expected_active = expected_active || order[ready] == candidate;
|
||||
}
|
||||
require(bluepad32_input_backend_snapshot(candidate, &snapshot) ==
|
||||
require(read_controller_state(candidate, &snapshot) ==
|
||||
expected_active,
|
||||
"only ready indexed slots may become active");
|
||||
}
|
||||
|
|
@ -403,7 +667,7 @@ void test_ready_order(bool reverse) {
|
|||
|
||||
for (int candidate = 0; candidate < kSlotCount; ++candidate) {
|
||||
ControllerState snapshot{};
|
||||
require(bluepad32_input_backend_snapshot(candidate, &snapshot) ==
|
||||
require(read_controller_state(candidate, &snapshot) ==
|
||||
(candidate != slot),
|
||||
"disconnect must preserve every surviving slot");
|
||||
}
|
||||
|
|
@ -444,7 +708,7 @@ void test_rejections() {
|
|||
collision_data.gamepad.buttons = BUTTON_B;
|
||||
platform_on_controller_data(&collision, &collision_data);
|
||||
ControllerState snapshot{};
|
||||
require(bluepad32_input_backend_snapshot(0, &snapshot),
|
||||
require(read_controller_state(0, &snapshot),
|
||||
"occupied slot must stay active");
|
||||
require(!snapshot.button_east,
|
||||
"mismatched device input must not enter the occupied slot");
|
||||
|
|
@ -453,13 +717,13 @@ void test_rejections() {
|
|||
slot_zero_data.klass = UNI_CONTROLLER_CLASS_GAMEPAD;
|
||||
slot_zero_data.gamepad.accel[0] = 8192;
|
||||
platform_on_controller_data(&slot_zero, &slot_zero_data);
|
||||
require(bluepad32_input_backend_snapshot(0, &snapshot) &&
|
||||
require(read_controller_state(0, &snapshot) &&
|
||||
snapshot.motion_sample_count == 3,
|
||||
"valid slot input must remain observable");
|
||||
require(!bluepad32_input_backend_snapshot(4, &snapshot),
|
||||
require(!read_controller_state(4, &snapshot),
|
||||
"public snapshot must reject slot 4");
|
||||
bluepad32_input_backend_report_sent(4);
|
||||
require(bluepad32_input_backend_snapshot(0, &snapshot) &&
|
||||
require(read_controller_state(0, &snapshot) &&
|
||||
snapshot.motion_sample_count == 3,
|
||||
"slot 4 acknowledgement must not consume slot 0 IMU");
|
||||
bluepad32_input_backend_queue_rumble(4, ControllerRumbleOutput{1, 2});
|
||||
|
|
@ -469,6 +733,7 @@ void test_rejections() {
|
|||
}
|
||||
|
||||
void test_independent_lifecycle() {
|
||||
test_identity_encoding_contract();
|
||||
start_pairing_backend();
|
||||
|
||||
uni_hid_device_t aborted = device(0);
|
||||
|
|
@ -500,7 +765,40 @@ void test_independent_lifecycle() {
|
|||
"pre-ready disconnect must restart Classic and BLE scans");
|
||||
|
||||
uni_hid_device_t devices[kSlotCount] = {
|
||||
device(0), device(1), device(2), device(3)};
|
||||
device(0, true, UNI_BT_CONN_PROTOCOL_BR_EDR),
|
||||
device(1, true, UNI_BT_CONN_PROTOCOL_BLE),
|
||||
device(2, true, UNI_BT_CONN_PROTOCOL_BLE),
|
||||
device(3, true, UNI_BT_CONN_PROTOCOL_BLE)};
|
||||
const bd_addr_t classic_address =
|
||||
{0x10, 0x11, 0x12, 0x13, 0x14, 0x15};
|
||||
const bd_addr_t resolved_connection_address =
|
||||
{0x41, 0x21, 0x22, 0x23, 0x24, 0x25};
|
||||
const bd_addr_t created_connection_address =
|
||||
{0x42, 0x31, 0x32, 0x33, 0x34, 0x35};
|
||||
const bd_addr_t reencrypted_connection_address =
|
||||
{0x43, 0x41, 0x42, 0x43, 0x44, 0x45};
|
||||
memcpy(devices[0].conn.btaddr, classic_address,
|
||||
sizeof(classic_address));
|
||||
memcpy(devices[1].conn.btaddr, resolved_connection_address,
|
||||
sizeof(resolved_connection_address));
|
||||
memcpy(devices[2].conn.btaddr, created_connection_address,
|
||||
sizeof(created_connection_address));
|
||||
memcpy(devices[3].conn.btaddr, reencrypted_connection_address,
|
||||
sizeof(reencrypted_connection_address));
|
||||
const bd_addr_t resolved_address =
|
||||
{0x20, 0x21, 0x22, 0x23, 0x24, 0x25};
|
||||
const bd_addr_t reencrypted_address =
|
||||
{0x30, 0x31, 0x32, 0x33, 0x34, 0x35};
|
||||
dispatch_identity_event(
|
||||
SM_EVENT_IDENTITY_RESOLVING_SUCCEEDED, devices[1],
|
||||
BD_ADDR_TYPE_LE_PUBLIC, resolved_address);
|
||||
register_lookup_device(&devices[3]);
|
||||
dispatch_identity_event(
|
||||
SM_EVENT_REENCRYPTION_STARTED, devices[3],
|
||||
BD_ADDR_TYPE_LE_RANDOM, reencrypted_address);
|
||||
dispatch_identity_event(
|
||||
SM_EVENT_REENCRYPTION_COMPLETE, devices[3],
|
||||
BD_ADDR_TYPE_LE_RANDOM, reencrypted_address);
|
||||
for (int slot = 0; slot < kSlotCount; ++slot) {
|
||||
platform_on_device_connected(&devices[slot]);
|
||||
require(g_slots[slot].device == &devices[slot] &&
|
||||
|
|
@ -539,6 +837,73 @@ void test_independent_lifecycle() {
|
|||
!scanning_enabled && !incoming_connections,
|
||||
"four ready lifecycle devices must stop connection policy");
|
||||
|
||||
Bluepad32SlotSnapshot lifecycle_snapshots[kSlotCount]{};
|
||||
uint32_t baseline_connection_generations[kSlotCount]{};
|
||||
for (int slot = 0; slot < kSlotCount; ++slot) {
|
||||
bluepad32_input_backend_snapshot(
|
||||
static_cast<uint8_t>(slot), &lifecycle_snapshots[slot]);
|
||||
require(lifecycle_snapshots[slot].active,
|
||||
"ready slot snapshot must publish active state");
|
||||
baseline_connection_generations[slot] =
|
||||
lifecycle_snapshots[slot].connection_generation;
|
||||
}
|
||||
require_identity(
|
||||
lifecycle_snapshots[0].identity, true,
|
||||
ControllerTransport::kClassic, BD_ADDR_TYPE_UNKNOWN,
|
||||
devices[0].conn.btaddr, devices[0].vendor_id,
|
||||
devices[0].product_id,
|
||||
"Classic snapshot identity must use the connected device address");
|
||||
require_identity(
|
||||
lifecycle_snapshots[1].identity, true,
|
||||
ControllerTransport::kBle, BD_ADDR_TYPE_LE_PUBLIC,
|
||||
resolved_address, devices[1].vendor_id, devices[1].product_id,
|
||||
"resolved BLE snapshot must use the stable identity address");
|
||||
require(controller_identity_is_global(
|
||||
lifecycle_snapshots[2].identity),
|
||||
"unresolved BLE snapshot must use the global unstable identity");
|
||||
require_identity(
|
||||
lifecycle_snapshots[3].identity, true,
|
||||
ControllerTransport::kBle, BD_ADDR_TYPE_LE_RANDOM,
|
||||
reencrypted_address, devices[3].vendor_id,
|
||||
devices[3].product_id,
|
||||
"reencrypted BLE snapshot must use the bonded identity address");
|
||||
require(observed_profile_identity_count == 3 &&
|
||||
controller_identity_equal(
|
||||
observed_profile_identities[0],
|
||||
lifecycle_snapshots[1].identity) &&
|
||||
controller_identity_equal(
|
||||
observed_profile_identities[1],
|
||||
lifecycle_snapshots[3].identity) &&
|
||||
controller_identity_equal(
|
||||
observed_profile_identities[2],
|
||||
lifecycle_snapshots[0].identity),
|
||||
"only stable ready identities must be enrolled for profiles");
|
||||
require(baseline_connection_generations[0] ==
|
||||
first_pending_generation + 1 &&
|
||||
baseline_connection_generations[1] ==
|
||||
baseline_connection_generations[2] &&
|
||||
baseline_connection_generations[2] ==
|
||||
baseline_connection_generations[3],
|
||||
"connection generations must isolate each slot lifecycle");
|
||||
|
||||
const bd_addr_t created_address =
|
||||
{0x40, 0x41, 0x42, 0x43, 0x44, 0x45};
|
||||
register_lookup_device(&devices[2]);
|
||||
dispatch_identity_event(
|
||||
SM_EVENT_IDENTITY_CREATED, devices[2],
|
||||
BD_ADDR_TYPE_LE_RANDOM, created_address);
|
||||
bluepad32_input_backend_snapshot(2, &lifecycle_snapshots[2]);
|
||||
require_identity(
|
||||
lifecycle_snapshots[2].identity, true,
|
||||
ControllerTransport::kBle, BD_ADDR_TYPE_LE_RANDOM,
|
||||
created_address, devices[2].vendor_id, devices[2].product_id,
|
||||
"new BLE identity event must update an active slot snapshot");
|
||||
require(observed_profile_identity_count == 4 &&
|
||||
controller_identity_equal(
|
||||
observed_profile_identities[3],
|
||||
lifecycle_snapshots[2].identity),
|
||||
"late BLE identity creation must enroll the stable identity");
|
||||
|
||||
const uint32_t buttons[kSlotCount] = {
|
||||
BUTTON_B, BUTTON_A, BUTTON_X, BUTTON_Y};
|
||||
uni_controller_t data[kSlotCount]{};
|
||||
|
|
@ -552,7 +917,7 @@ void test_independent_lifecycle() {
|
|||
|
||||
ControllerState states[kSlotCount]{};
|
||||
for (int slot = 0; slot < kSlotCount; ++slot) {
|
||||
require(bluepad32_input_backend_snapshot(slot, &states[slot]) &&
|
||||
require(read_controller_state(slot, &states[slot]) &&
|
||||
states[slot].motion_sample_count == 3,
|
||||
"every slot must expose independent input and IMU");
|
||||
}
|
||||
|
|
@ -571,13 +936,13 @@ void test_independent_lifecycle() {
|
|||
|
||||
bluepad32_input_backend_report_sent(3);
|
||||
for (int slot = 0; slot < kSlotCount; ++slot) {
|
||||
require(bluepad32_input_backend_snapshot(slot, &states[slot]) &&
|
||||
require(read_controller_state(slot, &states[slot]) &&
|
||||
states[slot].motion_sample_count == (slot == 3 ? 0 : 3),
|
||||
"slot 3 acknowledgement must not consume slots 0-2 IMU");
|
||||
}
|
||||
for (int slot = 0; slot < 3; ++slot) {
|
||||
bluepad32_input_backend_report_sent(slot);
|
||||
require(bluepad32_input_backend_snapshot(slot, &states[slot]) &&
|
||||
require(read_controller_state(slot, &states[slot]) &&
|
||||
states[slot].motion_sample_count == 0,
|
||||
"each slot acknowledgement must consume only its own IMU");
|
||||
}
|
||||
|
|
@ -605,24 +970,43 @@ void test_independent_lifecycle() {
|
|||
|
||||
bluepad32_input_backend_queue_rumble(3, ControllerRumbleOutput{55, 66});
|
||||
const uint32_t disconnected_generation =
|
||||
g_slots[3].connection_generation;
|
||||
baseline_connection_generations[3];
|
||||
const int starts_before_slot_three_disconnect = scan_starts;
|
||||
platform_on_device_disconnected(&devices[3]);
|
||||
require(scan_starts == starts_before_slot_three_disconnect + 1 &&
|
||||
scanning_enabled && incoming_connections,
|
||||
"slot 3 disconnect must resume scanning and incoming connections");
|
||||
require(!bluepad32_input_backend_snapshot(3, &states[3]) &&
|
||||
require(!read_controller_state(3, &states[3]) &&
|
||||
!states[3].button_north && states[3].left_stick_x == 0,
|
||||
"slot 3 disconnect must publish protocol-neutral state");
|
||||
require(bluepad32_input_backend_snapshot(0, &states[0]) &&
|
||||
bluepad32_input_backend_snapshot(3, &lifecycle_snapshots[3]);
|
||||
require(!lifecycle_snapshots[3].active &&
|
||||
lifecycle_snapshots[3].connection_generation ==
|
||||
disconnected_generation + 1 &&
|
||||
controller_identity_is_global(
|
||||
lifecycle_snapshots[3].identity) &&
|
||||
!lifecycle_snapshots[3].state.button_north &&
|
||||
lifecycle_snapshots[3].state.left_stick_x == 0,
|
||||
"disconnect snapshot must atomically publish neutral state, "
|
||||
"cleared identity, and a new connection generation");
|
||||
for (int survivor = 0; survivor < 3; ++survivor) {
|
||||
bluepad32_input_backend_snapshot(
|
||||
static_cast<uint8_t>(survivor),
|
||||
&lifecycle_snapshots[survivor]);
|
||||
require(lifecycle_snapshots[survivor].active &&
|
||||
lifecycle_snapshots[survivor].connection_generation ==
|
||||
baseline_connection_generations[survivor],
|
||||
"disconnect generation must not leak into surviving slots");
|
||||
}
|
||||
require(read_controller_state(0, &states[0]) &&
|
||||
states[0].button_east &&
|
||||
bluepad32_input_backend_snapshot(1, &states[1]) &&
|
||||
read_controller_state(1, &states[1]) &&
|
||||
states[1].button_south &&
|
||||
bluepad32_input_backend_snapshot(2, &states[2]) &&
|
||||
read_controller_state(2, &states[2]) &&
|
||||
states[2].button_west,
|
||||
"slot 3 disconnect must preserve slots 0-2");
|
||||
platform_on_controller_data(&devices[0], &data[0]);
|
||||
require(bluepad32_input_backend_snapshot(0, &states[0]) &&
|
||||
require(read_controller_state(0, &states[0]) &&
|
||||
states[0].button_east,
|
||||
"slot 0 input must continue while slot 3 is disconnected");
|
||||
const int slot_zero_calls_while_scanning = devices[0].rumble_calls;
|
||||
|
|
@ -633,13 +1017,26 @@ void test_independent_lifecycle() {
|
|||
devices[0].last_high == 116,
|
||||
"slot 0 rumble must continue while slot 3 is disconnected");
|
||||
|
||||
uni_hid_device_t slot_three_replacement = device(3);
|
||||
uni_hid_device_t slot_three_replacement =
|
||||
device(3, true, UNI_BT_CONN_PROTOCOL_BLE);
|
||||
memcpy(slot_three_replacement.conn.btaddr,
|
||||
devices[3].conn.btaddr, sizeof(devices[3].conn.btaddr));
|
||||
require(platform_on_device_ready(&slot_three_replacement) ==
|
||||
UNI_ERROR_SUCCESS,
|
||||
"slot 3 replacement must bind to the freed indexed slot");
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(slot_three_replacement.rumble_calls == 0,
|
||||
"slot 3 replacement must not receive disconnected device rumble");
|
||||
bluepad32_input_backend_snapshot(3, &lifecycle_snapshots[3]);
|
||||
require(lifecycle_snapshots[3].active &&
|
||||
lifecycle_snapshots[3].connection_generation ==
|
||||
disconnected_generation + 1 &&
|
||||
controller_identity_is_global(
|
||||
lifecycle_snapshots[3].identity),
|
||||
"replacement must keep the new generation and cannot inherit "
|
||||
"the disconnected BLE identity");
|
||||
require(observed_profile_identity_count == 4,
|
||||
"unstable replacement must not be enrolled for profiles");
|
||||
|
||||
g_slots[3].pending_rumble = {
|
||||
3, disconnected_generation, ControllerRumbleOutput{77, 88}};
|
||||
|
|
@ -653,14 +1050,14 @@ void test_independent_lifecycle() {
|
|||
replacement_data.gamepad.buttons = BUTTON_Y;
|
||||
replacement_data.gamepad.accel[0] = 9000;
|
||||
platform_on_controller_data(&slot_three_replacement, &replacement_data);
|
||||
require(bluepad32_input_backend_snapshot(3, &states[3]) &&
|
||||
require(read_controller_state(3, &states[3]) &&
|
||||
states[3].button_north && states[3].motion_sample_count == 3,
|
||||
"replacement input and IMU must populate only slot 3");
|
||||
require(bluepad32_input_backend_snapshot(0, &states[0]) &&
|
||||
require(read_controller_state(0, &states[0]) &&
|
||||
states[0].button_east &&
|
||||
bluepad32_input_backend_snapshot(1, &states[1]) &&
|
||||
read_controller_state(1, &states[1]) &&
|
||||
states[1].button_south &&
|
||||
bluepad32_input_backend_snapshot(2, &states[2]) &&
|
||||
read_controller_state(2, &states[2]) &&
|
||||
states[2].button_west,
|
||||
"slot 3 replacement must not disturb slots 0-2");
|
||||
|
||||
|
|
@ -706,15 +1103,14 @@ void test_independent_lifecycle() {
|
|||
require(scan_starts == starts_before_disconnect + 1 &&
|
||||
scanning_enabled && incoming_connections,
|
||||
"disconnecting slots 0-2 must resume connection policy");
|
||||
require(!bluepad32_input_backend_snapshot(slot, &states[slot]) &&
|
||||
require(!read_controller_state(slot, &states[slot]) &&
|
||||
states[slot].left_stick_x == 0,
|
||||
"disconnect must publish protocol-neutral state");
|
||||
for (int survivor = 0; survivor < kSlotCount; ++survivor) {
|
||||
if (survivor == slot) {
|
||||
continue;
|
||||
}
|
||||
require(bluepad32_input_backend_snapshot(survivor,
|
||||
&states[survivor]),
|
||||
require(read_controller_state(survivor, &states[survivor]),
|
||||
"disconnect must preserve all three survivors");
|
||||
}
|
||||
require(platform_on_device_ready(&replacements[slot]) ==
|
||||
|
|
@ -898,7 +1294,7 @@ void test_abxy_hotkey() {
|
|||
input.gamepad.buttons = BUTTON_A;
|
||||
platform_on_controller_data(&slot_zero, &input);
|
||||
ControllerState snapshot{};
|
||||
require(bluepad32_input_backend_snapshot(0, &snapshot),
|
||||
require(read_controller_state(0, &snapshot),
|
||||
"slot 0 ABXY state was not published");
|
||||
require_south_button_mapping(
|
||||
snapshot, kDefaultSwapAbxy,
|
||||
|
|
@ -908,7 +1304,7 @@ void test_abxy_hotkey() {
|
|||
kAbxyHotkeyButtonMask | BUTTON_A;
|
||||
input.gamepad.misc_buttons = kAbxyHotkeyMiscMask;
|
||||
platform_on_controller_data(&slot_zero, &input);
|
||||
require(bluepad32_input_backend_snapshot(0, &snapshot),
|
||||
require(read_controller_state(0, &snapshot),
|
||||
"toggled slot 0 state was not published");
|
||||
require_south_button_mapping(
|
||||
snapshot, !kDefaultSwapAbxy,
|
||||
|
|
@ -960,7 +1356,7 @@ void test_abxy_hotkey() {
|
|||
peer_input.klass = UNI_CONTROLLER_CLASS_GAMEPAD;
|
||||
peer_input.gamepad.buttons = BUTTON_A;
|
||||
platform_on_controller_data(&slot_one, &peer_input);
|
||||
require(bluepad32_input_backend_snapshot(1, &snapshot),
|
||||
require(read_controller_state(1, &snapshot),
|
||||
"slot 1 ABXY state was not published");
|
||||
require_south_button_mapping(
|
||||
snapshot, kDefaultSwapAbxy,
|
||||
|
|
@ -988,7 +1384,7 @@ void test_motion_hotkey() {
|
|||
input.gamepad.accel[0] = 8192;
|
||||
platform_on_controller_data(&slot_zero, &input);
|
||||
ControllerState snapshot{};
|
||||
require(bluepad32_input_backend_snapshot(0, &snapshot) &&
|
||||
require(read_controller_state(0, &snapshot) &&
|
||||
snapshot.motion_sample_count ==
|
||||
(kDefaultMotionEnabled ? 3 : 0),
|
||||
"slot 0 did not start with configured motion state");
|
||||
|
|
@ -997,7 +1393,7 @@ void test_motion_hotkey() {
|
|||
input.gamepad.buttons = kMotionHotkeyButtonMask;
|
||||
input.gamepad.misc_buttons = kMotionHotkeyMiscMask;
|
||||
platform_on_controller_data(&slot_zero, &input);
|
||||
require(bluepad32_input_backend_snapshot(0, &snapshot) &&
|
||||
require(read_controller_state(0, &snapshot) &&
|
||||
snapshot.motion_sample_count ==
|
||||
(kDefaultMotionEnabled ? 0 : 3) &&
|
||||
!snapshot.dpad_up && !snapshot.button_right_shoulder &&
|
||||
|
|
@ -1030,7 +1426,7 @@ void test_motion_hotkey() {
|
|||
peer_input.klass = UNI_CONTROLLER_CLASS_GAMEPAD;
|
||||
peer_input.gamepad.accel[0] = 8192;
|
||||
platform_on_controller_data(&slot_one, &peer_input);
|
||||
require(bluepad32_input_backend_snapshot(1, &snapshot) &&
|
||||
require(read_controller_state(1, &snapshot) &&
|
||||
snapshot.motion_sample_count ==
|
||||
(kDefaultMotionEnabled ? 3 : 0),
|
||||
"slot 0 motion chord changed slot 1 motion state");
|
||||
|
|
@ -1042,7 +1438,7 @@ void test_motion_hotkey() {
|
|||
input.gamepad.buttons = kMotionHotkeyButtonMask;
|
||||
input.gamepad.misc_buttons = kMotionHotkeyMiscMask;
|
||||
platform_on_controller_data(&slot_zero, &input);
|
||||
require(bluepad32_input_backend_snapshot(0, &snapshot) &&
|
||||
require(read_controller_state(0, &snapshot) &&
|
||||
snapshot.motion_sample_count ==
|
||||
(kDefaultMotionEnabled ? 3 : 0),
|
||||
"released motion chord did not re-arm or restore motion");
|
||||
|
|
@ -1078,7 +1474,7 @@ void test_protocol_neutral_analog_state() {
|
|||
platform_on_controller_data(&controller, &input);
|
||||
|
||||
ControllerState state{};
|
||||
require(bluepad32_input_backend_snapshot(0, &state),
|
||||
require(read_controller_state(0, &state),
|
||||
"analog state was not published");
|
||||
require(state.left_stick_x == INT16_MIN &&
|
||||
state.left_stick_y == 0 &&
|
||||
|
|
@ -1096,7 +1492,7 @@ void test_protocol_neutral_analog_state() {
|
|||
input.gamepad.buttons =
|
||||
BUTTON_TRIGGER_L | BUTTON_TRIGGER_R;
|
||||
platform_on_controller_data(&controller, &input);
|
||||
require(bluepad32_input_backend_snapshot(0, &state) &&
|
||||
require(read_controller_state(0, &state) &&
|
||||
state.left_trigger == UINT16_MAX &&
|
||||
state.right_trigger == UINT16_MAX,
|
||||
"digital trigger buttons did not map to full analog range");
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ typedef uint8_t bd_addr_t[6];
|
|||
typedef uint8_t link_key_t[16];
|
||||
typedef uint8_t sm_key_t[16];
|
||||
typedef int link_key_type_t;
|
||||
typedef uint16_t hci_con_handle_t;
|
||||
|
||||
enum bd_addr_type_t {
|
||||
BD_ADDR_TYPE_LE_PUBLIC = 0,
|
||||
|
|
@ -29,6 +30,12 @@ enum {
|
|||
HCI_EVENT_PACKET = 4,
|
||||
HCI_EVENT_USER_CONFIRMATION_REQUEST = 0x33,
|
||||
HCI_EVENT_USER_PASSKEY_REQUEST = 0x34,
|
||||
SM_EVENT_IDENTITY_RESOLVING_STARTED = 0xcd,
|
||||
SM_EVENT_IDENTITY_RESOLVING_FAILED = 0xce,
|
||||
SM_EVENT_IDENTITY_RESOLVING_SUCCEEDED = 0xcf,
|
||||
SM_EVENT_IDENTITY_CREATED = 0xd3,
|
||||
SM_EVENT_REENCRYPTION_STARTED = 0xd6,
|
||||
SM_EVENT_REENCRYPTION_COMPLETE = 0xd7,
|
||||
SM_STK_GENERATION_METHOD_JUST_WORKS = 0x01,
|
||||
SM_STK_GENERATION_METHOD_OOB = 0x02,
|
||||
SM_STK_GENERATION_METHOD_PASSKEY = 0x04,
|
||||
|
|
@ -114,10 +121,13 @@ enum uni_bt_conn_protocol_t {
|
|||
|
||||
struct uni_bt_conn_t {
|
||||
bd_addr_t btaddr;
|
||||
hci_con_handle_t handle;
|
||||
uni_bt_conn_protocol_t protocol;
|
||||
};
|
||||
|
||||
struct uni_hid_device_t {
|
||||
uint16_t vendor_id;
|
||||
uint16_t product_id;
|
||||
uni_bt_conn_t conn;
|
||||
int idx;
|
||||
bool gamepad;
|
||||
|
|
@ -154,6 +164,8 @@ struct uni_platform {
|
|||
bool uni_hid_device_is_gamepad(const uni_hid_device_t* device);
|
||||
int uni_hid_device_get_idx_for_instance(const uni_hid_device_t* device);
|
||||
void uni_hid_device_disconnect(uni_hid_device_t* device);
|
||||
uni_hid_device_t* uni_hid_device_get_instance_for_connection_handle(
|
||||
hci_con_handle_t handle);
|
||||
void uni_bt_allow_incoming_connections(bool enabled);
|
||||
void uni_bt_start_scanning_and_autoconnect_unsafe();
|
||||
void uni_bt_stop_scanning_unsafe();
|
||||
|
|
@ -181,10 +193,48 @@ int gap_ssp_passkey_response(const bd_addr_t address, uint32_t passkey);
|
|||
int gap_ssp_passkey_negative(const bd_addr_t address);
|
||||
void hci_add_event_handler(
|
||||
btstack_packet_callback_registration_t* callback_handler);
|
||||
void sm_add_event_handler(
|
||||
btstack_packet_callback_registration_t* callback_handler);
|
||||
uint8_t hci_event_packet_get_type(const uint8_t* packet);
|
||||
void hci_event_user_confirmation_request_get_bd_addr(
|
||||
const uint8_t* packet, bd_addr_t address);
|
||||
void hci_event_user_passkey_request_get_bd_addr(
|
||||
const uint8_t* packet, bd_addr_t address);
|
||||
hci_con_handle_t sm_event_identity_resolving_started_get_handle(
|
||||
const uint8_t* packet);
|
||||
hci_con_handle_t sm_event_identity_resolving_failed_get_handle(
|
||||
const uint8_t* packet);
|
||||
hci_con_handle_t sm_event_identity_resolving_succeeded_get_handle(
|
||||
const uint8_t* packet);
|
||||
uint8_t sm_event_identity_resolving_succeeded_get_addr_type(
|
||||
const uint8_t* packet);
|
||||
void sm_event_identity_resolving_succeeded_get_address(
|
||||
const uint8_t* packet, bd_addr_t address);
|
||||
uint8_t sm_event_identity_resolving_succeeded_get_identity_addr_type(
|
||||
const uint8_t* packet);
|
||||
void sm_event_identity_resolving_succeeded_get_identity_address(
|
||||
const uint8_t* packet, bd_addr_t address);
|
||||
hci_con_handle_t sm_event_identity_created_get_handle(
|
||||
const uint8_t* packet);
|
||||
void sm_event_identity_created_get_address(
|
||||
const uint8_t* packet, bd_addr_t address);
|
||||
uint8_t sm_event_identity_created_get_identity_addr_type(
|
||||
const uint8_t* packet);
|
||||
void sm_event_identity_created_get_identity_address(
|
||||
const uint8_t* packet, bd_addr_t address);
|
||||
hci_con_handle_t sm_event_reencryption_started_get_handle(
|
||||
const uint8_t* packet);
|
||||
uint8_t sm_event_reencryption_started_get_addr_type(
|
||||
const uint8_t* packet);
|
||||
void sm_event_reencryption_started_get_address(
|
||||
const uint8_t* packet, bd_addr_t address);
|
||||
hci_con_handle_t sm_event_reencryption_complete_get_handle(
|
||||
const uint8_t* packet);
|
||||
uint8_t sm_event_reencryption_complete_get_addr_type(
|
||||
const uint8_t* packet);
|
||||
void sm_event_reencryption_complete_get_address(
|
||||
const uint8_t* packet, bd_addr_t address);
|
||||
uint8_t sm_event_reencryption_complete_get_status(
|
||||
const uint8_t* packet);
|
||||
void uni_platform_set_custom(uni_platform* platform);
|
||||
int uni_init(int argc, const char** argv);
|
||||
|
|
|
|||
162
tests/controller_profile_test.cpp
Normal file
162
tests/controller_profile_test.cpp
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
#include "controller_identity.h"
|
||||
#include "controller_profile.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
|
||||
namespace {
|
||||
|
||||
uint8_t encoded_database[CONTROLLER_PROFILE_DATABASE_ENCODED_SIZE]{};
|
||||
ControllerProfileDatabase database{};
|
||||
ControllerProfileDatabase decoded_database{};
|
||||
|
||||
void require(bool condition, const char* message) {
|
||||
if (!condition) {
|
||||
std::cerr << message << '\n';
|
||||
std::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
ControllerIdentity identity(uint8_t suffix) {
|
||||
ControllerIdentity value{};
|
||||
value.stable = true;
|
||||
value.transport = ControllerTransport::kClassic;
|
||||
value.address[5] = suffix;
|
||||
value.vendor_id = 0x057e;
|
||||
value.product_id = static_cast<uint16_t>(0x2000u + suffix);
|
||||
return value;
|
||||
}
|
||||
|
||||
bool read_encoded_database(void*, size_t offset, uint8_t* output,
|
||||
size_t size) {
|
||||
if (offset > sizeof(encoded_database) ||
|
||||
size > sizeof(encoded_database) - offset) {
|
||||
return false;
|
||||
}
|
||||
memcpy(output, &encoded_database[offset], size);
|
||||
return true;
|
||||
}
|
||||
|
||||
void test_profile_wire_schema() {
|
||||
const ControllerProfile profile =
|
||||
controller_profile_default(controller_identity_global(), 0);
|
||||
uint8_t encoded[CONTROLLER_PROFILE_ENCODED_SIZE]{};
|
||||
require(controller_profile_encode(profile, encoded, sizeof(encoded)),
|
||||
"default profile did not encode");
|
||||
require(encoded[0] == 1 && encoded[1] == 0 &&
|
||||
encoded[2] == 0 && encoded[3] == 1,
|
||||
"profile header is not little-endian v1/256");
|
||||
for (uint8_t index = 0;
|
||||
index < CONTROLLER_PROFILE_LOGICAL_BUTTON_COUNT; ++index) {
|
||||
require(encoded[4 + index] == index,
|
||||
"default direct mapping is not identity");
|
||||
}
|
||||
require(encoded[26] == 0xff && encoded[27] == 0x7f &&
|
||||
encoded[30] == 0,
|
||||
"default stick encoding changed");
|
||||
require(encoded[54] == 0xff && encoded[55] == 0xff &&
|
||||
encoded[58] == 0x00 && encoded[59] == 0x80,
|
||||
"default trigger encoding changed");
|
||||
require(encoded[72] == 0xff && encoded[73] == 0xff &&
|
||||
encoded[74] == 3 && encoded[78] == 0xff &&
|
||||
encoded[79] == 0xff && encoded[80] == 1,
|
||||
"default rumble or macro encoding changed");
|
||||
for (uint8_t index = 0;
|
||||
index < CONTROLLER_PROFILE_MACRO_STEP_CAPACITY; ++index) {
|
||||
require(encoded[100 + index * 19] == 1,
|
||||
"unused macro step is not canonical end");
|
||||
}
|
||||
|
||||
ControllerProfile decoded{};
|
||||
require(controller_profile_decode(encoded, sizeof(encoded), &decoded),
|
||||
"default profile did not decode");
|
||||
encoded[252] = 1;
|
||||
require(!controller_profile_decode(encoded, sizeof(encoded), &decoded),
|
||||
"nonzero reserved profile byte was accepted");
|
||||
|
||||
ControllerProfile invalid = profile;
|
||||
invalid.button_map[0] = 16;
|
||||
require(!controller_profile_validate(invalid),
|
||||
"invalid direct output was accepted");
|
||||
invalid = profile;
|
||||
invalid.sticks[0].inner_deadzone =
|
||||
invalid.sticks[0].outer_saturation;
|
||||
require(!controller_profile_validate(invalid),
|
||||
"empty stick range was accepted");
|
||||
invalid = profile;
|
||||
invalid.triggers[0].digital_threshold = 0;
|
||||
invalid.triggers[0].lower_deadzone = 1;
|
||||
require(!controller_profile_validate(invalid),
|
||||
"trigger threshold outside its range was accepted");
|
||||
invalid = profile;
|
||||
invalid.turbo_modes[0] =
|
||||
static_cast<ControllerProfileTurboMode>(3);
|
||||
require(!controller_profile_validate(invalid),
|
||||
"invalid Turbo mode was accepted");
|
||||
invalid = profile;
|
||||
invalid.macro_step_count = 2;
|
||||
invalid.macro_steps[0].type =
|
||||
ControllerProfileMacroStepType::kState;
|
||||
invalid.macro_steps[0].duration_ms =
|
||||
CONTROLLER_PROFILE_MAX_WAIT_MS + 1;
|
||||
require(!controller_profile_validate(invalid),
|
||||
"unbounded macro wait was accepted");
|
||||
invalid.macro_steps[0].duration_ms =
|
||||
CONTROLLER_PROFILE_MAX_WAIT_MS;
|
||||
invalid.macro_steps[1].type =
|
||||
ControllerProfileMacroStepType::kState;
|
||||
require(!controller_profile_validate(invalid),
|
||||
"macro without a final end was accepted");
|
||||
}
|
||||
|
||||
void test_database_round_trip_and_capacity() {
|
||||
controller_profile_database_default(&database);
|
||||
for (uint8_t index = 0;
|
||||
index < CONTROLLER_PROFILE_STABLE_IDENTITY_CAPACITY; ++index) {
|
||||
ControllerProfileDatabaseEntry* entry = nullptr;
|
||||
require(controller_profile_database_ensure(
|
||||
&database, identity(static_cast<uint8_t>(index + 1)),
|
||||
&entry) == ControllerProfileDatabaseResult::kOk &&
|
||||
entry != nullptr,
|
||||
"stable identity was not added");
|
||||
entry->active_profile = index % CONTROLLER_PROFILE_COUNT;
|
||||
}
|
||||
ControllerProfileDatabaseEntry* rejected = nullptr;
|
||||
require(controller_profile_database_ensure(
|
||||
&database, identity(99), &rejected) ==
|
||||
ControllerProfileDatabaseResult::kFull,
|
||||
"seventeenth stable identity was not rejected");
|
||||
require(controller_profile_database_find(database, identity(1)) !=
|
||||
nullptr,
|
||||
"full-table rejection evicted an existing identity");
|
||||
|
||||
for (size_t offset = 0; offset < sizeof(encoded_database);
|
||||
offset += CONTROLLER_PROFILE_ENCODED_SIZE) {
|
||||
const size_t size = sizeof(encoded_database) - offset <
|
||||
CONTROLLER_PROFILE_ENCODED_SIZE
|
||||
? sizeof(encoded_database) - offset
|
||||
: CONTROLLER_PROFILE_ENCODED_SIZE;
|
||||
require(controller_profile_database_encode_range(
|
||||
database, offset, &encoded_database[offset], size),
|
||||
"database range did not encode");
|
||||
}
|
||||
require(controller_profile_database_decode(
|
||||
read_encoded_database, nullptr, &decoded_database),
|
||||
"database did not decode");
|
||||
require(controller_profile_database_find(
|
||||
decoded_database, identity(16)) != nullptr,
|
||||
"last database identity did not round trip");
|
||||
|
||||
encoded_database[12] = 1;
|
||||
require(!controller_profile_database_decode(
|
||||
read_encoded_database, nullptr, &decoded_database),
|
||||
"nonzero database header reservation was accepted");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
int main() {
|
||||
test_profile_wire_schema();
|
||||
test_database_round_trip_and_capacity();
|
||||
return 0;
|
||||
}
|
||||
186
tests/profile_service_test.cpp
Normal file
186
tests/profile_service_test.cpp
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
#include "controller_identity.h"
|
||||
#include "controller_profile.h"
|
||||
#include "pico_profile_storage.h"
|
||||
#include "profile_service.h"
|
||||
#include "profile_storage.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
|
||||
namespace {
|
||||
|
||||
struct FakeFlash {
|
||||
uint8_t bytes[PROFILE_STORAGE_BANK_COUNT][PROFILE_STORAGE_BANK_SIZE];
|
||||
};
|
||||
|
||||
FakeFlash flash{};
|
||||
|
||||
void require(bool condition, const char* message) {
|
||||
if (!condition) {
|
||||
std::cerr << message << '\n';
|
||||
std::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
bool fake_read(void* context, uint8_t bank, size_t offset,
|
||||
uint8_t* output, size_t size) {
|
||||
auto* storage = static_cast<FakeFlash*>(context);
|
||||
if (bank >= PROFILE_STORAGE_BANK_COUNT || output == nullptr ||
|
||||
offset > PROFILE_STORAGE_BANK_SIZE ||
|
||||
size > PROFILE_STORAGE_BANK_SIZE - offset) {
|
||||
return false;
|
||||
}
|
||||
memcpy(output, &storage->bytes[bank][offset], size);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool fake_erase_sector(void* context, uint8_t bank, size_t offset) {
|
||||
auto* storage = static_cast<FakeFlash*>(context);
|
||||
if (bank >= PROFILE_STORAGE_BANK_COUNT ||
|
||||
offset % PROFILE_STORAGE_SECTOR_SIZE != 0 ||
|
||||
offset > PROFILE_STORAGE_BANK_SIZE ||
|
||||
PROFILE_STORAGE_SECTOR_SIZE > PROFILE_STORAGE_BANK_SIZE - offset) {
|
||||
return false;
|
||||
}
|
||||
memset(&storage->bytes[bank][offset], 0xff,
|
||||
PROFILE_STORAGE_SECTOR_SIZE);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool fake_program(void* context, uint8_t bank, size_t offset,
|
||||
const uint8_t* data, size_t size) {
|
||||
auto* storage = static_cast<FakeFlash*>(context);
|
||||
if (bank >= PROFILE_STORAGE_BANK_COUNT || data == nullptr ||
|
||||
size != PROFILE_STORAGE_PAGE_SIZE ||
|
||||
offset % PROFILE_STORAGE_PAGE_SIZE != 0 ||
|
||||
offset > PROFILE_STORAGE_BANK_SIZE ||
|
||||
size > PROFILE_STORAGE_BANK_SIZE - offset) {
|
||||
return false;
|
||||
}
|
||||
for (size_t index = 0; index < size; ++index) {
|
||||
storage->bytes[bank][offset + index] &= data[index];
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
ProfileStorageIo fake_io() {
|
||||
return {
|
||||
&flash,
|
||||
PROFILE_STORAGE_BANK_SIZE,
|
||||
PROFILE_STORAGE_SECTOR_SIZE,
|
||||
PROFILE_STORAGE_PAGE_SIZE,
|
||||
fake_read,
|
||||
fake_erase_sector,
|
||||
fake_program,
|
||||
};
|
||||
}
|
||||
|
||||
ProfileServiceTransactionSnapshot transaction_snapshot() {
|
||||
ProfileServiceTransactionSnapshot snapshot{};
|
||||
profile_service_transaction_snapshot(&snapshot);
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
ControllerProfileDatabase reload_database(
|
||||
const ProfileServiceTransactionSnapshot& transaction,
|
||||
uint32_t expected_generation) {
|
||||
ControllerProfileDatabase recovered{};
|
||||
ProfileStorage storage;
|
||||
require(storage.initialize(fake_io(), &recovered) &&
|
||||
storage.snapshot().valid &&
|
||||
storage.snapshot().generation == expected_generation &&
|
||||
storage.snapshot().generation ==
|
||||
transaction.transaction.stored_generation &&
|
||||
storage.snapshot().payload_crc ==
|
||||
transaction.transaction.stored_crc,
|
||||
"terminal transaction status did not identify persisted storage");
|
||||
return recovered;
|
||||
}
|
||||
|
||||
void test_pending_commands_are_not_decoded_as_profile_writes() {
|
||||
memset(flash.bytes, 0xff, sizeof(flash.bytes));
|
||||
profile_service_prepare();
|
||||
profile_service_initialize_on_storage_core();
|
||||
|
||||
const ControllerIdentity identity = controller_identity_global();
|
||||
constexpr uint8_t kProfileIndex = 2;
|
||||
ControllerProfile customized =
|
||||
controller_profile_default(identity, kProfileIndex);
|
||||
customized.strong_rumble_scale = 17;
|
||||
uint8_t encoded[CONTROLLER_PROFILE_ENCODED_SIZE]{};
|
||||
require(controller_profile_encode(customized, encoded, sizeof(encoded)),
|
||||
"customized profile did not encode");
|
||||
|
||||
constexpr uint32_t kWriteTransactionId = 0x10203040;
|
||||
require(profile_service_begin(
|
||||
kWriteTransactionId, identity, kProfileIndex,
|
||||
CONTROLLER_PROFILE_SCHEMA_VERSION, sizeof(encoded),
|
||||
profile_storage_crc32(encoded, sizeof(encoded))) ==
|
||||
ConfigurationTransactionStatus::kReceiving &&
|
||||
profile_service_append(kWriteTransactionId, 0, encoded,
|
||||
sizeof(encoded)) ==
|
||||
ConfigurationTransactionStatus::kReceiving &&
|
||||
profile_service_commit(kWriteTransactionId) ==
|
||||
ConfigurationTransactionStatus::kPending,
|
||||
"profile write did not reach pending");
|
||||
profile_service_task_on_storage_core(0);
|
||||
require(transaction_snapshot().transaction.status ==
|
||||
ConfigurationTransactionStatus::kCommitted,
|
||||
"profile write baseline did not commit");
|
||||
|
||||
constexpr uint32_t kResetTransactionId = 0xa5a55a5a;
|
||||
require(profile_service_reset(kResetTransactionId, identity,
|
||||
kProfileIndex) ==
|
||||
ConfigurationTransactionStatus::kPending,
|
||||
"profile reset did not reach pending");
|
||||
ProfileServiceTransactionSnapshot reset = transaction_snapshot();
|
||||
require(reset.transaction.transaction_id == kResetTransactionId &&
|
||||
reset.transaction.status ==
|
||||
ConfigurationTransactionStatus::kPending,
|
||||
"pending reset lost its transaction identity");
|
||||
|
||||
profile_service_task_on_storage_core(1000);
|
||||
reset = transaction_snapshot();
|
||||
require(reset.transaction.transaction_id == kResetTransactionId &&
|
||||
reset.transaction.status ==
|
||||
ConfigurationTransactionStatus::kCommitted,
|
||||
"one reset tick decoded profile payload or published a malformed result");
|
||||
ControllerProfileDatabase recovered = reload_database(reset, 2);
|
||||
require(recovered.fallback_profiles[kProfileIndex].strong_rumble_scale ==
|
||||
UINT8_MAX,
|
||||
"terminal reset status was published before reset persisted");
|
||||
|
||||
constexpr uint32_t kActivateTransactionId = 0x50607080;
|
||||
constexpr uint8_t kActivatedProfile = 3;
|
||||
require(profile_service_activate(kActivateTransactionId, identity,
|
||||
kActivatedProfile) ==
|
||||
ConfigurationTransactionStatus::kPending,
|
||||
"profile activation did not reach pending");
|
||||
ProfileServiceTransactionSnapshot activate = transaction_snapshot();
|
||||
require(activate.transaction.transaction_id == kActivateTransactionId &&
|
||||
activate.transaction.status ==
|
||||
ConfigurationTransactionStatus::kPending,
|
||||
"pending activation lost its transaction identity");
|
||||
|
||||
profile_service_task_on_storage_core(2000);
|
||||
activate = transaction_snapshot();
|
||||
require(activate.transaction.transaction_id == kActivateTransactionId &&
|
||||
activate.transaction.status ==
|
||||
ConfigurationTransactionStatus::kCommitted,
|
||||
"one activation tick decoded profile payload or published a malformed result");
|
||||
recovered = reload_database(activate, 3);
|
||||
require(recovered.fallback_active_profile == kActivatedProfile,
|
||||
"terminal activation status was published before activation persisted");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ProfileStorageIo pico_profile_storage_io() {
|
||||
return fake_io();
|
||||
}
|
||||
|
||||
int main() {
|
||||
test_pending_commands_are_not_decoded_as_profile_writes();
|
||||
return 0;
|
||||
}
|
||||
235
tests/profile_storage_test.cpp
Normal file
235
tests/profile_storage_test.cpp
Normal file
|
|
@ -0,0 +1,235 @@
|
|||
#include "controller_identity.h"
|
||||
#include "controller_profile.h"
|
||||
#include "profile_storage.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
|
||||
namespace {
|
||||
|
||||
struct FakeFlash {
|
||||
uint8_t bytes[PROFILE_STORAGE_BANK_COUNT][PROFILE_STORAGE_BANK_SIZE];
|
||||
int successful_programs = 0;
|
||||
int fail_after_programs = -1;
|
||||
bool corrupt_next_program = false;
|
||||
bool fail_reads_after_header_program = false;
|
||||
bool header_programmed = false;
|
||||
};
|
||||
|
||||
FakeFlash flash{};
|
||||
ControllerProfileDatabase database{};
|
||||
ControllerProfileDatabase recovered_database{};
|
||||
|
||||
void require(bool condition, const char* message) {
|
||||
if (!condition) {
|
||||
std::cerr << message << '\n';
|
||||
std::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
void erase_all() {
|
||||
memset(flash.bytes, 0xff, sizeof(flash.bytes));
|
||||
flash.successful_programs = 0;
|
||||
flash.fail_after_programs = -1;
|
||||
flash.corrupt_next_program = false;
|
||||
flash.fail_reads_after_header_program = false;
|
||||
flash.header_programmed = false;
|
||||
}
|
||||
|
||||
bool fake_read(void* context, uint8_t bank, size_t offset,
|
||||
uint8_t* output, size_t size) {
|
||||
auto* storage = static_cast<FakeFlash*>(context);
|
||||
if (storage->fail_reads_after_header_program &&
|
||||
storage->header_programmed) {
|
||||
return false;
|
||||
}
|
||||
if (bank >= PROFILE_STORAGE_BANK_COUNT ||
|
||||
offset > PROFILE_STORAGE_BANK_SIZE ||
|
||||
size > PROFILE_STORAGE_BANK_SIZE - offset) {
|
||||
return false;
|
||||
}
|
||||
memcpy(output, &storage->bytes[bank][offset], size);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool fake_erase_sector(void* context, uint8_t bank, size_t offset) {
|
||||
auto* storage = static_cast<FakeFlash*>(context);
|
||||
if (bank >= PROFILE_STORAGE_BANK_COUNT ||
|
||||
offset % PROFILE_STORAGE_SECTOR_SIZE != 0 ||
|
||||
offset > PROFILE_STORAGE_BANK_SIZE ||
|
||||
PROFILE_STORAGE_SECTOR_SIZE >
|
||||
PROFILE_STORAGE_BANK_SIZE - offset) {
|
||||
return false;
|
||||
}
|
||||
memset(&storage->bytes[bank][offset], 0xff,
|
||||
PROFILE_STORAGE_SECTOR_SIZE);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool fake_program(void* context, uint8_t bank, size_t offset,
|
||||
const uint8_t* data, size_t size) {
|
||||
auto* storage = static_cast<FakeFlash*>(context);
|
||||
if (bank >= PROFILE_STORAGE_BANK_COUNT || data == nullptr ||
|
||||
size != PROFILE_STORAGE_PAGE_SIZE ||
|
||||
offset % PROFILE_STORAGE_PAGE_SIZE != 0 ||
|
||||
offset > PROFILE_STORAGE_BANK_SIZE ||
|
||||
size > PROFILE_STORAGE_BANK_SIZE - offset) {
|
||||
return false;
|
||||
}
|
||||
if (storage->fail_after_programs >= 0 &&
|
||||
storage->successful_programs >= storage->fail_after_programs) {
|
||||
return false;
|
||||
}
|
||||
for (size_t index = 0; index < size; ++index) {
|
||||
storage->bytes[bank][offset + index] &= data[index];
|
||||
}
|
||||
if (storage->corrupt_next_program) {
|
||||
storage->bytes[bank][offset] ^= 1;
|
||||
storage->corrupt_next_program = false;
|
||||
}
|
||||
if (offset == 0) {
|
||||
storage->header_programmed = true;
|
||||
}
|
||||
++storage->successful_programs;
|
||||
return true;
|
||||
}
|
||||
|
||||
ProfileStorageIo fake_io() {
|
||||
return {
|
||||
&flash,
|
||||
PROFILE_STORAGE_BANK_SIZE,
|
||||
PROFILE_STORAGE_SECTOR_SIZE,
|
||||
PROFILE_STORAGE_PAGE_SIZE,
|
||||
fake_read,
|
||||
fake_erase_sector,
|
||||
fake_program,
|
||||
};
|
||||
}
|
||||
|
||||
void test_two_bank_recovery() {
|
||||
erase_all();
|
||||
controller_profile_database_default(&database);
|
||||
ProfileStorage storage;
|
||||
require(storage.initialize(fake_io(), &database) &&
|
||||
!storage.snapshot().valid,
|
||||
"erased profile storage did not initialize empty");
|
||||
require(storage.commit(database) == ProfileStorageResult::kOk &&
|
||||
storage.snapshot().generation == 1,
|
||||
"first profile database did not commit");
|
||||
const int programs_after_first = flash.successful_programs;
|
||||
require(storage.commit(database) == ProfileStorageResult::kUnchanged &&
|
||||
flash.successful_programs == programs_after_first,
|
||||
"unchanged profile database consumed flash writes");
|
||||
|
||||
database.fallback_profiles[0].button_map[0] = 1;
|
||||
require(storage.commit(database) == ProfileStorageResult::kOk &&
|
||||
storage.snapshot().generation == 2,
|
||||
"second profile database generation did not commit");
|
||||
ProfileStorage reloaded;
|
||||
require(reloaded.initialize(fake_io(), &recovered_database) &&
|
||||
reloaded.snapshot().generation == 2 &&
|
||||
recovered_database.fallback_profiles[0].button_map[0] == 1,
|
||||
"latest profile database did not survive reload");
|
||||
|
||||
const uint8_t newest_bank = reloaded.snapshot().active_bank;
|
||||
flash.bytes[newest_bank][PROFILE_STORAGE_RECORD_HEADER_SIZE + 4] ^= 1;
|
||||
ProfileStorage after_corruption;
|
||||
require(after_corruption.initialize(fake_io(), &recovered_database) &&
|
||||
after_corruption.snapshot().generation == 1 &&
|
||||
recovered_database.fallback_profiles[0].button_map[0] == 0,
|
||||
"corrupt newest profile bank did not roll back");
|
||||
}
|
||||
|
||||
void test_interrupted_commit_retains_previous_bank() {
|
||||
erase_all();
|
||||
controller_profile_database_default(&database);
|
||||
ProfileStorage storage;
|
||||
require(storage.initialize(fake_io(), &database) &&
|
||||
storage.commit(database) == ProfileStorageResult::kOk,
|
||||
"interruption baseline did not commit");
|
||||
database.fallback_profiles[1].button_map[2] = 3;
|
||||
flash.fail_after_programs = flash.successful_programs + 1;
|
||||
require(storage.commit(database) == ProfileStorageResult::kIoError,
|
||||
"interrupted profile write reported success");
|
||||
|
||||
flash.fail_after_programs = -1;
|
||||
ProfileStorage recovered;
|
||||
require(recovered.initialize(fake_io(), &recovered_database) &&
|
||||
recovered.snapshot().generation == 1 &&
|
||||
recovered_database.fallback_profiles[1].button_map[2] == 2,
|
||||
"interrupted profile write replaced previous bank");
|
||||
}
|
||||
|
||||
void test_successful_header_program_is_commit_point() {
|
||||
erase_all();
|
||||
controller_profile_database_default(&database);
|
||||
ProfileStorage storage;
|
||||
require(storage.initialize(fake_io(), &database) &&
|
||||
storage.commit(database) == ProfileStorageResult::kOk,
|
||||
"commit-point baseline did not commit");
|
||||
|
||||
database.fallback_profiles[1].strong_rumble_scale = 17;
|
||||
flash.header_programmed = false;
|
||||
flash.fail_reads_after_header_program = true;
|
||||
require(storage.commit(database) == ProfileStorageResult::kOk &&
|
||||
storage.snapshot().generation == 2,
|
||||
"successful header program was rolled back by a later read");
|
||||
|
||||
flash.fail_reads_after_header_program = false;
|
||||
ProfileStorage recovered;
|
||||
require(recovered.initialize(fake_io(), &recovered_database) &&
|
||||
recovered.snapshot().generation == 2 &&
|
||||
recovered_database.fallback_profiles[1]
|
||||
.strong_rumble_scale == 17,
|
||||
"committed header did not recover after transient read failure");
|
||||
}
|
||||
|
||||
void test_payload_corruption_prevents_header_publication() {
|
||||
erase_all();
|
||||
controller_profile_database_default(&database);
|
||||
ProfileStorage storage;
|
||||
require(storage.initialize(fake_io(), &database) &&
|
||||
storage.commit(database) == ProfileStorageResult::kOk,
|
||||
"corruption baseline did not commit");
|
||||
const ProfileStorageSnapshot previous = storage.snapshot();
|
||||
const uint8_t target_bank = previous.active_bank ^ 1u;
|
||||
const int programs_before_corruption = flash.successful_programs;
|
||||
constexpr int kPayloadProgramCount =
|
||||
(CONTROLLER_PROFILE_DATABASE_ENCODED_SIZE +
|
||||
PROFILE_STORAGE_PAGE_SIZE - 1) /
|
||||
PROFILE_STORAGE_PAGE_SIZE;
|
||||
|
||||
database.fallback_profiles[1].button_map[2] = 3;
|
||||
flash.corrupt_next_program = true;
|
||||
require(storage.commit(database) == ProfileStorageResult::kIoError &&
|
||||
flash.successful_programs ==
|
||||
programs_before_corruption + kPayloadProgramCount,
|
||||
"corrupt payload programming reached the header program");
|
||||
for (size_t index = 0; index < PROFILE_STORAGE_RECORD_HEADER_SIZE;
|
||||
++index) {
|
||||
require(flash.bytes[target_bank][index] == 0xff,
|
||||
"rejected corrupt payload published a discoverable header");
|
||||
}
|
||||
require(storage.snapshot().valid == previous.valid &&
|
||||
storage.snapshot().generation == previous.generation &&
|
||||
storage.snapshot().payload_crc == previous.payload_crc &&
|
||||
storage.snapshot().active_bank == previous.active_bank,
|
||||
"rejected corrupt programming changed the storage snapshot");
|
||||
|
||||
ProfileStorage recovered;
|
||||
require(recovered.initialize(fake_io(), &recovered_database) &&
|
||||
recovered.snapshot().generation == previous.generation &&
|
||||
recovered.snapshot().active_bank == previous.active_bank &&
|
||||
recovered_database.fallback_profiles[1].button_map[2] == 2,
|
||||
"headerless corrupt payload was recovered");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
int main() {
|
||||
test_two_bank_recovery();
|
||||
test_interrupted_commit_retains_previous_bank();
|
||||
test_successful_header_program_is_commit_point();
|
||||
test_payload_corruption_prevents_header_publication();
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -1,7 +1,9 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import struct
|
||||
import zlib
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
|
@ -56,7 +58,45 @@ class FakeDevice:
|
|||
),
|
||||
]
|
||||
self.pairing_generation = 4
|
||||
self.global_identity = config_manager.ControllerIdentity.global_fallback()
|
||||
self.stable_identity = config_manager.ControllerIdentity(
|
||||
True,
|
||||
config_manager.TRANSPORT_CLASSIC,
|
||||
0,
|
||||
bytes.fromhex("102030405060"),
|
||||
0x045E,
|
||||
0x02FD,
|
||||
)
|
||||
self.profile_identities = [
|
||||
self.global_identity,
|
||||
self.stable_identity,
|
||||
]
|
||||
self.active_profiles = {
|
||||
identity.to_bytes(): index
|
||||
for identity, index in zip(self.profile_identities, (0, 1))
|
||||
}
|
||||
default_profile = config_manager.ControllerProfile.default().to_bytes()
|
||||
self.profiles = {
|
||||
(identity.to_bytes(), index): default_profile
|
||||
for identity in self.profile_identities
|
||||
for index in range(config_manager.PROFILE_CAPACITY)
|
||||
}
|
||||
self.selected_profile = (self.global_identity.to_bytes(), 0)
|
||||
self.profile_generation = 7
|
||||
self.profile_transaction_id = 0
|
||||
self.profile_transaction_identity = self.global_identity.to_bytes()
|
||||
self.profile_transaction_index = 0
|
||||
self.profile_transaction_payload = bytearray()
|
||||
self.profile_transaction_expected_size = 0
|
||||
self.profile_transaction_expected_crc = 0
|
||||
self.profile_transaction_status = config_manager.STATUS_OK
|
||||
self.fail_profile_commit_status: int | None = None
|
||||
self.bad_profile_response_crc = False
|
||||
self.requests: list[int] = []
|
||||
self.profile_chunk_sizes: list[int] = []
|
||||
self.pending_profile_mutation: tuple[int, bytes, int] | None = None
|
||||
self.profile_transaction_pending_reads = 0
|
||||
self.profile_status_responses: list[tuple[int, int]] = []
|
||||
|
||||
def _pairing_payload(self) -> bytes:
|
||||
payload = bytearray([len(self.records), 0, 0, 0])
|
||||
|
|
@ -77,6 +117,71 @@ class FakeDevice:
|
|||
stored_crc,
|
||||
)
|
||||
|
||||
def _profile_list_payload(self) -> bytes:
|
||||
payload = bytearray([len(self.profile_identities)])
|
||||
for identity in self.profile_identities:
|
||||
payload.extend(identity.to_bytes())
|
||||
payload.extend((self.active_profiles[identity.to_bytes()], 0))
|
||||
return bytes(payload)
|
||||
|
||||
def _profile_transaction_payload(self) -> bytes:
|
||||
stored = self.profiles.get(
|
||||
(
|
||||
self.profile_transaction_identity,
|
||||
self.profile_transaction_index,
|
||||
),
|
||||
bytes(config_manager.PROFILE_SIZE),
|
||||
)
|
||||
return struct.pack(
|
||||
"<IHHIII",
|
||||
self.profile_transaction_id,
|
||||
len(self.profile_transaction_payload),
|
||||
self.profile_transaction_expected_size,
|
||||
self.profile_transaction_expected_crc,
|
||||
self.profile_generation,
|
||||
zlib.crc32(stored) & 0xFFFFFFFF,
|
||||
)
|
||||
|
||||
def _queue_profile_mutation(self, operation: int, payload: bytes) -> None:
|
||||
assert len(payload) == 19
|
||||
self.profile_transaction_id = struct.unpack_from("<I", payload)[0]
|
||||
assert self.profile_transaction_id != 0
|
||||
self.profile_transaction_identity = payload[4:18]
|
||||
self.profile_transaction_index = payload[18]
|
||||
self.profile_transaction_payload = bytearray()
|
||||
self.profile_transaction_expected_size = 0
|
||||
self.profile_transaction_expected_crc = 0
|
||||
self.profile_transaction_status = config_manager.STATUS_PENDING
|
||||
self.profile_transaction_pending_reads = 1
|
||||
self.pending_profile_mutation = (
|
||||
operation,
|
||||
self.profile_transaction_identity,
|
||||
self.profile_transaction_index,
|
||||
)
|
||||
|
||||
def _complete_profile_mutation(self) -> None:
|
||||
assert self.pending_profile_mutation is not None
|
||||
operation, identity, profile_index = self.pending_profile_mutation
|
||||
if self.fail_profile_commit_status is not None:
|
||||
self.profile_transaction_status = self.fail_profile_commit_status
|
||||
self.pending_profile_mutation = None
|
||||
return
|
||||
if operation == config_manager.OP_PROFILE_RESET:
|
||||
indices = (
|
||||
range(config_manager.PROFILE_CAPACITY)
|
||||
if profile_index == config_manager.PROFILE_NONE_BUTTON
|
||||
else (profile_index,)
|
||||
)
|
||||
default = config_manager.ControllerProfile.default().to_bytes()
|
||||
for reset_index in indices:
|
||||
self.profiles[(identity, reset_index)] = default
|
||||
else:
|
||||
assert operation == config_manager.OP_PROFILE_ACTIVATE
|
||||
self.active_profiles[identity] = profile_index
|
||||
self.profile_generation += 1
|
||||
self.profile_transaction_status = config_manager.STATUS_OK
|
||||
self.pending_profile_mutation = None
|
||||
|
||||
def ctrl_transfer(
|
||||
self,
|
||||
bm_request_type: int,
|
||||
|
|
@ -92,7 +197,9 @@ class FakeDevice:
|
|||
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]))
|
||||
return make_response(
|
||||
request, bytes([0, 2, 0, 2, 0, 0, 0, 2])
|
||||
)
|
||||
if request == config_manager.OP_CONFIGURATION_READ:
|
||||
return make_response(
|
||||
request,
|
||||
|
|
@ -114,6 +221,44 @@ class FakeDevice:
|
|||
self._pairing_payload(),
|
||||
generation=self.pairing_generation,
|
||||
)
|
||||
if request == config_manager.OP_PROFILE_LIST:
|
||||
return make_response(
|
||||
request,
|
||||
self._profile_list_payload(),
|
||||
schema=config_manager.PROFILE_SCHEMA_VERSION,
|
||||
generation=self.profile_generation,
|
||||
)
|
||||
if request == config_manager.OP_PROFILE_READ:
|
||||
response = bytearray(
|
||||
make_response(
|
||||
request,
|
||||
self.profiles[self.selected_profile],
|
||||
schema=config_manager.PROFILE_SCHEMA_VERSION,
|
||||
generation=self.profile_generation,
|
||||
)
|
||||
)
|
||||
if self.bad_profile_response_crc:
|
||||
response[-1] ^= 1
|
||||
return bytes(response)
|
||||
if request == config_manager.OP_PROFILE_TRANSACTION_STATUS:
|
||||
if self.profile_transaction_status == config_manager.STATUS_PENDING:
|
||||
if self.profile_transaction_pending_reads:
|
||||
self.profile_transaction_pending_reads -= 1
|
||||
elif self.pending_profile_mutation is not None:
|
||||
self._complete_profile_mutation()
|
||||
self.profile_status_responses.append(
|
||||
(
|
||||
self.profile_transaction_id,
|
||||
self.profile_transaction_status,
|
||||
)
|
||||
)
|
||||
return make_response(
|
||||
request,
|
||||
self._profile_transaction_payload(),
|
||||
status=self.profile_transaction_status,
|
||||
schema=config_manager.PROFILE_SCHEMA_VERSION,
|
||||
generation=self.profile_generation,
|
||||
)
|
||||
raise AssertionError(f"unexpected IN request {request}")
|
||||
|
||||
assert bm_request_type == 0x40
|
||||
|
|
@ -165,11 +310,138 @@ class FakeDevice:
|
|||
elif request == config_manager.OP_PAIRING_CLEAR:
|
||||
self.records = []
|
||||
self.pairing_generation += 1
|
||||
elif request == config_manager.OP_PROFILE_SELECT:
|
||||
assert len(payload) == 15
|
||||
self.selected_profile = (payload[:14], payload[14])
|
||||
assert self.selected_profile in self.profiles
|
||||
elif request == config_manager.OP_PROFILE_BEGIN:
|
||||
assert len(payload) == 28
|
||||
self.profile_transaction_id = struct.unpack_from("<I", payload)[0]
|
||||
self.profile_transaction_identity = payload[4:18]
|
||||
(
|
||||
self.profile_transaction_index,
|
||||
reserved,
|
||||
schema,
|
||||
self.profile_transaction_expected_size,
|
||||
self.profile_transaction_expected_crc,
|
||||
) = struct.unpack_from("<BBHHI", payload, 18)
|
||||
assert reserved == 0
|
||||
assert schema == config_manager.PROFILE_SCHEMA_VERSION
|
||||
assert (
|
||||
self.profile_transaction_expected_size
|
||||
== config_manager.PROFILE_SIZE
|
||||
)
|
||||
self.profile_transaction_payload = bytearray()
|
||||
self.profile_transaction_status = config_manager.STATUS_PENDING
|
||||
self.profile_chunk_sizes = []
|
||||
elif request == config_manager.OP_PROFILE_CHUNK:
|
||||
transaction_id, offset, chunk_size = struct.unpack_from(
|
||||
"<IHH", payload
|
||||
)
|
||||
assert transaction_id == self.profile_transaction_id
|
||||
assert offset == len(self.profile_transaction_payload)
|
||||
chunk = payload[8 : 8 + chunk_size]
|
||||
assert len(chunk) == chunk_size
|
||||
self.profile_transaction_payload.extend(chunk)
|
||||
self.profile_chunk_sizes.append(chunk_size)
|
||||
elif request == config_manager.OP_PROFILE_COMMIT:
|
||||
assert (
|
||||
struct.unpack("<I", payload)[0]
|
||||
== self.profile_transaction_id
|
||||
)
|
||||
assert (
|
||||
len(self.profile_transaction_payload)
|
||||
== self.profile_transaction_expected_size
|
||||
)
|
||||
assert (
|
||||
zlib.crc32(self.profile_transaction_payload) & 0xFFFFFFFF
|
||||
) == self.profile_transaction_expected_crc
|
||||
if self.fail_profile_commit_status is None:
|
||||
key = (
|
||||
self.profile_transaction_identity,
|
||||
self.profile_transaction_index,
|
||||
)
|
||||
self.profiles[key] = bytes(self.profile_transaction_payload)
|
||||
self.profile_generation += 1
|
||||
self.profile_transaction_status = config_manager.STATUS_OK
|
||||
else:
|
||||
self.profile_transaction_status = (
|
||||
self.fail_profile_commit_status
|
||||
)
|
||||
elif request == config_manager.OP_PROFILE_RESET:
|
||||
self._queue_profile_mutation(request, payload)
|
||||
assert (
|
||||
self.profile_transaction_index
|
||||
== config_manager.PROFILE_NONE_BUTTON
|
||||
or 0
|
||||
<= self.profile_transaction_index
|
||||
< config_manager.PROFILE_CAPACITY
|
||||
)
|
||||
elif request == config_manager.OP_PROFILE_ACTIVATE:
|
||||
self._queue_profile_mutation(request, payload)
|
||||
assert (
|
||||
0
|
||||
<= self.profile_transaction_index
|
||||
< config_manager.PROFILE_CAPACITY
|
||||
)
|
||||
else:
|
||||
raise AssertionError(f"unexpected OUT request {request}")
|
||||
return len(encoded)
|
||||
|
||||
|
||||
def custom_profile() -> config_manager.ControllerProfile:
|
||||
return config_manager.ControllerProfile(
|
||||
button_map=(
|
||||
1,
|
||||
0,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
9,
|
||||
10,
|
||||
11,
|
||||
12,
|
||||
13,
|
||||
15,
|
||||
config_manager.PROFILE_NONE_BUTTON,
|
||||
),
|
||||
left_stick=config_manager.StickConfig(
|
||||
-123, 456, 1000, 30000, 384, True, False
|
||||
),
|
||||
right_stick=config_manager.StickConfig(
|
||||
789, -321, 500, 31000, 192, False, True
|
||||
),
|
||||
left_trigger=config_manager.TriggerConfig(100, 65000, 320, 32000),
|
||||
right_trigger=config_manager.TriggerConfig(200, 64000, 224, 33000),
|
||||
weak_rumble_scale=77,
|
||||
strong_rumble_scale=201,
|
||||
confirmation_policy=2,
|
||||
switching_chord=(1 << 6) | (1 << 7),
|
||||
macro_trigger=0,
|
||||
macro_cancel=1,
|
||||
macro_steps=(
|
||||
config_manager.MacroStep(
|
||||
0,
|
||||
config_manager.MACRO_OVERRIDE_MASK,
|
||||
config_manager.PROFILE_MAXIMUM_WAIT_MS,
|
||||
(1 << 0) | (1 << 12),
|
||||
-32768,
|
||||
32767,
|
||||
-1000,
|
||||
1000,
|
||||
12345,
|
||||
54321,
|
||||
),
|
||||
config_manager.MacroStep.end(),
|
||||
),
|
||||
turbo_modes=(0, 1, 2) + (0,) * 13,
|
||||
)
|
||||
|
||||
|
||||
def test_response_validation() -> None:
|
||||
payload = make_response(config_manager.OP_INFO, b"12345678")
|
||||
envelope = config_manager.parse_response(payload, config_manager.OP_INFO)
|
||||
|
|
@ -205,6 +477,352 @@ def test_configuration_transaction_and_reset() -> None:
|
|||
assert config_manager.read_configuration(device).pairing_window_seconds == 60
|
||||
|
||||
|
||||
def test_identity_and_profile_binary_json_round_trip() -> None:
|
||||
identity = config_manager.ControllerIdentity(
|
||||
True,
|
||||
config_manager.TRANSPORT_BLE,
|
||||
3,
|
||||
bytes.fromhex("A1B2C3D4E5F6"),
|
||||
0x1234,
|
||||
0xABCD,
|
||||
)
|
||||
encoded_identity = identity.to_bytes()
|
||||
assert encoded_identity == bytes.fromhex(
|
||||
"01020300A1B2C3D4E5F63412CDAB"
|
||||
)
|
||||
assert config_manager.ControllerIdentity.from_bytes(encoded_identity) == identity
|
||||
assert (
|
||||
config_manager.ControllerIdentity.global_fallback().to_bytes()
|
||||
== bytes(config_manager.CONTROLLER_IDENTITY_SIZE)
|
||||
)
|
||||
malformed_identity = bytearray(encoded_identity)
|
||||
malformed_identity[3] = 1
|
||||
with pytest.raises(config_manager.ConfigManagerError):
|
||||
config_manager.ControllerIdentity.from_bytes(malformed_identity)
|
||||
|
||||
profile = custom_profile()
|
||||
encoded = profile.to_bytes()
|
||||
assert len(encoded) == config_manager.PROFILE_SIZE
|
||||
assert struct.unpack_from("<HH", encoded) == (
|
||||
config_manager.PROFILE_SCHEMA_VERSION,
|
||||
config_manager.PROFILE_SIZE,
|
||||
)
|
||||
assert encoded[75] == encoded[81] == 0
|
||||
assert encoded[98:100] == b"\x00\x00"
|
||||
assert encoded[252:] == bytes(4)
|
||||
assert config_manager.ControllerProfile.from_bytes(encoded) == profile
|
||||
|
||||
serialized = profile.to_json()
|
||||
assert serialized.startswith('{\n "schema_version": 1,\n "size": 256,')
|
||||
decoded = config_manager.ControllerProfile.from_json(serialized)
|
||||
assert decoded == profile
|
||||
assert decoded.to_json() == serialized
|
||||
|
||||
|
||||
def test_profile_list_select_read_and_chunked_commit() -> None:
|
||||
device = FakeDevice()
|
||||
entries = config_manager.list_profiles(device)
|
||||
assert entries == (
|
||||
config_manager.ProfileListEntry(device.global_identity, 0),
|
||||
config_manager.ProfileListEntry(device.stable_identity, 1),
|
||||
)
|
||||
assert (
|
||||
config_manager.read_profile(device, device.stable_identity, 1)
|
||||
== config_manager.ControllerProfile.default()
|
||||
)
|
||||
assert device.requests[-2:] == [
|
||||
config_manager.OP_PROFILE_SELECT,
|
||||
config_manager.OP_PROFILE_READ,
|
||||
]
|
||||
|
||||
profile = custom_profile()
|
||||
status = config_manager.write_profile(
|
||||
device, device.stable_identity, 2, profile, 1.0
|
||||
)
|
||||
assert status.status == config_manager.STATUS_OK
|
||||
assert status.stored_generation == 8
|
||||
assert device.profile_chunk_sizes == [40, 40, 40, 40, 40, 40, 16]
|
||||
assert config_manager.OP_PROFILE_TRANSACTION_STATUS in device.requests
|
||||
assert (
|
||||
config_manager.read_profile(device, device.stable_identity, 2)
|
||||
== profile
|
||||
)
|
||||
|
||||
|
||||
def test_profile_reset_and_activate_wait_for_correlated_transactions(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
device = FakeDevice()
|
||||
generated_ids = iter((0, 0xA5A55A5A))
|
||||
monkeypatch.setattr(
|
||||
config_manager.secrets, "randbits", lambda _bits: next(generated_ids)
|
||||
)
|
||||
identity = device.stable_identity
|
||||
profile_key = (identity.to_bytes(), 2)
|
||||
device.profiles[profile_key] = custom_profile().to_bytes()
|
||||
|
||||
reset = config_manager.reset_profile(device, identity, 2, 1.0)
|
||||
|
||||
assert reset.transaction_id == device.profile_transaction_id == 1
|
||||
assert reset.status == config_manager.STATUS_OK
|
||||
assert reset.stored_generation == 8
|
||||
assert device.profile_status_responses == [
|
||||
(1, config_manager.STATUS_PENDING),
|
||||
(1, config_manager.STATUS_OK),
|
||||
]
|
||||
assert (
|
||||
device.profiles[profile_key]
|
||||
== config_manager.ControllerProfile.default().to_bytes()
|
||||
)
|
||||
|
||||
device.profile_status_responses.clear()
|
||||
activated = config_manager.activate_profile(device, identity, 3, 1.0)
|
||||
|
||||
assert (
|
||||
activated.transaction_id
|
||||
== device.profile_transaction_id
|
||||
== 0xA5A55A5A
|
||||
)
|
||||
assert activated.status == config_manager.STATUS_OK
|
||||
assert activated.stored_generation == 9
|
||||
assert device.profile_status_responses == [
|
||||
(0xA5A55A5A, config_manager.STATUS_PENDING),
|
||||
(0xA5A55A5A, config_manager.STATUS_OK),
|
||||
]
|
||||
assert device.active_profiles[identity.to_bytes()] == 3
|
||||
|
||||
|
||||
def test_profile_cli_surfaces_late_storage_failure(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
) -> None:
|
||||
device = FakeDevice()
|
||||
device.fail_profile_commit_status = 8
|
||||
monkeypatch.setattr(config_manager, "_candidate_devices", lambda: [device])
|
||||
previous_active = device.active_profiles[device.stable_identity.to_bytes()]
|
||||
|
||||
assert (
|
||||
config_manager.main(
|
||||
["profiles", "activate", "4", "--identity", "1"]
|
||||
)
|
||||
== 1
|
||||
)
|
||||
|
||||
output = capsys.readouterr()
|
||||
assert output.out == ""
|
||||
assert "storage failure" in output.err
|
||||
assert (
|
||||
device.active_profiles[device.stable_identity.to_bytes()]
|
||||
== previous_active
|
||||
)
|
||||
assert device.profile_status_responses == [
|
||||
(device.profile_transaction_id, config_manager.STATUS_PENDING),
|
||||
(device.profile_transaction_id, 8),
|
||||
]
|
||||
|
||||
|
||||
def test_profile_cli_json_round_trip_activate_and_reset(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
device = FakeDevice()
|
||||
monkeypatch.setattr(config_manager, "_candidate_devices", lambda: [device])
|
||||
export_path = tmp_path / "profile.json"
|
||||
|
||||
assert config_manager.main(["profiles", "list"]) == 0
|
||||
output = capsys.readouterr().out
|
||||
assert "0: global fallback (active profile 1)" in output
|
||||
assert "1: Classic 10:20:30:40:50:60" in output
|
||||
|
||||
device.profiles[(device.stable_identity.to_bytes(), 1)] = (
|
||||
custom_profile().to_bytes()
|
||||
)
|
||||
assert (
|
||||
config_manager.main(
|
||||
[
|
||||
"profiles",
|
||||
"export",
|
||||
"2",
|
||||
str(export_path),
|
||||
"--identity",
|
||||
"1",
|
||||
]
|
||||
)
|
||||
== 0
|
||||
)
|
||||
_ = capsys.readouterr()
|
||||
exported = config_manager.ControllerProfile.from_json(
|
||||
export_path.read_text(encoding="utf-8")
|
||||
)
|
||||
assert exported == custom_profile()
|
||||
|
||||
device.profiles[(device.stable_identity.to_bytes(), 1)] = (
|
||||
config_manager.ControllerProfile.default().to_bytes()
|
||||
)
|
||||
assert (
|
||||
config_manager.main(
|
||||
[
|
||||
"profiles",
|
||||
"import",
|
||||
"2",
|
||||
str(export_path),
|
||||
"--identity",
|
||||
"1",
|
||||
]
|
||||
)
|
||||
== 0
|
||||
)
|
||||
assert (
|
||||
device.profiles[(device.stable_identity.to_bytes(), 1)]
|
||||
== custom_profile().to_bytes()
|
||||
)
|
||||
_ = capsys.readouterr()
|
||||
|
||||
assert (
|
||||
config_manager.main(
|
||||
["profiles", "activate", "4", "--identity", "1"]
|
||||
)
|
||||
== 0
|
||||
)
|
||||
assert device.active_profiles[device.stable_identity.to_bytes()] == 3
|
||||
_ = capsys.readouterr()
|
||||
|
||||
before_reset_requests = len(device.requests)
|
||||
assert (
|
||||
config_manager.main(
|
||||
["profiles", "reset", "2", "--identity", "1"]
|
||||
)
|
||||
== 2
|
||||
)
|
||||
assert "requires --yes" in capsys.readouterr().err
|
||||
assert len(device.requests) == before_reset_requests
|
||||
|
||||
assert (
|
||||
config_manager.main(
|
||||
[
|
||||
"profiles",
|
||||
"reset",
|
||||
"2",
|
||||
"--identity",
|
||||
"1",
|
||||
"--yes",
|
||||
]
|
||||
)
|
||||
== 0
|
||||
)
|
||||
assert (
|
||||
device.profiles[(device.stable_identity.to_bytes(), 1)]
|
||||
== config_manager.ControllerProfile.default().to_bytes()
|
||||
)
|
||||
_ = capsys.readouterr()
|
||||
assert (
|
||||
config_manager.main(
|
||||
[
|
||||
"profiles",
|
||||
"reset",
|
||||
"all",
|
||||
"--identity",
|
||||
"1",
|
||||
"--yes",
|
||||
]
|
||||
)
|
||||
== 0
|
||||
)
|
||||
|
||||
|
||||
def test_malformed_profiles_are_rejected_before_usb(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
malformed_binary = bytearray(config_manager.ControllerProfile.default().to_bytes())
|
||||
malformed_binary[75] = 1
|
||||
with pytest.raises(
|
||||
config_manager.ConfigManagerError, match="reserved fields"
|
||||
):
|
||||
config_manager.ControllerProfile.from_bytes(malformed_binary)
|
||||
|
||||
profile_object = config_manager.ControllerProfile.default().to_json_object()
|
||||
del profile_object["turbo"]
|
||||
missing_path = tmp_path / "missing.json"
|
||||
missing_path.write_text(json.dumps(profile_object), encoding="utf-8")
|
||||
|
||||
profile_object = config_manager.ControllerProfile.default().to_json_object()
|
||||
profile_object["reserved"] = 0
|
||||
unknown_path = tmp_path / "unknown.json"
|
||||
unknown_path.write_text(json.dumps(profile_object), encoding="utf-8")
|
||||
|
||||
profile_object = config_manager.ControllerProfile.default().to_json_object()
|
||||
profile_object["rumble"]["confirmation_policy"] = "invalid"
|
||||
enum_path = tmp_path / "enum.json"
|
||||
enum_path.write_text(json.dumps(profile_object), encoding="utf-8")
|
||||
|
||||
profile_object = config_manager.ControllerProfile.default().to_json_object()
|
||||
profile_object["sticks"]["left"]["outer_saturation"] = 0
|
||||
range_path = tmp_path / "range.json"
|
||||
range_path.write_text(json.dumps(profile_object), encoding="utf-8")
|
||||
|
||||
usb_lookups = 0
|
||||
|
||||
def candidates() -> list[FakeDevice]:
|
||||
nonlocal usb_lookups
|
||||
usb_lookups += 1
|
||||
return [FakeDevice()]
|
||||
|
||||
monkeypatch.setattr(config_manager, "_candidate_devices", candidates)
|
||||
for path in (missing_path, unknown_path, enum_path, range_path):
|
||||
assert (
|
||||
config_manager.main(["profiles", "import", "1", str(path)]) == 1
|
||||
)
|
||||
assert "error:" in capsys.readouterr().err
|
||||
assert usb_lookups == 0
|
||||
|
||||
|
||||
def test_profile_crc_status_failures_and_identity_bounds(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
device = FakeDevice()
|
||||
device.bad_profile_response_crc = True
|
||||
with pytest.raises(
|
||||
config_manager.ConfigManagerError, match="response CRC mismatch"
|
||||
):
|
||||
config_manager.read_profile(device, device.global_identity, 0)
|
||||
|
||||
failing_device = FakeDevice()
|
||||
failing_device.fail_profile_commit_status = 6
|
||||
with pytest.raises(config_manager.ConfigManagerError, match="CRC mismatch"):
|
||||
config_manager.write_profile(
|
||||
failing_device,
|
||||
failing_device.global_identity,
|
||||
0,
|
||||
custom_profile(),
|
||||
1.0,
|
||||
)
|
||||
|
||||
bounded_device = FakeDevice()
|
||||
monkeypatch.setattr(
|
||||
config_manager, "_candidate_devices", lambda: [bounded_device]
|
||||
)
|
||||
assert (
|
||||
config_manager.main(
|
||||
[
|
||||
"profiles",
|
||||
"export",
|
||||
"1",
|
||||
str(tmp_path / "unused.json"),
|
||||
"--identity",
|
||||
"2",
|
||||
]
|
||||
)
|
||||
== 1
|
||||
)
|
||||
assert "identity index 2 is out of range" in capsys.readouterr().err
|
||||
assert config_manager.OP_PROFILE_SELECT not in bounded_device.requests
|
||||
|
||||
|
||||
def test_status_and_pairing_commands(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
|
|
|
|||
30
tests/test_controller_profile_native.py
Normal file
30
tests/test_controller_profile_native.py
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def test_controller_profile_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 / "controller_profile_test"
|
||||
subprocess.run(
|
||||
[
|
||||
compiler,
|
||||
"-std=c++17",
|
||||
"-Wall",
|
||||
"-Wextra",
|
||||
"-Werror",
|
||||
"-pedantic",
|
||||
f"-I{root}",
|
||||
str(root / "tests" / "controller_profile_test.cpp"),
|
||||
str(root / "controller_identity.cpp"),
|
||||
str(root / "controller_profile.cpp"),
|
||||
"-o",
|
||||
str(executable),
|
||||
],
|
||||
check=True,
|
||||
cwd=root,
|
||||
)
|
||||
subprocess.run([str(executable)], check=True, cwd=root)
|
||||
33
tests/test_profile_service_native.py
Normal file
33
tests/test_profile_service_native.py
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def test_profile_service_native(tmp_path: Path) -> None:
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
compiler = shutil.which("c++") or shutil.which("g++")
|
||||
assert compiler is not None, "a host C++ compiler is required"
|
||||
|
||||
executable = tmp_path / "profile_service_test"
|
||||
subprocess.run(
|
||||
[
|
||||
compiler,
|
||||
"-std=c++17",
|
||||
"-Wall",
|
||||
"-Wextra",
|
||||
"-Werror",
|
||||
"-pedantic",
|
||||
f"-I{root / 'tests' / 'bluepad32_native_stubs'}",
|
||||
f"-I{root}",
|
||||
str(root / "tests" / "profile_service_test.cpp"),
|
||||
str(root / "controller_identity.cpp"),
|
||||
str(root / "controller_profile.cpp"),
|
||||
str(root / "profile_storage.cpp"),
|
||||
str(root / "profile_service.cpp"),
|
||||
"-o",
|
||||
str(executable),
|
||||
],
|
||||
check=True,
|
||||
cwd=root,
|
||||
)
|
||||
subprocess.run([str(executable)], check=True, cwd=root)
|
||||
31
tests/test_profile_storage_native.py
Normal file
31
tests/test_profile_storage_native.py
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def test_profile_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 / "profile_storage_test"
|
||||
subprocess.run(
|
||||
[
|
||||
compiler,
|
||||
"-std=c++17",
|
||||
"-Wall",
|
||||
"-Wextra",
|
||||
"-Werror",
|
||||
"-pedantic",
|
||||
f"-I{root}",
|
||||
str(root / "tests" / "profile_storage_test.cpp"),
|
||||
str(root / "controller_identity.cpp"),
|
||||
str(root / "controller_profile.cpp"),
|
||||
str(root / "profile_storage.cpp"),
|
||||
"-o",
|
||||
str(executable),
|
||||
],
|
||||
check=True,
|
||||
cwd=root,
|
||||
)
|
||||
subprocess.run([str(executable)], check=True, cwd=root)
|
||||
|
|
@ -11,6 +11,9 @@ namespace {
|
|||
|
||||
Bluepad32PairingSnapshot current_pairings{};
|
||||
ConfigurationServiceSnapshot current_configuration{};
|
||||
ProfileServiceListSnapshot current_profile_list{};
|
||||
ProfileServiceSelectedSnapshot current_profile_selected{};
|
||||
ProfileServiceTransactionSnapshot current_profile_transaction{};
|
||||
bool refresh_requested = false;
|
||||
bool clear_requested = false;
|
||||
std::vector<uint8_t> control_payload;
|
||||
|
|
@ -20,6 +23,16 @@ uint32_t append_transaction_id = 0;
|
|||
uint32_t commit_transaction_id = 0;
|
||||
size_t append_offset = 0;
|
||||
std::vector<uint8_t> appended_bytes;
|
||||
ControllerIdentity profile_identity{};
|
||||
uint8_t profile_index = 0;
|
||||
uint16_t profile_schema = 0;
|
||||
size_t profile_size = 0;
|
||||
uint32_t profile_crc = 0;
|
||||
bool profile_reset_requested = false;
|
||||
uint32_t profile_reset_transaction_id = 0;
|
||||
uint32_t profile_commit_transaction_id = 0;
|
||||
bool profile_activate_requested = false;
|
||||
uint32_t profile_activate_transaction_id = 0;
|
||||
|
||||
void require(bool condition, const char* message) {
|
||||
if (!condition) {
|
||||
|
|
@ -42,6 +55,13 @@ void write_u32(std::vector<uint8_t>* output, size_t offset,
|
|||
(*output)[offset + 3] = static_cast<uint8_t>(value >> 24);
|
||||
}
|
||||
|
||||
uint32_t read_u32(const std::vector<uint8_t>& input, size_t offset) {
|
||||
return static_cast<uint32_t>(input[offset]) |
|
||||
(static_cast<uint32_t>(input[offset + 1]) << 8) |
|
||||
(static_cast<uint32_t>(input[offset + 2]) << 16) |
|
||||
(static_cast<uint32_t>(input[offset + 3]) << 24);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> make_request(
|
||||
UsbConfigurationManagement::Operation operation,
|
||||
const std::vector<uint8_t>& payload) {
|
||||
|
|
@ -213,6 +233,173 @@ void test_vendor_requests() {
|
|||
"request with invalid magic was accepted");
|
||||
}
|
||||
|
||||
void test_profile_vendor_requests() {
|
||||
using namespace UsbConfigurationManagement;
|
||||
ControllerIdentity expected_identity{};
|
||||
expected_identity.stable = true;
|
||||
expected_identity.transport = ControllerTransport::kClassic;
|
||||
expected_identity.address[5] = 7;
|
||||
expected_identity.vendor_id = 0x057e;
|
||||
expected_identity.product_id = 0x2009;
|
||||
|
||||
current_profile_list = {};
|
||||
current_profile_list.metadata.state = ProfileServiceState::kReady;
|
||||
current_profile_list.metadata.generation = 9;
|
||||
current_profile_list.count = 2;
|
||||
current_profile_list.rows[0].identity = controller_identity_global();
|
||||
current_profile_list.rows[1].identity = expected_identity;
|
||||
current_profile_list.rows[1].active_profile = 2;
|
||||
tusb_control_request_t request = setup_request(
|
||||
Operation::kProfileList, TUSB_DIR_IN, kMaximumResponseSize);
|
||||
require(tud_vendor_control_xfer_cb(
|
||||
0, CONTROL_STAGE_SETUP, &request) &&
|
||||
control_payload.size() == kResponseHeaderSize + 33 &&
|
||||
control_payload[5] ==
|
||||
static_cast<uint8_t>(Operation::kProfileList) &&
|
||||
control_payload[10] == CONTROLLER_PROFILE_SCHEMA_VERSION &&
|
||||
control_payload[kResponseHeaderSize] == 2 &&
|
||||
control_payload[kResponseHeaderSize + 31] == 2,
|
||||
"profile list response was not encoded");
|
||||
|
||||
current_profile_selected = {};
|
||||
current_profile_selected.metadata.state =
|
||||
ProfileServiceState::kReady;
|
||||
current_profile_selected.metadata.generation = 9;
|
||||
current_profile_selected.valid = true;
|
||||
current_profile_selected.status =
|
||||
ConfigurationTransactionStatus::kCommitted;
|
||||
current_profile_selected.identity = expected_identity;
|
||||
current_profile_selected.profile_index = 2;
|
||||
current_profile_selected.profile =
|
||||
controller_profile_default(expected_identity, 2);
|
||||
request = setup_request(
|
||||
Operation::kProfileRead, TUSB_DIR_IN, kMaximumResponseSize);
|
||||
require(tud_vendor_control_xfer_cb(
|
||||
0, CONTROL_STAGE_SETUP, &request) &&
|
||||
control_payload.size() ==
|
||||
kResponseHeaderSize +
|
||||
CONTROLLER_PROFILE_ENCODED_SIZE &&
|
||||
control_payload[kResponseHeaderSize] == 1 &&
|
||||
control_payload[kResponseHeaderSize + 2] == 0 &&
|
||||
control_payload[kResponseHeaderSize + 3] == 1,
|
||||
"selected profile response was not encoded");
|
||||
|
||||
current_profile_transaction = {};
|
||||
current_profile_transaction.metadata.state =
|
||||
ProfileServiceState::kReady;
|
||||
current_profile_transaction.metadata.generation = 9;
|
||||
current_profile_transaction.transaction.transaction_id = 0x01020304;
|
||||
current_profile_transaction.transaction.status =
|
||||
ConfigurationTransactionStatus::kPending;
|
||||
request = setup_request(
|
||||
Operation::kProfileTransactionStatus, TUSB_DIR_IN,
|
||||
kMaximumResponseSize);
|
||||
require(tud_vendor_control_xfer_cb(
|
||||
0, CONTROL_STAGE_SETUP, &request) &&
|
||||
control_payload.size() == kResponseHeaderSize + 20 &&
|
||||
control_payload[6] ==
|
||||
static_cast<uint8_t>(Status::kPending) &&
|
||||
read_u32(control_payload, kResponseHeaderSize) ==
|
||||
0x01020304,
|
||||
"pending profile transaction status lost its transaction ID");
|
||||
|
||||
current_profile_transaction.transaction.status =
|
||||
ConfigurationTransactionStatus::kCommitted;
|
||||
current_profile_transaction.transaction.stored_generation =
|
||||
0x11223344;
|
||||
current_profile_transaction.transaction.stored_crc = 0xaabbccdd;
|
||||
require(tud_vendor_control_xfer_cb(
|
||||
0, CONTROL_STAGE_SETUP, &request) &&
|
||||
control_payload[6] == static_cast<uint8_t>(Status::kOk) &&
|
||||
read_u32(control_payload, kResponseHeaderSize) ==
|
||||
0x01020304 &&
|
||||
read_u32(control_payload, kResponseHeaderSize + 12) ==
|
||||
0x11223344 &&
|
||||
read_u32(control_payload, kResponseHeaderSize + 16) ==
|
||||
0xaabbccdd,
|
||||
"final profile transaction status lost its commit result");
|
||||
|
||||
std::vector<uint8_t> identity_payload(15);
|
||||
require(controller_identity_encode(
|
||||
expected_identity, identity_payload.data(),
|
||||
CONTROLLER_IDENTITY_ENCODED_SIZE),
|
||||
"profile test identity did not encode");
|
||||
identity_payload[14] = 2;
|
||||
perform_out(Operation::kProfileSelect, identity_payload);
|
||||
require(controller_identity_equal(expected_identity,
|
||||
profile_identity) &&
|
||||
profile_index == 2,
|
||||
"profile selection was not dispatched");
|
||||
|
||||
std::vector<uint8_t> begin(28);
|
||||
write_u32(&begin, 0, 0x55667788);
|
||||
require(controller_identity_encode(
|
||||
expected_identity, &begin[4],
|
||||
CONTROLLER_IDENTITY_ENCODED_SIZE),
|
||||
"profile begin identity did not encode");
|
||||
begin[18] = 1;
|
||||
write_u16(&begin, 20, CONTROLLER_PROFILE_SCHEMA_VERSION);
|
||||
write_u16(&begin, 22, CONTROLLER_PROFILE_ENCODED_SIZE);
|
||||
write_u32(&begin, 24, 0xaabbccdd);
|
||||
perform_out(Operation::kProfileBegin, begin);
|
||||
require(begin_transaction_id == 0x55667788 &&
|
||||
profile_index == 1 &&
|
||||
profile_schema == CONTROLLER_PROFILE_SCHEMA_VERSION &&
|
||||
profile_size == CONTROLLER_PROFILE_ENCODED_SIZE &&
|
||||
profile_crc == 0xaabbccdd,
|
||||
"profile begin was not dispatched");
|
||||
|
||||
std::vector<uint8_t> chunk(48);
|
||||
write_u32(&chunk, 0, 0x55667788);
|
||||
write_u16(&chunk, 4, 0);
|
||||
write_u16(&chunk, 6, 40);
|
||||
perform_out(Operation::kProfileChunk, chunk);
|
||||
require(append_transaction_id == 0x55667788 &&
|
||||
append_offset == 0 && appended_bytes.size() == 40,
|
||||
"profile chunk was not dispatched");
|
||||
|
||||
std::vector<uint8_t> commit(4);
|
||||
write_u32(&commit, 0, 0x55667788);
|
||||
perform_out(Operation::kProfileCommit, commit);
|
||||
require(profile_commit_transaction_id == 0x55667788,
|
||||
"profile commit was not dispatched");
|
||||
|
||||
std::vector<uint8_t> mutation(19);
|
||||
write_u32(&mutation, 0, 0x10203040);
|
||||
require(controller_identity_encode(
|
||||
expected_identity, &mutation[4],
|
||||
CONTROLLER_IDENTITY_ENCODED_SIZE),
|
||||
"profile mutation identity did not encode");
|
||||
mutation[18] = CONTROLLER_PROFILE_ALL;
|
||||
perform_out(Operation::kProfileReset, mutation);
|
||||
require(profile_reset_requested &&
|
||||
profile_reset_transaction_id == 0x10203040,
|
||||
"profile reset transaction was not dispatched");
|
||||
write_u32(&mutation, 0, 0x50607080);
|
||||
mutation[18] = 3;
|
||||
perform_out(Operation::kProfileActivate, mutation);
|
||||
require(profile_activate_requested && profile_index == 3 &&
|
||||
profile_activate_transaction_id == 0x50607080,
|
||||
"profile activation transaction was not dispatched");
|
||||
|
||||
write_u32(&mutation, 0, 0);
|
||||
perform_out(Operation::kProfileActivate, mutation, false);
|
||||
request = setup_request(
|
||||
Operation::kProfileReset, TUSB_DIR_OUT, kRequestHeaderSize + 15);
|
||||
require(!tud_vendor_control_xfer_cb(
|
||||
0, CONTROL_STAGE_SETUP, &request),
|
||||
"legacy profile reset payload was accepted");
|
||||
|
||||
begin[19] = 1;
|
||||
perform_out(Operation::kProfileBegin, begin, false);
|
||||
request = setup_request(
|
||||
Operation::kProfileSelect, TUSB_DIR_OUT,
|
||||
kRequestHeaderSize + 14);
|
||||
require(!tud_vendor_control_xfer_cb(
|
||||
0, CONTROL_STAGE_SETUP, &request),
|
||||
"short profile selection request was accepted");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
uint32_t configuration_crc32(const uint8_t* data, size_t size) {
|
||||
|
|
@ -256,6 +443,75 @@ ConfigurationTransactionStatus configuration_service_reset(uint32_t) {
|
|||
return ConfigurationTransactionStatus::kPending;
|
||||
}
|
||||
|
||||
ConfigurationTransactionStatus profile_service_select(
|
||||
const ControllerIdentity& identity, uint8_t selected_profile) {
|
||||
profile_identity = identity;
|
||||
profile_index = selected_profile;
|
||||
return ConfigurationTransactionStatus::kPending;
|
||||
}
|
||||
|
||||
ConfigurationTransactionStatus profile_service_begin(
|
||||
uint32_t transaction_id, const ControllerIdentity& identity,
|
||||
uint8_t selected_profile, uint16_t schema_version,
|
||||
size_t payload_size, uint32_t payload_crc) {
|
||||
begin_transaction_id = transaction_id;
|
||||
profile_identity = identity;
|
||||
profile_index = selected_profile;
|
||||
profile_schema = schema_version;
|
||||
profile_size = payload_size;
|
||||
profile_crc = payload_crc;
|
||||
return ConfigurationTransactionStatus::kReceiving;
|
||||
}
|
||||
|
||||
ConfigurationTransactionStatus profile_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 profile_service_commit(
|
||||
uint32_t transaction_id) {
|
||||
profile_commit_transaction_id = transaction_id;
|
||||
return ConfigurationTransactionStatus::kPending;
|
||||
}
|
||||
|
||||
ConfigurationTransactionStatus profile_service_reset(
|
||||
uint32_t transaction_id, const ControllerIdentity& identity,
|
||||
uint8_t selected_profile) {
|
||||
profile_reset_transaction_id = transaction_id;
|
||||
profile_identity = identity;
|
||||
profile_index = selected_profile;
|
||||
profile_reset_requested = true;
|
||||
return ConfigurationTransactionStatus::kPending;
|
||||
}
|
||||
|
||||
ConfigurationTransactionStatus profile_service_activate(
|
||||
uint32_t transaction_id, const ControllerIdentity& identity,
|
||||
uint8_t selected_profile) {
|
||||
profile_activate_transaction_id = transaction_id;
|
||||
profile_identity = identity;
|
||||
profile_index = selected_profile;
|
||||
profile_activate_requested = true;
|
||||
return ConfigurationTransactionStatus::kPending;
|
||||
}
|
||||
|
||||
void profile_service_list_snapshot(ProfileServiceListSnapshot* output) {
|
||||
*output = current_profile_list;
|
||||
}
|
||||
|
||||
void profile_service_selected_snapshot(
|
||||
ProfileServiceSelectedSnapshot* output) {
|
||||
*output = current_profile_selected;
|
||||
}
|
||||
|
||||
void profile_service_transaction_snapshot(
|
||||
ProfileServiceTransactionSnapshot* output) {
|
||||
*output = current_profile_transaction;
|
||||
}
|
||||
|
||||
void bluepad32_input_backend_request_pairing_snapshot() {
|
||||
refresh_requested = true;
|
||||
}
|
||||
|
|
@ -288,6 +544,8 @@ bool tud_control_status(uint8_t, const tusb_control_request_t*) {
|
|||
}
|
||||
|
||||
#include "../adapter_configuration.cpp"
|
||||
#include "../controller_identity.cpp"
|
||||
#include "../controller_profile.cpp"
|
||||
#include "../usb_configuration_management.cpp"
|
||||
|
||||
int main() {
|
||||
|
|
@ -297,5 +555,6 @@ int main() {
|
|||
test_envelope_encoding();
|
||||
test_pairing_encoding();
|
||||
test_vendor_requests();
|
||||
test_profile_vendor_requests();
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,17 +61,39 @@ Status transaction_status(ConfigurationTransactionStatus status) {
|
|||
}
|
||||
return Status::kStorageError;
|
||||
}
|
||||
Status profile_service_status(const ProfileServiceMetadata& metadata) {
|
||||
if (metadata.state == ProfileServiceState::kLoading) {
|
||||
return Status::kPending;
|
||||
}
|
||||
if (metadata.state == ProfileServiceState::kStorageError) {
|
||||
return Status::kStorageError;
|
||||
}
|
||||
return Status::kOk;
|
||||
}
|
||||
|
||||
|
||||
bool valid_out_size(Operation operation, size_t size) {
|
||||
switch (operation) {
|
||||
case Operation::kConfigurationBegin:
|
||||
return size == kRequestHeaderSize + 12;
|
||||
case Operation::kProfileBegin:
|
||||
return size == kRequestHeaderSize + 28;
|
||||
case Operation::kConfigurationChunk:
|
||||
return size > kRequestHeaderSize + 8 &&
|
||||
size <= kMaximumRequestSize;
|
||||
case Operation::kProfileChunk:
|
||||
return size > kRequestHeaderSize + 8 &&
|
||||
size <= kMaximumRequestSize;
|
||||
case Operation::kConfigurationCommit:
|
||||
case Operation::kConfigurationReset:
|
||||
return size == kRequestHeaderSize + 4;
|
||||
case Operation::kProfileCommit:
|
||||
return size == kRequestHeaderSize + 4;
|
||||
case Operation::kProfileSelect:
|
||||
return size == kRequestHeaderSize + 15;
|
||||
case Operation::kProfileReset:
|
||||
case Operation::kProfileActivate:
|
||||
return size == kRequestHeaderSize + 19;
|
||||
case Operation::kPairingRefresh:
|
||||
case Operation::kPairingClear:
|
||||
return size == kRequestHeaderSize;
|
||||
|
|
@ -218,6 +240,74 @@ size_t encode_pairing_snapshot(const Bluepad32PairingSnapshot& snapshot,
|
|||
payload, offset, output, output_size);
|
||||
}
|
||||
|
||||
size_t encode_profile_list(const ProfileServiceListSnapshot& snapshot,
|
||||
uint8_t* output, size_t output_size) {
|
||||
if (snapshot.count > PROFILE_SERVICE_LIST_CAPACITY) {
|
||||
return 0;
|
||||
}
|
||||
uint8_t payload[kProfileListPayloadSize]{};
|
||||
payload[0] = snapshot.count;
|
||||
size_t offset = 1;
|
||||
for (uint8_t index = 0; index < snapshot.count; ++index) {
|
||||
if (!controller_identity_encode(snapshot.rows[index].identity,
|
||||
&payload[offset],
|
||||
CONTROLLER_IDENTITY_ENCODED_SIZE) ||
|
||||
snapshot.rows[index].active_profile >=
|
||||
CONTROLLER_PROFILE_COUNT) {
|
||||
return 0;
|
||||
}
|
||||
payload[offset + 14] = snapshot.rows[index].active_profile;
|
||||
offset += 16;
|
||||
}
|
||||
return encode_response(
|
||||
Operation::kProfileList, profile_service_status(snapshot.metadata),
|
||||
0, CONTROLLER_PROFILE_SCHEMA_VERSION, snapshot.metadata.generation,
|
||||
payload, offset, output, output_size);
|
||||
}
|
||||
|
||||
size_t encode_profile_read(const ProfileServiceSelectedSnapshot& snapshot,
|
||||
uint8_t* output, size_t output_size) {
|
||||
Status status = profile_service_status(snapshot.metadata);
|
||||
if (status == Status::kOk) {
|
||||
status = transaction_status(snapshot.status);
|
||||
}
|
||||
uint8_t payload[CONTROLLER_PROFILE_ENCODED_SIZE]{};
|
||||
size_t payload_size = 0;
|
||||
if (snapshot.valid) {
|
||||
if (!controller_profile_encode(snapshot.profile, payload,
|
||||
sizeof(payload))) {
|
||||
return 0;
|
||||
}
|
||||
payload_size = sizeof(payload);
|
||||
}
|
||||
return encode_response(
|
||||
Operation::kProfileRead, status, 0,
|
||||
CONTROLLER_PROFILE_SCHEMA_VERSION, snapshot.metadata.generation,
|
||||
payload, payload_size, output, output_size);
|
||||
}
|
||||
|
||||
size_t encode_profile_transaction(
|
||||
const ProfileServiceTransactionSnapshot& snapshot,
|
||||
uint8_t* output, size_t output_size) {
|
||||
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);
|
||||
Status status = profile_service_status(snapshot.metadata);
|
||||
if (status == Status::kOk) {
|
||||
status = transaction_status(transaction.status);
|
||||
}
|
||||
return encode_response(
|
||||
Operation::kProfileTransactionStatus, status, 0,
|
||||
CONTROLLER_PROFILE_SCHEMA_VERSION, snapshot.metadata.generation,
|
||||
payload, sizeof(payload), output, output_size);
|
||||
}
|
||||
|
||||
} // namespace UsbConfigurationManagement
|
||||
|
||||
namespace {
|
||||
|
|
@ -286,6 +376,96 @@ bool process_out_request() {
|
|||
(static_cast<uint32_t>(payload[2]) << 16) |
|
||||
(static_cast<uint32_t>(payload[3]) << 24));
|
||||
return true;
|
||||
case Operation::kProfileSelect: {
|
||||
ControllerIdentity identity{};
|
||||
if (payload[14] >= CONTROLLER_PROFILE_COUNT ||
|
||||
!controller_identity_decode(
|
||||
payload, CONTROLLER_IDENTITY_ENCODED_SIZE, &identity)) {
|
||||
return false;
|
||||
}
|
||||
const ConfigurationTransactionStatus status =
|
||||
profile_service_select(identity, payload[14]);
|
||||
return status == ConfigurationTransactionStatus::kCommitted ||
|
||||
status == ConfigurationTransactionStatus::kPending;
|
||||
}
|
||||
case Operation::kProfileBegin: {
|
||||
ControllerIdentity identity{};
|
||||
if (payload[19] != 0 ||
|
||||
!controller_identity_decode(
|
||||
&payload[4], CONTROLLER_IDENTITY_ENCODED_SIZE,
|
||||
&identity)) {
|
||||
return false;
|
||||
}
|
||||
profile_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),
|
||||
identity, payload[18],
|
||||
static_cast<uint16_t>(payload[20] |
|
||||
(payload[21] << 8)),
|
||||
static_cast<uint16_t>(payload[22] |
|
||||
(payload[23] << 8)),
|
||||
static_cast<uint32_t>(payload[24]) |
|
||||
(static_cast<uint32_t>(payload[25]) << 8) |
|
||||
(static_cast<uint32_t>(payload[26]) << 16) |
|
||||
(static_cast<uint32_t>(payload[27]) << 24));
|
||||
return true;
|
||||
}
|
||||
case Operation::kProfileChunk: {
|
||||
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;
|
||||
}
|
||||
profile_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::kProfileCommit:
|
||||
profile_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::kProfileReset:
|
||||
case Operation::kProfileActivate: {
|
||||
const uint32_t transaction_id =
|
||||
static_cast<uint32_t>(payload[0]) |
|
||||
(static_cast<uint32_t>(payload[1]) << 8) |
|
||||
(static_cast<uint32_t>(payload[2]) << 16) |
|
||||
(static_cast<uint32_t>(payload[3]) << 24);
|
||||
ControllerIdentity identity{};
|
||||
if (transaction_id == 0 ||
|
||||
(request.operation == Operation::kProfileActivate &&
|
||||
payload[18] >= CONTROLLER_PROFILE_COUNT) ||
|
||||
(request.operation == Operation::kProfileReset &&
|
||||
payload[18] != CONTROLLER_PROFILE_ALL &&
|
||||
payload[18] >= CONTROLLER_PROFILE_COUNT) ||
|
||||
!controller_identity_decode(
|
||||
&payload[4], CONTROLLER_IDENTITY_ENCODED_SIZE,
|
||||
&identity)) {
|
||||
return false;
|
||||
}
|
||||
const ConfigurationTransactionStatus status =
|
||||
request.operation == Operation::kProfileReset
|
||||
? profile_service_reset(
|
||||
transaction_id, identity, payload[18])
|
||||
: profile_service_activate(
|
||||
transaction_id, identity, payload[18]);
|
||||
return status == ConfigurationTransactionStatus::kPending ||
|
||||
status == ConfigurationTransactionStatus::kUnchanged ||
|
||||
status == ConfigurationTransactionStatus::kCommitted;
|
||||
}
|
||||
case Operation::kPairingRefresh:
|
||||
bluepad32_input_backend_request_pairing_snapshot();
|
||||
return true;
|
||||
|
|
@ -367,6 +547,27 @@ extern "C" bool tud_vendor_control_xfer_cb(
|
|||
snapshot, response, sizeof(response));
|
||||
break;
|
||||
}
|
||||
case Operation::kProfileList: {
|
||||
ProfileServiceListSnapshot snapshot{};
|
||||
profile_service_list_snapshot(&snapshot);
|
||||
response_size = encode_profile_list(
|
||||
snapshot, response, sizeof(response));
|
||||
break;
|
||||
}
|
||||
case Operation::kProfileRead: {
|
||||
ProfileServiceSelectedSnapshot snapshot{};
|
||||
profile_service_selected_snapshot(&snapshot);
|
||||
response_size = encode_profile_read(
|
||||
snapshot, response, sizeof(response));
|
||||
break;
|
||||
}
|
||||
case Operation::kProfileTransactionStatus: {
|
||||
ProfileServiceTransactionSnapshot snapshot{};
|
||||
profile_service_transaction_snapshot(&snapshot);
|
||||
response_size = encode_profile_transaction(
|
||||
snapshot, response, sizeof(response));
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
#include "bluepad32_input_backend.h"
|
||||
#include "configuration_service.h"
|
||||
#include "profile_service.h"
|
||||
|
||||
namespace UsbConfigurationManagement {
|
||||
|
||||
|
|
@ -16,11 +17,14 @@ constexpr size_t kResponseHeaderSize = 20;
|
|||
constexpr size_t kPairingRecordSize = 8;
|
||||
constexpr size_t kPairingPayloadHeaderSize = 4;
|
||||
constexpr size_t kMaximumRequestSize = 64;
|
||||
constexpr size_t kProfileListPayloadSize =
|
||||
1 + PROFILE_SERVICE_LIST_CAPACITY * 16;
|
||||
constexpr size_t kMaximumResponseSize =
|
||||
kResponseHeaderSize + kPairingPayloadHeaderSize +
|
||||
BLUEPAD32_PAIRING_RECORD_CAPACITY * kPairingRecordSize;
|
||||
kResponseHeaderSize + kProfileListPayloadSize;
|
||||
constexpr size_t kMaximumChunkSize =
|
||||
kMaximumRequestSize - kRequestHeaderSize - 8;
|
||||
static_assert(kMaximumResponseSize == 293,
|
||||
"profile list no longer fits the EP0 response buffer");
|
||||
|
||||
enum class Operation : uint8_t {
|
||||
kInfo = 0x01,
|
||||
|
|
@ -33,6 +37,15 @@ enum class Operation : uint8_t {
|
|||
kPairingRead = 0x20,
|
||||
kPairingRefresh = 0x21,
|
||||
kPairingClear = 0x22,
|
||||
kProfileList = 0x30,
|
||||
kProfileSelect = 0x31,
|
||||
kProfileRead = 0x32,
|
||||
kProfileBegin = 0x33,
|
||||
kProfileChunk = 0x34,
|
||||
kProfileCommit = 0x35,
|
||||
kProfileReset = 0x36,
|
||||
kProfileActivate = 0x37,
|
||||
kProfileTransactionStatus = 0x38,
|
||||
};
|
||||
|
||||
enum class Status : uint8_t {
|
||||
|
|
@ -61,5 +74,12 @@ size_t encode_response(Operation operation, Status status, uint8_t flags,
|
|||
uint8_t* output, size_t output_size);
|
||||
size_t encode_pairing_snapshot(const Bluepad32PairingSnapshot& snapshot,
|
||||
uint8_t* output, size_t output_size);
|
||||
size_t encode_profile_list(const ProfileServiceListSnapshot& snapshot,
|
||||
uint8_t* output, size_t output_size);
|
||||
size_t encode_profile_read(const ProfileServiceSelectedSnapshot& snapshot,
|
||||
uint8_t* output, size_t output_size);
|
||||
size_t encode_profile_transaction(
|
||||
const ProfileServiceTransactionSnapshot& snapshot,
|
||||
uint8_t* output, size_t output_size);
|
||||
|
||||
} // namespace UsbConfigurationManagement
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue