Add selectable Wii IR aiming and DolphinBar keepalive

This commit is contained in:
Joey Yakimowich-Payne 2026-09-09 17:13:27 -06:00
commit 5ecfca105a
27 changed files with 4079 additions and 53 deletions

View file

@ -0,0 +1,33 @@
from __future__ import annotations
import shutil
import subprocess
from pathlib import Path
def test_wii_ir_aiming_output_contract(tmp_path: Path) -> None:
root = Path(__file__).resolve().parents[1]
compiler = shutil.which("c++") or shutil.which("g++")
assert compiler is not None, "a host C++ compiler is required"
executable = tmp_path / "wii_ir_aiming_test"
subprocess.run(
[
compiler,
"-std=c++17",
"-Wall",
"-Wextra",
"-Werror",
"-pedantic",
"-DSWITCH_PICO_WII_IR_GYRO=1",
f"-I{root / 'tests' / 'wii_ir_aiming_native_stubs'}",
f"-I{root / 'src' / 'firmware'}",
str(root / "tests" / "wii_ir_aiming_test.cpp"),
str(root / "src" / "firmware" / "input" / "wii_ir_pointer.cpp"),
str(root / "src" / "firmware" / "input" / "wii_ir_tracker.cpp"),
"-o",
str(executable),
],
check=True,
cwd=root,
)
subprocess.run([str(executable)], check=True, cwd=root)

View file

@ -0,0 +1,31 @@
from __future__ import annotations
import shutil
import subprocess
from pathlib import Path
def test_wii_ir_tracker_native(tmp_path: Path) -> None:
root = Path(__file__).resolve().parents[1]
compiler = shutil.which("c++") or shutil.which("g++")
assert compiler is not None, "a host C++ compiler is required"
executable = tmp_path / "wii_ir_tracker_test"
subprocess.run(
[
compiler,
"-std=c++17",
"-Wall",
"-Wextra",
"-Werror",
"-pedantic",
f"-I{root / 'src' / 'firmware'}",
str(root / "tests" / "wii_ir_tracker_test.cpp"),
str(root / "src" / "firmware" / "input" / "wii_ir_tracker.cpp"),
"-o",
str(executable),
],
check=True,
cwd=root,
)
subprocess.run([str(executable)], check=True, cwd=root)

View file

@ -0,0 +1,6 @@
#pragma once
struct critical_section_t {};
inline unsigned next_striped_spin_lock_num() { return 16; }
inline void critical_section_init_with_lock_num(critical_section_t*, unsigned) {}
inline void critical_section_enter_blocking(critical_section_t*) {}
inline void critical_section_exit(critical_section_t*) {}

View file

@ -0,0 +1,3 @@
#pragma once
#include <stdint.h>
uint32_t time_us_32();

View file

@ -0,0 +1,174 @@
#include "input/wii_ir_pointer.h"
#include <cmath>
#include <cstdint>
#include <iostream>
static uint32_t clock_us;
uint32_t time_us_32() { return clock_us; }
namespace {
int failures;
void expect(bool okay, const char* message) {
if (!okay) { std::cerr << message << '\n'; ++failures; }
}
struct Rotation {
int64_t x = 0;
int64_t y = 0;
int64_t z = 0;
};
struct Rig {
uint32_t sequence = 0;
uint8_t visible_mask;
explicit Rig(uint8_t mask = 3) : visible_mask(mask) {
wii_ir_pointer_init();
wii_ir_pointer_reset();
clock_us = 100000;
update_motion();
expect(wii_ir_gyro_select(0, 1, true), "IR source must be selectable");
send(); // Arm output before establishing a fresh camera baseline.
for (int i = 0; i < 12; ++i) {
input(0, 0);
if (i % 3 == 2) send();
}
}
void update_motion() {
const ControllerMotionSample physical{0, 0, 4096, 11, 22, 33};
wii_ir_gyro_update_motion(0, 1, true, physical);
}
void input(int dx, int dy, uint16_t buttons = 0, bool nunchuk_c = false) {
clock_us += 5000;
update_motion();
const uint16_t x[4] = {static_cast<uint16_t>(400 + dx), static_cast<uint16_t>(624 + dx), 0, 0};
const uint16_t y[4] = {static_cast<uint16_t>(384 + dy), static_cast<uint16_t>(384 + dy), 0, 0};
wii_ir_pointer_observe(0, 1, ++sequence, buttons, x, y, visible_mask, nunchuk_c);
}
ControllerState prepare(WiiIrGyroReport& ticket) {
ControllerState state{};
expect(wii_ir_gyro_prepare(0, clock_us, &state, &ticket), "selected IR must supply motion");
expect(state.motion_sample_count == 3, "IR reports contain three 5ms samples");
for (const auto& sample : state.motion_samples) {
expect(sample.accel_x == 0 && sample.accel_y == 0 && sample.accel_z == 4096,
"changing aiming axes must preserve the accelerometer");
}
return state;
}
ControllerMotionSample send() {
WiiIrGyroReport ticket{};
const ControllerState state = prepare(ticket);
const auto first = state.motion_samples[0];
for (const auto& sample : state.motion_samples) {
expect(sample.gyro_x == first.gyro_x && sample.gyro_y == first.gyro_y && sample.gyro_z == first.gyro_z,
"the report's angular displacement must be spread consistently over 15ms");
}
wii_ir_gyro_commit(ticket);
return first;
}
Rotation sweep(int horizontal, int vertical, uint16_t buttons = 0, bool nunchuk_c = false) {
Rotation total;
for (int i = 1; i <= 300; ++i) {
const int travel = i < 100 ? i : 100;
input(horizontal * travel, vertical * travel, buttons, nunchuk_c);
if (i % 3 == 0) {
const auto sample = send();
total.x += sample.gyro_x;
total.y += sample.gyro_y;
total.z += sample.gyro_z;
}
}
return total;
}
};
void horizontal_aim_matches_physical_yaw() {
for (int direction : {-1, 1}) {
Rig rig;
const auto rotation = rig.sweep(direction, 0);
// The physical Wii reference has gravity and horizontal yaw on Z.
// Check angular displacement, not merely a nonzero output channel.
const double expected = std::atan(100.0 / 1726.7951);
const double yaw = rotation.z * 0.015 / 818.5;
expect(std::abs(yaw - direction * expected) < expected * 0.05,
"IR yaw must match the physical rotation implied by camera travel");
expect(rotation.x == 0 && std::abs(rotation.y) < std::abs(rotation.z) / 50 + 3,
"horizontal pointing must not produce roll or vertical aiming");
}
}
void vertical_aim_retains_second_gyro_axis() {
for (int direction : {-1, 1}) {
Rig rig;
const auto rotation = rig.sweep(0, direction);
expect(rotation.y * direction < 0, "vertical IR direction and gyro Y must remain unchanged");
expect(rotation.x == 0 && rotation.z == 0, "vertical IR must not generate horizontal/roll movement");
}
}
void reposition_requires_c_and_one() {
{
Rig rig;
expect(rig.sweep(1, 0, 0x0002, false).z > 0,
"1 alone must no longer pause IR aiming");
}
{
Rig rig;
expect(rig.sweep(1, 0, 0, true).z > 0,
"Nunchuk C alone must not pause IR aiming");
}
Rig rig;
const auto held = rig.sweep(1, 1, 0x0002, true);
expect(held.x == 0 && held.y == 0 && held.z == 0,
"C + 1 must suppress aiming on every gyro axis while repositioning");
for (int i = 0; i < 6; ++i) {
rig.input(100, 100);
if (i % 3 == 2) {
const auto released = rig.send();
expect(released.gyro_x == 0 && released.gyro_y == 0 && released.gyro_z == 0,
"releasing C + 1 must establish a new baseline, not replay held motion");
}
}
int64_t movement = 0;
for (int i = 1; i <= 30; ++i) {
rig.input(100 + i, 100);
if (i % 3 == 0) movement += rig.send().gyro_z;
}
expect(movement > 0, "horizontal aiming must resume after reposition release");
}
void failed_send_preserves_horizontal_motion_once() {
Rig rig;
for (int i = 0; i < 3; ++i) rig.input(20, 0);
WiiIrGyroReport first{};
const auto waiting = rig.prepare(first).motion_samples[0];
expect(waiting.gyro_z > 0, "horizontal motion must be pending before the failed send");
WiiIrGyroReport retry{};
const auto repeated = rig.prepare(retry).motion_samples[0];
expect(repeated.gyro_z == waiting.gyro_z && repeated.gyro_x == 0,
"an uncommitted send must retain horizontal movement on its correct axis");
wii_ir_gyro_commit(first);
const auto consumed = rig.send();
expect(consumed.gyro_x == 0 && consumed.gyro_y == 0 && consumed.gyro_z == 0,
"successfully sent motion must not be emitted twice");
}
void single_spot_retains_horizontal_aiming() {
Rig rig(1);
const auto rotation = rig.sweep(1, 0);
const double expected = std::atan((500 - 511.5) / 1726.7951) -
std::atan((400 - 511.5) / 1726.7951);
expect(std::abs(rotation.z * 0.015 / 818.5 - expected) < expected * 0.05,
"one persistent spot must deliver its full yaw without an 80ms timeout");
expect(rotation.x == 0 && rotation.y == 0,
"single-spot horizontal travel must not create roll or pitch");
}
} // namespace
int main() {
horizontal_aim_matches_physical_yaw();
vertical_aim_retains_second_gyro_axis();
reposition_requires_c_and_one();
failed_send_preserves_horizontal_motion_once();
single_spot_retains_horizontal_aiming();
if (failures) return 1;
std::cout << "IR aiming output axes passed\n";
}

View file

@ -0,0 +1,378 @@
#include "input/wii_ir_tracker.h"
#include <algorithm>
#include <array>
#include <cmath>
#include <cstdint>
#include <iostream>
#include <limits>
namespace {
int failures = 0;
void expect(bool condition, const char* message) {
if (!condition) {
std::cerr << message << '\n';
++failures;
}
}
bool near(float actual, float expected, float tolerance) {
return std::isfinite(actual) && std::fabs(actual - expected) <= tolerance;
}
struct Frame {
std::array<uint16_t, 4> x{};
std::array<uint16_t, 4> y{};
uint8_t mask = 0;
};
Frame pair(uint16_t ax, uint16_t ay, uint16_t bx, uint16_t by) {
return {{ax, bx, 0, 0}, {ay, by, 0, 0}, 0x03};
}
struct Camera {
WiiIrTracker tracker;
uint32_t now;
explicit Camera(WiiIrCameraModel model = {}, uint32_t start = 100000,
bool relative_motion = false)
: tracker(model, relative_motion), now(start) {}
WiiIrTrackingResult update(const Frame& frame, uint32_t elapsed = 10000,
float gravity_roll = 0, bool gravity_valid = false) {
now += elapsed;
return tracker.update(frame.x.data(), frame.y.data(), frame.mask, now,
gravity_roll, gravity_valid);
}
WiiIrTrackingResult acquire(const Frame& frame, float gravity_roll = 0,
bool gravity_valid = false) {
expect(!update(frame, 10000, gravity_roll, gravity_valid).tracked,
"one cold sample must not start tracking");
const auto result = update(frame, 10000, gravity_roll, gravity_valid);
expect(result.tracked && result.rebased && !result.inferred,
"two consistent full-pair samples must acquire with a rebase");
return result;
}
WiiIrTrackingResult settle(const Frame& frame) {
WiiIrTrackingResult result;
for (int i = 0; i < 40; ++i) {
result = update(frame);
expect(result.tracked && !result.rebased,
"an unchanged acquired pair must remain continuously tracked");
}
return result;
}
};
void test_slot_permutation_and_far_reflection() {
Camera camera;
const auto original = pair(420, 350, 620, 390);
camera.acquire(original);
const auto baseline = camera.settle(original);
// Swap the endpoint slots, then move them to different slots. The final
// frame also has a distant reflection that must not replace either end.
const std::array<Frame, 3> frames{{
pair(620, 390, 420, 350),
{{620, 0, 420, 0}, {390, 0, 350, 0}, 0x05},
{{620, 30, 420, 0}, {390, 700, 350, 0}, 0x07},
}};
const std::array<uint8_t, 3> selected_masks{{0x03, 0x05, 0x05}};
for (size_t i = 0; i < frames.size(); ++i) {
const auto result = camera.update(frames[i]);
expect(result.tracked && !result.rebased && !result.inferred,
"slot changes and a far reflection must not interrupt a matched pair");
expect(result.pair_mask == selected_masks[i],
"the selected pair must exclude the reflection and identify current slots");
expect(near(result.yaw_radians, baseline.yaw_radians, 0.001f) &&
near(result.pitch_radians, baseline.pitch_radians, 0.001f),
"unchanged physical spots must retain their bearings after slot changes");
expect(near(result.range_in_bar_widths, baseline.range_in_bar_widths, 0.02f),
"a reflection must not corrupt the tracked range");
}
}
void test_single_marker_expires_despite_continued_reports() {
Camera camera;
const auto full = pair(400, 384, 624, 384);
camera.acquire(full);
const auto baseline = camera.settle(full);
WiiIrTrackingResult result;
for (int i = 1; i <= 7; ++i) {
// The surviving endpoint also changes slot from the full-pair frame.
const Frame single{{0, 0, 0, static_cast<uint16_t>(400 + 2 * i)},
{0, 0, 0, 384}, 0x08};
result = camera.update(single);
expect(result.tracked && result.inferred && !result.rebased,
"an unambiguous moving endpoint must track briefly using inferred geometry");
expect(result.pair_mask == 0x08,
"inferred tracking must identify only the currently visible endpoint");
}
expect(result.yaw_radians < baseline.yaw_radians - 0.0005f,
"single-marker inference must produce the surviving endpoint's motion");
const Frame single{{0, 0, 0, 418}, {0, 0, 0, 384}, 0x08};
expect(!camera.update(single, 20000).tracked,
"single-marker inference must expire after 80ms from the last full pair");
for (int i = 0; i < 4; ++i) {
expect(!camera.update(single).tracked,
"continued single-marker reports must not restart the inference lifetime");
}
}
void test_full_pair_return_rebases_inference_correction() {
Camera camera;
camera.acquire(pair(400, 384, 624, 384));
const Frame single{{408, 0, 0, 0}, {384, 0, 0, 0}, 0x01};
const auto inferred = camera.update(single);
expect(inferred.tracked && inferred.inferred,
"a briefly missing endpoint must enter inferred tracking");
// Inference placed the missing endpoint at x=632. Its real return corrects
// that assumption, which must not become an emitted aiming delta.
const auto returned = camera.update(pair(408, 384, 650, 384));
expect(returned.tracked && !returned.inferred && returned.rebased,
"a correcting full-pair return must rebase rather than emit an inference jump");
expect(camera.update(pair(408, 384, 650, 384)).tracked,
"the returned full pair must remain usable after the correction");
}
void test_zero_marker_loss_and_reacquisition() {
Camera camera;
const auto original = pair(400, 350, 620, 350);
camera.acquire(original);
camera.settle(original);
expect(!camera.update(Frame{}).tracked,
"zero visible markers must stop tracking on the first empty frame");
const auto returned = pair(430, 350, 650, 350);
auto result = camera.update(returned);
if (!result.tracked) {
result = camera.update(returned);
}
expect(result.tracked && result.rebased && !result.inferred,
"the first tracked pair after loss must rebase, not bridge the missing interval");
result = camera.update(returned);
expect(result.tracked && !result.rebased,
"reacquisition must resume continuous tracking after the initial rebase");
}
void test_configured_pinhole_bearings_and_range() {
const WiiIrCameraModel model{1000.0f, 800.0f, 480.0f, 360.0f};
Camera camera(model);
const auto frame = pair(480, 440, 680, 560);
camera.acquire(frame);
const auto result = camera.settle(frame);
// Normalized endpoints (0,.1), (.2,.25) have span .25. Rotating their
// midpoint (.1,.175) into the bar frame gives (.185,.08).
const float expected_yaw = -std::atan2(0.185f, 1.0f);
const float expected_pitch = -std::atan2(0.08f, std::sqrt(1.0f + 0.185f * 0.185f));
expect(near(result.yaw_radians, expected_yaw, 0.001f),
"settled yaw must use the configured center and normalized bar-frame bearing");
expect(near(result.pitch_radians, expected_pitch, 0.001f),
"settled pitch must use independent vertical focal length and spherical bearing");
expect(near(result.range_in_bar_widths, 4.0f, 0.02f),
"range must be reciprocal normalized span, in bar widths rather than pixels");
}
void test_jitter_attenuation_preserves_slow_motion() {
Camera camera(WiiIrCameraModel{1000.0f, 1000.0f, 512.0f, 384.0f});
const auto stationary = pair(412, 384, 612, 384);
camera.acquire(stationary);
const auto baseline = camera.settle(stationary);
float peak_jitter = 0.0f;
for (int i = 0; i < 64; ++i) {
// One-pixel endpoint noise gives half-pixel midpoint noise even though
// the public camera coordinates are integers.
const auto jitter = pair(static_cast<uint16_t>(i % 2 == 0 ? 411 : 413),
384, 612, 384);
const auto result = camera.update(jitter, 5000);
expect(result.tracked && !result.rebased,
"subpixel midpoint jitter must not cause tracking loss or rebases");
peak_jitter = std::max(peak_jitter, std::fabs(result.yaw_radians - baseline.yaw_radians));
}
const float raw_jitter = std::atan2(0.5f, 1000.0f);
expect(peak_jitter < raw_jitter * 0.6f,
"high-frequency subpixel midpoint jitter must be substantially attenuated");
const auto resting = camera.settle(stationary);
float previous_yaw = resting.yaw_radians;
for (int step = 1; step <= 16; ++step) {
const auto moving = pair(static_cast<uint16_t>(412 - step), 384, 612, 384);
for (int hold = 0; hold < 8; ++hold) {
const auto result = camera.update(moving);
expect(result.tracked && !result.rebased,
"slow accumulated movement must remain continuously tracked");
expect(result.yaw_radians >= previous_yaw - 0.00001f,
"monotonic slow motion must not cause filter-driven direction reversals");
previous_yaw = result.yaw_radians;
}
}
expect(previous_yaw > resting.yaw_radians + 0.006f,
"half-pixel motion steps must accumulate instead of dying in a per-frame dead zone");
}
void test_optional_gravity_rejects_wrong_pair() {
const auto vertical = pair(512, 260, 512, 500);
Camera constrained;
for (int i = 0; i < 3; ++i) {
expect(!constrained.update(vertical, 10000, 0.0f, true).tracked,
"a vertical pair must not acquire against a valid horizontal gravity prior");
}
Camera unconstrained;
unconstrained.acquire(vertical);
const auto free_result = unconstrained.update(vertical, 10000, 0.0f, false);
expect(free_result.tracked && !free_result.rebased,
"the same vertical geometry must remain usable without a valid gravity prior");
Camera dynamic;
const auto horizontal = pair(400, 384, 624, 384);
dynamic.acquire(horizontal, 0.0f, true);
const auto result = dynamic.update(pair(410, 384, 634, 384), 10000, 1.5707963f, false);
expect(result.tracked && !result.rebased,
"an invalid dynamic gravity hint must not disable otherwise continuous IR tracking");
}
Frame rolled_pair(float radians, const WiiIrCameraModel& model) {
const float cosine = std::cos(radians);
const float sine = std::sin(radians);
Frame frame;
for (size_t i = 0; i < 2; ++i) {
const float bar_x = i == 0 ? -0.02f : 0.18f;
const float bar_y = 0.04f;
const float camera_x = bar_x * cosine - bar_y * sine;
const float camera_y = bar_x * sine + bar_y * cosine;
frame.x[i] = static_cast<uint16_t>(std::lround(model.cx + model.fx * camera_x));
frame.y[i] = static_cast<uint16_t>(std::lround(model.cy + model.fy * camera_y));
}
frame.mask = 0x03;
return frame;
}
void test_roll_through_vertical_preserves_endpoint_order() {
const WiiIrCameraModel model{1000.0f, 800.0f, 512.0f, 384.0f};
Camera camera(model);
const auto initial = rolled_pair(0.0f, model);
camera.acquire(initial);
const auto baseline = camera.settle(initial);
for (int degrees = 5; degrees <= 120; degrees += 5) {
const float radians = static_cast<float>(degrees) * 0.01745329252f;
const auto result = camera.update(rolled_pair(radians, model));
expect(result.tracked && !result.rebased && !result.inferred,
"rolling a matched pair through vertical must preserve continuous tracking");
expect(near(result.yaw_radians, baseline.yaw_radians, 0.0025f) &&
near(result.pitch_radians, baseline.pitch_radians, 0.0025f),
"passing 90 degrees of roll must not flip endpoint order or invert bearings");
expect(near(result.range_in_bar_widths, 5.0f, 0.08f),
"roll with unequal focal lengths must preserve normalized pair range");
}
}
void test_timestamp_wrap_preserves_continuous_motion() {
Camera ordinary({}, 100000);
Camera wrapping({}, std::numeric_limits<uint32_t>::max() - 50000u);
const auto initial = pair(400, 384, 624, 384);
ordinary.acquire(initial);
const auto baseline = wrapping.acquire(initial);
float previous_yaw = baseline.yaw_radians;
for (int step = 1; step <= 16; ++step) {
const auto frame = pair(static_cast<uint16_t>(400 - 3 * step), 384,
static_cast<uint16_t>(624 - 3 * step), 384);
const auto reference = ordinary.update(frame);
const auto result = wrapping.update(frame);
expect(result.tracked && !result.rebased,
"uint32 timestamp wrap must not interrupt continuous pair motion");
expect(near(result.yaw_radians, reference.yaw_radians, 0.00001f) &&
near(result.pitch_radians, reference.pitch_radians, 0.00001f),
"wrapped and ordinary clocks must yield the same time-filtered bearings");
expect(std::fabs(result.yaw_radians - previous_yaw) < 0.02f,
"timestamp wrap must not create an angular discontinuity");
previous_yaw = result.yaw_radians;
}
expect(previous_yaw > baseline.yaw_radians + 0.015f,
"motion across timestamp wrap must advance the bearing rather than freeze it");
}
void test_relative_tracking_without_recognizable_bar() {
const WiiIrCameraModel model{1000.0f, 1000.0f, 512.0f, 384.0f};
// One endpoint alone, and the four-spot/reflection layout captured on
// hardware. Neither provides a uniquely identifiable full sensor bar.
for (unsigned count : {1u, 4u}) {
Camera camera(model, 100000, true);
const Frame scene{{184, 193, 687, 739}, {623, 536, 453, 549}, 15};
WiiIrTrackingResult result;
for (unsigned step = 0; step <= 160; ++step) {
Frame frame;
const unsigned travel = std::min(step, 80u);
for (unsigned i = 0; i < count; ++i) {
const unsigned slot = (i + step) % 4;
frame.x[slot] = scene.x[i] + travel;
frame.y[slot] = scene.y[i];
frame.mask |= 1u << slot;
}
result = camera.update(frame, 10000, 0.0f, true);
expect(result.tracked && (step == 0 || !result.rebased),
"persistent spots must retain motion beyond 80ms without a full bar");
}
float expected = 0;
for (unsigned i = 0; i < count; ++i) {
expected -= std::atan((scene.x[i] + 80 - model.cx) / model.fx) -
std::atan((scene.x[i] - model.cx) / model.fx);
}
expected /= count;
expect(near(result.yaw_radians, expected, 0.001f),
"single/multiple-spot motion must retain the measured angular travel");
expect(near(result.pitch_radians, 0, 0.0001f),
"slot changes and extra spots must not create vertical motion");
}
}
void test_relative_loss_and_conflicting_matches_rebase() {
Camera camera({}, 100000, true);
const Frame first{{400, 0, 0, 0}, {384, 0, 0, 0}, 1};
camera.update(first);
camera.update(Frame{{410, 0, 0, 0}, {384, 0, 0, 0}, 1});
expect(!camera.update(Frame{}).tracked,
"relative aiming must stop immediately when every spot disappears");
auto result = camera.update(Frame{{700, 0, 0, 0}, {384, 0, 0, 0}, 1});
expect(result.tracked && result.rebased,
"reacquisition must discard movement across the missing interval");
result = camera.update(first);
expect(result.rebased, "an implausible reflection jump must not generate aiming");
Camera conflicting({}, 100000, true);
conflicting.update(pair(400, 384, 600, 384));
result = conflicting.update(pair(410, 384, 590, 384));
expect(result.rebased,
"two contradictory matches must not choose an arbitrary aiming direction");
result = conflicting.update(pair(410, 384, 590, 384));
expect(result.tracked && !result.rebased && near(result.yaw_radians, 0, 0.0001f),
"settled observations must resume without replaying rejected movement");
}
} // namespace
int main() {
test_slot_permutation_and_far_reflection();
test_single_marker_expires_despite_continued_reports();
test_full_pair_return_rebases_inference_correction();
test_zero_marker_loss_and_reacquisition();
test_configured_pinhole_bearings_and_range();
test_jitter_attenuation_preserves_slow_motion();
test_optional_gravity_rejects_wrong_pair();
test_roll_through_vertical_preserves_endpoint_order();
test_timestamp_wrap_preserves_continuous_motion();
test_relative_tracking_without_recognizable_bar();
test_relative_loss_and_conflicting_matches_rebase();
if (failures != 0) {
std::cerr << failures << " Wii IR tracker test(s) failed\n";
return 1;
}
return 0;
}