switch-pico/tests/switch_haptics_test.cpp

218 lines
8.3 KiB
C++

#include "switch_haptics.h"
#include <array>
#include <cstdint>
#include <iostream>
namespace {
int failures = 0;
void expect_output(const char* scenario, ControllerRumbleOutput actual,
uint8_t expected_low, uint8_t expected_high) {
if (actual.low_frequency_magnitude == expected_low &&
actual.high_frequency_magnitude == expected_high) {
return;
}
std::cerr << scenario << ": expected low/high "
<< static_cast<unsigned>(expected_low) << "/"
<< static_cast<unsigned>(expected_high) << ", got "
<< static_cast<unsigned>(actual.low_frequency_magnitude) << "/"
<< static_cast<unsigned>(actual.high_frequency_magnitude) << '\n';
++failures;
}
uint32_t type_2(uint8_t high_frequency, uint8_t high_amplitude,
uint8_t low_frequency, uint8_t low_amplitude) {
return (1u << 30u) |
((static_cast<uint32_t>(low_amplitude) & 0x7fu) << 23u) |
((static_cast<uint32_t>(low_frequency) & 0x7fu) << 16u) |
((static_cast<uint32_t>(high_amplitude) & 0x7fu) << 9u) |
((static_cast<uint32_t>(high_frequency) & 0x7fu) << 2u);
}
uint32_t type_1_one_sample(uint8_t high_command, uint8_t low_command) {
return (1u << 30u) |
((static_cast<uint32_t>(low_command) & 0x1fu) << 25u) |
((static_cast<uint32_t>(high_command) & 0x1fu) << 20u);
}
uint32_t type_1_three_samples(uint8_t high_0, uint8_t low_0,
uint8_t high_1, uint8_t low_1,
uint8_t high_2, uint8_t low_2) {
return (3u << 30u) |
((static_cast<uint32_t>(low_0) & 0x1fu) << 25u) |
((static_cast<uint32_t>(high_0) & 0x1fu) << 20u) |
((static_cast<uint32_t>(low_1) & 0x1fu) << 15u) |
((static_cast<uint32_t>(high_1) & 0x1fu) << 10u) |
((static_cast<uint32_t>(low_2) & 0x1fu) << 5u) |
(static_cast<uint32_t>(high_2) & 0x1fu);
}
std::array<uint8_t, 8> payload(uint32_t left, uint32_t right) {
std::array<uint8_t, 8> bytes{};
const uint32_t words[2] = {left, right};
for (unsigned actuator = 0; actuator < 2; ++actuator) {
const unsigned offset = actuator * 4u;
bytes[offset] = static_cast<uint8_t>(words[actuator]);
bytes[offset + 1u] = static_cast<uint8_t>(words[actuator] >> 8u);
bytes[offset + 2u] = static_cast<uint8_t>(words[actuator] >> 16u);
bytes[offset + 3u] = static_cast<uint8_t>(words[actuator] >> 24u);
}
return bytes;
}
void test_neutral_and_per_actuator_reset() {
constexpr uint32_t neutral = 0x40400100u;
SwitchHapticsDecoder decoder;
auto frame = payload(neutral, neutral);
expect_output("explicit neutral", decoder.decode(frame.data()), 0, 0);
frame = payload(type_2(90, 16, 50, 127), type_2(100, 32, 40, 16));
expect_output("active actuators", decoder.decode(frame.data()), 250, 32);
decoder.reset();
frame = payload(1u << 5u, 1u << 5u);
expect_output("explicit decoder reset", decoder.decode(frame.data()), 0, 0);
frame = payload(type_2(90, 16, 50, 127), type_2(100, 32, 40, 16));
decoder.decode(frame.data());
frame = payload(0, type_2(100, 32, 40, 16));
expect_output("zero resets only left actuator", decoder.decode(frame.data()), 16, 32);
frame = payload(0, neutral);
expect_output("neutral resets right actuator", decoder.decode(frame.data()), 0, 0);
}
void test_type_2_full_state_and_band_mapping() {
constexpr uint32_t neutral = 0x40400100u;
SwitchHapticsDecoder decoder;
const auto frame = payload(type_2(100, 32, 20, 16), neutral);
expect_output("type-2 low/high mapping", decoder.decode(frame.data()), 16, 32);
}
void test_type_1_relative_update_and_idempotence() {
constexpr uint32_t neutral = 0x40400100u;
SwitchHapticsDecoder decoder;
auto frame = payload(type_2(64, 16, 64, 16), neutral);
expect_output("relative update initial state", decoder.decode(frame.data()), 16, 16);
frame = payload(type_1_one_sample(17, 20), neutral);
expect_output("type-1 relative update", decoder.decode(frame.data()), 17, 18);
expect_output("identical delta is idempotent", decoder.decode(frame.data()), 17, 18);
}
void test_subsample_peak_and_repeated_current_state() {
constexpr uint32_t neutral = 0x40400100u;
SwitchHapticsDecoder decoder;
auto frame = payload(type_2(64, 16, 64, 16), neutral);
decoder.decode(frame.data());
frame = payload(type_1_three_samples(17, 17, 29, 29, 24, 24), neutral);
expect_output("peak across three subsamples", decoder.decode(frame.data()), 18, 18);
expect_output("repeat returns final cumulative state", decoder.decode(frame.data()), 16, 16);
}
void test_left_right_peak_combination() {
SwitchHapticsDecoder decoder;
const auto frame = payload(type_2(90, 1, 50, 127), type_2(100, 32, 40, 1));
expect_output("independent actuator band peaks", decoder.decode(frame.data()), 250, 32);
}
void test_type_3_and_type_4_frames() {
constexpr uint32_t neutral = 0x40400100u;
SwitchHapticsDecoder decoder;
auto frame = payload(type_2(64, 16, 64, 16), neutral);
decoder.decode(frame.data());
const uint32_t type3 = (2u << 30u) | 1u | (70u << 1u) |
(24u << 8u) | (17u << 13u) |
(20u << 18u) | (32u << 23u);
frame = payload(type3, neutral);
expect_output("type-3 full plus relative samples", decoder.decode(frame.data()), 18, 32);
const uint32_t type4_low_amplitude = (1u << 30u) | 2u | (32u << 23u);
frame = payload(type4_low_amplitude, neutral);
expect_output("type-4 low amplitude selection", decoder.decode(frame.data()), 32, 32);
const uint32_t type4_high_amplitude = (1u << 30u) | 3u | (127u << 23u);
frame = payload(type4_high_amplitude, neutral);
expect_output("type-4 high amplitude selection", decoder.decode(frame.data()), 32, 250);
}
void test_malformed_and_reserved_words_preserve_state() {
constexpr uint32_t neutral = 0x40400100u;
SwitchHapticsDecoder decoder;
auto frame = payload(type_2(100, 32, 20, 16), neutral);
decoder.decode(frame.data());
frame = payload((1u << 30u) | 1u, neutral);
expect_output("reserved type discriminator", decoder.decode(frame.data()), 16, 32);
frame = payload(1u << 5u, neutral);
expect_output("zero-frame word clears high band", decoder.decode(frame.data()), 16, 0);
}
void test_output_report_normalization() {
const uint8_t stripped[] = {
0x0a,
0x00, 0x01, 0x40, 0x40, 0x00, 0x01, 0x40, 0x40,
};
uint8_t output[64]{};
size_t size = normalize_switch_output_report(0x01, stripped, sizeof(stripped), output);
if (size != sizeof(stripped) + 1 || output[0] != 0x01 ||
output[1] != 0x0a || output[2] != 0x00 || output[9] != 0x40) {
std::cerr << "stripped 0x01 report normalization failed\n";
++failures;
}
size = normalize_switch_output_report(0x10, stripped, sizeof(stripped), output);
if (size != sizeof(stripped) + 1 || output[0] != 0x10 ||
output[1] != 0x0a || output[2] != 0x00 || output[9] != 0x40) {
std::cerr << "stripped 0x10 report normalization failed\n";
++failures;
}
const uint8_t complete[] = {
0x10, 0x0a,
0x00, 0x01, 0x40, 0x40, 0x00, 0x01, 0x40, 0x40,
};
size = normalize_switch_output_report(0, complete, sizeof(complete), output);
if (size != sizeof(complete) || output[0] != 0x10 ||
output[1] != 0x0a || output[9] != 0x40) {
std::cerr << "complete interrupt report normalization failed\n";
++failures;
}
std::array<uint8_t, 64> oversized{};
if (normalize_switch_output_report(0x01, oversized.data(), oversized.size(), output) != 0) {
std::cerr << "oversized stripped report was accepted\n";
++failures;
}
}
} // namespace
int main() {
test_neutral_and_per_actuator_reset();
test_type_2_full_state_and_band_mapping();
test_type_1_relative_update_and_idempotence();
test_subsample_peak_and_repeated_current_state();
test_left_right_peak_combination();
test_type_3_and_type_4_frames();
test_malformed_and_reserved_words_preserve_state();
test_output_report_normalization();
if (failures != 0) {
std::cerr << failures << " haptics test(s) failed\n";
return 1;
}
return 0;
}