Add configurable accelerometer swing gestures for Wii Remotes
This commit is contained in:
parent
c7261c9338
commit
9927e439b7
25 changed files with 831 additions and 64 deletions
|
|
@ -13,6 +13,10 @@ typedef enum wii_flags {
|
|||
} wii_mode_t;
|
||||
|
||||
void uni_hid_parser_wii_set_mode(uni_hid_device_t* device, wii_mode_t mode);
|
||||
static inline bool uni_hid_parser_wii_accel_snapshot(
|
||||
uni_hid_device_t*, int32_t[3], uint32_t*) {
|
||||
return false;
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <cmath>
|
||||
|
||||
namespace {
|
||||
|
||||
|
|
@ -1215,6 +1216,103 @@ void test_extra_hotkeys_consume_mappings_and_rearm() {
|
|||
"released extra switching chord did not rearm");
|
||||
}
|
||||
|
||||
void test_accelerometer_swing_requires_evidence_and_settle() {
|
||||
WiiSwingDetector detector;
|
||||
WiiAccelerometerSample sample{};
|
||||
sample.valid = true;
|
||||
uint32_t now = UINT32_MAX - 100u;
|
||||
auto feed = [&](int16_t x, int16_t y, int16_t z) {
|
||||
now += 10;
|
||||
sample = {x, y, z, sample.sequence + 1, now, true};
|
||||
return detector.update(sample, now, 1, true);
|
||||
};
|
||||
for (int i = 0; i < 20; ++i)
|
||||
require(!feed(0, 0, 4096), "resting gravity armed a swing output");
|
||||
for (int degrees = 0; degrees <= 90; degrees += 3) {
|
||||
const double angle = degrees * 0.017453292519943;
|
||||
require(!feed(static_cast<int16_t>(4096 * std::sin(angle)), 0,
|
||||
static_cast<int16_t>(4096 * std::cos(angle))),
|
||||
"ordinary rotation of gravity triggered a sword swing");
|
||||
}
|
||||
for (int i = 0; i < 40; ++i) feed(4096, 0, 0);
|
||||
require(!feed(4096, 10000, 0), "a single acceleration spike triggered a swing");
|
||||
for (int i = 0; i < 5; ++i) {
|
||||
now += 2;
|
||||
require(!detector.update(sample, now, 1, true),
|
||||
"repeated reads of one sample accumulated swing evidence");
|
||||
}
|
||||
require(feed(4096, 10000, 0), "sustained acceleration did not produce a swing");
|
||||
for (int i = 0; i < 50; ++i) {
|
||||
const bool pressed = feed(4096, 10000, 0);
|
||||
if (i >= 8) require(!pressed, "continuous shaking retriggered without settling");
|
||||
}
|
||||
for (int i = 0; i < 35; ++i)
|
||||
require(!feed(4096, 0, 0), "settling generated a second button pulse");
|
||||
require(!feed(4096, -10000, 0) && feed(4096, -10000, 0),
|
||||
"settled detector did not accept an opposite-direction swing");
|
||||
require(!detector.update(sample, now + 151, 1, true),
|
||||
"stale acceleration retained a button press");
|
||||
}
|
||||
|
||||
void test_swing_output_isolated_from_motion_remaps_and_profiles() {
|
||||
prepare_profiles();
|
||||
rows[0].profiles[0].swing.button = 2; // Final logical west / Switch Y.
|
||||
rows[0].profiles[0].button_map[2] = 1;
|
||||
rows[1].profiles[0].swing.button = 2;
|
||||
auto snapshot = make_snapshot(0);
|
||||
snapshot.state.button_south = true;
|
||||
snapshot.pre_hotkey_button_mask = 1;
|
||||
uint32_t now = 0;
|
||||
auto feed = [&](int16_t x) {
|
||||
now += 10;
|
||||
snapshot.accelerometer = {x, 0, 4096, snapshot.accelerometer.sequence + 1, now, true};
|
||||
return runtime_transform(0, snapshot, now);
|
||||
};
|
||||
for (int i = 0; i < 20; ++i) feed(0);
|
||||
feed(10000);
|
||||
auto output = feed(10000);
|
||||
require(output.state.button_west && output.state.button_south &&
|
||||
!output.state.button_east && output.state.motion_sample_count == 0,
|
||||
"accelerometer swing required gyro output, remapped its target, or lost physical input");
|
||||
require(!runtime_transform(1, make_snapshot(1), now).state.button_west,
|
||||
"a swing leaked to another controller slot");
|
||||
snapshot.state.button_west = true;
|
||||
rows[0].profiles[0].button_map[2] = 2;
|
||||
++database_generation;
|
||||
output = runtime_transform(0, snapshot, now + 1);
|
||||
require(output.state.button_west, "gesture cancellation released a physical target button");
|
||||
snapshot.state.button_west = false;
|
||||
rows[0].profiles[0].swing.button = 3;
|
||||
++database_generation;
|
||||
output = runtime_transform(0, snapshot, now + 2);
|
||||
require(!output.state.button_west && !output.state.button_north,
|
||||
"profile refresh replayed a swing onto old or new target");
|
||||
}
|
||||
|
||||
void test_swing_modifier_release_cancels_and_requires_fresh_settle() {
|
||||
prepare_profiles();
|
||||
rows[0].profiles[0].swing = {2, 1, 0};
|
||||
auto snapshot = make_snapshot(0);
|
||||
uint32_t now = 0;
|
||||
auto feed = [&](int16_t x, bool held) {
|
||||
now += 10;
|
||||
apply_button_mask(held ? 1 : 0, &snapshot);
|
||||
snapshot.accelerometer = {x, 0, 4096, snapshot.accelerometer.sequence + 1, now, true};
|
||||
return runtime_transform(0, snapshot, now);
|
||||
};
|
||||
for (int i = 0; i < 20; ++i) feed(0, true);
|
||||
feed(10000, true);
|
||||
require(feed(10000, true).state.button_west, "held modifier failed to allow a swing");
|
||||
require(!feed(10000, false).state.button_west, "modifier release retained swing output");
|
||||
require(!feed(10000, true).state.button_west, "repressing modifier during motion retriggered");
|
||||
for (int i = 0; i < 60; ++i) feed(0, true);
|
||||
feed(10000, true);
|
||||
require(feed(10000, true).state.button_west, "settling after modifier release did not rearm");
|
||||
snapshot.active = false;
|
||||
require(!runtime_transform(0, snapshot, now + 1).state.button_west,
|
||||
"disconnect retained gesture output");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool bluepad32_input_backend_toggle_motion(
|
||||
|
|
@ -1288,5 +1386,8 @@ int main() {
|
|||
test_held_synthetic_sources_and_disconnect_rearming();
|
||||
test_shortcut_selector_rollover_without_modifier_release();
|
||||
test_extra_hotkeys_consume_mappings_and_rearm();
|
||||
test_accelerometer_swing_requires_evidence_and_settle();
|
||||
test_swing_output_isolated_from_motion_remaps_and_profiles();
|
||||
test_swing_modifier_release_cancels_and_requires_fresh_settle();
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -123,9 +123,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] == 7 && encoded[1] == 0 &&
|
||||
require(encoded[0] == 8 && encoded[1] == 0 &&
|
||||
encoded[2] == 0x80 && encoded[3] == 1,
|
||||
"profile header is not little-endian v7/384");
|
||||
"profile header is not little-endian v8/384");
|
||||
for (uint8_t index = 0;
|
||||
index < CONTROLLER_PROFILE_LOGICAL_BUTTON_COUNT; ++index) {
|
||||
require(encoded[4 + index] == index,
|
||||
|
|
@ -276,6 +276,10 @@ void test_legacy_profile_migration() {
|
|||
kLegacyDefaultProfile, sizeof(kLegacyDefaultProfile),
|
||||
&migrated),
|
||||
"legacy default profile did not decode");
|
||||
require(migrated.swing.button == CONTROLLER_PROFILE_NO_BUTTON &&
|
||||
migrated.swing.sensitivity == 1 &&
|
||||
migrated.swing.modifier == CONTROLLER_PROFILE_NO_BUTTON,
|
||||
"legacy padding enabled a swing gesture");
|
||||
require(migrated.triggers[0].digital_threshold ==
|
||||
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD &&
|
||||
migrated.triggers[1].digital_threshold ==
|
||||
|
|
@ -390,6 +394,37 @@ void test_legacy_profile_migration() {
|
|||
migrated.macro_steps[0].output_button_mask == 1,
|
||||
"legacy nonempty macro did not migrate into shared pool");
|
||||
|
||||
previous_encoded[60] = CONTROLLER_PROFILE_RIGHT_TRIGGER_CONTROL;
|
||||
previous_encoded[70] = CONTROLLER_PROFILE_LEFT_TRIGGER_CONTROL;
|
||||
previous_encoded[76] = 3;
|
||||
previous_encoded[78] = 1;
|
||||
previous_encoded[79] = 0;
|
||||
previous_encoded[81] = 11;
|
||||
previous_encoded[98] = 4;
|
||||
for (uint8_t version = 3; version <= 4; ++version) {
|
||||
previous_encoded[0] = version;
|
||||
previous_encoded[75] = version == 4 ? 0x21 : 0;
|
||||
migrated.swing = {2, 2, 24};
|
||||
require(controller_profile_decode(
|
||||
previous_encoded, sizeof(previous_encoded), &migrated) &&
|
||||
migrated.triggers[0].output ==
|
||||
CONTROLLER_PROFILE_RIGHT_TRIGGER_CONTROL &&
|
||||
migrated.triggers[1].output ==
|
||||
CONTROLLER_PROFILE_LEFT_TRIGGER_CONTROL &&
|
||||
migrated.switching_chord ==
|
||||
(3u | (version == 4 ? 1u << 16 : 0u)) &&
|
||||
migrated.motion_toggle_chord ==
|
||||
(4u | (version == 4 ? 1u << 17 : 0u)) &&
|
||||
migrated.macros[0].trigger_mask == 1 &&
|
||||
migrated.macros[0].cancel_control == 11 &&
|
||||
migrated.macros[0].step_count == 1 &&
|
||||
migrated.macro_steps[0].output_button_mask == 1 &&
|
||||
migrated.swing.button == CONTROLLER_PROFILE_NO_BUTTON &&
|
||||
migrated.swing.sensitivity == 1 &&
|
||||
migrated.swing.modifier == CONTROLLER_PROFILE_NO_BUTTON,
|
||||
"legacy control mapping or action migration lost settings");
|
||||
}
|
||||
|
||||
ControllerProfile current =
|
||||
controller_profile_default(controller_identity_global(), 0);
|
||||
current.triggers[0].digital_threshold = 0x8000;
|
||||
|
|
@ -514,7 +549,10 @@ void test_set_b_sparse_extension_and_migration() {
|
|||
decoded.macro_steps[7].right_trigger == 0xabcd &&
|
||||
decoded.macros[0].mode == ControllerProfileMacroMode::kOnce &&
|
||||
decoded.shortcuts.modifier == CONTROLLER_PROFILE_NO_BUTTON &&
|
||||
decoded.turbo_override_mask == 0,
|
||||
decoded.turbo_override_mask == 0 &&
|
||||
decoded.swing.button == CONTROLLER_PROFILE_NO_BUTTON &&
|
||||
decoded.swing.sensitivity == 1 &&
|
||||
decoded.swing.modifier == CONTROLLER_PROFILE_NO_BUTTON,
|
||||
"schema5 full136-byte macro stream did not migrate");
|
||||
require(controller_profile_encode(decoded, encoded, sizeof(encoded)) &&
|
||||
memcmp(&legacy[4], &encoded[4], sizeof(legacy) - 4) == 0,
|
||||
|
|
@ -595,6 +633,10 @@ void test_schema6_migration_preserves_every_setting() {
|
|||
"schema6 profile could not upgrade");
|
||||
require(memcmp(upgraded + 2, kLegacySchema6Profile + 2, 342) == 0,
|
||||
"schema6 migration changed an existing encoded setting");
|
||||
require(profile.swing.button == CONTROLLER_PROFILE_NO_BUTTON &&
|
||||
profile.swing.sensitivity == 1 &&
|
||||
profile.swing.modifier == CONTROLLER_PROFILE_NO_BUTTON,
|
||||
"schema6 padding became a swing gesture");
|
||||
for (uint8_t index = 0; index < CONTROLLER_PROFILE_EXTRA_BUTTON_COUNT; ++index) {
|
||||
require(profile.extra_button_map[index] == CONTROLLER_PROFILE_NO_BUTTON &&
|
||||
profile.shift.extra_button_map[index] == CONTROLLER_PROFILE_NO_BUTTON,
|
||||
|
|
@ -681,6 +723,89 @@ void test_extra_control_schema_round_trip_and_output_limits() {
|
|||
"Shift extra mapping admitted an analog destination");
|
||||
}
|
||||
|
||||
void test_schema7_migration_preserves_extra_controls_and_macros() {
|
||||
uint8_t legacy[sizeof(kLegacySchema6Profile)]{};
|
||||
memcpy(legacy, kLegacySchema6Profile, sizeof(legacy));
|
||||
legacy[0] = 7;
|
||||
const uint8_t extra_map[] = {0, 16, 17, 12, 13, 14, 15};
|
||||
const uint8_t shifted_map[] = {15, 14, 13, 12, 3, 2, 0xff};
|
||||
memcpy(legacy + 344, extra_map, sizeof(extra_map));
|
||||
memcpy(legacy + 351, shifted_map, sizeof(shifted_map));
|
||||
legacy[256] = 18;
|
||||
legacy[266] = 24;
|
||||
for (uint8_t index = 0; index < CONTROLLER_PROFILE_MACRO_COUNT; ++index) {
|
||||
legacy[98 + index * 6] = static_cast<uint8_t>(
|
||||
(legacy[98 + index * 6] & 3u) | ((24 - index) << 2));
|
||||
legacy[358 + index] = static_cast<uint8_t>(1u << index);
|
||||
}
|
||||
legacy[362] = 0x55;
|
||||
legacy[363] = 0x2a;
|
||||
ControllerProfile migrated{};
|
||||
migrated.swing = {2, 2, 24};
|
||||
uint8_t upgraded[CONTROLLER_PROFILE_ENCODED_SIZE]{};
|
||||
require(controller_profile_decode(legacy, sizeof(legacy), &migrated) &&
|
||||
controller_profile_encode(migrated, upgraded, sizeof(upgraded)) &&
|
||||
memcmp(legacy + 2, upgraded + 2, 362) == 0 &&
|
||||
migrated.swing.button == CONTROLLER_PROFILE_NO_BUTTON &&
|
||||
migrated.swing.sensitivity == 1 &&
|
||||
migrated.swing.modifier == CONTROLLER_PROFILE_NO_BUTTON,
|
||||
"schema7 migration lost existing settings or enabled swing");
|
||||
for (size_t offset = 364; offset < sizeof(legacy); ++offset) {
|
||||
legacy[offset] = 1;
|
||||
require(!controller_profile_decode(legacy, sizeof(legacy), &migrated),
|
||||
"schema7 interpreted reserved padding as swing settings");
|
||||
legacy[offset] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void test_swing_wire_settings_and_rejection() {
|
||||
ControllerProfile profile =
|
||||
controller_profile_default(controller_identity_global(), 0);
|
||||
profile.swing = {15, 2, 24};
|
||||
uint8_t encoded[CONTROLLER_PROFILE_ENCODED_SIZE]{};
|
||||
ControllerProfile decoded{};
|
||||
require(controller_profile_encode(profile, encoded, sizeof(encoded)) &&
|
||||
encoded[364] == 15 && encoded[365] == 2 && encoded[366] == 24 &&
|
||||
controller_profile_decode(encoded, sizeof(encoded), &decoded) &&
|
||||
decoded.swing.button == 15 && decoded.swing.sensitivity == 2 &&
|
||||
decoded.swing.modifier == 24,
|
||||
"swing wire settings did not round-trip");
|
||||
const ControllerProfileSwingConfiguration invalid_settings[] = {
|
||||
{16, 2, 24}, {254, 2, 24}, {15, 3, 24}, {15, 255, 24},
|
||||
{15, 2, 25}, {15, 2, 254}, {255, 3, 255}, {255, 1, 25}};
|
||||
for (const auto& invalid : invalid_settings) {
|
||||
profile.swing = invalid;
|
||||
require(!controller_profile_validate(profile) &&
|
||||
!controller_profile_encode(profile, encoded, sizeof(encoded)),
|
||||
"invalid swing settings were accepted for encoding");
|
||||
encoded[364] = invalid.button;
|
||||
encoded[365] = invalid.sensitivity;
|
||||
encoded[366] = invalid.modifier;
|
||||
require(!controller_profile_decode(encoded, sizeof(encoded), &decoded),
|
||||
"invalid swing wire settings were accepted");
|
||||
}
|
||||
profile.swing = {0, 0, 0};
|
||||
require(controller_profile_encode(profile, encoded, sizeof(encoded)) &&
|
||||
controller_profile_decode(encoded, sizeof(encoded), &decoded) &&
|
||||
decoded.swing.button == 0 && decoded.swing.sensitivity == 0 &&
|
||||
decoded.swing.modifier == 0,
|
||||
"zero-valued swing settings were mistaken for disabled settings");
|
||||
for (size_t offset = 367; offset < sizeof(encoded); ++offset) {
|
||||
encoded[offset] = 1;
|
||||
require(!controller_profile_decode(encoded, sizeof(encoded), &decoded),
|
||||
"nonzero swing extension reservation was accepted");
|
||||
encoded[offset] = 0;
|
||||
}
|
||||
profile.swing = {CONTROLLER_PROFILE_NO_BUTTON, 0,
|
||||
CONTROLLER_PROFILE_NO_BUTTON};
|
||||
require(controller_profile_encode(profile, encoded, sizeof(encoded)) &&
|
||||
controller_profile_decode(encoded, sizeof(encoded), &decoded) &&
|
||||
decoded.swing.button == CONTROLLER_PROFILE_NO_BUTTON &&
|
||||
decoded.swing.sensitivity == 0 &&
|
||||
decoded.swing.modifier == CONTROLLER_PROFILE_NO_BUTTON,
|
||||
"disabled swing did not preserve sensitivity and absent modifier");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
int main() {
|
||||
test_pair_identity_wire_and_member_validation();
|
||||
|
|
@ -691,5 +816,7 @@ int main() {
|
|||
test_legacy_database_strides();
|
||||
test_schema6_migration_preserves_every_setting();
|
||||
test_extra_control_schema_round_trip_and_output_limits();
|
||||
test_schema7_migration_preserves_extra_controls_and_macros();
|
||||
test_swing_wire_settings_and_rejection();
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -295,7 +295,7 @@ void test_profile_bounds_and_transaction_namespace() {
|
|||
"transaction namespaces were not enforced");
|
||||
}
|
||||
|
||||
void test_schema7_validation_and_atomic_selection() {
|
||||
void test_current_schema_validation_and_atomic_selection() {
|
||||
const ControllerIdentity id = stable_identity();
|
||||
require(profile_service_select(id, 6) ==
|
||||
ConfigurationTransactionStatus::kCommitted,
|
||||
|
|
@ -308,6 +308,7 @@ void test_schema7_validation_and_atomic_selection() {
|
|||
updated.turbo_defaults = {30, 1, 255};
|
||||
updated.extra_button_map[6] = 16;
|
||||
updated.shift.extra_button_map[0] = 3;
|
||||
updated.swing = {2, 2, 24};
|
||||
updated.macros[0].trigger_mask = 1u << 24;
|
||||
updated.macros[0].cancel_control = 19;
|
||||
updated.macros[0].step_count = 1;
|
||||
|
|
@ -320,11 +321,15 @@ void test_schema7_validation_and_atomic_selection() {
|
|||
uint8_t encoded[CONTROLLER_PROFILE_ENCODED_SIZE]{};
|
||||
require(controller_profile_encode(updated, encoded, sizeof(encoded)),
|
||||
"extended service profile did not encode");
|
||||
require(profile_service_begin(20, id, 6, 6, 384, 0) ==
|
||||
require(profile_service_begin(
|
||||
20, id, 6, CONTROLLER_PROFILE_EXTRA_CONTROL_SCHEMA_VERSION,
|
||||
CONTROLLER_PROFILE_ENCODED_SIZE, 0) ==
|
||||
ConfigurationTransactionStatus::kUnsupportedSchema &&
|
||||
profile_service_begin(21, id, 6, 7, 256, 0) ==
|
||||
profile_service_begin(21, id, 6, CONTROLLER_PROFILE_SCHEMA_VERSION,
|
||||
256, 0) ==
|
||||
ConfigurationTransactionStatus::kMalformed &&
|
||||
profile_service_begin(22, id, 6, 7, 385, 0) ==
|
||||
profile_service_begin(22, id, 6, CONTROLLER_PROFILE_SCHEMA_VERSION,
|
||||
385, 0) ==
|
||||
ConfigurationTransactionStatus::kTooLarge,
|
||||
"service admitted old-schema or incorrectly-sized writes");
|
||||
|
||||
|
|
@ -347,6 +352,25 @@ void test_schema7_validation_and_atomic_selection() {
|
|||
old_selection.profile.turbo_defaults.rate_hz,
|
||||
"rejected extension replaced the old selected snapshot");
|
||||
|
||||
require(controller_profile_encode(updated, encoded, sizeof(encoded)),
|
||||
"swing rejection baseline did not encode");
|
||||
encoded[366] = CONTROLLER_PROFILE_LOGICAL_CONTROL_COUNT;
|
||||
require(profile_service_begin(
|
||||
25, id, 6, CONTROLLER_PROFILE_SCHEMA_VERSION, sizeof(encoded),
|
||||
profile_storage_crc32(encoded, sizeof(encoded))) ==
|
||||
ConfigurationTransactionStatus::kReceiving &&
|
||||
profile_service_append(25, 0, encoded, sizeof(encoded)) ==
|
||||
ConfigurationTransactionStatus::kReceiving &&
|
||||
profile_service_commit(25) ==
|
||||
ConfigurationTransactionStatus::kMalformed,
|
||||
"service admitted an invalid swing modifier with a valid CRC");
|
||||
profile_service_selected_snapshot(&selected);
|
||||
require(selected.metadata.generation == old_selection.metadata.generation &&
|
||||
selected.profile.swing.button == old_selection.profile.swing.button &&
|
||||
active_snapshot(id).profile.swing.button ==
|
||||
old_selection.profile.swing.button,
|
||||
"rejected swing replaced a selected or active profile");
|
||||
|
||||
require(controller_profile_encode(updated, encoded, sizeof(encoded)),
|
||||
"valid replacement did not encode");
|
||||
require(profile_service_begin(
|
||||
|
|
@ -373,10 +397,16 @@ void test_schema7_validation_and_atomic_selection() {
|
|||
selected.profile.macros[0].repeat_count == 255 &&
|
||||
selected.profile.extra_button_map[6] == 16 &&
|
||||
selected.profile.shift.extra_button_map[0] == 3 &&
|
||||
selected.profile.swing.button == 2 &&
|
||||
selected.profile.swing.sensitivity == 2 &&
|
||||
selected.profile.swing.modifier == 24 &&
|
||||
active_snapshot(id).profile.swing.button == 2 &&
|
||||
active_snapshot(id).profile.swing.sensitivity == 2 &&
|
||||
active_snapshot(id).profile.swing.modifier == 24 &&
|
||||
active_snapshot(id).profile.macros[0].trigger_mask == (1u << 24) &&
|
||||
active_snapshot(id).profile.macros[0].cancel_control == 19 &&
|
||||
active_snapshot(id).profile.turbo_defaults.rate_hz == 30,
|
||||
"committed schema7 profile did not atomically refresh snapshots");
|
||||
"committed profile did not atomically refresh snapshots");
|
||||
}
|
||||
|
||||
void test_catalog1_selected_and_active_snapshots_migrate() {
|
||||
|
|
@ -568,7 +598,7 @@ ProfileStorageIo pico_profile_storage_io() { return fake_io(); }
|
|||
int main() {
|
||||
test_eight_profile_transactions_and_active_cache();
|
||||
test_profile_bounds_and_transaction_namespace();
|
||||
test_schema7_validation_and_atomic_selection();
|
||||
test_current_schema_validation_and_atomic_selection();
|
||||
test_catalog1_selected_and_active_snapshots_migrate();
|
||||
test_pair_publication_failure_recovery_and_independence();
|
||||
std::cout << "profile service tests passed\n";
|
||||
|
|
|
|||
|
|
@ -522,6 +522,15 @@ void test_compaction_preserves_latest_records() {
|
|||
"compaction metadata did not append");
|
||||
profile.extra_button_map[0] = 16;
|
||||
profile.shift.extra_button_map[6] = 15;
|
||||
profile.swing = {2, 2, 24};
|
||||
ControllerProfile other = controller_profile_default(global, 7);
|
||||
other.swing = {15, 0, CONTROLLER_PROFILE_NO_BUTTON};
|
||||
const ControllerIdentity stable = identity(1);
|
||||
require(storage.set(global, 7, other) == ProfileStorageResult::kOk,
|
||||
"second swing profile did not append");
|
||||
other.swing = {0, 1, 16};
|
||||
require(storage.set(stable, 0, other) == ProfileStorageResult::kOk,
|
||||
"stable identity swing profile did not append");
|
||||
for (uint16_t write = 1; write <= 260; ++write) {
|
||||
profile.weak_rumble_scale = static_cast<uint8_t>(write);
|
||||
require(storage.set(global, 0, profile) == ProfileStorageResult::kOk,
|
||||
|
|
@ -538,6 +547,9 @@ void test_compaction_preserves_latest_records() {
|
|||
recovered.weak_rumble_scale == static_cast<uint8_t>(260) &&
|
||||
recovered.extra_button_map[0] == 16 &&
|
||||
recovered.shift.extra_button_map[6] == 15 &&
|
||||
recovered.swing.button == 2 &&
|
||||
recovered.swing.sensitivity == 2 &&
|
||||
recovered.swing.modifier == 24 &&
|
||||
reloaded.get_alias(global, metadata, sizeof(metadata)) ==
|
||||
ProfileStorageResult::kOk &&
|
||||
strcmp(metadata, "Fallback") == 0 &&
|
||||
|
|
@ -546,6 +558,17 @@ void test_compaction_preserves_latest_records() {
|
|||
ProfileStorageResult::kOk &&
|
||||
strcmp(metadata, "Compacted") == 0,
|
||||
"compaction did not preserve profiles and metadata");
|
||||
require(reloaded.get(global, 7, &recovered) == ProfileStorageResult::kOk &&
|
||||
recovered.swing.button == 15 &&
|
||||
recovered.swing.sensitivity == 0 &&
|
||||
recovered.swing.modifier == CONTROLLER_PROFILE_NO_BUTTON &&
|
||||
reloaded.get(stable, 0, &recovered) == ProfileStorageResult::kOk &&
|
||||
recovered.swing.button == 0 &&
|
||||
recovered.swing.sensitivity == 1 &&
|
||||
recovered.swing.modifier == 16 &&
|
||||
reloaded.get(global, 1, &recovered) == ProfileStorageResult::kOk &&
|
||||
recovered.swing.button == CONTROLLER_PROFILE_NO_BUTTON,
|
||||
"compacted swing settings bled across profiles or identities");
|
||||
}
|
||||
|
||||
void test_legacy_migration_is_atomic_and_complete() {
|
||||
|
|
|
|||
|
|
@ -102,6 +102,7 @@ def test_bluepad32_backend_lifecycle_native(tmp_path: Path) -> None:
|
|||
/ "controller_profile_runtime.cpp"
|
||||
),
|
||||
str(root / "src" / "firmware" / "profile" / "profile_storage.cpp"),
|
||||
str(root / "src" / "firmware" / "input" / "wii_swing.cpp"),
|
||||
str(root / "bluepad32_config" / "parser" / "uni_switch2_haptics.c"),
|
||||
str(
|
||||
root / "src" / "firmware" / "input" / "controller_macro_capture.cpp"
|
||||
|
|
|
|||
|
|
@ -1823,6 +1823,7 @@ def test_identity_and_profile_binary_json_round_trip() -> None:
|
|||
config_manager.PROFILE_LEGACY_DEFAULT_DIGITAL_THRESHOLD,
|
||||
)
|
||||
migrated_default = config_manager.ControllerProfile.from_bytes(legacy_default_wire)
|
||||
assert migrated_default.swing.button == config_manager.PROFILE_NONE_BUTTON
|
||||
assert (
|
||||
migrated_default.left_trigger.digital_threshold
|
||||
== config_manager.PROFILE_DEFAULT_DIGITAL_THRESHOLD
|
||||
|
|
@ -1857,6 +1858,7 @@ def test_identity_and_profile_binary_json_round_trip() -> None:
|
|||
macro_cancel=1,
|
||||
)
|
||||
migrated_previous = config_manager.ControllerProfile.from_bytes(previous_wire)
|
||||
assert migrated_previous.swing.button == config_manager.PROFILE_NONE_BUTTON
|
||||
assert migrated_previous.left_trigger.output == 16
|
||||
assert migrated_previous.right_trigger.output == 17
|
||||
assert migrated_previous.macros[0].trigger_mask == 1
|
||||
|
|
@ -1867,7 +1869,7 @@ def test_identity_and_profile_binary_json_round_trip() -> None:
|
|||
legacy_json_object = default_profile.to_json_object()
|
||||
legacy_json_object["schema_version"] = config_manager.PROFILE_LEGACY_SCHEMA_VERSION
|
||||
legacy_json_object["size"] = config_manager.PROFILE_LEGACY_SIZE
|
||||
for field in ("shortcuts", "shift", "turbo_settings", "extra_button_map"):
|
||||
for field in ("shortcuts", "shift", "turbo_settings", "extra_button_map", "swing"):
|
||||
del legacy_json_object[field]
|
||||
del legacy_json_object["motion_toggle_chord"]
|
||||
del legacy_json_object["triggers"]["left"]["output"]
|
||||
|
|
@ -1930,7 +1932,7 @@ def test_schema5_full_macro_stream_migrates_bytes_and_json(monkeypatch) -> None:
|
|||
|
||||
obj["schema_version"] = 5
|
||||
obj["size"] = 256
|
||||
for field in ("shortcuts", "shift", "turbo_settings", "extra_button_map"):
|
||||
for field in ("shortcuts", "shift", "turbo_settings", "extra_button_map", "swing"):
|
||||
del obj[field]
|
||||
for macro in obj["macros"]:
|
||||
del macro["playback"]
|
||||
|
|
@ -1970,7 +1972,8 @@ def test_set_b_sparse_settings_and_macro_modes_round_trip() -> None:
|
|||
assert encoded[294:336] == bytes(42)
|
||||
assert encoded[336:344] == bytes((3, 255, 0, 1, 1, 1, 2, 1))
|
||||
assert encoded[344:358] == bytes([255]) * 14
|
||||
assert encoded[358:] == bytes(26)
|
||||
assert encoded[358:364] == bytes(6)
|
||||
assert encoded[367:] == bytes(17)
|
||||
assert config_manager.ControllerProfile.from_bytes(encoded) == profile
|
||||
assert config_manager.ControllerProfile.from_json(profile.to_json()) == profile
|
||||
|
||||
|
|
@ -1981,6 +1984,7 @@ def test_set_b_sparse_settings_and_macro_modes_round_trip() -> None:
|
|||
legacy_json["schema_version"] = 6
|
||||
del legacy_json["extra_button_map"]
|
||||
del legacy_json["shift"]["extra_button_map"]
|
||||
del legacy_json["swing"]
|
||||
assert config_manager.ControllerProfile.from_bytes(legacy_wire) == profile
|
||||
assert config_manager.ControllerProfile.from_json_object(legacy_json) == profile
|
||||
device = FakeDevice()
|
||||
|
|
@ -1994,7 +1998,8 @@ def test_set_b_sparse_settings_and_macro_modes_round_trip() -> None:
|
|||
assert old_listing[1] == config_manager.ProfileListEntry(device.stable_identity, 1, "Custom controller")
|
||||
|
||||
|
||||
def test_schema7_extra_controls_keep_output_channels_and_wire_layout() -> None:
|
||||
@pytest.mark.parametrize("version", [7, 8])
|
||||
def test_schema7_extra_controls_keep_output_channels_and_wire_layout(version: int) -> None:
|
||||
obj = custom_profile().to_json_object()
|
||||
obj["extra_button_map"] = dict(zip(
|
||||
config_manager.EXTRA_BUTTONS,
|
||||
|
|
@ -2013,16 +2018,32 @@ def test_schema7_extra_controls_keep_output_channels_and_wire_layout() -> None:
|
|||
for index, names in enumerate((["c", "gl"], ["gr"], ["left_sl", "left_sr"], ["right_sl", "right_sr"])):
|
||||
obj["macros"][index]["trigger"] = names
|
||||
obj["macros"][index]["cancel"] = config_manager.EXTRA_BUTTONS[index + 3]
|
||||
obj["schema_version"] = version
|
||||
if version == 7:
|
||||
del obj["swing"]
|
||||
profile = config_manager.ControllerProfile.from_json_object(obj)
|
||||
encoded = profile.to_bytes()
|
||||
assert encoded[:4] == struct.pack("<HH", 7, 384)
|
||||
assert encoded[:4] == struct.pack("<HH", config_manager.PROFILE_SCHEMA_VERSION, 384)
|
||||
assert encoded[344:351] == bytes((0, 17, 16, 255, 14, 7, 9))
|
||||
assert encoded[351:358] == bytes((1, 255, 2, 3, 8, 12, 15))
|
||||
assert encoded[358:364] == bytes((3, 4, 24, 96, 65, 10))
|
||||
assert encoded[364:] == bytes(20)
|
||||
assert config_manager.ControllerProfile.from_bytes(encoded) == profile
|
||||
assert config_manager.ControllerProfile.from_json(profile.to_json()) == profile
|
||||
assert len(profile.button_map) == len(profile.shift.button_map) == len(profile.turbo_modes) == 16
|
||||
assert profile.swing.button == config_manager.PROFILE_NONE_BUTTON
|
||||
legacy_wire = bytearray(encoded)
|
||||
struct.pack_into("<H", legacy_wire, 0, version)
|
||||
if version == 7:
|
||||
legacy_wire[364:] = bytes(20)
|
||||
device = FakeDevice()
|
||||
device.profiles[(device.stable_identity.to_bytes(), 1)] = bytes(legacy_wire)
|
||||
assert config_manager.read_profile(device, device.stable_identity, 1) == profile
|
||||
listing = config_manager.parse_profile_list(config_manager.parse_response(
|
||||
make_response(config_manager.OP_PROFILE_LIST, device._profile_list_payload(), schema=version),
|
||||
config_manager.OP_PROFILE_LIST,
|
||||
))
|
||||
assert listing[1].identity == device.stable_identity
|
||||
assert listing[1].active_profile_index == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize("path", [
|
||||
|
|
@ -2058,6 +2079,8 @@ def test_schema6_rejects_schema7_controls_in_old_fields(offset: int, value: int)
|
|||
])
|
||||
def test_schema7_rejects_extra_map_and_mask_overflow(offset: int, value: int) -> None:
|
||||
payload = bytearray(config_manager.ControllerProfile.default().to_bytes())
|
||||
struct.pack_into("<H", payload, 0, 7)
|
||||
payload[364:] = bytes(20)
|
||||
payload[offset] = value
|
||||
with pytest.raises(config_manager.ConfigManagerError):
|
||||
config_manager.ControllerProfile.from_bytes(payload)
|
||||
|
|
@ -2083,6 +2106,7 @@ def test_legacy_control_profiles_preserve_custom_actions(version: int) -> None:
|
|||
assert profile.macros[0].trigger_mask == 3
|
||||
assert profile.macros[0].cancel_control == (17 if version == 4 else 2)
|
||||
assert profile.extra_button_map == (255,) * 7
|
||||
assert profile.swing.button == config_manager.PROFILE_NONE_BUTTON
|
||||
obj = profile.to_json_object()
|
||||
obj["schema_version"] = version
|
||||
obj["size"] = 256
|
||||
|
|
@ -2091,12 +2115,91 @@ def test_legacy_control_profiles_preserve_custom_actions(version: int) -> None:
|
|||
macro.pop("repeat_count")
|
||||
macro["steps"].append(config_manager.MacroStep.end().to_json_object())
|
||||
obj["macro"] = macro
|
||||
for key in ("shortcuts", "shift", "turbo_settings", "extra_button_map"):
|
||||
for key in ("shortcuts", "shift", "turbo_settings", "extra_button_map", "swing"):
|
||||
del obj[key]
|
||||
assert config_manager.ControllerProfile.from_json_object(obj) == profile
|
||||
assert config_manager.ControllerProfile.from_bytes(profile.to_bytes()) == profile
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("button", "sensitivity", "modifier"), [
|
||||
("south", "low", "right_sr"),
|
||||
("dpad_right", "high", "south"),
|
||||
("west", "medium", "right_trigger"),
|
||||
(None, "high", None),
|
||||
])
|
||||
def test_swing_profile_round_trip_preserves_other_settings(
|
||||
button: str | None, sensitivity: str, modifier: str | None,
|
||||
) -> None:
|
||||
before = custom_profile()
|
||||
obj = before.to_json_object()
|
||||
obj["swing"] = {
|
||||
"button": button, "sensitivity": sensitivity, "modifier": modifier,
|
||||
}
|
||||
profile = config_manager.ControllerProfile.from_json_object(obj)
|
||||
assert replace(profile, swing=before.swing) == before
|
||||
assert profile.to_json_object()["swing"] == obj["swing"]
|
||||
assert config_manager.ControllerProfile.from_json(profile.to_json()) == profile
|
||||
assert config_manager.ControllerProfile.from_bytes(profile.to_bytes()) == profile
|
||||
device = FakeDevice()
|
||||
config_manager.write_profile(device, device.stable_identity, 2, profile, 1.0)
|
||||
assert config_manager.read_profile(device, device.stable_identity, 2) == profile
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("field", "value"), [
|
||||
("button", "left_trigger"),
|
||||
("button", "right_sr"),
|
||||
("button", 0),
|
||||
("sensitivity", "extreme"),
|
||||
("sensitivity", None),
|
||||
("sensitivity", 1),
|
||||
("modifier", "unknown"),
|
||||
("modifier", False),
|
||||
])
|
||||
def test_swing_rejects_invalid_json_settings(field: str, value: object) -> None:
|
||||
obj = config_manager.ControllerProfile.default().to_json_object()
|
||||
obj["swing"][field] = value
|
||||
with pytest.raises(config_manager.ConfigManagerError):
|
||||
config_manager.ControllerProfile.from_json_object(obj)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("field", "value"), [
|
||||
("button", -1), ("button", 16), ("button", True),
|
||||
("sensitivity", -1), ("sensitivity", 3), ("sensitivity", False),
|
||||
("modifier", -1), ("modifier", 25), ("modifier", 256),
|
||||
])
|
||||
def test_swing_rejects_invalid_in_memory_settings(field: str, value: object) -> None:
|
||||
with pytest.raises(config_manager.ConfigManagerError):
|
||||
replace(config_manager.ProfileSwing(), **{field: value})
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("offset", "value"), [
|
||||
(364, 16), (365, 3), (366, 25), (367, 1),
|
||||
])
|
||||
def test_swing_rejects_corrupt_wire_settings(offset: int, value: int) -> None:
|
||||
payload = bytearray(config_manager.ControllerProfile.default().to_bytes())
|
||||
payload[offset] = value
|
||||
with pytest.raises(config_manager.ConfigManagerError):
|
||||
config_manager.ControllerProfile.from_bytes(payload)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mutation", [
|
||||
"missing", "unknown", "missing_button", "missing_sensitivity", "missing_modifier",
|
||||
"legacy_field",
|
||||
])
|
||||
def test_swing_json_fields_are_strict(mutation: str) -> None:
|
||||
obj = config_manager.ControllerProfile.default().to_json_object()
|
||||
if mutation == "missing":
|
||||
del obj["swing"]
|
||||
elif mutation == "unknown":
|
||||
obj["swing"]["enabled"] = False
|
||||
elif mutation.startswith("missing_"):
|
||||
del obj["swing"][mutation.removeprefix("missing_")]
|
||||
else:
|
||||
obj["schema_version"] = 7
|
||||
with pytest.raises(config_manager.ConfigManagerError):
|
||||
config_manager.ControllerProfile.from_json_object(obj)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("path", "value"),
|
||||
[
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ def test_controller_profile_runtime_native(tmp_path: Path) -> None:
|
|||
str(root / "src" / "firmware" / "profile" / "controller_profile_transform.cpp"),
|
||||
str(root / "src" / "firmware" / "profile" / "controller_synthetic_input.cpp"),
|
||||
str(root / "src" / "firmware" / "profile" / "controller_profile_runtime.cpp"),
|
||||
str(root / "src" / "firmware" / "input" / "wii_swing.cpp"),
|
||||
"-o",
|
||||
str(executable),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -106,6 +106,7 @@ def test_editor_serves_assets_and_complete_schema(
|
|||
assert schema["controls"] == list(config_manager.LOGICAL_CONTROLS)
|
||||
assert schema["rumble_policies"] == list(config_manager.RUMBLE_POLICIES)
|
||||
assert schema["turbo_modes"] == list(config_manager.TURBO_MODES)
|
||||
assert schema["swing_sensitivities"] == ["low", "medium", "high"]
|
||||
assert schema["macro_overrides"] == list(config_manager.MACRO_OVERRIDE_NAMES)
|
||||
assert schema["profile_capacity"] == 8
|
||||
assert (
|
||||
|
|
@ -141,15 +142,19 @@ def test_switch2_input_choices_are_never_output_targets(
|
|||
assert schema["output_controls"] == list(config_manager.OUTPUT_CONTROLS)
|
||||
|
||||
|
||||
def test_editor_migrates_schema6_and_saves_extra_mappings_without_metadata_loss(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
@pytest.mark.parametrize("version", [6, 7])
|
||||
def test_editor_migrates_old_profiles_and_saves_swing_without_metadata_loss(
|
||||
monkeypatch: pytest.MonkeyPatch, version: int,
|
||||
) -> None:
|
||||
device = FakeDevice()
|
||||
key = (device.stable_identity.to_bytes(), 1)
|
||||
profile = custom_profile()
|
||||
legacy_wire = bytearray(profile.to_bytes())
|
||||
legacy_wire[:2] = b"\x06\x00"
|
||||
legacy_wire[344:] = bytes(40)
|
||||
legacy_wire[:2] = version.to_bytes(2, "little")
|
||||
if version == 6:
|
||||
legacy_wire[344:] = bytes(40)
|
||||
else:
|
||||
legacy_wire[364:] = bytes(20)
|
||||
device.profiles[key] = bytes(legacy_wire)
|
||||
device.profile_aliases[key[0]] = "Living room"
|
||||
device.profile_names[key] = "Racing"
|
||||
|
|
@ -158,6 +163,7 @@ def test_editor_migrates_schema6_and_saves_extra_mappings_without_metadata_loss(
|
|||
status, migrated = request_json(f"{base_url}/api/profiles/1/2")
|
||||
assert status == 200
|
||||
assert config_manager.ControllerProfile.from_json_object(migrated["profile"]) == profile
|
||||
assert migrated["profile"]["swing"]["button"] is None
|
||||
draft = migrated["profile"]
|
||||
draft["extra_button_map"] = dict(zip(config_manager.EXTRA_BUTTONS, config_manager.LOGICAL_BUTTONS[:7]))
|
||||
draft["shift"]["mode"] = "hold"
|
||||
|
|
@ -167,6 +173,9 @@ def test_editor_migrates_schema6_and_saves_extra_mappings_without_metadata_loss(
|
|||
draft["macros"][0]["cancel"] = "gr"
|
||||
draft["switching_chord"] = ["left_sl", "left_sr"]
|
||||
draft["motion_toggle_chord"] = ["right_sl", "c"]
|
||||
draft["swing"] = {
|
||||
"button": "west", "sensitivity": "high", "modifier": "right_trigger",
|
||||
}
|
||||
status, validated = request_json(
|
||||
f"{base_url}/api/profiles/validate", method="POST", value=draft, token=token,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -112,6 +112,7 @@ static void reset_fixture(uint16_t product_id, bool motionplus, extension_t exte
|
|||
memset(&f, 0, sizeof(f));
|
||||
f.device.vendor_id = 0x057e;
|
||||
f.device.product_id = product_id;
|
||||
f.device.report_parser.setup = uni_hid_parser_wii_setup;
|
||||
f.device.conn.connected = true;
|
||||
f.device.conn.interrupt_cid = 0x40;
|
||||
f.extension = extension;
|
||||
|
|
@ -454,6 +455,31 @@ static void absent_motionplus_keeps_calibrated_remote(void) {
|
|||
expect_vector(f.device.controller.gamepad.gyro, 0, 0, 0);
|
||||
}
|
||||
|
||||
static void accelerometer_snapshot_requires_fresh_calibrated_reports(void) {
|
||||
reset_fixture(0x0306, false, EXT_NONE);
|
||||
connect_device();
|
||||
int32_t acceleration[3];
|
||||
uint32_t sequence;
|
||||
assert(!uni_hid_parser_wii_accel_snapshot(&f.device, acceleration, &sequence));
|
||||
send_core_and_accel();
|
||||
assert(uni_hid_parser_wii_accel_snapshot(&f.device, acceleration, &sequence));
|
||||
expect_vector(acceleration, -8192, 2048, -4096);
|
||||
const uint32_t first = sequence;
|
||||
send_ack(0x16, 0);
|
||||
const uint8_t short_report[5] = {0x31};
|
||||
feed(short_report, sizeof(short_report));
|
||||
assert(uni_hid_parser_wii_accel_snapshot(&f.device, acceleration, &sequence));
|
||||
assert(sequence == first);
|
||||
send_core_and_accel();
|
||||
assert(uni_hid_parser_wii_accel_snapshot(&f.device, acceleration, &sequence));
|
||||
assert(sequence != first); // Identical readings can still be fresh.
|
||||
reset_fixture(0x0306, false, EXT_NONE);
|
||||
f.fail_accel_reads = true;
|
||||
connect_device();
|
||||
send_core_and_accel();
|
||||
assert(!uni_hid_parser_wii_accel_snapshot(&f.device, acceleration, &sequence));
|
||||
}
|
||||
|
||||
static void setup_read_and_write_errors_leave_buttons_ready(void) {
|
||||
reset_fixture(0x0330, true, EXT_NONE);
|
||||
f.fail_read_address = 0xa60030;
|
||||
|
|
@ -890,6 +916,7 @@ static void run_case(const char* name, void (*test)(void)) {
|
|||
}
|
||||
|
||||
int main(void) {
|
||||
run_case("fresh calibrated accelerometer snapshots without MotionPlus", accelerometer_snapshot_requires_fresh_calibrated_reports);
|
||||
run_case("calibrated Nunchuk left-stick endpoints and replacement", calibrated_nunchuk_left_stick_endpoints_and_replacement);
|
||||
run_case("unavailable Nunchuk calibration uses safe nominal travel", unavailable_nunchuk_calibration_keeps_safe_nominal_stick);
|
||||
run_case("integrated MotionPlus calibration and per-axis slow bits", integrated_motionplus_calibration_and_slow_bits);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue