Apply persistent controller profiles

This commit is contained in:
Joey Yakimowich-Payne 2026-09-02 17:35:13 -06:00
commit cfec816dac
25 changed files with 2179 additions and 96 deletions

View file

@ -0,0 +1,63 @@
#pragma once
#include <stdint.h>
#include "controller_profile.h"
constexpr uint8_t kLegacyDefaultProfile[CONTROLLER_PROFILE_ENCODED_SIZE] = {
0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b,
0x0c, 0x0d, 0x0e, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x7f, 0x00, 0x01, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x7f, 0x00, 0x01, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0x00, 0x01, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00,
0xff, 0xff, 0x00, 0x01, 0x00, 0x80, 0x00, 0x00, 0xff, 0xff, 0x03, 0x00, 0x00, 0x00, 0xff, 0xff,
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};
constexpr uint8_t
kLegacyNarrowRawRangeProfile[CONTROLLER_PROFILE_ENCODED_SIZE] = {
0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b,
0x0c, 0x0d, 0x0e, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x7f, 0x00, 0x01, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x7f, 0x00, 0x01, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x30, 0x75, 0x40, 0x9c, 0x00, 0x01, 0x00, 0x80, 0x00, 0x00, 0x30, 0x75,
0x40, 0x9c, 0x00, 0x01, 0x00, 0x80, 0x00, 0x00, 0xff, 0xff, 0x03, 0x00, 0x00, 0x00, 0xff, 0xff,
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};
constexpr uint8_t kLegacyCustomThresholdProfile[CONTROLLER_PROFILE_ENCODED_SIZE] = {
0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b,
0x0c, 0x0d, 0x0e, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x7f, 0x00, 0x01, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x7f, 0x00, 0x01, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0x00, 0x01, 0x34, 0x12, 0x00, 0x00, 0x00, 0x00,
0xff, 0xff, 0x00, 0x01, 0xcd, 0xab, 0x00, 0x00, 0xff, 0xff, 0x03, 0x00, 0x00, 0x00, 0xff, 0xff,
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};

View file

@ -0,0 +1,273 @@
#include "controller_profile_runtime.h"
#include "controller_identity.h"
#include "controller_profile.h"
#include "profile_service.h"
#include <array>
#include <cstdlib>
#include <cstring>
#include <iostream>
namespace {
struct FakeProfileRow {
ControllerIdentity identity{};
uint8_t active_profile = 0;
ControllerProfile profiles[CONTROLLER_PROFILE_COUNT]{};
};
std::array<FakeProfileRow, CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT> rows{};
uint32_t database_generation = 7;
unsigned active_snapshot_count = 0;
void require(bool condition, const char* message) {
if (!condition) {
std::cerr << message << '\n';
std::exit(1);
}
}
ControllerIdentity make_identity(uint8_t value) {
ControllerIdentity identity{};
identity.stable = true;
identity.transport = ControllerTransport::kClassic;
identity.address[0] = value;
identity.address[5] = static_cast<uint8_t>(value + 0x40u);
identity.vendor_id = static_cast<uint16_t>(0x1000u + value);
identity.product_id = static_cast<uint16_t>(0x2000u + value);
return identity;
}
void prepare_profiles() {
database_generation = 7;
active_snapshot_count = 0;
for (uint8_t slot = 0;
slot < CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT; ++slot) {
FakeProfileRow& row = rows[slot];
row = {};
row.identity = make_identity(static_cast<uint8_t>(slot + 1u));
for (uint8_t profile_index = 0;
profile_index < CONTROLLER_PROFILE_COUNT; ++profile_index) {
row.profiles[profile_index] =
controller_profile_default(row.identity, profile_index);
}
row.profiles[0].triggers[0].digital_threshold =
static_cast<uint16_t>(1000u + slot);
row.profiles[0].triggers[1].digital_threshold =
static_cast<uint16_t>(5000u + slot);
row.profiles[0].confirmation_policy =
slot == 0 ? ControllerProfileConfirmationPolicy::kLed
: ControllerProfileConfirmationPolicy::kRumble;
}
rows[0].profiles[0].strong_rumble_scale = 0;
rows[0].profiles[0].weak_rumble_scale = UINT8_MAX;
rows[1].profiles[0].strong_rumble_scale = UINT8_MAX;
rows[1].profiles[0].weak_rumble_scale = 0;
controller_profile_runtime_reset();
}
Bluepad32SlotSnapshot make_snapshot(uint8_t slot,
uint32_t connection_generation = 1) {
Bluepad32SlotSnapshot snapshot{};
snapshot.active = true;
snapshot.connection_generation = connection_generation;
snapshot.identity = rows[slot].identity;
snapshot.state = controller_neutral_state();
return snapshot;
}
bool motion_equal(const ControllerState& first,
const ControllerState& second) {
return first.motion_sample_count == second.motion_sample_count &&
std::memcmp(first.motion_samples, second.motion_samples,
sizeof(first.motion_samples)) == 0;
}
void test_four_slot_cache_and_unchanged_generation() {
prepare_profiles();
std::array<Bluepad32SlotSnapshot,
CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT>
snapshots{};
std::array<ControllerProfileTransformResult,
CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT>
transformed{};
for (uint8_t slot = 0;
slot < CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT; ++slot) {
snapshots[slot] = make_snapshot(slot);
transformed[slot] =
controller_profile_runtime_transform(slot, snapshots[slot]);
require(transformed[slot].left_trigger_digital_threshold ==
static_cast<uint16_t>(1000u + slot) &&
transformed[slot].right_trigger_digital_threshold ==
static_cast<uint16_t>(5000u + slot),
"a slot did not receive its own cached profile");
}
require(active_snapshot_count == CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT,
"initial slot loads did not fetch exactly one profile each");
rows[0].profiles[0].triggers[0].digital_threshold = 65000;
for (uint8_t slot = 0;
slot < CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT; ++slot) {
transformed[slot] =
controller_profile_runtime_transform(slot, snapshots[slot]);
}
require(active_snapshot_count == CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT,
"unchanged generations copied profiles on the report path");
require(transformed[0].left_trigger_digital_threshold == 1000,
"an unchanged generation bypassed the slot cache");
snapshots[2].connection_generation = 2;
transformed[2] =
controller_profile_runtime_transform(2, snapshots[2]);
require(active_snapshot_count ==
CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT + 1 &&
transformed[2].left_trigger_digital_threshold == 1002,
"one slot connection generation did not refresh in isolation");
snapshots[3].identity = rows[1].identity;
transformed[3] =
controller_profile_runtime_transform(3, snapshots[3]);
require(active_snapshot_count ==
CONTROLLER_PROFILE_RUNTIME_SLOT_COUNT + 2 &&
transformed[3].left_trigger_digital_threshold == 1001,
"an exact identity change did not refresh only its slot");
}
void test_activation_disconnect_and_default_preservation() {
prepare_profiles();
Bluepad32SlotSnapshot snapshot = make_snapshot(0);
snapshot.state.button_south = true;
ControllerProfileTransformResult transformed =
controller_profile_runtime_transform(0, snapshot);
require(transformed.state.button_south &&
transformed.left_trigger_digital_threshold == 1000,
"initial active profile was not applied");
rows[0].active_profile = 1;
rows[0].profiles[1].button_map[
static_cast<uint8_t>(ControllerProfileLogicalButton::kSouth)] =
static_cast<uint8_t>(ControllerProfileLogicalButton::kNorth);
rows[0].profiles[1].triggers[0].digital_threshold = 12345;
++database_generation;
transformed = controller_profile_runtime_transform(0, snapshot);
require(!transformed.state.button_south && transformed.state.button_north &&
transformed.left_trigger_digital_threshold == 12345,
"profile activation generation did not refresh the slot cache");
const unsigned reads_before_disconnect = active_snapshot_count;
snapshot.active = false;
transformed = controller_profile_runtime_transform(0, snapshot);
require(!transformed.state.button_south && !transformed.state.button_north &&
transformed.left_trigger_digital_threshold ==
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD &&
active_snapshot_count == reads_before_disconnect,
"disconnect did not neutralize output without fetching a profile");
transformed = controller_profile_runtime_transform(0, snapshot);
require(active_snapshot_count == reads_before_disconnect,
"an unchanged inactive slot repeatedly cleared or fetched state");
snapshot.active = true;
transformed = controller_profile_runtime_transform(0, snapshot);
require(active_snapshot_count == reads_before_disconnect + 1 &&
transformed.state.button_north,
"reconnection did not reload a cleared slot cache");
Bluepad32SlotSnapshot default_snapshot = make_snapshot(2);
rows[2].profiles[0] =
controller_profile_default(rows[2].identity, 0);
++database_generation;
default_snapshot.state.button_east = true;
default_snapshot.state.dpad_left = true;
default_snapshot.state.left_trigger = 32123;
default_snapshot.state.right_trigger = 54321;
default_snapshot.state.left_stick_x = -12345;
default_snapshot.state.right_stick_y = 23456;
default_snapshot.state.motion_sample_count = 2;
default_snapshot.state.motion_samples[0] = {1, 2, 3, 4, 5, 6};
default_snapshot.state.motion_samples[1] = {-1, -2, -3, -4, -5, -6};
transformed = controller_profile_runtime_transform(2, default_snapshot);
require(transformed.state.button_east && transformed.state.dpad_left &&
transformed.state.left_trigger == 32123 &&
transformed.state.right_trigger == 54321 &&
transformed.state.left_stick_x == -12345 &&
transformed.state.right_stick_y == 23456 &&
motion_equal(transformed.state, default_snapshot.state) &&
transformed.left_trigger_digital_threshold ==
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD &&
transformed.right_trigger_digital_threshold ==
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD,
"default runtime profile changed serializer input or motion");
}
void test_analog_thresholds_rumble_and_local_confirmation() {
prepare_profiles();
Bluepad32SlotSnapshot first = make_snapshot(0);
Bluepad32SlotSnapshot second = make_snapshot(1);
first.state.left_trigger = second.state.left_trigger = 23456;
first.state.right_trigger = second.state.right_trigger = 45678;
const ControllerProfileTransformResult first_output =
controller_profile_runtime_transform(0, first);
const ControllerProfileTransformResult second_output =
controller_profile_runtime_transform(1, second);
require(first_output.state.left_trigger ==
second_output.state.left_trigger &&
first_output.state.right_trigger ==
second_output.state.right_trigger &&
first_output.left_trigger_digital_threshold !=
second_output.left_trigger_digital_threshold,
"digital thresholds changed XInput-visible analog trigger state");
const ControllerRumbleOutput host{91, 73};
const ControllerRumbleOutput first_host =
controller_profile_runtime_scale_host_rumble(0, first, host);
const ControllerRumbleOutput second_host =
controller_profile_runtime_scale_host_rumble(1, second, host);
require(first_host.low_frequency_magnitude == 0 &&
first_host.high_frequency_magnitude == 73 &&
second_host.low_frequency_magnitude == 91 &&
second_host.high_frequency_magnitude == 0,
"host rumble was not scaled through each slot profile");
const ControllerProfileRuntimeLocalConfirmation confirmation =
controller_profile_runtime_local_confirmation(0, first, host);
require(confirmation.rumble.low_frequency_magnitude == 91 &&
confirmation.rumble.high_frequency_magnitude == 73 &&
confirmation.policy ==
ControllerProfileConfirmationPolicy::kLed,
"local confirmation was scaled or lost its profile policy");
}
} // namespace
uint32_t profile_service_database_generation() {
return database_generation;
}
void profile_service_active_profile_snapshot(
const ControllerIdentity& identity,
ProfileServiceActiveProfileSnapshot* output) {
if (output == nullptr) {
return;
}
++active_snapshot_count;
*output = {};
output->metadata.state = ProfileServiceState::kReady;
output->metadata.generation = database_generation;
for (const FakeProfileRow& row : rows) {
if (!controller_identity_equal(row.identity, identity)) {
continue;
}
output->valid = true;
output->profile_index = row.active_profile;
output->profile = row.profiles[row.active_profile];
return;
}
}
int main() {
test_four_slot_cache_and_unchanged_generation();
test_activation_disconnect_and_default_preservation();
test_analog_thresholds_rumble_and_local_confirmation();
return 0;
}

View file

@ -1,5 +1,6 @@
#include "controller_identity.h"
#include "controller_profile.h"
#include "tests/controller_profile_legacy_fixtures.h"
#include <cstdlib>
#include <cstring>
@ -44,9 +45,9 @@ void test_profile_wire_schema() {
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 &&
require(encoded[0] == 2 && encoded[1] == 0 &&
encoded[2] == 0 && encoded[3] == 1,
"profile header is not little-endian v1/256");
"profile header is not little-endian v2/256");
for (uint8_t index = 0;
index < CONTROLLER_PROFILE_LOGICAL_BUTTON_COUNT; ++index) {
require(encoded[4 + index] == index,
@ -56,7 +57,12 @@ void test_profile_wire_schema() {
encoded[30] == 0,
"default stick encoding changed");
require(encoded[54] == 0xff && encoded[55] == 0xff &&
encoded[58] == 0x00 && encoded[59] == 0x80,
encoded[58] ==
static_cast<uint8_t>(
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD) &&
encoded[59] ==
static_cast<uint8_t>(
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD >> 8),
"default trigger encoding changed");
require(encoded[72] == 0xff && encoded[73] == 0xff &&
encoded[74] == 3 && encoded[78] == 0xff &&
@ -84,11 +90,35 @@ void test_profile_wire_schema() {
invalid.sticks[0].outer_saturation;
require(!controller_profile_validate(invalid),
"empty stick range was accepted");
ControllerProfile boundary = profile;
boundary.triggers[0].lower_deadzone = 30000;
boundary.triggers[0].upper_saturation = 40000;
boundary.triggers[0].digital_threshold = 0;
uint8_t boundary_encoded[CONTROLLER_PROFILE_ENCODED_SIZE]{};
require(controller_profile_encode(boundary, boundary_encoded,
sizeof(boundary_encoded)) &&
controller_profile_decode(boundary_encoded,
sizeof(boundary_encoded),
&decoded) &&
decoded.triggers[0].digital_threshold == 0,
"current profile rejected zero transformed trigger threshold");
boundary.triggers[0].digital_threshold = UINT16_MAX;
require(controller_profile_encode(boundary, boundary_encoded,
sizeof(boundary_encoded)) &&
controller_profile_decode(boundary_encoded,
sizeof(boundary_encoded),
&decoded) &&
decoded.triggers[0].digital_threshold == UINT16_MAX,
"current profile rejected maximum transformed trigger threshold");
invalid = profile;
invalid.triggers[0].digital_threshold = 0;
invalid.triggers[0].lower_deadzone = 1;
invalid.triggers[0].lower_deadzone =
invalid.triggers[0].upper_saturation;
require(!controller_profile_validate(invalid),
"trigger threshold outside its range was accepted");
"empty raw trigger range was accepted");
invalid.triggers[0].lower_deadzone = UINT16_MAX;
invalid.triggers[0].upper_saturation = UINT16_MAX - 1;
require(!controller_profile_validate(invalid),
"reversed raw trigger range was accepted");
invalid = profile;
invalid.turbo_modes[0] =
static_cast<ControllerProfileTurboMode>(3);
@ -110,6 +140,97 @@ void test_profile_wire_schema() {
"macro without a final end was accepted");
}
void test_legacy_profile_migration() {
ControllerProfile migrated{};
require(controller_profile_decode(
kLegacyDefaultProfile, sizeof(kLegacyDefaultProfile),
&migrated),
"legacy default profile did not decode");
require(migrated.triggers[0].digital_threshold ==
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD &&
migrated.triggers[1].digital_threshold ==
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD,
"legacy inherited thresholds were not migrated");
uint8_t encoded[CONTROLLER_PROFILE_ENCODED_SIZE]{};
require(controller_profile_encode(migrated, encoded, sizeof(encoded)),
"migrated default profile did not encode");
for (size_t index = 0; index < sizeof(encoded); ++index) {
const bool schema_byte = index == 0;
const bool threshold_byte =
(index >= 58 && index < 60) ||
(index >= 68 && index < 70);
if (!schema_byte && !threshold_byte) {
require(encoded[index] == kLegacyDefaultProfile[index],
"legacy default profile changed an unrelated byte");
}
}
require(encoded[0] == CONTROLLER_PROFILE_SCHEMA_VERSION &&
encoded[58] ==
static_cast<uint8_t>(
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD) &&
encoded[59] ==
static_cast<uint8_t>(
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD >> 8) &&
encoded[68] ==
static_cast<uint8_t>(
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD) &&
encoded[69] ==
static_cast<uint8_t>(
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD >> 8),
"migrated default profile did not encode as v2");
require(controller_profile_decode(
kLegacyNarrowRawRangeProfile,
sizeof(kLegacyNarrowRawRangeProfile), &migrated),
"legacy narrow-raw-range profile did not decode");
require(migrated.triggers[0].lower_deadzone == 30000 &&
migrated.triggers[0].upper_saturation == 40000 &&
migrated.triggers[0].digital_threshold ==
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD &&
migrated.triggers[1].lower_deadzone == 30000 &&
migrated.triggers[1].upper_saturation == 40000 &&
migrated.triggers[1].digital_threshold ==
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD,
"legacy narrow raw range or inherited threshold was not migrated");
require(controller_profile_encode(migrated, encoded, sizeof(encoded)),
"migrated narrow-raw-range profile did not encode");
for (size_t index = 0; index < sizeof(encoded); ++index) {
const bool schema_byte = index == 0;
const bool threshold_byte =
(index >= 58 && index < 60) ||
(index >= 68 && index < 70);
if (!schema_byte && !threshold_byte) {
require(
encoded[index] == kLegacyNarrowRawRangeProfile[index],
"narrow-raw-range migration changed unrelated profile data");
}
}
require(controller_profile_decode(
kLegacyCustomThresholdProfile,
sizeof(kLegacyCustomThresholdProfile), &migrated),
"legacy custom-threshold profile did not decode");
require(migrated.triggers[0].digital_threshold == 0x1234 &&
migrated.triggers[1].digital_threshold == 0xabcd,
"legacy custom thresholds were not preserved");
require(controller_profile_encode(migrated, encoded, sizeof(encoded)),
"legacy custom-threshold profile did not re-encode");
for (size_t index = 1; index < sizeof(encoded); ++index) {
require(encoded[index] == kLegacyCustomThresholdProfile[index],
"legacy custom-threshold profile changed data");
}
ControllerProfile current =
controller_profile_default(controller_identity_global(), 0);
current.triggers[0].digital_threshold = 0x8000;
require(controller_profile_encode(current, encoded, sizeof(encoded)) &&
controller_profile_decode(encoded, sizeof(encoded),
&migrated) &&
migrated.triggers[0].digital_threshold == 0x8000,
"v2 custom threshold matching the legacy default was migrated");
}
void test_database_round_trip_and_capacity() {
controller_profile_database_default(&database);
for (uint8_t index = 0;
@ -141,6 +262,10 @@ void test_database_round_trip_and_capacity() {
database, offset, &encoded_database[offset], size),
"database range did not encode");
}
require(encoded_database[4] ==
CONTROLLER_PROFILE_DATABASE_SCHEMA_VERSION &&
encoded_database[5] == 0,
"database encoder did not emit v2");
require(controller_profile_database_decode(
read_encoded_database, nullptr, &decoded_database),
"database did not decode");
@ -157,6 +282,7 @@ void test_database_round_trip_and_capacity() {
} // namespace
int main() {
test_profile_wire_schema();
test_legacy_profile_migration();
test_database_round_trip_and_capacity();
return 0;
}

View file

@ -0,0 +1,385 @@
#include "controller_identity.h"
#include "controller_profile.h"
#include "controller_profile_transform.h"
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <limits.h>
namespace {
void require(bool condition, const char* message) {
if (!condition) {
std::cerr << message << '\n';
std::exit(1);
}
}
ControllerProfile default_profile() {
return controller_profile_default(controller_identity_global(), 0);
}
bool states_equal(const ControllerState& left, const ControllerState& right) {
return left.dpad_up == right.dpad_up &&
left.dpad_down == right.dpad_down &&
left.dpad_left == right.dpad_left &&
left.dpad_right == right.dpad_right &&
left.button_south == right.button_south &&
left.button_east == right.button_east &&
left.button_west == right.button_west &&
left.button_north == right.button_north &&
left.button_left_shoulder == right.button_left_shoulder &&
left.button_right_shoulder == right.button_right_shoulder &&
left.button_select == right.button_select &&
left.button_start == right.button_start &&
left.button_system == right.button_system &&
left.button_capture == right.button_capture &&
left.button_left_stick == right.button_left_stick &&
left.button_right_stick == right.button_right_stick &&
left.left_trigger == right.left_trigger &&
left.right_trigger == right.right_trigger &&
left.left_stick_x == right.left_stick_x &&
left.left_stick_y == right.left_stick_y &&
left.right_stick_x == right.right_stick_x &&
left.right_stick_y == right.right_stick_y &&
left.motion_sample_count == right.motion_sample_count &&
std::memcmp(left.motion_samples, right.motion_samples,
sizeof(left.motion_samples)) == 0;
}
ControllerProfileTransformResult transform_left_stick(
const ControllerProfileStickConfiguration& configuration, int16_t x,
int16_t y) {
ControllerProfile profile = default_profile();
profile.sticks[0] = configuration;
ControllerState state{};
state.left_stick_x = x;
state.left_stick_y = y;
return controller_profile_transform(state, profile);
}
uint16_t transform_left_trigger(
const ControllerProfileTriggerConfiguration& configuration,
uint16_t value) {
ControllerProfile profile = default_profile();
profile.triggers[0] = configuration;
ControllerState state{};
state.left_trigger = value;
return controller_profile_transform(state, profile).state.left_trigger;
}
void test_button_masks_and_direct_mapping() {
ControllerState state{};
controller_profile_apply_button_mask(UINT16_MAX, &state);
require(controller_profile_extract_button_mask(state) == UINT16_MAX,
"button mask application omitted a logical button");
controller_profile_apply_button_mask(0, &state);
require(controller_profile_extract_button_mask(state) == 0,
"zero button mask did not clear every logical button");
controller_profile_apply_button_mask(UINT16_MAX, nullptr);
for (uint8_t input = 0;
input < CONTROLLER_PROFILE_LOGICAL_BUTTON_COUNT; ++input) {
ControllerProfile profile = default_profile();
const uint8_t output = static_cast<uint8_t>(
CONTROLLER_PROFILE_LOGICAL_BUTTON_COUNT - 1u - input);
profile.button_map[input] = output;
state = {};
controller_profile_apply_button_mask(
static_cast<uint16_t>(1u << input), &state);
const ControllerProfileTransformResult transformed =
controller_profile_transform(state, profile);
require(controller_profile_extract_button_mask(transformed.state) ==
static_cast<uint16_t>(1u << output),
"a logical button did not map directly to its output");
}
ControllerProfile profile = default_profile();
profile.button_map[0] = 1;
profile.button_map[1] = 2;
require(controller_profile_map_button_mask(1u, profile) == 2u,
"button mapping recursively remapped an output");
profile.button_map[1] = 1;
require(controller_profile_map_button_mask(3u, profile) == 2u,
"duplicate mapped outputs were not combined");
profile.button_map[0] = CONTROLLER_PROFILE_NO_BUTTON;
require(controller_profile_map_button_mask(1u, profile) == 0,
"disabled button mapping still produced output");
ControllerProfile invalid = default_profile();
invalid.button_map[0] = CONTROLLER_PROFILE_LOGICAL_BUTTON_COUNT;
require(!controller_profile_validate(invalid),
"logical output 16 was accepted");
invalid.button_map[0] = 0xfe;
require(!controller_profile_validate(invalid),
"logical output 0xfe was accepted");
invalid.button_map[0] = CONTROLLER_PROFILE_NO_BUTTON;
require(controller_profile_validate(invalid),
"disabled logical output 0xff was rejected");
}
void test_stick_center_boundaries_and_inversion() {
ControllerProfileStickConfiguration configuration{};
configuration.center_x = 1234;
configuration.center_y = -2345;
configuration.inner_deadzone = 1000;
configuration.outer_saturation = 20000;
configuration.curve_q8_8 = 256;
ControllerProfileTransformResult transformed = transform_left_stick(
configuration, configuration.center_x, configuration.center_y);
require(transformed.state.left_stick_x == 0 &&
transformed.state.left_stick_y == 0,
"center calibration did not precede stick shaping");
transformed = transform_left_stick(
configuration,
static_cast<int16_t>(configuration.center_x +
configuration.inner_deadzone),
configuration.center_y);
require(transformed.state.left_stick_x == 0 &&
transformed.state.left_stick_y == 0,
"inner deadzone boundary was not neutral");
transformed = transform_left_stick(
configuration,
static_cast<int16_t>(configuration.center_x +
configuration.inner_deadzone + 1),
configuration.center_y);
require(transformed.state.left_stick_x > 0,
"first value outside inner deadzone stayed neutral");
configuration.center_x = 0;
configuration.center_y = 0;
configuration.inner_deadzone = 0;
configuration.outer_saturation = 20000;
transformed = transform_left_stick(configuration, 19999, 0);
require(transformed.state.left_stick_x > 0 &&
transformed.state.left_stick_x < INT16_MAX,
"value below outer saturation reached an endpoint");
transformed = transform_left_stick(configuration, 20000, 0);
require(transformed.state.left_stick_x == INT16_MAX,
"positive outer saturation boundary missed endpoint");
transformed = transform_left_stick(configuration, -20000, 0);
require(transformed.state.left_stick_x == INT16_MIN,
"negative outer saturation boundary missed endpoint");
configuration.outer_saturation = 32767;
configuration.invert_x = true;
configuration.invert_y = true;
transformed = transform_left_stick(configuration, INT16_MIN, 0);
require(transformed.state.left_stick_x == INT16_MAX,
"negative stick endpoint did not invert to positive endpoint");
transformed = transform_left_stick(configuration, 0, INT16_MAX);
require(transformed.state.left_stick_y == INT16_MIN,
"positive stick endpoint did not invert to negative endpoint");
configuration.invert_x = false;
configuration.invert_y = false;
configuration.outer_saturation = 20000;
transformed = transform_left_stick(configuration, 15000, 15000);
require(transformed.state.left_stick_x < INT16_MAX &&
transformed.state.left_stick_y < INT16_MAX,
"stick magnitude was not Chebyshev magnitude");
}
void test_stick_curves_and_monotonicity() {
ControllerProfileStickConfiguration linear{};
linear.inner_deadzone = 0;
linear.outer_saturation = 32767;
linear.curve_q8_8 = 256;
ControllerProfileStickConfiguration slow = linear;
slow.curve_q8_8 = 512;
ControllerProfileStickConfiguration fast = linear;
fast.curve_q8_8 = 128;
const int16_t linear_mid =
transform_left_stick(linear, 16384, 0).state.left_stick_x;
const int16_t slow_mid =
transform_left_stick(slow, 16384, 0).state.left_stick_x;
const int16_t fast_mid =
transform_left_stick(fast, 16384, 0).state.left_stick_x;
require(slow_mid < linear_mid && linear_mid < fast_mid,
"stick curve directions are reversed or ineffective");
require(transform_left_stick(slow, 0, 0).state.left_stick_x == 0 &&
transform_left_stick(fast, 32767, 0)
.state.left_stick_x == INT16_MAX,
"stick response curve did not preserve endpoints");
const uint16_t curves[] = {1, 128, 256, 512, UINT16_MAX};
for (uint16_t curve : curves) {
ControllerProfileStickConfiguration configuration = linear;
configuration.curve_q8_8 = curve;
int16_t previous = 0;
for (int32_t input = 0; input <= INT16_MAX; ++input) {
const int16_t output = transform_left_stick(
configuration,
static_cast<int16_t>(input), 0)
.state.left_stick_x;
require(output >= previous,
"stick response was not monotonic");
previous = output;
}
require(previous == INT16_MAX,
"monotonic stick response missed positive endpoint");
}
}
void test_trigger_boundaries_curves_and_thresholds() {
ControllerProfileTriggerConfiguration configuration{};
configuration.lower_deadzone = 1000;
configuration.upper_saturation = 60000;
configuration.curve_q8_8 = 256;
configuration.digital_threshold = 32000;
require(transform_left_trigger(configuration, 999) == 0 &&
transform_left_trigger(configuration, 1000) == 0,
"trigger lower deadzone boundary was not zero");
require(transform_left_trigger(configuration, 1001) > 0,
"first trigger value above lower deadzone stayed zero");
require(transform_left_trigger(configuration, 59999) < UINT16_MAX &&
transform_left_trigger(configuration, 60000) == UINT16_MAX &&
transform_left_trigger(configuration, UINT16_MAX) ==
UINT16_MAX,
"trigger upper saturation boundary missed full scale");
ControllerProfileTriggerConfiguration slow = configuration;
slow.curve_q8_8 = 512;
ControllerProfileTriggerConfiguration fast = configuration;
fast.curve_q8_8 = 128;
const uint16_t midpoint = static_cast<uint16_t>(
(static_cast<uint32_t>(configuration.lower_deadzone) +
configuration.upper_saturation) /
2u);
const uint16_t linear_mid =
transform_left_trigger(configuration, midpoint);
const uint16_t slow_mid = transform_left_trigger(slow, midpoint);
const uint16_t fast_mid = transform_left_trigger(fast, midpoint);
require(slow_mid < linear_mid && linear_mid < fast_mid,
"trigger curve directions are reversed or ineffective");
require(transform_left_trigger(slow, configuration.lower_deadzone) == 0 &&
transform_left_trigger(fast,
configuration.upper_saturation) ==
UINT16_MAX,
"trigger response curve did not preserve endpoints");
uint16_t previous = 0;
for (uint32_t input = 0; input <= UINT16_MAX; ++input) {
const uint16_t output = transform_left_trigger(
slow, static_cast<uint16_t>(input));
require(output >= previous,
"trigger response was not monotonic");
previous = output;
}
ControllerProfile profile = default_profile();
profile.triggers[0] = slow;
profile.triggers[0].digital_threshold = 12345;
profile.triggers[1].digital_threshold = 54321;
ControllerState state{};
state.left_trigger = midpoint;
const ControllerProfileTransformResult transformed =
controller_profile_transform(state, profile);
require(transformed.left_trigger_digital_threshold == 12345 &&
transformed.right_trigger_digital_threshold == 54321,
"profile-owned digital thresholds were not returned");
}
void test_default_whole_state_equivalence() {
ControllerState input{};
input.dpad_up = true;
input.dpad_right = true;
input.button_south = true;
input.button_west = true;
input.button_left_shoulder = true;
input.button_select = true;
input.button_system = true;
input.button_capture = true;
input.button_right_stick = true;
input.left_trigger = 0;
input.right_trigger = UINT16_MAX;
input.left_stick_x = INT16_MIN;
input.left_stick_y = INT16_MAX;
input.right_stick_x = INT16_MAX;
input.right_stick_y = INT16_MIN;
input.motion_sample_count = CONTROLLER_MOTION_SAMPLE_CAPACITY;
input.motion_samples[0] =
{INT16_MIN, -30000, -1, 0, 1, INT16_MAX};
input.motion_samples[1] = {1, 2, 3, 4, 5, 6};
input.motion_samples[2] =
{INT16_MAX, 30000, 1, 0, -1, INT16_MIN};
const ControllerProfile profile = default_profile();
const ControllerProfileTransformResult transformed =
controller_profile_transform(input, profile);
require(states_equal(input, transformed.state),
"default profile changed whole controller state or motion");
require(transformed.left_trigger_digital_threshold ==
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD &&
transformed.right_trigger_digital_threshold ==
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD &&
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD == 22934,
"default digital threshold changed");
for (uint32_t trigger = 0; trigger <= UINT16_MAX; ++trigger) {
ControllerState trigger_state{};
trigger_state.left_trigger = static_cast<uint16_t>(trigger);
trigger_state.right_trigger = static_cast<uint16_t>(trigger);
const ControllerState output =
controller_profile_transform(trigger_state, profile).state;
require(output.left_trigger == trigger_state.left_trigger &&
output.right_trigger == trigger_state.right_trigger,
"default profile lost full trigger analog precision");
}
}
void test_rumble_scaling_and_confirmation_policy() {
ControllerProfile profile = default_profile();
for (uint16_t magnitude = 0; magnitude <= UINT8_MAX; ++magnitude) {
const ControllerRumbleOutput input{
static_cast<uint8_t>(magnitude),
static_cast<uint8_t>(UINT8_MAX - magnitude),
};
const ControllerRumbleOutput output =
controller_profile_scale_host_rumble(input, profile);
require(output.low_frequency_magnitude ==
input.low_frequency_magnitude &&
output.high_frequency_magnitude ==
input.high_frequency_magnitude,
"default rumble scaling was not bit-exact identity");
}
profile.strong_rumble_scale = 0;
profile.weak_rumble_scale = 0;
ControllerRumbleOutput output = controller_profile_scale_host_rumble(
{UINT8_MAX, UINT8_MAX}, profile);
require(output.low_frequency_magnitude == 0 &&
output.high_frequency_magnitude == 0,
"zero rumble scales did not mute both bands");
profile.strong_rumble_scale = 128;
profile.weak_rumble_scale = 64;
output = controller_profile_scale_host_rumble({200, 201}, profile);
require(output.low_frequency_magnitude == 100 &&
output.high_frequency_magnitude == 50,
"strong/weak mid-scale rumble mapping was incorrect");
require(controller_profile_scale_rumble_magnitude(UINT8_MAX,
UINT8_MAX) ==
UINT8_MAX,
"full rumble scaling did not saturate at uint8 maximum");
profile.confirmation_policy = ControllerProfileConfirmationPolicy::kLed;
require(controller_profile_confirmation_policy(profile) ==
ControllerProfileConfirmationPolicy::kLed,
"confirmation policy was not exposed unchanged");
}
} // namespace
int main() {
test_button_masks_and_direct_mapping();
test_stick_center_boundaries_and_inversion();
test_stick_curves_and_monotonicity();
test_trigger_boundaries_curves_and_thresholds();
test_default_whole_state_equivalence();
test_rumble_scaling_and_confirmation_policy();
return 0;
}

View file

@ -82,6 +82,13 @@ ProfileServiceTransactionSnapshot transaction_snapshot() {
return snapshot;
}
ProfileServiceActiveProfileSnapshot active_profile_snapshot(
const ControllerIdentity& identity) {
ProfileServiceActiveProfileSnapshot snapshot{};
profile_service_active_profile_snapshot(identity, &snapshot);
return snapshot;
}
ControllerProfileDatabase reload_database(
const ProfileServiceTransactionSnapshot& transaction,
uint32_t expected_generation) {
@ -102,6 +109,14 @@ 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();
ProfileServiceActiveProfileSnapshot active =
active_profile_snapshot(controller_identity_global());
require(active.valid &&
active.metadata.state == ProfileServiceState::kReady &&
active.metadata.generation == 0 &&
profile_service_database_generation() == 0 &&
active.profile_index == 0,
"initial active profile snapshot was not coherent");
const ControllerIdentity identity = controller_identity_global();
constexpr uint8_t kProfileIndex = 2;
@ -128,6 +143,11 @@ void test_pending_commands_are_not_decoded_as_profile_writes() {
require(transaction_snapshot().transaction.status ==
ConfigurationTransactionStatus::kCommitted,
"profile write baseline did not commit");
active = active_profile_snapshot(identity);
require(active.valid && active.metadata.generation == 1 &&
profile_service_database_generation() == 1 &&
active.profile_index == 0,
"profile write did not publish one coherent generation");
constexpr uint32_t kResetTransactionId = 0xa5a55a5a;
require(profile_service_reset(kResetTransactionId, identity,
@ -150,6 +170,11 @@ void test_pending_commands_are_not_decoded_as_profile_writes() {
require(recovered.fallback_profiles[kProfileIndex].strong_rumble_scale ==
UINT8_MAX,
"terminal reset status was published before reset persisted");
active = active_profile_snapshot(identity);
require(active.valid && active.metadata.generation == 2 &&
profile_service_database_generation() == 2 &&
active.profile_index == 0,
"profile reset did not refresh the active snapshot generation");
constexpr uint32_t kActivateTransactionId = 0x50607080;
constexpr uint8_t kActivatedProfile = 3;
@ -162,6 +187,10 @@ void test_pending_commands_are_not_decoded_as_profile_writes() {
activate.transaction.status ==
ConfigurationTransactionStatus::kPending,
"pending activation lost its transaction identity");
active = active_profile_snapshot(identity);
require(active.valid && active.metadata.generation == 2 &&
active.profile_index == 0,
"pending activation leaked an uncommitted active profile");
profile_service_task_on_storage_core(2000);
activate = transaction_snapshot();
@ -172,6 +201,14 @@ void test_pending_commands_are_not_decoded_as_profile_writes() {
recovered = reload_database(activate, 3);
require(recovered.fallback_active_profile == kActivatedProfile,
"terminal activation status was published before activation persisted");
active = active_profile_snapshot(identity);
require(active.valid && active.metadata.generation == 3 &&
profile_service_database_generation() == 3 &&
active.profile_index == kActivatedProfile &&
active.profile.strong_rumble_scale ==
recovered.fallback_profiles[kActivatedProfile]
.strong_rumble_scale,
"activation did not publish profile, index, and generation together");
}
} // namespace

View file

@ -1,6 +1,7 @@
#include "controller_identity.h"
#include "controller_profile.h"
#include "profile_storage.h"
#include "tests/controller_profile_legacy_fixtures.h"
#include <cstdlib>
#include <cstring>
@ -15,6 +16,7 @@ struct FakeFlash {
bool corrupt_next_program = false;
bool fail_reads_after_header_program = false;
bool header_programmed = false;
int erase_count = 0;
};
FakeFlash flash{};
@ -35,6 +37,7 @@ void erase_all() {
flash.corrupt_next_program = false;
flash.fail_reads_after_header_program = false;
flash.header_programmed = false;
flash.erase_count = 0;
}
bool fake_read(void* context, uint8_t bank, size_t offset,
@ -62,6 +65,7 @@ bool fake_erase_sector(void* context, uint8_t bank, size_t offset) {
PROFILE_STORAGE_BANK_SIZE - offset) {
return false;
}
++storage->erase_count;
memset(&storage->bytes[bank][offset], 0xff,
PROFILE_STORAGE_SECTOR_SIZE);
return true;
@ -107,6 +111,91 @@ ProfileStorageIo fake_io() {
};
}
uint16_t fixture_read_u16(const uint8_t* input) {
return static_cast<uint16_t>(input[0]) |
static_cast<uint16_t>(input[1] << 8);
}
void fixture_write_u16(uint8_t* output, uint16_t value) {
output[0] = static_cast<uint8_t>(value);
output[1] = static_cast<uint8_t>(value >> 8);
}
void fixture_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);
}
void install_legacy_database_bank_fixture() {
erase_all();
constexpr uint8_t kBank = 1;
constexpr uint32_t kGeneration = 41;
constexpr size_t kFallbackOffset =
CONTROLLER_PROFILE_DATABASE_HEADER_SIZE;
constexpr size_t kEntryOffset =
kFallbackOffset +
CONTROLLER_PROFILE_COUNT * CONTROLLER_PROFILE_ENCODED_SIZE;
constexpr uint8_t kEntryHeader[CONTROLLER_PROFILE_DATABASE_ENTRY_HEADER_SIZE] = {
1, 1, 7, 0, 1, 2, 3, 4, 5, 6, 0x7e, 0x05, 0x09, 0x20, 3, 1,
};
uint8_t* const record = flash.bytes[kBank];
uint8_t* const payload = record + PROFILE_STORAGE_RECORD_HEADER_SIZE;
memset(record, 0, PROFILE_STORAGE_RECORD_HEADER_SIZE);
memset(payload, 0, CONTROLLER_PROFILE_DATABASE_ENCODED_SIZE);
memcpy(payload, "SPDB", 4);
fixture_write_u16(&payload[4],
CONTROLLER_PROFILE_DATABASE_LEGACY_SCHEMA_VERSION);
fixture_write_u16(
&payload[6],
static_cast<uint16_t>(CONTROLLER_PROFILE_DATABASE_ENCODED_SIZE));
payload[8] = CONTROLLER_PROFILE_STABLE_IDENTITY_CAPACITY;
payload[9] = CONTROLLER_PROFILE_COUNT;
payload[10] = 2;
payload[11] = 1;
for (uint8_t profile_index = 0;
profile_index < CONTROLLER_PROFILE_COUNT; ++profile_index) {
const uint8_t* fixture =
profile_index == 0
? kLegacyNarrowRawRangeProfile
: profile_index == 1 ? kLegacyCustomThresholdProfile
: kLegacyDefaultProfile;
memcpy(&payload[kFallbackOffset +
profile_index * CONTROLLER_PROFILE_ENCODED_SIZE],
fixture, CONTROLLER_PROFILE_ENCODED_SIZE);
}
memcpy(&payload[kEntryOffset], kEntryHeader, sizeof(kEntryHeader));
for (uint8_t profile_index = 0;
profile_index < CONTROLLER_PROFILE_COUNT; ++profile_index) {
const uint8_t* fixture =
profile_index == 0
? kLegacyNarrowRawRangeProfile
: profile_index == 3 ? kLegacyCustomThresholdProfile
: kLegacyDefaultProfile;
memcpy(&payload[kEntryOffset +
CONTROLLER_PROFILE_DATABASE_ENTRY_HEADER_SIZE +
profile_index * CONTROLLER_PROFILE_ENCODED_SIZE],
fixture, CONTROLLER_PROFILE_ENCODED_SIZE);
}
const uint32_t payload_crc = profile_storage_crc32(
payload, CONTROLLER_PROFILE_DATABASE_ENCODED_SIZE);
memcpy(record, "SPPF", 4);
fixture_write_u16(&record[4], 1);
fixture_write_u16(&record[6],
CONTROLLER_PROFILE_DATABASE_LEGACY_SCHEMA_VERSION);
fixture_write_u32(&record[8], kGeneration);
fixture_write_u32(
&record[12],
static_cast<uint32_t>(CONTROLLER_PROFILE_DATABASE_ENCODED_SIZE));
fixture_write_u32(&record[16], payload_crc);
fixture_write_u32(&record[20], profile_storage_crc32(record, 20));
}
void test_two_bank_recovery() {
erase_all();
controller_profile_database_default(&database);
@ -225,11 +314,180 @@ void test_payload_corruption_prevents_header_publication() {
"headerless corrupt payload was recovered");
}
void test_legacy_database_bank_migration() {
install_legacy_database_bank_fixture();
ProfileStorage storage;
require(storage.initialize(fake_io(), &recovered_database) &&
storage.snapshot().valid &&
storage.snapshot().active_bank == 1 &&
storage.snapshot().generation == 41,
"legacy v1 database bank was not selected");
require(flash.erase_count == 0,
"legacy bank admission erased flash");
require(recovered_database.fallback_active_profile == 2,
"legacy fallback active profile was not preserved");
for (uint8_t profile_index = 0;
profile_index < CONTROLLER_PROFILE_COUNT; ++profile_index) {
const uint16_t expected_left =
profile_index == 1
? 0x1234
: CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD;
const uint16_t expected_right =
profile_index == 1
? 0xabcd
: CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD;
require(recovered_database.fallback_profiles[profile_index]
.triggers[0]
.digital_threshold == expected_left &&
recovered_database.fallback_profiles[profile_index]
.triggers[1]
.digital_threshold == expected_right,
"legacy fallback thresholds were not selectively migrated");
}
require(recovered_database.fallback_profiles[0]
.triggers[0]
.lower_deadzone == 30000 &&
recovered_database.fallback_profiles[0]
.triggers[0]
.upper_saturation == 40000 &&
recovered_database.fallback_profiles[0]
.triggers[1]
.lower_deadzone == 30000 &&
recovered_database.fallback_profiles[0]
.triggers[1]
.upper_saturation == 40000,
"legacy fallback raw trigger ranges were not preserved");
const ControllerProfileDatabaseEntry& entry =
recovered_database.entries[0];
require(entry.used && entry.active_profile == 3 &&
entry.identity.stable &&
entry.identity.transport == ControllerTransport::kClassic &&
entry.identity.address_type == 7 &&
entry.identity.address[0] == 1 &&
entry.identity.address[5] == 6 &&
entry.identity.vendor_id == 0x057e &&
entry.identity.product_id == 0x2009,
"legacy entry identity or active profile was not preserved");
for (uint8_t profile_index = 0;
profile_index < CONTROLLER_PROFILE_COUNT; ++profile_index) {
const uint16_t expected_left =
profile_index == 3
? 0x1234
: CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD;
const uint16_t expected_right =
profile_index == 3
? 0xabcd
: CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD;
require(entry.profiles[profile_index]
.triggers[0]
.digital_threshold == expected_left &&
entry.profiles[profile_index]
.triggers[1]
.digital_threshold == expected_right,
"legacy entry thresholds were not selectively migrated");
}
require(entry.profiles[0].triggers[0].lower_deadzone == 30000 &&
entry.profiles[0].triggers[0].upper_saturation == 40000 &&
entry.profiles[0].triggers[1].lower_deadzone == 30000 &&
entry.profiles[0].triggers[1].upper_saturation == 40000,
"legacy entry raw trigger ranges were not preserved");
recovered_database.fallback_profiles[2].weak_rumble_scale = 17;
require(storage.commit(recovered_database) ==
ProfileStorageResult::kOk &&
storage.snapshot().active_bank == 0 &&
storage.snapshot().generation == 42,
"mutation after legacy admission did not commit");
const uint8_t* const current_record = flash.bytes[0];
const uint8_t* const current_payload =
current_record + PROFILE_STORAGE_RECORD_HEADER_SIZE;
require(fixture_read_u16(&current_record[6]) ==
CONTROLLER_PROFILE_DATABASE_SCHEMA_VERSION &&
fixture_read_u16(&current_payload[4]) ==
CONTROLLER_PROFILE_DATABASE_SCHEMA_VERSION,
"post-migration commit did not emit v2 storage schemas");
constexpr size_t kFallbackOffset =
CONTROLLER_PROFILE_DATABASE_HEADER_SIZE;
constexpr size_t kEntryOffset =
kFallbackOffset +
CONTROLLER_PROFILE_COUNT * CONTROLLER_PROFILE_ENCODED_SIZE;
for (uint8_t profile_index = 0;
profile_index < CONTROLLER_PROFILE_COUNT; ++profile_index) {
require(fixture_read_u16(
&current_payload[kFallbackOffset +
profile_index *
CONTROLLER_PROFILE_ENCODED_SIZE]) ==
CONTROLLER_PROFILE_SCHEMA_VERSION &&
fixture_read_u16(
&current_payload[
kEntryOffset +
CONTROLLER_PROFILE_DATABASE_ENTRY_HEADER_SIZE +
profile_index *
CONTROLLER_PROFILE_ENCODED_SIZE]) ==
CONTROLLER_PROFILE_SCHEMA_VERSION,
"post-migration commit retained a v1 profile");
}
require(fixture_read_u16(&flash.bytes[1][6]) ==
CONTROLLER_PROFILE_DATABASE_LEGACY_SCHEMA_VERSION &&
fixture_read_u16(
&flash.bytes[1][PROFILE_STORAGE_RECORD_HEADER_SIZE + 4]) ==
CONTROLLER_PROFILE_DATABASE_LEGACY_SCHEMA_VERSION,
"post-migration commit erased or rewrote the admitted legacy bank");
ProfileStorage reloaded;
require(reloaded.initialize(fake_io(), &database) &&
reloaded.snapshot().generation == 42 &&
database.fallback_profiles[2].weak_rumble_scale == 17 &&
database.fallback_profiles[0]
.triggers[0]
.lower_deadzone == 30000 &&
database.fallback_profiles[0]
.triggers[0]
.upper_saturation == 40000 &&
database.fallback_profiles[0]
.triggers[0]
.digital_threshold ==
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD &&
database.fallback_profiles[1]
.triggers[0]
.digital_threshold == 0x1234 &&
database.fallback_profiles[1]
.triggers[1]
.digital_threshold == 0xabcd &&
database.entries[0].used &&
database.entries[0].active_profile == 3 &&
database.entries[0]
.profiles[0]
.triggers[0]
.lower_deadzone == 30000 &&
database.entries[0]
.profiles[0]
.triggers[0]
.upper_saturation == 40000 &&
database.entries[0]
.profiles[0]
.triggers[0]
.digital_threshold ==
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD &&
database.entries[0]
.profiles[3]
.triggers[0]
.digital_threshold == 0x1234 &&
database.entries[0]
.profiles[3]
.triggers[1]
.digital_threshold == 0xabcd,
"v2 migration commit did not reload without data loss");
}
} // 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();
test_legacy_database_bank_migration();
return 0;
}

View file

@ -289,7 +289,9 @@ void test_input_reports_and_timers_are_isolated() {
states[3].button_west = true;
for (uint8_t instance = 0; instance < kInstanceCount; ++instance) {
switch_pro_set_input(instance, states[instance]);
switch_pro_set_input(instance, states[instance],
SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD,
SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD);
}
now_ms = 15;
@ -328,7 +330,9 @@ void test_input_reports_and_timers_are_isolated() {
ControllerState changed_zero = states[0];
changed_zero.button_east = false;
changed_zero.button_system = true;
switch_pro_set_input(0, changed_zero);
switch_pro_set_input(0, changed_zero,
SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD,
SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD);
now_ms = 30;
expect(switch_pro_task(0),
"instance 0 did not apply its changed input state");
@ -341,7 +345,9 @@ void test_input_reports_and_timers_are_isolated() {
ControllerState changed_three = states[3];
changed_three.button_west = false;
changed_three.button_capture = true;
switch_pro_set_input(3, changed_three);
switch_pro_set_input(3, changed_three,
SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD,
SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD);
now_ms = 45;
expect(switch_pro_task(3),
"instance 3 did not apply its changed input state");
@ -370,8 +376,10 @@ void test_callback_send_and_imu_modes_are_isolated() {
ControllerState one = zero;
one.button_north = true;
one.motion_samples[0] = {1001, 2002, 3003, 4004, 5005, 6006};
switch_pro_set_input(0, zero);
switch_pro_set_input(1, one);
switch_pro_set_input(0, zero, SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD,
SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD);
switch_pro_set_input(1, one, SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD,
SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD);
now_ms = 21;
expect(switch_pro_task(0), "raw-IMU instance did not send input");
expect(switch_pro_task(1), "off-IMU instance timer did not send input");
@ -400,8 +408,11 @@ void test_callback_send_and_imu_modes_are_isolated() {
stationary.right_stick_x = stationary.right_stick_y = 0;
stationary.motion_sample_count = 1;
stationary.motion_samples[0] = {1000, 2000, 3000, 0, 0, 0};
switch_pro_set_input(0, moving);
switch_pro_set_input(1, stationary);
switch_pro_set_input(0, moving, SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD,
SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD);
switch_pro_set_input(1, stationary,
SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD,
SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD);
now_ms = 21;
expect(switch_pro_task(0), "moving quaternion instance did not report");
expect(switch_pro_task(1), "stationary quaternion timer crossed instances");
@ -578,7 +589,9 @@ void test_lifecycle_and_invalid_instances() {
ControllerState ignored{};
ignored.button_system = true;
switch_pro_init(kInvalidInstance);
switch_pro_set_input(kInvalidInstance, ignored);
switch_pro_set_input(kInvalidInstance, ignored,
SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD,
SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD);
switch_pro_set_rumble_callback(kInvalidInstance, rumble_callback);
expect(!switch_pro_task(kInvalidInstance),
"invalid instance ran a driver task");
@ -598,7 +611,8 @@ void test_protocol_neutral_trigger_threshold() {
state.left_trigger =
static_cast<uint16_t>(SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD - 1u);
state.right_trigger = SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD;
switch_pro_set_input(0, state);
switch_pro_set_input(0, state, SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD,
SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD);
now_ms = 15;
expect(switch_pro_task(0), "trigger threshold report was not sent");
SwitchProReport report = copy_switch_report(latest_regular_report(0));
@ -607,7 +621,8 @@ void test_protocol_neutral_trigger_threshold() {
state.left_trigger = SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD;
state.right_trigger = CONTROLLER_TRIGGER_MIN;
switch_pro_set_input(0, state);
switch_pro_set_input(0, state, SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD,
SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD);
now_ms = 30;
expect(switch_pro_task(0), "second trigger threshold report was not sent");
report = copy_switch_report(latest_regular_report(0));
@ -615,13 +630,36 @@ void test_protocol_neutral_trigger_threshold() {
"Switch trigger threshold changed at the upper boundary");
}
void test_custom_trigger_thresholds_are_isolated() {
initialize_contexts();
ControllerState state{};
state.left_trigger = 300;
state.right_trigger = 300;
switch_pro_set_input(0, state, 300, 301);
switch_pro_set_input(1, state, 301, 300);
now_ms = 15;
expect(switch_pro_task(0) && switch_pro_task(1),
"custom trigger threshold reports were not sent");
const SwitchProReport first =
copy_switch_report(latest_regular_report(0));
const SwitchProReport second =
copy_switch_report(latest_regular_report(1));
expect(first.inputs.buttonZL && !first.inputs.buttonZR,
"instance 0 did not use its exact left/right trigger thresholds");
expect(!second.inputs.buttonZL && second.inputs.buttonZR,
"instance 1 trigger thresholds crossed HID contexts");
}
void test_uart_parser_is_pure() {
initialize_contexts();
ControllerState driver_state{};
driver_state.left_stick_x = driver_state.left_stick_y =
driver_state.right_stick_x = driver_state.right_stick_y = 0;
driver_state.button_north = true;
switch_pro_set_input(0, driver_state);
switch_pro_set_input(0, driver_state,
SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD,
SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD);
now_ms = 15;
switch_pro_task(0);
@ -724,6 +762,7 @@ int main() {
test_grip_colors_are_isolated();
test_lifecycle_and_invalid_instances();
test_protocol_neutral_trigger_threshold();
test_custom_trigger_thresholds_are_isolated();
test_uart_parser_is_pure();
if (failures != 0) {
std::cerr << failures << " driver context test(s) failed\n";

View file

@ -500,6 +500,13 @@ def test_identity_and_profile_binary_json_round_trip() -> None:
with pytest.raises(config_manager.ConfigManagerError):
config_manager.ControllerIdentity.from_bytes(malformed_identity)
default_profile = config_manager.ControllerProfile.default()
assert default_profile.left_trigger.digital_threshold == 22934
assert default_profile.right_trigger.digital_threshold == 22934
default_wire = default_profile.to_bytes()
assert struct.unpack_from("<H", default_wire, 58)[0] == 22934
assert struct.unpack_from("<H", default_wire, 68)[0] == 22934
profile = custom_profile()
encoded = profile.to_bytes()
assert len(encoded) == config_manager.PROFILE_SIZE
@ -513,11 +520,143 @@ def test_identity_and_profile_binary_json_round_trip() -> None:
assert config_manager.ControllerProfile.from_bytes(encoded) == profile
serialized = profile.to_json()
assert serialized.startswith('{\n "schema_version": 1,\n "size": 256,')
assert serialized.startswith('{\n "schema_version": 2,\n "size": 256,')
decoded = config_manager.ControllerProfile.from_json(serialized)
assert decoded == profile
assert decoded.to_json() == serialized
legacy_default_wire = bytearray(default_wire)
struct.pack_into(
"<H",
legacy_default_wire,
0,
config_manager.PROFILE_LEGACY_SCHEMA_VERSION,
)
struct.pack_into(
"<HHHH",
legacy_default_wire,
52,
30000,
40000,
256,
config_manager.PROFILE_LEGACY_DEFAULT_DIGITAL_THRESHOLD,
)
struct.pack_into(
"<HHHH",
legacy_default_wire,
62,
30000,
40000,
256,
config_manager.PROFILE_LEGACY_DEFAULT_DIGITAL_THRESHOLD,
)
migrated_default = config_manager.ControllerProfile.from_bytes(
legacy_default_wire
)
assert (
migrated_default.left_trigger.digital_threshold
== config_manager.PROFILE_DEFAULT_DIGITAL_THRESHOLD
)
assert (
migrated_default.right_trigger.digital_threshold
== config_manager.PROFILE_DEFAULT_DIGITAL_THRESHOLD
)
assert migrated_default.left_trigger.lower_deadzone == 30000
assert migrated_default.left_trigger.upper_saturation == 40000
assert migrated_default.right_trigger.lower_deadzone == 30000
assert migrated_default.right_trigger.upper_saturation == 40000
assert migrated_default.to_bytes()[0] == config_manager.PROFILE_SCHEMA_VERSION
current_old_value_wire = bytearray(default_wire)
struct.pack_into(
"<H",
current_old_value_wire,
58,
config_manager.PROFILE_LEGACY_DEFAULT_DIGITAL_THRESHOLD,
)
assert (
config_manager.ControllerProfile.from_bytes(
current_old_value_wire
).left_trigger.digital_threshold
== config_manager.PROFILE_LEGACY_DEFAULT_DIGITAL_THRESHOLD
)
legacy_custom_wire = bytearray(encoded)
struct.pack_into(
"<H",
legacy_custom_wire,
0,
config_manager.PROFILE_LEGACY_SCHEMA_VERSION,
)
assert (
config_manager.ControllerProfile.from_bytes(legacy_custom_wire)
== profile
)
legacy_json_object = default_profile.to_json_object()
legacy_json_object["schema_version"] = (
config_manager.PROFILE_LEGACY_SCHEMA_VERSION
)
legacy_json_object["triggers"]["left"]["digital_threshold"] = (
config_manager.PROFILE_LEGACY_DEFAULT_DIGITAL_THRESHOLD
)
legacy_json_object["triggers"]["right"]["digital_threshold"] = 33000
legacy_json_object["triggers"]["left"]["lower_deadzone"] = 30000
legacy_json_object["triggers"]["left"]["upper_saturation"] = 40000
migrated_json = config_manager.ControllerProfile.from_json_object(
legacy_json_object
)
assert (
migrated_json.left_trigger.digital_threshold
== config_manager.PROFILE_DEFAULT_DIGITAL_THRESHOLD
)
assert migrated_json.right_trigger.digital_threshold == 33000
assert migrated_json.left_trigger.lower_deadzone == 30000
assert migrated_json.left_trigger.upper_saturation == 40000
def test_trigger_threshold_uses_transformed_output_domain() -> None:
for threshold in (0, 0xFFFF):
trigger = config_manager.TriggerConfig(30000, 40000, 256, threshold)
assert config_manager.TriggerConfig.from_bytes(trigger.to_bytes()) == trigger
assert (
config_manager.TriggerConfig.from_json_object(
trigger.to_json_object(), "trigger"
)
== trigger
)
current_wire = bytearray(
config_manager.ControllerProfile.default().to_bytes()
)
struct.pack_into(
"<HHHH", current_wire, 52, 30000, 40000, 256, threshold
)
current_profile = config_manager.ControllerProfile.from_bytes(
current_wire
)
assert current_profile.left_trigger.digital_threshold == threshold
assert current_profile.to_bytes() == current_wire
for threshold in (-1, 0x10000):
with pytest.raises(
config_manager.ConfigManagerError,
match="trigger digital_threshold",
):
config_manager.TriggerConfig(30000, 40000, 256, threshold)
for lower_deadzone, upper_saturation in (
(40000, 40000),
(40001, 40000),
):
with pytest.raises(
config_manager.ConfigManagerError,
match="lower_deadzone must be below upper_saturation",
):
config_manager.TriggerConfig(
lower_deadzone, upper_saturation, 256, 0
)
def test_profile_list_select_read_and_chunked_commit() -> None:
device = FakeDevice()

View file

@ -0,0 +1,32 @@
import shutil
import subprocess
from pathlib import Path
def test_controller_profile_runtime_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_runtime_test"
subprocess.run(
[
compiler,
"-std=c++17",
"-Wall",
"-Wextra",
"-Werror",
"-pedantic",
f"-I{root}",
str(root / "tests" / "controller_profile_runtime_test.cpp"),
str(root / "controller_identity.cpp"),
str(root / "controller_profile.cpp"),
str(root / "controller_profile_transform.cpp"),
str(root / "controller_profile_runtime.cpp"),
"-o",
str(executable),
],
check=True,
cwd=root,
)
subprocess.run([str(executable)], check=True, cwd=root)

View file

@ -0,0 +1,31 @@
import shutil
import subprocess
from pathlib import Path
def test_controller_profile_transform_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_transform_test"
subprocess.run(
[
compiler,
"-std=c++17",
"-Wall",
"-Wextra",
"-Werror",
"-pedantic",
f"-I{root}",
str(root / "tests" / "controller_profile_transform_test.cpp"),
str(root / "controller_identity.cpp"),
str(root / "controller_profile.cpp"),
str(root / "controller_profile_transform.cpp"),
"-o",
str(executable),
],
check=True,
cwd=root,
)
subprocess.run([str(executable)], check=True, cwd=root)

View file

@ -279,7 +279,12 @@ void test_profile_vendor_requests() {
control_payload.size() ==
kResponseHeaderSize +
CONTROLLER_PROFILE_ENCODED_SIZE &&
control_payload[kResponseHeaderSize] == 1 &&
control_payload[10] ==
CONTROLLER_PROFILE_SCHEMA_VERSION &&
control_payload[kResponseHeaderSize] ==
static_cast<uint8_t>(
CONTROLLER_PROFILE_SCHEMA_VERSION) &&
control_payload[kResponseHeaderSize + 1] == 0 &&
control_payload[kResponseHeaderSize + 2] == 0 &&
control_payload[kResponseHeaderSize + 3] == 1,
"selected profile response was not encoded");