Add Nunchuk and combined motion gestures with button or macro actions
This commit is contained in:
parent
4cbe8348ee
commit
485ad39709
26 changed files with 1969 additions and 204 deletions
|
|
@ -32,6 +32,23 @@ uni_platform* installed_platform = nullptr;
|
|||
bool observed_status_led_on = false;
|
||||
int observed_status_led_writes = 0;
|
||||
uint32_t now_ms = 0;
|
||||
struct WiiAccelFixture {
|
||||
uni_hid_device_t* device = nullptr;
|
||||
int32_t acceleration[3]{};
|
||||
uint32_t sequence = 0;
|
||||
bool valid = false;
|
||||
};
|
||||
WiiAccelFixture remote_accel_fixture;
|
||||
WiiAccelFixture nunchuk_accel_fixture;
|
||||
|
||||
bool wii_accel_fixture_snapshot(const WiiAccelFixture& fixture,
|
||||
uni_hid_device_t* controller,
|
||||
int32_t acceleration[3], uint32_t* sequence) {
|
||||
if (!fixture.valid || fixture.device != controller) return false;
|
||||
memcpy(acceleration, fixture.acceleration, sizeof(fixture.acceleration));
|
||||
*sequence = fixture.sequence;
|
||||
return true;
|
||||
}
|
||||
bool bondable = true;
|
||||
bool ssp_auto_accept = true;
|
||||
uint8_t accepted_stk_methods = 0xff;
|
||||
|
|
@ -722,6 +739,16 @@ extern "C" void uni_hid_parser_wii_set_mode(
|
|||
"Wii mode reconfiguration must retain the ready connection");
|
||||
}
|
||||
|
||||
extern "C" bool uni_hid_parser_wii_accel_snapshot(
|
||||
uni_hid_device_t* controller, int32_t acceleration[3], uint32_t* sequence) {
|
||||
return wii_accel_fixture_snapshot(remote_accel_fixture, controller, acceleration, sequence);
|
||||
}
|
||||
|
||||
extern "C" bool uni_hid_parser_wii_nunchuk_accel_snapshot(
|
||||
uni_hid_device_t* controller, int32_t acceleration[3], uint32_t* sequence) {
|
||||
return wii_accel_fixture_snapshot(nunchuk_accel_fixture, controller, acceleration, sequence);
|
||||
}
|
||||
|
||||
#ifdef SWITCH_PICO_HAPTICS_EXPERIMENT
|
||||
namespace {
|
||||
std::vector<btstack_timer_source_t*> native_timers;
|
||||
|
|
@ -5496,6 +5523,81 @@ uni_hid_device_t wii_device(int index) {
|
|||
return result;
|
||||
}
|
||||
|
||||
void test_wii_accelerometer_streams() {
|
||||
start_pairing_backend();
|
||||
auto remote = wii_device(0);
|
||||
remote.controller_subtype = CONTROLLER_SUBTYPE_WIIMOTE_NUNCHUK_ACCEL;
|
||||
require(platform_on_device_ready(&remote) == UNI_ERROR_SUCCESS, "Wii must become ready");
|
||||
const auto generation = slot_snapshot(0).connection_generation;
|
||||
if (kDefaultMotionEnabled) {
|
||||
require(bluepad32_input_backend_toggle_motion(0, generation),
|
||||
"Wii console motion must be disabled for independent gesture streams");
|
||||
}
|
||||
remote_accel_fixture = {&remote, {8192, 0, 0}, 7, true};
|
||||
nunchuk_accel_fixture = {&remote, {0, 8192, 0}, 9, false};
|
||||
uni_controller_t input{};
|
||||
input.klass = UNI_CONTROLLER_CLASS_GAMEPAD;
|
||||
input.gamepad.accel[0] = 8192;
|
||||
input.gamepad.gyro[2] = 1024;
|
||||
now_ms = 100;
|
||||
platform_on_controller_data(&remote, &input);
|
||||
auto snapshot = slot_snapshot(0);
|
||||
require(snapshot.state.motion_sample_count == 0 &&
|
||||
snapshot.accelerometer.valid && !snapshot.nunchuk_accelerometer.valid &&
|
||||
snapshot.accelerometer.y == -4096 && snapshot.accelerometer.timestamp_ms == 100,
|
||||
"Remote gesture acceleration must remain calibrated with console motion off");
|
||||
|
||||
now_ms = 125;
|
||||
nunchuk_accel_fixture.valid = true;
|
||||
platform_on_controller_data(&remote, &input);
|
||||
snapshot = slot_snapshot(0);
|
||||
require(snapshot.state.motion_sample_count == 0 &&
|
||||
snapshot.nunchuk_accelerometer.valid && snapshot.nunchuk_accelerometer.z == 4096 &&
|
||||
snapshot.nunchuk_accelerometer.timestamp_ms == 125 &&
|
||||
snapshot.accelerometer.timestamp_ms == 100,
|
||||
"Nunchuk interleave must publish its own sample without refreshing the Remote");
|
||||
|
||||
now_ms = 150;
|
||||
++remote_accel_fixture.sequence; // Same physical values, genuinely new report.
|
||||
platform_on_controller_data(&remote, &input);
|
||||
now_ms = 200; // Status/ack callback exposes the same parser snapshots.
|
||||
platform_on_controller_data(&remote, &input);
|
||||
snapshot = slot_snapshot(0);
|
||||
bluepad32_input_backend_report_sent(0);
|
||||
snapshot = slot_snapshot(0);
|
||||
require(snapshot.accelerometer.timestamp_ms == 150 &&
|
||||
snapshot.nunchuk_accelerometer.timestamp_ms == 125 &&
|
||||
snapshot.accelerometer.valid && snapshot.nunchuk_accelerometer.valid,
|
||||
"cached parser callbacks and USB consumption must not freshen either sensor");
|
||||
|
||||
now_ms = 225;
|
||||
nunchuk_accel_fixture.valid = false;
|
||||
platform_on_controller_data(&remote, &input);
|
||||
snapshot = slot_snapshot(0);
|
||||
require(!snapshot.nunchuk_accelerometer.valid &&
|
||||
snapshot.accelerometer.valid && snapshot.accelerometer.timestamp_ms == 150,
|
||||
"Nunchuk detach/calibration failure must immediately invalidate only its own sensor");
|
||||
now_ms = 250;
|
||||
nunchuk_accel_fixture.valid = true;
|
||||
++nunchuk_accel_fixture.sequence;
|
||||
platform_on_controller_data(&remote, &input);
|
||||
snapshot = slot_snapshot(0);
|
||||
require(snapshot.nunchuk_accelerometer.valid &&
|
||||
snapshot.nunchuk_accelerometer.timestamp_ms == 250 &&
|
||||
snapshot.accelerometer.timestamp_ms == 150,
|
||||
"replacement Nunchuk must resume with its own fresh timestamp");
|
||||
|
||||
platform_on_device_disconnected(&remote);
|
||||
snapshot = slot_snapshot(0);
|
||||
require(!snapshot.accelerometer.valid && !snapshot.nunchuk_accelerometer.valid,
|
||||
"Remote disconnect must invalidate both accelerometer streams");
|
||||
auto replacement = wii_device(0);
|
||||
require(platform_on_device_ready(&replacement) == UNI_ERROR_SUCCESS, "replacement Wii must become ready");
|
||||
snapshot = slot_snapshot(0);
|
||||
require(!snapshot.accelerometer.valid && !snapshot.nunchuk_accelerometer.valid,
|
||||
"reused backend slot must not inherit either sensor sample");
|
||||
}
|
||||
|
||||
void dispatch_wii_requests() {
|
||||
wii_dispatch_active = true;
|
||||
now_ms += g_rumble_timer.timeout_ms;
|
||||
|
|
@ -5945,6 +6047,10 @@ int main(int argc, char** argv) {
|
|||
test_wii_orientation_races();
|
||||
return 0;
|
||||
}
|
||||
if (scenario == "wii-accelerometers") {
|
||||
test_wii_accelerometer_streams();
|
||||
return 0;
|
||||
}
|
||||
if (scenario == "switch2-hd-pro") {
|
||||
test_switch2_hd_pro();
|
||||
} else if (scenario == "switch2-gesture-timing") {
|
||||
|
|
|
|||
|
|
@ -13,10 +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;
|
||||
}
|
||||
bool uni_hid_parser_wii_accel_snapshot(
|
||||
uni_hid_device_t*, int32_t[3], uint32_t*);
|
||||
bool uni_hid_parser_wii_nunchuk_accel_snapshot(
|
||||
uni_hid_device_t*, int32_t[3], uint32_t*);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1363,6 +1363,226 @@ void test_early_rebound_cannot_become_a_delayed_swing() {
|
|||
require(presses == 2, "a new stroke after cooldown and release must trigger");
|
||||
}
|
||||
|
||||
void test_combined_swings_have_priority_at_window_boundary() {
|
||||
for (bool nunchuk_first : {false, true}) {
|
||||
for (unsigned delay : {0u, 100u, 110u}) {
|
||||
WiiSwingGestures gestures;
|
||||
uint32_t now = UINT32_MAX - 100u;
|
||||
uint32_t sequence = 0;
|
||||
unsigned counts[3]{};
|
||||
auto feed = [&](int16_t first, int16_t second) {
|
||||
now += 10;
|
||||
++sequence;
|
||||
WiiAccelerometerSample remote{
|
||||
nunchuk_first ? second : first, 0, 4096, sequence, now, true};
|
||||
WiiAccelerometerSample nunchuk{
|
||||
nunchuk_first ? first : second, 0, 4096, sequence, now, true};
|
||||
auto result = gestures.update(remote, nunchuk, now, 1, 1, 7, 100);
|
||||
for (unsigned i = 0; i < 3; ++i)
|
||||
if (result.started & (1u << i)) ++counts[i];
|
||||
return result;
|
||||
};
|
||||
for (unsigned i = 0; i < 20; ++i) feed(0, 0);
|
||||
for (unsigned step = 0; step < 40; ++step) {
|
||||
auto result = feed(10000, step * 10 >= delay ? 10000 : 0);
|
||||
if (delay <= 100)
|
||||
require((result.active & 3u) == 0, "combined gesture leaked individual buttons");
|
||||
}
|
||||
if (delay <= 100)
|
||||
require(counts[2] == 1 && counts[0] == 0 && counts[1] == 0,
|
||||
"simultaneous/in-window strokes must start only the combined action");
|
||||
else
|
||||
require(counts[2] == 0 && counts[0] == 1 && counts[1] == 1,
|
||||
"out-of-window strokes must remain two individual actions");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void test_combined_pending_cancels_on_detach_and_modifier_release() {
|
||||
for (bool detach : {false, true}) {
|
||||
WiiSwingGestures gestures;
|
||||
uint32_t now = 0, sequence = 0;
|
||||
auto feed = [&](int16_t force, uint8_t allowed, bool nunchuk_valid) {
|
||||
now += 10;
|
||||
++sequence;
|
||||
WiiAccelerometerSample remote{force, 0, 4096, sequence, now, true};
|
||||
WiiAccelerometerSample nunchuk{0, 0, 4096, sequence, now, nunchuk_valid};
|
||||
return gestures.update(remote, nunchuk, now, 1, 1, allowed, 100);
|
||||
};
|
||||
for (unsigned i = 0; i < 20; ++i) feed(0, 7, true);
|
||||
feed(10000, 7, true);
|
||||
require(feed(10000, 7, true).started == 0, "single must wait while combined is eligible");
|
||||
for (unsigned i = 0; i < 20; ++i)
|
||||
require(feed(10000, detach ? 7 : 3, !detach).started == 0,
|
||||
"detaching or releasing combined modifier replayed a pending action");
|
||||
}
|
||||
WiiSwingGestures remote_only;
|
||||
WiiAccelerometerSample missing{};
|
||||
WiiAccelerometerSample sample{0, 0, 4096, 0, 0, true};
|
||||
for (unsigned i = 0; i < 20; ++i) {
|
||||
sample.timestamp_ms += 10;
|
||||
++sample.sequence;
|
||||
remote_only.update(sample, missing, sample.timestamp_ms, 1, 1, 7, 100);
|
||||
}
|
||||
sample.x = 10000;
|
||||
++sample.sequence;
|
||||
sample.timestamp_ms += 10;
|
||||
remote_only.update(sample, missing, sample.timestamp_ms, 1, 1, 7, 100);
|
||||
++sample.sequence;
|
||||
sample.timestamp_ms += 10;
|
||||
require(remote_only.update(sample, missing, sample.timestamp_ms, 1, 1, 7, 100).started == 1,
|
||||
"absent Nunchuk must not delay the Remote gesture");
|
||||
}
|
||||
|
||||
void test_nunchuk_button_and_combined_macro_runtime() {
|
||||
prepare_profiles();
|
||||
auto& profile = rows[0].profiles[0];
|
||||
profile.swing.button = 2;
|
||||
profile.nunchuk_swing.button = 1;
|
||||
profile.combined_swing.macro = 0;
|
||||
profile.macros[0].step_count = 1;
|
||||
profile.macros[0].mode = ControllerProfileMacroMode::kWhileHeld;
|
||||
profile.macros[0].trigger_mask = 1; // Not held during the combined gesture.
|
||||
profile.macro_step_count = 1;
|
||||
profile.macro_steps[0].override_flags = kControllerProfileOverrideButtons;
|
||||
profile.macro_steps[0].duration_ms = 100;
|
||||
profile.macro_steps[0].output_button_mask = 8;
|
||||
for (unsigned i = 1; i < CONTROLLER_PROFILE_MACRO_COUNT; ++i)
|
||||
profile.macros[i].first_step = 1;
|
||||
auto snapshot = make_snapshot(0);
|
||||
uint32_t now = 0, sequence = 0;
|
||||
auto feed = [&](int16_t remote, int16_t nunchuk) {
|
||||
now += 10;
|
||||
++sequence;
|
||||
snapshot.accelerometer = {remote, 0, 4096, sequence, now, true};
|
||||
snapshot.nunchuk_accelerometer = {nunchuk, 0, 4096, sequence, now, true};
|
||||
return runtime_transform(0, snapshot, now);
|
||||
};
|
||||
for (unsigned i = 0; i < 20; ++i) feed(0, 0);
|
||||
feed(10000, 10000);
|
||||
auto result = feed(10000, 10000);
|
||||
require(result.state.button_north && !result.state.button_west && !result.state.button_east,
|
||||
"combined gesture must start its macro instead of both individual buttons");
|
||||
for (unsigned i = 0; i < 8; ++i)
|
||||
require(feed(0, 0).state.button_north, "gesture macro stopped when physical trigger was absent");
|
||||
feed(0, 0);
|
||||
require(!feed(0, 0).state.button_north, "gesture macro must finish exactly one cycle");
|
||||
for (unsigned i = 0; i < 30; ++i) feed(0, 0);
|
||||
feed(0, 10000);
|
||||
require(!feed(0, 10000).state.button_east, "Nunchuk alone must wait for combination window");
|
||||
for (unsigned i = 0; i < 10; ++i) feed(0, 10000);
|
||||
result = feed(0, 10000);
|
||||
require(result.state.button_east && !result.state.button_west && !result.state.button_north,
|
||||
"Nunchuk-only gesture must produce its configured button after the window");
|
||||
}
|
||||
|
||||
void test_gesture_macros_rearm_during_continuous_swings() {
|
||||
prepare_profiles();
|
||||
auto& profile = rows[0].profiles[0];
|
||||
profile.swing.macro = 0;
|
||||
profile.macros[0].step_count = 1;
|
||||
profile.macros[0].mode = ControllerProfileMacroMode::kRepeat;
|
||||
profile.macros[0].repeat_count = 5;
|
||||
profile.macro_step_count = 1;
|
||||
profile.macro_steps[0].override_flags = kControllerProfileOverrideButtons;
|
||||
profile.macro_steps[0].duration_ms = 100;
|
||||
profile.macro_steps[0].output_button_mask = 8;
|
||||
auto snapshot = make_snapshot(0);
|
||||
uint32_t now = 0;
|
||||
unsigned starts = 0;
|
||||
bool held = false;
|
||||
auto feed = [&](int16_t force) {
|
||||
now += 10;
|
||||
snapshot.accelerometer = {force, 0, 4096, snapshot.accelerometer.sequence + 1, now, true};
|
||||
const bool pressed = runtime_transform(0, snapshot, now).state.button_north;
|
||||
if (pressed && !held) ++starts;
|
||||
held = pressed;
|
||||
};
|
||||
for (unsigned i = 0; i < 20; ++i) feed(0);
|
||||
for (unsigned stroke = 0; stroke < 4; ++stroke) {
|
||||
const int direction = stroke % 2 ? -1 : 1;
|
||||
for (unsigned i = 0; i < 24; ++i) feed(direction * 10000);
|
||||
for (unsigned i = 0; i < 8; ++i) feed(direction * 2000);
|
||||
require(starts == stroke + 1 && !held,
|
||||
"gesture macro must play once per stroke without requiring a full stop");
|
||||
}
|
||||
}
|
||||
|
||||
struct CombinedConfirmationRig {
|
||||
WiiSwingGestures gestures;
|
||||
uint32_t now = 1000, sequence = 0;
|
||||
unsigned counts[3]{};
|
||||
CombinedConfirmationRig() {
|
||||
for (unsigned i = 0; i < 20; ++i) feed(0, 0);
|
||||
}
|
||||
WiiSwingGestureResult feed(int16_t remote_force, int16_t nunchuk_force,
|
||||
uint8_t allowed = 7) {
|
||||
now += 10;
|
||||
++sequence;
|
||||
WiiAccelerometerSample remote{remote_force, 0, 4096, sequence, now, true};
|
||||
WiiAccelerometerSample nunchuk{nunchuk_force, 0, 4096, sequence, now, true};
|
||||
const auto result = gestures.update(remote, nunchuk, now, 1, 1, allowed, 100);
|
||||
for (unsigned i = 0; i < 3; ++i)
|
||||
if (result.started & (1u << i)) ++counts[i];
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
void test_combined_accepts_smaller_motion_before_or_after_full_swing() {
|
||||
for (bool weak_nunchuk : {false, true}) {
|
||||
for (int delay : {-60, 0, 60}) {
|
||||
CombinedConfirmationRig rig;
|
||||
for (int step = 0; step < 40; ++step) {
|
||||
const int strong_start = delay < 0 ? -delay : 0;
|
||||
const int weak_start = delay > 0 ? delay : 0;
|
||||
const int16_t strong = step * 10 >= strong_start ? 10000 : 0;
|
||||
const int16_t weak = step * 10 >= weak_start ? 4500 : 0;
|
||||
const auto result = rig.feed(weak_nunchuk ? strong : weak,
|
||||
weak_nunchuk ? weak : strong);
|
||||
require((result.active & 3) == 0, "confirmed combined motion leaked a single action");
|
||||
}
|
||||
require(rig.counts[2] == 1 && rig.counts[0] == 0 && rig.counts[1] == 0,
|
||||
"one full swing plus smaller companion motion must combine in either order");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void test_confirmation_does_not_weaken_individuals_or_accept_noise() {
|
||||
for (uint8_t allowed : {uint8_t{3}, uint8_t{7}}) {
|
||||
CombinedConfirmationRig rig;
|
||||
for (unsigned i = 0; i < 40; ++i) rig.feed(4500, 4500, allowed);
|
||||
require(rig.counts[0] == 0 && rig.counts[1] == 0 && rig.counts[2] == 0,
|
||||
"two subthreshold movements must not generate any action");
|
||||
}
|
||||
for (bool spike : {false, true}) {
|
||||
CombinedConfirmationRig rig;
|
||||
for (unsigned i = 0; i < 40; ++i)
|
||||
rig.feed(10000, spike && i == 2 ? 4500 : 0);
|
||||
require(rig.counts[0] == 1 && rig.counts[1] == 0 && rig.counts[2] == 0,
|
||||
"a stationary companion or one noisy sample must not confirm a combined swing");
|
||||
}
|
||||
}
|
||||
|
||||
void test_consumed_confirmation_suppresses_late_full_swing() {
|
||||
CombinedConfirmationRig rig;
|
||||
for (unsigned i = 0; i < 45; ++i)
|
||||
rig.feed(10000, i < 8 ? 4500 : 10000);
|
||||
require(rig.counts[2] == 1 && rig.counts[0] == 0 && rig.counts[1] == 0,
|
||||
"a confirmation growing into a full swing must not leak a later individual action");
|
||||
}
|
||||
|
||||
void test_expired_or_discarded_confirmation_cannot_be_reused() {
|
||||
for (bool discard : {false, true}) {
|
||||
CombinedConfirmationRig rig;
|
||||
for (unsigned i = 0; i < 3; ++i) rig.feed(0, 4500);
|
||||
if (discard) rig.gestures.discard_actions();
|
||||
else for (unsigned i = 0; i < 12; ++i) rig.feed(0, 0);
|
||||
for (unsigned i = 0; i < 40; ++i) rig.feed(10000, 0);
|
||||
require(rig.counts[0] == 1 && rig.counts[1] == 0 && rig.counts[2] == 0,
|
||||
"expired or macro-discarded evidence must not confirm a later swing");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool bluepad32_input_backend_toggle_motion(
|
||||
|
|
@ -1441,5 +1661,13 @@ int main() {
|
|||
test_swing_modifier_release_cancels_and_requires_fresh_settle();
|
||||
test_back_and_forth_swings_do_not_require_full_stops();
|
||||
test_early_rebound_cannot_become_a_delayed_swing();
|
||||
test_combined_swings_have_priority_at_window_boundary();
|
||||
test_combined_pending_cancels_on_detach_and_modifier_release();
|
||||
test_nunchuk_button_and_combined_macro_runtime();
|
||||
test_gesture_macros_rearm_during_continuous_swings();
|
||||
test_combined_accepts_smaller_motion_before_or_after_full_swing();
|
||||
test_confirmation_does_not_weaken_individuals_or_accept_noise();
|
||||
test_consumed_confirmation_suppresses_late_full_swing();
|
||||
test_expired_or_discarded_confirmation_cannot_be_reused();
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,6 +39,19 @@ bool read_encoded_database(void*, size_t offset, uint8_t* output,
|
|||
return true;
|
||||
}
|
||||
|
||||
void require_no_new_swing_actions(const ControllerProfile& profile) {
|
||||
require(profile.swing.macro == CONTROLLER_PROFILE_NO_BUTTON &&
|
||||
profile.nunchuk_swing.button == CONTROLLER_PROFILE_NO_BUTTON &&
|
||||
profile.nunchuk_swing.macro == CONTROLLER_PROFILE_NO_BUTTON &&
|
||||
profile.nunchuk_swing.sensitivity == 1 &&
|
||||
profile.nunchuk_swing.modifier == CONTROLLER_PROFILE_NO_BUTTON &&
|
||||
profile.combined_swing.button == CONTROLLER_PROFILE_NO_BUTTON &&
|
||||
profile.combined_swing.macro == CONTROLLER_PROFILE_NO_BUTTON &&
|
||||
profile.combined_swing.modifier == CONTROLLER_PROFILE_NO_BUTTON &&
|
||||
profile.combination_window_ms == 100,
|
||||
"legacy profile enabled a new gesture action or lost its defaults");
|
||||
}
|
||||
|
||||
void test_pair_identity_wire_and_member_validation() {
|
||||
ControllerIdentity left = identity(1);
|
||||
left.transport = ControllerTransport::kBle;
|
||||
|
|
@ -123,9 +136,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] == 8 && encoded[1] == 0 &&
|
||||
require(encoded[0] == 9 && encoded[1] == 0 &&
|
||||
encoded[2] == 0x80 && encoded[3] == 1,
|
||||
"profile header is not little-endian v8/384");
|
||||
"profile header is not little-endian v9/384");
|
||||
for (uint8_t index = 0;
|
||||
index < CONTROLLER_PROFILE_LOGICAL_BUTTON_COUNT; ++index) {
|
||||
require(encoded[4 + index] == index,
|
||||
|
|
@ -280,6 +293,7 @@ void test_legacy_profile_migration() {
|
|||
migrated.swing.sensitivity == 1 &&
|
||||
migrated.swing.modifier == CONTROLLER_PROFILE_NO_BUTTON,
|
||||
"legacy padding enabled a swing gesture");
|
||||
require_no_new_swing_actions(migrated);
|
||||
require(migrated.triggers[0].digital_threshold ==
|
||||
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD &&
|
||||
migrated.triggers[1].digital_threshold ==
|
||||
|
|
@ -405,6 +419,9 @@ void test_legacy_profile_migration() {
|
|||
previous_encoded[0] = version;
|
||||
previous_encoded[75] = version == 4 ? 0x21 : 0;
|
||||
migrated.swing = {2, 2, 24};
|
||||
migrated.nunchuk_swing = {0, 0, 0};
|
||||
migrated.combined_swing = {15, CONTROLLER_PROFILE_NO_BUTTON, 24};
|
||||
migrated.combination_window_ms = 30;
|
||||
require(controller_profile_decode(
|
||||
previous_encoded, sizeof(previous_encoded), &migrated) &&
|
||||
migrated.triggers[0].output ==
|
||||
|
|
@ -423,6 +440,7 @@ void test_legacy_profile_migration() {
|
|||
migrated.swing.sensitivity == 1 &&
|
||||
migrated.swing.modifier == CONTROLLER_PROFILE_NO_BUTTON,
|
||||
"legacy control mapping or action migration lost settings");
|
||||
require_no_new_swing_actions(migrated);
|
||||
}
|
||||
|
||||
ControllerProfile current =
|
||||
|
|
@ -554,6 +572,7 @@ void test_set_b_sparse_extension_and_migration() {
|
|||
decoded.swing.sensitivity == 1 &&
|
||||
decoded.swing.modifier == CONTROLLER_PROFILE_NO_BUTTON,
|
||||
"schema5 full136-byte macro stream did not migrate");
|
||||
require_no_new_swing_actions(decoded);
|
||||
require(controller_profile_encode(decoded, encoded, sizeof(encoded)) &&
|
||||
memcmp(&legacy[4], &encoded[4], sizeof(legacy) - 4) == 0,
|
||||
"schema5 migration changed existing profile data");
|
||||
|
|
@ -637,6 +656,7 @@ void test_schema6_migration_preserves_every_setting() {
|
|||
profile.swing.sensitivity == 1 &&
|
||||
profile.swing.modifier == CONTROLLER_PROFILE_NO_BUTTON,
|
||||
"schema6 padding became a swing gesture");
|
||||
require_no_new_swing_actions(profile);
|
||||
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,
|
||||
|
|
@ -750,6 +770,7 @@ void test_schema7_migration_preserves_extra_controls_and_macros() {
|
|||
migrated.swing.sensitivity == 1 &&
|
||||
migrated.swing.modifier == CONTROLLER_PROFILE_NO_BUTTON,
|
||||
"schema7 migration lost existing settings or enabled swing");
|
||||
require_no_new_swing_actions(migrated);
|
||||
for (size_t offset = 364; offset < sizeof(legacy); ++offset) {
|
||||
legacy[offset] = 1;
|
||||
require(!controller_profile_decode(legacy, sizeof(legacy), &migrated),
|
||||
|
|
@ -758,39 +779,124 @@ void test_schema7_migration_preserves_extra_controls_and_macros() {
|
|||
}
|
||||
}
|
||||
|
||||
void test_schema8_migration_preserves_remote_swing() {
|
||||
uint8_t legacy[sizeof(kLegacySchema6Profile)]{};
|
||||
memcpy(legacy, kLegacySchema6Profile, sizeof(legacy));
|
||||
legacy[0] = 8;
|
||||
legacy[344] = 16;
|
||||
legacy[351] = 15;
|
||||
legacy[358] = 0x40;
|
||||
legacy[362] = 0x55;
|
||||
legacy[363] = 0x2a;
|
||||
legacy[364] = 15;
|
||||
legacy[365] = 2;
|
||||
legacy[366] = 24;
|
||||
ControllerProfile migrated{};
|
||||
migrated.swing.macro = 0;
|
||||
migrated.nunchuk_swing = {0, 0, 0};
|
||||
migrated.combined_swing = {1, CONTROLLER_PROFILE_NO_BUTTON, 16};
|
||||
migrated.combination_window_ms = 200;
|
||||
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, 365) == 0 &&
|
||||
migrated.swing.button == 15 &&
|
||||
migrated.swing.sensitivity == 2 &&
|
||||
migrated.swing.modifier == 24,
|
||||
"schema8 migration changed Remote, extras, or macro settings");
|
||||
require_no_new_swing_actions(migrated);
|
||||
for (size_t offset = 367; offset < sizeof(legacy); ++offset) {
|
||||
legacy[offset] = 1;
|
||||
require(!controller_profile_decode(legacy, sizeof(legacy), &migrated),
|
||||
"schema8 interpreted reserved padding as new swing actions");
|
||||
legacy[offset] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void test_swing_wire_settings_and_rejection() {
|
||||
ControllerProfile profile =
|
||||
controller_profile_default(controller_identity_global(), 0);
|
||||
profile.swing = {15, 2, 24};
|
||||
profile.nunchuk_swing = {0, 0, 0};
|
||||
profile.combined_swing = {4, CONTROLLER_PROFILE_NO_BUTTON, 16};
|
||||
profile.combination_window_ms = 30;
|
||||
profile.macros[0].step_count = 1;
|
||||
profile.macro_step_count = 1;
|
||||
profile.macro_steps[0].duration_ms = 1;
|
||||
for (uint8_t index = 1; index < CONTROLLER_PROFILE_MACRO_COUNT; ++index) {
|
||||
profile.macros[index].first_step = 1;
|
||||
}
|
||||
uint8_t encoded[CONTROLLER_PROFILE_ENCODED_SIZE]{};
|
||||
ControllerProfile decoded{};
|
||||
const uint8_t button_actions[] = {
|
||||
15, 2, 24, 255, 0, 0, 0, 255, 4, 255, 16, 30};
|
||||
require(controller_profile_encode(profile, encoded, sizeof(encoded)) &&
|
||||
encoded[364] == 15 && encoded[365] == 2 && encoded[366] == 24 &&
|
||||
memcmp(encoded + 364, button_actions, sizeof(button_actions)) == 0 &&
|
||||
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");
|
||||
decoded.swing.modifier == 24 &&
|
||||
decoded.nunchuk_swing.button == 0 &&
|
||||
decoded.nunchuk_swing.sensitivity == 0 &&
|
||||
decoded.nunchuk_swing.modifier == 0 &&
|
||||
decoded.combined_swing.button == 4 &&
|
||||
decoded.combined_swing.modifier == 16 &&
|
||||
decoded.combination_window_ms == 30,
|
||||
"independent swing button actions did not use the schema9 layout");
|
||||
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");
|
||||
{15, 2, 25}, {15, 2, 254}, {255, 3, 255}, {255, 1, 25},
|
||||
{255, 1, 24, 4}, {255, 1, 24, 254}, {0, 1, 24, 0}};
|
||||
for (uint8_t sensor = 0; sensor < 2; ++sensor) {
|
||||
const size_t offset = 364 + sensor * 4;
|
||||
for (const auto& settings : invalid_settings) {
|
||||
ControllerProfile invalid = profile;
|
||||
(sensor == 0 ? invalid.swing : invalid.nunchuk_swing) = settings;
|
||||
uint8_t malformed[sizeof(encoded)]{};
|
||||
require(!controller_profile_validate(invalid) &&
|
||||
!controller_profile_encode(invalid, malformed,
|
||||
sizeof(malformed)),
|
||||
"invalid single-sensor swing action was accepted for encoding");
|
||||
memcpy(malformed, encoded, sizeof(malformed));
|
||||
malformed[offset] = settings.button;
|
||||
malformed[offset + 1] = settings.sensitivity;
|
||||
malformed[offset + 2] = settings.modifier;
|
||||
malformed[offset + 3] = settings.macro;
|
||||
require(!controller_profile_decode(malformed, sizeof(malformed), &decoded),
|
||||
"invalid single-sensor swing wire action was 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) {
|
||||
const ControllerProfileCombinedSwingConfiguration invalid_combined[] = {
|
||||
{16, 255, 24}, {254, 255, 24}, {255, 4, 24},
|
||||
{255, 254, 24}, {0, 0, 24}, {15, 255, 25}, {255, 255, 254}};
|
||||
for (const auto& settings : invalid_combined) {
|
||||
ControllerProfile invalid = profile;
|
||||
invalid.combined_swing = settings;
|
||||
uint8_t malformed[sizeof(encoded)]{};
|
||||
require(!controller_profile_validate(invalid) &&
|
||||
!controller_profile_encode(invalid, malformed, sizeof(malformed)),
|
||||
"invalid combined swing action was accepted for encoding");
|
||||
memcpy(malformed, encoded, sizeof(malformed));
|
||||
malformed[372] = settings.button;
|
||||
malformed[373] = settings.macro;
|
||||
malformed[374] = settings.modifier;
|
||||
require(!controller_profile_decode(malformed, sizeof(malformed), &decoded),
|
||||
"invalid combined swing wire action was accepted");
|
||||
}
|
||||
const uint8_t invalid_windows[] = {29, 201};
|
||||
for (uint8_t window : invalid_windows) {
|
||||
ControllerProfile invalid = profile;
|
||||
invalid.combination_window_ms = window;
|
||||
uint8_t malformed[sizeof(encoded)]{};
|
||||
require(!controller_profile_validate(invalid) &&
|
||||
!controller_profile_encode(invalid, malformed, sizeof(malformed)),
|
||||
"out-of-range combination window was accepted for encoding");
|
||||
memcpy(malformed, encoded, sizeof(malformed));
|
||||
malformed[375] = window;
|
||||
require(!controller_profile_decode(malformed, sizeof(malformed), &decoded),
|
||||
"out-of-range combination window was accepted from wire");
|
||||
}
|
||||
for (size_t offset = 376; offset < sizeof(encoded); ++offset) {
|
||||
require(encoded[offset] == 0, "schema9 reserved tail was not zero");
|
||||
encoded[offset] = 1;
|
||||
require(!controller_profile_decode(encoded, sizeof(encoded), &decoded),
|
||||
"nonzero swing extension reservation was accepted");
|
||||
|
|
@ -798,12 +904,85 @@ void test_swing_wire_settings_and_rejection() {
|
|||
}
|
||||
profile.swing = {CONTROLLER_PROFILE_NO_BUTTON, 0,
|
||||
CONTROLLER_PROFILE_NO_BUTTON};
|
||||
profile.nunchuk_swing = profile.swing;
|
||||
profile.combined_swing = {};
|
||||
require(controller_profile_encode(profile, encoded, sizeof(encoded)) &&
|
||||
controller_profile_decode(encoded, sizeof(encoded), &decoded) &&
|
||||
decoded.swing.button == CONTROLLER_PROFILE_NO_BUTTON &&
|
||||
decoded.swing.macro == CONTROLLER_PROFILE_NO_BUTTON &&
|
||||
decoded.swing.sensitivity == 0 &&
|
||||
decoded.swing.modifier == CONTROLLER_PROFILE_NO_BUTTON,
|
||||
"disabled swing did not preserve sensitivity and absent modifier");
|
||||
decoded.nunchuk_swing.button == CONTROLLER_PROFILE_NO_BUTTON &&
|
||||
decoded.nunchuk_swing.macro == CONTROLLER_PROFILE_NO_BUTTON &&
|
||||
decoded.nunchuk_swing.sensitivity == 0 &&
|
||||
decoded.combined_swing.button == CONTROLLER_PROFILE_NO_BUTTON &&
|
||||
decoded.combined_swing.macro == CONTROLLER_PROFILE_NO_BUTTON,
|
||||
"disabled gestures became actions or lost their sensitivity");
|
||||
}
|
||||
|
||||
void test_swing_macro_targets_require_playable_steps() {
|
||||
ControllerProfile profile =
|
||||
controller_profile_default(controller_identity_global(), 0);
|
||||
profile.macros[0].step_count = 2;
|
||||
for (uint8_t index = 1; index < CONTROLLER_PROFILE_MACRO_COUNT; ++index) {
|
||||
profile.macros[index].first_step = 2;
|
||||
}
|
||||
profile.macros[3].step_count = 1;
|
||||
profile.macro_step_count = 3;
|
||||
profile.macro_steps[1].duration_ms = 1;
|
||||
profile.macro_steps[2].duration_ms = 10000;
|
||||
profile.swing = {255, 2, 24, 0};
|
||||
profile.nunchuk_swing = {255, 0, 255, 3};
|
||||
profile.combined_swing = {255, 0, 0};
|
||||
profile.combination_window_ms = 200;
|
||||
uint8_t encoded[CONTROLLER_PROFILE_ENCODED_SIZE]{};
|
||||
ControllerProfile decoded{};
|
||||
const uint8_t macro_actions[] = {
|
||||
255, 2, 24, 0, 255, 0, 255, 3, 255, 0, 0, 200};
|
||||
for (uint8_t mode = 0; mode <= 3; ++mode) {
|
||||
profile.macros[0].mode = static_cast<ControllerProfileMacroMode>(mode);
|
||||
require(controller_profile_encode(profile, encoded, sizeof(encoded)) &&
|
||||
memcmp(encoded + 364, macro_actions, sizeof(macro_actions)) == 0 &&
|
||||
controller_profile_decode(encoded, sizeof(encoded), &decoded) &&
|
||||
decoded.swing.macro == 0 && decoded.nunchuk_swing.macro == 3 &&
|
||||
decoded.combined_swing.macro == 0 &&
|
||||
decoded.combination_window_ms == 200 &&
|
||||
decoded.macros[0].mode == profile.macros[0].mode &&
|
||||
decoded.macros[0].trigger_mask == 0 &&
|
||||
decoded.macro_steps[0].duration_ms == 0 &&
|
||||
decoded.macro_steps[1].duration_ms == 1 &&
|
||||
decoded.macro_steps[2].duration_ms == 10000,
|
||||
"gesture macro targets lost steps, playback mode, or index boundaries");
|
||||
}
|
||||
profile.swing = {};
|
||||
profile.nunchuk_swing = {};
|
||||
profile.combined_swing = {};
|
||||
profile.macros[0].mode = ControllerProfileMacroMode::kOnce;
|
||||
profile.macro_steps[1].duration_ms = 0;
|
||||
require(controller_profile_encode(profile, encoded, sizeof(encoded)),
|
||||
"unbound zero-duration one-shot macro was rejected");
|
||||
const size_t macro_offsets[] = {367, 371, 373};
|
||||
for (uint8_t gesture = 0; gesture < 3; ++gesture) {
|
||||
for (uint8_t target = 0; target < 2; ++target) {
|
||||
ControllerProfile invalid = profile;
|
||||
uint8_t* bindings[] = {&invalid.swing.macro,
|
||||
&invalid.nunchuk_swing.macro,
|
||||
&invalid.combined_swing.macro};
|
||||
*bindings[gesture] = target;
|
||||
uint8_t malformed[sizeof(encoded)]{};
|
||||
require(!controller_profile_validate(invalid) &&
|
||||
!controller_profile_encode(invalid, malformed, sizeof(malformed)),
|
||||
"gesture accepted a zero-duration or empty macro target");
|
||||
memcpy(malformed, encoded, sizeof(malformed));
|
||||
malformed[macro_offsets[gesture]] = target;
|
||||
require(!controller_profile_decode(malformed, sizeof(malformed), &decoded),
|
||||
"wire gesture accepted a zero-duration or empty macro target");
|
||||
}
|
||||
}
|
||||
profile.macro_steps[1].duration_ms = 1;
|
||||
profile.macro_steps[2].duration_ms = 0;
|
||||
profile.nunchuk_swing.macro = 3;
|
||||
require(!controller_profile_validate(profile),
|
||||
"gesture used another macro's duration to validate its target");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
|
@ -817,6 +996,8 @@ int main() {
|
|||
test_schema6_migration_preserves_every_setting();
|
||||
test_extra_control_schema_round_trip_and_output_limits();
|
||||
test_schema7_migration_preserves_extra_controls_and_macros();
|
||||
test_schema8_migration_preserves_remote_swing();
|
||||
test_swing_wire_settings_and_rejection();
|
||||
test_swing_macro_targets_require_playable_steps();
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -813,6 +813,60 @@ void test_extra_shift_and_macro_sources_are_consumed() {
|
|||
"while-held macro survived release of an extra trigger source");
|
||||
}
|
||||
|
||||
void test_gesture_macros_play_once_and_preserve_trigger_inputs() {
|
||||
for (auto mode : {ControllerProfileMacroMode::kWhileHeld,
|
||||
ControllerProfileMacroMode::kToggle,
|
||||
ControllerProfileMacroMode::kRepeat}) {
|
||||
auto profile = profile_with_macro(ControllerProfileLogicalButton::kSouth);
|
||||
profile.macro_step_count = 1;
|
||||
profile.macros[0].step_count = 1;
|
||||
profile.macros[0].mode = mode;
|
||||
profile.macros[0].repeat_count = 5;
|
||||
profile.macro_steps[0].duration_ms = 100;
|
||||
profile.macro_steps[0].override_flags = kControllerProfileOverrideLeftStick;
|
||||
profile.macro_steps[0].left_stick_x = 12345;
|
||||
ControllerSyntheticInputContext context{};
|
||||
ControllerState input{};
|
||||
auto output = controller_synthetic_input_apply(&context, input, profile, 1000, 0, false, 0);
|
||||
require(output.state.left_stick_x == 12345, "gesture must start macro immediately");
|
||||
input.button_south = true;
|
||||
output = controller_synthetic_input_apply(&context, input, profile, 1050);
|
||||
require(output.state.left_stick_x == 12345 && output.state.button_south,
|
||||
"gesture macro consumed its unrelated physical trigger button");
|
||||
input.button_south = false;
|
||||
output = controller_synthetic_input_apply(&context, input, profile, 1090);
|
||||
require(output.state.left_stick_x == 12345, "gesture macro stopped on trigger release");
|
||||
output = controller_synthetic_input_apply(&context, input, profile, 1100);
|
||||
require(output.state.left_stick_x == 0 && !context.macro_active,
|
||||
"gesture macro repeated according to physical playback mode");
|
||||
output = controller_synthetic_input_apply(&context, input, profile, 1200, 0, false, 0);
|
||||
require(output.state.left_stick_x == 12345, "later gesture could not restart completed macro");
|
||||
input.button_capture = true;
|
||||
output = controller_synthetic_input_apply(&context, input, profile, 1210, 0, false, 0);
|
||||
require(!context.macro_active && output.state.left_stick_x == 0,
|
||||
"explicit cancel must beat a simultaneous gesture request");
|
||||
}
|
||||
}
|
||||
|
||||
void test_physical_macro_precedes_simultaneous_gesture_request() {
|
||||
auto profile = controller_profile_default(controller_identity_global(), 0);
|
||||
profile.macro_step_count = 2;
|
||||
for (unsigned i = 0; i < 2; ++i) {
|
||||
profile.macros[i].first_step = i;
|
||||
profile.macros[i].step_count = 1;
|
||||
profile.macro_steps[i].override_flags = kControllerProfileOverrideButtons;
|
||||
profile.macro_steps[i].duration_ms = 100;
|
||||
profile.macro_steps[i].output_button_mask = i == 0 ? 4 : 8;
|
||||
}
|
||||
profile.macros[1].trigger_mask = 1;
|
||||
ControllerState input{};
|
||||
input.button_south = true;
|
||||
ControllerSyntheticInputContext context{};
|
||||
const auto output = controller_synthetic_input_apply(&context, input, profile, 100, 0, false, 0);
|
||||
require(output.state.button_north && !output.state.button_west,
|
||||
"gesture request stole a simultaneous physical macro trigger");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
|
|
@ -829,5 +883,7 @@ int main() {
|
|||
test_shift_maps_consumption_and_physical_bindings();
|
||||
test_macro_playback_modes_and_bounded_cycle_skips();
|
||||
test_extra_shift_and_macro_sources_are_consumed();
|
||||
test_gesture_macros_play_once_and_preserve_trigger_inputs();
|
||||
test_physical_macro_precedes_simultaneous_gesture_request();
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -309,6 +309,9 @@ void test_current_schema_validation_and_atomic_selection() {
|
|||
updated.extra_button_map[6] = 16;
|
||||
updated.shift.extra_button_map[0] = 3;
|
||||
updated.swing = {2, 2, 24};
|
||||
updated.nunchuk_swing = {255, 0, 18, 0};
|
||||
updated.combined_swing = {15, 255, 16};
|
||||
updated.combination_window_ms = 200;
|
||||
updated.macros[0].trigger_mask = 1u << 24;
|
||||
updated.macros[0].cancel_control = 19;
|
||||
updated.macros[0].step_count = 1;
|
||||
|
|
@ -322,7 +325,7 @@ void test_current_schema_validation_and_atomic_selection() {
|
|||
require(controller_profile_encode(updated, encoded, sizeof(encoded)),
|
||||
"extended service profile did not encode");
|
||||
require(profile_service_begin(
|
||||
20, id, 6, CONTROLLER_PROFILE_EXTRA_CONTROL_SCHEMA_VERSION,
|
||||
20, id, 6, CONTROLLER_PROFILE_SWING_SCHEMA_VERSION,
|
||||
CONTROLLER_PROFILE_ENCODED_SIZE, 0) ==
|
||||
ConfigurationTransactionStatus::kUnsupportedSchema &&
|
||||
profile_service_begin(21, id, 6, CONTROLLER_PROFILE_SCHEMA_VERSION,
|
||||
|
|
@ -352,24 +355,46 @@ void test_current_schema_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");
|
||||
const struct {
|
||||
size_t offset;
|
||||
uint8_t value;
|
||||
} malformed_gestures[] = {
|
||||
{366, 25}, {367, 0}, {368, 0}, {369, 3}, {370, 25},
|
||||
{371, 1}, {371, 4}, {372, 16}, {373, 0}, {374, 25},
|
||||
{375, 29}, {375, 201}, {376, 1}, {121, 0}};
|
||||
uint8_t stored_before[sizeof(flash.bytes)];
|
||||
memcpy(stored_before, flash.bytes, sizeof(stored_before));
|
||||
uint32_t transaction_id = 25;
|
||||
for (const auto& malformed : malformed_gestures) {
|
||||
require(controller_profile_encode(updated, encoded, sizeof(encoded)),
|
||||
"gesture rejection baseline did not encode");
|
||||
encoded[malformed.offset] = malformed.value;
|
||||
require(profile_service_begin(
|
||||
transaction_id, id, 6, CONTROLLER_PROFILE_SCHEMA_VERSION,
|
||||
sizeof(encoded), profile_storage_crc32(encoded, sizeof(encoded))) ==
|
||||
ConfigurationTransactionStatus::kReceiving &&
|
||||
profile_service_append(transaction_id, 0, encoded, sizeof(encoded)) ==
|
||||
ConfigurationTransactionStatus::kReceiving &&
|
||||
profile_service_commit(transaction_id) ==
|
||||
ConfigurationTransactionStatus::kMalformed,
|
||||
"service admitted a malformed gesture or unplayable macro with a valid CRC");
|
||||
profile_service_selected_snapshot(&selected);
|
||||
const auto active = active_snapshot(id);
|
||||
require(selected.metadata.generation == old_selection.metadata.generation &&
|
||||
selected.profile.swing.button == old_selection.profile.swing.button &&
|
||||
selected.profile.nunchuk_swing.macro ==
|
||||
old_selection.profile.nunchuk_swing.macro &&
|
||||
selected.profile.combined_swing.button ==
|
||||
old_selection.profile.combined_swing.button &&
|
||||
active.profile.swing.button == old_selection.profile.swing.button &&
|
||||
active.profile.nunchuk_swing.macro ==
|
||||
old_selection.profile.nunchuk_swing.macro &&
|
||||
active.profile.combined_swing.button ==
|
||||
old_selection.profile.combined_swing.button &&
|
||||
memcmp(stored_before, flash.bytes, sizeof(stored_before)) == 0,
|
||||
"rejected gesture replaced a selected, active, or stored profile");
|
||||
++transaction_id;
|
||||
}
|
||||
|
||||
require(controller_profile_encode(updated, encoded, sizeof(encoded)),
|
||||
"valid replacement did not encode");
|
||||
|
|
@ -387,7 +412,11 @@ void test_current_schema_validation_and_atomic_selection() {
|
|||
profile_service_selected_snapshot(&selected);
|
||||
require(selected.metadata.generation == old_selection.metadata.generation &&
|
||||
active_snapshot(id).profile.turbo_defaults.rate_hz ==
|
||||
old_selection.profile.turbo_defaults.rate_hz,
|
||||
old_selection.profile.turbo_defaults.rate_hz &&
|
||||
selected.profile.nunchuk_swing.macro ==
|
||||
old_selection.profile.nunchuk_swing.macro &&
|
||||
active_snapshot(id).profile.combined_swing.button ==
|
||||
old_selection.profile.combined_swing.button,
|
||||
"pending write replaced a selected or active profile before commit");
|
||||
profile_service_task_on_storage_core(8000);
|
||||
profile_service_selected_snapshot(&selected);
|
||||
|
|
@ -400,9 +429,21 @@ void test_current_schema_validation_and_atomic_selection() {
|
|||
selected.profile.swing.button == 2 &&
|
||||
selected.profile.swing.sensitivity == 2 &&
|
||||
selected.profile.swing.modifier == 24 &&
|
||||
selected.profile.swing.macro == CONTROLLER_PROFILE_NO_BUTTON &&
|
||||
selected.profile.nunchuk_swing.button == CONTROLLER_PROFILE_NO_BUTTON &&
|
||||
selected.profile.nunchuk_swing.macro == 0 &&
|
||||
selected.profile.nunchuk_swing.sensitivity == 0 &&
|
||||
selected.profile.nunchuk_swing.modifier == 18 &&
|
||||
selected.profile.combined_swing.button == 15 &&
|
||||
selected.profile.combined_swing.macro == CONTROLLER_PROFILE_NO_BUTTON &&
|
||||
selected.profile.combined_swing.modifier == 16 &&
|
||||
selected.profile.combination_window_ms == 200 &&
|
||||
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.nunchuk_swing.macro == 0 &&
|
||||
active_snapshot(id).profile.combined_swing.button == 15 &&
|
||||
active_snapshot(id).profile.combination_window_ms == 200 &&
|
||||
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,
|
||||
|
|
@ -470,6 +511,16 @@ void test_catalog1_selected_and_active_snapshots_migrate() {
|
|||
selected.profile.shortcuts.modifier == CONTROLLER_PROFILE_NO_BUTTON &&
|
||||
selected.profile.shift.mode == ControllerProfileShiftMode::kOff &&
|
||||
selected.profile.macros[0].mode == ControllerProfileMacroMode::kOnce &&
|
||||
selected.profile.swing.macro == CONTROLLER_PROFILE_NO_BUTTON &&
|
||||
selected.profile.nunchuk_swing.button == CONTROLLER_PROFILE_NO_BUTTON &&
|
||||
selected.profile.nunchuk_swing.macro == CONTROLLER_PROFILE_NO_BUTTON &&
|
||||
selected.profile.combined_swing.button == CONTROLLER_PROFILE_NO_BUTTON &&
|
||||
selected.profile.combined_swing.macro == CONTROLLER_PROFILE_NO_BUTTON &&
|
||||
active.profile.swing.macro == CONTROLLER_PROFILE_NO_BUTTON &&
|
||||
active.profile.nunchuk_swing.button == CONTROLLER_PROFILE_NO_BUTTON &&
|
||||
active.profile.nunchuk_swing.macro == CONTROLLER_PROFILE_NO_BUTTON &&
|
||||
active.profile.combined_swing.button == CONTROLLER_PROFILE_NO_BUTTON &&
|
||||
active.profile.combined_swing.macro == CONTROLLER_PROFILE_NO_BUTTON &&
|
||||
selected.metadata.generation > 42,
|
||||
"old selection/activation snapshots lost migrated content or defaults");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -523,12 +523,31 @@ void test_compaction_preserves_latest_records() {
|
|||
profile.extra_button_map[0] = 16;
|
||||
profile.shift.extra_button_map[6] = 15;
|
||||
profile.swing = {2, 2, 24};
|
||||
profile.nunchuk_swing = {255, 0, 18, 3};
|
||||
profile.combined_swing = {4, 255, 16};
|
||||
profile.combination_window_ms = 30;
|
||||
profile.macros[3].step_count = 1;
|
||||
profile.macros[3].mode = ControllerProfileMacroMode::kToggle;
|
||||
profile.macro_step_count = 1;
|
||||
profile.macro_steps[0].duration_ms = 50;
|
||||
ControllerProfile other = controller_profile_default(global, 7);
|
||||
other.swing = {15, 0, CONTROLLER_PROFILE_NO_BUTTON};
|
||||
other.nunchuk_swing = {1, 2, 24};
|
||||
other.combined_swing = {255, 0, 255};
|
||||
other.combination_window_ms = 200;
|
||||
other.macros[0].step_count = 1;
|
||||
other.macro_step_count = 1;
|
||||
other.macro_steps[0].duration_ms = 75;
|
||||
for (uint8_t index = 1; index < CONTROLLER_PROFILE_MACRO_COUNT; ++index) {
|
||||
other.macros[index].first_step = 1;
|
||||
}
|
||||
const ControllerIdentity stable = identity(1);
|
||||
require(storage.set(global, 7, other) == ProfileStorageResult::kOk,
|
||||
"second swing profile did not append");
|
||||
other.swing = {0, 1, 16};
|
||||
other.swing = {255, 1, 16, 0};
|
||||
other.nunchuk_swing = {3, 1, 17};
|
||||
other.combined_swing = {14, 255, 24};
|
||||
other.combination_window_ms = 120;
|
||||
require(storage.set(stable, 0, other) == ProfileStorageResult::kOk,
|
||||
"stable identity swing profile did not append");
|
||||
for (uint16_t write = 1; write <= 260; ++write) {
|
||||
|
|
@ -550,6 +569,17 @@ void test_compaction_preserves_latest_records() {
|
|||
recovered.swing.button == 2 &&
|
||||
recovered.swing.sensitivity == 2 &&
|
||||
recovered.swing.modifier == 24 &&
|
||||
recovered.swing.macro == CONTROLLER_PROFILE_NO_BUTTON &&
|
||||
recovered.nunchuk_swing.button == CONTROLLER_PROFILE_NO_BUTTON &&
|
||||
recovered.nunchuk_swing.sensitivity == 0 &&
|
||||
recovered.nunchuk_swing.modifier == 18 &&
|
||||
recovered.nunchuk_swing.macro == 3 &&
|
||||
recovered.combined_swing.button == 4 &&
|
||||
recovered.combined_swing.macro == CONTROLLER_PROFILE_NO_BUTTON &&
|
||||
recovered.combined_swing.modifier == 16 &&
|
||||
recovered.combination_window_ms == 30 &&
|
||||
recovered.macros[3].mode == ControllerProfileMacroMode::kToggle &&
|
||||
recovered.macro_steps[0].duration_ms == 50 &&
|
||||
reloaded.get_alias(global, metadata, sizeof(metadata)) ==
|
||||
ProfileStorageResult::kOk &&
|
||||
strcmp(metadata, "Fallback") == 0 &&
|
||||
|
|
@ -558,16 +588,45 @@ void test_compaction_preserves_latest_records() {
|
|||
ProfileStorageResult::kOk &&
|
||||
strcmp(metadata, "Compacted") == 0,
|
||||
"compaction did not preserve profiles and metadata");
|
||||
const uint32_t generation = reloaded.snapshot().generation;
|
||||
const int programs = flash.programs;
|
||||
recovered.nunchuk_swing.button = 0;
|
||||
require(reloaded.set(global, 0, recovered) ==
|
||||
ProfileStorageResult::kInvalidArgument &&
|
||||
reloaded.snapshot().generation == generation &&
|
||||
flash.programs == programs &&
|
||||
reloaded.get(global, 0, &recovered) == ProfileStorageResult::kOk &&
|
||||
recovered.nunchuk_swing.button == CONTROLLER_PROFILE_NO_BUTTON &&
|
||||
recovered.nunchuk_swing.macro == 3,
|
||||
"rejected gesture action modified the stored profile");
|
||||
require(reloaded.get(global, 7, &recovered) == ProfileStorageResult::kOk &&
|
||||
recovered.swing.button == 15 &&
|
||||
recovered.swing.sensitivity == 0 &&
|
||||
recovered.swing.modifier == CONTROLLER_PROFILE_NO_BUTTON &&
|
||||
recovered.swing.macro == CONTROLLER_PROFILE_NO_BUTTON &&
|
||||
recovered.nunchuk_swing.button == 1 &&
|
||||
recovered.nunchuk_swing.macro == CONTROLLER_PROFILE_NO_BUTTON &&
|
||||
recovered.combined_swing.button == CONTROLLER_PROFILE_NO_BUTTON &&
|
||||
recovered.combined_swing.macro == 0 &&
|
||||
recovered.combination_window_ms == 200 &&
|
||||
recovered.macro_steps[0].duration_ms == 75 &&
|
||||
reloaded.get(stable, 0, &recovered) == ProfileStorageResult::kOk &&
|
||||
recovered.swing.button == 0 &&
|
||||
recovered.swing.button == CONTROLLER_PROFILE_NO_BUTTON &&
|
||||
recovered.swing.macro == 0 &&
|
||||
recovered.swing.sensitivity == 1 &&
|
||||
recovered.swing.modifier == 16 &&
|
||||
recovered.nunchuk_swing.button == 3 &&
|
||||
recovered.nunchuk_swing.macro == CONTROLLER_PROFILE_NO_BUTTON &&
|
||||
recovered.combined_swing.button == 14 &&
|
||||
recovered.combined_swing.macro == CONTROLLER_PROFILE_NO_BUTTON &&
|
||||
recovered.combination_window_ms == 120 &&
|
||||
reloaded.get(global, 1, &recovered) == ProfileStorageResult::kOk &&
|
||||
recovered.swing.button == CONTROLLER_PROFILE_NO_BUTTON,
|
||||
recovered.swing.button == CONTROLLER_PROFILE_NO_BUTTON &&
|
||||
recovered.swing.macro == CONTROLLER_PROFILE_NO_BUTTON &&
|
||||
recovered.nunchuk_swing.button == CONTROLLER_PROFILE_NO_BUTTON &&
|
||||
recovered.nunchuk_swing.macro == CONTROLLER_PROFILE_NO_BUTTON &&
|
||||
recovered.combined_swing.button == CONTROLLER_PROFILE_NO_BUTTON &&
|
||||
recovered.combined_swing.macro == CONTROLLER_PROFILE_NO_BUTTON,
|
||||
"compacted swing settings bled across profiles or identities");
|
||||
}
|
||||
|
||||
|
|
@ -823,6 +882,62 @@ void test_schema6_read_migration_is_lazy_and_edit_preserves_metadata() {
|
|||
"editing a schema6 profile lost its active index, alias, or name");
|
||||
}
|
||||
|
||||
void test_schema8_swing_migration_is_lazy_and_preserves_existing_data() {
|
||||
erase_all();
|
||||
install_populated_catalog(3);
|
||||
const auto id = controller_identity_global();
|
||||
ControllerProfile original = catalog_profile(0, 0, true);
|
||||
original.extra_button_map[6] = 16;
|
||||
original.shift.extra_button_map[0] = 15;
|
||||
original.swing = {2, 0, 24};
|
||||
uint8_t legacy[CONTROLLER_PROFILE_ENCODED_SIZE]{};
|
||||
require(controller_profile_encode(original, legacy, sizeof(legacy)),
|
||||
"schema8 storage fixture did not encode");
|
||||
write_u16(legacy, CONTROLLER_PROFILE_SWING_SCHEMA_VERSION);
|
||||
memset(legacy + 367, 0, sizeof(legacy) - 367);
|
||||
install_catalog_record(3, PROFILE_STORAGE_RECORDS_OFFSET, 1, id, 0, 1001,
|
||||
legacy, sizeof(legacy));
|
||||
ProfileStorage storage;
|
||||
ControllerProfile migrated{};
|
||||
require(storage.initialize(fake_io()) &&
|
||||
storage.get(id, 0, &migrated) == ProfileStorageResult::kOk &&
|
||||
migrated.swing.button == 2 && migrated.swing.sensitivity == 0 &&
|
||||
migrated.swing.modifier == 24 &&
|
||||
migrated.swing.macro == CONTROLLER_PROFILE_NO_BUTTON &&
|
||||
migrated.nunchuk_swing.button == CONTROLLER_PROFILE_NO_BUTTON &&
|
||||
migrated.nunchuk_swing.macro == CONTROLLER_PROFILE_NO_BUTTON &&
|
||||
migrated.combined_swing.button == CONTROLLER_PROFILE_NO_BUTTON &&
|
||||
migrated.combined_swing.macro == CONTROLLER_PROFILE_NO_BUTTON &&
|
||||
migrated.combination_window_ms == 100 &&
|
||||
storage.set(id, 0, migrated) == ProfileStorageResult::kUnchanged &&
|
||||
flash.programs == 0 && flash.erases == 0,
|
||||
"schema8 read enabled new gestures or unnecessarily rewrote flash");
|
||||
migrated.nunchuk_swing = {255, 2, 18, 1};
|
||||
migrated.combined_swing = {255, 0, 16};
|
||||
migrated.combination_window_ms = 200;
|
||||
require(storage.set(id, 0, migrated) == ProfileStorageResult::kOk,
|
||||
"schema8 profile could not add gesture macro actions");
|
||||
ProfileStorage reloaded;
|
||||
ControllerProfile recovered{};
|
||||
uint8_t upgraded[CONTROLLER_PROFILE_ENCODED_SIZE]{};
|
||||
char metadata[PROFILE_STORAGE_METADATA_PAYLOAD_SIZE]{};
|
||||
require(reloaded.initialize(fake_io()) &&
|
||||
reloaded.get(id, 0, &recovered) == ProfileStorageResult::kOk &&
|
||||
controller_profile_encode(recovered, upgraded, sizeof(upgraded)) &&
|
||||
memcmp(legacy + 2, upgraded + 2, 365) == 0 &&
|
||||
recovered.nunchuk_swing.macro == 1 &&
|
||||
recovered.combined_swing.macro == 0 &&
|
||||
recovered.combination_window_ms == 200 &&
|
||||
reloaded.find(id)->active_profile == 0 &&
|
||||
reloaded.get_alias(id, metadata, sizeof(metadata)) ==
|
||||
ProfileStorageResult::kOk &&
|
||||
strcmp(metadata, "A") == 0 &&
|
||||
reloaded.get_profile_name(id, 0, metadata, sizeof(metadata)) ==
|
||||
ProfileStorageResult::kOk &&
|
||||
strcmp(metadata, "A0") == 0,
|
||||
"editing migrated gestures lost Remote, macros, extras, or metadata");
|
||||
}
|
||||
|
||||
void require_pair_bank(const ProfileStorage &storage,
|
||||
const ControllerIdentity &owner, uint8_t base,
|
||||
uint8_t active, char name_prefix) {
|
||||
|
|
@ -1052,6 +1167,7 @@ int main() {
|
|||
test_late_second_page_program_is_not_reused();
|
||||
test_unreadable_legacy_data_is_not_erased();
|
||||
test_schema6_read_migration_is_lazy_and_edit_preserves_metadata();
|
||||
test_schema8_swing_migration_is_lazy_and_preserves_existing_data();
|
||||
test_pair_seed_independence_reconnect_and_compaction();
|
||||
test_pair_capacity_and_unseeded_mutations();
|
||||
test_pair_seed_interruption_and_ambiguous_readback();
|
||||
|
|
|
|||
|
|
@ -125,6 +125,7 @@ def test_bluepad32_backend_lifecycle_native(tmp_path: Path) -> None:
|
|||
for scenario in (
|
||||
"wii-orientation",
|
||||
"wii-orientation-races",
|
||||
"wii-accelerometers",
|
||||
"switch2-individual-core-start",
|
||||
"switch2-individual-forward",
|
||||
"switch2-individual-reverse",
|
||||
|
|
|
|||
|
|
@ -1869,7 +1869,10 @@ 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", "swing"):
|
||||
for field in (
|
||||
"shortcuts", "shift", "turbo_settings", "extra_button_map", "swing",
|
||||
"nunchuk_swing", "combined_swing", "combination_window_ms",
|
||||
):
|
||||
del legacy_json_object[field]
|
||||
del legacy_json_object["motion_toggle_chord"]
|
||||
del legacy_json_object["triggers"]["left"]["output"]
|
||||
|
|
@ -1932,7 +1935,10 @@ 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", "swing"):
|
||||
for field in (
|
||||
"shortcuts", "shift", "turbo_settings", "extra_button_map", "swing",
|
||||
"nunchuk_swing", "combined_swing", "combination_window_ms",
|
||||
):
|
||||
del obj[field]
|
||||
for macro in obj["macros"]:
|
||||
del macro["playback"]
|
||||
|
|
@ -1973,7 +1979,6 @@ def test_set_b_sparse_settings_and_macro_modes_round_trip() -> None:
|
|||
assert encoded[336:344] == bytes((3, 255, 0, 1, 1, 1, 2, 1))
|
||||
assert encoded[344:358] == bytes([255]) * 14
|
||||
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
|
||||
|
||||
|
|
@ -1984,7 +1989,8 @@ 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"]
|
||||
for field in ("swing", "nunchuk_swing", "combined_swing", "combination_window_ms"):
|
||||
del legacy_json[field]
|
||||
assert config_manager.ControllerProfile.from_bytes(legacy_wire) == profile
|
||||
assert config_manager.ControllerProfile.from_json_object(legacy_json) == profile
|
||||
device = FakeDevice()
|
||||
|
|
@ -1998,7 +2004,7 @@ 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")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("version", [7, 8])
|
||||
@pytest.mark.parametrize("version", [7, 8, 9])
|
||||
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(
|
||||
|
|
@ -2021,6 +2027,11 @@ def test_schema7_extra_controls_keep_output_channels_and_wire_layout(version: in
|
|||
obj["schema_version"] = version
|
||||
if version == 7:
|
||||
del obj["swing"]
|
||||
elif version == 8:
|
||||
del obj["swing"]["macro"]
|
||||
if version < 9:
|
||||
for field in ("nunchuk_swing", "combined_swing", "combination_window_ms"):
|
||||
del obj[field]
|
||||
profile = config_manager.ControllerProfile.from_json_object(obj)
|
||||
encoded = profile.to_bytes()
|
||||
assert encoded[:4] == struct.pack("<HH", config_manager.PROFILE_SCHEMA_VERSION, 384)
|
||||
|
|
@ -2035,6 +2046,8 @@ def test_schema7_extra_controls_keep_output_channels_and_wire_layout(version: in
|
|||
struct.pack_into("<H", legacy_wire, 0, version)
|
||||
if version == 7:
|
||||
legacy_wire[364:] = bytes(20)
|
||||
elif version == 8:
|
||||
legacy_wire[367:] = bytes(17)
|
||||
device = FakeDevice()
|
||||
device.profiles[(device.stable_identity.to_bytes(), 1)] = bytes(legacy_wire)
|
||||
assert config_manager.read_profile(device, device.stable_identity, 1) == profile
|
||||
|
|
@ -2115,7 +2128,10 @@ 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", "swing"):
|
||||
for key in (
|
||||
"shortcuts", "shift", "turbo_settings", "extra_button_map", "swing",
|
||||
"nunchuk_swing", "combined_swing", "combination_window_ms",
|
||||
):
|
||||
del obj[key]
|
||||
assert config_manager.ControllerProfile.from_json_object(obj) == profile
|
||||
assert config_manager.ControllerProfile.from_bytes(profile.to_bytes()) == profile
|
||||
|
|
@ -2134,6 +2150,7 @@ def test_swing_profile_round_trip_preserves_other_settings(
|
|||
obj = before.to_json_object()
|
||||
obj["swing"] = {
|
||||
"button": button, "sensitivity": sensitivity, "modifier": modifier,
|
||||
"macro": None,
|
||||
}
|
||||
profile = config_manager.ControllerProfile.from_json_object(obj)
|
||||
assert replace(profile, swing=before.swing) == before
|
||||
|
|
@ -2154,6 +2171,7 @@ def test_swing_profile_round_trip_preserves_other_settings(
|
|||
("sensitivity", 1),
|
||||
("modifier", "unknown"),
|
||||
("modifier", False),
|
||||
("macro", 0), ("macro", 5), ("macro", True), ("macro", "1"),
|
||||
])
|
||||
def test_swing_rejects_invalid_json_settings(field: str, value: object) -> None:
|
||||
obj = config_manager.ControllerProfile.default().to_json_object()
|
||||
|
|
@ -2166,6 +2184,7 @@ def test_swing_rejects_invalid_json_settings(field: str, value: object) -> None:
|
|||
("button", -1), ("button", 16), ("button", True),
|
||||
("sensitivity", -1), ("sensitivity", 3), ("sensitivity", False),
|
||||
("modifier", -1), ("modifier", 25), ("modifier", 256),
|
||||
("macro", -1), ("macro", 4), ("macro", False),
|
||||
])
|
||||
def test_swing_rejects_invalid_in_memory_settings(field: str, value: object) -> None:
|
||||
with pytest.raises(config_manager.ConfigManagerError):
|
||||
|
|
@ -2173,7 +2192,9 @@ def test_swing_rejects_invalid_in_memory_settings(field: str, value: object) ->
|
|||
|
||||
|
||||
@pytest.mark.parametrize(("offset", "value"), [
|
||||
(364, 16), (365, 3), (366, 25), (367, 1),
|
||||
(364, 16), (365, 3), (366, 25), (367, 4),
|
||||
(368, 16), (369, 3), (370, 25), (371, 4),
|
||||
(372, 16), (373, 4), (374, 25), (375, 29), (375, 201), (376, 1),
|
||||
])
|
||||
def test_swing_rejects_corrupt_wire_settings(offset: int, value: int) -> None:
|
||||
payload = bytearray(config_manager.ControllerProfile.default().to_bytes())
|
||||
|
|
@ -2184,6 +2205,7 @@ def test_swing_rejects_corrupt_wire_settings(offset: int, value: int) -> None:
|
|||
|
||||
@pytest.mark.parametrize("mutation", [
|
||||
"missing", "unknown", "missing_button", "missing_sensitivity", "missing_modifier",
|
||||
"missing_macro",
|
||||
"legacy_field",
|
||||
])
|
||||
def test_swing_json_fields_are_strict(mutation: str) -> None:
|
||||
|
|
@ -2195,11 +2217,117 @@ def test_swing_json_fields_are_strict(mutation: str) -> None:
|
|||
elif mutation.startswith("missing_"):
|
||||
del obj["swing"][mutation.removeprefix("missing_")]
|
||||
else:
|
||||
for field in ("nunchuk_swing", "combined_swing", "combination_window_ms"):
|
||||
del obj[field]
|
||||
obj["schema_version"] = 7
|
||||
with pytest.raises(config_manager.ConfigManagerError):
|
||||
config_manager.ControllerProfile.from_json_object(obj)
|
||||
|
||||
|
||||
def test_schema8_swing_migration_preserves_remote_binding() -> None:
|
||||
profile = replace(custom_profile(), swing=config_manager.ProfileSwing(2, 2, 24))
|
||||
legacy_wire = bytearray(profile.to_bytes())
|
||||
struct.pack_into("<H", legacy_wire, 0, 8)
|
||||
legacy_wire[367:] = bytes(17)
|
||||
legacy_json = profile.to_json_object()
|
||||
legacy_json["schema_version"] = 8
|
||||
del legacy_json["swing"]["macro"]
|
||||
for field in ("nunchuk_swing", "combined_swing", "combination_window_ms"):
|
||||
del legacy_json[field]
|
||||
assert config_manager.ControllerProfile.from_bytes(legacy_wire) == profile
|
||||
assert config_manager.ControllerProfile.from_json_object(legacy_json) == profile
|
||||
assert config_manager.ControllerProfile.from_bytes(profile.to_bytes()) == profile
|
||||
legacy_wire[367] = 1
|
||||
with pytest.raises(config_manager.ConfigManagerError):
|
||||
config_manager.ControllerProfile.from_bytes(legacy_wire)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("macro_number", [1, 4])
|
||||
def test_gesture_macro_json_indices_and_wire_layout(macro_number: int) -> None:
|
||||
obj = custom_profile().to_json_object()
|
||||
obj["macros"][macro_number - 1] = {
|
||||
**obj["macros"][0], "trigger": [], "playback": "toggle",
|
||||
}
|
||||
obj["swing"].update(macro=macro_number, sensitivity="high", modifier="right_sr")
|
||||
obj["nunchuk_swing"].update(button="dpad_right", sensitivity="low", modifier="c")
|
||||
obj["combined_swing"].update(macro=macro_number, modifier="left_trigger")
|
||||
obj["combination_window_ms"] = 200
|
||||
profile = config_manager.ControllerProfile.from_json_object(obj)
|
||||
assert profile.swing.macro == profile.combined_swing.macro == macro_number - 1
|
||||
encoded = profile.to_bytes()
|
||||
assert encoded[364:376] == bytes((
|
||||
255, 2, 24, macro_number - 1, 15, 0, 18, 255,
|
||||
255, macro_number - 1, 16, 200,
|
||||
))
|
||||
assert encoded[376:] == bytes(8)
|
||||
assert config_manager.ControllerProfile.from_bytes(encoded) == profile
|
||||
assert profile.to_json_object() == obj
|
||||
|
||||
|
||||
@pytest.mark.parametrize("gesture", ["swing", "nunchuk_swing", "combined_swing"])
|
||||
def test_gesture_actions_are_exclusive(gesture: str) -> None:
|
||||
obj = custom_profile().to_json_object()
|
||||
obj[gesture].update(button="south", macro=1)
|
||||
with pytest.raises(config_manager.ConfigManagerError):
|
||||
config_manager.ControllerProfile.from_json_object(obj)
|
||||
action_type = (
|
||||
config_manager.ProfileCombinedSwing if gesture == "combined_swing"
|
||||
else config_manager.ProfileSwing
|
||||
)
|
||||
with pytest.raises(config_manager.ConfigManagerError):
|
||||
action_type(button=0, macro=0)
|
||||
payload = bytearray(custom_profile().to_bytes())
|
||||
button_offset, macro_offset = {
|
||||
"swing": (364, 367), "nunchuk_swing": (368, 371), "combined_swing": (372, 373),
|
||||
}[gesture]
|
||||
payload[button_offset] = payload[macro_offset] = 0
|
||||
with pytest.raises(config_manager.ConfigManagerError):
|
||||
config_manager.ControllerProfile.from_bytes(payload)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("gesture", ["swing", "nunchuk_swing", "combined_swing"])
|
||||
@pytest.mark.parametrize("empty", [False, True])
|
||||
def test_gesture_macro_requires_configured_positive_duration_target(
|
||||
gesture: str, empty: bool,
|
||||
) -> None:
|
||||
obj = custom_profile().to_json_object()
|
||||
target = obj["macros"][0]
|
||||
target["trigger"] = []
|
||||
if empty:
|
||||
target["steps"] = []
|
||||
else:
|
||||
for step in target["steps"]:
|
||||
step["duration_ms"] = 0
|
||||
unbound = config_manager.ControllerProfile.from_json_object(obj)
|
||||
obj[gesture]["macro"] = 1
|
||||
with pytest.raises(config_manager.ConfigManagerError):
|
||||
config_manager.ControllerProfile.from_json_object(obj)
|
||||
with pytest.raises(config_manager.ConfigManagerError):
|
||||
replace(unbound, **{gesture: replace(getattr(unbound, gesture), macro=0)})
|
||||
payload = bytearray(unbound.to_bytes())
|
||||
payload[{"swing": 367, "nunchuk_swing": 371, "combined_swing": 373}[gesture]] = 0
|
||||
with pytest.raises(config_manager.ConfigManagerError):
|
||||
config_manager.ControllerProfile.from_bytes(payload)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("window", [30, 200])
|
||||
def test_combination_window_inclusive_bounds_round_trip(window: int) -> None:
|
||||
obj = config_manager.ControllerProfile.default().to_json_object()
|
||||
obj["combination_window_ms"] = window
|
||||
profile = config_manager.ControllerProfile.from_json_object(obj)
|
||||
assert config_manager.ControllerProfile.from_bytes(profile.to_bytes()).combination_window_ms == window
|
||||
|
||||
|
||||
@pytest.mark.parametrize("window", [29, 201, True, 100.0, None])
|
||||
def test_combination_window_rejects_out_of_range_or_noninteger_values(window: object) -> None:
|
||||
obj = config_manager.ControllerProfile.default().to_json_object()
|
||||
obj["combination_window_ms"] = window
|
||||
with pytest.raises(config_manager.ConfigManagerError):
|
||||
config_manager.ControllerProfile.from_json_object(obj)
|
||||
with pytest.raises(config_manager.ConfigManagerError):
|
||||
replace(config_manager.ControllerProfile.default(), combination_window_ms=window)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("path", "value"),
|
||||
[
|
||||
|
|
|
|||
|
|
@ -107,6 +107,7 @@ def test_editor_serves_assets_and_complete_schema(
|
|||
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["combination_window_bounds"] == {"min": 30, "max": 200}
|
||||
assert schema["macro_overrides"] == list(config_manager.MACRO_OVERRIDE_NAMES)
|
||||
assert schema["profile_capacity"] == 8
|
||||
assert (
|
||||
|
|
@ -142,19 +143,23 @@ def test_switch2_input_choices_are_never_output_targets(
|
|||
assert schema["output_controls"] == list(config_manager.OUTPUT_CONTROLS)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("version", [6, 7])
|
||||
@pytest.mark.parametrize("version", [6, 7, 8])
|
||||
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()
|
||||
if version == 8:
|
||||
profile = replace(profile, swing=config_manager.ProfileSwing(3, 0, 24))
|
||||
legacy_wire = bytearray(profile.to_bytes())
|
||||
legacy_wire[:2] = version.to_bytes(2, "little")
|
||||
if version == 6:
|
||||
legacy_wire[344:] = bytes(40)
|
||||
else:
|
||||
elif version == 7:
|
||||
legacy_wire[364:] = bytes(20)
|
||||
else:
|
||||
legacy_wire[367:] = bytes(17)
|
||||
device.profiles[key] = bytes(legacy_wire)
|
||||
device.profile_aliases[key[0]] = "Living room"
|
||||
device.profile_names[key] = "Racing"
|
||||
|
|
@ -163,7 +168,7 @@ def test_editor_migrates_old_profiles_and_saves_swing_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
|
||||
assert migrated["profile"]["swing"] == profile.swing.to_json_object()
|
||||
draft = migrated["profile"]
|
||||
draft["extra_button_map"] = dict(zip(config_manager.EXTRA_BUTTONS, config_manager.LOGICAL_BUTTONS[:7]))
|
||||
draft["shift"]["mode"] = "hold"
|
||||
|
|
@ -175,7 +180,11 @@ def test_editor_migrates_old_profiles_and_saves_swing_without_metadata_loss(
|
|||
draft["motion_toggle_chord"] = ["right_sl", "c"]
|
||||
draft["swing"] = {
|
||||
"button": "west", "sensitivity": "high", "modifier": "right_trigger",
|
||||
"macro": None,
|
||||
}
|
||||
draft["nunchuk_swing"].update(macro=1, sensitivity="low", modifier="c")
|
||||
draft["combined_swing"].update(macro=2, modifier="left_trigger")
|
||||
draft["combination_window_ms"] = 30
|
||||
status, validated = request_json(
|
||||
f"{base_url}/api/profiles/validate", method="POST", value=draft, token=token,
|
||||
)
|
||||
|
|
@ -199,6 +208,27 @@ def test_editor_migrates_old_profiles_and_saves_swing_without_metadata_loss(
|
|||
assert sample["buttons"] == ["south", "dpad_up", "dpad_right"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("gesture", ["swing", "nunchuk_swing", "combined_swing"])
|
||||
def test_editor_rejects_unconfigured_gesture_macro_without_saving(
|
||||
monkeypatch: pytest.MonkeyPatch, gesture: str,
|
||||
) -> None:
|
||||
device = FakeDevice()
|
||||
key = (device.stable_identity.to_bytes(), 1)
|
||||
original = device.profiles[key]
|
||||
draft = config_manager.ControllerProfile.default().to_json_object()
|
||||
draft[gesture]["macro"] = 4
|
||||
with running_server(monkeypatch, device) as (base_url, token):
|
||||
for endpoint, method in (
|
||||
("/api/profiles/validate", "POST"), ("/api/profiles/1/2", "PUT"),
|
||||
):
|
||||
status, result = request_json(
|
||||
f"{base_url}{endpoint}", method=method, value=draft, token=token,
|
||||
)
|
||||
assert status == 400
|
||||
assert "error" in result
|
||||
assert device.profiles[key] == original
|
||||
|
||||
|
||||
def test_editor_identifies_connected_controller_artwork(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
|
|
|
|||
|
|
@ -99,8 +99,7 @@ static void update_mp_checksum(void) {
|
|||
put_be16(&f.mp_calibration[30], crc);
|
||||
}
|
||||
|
||||
static void set_nunchuk_stick_calibration(const uint8_t values[6]) {
|
||||
memcpy(f.nunchuk_calibration + 8, values, 6);
|
||||
static void update_nunchuk_checksum(void) {
|
||||
uint8_t checksum = 0x55;
|
||||
for (unsigned i = 0; i < 14; ++i)
|
||||
checksum += f.nunchuk_calibration[i];
|
||||
|
|
@ -108,6 +107,17 @@ static void set_nunchuk_stick_calibration(const uint8_t values[6]) {
|
|||
f.nunchuk_calibration[15] = checksum + 0x55;
|
||||
}
|
||||
|
||||
static void set_nunchuk_stick_calibration(const uint8_t values[6]) {
|
||||
memcpy(f.nunchuk_calibration + 8, values, 6);
|
||||
update_nunchuk_checksum();
|
||||
}
|
||||
|
||||
static void set_nunchuk_accel_calibration(const uint16_t zero[3], const uint16_t one_g[3]) {
|
||||
pack_accel_calibration(f.nunchuk_calibration, zero);
|
||||
pack_accel_calibration(f.nunchuk_calibration + 4, one_g);
|
||||
update_nunchuk_checksum();
|
||||
}
|
||||
|
||||
static void reset_fixture(uint16_t product_id, bool motionplus, extension_t extension) {
|
||||
memset(&f, 0, sizeof(f));
|
||||
f.device.vendor_id = 0x057e;
|
||||
|
|
@ -122,6 +132,10 @@ static void reset_fixture(uint16_t product_id, bool motionplus, extension_t exte
|
|||
const uint8_t nunchuk_identity[6] = {0, 0, 0xa4, 0x20, 0, 0};
|
||||
memcpy(f.extension_identity, nunchuk_identity, sizeof(nunchuk_identity));
|
||||
set_nunchuk_stick_calibration((const uint8_t[]){224, 32, 128, 224, 32, 128});
|
||||
// Dolphin Nunchuk::CalibrationData uses the same packed 10-bit points as
|
||||
// the Remote, but its own centers and sensitivities.
|
||||
set_nunchuk_accel_calibration((const uint16_t[]){501, 510, 519},
|
||||
(const uint16_t[]){693, 766, 647});
|
||||
|
||||
// Non-default centers, unequal spans, and nonzero packed low bits catch
|
||||
// nominal 0x200/100-count calibration and high-byte-only decoding.
|
||||
|
|
@ -417,6 +431,21 @@ static void send_nunchuk_controls(bool passthrough) {
|
|||
expect_accel();
|
||||
}
|
||||
|
||||
static void send_nunchuk_sample(const uint8_t extension[6]) {
|
||||
// Extension-only input proves its sequence does not need Remote accel.
|
||||
uint8_t report[11] = {0x32};
|
||||
memcpy(report + 3, extension, 6);
|
||||
feed(report, sizeof(report));
|
||||
}
|
||||
|
||||
static uint32_t expect_nunchuk_accel(int32_t x, int32_t y, int32_t z) {
|
||||
int32_t acceleration[3];
|
||||
uint32_t sequence;
|
||||
assert(uni_hid_parser_wii_nunchuk_accel_snapshot(&f.device, acceleration, &sequence));
|
||||
expect_vector(acceleration, x, y, z);
|
||||
return sequence;
|
||||
}
|
||||
|
||||
static void integrated_motionplus_calibration_and_slow_bits(void) {
|
||||
reset_fixture(0x0330, true, EXT_NONE);
|
||||
connect_device();
|
||||
|
|
@ -909,6 +938,172 @@ static void unavailable_nunchuk_calibration_keeps_safe_nominal_stick(void) {
|
|||
expect_nunchuk_stick(224, 32, 511, 511);
|
||||
}
|
||||
|
||||
static void plain_nunchuk_accel_uses_own_packed_factory_points(void) {
|
||||
reset_fixture(0x0306, false, EXT_NUNCHUK);
|
||||
connect_device();
|
||||
int32_t acceleration[3];
|
||||
uint32_t remote_sequence;
|
||||
assert(!uni_hid_parser_wii_nunchuk_accel_snapshot(&f.device, acceleration, &remote_sequence));
|
||||
assert(!uni_hid_parser_wii_accel_snapshot(&f.device, acceleration, &remote_sequence));
|
||||
|
||||
// Native (693,382,551) = (+1g,-0.5g,+0.25g). Each axis has a different
|
||||
// packed low-bit value (1,2,3), distinct calibration, and a non-default zero.
|
||||
const uint8_t extension[6] = {160, 192, 173, 95, 137, 0xe5};
|
||||
send_nunchuk_sample(extension);
|
||||
uint32_t first = expect_nunchuk_accel(-8192, 2048, -4096);
|
||||
assert(f.device.controller.gamepad.buttons == BUTTON_X);
|
||||
assert(f.device.controller.gamepad.axis_x == 170 && f.device.controller.gamepad.axis_y == -341);
|
||||
assert(!uni_hid_parser_wii_accel_snapshot(&f.device, acceleration, &remote_sequence));
|
||||
expect_vector(f.device.controller.gamepad.accel, 0, 0, 0);
|
||||
|
||||
uint8_t remote[6] = {0x31};
|
||||
put_accel_report(remote, 614, 442, 534);
|
||||
feed(remote, sizeof(remote));
|
||||
assert(uni_hid_parser_wii_accel_snapshot(&f.device, acceleration, &remote_sequence));
|
||||
assert(expect_nunchuk_accel(-8192, 2048, -4096) == first);
|
||||
send_nunchuk_sample(extension);
|
||||
assert(expect_nunchuk_accel(-8192, 2048, -4096) != first);
|
||||
uint32_t sequence;
|
||||
assert(uni_hid_parser_wii_accel_snapshot(&f.device, acceleration, &sequence));
|
||||
assert(sequence == remote_sequence);
|
||||
expect_accel(); // Nunchuk does not overwrite the Remote's console IMU.
|
||||
}
|
||||
|
||||
static void passthrough_nunchuk_accel_bitpacking_and_freshness(void) {
|
||||
reset_fixture(0x0330, true, EXT_NUNCHUK);
|
||||
set_nunchuk_accel_calibration((const uint16_t[]){502, 510, 518},
|
||||
(const uint16_t[]){694, 766, 646});
|
||||
connect_device();
|
||||
// MP relocates AZ bit2 to byte5 bit7, not the connected bit in byte4.
|
||||
// Bits 4/5/6/7 here reconstruct (694,382,550); C held, Z released.
|
||||
uint8_t extension[6] = {160, 192, 173, 95, 137, 0xf4};
|
||||
send_nunchuk_sample(extension);
|
||||
uint32_t first = expect_nunchuk_accel(-8192, 2048, -4096);
|
||||
assert(f.device.controller.gamepad.buttons == BUTTON_X);
|
||||
send_motion(7760, 8200, 7560, false, true, false);
|
||||
assert(expect_nunchuk_accel(-8192, 2048, -4096) == first);
|
||||
int32_t acceleration[3];
|
||||
uint32_t remote_sequence;
|
||||
assert(uni_hid_parser_wii_accel_snapshot(&f.device, acceleration, &remote_sequence));
|
||||
send_ack(0x16, 0);
|
||||
send_status(false); // MP synthetic detach: the mapped ID still exists.
|
||||
finish_setup();
|
||||
uint8_t short_report[8] = {0x32};
|
||||
feed(short_report, sizeof(short_report));
|
||||
send_nunchuk_sample((const uint8_t[]){255, 255, 255, 255, 255, 255});
|
||||
assert(expect_nunchuk_accel(-8192, 2048, -4096) == first);
|
||||
uint32_t sequence;
|
||||
assert(uni_hid_parser_wii_accel_snapshot(&f.device, acceleration, &sequence));
|
||||
assert(sequence == remote_sequence);
|
||||
expect_accel();
|
||||
expect_vector(f.device.controller.gamepad.gyro, 360 * 1024, -120 * 1024, 40 * 1024);
|
||||
|
||||
// Different low bits distinguish the X/Y selectors and both relocated Z
|
||||
// bits; clear bit0 is the missing precision, never a synthesized bit.
|
||||
extension[5] = 0x94; // (694,380,548).
|
||||
send_nunchuk_sample(extension);
|
||||
assert(expect_nunchuk_accel(-8192, 1920, -4160) != first);
|
||||
extension[5] = 0x64; // (692,382,546).
|
||||
send_nunchuk_sample(extension);
|
||||
first = expect_nunchuk_accel(-8106, 1792, -4096);
|
||||
send_nunchuk_sample(extension);
|
||||
assert(expect_nunchuk_accel(-8106, 1792, -4096) != first);
|
||||
}
|
||||
|
||||
static void nunchuk_accel_calibration_is_independent_of_stick_and_remote(void) {
|
||||
reset_fixture(0x0306, false, EXT_NUNCHUK);
|
||||
f.fail_accel_reads = true;
|
||||
set_nunchuk_stick_calibration((const uint8_t[]){128, 32, 128, 224, 32, 128});
|
||||
connect_device();
|
||||
send_nunchuk_sample((const uint8_t[]){160, 192, 173, 95, 137, 0xe5});
|
||||
expect_nunchuk_accel(-8192, 2048, -4096);
|
||||
assert(f.device.controller.gamepad.axis_x == 170 && f.device.controller.gamepad.axis_y == -341);
|
||||
int32_t acceleration[3];
|
||||
uint32_t sequence;
|
||||
assert(!uni_hid_parser_wii_accel_snapshot(&f.device, acceleration, &sequence));
|
||||
|
||||
// Each axis must have a real span, not merely a valid checksum or X span.
|
||||
for (unsigned axis = 0; axis < 3; ++axis) {
|
||||
reset_fixture(0x0330, true, EXT_NUNCHUK);
|
||||
uint16_t one_g[3] = {693, 766, 647};
|
||||
const uint16_t zero[3] = {501, 510, 519};
|
||||
one_g[axis] = zero[axis];
|
||||
set_nunchuk_accel_calibration(zero, one_g);
|
||||
set_nunchuk_stick_calibration((const uint8_t[]){220, 40, 124, 210, 30, 126});
|
||||
connect_device();
|
||||
expect_nunchuk_stick(124, 126, 0, 0);
|
||||
expect_nunchuk_stick(220, 30, 511, 511);
|
||||
send_motion(7760, 8200, 7560, false, true, false);
|
||||
expect_accel();
|
||||
expect_vector(f.device.controller.gamepad.gyro, 360 * 1024, -120 * 1024, 40 * 1024);
|
||||
assert(!uni_hid_parser_wii_nunchuk_accel_snapshot(&f.device, acceleration, &sequence));
|
||||
}
|
||||
|
||||
// Either checksum byte or an unreadable block disables only Nunchuk motion.
|
||||
for (unsigned failure = 0; failure < 3; ++failure) {
|
||||
reset_fixture(0x0306, false, EXT_NUNCHUK);
|
||||
if (failure < 2)
|
||||
f.nunchuk_calibration[14 + failure] ^= 1;
|
||||
else
|
||||
f.fail_read_address = 0xa40020;
|
||||
connect_device();
|
||||
send_nunchuk_controls(false);
|
||||
assert(!uni_hid_parser_wii_nunchuk_accel_snapshot(&f.device, acceleration, &sequence));
|
||||
}
|
||||
}
|
||||
|
||||
static void nunchuk_accel_detach_and_replacement_require_fresh_calibration(void) {
|
||||
for (unsigned passthrough = 0; passthrough < 2; ++passthrough) {
|
||||
reset_fixture(passthrough ? 0x0330 : 0x0306, passthrough, EXT_NUNCHUK);
|
||||
connect_device();
|
||||
send_nunchuk_controls(passthrough);
|
||||
int32_t acceleration[3];
|
||||
uint32_t original_sequence;
|
||||
assert(uni_hid_parser_wii_nunchuk_accel_snapshot(&f.device, acceleration, &original_sequence));
|
||||
f.extension = EXT_NONE;
|
||||
if (passthrough)
|
||||
send_motion(7760, 8200, 7560, false, true, false);
|
||||
else
|
||||
send_status(false);
|
||||
uint32_t sequence;
|
||||
assert(!uni_hid_parser_wii_nunchuk_accel_snapshot(&f.device, acceleration, &sequence));
|
||||
finish_setup();
|
||||
// In-flight extension bytes must not resurrect the detached stream.
|
||||
send_nunchuk_sample((const uint8_t[]){160, 192, 128, 128, 128, 0});
|
||||
assert(!uni_hid_parser_wii_nunchuk_accel_snapshot(&f.device, acceleration, &sequence));
|
||||
set_nunchuk_accel_calibration((const uint16_t[]){512, 512, 512},
|
||||
(const uint16_t[]){712, 752, 672});
|
||||
f.extension = EXT_NUNCHUK;
|
||||
if (passthrough)
|
||||
send_motion(7760, 8200, 7560, false, true, false);
|
||||
else
|
||||
send_status(true);
|
||||
finish_setup();
|
||||
assert(!uni_hid_parser_wii_nunchuk_accel_snapshot(&f.device, acceleration, &sequence));
|
||||
send_nunchuk_controls(passthrough);
|
||||
assert(expect_nunchuk_accel(0, 0, 0) != original_sequence);
|
||||
expect_accel();
|
||||
|
||||
// Replacement with unreadable calibration must not inherit old points.
|
||||
f.extension = EXT_NONE;
|
||||
if (passthrough)
|
||||
send_motion(7760, 8200, 7560, false, true, false);
|
||||
else
|
||||
send_status(false);
|
||||
finish_setup();
|
||||
f.fail_read_address = 0xa40020;
|
||||
f.extension = EXT_NUNCHUK;
|
||||
if (passthrough)
|
||||
send_motion(7760, 8200, 7560, false, true, false);
|
||||
else
|
||||
send_status(true);
|
||||
finish_setup();
|
||||
send_nunchuk_controls(passthrough);
|
||||
assert(!uni_hid_parser_wii_nunchuk_accel_snapshot(&f.device, acceleration, &sequence));
|
||||
expect_accel();
|
||||
}
|
||||
}
|
||||
|
||||
static void run_case(const char* name, void (*test)(void)) {
|
||||
printf("Wii parser: %s\n", name);
|
||||
fflush(stdout);
|
||||
|
|
@ -917,6 +1112,10 @@ 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("plain Nunchuk independent packed acceleration", plain_nunchuk_accel_uses_own_packed_factory_points);
|
||||
run_case("MotionPlus Nunchuk acceleration bitpacking and freshness", passthrough_nunchuk_accel_bitpacking_and_freshness);
|
||||
run_case("Nunchuk accelerometer calibration independent of stick and Remote", nunchuk_accel_calibration_is_independent_of_stick_and_remote);
|
||||
run_case("Nunchuk acceleration detach and replacement", nunchuk_accel_detach_and_replacement_require_fresh_calibration);
|
||||
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