#include #include #include #include #include #include "parser/uni_hid_parser_wii.h" #include "parser/uni_hid_parser_native_motion.h" #include "uni_hid_device.h" // The real staged parser and gamepad definitions are linked. Only transport and // the ready notification are substituted; no parser-private state is inspected. // Wire fixtures follow WiiBrew's Wiimote and Wii Motion Plus protocol pages. typedef struct { uint16_t len; uint8_t bytes[32]; } transaction_t; typedef enum { EXT_NONE, EXT_NUNCHUK, EXT_WII_U_PRO } extension_t; typedef struct { uni_hid_device_t device; transaction_t sent[128]; unsigned sent_count; unsigned cursor; unsigned ready_count; extension_t extension; bool motionplus; bool mp_initialized; bool mp_active; bool status_before_activation_ack; bool status_after_activation_ack; bool deactivation_status_before_ack; bool deactivation_status_after_ack; unsigned initializing_extension_reads; unsigned transient_extension_read_errors; uint8_t extension_identity[6]; bool fail_accel_reads; uint8_t connection_buttons; uint8_t activation_mode; uint8_t report_mode; uint32_t fail_read_address; uint32_t fail_write_address; uint8_t accel_calibration[20]; uint8_t mp_calibration[32]; uint8_t nunchuk_calibration[16]; } fixture_t; static fixture_t f; void uni_hid_device_send_intr_report(uni_hid_device_t* d, const uint8_t* report, uint16_t len) { assert(d == &f.device); assert(f.sent_count < 128 && len >= 2 && len <= sizeof(f.sent[0].bytes)); assert(report[0] == 0xa2); transaction_t* transaction = &f.sent[f.sent_count++]; transaction->len = len; memcpy(transaction->bytes, report, len); } bool uni_hid_device_set_ready_complete(uni_hid_device_t* d) { assert(d == &f.device); ++f.ready_count; d->conn.state = UNI_BT_CONN_STATE_DEVICE_READY; return true; } void uni_log(const char* fmt, ...) { (void)fmt; } void printf_hexdump(const void* data, int len) { (void)data; (void)len; } // This parser fixture does not schedule rumble. Teardown still asks BTstack // to remove both timers; neither is present in the fixture's empty timer list. bool btstack_run_loop_remove_timer(btstack_timer_source_t* timer) { assert(timer != NULL); return false; } static void put_be16(uint8_t* output, uint16_t value) { output[0] = value >> 8; output[1] = value; } static void put_le16(uint8_t* output, uint16_t value) { output[0] = value; output[1] = value >> 8; } static void pack_accel_calibration(uint8_t* output, const uint16_t values[3]) { output[3] = 0; for (unsigned axis = 0; axis < 3; ++axis) { output[axis] = values[axis] >> 2; output[3] |= (values[axis] & 3) << (4 - 2 * axis); } } static void update_mp_checksum(void) { // CRC32 covers fast[0..13] followed by slow[0..13], not the CRC slots. uint32_t crc = UINT32_MAX; for (unsigned block = 0; block < 2; ++block) { for (unsigned byte = 0; byte < 14; ++byte) { crc ^= f.mp_calibration[block * 16 + byte]; for (unsigned bit = 0; bit < 8; ++bit) crc = (crc >> 1) ^ ((crc & 1) ? UINT32_C(0xedb88320) : 0); } } crc ^= UINT32_MAX; put_be16(&f.mp_calibration[14], crc >> 16); put_be16(&f.mp_calibration[30], crc); } static void update_nunchuk_checksum(void) { uint8_t checksum = 0x55; for (unsigned i = 0; i < 14; ++i) checksum += f.nunchuk_calibration[i]; f.nunchuk_calibration[14] = checksum; f.nunchuk_calibration[15] = checksum + 0x55; } static void set_nunchuk_stick_calibration(const uint8_t values[6]) { memcpy(f.nunchuk_calibration + 8, values, 6); update_nunchuk_checksum(); } static void set_nunchuk_accel_calibration(const uint16_t zero[3], const uint16_t one_g[3]) { pack_accel_calibration(f.nunchuk_calibration, zero); pack_accel_calibration(f.nunchuk_calibration + 4, one_g); update_nunchuk_checksum(); } static void reset_fixture(uint16_t product_id, bool motionplus, extension_t extension) { memset(&f, 0, sizeof(f)); f.device.vendor_id = 0x057e; f.device.product_id = product_id; f.device.report_parser.setup = uni_hid_parser_wii_setup; f.device.conn.connected = true; f.device.conn.interrupt_cid = 0x40; f.extension = extension; f.motionplus = motionplus; f.fail_read_address = UINT32_MAX; f.fail_write_address = UINT32_MAX; const uint8_t nunchuk_identity[6] = {0, 0, 0xa4, 0x20, 0, 0}; memcpy(f.extension_identity, nunchuk_identity, sizeof(nunchuk_identity)); set_nunchuk_stick_calibration((const uint8_t[]){224, 32, 128, 224, 32, 128}); // Dolphin Nunchuk::CalibrationData uses the same packed 10-bit points as // the Remote, but its own centers and sensitivities. set_nunchuk_accel_calibration((const uint16_t[]){501, 510, 519}, (const uint16_t[]){693, 766, 647}); // Non-default centers, unequal spans, and nonzero packed low bits catch // nominal 0x200/100-count calibration and high-byte-only decoding. const uint16_t zero[3] = {510, 506, 514}; const uint16_t one_g[3] = {614, 634, 594}; pack_accel_calibration(f.accel_calibration, zero); pack_accel_calibration(&f.accel_calibration[4], one_g); uint8_t checksum = 0x55; for (unsigned byte = 0; byte < 9; ++byte) checksum += f.accel_calibration[byte]; f.accel_calibration[9] = checksum; memcpy(&f.accel_calibration[10], f.accel_calibration, 10); // MP words have 16-bit precision; reports have 14 bits. Factory yaw and // pitch spans are negative, whereas roll is positive. Fast and slow also // deliberately have different centers and sensitivities on every axis. const uint16_t zero_fast[3] = {32000, 31600, 31200}; const uint16_t scale_fast[3] = {22400, 36400, 28000}; const uint16_t zero_slow[3] = {32400, 32000, 31600}; const uint16_t scale_slow[3] = {21600, 37400, 28000}; for (unsigned axis = 0; axis < 3; ++axis) { put_be16(&f.mp_calibration[2 * axis], zero_fast[axis]); put_be16(&f.mp_calibration[6 + 2 * axis], scale_fast[axis]); put_be16(&f.mp_calibration[16 + 2 * axis], zero_slow[axis]); put_be16(&f.mp_calibration[22 + 2 * axis], scale_slow[axis]); } f.mp_calibration[12] = 200; // Fast calibration is at 1200 degrees/s. f.mp_calibration[28] = 45; // Slow calibration is at 270 degrees/s. f.mp_calibration[13] = 0x31; f.mp_calibration[29] = 0x72; update_mp_checksum(); } static void feed(const uint8_t* report, uint16_t len) { uni_hid_parser_wii_init_report(&f.device); uni_hid_parser_wii_parse_input_report(&f.device, report, len); } static void send_status(bool extension_present) { const uint8_t report[] = {0x20, f.connection_buttons, 0, extension_present ? 2 : 0, 0, 0, 0xc0}; feed(report, sizeof(report)); } static void send_ack(uint8_t command, uint8_t error) { const uint8_t report[] = {0x22, 0, 0, command, error}; feed(report, sizeof(report)); } static uint32_t transaction_address(const transaction_t* transaction) { assert(transaction->len >= 8); const uint8_t* bytes = transaction->bytes; return ((uint32_t)bytes[3] << 16) | ((uint32_t)bytes[4] << 8) | bytes[5]; } static void send_read_reply(uint16_t address, const uint8_t* data, uint8_t size, uint8_t error) { assert(size >= 1 && size <= 16); uint8_t report[22] = {0x21, 0, 0, (uint8_t)(((size - 1) << 4) | error)}; put_be16(&report[4], address); if (!error) memcpy(&report[6], data, size); feed(report, sizeof(report)); } static bool read_wire_byte(uint8_t space, uint32_t address, uint8_t* value) { if (space == 0) { if (address >= 0x16 && address < 0x2a && !f.fail_accel_reads) { *value = f.accel_calibration[address - 0x16]; return true; } return false; } assert(space == 4); uint32_t bank = address >> 16; uint16_t offset = address; if ((bank == 0xa6 && f.motionplus && !f.mp_active) || (bank == 0xa4 && f.mp_active)) { if (offset >= 0x20 && offset < 0x40) { *value = f.mp_calibration[offset - 0x20]; return true; } if (offset >= 0xfa && offset <= 0xff) { // RVL-CNT-01-TR hardware returns an A4 signature even when read // through inactive A600FA; the ID is not an echo of the read bank. const bool integrated = f.device.product_id == 0x0330; const uint8_t identity[] = {integrated ? 1 : 0, 0, integrated ? 0xa4 : (uint8_t)bank, 0x20, f.mp_active ? f.activation_mode : 0, 5}; *value = identity[offset - 0xfa]; return true; } } if (bank == 0xa4 && !f.mp_active && f.extension == EXT_NUNCHUK && offset >= 0x20 && offset < 0x30) { *value = f.nunchuk_calibration[offset - 0x20]; return true; } if (bank == 0xa4 && !f.mp_active && f.extension != EXT_NONE && offset >= 0xfa && offset <= 0xff) { uint8_t identity[6]; memcpy(identity, f.extension_identity, sizeof(identity)); if (f.extension == EXT_WII_U_PRO) { identity[4] = 1; identity[5] = 0x20; } *value = identity[offset - 0xfa]; return true; } return false; } static void answer_transaction(const transaction_t* transaction) { const uint8_t* bytes = transaction->bytes; switch (bytes[1]) { case 0x11: // LEDs do not acknowledge unless explicitly requested. if (bytes[2] & 2) send_ack(0x11, 0); break; case 0x12: assert(transaction->len == 4); f.report_mode = bytes[3]; if (bytes[2] & 2) send_ack(0x12, 0); break; case 0x15: send_status(f.extension != EXT_NONE || f.mp_active); break; case 0x16: { uint32_t address = transaction_address(transaction); assert((bytes[2] & 0xfe) == 4); assert(bytes[6] == 1 && transaction->len >= 8); uint8_t error = 0; bool deactivated = false; bool activated = false; if (address == f.fail_write_address || ((address >> 16) == 0xa6 && !f.motionplus)) { error = 7; } else if (address == 0xa600f0) { assert(bytes[7] == 0x55); f.mp_initialized = true; } else if (address == 0xa600fe) { // Initialization before activation and the passthrough selector // are protocol requirements, not a pinned parser FSM sequence. assert(f.mp_initialized); assert(bytes[7] == 4 || bytes[7] == 5); f.mp_active = true; f.activation_mode = bytes[7]; activated = true; if (f.status_before_activation_ack) send_status(true); } else if (address == 0xa400f0) { assert(bytes[7] == 0x55); deactivated = f.mp_active; f.mp_active = false; if (deactivated && f.deactivation_status_before_ack) { send_status(false); send_status(true); } } else { assert((address == 0xa400fb || address == 0xa600fb) && bytes[7] == 0); } send_ack(0x16, error); if (deactivated && f.deactivation_status_after_ack) { send_status(false); send_status(true); } if (activated && f.status_after_activation_ack) send_status(true); break; } case 0x17: { uint32_t address = transaction_address(transaction); uint16_t remaining = ((uint16_t)bytes[6] << 8) | bytes[7]; assert(remaining && remaining <= 32); if (address == 0xa400fa && !f.mp_active && f.extension != EXT_NONE) { if (f.transient_extension_read_errors) { --f.transient_extension_read_errors; send_read_reply(address, NULL, 1, 7); break; } if (f.initializing_extension_reads) { --f.initializing_extension_reads; const uint8_t initializing[6] = {0xff, 0xff, 0xff, 0xff, 0xff, 0xff}; send_read_reply(address, initializing, sizeof(initializing), 0); break; } } if (address == f.fail_read_address) { send_read_reply(address, NULL, 1, 7); break; } while (remaining) { uint8_t data[16]; uint8_t size = remaining > 16 ? 16 : remaining; bool available = true; for (unsigned i = 0; i < size; ++i) available &= read_wire_byte(bytes[2] & 0xfe, address + i, &data[i]); send_read_reply(address, data, size, available ? 0 : 7); if (!available) break; address += size; remaining -= size; } break; } default: assert(!"unexpected Wii output report"); } } static void finish_setup(void) { // Output may be emitted recursively by each reply. Bound the wire dialogue // so retry loops fail instead of hanging pytest. for (unsigned step = 0; step < 128 && f.cursor < f.sent_count; ++step) { transaction_t transaction = f.sent[f.cursor++]; answer_transaction(&transaction); } assert(f.cursor == f.sent_count); assert(f.ready_count == 1); assert(f.report_mode != 0); } static void connect_device(void) { uni_hid_parser_wii_setup(&f.device); finish_setup(); } static transaction_t stop_at_read(uint32_t address) { for (unsigned step = 0; step < 128 && f.cursor < f.sent_count; ++step) { transaction_t transaction = f.sent[f.cursor++]; if (transaction.bytes[1] == 0x17 && transaction_address(&transaction) == address) return transaction; answer_transaction(&transaction); } assert(!"required Wii memory request was not sent"); return (transaction_t){0}; } static void put_accel_report(uint8_t* report, uint16_t x, uint16_t y, uint16_t z) { assert(!(y & 1) && !(z & 1)); // Y/Z wire samples have no least-significant bit. report[1] |= (x & 3) << 5; report[2] |= ((y & 2) << 4) | ((z & 2) << 5); report[3] = x >> 2; report[4] = y >> 2; report[5] = z >> 2; } static void put_gyro_report(uint8_t* extension, uint16_t yaw, uint16_t roll, uint16_t pitch, bool yaw_slow, bool roll_slow, bool pitch_slow) { extension[0] = yaw; extension[1] = roll; extension[2] = pitch; extension[3] = ((yaw >> 8) << 2) | (yaw_slow ? 2 : 0) | (pitch_slow ? 1 : 0); extension[4] = ((roll >> 8) << 2) | (roll_slow ? 2 : 0) | (f.extension != EXT_NONE ? 1 : 0); extension[5] = ((pitch >> 8) << 2) | 2; } static void send_motion(uint16_t yaw, uint16_t roll, uint16_t pitch, bool yaw_slow, bool roll_slow, bool pitch_slow) { uint8_t report[22] = {0x35, 1, 0x0a}; // Left + A + 1, no connection-time A needed. put_accel_report(report, 614, 442, 534); put_gyro_report(&report[6], yaw, roll, pitch, yaw_slow, roll_slow, pitch_slow); feed(report, sizeof(report)); } static void expect_vector(const int32_t vector[3], int32_t x, int32_t y, int32_t z) { assert(vector[0] == x); assert(vector[1] == y); assert(vector[2] == z); } static void expect_accel(void) { // Native (+1g, -0.5g, +0.25g) maps to SDL (-X, +Z, +Y). expect_vector(f.device.controller.gamepad.accel, -8192, 2048, -4096); } static void send_core_and_accel(void) { uint8_t report[22] = {f.report_mode, 1, 0x0a}; assert(f.report_mode == 0x30 || f.report_mode == 0x31 || f.report_mode == 0x35); uint16_t len = f.report_mode == 0x35 ? 22 : f.report_mode == 0x31 ? 6 : 3; if (len >= 6) put_accel_report(report, 614, 442, 534); feed(report, len); assert(f.device.controller.gamepad.dpad == DPAD_DOWN); assert(f.device.controller.gamepad.buttons == (BUTTON_X | BUTTON_A)); } static void send_nunchuk_controls(bool passthrough) { uint8_t report[22] = {0x35}; put_accel_report(report, 614, 442, 534); const uint8_t extension[6] = {160, 192, 128, 128, passthrough ? 129 : 128, passthrough ? 4 : 1}; memcpy(report + 6, extension, sizeof(extension)); feed(report, sizeof(report)); assert(f.device.controller.gamepad.buttons == BUTTON_X); assert(f.device.controller.gamepad.axis_x == 170 && f.device.controller.gamepad.axis_y == -341); assert(f.device.controller.gamepad.axis_rx == 0 && f.device.controller.gamepad.axis_ry == 0); assert(f.device.controller_subtype == CONTROLLER_SUBTYPE_WIIMOTE_NUNCHUK_ACCEL); expect_accel(); } static void send_nunchuk_sample(const uint8_t extension[6]) { // Extension-only input proves its sequence does not need Remote accel. uint8_t report[11] = {0x32}; memcpy(report + 3, extension, 6); feed(report, sizeof(report)); } static uint32_t expect_nunchuk_accel(int32_t x, int32_t y, int32_t z) { int32_t acceleration[3]; uint32_t sequence; assert(uni_hid_parser_wii_nunchuk_accel_snapshot(&f.device, acceleration, &sequence)); expect_vector(acceleration, x, y, z); return sequence; } static void integrated_motionplus_calibration_and_slow_bits(void) { reset_fixture(0x0330, true, EXT_NONE); connect_device(); assert(f.mp_active && f.activation_mode == 4 && f.report_mode == 0x35); // Each axis is the only slow axis in one packet. A shared slow flag or // swapped e[3]/e[4] bits cannot accidentally pass all three vectors. send_motion(7760, 8200, 7560, false, true, false); expect_vector(f.device.controller.gamepad.gyro, 360 * 1024, -120 * 1024, 40 * 1024); expect_accel(); assert(f.device.controller.gamepad.dpad == DPAD_DOWN); assert(f.device.controller.gamepad.buttons == (BUTTON_X | BUTTON_A)); send_motion(8300, 7660, 7560, true, false, false); expect_vector(f.device.controller.gamepad.gyro, 360 * 1024, 20 * 1024, -240 * 1024); send_motion(7760, 7660, 8100, false, false, true); expect_vector(f.device.controller.gamepad.gyro, -60 * 1024, -120 * 1024, -240 * 1024); } static void external_motionplus_without_extension_status(void) { reset_fixture(0x0306, true, EXT_NONE); f.status_before_activation_ack = true; connect_device(); assert(f.mp_active && f.activation_mode == 4 && f.report_mode == 0x35); send_motion(7760, 8200, 7560, false, true, false); expect_vector(f.device.controller.gamepad.gyro, 360 * 1024, -120 * 1024, 40 * 1024); expect_accel(); } static void absent_motionplus_keeps_calibrated_remote(void) { reset_fixture(0x0306, false, EXT_NONE); connect_device(); assert(!f.mp_active && f.report_mode == 0x31); send_core_and_accel(); expect_accel(); expect_vector(f.device.controller.gamepad.gyro, 0, 0, 0); } static void accelerometer_snapshot_requires_fresh_calibrated_reports(void) { reset_fixture(0x0306, false, EXT_NONE); connect_device(); int32_t acceleration[3]; uint32_t sequence; assert(!uni_hid_parser_wii_accel_snapshot(&f.device, acceleration, &sequence)); send_core_and_accel(); assert(uni_hid_parser_wii_accel_snapshot(&f.device, acceleration, &sequence)); expect_vector(acceleration, -8192, 2048, -4096); const uint32_t first = sequence; #if SWITCH2_BRIDGE_FULL_INPUT uni_native_motion_snapshot_t native; assert(uni_hid_parser_native_motion_snapshot(&f.device, &native)); assert(native.accel_valid && !native.gyro_valid && !native.report_tracked); expect_vector(native.accel_q13, -8192, 2048, -4096); #endif send_ack(0x16, 0); const uint8_t short_report[5] = {0x31}; feed(short_report, sizeof(short_report)); assert(uni_hid_parser_wii_accel_snapshot(&f.device, acceleration, &sequence)); assert(sequence == first); #if SWITCH2_BRIDGE_FULL_INPUT assert(uni_hid_parser_native_motion_snapshot(&f.device, &native)); assert(native.accel_sequence == first && !native.gyro_valid); #endif send_core_and_accel(); assert(uni_hid_parser_wii_accel_snapshot(&f.device, acceleration, &sequence)); assert(sequence != first); // Identical readings can still be fresh. reset_fixture(0x0306, false, EXT_NONE); f.fail_accel_reads = true; connect_device(); send_core_and_accel(); assert(!uni_hid_parser_wii_accel_snapshot(&f.device, acceleration, &sequence)); } static void gyro_snapshot_advances_only_on_calibrated_motionplus_packets(void) { reset_fixture(0x0330, true, EXT_NUNCHUK); connect_device(); int32_t gyro[3]; uint32_t sequence; assert(!uni_hid_parser_wii_gyro_snapshot(&f.device, gyro, &sequence)); send_nunchuk_controls(true); assert(!uni_hid_parser_wii_gyro_snapshot(&f.device, gyro, &sequence)); send_motion(7760, 8200, 7560, false, true, false); assert(uni_hid_parser_wii_gyro_snapshot(&f.device, gyro, &sequence)); expect_vector(gyro, 360 * 1024, -120 * 1024, 40 * 1024); const uint32_t first = sequence; #if SWITCH2_BRIDGE_FULL_INPUT uni_native_motion_snapshot_t native; assert(uni_hid_parser_native_motion_snapshot(&f.device, &native)); assert(native.accel_valid && native.gyro_valid); expect_vector(native.gyro_q10, 360 * 1024, -120 * 1024, 40 * 1024); #endif send_nunchuk_controls(true); send_ack(0x16, 0); send_status(true); finish_setup(); uint8_t short_report[11] = {0x35}; feed(short_report, sizeof(short_report)); assert(uni_hid_parser_wii_gyro_snapshot(&f.device, gyro, &sequence)); assert(sequence == first); expect_vector(gyro, 360 * 1024, -120 * 1024, 40 * 1024); #if SWITCH2_BRIDGE_FULL_INPUT assert(uni_hid_parser_native_motion_snapshot(&f.device, &native)); assert(native.gyro_sequence == first); #endif send_motion(7760, 8200, 7560, false, true, false); assert(uni_hid_parser_wii_gyro_snapshot(&f.device, gyro, &sequence)); assert(sequence != first); // Equal values do not mean a duplicate packet. reset_fixture(0x0330, true, EXT_NONE); f.mp_calibration[30] ^= 1; connect_device(); send_motion(7760, 8200, 7560, false, true, false); assert(!uni_hid_parser_wii_gyro_snapshot(&f.device, gyro, &sequence)); assert(uni_hid_parser_wii_accel_snapshot(&f.device, gyro, &sequence)); } static void gyro_snapshot_invalidates_on_topology_and_teardown(void) { reset_fixture(0x0306, true, EXT_NONE); connect_device(); send_motion(7760, 8200, 7560, false, true, false); int32_t gyro[3]; uint32_t sequence; assert(uni_hid_parser_wii_gyro_snapshot(&f.device, gyro, &sequence)); const uint32_t first = sequence; assert(uni_hid_parser_wii_rumble_ready(&f.device)); f.extension = EXT_NUNCHUK; send_motion(7760, 8200, 7560, false, true, false); assert(!uni_hid_parser_wii_gyro_snapshot(&f.device, gyro, &sequence)); assert(!uni_hid_parser_wii_rumble_ready(&f.device)); finish_setup(); assert(uni_hid_parser_wii_rumble_ready(&f.device)); send_nunchuk_controls(true); assert(!uni_hid_parser_wii_gyro_snapshot(&f.device, gyro, &sequence)); send_motion(7760, 8200, 7560, false, true, false); assert(uni_hid_parser_wii_gyro_snapshot(&f.device, gyro, &sequence)); assert(sequence > first); // Topology setup cannot reuse a pre-hotplug ID. f.extension = EXT_NONE; f.motionplus = false; f.mp_active = false; send_status(false); finish_setup(); assert(!uni_hid_parser_wii_gyro_snapshot(&f.device, gyro, &sequence)); send_core_and_accel(); assert(uni_hid_parser_wii_accel_snapshot(&f.device, gyro, &sequence)); f.motionplus = true; send_status(true); finish_setup(); assert(!uni_hid_parser_wii_gyro_snapshot(&f.device, gyro, &sequence)); send_motion(7760, 8200, 7560, false, true, false); // The first gyro packet resolves the active MP's downstream topology, // which the attachment status bit alone cannot distinguish. finish_setup(); send_motion(7760, 8200, 7560, false, true, false); assert(uni_hid_parser_wii_gyro_snapshot(&f.device, gyro, &sequence)); assert(sequence > first); uni_hid_parser_wii_teardown(&f.device); assert(!uni_hid_parser_wii_gyro_snapshot(&f.device, gyro, &sequence)); assert(!uni_hid_parser_wii_rumble_ready(&f.device)); #if SWITCH2_BRIDGE_FULL_INPUT uni_native_motion_snapshot_t retired; assert(uni_hid_parser_native_motion_snapshot(&f.device, &retired)); assert(!retired.accel_valid && !retired.gyro_valid); #endif f.ready_count = 0; // A new parser connection gets its own ready notification. uni_hid_parser_wii_setup(&f.device); finish_setup(); assert(!uni_hid_parser_wii_gyro_snapshot(&f.device, gyro, &sequence)); #if SWITCH2_BRIDGE_FULL_INPUT send_motion(7760, 8200, 7560, false, true, false); // An already-active MP first resolves its downstream topology after reconnect. finish_setup(); send_motion(7760, 8200, 7560, false, true, false); assert(uni_hid_parser_native_motion_snapshot(&f.device, &retired)); assert(retired.gyro_valid && retired.gyro_sequence != first); #endif } static void setup_read_and_write_errors_leave_buttons_ready(void) { reset_fixture(0x0330, true, EXT_NONE); f.fail_read_address = 0xa60030; connect_device(); send_core_and_accel(); expect_vector(f.device.controller.gamepad.gyro, 0, 0, 0); reset_fixture(0x0330, true, EXT_NONE); f.fail_write_address = 0xa600fe; connect_device(); assert(!f.mp_active); send_core_and_accel(); expect_vector(f.device.controller.gamepad.gyro, 0, 0, 0); reset_fixture(0x0306, false, EXT_NUNCHUK); f.fail_write_address = 0xa400f0; connect_device(); // A failed reset ACK is not an extension identity. If the subsequent read // succeeds, preserve the real Nunchuk rather than forcing standalone mode. send_nunchuk_controls(false); expect_vector(f.device.controller.gamepad.gyro, 0, 0, 0); } static void failed_calibration_never_fabricates_motion(void) { reset_fixture(0x0330, true, EXT_NONE); f.accel_calibration[9] ^= 1; f.accel_calibration[19] ^= 1; connect_device(); send_motion(7760, 8200, 7560, false, true, false); expect_vector(f.device.controller.gamepad.accel, 0, 0, 0); expect_vector(f.device.controller.gamepad.gyro, 360 * 1024, -120 * 1024, 40 * 1024); assert(f.device.controller.gamepad.buttons == (BUTTON_X | BUTTON_A)); reset_fixture(0x0330, true, EXT_NONE); f.mp_calibration[30] ^= 1; connect_device(); if (f.report_mode == 0x35) send_motion(7760, 8200, 7560, false, true, false); else send_core_and_accel(); expect_vector(f.device.controller.gamepad.gyro, 0, 0, 0); expect_accel(); assert(f.device.controller.gamepad.buttons == (BUTTON_X | BUTTON_A)); // A correct CRC alone is insufficient: zero sensitivity must be rejected. reset_fixture(0x0330, true, EXT_NONE); memcpy(&f.mp_calibration[6], f.mp_calibration, 2); update_mp_checksum(); connect_device(); if (f.report_mode == 0x35) send_motion(7760, 8200, 7560, false, true, false); else send_core_and_accel(); expect_vector(f.device.controller.gamepad.gyro, 0, 0, 0); expect_accel(); } static void accel_backup_and_memory_error_recovery(void) { reset_fixture(0x0306, false, EXT_NONE); f.accel_calibration[9] ^= 1; connect_device(); send_core_and_accel(); expect_accel(); reset_fixture(0x0306, false, EXT_NONE); f.fail_accel_reads = true; connect_device(); send_core_and_accel(); expect_vector(f.device.controller.gamepad.accel, 0, 0, 0); expect_vector(f.device.controller.gamepad.gyro, 0, 0, 0); } static void malformed_and_mismatched_setup_replies_do_not_advance(void) { reset_fixture(0x0330, true, EXT_NONE); uni_hid_parser_wii_setup(&f.device); transaction_t request = stop_at_read(0xa60020); unsigned count = f.sent_count; const uint8_t empty[] = {0x21}; const uint8_t short_header[] = {0x21, 0, 0, 0xf0, 0}; const uint8_t short_data[] = {0x21, 0, 0, 0xf0, 0, 0x20, 0}; const uint8_t short_ack[] = {0x22, 0, 0, 0x16}; feed(empty, 0); feed(empty, sizeof(empty)); feed(short_header, sizeof(short_header)); feed(short_data, sizeof(short_data)); feed(short_ack, sizeof(short_ack)); send_ack(0x12, 0); send_ack(0x16, 0); // No write is outstanding. send_read_reply(0x30, &f.mp_calibration[16], 16, 0); send_read_reply(0x20, f.mp_calibration, 15, 0); assert(f.sent_count == count && f.ready_count == 0); // Accept either two 16-byte reads or one 32-byte read; both produce the // same two wire replies, since a 0x21 response carries at most 16 bytes. unsigned requested = ((unsigned)request.bytes[6] << 8) | request.bytes[7]; assert(requested == 16 || requested == 32); send_read_reply(0x20, f.mp_calibration, 16, 0); if (requested == 16) (void)stop_at_read(0xa60030); count = f.sent_count; // Duplicate first half cannot satisfy the outstanding second-half read. send_read_reply(0x20, f.mp_calibration, 16, 0); assert(f.sent_count == count && f.ready_count == 0); send_read_reply(0x30, &f.mp_calibration[16], 16, 0); finish_setup(); send_motion(7760, 8200, 7560, false, true, false); expect_vector(f.device.controller.gamepad.gyro, 360 * 1024, -120 * 1024, 40 * 1024); } static void short_motion_reports_do_not_publish_partial_samples(void) { reset_fixture(0x0330, true, EXT_NONE); connect_device(); uint8_t report[22]; memset(report, 0xff, sizeof(report)); report[0] = 0x35; unsigned count = f.sent_count; // The sixth extension byte contains pitch MSBs and the MP discriminator. // No prefix missing that byte constitutes a valid combined sample. for (uint16_t len = 1; len < 12; ++len) { feed(report, len); expect_vector(f.device.controller.gamepad.accel, 0, 0, 0); expect_vector(f.device.controller.gamepad.gyro, 0, 0, 0); } report[0] = 0x31; feed(report, 5); expect_vector(f.device.controller.gamepad.accel, 0, 0, 0); report[0] = 0x20; feed(report, 6); assert(f.sent_count == count && f.ready_count == 1); send_motion(7760, 8200, 7560, false, true, false); expect_vector(f.device.controller.gamepad.gyro, 360 * 1024, -120 * 1024, 40 * 1024); expect_accel(); } static void nunchuk_passthrough_retains_controls_between_samples(void) { reset_fixture(0x0330, true, EXT_NUNCHUK); connect_device(); assert(f.mp_active && f.activation_mode == 5 && f.report_mode == 0x35); uint8_t nunchuk[22] = {0x35, 1, 0x0a}; put_accel_report(nunchuk, 614, 442, 534); const uint8_t extension[6] = {160, 96, 128, 128, 129, 4}; // C held, Z released. memcpy(&nunchuk[6], extension, sizeof(extension)); feed(nunchuk, sizeof(nunchuk)); assert(f.device.controller.gamepad.dpad == DPAD_LEFT); assert(f.device.controller.gamepad.buttons == (BUTTON_B | BUTTON_SHOULDER_L | BUTTON_X)); assert(f.device.controller.gamepad.axis_x == 170 && f.device.controller.gamepad.axis_y == 170); uint8_t gyro[22] = {0x35}; // Release core controls but keep the Nunchuk held. put_accel_report(gyro, 614, 442, 534); put_gyro_report(&gyro[6], 7760, 8200, 7560, false, true, false); feed(gyro, sizeof(gyro)); assert(f.device.controller.gamepad.buttons == BUTTON_X); assert(f.device.controller.gamepad.dpad == 0); assert(f.device.controller.gamepad.axis_x == 170 && f.device.controller.gamepad.axis_y == 170); expect_vector(f.device.controller.gamepad.gyro, 360 * 1024, -120 * 1024, 40 * 1024); expect_accel(); nunchuk[2] &= 0x60; // Preserve accel low bits, release core buttons. nunchuk[1] &= 0x60; nunchuk[11] = 8; // C released, Z held: moved bits 3/2, not plain bits 1/0. feed(nunchuk, sizeof(nunchuk)); assert(f.device.controller.gamepad.buttons == BUTTON_Y); feed(gyro, sizeof(gyro)); assert(f.device.controller.gamepad.buttons == BUTTON_Y); assert(f.device.controller.gamepad.axis_x == 170 && f.device.controller.gamepad.axis_y == 170); nunchuk[6] = nunchuk[7] = 128; nunchuk[11] = 12; // Both released; stale cached presses must now disappear. feed(nunchuk, sizeof(nunchuk)); feed(gyro, sizeof(gyro)); assert(f.device.controller.gamepad.buttons == 0); assert(f.device.controller.gamepad.axis_rx == 0 && f.device.controller.gamepad.axis_ry == 0); assert(f.device.controller.gamepad.axis_x == 0 && f.device.controller.gamepad.axis_y == 0); } static void legacy_plain_nunchuk_and_wii_u_pro(void) { reset_fixture(0x0306, false, EXT_NUNCHUK); connect_device(); assert(!f.mp_active && (f.report_mode == 0x32 || f.report_mode == 0x35)); uint8_t nunchuk[22] = {f.report_mode, 1, 0x0a}; unsigned offset = f.report_mode == 0x35 ? 6 : 3; if (offset == 6) put_accel_report(nunchuk, 614, 442, 534); const uint8_t extension[6] = {160, 192, 128, 128, 128, 1}; // Plain C held, Z released. memcpy(&nunchuk[offset], extension, sizeof(extension)); feed(nunchuk, f.report_mode == 0x35 ? 22 : 11); assert(f.device.controller.gamepad.dpad == DPAD_LEFT); assert(f.device.controller.gamepad.buttons == (BUTTON_B | BUTTON_SHOULDER_L | BUTTON_X)); assert(f.device.controller.gamepad.axis_x == 170 && f.device.controller.gamepad.axis_y == -341); assert(f.device.controller.gamepad.axis_rx == 0 && f.device.controller.gamepad.axis_ry == 0); reset_fixture(0x0330, false, EXT_WII_U_PRO); connect_device(); assert(!f.mp_active && f.report_mode == 0x34); uint8_t pro[22] = {0x34}; put_le16(&pro[3], 0x0a80); put_le16(&pro[5], 0x0580); put_le16(&pro[7], 0x06c0); put_le16(&pro[9], 0x0940); pro[11] = 0x7b; // Right and + held. pro[12] = 0xbf; // Native B held. pro[13] = 0xff; feed(pro, sizeof(pro)); assert(f.device.controller.gamepad.axis_x == 256 && f.device.controller.gamepad.axis_y == 128); assert(f.device.controller.gamepad.axis_rx == -256 && f.device.controller.gamepad.axis_ry == -128); assert(f.device.controller.gamepad.dpad == DPAD_RIGHT); assert(f.device.controller.gamepad.buttons == BUTTON_A); assert(f.device.controller.gamepad.misc_buttons == MISC_BUTTON_START); expect_vector(f.device.controller.gamepad.accel, 0, 0, 0); expect_vector(f.device.controller.gamepad.gyro, 0, 0, 0); #if SWITCH2_BRIDGE_FULL_INPUT uni_native_motion_snapshot_t native; assert(uni_hid_parser_native_motion_snapshot(&f.device, &native)); assert(!native.accel_valid && !native.gyro_valid); #endif } static void plus_selects_vertical_without_disabling_motion(void) { reset_fixture(0x0330, true, EXT_NONE); f.connection_buttons = 0x10; connect_device(); assert(f.mp_active && f.report_mode == 0x35); send_motion(7760, 8200, 7560, false, true, false); assert(f.device.controller.gamepad.dpad == DPAD_LEFT); assert(f.device.controller.gamepad.buttons == (BUTTON_B | BUTTON_X)); expect_accel(); } static void unsolicited_status_restores_stream_without_duplicate_ready(void) { reset_fixture(0x0330, true, EXT_NONE); connect_device(); unsigned start = f.sent_count; send_status(true); finish_setup(); bool restored = false; for (unsigned i = start; i < f.sent_count; ++i) { if (f.sent[i].bytes[1] == 0x12 && f.sent[i].bytes[3] == 0x35) restored = true; assert(f.sent[i].bytes[1] != 0x16); // Reinitialization deactivates MP. } assert(restored && f.ready_count == 1 && f.mp_active); start = f.sent_count; send_ack(0x16, 0); send_read_reply(0xfa, (const uint8_t[]){0, 0, 0xa6, 0x20, 0, 5}, 6, 0); assert(f.sent_count == start && f.ready_count == 1); send_motion(7760, 8200, 7560, false, true, false); expect_vector(f.device.controller.gamepad.gyro, 360 * 1024, -120 * 1024, 40 * 1024); } static void initializing_and_vendor_nunchuk_ids_at_connection(void) { reset_fixture(0x0330, true, EXT_NUNCHUK); // Some third-party revisions do not have the canonical 0000 prefix. f.extension_identity[0] = 1; f.initializing_extension_reads = 2; f.transient_extension_read_errors = 1; f.status_after_activation_ack = true; connect_device(); assert(f.mp_active && f.activation_mode == 5); send_nunchuk_controls(true); send_motion(7760, 8200, 7560, false, true, false); expect_vector(f.device.controller.gamepad.gyro, 360 * 1024, -120 * 1024, 40 * 1024); assert(f.device.controller.gamepad.axis_x == 170 && f.device.controller.gamepad.buttons & BUTTON_X); // An all-zero suffix is not proof of absence: SDL and upstream recognize // the zero-filled ID documented for a replugged wireless BladeFX adapter. reset_fixture(0x0306, false, EXT_NUNCHUK); memset(f.extension_identity, 0, sizeof(f.extension_identity)); connect_device(); send_nunchuk_controls(false); } static void motionplus_nunchuk_hotplug_and_detach(void) { reset_fixture(0x0330, true, EXT_NONE); connect_device(); assert(f.device.controller_subtype == CONTROLLER_SUBTYPE_WIIMOTE_HORIZONTAL); f.deactivation_status_before_ack = true; f.status_after_activation_ack = true; f.extension = EXT_NUNCHUK; // Downstream hotplug is carried in MP data, not the ordinary status bit. send_motion(7760, 8200, 7560, false, true, false); finish_setup(); assert(f.mp_active && f.activation_mode == 5 && f.ready_count == 1); send_nunchuk_controls(true); // Unsolicited status starts a mapped-ID check. A detach during that pending // read must be deferred, not overwrite its address-less transaction state. send_status(false); transaction_t verify = stop_at_read(0xa400fa); f.extension = EXT_NONE; uint8_t gyro[22] = {0x35}; put_accel_report(gyro, 614, 442, 534); put_gyro_report(gyro + 6, 7760, 8200, 7560, false, true, false); feed(gyro, sizeof(gyro)); assert(f.device.controller.gamepad.axis_rx == 0 && f.device.controller.gamepad.axis_ry == 0); assert(f.device.controller.gamepad.axis_x == 0 && f.device.controller.gamepad.axis_y == 0); assert(f.device.controller.gamepad.buttons == 0); assert(f.device.controller_subtype == CONTROLLER_SUBTYPE_WIIMOTE_HORIZONTAL); expect_vector(f.device.controller.gamepad.gyro, 360 * 1024, -120 * 1024, 40 * 1024); answer_transaction(&verify); finish_setup(); assert(f.mp_active && f.activation_mode == 4 && f.ready_count == 1); send_motion(7760, 8200, 7560, false, true, false); assert(f.device.controller.gamepad.axis_rx == 0 && f.device.controller.gamepad.axis_ry == 0); assert(f.device.controller.gamepad.axis_x == 0 && f.device.controller.gamepad.axis_y == 0); expect_vector(f.device.controller.gamepad.gyro, 360 * 1024, -120 * 1024, 40 * 1024); expect_accel(); } static void external_motionplus_hotplug_retains_selected_orientation(void) { reset_fixture(0x0306, true, EXT_NONE); f.connection_buttons = 0x10; connect_device(); f.deactivation_status_after_ack = true; f.status_before_activation_ack = true; f.extension = EXT_NUNCHUK; send_motion(7760, 8200, 7560, false, true, false); finish_setup(); send_nunchuk_controls(true); f.extension = EXT_NONE; send_motion(7760, 8200, 7560, false, true, false); finish_setup(); assert(f.device.controller_subtype == CONTROLLER_SUBTYPE_WIIMOTE_VERTICAL); assert(f.mp_active && f.activation_mode == 4 && f.ready_count == 1); send_motion(7760, 8200, 7560, false, true, false); assert(f.device.controller.gamepad.dpad == DPAD_LEFT); expect_vector(f.device.controller.gamepad.gyro, 360 * 1024, -120 * 1024, 40 * 1024); // Removing the external MP itself clears its stale angular velocity, while // the Remote's independently calibrated accelerometer remains available. f.motionplus = false; f.mp_active = false; send_status(false); finish_setup(); uint8_t remote[6] = {0x31, 1, 0x0a}; put_accel_report(remote, 614, 442, 534); feed(remote, sizeof(remote)); assert(f.device.controller.gamepad.dpad == DPAD_LEFT); assert(f.device.controller.gamepad.buttons == (BUTTON_B | BUTTON_X)); expect_vector(f.device.controller.gamepad.gyro, 0, 0, 0); expect_accel(); assert(f.device.controller_subtype == CONTROLLER_SUBTYPE_WIIMOTE_VERTICAL); } static void plain_nunchuk_hotplug_and_setup_race(void) { reset_fixture(0x0306, false, EXT_NONE); uni_hid_parser_wii_setup(&f.device); transaction_t accel = stop_at_read(0x16); // An attachment status while EEPROM is being read must not lose that read. f.extension = EXT_NUNCHUK; send_status(true); answer_transaction(&accel); finish_setup(); send_nunchuk_controls(false); f.extension = EXT_NONE; send_status(false); assert(f.device.controller.gamepad.axis_rx == 0 && f.device.controller.gamepad.axis_ry == 0); assert(f.device.controller.gamepad.axis_x == 0 && f.device.controller.gamepad.axis_y == 0); assert(f.device.controller.gamepad.buttons == 0); finish_setup(); assert(f.device.controller_subtype == CONTROLLER_SUBTYPE_WIIMOTE_HORIZONTAL); send_core_and_accel(); expect_accel(); uni_hid_parser_wii_set_mode(&f.device, WII_MODE_VERTICAL); finish_setup(); f.extension = EXT_NUNCHUK; send_status(true); finish_setup(); send_nunchuk_controls(false); f.extension = EXT_NONE; send_status(false); finish_setup(); assert(f.device.controller_subtype == CONTROLLER_SUBTYPE_WIIMOTE_VERTICAL); assert(f.ready_count == 1); } static void expect_nunchuk_stick(uint8_t x, uint8_t y, int expected_x, int expected_y) { uint8_t report[22] = {0x35}; put_accel_report(report, 614, 442, 534); report[6] = x; report[7] = y; report[10] = 1; // Extension connected. report[11] = f.mp_active ? 12 : 3; // C/Z released in the selected format. feed(report, sizeof(report)); assert(f.device.controller.gamepad.axis_x == expected_x); assert(f.device.controller.gamepad.axis_y == expected_y); assert(f.device.controller.gamepad.axis_rx == 0 && f.device.controller.gamepad.axis_ry == 0); } static void calibrated_nunchuk_left_stick_endpoints_and_replacement(void) { reset_fixture(0x0330, true, EXT_NUNCHUK); set_nunchuk_stick_calibration((const uint8_t[]){220, 40, 124, 210, 30, 126}); connect_device(); expect_nunchuk_stick(124, 126, 0, 0); expect_nunchuk_stick(220, 126, 511, 0); expect_nunchuk_stick(40, 126, -512, 0); expect_nunchuk_stick(124, 210, 0, -512); expect_nunchuk_stick(124, 30, 0, 511); expect_nunchuk_stick(172, 168, 256, -256); expect_nunchuk_stick(82, 78, -256, 256); expect_nunchuk_stick(255, 0, 511, 511); expect_nunchuk_stick(0, 255, -512, -512); send_motion(7760, 8200, 7560, false, true, false); assert(f.device.controller.gamepad.axis_x == -512 && f.device.controller.gamepad.axis_y == -512); f.extension = EXT_NONE; send_motion(7760, 8200, 7560, false, true, false); finish_setup(); assert(f.device.controller.gamepad.axis_x == 0 && f.device.controller.gamepad.axis_y == 0); // A different Nunchuk must not inherit the previous extension's center/range. set_nunchuk_stick_calibration((const uint8_t[]){230, 50, 130, 220, 40, 120}); f.extension = EXT_NUNCHUK; send_motion(7760, 8200, 7560, false, true, false); finish_setup(); expect_nunchuk_stick(130, 120, 0, 0); expect_nunchuk_stick(230, 40, 511, 511); } static void unavailable_nunchuk_calibration_keeps_safe_nominal_stick(void) { reset_fixture(0x0306, false, EXT_NUNCHUK); f.fail_read_address = 0xa40020; connect_device(); expect_nunchuk_stick(128, 128, 0, 0); expect_nunchuk_stick(224, 32, 511, 511); reset_fixture(0x0330, true, EXT_NUNCHUK); set_nunchuk_stick_calibration((const uint8_t[]){128, 32, 128, 224, 32, 128}); connect_device(); // Correct checksum but zero positive span. expect_nunchuk_stick(160, 96, 170, 170); reset_fixture(0x0330, true, EXT_NUNCHUK); f.nunchuk_calibration[15] ^= 1; connect_device(); expect_nunchuk_stick(128, 128, 0, 0); expect_nunchuk_stick(224, 32, 511, 511); } static void plain_nunchuk_accel_uses_own_packed_factory_points(void) { reset_fixture(0x0306, false, EXT_NUNCHUK); connect_device(); int32_t acceleration[3]; uint32_t remote_sequence; assert(!uni_hid_parser_wii_nunchuk_accel_snapshot(&f.device, acceleration, &remote_sequence)); assert(!uni_hid_parser_wii_accel_snapshot(&f.device, acceleration, &remote_sequence)); // Native (693,382,551) = (+1g,-0.5g,+0.25g). Each axis has a different // packed low-bit value (1,2,3), distinct calibration, and a non-default zero. const uint8_t extension[6] = {160, 192, 173, 95, 137, 0xe5}; send_nunchuk_sample(extension); uint32_t first = expect_nunchuk_accel(-8192, 2048, -4096); assert(f.device.controller.gamepad.buttons == BUTTON_X); assert(f.device.controller.gamepad.axis_x == 170 && f.device.controller.gamepad.axis_y == -341); assert(!uni_hid_parser_wii_accel_snapshot(&f.device, acceleration, &remote_sequence)); expect_vector(f.device.controller.gamepad.accel, 0, 0, 0); uint8_t remote[6] = {0x31}; put_accel_report(remote, 614, 442, 534); feed(remote, sizeof(remote)); assert(uni_hid_parser_wii_accel_snapshot(&f.device, acceleration, &remote_sequence)); assert(expect_nunchuk_accel(-8192, 2048, -4096) == first); send_nunchuk_sample(extension); assert(expect_nunchuk_accel(-8192, 2048, -4096) != first); uint32_t sequence; assert(uni_hid_parser_wii_accel_snapshot(&f.device, acceleration, &sequence)); assert(sequence == remote_sequence); expect_accel(); // Nunchuk does not overwrite the Remote's console IMU. } static void passthrough_nunchuk_accel_bitpacking_and_freshness(void) { reset_fixture(0x0330, true, EXT_NUNCHUK); set_nunchuk_accel_calibration((const uint16_t[]){502, 510, 518}, (const uint16_t[]){694, 766, 646}); connect_device(); // MP relocates AZ bit2 to byte5 bit7, not the connected bit in byte4. // Bits 4/5/6/7 here reconstruct (694,382,550); C held, Z released. uint8_t extension[6] = {160, 192, 173, 95, 137, 0xf4}; send_nunchuk_sample(extension); uint32_t first = expect_nunchuk_accel(-8192, 2048, -4096); assert(f.device.controller.gamepad.buttons == BUTTON_X); send_motion(7760, 8200, 7560, false, true, false); assert(expect_nunchuk_accel(-8192, 2048, -4096) == first); int32_t acceleration[3]; uint32_t remote_sequence; assert(uni_hid_parser_wii_accel_snapshot(&f.device, acceleration, &remote_sequence)); send_ack(0x16, 0); send_status(false); // MP synthetic detach: the mapped ID still exists. finish_setup(); uint8_t short_report[8] = {0x32}; feed(short_report, sizeof(short_report)); send_nunchuk_sample((const uint8_t[]){255, 255, 255, 255, 255, 255}); assert(expect_nunchuk_accel(-8192, 2048, -4096) == first); uint32_t sequence; assert(uni_hid_parser_wii_accel_snapshot(&f.device, acceleration, &sequence)); assert(sequence == remote_sequence); expect_accel(); expect_vector(f.device.controller.gamepad.gyro, 360 * 1024, -120 * 1024, 40 * 1024); // Different low bits distinguish the X/Y selectors and both relocated Z // bits; clear bit0 is the missing precision, never a synthesized bit. extension[5] = 0x94; // (694,380,548). send_nunchuk_sample(extension); assert(expect_nunchuk_accel(-8192, 1920, -4160) != first); extension[5] = 0x64; // (692,382,546). send_nunchuk_sample(extension); first = expect_nunchuk_accel(-8106, 1792, -4096); send_nunchuk_sample(extension); assert(expect_nunchuk_accel(-8106, 1792, -4096) != first); } static void nunchuk_accel_calibration_is_independent_of_stick_and_remote(void) { reset_fixture(0x0306, false, EXT_NUNCHUK); f.fail_accel_reads = true; set_nunchuk_stick_calibration((const uint8_t[]){128, 32, 128, 224, 32, 128}); connect_device(); send_nunchuk_sample((const uint8_t[]){160, 192, 173, 95, 137, 0xe5}); expect_nunchuk_accel(-8192, 2048, -4096); assert(f.device.controller.gamepad.axis_x == 170 && f.device.controller.gamepad.axis_y == -341); int32_t acceleration[3]; uint32_t sequence; assert(!uni_hid_parser_wii_accel_snapshot(&f.device, acceleration, &sequence)); // Each axis must have a real span, not merely a valid checksum or X span. for (unsigned axis = 0; axis < 3; ++axis) { reset_fixture(0x0330, true, EXT_NUNCHUK); uint16_t one_g[3] = {693, 766, 647}; const uint16_t zero[3] = {501, 510, 519}; one_g[axis] = zero[axis]; set_nunchuk_accel_calibration(zero, one_g); set_nunchuk_stick_calibration((const uint8_t[]){220, 40, 124, 210, 30, 126}); connect_device(); expect_nunchuk_stick(124, 126, 0, 0); expect_nunchuk_stick(220, 30, 511, 511); send_motion(7760, 8200, 7560, false, true, false); expect_accel(); expect_vector(f.device.controller.gamepad.gyro, 360 * 1024, -120 * 1024, 40 * 1024); assert(!uni_hid_parser_wii_nunchuk_accel_snapshot(&f.device, acceleration, &sequence)); } // Either checksum byte or an unreadable block disables only Nunchuk motion. for (unsigned failure = 0; failure < 3; ++failure) { reset_fixture(0x0306, false, EXT_NUNCHUK); if (failure < 2) f.nunchuk_calibration[14 + failure] ^= 1; else f.fail_read_address = 0xa40020; connect_device(); send_nunchuk_controls(false); assert(!uni_hid_parser_wii_nunchuk_accel_snapshot(&f.device, acceleration, &sequence)); } } static void nunchuk_accel_detach_and_replacement_require_fresh_calibration(void) { for (unsigned passthrough = 0; passthrough < 2; ++passthrough) { reset_fixture(passthrough ? 0x0330 : 0x0306, passthrough, EXT_NUNCHUK); connect_device(); send_nunchuk_controls(passthrough); int32_t acceleration[3]; uint32_t original_sequence; assert(uni_hid_parser_wii_nunchuk_accel_snapshot(&f.device, acceleration, &original_sequence)); f.extension = EXT_NONE; if (passthrough) send_motion(7760, 8200, 7560, false, true, false); else send_status(false); uint32_t sequence; assert(!uni_hid_parser_wii_nunchuk_accel_snapshot(&f.device, acceleration, &sequence)); finish_setup(); // In-flight extension bytes must not resurrect the detached stream. send_nunchuk_sample((const uint8_t[]){160, 192, 128, 128, 128, 0}); assert(!uni_hid_parser_wii_nunchuk_accel_snapshot(&f.device, acceleration, &sequence)); set_nunchuk_accel_calibration((const uint16_t[]){512, 512, 512}, (const uint16_t[]){712, 752, 672}); f.extension = EXT_NUNCHUK; if (passthrough) send_motion(7760, 8200, 7560, false, true, false); else send_status(true); finish_setup(); assert(!uni_hid_parser_wii_nunchuk_accel_snapshot(&f.device, acceleration, &sequence)); send_nunchuk_controls(passthrough); assert(expect_nunchuk_accel(0, 0, 0) != original_sequence); expect_accel(); // Replacement with unreadable calibration must not inherit old points. f.extension = EXT_NONE; if (passthrough) send_motion(7760, 8200, 7560, false, true, false); else send_status(false); finish_setup(); f.fail_read_address = 0xa40020; f.extension = EXT_NUNCHUK; if (passthrough) send_motion(7760, 8200, 7560, false, true, false); else send_status(true); finish_setup(); send_nunchuk_controls(passthrough); assert(!uni_hid_parser_wii_nunchuk_accel_snapshot(&f.device, acceleration, &sequence)); expect_accel(); } } static void battery_status_survives_input_reports(void) { reset_fixture(0x0306, false, EXT_NONE); assert(f.device.controller.battery == UNI_CONTROLLER_BATTERY_NOT_AVAILABLE); uni_hid_parser_wii_setup(&f.device); finish_setup(); assert(f.device.controller.battery == UNI_CONTROLLER_BATTERY_FULL); send_core_and_accel(); assert(f.device.controller.battery == UNI_CONTROLLER_BATTERY_FULL); uint8_t status[] = {0x20, 0, 0, 0, 0, 0, 52}; feed(status, sizeof(status)); finish_setup(); assert(f.device.controller.battery == 179); // Measured 70% band, not a full-charge placeholder. send_core_and_accel(); assert(f.device.controller.battery == 179); status[6] = 0; feed(status, sizeof(status) - 1); assert(f.device.controller.battery == 179); // Truncation cannot erase the last measurement. feed(status, sizeof(status)); finish_setup(); assert(f.device.controller.battery == 13); // Measured low band remains distinct from unknown. send_core_and_accel(); assert(f.device.controller.battery == 13); uni_hid_parser_wii_setup(&f.device); assert(f.device.controller.battery == UNI_CONTROLLER_BATTERY_NOT_AVAILABLE); } static void run_case(const char* name, void (*test)(void)) { printf("Wii parser: %s\n", name); fflush(stdout); test(); } int main(void) { run_case("real battery retained between status and input reports", battery_status_survives_input_reports); run_case("fresh calibrated accelerometer snapshots without MotionPlus", accelerometer_snapshot_requires_fresh_calibrated_reports); run_case("independent fresh calibrated MotionPlus gyro snapshots", gyro_snapshot_advances_only_on_calibrated_motionplus_packets); run_case("gyro validity through hotplug, replacement and teardown", gyro_snapshot_invalidates_on_topology_and_teardown); run_case("plain Nunchuk independent packed acceleration", plain_nunchuk_accel_uses_own_packed_factory_points); run_case("MotionPlus Nunchuk acceleration bitpacking and freshness", passthrough_nunchuk_accel_bitpacking_and_freshness); run_case("Nunchuk accelerometer calibration independent of stick and Remote", nunchuk_accel_calibration_is_independent_of_stick_and_remote); run_case("Nunchuk acceleration detach and replacement", nunchuk_accel_detach_and_replacement_require_fresh_calibration); run_case("calibrated Nunchuk left-stick endpoints and replacement", calibrated_nunchuk_left_stick_endpoints_and_replacement); run_case("unavailable Nunchuk calibration uses safe nominal travel", unavailable_nunchuk_calibration_keeps_safe_nominal_stick); run_case("integrated MotionPlus calibration and per-axis slow bits", integrated_motionplus_calibration_and_slow_bits); run_case("external MotionPlus with absent extension status bit", external_motionplus_without_extension_status); run_case("absent MotionPlus retains calibrated remote", absent_motionplus_keeps_calibrated_remote); run_case("setup read/write errors leave buttons ready", setup_read_and_write_errors_leave_buttons_ready); run_case("invalid calibration suppresses only unavailable motion", failed_calibration_never_fabricates_motion); run_case("accelerometer backup and EEPROM errors", accel_backup_and_memory_error_recovery); run_case("malformed and mismatched setup replies", malformed_and_mismatched_setup_replies_do_not_advance); run_case("short reports reject partial motion samples", short_motion_reports_do_not_publish_partial_samples); run_case("Nunchuk passthrough retains and releases held controls", nunchuk_passthrough_retains_controls_between_samples); run_case("legacy plain Nunchuk and Wii U Pro mappings", legacy_plain_nunchuk_and_wii_u_pro); run_case("connection-time plus selects vertical motion mode", plus_selects_vertical_without_disabling_motion); run_case("unsolicited status restores reporting exactly once", unsolicited_status_restores_stream_without_duplicate_ready); run_case("initializing and vendor Nunchuk identities", initializing_and_vendor_nunchuk_ids_at_connection); run_case("integrated MotionPlus downstream hotplug and detach", motionplus_nunchuk_hotplug_and_detach); run_case("external MotionPlus preserves selected standalone orientation", external_motionplus_hotplug_retains_selected_orientation); run_case("plain Nunchuk hotplug and setup-time status race", plain_nunchuk_hotplug_and_setup_race); puts("Wii parser native contracts passed"); return 0; }