switch-pico/tests/firmware/test_switch2_reports.cpp
Joey Yakimowich-Payne f8de986717 Implement Switch2 reports
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-08-11 12:23:07 +09:00

186 lines
7.7 KiB
C++

#include "test_support.h"
#include <cstddef>
#include <cstdint>
#include "../../switch2_reports.h"
namespace {
SwitchInputState neutral_state() {
SwitchInputState state{};
state.lx = SWITCH_PRO_JOYSTICK_MID;
state.ly = SWITCH_PRO_JOYSTICK_MID;
state.rx = SWITCH_PRO_JOYSTICK_MID;
state.ry = SWITCH_PRO_JOYSTICK_MID;
return state;
}
bool bytes_are_zero(
const std::array<uint8_t, 63>& payload,
std::size_t begin,
std::size_t end) {
for (std::size_t index = begin; index < end; ++index) {
if (payload[index] != 0) return false;
}
return true;
}
bool switch2_neutral_reports_pack_documented_constants() {
// Given: neutral normalized input and distinct counters.
const SwitchInputState state = neutral_state();
// When: common and Pro reports are built.
const Switch2InputReport common = switch2_build_input_report(
Switch2InputReportId::Common, state, 0x78563412);
const Switch2InputReport pro = switch2_build_input_report(
Switch2InputReportId::Pro, state, 0x1234);
// Then: IDs, counters, neutral sticks, and documented constants are exact.
CHECK(static_cast<uint8_t>(common.id) == 0x05);
CHECK(static_cast<uint8_t>(pro.id) == 0x09);
CHECK(common.payload.size() + 1 == 64 && pro.payload.size() + 1 == 64);
CHECK(common.payload[0] == 0x12 && common.payload[1] == 0x34);
CHECK(common.payload[2] == 0x56 && common.payload[3] == 0x78);
CHECK(common.payload[10] == 0xFF && common.payload[11] == 0xF7 && common.payload[12] == 0x7F);
CHECK(common.payload[13] == 0xFF && common.payload[14] == 0xF7 && common.payload[15] == 0x7F);
CHECK(common.payload[0x29] == 0x01);
CHECK(pro.payload[0] == 0x34);
CHECK(pro.payload[1] == 0x01);
CHECK(pro.payload[5] == 0xFF && pro.payload[6] == 0xF7 && pro.payload[7] == 0x7F);
CHECK(pro.payload[8] == 0xFF && pro.payload[9] == 0xF7 && pro.payload[10] == 0x7F);
CHECK(pro.payload[11] == 0x30);
return true;
}
bool switch2_report_counters_roll_over_deterministically() {
// Given: counters at and beyond the Pro report's eight-bit boundary.
const SwitchInputState state = neutral_state();
// When: reports are built at 255 and 256.
const Switch2InputReport common = switch2_build_input_report(
Switch2InputReportId::Common, state, 0xFFFFFFFF);
const Switch2InputReport pro_255 = switch2_build_input_report(
Switch2InputReportId::Pro, state, 255);
const Switch2InputReport pro_256 = switch2_build_input_report(
Switch2InputReportId::Pro, state, 256);
// Then: common remains LE32 while Pro uses the low eight bits.
CHECK(common.payload[0] == 0xFF && common.payload[1] == 0xFF);
CHECK(common.payload[2] == 0xFF && common.payload[3] == 0xFF);
CHECK(pro_255.payload[0] == 0xFF);
CHECK(pro_256.payload[0] == 0x00);
return true;
}
bool switch2_reports_map_every_shared_button() {
struct ButtonCase {
bool SwitchInputState::*field;
uint8_t common_offset;
uint8_t common_mask;
uint8_t pro_offset;
uint8_t pro_mask;
};
static constexpr ButtonCase cases[] = {
{&SwitchInputState::button_y, 4, 0x01, 2, 0x04},
{&SwitchInputState::button_b, 4, 0x04, 2, 0x01},
{&SwitchInputState::button_a, 4, 0x08, 2, 0x02},
{&SwitchInputState::button_x, 4, 0x02, 2, 0x08},
{&SwitchInputState::button_r, 4, 0x40, 2, 0x10},
{&SwitchInputState::button_zr, 4, 0x80, 2, 0x20},
{&SwitchInputState::button_minus, 5, 0x01, 3, 0x40},
{&SwitchInputState::button_plus, 5, 0x02, 2, 0x40},
{&SwitchInputState::button_r3, 5, 0x04, 2, 0x80},
{&SwitchInputState::button_l3, 5, 0x08, 3, 0x80},
{&SwitchInputState::button_home, 5, 0x10, 4, 0x01},
{&SwitchInputState::button_capture, 5, 0x20, 4, 0x02},
{&SwitchInputState::dpad_down, 6, 0x01, 3, 0x01},
{&SwitchInputState::dpad_up, 6, 0x02, 3, 0x08},
{&SwitchInputState::dpad_right, 6, 0x04, 3, 0x02},
{&SwitchInputState::dpad_left, 6, 0x08, 3, 0x04},
{&SwitchInputState::button_l, 6, 0x40, 3, 0x10},
{&SwitchInputState::button_zl, 6, 0x80, 3, 0x20},
};
// Given/When: every shared button is built independently in both reports.
for (const ButtonCase& button : cases) {
SwitchInputState state = neutral_state();
state.*(button.field) = true;
const Switch2InputReport common = switch2_build_input_report(
Switch2InputReportId::Common, state, 0);
const Switch2InputReport pro = switch2_build_input_report(
Switch2InputReportId::Pro, state, 0);
// Then: only the captured byte and bit for that button is set.
for (uint8_t offset = 4; offset <= 7; ++offset) {
CHECK(common.payload[offset] ==
(offset == button.common_offset ? button.common_mask : 0));
}
for (uint8_t offset = 2; offset <= 4; ++offset) {
CHECK(pro.payload[offset] ==
(offset == button.pro_offset ? button.pro_mask : 0));
}
}
return true;
}
bool switch2_reports_pack_twelve_bit_stick_extremes() {
// Given: four distinct normalized axis values.
SwitchInputState state{};
state.lx = 0x0000;
state.ly = 0xFFFF;
state.rx = 0x1234;
state.ry = 0xABCD;
// When: both report formats are built.
const Switch2InputReport common = switch2_build_input_report(
Switch2InputReportId::Common, state, 0);
const Switch2InputReport pro = switch2_build_input_report(
Switch2InputReportId::Pro, state, 0);
// Then: axes are reduced and packed in captured 12-bit little-endian form.
const uint8_t expected_left[] = {0x00, 0xF0, 0xFF};
const uint8_t expected_right[] = {0x23, 0xC1, 0xAB};
for (uint8_t index = 0; index < 3; ++index) {
CHECK(common.payload[10 + index] == expected_left[index]);
CHECK(common.payload[13 + index] == expected_right[index]);
CHECK(pro.payload[5 + index] == expected_left[index]);
CHECK(pro.payload[8 + index] == expected_right[index]);
}
return true;
}
bool switch2_reports_zero_unknowns_and_ignore_imu() {
// Given: populated IMU input that has no known Switch 2 packing.
SwitchInputState state = neutral_state();
state.imu_sample_count = 3;
state.imu_samples[0] = {1, 2, 3, 4, 5, 6};
state.imu_samples[1] = {7, 8, 9, 10, 11, 12};
state.imu_samples[2] = {13, 14, 15, 16, 17, 18};
// When: both reports are built twice from identical input.
const Switch2InputReport common = switch2_build_input_report(
Switch2InputReportId::Common, state, 7);
const Switch2InputReport common_again = switch2_build_input_report(
Switch2InputReportId::Common, state, 7);
const Switch2InputReport pro = switch2_build_input_report(
Switch2InputReportId::Pro, state, 7);
// Then: unknown power/sensor/motion fields stay deterministic and zero.
CHECK(common.payload == common_again.payload);
CHECK(bytes_are_zero(common.payload, 8, 10));
CHECK(bytes_are_zero(common.payload, 16, 0x29));
CHECK(bytes_are_zero(common.payload, 0x2A, 63));
CHECK(bytes_are_zero(pro.payload, 12, 63));
return true;
}
} // namespace
void run_switch2_report_tests(TestRunner& runner) {
runner.run("Switch 2 neutral report constants", switch2_neutral_reports_pack_documented_constants);
runner.run("Switch 2 report counter rollover", switch2_report_counters_roll_over_deterministically);
runner.run("Switch 2 report button mapping", switch2_reports_map_every_shared_button);
runner.run("Switch 2 report stick packing", switch2_reports_pack_twelve_bit_stick_extremes);
runner.run("Switch 2 report unknown fields", switch2_reports_zero_unknowns_and_ignore_imu);
}