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,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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue