diff --git a/switch2_descriptors.cpp b/switch2_descriptors.cpp new file mode 100644 index 0000000..1de4fa6 --- /dev/null +++ b/switch2_descriptors.cpp @@ -0,0 +1,64 @@ +#include "switch2_descriptors.h" + +// Captured Pro Controller 2 descriptors: +// https://github.com/ndeadly/switch2_controller_research/blob/d1c5a7f7ba298f83017fae84952a4e6d2ef8fc92/descriptors.md +const uint8_t switch2_device_descriptor[] = { + 0x12, 0x01, 0x00, 0x02, 0xEF, 0x02, 0x01, 0x40, 0x7E, + 0x05, 0x69, 0x20, 0x00, 0x02, 0x01, 0x02, 0x03, 0x01, +}; +const size_t switch2_device_descriptor_length = sizeof(switch2_device_descriptor); + +// The capture has five interfaces and a 268-byte configuration. This explicit +// experimental subset keeps captured HID/vendor interfaces 0-1 only, changes +// wTotalLength to 80 and bNumInterfaces to 2, and zeros uncaptured string +// indices iConfiguration and iInterface. Audio interfaces 2-4 are omitted. +const uint8_t switch2_configuration_descriptor[] = { + 0x09, 0x02, 0x50, 0x00, 0x02, 0x01, 0x00, 0xC0, 0xFA, + + 0x08, 0x0B, 0x00, 0x01, 0x03, 0x00, 0x00, 0x00, + 0x09, 0x04, 0x00, 0x00, 0x02, 0x03, 0x00, 0x00, 0x00, + 0x09, 0x21, 0x11, 0x01, 0x00, 0x01, 0x22, 0x61, 0x00, + 0x07, 0x05, 0x81, 0x03, 0x40, 0x00, 0x04, + 0x07, 0x05, 0x01, 0x03, 0x40, 0x00, 0x04, + + 0x08, 0x0B, 0x01, 0x01, 0xFF, 0x00, 0x00, 0x00, + 0x09, 0x04, 0x01, 0x00, 0x02, 0xFF, 0x00, 0x00, 0x00, + 0x07, 0x05, 0x02, 0x02, 0x40, 0x00, 0x00, + 0x07, 0x05, 0x82, 0x02, 0x40, 0x00, 0x00, +}; +const size_t switch2_configuration_descriptor_length = + sizeof(switch2_configuration_descriptor); + +const uint8_t switch2_hid_report_descriptor[] = { + 0x05, 0x01, 0x09, 0x05, 0xA1, 0x01, 0x85, 0x05, + 0x05, 0xFF, 0x09, 0x01, 0x15, 0x00, 0x26, 0xFF, 0x00, + 0x95, 0x3F, 0x75, 0x08, 0x81, 0x02, + + 0x85, 0x09, 0x09, 0x01, 0x95, 0x02, 0x81, 0x02, + 0x05, 0x09, 0x19, 0x01, 0x29, 0x15, 0x25, 0x01, + 0x95, 0x15, 0x75, 0x01, 0x81, 0x02, + 0x95, 0x01, 0x75, 0x03, 0x81, 0x03, + 0x05, 0x01, 0x09, 0x01, 0xA1, 0x00, + 0x09, 0x30, 0x09, 0x31, 0x09, 0x33, 0x09, 0x35, + 0x26, 0xFF, 0x0F, 0x95, 0x04, 0x75, 0x0C, 0x81, 0x02, 0xC0, + 0x05, 0xFF, 0x09, 0x02, 0x26, 0xFF, 0x00, + 0x95, 0x34, 0x75, 0x08, 0x81, 0x02, + + 0x85, 0x02, 0x09, 0x01, 0x95, 0x3F, 0x91, 0x02, 0xC0, +}; +const size_t switch2_hid_report_descriptor_length = + sizeof(switch2_hid_report_descriptor); + +const uint8_t switch2_string_language[] = {0x09, 0x04}; +const size_t switch2_string_language_length = sizeof(switch2_string_language); +const uint8_t switch2_string_manufacturer[] = "Nintendo"; +const size_t switch2_string_manufacturer_length = + sizeof(switch2_string_manufacturer) - 1; +const uint8_t switch2_string_product[] = "Switch 2 Pro Controller"; +const size_t switch2_string_product_length = sizeof(switch2_string_product) - 1; +const uint8_t switch2_string_serial[] = "00"; +const size_t switch2_string_serial_length = sizeof(switch2_string_serial) - 1; + +static_assert(sizeof(switch2_device_descriptor) == 18); +static_assert(sizeof(switch2_configuration_descriptor) == 80); +static_assert(sizeof(switch2_hid_report_descriptor) == 97); diff --git a/tests/firmware/test_switch2_descriptors.cpp b/tests/firmware/test_switch2_descriptors.cpp new file mode 100644 index 0000000..2530ca5 --- /dev/null +++ b/tests/firmware/test_switch2_descriptors.cpp @@ -0,0 +1,166 @@ +#include "test_support.h" + +#include +#include +#include + +#include "../../switch2_descriptors.h" + +namespace { + +bool bytes_equal( + const uint8_t* actual, + std::size_t actual_length, + const uint8_t* expected, + std::size_t expected_length) { + return actual_length == expected_length && + std::memcmp(actual, expected, expected_length) == 0; +} + +bool switch2_descriptor_bytes_match_pinned_capture_subset() { + // Given: captured Pro Controller 2 bytes with the documented subset edits. + static constexpr uint8_t expected_device[] = { + 0x12, 0x01, 0x00, 0x02, 0xEF, 0x02, 0x01, 0x40, 0x7E, + 0x05, 0x69, 0x20, 0x00, 0x02, 0x01, 0x02, 0x03, 0x01, + }; + static constexpr uint8_t expected_configuration[] = { + 0x09, 0x02, 0x50, 0x00, 0x02, 0x01, 0x00, 0xC0, 0xFA, + 0x08, 0x0B, 0x00, 0x01, 0x03, 0x00, 0x00, 0x00, + 0x09, 0x04, 0x00, 0x00, 0x02, 0x03, 0x00, 0x00, 0x00, + 0x09, 0x21, 0x11, 0x01, 0x00, 0x01, 0x22, 0x61, 0x00, + 0x07, 0x05, 0x81, 0x03, 0x40, 0x00, 0x04, + 0x07, 0x05, 0x01, 0x03, 0x40, 0x00, 0x04, + 0x08, 0x0B, 0x01, 0x01, 0xFF, 0x00, 0x00, 0x00, + 0x09, 0x04, 0x01, 0x00, 0x02, 0xFF, 0x00, 0x00, 0x00, + 0x07, 0x05, 0x02, 0x02, 0x40, 0x00, 0x00, + 0x07, 0x05, 0x82, 0x02, 0x40, 0x00, 0x00, + }; + static constexpr uint8_t expected_hid[] = { + 0x05, 0x01, 0x09, 0x05, 0xA1, 0x01, 0x85, 0x05, 0x05, 0xFF, 0x09, 0x01, 0x15, 0x00, 0x26, 0xFF, + 0x00, 0x95, 0x3F, 0x75, 0x08, 0x81, 0x02, 0x85, 0x09, 0x09, 0x01, 0x95, 0x02, 0x81, 0x02, 0x05, + 0x09, 0x19, 0x01, 0x29, 0x15, 0x25, 0x01, 0x95, 0x15, 0x75, 0x01, 0x81, 0x02, 0x95, 0x01, 0x75, + 0x03, 0x81, 0x03, 0x05, 0x01, 0x09, 0x01, 0xA1, 0x00, 0x09, 0x30, 0x09, 0x31, 0x09, 0x33, 0x09, + 0x35, 0x26, 0xFF, 0x0F, 0x95, 0x04, 0x75, 0x0C, 0x81, 0x02, 0xC0, 0x05, 0xFF, 0x09, 0x02, 0x26, + 0xFF, 0x00, 0x95, 0x34, 0x75, 0x08, 0x81, 0x02, 0x85, 0x02, 0x09, 0x01, 0x95, 0x3F, 0x91, 0x02, + 0xC0, + }; + + // When/Then: all exported descriptor bytes and lengths match exactly. + CHECK(bytes_equal(switch2_device_descriptor, switch2_device_descriptor_length, + expected_device, sizeof(expected_device))); + CHECK(bytes_equal(switch2_configuration_descriptor, switch2_configuration_descriptor_length, + expected_configuration, sizeof(expected_configuration))); + CHECK(bytes_equal(switch2_hid_report_descriptor, switch2_hid_report_descriptor_length, + expected_hid, sizeof(expected_hid))); + return true; +} + +bool switch2_configuration_iterates_two_interfaces_and_four_endpoints() { + // Given: the deliberately reduced configuration descriptor. + std::size_t offset = 0; + uint8_t interface_count = 0; + uint8_t endpoint_count = 0; + const uint8_t expected_endpoints[][5] = { + {0x81, 0x03, 0x40, 0x00, 0x04}, + {0x01, 0x03, 0x40, 0x00, 0x04}, + {0x02, 0x02, 0x40, 0x00, 0x00}, + {0x82, 0x02, 0x40, 0x00, 0x00}, + }; + + // When: every USB descriptor is iterated by bLength. + while (offset < switch2_configuration_descriptor_length) { + const uint8_t length = switch2_configuration_descriptor[offset]; + CHECK(length >= 2); + CHECK(offset + length <= switch2_configuration_descriptor_length); + const uint8_t type = switch2_configuration_descriptor[offset + 1]; + if (type == 0x04) { + CHECK(length == 9); + CHECK(switch2_configuration_descriptor[offset + 5] != 0x01); + CHECK(switch2_configuration_descriptor[offset + 8] == 0x00); + ++interface_count; + } else if (type == 0x05) { + CHECK(length == 7); + CHECK(endpoint_count < 4); + CHECK(std::memcmp(&switch2_configuration_descriptor[offset + 2], + expected_endpoints[endpoint_count], 5) == 0); + ++endpoint_count; + } + offset += length; + } + + // Then: iteration is exact, contains only HID/vendor, and omits audio. + CHECK(offset == switch2_configuration_descriptor_length); + CHECK(switch2_configuration_descriptor[6] == 0x00); + CHECK(interface_count == 2); + CHECK(endpoint_count == 4); + return true; +} + +bool switch2_hid_reports_are_exactly_sixty_three_payload_bytes() { + // Given: the captured HID report descriptor. + uint32_t report_size = 0; + uint32_t report_count = 0; + uint32_t report_id = 0; + uint32_t input_05_bits = 0; + uint32_t input_09_bits = 0; + uint32_t output_02_bits = 0; + + // When: HID short items are parsed and report fields accumulated. + for (std::size_t offset = 0; offset < switch2_hid_report_descriptor_length;) { + const uint8_t prefix = switch2_hid_report_descriptor[offset++]; + CHECK(prefix != 0xFE); + const uint8_t size_code = prefix & 0x03; + const uint8_t data_size = size_code == 3 ? 4 : size_code; + CHECK(offset + data_size <= switch2_hid_report_descriptor_length); + uint32_t value = 0; + for (uint8_t index = 0; index < data_size; ++index) { + value |= static_cast(switch2_hid_report_descriptor[offset + index]) << (8 * index); + } + offset += data_size; + const uint8_t type = (prefix >> 2) & 0x03; + const uint8_t tag = (prefix >> 4) & 0x0F; + if (type == 1 && tag == 7) report_size = value; + if (type == 1 && tag == 8) report_id = value; + if (type == 1 && tag == 9) report_count = value; + if (type == 0 && tag == 8 && report_id == 0x05) input_05_bits += report_size * report_count; + if (type == 0 && tag == 8 && report_id == 0x09) input_09_bits += report_size * report_count; + if (type == 0 && tag == 9 && report_id == 0x02) output_02_bits += report_size * report_count; + } + + // Then: each supported transfer is 63 payload bytes plus its report ID. + CHECK(input_05_bits == 63 * 8); + CHECK(input_09_bits == 63 * 8); + CHECK(output_02_bits == 63 * 8); + return true; +} + +bool switch2_string_bytes_match_pinned_capture() { + // Given/When: the known language and ASCII strings are inspected. + static constexpr uint8_t language[] = {0x09, 0x04}; + static constexpr uint8_t manufacturer[] = "Nintendo"; + static constexpr uint8_t product[] = "Switch 2 Pro Controller"; + static constexpr uint8_t serial[] = "00"; + + // Then: exported lengths exclude the C terminator and bytes remain exact. + CHECK(bytes_equal(switch2_string_language, switch2_string_language_length, + language, sizeof(language))); + CHECK(bytes_equal(switch2_string_manufacturer, switch2_string_manufacturer_length, + manufacturer, sizeof(manufacturer) - 1)); + CHECK(bytes_equal(switch2_string_product, switch2_string_product_length, + product, sizeof(product) - 1)); + CHECK(bytes_equal(switch2_string_serial, switch2_string_serial_length, + serial, sizeof(serial) - 1)); + CHECK(switch2_string_manufacturer[switch2_string_manufacturer_length] == 0); + CHECK(switch2_string_product[switch2_string_product_length] == 0); + CHECK(switch2_string_serial[switch2_string_serial_length] == 0); + return true; +} + +} // namespace + +void run_switch2_descriptor_tests(TestRunner& runner) { + runner.run("Switch 2 descriptor exact bytes", switch2_descriptor_bytes_match_pinned_capture_subset); + runner.run("Switch 2 configuration iteration", switch2_configuration_iterates_two_interfaces_and_four_endpoints); + runner.run("Switch 2 HID report sizes", switch2_hid_reports_are_exactly_sixty_three_payload_bytes); + runner.run("Switch 2 string bytes", switch2_string_bytes_match_pinned_capture); +}