Add stock-USB native Joy-Con R/L hub bridge
This commit is contained in:
parent
9f6dddb790
commit
1748910316
41 changed files with 7101 additions and 909 deletions
|
|
@ -1,4 +1,5 @@
|
|||
#include "controller_input.h"
|
||||
#include "model.h"
|
||||
#include "input/bluepad32_input_backend.h"
|
||||
#include "input/switch2_mouse_capture.h"
|
||||
#include "platform/pico/bootsel_pairing_button.h"
|
||||
|
|
@ -23,6 +24,8 @@ void bluepad32_input_backend_open_pairing_window() { ++pairing_requests; }
|
|||
uint32_t bluepad32_input_backend_clear_pairings() { ++clear_requests; return 1; }
|
||||
static const uint8_t source_address[] = {0x98,0xe2,0x55,7,0xdf,0};
|
||||
static const uint8_t other_address[] = {0x98,0xe2,0x55,7,0xdf,1};
|
||||
static constexpr uint16_t other_product_id = SWITCH2_PROBE_JOYCON_LEFT ? 0x2066 : 0x2067;
|
||||
static constexpr uint8_t other_report_id = SWITCH2_PROBE_JOYCON_LEFT ? 8 : 7;
|
||||
void system_clock_initialize() {}
|
||||
void bluepad32_input_backend_init() { stage = 1; }
|
||||
void controller_profile_runtime_reset() {}
|
||||
|
|
@ -42,8 +45,8 @@ static NativeReport native_report(uint8_t counter, uint8_t motion_length,
|
|||
int16_t x = 1, int16_t y = -2) {
|
||||
NativeReport report{};
|
||||
// Deliberately opaque, nonzero bytes, including NFC and reserved fields.
|
||||
// Byte 15 declares 30/40 packed motion bytes at 16..55; do not decode them
|
||||
// or normalize the unused tail of a 30-byte sample.
|
||||
// The model-specific length declares 30/40 packed motion bytes; do not
|
||||
// decode them or normalize the unused tail of a 30-byte sample.
|
||||
for (size_t i = 0; i < report.size(); ++i)
|
||||
report[i] = static_cast<uint8_t>((i * 37 + counter) % 255 + 1);
|
||||
report[0] = counter;
|
||||
|
|
@ -57,12 +60,13 @@ static NativeReport native_report(uint8_t counter, uint8_t motion_length,
|
|||
report[11] = static_cast<uint8_t>(y);
|
||||
report[12] = static_cast<uint16_t>(y) >> 8;
|
||||
report[13] = 0x1b;
|
||||
report[15] = motion_length;
|
||||
report[PROBE_IMU_LENGTH_OFFSET] = motion_length;
|
||||
return report;
|
||||
}
|
||||
|
||||
static void emit(const NativeReport& report, const uint8_t* address = source_address,
|
||||
uint16_t product_id = 0x2066, uint8_t report_id = 8,
|
||||
uint16_t product_id = PROBE_JOYCON_PID,
|
||||
uint8_t report_id = PROBE_NATIVE_REPORT_ID,
|
||||
uint16_t length = 63) {
|
||||
assert(length <= report.size());
|
||||
switch_pico_switch2_mouse_report(product_id, address, report_id, report.data(),
|
||||
|
|
@ -70,33 +74,34 @@ static void emit(const NativeReport& report, const uint8_t* address = source_add
|
|||
}
|
||||
|
||||
static void disconnect(const uint8_t* address = source_address,
|
||||
uint16_t product_id = 0x2066) {
|
||||
uint16_t product_id = PROBE_JOYCON_PID) {
|
||||
switch_pico_switch2_mouse_report(product_id, address, 0, nullptr, 0,
|
||||
static_cast<uint32_t>(now));
|
||||
}
|
||||
|
||||
static probe_controller_input poll(uint32_t timestamp = static_cast<uint32_t>(now)) {
|
||||
static probe_controller_input poll(uint32_t timestamp = static_cast<uint32_t>(now),
|
||||
uint8_t instance = 0) {
|
||||
probe_controller_input input{};
|
||||
probe_controller_input_poll(timestamp, &input);
|
||||
probe_controller_input_poll(instance, timestamp, &input);
|
||||
return input;
|
||||
}
|
||||
|
||||
static uint32_t expect_report(const NativeReport& expected,
|
||||
uint32_t timestamp = static_cast<uint32_t>(now)) {
|
||||
uint32_t timestamp = static_cast<uint32_t>(now),
|
||||
uint8_t instance = 0) {
|
||||
NativeReport actual;
|
||||
actual.fill(0xa5);
|
||||
const uint32_t serial =
|
||||
probe_controller_input_peek_native_report(timestamp, actual.data());
|
||||
probe_controller_input_peek_native_report(instance, timestamp, actual.data());
|
||||
assert(serial != 0 && actual == expected);
|
||||
return serial;
|
||||
}
|
||||
|
||||
static void expect_empty() {
|
||||
static void expect_empty(uint8_t instance = 0) {
|
||||
NativeReport actual;
|
||||
actual.fill(0xa5);
|
||||
const auto untouched = actual;
|
||||
assert(probe_controller_input_peek_native_report(
|
||||
static_cast<uint32_t>(now), actual.data()) == 0);
|
||||
assert(probe_controller_input_peek_native_report(instance, static_cast<uint32_t>(now), actual.data()) == 0);
|
||||
assert(actual == untouched);
|
||||
}
|
||||
|
||||
|
|
@ -111,15 +116,15 @@ static void expect_inactive(const probe_controller_input& input) {
|
|||
static void test_startup_pairing_and_stream_gate() {
|
||||
next_button_event = BootselPairingButtonEvent::kOpenPairing;
|
||||
assert(!probe_controller_input_pairing_task() && button_polls == 0 && pairing_requests == 0);
|
||||
probe_controller_input_set_native_stream(true);
|
||||
probe_controller_input_set_native_stream(0, true);
|
||||
expect_empty();
|
||||
assert(!probe_controller_input_commit_native_report(1));
|
||||
assert(!probe_controller_input_commit_native_report(0, 1));
|
||||
expect_inactive(poll());
|
||||
probe_controller_input_clock_init();
|
||||
probe_controller_input_init();
|
||||
|
||||
// Even an enable request after init must not open the pre-flash-ready gate.
|
||||
probe_controller_input_set_native_stream(true);
|
||||
probe_controller_input_set_native_stream(0, true);
|
||||
const auto report = native_report(0x31, 30, -6, 9);
|
||||
emit(report);
|
||||
expect_empty();
|
||||
|
|
@ -137,21 +142,21 @@ static void test_startup_pairing_and_stream_gate() {
|
|||
assert(input.stick[0] == 0x23 && input.stick[1] == 0x81 && input.stick[2] == 0x45);
|
||||
assert(input.native_status == 0x38 && input.mouse_surface == 0x1b);
|
||||
assert(input.mouse_total_x == -6 && input.mouse_total_y == 9);
|
||||
probe_controller_input_set_native_stream(true);
|
||||
probe_controller_input_set_native_stream(0, true);
|
||||
expect_empty(); // Enabling never replays the latest input or raw ring.
|
||||
emit(report);
|
||||
const uint32_t pending = expect_report(report);
|
||||
probe_controller_input_set_native_stream(false);
|
||||
assert(!probe_controller_input_commit_native_report(pending));
|
||||
probe_controller_input_set_native_stream(0, false);
|
||||
assert(!probe_controller_input_commit_native_report(0, pending));
|
||||
emit(report); // Selected input continues updating while native USB is gated.
|
||||
assert(poll().active);
|
||||
expect_empty();
|
||||
probe_controller_input_set_native_stream(true);
|
||||
probe_controller_input_set_native_stream(0, true);
|
||||
expect_empty();
|
||||
emit(report);
|
||||
const uint32_t resumed = expect_report(report);
|
||||
assert(resumed > pending);
|
||||
assert(probe_controller_input_commit_native_report(resumed));
|
||||
assert(probe_controller_input_commit_native_report(0, resumed));
|
||||
expect_empty();
|
||||
}
|
||||
|
||||
|
|
@ -170,26 +175,26 @@ static void test_opaque_fidelity_order_and_retry() {
|
|||
++now; emit(last);
|
||||
const uint32_t last_serial = poll().serial;
|
||||
assert(last_serial > first_serial);
|
||||
assert(!probe_controller_input_commit_native_report(last_serial));
|
||||
assert(!probe_controller_input_commit_native_report(0));
|
||||
probe_controller_input_set_native_stream(true);
|
||||
assert(!probe_controller_input_commit_native_report(0, last_serial));
|
||||
assert(!probe_controller_input_commit_native_report(0, 0));
|
||||
probe_controller_input_set_native_stream(0, true);
|
||||
assert(expect_report(first) == first_serial);
|
||||
assert(expect_report(first) == first_serial);
|
||||
assert(probe_controller_input_commit_native_report(first_serial));
|
||||
assert(!probe_controller_input_commit_native_report(first_serial));
|
||||
assert(probe_controller_input_commit_native_report(0, first_serial));
|
||||
assert(!probe_controller_input_commit_native_report(0, first_serial));
|
||||
|
||||
const uint32_t second_serial = expect_report(repeated);
|
||||
assert(second_serial > first_serial);
|
||||
assert(probe_controller_input_commit_native_report(second_serial));
|
||||
assert(probe_controller_input_commit_native_report(0, second_serial));
|
||||
const uint32_t third_serial = expect_report(repeated);
|
||||
assert(third_serial > second_serial);
|
||||
assert(!probe_controller_input_commit_native_report(second_serial));
|
||||
assert(!probe_controller_input_commit_native_report(0, second_serial));
|
||||
assert(expect_report(repeated) == third_serial);
|
||||
assert(probe_controller_input_commit_native_report(third_serial));
|
||||
assert(probe_controller_input_commit_native_report(0, third_serial));
|
||||
assert(expect_report(last) == last_serial);
|
||||
assert(probe_controller_input_commit_native_report(last_serial));
|
||||
assert(probe_controller_input_commit_native_report(0, last_serial));
|
||||
expect_empty();
|
||||
assert(!probe_controller_input_commit_native_report(last_serial));
|
||||
assert(!probe_controller_input_commit_native_report(0, last_serial));
|
||||
expect_empty(); // No cached duplicate report when the source has not advanced.
|
||||
}
|
||||
|
||||
|
|
@ -203,17 +208,20 @@ static void test_selected_source_isolation_and_reconnect() {
|
|||
const uint32_t first_serial = expect_report(first);
|
||||
assert(first_serial == selected.serial);
|
||||
for (unsigned i = 0; i < 30; ++i) emit(unrelated, other_address);
|
||||
emit(unrelated, source_address, 0x2067); // Left Joy-Con at the same address.
|
||||
emit(unrelated, source_address, 0x2066, 5);
|
||||
emit(unrelated, source_address, 0x2066, 0xc0, 12);
|
||||
emit(unrelated, source_address, 0x2066, 8, 62);
|
||||
emit(unrelated, source_address, other_product_id, other_report_id);
|
||||
emit(unrelated, source_address, PROBE_JOYCON_PID, other_report_id);
|
||||
emit(unrelated, source_address, PROBE_JOYCON_PID, 5);
|
||||
emit(unrelated, source_address, PROBE_JOYCON_PID, 0xc0, 12);
|
||||
emit(unrelated, source_address, PROBE_JOYCON_PID, PROBE_NATIVE_REPORT_ID, 62);
|
||||
emit(unrelated, source_address, PROBE_JOYCON_PID, 0, 1); // Not a teardown.
|
||||
uint8_t oversized[64];
|
||||
memcpy(oversized, unrelated.data(), unrelated.size());
|
||||
oversized[63] = 0x5a;
|
||||
switch_pico_switch2_mouse_report(0x2066, source_address, 8, oversized,
|
||||
switch_pico_switch2_mouse_report(PROBE_JOYCON_PID, source_address,
|
||||
PROBE_NATIVE_REPORT_ID, oversized,
|
||||
sizeof(oversized), static_cast<uint32_t>(now));
|
||||
disconnect(other_address);
|
||||
disconnect(source_address, 0x2067);
|
||||
disconnect(source_address, other_product_id);
|
||||
const auto isolated = poll();
|
||||
assert(isolated.active && isolated.serial == selected.serial);
|
||||
assert(isolated.mouse_epoch == selected.mouse_epoch);
|
||||
|
|
@ -222,9 +230,9 @@ static void test_selected_source_isolation_and_reconnect() {
|
|||
assert(expect_report(first) == first_serial);
|
||||
++now; emit(second);
|
||||
const uint32_t second_serial = poll().serial;
|
||||
assert(probe_controller_input_commit_native_report(first_serial));
|
||||
assert(probe_controller_input_commit_native_report(0, first_serial));
|
||||
assert(expect_report(second) == second_serial);
|
||||
assert(probe_controller_input_commit_native_report(second_serial));
|
||||
assert(probe_controller_input_commit_native_report(0, second_serial));
|
||||
expect_empty(); // Unrelated ring entries neither evict nor enter the FIFO.
|
||||
|
||||
emit(first);
|
||||
|
|
@ -234,10 +242,10 @@ static void test_selected_source_isolation_and_reconnect() {
|
|||
const auto reconnected = poll();
|
||||
assert(reconnected.active && reconnected.mouse_epoch != selected.mouse_epoch);
|
||||
assert(reconnected.mouse_total_x == 31 && reconnected.mouse_total_y == -37);
|
||||
assert(!probe_controller_input_commit_native_report(disconnected_serial));
|
||||
assert(!probe_controller_input_commit_native_report(0, disconnected_serial));
|
||||
assert(expect_report(second) == reconnected.serial);
|
||||
assert(reconnected.serial > disconnected_serial);
|
||||
assert(probe_controller_input_commit_native_report(reconnected.serial));
|
||||
assert(probe_controller_input_commit_native_report(0, reconnected.serial));
|
||||
expect_empty();
|
||||
|
||||
emit(first);
|
||||
|
|
@ -246,35 +254,133 @@ static void test_selected_source_isolation_and_reconnect() {
|
|||
for (unsigned i = 0; i < 30; ++i) emit(unrelated, other_address);
|
||||
expect_inactive(poll());
|
||||
expect_empty();
|
||||
assert(!probe_controller_input_commit_native_report(pending));
|
||||
assert(!probe_controller_input_commit_native_report(0, pending));
|
||||
++now; emit(second);
|
||||
const auto resumed = poll();
|
||||
assert(resumed.active && resumed.mouse_epoch != reconnected.mouse_epoch);
|
||||
const uint32_t resumed_serial = expect_report(second);
|
||||
assert(resumed_serial > pending);
|
||||
assert(probe_controller_input_commit_native_report(resumed_serial));
|
||||
assert(probe_controller_input_commit_native_report(0, resumed_serial));
|
||||
|
||||
emit(first);
|
||||
const uint32_t old_source = expect_report(first);
|
||||
emit(unrelated, other_address);
|
||||
switch2_mouse_capture_select_input(other_address);
|
||||
switch2_mouse_capture_select_input(0, other_address, PROBE_JOYCON_PID);
|
||||
expect_inactive(poll());
|
||||
expect_empty();
|
||||
assert(!probe_controller_input_commit_native_report(old_source));
|
||||
probe_controller_input_set_native_stream(true);
|
||||
assert(!probe_controller_input_commit_native_report(0, old_source));
|
||||
probe_controller_input_set_native_stream(0, true);
|
||||
expect_empty(); // Selection cannot revive the other peer's raw history.
|
||||
emit(first);
|
||||
expect_empty();
|
||||
emit(unrelated, other_address);
|
||||
const uint32_t new_source = expect_report(unrelated);
|
||||
assert(new_source > old_source);
|
||||
switch2_mouse_capture_select_input(source_address);
|
||||
probe_controller_input_set_native_stream(true);
|
||||
switch2_mouse_capture_select_input(0, source_address, PROBE_JOYCON_PID);
|
||||
expect_inactive(poll());
|
||||
probe_controller_input_set_native_stream(0, true);
|
||||
expect_empty();
|
||||
assert(!probe_controller_input_commit_native_report(new_source));
|
||||
assert(!probe_controller_input_commit_native_report(0, new_source));
|
||||
emit(second);
|
||||
const uint32_t restored = expect_report(second);
|
||||
assert(restored > new_source);
|
||||
assert(probe_controller_input_commit_native_report(restored));
|
||||
assert(probe_controller_input_commit_native_report(0, restored));
|
||||
}
|
||||
|
||||
static void test_side_switch_and_sample_ownership() {
|
||||
now = 500;
|
||||
const auto first = native_report(0x71, 30);
|
||||
auto opposite = native_report(0x72, 40);
|
||||
opposite[SWITCH2_PROBE_JOYCON_LEFT ? 15 : 14] = 40;
|
||||
emit(first);
|
||||
const uint32_t old_packet = expect_report(first);
|
||||
assert(poll().active);
|
||||
uint64_t old_cue = 0;
|
||||
assert(probe_controller_input_play_sample(0, 3, &old_cue) && old_cue != 0);
|
||||
uint64_t taken = 0;
|
||||
uint8_t sample = 0;
|
||||
// Unrelated callers cannot take a cue, even with a timestamp that would
|
||||
// otherwise expire it. Ownership is checked before mutating its lifetime.
|
||||
assert(!switch_pico_switch2_sample_take(other_product_id, source_address,
|
||||
now + 2000, &sample, &taken));
|
||||
assert(!switch_pico_switch2_sample_take(PROBE_JOYCON_PID, other_address,
|
||||
now + 2000, &sample, &taken));
|
||||
assert(probe_controller_input_sample_result(0, old_cue, now) == 0);
|
||||
assert(!switch_pico_switch2_sample_result(PROBE_JOYCON_PID, source_address,
|
||||
old_cue, 1, now)); // Not dispatched.
|
||||
assert(switch_pico_switch2_sample_take(PROBE_JOYCON_PID, source_address,
|
||||
now, &sample, &taken));
|
||||
assert(taken == old_cue && sample == 3);
|
||||
switch2_mouse_capture_select_input(0, source_address, 0x2069); // Invalid PID.
|
||||
switch2_mouse_capture_select_input(0, nullptr, PROBE_JOYCON_PID);
|
||||
assert(expect_report(first) == old_packet && poll().active);
|
||||
assert(probe_controller_input_sample_result(0, old_cue, now) == 0);
|
||||
|
||||
// Same address, different side is still a new source. Native and cue
|
||||
// tokens from the prior selection cannot acknowledge or consume it.
|
||||
switch2_mouse_capture_select_input(0, source_address, other_product_id);
|
||||
expect_empty();
|
||||
expect_inactive(poll());
|
||||
assert(!probe_controller_input_commit_native_report(0, old_packet));
|
||||
assert(probe_controller_input_sample_result(0, old_cue, now) == -1);
|
||||
uint64_t new_cue = 0;
|
||||
assert(!probe_controller_input_play_sample(0, 4, &new_cue));
|
||||
emit(first);
|
||||
emit(opposite, source_address, other_product_id, PROBE_NATIVE_REPORT_ID);
|
||||
expect_inactive(poll());
|
||||
emit(opposite, source_address, other_product_id, other_report_id);
|
||||
assert(poll().active);
|
||||
expect_empty(); // Selection disabled native output even for valid input.
|
||||
probe_controller_input_set_native_stream(0, true);
|
||||
expect_empty();
|
||||
emit(opposite, source_address, other_product_id, other_report_id);
|
||||
const uint32_t new_packet = expect_report(opposite);
|
||||
assert(new_packet > old_packet);
|
||||
assert(probe_controller_input_play_sample(0, 4, &new_cue) && new_cue > old_cue);
|
||||
assert(!switch_pico_switch2_sample_result(PROBE_JOYCON_PID, source_address,
|
||||
old_cue, 1, now + 2000));
|
||||
assert(switch_pico_switch2_sample_take(other_product_id, source_address,
|
||||
now, &sample, &taken));
|
||||
assert(sample == 4 && taken == new_cue);
|
||||
assert(!switch_pico_switch2_sample_result(PROBE_JOYCON_PID, source_address,
|
||||
new_cue, 1, now + 2000));
|
||||
assert(!switch_pico_switch2_sample_result(other_product_id, other_address,
|
||||
new_cue, -1, now + 2000));
|
||||
assert(!switch_pico_switch2_sample_result(other_product_id, source_address,
|
||||
old_cue, 1, now + 2000));
|
||||
assert(probe_controller_input_sample_result(0, old_cue, now + 2000) == -1);
|
||||
disconnect(source_address);
|
||||
assert(poll().active && expect_report(opposite) == new_packet);
|
||||
assert(probe_controller_input_sample_result(0, new_cue, now) == 0);
|
||||
assert(switch_pico_switch2_sample_result(other_product_id, source_address,
|
||||
new_cue, 1, now));
|
||||
disconnect(source_address, other_product_id);
|
||||
expect_inactive(poll());
|
||||
expect_empty();
|
||||
assert(!probe_controller_input_commit_native_report(0, new_packet));
|
||||
assert(probe_controller_input_sample_result(0, new_cue, now) == -1);
|
||||
|
||||
emit(opposite, source_address, other_product_id, other_report_id);
|
||||
assert(probe_controller_input_play_sample(0, 5, &new_cue) && new_cue > old_cue);
|
||||
old_cue = new_cue;
|
||||
switch2_mouse_capture_select_input(0, other_address, other_product_id);
|
||||
expect_inactive(poll());
|
||||
assert(probe_controller_input_sample_result(0, old_cue, now) == -1);
|
||||
assert(!probe_controller_input_play_sample(0, 6, &new_cue));
|
||||
emit(opposite, other_address, other_product_id, other_report_id);
|
||||
assert(probe_controller_input_play_sample(0, 6, &new_cue) && new_cue > old_cue);
|
||||
assert(switch_pico_switch2_sample_take(other_product_id, other_address,
|
||||
now, &sample, &taken));
|
||||
assert(taken == new_cue && sample == 6);
|
||||
assert(!switch_pico_switch2_sample_result(other_product_id, source_address,
|
||||
old_cue, 1, now));
|
||||
assert(switch_pico_switch2_sample_result(other_product_id, other_address,
|
||||
new_cue, 1, now));
|
||||
assert(probe_controller_input_sample_result(0, new_cue, now) == 1);
|
||||
assert(probe_controller_input_sample_result(0, new_cue, now) == -1);
|
||||
switch2_mouse_capture_select_input(0, source_address, PROBE_JOYCON_PID);
|
||||
probe_controller_input_set_native_stream(0, true);
|
||||
expect_empty();
|
||||
}
|
||||
|
||||
static void test_bounded_overflow() {
|
||||
|
|
@ -290,16 +396,16 @@ static void test_bounded_overflow() {
|
|||
assert(expect_report(first) == old_serial);
|
||||
const auto newest = native_report(0xbb, 30, -101, 103);
|
||||
++now; emit(newest);
|
||||
assert(!probe_controller_input_commit_native_report(old_serial));
|
||||
assert(!probe_controller_input_commit_native_report(0, old_serial));
|
||||
const uint32_t newest_serial = expect_report(newest);
|
||||
assert(newest_serial > old_serial);
|
||||
const auto following = native_report(0xbc, 40, 107, -109);
|
||||
++now; emit(following);
|
||||
assert(expect_report(newest) == newest_serial);
|
||||
assert(probe_controller_input_commit_native_report(newest_serial));
|
||||
assert(probe_controller_input_commit_native_report(0, newest_serial));
|
||||
const uint32_t following_serial = expect_report(following);
|
||||
assert(following_serial > newest_serial);
|
||||
assert(probe_controller_input_commit_native_report(following_serial));
|
||||
assert(probe_controller_input_commit_native_report(0, following_serial));
|
||||
expect_empty(); // Overflow discarded all prior history, not merely its head.
|
||||
}
|
||||
|
||||
|
|
@ -315,7 +421,7 @@ static void test_expiry_and_wrapping_clock() {
|
|||
emit(fresh, other_address); // Wrong-source traffic cannot refresh the timeout.
|
||||
expect_inactive(poll());
|
||||
expect_empty();
|
||||
assert(!probe_controller_input_commit_native_report(expired));
|
||||
assert(!probe_controller_input_commit_native_report(0, expired));
|
||||
|
||||
++now; emit(first);
|
||||
const uint32_t stale_head = expect_report(first);
|
||||
|
|
@ -324,11 +430,11 @@ static void test_expiry_and_wrapping_clock() {
|
|||
++now;
|
||||
assert(poll().active); // Latest source is fresh, but its queued head is not.
|
||||
expect_empty();
|
||||
assert(!probe_controller_input_commit_native_report(stale_head));
|
||||
assert(!probe_controller_input_commit_native_report(0, stale_head));
|
||||
emit(fresh);
|
||||
const uint32_t resumed = expect_report(fresh);
|
||||
assert(resumed > stale_head);
|
||||
assert(probe_controller_input_commit_native_report(resumed));
|
||||
assert(probe_controller_input_commit_native_report(0, resumed));
|
||||
expect_empty();
|
||||
|
||||
now = static_cast<uint64_t>(UINT32_MAX) - 100;
|
||||
|
|
@ -344,20 +450,139 @@ static void test_expiry_and_wrapping_clock() {
|
|||
++now;
|
||||
expect_inactive(poll());
|
||||
expect_empty();
|
||||
assert(!probe_controller_input_commit_native_report(wrapped));
|
||||
assert(!probe_controller_input_commit_native_report(0, wrapped));
|
||||
++now; emit(fresh);
|
||||
assert(poll().active);
|
||||
const uint32_t after_wrap = expect_report(fresh);
|
||||
assert(after_wrap > wrapped);
|
||||
assert(probe_controller_input_commit_native_report(after_wrap));
|
||||
assert(probe_controller_input_commit_native_report(0, after_wrap));
|
||||
expect_empty();
|
||||
}
|
||||
|
||||
#if SWITCH2_PROBE_COMPOSITE
|
||||
static void test_simultaneous_sources() {
|
||||
const uint8_t left_address[] = {0x98,0xe2,0x55,7,0xe9,0xd3};
|
||||
now = 10000;
|
||||
disconnect();
|
||||
const auto right = native_report(0x31, 30, 7, -9);
|
||||
auto left = native_report(0x62, 40, -13, 17);
|
||||
left[probe_model_imu_length_offset(1)] = 40;
|
||||
probe_controller_input_set_native_stream(0, true);
|
||||
probe_controller_input_set_native_stream(1, true);
|
||||
expect_empty(0);
|
||||
expect_empty(1);
|
||||
emit(right);
|
||||
const uint32_t r0 = expect_report(right);
|
||||
emit(left, left_address, probe_model_pid(1), probe_model_report_id(1));
|
||||
const uint32_t l0 = expect_report(left, now, 1);
|
||||
emit(right);
|
||||
const uint32_t r1 = poll().serial;
|
||||
emit(left, left_address, probe_model_pid(1), probe_model_report_id(1));
|
||||
const auto li = poll(now, 1);
|
||||
const auto ri = poll();
|
||||
assert(r0 < l0 && l0 < r1 && r1 < li.serial);
|
||||
assert(ri.mouse_epoch == r0 && li.mouse_epoch == l0);
|
||||
assert(ri.mouse_total_x == 14 && ri.mouse_total_y == -18);
|
||||
assert(li.mouse_total_x == -26 && li.mouse_total_y == 34);
|
||||
// R is already owned: selecting it for L must not duplicate or steal it.
|
||||
switch2_mouse_capture_select_input(1, source_address, probe_model_pid(0));
|
||||
assert(expect_report(right) == r0);
|
||||
assert(expect_report(left, now, 1) == l0);
|
||||
assert(!probe_controller_input_commit_native_report(1, r0));
|
||||
assert(!probe_controller_input_commit_native_report(0, l0));
|
||||
assert(probe_controller_input_commit_native_report(1, l0));
|
||||
assert(expect_report(left, now, 1) == li.serial);
|
||||
assert(probe_controller_input_commit_native_report(1, li.serial));
|
||||
expect_empty(1);
|
||||
assert(expect_report(right) == r0); // L consumption never moves stalled R.
|
||||
|
||||
// R overflow drops only its own backlog; L's retry remains byte-identical.
|
||||
emit(left, left_address, probe_model_pid(1), probe_model_report_id(1));
|
||||
const uint32_t left_retry = expect_report(left, now, 1);
|
||||
for (unsigned i = 0; i < 31; ++i) emit(right);
|
||||
assert(!probe_controller_input_commit_native_report(0, r0));
|
||||
assert(expect_report(left, now, 1) == left_retry);
|
||||
const uint32_t r2 = expect_report(right);
|
||||
assert(r2 > left_retry);
|
||||
|
||||
uint64_t rcue, lcue, taken;
|
||||
uint8_t sample;
|
||||
assert(probe_controller_input_play_sample(0, 3, &rcue));
|
||||
assert(probe_controller_input_play_sample(1, 5, &lcue) && lcue > rcue);
|
||||
assert(switch_pico_switch2_sample_take(probe_model_pid(0), source_address, now, &sample, &taken));
|
||||
assert(sample == 3 && taken == rcue);
|
||||
assert(switch_pico_switch2_sample_take(probe_model_pid(1), left_address, now, &sample, &taken));
|
||||
assert(sample == 5 && taken == lcue);
|
||||
assert(!switch_pico_switch2_sample_result(probe_model_pid(1), left_address, rcue, 1, now + 2000));
|
||||
assert(!switch_pico_switch2_sample_result(probe_model_pid(0), source_address, lcue, -1, now + 2000));
|
||||
assert(probe_controller_input_sample_result(0, lcue, now + 2000) == -1);
|
||||
assert(probe_controller_input_sample_result(1, rcue, now + 2000) == -1);
|
||||
assert(probe_controller_input_sample_result(0, rcue, now) == 0);
|
||||
assert(probe_controller_input_sample_result(1, lcue, now) == 0);
|
||||
|
||||
probe_controller_input_set_native_stream(0, false);
|
||||
probe_controller_input_cancel_sample(0);
|
||||
assert(probe_controller_input_sample_result(0, rcue, now) == -1);
|
||||
assert(probe_controller_input_sample_result(1, lcue, now) == 0);
|
||||
assert(expect_report(left, now, 1) == left_retry);
|
||||
assert(switch_pico_switch2_sample_result(probe_model_pid(1), left_address, lcue, 1, now));
|
||||
assert(probe_controller_input_sample_result(1, lcue, now) == 1);
|
||||
assert(probe_controller_input_sample_result(1, lcue, now) == -1);
|
||||
|
||||
// R selection/disconnect cannot revoke L's pending packet or cue.
|
||||
assert(probe_controller_input_play_sample(1, 6, &lcue));
|
||||
switch2_mouse_capture_select_input(0, other_address, probe_model_pid(0));
|
||||
disconnect(source_address, probe_model_pid(0));
|
||||
assert(expect_report(left, now, 1) == left_retry);
|
||||
assert(probe_controller_input_sample_result(1, lcue, now) == 0);
|
||||
assert(poll(now, 1).mouse_epoch == l0);
|
||||
switch2_mouse_capture_select_input(0, source_address, probe_model_pid(0));
|
||||
probe_controller_input_set_native_stream(0, true);
|
||||
emit(right);
|
||||
const uint32_t right_retry = expect_report(right);
|
||||
assert(probe_controller_input_play_sample(0, 7, &rcue) && rcue > lcue);
|
||||
disconnect(left_address, probe_model_pid(1));
|
||||
expect_empty(1);
|
||||
expect_inactive(poll(now, 1));
|
||||
assert(probe_controller_input_sample_result(1, lcue, now) == -1);
|
||||
assert(expect_report(right) == right_retry);
|
||||
assert(probe_controller_input_sample_result(0, rcue, now) == 0);
|
||||
assert(switch_pico_switch2_sample_take(probe_model_pid(0), source_address, now, &sample, &taken));
|
||||
assert(sample == 7 && taken == rcue);
|
||||
assert(switch_pico_switch2_sample_result(probe_model_pid(0), source_address, rcue, 1, now));
|
||||
assert(probe_controller_input_sample_result(0, rcue, now) == 1);
|
||||
|
||||
// Reconnection creates a distinct L epoch and does not replay its old queue.
|
||||
emit(left, left_address, probe_model_pid(1), probe_model_report_id(1));
|
||||
const auto resumed_left = poll(now, 1);
|
||||
assert(resumed_left.mouse_epoch != l0 && resumed_left.mouse_total_x == -13);
|
||||
assert(!probe_controller_input_commit_native_report(1, left_retry));
|
||||
assert(probe_controller_input_commit_native_report(1, expect_report(left, now, 1)));
|
||||
assert(probe_controller_input_commit_native_report(0, right_retry));
|
||||
expect_empty(0);
|
||||
expect_empty(1);
|
||||
|
||||
// A refreshed L packet cannot refresh R's independent source deadline.
|
||||
now += 499;
|
||||
emit(left, left_address, probe_model_pid(1), probe_model_report_id(1));
|
||||
++now;
|
||||
expect_inactive(poll());
|
||||
assert(poll(now, 1).active);
|
||||
assert(probe_controller_input_commit_native_report(1, expect_report(left, now, 1)));
|
||||
expect_empty(0);
|
||||
expect_empty(1);
|
||||
}
|
||||
#endif
|
||||
|
||||
int main() {
|
||||
test_startup_pairing_and_stream_gate();
|
||||
test_opaque_fidelity_order_and_retry();
|
||||
test_selected_source_isolation_and_reconnect();
|
||||
test_side_switch_and_sample_ownership();
|
||||
test_bounded_overflow();
|
||||
test_expiry_and_wrapping_clock();
|
||||
#if SWITCH2_PROBE_COMPOSITE
|
||||
test_simultaneous_sources();
|
||||
#endif
|
||||
puts("Native packet fidelity, FIFO retry/order, source barriers, overflow, expiry and pairing passed");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -167,7 +167,7 @@ void uni_hid_device_set_ready(uni_hid_device_t* d) {
|
|||
uni_hid_parser_switch2_setup(d);
|
||||
}
|
||||
bool uni_hid_device_set_ready_complete(uni_hid_device_t* d) {
|
||||
assert(d->conn.connected && connected_events == 1);
|
||||
assert(d->conn.connected && connected_events > ready);
|
||||
++ready;
|
||||
d->conn.state = UNI_BT_CONN_STATE_DEVICE_READY;
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -2,35 +2,286 @@
|
|||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#include "protocol.h"
|
||||
#include "descriptors.h"
|
||||
#include "memory.h"
|
||||
|
||||
static const uint32_t common_button_bits[2][16] = {
|
||||
{
|
||||
0x000004, 0x000008, 0x000001, 0x000002, 0x000040, 0x000080, 0x000200, 0x000400,
|
||||
0x001000, 0, 0, 0, 0x004000, 0, 0x000010, 0x000020,
|
||||
},
|
||||
{
|
||||
0x010000, 0x040000, 0x080000, 0x020000, 0x400000, 0x800000, 0x000100, 0x000800,
|
||||
0x002000, 0, 0, 0, 0, 0, 0x100000, 0x200000,
|
||||
},
|
||||
};
|
||||
|
||||
static void initialize(probe_protocol_state* state) {
|
||||
static const uint8_t command[] = {
|
||||
0x03, 0x91, 0, 0x0d, 0, 8, 0, 0, 1, 0, 1, 2, 3, 4, 5, 6,
|
||||
};
|
||||
uint8_t reply[12];
|
||||
assert(probe_protocol_command(state, command, sizeof(command), reply, sizeof(reply), NULL) == 12);
|
||||
}
|
||||
|
||||
static void set_features(probe_protocol_state* state, uint8_t subcommand, uint8_t flags) {
|
||||
const uint8_t command[] = {0x0c, 0x91, 0, subcommand, 0, 4, 0, 0, flags, 0, 0, 0};
|
||||
uint8_t reply[12];
|
||||
assert(probe_protocol_command(state, command, sizeof(command), reply, sizeof(reply), NULL) == 12);
|
||||
}
|
||||
|
||||
static void select_report(probe_protocol_state* state, uint8_t report_id) {
|
||||
const uint8_t command[] = {0x03, 0x91, 0, 0x0a, 0, 4, 0, 0, report_id, 0, 0, 0};
|
||||
uint8_t reply[8];
|
||||
assert(probe_protocol_command(state, command, sizeof(command), reply, sizeof(reply), NULL) == 8);
|
||||
}
|
||||
|
||||
static void test_descriptors(void) {
|
||||
const uint16_t product_id = probe_device_descriptor[10] |
|
||||
((uint16_t)probe_device_descriptor[11] << 8);
|
||||
assert(product_id == (SWITCH2_PROBE_JOYCON_LEFT ? 0x2067 : 0x2066));
|
||||
assert(probe_configuration_descriptor[2] == sizeof(probe_configuration_descriptor));
|
||||
assert(probe_configuration_descriptor[4] == 2 * PROBE_CONTROLLER_COUNT);
|
||||
unsigned interface_count = 0, endpoint_count = 0;
|
||||
unsigned interface = 0, seen_endpoints = 0;
|
||||
for (size_t offset = 9; offset < sizeof(probe_configuration_descriptor);) {
|
||||
const uint8_t* descriptor = probe_configuration_descriptor + offset;
|
||||
assert(descriptor[0] >= 2);
|
||||
assert(offset + descriptor[0] <= sizeof(probe_configuration_descriptor));
|
||||
if (descriptor[1] == 4) {
|
||||
assert(descriptor[0] == 9);
|
||||
interface = descriptor[2];
|
||||
assert(interface == interface_count++);
|
||||
assert(descriptor[4] == 2);
|
||||
assert(descriptor[5] == (interface % 2 ? 0xff : 3));
|
||||
assert(descriptor[8] == 5 + interface);
|
||||
} else if (descriptor[1] == 5) {
|
||||
assert(descriptor[0] == 7);
|
||||
const unsigned endpoint = descriptor[2] & 0x0f;
|
||||
assert(endpoint == interface + 1);
|
||||
const unsigned bit = endpoint + ((descriptor[2] & 0x80) ? 8 : 0);
|
||||
assert(!(seen_endpoints & (1u << bit)));
|
||||
seen_endpoints |= 1u << bit;
|
||||
assert(descriptor[3] == (interface % 2 ? 2 : 3));
|
||||
++endpoint_count;
|
||||
}
|
||||
offset += descriptor[0];
|
||||
}
|
||||
assert(interface_count == 2 * PROBE_CONTROLLER_COUNT);
|
||||
assert(endpoint_count == 4 * PROBE_CONTROLLER_COUNT);
|
||||
// Read HID short items as a host would: each function advertises only its
|
||||
// own native report plus common 05, with sizes matching report generation.
|
||||
for (uint8_t instance = 0; instance < PROBE_CONTROLLER_COUNT; ++instance) {
|
||||
const uint8_t* descriptor = probe_hid_report_descriptors[instance];
|
||||
unsigned input_bits[256] = {0}, output_bits[256] = {0};
|
||||
unsigned report_id = 0, report_size = 0, report_count = 0;
|
||||
for (size_t offset = 0; offset < sizeof(probe_hid_report_descriptors[instance]);) {
|
||||
const uint8_t prefix = descriptor[offset++];
|
||||
assert(prefix != 0xfe);
|
||||
const unsigned size = (prefix & 3) == 3 ? 4 : prefix & 3;
|
||||
assert(offset + size <= sizeof(probe_hid_report_descriptors[instance]));
|
||||
uint32_t value = 0;
|
||||
for (unsigned i = 0; i < size; ++i)
|
||||
value |= (uint32_t)descriptor[offset++] << (8 * i);
|
||||
switch (prefix & 0xfc) {
|
||||
case 0x74: report_size = value; break;
|
||||
case 0x94: report_count = value; break;
|
||||
case 0x84: report_id = value; assert(report_id < 256); break;
|
||||
case 0x80: input_bits[report_id] += report_size * report_count; break;
|
||||
case 0x90: output_bits[report_id] += report_size * report_count; break;
|
||||
}
|
||||
}
|
||||
const bool is_left = SWITCH2_PROBE_COMPOSITE ? instance == 1 : SWITCH2_PROBE_JOYCON_LEFT;
|
||||
probe_protocol_state state;
|
||||
probe_protocol_reset(&state, is_left);
|
||||
initialize(&state);
|
||||
uint8_t report[PROBE_INPUT_SIZE];
|
||||
for (unsigned id = 0; id < 256; ++id) {
|
||||
const size_t expected = (id == 5 || id == (is_left ? 7u : 8u)) ? sizeof(report) : 0;
|
||||
assert(input_bits[id] == expected * 8u);
|
||||
assert(probe_protocol_report(&state, (uint8_t)id, report, sizeof(report)) == expected);
|
||||
assert(output_bits[id] == (id == 1 ? 63u * 8u : 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void test_report_selection_and_reset(bool is_left) {
|
||||
const uint8_t native_id = is_left ? 7 : 8, opposite_id = is_left ? 8 : 7;
|
||||
probe_protocol_state state;
|
||||
probe_protocol_reset(&state, is_left);
|
||||
uint8_t report[PROBE_INPUT_SIZE];
|
||||
assert(probe_protocol_report(&state, native_id, report, sizeof(report)) == 0);
|
||||
initialize(&state);
|
||||
assert(state.report_id == native_id);
|
||||
assert(probe_protocol_report(&state, state.report_id, report, sizeof(report)) == sizeof(report));
|
||||
select_report(&state, 5);
|
||||
assert(state.report_id == 5);
|
||||
select_report(&state, opposite_id);
|
||||
assert(state.report_id == 5); // Unsupported IDs are ACKed but ignored.
|
||||
memset(report, 0xa5, sizeof(report));
|
||||
assert(probe_protocol_report(&state, opposite_id, report, sizeof(report)) == 0);
|
||||
for (size_t i = 0; i < sizeof(report); ++i) assert(report[i] == 0xa5);
|
||||
select_report(&state, native_id);
|
||||
assert(state.report_id == native_id);
|
||||
assert(probe_protocol_report(&state, native_id, report, sizeof(report) - 1) == 0);
|
||||
state.report_counter = 0x12345678;
|
||||
initialize(&state); // Repeated USB initialization must not rewind a live stream.
|
||||
assert(probe_protocol_report(&state, native_id, report, sizeof(report)) == sizeof(report));
|
||||
assert(report[0] == 0x78);
|
||||
select_report(&state, 5);
|
||||
probe_protocol_reset(&state, is_left);
|
||||
assert(probe_protocol_report(&state, native_id, report, sizeof(report)) == 0);
|
||||
initialize(&state);
|
||||
assert(state.report_id == native_id);
|
||||
assert(probe_protocol_report(&state, state.report_id, report, sizeof(report)) == sizeof(report));
|
||||
assert(report[0] == 0);
|
||||
}
|
||||
|
||||
static void test_buttons_stick_and_feature_control(bool is_left) {
|
||||
const uint8_t native_id = is_left ? 7 : 8;
|
||||
const unsigned common_stick_offset = is_left ? 10 : 13;
|
||||
const unsigned absent_stick_offset = is_left ? 13 : 10;
|
||||
const unsigned common_rail_offset = is_left ? 6 : 4;
|
||||
probe_protocol_state state;
|
||||
probe_protocol_reset(&state, is_left);
|
||||
initialize(&state);
|
||||
set_features(&state, 2, 3);
|
||||
set_features(&state, 4, 3);
|
||||
state.controller_active = true;
|
||||
const uint8_t stick[] = {0x23, 0x61, 0x45};
|
||||
const uint8_t calibrated_center[] = {0xff, 0x47, 0x81};
|
||||
const uint8_t neutral[] = {0, 8, 0x80};
|
||||
memcpy(state.controller_stick, stick, sizeof(stick));
|
||||
memcpy(state.stick_center, calibrated_center, sizeof(calibrated_center));
|
||||
uint8_t native[PROBE_INPUT_SIZE], common[PROBE_INPUT_SIZE];
|
||||
for (unsigned bit = 0; bit < 16; ++bit) {
|
||||
memset(state.controller_buttons, 0, sizeof(state.controller_buttons));
|
||||
state.controller_buttons[bit / 8] = (uint8_t)(1u << (bit % 8));
|
||||
assert(probe_protocol_report(&state, native_id, native, sizeof(native)) == sizeof(native));
|
||||
assert(probe_protocol_report(&state, 5, common, sizeof(common)) == sizeof(common));
|
||||
const uint16_t expected_native = common_button_bits[is_left][bit] ? (uint16_t)(1u << bit) : 0;
|
||||
assert((uint16_t)(native[2] | ((uint16_t)native[3] << 8)) == expected_native);
|
||||
for (unsigned byte = 0; byte < 4; ++byte)
|
||||
assert(common[4 + byte] == (uint8_t)(common_button_bits[is_left][bit] >> (8 * byte)));
|
||||
assert(memcmp(native + 5, stick, sizeof(stick)) == 0);
|
||||
assert(memcmp(common + common_stick_offset, stick, sizeof(stick)) == 0);
|
||||
assert(memcmp(common + absent_stick_offset, neutral, sizeof(neutral)) == 0);
|
||||
}
|
||||
// Host feature disable gates the live controls without losing calibration.
|
||||
set_features(&state, 5, 3);
|
||||
assert(probe_protocol_report(&state, native_id, native, sizeof(native)) == sizeof(native));
|
||||
assert(native[2] == 0 && native[3] == 0);
|
||||
assert(memcmp(native + 5, calibrated_center, sizeof(calibrated_center)) == 0);
|
||||
assert(probe_protocol_report(&state, 5, common, sizeof(common)) == sizeof(common));
|
||||
assert(common[4] == 0 && common[5] == 0 && common[6] == 0 && common[7] == 0);
|
||||
assert(memcmp(common + common_stick_offset, calibrated_center, sizeof(calibrated_center)) == 0);
|
||||
set_features(&state, 4, 3);
|
||||
state.controller_active = false;
|
||||
assert(probe_protocol_report(&state, native_id, native, sizeof(native)) == sizeof(native));
|
||||
assert(native[2] == 0 && native[3] == 0);
|
||||
assert(memcmp(native + 5, calibrated_center, sizeof(calibrated_center)) == 0);
|
||||
state.test_rail_buttons = true;
|
||||
assert(probe_protocol_report(&state, native_id, native, sizeof(native)) == sizeof(native));
|
||||
assert(native[2] == 0 && native[3] == 0xc0);
|
||||
assert(probe_protocol_report(&state, 5, common, sizeof(common)) == sizeof(common));
|
||||
assert(common[common_rail_offset] == 0x30);
|
||||
set_features(&state, 5, 1);
|
||||
assert(probe_protocol_report(&state, native_id, native, sizeof(native)) == sizeof(native));
|
||||
assert(native[3] == 0);
|
||||
}
|
||||
|
||||
static void test_opaque_native_feature_gates(bool is_left) {
|
||||
const unsigned imu_length_offset = is_left ? 14 : 15;
|
||||
#ifdef SWITCH2_PROBE_ZERO_NATIVE_IMU_PAYLOAD
|
||||
const unsigned imu_data_offset = imu_length_offset + 1;
|
||||
#endif
|
||||
probe_protocol_state state;
|
||||
probe_protocol_reset(&state, is_left);
|
||||
set_features(&state, 2, 0x17);
|
||||
set_features(&state, 4, 0x17);
|
||||
uint8_t source[PROBE_INPUT_SIZE], actual[PROBE_INPUT_SIZE], expected[PROBE_INPUT_SIZE];
|
||||
for (size_t i = 0; i < sizeof(source); ++i) source[i] = (uint8_t)(i * 3 + 1);
|
||||
source[imu_length_offset] = 30;
|
||||
memcpy(expected, source, sizeof(expected));
|
||||
#ifdef SWITCH2_PROBE_OMIT_NATIVE_IMU
|
||||
memset(expected + imu_length_offset, 0, 41);
|
||||
#elif defined(SWITCH2_PROBE_ZERO_NATIVE_IMU_PAYLOAD)
|
||||
memset(expected + imu_data_offset, 0, 40);
|
||||
#endif
|
||||
memcpy(actual, source, sizeof(actual));
|
||||
probe_protocol_gate_native_report(&state, actual);
|
||||
assert(memcmp(actual, expected, sizeof(actual)) == 0);
|
||||
// IMU disable must leave mouse, NFC (R), and reserved tail bytes untouched.
|
||||
set_features(&state, 5, 4);
|
||||
memcpy(actual, source, sizeof(actual));
|
||||
memset(expected + imu_length_offset, 0, 41);
|
||||
probe_protocol_gate_native_report(&state, actual);
|
||||
assert(memcmp(actual, expected, sizeof(actual)) == 0);
|
||||
set_features(&state, 4, 4);
|
||||
set_features(&state, 5, 0x13);
|
||||
memcpy(actual, source, sizeof(actual));
|
||||
memcpy(expected, source, sizeof(expected));
|
||||
memset(expected + 2, 0, 2);
|
||||
memcpy(expected + 5, state.stick_center, sizeof(state.stick_center));
|
||||
memset(expected + 9, 0, 5);
|
||||
#ifdef SWITCH2_PROBE_OMIT_NATIVE_IMU
|
||||
memset(expected + imu_length_offset, 0, 41);
|
||||
#elif defined(SWITCH2_PROBE_ZERO_NATIVE_IMU_PAYLOAD)
|
||||
memset(expected + imu_data_offset, 0, 40);
|
||||
#endif
|
||||
probe_protocol_gate_native_report(&state, actual);
|
||||
assert(memcmp(actual, expected, sizeof(actual)) == 0);
|
||||
}
|
||||
|
||||
static const uint8_t sample_command[] = {
|
||||
0x0a, 0x91, 0, 0x02, 0, 4, 0, 0, 3, 0, 0, 0,
|
||||
};
|
||||
static unsigned source_calls;
|
||||
static uint8_t expected_sample = 3;
|
||||
static bool source_available = true;
|
||||
static uint64_t source_token = UINT64_C(0x1234567800000001);
|
||||
typedef struct {
|
||||
unsigned source_calls;
|
||||
uint8_t expected_sample;
|
||||
bool source_available;
|
||||
uint64_t source_token;
|
||||
bool storage_available;
|
||||
unsigned saves;
|
||||
uint8_t pairing_blob[PROBE_PAIRING_BLOB_SIZE];
|
||||
} controller_context;
|
||||
|
||||
static bool play_sample(uint8_t sample_id, uint64_t* token) {
|
||||
++source_calls;
|
||||
assert(sample_id == expected_sample);
|
||||
*token = source_token;
|
||||
return source_available;
|
||||
static bool play_sample(void* context, uint8_t sample_id, uint64_t* token) {
|
||||
controller_context* controller = context;
|
||||
++controller->source_calls;
|
||||
assert(sample_id == controller->expected_sample);
|
||||
*token = controller->source_token;
|
||||
return controller->source_available;
|
||||
}
|
||||
|
||||
static bool save_pairing(void* context, const uint8_t* blob, size_t size) {
|
||||
controller_context* controller = context;
|
||||
assert(size == sizeof(controller->pairing_blob));
|
||||
if (!controller->storage_available) return false;
|
||||
memcpy(controller->pairing_blob, blob, size);
|
||||
++controller->saves;
|
||||
return true;
|
||||
}
|
||||
|
||||
static void expect_no_dispatch(probe_protocol_state* state, const uint8_t* command,
|
||||
size_t length, size_t capacity) {
|
||||
uint8_t reply[8];
|
||||
uint64_t token = UINT64_MAX;
|
||||
const unsigned calls_before = source_calls;
|
||||
const controller_context* controller = state->context;
|
||||
const unsigned calls_before = controller->source_calls;
|
||||
assert(probe_protocol_command(state, command, length, reply, capacity, &token) == 0);
|
||||
assert(token == 0);
|
||||
assert(source_calls == calls_before);
|
||||
assert(controller->source_calls == calls_before);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
static void test_sample_dispatch(void) {
|
||||
controller_context controller = {
|
||||
.expected_sample = 3, .source_available = true,
|
||||
.source_token = UINT64_C(0x1234567800000001),
|
||||
};
|
||||
probe_protocol_state state;
|
||||
probe_protocol_reset(&state);
|
||||
probe_protocol_reset(&state, false);
|
||||
state.context = &controller;
|
||||
state.play_sample = play_sample;
|
||||
|
||||
// Each transport/header field and reserved payload byte is a dispatch gate.
|
||||
|
|
@ -55,22 +306,22 @@ int main(void) {
|
|||
|
||||
// Synchronous-only callers cannot accidentally acknowledge a sample.
|
||||
uint8_t reply[8];
|
||||
const unsigned calls_before = source_calls;
|
||||
const unsigned calls_before = controller.source_calls;
|
||||
assert(probe_protocol_command(&state, sample_command, sizeof(sample_command),
|
||||
reply, sizeof(reply), NULL) == 0);
|
||||
assert(source_calls == calls_before);
|
||||
assert(controller.source_calls == calls_before);
|
||||
state.play_sample = NULL;
|
||||
expect_no_dispatch(&state, sample_command, sizeof(sample_command), sizeof(reply));
|
||||
state.play_sample = play_sample;
|
||||
|
||||
// A rejected request must not leak even a token written by the source.
|
||||
uint64_t token = UINT64_MAX;
|
||||
source_available = false;
|
||||
controller.source_available = false;
|
||||
assert(probe_protocol_command(&state, sample_command, sizeof(sample_command),
|
||||
reply, sizeof(reply), &token) == 0);
|
||||
assert(token == 0);
|
||||
source_available = true;
|
||||
source_token = 0;
|
||||
controller.source_available = true;
|
||||
controller.source_token = 0;
|
||||
token = UINT64_MAX;
|
||||
assert(probe_protocol_command(&state, sample_command, sizeof(sample_command),
|
||||
reply, sizeof(reply), &token) == 0);
|
||||
|
|
@ -79,17 +330,17 @@ int main(void) {
|
|||
// The observed sample and range boundaries only produce deferred replies.
|
||||
const uint8_t samples[] = {3, 0, 7};
|
||||
const uint8_t sample_ack[] = {0x0a, 0x01, 0, 0x02, 0, 0xf8, 0, 0};
|
||||
source_token = UINT64_C(0x1234567800000001);
|
||||
controller.source_token = UINT64_C(0x1234567800000001);
|
||||
for (size_t i = 0; i < sizeof(samples); ++i) {
|
||||
uint8_t command[sizeof(sample_command)];
|
||||
memcpy(command, sample_command, sizeof(command));
|
||||
command[8] = expected_sample = samples[i];
|
||||
command[8] = controller.expected_sample = samples[i];
|
||||
token = 0;
|
||||
assert(probe_protocol_command(&state, command, sizeof(command), reply,
|
||||
sizeof(reply), &token) == sizeof(sample_ack));
|
||||
assert(token == source_token);
|
||||
assert(token == controller.source_token);
|
||||
assert(memcmp(reply, sample_ack, sizeof(sample_ack)) == 0);
|
||||
++source_token;
|
||||
++controller.source_token;
|
||||
}
|
||||
|
||||
// Ordinary report selection retains its immediate, empty USB ACK.
|
||||
|
|
@ -99,5 +350,265 @@ int main(void) {
|
|||
reply, sizeof(reply), &token) == sizeof(select_ack));
|
||||
assert(token == 0);
|
||||
assert(memcmp(reply, select_ack, sizeof(select_ack)) == 0);
|
||||
}
|
||||
|
||||
static void test_interleaved_reports_and_features(void) {
|
||||
probe_protocol_state right, left;
|
||||
probe_protocol_reset(&right, false);
|
||||
probe_protocol_reset(&left, true);
|
||||
uint8_t reports[2][PROBE_INPUT_SIZE];
|
||||
initialize(&right);
|
||||
assert(probe_protocol_report(&right, 8, reports[0], sizeof(reports[0])) == PROBE_INPUT_SIZE);
|
||||
assert(probe_protocol_report(&left, 7, reports[1], sizeof(reports[1])) == 0);
|
||||
initialize(&left);
|
||||
select_report(&right, 5);
|
||||
select_report(&left, 8);
|
||||
select_report(&right, 7);
|
||||
assert(right.report_id == 5 && left.report_id == 7);
|
||||
set_features(&right, 2, 0x17);
|
||||
set_features(&right, 4, 0x17);
|
||||
set_features(&left, 2, 0x03);
|
||||
set_features(&left, 4, 0x17);
|
||||
right.controller_active = left.controller_active = true;
|
||||
right.controller_buttons[0] = 0x84; // A + Plus.
|
||||
left.controller_buttons[0] = 0x41; // Down + Minus.
|
||||
const uint8_t sticks[2][3] = {{0x11, 0x22, 0x33}, {0x44, 0x55, 0x66}};
|
||||
const uint8_t neutral[] = {0, 8, 0x80};
|
||||
memcpy(right.controller_stick, sticks[0], 3);
|
||||
memcpy(left.controller_stick, sticks[1], 3);
|
||||
assert(probe_protocol_report(&right, right.report_id, reports[0], sizeof(reports[0])) == PROBE_INPUT_SIZE);
|
||||
assert(probe_protocol_report(&left, left.report_id, reports[1], sizeof(reports[1])) == PROBE_INPUT_SIZE);
|
||||
assert(reports[0][4] == 1 && reports[0][5] == 4 && reports[0][6] == 0);
|
||||
assert(memcmp(reports[0] + 10, neutral, 3) == 0);
|
||||
assert(memcmp(reports[0] + 13, sticks[0], 3) == 0);
|
||||
assert(reports[1][2] == 0x41 && reports[1][3] == 0);
|
||||
assert(memcmp(reports[1] + 5, sticks[1], 3) == 0);
|
||||
select_report(&left, 5);
|
||||
assert(probe_protocol_report(&left, left.report_id, reports[1], sizeof(reports[1])) == PROBE_INPUT_SIZE);
|
||||
assert(reports[1][4] == 0 && reports[1][5] == 1 && reports[1][6] == 1);
|
||||
assert(memcmp(reports[1] + 10, sticks[1], 3) == 0);
|
||||
assert(memcmp(reports[1] + 13, neutral, 3) == 0);
|
||||
|
||||
uint8_t source[PROBE_INPUT_SIZE], expected[2][PROBE_INPUT_SIZE];
|
||||
for (size_t i = 0; i < sizeof(source); ++i) source[i] = (uint8_t)(i * 3 + 1);
|
||||
memcpy(expected[0], source, sizeof(source));
|
||||
#ifdef SWITCH2_PROBE_OMIT_NATIVE_IMU
|
||||
memset(expected[0] + 15, 0, 41);
|
||||
#elif defined(SWITCH2_PROBE_ZERO_NATIVE_IMU_PAYLOAD)
|
||||
memset(expected[0] + 16, 0, 40);
|
||||
#endif
|
||||
memcpy(expected[1], source, sizeof(source));
|
||||
memset(expected[1] + 9, 0, 5);
|
||||
memset(expected[1] + 14, 0, 41);
|
||||
memcpy(reports[0], source, sizeof(source));
|
||||
memcpy(reports[1], source, sizeof(source));
|
||||
probe_protocol_gate_native_report(&left, reports[1]);
|
||||
probe_protocol_gate_native_report(&right, reports[0]);
|
||||
assert(memcmp(reports, expected, sizeof(reports)) == 0);
|
||||
|
||||
// Reverse the negotiated gates without changing either donor's opaque bytes.
|
||||
set_features(&right, 5, 0x15);
|
||||
set_features(&left, 2, 0x17);
|
||||
set_features(&left, 4, 0x17);
|
||||
memcpy(expected[0], source, sizeof(source));
|
||||
memset(expected[0] + 2, 0, 2);
|
||||
memset(expected[0] + 9, 0, 5);
|
||||
memset(expected[0] + 15, 0, 41);
|
||||
memcpy(expected[1], source, sizeof(source));
|
||||
#ifdef SWITCH2_PROBE_OMIT_NATIVE_IMU
|
||||
memset(expected[1] + 14, 0, 41);
|
||||
#elif defined(SWITCH2_PROBE_ZERO_NATIVE_IMU_PAYLOAD)
|
||||
memset(expected[1] + 15, 0, 40);
|
||||
#endif
|
||||
memcpy(reports[0], source, sizeof(source));
|
||||
memcpy(reports[1], source, sizeof(source));
|
||||
probe_protocol_gate_native_report(&right, reports[0]);
|
||||
probe_protocol_gate_native_report(&left, reports[1]);
|
||||
assert(memcmp(reports, expected, sizeof(reports)) == 0);
|
||||
probe_protocol_reset(&right, false);
|
||||
assert(probe_protocol_report(&right, 8, reports[0], sizeof(reports[0])) == 0);
|
||||
assert(probe_protocol_report(&left, 5, reports[1], sizeof(reports[1])) == PROBE_INPUT_SIZE);
|
||||
assert(reports[1][5] == 1 && reports[1][6] == 1);
|
||||
memcpy(reports[1], source, sizeof(source));
|
||||
probe_protocol_gate_native_report(&left, reports[1]);
|
||||
assert(memcmp(reports[1], expected[1], sizeof(reports[1])) == 0);
|
||||
}
|
||||
|
||||
static void test_interleaved_callbacks_and_pairing(void) {
|
||||
const uint8_t addresses[2][6] = {
|
||||
{0x64, 0xf9, 0xd8, 0x93, 0x05, 0xa2},
|
||||
{0x65, 0xf9, 0xd8, 0x93, 0x05, 0xa2},
|
||||
};
|
||||
controller_context controllers[2] = {
|
||||
{.expected_sample = 3, .source_available = true,
|
||||
.source_token = UINT64_C(0x100000001), .storage_available = true},
|
||||
{.expected_sample = 3, .source_available = false,
|
||||
.source_token = UINT64_C(0x200000001), .storage_available = false},
|
||||
};
|
||||
probe_protocol_state states[2];
|
||||
for (unsigned side = 0; side < 2; ++side) {
|
||||
probe_protocol_reset(&states[side], side != 0);
|
||||
states[side].context = &controllers[side];
|
||||
states[side].play_sample = play_sample;
|
||||
states[side].save_pairing = save_pairing;
|
||||
memcpy(states[side].controller_address, addresses[side], 6);
|
||||
}
|
||||
uint8_t reply[PROBE_REPLY_MAX_SIZE];
|
||||
uint8_t cue_replies[2][8];
|
||||
uint64_t tokens[2] = {0, UINT64_MAX};
|
||||
assert(probe_protocol_command(&states[0], sample_command, sizeof(sample_command),
|
||||
cue_replies[0], 8, &tokens[0]) == 8);
|
||||
assert(probe_protocol_command(&states[1], sample_command, sizeof(sample_command),
|
||||
cue_replies[1], 8, &tokens[1]) == 0);
|
||||
assert(tokens[0] == UINT64_C(0x100000001) && tokens[1] == 0);
|
||||
controllers[1].source_available = true;
|
||||
assert(probe_protocol_command(&states[1], sample_command, sizeof(sample_command),
|
||||
cue_replies[1], 8, &tokens[1]) == 8);
|
||||
assert(tokens[0] == UINT64_C(0x100000001) && tokens[1] == UINT64_C(0x200000001));
|
||||
const uint8_t cue_ack[] = {0x0a, 1, 0, 2, 0, 0xf8, 0, 0};
|
||||
assert(memcmp(cue_replies[0], cue_ack, 8) == 0);
|
||||
assert(memcmp(cue_replies[1], cue_ack, 8) == 0);
|
||||
|
||||
const uint8_t hosts[2][16] = {
|
||||
{0x15, 0x91, 0, 1, 0, 8, 0, 0, 0, 1, 1, 2, 3, 4, 5, 6},
|
||||
{0x15, 0x91, 0, 1, 0, 8, 0, 0, 0, 1, 7, 8, 9, 10, 11, 12},
|
||||
};
|
||||
const uint8_t device_component[] = {
|
||||
0x5c, 0xf6, 0xee, 0x79, 0x2c, 0xdf, 0x05, 0xe1,
|
||||
0xba, 0x2b, 0x63, 0x25, 0xc4, 0x1a, 0x5f, 0x10,
|
||||
};
|
||||
const uint8_t ciphertexts[2][16] = {
|
||||
{0x69, 0xc4, 0xe0, 0xd8, 0x6a, 0x7b, 0x04, 0x30,
|
||||
0xd8, 0xcd, 0xb7, 0x80, 0x70, 0xb4, 0xc5, 0x5a},
|
||||
{0x66, 0xe9, 0x4b, 0xd4, 0xef, 0x8a, 0x2c, 0x3b,
|
||||
0x88, 0x4c, 0xfa, 0x59, 0xca, 0x34, 0x2b, 0x2e},
|
||||
};
|
||||
uint8_t challenges[2][25] = {
|
||||
{0x15, 0x91, 0, 2, 0, 17, 0, 0, 0},
|
||||
{0x15, 0x91, 0, 2, 0, 17, 0, 0, 0},
|
||||
};
|
||||
const uint8_t finalize[] = {0x15, 0x91, 0, 3, 0, 1, 0, 0, 0};
|
||||
for (unsigned side = 0; side < 2; ++side) {
|
||||
assert(probe_protocol_command(&states[side], hosts[side], sizeof(hosts[side]),
|
||||
reply, sizeof(reply), NULL) == 17);
|
||||
assert(memcmp(reply + 11, addresses[side], 6) == 0);
|
||||
}
|
||||
for (unsigned side = 0; side < 2; ++side) {
|
||||
uint8_t key[] = {0x15, 0x91, 0, 4, 0, 17, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
|
||||
for (unsigned i = 0; i < 16; ++i) {
|
||||
// R uses AES's 000102...0f / 001122...ff vector; L uses all zeros.
|
||||
key[9 + i] = device_component[i] ^ (side ? 0 : 15u - i);
|
||||
challenges[side][9 + i] = side ? 0 : (uint8_t)((15u - i) * 0x11u);
|
||||
}
|
||||
assert(probe_protocol_command(&states[side], key, sizeof(key),
|
||||
reply, sizeof(reply), NULL) == 25);
|
||||
}
|
||||
assert(probe_protocol_command(&states[0], challenges[0], sizeof(challenges[0]),
|
||||
reply, sizeof(reply), NULL) == 25);
|
||||
assert(memcmp(reply + 9, ciphertexts[0], 16) == 0);
|
||||
// Right confirmation cannot authorize the left's finalize.
|
||||
assert(probe_protocol_command(&states[1], finalize, sizeof(finalize),
|
||||
reply, sizeof(reply), NULL) == 0);
|
||||
assert(controllers[0].saves == 0 && controllers[1].saves == 0);
|
||||
assert(probe_protocol_command(&states[1], challenges[1], sizeof(challenges[1]),
|
||||
reply, sizeof(reply), NULL) == 25);
|
||||
assert(memcmp(reply + 9, ciphertexts[1], 16) == 0);
|
||||
assert(probe_protocol_command(&states[0], finalize, sizeof(finalize),
|
||||
reply, sizeof(reply), NULL) == 9);
|
||||
assert(reply[8] == 1);
|
||||
assert(probe_protocol_command(&states[1], finalize, sizeof(finalize),
|
||||
reply, sizeof(reply), NULL) == 0);
|
||||
assert(controllers[0].saves == 1 && controllers[1].saves == 0);
|
||||
controllers[1].storage_available = true;
|
||||
assert(probe_protocol_command(&states[1], finalize, sizeof(finalize),
|
||||
reply, sizeof(reply), NULL) == 9);
|
||||
assert(reply[8] == 1);
|
||||
assert(controllers[0].saves == 1 && controllers[1].saves == 1);
|
||||
|
||||
// After independent resets, each durable record must resume only its own
|
||||
// challenge association; swapping the two contexts' records is rejected.
|
||||
for (unsigned side = 0; side < 2; ++side) {
|
||||
probe_protocol_reset(&states[side], side != 0);
|
||||
memcpy(states[side].controller_address, addresses[side], 6);
|
||||
assert(!probe_protocol_restore_pairing(&states[side], controllers[1 - side].pairing_blob,
|
||||
PROBE_PAIRING_BLOB_SIZE));
|
||||
assert(probe_protocol_restore_pairing(&states[side], controllers[side].pairing_blob,
|
||||
PROBE_PAIRING_BLOB_SIZE));
|
||||
}
|
||||
for (unsigned side = 0; side < 2; ++side) {
|
||||
assert(probe_protocol_command(&states[side], hosts[1 - side], sizeof(hosts[0]),
|
||||
reply, sizeof(reply), NULL) == 17);
|
||||
assert(probe_protocol_command(&states[side], challenges[side], sizeof(challenges[side]),
|
||||
reply, sizeof(reply), NULL) == 0);
|
||||
assert(probe_protocol_command(&states[side], hosts[side], sizeof(hosts[side]),
|
||||
reply, sizeof(reply), NULL) == 17);
|
||||
assert(probe_protocol_command(&states[side], challenges[side], sizeof(challenges[side]),
|
||||
reply, sizeof(reply), NULL) == 25);
|
||||
assert(memcmp(reply + 9, ciphertexts[side], 16) == 0);
|
||||
}
|
||||
}
|
||||
|
||||
static bool read_memory(void* context, uint32_t address, uint8_t* output, size_t length) {
|
||||
return probe_memory_read(*(const uint8_t*)context, address, output, length);
|
||||
}
|
||||
|
||||
static void test_indexed_memory(void) {
|
||||
probe_protocol_state states[PROBE_CONTROLLER_COUNT];
|
||||
uint8_t instances[PROBE_CONTROLLER_COUNT];
|
||||
for (uint8_t instance = 0; instance < PROBE_CONTROLLER_COUNT; ++instance) {
|
||||
instances[instance] = instance;
|
||||
probe_protocol_reset(&states[instance], probe_model_is_left(instance));
|
||||
states[instance].context = &instances[instance];
|
||||
states[instance].read_memory = read_memory;
|
||||
}
|
||||
const uint8_t calibrations[2][9] = {
|
||||
{0x10, 0x08, 0x81, 0, 3, 0x30, 0, 4, 0x40}, // Valid user override.
|
||||
{0, 0x09, 0x90, 0, 3, 0x30, 0, 4, 0x40}, // Invalid user, factory fallback.
|
||||
};
|
||||
const uint8_t command[] = {
|
||||
0x02, 0x91, 0, 4, 0, 8, 0, 0, 9, 0x7e, 0, 0, 0xa8, 0x30, 1, 0,
|
||||
};
|
||||
for (unsigned remaining = PROBE_CONTROLLER_COUNT; remaining; --remaining) {
|
||||
const uint8_t instance = (uint8_t)(remaining - 1);
|
||||
const bool is_left = SWITCH2_PROBE_COMPOSITE ? instance == 1 : SWITCH2_PROBE_JOYCON_LEFT;
|
||||
uint8_t reply[PROBE_REPLY_MAX_SIZE], calibration[9];
|
||||
assert(probe_memory_stick_calibration(instance, calibration));
|
||||
assert(memcmp(calibration, calibrations[is_left], sizeof(calibration)) == 0);
|
||||
assert(probe_protocol_command(&states[instance], command, sizeof(command),
|
||||
reply, sizeof(reply), NULL) == 25);
|
||||
const uint8_t factory[] = {0, is_left ? 9 : 8, is_left ? 0x90 : 0x80, 0, 3, 0x30, 0, 4, 0x40};
|
||||
assert(memcmp(reply + 16, factory, sizeof(factory)) == 0);
|
||||
const uint32_t ends[] = {0x14fff, 0x1fcfff};
|
||||
for (unsigned region = 0; region < 2; ++region) {
|
||||
uint8_t output[2] = {0xa5, 0xa5};
|
||||
assert(!probe_memory_read(instance, ends[region], output, sizeof(output)));
|
||||
assert(output[0] == 0xa5 && output[1] == 0xa5);
|
||||
assert(probe_memory_read(instance, ends[region], output, 1));
|
||||
assert(output[0] == (uint8_t)((region ? 0xf1 : 0xe1) + is_left));
|
||||
assert(output[1] == 0xa5);
|
||||
}
|
||||
}
|
||||
uint8_t output[9];
|
||||
memset(output, 0xa5, sizeof(output));
|
||||
const uint8_t invalid[] = {PROBE_CONTROLLER_COUNT, UINT8_MAX};
|
||||
for (size_t i = 0; i < sizeof(invalid); ++i) {
|
||||
assert(!probe_memory_read(invalid[i], 0x130a8, output, sizeof(output)));
|
||||
assert(!probe_memory_stick_calibration(invalid[i], output));
|
||||
for (size_t byte = 0; byte < sizeof(output); ++byte) assert(output[byte] == 0xa5);
|
||||
}
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
test_indexed_memory();
|
||||
test_descriptors();
|
||||
for (unsigned side = 0; side < 2; ++side) {
|
||||
test_report_selection_and_reset(side != 0);
|
||||
test_buttons_stick_and_feature_control(side != 0);
|
||||
test_opaque_native_feature_gates(side != 0);
|
||||
}
|
||||
test_sample_dispatch();
|
||||
test_interleaved_reports_and_features();
|
||||
test_interleaved_callbacks_and_pairing();
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -119,9 +119,9 @@ static void publish(bool gyro_fresh = true) {
|
|||
static uint32_t poll(bool commit = true, bool gyro_fresh = true) {
|
||||
now_us += 4000;
|
||||
publish(gyro_fresh);
|
||||
probe_controller_input_poll(to_ms_since_boot(now_us), &controls);
|
||||
const uint32_t token = probe_controller_input_peek_native_report(to_ms_since_boot(now_us), packet);
|
||||
if (commit && token) assert(probe_controller_input_commit_native_report(token));
|
||||
probe_controller_input_poll(0, to_ms_since_boot(now_us), &controls);
|
||||
const uint32_t token = probe_controller_input_peek_native_report(0, to_ms_since_boot(now_us), packet);
|
||||
if (commit && token) assert(probe_controller_input_commit_native_report(0, token));
|
||||
return token;
|
||||
}
|
||||
|
||||
|
|
@ -134,7 +134,7 @@ int main() {
|
|||
put_pair(calibration+6, 1600, 1700);
|
||||
probe_controller_input_set_stick_calibration(calibration);
|
||||
probe_controller_input_set_native_features(0x37);
|
||||
probe_controller_input_set_native_stream(true);
|
||||
probe_controller_input_set_native_stream(0, true);
|
||||
source.slot = 0;
|
||||
source.controller.active = true;
|
||||
source.controller.connection_generation = 7;
|
||||
|
|
@ -179,10 +179,10 @@ int main() {
|
|||
ir_x[0] += 8; ir_x[1] += 8;
|
||||
const uint32_t ticket = poll(false);
|
||||
uint8_t retry[63];
|
||||
assert(ticket && probe_controller_input_peek_native_report(to_ms_since_boot(now_us), retry) == ticket);
|
||||
assert(ticket && probe_controller_input_peek_native_report(0, to_ms_since_boot(now_us), retry) == ticket);
|
||||
assert(memcmp(packet, retry, sizeof(packet)) == 0);
|
||||
assert(probe_controller_input_commit_native_report(ticket));
|
||||
assert(!probe_controller_input_commit_native_report(ticket));
|
||||
assert(probe_controller_input_commit_native_report(0, ticket));
|
||||
assert(!probe_controller_input_commit_native_report(0, ticket));
|
||||
|
||||
// Tilting the Wii up moves camera spots down. Native Joy-Con Y must
|
||||
// reverse the desktop-pointer convention without changing consumption.
|
||||
|
|
@ -247,17 +247,17 @@ int main() {
|
|||
const uint32_t obsolete = poll(false, false);
|
||||
++source.controller.connection_generation;
|
||||
assert(poll(false));
|
||||
assert(!probe_controller_input_commit_native_report(obsolete));
|
||||
probe_controller_input_set_native_stream(false);
|
||||
assert(probe_controller_input_peek_native_report(to_ms_since_boot(now_us), retry) == 0);
|
||||
probe_controller_input_set_native_stream(true);
|
||||
assert(!probe_controller_input_commit_native_report(0, obsolete));
|
||||
probe_controller_input_set_native_stream(0, false);
|
||||
assert(probe_controller_input_peek_native_report(0, to_ms_since_boot(now_us), retry) == 0);
|
||||
probe_controller_input_set_native_stream(0, true);
|
||||
publish_during_snapshot = true;
|
||||
for (unsigned i = 0; i < 450; ++i) assert(poll());
|
||||
assert(packet[15] == 30);
|
||||
source.controller.active = false;
|
||||
now_us += 4000;
|
||||
probe_controller_input_poll(to_ms_since_boot(now_us), &controls);
|
||||
probe_controller_input_poll(0, to_ms_since_boot(now_us), &controls);
|
||||
assert(!controls.active);
|
||||
assert(probe_controller_input_peek_native_report(to_ms_since_boot(now_us), retry) == 0);
|
||||
assert(probe_controller_input_peek_native_report(0, to_ms_since_boot(now_us), retry) == 0);
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,26 +1,51 @@
|
|||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def test_native_packet_fidelity_and_lifecycle(tmp_path: Path) -> None:
|
||||
@pytest.mark.parametrize(
|
||||
("left", "composite"),
|
||||
[(False, False), (True, False), (False, True)],
|
||||
ids=["right", "left", "composite"],
|
||||
)
|
||||
def test_native_packet_fidelity_and_lifecycle(
|
||||
tmp_path: Path, left: bool, composite: bool
|
||||
) -> None:
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
cxx = shutil.which("c++") or shutil.which("g++")
|
||||
assert cxx is not None, "a host C++ compiler is required"
|
||||
probe = root / "tools" / "switch2_usb_probe"
|
||||
executable = tmp_path / "switch2_mouse_bridge_test"
|
||||
subprocess.run(
|
||||
[cxx, "-std=c++17", "-Wall", "-Wextra", "-Werror", "-pthread",
|
||||
"-DSWITCH_PICO_SWITCH2_USB_BRIDGE=1", "-DSWITCH_PICO_BLUEPAD32=1",
|
||||
"-DSWITCH_PICO_ENABLE_BLE=1", "-DSWITCH_PICO_SWITCH2_MOUSE_CAPTURE=1",
|
||||
"-DSWITCH_PICO_SWITCH2_MOUSE_CAPTURE_NATIVE=1",
|
||||
"-DSWITCH2_BRIDGE_SOURCE_ADDRESS_BYTES=0x98,0xe2,0x55,0x07,0xdf,0x00",
|
||||
f"-I{root / 'tests' / 'switch2_mouse_bridge_native_stubs'}",
|
||||
f"-I{probe}", f"-I{root / 'src' / 'firmware'}", f"-I{root / 'bluepad32_config'}",
|
||||
str(root / "tests" / "switch2_mouse_bridge_test.cpp"),
|
||||
str(probe / "controller_input.cpp"),
|
||||
str(root / "src" / "firmware" / "input" / "switch2_mouse_capture.cpp"),
|
||||
"-o", str(executable)],
|
||||
check=True, cwd=root,
|
||||
[
|
||||
cxx,
|
||||
"-std=c++17",
|
||||
"-Wall",
|
||||
"-Wextra",
|
||||
"-Werror",
|
||||
"-pthread",
|
||||
"-DSWITCH_PICO_SWITCH2_USB_BRIDGE=1",
|
||||
"-DSWITCH_PICO_BLUEPAD32=1",
|
||||
"-DSWITCH_PICO_ENABLE_BLE=1",
|
||||
"-DSWITCH_PICO_SWITCH2_MOUSE_CAPTURE=1",
|
||||
"-DSWITCH_PICO_SWITCH2_MOUSE_CAPTURE_NATIVE=1",
|
||||
f"-DSWITCH2_PROBE_JOYCON_LEFT={int(left)}",
|
||||
f"-DSWITCH2_PROBE_COMPOSITE={int(composite)}",
|
||||
"-DSWITCH2_BRIDGE_SOURCE_ADDRESS_BYTES=0x98,0xe2,0x55,0x07,0xdf,0x00",
|
||||
"-DSWITCH2_BRIDGE_SECOND_SOURCE_ADDRESS_BYTES=0x98,0xe2,0x55,0x07,0xe9,0xd3",
|
||||
f"-I{root / 'tests' / 'switch2_mouse_bridge_native_stubs'}",
|
||||
f"-I{probe}",
|
||||
f"-I{root / 'src' / 'firmware'}",
|
||||
f"-I{root / 'bluepad32_config'}",
|
||||
str(root / "tests" / "switch2_mouse_bridge_test.cpp"),
|
||||
str(probe / "controller_input.cpp"),
|
||||
str(root / "src" / "firmware" / "input" / "switch2_mouse_capture.cpp"),
|
||||
"-o",
|
||||
str(executable),
|
||||
],
|
||||
check=True,
|
||||
cwd=root,
|
||||
)
|
||||
subprocess.run([str(executable)], check=True, cwd=root)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
|
@ -12,23 +12,30 @@ sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "tools"))
|
|||
|
||||
from prepare_bluepad32 import prepare_bluepad32
|
||||
|
||||
|
||||
# Reuse the existing real-BTstack-header radio/run-loop fixture, but drive the
|
||||
# capture-enabled discovery path and link the actual cross-core capture mailbox.
|
||||
SOURCE = r'''
|
||||
SOURCE = r"""
|
||||
#define main parser_fixture_main
|
||||
#include "switch2_parser_native_test.c"
|
||||
#undef main
|
||||
#include "model.h"
|
||||
|
||||
void capture_init(void);
|
||||
bool capture_request(uint8_t id, uint64_t* token);
|
||||
int capture_result(uint64_t token);
|
||||
void capture_cancel(void);
|
||||
void capture_select(uint8_t instance, const uint8_t address[6], uint16_t product_id);
|
||||
void capture_native_stream(uint8_t instance, bool enabled);
|
||||
uint32_t capture_native_peek(uint8_t instance, uint8_t report[63]);
|
||||
bool capture_native_commit(uint8_t instance, uint32_t serial);
|
||||
bool capture_request(uint8_t instance, uint8_t id, uint64_t* token);
|
||||
int capture_result(uint8_t instance, uint64_t token);
|
||||
void capture_cancel(uint8_t instance);
|
||||
void capture_exhaust_records(void);
|
||||
|
||||
#define SECONDARY_HANDLE 0x144
|
||||
static const uint8_t secondary_uuid[16] = {
|
||||
0xd5,0xa9,0xe0,0x1e,0x2f,0xfc,0x4c,0xca,0xb2,0x0c,0x8b,0x67,0x14,0x2b,0xf4,0x42};
|
||||
static const uint8_t secondary_uuids[2][16] = {
|
||||
{0xd5,0xa9,0xe0,0x1e,0x2f,0xfc,0x4c,0xca,0xb2,0x0c,0x8b,0x67,0x14,0x2b,0xf4,0x42},
|
||||
{0xcc,0x1b,0xbb,0xb5,0x73,0x54,0x4d,0x32,0xa7,0x16,0xa8,0x1c,0xb2,0x41,0xa3,0x2a},
|
||||
};
|
||||
#define OTHER_PRODUCT_ID (SWITCH2_PROBE_JOYCON_LEFT ? UNI_SW2_JOYCON_R_PID : UNI_SW2_JOYCON_L_PID)
|
||||
static const uint8_t sample_ack[8] = {0x0a,1,1,2,0x10,0x78,0,0};
|
||||
|
||||
static void native_input(struct fixture_peer* peer) {
|
||||
|
|
@ -36,16 +43,16 @@ static void native_input(struct fixture_peer* peer) {
|
|||
notify(peer, SECONDARY_HANDLE, input, sizeof(input));
|
||||
}
|
||||
|
||||
static struct fixture_peer* sample_ready_on_handle(bool requests, uint32_t start,
|
||||
hci_con_handle_t handle) {
|
||||
reset();
|
||||
now_ms = start;
|
||||
capture_init();
|
||||
request_writes = requests;
|
||||
static struct fixture_peer* connect_sample_source(uint8_t instance,
|
||||
const uint8_t address[6],
|
||||
hci_con_handle_t handle) {
|
||||
const bool left = probe_model_is_left(instance);
|
||||
const unsigned previous_ready = ready;
|
||||
uint8_t advertisement_data[64];
|
||||
size_t advertisement_size = advertisement(advertisement_data, UNI_SW2_JOYCON_R_PID, false);
|
||||
size_t advertisement_size = advertisement(advertisement_data, probe_model_pid(instance), false);
|
||||
reverse_bytes(address, advertisement_data + 4, 6);
|
||||
assert(uni_bt_le_switch2_handle_advertisement(advertisement_data, advertisement_size));
|
||||
struct fixture_peer* peer = &peers[0];
|
||||
struct fixture_peer* peer = &peers[instance];
|
||||
peer->device.conn.handle = handle;
|
||||
peer->link_alive = true;
|
||||
uni_hid_parser_switch2_on_le_connected(&peer->device);
|
||||
|
|
@ -55,12 +62,12 @@ static struct fixture_peer* sample_ready_on_handle(bool requests, uint32_t start
|
|||
reverse_128(service_uuid, service + 12);
|
||||
event(peer, service, sizeof(service));
|
||||
query_done(peer, 0);
|
||||
const unsigned write = requests ? ATT_PROPERTY_WRITE : ATT_PROPERTY_WRITE_WITHOUT_RESPONSE;
|
||||
const unsigned write = request_writes ? ATT_PROPERTY_WRITE : ATT_PROPERTY_WRITE_WITHOUT_RESPONSE;
|
||||
characteristic(peer, INPUT_HANDLE, input_uuid, ATT_PROPERTY_NOTIFY);
|
||||
characteristic(peer, RESPONSE_HANDLE, response_uuid, ATT_PROPERTY_NOTIFY);
|
||||
characteristic(peer, COMMAND_HANDLE, command_uuid, write);
|
||||
characteristic(peer, RUMBLE_HANDLE, rumble_uuids[2], write);
|
||||
characteristic(peer, SECONDARY_HANDLE, secondary_uuid, ATT_PROPERTY_NOTIFY);
|
||||
characteristic(peer, RUMBLE_HANDLE, rumble_uuids[left ? 1 : 2], write);
|
||||
characteristic(peer, SECONDARY_HANDLE, secondary_uuids[left ? 1 : 0], ATT_PROPERTY_NOTIFY);
|
||||
query_done(peer, 0);
|
||||
descriptor(peer, RESPONSE_HANDLE + 2);
|
||||
query_done(peer, 0);
|
||||
|
|
@ -69,19 +76,19 @@ static struct fixture_peer* sample_ready_on_handle(bool requests, uint32_t start
|
|||
descriptor(peer, SECONDARY_HANDLE + 2);
|
||||
query_done(peer, 0);
|
||||
query_done(peer, 0);
|
||||
for (unsigned i = 0; i < 24 && !ready && !disconnected; ++i) {
|
||||
for (unsigned i = 0; i < 24 && ready == previous_ready && !disconnected; ++i) {
|
||||
if (peer->query == QUERY_CCCD) {
|
||||
query_done(peer, 0);
|
||||
} else if (peer->command[0] == 0x10) {
|
||||
uint8_t version[20] = {0x10,1,1,1,0x10,0x78,0,0};
|
||||
version[11] = 1;
|
||||
version[11] = left ? 0 : 1;
|
||||
if (peer->query == QUERY_WRITE) query_done(peer, 0);
|
||||
notify(peer, RESPONSE_HANDLE, version, sizeof(version));
|
||||
} else {
|
||||
acknowledge(peer, false);
|
||||
}
|
||||
}
|
||||
assert(ready == 1 && !disconnected);
|
||||
assert(ready == previous_ready + 1 && !disconnected);
|
||||
// Let the unchanged neutral-rumble budget quiesce before requesting a cue.
|
||||
for (unsigned i = 0; i < 6; ++i) {
|
||||
advance(13);
|
||||
|
|
@ -90,6 +97,15 @@ static struct fixture_peer* sample_ready_on_handle(bool requests, uint32_t start
|
|||
native_input(peer);
|
||||
return peer;
|
||||
}
|
||||
static struct fixture_peer* sample_ready_on_handle(bool requests, uint32_t start,
|
||||
hci_con_handle_t handle) {
|
||||
reset();
|
||||
now_ms = start;
|
||||
capture_init();
|
||||
request_writes = requests;
|
||||
return connect_sample_source(0, controller_address, handle);
|
||||
}
|
||||
|
||||
|
||||
static struct fixture_peer* sample_ready(bool requests, uint32_t start) {
|
||||
return sample_ready_on_handle(requests, start, 0);
|
||||
|
|
@ -97,34 +113,34 @@ static struct fixture_peer* sample_ready(bool requests, uint32_t start) {
|
|||
|
||||
static uint64_t request_sample(struct fixture_peer* peer, uint8_t id) {
|
||||
uint64_t token = 0;
|
||||
assert(capture_request(id, &token) && token);
|
||||
assert(capture_result(token) == 0);
|
||||
assert(capture_request(0, id, &token) && token);
|
||||
assert(capture_result(0, token) == 0);
|
||||
unsigned commands = peer->commands;
|
||||
advance(13);
|
||||
const uint8_t expected[12] = {0x0a,0x91,1,2,0,4,0,0,id,0,0,0};
|
||||
assert(peer->commands == commands + 1 && peer->command_length == sizeof(expected));
|
||||
assert(memcmp(peer->command, expected, sizeof(expected)) == 0);
|
||||
assert(capture_result(token) == 0);
|
||||
assert(capture_result(0, token) == 0);
|
||||
return token;
|
||||
}
|
||||
|
||||
static void successful_ack(struct fixture_peer* peer, uint64_t token) {
|
||||
if (peer->query == QUERY_WRITE) query_done(peer, 0);
|
||||
assert(capture_result(token) == 0);
|
||||
assert(capture_result(0, token) == 0);
|
||||
notify(peer, RESPONSE_HANDLE, sample_ack, sizeof(sample_ack));
|
||||
assert(capture_result(token) == 1);
|
||||
assert(capture_result(token) == -1);
|
||||
assert(capture_result(0, token) == 1);
|
||||
assert(capture_result(0, token) == -1);
|
||||
}
|
||||
|
||||
static void test_ack_order_and_source_matching(void) {
|
||||
struct fixture_peer* peer = sample_ready(true, 0);
|
||||
uint64_t token = request_sample(peer, 3), refused = 99;
|
||||
assert(!capture_request(4, &refused) && refused == 0);
|
||||
assert(!capture_request(0, 4, &refused) && refused == 0);
|
||||
uint8_t malformed[9] = {0x0a,1,1,2,0x10,0x78,0,0,0};
|
||||
notify(peer, RESPONSE_HANDLE, malformed, sizeof(malformed));
|
||||
malformed[3] = 1;
|
||||
notify(peer, RESPONSE_HANDLE, malformed, 8);
|
||||
assert(capture_result(token) == 0);
|
||||
assert(capture_result(0, token) == 0);
|
||||
// Deliver a genuine-shaped ACK from a different Bluetooth handle.
|
||||
uint8_t wrong_peer[20] = {GATT_EVENT_NOTIFICATION,18};
|
||||
little_endian_store_16(wrong_peer, 2, 99);
|
||||
|
|
@ -132,44 +148,91 @@ static void test_ack_order_and_source_matching(void) {
|
|||
little_endian_store_16(wrong_peer, 10, sizeof(sample_ack));
|
||||
memcpy(wrong_peer + 12, sample_ack, sizeof(sample_ack));
|
||||
peer->callback(HCI_EVENT_PACKET, 0, wrong_peer, sizeof(wrong_peer));
|
||||
assert(capture_result(token) == 0);
|
||||
assert(capture_result(0, token) == 0);
|
||||
notify(peer, RESPONSE_HANDLE, sample_ack, sizeof(sample_ack));
|
||||
assert(capture_result(token) == 0); // Application ACK cannot beat ATT completion.
|
||||
assert(capture_result(0, token) == 0); // Application ACK cannot beat ATT completion.
|
||||
query_done(peer, 0);
|
||||
assert(capture_result(token) == 1 && capture_result(token) == -1);
|
||||
assert(capture_result(0, token) == 1 && capture_result(0, token) == -1);
|
||||
for (uint8_t id = 0; id < 8; ++id) {
|
||||
uint64_t next = request_sample(peer, id);
|
||||
assert(next > token);
|
||||
token = next;
|
||||
successful_ack(peer, token); // ATT success alone is not application success.
|
||||
}
|
||||
assert(!capture_request(8, &refused) && !capture_request(3, NULL));
|
||||
assert(!capture_request(0, 8, &refused) && !capture_request(0, 3, NULL));
|
||||
}
|
||||
|
||||
static void test_native_notification_bounds_and_fidelity(void) {
|
||||
struct fixture_peer* peer = sample_ready(false, 0);
|
||||
capture_native_stream(0, true);
|
||||
uint8_t input[100], actual[63];
|
||||
for (unsigned i = 0; i < sizeof(input); ++i)
|
||||
input[i] = (uint8_t)(i * 37 + 11);
|
||||
input[PROBE_IMU_LENGTH_OFFSET] = 40;
|
||||
memset(actual, 0xa5, sizeof(actual));
|
||||
notify(peer, SECONDARY_HANDLE, input, 62);
|
||||
notify(peer, SECONDARY_HANDLE, input, 64);
|
||||
notify(peer, SECONDARY_HANDLE, input, 100);
|
||||
assert(capture_native_peek(0, actual) == 0 && actual[0] == 0xa5);
|
||||
notify(peer, SECONDARY_HANDLE, input, 63);
|
||||
uint32_t serial = capture_native_peek(0, actual);
|
||||
assert(serial && memcmp(actual, input, sizeof(actual)) == 0);
|
||||
assert(capture_native_peek(0, actual) == serial); // Failed USB submission retries intact.
|
||||
assert(capture_native_commit(0, serial) && !capture_native_commit(0, serial));
|
||||
assert(capture_native_peek(0, actual) == 0);
|
||||
}
|
||||
|
||||
static void test_selection_cannot_transfer_pending_ack(void) {
|
||||
struct fixture_peer* peer = sample_ready(true, 0);
|
||||
uint64_t old = request_sample(peer, 3), next = 0;
|
||||
uint8_t other[6];
|
||||
memcpy(other, controller_address, sizeof(other));
|
||||
++other[5];
|
||||
capture_select(0, other, PROBE_JOYCON_PID);
|
||||
assert(capture_result(0, old) == -1);
|
||||
native_input(peer);
|
||||
assert(!capture_request(0, 4, &next)); // Old address cannot activate new source.
|
||||
capture_select(0, controller_address, OTHER_PRODUCT_ID);
|
||||
native_input(peer);
|
||||
assert(!capture_request(0, 4, &next)); // Same address, wrong side still cannot.
|
||||
capture_select(0, controller_address, PROBE_JOYCON_PID);
|
||||
native_input(peer);
|
||||
assert(capture_request(0, 4, &next) && next > old);
|
||||
unsigned commands = peer->commands;
|
||||
advance(13);
|
||||
assert(peer->commands == commands);
|
||||
notify(peer, RESPONSE_HANDLE, sample_ack, sizeof(sample_ack));
|
||||
query_done(peer, 0); // Retired transaction drains without owning the new cue.
|
||||
assert(capture_result(0, old) == -1 && capture_result(0, next) == 0);
|
||||
advance(13);
|
||||
assert(peer->commands == commands + 1 && peer->command[8] == 4);
|
||||
successful_ack(peer, next);
|
||||
}
|
||||
|
||||
static void test_cancel_does_not_transfer_old_ack(void) {
|
||||
struct fixture_peer* peer = sample_ready(true, 0);
|
||||
uint64_t old = request_sample(peer, 3), next;
|
||||
capture_cancel();
|
||||
assert(capture_result(old) == -1);
|
||||
assert(capture_request(4, &next) && next > old);
|
||||
capture_cancel(0);
|
||||
assert(capture_result(0, old) == -1);
|
||||
assert(capture_request(0, 4, &next) && next > old);
|
||||
unsigned commands = peer->commands;
|
||||
advance(13);
|
||||
assert(peer->commands == commands); // Old untagged ACK must drain first.
|
||||
notify(peer, RESPONSE_HANDLE, sample_ack, sizeof(sample_ack));
|
||||
query_done(peer, 0);
|
||||
assert(capture_result(old) == -1 && capture_result(next) == 0);
|
||||
assert(capture_result(0, old) == -1 && capture_result(0, next) == 0);
|
||||
advance(13);
|
||||
assert(peer->commands == commands + 1 && peer->command[8] == 4);
|
||||
successful_ack(peer, next);
|
||||
|
||||
peer = sample_ready(false, 0);
|
||||
commands = peer->commands;
|
||||
assert(capture_request(3, &old));
|
||||
assert(capture_request(0, 3, &old));
|
||||
next_write_error = GATT_CLIENT_BUSY;
|
||||
advance(13);
|
||||
assert(peer->commands == commands);
|
||||
capture_cancel();
|
||||
assert(capture_request(4, &next) && next > old);
|
||||
capture_cancel(0);
|
||||
assert(capture_request(0, 4, &next) && next > old);
|
||||
advance(13);
|
||||
assert(peer->commands == commands + 1 && peer->command[8] == 4);
|
||||
successful_ack(peer, next);
|
||||
|
|
@ -180,7 +243,7 @@ static void test_rejection_disconnect_and_late_link_events(void) {
|
|||
uint64_t old = request_sample(peer, 3);
|
||||
notify(peer, RESPONSE_HANDLE, sample_ack, sizeof(sample_ack));
|
||||
query_done(peer, 0x0e);
|
||||
assert(disconnected == 1 && capture_result(old) == -1);
|
||||
assert(disconnected == 1 && capture_result(0, old) == -1);
|
||||
|
||||
peer = sample_ready(false, 0);
|
||||
old = request_sample(peer, 3);
|
||||
|
|
@ -188,15 +251,15 @@ static void test_rejection_disconnect_and_late_link_events(void) {
|
|||
memcpy(rejected, sample_ack, sizeof(rejected));
|
||||
rejected[5] = 0x81;
|
||||
notify(peer, RESPONSE_HANDLE, rejected, sizeof(rejected));
|
||||
assert(disconnected == 1 && capture_result(old) == -1);
|
||||
assert(disconnected == 1 && capture_result(0, old) == -1);
|
||||
|
||||
peer = sample_ready(false, 0);
|
||||
old = request_sample(peer, 3);
|
||||
btstack_packet_handler_t retired_callback = peer->callback;
|
||||
uni_hid_device_disconnect(&peer->device);
|
||||
assert(capture_result(old) == -1);
|
||||
assert(capture_result(0, old) == -1);
|
||||
uint64_t next = 0;
|
||||
assert(!capture_request(3, &next));
|
||||
assert(!capture_request(0, 3, &next));
|
||||
peer = sample_ready_on_handle(false, 0, 1);
|
||||
next = request_sample(peer, 4);
|
||||
assert(next > old);
|
||||
|
|
@ -207,31 +270,33 @@ static void test_rejection_disconnect_and_late_link_events(void) {
|
|||
little_endian_store_16(late, 10, sizeof(sample_ack));
|
||||
memcpy(late + 12, sample_ack, sizeof(sample_ack));
|
||||
retired_callback(HCI_EVENT_PACKET, 0, late, sizeof(late));
|
||||
assert(capture_result(next) == 0 && capture_result(old) == -1);
|
||||
assert(capture_result(0, next) == 0 && capture_result(0, old) == -1);
|
||||
successful_ack(peer, next);
|
||||
|
||||
old = request_sample(peer, 3);
|
||||
notify(peer, RESPONSE_HANDLE, sample_ack, sizeof(sample_ack));
|
||||
uni_hid_device_disconnect(&peer->device);
|
||||
assert(capture_result(old) == -1); // Disconnect revokes even unconsumed success.
|
||||
assert(capture_result(0, old) == -1); // Disconnect revokes even unconsumed success.
|
||||
}
|
||||
|
||||
static void test_freshness_timeout_and_clock_wrap(void) {
|
||||
capture_init();
|
||||
uint64_t token = 0;
|
||||
assert(!capture_request(3, &token));
|
||||
assert(!capture_request(0, 3, &token));
|
||||
uint8_t input[63] = {0};
|
||||
uint8_t other[6];
|
||||
memcpy(other, controller_address, sizeof(other));
|
||||
++other[5];
|
||||
switch_pico_switch2_mouse_report(UNI_SW2_JOYCON_L_PID, controller_address, 8, input, 63, now_ms);
|
||||
switch_pico_switch2_mouse_report(UNI_SW2_JOYCON_R_PID, other, 8, input, 63, now_ms);
|
||||
assert(!capture_request(3, &token));
|
||||
switch_pico_switch2_mouse_report(OTHER_PRODUCT_ID, controller_address,
|
||||
PROBE_NATIVE_REPORT_ID, input, 63, now_ms);
|
||||
switch_pico_switch2_mouse_report(PROBE_JOYCON_PID, other,
|
||||
PROBE_NATIVE_REPORT_ID, input, 63, now_ms);
|
||||
assert(!capture_request(0, 3, &token));
|
||||
|
||||
struct fixture_peer* peer = sample_ready(false, 0);
|
||||
token = request_sample(peer, 3);
|
||||
advance(500);
|
||||
assert(capture_result(token) == -1 && !capture_request(3, &token));
|
||||
assert(capture_result(0, token) == -1 && !capture_request(0, 3, &token));
|
||||
|
||||
peer = sample_ready(false, UINT32_MAX - 200);
|
||||
uint32_t started = now_ms;
|
||||
|
|
@ -242,11 +307,11 @@ static void test_freshness_timeout_and_clock_wrap(void) {
|
|||
}
|
||||
advance(1999 - (uint32_t)(now_ms - started));
|
||||
native_input(peer);
|
||||
assert(capture_result(token) == 0);
|
||||
assert(capture_result(0, token) == 0);
|
||||
advance(1);
|
||||
assert(capture_result(token) == -1);
|
||||
assert(capture_result(0, token) == -1);
|
||||
notify(peer, RESPONSE_HANDLE, sample_ack, sizeof(sample_ack));
|
||||
assert(capture_result(token) == -1);
|
||||
assert(capture_result(0, token) == -1);
|
||||
|
||||
peer = sample_ready(false, 0);
|
||||
token = request_sample(peer, 3);
|
||||
|
|
@ -254,53 +319,157 @@ static void test_freshness_timeout_and_clock_wrap(void) {
|
|||
advance(100);
|
||||
if (!disconnected) native_input(peer);
|
||||
}
|
||||
assert(disconnected == 1 && capture_result(token) == -1);
|
||||
assert(disconnected == 1 && capture_result(0, token) == -1);
|
||||
}
|
||||
|
||||
#if SWITCH2_PROBE_COMPOSITE || SWITCH2_PROBE_HUB
|
||||
static void test_simultaneous_native_sources_and_real_acks(void) {
|
||||
const uint8_t left_address[6] = {0xc0,0x22,0x33,0x44,0x55,0x67};
|
||||
struct fixture_peer* right = sample_ready(true, 0);
|
||||
struct fixture_peer* left = connect_sample_source(1, left_address, 1);
|
||||
assert(ready == 2 && right->link_alive && left->link_alive);
|
||||
capture_native_stream(0, true);
|
||||
capture_native_stream(1, true);
|
||||
uint8_t rinput[63], linput[63], actual[63];
|
||||
for (unsigned i = 0; i < sizeof(rinput); ++i) {
|
||||
rinput[i] = (uint8_t)(i * 17 + 3);
|
||||
linput[i] = (uint8_t)(i * 29 + 9);
|
||||
}
|
||||
rinput[probe_model_imu_length_offset(0)] = 30;
|
||||
linput[probe_model_imu_length_offset(1)] = 40;
|
||||
notify(right, SECONDARY_HANDLE, rinput, sizeof(rinput));
|
||||
uint32_t rserial = capture_native_peek(0, actual);
|
||||
assert(rserial && memcmp(actual, rinput, sizeof(actual)) == 0);
|
||||
notify(left, SECONDARY_HANDLE, linput, sizeof(linput));
|
||||
uint32_t lserial = capture_native_peek(1, actual);
|
||||
assert(lserial > rserial && memcmp(actual, linput, sizeof(actual)) == 0);
|
||||
assert(!capture_native_commit(0, lserial) && !capture_native_commit(1, rserial));
|
||||
assert(capture_native_commit(1, lserial));
|
||||
assert(capture_native_peek(1, actual) == 0);
|
||||
assert(capture_native_peek(0, actual) == rserial);
|
||||
assert(memcmp(actual, rinput, sizeof(actual)) == 0);
|
||||
|
||||
uint64_t rtoken, ltoken;
|
||||
assert(capture_request(0, 3, &rtoken));
|
||||
assert(capture_request(1, 6, <oken) && ltoken > rtoken);
|
||||
const unsigned rcommands = right->commands, lcommands = left->commands;
|
||||
advance(13);
|
||||
assert(right->commands == rcommands + 1 && right->command[0] == 0x0a && right->command[8] == 3);
|
||||
assert(left->commands == lcommands + 1 && left->command[0] == 0x0a && left->command[8] == 6);
|
||||
assert(capture_result(0, ltoken) == -1 && capture_result(1, rtoken) == -1);
|
||||
assert(capture_result(0, rtoken) == 0 && capture_result(1, ltoken) == 0);
|
||||
|
||||
// Opposite ATT/application ordering on live links: neither acknowledges its mate.
|
||||
notify(left, RESPONSE_HANDLE, sample_ack, sizeof(sample_ack));
|
||||
assert(capture_result(0, rtoken) == 0 && capture_result(1, ltoken) == 0);
|
||||
query_done(right, 0);
|
||||
assert(capture_result(0, rtoken) == 0 && capture_result(1, ltoken) == 0);
|
||||
notify(right, RESPONSE_HANDLE, sample_ack, sizeof(sample_ack));
|
||||
assert(capture_result(0, rtoken) == 1 && capture_result(0, rtoken) == -1);
|
||||
assert(capture_result(1, ltoken) == 0);
|
||||
query_done(left, 0);
|
||||
assert(capture_result(1, ltoken) == 1 && capture_result(1, ltoken) == -1);
|
||||
|
||||
assert(capture_request(0, 4, &rtoken));
|
||||
assert(capture_request(1, 7, <oken));
|
||||
advance(13);
|
||||
assert(right->command[8] == 4 && left->command[8] == 7);
|
||||
notify(left, SECONDARY_HANDLE, linput, sizeof(linput));
|
||||
lserial = capture_native_peek(1, actual);
|
||||
assert(lserial > rserial);
|
||||
uni_hid_device_disconnect(&right->device);
|
||||
assert(capture_result(0, rtoken) == -1 && capture_result(1, ltoken) == 0);
|
||||
assert(capture_native_peek(0, actual) == 0);
|
||||
assert(!capture_native_commit(0, rserial));
|
||||
assert(capture_native_peek(1, actual) == lserial);
|
||||
assert(memcmp(actual, linput, sizeof(actual)) == 0);
|
||||
query_done(left, 0);
|
||||
assert(capture_result(1, ltoken) == 0);
|
||||
notify(left, RESPONSE_HANDLE, sample_ack, sizeof(sample_ack));
|
||||
assert(capture_result(1, ltoken) == 1 && capture_result(1, ltoken) == -1);
|
||||
assert(capture_native_commit(1, lserial));
|
||||
assert(capture_native_peek(1, actual) == 0);
|
||||
}
|
||||
#endif
|
||||
|
||||
static void test_teardown_survives_capture_exhaustion(void) {
|
||||
struct fixture_peer* peer = sample_ready(false, 0);
|
||||
uint64_t token, next;
|
||||
assert(capture_request(3, &token));
|
||||
assert(capture_request(0, 3, &token));
|
||||
// The parser has not taken this request, so only source teardown can fail
|
||||
// the mailbox when the serialized capture cannot record another event.
|
||||
capture_exhaust_records();
|
||||
uni_hid_device_disconnect(&peer->device);
|
||||
assert(capture_result(token) == -1 && !capture_request(3, &next));
|
||||
assert(capture_result(0, token) == -1 && !capture_request(0, 3, &next));
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
test_ack_order_and_source_matching();
|
||||
test_native_notification_bounds_and_fidelity();
|
||||
test_selection_cannot_transfer_pending_ack();
|
||||
test_cancel_does_not_transfer_old_ack();
|
||||
test_rejection_disconnect_and_late_link_events();
|
||||
test_freshness_timeout_and_clock_wrap();
|
||||
#if SWITCH2_PROBE_COMPOSITE || SWITCH2_PROBE_HUB
|
||||
test_simultaneous_native_sources_and_real_acks();
|
||||
#endif
|
||||
test_teardown_survives_capture_exhaustion();
|
||||
reset();
|
||||
puts("Source sample relay ACK ordering, ownership, rejection and lifetime passed");
|
||||
return 0;
|
||||
}
|
||||
'''
|
||||
"""
|
||||
|
||||
CAPTURE = r'''
|
||||
CAPTURE = r"""
|
||||
#include "input/switch2_mouse_capture.cpp"
|
||||
#include "model.h"
|
||||
extern "C" uint32_t btstack_run_loop_get_time_ms(void);
|
||||
extern "C" void capture_init(void) {
|
||||
const uint8_t address[6] = {0xc0,0x22,0x33,0x44,0x55,0x66};
|
||||
switch2_mouse_capture_init();
|
||||
switch2_mouse_capture_select_input(address);
|
||||
switch2_mouse_capture_select_input(0, address, probe_model_pid(0));
|
||||
#if SWITCH2_PROBE_COMPOSITE || SWITCH2_PROBE_HUB
|
||||
const uint8_t second[6] = {0xc0,0x22,0x33,0x44,0x55,0x67};
|
||||
switch2_mouse_capture_select_input(1, second, probe_model_pid(1));
|
||||
#endif
|
||||
}
|
||||
extern "C" bool capture_request(uint8_t id, uint64_t* token) {
|
||||
return switch2_mouse_capture_request_sample(id, btstack_run_loop_get_time_ms(), token);
|
||||
extern "C" void capture_select(uint8_t instance, const uint8_t address[6], uint16_t product_id) {
|
||||
switch2_mouse_capture_select_input(instance, address, product_id);
|
||||
}
|
||||
extern "C" int capture_result(uint64_t token) {
|
||||
return switch2_mouse_capture_sample_result(token, btstack_run_loop_get_time_ms());
|
||||
extern "C" void capture_native_stream(uint8_t instance, bool enabled) {
|
||||
switch2_mouse_capture_set_native_stream(instance, enabled);
|
||||
}
|
||||
extern "C" void capture_cancel(void) { switch2_mouse_capture_cancel_sample(); }
|
||||
extern "C" uint32_t capture_native_peek(uint8_t instance, uint8_t report[63]) {
|
||||
return switch2_mouse_capture_peek_native_report(instance, btstack_run_loop_get_time_ms(), report);
|
||||
}
|
||||
extern "C" bool capture_native_commit(uint8_t instance, uint32_t serial) {
|
||||
return switch2_mouse_capture_commit_native_report(instance, serial);
|
||||
}
|
||||
extern "C" bool capture_request(uint8_t instance, uint8_t id, uint64_t* token) {
|
||||
return switch2_mouse_capture_request_sample(instance, id, btstack_run_loop_get_time_ms(), token);
|
||||
}
|
||||
extern "C" int capture_result(uint8_t instance, uint64_t token) {
|
||||
return switch2_mouse_capture_sample_result(instance, token, btstack_run_loop_get_time_ms());
|
||||
}
|
||||
extern "C" void capture_cancel(uint8_t instance) { switch2_mouse_capture_cancel_sample(instance); }
|
||||
// Simulate the boot-lifetime counter boundary without billions of reports.
|
||||
extern "C" void capture_exhaust_records(void) { g_total_records = UINT32_MAX; }
|
||||
'''
|
||||
"""
|
||||
|
||||
|
||||
def test_source_sample_requires_its_real_bluetooth_ack(tmp_path: Path) -> None:
|
||||
@pytest.mark.parametrize(
|
||||
("left", "composite", "hub"),
|
||||
[
|
||||
(False, False, False),
|
||||
(True, False, False),
|
||||
(False, True, False),
|
||||
(False, False, True),
|
||||
],
|
||||
ids=["right", "left", "composite", "hub"],
|
||||
)
|
||||
def test_source_sample_requires_its_real_bluetooth_ack(
|
||||
tmp_path: Path, left: bool, composite: bool, hub: bool
|
||||
) -> None:
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
cc = shutil.which("cc") or shutil.which("gcc")
|
||||
cxx = shutil.which("c++") or shutil.which("g++")
|
||||
|
|
@ -308,36 +477,85 @@ def test_source_sample_requires_its_real_bluetooth_ack(tmp_path: Path) -> None:
|
|||
sdks = [root / "build" / "_deps" / "pico_sdk-src", root / "external" / "pico-sdk"]
|
||||
if sdk := os.environ.get("PICO_SDK_PATH"):
|
||||
sdks.insert(0, Path(sdk))
|
||||
btstack = next((sdk / "lib" / "btstack" / "src" for sdk in sdks
|
||||
if (sdk / "lib" / "btstack" / "src" / "ble" / "gatt_client.h").is_file()), None)
|
||||
btstack = next(
|
||||
(
|
||||
sdk / "lib" / "btstack" / "src"
|
||||
for sdk in sdks
|
||||
if (sdk / "lib" / "btstack" / "src" / "ble" / "gatt_client.h").is_file()
|
||||
),
|
||||
None,
|
||||
)
|
||||
if btstack is None:
|
||||
pytest.skip("Pico SDK BTstack headers required; configure firmware or set PICO_SDK_PATH")
|
||||
prepared = prepare_bluepad32(root / "external" / "bluepad32",
|
||||
root / "patches" / "bluepad32-sdl3-imu.patch",
|
||||
tmp_path / "bluepad32-src")
|
||||
common = ["-O1", "-Wall", "-Wextra", "-ffunction-sections", "-fdata-sections",
|
||||
"-DSWITCH_PICO_SWITCH2_USB_BRIDGE=1", "-DSWITCH_PICO_SWITCH2_MOUSE_CAPTURE=1",
|
||||
"-DSWITCH_PICO_SWITCH2_MOUSE_CAPTURE_NATIVE=1", f"-I{root / 'bluepad32_config'}"]
|
||||
cflags = [*common, "-std=gnu11", "-DENABLE_BLE", "-DENABLE_CLASSIC",
|
||||
f"-I{root / 'tests'}", f"-I{root / 'tests' / 'switch2_parser_native_stubs'}",
|
||||
f"-I{prepared / 'src' / 'components' / 'bluepad32' / 'include'}", f"-I{btstack}",
|
||||
f"-I{btstack.parent / '3rd-party' / 'bluedroid' / 'encoder' / 'include'}",
|
||||
f"-I{btstack.parent / '3rd-party' / 'bluedroid' / 'decoder' / 'include'}",
|
||||
f"-I{btstack.parent / '3rd-party' / 'yxml'}"]
|
||||
pytest.skip(
|
||||
"Pico SDK BTstack headers required; configure firmware or set PICO_SDK_PATH"
|
||||
)
|
||||
prepared = prepare_bluepad32(
|
||||
root / "external" / "bluepad32",
|
||||
root / "patches" / "bluepad32-sdl3-imu.patch",
|
||||
tmp_path / "bluepad32-src",
|
||||
)
|
||||
common = [
|
||||
"-O1",
|
||||
"-Wall",
|
||||
"-Wextra",
|
||||
"-ffunction-sections",
|
||||
"-fdata-sections",
|
||||
"-DSWITCH_PICO_SWITCH2_USB_BRIDGE=1",
|
||||
"-DSWITCH_PICO_SWITCH2_MOUSE_CAPTURE=1",
|
||||
"-DSWITCH_PICO_SWITCH2_MOUSE_CAPTURE_NATIVE=1",
|
||||
f"-DSWITCH2_PROBE_JOYCON_LEFT={int(left)}",
|
||||
f"-DSWITCH2_PROBE_COMPOSITE={int(composite)}",
|
||||
f"-DSWITCH2_PROBE_HUB={int(hub)}",
|
||||
f"-I{root / 'tools' / 'switch2_usb_probe'}",
|
||||
f"-I{root / 'bluepad32_config'}",
|
||||
]
|
||||
cflags = [
|
||||
*common,
|
||||
"-std=gnu11",
|
||||
"-DENABLE_BLE",
|
||||
"-DENABLE_CLASSIC",
|
||||
f"-I{root / 'tests'}",
|
||||
f"-I{root / 'tests' / 'switch2_parser_native_stubs'}",
|
||||
f"-I{prepared / 'src' / 'components' / 'bluepad32' / 'include'}",
|
||||
f"-I{btstack}",
|
||||
f"-I{btstack.parent / '3rd-party' / 'bluedroid' / 'encoder' / 'include'}",
|
||||
f"-I{btstack.parent / '3rd-party' / 'bluedroid' / 'decoder' / 'include'}",
|
||||
f"-I{btstack.parent / '3rd-party' / 'yxml'}",
|
||||
]
|
||||
source = tmp_path / "sample_relay.c"
|
||||
source.write_text(SOURCE)
|
||||
capture = tmp_path / "capture.cpp"
|
||||
capture.write_text(CAPTURE)
|
||||
objects = []
|
||||
for index, path in enumerate((source, root / "bluepad32_config" / "parser" / "uni_hid_parser_switch2.c",
|
||||
root / "bluepad32_config" / "parser" / "uni_switch2_haptics.c",
|
||||
btstack / "btstack_util.c")):
|
||||
for index, path in enumerate(
|
||||
(
|
||||
source,
|
||||
root / "bluepad32_config" / "parser" / "uni_hid_parser_switch2.c",
|
||||
root / "bluepad32_config" / "parser" / "uni_switch2_haptics.c",
|
||||
btstack / "btstack_util.c",
|
||||
)
|
||||
):
|
||||
obj = tmp_path / f"source{index}.o"
|
||||
subprocess.run([cc, *cflags, "-c", str(path), "-o", str(obj)], check=True, cwd=root)
|
||||
subprocess.run(
|
||||
[cc, *cflags, "-c", str(path), "-o", str(obj)], check=True, cwd=root
|
||||
)
|
||||
objects.append(str(obj))
|
||||
executable = tmp_path / "sample_relay"
|
||||
subprocess.run([cxx, *common, "-std=c++17", "-pthread",
|
||||
f"-I{root / 'tests' / 'switch2_mouse_bridge_native_stubs'}",
|
||||
f"-I{root / 'src' / 'firmware'}", str(capture), *objects,
|
||||
"-Wl,--gc-sections", "-o", str(executable)], check=True, cwd=root)
|
||||
subprocess.run(
|
||||
[
|
||||
cxx,
|
||||
*common,
|
||||
"-std=c++17",
|
||||
"-pthread",
|
||||
f"-I{root / 'tests' / 'switch2_mouse_bridge_native_stubs'}",
|
||||
f"-I{root / 'src' / 'firmware'}",
|
||||
str(capture),
|
||||
*objects,
|
||||
"-Wl,--gc-sections",
|
||||
"-o",
|
||||
str(executable),
|
||||
],
|
||||
check=True,
|
||||
cwd=root,
|
||||
)
|
||||
subprocess.run([str(executable)], check=True, cwd=root)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,17 @@ from pathlib import Path
|
|||
import pytest
|
||||
|
||||
|
||||
def test_switch2_usb_probe_deferred_sample(tmp_path: Path) -> None:
|
||||
@pytest.mark.parametrize(
|
||||
("left", "composite"),
|
||||
[(False, False), (True, False), (False, True)],
|
||||
ids=["right", "left", "composite"],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"imu_mode", [None, "OMIT_NATIVE_IMU", "ZERO_NATIVE_IMU_PAYLOAD"]
|
||||
)
|
||||
def test_switch2_usb_probe_protocol(
|
||||
tmp_path: Path, left: bool, composite: bool, imu_mode: str | None
|
||||
) -> None:
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
compiler = shutil.which("cc") or shutil.which("gcc")
|
||||
assert compiler is not None, "a host C compiler is required"
|
||||
|
|
@ -19,13 +29,42 @@ def test_switch2_usb_probe_deferred_sample(tmp_path: Path) -> None:
|
|||
if sdk_path := os.environ.get("PICO_SDK_PATH"):
|
||||
sdk_candidates.insert(0, Path(sdk_path))
|
||||
mbedtls = next(
|
||||
(sdk / "lib" / "mbedtls" for sdk in sdk_candidates
|
||||
if (sdk / "lib" / "mbedtls" / "library" / "aes.c").is_file()),
|
||||
(
|
||||
sdk / "lib" / "mbedtls"
|
||||
for sdk in sdk_candidates
|
||||
if (sdk / "lib" / "mbedtls" / "library" / "aes.c").is_file()
|
||||
),
|
||||
None,
|
||||
)
|
||||
if mbedtls is None:
|
||||
pytest.skip("Pico SDK mbedTLS required; configure firmware or set PICO_SDK_PATH")
|
||||
pytest.skip(
|
||||
"Pico SDK mbedTLS required; configure firmware or set PICO_SDK_PATH"
|
||||
)
|
||||
probe = root / "tools" / "switch2_usb_probe"
|
||||
sides = [False, True] if composite else [left]
|
||||
factory_rows = []
|
||||
user_rows = []
|
||||
for is_left in sides:
|
||||
factory_center = "0x00, 0x09, 0x90" if is_left else "0x00, 0x08, 0x80"
|
||||
factory_rows.append(
|
||||
f"{{[0xa8] = {factory_center}, 0, 3, 0x30, 0, 4, 0x40,"
|
||||
f" [8191] = {0xE2 if is_left else 0xE1}}}"
|
||||
)
|
||||
# L deliberately has invalid user calibration despite valid magic.
|
||||
user_center = "0, 0, 0" if is_left else "0x10, 0x08, 0x81"
|
||||
user_rows.append(
|
||||
f"{{[0x40] = 0xb2, 0xa1, {user_center}, 0, 3, 0x30, 0, 4, 0x40,"
|
||||
f" [4095] = {0xF2 if is_left else 0xF1}}}"
|
||||
)
|
||||
(tmp_path / "probe_memory_data.h").write_text(
|
||||
'#include "model.h"\n'
|
||||
"static const uint8_t probe_factory_memories[PROBE_CONTROLLER_COUNT][8192] = {\n"
|
||||
+ ",\n".join(factory_rows)
|
||||
+ "\n};\n"
|
||||
"static const uint8_t probe_user_calibrations[PROBE_CONTROLLER_COUNT][4096] = {\n"
|
||||
+ ",\n".join(user_rows)
|
||||
+ "\n};\n"
|
||||
)
|
||||
executable = tmp_path / "switch2_usb_probe_protocol"
|
||||
subprocess.run(
|
||||
[
|
||||
|
|
@ -36,9 +75,14 @@ def test_switch2_usb_probe_deferred_sample(tmp_path: Path) -> None:
|
|||
"-Werror",
|
||||
"-pedantic",
|
||||
f'-DMBEDTLS_CONFIG_FILE="{probe / "mbedtls_config.h"}"',
|
||||
f"-DSWITCH2_PROBE_JOYCON_LEFT={int(left)}",
|
||||
f"-DSWITCH2_PROBE_COMPOSITE={int(composite)}",
|
||||
*([f"-DSWITCH2_PROBE_{imu_mode}=1"] if imu_mode else []),
|
||||
f"-I{probe}",
|
||||
f"-I{tmp_path}",
|
||||
f"-I{mbedtls / 'include'}",
|
||||
str(probe / "protocol.c"),
|
||||
str(probe / "memory.c"),
|
||||
str(mbedtls / "library" / "aes.c"),
|
||||
str(mbedtls / "library" / "platform_util.c"),
|
||||
str(root / "tests" / "switch2_usb_probe_protocol_test.c"),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue