switch-pico/tests/wii_ir_aiming_test.cpp

174 lines
6.8 KiB
C++

#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";
}