Start Wii motion immediately and track residual bias in background
This commit is contained in:
parent
a17ca603aa
commit
26a25a66af
14 changed files with 210 additions and 101 deletions
58
README.md
58
README.md
|
|
@ -606,8 +606,9 @@ byte-identical to 0.35. Firmware 0.37 was flashed with persistent storage
|
||||||
unchanged and both pairing records restored; its console chord test is pending.
|
unchanged and both pairing records restored; its console chord test is pending.
|
||||||
|
|
||||||
- **Motion:** factory-calibrated Wii acceleration and MotionPlus gyro have
|
- **Motion:** factory-calibrated Wii acceleration and MotionPlus gyro have
|
||||||
independent freshness counters. Keep the Remote still at startup for at least
|
independent freshness counters. Motion starts with the first usable fresh
|
||||||
1.5 seconds and 64 fresh gyro samples to estimate residual bias. The encoder
|
sensor pair; residual bias is refined in the background during quiet periods.
|
||||||
|
There is no mandatory startup settling period. The encoder
|
||||||
integrates real gyro into an orientation quaternion, using fresh near-1g
|
integrates real gyro into an orientation quaternion, using fresh near-1g
|
||||||
acceleration to correct tilt drift, and emits the recovered 30-byte native
|
acceleration to correct tilt drift, and emits the recovered 30-byte native
|
||||||
IMU format. Fresh full-bar observations gently correct relative heading
|
IMU format. Fresh full-bar observations gently correct relative heading
|
||||||
|
|
@ -827,9 +828,9 @@ The existing profile transform runs once for the full pad. R gets face buttons,
|
||||||
right stick/shoulder/trigger, plus and home; L gets the D-pad, left stick/shoulder/
|
right stick/shoulder/trigger, plus and home; L gets the D-pad, left stick/shoulder/
|
||||||
trigger, minus and capture. Each uses its own advertised stick calibration.
|
trigger, minus and capture. Each uses its own advertised stick calibration.
|
||||||
One shared motion integrator consumes only fresh, complete, CRC-checked and
|
One shared motion integrator consumes only fresh, complete, CRC-checked and
|
||||||
factory-calibrated DS5 sensor data. From 0.69, already-calibrated native sources
|
factory-calibrated DS5 sensor data. DS5 initializes from its first usable fresh
|
||||||
initialize from their first usable fresh sensor pair; the extra stationary
|
sensor pair. From 0.71, Wii also starts immediately and refines residual bias in
|
||||||
bias-estimation period is explicitly Wii-only. Invalid/stale sensors still
|
the background, without blocking IMU or resetting orientation. Invalid/stale sensors still
|
||||||
withhold IMU while controls remain available. Each USB half has independent
|
withhold IMU while controls remain available. Each USB half has independent
|
||||||
peek/commit, reset and backpressure state. No mouse movement or rail presses
|
peek/commit, reset and backpressure state. No mouse movement or rail presses
|
||||||
are invented.
|
are invented.
|
||||||
|
|
@ -863,9 +864,9 @@ decoded with changing counters and quaternions. The Wii-style stationary gate
|
||||||
had delayed readiness until about 30 seconds after boot in that run. Firmware
|
had delayed readiness until about 30 seconds after boot in that run. Firmware
|
||||||
0.69 removes that extra gate for factory-calibrated sources; the regression
|
0.69 removes that extra gate for factory-calibrated sources; the regression
|
||||||
checks first-sample output even while rotating, fresh-data recovery, and the
|
checks first-sample output even while rotating, fresh-data recovery, and the
|
||||||
unchanged Wii settling behavior. Wii factory calibration is already used, but
|
then-current Wii settling behavior. From 0.71, Wii no longer waits for that
|
||||||
its residual gyro bias still needs estimation; cached per-device bias without
|
estimate before emitting motion; it uses the nonblocking policy described below.
|
||||||
revalidation can drift and is not implemented here.
|
Factory calibration still applies, and bias is not cached across boots.
|
||||||
|
|
||||||
Translated full-controller builds expose `SWITCH2_BRIDGE_IMU_TARGET`:
|
Translated full-controller builds expose `SWITCH2_BRIDGE_IMU_TARGET`:
|
||||||
`LEFT`, `RIGHT`, or `BOTH` (default). For example, configure the existing private
|
`LEFT`, `RIGHT`, or `BOTH` (default). For example, configure the existing private
|
||||||
|
|
@ -900,8 +901,8 @@ profile behavior is retained; original Switch Joy-Con grouping is not added.
|
||||||
arrive. A paired left Joy-Con cannot refresh the right-owned sensor stream.
|
arrive. A paired left Joy-Con cannot refresh the right-owned sensor stream.
|
||||||
Wii acceleration cannot refresh a stalled MotionPlus gyro stream.
|
Wii acceleration cannot refresh a stalled MotionPlus gyro stream.
|
||||||
- `SWITCH2_BRIDGE_IMU_TARGET=LEFT|RIGHT|BOTH` also applies to `GAMEPAD`; Wii alone
|
- `SWITCH2_BRIDGE_IMU_TARGET=LEFT|RIGHT|BOTH` also applies to `GAMEPAD`; Wii alone
|
||||||
estimates residual bias while stationary. Factory calibration already runs;
|
refines residual bias in the background while apparently stationary, without
|
||||||
caching residual Wii bias across boots without revalidation is not implemented.
|
withholding valid IMU. Factory calibration still runs; no bias is saved across boots.
|
||||||
- Native cue requests use each source driver's bounded compatibility vibration.
|
- Native cue requests use each source driver's bounded compatibility vibration.
|
||||||
Mono drivers combine the two logical contributions; paired Switch2 Joy-Cons
|
Mono drivers combine the two logical contributions; paired Switch2 Joy-Cons
|
||||||
target their actual halves. This does not promise stereo, HD-waveform fidelity
|
target their actual halves. This does not promise stereo, HD-waveform fidelity
|
||||||
|
|
@ -913,10 +914,39 @@ profile behavior is retained; original Switch Joy-Con grouping is not added.
|
||||||
|
|
||||||
The private `build-switch2-native-gamepad` image uses mixed Bluetooth and the
|
The private `build-switch2-native-gamepad` image uses mixed Bluetooth and the
|
||||||
unchanged stock USB socket. Software regressions cover real parser calibration,
|
unchanged stock USB socket. Software regressions cover real parser calibration,
|
||||||
report integrity/freshness, source selection, split/reset/backpressure, Wii-only
|
report integrity/freshness, source selection, split/reset/backpressure, Wii
|
||||||
settling and cue lifetimes. DualSense, generic, existing Joy-Con/Wii, and ordinary
|
background correction and cue lifetimes. Version 0.70 was flashed with saved
|
||||||
AIO mixed/BLE/Classic firmware builds pass. The new generic image has not been
|
storage verified unchanged, and the user confirmed DualSense operation.
|
||||||
flashed or physically qualified across these controller families.
|
Other controller-family motion orientation and motor response still need
|
||||||
|
physical qualification.
|
||||||
|
|
||||||
|
**Nonblocking Wii motion (0.71):** both the `GAMEPAD` and dedicated `WII` paths
|
||||||
|
emit motion on the first usable fresh acceleration/gyro pair. Bias collection
|
||||||
|
requires 1.5 seconds, at least 64 distinct gyro samples, low sensor variation and
|
||||||
|
stable gravity direction, but runs alongside output rather than gating it.
|
||||||
|
Accepted targets are applied at no more than 5 dps of correction per second;
|
||||||
|
they never reset the quaternion or undo accumulated yaw. The absolute candidate
|
||||||
|
gyro-vector limit is 30 dps, retaining headroom for the recorded Wii residual of
|
||||||
|
roughly 13 dps per axis without permitting unbounded learning or ratcheting.
|
||||||
|
Large rates, shaking and changing tilt discard the candidate; invalid/stale
|
||||||
|
sensors retire both the learned correction and target. Fresh recovery starts
|
||||||
|
immediately. DualSense and other factory-only sources do not run this tracker.
|
||||||
|
|
||||||
|
Quiet periods still improve drift; initial drift can be substantial with a large
|
||||||
|
offset. A sufficiently steady rotation about gravity below the candidate limit
|
||||||
|
cannot be distinguished from bias using these sensors alone. This is not a
|
||||||
|
guarantee of drift-free aiming while continuously moving. Built-in MotionPlus
|
||||||
|
needs no accessory handling or manual calibration command.
|
||||||
|
|
||||||
|
All 31 focused regressions pass, including immediate Wii output, bounded
|
||||||
|
background convergence without a pose reset, motion rejection, duplicate-poll
|
||||||
|
invariance and lifecycle recovery. A throwaway production-estimator smoke run
|
||||||
|
kept output ready from its first sample while converging to the recorded-scale
|
||||||
|
offset by six seconds. Generic, dedicated Wii, DualSense and mixed AIO builds pass.
|
||||||
|
Version 0.71 was then flashed and verified, with the saved-storage region
|
||||||
|
byte-for-byte unchanged. The hub and both native children enumerated, and UART
|
||||||
|
confirmed the nonblocking policy. Physical Wii startup/drift qualification is
|
||||||
|
still pending.
|
||||||
|
|
||||||
For sensorless hardware, the checker supports `--input-only`: press real buttons
|
For sensorless hardware, the checker supports `--input-only`: press real buttons
|
||||||
and keep changing controls on both halves during the run. Neutral fallback
|
and keep changing controls on both halves during the run. Neutral fallback
|
||||||
|
|
|
||||||
|
|
@ -1479,7 +1479,7 @@ void publish_device_state(uint8_t slot, uni_hid_device_t* device,
|
||||||
g_native_snapshot.state_generation = target.state_generation;
|
g_native_snapshot.state_generation = target.state_generation;
|
||||||
g_native_snapshot.received_us = motion.received_us;
|
g_native_snapshot.received_us = motion.received_us;
|
||||||
g_native_snapshot.battery = device->controller.battery;
|
g_native_snapshot.battery = device->controller.battery;
|
||||||
g_native_snapshot.requires_stationary_bias =
|
g_native_snapshot.track_stationary_bias =
|
||||||
device->controller_type == CONTROLLER_TYPE_WiiController;
|
device->controller_type == CONTROLLER_TYPE_WiiController;
|
||||||
g_native_snapshot.accel_valid = motion.accel_valid;
|
g_native_snapshot.accel_valid = motion.accel_valid;
|
||||||
g_native_snapshot.gyro_valid = motion.gyro_valid;
|
g_native_snapshot.gyro_valid = motion.gyro_valid;
|
||||||
|
|
|
||||||
|
|
@ -119,7 +119,7 @@ struct Bluepad32NativeGamepadSnapshot {
|
||||||
uint8_t battery = 0;
|
uint8_t battery = 0;
|
||||||
bool accel_valid = false;
|
bool accel_valid = false;
|
||||||
bool gyro_valid = false;
|
bool gyro_valid = false;
|
||||||
bool requires_stationary_bias = false;
|
bool track_stationary_bias = false;
|
||||||
uint32_t accel_sequence = 0;
|
uint32_t accel_sequence = 0;
|
||||||
uint32_t gyro_sequence = 0;
|
uint32_t gyro_sequence = 0;
|
||||||
uint32_t accel_received_us = 0;
|
uint32_t accel_received_us = 0;
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,7 @@ void source_isolation() {
|
||||||
const auto initial = bridge_snapshot();
|
const auto initial = bridge_snapshot();
|
||||||
require(initial.slot == 0 && initial.controller.active && initial.controller.state.button_south &&
|
require(initial.slot == 0 && initial.controller.active && initial.controller.state.button_south &&
|
||||||
initial.battery == 176 && initial.accel_valid && initial.gyro_valid &&
|
initial.battery == 176 && initial.accel_valid && initial.gyro_valid &&
|
||||||
!initial.requires_stationary_bias && initial.accel_q13[1] == 8193 &&
|
!initial.track_stationary_bias && initial.accel_q13[1] == 8193 &&
|
||||||
initial.gyro_q10[2] == -123456 && initial.gyro_received_us == 100000,
|
initial.gyro_q10[2] == -123456 && initial.gyro_received_us == 100000,
|
||||||
"native snapshot must preserve coherent physical controls and calibrated precision");
|
"native snapshot must preserve coherent physical controls and calibrated precision");
|
||||||
now_ms = 120;
|
now_ms = 120;
|
||||||
|
|
@ -319,7 +319,7 @@ void sensorless_admission() {
|
||||||
uint64_t cue = 99;
|
uint64_t cue = 99;
|
||||||
require(initial.controller.active && initial.slot == 0 &&
|
require(initial.controller.active && initial.slot == 0 &&
|
||||||
initial.controller.state.button_south && !initial.accel_valid &&
|
initial.controller.state.button_south && !initial.accel_valid &&
|
||||||
!initial.gyro_valid && !initial.requires_stationary_bias &&
|
!initial.gyro_valid && !initial.track_stationary_bias &&
|
||||||
initial.accel_sequence == 0 && initial.gyro_sequence == 0 &&
|
initial.accel_sequence == 0 && initial.gyro_sequence == 0 &&
|
||||||
!bluepad32_input_backend_native_sample_request(0, 1, &cue) && cue == 0,
|
!bluepad32_input_backend_native_sample_request(0, 1, &cue) && cue == 0,
|
||||||
"sensorless controls remain live without invented IMU or rumble capability");
|
"sensorless controls remain live without invented IMU or rumble capability");
|
||||||
|
|
@ -356,9 +356,9 @@ void independent_motion() {
|
||||||
now_ms = 100;
|
now_ms = 100;
|
||||||
report_gamepad(ds4);
|
report_gamepad(ds4);
|
||||||
const auto first = bridge_snapshot();
|
const auto first = bridge_snapshot();
|
||||||
require(first.accel_valid && first.gyro_valid && !first.requires_stationary_bias &&
|
require(first.accel_valid && first.gyro_valid && !first.track_stationary_bias &&
|
||||||
first.accel_q13[1] == 8193 && first.gyro_q10[2] == -123456,
|
first.accel_q13[1] == 8193 && first.gyro_q10[2] == -123456,
|
||||||
"calibrated DS4 motion keeps raw precision without Wii settling");
|
"calibrated DS4 motion retains precision without Wii background correction");
|
||||||
now_ms = 110;
|
now_ms = 110;
|
||||||
++ds.report_sequence;
|
++ds.report_sequence;
|
||||||
++ds.accel_sequence;
|
++ds.accel_sequence;
|
||||||
|
|
@ -408,8 +408,8 @@ void independent_motion() {
|
||||||
sw.accel_valid = sw.gyro_valid = true;
|
sw.accel_valid = sw.gyro_valid = true;
|
||||||
report_gamepad(pro);
|
report_gamepad(pro);
|
||||||
require(bridge_snapshot().accel_valid && bridge_snapshot().gyro_valid &&
|
require(bridge_snapshot().accel_valid && bridge_snapshot().gyro_valid &&
|
||||||
!bridge_snapshot().requires_stationary_bias,
|
!bridge_snapshot().track_stationary_bias,
|
||||||
"validated Switch motion starts without Wii stationary settling");
|
"validated Switch motion does not enable Wii background correction");
|
||||||
platform_on_device_disconnected(&pro);
|
platform_on_device_disconnected(&pro);
|
||||||
|
|
||||||
auto remote = wii_device(2);
|
auto remote = wii_device(2);
|
||||||
|
|
@ -420,7 +420,7 @@ void independent_motion() {
|
||||||
now_ms = 200;
|
now_ms = 200;
|
||||||
report_gamepad(remote);
|
report_gamepad(remote);
|
||||||
require(bridge_snapshot().controller.state.button_south && bridge_snapshot().accel_valid &&
|
require(bridge_snapshot().controller.state.button_south && bridge_snapshot().accel_valid &&
|
||||||
!bridge_snapshot().gyro_valid && bridge_snapshot().requires_stationary_bias,
|
!bridge_snapshot().gyro_valid && bridge_snapshot().track_stationary_bias,
|
||||||
"Wii without MotionPlus remains acceleration-capable, not a fabricated full IMU");
|
"Wii without MotionPlus remains acceleration-capable, not a fabricated full IMU");
|
||||||
now_ms = 210;
|
now_ms = 210;
|
||||||
++wii.gyro_sequence;
|
++wii.gyro_sequence;
|
||||||
|
|
@ -428,7 +428,7 @@ void independent_motion() {
|
||||||
report_gamepad(remote);
|
report_gamepad(remote);
|
||||||
require(bridge_snapshot().gyro_valid && bridge_snapshot().gyro_received_us == 210000 &&
|
require(bridge_snapshot().gyro_valid && bridge_snapshot().gyro_received_us == 210000 &&
|
||||||
bridge_snapshot().accel_received_us == 200000 &&
|
bridge_snapshot().accel_received_us == 200000 &&
|
||||||
bridge_snapshot().requires_stationary_bias,
|
bridge_snapshot().track_stationary_bias,
|
||||||
"Wii MotionPlus ingress must retain independent acceleration age and Wii bias policy");
|
"Wii MotionPlus ingress must retain independent acceleration age and Wii bias policy");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -321,12 +321,13 @@ void selected_motion_target_keeps_both_control_halves() {
|
||||||
|
|
||||||
void wii_bias_and_independent_sensor_freshness() {
|
void wii_bias_and_independent_sensor_freshness() {
|
||||||
++source.controller.connection_generation;
|
++source.controller.connection_generation;
|
||||||
source.requires_stationary_bias = true;
|
source.track_stationary_bias = true;
|
||||||
source.gyro_q10[1] = 2 * 1024;
|
source.gyro_q10[1] = 2 * 1024;
|
||||||
publish(); pair();
|
publish(); pair();
|
||||||
assert(controls[0].active && controls[1].active);
|
assert(controls[0].active && controls[1].active);
|
||||||
assert(reports[0][2] == 0x01 && reports[1][2] == 0x08);
|
assert(reports[0][2] == 0x01 && reports[1][2] == 0x08);
|
||||||
assert(imu_length(0) == 0 && imu_length(1) == 0);
|
for (uint8_t instance = 0; instance < 2; ++instance)
|
||||||
|
assert(imu_length(instance) == ((SWITCH2_BRIDGE_IMU_TARGET_MASK & (1u << instance)) ? 30 : 0));
|
||||||
for (unsigned i = 0; i < 400; ++i) { publish(); pair(); }
|
for (unsigned i = 0; i < 400; ++i) { publish(); pair(); }
|
||||||
for (uint8_t instance = 0; instance < 2; ++instance) {
|
for (uint8_t instance = 0; instance < 2; ++instance) {
|
||||||
assert(imu_length(instance) == ((SWITCH2_BRIDGE_IMU_TARGET_MASK & (1u << instance)) ? 30 : 0));
|
assert(imu_length(instance) == ((SWITCH2_BRIDGE_IMU_TARGET_MASK & (1u << instance)) ? 30 : 0));
|
||||||
|
|
@ -344,12 +345,13 @@ void wii_bias_and_independent_sensor_freshness() {
|
||||||
publish(); pair();
|
publish(); pair();
|
||||||
assert(reports[0][2] == 0x01 && reports[1][2] == 0x08);
|
assert(reports[0][2] == 0x01 && reports[1][2] == 0x08);
|
||||||
assert(imu_length(0) == 0 && imu_length(1) == 0);
|
assert(imu_length(0) == 0 && imu_length(1) == 0);
|
||||||
// Returning real Wii sensors must settle again, not reuse the old bias.
|
// Fresh Wii sensors recover immediately, without borrowing the old bias.
|
||||||
source.gyro_valid = true;
|
source.gyro_valid = true;
|
||||||
publish(); pair();
|
publish(); pair();
|
||||||
assert(imu_length(0) == 0 && imu_length(1) == 0);
|
for (uint8_t instance = 0; instance < 2; ++instance)
|
||||||
|
assert(imu_length(instance) == ((SWITCH2_BRIDGE_IMU_TARGET_MASK & (1u << instance)) ? 30 : 0));
|
||||||
// A factory-calibrated source switching policy must initialize immediately.
|
// A factory-calibrated source switching policy must initialize immediately.
|
||||||
source.requires_stationary_bias = false;
|
source.track_stationary_bias = false;
|
||||||
publish(); pair();
|
publish(); pair();
|
||||||
for (uint8_t instance = 0; instance < 2; ++instance) {
|
for (uint8_t instance = 0; instance < 2; ++instance) {
|
||||||
assert(imu_length(instance) == ((SWITCH2_BRIDGE_IMU_TARGET_MASK & (1u << instance)) ? 30 : 0));
|
assert(imu_length(instance) == ((SWITCH2_BRIDGE_IMU_TARGET_MASK & (1u << instance)) ? 30 : 0));
|
||||||
|
|
|
||||||
|
|
@ -173,9 +173,11 @@ struct Rig {
|
||||||
ProbeNativeMotionSample sample{};
|
ProbeNativeMotionSample sample{};
|
||||||
uint32_t now;
|
uint32_t now;
|
||||||
uint32_t generation = 1;
|
uint32_t generation = 1;
|
||||||
static constexpr float residual[3]{0.4f, -0.3f, 0.2f};
|
static constexpr float residual[3]{0.0f, 0.0f, 0.0f};
|
||||||
|
ProbeNativeMotionBias policy = ProbeNativeMotionBias::kAlreadyCalibrated;
|
||||||
|
|
||||||
explicit Rig(uint32_t start = 0) : now(start) {
|
explicit Rig(uint32_t start = 0, ProbeNativeMotionBias mode = ProbeNativeMotionBias::kAlreadyCalibrated)
|
||||||
|
: now(start), policy(mode) {
|
||||||
sample.accel_valid = sample.gyro_valid = true;
|
sample.accel_valid = sample.gyro_valid = true;
|
||||||
sample.accel_g[2] = 1.0f;
|
sample.accel_g[2] = 1.0f;
|
||||||
rest();
|
rest();
|
||||||
|
|
@ -193,7 +195,7 @@ struct Rig {
|
||||||
++sample.gyro_sequence;
|
++sample.gyro_sequence;
|
||||||
sample.gyro_us = now;
|
sample.gyro_us = now;
|
||||||
}
|
}
|
||||||
motion.update(now, generation, sample, ProbeNativeMotionBias::kEstimateStationary);
|
motion.update(now, generation, sample, policy);
|
||||||
}
|
}
|
||||||
void settle() {
|
void settle() {
|
||||||
for (unsigned i = 0; i < 65; ++i) fresh();
|
for (unsigned i = 0; i < 65; ++i) fresh();
|
||||||
|
|
@ -207,35 +209,45 @@ struct Rig {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
void startup_needs_count_and_stillness() {
|
void background_bias_requires_quiet_samples_not_startup_delay() {
|
||||||
Rig fast;
|
Rig fast(0, ProbeNativeMotionBias::kTrackStationary);
|
||||||
|
fast.sample.gyro_dps[2] = 1.0f;
|
||||||
fast.fresh(0);
|
fast.fresh(0);
|
||||||
|
expect(fast.motion.ready(), "Wii motion must be available on the first real sensor pair");
|
||||||
for (unsigned i = 0; i < 63; ++i) fast.fresh(1000);
|
for (unsigned i = 0; i < 63; ++i) fast.fresh(1000);
|
||||||
expect(!fast.motion.ready(), "sixty-four packets alone cannot replace 1.5 seconds of stillness");
|
expect(fast.motion.bias()[2] == 0.0f, "packet count alone cannot establish a bias target");
|
||||||
for (unsigned i = 0; i < 56; ++i) fast.fresh();
|
for (unsigned i = 0; i < 56; ++i) fast.fresh();
|
||||||
expect(!fast.motion.ready(), "calibration must not finish before the stillness duration");
|
expect(fast.motion.bias()[2] == 0.0f, "partial quiet windows cannot change gyro correction");
|
||||||
|
const double before = camera_heading(fast.motion);
|
||||||
fast.fresh(37000);
|
fast.fresh(37000);
|
||||||
expect(fast.motion.ready(), "a stationary window may finish at exactly 1.5 seconds");
|
expect(fast.motion.ready() && fast.motion.bias()[2] == 0.0f, "learning a target must not reset or snap motion");
|
||||||
|
expect(std::abs(heading_change(fast.motion, before) - 0.037 * std::acos(-1.0) / 180.0) < 0.000001,
|
||||||
|
"calibration completion must preserve accumulated yaw");
|
||||||
|
fast.fresh();
|
||||||
|
expect(fast.motion.bias()[2] > 0.0f && fast.motion.bias()[2] <= 0.125001f,
|
||||||
|
"a confirmed target must be applied with a bounded slew, not a jump");
|
||||||
|
|
||||||
Rig slow;
|
Rig slow(0, ProbeNativeMotionBias::kTrackStationary);
|
||||||
|
slow.sample.gyro_dps[2] = 1.0f;
|
||||||
slow.fresh(0);
|
slow.fresh(0);
|
||||||
for (unsigned i = 0; i < 62; ++i) slow.fresh(30000);
|
for (unsigned i = 0; i < 62; ++i) slow.fresh(30000);
|
||||||
expect(!slow.motion.ready(), "elapsed stillness cannot replace sixty-four distinct gyro samples");
|
expect(slow.motion.ready() && slow.motion.bias()[2] == 0.0f,
|
||||||
|
"elapsed time without enough distinct samples cannot learn bias");
|
||||||
slow.fresh(30000);
|
slow.fresh(30000);
|
||||||
expect(slow.motion.ready(), "the sixty-fourth stationary sample can finish calibration");
|
slow.fresh(0);
|
||||||
expect(close({slow.motion.bias()[0], slow.motion.bias()[1], slow.motion.bias()[2]},
|
expect(slow.motion.bias()[2] == 0.0f, "zero elapsed time cannot increase correction strength");
|
||||||
{Rig::residual[0], Rig::residual[1], Rig::residual[2]}, 0.000001),
|
slow.fresh(25000);
|
||||||
"calibration must learn the selected sensor's residual bias");
|
expect(slow.motion.bias()[2] > 0.0f, "a complete quiet window must refine bias without gating output");
|
||||||
}
|
}
|
||||||
|
|
||||||
void quantized_wii_bias_calibrates_and_integrates() {
|
void quantized_wii_bias_calibrates_and_integrates() {
|
||||||
Rig rig;
|
Rig rig(0, ProbeNativeMotionBias::kTrackStationary);
|
||||||
constexpr float device_bias[3]{-13.0625f, 12.4375f, -13.125f};
|
constexpr float device_bias[3]{-13.0625f, 12.4375f, -13.125f};
|
||||||
constexpr int noise_q10[8]{-576, 192, -320, 576, -192, 320, -64, 64};
|
constexpr int noise_q10[8]{-576, 192, -320, 576, -192, 320, -64, 64};
|
||||||
const Vector gravity{8352.0 / 8192.0, 290.0 / 8192.0, -298.0 / 8192.0};
|
const Vector gravity{8352.0 / 8192.0, 290.0 / 8192.0, -298.0 / 8192.0};
|
||||||
// Deterministic Wii-scale Q13 steps and Q10 noise, with accel and gyro
|
// Deterministic Wii-scale Q13 steps and Q10 noise, with accel and gyro
|
||||||
// arriving independently at 200 / 100 Hz. Neither noise nor bias is motion.
|
// arriving independently at 200 / 100 Hz. Neither noise nor bias is motion.
|
||||||
for (unsigned i = 0; i < 320 && !rig.motion.ready(); ++i) {
|
for (unsigned i = 0; i < 4000; ++i) {
|
||||||
rig.sample.accel_g[0] = (8352 + (i & 1 ? 80 : -80)) / 8192.0f;
|
rig.sample.accel_g[0] = (8352 + (i & 1 ? 80 : -80)) / 8192.0f;
|
||||||
rig.sample.accel_g[1] = (290 + (i & 2 ? 40 : -40)) / 8192.0f;
|
rig.sample.accel_g[1] = (290 + (i & 2 ? 40 : -40)) / 8192.0f;
|
||||||
rig.sample.accel_g[2] = (-298 + (i & 4 ? 43 : -43)) / 8192.0f;
|
rig.sample.accel_g[2] = (-298 + (i & 4 ? 43 : -43)) / 8192.0f;
|
||||||
|
|
@ -245,11 +257,12 @@ void quantized_wii_bias_calibrates_and_integrates() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
rig.fresh(5000, true, i % 2 == 0);
|
rig.fresh(5000, true, i % 2 == 0);
|
||||||
|
expect(rig.motion.ready(), "quantized Wii motion must remain available throughout background learning");
|
||||||
}
|
}
|
||||||
expect(rig.motion.ready(), "quantized stationary Wii sensors with a large own-device bias must calibrate");
|
expect(rig.motion.ready(), "a large measured Wii residual must remain correctable in the background");
|
||||||
expect(close({rig.motion.bias()[0], rig.motion.bias()[1], rig.motion.bias()[2]},
|
expect(close({rig.motion.bias()[0], rig.motion.bias()[1], rig.motion.bias()[2]},
|
||||||
{device_bias[0], device_bias[1], device_bias[2]}, 0.01),
|
{device_bias[0], device_bias[1], device_bias[2]}, 0.02),
|
||||||
"stationary variation must average into the measured bias, not a nominal or donor zero");
|
"quiet sensor observations must converge to the measured device bias");
|
||||||
const Quaternion reference = rig.packet().q;
|
const Quaternion reference = rig.packet().q;
|
||||||
const double gravity_length = std::sqrt(gravity[0] * gravity[0] + gravity[1] * gravity[1] + gravity[2] * gravity[2]);
|
const double gravity_length = std::sqrt(gravity[0] * gravity[0] + gravity[1] * gravity[1] + gravity[2] * gravity[2]);
|
||||||
expect(close(rotate(reference, gravity), {0.0, 0.0, gravity_length}, 0.0002),
|
expect(close(rotate(reference, gravity), {0.0, 0.0, gravity_length}, 0.0002),
|
||||||
|
|
@ -345,7 +358,7 @@ void elapsed_time_not_packet_count_controls_rotation() {
|
||||||
|
|
||||||
void moving_startup_does_not_calibrate() {
|
void moving_startup_does_not_calibrate() {
|
||||||
for (unsigned movement = 0; movement < 4; ++movement) {
|
for (unsigned movement = 0; movement < 4; ++movement) {
|
||||||
Rig rig;
|
Rig rig(0, ProbeNativeMotionBias::kTrackStationary);
|
||||||
for (unsigned i = 0; i < 100; ++i) {
|
for (unsigned i = 0; i < 100; ++i) {
|
||||||
rig.rest();
|
rig.rest();
|
||||||
rig.sample.accel_g[0] = 0.0f;
|
rig.sample.accel_g[0] = 0.0f;
|
||||||
|
|
@ -364,32 +377,41 @@ void moving_startup_does_not_calibrate() {
|
||||||
if (movement == 3) rig.sample.gyro_dps[0] = i & 1 ? 1.0f : -1.0f;
|
if (movement == 3) rig.sample.gyro_dps[0] = i & 1 ? 1.0f : -1.0f;
|
||||||
rig.fresh();
|
rig.fresh();
|
||||||
}
|
}
|
||||||
expect(!rig.motion.ready(), "moving startup cannot be mistaken for a stationary calibration window");
|
expect(rig.motion.ready() && rig.motion.bias()[0] == 0.0f && rig.motion.bias()[1] == 0.0f &&
|
||||||
|
rig.motion.bias()[2] == 0.0f, "moving startup must stay live without learning movement as bias");
|
||||||
rig.rest();
|
rig.rest();
|
||||||
rig.sample.accel_g[0] = 0.0f;
|
rig.sample.accel_g[0] = 0.0f;
|
||||||
rig.sample.accel_g[1] = 0.0f;
|
rig.sample.accel_g[1] = 0.0f;
|
||||||
rig.sample.accel_g[2] = 1.0f;
|
rig.sample.accel_g[2] = 1.0f;
|
||||||
|
rig.sample.gyro_dps[2] = 1.0f;
|
||||||
for (unsigned i = 0; i < 128; ++i) rig.fresh();
|
for (unsigned i = 0; i < 128; ++i) rig.fresh();
|
||||||
expect(rig.motion.ready(), "a new stationary window must recover after changing motion ends");
|
expect(rig.motion.ready() && rig.motion.bias()[2] > 0.0f, "background correction must recover after movement");
|
||||||
}
|
}
|
||||||
Rig contaminated;
|
Rig contaminated(0, ProbeNativeMotionBias::kTrackStationary);
|
||||||
|
contaminated.sample.gyro_dps[2] = 1.0f;
|
||||||
for (unsigned i = 0; i < 40; ++i) contaminated.fresh();
|
for (unsigned i = 0; i < 40; ++i) contaminated.fresh();
|
||||||
contaminated.sample.gyro_dps[0] = 4.0f;
|
contaminated.sample.gyro_dps[0] = 4.0f;
|
||||||
contaminated.fresh();
|
contaminated.fresh();
|
||||||
contaminated.rest();
|
contaminated.rest();
|
||||||
|
contaminated.sample.gyro_dps[2] = 1.0f;
|
||||||
for (unsigned i = 0; i < 40; ++i) contaminated.fresh();
|
for (unsigned i = 0; i < 40; ++i) contaminated.fresh();
|
||||||
expect(!contaminated.motion.ready(), "a gyro impulse must discard, not pool, the partial bias window");
|
expect(contaminated.motion.ready() && contaminated.motion.bias()[2] == 0.0f,
|
||||||
|
"a gyro impulse must discard, not pool, the partial bias window");
|
||||||
for (unsigned i = 0; i < 24; ++i) contaminated.fresh();
|
for (unsigned i = 0; i < 24; ++i) contaminated.fresh();
|
||||||
expect(contaminated.motion.ready(), "an uncontaminated replacement window must eventually calibrate");
|
expect(contaminated.motion.bias()[2] == 0.0f, "a completed estimate must not snap the current correction");
|
||||||
|
contaminated.fresh();
|
||||||
|
expect(contaminated.motion.bias()[2] > 0.0f, "a replacement quiet window must eventually refine bias");
|
||||||
|
|
||||||
Rig interleaved;
|
Rig interleaved(0, ProbeNativeMotionBias::kTrackStationary);
|
||||||
|
interleaved.sample.gyro_dps[2] = 1.0f;
|
||||||
for (unsigned i = 0; i < 55; ++i) interleaved.fresh();
|
for (unsigned i = 0; i < 55; ++i) interleaved.fresh();
|
||||||
interleaved.sample.accel_g[0] = 0.1f;
|
interleaved.sample.accel_g[0] = 0.1f;
|
||||||
interleaved.fresh(5000, true, false);
|
interleaved.fresh(5000, true, false);
|
||||||
interleaved.sample.accel_g[0] = 0.0f;
|
interleaved.sample.accel_g[0] = 0.0f;
|
||||||
interleaved.fresh(5000, true, false);
|
interleaved.fresh(5000, true, false);
|
||||||
for (unsigned i = 0; i < 20; ++i) interleaved.fresh();
|
for (unsigned i = 0; i < 20; ++i) interleaved.fresh();
|
||||||
expect(!interleaved.motion.ready(), "acceleration-only movement must reset the gyro calibration window too");
|
expect(interleaved.motion.ready() && interleaved.motion.bias()[2] == 0.0f,
|
||||||
|
"acceleration-only movement must reset bias collection without blocking motion");
|
||||||
interleaved.settle();
|
interleaved.settle();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -413,8 +435,10 @@ void duplicate_sequences_and_interleaved_recovery() {
|
||||||
|
|
||||||
void lifecycle_invalidates_bias_and_orientation() {
|
void lifecycle_invalidates_bias_and_orientation() {
|
||||||
for (unsigned fault = 0; fault < 7; ++fault) {
|
for (unsigned fault = 0; fault < 7; ++fault) {
|
||||||
Rig rig;
|
Rig rig(0, ProbeNativeMotionBias::kTrackStationary);
|
||||||
rig.settle();
|
rig.sample.gyro_dps[2] = 1.0f;
|
||||||
|
for (unsigned i = 0; i < 400; ++i) rig.fresh(10000);
|
||||||
|
expect(rig.motion.bias()[2] == 1.0f, "quiet samples must establish a correction before a lifecycle fault");
|
||||||
if (fault == 0) rig.sample.accel_valid = false;
|
if (fault == 0) rig.sample.accel_valid = false;
|
||||||
if (fault == 1) rig.sample.gyro_valid = false;
|
if (fault == 1) rig.sample.gyro_valid = false;
|
||||||
if (fault == 2) rig.sample.accel_g[0] = std::numeric_limits<float>::quiet_NaN();
|
if (fault == 2) rig.sample.accel_g[0] = std::numeric_limits<float>::quiet_NaN();
|
||||||
|
|
@ -422,18 +446,21 @@ void lifecycle_invalidates_bias_and_orientation() {
|
||||||
if (fault == 4) ++rig.generation;
|
if (fault == 4) ++rig.generation;
|
||||||
if (fault == 5) rig.motion.reset();
|
if (fault == 5) rig.motion.reset();
|
||||||
rig.fresh(fault == 6 ? 50001 : 1000);
|
rig.fresh(fault == 6 ? 50001 : 1000);
|
||||||
expect(!rig.motion.ready(), "lifecycle or invalid sensor input must fail closed immediately");
|
expect(rig.motion.ready() == (fault >= 4), "invalid sensors fail closed; fresh sensors after reset recover immediately");
|
||||||
|
expect(rig.motion.bias()[2] == 0.0f, "faults must retire the old correction and its target");
|
||||||
|
++rig.generation;
|
||||||
rig.sample.accel_valid = rig.sample.gyro_valid = true;
|
rig.sample.accel_valid = rig.sample.gyro_valid = true;
|
||||||
rig.sample.accel_g[0] = rig.sample.accel_g[1] = 0.0f;
|
rig.sample.accel_g[0] = rig.sample.accel_g[1] = 0.0f;
|
||||||
rig.sample.accel_g[2] = -1.0f;
|
rig.sample.accel_g[2] = -1.0f;
|
||||||
rig.sample.gyro_dps[0] = -0.25f;
|
rig.sample.gyro_dps[0] = -0.25f;
|
||||||
rig.sample.gyro_dps[1] = 0.1f;
|
rig.sample.gyro_dps[1] = 0.1f;
|
||||||
rig.sample.gyro_dps[2] = -0.35f;
|
rig.sample.gyro_dps[2] = -0.35f;
|
||||||
rig.settle();
|
rig.fresh();
|
||||||
|
expect(rig.motion.ready() && close(rotate(rig.packet().q, {0.0, 0.0, -1.0}), {0.0, 0.0, 1.0}),
|
||||||
|
"a fresh connection must initialize from its real pose without waiting");
|
||||||
|
for (unsigned i = 0; i < 400; ++i) rig.fresh(10000);
|
||||||
expect(close({rig.motion.bias()[0], rig.motion.bias()[1], rig.motion.bias()[2]}, {-0.25, 0.1, -0.35}, 0.000001),
|
expect(close({rig.motion.bias()[0], rig.motion.bias()[1], rig.motion.bias()[2]}, {-0.25, 0.1, -0.35}, 0.000001),
|
||||||
"recovery must calibrate a new bias instead of borrowing the previous connection's bias");
|
"background recovery must learn the new source, not resume a retired target");
|
||||||
expect(close(rotate(rig.packet().q, {0.0, 0.0, -1.0}), {0.0, 0.0, 1.0}),
|
|
||||||
"recovery must align the new physical pose instead of replaying old orientation");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -462,8 +489,10 @@ void stale_boundaries_and_hidden_gaps() {
|
||||||
hidden.fresh(50000, false, false);
|
hidden.fresh(50000, false, false);
|
||||||
hidden.fresh(49999, false, false);
|
hidden.fresh(49999, false, false);
|
||||||
hidden.fresh(2);
|
hidden.fresh(2);
|
||||||
expect(!hidden.motion.ready(), "fresh replacements cannot hide an intervening stale sample interval");
|
expect(hidden.motion.ready() && close(rotate(hidden.packet().q, {1.0, 0.0, 0.0}), {1.0, 0.0, 0.0}),
|
||||||
|
"fresh replacements after a hidden gap must reinitialize, not integrate stale angular displacement");
|
||||||
hidden.rest();
|
hidden.rest();
|
||||||
|
hidden.fresh(0);
|
||||||
hidden.settle();
|
hidden.settle();
|
||||||
expect(close(rotate(hidden.packet().q, {1.0, 0.0, 0.0}), {1.0, 0.0, 0.0}),
|
expect(close(rotate(hidden.packet().q, {1.0, 0.0, 0.0}), {1.0, 0.0, 0.0}),
|
||||||
"stale angular displacement must not replay after recalibration");
|
"stale angular displacement must not replay after recalibration");
|
||||||
|
|
@ -482,7 +511,7 @@ void clocks_sequences_and_connections_wrap() {
|
||||||
rig.sample.accel_sequence = rig.sample.gyro_sequence = 0;
|
rig.sample.accel_sequence = rig.sample.gyro_sequence = 0;
|
||||||
rig.rest();
|
rig.rest();
|
||||||
rig.fresh(0);
|
rig.fresh(0);
|
||||||
expect(!rig.motion.ready(), "a new connection cannot inherit readiness even if it reuses sample identities");
|
expect(rig.motion.ready(), "a new connection must start immediately from its own fresh samples");
|
||||||
rig.settle();
|
rig.settle();
|
||||||
expect(close(rotate(rig.packet().q, {1.0, 0.0, 0.0}), {1.0, 0.0, 0.0}),
|
expect(close(rotate(rig.packet().q, {1.0, 0.0, 0.0}), {1.0, 0.0, 0.0}),
|
||||||
"a new connection must establish its own reference orientation");
|
"a new connection must establish its own reference orientation");
|
||||||
|
|
@ -793,12 +822,45 @@ void optical_vertical_forward_defers_the_anchor() {
|
||||||
"heading correction must recover after the forward projection leaves vertical");
|
"heading correction must recover after the forward projection leaves vertical");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void background_tracking_preserves_motion_and_sensor_time() {
|
||||||
|
Rig turn(0, ProbeNativeMotionBias::kTrackStationary);
|
||||||
|
turn.sample.gyro_dps[2] = 90.0f;
|
||||||
|
turn.fresh(0);
|
||||||
|
for (unsigned i = 1; i <= 1000; ++i) {
|
||||||
|
turn.fresh(10000);
|
||||||
|
expect(turn.motion.ready(), "continuous movement must never block Wii motion");
|
||||||
|
if (i == 100)
|
||||||
|
expect(close(rotate(turn.packet().q, {1.0, 0.0, 0.0}), {0.0, 1.0, 0.0}),
|
||||||
|
"startup rotation must reach native output immediately");
|
||||||
|
}
|
||||||
|
expect(turn.motion.bias()[2] == 0.0f, "large steady yaw must not be learned as a stationary offset");
|
||||||
|
|
||||||
|
Rig sparse(0, ProbeNativeMotionBias::kTrackStationary);
|
||||||
|
Rig frequent(0, ProbeNativeMotionBias::kTrackStationary);
|
||||||
|
sparse.sample.gyro_dps[2] = frequent.sample.gyro_dps[2] = 1.0f;
|
||||||
|
sparse.fresh(0);
|
||||||
|
frequent.fresh(0);
|
||||||
|
for (unsigned ms = 1; ms <= 4000; ++ms) {
|
||||||
|
frequent.fresh(1000, ms % 10 == 0, ms % 10 == 0);
|
||||||
|
if (ms % 10 == 0) sparse.fresh(10000);
|
||||||
|
}
|
||||||
|
expect(sparse.motion.bias()[2] == 1.0f && frequent.motion.bias()[2] == 1.0f,
|
||||||
|
"stationary gyro offset must converge under either polling cadence");
|
||||||
|
expect(std::abs(heading_change(sparse.motion, camera_heading(frequent.motion))) < 0.000001,
|
||||||
|
"duplicate polling must not increase background correction or change accumulated yaw");
|
||||||
|
const double corrected_heading = camera_heading(sparse.motion);
|
||||||
|
for (unsigned i = 0; i < 100; ++i) sparse.fresh(10000);
|
||||||
|
expect(std::abs(heading_change(sparse.motion, corrected_heading)) < 0.000001,
|
||||||
|
"learned correction must stop further stationary yaw drift without recentering");
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
int main() {
|
int main() {
|
||||||
codec_wire_edges();
|
codec_wire_edges();
|
||||||
codec_rejects_invalid_inputs();
|
codec_rejects_invalid_inputs();
|
||||||
startup_needs_count_and_stillness();
|
background_bias_requires_quiet_samples_not_startup_delay();
|
||||||
|
background_tracking_preserves_motion_and_sensor_time();
|
||||||
quantized_wii_bias_calibrates_and_integrates();
|
quantized_wii_bias_calibrates_and_integrates();
|
||||||
measured_gravity_sets_reference();
|
measured_gravity_sets_reference();
|
||||||
bias_corrected_body_rotation_reaches_wire();
|
bias_corrected_body_rotation_reaches_wire();
|
||||||
|
|
|
||||||
|
|
@ -144,7 +144,7 @@ int main() {
|
||||||
source.accel_q13[1] = 8192; // SDL up -> virtual native rail-down +X.
|
source.accel_q13[1] = 8192; // SDL up -> virtual native rail-down +X.
|
||||||
memcpy(source.gyro_q10, bias_q10, sizeof(bias_q10));
|
memcpy(source.gyro_q10, bias_q10, sizeof(bias_q10));
|
||||||
assert(poll());
|
assert(poll());
|
||||||
assert(controls.active && packet[15] == 0); // Wii alone still estimates residual bias before IMU output.
|
assert(controls.active && packet[15] == 30); // Wii IMU starts before background bias learning.
|
||||||
for (unsigned i = 1; i < 450; ++i) assert(poll());
|
for (unsigned i = 1; i < 450; ++i) assert(poll());
|
||||||
assert(controls.active && packet[15] == 30 && packet[19] == 0x0c);
|
assert(controls.active && packet[15] == 30 && packet[19] == 0x0c);
|
||||||
assert(signed32(packet+32) == (1 << 28));
|
assert(signed32(packet+32) == (1 << 28));
|
||||||
|
|
@ -222,7 +222,8 @@ int main() {
|
||||||
|
|
||||||
// Gyro-only motion remains available while the sensor bar is out of view.
|
// Gyro-only motion remains available while the sensor bar is out of view.
|
||||||
ir_mask = 0;
|
ir_mask = 0;
|
||||||
for (unsigned i = 0; i < 20; ++i) assert(poll());
|
// Give background correction a fresh quiet window after the simulated cooling.
|
||||||
|
for (unsigned i = 0; i < 750; ++i) assert(poll());
|
||||||
decode_quaternion(packet, initial);
|
decode_quaternion(packet, initial);
|
||||||
for (unsigned i = 0; i < 250; ++i) assert(poll());
|
for (unsigned i = 0; i < 250; ++i) assert(poll());
|
||||||
double after_bias[4]; decode_quaternion(packet, after_bias);
|
double after_bias[4]; decode_quaternion(packet, after_bias);
|
||||||
|
|
|
||||||
|
|
@ -290,7 +290,7 @@ void poll_wii_source(uint32_t now_ms) {
|
||||||
motion.gyro_dps[0] = static_cast<float>(g_wii.gyro_q10[1]) / 1024.0f;
|
motion.gyro_dps[0] = static_cast<float>(g_wii.gyro_q10[1]) / 1024.0f;
|
||||||
motion.gyro_dps[1] = -static_cast<float>(g_wii.gyro_q10[2]) / 1024.0f;
|
motion.gyro_dps[1] = -static_cast<float>(g_wii.gyro_q10[2]) / 1024.0f;
|
||||||
motion.gyro_dps[2] = -static_cast<float>(g_wii.gyro_q10[0]) / 1024.0f;
|
motion.gyro_dps[2] = -static_cast<float>(g_wii.gyro_q10[0]) / 1024.0f;
|
||||||
g_motion.update(now_us, g_wii_generation, motion, ProbeNativeMotionBias::kEstimateStationary);
|
g_motion.update(now_us, g_wii_generation, motion, ProbeNativeMotionBias::kTrackStationary);
|
||||||
WiiIrMouseReport optical{};
|
WiiIrMouseReport optical{};
|
||||||
(void)wii_ir_mouse_peek(&optical, 0);
|
(void)wii_ir_mouse_peek(&optical, 0);
|
||||||
// Core 1 may publish during the peek. Read the clock after the snapshot.
|
// Core 1 may publish during the peek. Read the clock after the snapshot.
|
||||||
|
|
@ -308,7 +308,7 @@ void poll_wii_source(uint32_t now_ms) {
|
||||||
lroundf(bias[2] * 1000));
|
lroundf(bias[2] * 1000));
|
||||||
} else {
|
} else {
|
||||||
probe_debug_printf("[PROBE] Wii native IMU %s\n", sensor_status ?
|
probe_debug_printf("[PROBE] Wii native IMU %s\n", sensor_status ?
|
||||||
"calibrating: keep still" : "waiting for fresh calibrated accelerometer/MotionPlus");
|
"waiting for a usable acceleration sample" : "waiting for fresh calibrated accelerometer/MotionPlus");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
update_wii_ir_gate(now_us);
|
update_wii_ir_gate(now_us);
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ void probe_controller_input_set_full_stick_calibration(uint8_t instance, const u
|
||||||
// enable preserves it. Joy-Con mode relays its bounded FIFO; right-only Wii
|
// enable preserves it. Joy-Con mode relays its bounded FIFO; right-only Wii
|
||||||
// mode synthesizes fresh calibrated sensors and the selected IR pointer.
|
// mode synthesizes fresh calibrated sensors and the selected IR pointer.
|
||||||
// Full-controller mode splits one supported gamepad into independent R/L output streams;
|
// Full-controller mode splits one supported gamepad into independent R/L output streams;
|
||||||
// controls remain live while motion is unavailable. Only Wii estimates stationary bias.
|
// controls remain live while motion is unavailable. Wii refines bias without blocking IMU.
|
||||||
// No pairing changes.
|
// No pairing changes.
|
||||||
void probe_controller_input_set_native_stream(uint8_t instance, bool enabled);
|
void probe_controller_input_set_native_stream(uint8_t instance, bool enabled);
|
||||||
// Copy one63-byte payload without report ID. Returns a boot-unique token, or0
|
// Copy one63-byte payload without report ID. Returns a boot-unique token, or0
|
||||||
|
|
|
||||||
|
|
@ -819,10 +819,10 @@ int main(void) {
|
||||||
instance, probe_storage_offset(instance));
|
instance, probe_storage_offset(instance));
|
||||||
#ifdef SWITCH_PICO_SWITCH2_USB_BRIDGE
|
#ifdef SWITCH_PICO_SWITCH2_USB_BRIDGE
|
||||||
#if SWITCH2_BRIDGE_WII_INPUT
|
#if SWITCH2_BRIDGE_WII_INPUT
|
||||||
probe_debug_printf("[PROBE] Wii IR drives native mouse movement; buttons retain profile mapping; keep Wii still for MotionPlus calibration\n");
|
probe_debug_printf("[PROBE] Wii IR drives native mouse movement; buttons retain profile mapping; MotionPlus bias learns in background\n");
|
||||||
probe_debug_printf("[PROBE] Hold BOOTSEL2s for pairing; Wii cue feedback uses bounded ERM patterns, not HD audio waveforms\n");
|
probe_debug_printf("[PROBE] Hold BOOTSEL2s for pairing; Wii cue feedback uses bounded ERM patterns, not HD audio waveforms\n");
|
||||||
#elif SWITCH2_BRIDGE_FULL_INPUT
|
#elif SWITCH2_BRIDGE_FULL_INPUT
|
||||||
probe_debug_printf("[PROBE] Full gamepad controls on R/L; IMU mask=%u; only Wii requires settling\n",
|
probe_debug_printf("[PROBE] Full gamepad controls on R/L; IMU mask=%u; Wii bias learns without startup settling\n",
|
||||||
(unsigned)SWITCH2_BRIDGE_IMU_TARGET_MASK);
|
(unsigned)SWITCH2_BRIDGE_IMU_TARGET_MASK);
|
||||||
probe_debug_printf("[PROBE] Hold BOOTSEL 2s for Bluetooth pairing (never clears pairings); cues use source capabilities\n");
|
probe_debug_printf("[PROBE] Hold BOOTSEL 2s for Bluetooth pairing (never clears pairings); cues use source capabilities\n");
|
||||||
#else
|
#else
|
||||||
|
|
|
||||||
|
|
@ -181,7 +181,7 @@ void refresh(uint32_t now_ms) {
|
||||||
source.accel_sequence == g_source.accel_sequence && source.gyro_sequence == g_source.gyro_sequence &&
|
source.accel_sequence == g_source.accel_sequence && source.gyro_sequence == g_source.gyro_sequence &&
|
||||||
source.accel_received_us == g_source.accel_received_us && source.gyro_received_us == g_source.gyro_received_us &&
|
source.accel_received_us == g_source.accel_received_us && source.gyro_received_us == g_source.gyro_received_us &&
|
||||||
source.accel_valid == g_source.accel_valid && source.gyro_valid == g_source.gyro_valid &&
|
source.accel_valid == g_source.accel_valid && source.gyro_valid == g_source.gyro_valid &&
|
||||||
source.requires_stationary_bias == g_source.requires_stationary_bias) return;
|
source.track_stationary_bias == g_source.track_stationary_bias) return;
|
||||||
if (changed_connection) {
|
if (changed_connection) {
|
||||||
lose_source(now_ms);
|
lose_source(now_ms);
|
||||||
g_motion.reset();
|
g_motion.reset();
|
||||||
|
|
@ -216,14 +216,13 @@ void refresh(uint32_t now_ms) {
|
||||||
sample.gyro_dps[1] = -static_cast<float>(source.gyro_q10[2]) / 1024.0f;
|
sample.gyro_dps[1] = -static_cast<float>(source.gyro_q10[2]) / 1024.0f;
|
||||||
sample.gyro_dps[2] = static_cast<float>(source.gyro_q10[1]) / 1024.0f;
|
sample.gyro_dps[2] = static_cast<float>(source.gyro_q10[1]) / 1024.0f;
|
||||||
g_motion.update(now_us, source.controller.connection_generation, sample,
|
g_motion.update(now_us, source.controller.connection_generation, sample,
|
||||||
source.requires_stationary_bias ? ProbeNativeMotionBias::kEstimateStationary :
|
source.track_stationary_bias ? ProbeNativeMotionBias::kTrackStationary :
|
||||||
ProbeNativeMotionBias::kAlreadyCalibrated);
|
ProbeNativeMotionBias::kAlreadyCalibrated);
|
||||||
const int status = !sensors_fresh(g_source, now_us) ? 0 : g_motion.ready() ? 2 : 1;
|
const int status = !sensors_fresh(g_source, now_us) ? 0 : g_motion.ready() ? 2 : 1;
|
||||||
if (status != g_sensor_status) {
|
if (status != g_sensor_status) {
|
||||||
g_sensor_status = status;
|
g_sensor_status = status;
|
||||||
probe_debug_printf("[PROBE] Native gamepad IMU %s\n", status == 2 ? "ready" :
|
probe_debug_printf("[PROBE] Native gamepad IMU %s\n", status == 2 ? "ready" :
|
||||||
status == 1 ? (source.requires_stationary_bias ? "Wii calibrating: keep still" :
|
status == 1 ? "waiting for a usable acceleration sample" : "waiting for supported fresh sensors");
|
||||||
"waiting for a usable acceleration sample") : "waiting for supported fresh sensors");
|
|
||||||
}
|
}
|
||||||
for (uint8_t i = 0; i < PROBE_CONTROLLER_COUNT; ++i) {
|
for (uint8_t i = 0; i < PROBE_CONTROLLER_COUNT; ++i) {
|
||||||
// Latest-only: a blocked endpoint never queues obsolete controls/IMU.
|
// Latest-only: a blocked endpoint never queues obsolete controls/IMU.
|
||||||
|
|
@ -329,7 +328,7 @@ bool probe_native_gamepad_input_commit_native_report(uint8_t instance, uint32_t
|
||||||
source.accel_sequence != g_source.accel_sequence || source.gyro_sequence != g_source.gyro_sequence ||
|
source.accel_sequence != g_source.accel_sequence || source.gyro_sequence != g_source.gyro_sequence ||
|
||||||
source.accel_received_us != g_source.accel_received_us || source.gyro_received_us != g_source.gyro_received_us ||
|
source.accel_received_us != g_source.accel_received_us || source.gyro_received_us != g_source.gyro_received_us ||
|
||||||
source.accel_valid != g_source.accel_valid || source.gyro_valid != g_source.gyro_valid ||
|
source.accel_valid != g_source.accel_valid || source.gyro_valid != g_source.gyro_valid ||
|
||||||
source.requires_stationary_bias != g_source.requires_stationary_bias ||
|
source.track_stationary_bias != g_source.track_stationary_bias ||
|
||||||
now_us - source.received_us >= kInputDeadlineUs || now_us - child.pending_us >= kOutputDeadlineUs ||
|
now_us - source.received_us >= kInputDeadlineUs || now_us - child.pending_us >= kOutputDeadlineUs ||
|
||||||
(child.pending_motion && !sensors_fresh(source, now_us))) return false;
|
(child.pending_motion && !sensors_fresh(source, now_us))) return false;
|
||||||
child.pending_token = 0;
|
child.pending_token = 0;
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,11 @@ constexpr uint32_t kVariationSamples = 16;
|
||||||
// over distinct samples, with roughly twice that measured noise allowance.
|
// over distinct samples, with roughly twice that measured noise allowance.
|
||||||
constexpr float kGyroRmsDps = 0.75f;
|
constexpr float kGyroRmsDps = 0.75f;
|
||||||
constexpr float kAccelRmsG = 0.025f;
|
constexpr float kAccelRmsG = 0.025f;
|
||||||
|
// The recorded Wii sample has roughly 13 dps residual on each axis. Keep that
|
||||||
|
// correctable, but never learn unrestricted gameplay rates or ratchet this
|
||||||
|
// limit relative to a previously learned bias. Low steady yaw remains ambiguous.
|
||||||
|
constexpr float kMaximumBiasDps = 30.0f;
|
||||||
|
constexpr float kBiasSlewDpsPerSecond = 5.0f;
|
||||||
// One degree between averaged gravity directions, independent of g scale.
|
// One degree between averaged gravity directions, independent of g scale.
|
||||||
constexpr float kGravityDirectionCosSquared = 0.9996954135f;
|
constexpr float kGravityDirectionCosSquared = 0.9996954135f;
|
||||||
constexpr float kGravityTimeConstantUs = 1000000.0f;
|
constexpr float kGravityTimeConstantUs = 1000000.0f;
|
||||||
|
|
@ -150,6 +155,7 @@ void ProbeNativeMotion::invalidate() {
|
||||||
acceleration_[i] = 0.0f;
|
acceleration_[i] = 0.0f;
|
||||||
gyro_dps_[i] = 0.0f;
|
gyro_dps_[i] = 0.0f;
|
||||||
bias_[i] = 0.0f;
|
bias_[i] = 0.0f;
|
||||||
|
bias_target_[i] = 0.0f;
|
||||||
}
|
}
|
||||||
// Keep the last observed identities until a connection change or explicit
|
// Keep the last observed identities until a connection change or explicit
|
||||||
// reset. Restoring availability cannot turn the same packet into new data.
|
// reset. Restoring availability cannot turn the same packet into new data.
|
||||||
|
|
@ -346,6 +352,7 @@ void ProbeNativeMotion::update(uint32_t now_us, uint32_t connection_generation,
|
||||||
}
|
}
|
||||||
const bool was_ready = ready_;
|
const bool was_ready = ready_;
|
||||||
const uint32_t accel_elapsed_us = was_ready && new_accel ? sample.accel_us - accel_us_ : 0;
|
const uint32_t accel_elapsed_us = was_ready && new_accel ? sample.accel_us - accel_us_ : 0;
|
||||||
|
const uint32_t gyro_elapsed_us = was_ready && new_gyro ? sample.gyro_us - gyro_us_ : 0;
|
||||||
float previous_gyro[3];
|
float previous_gyro[3];
|
||||||
if (was_ready && new_gyro) {
|
if (was_ready && new_gyro) {
|
||||||
for (unsigned i = 0; i < 3; ++i) previous_gyro[i] = gyro_dps_[i];
|
for (unsigned i = 0; i < 3; ++i) previous_gyro[i] = gyro_dps_[i];
|
||||||
|
|
@ -383,20 +390,31 @@ void ProbeNativeMotion::update(uint32_t now_us, uint32_t connection_generation,
|
||||||
invalidate();
|
invalidate();
|
||||||
}
|
}
|
||||||
if (ready_ && new_accel && !correct_gravity(accel_elapsed_us)) invalidate();
|
if (ready_ && new_accel && !correct_gravity(accel_elapsed_us)) invalidate();
|
||||||
return;
|
} else {
|
||||||
}
|
// Admission depends on real fresh sensors, never on a quiet interval.
|
||||||
|
|
||||||
if (bias_mode_ == ProbeNativeMotionBias::kAlreadyCalibrated) {
|
|
||||||
// Trust only the caller's validated calibrated samples, not an estimated
|
|
||||||
// stationary bias. The first acceleration fixes a relative gravity frame.
|
|
||||||
for (unsigned i = 0; i < 3; ++i) mean_accel_[i] = acceleration_[i];
|
for (unsigned i = 0; i < 3; ++i) mean_accel_[i] = acceleration_[i];
|
||||||
ready_ = initialize_orientation();
|
ready_ = initialize_orientation();
|
||||||
return;
|
}
|
||||||
|
if (ready_ && bias_mode_ == ProbeNativeMotionBias::kTrackStationary)
|
||||||
|
track_bias(new_accel, new_gyro, gyro_elapsed_us);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProbeNativeMotion::track_bias(bool new_accel, bool new_gyro, uint32_t gyro_elapsed_us) {
|
||||||
|
if (new_gyro && gyro_elapsed_us) {
|
||||||
|
float delta[3];
|
||||||
|
for (unsigned i = 0; i < 3; ++i) delta[i] = bias_target_[i] - bias_[i];
|
||||||
|
const float distance_squared = squared_norm(delta);
|
||||||
|
const float step = kBiasSlewDpsPerSecond * (static_cast<float>(gyro_elapsed_us) * 1e-6f);
|
||||||
|
if (distance_squared > 0.0f) {
|
||||||
|
const float weight = distance_squared > step * step ? step / std::sqrt(distance_squared) : 1.0f;
|
||||||
|
for (unsigned i = 0; i < 3; ++i) bias_[i] += delta[i] * weight;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const float acceleration_norm_squared = squared_norm(acceleration_);
|
const float acceleration_norm_squared = squared_norm(acceleration_);
|
||||||
if (acceleration_norm_squared < 0.85f * 0.85f ||
|
if (acceleration_norm_squared < 0.85f * 0.85f ||
|
||||||
acceleration_norm_squared > 1.15f * 1.15f) {
|
acceleration_norm_squared > 1.15f * 1.15f ||
|
||||||
|
squared_norm(gyro_dps_) > kMaximumBiasDps * kMaximumBiasDps) {
|
||||||
clear_candidate();
|
clear_candidate();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -428,9 +446,9 @@ void ProbeNativeMotion::update(uint32_t now_us, uint32_t connection_generation,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!new_gyro) return;
|
if (!new_gyro) return;
|
||||||
// An absolute angular-rate limit cannot distinguish motion from the bias
|
// Variation and changing gravity reject movement. A bounded, perfectly
|
||||||
// being estimated. Changing rates, acceleration and gravity direction can;
|
// steady rotation about gravity can still look like bias; never claim an
|
||||||
// perfectly steady rotation about gravity still requires the user to rest.
|
// absolute heading or suppress real input while collecting this estimate.
|
||||||
if (!accumulate_stationary(gyro_dps_, ++gyro_count_, mean_gyro_,
|
if (!accumulate_stationary(gyro_dps_, ++gyro_count_, mean_gyro_,
|
||||||
gyro_variation_, kGyroRmsDps)) {
|
gyro_variation_, kGyroRmsDps)) {
|
||||||
clear_candidate();
|
clear_candidate();
|
||||||
|
|
@ -438,12 +456,7 @@ void ProbeNativeMotion::update(uint32_t now_us, uint32_t connection_generation,
|
||||||
}
|
}
|
||||||
if (gyro_count_ >= kCalibrationSamples && accel_count_ >= kVariationSamples &&
|
if (gyro_count_ >= kCalibrationSamples && accel_count_ >= kVariationSamples &&
|
||||||
gyro_us_ - candidate_us_ >= kCalibrationUs) {
|
gyro_us_ - candidate_us_ >= kCalibrationUs) {
|
||||||
if (!initialize_orientation()) {
|
for (unsigned i = 0; i < 3; ++i) bias_target_[i] = mean_gyro_[i];
|
||||||
clear_candidate();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (unsigned i = 0; i < 3; ++i) bias_[i] = mean_gyro_[i];
|
|
||||||
ready_ = true;
|
|
||||||
clear_candidate();
|
clear_candidate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,15 +31,15 @@ struct ProbeNativeMotionSample {
|
||||||
|
|
||||||
enum class ProbeNativeMotionBias : uint8_t {
|
enum class ProbeNativeMotionBias : uint8_t {
|
||||||
kAlreadyCalibrated,
|
kAlreadyCalibrated,
|
||||||
kEstimateStationary,
|
kTrackStationary,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Core 0 only. Call update even when sensors are unavailable, and consult ready
|
// Core 0 only. Call update even when sensors are unavailable, and consult ready
|
||||||
// before using the orientation. Sequence identities, not polling, admit samples;
|
// before using the orientation. Sequence identities, not polling, admit samples;
|
||||||
// repeated sequences cannot refresh timestamps or contribute to calibration.
|
// repeated sequences cannot refresh timestamps or contribute to calibration.
|
||||||
// Already-calibrated sources initialize from the first usable fresh sensor pair.
|
// All sources initialize from the first usable fresh sensor pair. Wii optionally
|
||||||
// Wii explicitly requests stationary residual-bias estimation: constant rotation
|
// refines residual bias in the background without resetting orientation. Steady
|
||||||
// about gravity is indistinguishable from an unknown bias without another sensor.
|
// rotation about gravity remains indistinguishable from bias without a reference.
|
||||||
// Fresh near-1g acceleration corrects tilt drift. Heading remains gyro-derived
|
// Fresh near-1g acceleration corrects tilt drift. Heading remains gyro-derived
|
||||||
// unless a reliable optical heading observation is supplied.
|
// unless a reliable optical heading observation is supplied.
|
||||||
class ProbeNativeMotion {
|
class ProbeNativeMotion {
|
||||||
|
|
@ -67,6 +67,7 @@ private:
|
||||||
bool integrate(const float gyro_dps[3], uint32_t elapsed_us);
|
bool integrate(const float gyro_dps[3], uint32_t elapsed_us);
|
||||||
bool correct_gravity(uint32_t elapsed_us);
|
bool correct_gravity(uint32_t elapsed_us);
|
||||||
bool normalize_orientation();
|
bool normalize_orientation();
|
||||||
|
void track_bias(bool new_accel, bool new_gyro, uint32_t gyro_elapsed_us);
|
||||||
|
|
||||||
bool have_generation_ = false;
|
bool have_generation_ = false;
|
||||||
ProbeNativeMotionBias bias_mode_ = ProbeNativeMotionBias::kAlreadyCalibrated;
|
ProbeNativeMotionBias bias_mode_ = ProbeNativeMotionBias::kAlreadyCalibrated;
|
||||||
|
|
@ -100,6 +101,7 @@ private:
|
||||||
float acceleration_[3]{};
|
float acceleration_[3]{};
|
||||||
float gyro_dps_[3]{};
|
float gyro_dps_[3]{};
|
||||||
float bias_[3]{};
|
float bias_[3]{};
|
||||||
|
float bias_target_[3]{};
|
||||||
float mean_gyro_[3]{};
|
float mean_gyro_[3]{};
|
||||||
float mean_accel_[3]{};
|
float mean_accel_[3]{};
|
||||||
float gravity_reference_[3]{};
|
float gravity_reference_[3]{};
|
||||||
|
|
|
||||||
|
|
@ -369,9 +369,9 @@ function(switch2_usb_probe_configure target)
|
||||||
endif()
|
endif()
|
||||||
if(SWITCH2_PROBE_HUB AND SWITCH2_BRIDGE_FULL_INPUT)
|
if(SWITCH2_PROBE_HUB AND SWITCH2_BRIDGE_FULL_INPUT)
|
||||||
if(SWITCH2_PROBE_TRACE_NATIVE_INPUT)
|
if(SWITCH2_PROBE_TRACE_NATIVE_INPUT)
|
||||||
pico_set_program_version(${target} "0.70-native-gamepad-trace")
|
pico_set_program_version(${target} "0.71-native-gamepad-trace")
|
||||||
else()
|
else()
|
||||||
pico_set_program_version(${target} "0.70-native-gamepad")
|
pico_set_program_version(${target} "0.71-native-gamepad")
|
||||||
endif()
|
endif()
|
||||||
elseif(SWITCH2_PROBE_HUB)
|
elseif(SWITCH2_PROBE_HUB)
|
||||||
pico_set_program_version(${target} "0.67-native-hub-latency")
|
pico_set_program_version(${target} "0.67-native-hub-latency")
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue