switch-pico/tests/switch2_parser_native_test.c

1332 lines
59 KiB
C

#include <assert.h>
#include <stdio.h>
#include <string.h>
#include "protocol_fixture.h"
#include "parser/uni_hid_parser_switch2.h"
#include "parser/uni_hid_parser_native_motion.h"
#include "parser/uni_switch2_pairing.h"
#include "sdkconfig.h"
#if SWITCH2_BRIDGE_FULL_INPUT
void uni_hid_parser_wii_setup(uni_hid_device_t* d) { (void)d; assert(false); }
bool uni_hid_parser_wii_accel_snapshot(uni_hid_device_t* d, int32_t v[3], uint32_t* s) {
(void)d; (void)v; (void)s; assert(false); return false;
}
bool uni_hid_parser_wii_gyro_snapshot(uni_hid_device_t* d, int32_t v[3], uint32_t* s) {
(void)d; (void)v; (void)s; assert(false); return false;
}
#endif
#define PEERS CONFIG_BLUEPAD32_MAX_DEVICES
#define SERVICE_START 0x100
#define INPUT_HANDLE 0x104
#define RESPONSE_HANDLE 0x114
#define COMMAND_HANDLE 0x124
#define RUMBLE_HANDLE 0x134
static struct fixture_peer peers[PEERS];
static btstack_timer_source_t* timers[16];
static unsigned timer_count, connected, ready, disconnected, emitted, remembered, listeners;
static unsigned connected_events, disconnected_events;
static unsigned scan_stops, discovery_calls;
static uint16_t expected_discovery_pid;
static uint8_t expected_discovery_address_type;
static uint32_t now_ms;
static bool pairing_allowed, trusted, storage_ok, request_writes, admit, reject_connected;
static uint8_t next_write_error;
static uint8_t next_connect_error;
static bool scan_enabled, scan_running;
static const bd_addr_t host_address = {0x10, 0x21, 0x32, 0x43, 0x54, 0x65};
static const bd_addr_t controller_address = {0xc0, 0x22, 0x33, 0x44, 0x55, 0x66};
static const uint8_t service_uuid[16] = {0xab,0x7d,0xe9,0xbe,0x89,0xfe,0x49,0xad,0x82,0x8f,0x11,0x8f,0x09,0xdf,0x7f,0xd0};
static const uint8_t input_uuid[16] = {0xab,0x7d,0xe9,0xbe,0x89,0xfe,0x49,0xad,0x82,0x8f,0x11,0x8f,0x09,0xdf,0x7f,0xd2};
static const uint8_t response_uuid[16] = {0xc7,0x65,0xa9,0x61,0xd9,0xd8,0x4d,0x36,0xa2,0x0a,0x53,0x15,0xb1,0x11,0x83,0x6a};
static const uint8_t command_uuid[16] = {0x64,0x9d,0x4a,0xc9,0x8e,0xb7,0x4e,0x6c,0xaf,0x44,0x1e,0xa5,0x4f,0xe5,0xf0,0x05};
static const uint8_t rumble_uuids[3][16] = {
{0xcc,0x48,0x3f,0x51,0x92,0x58,0x42,0x7d,0xa9,0x39,0x63,0x0c,0x31,0xf7,0x2b,0x05},
{0x28,0x93,0x26,0xcb,0xa4,0x71,0x48,0x5d,0xa8,0xf4,0x24,0x0c,0x14,0xf1,0x82,0x41},
{0xfa,0x19,0xb0,0xfb,0xcd,0x1f,0x46,0xa7,0x84,0xa1,0xbb,0xb0,0x9e,0x00,0xc1,0x49},
};
static struct fixture_peer* peer_for_handle(hci_con_handle_t handle) {
for (unsigned i = 0; i < PEERS; ++i)
if (peers[i].used && peers[i].device.conn.handle == handle)
return &peers[i];
return NULL;
}
static void advance(uint32_t milliseconds) {
uint32_t target = now_ms + milliseconds;
for (;;) {
int earliest = -1;
for (unsigned i = 0; i < timer_count; ++i) {
if ((int32_t)(timers[i]->timeout - target) <= 0 &&
(earliest < 0 || (int32_t)(timers[i]->timeout - timers[earliest]->timeout) < 0))
earliest = (int)i;
}
if (earliest < 0)
break;
btstack_timer_source_t* timer = timers[earliest];
now_ms = timer->timeout;
btstack_run_loop_remove_timer(timer);
timer->process(timer);
}
now_ms = target;
}
void btstack_run_loop_set_timer(btstack_timer_source_t* timer, uint32_t ms) { timer->timeout = now_ms + ms; }
void btstack_run_loop_add_timer(btstack_timer_source_t* timer) {
for (unsigned i = 0; i < timer_count; ++i)
assert(timers[i] != timer);
assert(timer_count < 16);
timers[timer_count++] = timer;
}
int btstack_run_loop_remove_timer(btstack_timer_source_t* timer) {
for (unsigned i = 0; i < timer_count; ++i) {
if (timers[i] == timer) {
timers[i] = timers[--timer_count];
return true;
}
}
return false;
}
void btstack_run_loop_set_timer_context(btstack_timer_source_t* timer, void* context) { timer->context = context; }
void btstack_run_loop_set_timer_handler(btstack_timer_source_t* timer, void (*handler)(btstack_timer_source_t*)) { timer->process = handler; }
void* btstack_run_loop_get_timer_context(btstack_timer_source_t* timer) { return timer->context; }
uint32_t btstack_run_loop_get_time_ms(void) { return now_ms; }
void uni_log(const char* format, ...) { (void)format; }
bool switch_pico_switch2_pairing_allowed(void) { return pairing_allowed; }
bool uni_switch2_pairing_known(uint8_t type, const uint8_t address[6]) { (void)type; (void)address; return trusted; }
bool uni_switch2_pairing_remember(uint8_t type, const uint8_t address[6]) {
assert(type <= 1 && memcmp(address, controller_address, 6) == 0);
++remembered;
return storage_ok;
}
void gap_local_bd_addr(bd_addr_t address) { memcpy(address, host_address, 6); }
void gap_stop_scan(void) { ++scan_stops; scan_running = false; }
void uni_bt_le_resume_scanning_if_enabled(void) { if (scan_enabled) scan_running = true; }
uint8_t gap_connect(const bd_addr_t address, bd_addr_type_t type) {
(void)address; (void)type; ++connected;
uint8_t status = next_connect_error;
next_connect_error = 0;
return status;
}
int gap_update_connection_parameters(hci_con_handle_t handle, uint16_t min, uint16_t max, uint16_t latency, uint16_t timeout) {
(void)handle; (void)min; (void)max; (void)latency; (void)timeout; return 0;
}
gap_connection_type_t gap_get_connection_type(hci_con_handle_t handle) {
for (unsigned i = 0; i < PEERS; ++i)
if (peers[i].link_alive && peers[i].device.conn.handle == handle)
return GAP_CONNECTION_LE;
return GAP_CONNECTION_INVALID;
}
uni_hid_device_t* uni_hid_device_create(bd_addr_t address) {
for (unsigned i = 0; i < PEERS; ++i) {
if (!peers[i].used) {
memset(&peers[i], 0, sizeof(peers[i]));
peers[i].used = true;
peers[i].device.conn.handle = UNI_BT_CONN_HANDLE_INVALID;
memcpy(peers[i].device.conn.btaddr, address, 6);
return &peers[i].device;
}
}
return NULL;
}
uni_hid_device_t* uni_hid_device_get_instance_for_address(bd_addr_t address) {
for (unsigned i = 0; i < PEERS; ++i)
if (peers[i].used && memcmp(peers[i].device.conn.btaddr, address, 6) == 0)
return &peers[i].device;
return NULL;
}
uni_hid_device_t* uni_hid_device_get_instance_for_connection_handle(hci_con_handle_t handle) {
struct fixture_peer* peer = peer_for_handle(handle);
return peer ? &peer->device : NULL;
}
uni_error_t uni_hid_device_on_device_discovered(bd_addr_t address, const char* name, uint16_t cod, uint8_t rssi) {
++discovery_calls;
uni_hid_device_t* d = uni_hid_device_get_instance_for_address(address);
uint8_t type = 0xff;
assert(d && uni_hid_parser_switch2_is_ble_device(d));
assert(uni_hid_parser_switch2_identity_address_type(d, &type));
(void)name;
assert(d->cod == cod && d->conn.rssi == rssi);
if (expected_discovery_pid) {
assert(d->product_id == expected_discovery_pid);
assert(type == expected_discovery_address_type);
}
return admit ? UNI_ERROR_SUCCESS : (uni_error_t)1;
}
void uni_hid_device_set_vendor_id(uni_hid_device_t* d, uint16_t value) { d->vendor_id = value; }
void uni_hid_device_set_product_id(uni_hid_device_t* d, uint16_t value) { d->product_id = value; }
void uni_hid_device_set_cod(uni_hid_device_t* d, uint32_t value) { d->cod = value; }
void uni_hid_device_set_name(uni_hid_device_t* d, const char* value) { (void)d; (void)value; }
void uni_hid_device_guess_controller_type_from_pid_vid(uni_hid_device_t* d) { (void)d; }
void uni_bt_conn_set_protocol(uni_bt_conn_t* conn, uni_bt_conn_protocol_t value) { conn->protocol = value; }
void uni_bt_conn_set_state(uni_bt_conn_t* conn, uni_bt_conn_state_t value) { conn->state = value; }
void uni_hid_device_connect(uni_hid_device_t* d) {
assert(!d->conn.connected);
d->conn.connected = true;
++connected_events;
if (reject_connected) {
uni_hid_device_disconnect(d);
uni_hid_device_delete(d);
}
}
void uni_hid_device_set_ready(uni_hid_device_t* d) {
d->conn.state = UNI_BT_CONN_STATE_DEVICE_PENDING_READY;
uni_hid_parser_switch2_setup(d);
}
bool uni_hid_device_set_ready_complete(uni_hid_device_t* d) {
assert(d->conn.connected && connected_events > ready);
++ready;
d->conn.state = UNI_BT_CONN_STATE_DEVICE_READY;
return true;
}
void uni_hid_device_disconnect(uni_hid_device_t* d) {
++disconnected;
if (d->conn.connected)
++disconnected_events;
d->conn.connected = false;
uni_hid_parser_switch2_teardown(d);
struct fixture_peer* peer = peer_for_handle(d->conn.handle);
if (peer)
peer->link_alive = false;
}
void uni_hid_device_delete(uni_hid_device_t* d) {
uni_hid_parser_switch2_teardown(d);
for (unsigned i = 0; i < PEERS; ++i)
if (&peers[i].device == d)
peers[i].used = false;
}
void uni_hid_device_process_controller(uni_hid_device_t* d) { (void)d; ++emitted; }
static uint8_t begin_query(btstack_packet_handler_t callback, hci_con_handle_t handle, enum fixture_query query) {
struct fixture_peer* peer = peer_for_handle(handle);
assert(peer && peer->query == QUERY_NONE);
peer->callback = callback;
peer->query = query;
return 0;
}
uint8_t gatt_client_discover_primary_services_by_uuid128(btstack_packet_handler_t callback, hci_con_handle_t handle, const uint8_t* uuid) {
assert(memcmp(uuid, service_uuid, 16) == 0);
return begin_query(callback, handle, QUERY_SERVICE);
}
uint8_t gatt_client_discover_characteristics_for_service(btstack_packet_handler_t callback, hci_con_handle_t handle, gatt_client_service_t* service) {
assert(service->start_group_handle == SERVICE_START);
return begin_query(callback, handle, QUERY_CHARACTERISTICS);
}
uint8_t gatt_client_discover_characteristic_descriptors(btstack_packet_handler_t callback, hci_con_handle_t handle, gatt_client_characteristic_t* ch) {
peer_for_handle(handle)->descriptor_value = ch->value_handle;
return begin_query(callback, handle, QUERY_DESCRIPTORS);
}
uint8_t gatt_client_write_characteristic_descriptor_using_descriptor_handle(btstack_packet_handler_t callback, hci_con_handle_t handle,
uint16_t descriptor, uint16_t length, uint8_t* value) {
struct fixture_peer* peer = peer_for_handle(handle);
peer->cccd_handle = descriptor;
peer->pending_write = value;
peer->pending_length = length;
return begin_query(callback, handle, QUERY_CCCD);
}
void gatt_client_listen_for_characteristic_value_updates(gatt_client_notification_t* registration, btstack_packet_handler_t callback,
hci_con_handle_t handle, gatt_client_characteristic_t* ch) {
registration->callback = callback;
registration->con_handle = handle;
registration->attribute_handle = ch->value_handle;
++listeners;
}
void gatt_client_stop_listening_for_characteristic_value_updates(gatt_client_notification_t* registration) { (void)registration; assert(listeners); --listeners; }
static uint8_t capture_write(hci_con_handle_t handle, uint16_t value_handle, uint16_t length, uint8_t* value) {
if (next_write_error) {
uint8_t error = next_write_error;
next_write_error = 0;
return error;
}
struct fixture_peer* peer = peer_for_handle(handle);
assert(peer);
if (value_handle == COMMAND_HANDLE) {
assert(length <= sizeof(peer->command));
memcpy(peer->command, value, length);
peer->command_length = length;
++peer->commands;
} else {
assert(value_handle == RUMBLE_HANDLE && length <= sizeof(peer->rumble));
memcpy(peer->rumble, value, length);
peer->rumble_length = length;
unsigned slot = peer->rumbles % FIXTURE_RUMBLE_HISTORY;
memcpy(peer->rumble_history[slot], value, length);
peer->rumble_times[slot] = now_ms;
++peer->rumbles;
}
return 0;
}
uint8_t gatt_client_write_value_of_characteristic_without_response(hci_con_handle_t handle, uint16_t value_handle, uint16_t length, uint8_t* value) {
return capture_write(handle, value_handle, length, value);
}
uint8_t gatt_client_write_value_of_characteristic(btstack_packet_handler_t callback, hci_con_handle_t handle,
uint16_t value_handle, uint16_t length, uint8_t* value) {
uint8_t status = capture_write(handle, value_handle, length, value);
if (status)
return status;
struct fixture_peer* peer = peer_for_handle(handle);
peer->pending_write = value;
peer->pending_length = length;
memcpy(peer->pending_snapshot, value, length);
return begin_query(callback, handle, QUERY_WRITE);
}
// Serialization here models BTstack-generated events; production accessors and
// types come directly from the SDK rather than a parallel mock btstack.h ABI.
void gatt_client_deserialize_service(const uint8_t* data, int offset, gatt_client_service_t* service) {
service->start_group_handle = little_endian_read_16(data, offset);
service->end_group_handle = little_endian_read_16(data, offset + 2);
reverse_128(data + offset + 4, service->uuid128);
service->uuid16 = 0;
}
void gatt_client_deserialize_characteristic(const uint8_t* data, int offset, gatt_client_characteristic_t* ch) {
ch->start_handle = little_endian_read_16(data, offset);
ch->value_handle = little_endian_read_16(data, offset + 2);
ch->end_handle = little_endian_read_16(data, offset + 4);
ch->properties = little_endian_read_16(data, offset + 6);
reverse_128(data + offset + 8, ch->uuid128);
ch->uuid16 = 0;
}
void gatt_client_deserialize_characteristic_descriptor(const uint8_t* data, int offset, gatt_client_characteristic_descriptor_t* descriptor) {
descriptor->handle = little_endian_read_16(data, offset);
reverse_128(data + offset + 2, descriptor->uuid128);
descriptor->uuid16 = (uint16_t)big_endian_read_32(descriptor->uuid128, 0);
}
static void event(struct fixture_peer* peer, uint8_t* data, uint16_t size) {
data[1] = (uint8_t)(size - 2);
little_endian_store_16(data, 2, peer->device.conn.handle);
peer->callback(HCI_EVENT_PACKET, 0, data, size);
}
static void query_done(struct fixture_peer* peer, uint8_t status) {
uint8_t data[9] = {GATT_EVENT_QUERY_COMPLETE};
if (peer->query == QUERY_CCCD)
assert(peer->pending_length == 2 && peer->pending_write[0] == 1 && peer->pending_write[1] == 0);
if (peer->query == QUERY_WRITE)
assert(memcmp(peer->pending_write, peer->pending_snapshot, peer->pending_length) == 0);
peer->query = QUERY_NONE;
data[8] = status;
event(peer, data, sizeof(data));
}
static void characteristic(struct fixture_peer* peer, uint16_t handle, const uint8_t* uuid, uint16_t properties) {
uint8_t data[32] = {GATT_EVENT_CHARACTERISTIC_QUERY_RESULT};
little_endian_store_16(data, 8, handle - 1);
little_endian_store_16(data, 10, handle);
little_endian_store_16(data, 12, handle + 3);
little_endian_store_16(data, 14, properties);
reverse_128(uuid, data + 16);
event(peer, data, sizeof(data));
}
static void descriptor(struct fixture_peer* peer, uint16_t handle) {
const uint8_t cccd_uuid[16] = {0,0,0x29,0x02,0,0,0x10,0,0x80,0,0,0x80,0x5f,0x9b,0x34,0xfb};
uint8_t data[26] = {GATT_EVENT_ALL_CHARACTERISTIC_DESCRIPTORS_QUERY_RESULT};
little_endian_store_16(data, 8, handle);
reverse_128(cccd_uuid, data + 10);
event(peer, data, sizeof(data));
}
static void discover(struct fixture_peer* peer) {
uint8_t service[28] = {GATT_EVENT_SERVICE_QUERY_RESULT};
little_endian_store_16(service, 8, SERVICE_START);
little_endian_store_16(service, 10, SERVICE_START + 0x60);
reverse_128(service_uuid, service + 12);
event(peer, service, sizeof(service));
query_done(peer, 0);
characteristic(peer, INPUT_HANDLE, input_uuid, ATT_PROPERTY_NOTIFY);
characteristic(peer, RESPONSE_HANDLE, response_uuid, ATT_PROPERTY_NOTIFY);
characteristic(peer, COMMAND_HANDLE, command_uuid, request_writes ? ATT_PROPERTY_WRITE : ATT_PROPERTY_WRITE_WITHOUT_RESPONSE);
unsigned kind = peer->device.product_id == UNI_SW2_PRO_PID ? 0 : peer->device.product_id == UNI_SW2_JOYCON_L_PID ? 1 : 2;
characteristic(peer, RUMBLE_HANDLE, rumble_uuids[kind], request_writes ? ATT_PROPERTY_WRITE : ATT_PROPERTY_WRITE_WITHOUT_RESPONSE);
query_done(peer, 0);
}
static void subscribe_response(struct fixture_peer* peer) {
// Deliberately not value_handle+1: parser must discover, not guess CCCDs.
descriptor(peer, RESPONSE_HANDLE + 2);
query_done(peer, 0);
descriptor(peer, INPUT_HANDLE + 2);
query_done(peer, 0);
assert(peer->cccd_handle == RESPONSE_HANDLE + 2);
query_done(peer, 0);
}
static void notify(struct fixture_peer* peer, uint16_t handle, const uint8_t* value, uint16_t length) {
uint8_t data[112] = {GATT_EVENT_NOTIFICATION};
assert(length <= sizeof(data) - 12);
little_endian_store_16(data, 8, handle);
little_endian_store_16(data, 10, length);
memcpy(data + 12, value, length);
event(peer, data, 12 + length);
}
static void packed(uint8_t* data, uint16_t x, uint16_t y) {
data[0] = (uint8_t)x;
data[1] = (uint8_t)((x >> 8) | (y << 4));
data[2] = (uint8_t)(y >> 4);
}
static unsigned response_data(struct fixture_peer* peer, uint8_t* out, bool erased) {
memset(out, 0, 96);
out[0] = peer->command[0];
out[1] = out[2] = 1;
out[3] = peer->command[3];
out[5] = 0x78;
if (out[0] == 2) {
uint8_t length = peer->command[8];
uint32_t address = little_endian_read_32(peer->command, 12);
out[8] = length;
little_endian_store_32(out, 12, address);
if (address == 0x13000) {
little_endian_store_16(out, 16 + 18, UNI_SW2_NINTENDO_VID);
little_endian_store_16(out, 16 + 20, peer->device.product_id);
} else if (address == 0x13044) {
if (erased)
memset(out + 16, 0xff, length);
} else {
if (erased)
memset(out + 16, 0xff, length);
else {
packed(out + 16, 1900, 2100);
packed(out + 19, 1200, 1400);
packed(out + 22, 1500, 1700);
}
}
return 16 + length;
}
if (out[0] == 0x15) {
out[8] = 1;
return out[3] == 1 ? 17 : out[3] == 3 ? 9 : 25;
}
return 8;
}
static void acknowledge(struct fixture_peer* peer, bool erased) {
uint8_t response[96];
unsigned length = response_data(peer, response, erased);
if (peer->query == QUERY_WRITE)
query_done(peer, 0);
notify(peer, RESPONSE_HANDLE, response, length);
}
static void finish_setup(struct fixture_peer* peer) {
for (unsigned limit = 0; limit < 24 && !ready && !disconnected; ++limit) {
if (peer->query == QUERY_CCCD) {
assert(peer->cccd_handle == INPUT_HANDLE + 2);
query_done(peer, 0);
} else {
acknowledge(peer, false);
}
}
assert(ready == 1 && disconnected == 0);
}
static size_t advertisement(uint8_t* packet, uint16_t pid, bool fresh) {
memset(packet, 0, 64);
packet[0] = GAP_EVENT_ADVERTISING_REPORT;
packet[2] = 0;
packet[3] = BD_ADDR_TYPE_LE_PUBLIC;
reverse_bytes(controller_address, packet + 4, 6);
packet[10] = (uint8_t)-35;
packet[11] = 20;
packet[12] = 19;
packet[13] = 0xff;
uint8_t* mfg = packet + 14;
little_endian_store_16(mfg, 0, 0x0553);
little_endian_store_16(mfg, 5, UNI_SW2_NINTENDO_VID);
little_endian_store_16(mfg, 7, pid);
if (!fresh)
reverse_bytes(host_address, mfg + 12, 6);
packet[1] = 30;
return 32;
}
static struct fixture_peer* connect_peer(uint16_t pid, bool fresh) {
uint8_t packet[64];
size_t size = advertisement(packet, pid, fresh);
assert(uni_bt_le_switch2_handle_advertisement(packet, size));
assert(connected == 1);
struct fixture_peer* peer = &peers[0];
peer->device.conn.handle = 0; // Handle zero is valid, including on retirement.
peer->link_alive = true;
uni_hid_parser_switch2_on_le_connected(&peer->device);
return peer;
}
static void reset(void) {
for (unsigned i = 0; i < PEERS; ++i) {
if (peers[i].used)
uni_hid_parser_switch2_teardown(&peers[i].device);
}
assert(timer_count == 0 && listeners == 0);
memset(peers, 0, sizeof(peers));
connected = ready = disconnected = emitted = remembered = 0;
connected_events = disconnected_events = 0;
scan_stops = discovery_calls = 0;
expected_discovery_pid = 0;
expected_discovery_address_type = BD_ADDR_TYPE_LE_PUBLIC;
now_ms = 0;
next_write_error = 0;
next_connect_error = 0;
scan_enabled = scan_running = true;
pairing_allowed = trusted = storage_ok = admit = true;
request_writes = false;
reject_connected = false;
}
static void test_connected_callback_rejection(void) {
reset();
reject_connected = true;
struct fixture_peer* peer = connect_peer(UNI_SW2_PRO_PID, false);
assert(connected_events == 1 && disconnected_events == 1 && ready == 0);
assert(peer->query == QUERY_NONE && timer_count == 0 && listeners == 0);
advance(3000);
assert(ready == 0 && peer->commands == 0);
}
static void test_advertisement_bounds_and_admission(void) {
reset();
uint8_t packet[64];
size_t size = advertisement(packet, UNI_SW2_PRO_PID, true);
for (size_t n = 0; n < size; ++n)
assert(!uni_bt_le_switch2_handle_advertisement(packet, (uint16_t)n));
packet[12] = 20;
assert(!uni_bt_le_switch2_handle_advertisement(packet, size));
packet[12] = 19;
pairing_allowed = false;
assert(uni_bt_le_switch2_handle_advertisement(packet, size) && connected == 0);
advertisement(packet, UNI_SW2_PRO_PID, false);
trusted = false;
assert(uni_bt_le_switch2_handle_advertisement(packet, size) && connected == 0);
trusted = true;
memcpy(packet + 26, host_address, 6); // Wrong byte order must not reconnect.
assert(uni_bt_le_switch2_handle_advertisement(packet, size) && connected == 0);
advertisement(packet, UNI_SW2_PRO_PID, false);
packet[3] = BD_ADDR_TYPE_LE_RANDOM;
packet[9] = 0x40; // Resolving private address, not static identity.
assert(uni_bt_le_switch2_handle_advertisement(packet, size) && connected == 0);
assert(discovery_calls == 0 && scan_stops == 0);
advertisement(packet, UNI_SW2_PRO_PID, false);
admit = false;
assert(uni_bt_le_switch2_handle_advertisement(packet, size) && connected == 0);
admit = true;
assert(uni_bt_le_switch2_handle_advertisement(packet, size) && connected == 1);
uint8_t type = 0xff;
assert(uni_hid_parser_switch2_identity_address_type(&peers[0].device, &type) && type == BD_ADDR_TYPE_LE_PUBLIC);
}
static void test_discovery_metadata_and_rejection_isolation(void) {
reset();
expected_discovery_pid = UNI_SW2_JOYCON_L_PID;
struct fixture_peer* mate = connect_peer(UNI_SW2_JOYCON_L_PID, false);
discover(mate);
subscribe_response(mate);
finish_setup(mate);
assert(ready == 1 && discovery_calls == 1);
const unsigned mate_timers = timer_count;
const unsigned mate_listeners = listeners;
uint8_t packet[64];
size_t size = advertisement(packet, UNI_SW2_JOYCON_R_PID, false);
packet[3] = BD_ADDR_TYPE_LE_RANDOM;
packet[4] ^= 1; // A different static identity from the active mate.
bd_addr_t candidate_address;
gap_event_advertising_report_get_address(packet, candidate_address);
expected_discovery_pid = UNI_SW2_JOYCON_R_PID;
expected_discovery_address_type = BD_ADDR_TYPE_LE_RANDOM;
admit = false;
// More rejections than slots must not exhaust either device or parser storage.
for (unsigned i = 0; i <= CONFIG_BLUEPAD32_MAX_DEVICES; ++i) {
assert(uni_bt_le_switch2_handle_advertisement(packet, size));
assert(discovery_calls == i + 2);
assert(!uni_hid_device_get_instance_for_address(candidate_address));
for (unsigned slot = 1; slot < PEERS; ++slot) {
uint8_t type;
assert(!peers[slot].used);
assert(!uni_hid_parser_switch2_identity_address_type(&peers[slot].device, &type));
}
assert(connected == 1 && scan_stops == 1 && disconnected == 0);
assert(timer_count == mate_timers && listeners == mate_listeners);
assert(mate->used && mate->link_alive && mate->device.conn.state == UNI_BT_CONN_STATE_DEVICE_READY);
}
// A rejected candidate must remain discoverable once policy permits it.
admit = true;
assert(uni_bt_le_switch2_handle_advertisement(packet, size));
assert(uni_hid_device_get_instance_for_address(candidate_address));
assert(connected == 2 && scan_stops == 2 && disconnected == 0);
unsigned admitted_calls = discovery_calls;
assert(uni_bt_le_switch2_handle_advertisement(packet, size));
assert(discovery_calls == admitted_calls && connected == 2 && scan_stops == 2);
assert(mate->used && mate->link_alive);
reset();
size = advertisement(packet, UNI_SW2_JOYCON_R_PID, false);
admit = false;
assert(uni_bt_le_switch2_handle_advertisement(packet, size));
advance(3000);
assert(discovery_calls == 1 && connected == 0 && scan_stops == 0 && disconnected == 0);
assert(timer_count == 0 && listeners == 0);
for (unsigned slot = 0; slot < PEERS; ++slot)
assert(!peers[slot].used);
}
static void test_missing_descriptor_and_setup_timeout(void) {
reset();
struct fixture_peer* peer = connect_peer(UNI_SW2_PRO_PID, false);
discover(peer);
descriptor(peer, RESPONSE_HANDLE); // CCCD outside characteristic's descriptor range.
assert(disconnected == 1 && ready == 0 && timer_count == 0 && listeners == 0);
reset();
peer = connect_peer(UNI_SW2_PRO_PID, false);
discover(peer);
subscribe_response(peer);
uint8_t response[96];
unsigned length = response_data(peer, response, false);
response[3] ^= 1; // Matching command but wrong subcommand.
notify(peer, RESPONSE_HANDLE, response, length);
response[3] ^= 1;
response[12] ^= 1; // Matching command/subcommand but stale flash address.
notify(peer, RESPONSE_HANDLE, response, length);
uint8_t truncated[12] = {GATT_EVENT_NOTIFICATION};
little_endian_store_16(truncated, 8, RESPONSE_HANDLE);
little_endian_store_16(truncated, 10, 0xffff);
event(peer, truncated, sizeof(truncated));
assert(peer->commands == 1 && ready == 0);
advance(2000);
assert(disconnected == 1 && ready == 0 && timer_count == 0 && listeners == 0);
advance(5000);
assert(peer->commands == 1);
}
static void test_pairing_gate_and_write_ack_order(void) {
reset();
request_writes = true;
struct fixture_peer* peer = connect_peer(UNI_SW2_PRO_PID, true);
discover(peer);
subscribe_response(peer);
uint8_t response[96];
unsigned length = response_data(peer, response, false);
notify(peer, RESPONSE_HANDLE, response, length); // App ACK arrives before ATT write result.
assert(peer->commands == 1 && !remembered && !ready);
query_done(peer, 0);
assert(peer->command[0] == 0x15 && peer->command[3] == 1);
for (unsigned i = 0; i < 6; ++i)
assert(peer->command[10 + i] == host_address[5 - i] && peer->command[16 + i] == host_address[5 - i]);
pairing_allowed = false;
acknowledge(peer, false);
assert(disconnected == 1 && remembered == 0 && ready == 0);
assert(connected_events == 1 && disconnected_events == 1);
reset();
peer = connect_peer(UNI_SW2_PRO_PID, true);
discover(peer);
subscribe_response(peer);
storage_ok = false;
for (unsigned i = 0; i < 5; ++i)
acknowledge(peer, false); // Info and four app pairing commands.
assert(remembered == 1 && disconnected == 1 && ready == 0 && timer_count == 0);
reset();
peer = connect_peer(UNI_SW2_PRO_PID, false);
discover(peer);
subscribe_response(peer);
length = response_data(peer, response, false);
response[5] = 0x81;
notify(peer, RESPONSE_HANDLE, response, length);
assert(disconnected == 1 && ready == 0);
reset();
request_writes = true;
peer = connect_peer(UNI_SW2_PRO_PID, false);
discover(peer);
subscribe_response(peer);
length = response_data(peer, response, false);
notify(peer, RESPONSE_HANDLE, response, length);
query_done(peer, ATT_ERROR_INSUFFICIENT_AUTHENTICATION);
assert(disconnected == 1 && ready == 0);
}
static void test_calibration_physical_inputs_and_sensor_units(void) {
reset();
struct fixture_peer* peer = connect_peer(UNI_SW2_JOYCON_R_PID, false);
discover(peer);
subscribe_response(peer);
acknowledge(peer, false); // Info.
acknowledge(peer, true); // Missing user calibration falls back to factory.
assert(little_endian_read_32(peer->command, 12) == 0x130a8);
finish_setup(peer);
uint8_t report[63] = {0};
little_endian_store_32(report, 4, 0x4000 | 0x20 | 0x10 | 0x04);
packed(report + 10, 4095, 4095); // Nonexistent left stick must stay neutral.
packed(report + 13, 3100, 400); // Calibrated +X/-Y full travel.
notify(peer, INPUT_HANDLE, report, sizeof(report));
uni_gamepad_t* gp = &peer->device.controller.gamepad;
assert(gp->axis_x == 0 && gp->axis_y == 0 && gp->axis_rx == 511 && gp->axis_ry == 511);
assert(gp->buttons == BUTTON_A && gp->misc_buttons == 0);
assert(uni_hid_parser_switch2_extra_buttons(&peer->device) == (UNI_SW2_BUTTON_C | UNI_SW2_BUTTON_RIGHT_SL | UNI_SW2_BUTTON_RIGHT_SR));
unsigned previous = emitted;
notify(peer, INPUT_HANDLE, report, 62);
assert(emitted == previous && gp->axis_rx == 511);
little_endian_store_16(report, 48, 4096);
little_endian_store_16(report, 50, 8192);
little_endian_store_16(report, 52, (uint16_t)-4096);
little_endian_store_16(report, 54, 32767);
little_endian_store_16(report, 56, 0);
little_endian_store_16(report, 58, 0);
for (unsigned sample = 0; sample < 46; ++sample) {
little_endian_store_32(report, 42, 0xffff0000u + sample * 10000u);
notify(peer, INPUT_HANDLE, report, sizeof(report));
advance(10);
}
assert(gp->accel[0] == 8192 && gp->accel[1] == -8192 && gp->accel[2] == -16384);
assert(gp->gyro[0] >= 2041740 && gp->gyro[0] <= 2041750);
#if SWITCH2_BRIDGE_FULL_INPUT
uni_native_motion_snapshot_t first, duplicate;
assert(uni_hid_parser_native_motion_snapshot(&peer->device, &first));
assert(first.accel_valid && first.gyro_valid && first.accel_q13[2] == -16384);
#endif
notify(peer, INPUT_HANDLE, report, sizeof(report)); // Repeated sensor sample must not become zero.
#if SWITCH2_BRIDGE_FULL_INPUT
assert(uni_hid_parser_native_motion_snapshot(&peer->device, &duplicate));
assert(duplicate.accel_sequence == first.accel_sequence && duplicate.gyro_sequence == first.gyro_sequence);
assert(duplicate.gyro_q10[0] >= 2041740);
#else
assert(gp->gyro[0] >= 2041740);
#endif
reset();
peer = connect_peer(UNI_SW2_JOYCON_L_PID, false);
discover(peer);
subscribe_response(peer);
finish_setup(peer);
memset(report, 0, sizeof(report));
little_endian_store_32(report, 4, 0x300000);
packed(report + 10, 1900, 2100);
notify(peer, INPUT_HANDLE, report, sizeof(report));
assert(peer->device.controller.gamepad.misc_buttons == 0); // Rail buttons are NOT Home/Capture.
assert(uni_hid_parser_switch2_extra_buttons(&peer->device) == (UNI_SW2_BUTTON_LEFT_SL | UNI_SW2_BUTTON_LEFT_SR));
}
#if SWITCH2_BRIDGE_FULL_INPUT
static void test_native_independent_motion_and_lifetime(void) {
const uint16_t products[] = {UNI_SW2_PRO_PID, UNI_SW2_JOYCON_L_PID, UNI_SW2_JOYCON_R_PID};
uint32_t previous_sequence = 0;
for (unsigned product = 0; product < 3; ++product) {
for (unsigned invalid_bias = 0; invalid_bias < 2; ++invalid_bias) {
reset();
struct fixture_peer* peer = connect_peer(products[product], false);
discover(peer);
subscribe_response(peer);
for (unsigned step = 0; step < 10; ++step) {
if (peer->command[0] == 2 && little_endian_read_32(peer->command, 12) == 0x13044)
break;
acknowledge(peer, false);
}
assert(little_endian_read_32(peer->command, 12) == 0x13044);
acknowledge(peer, invalid_bias);
finish_setup(peer);
uni_native_motion_snapshot_t native;
assert(uni_hid_parser_native_motion_snapshot(&peer->device, &native));
assert(!native.report_valid && !native.accel_valid && !native.gyro_valid);
uint8_t report[63] = {0};
little_endian_store_16(report, 48, 4096);
little_endian_store_16(report, 54, 32767);
little_endian_store_32(report, 42, 1000);
notify(peer, INPUT_HANDLE, report, sizeof(report));
assert(uni_hid_parser_native_motion_snapshot(&peer->device, &native));
assert(native.report_valid && native.accel_valid && native.accel_q13[0] == 8192);
assert(!native.gyro_valid && native.accel_sequence != previous_sequence);
for (unsigned sample = 1; sample < 46; ++sample) {
advance(10);
little_endian_store_32(report, 42, 1000 + sample * 10000);
notify(peer, INPUT_HANDLE, report, sizeof(report));
}
assert(uni_hid_parser_native_motion_snapshot(&peer->device, &native));
assert(native.accel_valid && native.gyro_valid == !invalid_bias);
uint32_t accel = native.accel_sequence, gyro = native.gyro_sequence;
little_endian_store_32(report, 42, 1000); // Regressed sensor clock.
notify(peer, INPUT_HANDLE, report, sizeof(report));
assert(uni_hid_parser_native_motion_snapshot(&peer->device, &native));
assert(native.accel_sequence == accel && native.gyro_sequence == gyro);
uni_hid_parser_switch2_parse_input_report(&peer->device, report, 62);
assert(uni_hid_parser_native_motion_snapshot(&peer->device, &native));
assert(!native.report_valid && native.accel_sequence == accel);
previous_sequence = accel;
uni_hid_parser_switch2_teardown(&peer->device);
assert(!uni_hid_parser_native_motion_snapshot(&peer->device, &native));
assert(!native.accel_valid && !native.gyro_valid);
}
}
}
#endif
static uint64_t rumble_frame(const struct fixture_peer* peer) {
uint64_t value = 0;
for (unsigned i = 0; i < 5; ++i)
value |= (uint64_t)peer->rumble[2 + i] << (8 * i);
return value;
}
static bool rumble_active(const struct fixture_peer* peer) {
uint64_t frame = rumble_frame(peer);
return ((frame >> 10) & 1023) != 0 || ((frame >> 30) & 1023) != 0;
}
static void test_rumble_delay_expiry_retry_and_teardown(void) {
reset();
struct fixture_peer* peer = connect_peer(UNI_SW2_PRO_PID, true);
discover(peer);
subscribe_response(peer);
finish_setup(peer);
assert(remembered == 1);
uni_hid_parser_switch2_set_player_leds(&peer->device, 0x0a);
assert(peer->command[8] == 0x0a);
acknowledge(peer, false);
uni_hid_parser_switch2_play_dual_rumble(&peer->device, 20, 50, 40, 80);
advance(19);
assert(peer->rumble_length == 33 && !rumble_active(peer));
advance(1);
assert(rumble_active(peer));
assert(((rumble_frame(peer) >> 10) & 1023) == 320 && ((rumble_frame(peer) >> 30) & 1023) == 160);
uint8_t successful_id = peer->rumble[1];
next_write_error = BTSTACK_ACL_BUFFERS_FULL;
advance(13);
assert(peer->rumble[1] == successful_id);
advance(13);
assert(peer->rumble[1] == (uint8_t)(0x50 | ((successful_id + 1) & 15)) && rumble_active(peer));
advance(24);
assert(!rumble_active(peer));
uni_hid_parser_switch2_play_dual_rumble(&peer->device, 0, UINT16_MAX, 12, 34);
advance(70000); // Held local feedback does not expire at65.535 seconds.
assert(((rumble_frame(peer) >> 10) & 1023) == 136 && ((rumble_frame(peer) >> 30) & 1023) == 48);
uni_hid_parser_switch2_play_dual_rumble(&peer->device, 0, 100, 255, 255);
advance(1);
assert(rumble_active(peer));
uni_hid_parser_switch2_play_dual_rumble(&peer->device, 0, 0, 0, 0);
advance(1);
assert(!rumble_active(peer));
unsigned writes = peer->rumbles;
uni_hid_device_disconnect(&peer->device);
assert(disconnected_events == 1);
uni_hid_parser_switch2_teardown(&peer->device);
assert(timer_count == 0 && listeners == 0);
advance(5000);
assert(peer->rumbles == writes);
assert(!uni_hid_parser_switch2_identity_address_type(&peer->device, &(uint8_t){0}));
}
static void test_write_request_buffers_and_failed_completion(void) {
reset();
request_writes = true;
struct fixture_peer* peer = connect_peer(UNI_SW2_JOYCON_L_PID, false);
discover(peer);
subscribe_response(peer);
finish_setup(peer);
advance(1);
assert(peer->rumble_length == 17 && peer->query == QUERY_WRITE);
uint8_t pending[33];
uint16_t length = peer->pending_length;
const uint8_t* pending_buffer = peer->pending_write;
memcpy(pending, pending_buffer, length);
uni_hid_parser_switch2_play_dual_rumble(&peer->device, 0, UINT16_MAX, 20, 30);
uni_hid_parser_switch2_set_player_leds(&peer->device, 0x05);
advance(13);
assert(memcmp(pending, pending_buffer, length) == 0); // ATT still borrows this buffer.
query_done(peer, 0);
assert(peer->command[0] == 9 && peer->command[8] == 0x05);
acknowledge(peer, false);
advance(13);
assert(rumble_active(peer) && peer->rumble[1] == 0x51);
query_done(peer, ATT_ERROR_UNLIKELY_ERROR);
assert(disconnected_events == 1 && timer_count == 0 && listeners == 0);
unsigned writes = peer->rumbles;
advance(3000);
assert(peer->rumbles == writes);
}
static struct fixture_peer* ready_peer(uint16_t pid, bool requests) {
reset();
request_writes = requests;
struct fixture_peer* peer = connect_peer(pid, false);
discover(peer);
subscribe_response(peer);
finish_setup(peer);
return peer;
}
static uni_switch2_haptics_frame_t native_frame(unsigned left, unsigned right, unsigned seed) {
uni_switch2_haptics_frame_t frame = {0};
frame.sides[0].count = left;
frame.sides[1].count = right;
for (unsigned side = 0; side < 2; ++side) {
for (unsigned i = 0; i < frame.sides[side].count; ++i) {
uni_switch2_haptics_encode_sample(frame.sides[side].samples[i],
10 + seed + 5 * i + side, 80 + seed + i + side,
1000 + seed * 100 + i * 200 + side * 3000,
5000 + seed * 100 + i * 700 + side * 500);
}
}
return frame;
}
static uni_switch2_haptics_side_t final_hold(const uni_switch2_haptics_side_t* source) {
uni_switch2_haptics_side_t side = {.count = 1};
memcpy(side.samples[0], source->samples[source->count - 1], 5);
return side;
}
static void assert_block(const struct fixture_peer* peer, unsigned side,
const uni_switch2_haptics_side_t* expected) {
assert(peer->rumble_length >= 17 + side * 16);
const uint8_t* block = peer->rumble + 1 + side * 16;
assert((block[0] & 0xf0) == (0x40 | (expected->count << 4)));
assert(memcmp(block + 1, expected->samples, 5 * expected->count) == 0);
for (unsigned i = 1 + 5 * expected->count; i < 16; ++i)
assert(block[i] == 0);
}
static void assert_silent(const struct fixture_peer* peer, unsigned side) {
const uint8_t* block = peer->rumble + 1 + side * 16;
assert((block[0] & 0xf0) == 0x50);
assert(!(block[2] & 0xfc) && !(block[3] & 0x0f) && !(block[4] & 0xc0) && !block[5]);
for (unsigned i = 6; i < 16; ++i)
assert(block[i] == 0);
}
static void test_native_fifo_stereo_counts_and_retry(void) {
struct fixture_peer* peer = ready_peer(UNI_SW2_PRO_PID, false);
uni_switch2_haptics_frame_t a = native_frame(3, 2, 1);
uni_switch2_haptics_frame_t b = native_frame(2, 0, 2);
uni_switch2_haptics_frame_t c = native_frame(1, 3, 3);
uint32_t drops = uni_hid_parser_switch2_haptics_dropped();
assert(uni_hid_parser_switch2_queue_haptics(&peer->device, &a, now_ms));
assert(uni_hid_parser_switch2_queue_haptics(&peer->device, &b, now_ms));
next_write_error = BTSTACK_ACL_BUFFERS_FULL;
advance(1);
assert(peer->rumbles == 0);
advance(13);
assert(peer->rumbles == 1 && peer->rumble_length == 33);
assert(peer->rumble[1] == 0x70 && peer->rumble[17] == 0x60);
assert_block(peer, 0, &a.sides[0]);
assert_block(peer, 1, &a.sides[1]);
// C arrives while A plays, with enough source lifetime for all three frames.
assert(uni_hid_parser_switch2_queue_haptics(&peer->device, &c, now_ms));
advance(15);
assert(peer->rumbles == 1); // Three samples cannot be interrupted at13ms.
advance(1);
assert(peer->rumbles == 2 && peer->rumble[1] == 0x61);
assert_block(peer, 0, &b.sides[0]);
uni_switch2_haptics_side_t right = final_hold(&a.sides[1]);
assert_block(peer, 1, &right);
advance(10);
assert(peer->rumbles == 2);
advance(1);
assert(peer->rumbles == 3 && peer->rumble[1] == 0x52 && peer->rumble[17] == 0x72);
assert_block(peer, 0, &c.sides[0]);
assert_block(peer, 1, &c.sides[1]);
assert(peer->rumble_times[1] - peer->rumble_times[0] == 16);
assert(peer->rumble_times[2] - peer->rumble_times[1] == 11);
assert(uni_hid_parser_switch2_haptics_dropped() == drops);
advance(23); // C expires at receipt14 +50, not transmission41 +50.
assert_silent(peer, 0);
assert_silent(peer, 1);
}
static void test_native_hold_and_absent_side_watchdogs(void) {
struct fixture_peer* peer = ready_peer(UNI_SW2_PRO_PID, false);
uni_switch2_haptics_frame_t a = native_frame(3, 2, 4);
assert(uni_hid_parser_switch2_queue_haptics(&peer->device, &a, now_ms));
advance(1);
assert_block(peer, 0, &a.sides[0]);
advance(15);
assert(peer->rumbles == 1);
advance(1);
uni_switch2_haptics_side_t left = final_hold(&a.sides[0]);
uni_switch2_haptics_side_t right = final_hold(&a.sides[1]);
assert(peer->rumbles == 2);
assert_block(peer, 0, &left);
assert_block(peer, 1, &right);
advance(3); // t20; only the left actuator receives a fresh source update.
uni_switch2_haptics_frame_t b = native_frame(1, 0, 5);
assert(uni_hid_parser_switch2_queue_haptics(&peer->device, &b, now_ms));
advance(3); // Last count1 hold guarded until t23.
assert_block(peer, 0, &b.sides[0]);
assert_block(peer, 1, &right);
advance(27);
assert_block(peer, 0, &b.sides[0]);
assert_silent(peer, 1); // Absent side must not gain a new50ms watchdog.
advance(20);
assert_silent(peer, 0);
assert_silent(peer, 1);
}
static void test_feedback_advances_host_and_resumes_only_final_samples(void) {
struct fixture_peer* peer = ready_peer(UNI_SW2_PRO_PID, false);
uint32_t drops = uni_hid_parser_switch2_haptics_dropped();
uni_switch2_haptics_frame_t a = native_frame(2, 3, 6);
uni_hid_parser_switch2_play_dual_rumble(&peer->device, 0, 40, 20, 30);
assert(uni_hid_parser_switch2_queue_haptics(&peer->device, &a, now_ms));
advance(1);
assert(((rumble_frame(peer) >> 10) & 1023) == 120);
assert((peer->rumble[1] & 0xf0) == 0x50);
advance(9);
uni_switch2_haptics_frame_t b = native_frame(3, 0, 7);
assert(uni_hid_parser_switch2_queue_haptics(&peer->device, &b, now_ms));
advance(1);
assert(((rumble_frame(peer) >> 10) & 1023) == 120);
uni_hid_parser_switch2_play_dual_rumble(&peer->device, 0, 0, 0, 0);
advance(1);
uni_switch2_haptics_side_t left = final_hold(&b.sides[0]);
uni_switch2_haptics_side_t right = final_hold(&a.sides[1]);
assert_block(peer, 0, &left);
assert_block(peer, 1, &right);
for (unsigned i = 0; i < peer->rumbles; ++i) {
assert((peer->rumble_history[i][1] & 0xf0) == 0x50);
assert((peer->rumble_history[i][17] & 0xf0) == 0x50);
}
uni_hid_parser_switch2_play_dual_rumble(&peer->device, 0, 100, 50, 60);
advance(48); // Both original host watchdogs elapse under feedback.
uni_hid_parser_switch2_play_dual_rumble(&peer->device, 0, 0, 0, 0);
advance(1);
assert_silent(peer, 0);
assert_silent(peer, 1);
assert(uni_hid_parser_switch2_haptics_dropped() == drops);
}
static void test_full_fifo_stop_barrier_and_async_completion(void) {
struct fixture_peer* peer = ready_peer(UNI_SW2_JOYCON_R_PID, true);
uni_switch2_haptics_frame_t a = native_frame(3, 0, 8);
uni_switch2_haptics_frame_t b = native_frame(2, 0, 9);
uint32_t drops = uni_hid_parser_switch2_haptics_dropped();
assert(uni_hid_parser_switch2_queue_haptics(&peer->device, &a, now_ms));
advance(1);
assert(peer->query == QUERY_WRITE && peer->rumble_length == 17);
const uint8_t* borrowed = peer->pending_write;
uint8_t snapshot[17];
memcpy(snapshot, borrowed, sizeof(snapshot));
for (unsigned i = 1; i < 16; ++i)
assert(uni_hid_parser_switch2_queue_haptics(&peer->device, &a, now_ms));
assert(!uni_hid_parser_switch2_queue_haptics(&peer->device, &a, now_ms));
assert(uni_hid_parser_switch2_haptics_dropped() == drops);
uni_switch2_haptics_frame_t stop;
uni_switch2_haptics_silence(&stop);
stop.sides[1].count = 0; // Physical Joy-Con stop is not a logical stereo stop.
assert(uni_hid_parser_switch2_queue_haptics(&peer->device, &stop, now_ms - 100));
assert(uni_hid_parser_switch2_haptics_dropped() == drops + 16);
assert(uni_hid_parser_switch2_queue_haptics(&peer->device, &b, now_ms));
advance(1);
assert(memcmp(snapshot, borrowed, sizeof(snapshot)) == 0);
query_done(peer, 0); // Completion of old epoch must not consume b/the stop.
advance(1);
assert(peer->rumbles == 2 && peer->rumble[1] == 0x51);
assert_silent(peer, 0);
query_done(peer, 0);
advance(6);
assert(peer->rumbles == 3 && peer->rumble[1] == 0x62);
assert_block(peer, 0, &b.sides[0]);
query_done(peer, 0);
advance(11);
uni_switch2_haptics_side_t hold = final_hold(&b.sides[0]);
assert_block(peer, 0, &hold);
query_done(peer, 0);
}
static void test_async_expiry_and_topology_reset_do_not_revive_state(void) {
struct fixture_peer* peer = ready_peer(UNI_SW2_PRO_PID, true);
uni_switch2_haptics_frame_t a = native_frame(3, 2, 10);
uint32_t drops = uni_hid_parser_switch2_haptics_dropped();
assert(uni_hid_parser_switch2_queue_haptics(&peer->device, &a, now_ms));
advance(1);
advance(49); // Write request is still borrowing the original batch.
assert(peer->rumbles == 1 && uni_hid_parser_switch2_haptics_dropped() == drops + 1);
query_done(peer, 0);
advance(1);
assert_silent(peer, 0);
assert_silent(peer, 1);
query_done(peer, 0);
uni_hid_parser_switch2_play_dual_rumble(&peer->device, 0, UINT16_MAX, 40, 50);
advance(6);
assert(rumble_active(peer) && peer->query == QUERY_WRITE);
uni_switch2_haptics_frame_t b = native_frame(2, 1, 11);
assert(uni_hid_parser_switch2_queue_haptics(&peer->device, &a, now_ms));
uni_hid_parser_switch2_reset_haptics(&peer->device);
assert(uni_hid_parser_switch2_queue_haptics(&peer->device, &b, now_ms));
advance(1);
query_done(peer, 0); // Old local overlay must not clear the reset neutral.
advance(1);
assert_silent(peer, 0);
assert_silent(peer, 1);
query_done(peer, 0);
advance(6);
assert_block(peer, 0, &b.sides[0]);
assert_block(peer, 1, &b.sides[1]);
query_done(peer, 0);
}
static void test_host_stop_does_not_cancel_local_feedback(void) {
struct fixture_peer* peer = ready_peer(UNI_SW2_PRO_PID, false);
uni_hid_parser_switch2_play_dual_rumble(&peer->device, 0, UINT16_MAX, 100, 200);
uni_switch2_haptics_frame_t a = native_frame(3, 3, 12);
assert(uni_hid_parser_switch2_queue_haptics(&peer->device, &a, now_ms));
advance(1);
assert(uni_hid_parser_switch2_queue_rumble(&peer->device, 0, 0, 0, now_ms));
advance(1);
assert(((rumble_frame(peer) >> 10) & 1023) == 800);
uni_hid_parser_switch2_play_dual_rumble(&peer->device, 0, 0, 0, 0);
advance(1);
assert_silent(peer, 0);
assert_silent(peer, 1);
}
static void test_conventional_host_lifetimes_and_invalid_native_input(void) {
struct fixture_peer* peer = ready_peer(UNI_SW2_JOYCON_L_PID, false);
assert(uni_hid_parser_switch2_queue_rumble(&peer->device, 255, 200, UINT16_MAX, now_ms));
advance(70000);
assert((rumble_frame(peer) & 1023) == 0xe1);
assert(((rumble_frame(peer) >> 20) & 1023) == 0x1e1);
assert(((rumble_frame(peer) >> 10) & 1023) == 800);
assert(((rumble_frame(peer) >> 30) & 1023) == 1020);
assert((peer->rumble[1] & 0xf0) == 0x50);
for (unsigned i = 7; i < 17; ++i)
assert(peer->rumble[i] == 0);
assert(uni_hid_parser_switch2_queue_rumble(&peer->device, 1, 2, 20, now_ms - 10));
advance(9);
assert(((rumble_frame(peer) >> 10) & 1023) == 8);
advance(1);
assert_silent(peer, 0);
uint32_t drops = uni_hid_parser_switch2_haptics_dropped();
uni_switch2_haptics_frame_t invalid = native_frame(1, 0, 13);
invalid.sides[0].count = 4;
assert(uni_hid_parser_switch2_queue_haptics(&peer->device, &invalid, now_ms));
uni_switch2_haptics_frame_t stale = native_frame(1, 0, 14);
assert(uni_hid_parser_switch2_queue_haptics(&peer->device, &stale, now_ms - 50));
assert(uni_hid_parser_switch2_queue_rumble(&peer->device, 10, 20, 100, now_ms - 50));
assert(uni_hid_parser_switch2_haptics_dropped() == drops + 3);
advance(13);
assert_silent(peer, 0);
}
static void test_native_watchdog_clock_wrap(void) {
reset();
now_ms = UINT32_MAX - 20;
struct fixture_peer* peer = connect_peer(UNI_SW2_JOYCON_L_PID, false);
discover(peer);
subscribe_response(peer);
finish_setup(peer);
uni_switch2_haptics_frame_t frame = native_frame(1, 0, 15);
assert(uni_hid_parser_switch2_queue_haptics(&peer->device, &frame, now_ms));
advance(49);
assert_block(peer, 0, &frame.sides[0]);
advance(1);
assert_silent(peer, 0);
}
static void test_identical_hold_coalescing_refreshes_borrowed_head(void) {
struct fixture_peer* peer = ready_peer(UNI_SW2_JOYCON_L_PID, true);
uni_switch2_haptics_frame_t frame = native_frame(1, 0, 16);
uint32_t drops = uni_hid_parser_switch2_haptics_dropped();
assert(uni_hid_parser_switch2_queue_haptics(&peer->device, &frame, now_ms));
advance(1);
const uint8_t* borrowed = peer->pending_write;
uint8_t snapshot[17];
memcpy(snapshot, borrowed, sizeof(snapshot));
advance(39);
for (unsigned i = 0; i < 32; ++i)
assert(uni_hid_parser_switch2_queue_haptics(&peer->device, &frame, now_ms));
assert(memcmp(snapshot, borrowed, sizeof(snapshot)) == 0);
advance(10); // The original t0 hold would expire now without source refresh.
assert(uni_hid_parser_switch2_haptics_dropped() == drops);
query_done(peer, 0);
advance(39);
assert_block(peer, 0, &frame.sides[0]);
query_done(peer, 0);
advance(1); // Refreshed deadline remains t40+50, not ATT completion+50.
assert_silent(peer, 0);
assert(uni_hid_parser_switch2_haptics_dropped() == drops);
query_done(peer, 0);
}
static void test_hold_coalescing_requires_identical_samples_and_side_masks(void) {
struct fixture_peer* peer = ready_peer(UNI_SW2_PRO_PID, true);
uni_switch2_haptics_frame_t frame;
for (unsigned i = 0; i < 16; ++i) {
frame = native_frame(1, 1, i + 1);
assert(uni_hid_parser_switch2_queue_haptics(&peer->device, &frame, now_ms));
}
// An identical tail hold can refresh even at capacity. A partial-side update
// is a different command, as is a changed sample; neither may erase history.
assert(uni_hid_parser_switch2_queue_haptics(&peer->device, &frame, now_ms));
frame.sides[1].count = 0;
assert(!uni_hid_parser_switch2_queue_haptics(&peer->device, &frame, now_ms));
frame = native_frame(1, 1, 17);
assert(!uni_hid_parser_switch2_queue_haptics(&peer->device, &frame, now_ms));
advance(1);
uni_switch2_haptics_frame_t first = native_frame(1, 1, 1);
assert_block(peer, 0, &first.sides[0]);
assert_block(peer, 1, &first.sides[1]);
}
static void test_expired_history_does_not_starve_fresh_sequences(void) {
struct fixture_peer* peer = ready_peer(UNI_SW2_PRO_PID, false);
uni_switch2_haptics_frame_t a = native_frame(3, 3, 20);
uni_switch2_haptics_frame_t stale = native_frame(3, 3, 21);
uni_switch2_haptics_frame_t fresh = native_frame(3, 3, 22);
assert(uni_hid_parser_switch2_queue_haptics(&peer->device, &a, now_ms));
advance(1);
unsigned sent = peer->rumbles;
assert(uni_hid_parser_switch2_queue_haptics(&peer->device, &stale, now_ms - 40));
assert(uni_hid_parser_switch2_queue_haptics(&peer->device, &fresh, now_ms));
advance(10);
assert(peer->rumbles == sent); // Unplayed stale history must not interrupt A.
advance(6);
assert_block(peer, 0, &fresh.sides[0]);
assert_block(peer, 1, &fresh.sides[1]);
peer = ready_peer(UNI_SW2_PRO_PID, false);
a = native_frame(1, 1, 23);
assert(uni_hid_parser_switch2_queue_haptics(&peer->device, &a, now_ms - 43));
advance(1);
assert(uni_hid_parser_switch2_queue_haptics(&peer->device, &fresh, now_ms));
advance(6); // A's source watchdog expires as its playback guard ends.
assert_block(peer, 0, &fresh.sides[0]); // No unnecessary silent/HOLD packet.
peer = ready_peer(UNI_SW2_PRO_PID, false);
uint32_t drops = uni_hid_parser_switch2_haptics_dropped();
assert(uni_hid_parser_switch2_queue_haptics(&peer->device, &fresh, now_ms - 40));
advance(1);
assert_silent(peer, 0); // Nine ms cannot contain a complete three-frame batch.
assert(uni_hid_parser_switch2_haptics_dropped() == drops + 1);
}
static void test_gatt_busy_retries_without_disconnect_or_sequence_loss(void) {
struct fixture_peer* peer = ready_peer(UNI_SW2_PRO_PID, false);
uni_switch2_haptics_frame_t frame = native_frame(3, 2, 24);
uint32_t drops = uni_hid_parser_switch2_haptics_dropped();
next_write_error = GATT_CLIENT_BUSY;
assert(uni_hid_parser_switch2_queue_haptics(&peer->device, &frame, now_ms));
advance(1);
assert(!disconnected && peer->rumbles == 0);
advance(13);
assert(!disconnected && peer->rumbles == 1);
assert((peer->rumble[1] & 15) == 0);
assert_block(peer, 0, &frame.sides[0]);
assert_block(peer, 1, &frame.sides[1]);
// The SDK uses the same busy status on command writes such as player LEDs.
next_write_error = GATT_CLIENT_BUSY;
uni_hid_parser_switch2_set_player_leds(&peer->device, 3);
assert(!disconnected);
advance(13);
assert(peer->command[0] == 0x09 && peer->command[8] == 3);
acknowledge(peer, false);
assert(!disconnected && uni_hid_parser_switch2_haptics_dropped() == drops);
}
static void test_immediate_connection_failure_preserves_reconnect_discovery(void) {
reset();
uint8_t packet[64];
size_t size = advertisement(packet, UNI_SW2_JOYCON_R_PID, false);
next_connect_error = ERROR_CODE_COMMAND_DISALLOWED;
assert(uni_bt_le_switch2_handle_advertisement(packet, size));
assert(scan_running && !peers[0].used && !timer_count && !listeners);
assert(uni_bt_le_switch2_handle_advertisement(packet, size));
assert(peers[0].used && connected == 2); // The same remembered peer can retry.
reset();
scan_enabled = scan_running = false;
next_connect_error = ERROR_CODE_COMMAND_DISALLOWED;
assert(uni_bt_le_switch2_handle_advertisement(packet, size));
assert(!scan_running && !peers[0].used); // Explicit stop must not be overridden.
}
static void test_idle_neutral_output_quiesces_and_resumes(void) {
struct fixture_peer* peer = ready_peer(UNI_SW2_JOYCON_L_PID, false);
advance(100);
assert(peer->rumbles == 3 && !rumble_active(peer));
unsigned idle_writes = peer->rumbles;
advance(1300);
assert(peer->rumbles == idle_writes);
// Repeated host stops must not restart an already completed neutral burst.
for (unsigned i = 0; i < 20; ++i) {
assert(uni_hid_parser_switch2_queue_rumble(&peer->device, 0, 0, 0, now_ms));
advance(8);
}
assert(peer->rumbles == idle_writes);
// Quiet output must not delay a future effect or a finite watchdog stop.
uni_hid_parser_switch2_play_dual_rumble(&peer->device, 20, 50, 40, 80);
advance(19);
assert(peer->rumbles == idle_writes);
advance(1);
assert(rumble_active(peer));
advance(20);
assert(peer->rumbles > idle_writes + 1 && rumble_active(peer));
advance(30);
assert(!rumble_active(peer));
advance(100);
idle_writes = peer->rumbles;
advance(1300);
assert(peer->rumbles == idle_writes);
uni_switch2_haptics_frame_t frame = native_frame(3, 0, 8);
assert(uni_hid_parser_switch2_queue_haptics(&peer->device, &frame, now_ms));
advance(1);
assert_block(peer, 0, &frame.sides[0]);
advance(100);
assert_silent(peer, 0);
idle_writes = peer->rumbles;
advance(1300);
assert(peer->rumbles == idle_writes);
}
static void test_neutral_budget_waits_for_success_and_stale_active_completion(void) {
struct fixture_peer* peer = ready_peer(UNI_SW2_JOYCON_R_PID, true);
next_write_error = BTSTACK_ACL_BUFFERS_FULL;
advance(1);
assert(peer->rumbles == 0);
advance(13);
assert(peer->rumbles == 1 && peer->query == QUERY_WRITE);
advance(100);
assert(peer->rumbles == 1); // Pending ATT writes cannot count as repeated stops.
query_done(peer, 0);
advance(13);
assert(peer->rumbles == 2);
query_done(peer, 0);
advance(13);
assert(peer->rumbles == 3);
query_done(peer, 0);
advance(100);
assert(peer->rumbles == 3 && peer->query == QUERY_NONE);
// Reset while an active write borrows its buffer. Its stale completion must
// invalidate the earlier idle state and require a fresh stop burst.
uni_hid_parser_switch2_play_dual_rumble(&peer->device, 0, UINT16_MAX, 40, 80);
advance(1);
assert(rumble_active(peer) && peer->query == QUERY_WRITE);
uni_hid_parser_switch2_reset_haptics(&peer->device);
query_done(peer, 0);
for (unsigned i = 0; i < 3; ++i) {
advance(i ? 13 : 1);
assert(peer->rumbles == 5 + i && peer->query == QUERY_WRITE);
assert_silent(peer, 0);
query_done(peer, 0);
}
advance(100);
assert(peer->rumbles == 7 && peer->query == QUERY_NONE);
}
int main(void) {
#if SWITCH2_BRIDGE_FULL_INPUT
test_native_independent_motion_and_lifetime();
#endif
test_connected_callback_rejection();
test_advertisement_bounds_and_admission();
test_discovery_metadata_and_rejection_isolation();
test_immediate_connection_failure_preserves_reconnect_discovery();
test_missing_descriptor_and_setup_timeout();
test_pairing_gate_and_write_ack_order();
test_calibration_physical_inputs_and_sensor_units();
test_rumble_delay_expiry_retry_and_teardown();
test_write_request_buffers_and_failed_completion();
test_native_fifo_stereo_counts_and_retry();
test_native_hold_and_absent_side_watchdogs();
test_feedback_advances_host_and_resumes_only_final_samples();
test_full_fifo_stop_barrier_and_async_completion();
test_async_expiry_and_topology_reset_do_not_revive_state();
test_host_stop_does_not_cancel_local_feedback();
test_conventional_host_lifetimes_and_invalid_native_input();
test_native_watchdog_clock_wrap();
test_identical_hold_coalescing_refreshes_borrowed_head();
test_hold_coalescing_requires_identical_samples_and_side_masks();
test_expired_history_does_not_starve_fresh_sequences();
test_gatt_busy_retries_without_disconnect_or_sequence_loss();
test_idle_neutral_output_quiesces_and_resumes();
test_neutral_budget_waits_for_success_and_stale_active_completion();
reset();
puts("Switch2 protocol boundaries, setup failure, pairing, calibration, physical input, motion and rumble passed");
return 0;
}