#include "usb/usb_configuration_management.h" #include #include #include #include #include #include "usb/usb_output_driver.h" #include "input/haptics_experiment.h" #ifdef SWITCH_PICO_HAPTICS_EXPERIMENT #include "input/haptics_transport_probe.h" #endif namespace { Bluepad32PairingSnapshot current_pairings{}; ConfigurationServiceSnapshot current_configuration{}; ProfileServiceListSnapshot current_profile_list{}; ProfileServiceSelectedSnapshot current_profile_selected{}; ProfileServiceTransactionSnapshot current_profile_transaction{}; ProfileServiceMetadataSnapshot current_profile_metadata{}; Bluepad32PlaytestSnapshot current_playtest[ BLUEPAD32_INPUT_BACKEND_SLOT_COUNT]{}; AdapterUsbMode current_active_mode = AdapterUsbMode::kSwitchProbe; uint8_t current_capabilities = USB_OUTPUT_CAPABILITY_INPUT | USB_OUTPUT_CAPABILITY_RUMBLE | USB_OUTPUT_CAPABILITY_MOTION; ConfigurationTransactionStatus mode_set_result = ConfigurationTransactionStatus::kPending; uint32_t mode_set_transaction_id = 0; AdapterRequestedMode mode_set_requested_mode = AdapterRequestedMode::kAuto; AdapterModeAvailability mode_set_availability{}; AdapterModeAvailability runtime_mode_availability{true, true, true, true}; uint32_t mode_availability_query_count = 0; uint32_t mode_set_call_count = 0; uint32_t correlated_reboot_transaction_id = 0; uint32_t reboot_transaction_id = 0; uint32_t reboot_call_count = 0; bool bootsel_reboot_requested = false; bool refresh_requested = false; bool clear_requested = false; Bluepad32BackendDiagnostics current_diagnostics{}; Bluepad32Switch2WakeStatus current_wake_status{}; uint32_t wake_request_calls = 0; bool accept_wake_request = true; std::vector control_payload; std::vector next_out_payload; uint32_t begin_transaction_id = 0; uint32_t append_transaction_id = 0; uint32_t commit_transaction_id = 0; size_t append_offset = 0; std::vector appended_bytes; ControllerIdentity profile_identity{}; uint8_t profile_index = 0; uint16_t profile_schema = 0; size_t profile_size = 0; uint32_t profile_crc = 0; bool profile_reset_requested = false; uint32_t profile_reset_transaction_id = 0; uint32_t profile_commit_transaction_id = 0; bool profile_activate_requested = false; uint32_t profile_activate_transaction_id = 0; bool profile_metadata_requested = false; uint32_t profile_metadata_transaction_id = 0; uint8_t profile_metadata_index = 0; std::string profile_metadata_value; bool identify_requested = false; #ifdef SWITCH_PICO_HAPTICS_EXPERIMENT HapticsExperimentDiagnostics current_haptics{}; uint32_t haptics_request_count = 0; HapticsTransportProbe current_transport{}; #endif void require(bool condition, const char* message) { if (!condition) { std::cerr << message << '\n'; std::exit(1); } } void write_u16(std::vector* output, size_t offset, uint16_t value) { (*output)[offset] = static_cast(value); (*output)[offset + 1] = static_cast(value >> 8); } uint16_t read_u16(const std::vector& input, size_t offset) { return static_cast(input[offset]) | static_cast(input[offset + 1] << 8); } void write_u32(std::vector* output, size_t offset, uint32_t value) { (*output)[offset] = static_cast(value); (*output)[offset + 1] = static_cast(value >> 8); (*output)[offset + 2] = static_cast(value >> 16); (*output)[offset + 3] = static_cast(value >> 24); } uint32_t read_u32(const std::vector& input, size_t offset) { return static_cast(input[offset]) | (static_cast(input[offset + 1]) << 8) | (static_cast(input[offset + 2]) << 16) | (static_cast(input[offset + 3]) << 24); } std::vector make_request( UsbConfigurationManagement::Operation operation, const std::vector& payload) { using namespace UsbConfigurationManagement; std::vector request(kRequestHeaderSize + payload.size()); memcpy(request.data(), "SPMG", 4); request[4] = kProtocolVersion; request[5] = static_cast(operation); write_u16(&request, 8, static_cast(payload.size())); write_u32(&request, 12, configuration_crc32(payload.data(), payload.size())); memcpy(request.data() + kRequestHeaderSize, payload.data(), payload.size()); return request; } tusb_control_request_t setup_request( UsbConfigurationManagement::Operation operation, uint8_t direction, uint16_t length) { tusb_control_request_t request{}; request.bmRequestType_bit.recipient = TUSB_REQ_RCPT_DEVICE; request.bmRequestType_bit.type = TUSB_REQ_TYPE_VENDOR; request.bmRequestType_bit.direction = direction; request.bRequest = static_cast(operation); request.wValue = UsbConfigurationManagement::kRequestValue; request.wIndex = UsbConfigurationManagement::kRequestIndex; request.wLength = length; return request; } void test_envelope_encoding() { using namespace UsbConfigurationManagement; const uint8_t payload[] = {1, 2, 3}; uint8_t encoded[32]{}; const size_t size = encode_response( Operation::kConfigurationRead, Status::kOk, 5, 1, 0x78563412, payload, sizeof(payload), encoded, sizeof(encoded)); require(size == kResponseHeaderSize + sizeof(payload) && memcmp(encoded, "SPMG", 4) == 0 && encoded[4] == kProtocolVersion && encoded[5] == static_cast(Operation::kConfigurationRead) && encoded[6] == static_cast(Status::kOk) && encoded[7] == 5 && encoded[8] == 3 && encoded[10] == 1 && encoded[12] == 0x12 && encoded[15] == 0x78 && memcmp(&encoded[kResponseHeaderSize], payload, sizeof(payload)) == 0, "versioned response envelope encoded incorrectly"); require(encode_response( Operation::kConfigurationRead, Status::kOk, 0, 1, 0, payload, sizeof(payload), encoded, size - 1) == 0, "response encoder accepted a short destination"); } void test_pairing_encoding() { using namespace UsbConfigurationManagement; Bluepad32PairingSnapshot snapshot{}; snapshot.generation = 0x78563412; snapshot.status = Bluepad32PairingSnapshotStatus::kReady; snapshot.record_count = 2; snapshot.overflow = true; snapshot.records[0].transport = Bluepad32PairingTransport::kClassic; snapshot.records[0].address_type = 0xfe; const uint8_t classic_address[6] = {1, 2, 3, 4, 5, 6}; memcpy(snapshot.records[0].address, classic_address, 6); snapshot.records[1].transport = Bluepad32PairingTransport::kBle; snapshot.records[1].address_type = 2; const uint8_t ble_address[6] = {6, 5, 4, 3, 2, 1}; memcpy(snapshot.records[1].address, ble_address, 6); uint8_t payload[kMaximumResponseSize]{}; const size_t size = encode_pairing_snapshot(snapshot, payload, sizeof(payload)); require(size == kResponseHeaderSize + kPairingPayloadHeaderSize + 2 * kPairingRecordSize && payload[5] == static_cast(Operation::kPairingRead) && payload[7] == 1 && payload[12] == 0x12 && payload[kResponseHeaderSize] == 2 && payload[kResponseHeaderSize + 1] == 1 && payload[kResponseHeaderSize + 4] == 1 && payload[kResponseHeaderSize + 5] == 0xfe && memcmp(&payload[kResponseHeaderSize + 6], classic_address, 6) == 0, "pairings were not migrated into the versioned envelope"); snapshot.status = Bluepad32PairingSnapshotStatus::kFailed; require(encode_pairing_snapshot(snapshot, payload, sizeof(payload)) == size && payload[6] == static_cast(Status::kStorageError), "failed persistent pairing clear must report storage failure, not success or pending"); } void perform_out(UsbConfigurationManagement::Operation operation, const std::vector& payload, bool expected_ack = true) { next_out_payload = make_request(operation, payload); tusb_control_request_t request = setup_request( operation, TUSB_DIR_OUT, static_cast(next_out_payload.size())); require(usb_configuration_management_vendor_control( 0, CONTROL_STAGE_SETUP, &request), "valid OUT setup was rejected"); require(usb_configuration_management_vendor_control( 0, CONTROL_STAGE_ACK, &request) == expected_ack, "OUT acknowledgement result was incorrect"); } void test_vendor_requests() { using namespace UsbConfigurationManagement; current_pairings = {}; current_pairings.generation = 7; current_pairings.status = Bluepad32PairingSnapshotStatus::kReady; current_pairings.record_count = 1; current_pairings.records[0].transport = Bluepad32PairingTransport::kClassic; tusb_control_request_t request = setup_request( Operation::kPairingRead, TUSB_DIR_IN, kMaximumResponseSize); require(usb_configuration_management_vendor_control( 0, CONTROL_STAGE_SETUP, &request) && control_payload[5] == static_cast(Operation::kPairingRead) && control_payload[12] == 7, "pairing read did not use the versioned envelope"); current_diagnostics = { 6, 1200, 120, 5000, 8, 2, 10, 2, 2, 1, 1, 3, UINT32_MAX, }; request = setup_request( Operation::kRuntimeDiagnostics, TUSB_DIR_IN, kMaximumResponseSize); require( usb_configuration_management_vendor_control( 0, CONTROL_STAGE_SETUP, &request) && control_payload[5] == static_cast(Operation::kRuntimeDiagnostics) && read_u32(control_payload, kResponseHeaderSize) == 6 && read_u32(control_payload, kResponseHeaderSize + 4) == 1200 && control_payload[kResponseHeaderSize + 28] == 2 && control_payload[kResponseHeaderSize + 31] == 1 && read_u32(control_payload, kResponseHeaderSize + 32) == 3 && read_u32(control_payload, kResponseHeaderSize + 36) == UINT32_MAX, "runtime diagnostics did not expose backend counters"); perform_out(Operation::kPairingRefresh, {}); require(refresh_requested, "pairing refresh was not dispatched"); perform_out(Operation::kBootselReboot, {}); require(bootsel_reboot_requested, "BOOTSEL reboot request was not dispatched"); perform_out(Operation::kPairingClear, {}); require(clear_requested, "pairing clear was not dispatched"); std::vector begin(12); write_u32(&begin, 0, 0x11223344); write_u16(&begin, 4, ADAPTER_CONFIGURATION_SCHEMA_VERSION); write_u16(&begin, 6, ADAPTER_CONFIGURATION_ENCODED_SIZE); write_u32(&begin, 8, 0xaabbccdd); perform_out(Operation::kConfigurationBegin, begin); require(begin_transaction_id == 0x11223344, "configuration begin was not dispatched"); std::vector chunk(12); write_u32(&chunk, 0, 0x11223344); write_u16(&chunk, 4, 0); write_u16(&chunk, 6, 4); chunk[8] = 60; perform_out(Operation::kConfigurationChunk, chunk); require(append_transaction_id == 0x11223344 && append_offset == 0 && appended_bytes.size() == 4, "configuration chunk was not dispatched"); std::vector commit(4); write_u32(&commit, 0, 0x11223344); perform_out(Operation::kConfigurationCommit, commit); require(commit_transaction_id == 0x11223344, "configuration commit was not dispatched"); next_out_payload = make_request(Operation::kPairingRefresh, {}); next_out_payload[12] ^= 1; request = setup_request( Operation::kPairingRefresh, TUSB_DIR_OUT, static_cast(next_out_payload.size())); require(usb_configuration_management_vendor_control( 0, CONTROL_STAGE_SETUP, &request) && !usb_configuration_management_vendor_control( 0, CONTROL_STAGE_ACK, &request), "bad request CRC was accepted"); request.wValue = 0; require(!usb_configuration_management_vendor_control( 0, CONTROL_STAGE_SETUP, &request), "request with invalid magic was accepted"); } void test_mode_vendor_requests() { using namespace UsbConfigurationManagement; current_configuration.configuration.requested_mode = AdapterRequestedMode::kXInput; current_configuration.configuration.joycon_mode = JoyConMode::kIndividual; current_active_mode = AdapterUsbMode::kSwitchProbe; tusb_control_request_t request = setup_request(Operation::kInfo, TUSB_DIR_IN, kMaximumResponseSize); require(usb_configuration_management_vendor_control( 0, CONTROL_STAGE_SETUP, &request) && control_payload[kResponseHeaderSize + 4] == static_cast(AdapterUsbMode::kSwitchProbe) && control_payload[kResponseHeaderSize + 5] == current_capabilities, "info response did not report active mode capabilities"); current_active_mode = AdapterUsbMode::kDInput; current_capabilities = USB_OUTPUT_CAPABILITY_INPUT; require(usb_configuration_management_vendor_control( 0, CONTROL_STAGE_SETUP, &request) && control_payload[kResponseHeaderSize + 4] == static_cast(AdapterUsbMode::kDInput) && control_payload[kResponseHeaderSize + 5] == USB_OUTPUT_CAPABILITY_INPUT, "generic info response promised unsupported output capabilities"); current_active_mode = AdapterUsbMode::kSwitchProbe; current_capabilities = USB_OUTPUT_CAPABILITY_INPUT | USB_OUTPUT_CAPABILITY_RUMBLE | USB_OUTPUT_CAPABILITY_MOTION; request = setup_request( Operation::kConfigurationRead, TUSB_DIR_IN, kMaximumResponseSize); require(usb_configuration_management_vendor_control( 0, CONTROL_STAGE_SETUP, &request) && control_payload.size() == kResponseHeaderSize + ADAPTER_CONFIGURATION_ENCODED_SIZE && control_payload[10] == ADAPTER_CONFIGURATION_SCHEMA_VERSION && control_payload[kResponseHeaderSize + 2] == static_cast(AdapterRequestedMode::kXInput) && control_payload[kResponseHeaderSize + 4] == static_cast(JoyConMode::kIndividual), "configuration response did not report the saved USB and player modes"); std::vector mode_set(5); write_u32(&mode_set, 0, 0x12345678); mode_set[4] = static_cast(AdapterRequestedMode::kXInput); const std::vector encoded = make_request(Operation::kModeSet, mode_set); require(encoded.size() == kRequestHeaderSize + 5 && encoded[5] == static_cast(Operation::kModeSet) && encoded[8] == 5 && read_u32(encoded, kRequestHeaderSize) == 0x12345678 && encoded[kRequestHeaderSize + 4] == static_cast( AdapterRequestedMode::kXInput), "mode-set request envelope does not match the protocol"); mode_set_result = ConfigurationTransactionStatus::kPending; perform_out(Operation::kModeSet, mode_set); require(mode_set_transaction_id == 0x12345678 && mode_set_requested_mode == AdapterRequestedMode::kXInput && mode_set_availability.switch_mode && mode_set_availability.xinput_mode && mode_set_availability.dinput_mode && mode_set_availability.mac_mode && mode_availability_query_count == 1, "XInput mode set did not use runtime availability"); write_u32(&mode_set, 0, 0x12345679); mode_set[4] = static_cast(AdapterRequestedMode::kSwitch); perform_out(Operation::kModeSet, mode_set); require(mode_set_requested_mode == AdapterRequestedMode::kSwitch && mode_availability_query_count == 2, "Switch mode set did not use runtime availability"); mode_set_result = ConfigurationTransactionStatus::kBusy; write_u32(&mode_set, 0, 0x1234567a); perform_out(Operation::kModeSet, mode_set, false); mode_set_result = ConfigurationTransactionStatus::kStorageError; write_u32(&mode_set, 0, 0x1234567b); perform_out(Operation::kModeSet, mode_set, false); const uint32_t calls_before_invalid = mode_set_call_count; const uint32_t availability_queries_before_invalid = mode_availability_query_count; for (const uint32_t transaction_id : {0u, 0x80000000u}) { write_u32(&mode_set, 0, transaction_id); perform_out(Operation::kModeSet, mode_set, false); } write_u32(&mode_set, 0, 0x1234567c); mode_set[4] = 0xff; perform_out(Operation::kModeSet, mode_set, false); require(mode_set_call_count == calls_before_invalid && mode_availability_query_count == availability_queries_before_invalid, "malformed mode request reached runtime mode selection"); mode_set_result = ConfigurationTransactionStatus::kPending; for (const AdapterRequestedMode generic_mode : { AdapterRequestedMode::kDInput, AdapterRequestedMode::kMac, }) { write_u32(&mode_set, 0, 0x12345680u + static_cast(generic_mode)); mode_set[4] = static_cast(generic_mode); perform_out(Operation::kModeSet, mode_set); require(mode_set_requested_mode == generic_mode, "generic mode request was not dispatched"); } require(mode_set_call_count == calls_before_invalid + 2 && mode_availability_query_count == availability_queries_before_invalid + 2, "generic modes did not use runtime availability"); request = setup_request( Operation::kModeSet, TUSB_DIR_OUT, kRequestHeaderSize + 4); require(!usb_configuration_management_vendor_control( 0, CONTROL_STAGE_SETUP, &request), "short mode-set request was accepted"); std::vector reboot(4); write_u32(&reboot, 0, 0x12345678); const std::vector encoded_reboot = make_request(Operation::kReboot, reboot); require(encoded_reboot.size() == kRequestHeaderSize + 4 && encoded_reboot[5] == static_cast(Operation::kReboot) && encoded_reboot[8] == 4 && read_u32(encoded_reboot, kRequestHeaderSize) == 0x12345678, "reboot request envelope does not match the protocol"); correlated_reboot_transaction_id = 0x12345678; perform_out(Operation::kReboot, reboot); require(reboot_transaction_id == correlated_reboot_transaction_id, "correlated reboot request was not dispatched"); write_u32(&reboot, 0, 0x12345679); perform_out(Operation::kReboot, reboot, false); const uint32_t reboot_calls_before_invalid = reboot_call_count; for (const uint32_t transaction_id : {0u, 0x80000000u}) { write_u32(&reboot, 0, transaction_id); perform_out(Operation::kReboot, reboot, false); } request = setup_request( Operation::kReboot, TUSB_DIR_OUT, kRequestHeaderSize + 5); require(!usb_configuration_management_vendor_control( 0, CONTROL_STAGE_SETUP, &request), "oversized reboot request was accepted"); require(reboot_call_count == reboot_calls_before_invalid, "malformed reboot transaction reached the helper"); } void test_profile_vendor_requests() { using namespace UsbConfigurationManagement; ControllerIdentity expected_identity{}; expected_identity.stable = true; expected_identity.transport = ControllerTransport::kClassic; expected_identity.address[5] = 7; expected_identity.vendor_id = 0x057e; expected_identity.product_id = 0x2009; current_profile_list = {}; current_profile_list.metadata.state = ProfileServiceState::kReady; current_profile_list.metadata.generation = 9; current_profile_list.count = 2; current_profile_list.rows[0].identity = controller_identity_global(); current_profile_list.rows[1].identity = expected_identity; current_profile_list.rows[1].active_profile = 2; tusb_control_request_t request = setup_request( Operation::kProfileList, TUSB_DIR_IN, kMaximumResponseSize); require(usb_configuration_management_vendor_control( 0, CONTROL_STAGE_SETUP, &request) && control_payload.size() == kResponseHeaderSize + 97 && control_payload[5] == static_cast(Operation::kProfileList) && control_payload[10] == CONTROLLER_PROFILE_SCHEMA_VERSION && control_payload[kResponseHeaderSize] == 2 && control_payload[kResponseHeaderSize + 63] == 2, "profile list response was not encoded"); current_profile_selected = {}; current_profile_selected.metadata.state = ProfileServiceState::kReady; current_profile_selected.metadata.generation = 9; current_profile_selected.valid = true; current_profile_selected.status = ConfigurationTransactionStatus::kCommitted; current_profile_selected.identity = expected_identity; current_profile_selected.profile_index = 2; current_profile_selected.profile = controller_profile_default(expected_identity, 2); request = setup_request( Operation::kProfileRead, TUSB_DIR_IN, kMaximumResponseSize); require(usb_configuration_management_vendor_control( 0, CONTROL_STAGE_SETUP, &request) && control_payload.size() == kResponseHeaderSize + CONTROLLER_PROFILE_ENCODED_SIZE && control_payload[10] == CONTROLLER_PROFILE_SCHEMA_VERSION && control_payload[kResponseHeaderSize] == static_cast( CONTROLLER_PROFILE_SCHEMA_VERSION) && control_payload[kResponseHeaderSize + 1] == 0 && control_payload[kResponseHeaderSize + 2] == 0x80 && control_payload[kResponseHeaderSize + 3] == 1, "selected profile response was not encoded"); current_playtest[2] = {}; current_playtest[2].active = true; current_playtest[2].connection_generation = 0x11223344; current_playtest[2].state_generation = 0x55667788; current_playtest[2].identity = expected_identity; current_playtest[2].physical_button_mask = 0x8001; current_playtest[2].state.extra_buttons = 0x7f; current_playtest[2].controller_layout = Bluepad32ControllerLayout::kJoyCon2MergedPair; current_playtest[2].state.left_stick_x = -1234; current_playtest[2].state.left_stick_y = 2345; current_playtest[2].state.right_stick_x = INT16_MIN; current_playtest[2].state.right_stick_y = INT16_MAX; current_playtest[2].state.left_trigger = 123; current_playtest[2].state.right_trigger = 65000; current_playtest[2].battery = 201; current_playtest[2].capabilities = 0x0f; current_playtest[2].state.motion_sample_count = 1; current_playtest[2].state.motion_samples[0] = {1, -2, 3, -4, 5, -6}; request = setup_request( Operation::kProfilePlaytest, TUSB_DIR_IN, kMaximumResponseSize); require(usb_configuration_management_vendor_control( 0, CONTROL_STAGE_SETUP, &request) && control_payload.size() == kResponseHeaderSize + kProfilePlaytestPayloadSize && control_payload[5] == static_cast(Operation::kProfilePlaytest) && control_payload[7] == 3 && control_payload[10] == kProfilePlaytestSchemaVersion && control_payload[kResponseHeaderSize] == 3 && control_payload[kResponseHeaderSize + 1] == 2 && read_u16(control_payload, kResponseHeaderSize + 2) == 0x8001 && read_u32(control_payload, kResponseHeaderSize + 4) == 0x11223344 && read_u32(control_payload, kResponseHeaderSize + 8) == 0x55667788 && static_cast(read_u16( control_payload, kResponseHeaderSize + 26)) == -1234 && read_u16(control_payload, kResponseHeaderSize + 36) == 65000 && static_cast(read_u16( control_payload, kResponseHeaderSize + 52)) == -6 && control_payload[kResponseHeaderSize + 39] == 201 && control_payload[kResponseHeaderSize + 40] == 0x0f && control_payload[kResponseHeaderSize + 54] == 0x7f && control_payload[kResponseHeaderSize + 55] == 3, "profile playtest response lost live controller state"); current_playtest[2].controller_layout = Bluepad32ControllerLayout::kWiiVertical; require(usb_configuration_management_vendor_control( 0, CONTROL_STAGE_SETUP, &request) && control_payload[10] == 5 && control_payload[kResponseHeaderSize + 55] == 7, "Wii orientation must be available through schema-5 playtest"); current_playtest[2].controller_layout = static_cast(8); uint8_t invalid_playtest[kMaximumResponseSize]{}; require(encode_profile_playtest( 2, current_playtest[2], invalid_playtest, sizeof(invalid_playtest)) == 0, "unknown playtest layout metadata must be rejected"); current_playtest[2].controller_layout = Bluepad32ControllerLayout::kJoyCon2MergedPair; current_playtest[2].active = false; require(usb_configuration_management_vendor_control( 0, CONTROL_STAGE_SETUP, &request) && control_payload[kResponseHeaderSize] == 0 && control_payload[kResponseHeaderSize + 1] == 0xff && control_payload[kResponseHeaderSize + 54] == 0 && control_payload[kResponseHeaderSize + 55] == 0, "disconnected profile playtest was not encoded"); current_profile_metadata = {}; current_profile_metadata.metadata.state = ProfileServiceState::kReady; current_profile_metadata.metadata.generation = 10; current_profile_metadata.status = ConfigurationTransactionStatus::kCommitted; current_profile_metadata.valid = true; memcpy(current_profile_metadata.alias, "Desk pad", 9); memcpy(current_profile_metadata.profile_names[7], "Desktop", 8); request = setup_request( Operation::kProfileMetadataRead, TUSB_DIR_IN, kMaximumResponseSize); require(usb_configuration_management_vendor_control( 0, CONTROL_STAGE_SETUP, &request) && control_payload.size() == kResponseHeaderSize + kProfileMetadataPayloadSize && control_payload[kResponseHeaderSize] == 8 && memcmp(&control_payload[kResponseHeaderSize + 1], "Desk pad", 8) == 0 && control_payload[ kResponseHeaderSize + 8 * (PROFILE_SERVICE_METADATA_MAX_BYTES + 1)] == 7, "profile metadata response was not encoded"); std::vector metadata(27); write_u32(&metadata, 0, 0x12345678); require(controller_identity_encode( expected_identity, &metadata[4], CONTROLLER_IDENTITY_ENCODED_SIZE), "metadata identity did not encode"); metadata[18] = 7; metadata[19] = 7; memcpy(&metadata[20], "Desktop", 7); perform_out(Operation::kProfileMetadataSet, metadata); require(profile_metadata_requested && profile_metadata_transaction_id == 0x12345678 && profile_metadata_index == 7 && profile_metadata_value == "Desktop", "profile metadata mutation was not dispatched"); std::vector identify(CONTROLLER_IDENTITY_ENCODED_SIZE); require(controller_identity_encode( expected_identity, identify.data(), identify.size()), "identify identity did not encode"); perform_out(Operation::kProfileIdentify, identify); require(identify_requested, "controller Identify request was not dispatched"); std::vector orientation = identify; orientation.resize(CONTROLLER_IDENTITY_ENCODED_SIZE + 5); write_u32(&orientation, CONTROLLER_IDENTITY_ENCODED_SIZE, 0x11223344); orientation.back() = 1; perform_out(Operation::kWiiOrientation, orientation); orientation.back() = 2; next_out_payload = make_request(Operation::kWiiOrientation, orientation); request = setup_request(Operation::kWiiOrientation, TUSB_DIR_OUT, static_cast(next_out_payload.size())); require(usb_configuration_management_vendor_control( 0, CONTROL_STAGE_SETUP, &request) && !usb_configuration_management_vendor_control( 0, CONTROL_STAGE_ACK, &request), "invalid Wii orientation was treated as vertical"); request.wLength--; require(!usb_configuration_management_vendor_control( 0, CONTROL_STAGE_SETUP, &request), "truncated Wii orientation request was accepted"); current_profile_transaction = {}; current_profile_transaction.metadata.state = ProfileServiceState::kReady; current_profile_transaction.metadata.generation = 9; current_profile_transaction.transaction.transaction_id = 0x01020304; current_profile_transaction.transaction.status = ConfigurationTransactionStatus::kPending; request = setup_request( Operation::kProfileTransactionStatus, TUSB_DIR_IN, kMaximumResponseSize); require(usb_configuration_management_vendor_control( 0, CONTROL_STAGE_SETUP, &request) && control_payload.size() == kResponseHeaderSize + 20 && control_payload[6] == static_cast(Status::kPending) && read_u32(control_payload, kResponseHeaderSize) == 0x01020304, "pending profile transaction status lost its transaction ID"); current_profile_transaction.transaction.status = ConfigurationTransactionStatus::kCommitted; current_profile_transaction.transaction.stored_generation = 0x11223344; current_profile_transaction.transaction.stored_crc = 0xaabbccdd; require(usb_configuration_management_vendor_control( 0, CONTROL_STAGE_SETUP, &request) && control_payload[6] == static_cast(Status::kOk) && read_u32(control_payload, kResponseHeaderSize) == 0x01020304 && read_u32(control_payload, kResponseHeaderSize + 12) == 0x11223344 && read_u32(control_payload, kResponseHeaderSize + 16) == 0xaabbccdd, "final profile transaction status lost its commit result"); std::vector identity_payload(15); require(controller_identity_encode( expected_identity, identity_payload.data(), CONTROLLER_IDENTITY_ENCODED_SIZE), "profile test identity did not encode"); identity_payload[14] = 2; perform_out(Operation::kProfileSelect, identity_payload); require(controller_identity_equal(expected_identity, profile_identity) && profile_index == 2, "profile selection was not dispatched"); std::vector begin(28); write_u32(&begin, 0, 0x55667788); require(controller_identity_encode( expected_identity, &begin[4], CONTROLLER_IDENTITY_ENCODED_SIZE), "profile begin identity did not encode"); begin[18] = 1; write_u16(&begin, 20, CONTROLLER_PROFILE_SCHEMA_VERSION); write_u16(&begin, 22, CONTROLLER_PROFILE_ENCODED_SIZE); write_u32(&begin, 24, 0xaabbccdd); perform_out(Operation::kProfileBegin, begin); require(begin_transaction_id == 0x55667788 && profile_index == 1 && profile_schema == CONTROLLER_PROFILE_SCHEMA_VERSION && profile_size == CONTROLLER_PROFILE_ENCODED_SIZE && profile_crc == 0xaabbccdd, "profile begin was not dispatched"); std::vector chunk(48); write_u32(&chunk, 0, 0x55667788); write_u16(&chunk, 4, 0); write_u16(&chunk, 6, 40); perform_out(Operation::kProfileChunk, chunk); require(append_transaction_id == 0x55667788 && append_offset == 0 && appended_bytes.size() == 40, "profile chunk was not dispatched"); std::vector commit(4); write_u32(&commit, 0, 0x55667788); perform_out(Operation::kProfileCommit, commit); require(profile_commit_transaction_id == 0x55667788, "profile commit was not dispatched"); std::vector mutation(19); write_u32(&mutation, 0, 0x10203040); require(controller_identity_encode( expected_identity, &mutation[4], CONTROLLER_IDENTITY_ENCODED_SIZE), "profile mutation identity did not encode"); mutation[18] = CONTROLLER_PROFILE_ALL; perform_out(Operation::kProfileReset, mutation); require(profile_reset_requested && profile_reset_transaction_id == 0x10203040, "profile reset transaction was not dispatched"); write_u32(&mutation, 0, 0x50607080); mutation[18] = 3; perform_out(Operation::kProfileActivate, mutation); require(profile_activate_requested && profile_index == 3 && profile_activate_transaction_id == 0x50607080, "profile activation transaction was not dispatched"); write_u32(&mutation, 0, 0); perform_out(Operation::kProfileActivate, mutation, false); request = setup_request( Operation::kProfileReset, TUSB_DIR_OUT, kRequestHeaderSize + 15); require(!usb_configuration_management_vendor_control( 0, CONTROL_STAGE_SETUP, &request), "legacy profile reset payload was accepted"); begin[19] = 1; perform_out(Operation::kProfileBegin, begin, false); request = setup_request( Operation::kProfileSelect, TUSB_DIR_OUT, kRequestHeaderSize + 14); require(!usb_configuration_management_vendor_control( 0, CONTROL_STAGE_SETUP, &request), "short profile selection request was accepted"); } std::vector read_wake_status() { using namespace UsbConfigurationManagement; const auto setup = setup_request(Operation::kSwitch2Wake, TUSB_DIR_IN, 40); require(usb_configuration_management_vendor_control(0, CONTROL_STAGE_SETUP, &setup) && usb_configuration_management_vendor_control(0, CONTROL_STAGE_DATA, &setup) && usb_configuration_management_vendor_control(0, CONTROL_STAGE_ACK, &setup), "read-only wake status transfer was rejected"); require(control_payload.size() == 40 && control_payload[5] == 0x05 && control_payload[6] == 0 && control_payload[7] == 0 && read_u16(control_payload, 8) == 20 && read_u16(control_payload, 10) == 1 && read_u32(control_payload, 12) == read_u32(control_payload, 20) && control_payload[27] == 0 && read_u32(control_payload, 16) == configuration_crc32(control_payload.data() + 20, 20), "wake schema, request correlation, reserved byte or CRC is invalid"); return control_payload; } void test_switch2_wake_requests() { using namespace UsbConfigurationManagement; std::vector payload(4); write_u32(&payload, 0, 0x12345678); const auto setup = setup_request(Operation::kSwitch2Wake, TUSB_DIR_OUT, 20); for (uint16_t size : {16, 19, 21}) { const auto malformed = setup_request(Operation::kSwitch2Wake, TUSB_DIR_OUT, size); require(!usb_configuration_management_vendor_control(0, CONTROL_STAGE_SETUP, &malformed), "wake OUT accepted a non-u32 payload size"); } const auto begin = [&] { next_out_payload = make_request(Operation::kSwitch2Wake, payload); require(usb_configuration_management_vendor_control(0, CONTROL_STAGE_SETUP, &setup), "wake OUT setup was rejected"); }; begin(); require(!usb_configuration_management_vendor_control(0, CONTROL_STAGE_ACK, &setup) && wake_request_calls == 0, "wake work must be accepted during DATA, never after an unchecked status ACK"); for (uint32_t id : {0u, 0x80000000u}) { write_u32(&payload, 0, id); begin(); require(!usb_configuration_management_vendor_control(0, CONTROL_STAGE_DATA, &setup) && !usb_configuration_management_vendor_control(0, CONTROL_STAGE_ACK, &setup) && wake_request_calls == 0, "invalid wake ID reached the backend or obtained a status ACK"); } write_u32(&payload, 0, 0x12345678); next_out_payload = make_request(Operation::kSwitch2Wake, payload); next_out_payload[12] ^= 1; require(usb_configuration_management_vendor_control(0, CONTROL_STAGE_SETUP, &setup) && !usb_configuration_management_vendor_control(0, CONTROL_STAGE_DATA, &setup) && wake_request_calls == 0, "corrupt wake envelope dispatched radio work"); begin(); auto wrong_setup = setup; --wrong_setup.wLength; require(!usb_configuration_management_vendor_control(1, CONTROL_STAGE_DATA, &setup) && !usb_configuration_management_vendor_control(0, CONTROL_STAGE_DATA, &wrong_setup) && wake_request_calls == 0, "another pipe or SETUP must not consume the staged wake request"); require(usb_configuration_management_vendor_control(0, CONTROL_STAGE_DATA, &setup) && wake_request_calls == 1, "wake acceptance must occur before the status ACK"); require(!usb_configuration_management_vendor_control(0, CONTROL_STAGE_DATA, &setup) && usb_configuration_management_vendor_control(0, CONTROL_STAGE_ACK, &setup) && !usb_configuration_management_vendor_control(0, CONTROL_STAGE_ACK, &setup) && wake_request_calls == 1, "duplicate USB stages must not enqueue another wake"); accept_wake_request = false; begin(); require(!usb_configuration_management_vendor_control(0, CONTROL_STAGE_DATA, &setup), "a busy mailbox must reject before status ACK"); accept_wake_request = true; require(!usb_configuration_management_vendor_control(0, CONTROL_STAGE_DATA, &setup) && !usb_configuration_management_vendor_control(0, CONTROL_STAGE_ACK, &setup) && wake_request_calls == 2, "a duplicate rejected DATA must not become accepted after the mailbox frees"); begin(); current_wake_status = {0x12345678, Bluepad32Switch2WakeState::kFailed, true, false, 7, 4, 2}; const auto status = read_wake_status(); // IN SETUP cancels the staged OUT. require(read_u32(status, 20) == 0x12345678 && status[24] == 6 && status[25] == 1 && status[26] == 0 && read_u32(status, 28) == 7 && read_u32(status, 32) == 4 && read_u32(status, 36) == 2, "a failed asynchronous burst must remain correlated in the wire status"); require(read_wake_status() == status && !usb_configuration_management_vendor_control(0, CONTROL_STAGE_DATA, &setup) && !usb_configuration_management_vendor_control(0, CONTROL_STAGE_ACK, &setup) && wake_request_calls == 2, "status reads or stale OUT stages must never enqueue or consume wake work"); } std::vector read_haptics_payload() { using namespace UsbConfigurationManagement; tusb_control_request_t request = setup_request( Operation::kHapticsExperiment, TUSB_DIR_IN, kMaximumResponseSize); require(usb_configuration_management_vendor_control( 0, CONTROL_STAGE_SETUP, &request), "experiment diagnostics IN was rejected"); require(control_payload.size() == kResponseHeaderSize + 84 && control_payload[5] == 0x40 && control_payload[6] == static_cast(Status::kOk) && control_payload[7] == 0 && read_u16(control_payload, 8) == 84 && read_u16(control_payload, 10) == 5, "experiment schema-5 envelope is invalid"); std::vector payload( control_payload.begin() + kResponseHeaderSize, control_payload.end()); require(read_u32(control_payload, 16) == configuration_crc32(payload.data(), payload.size()), "experiment response CRC is invalid"); require(payload[71] == 0 && payload[73] == 32 && payload[74] <= 1 && payload[75] == 0, "experiment reserved payload bytes must remain zero"); return payload; } #ifdef SWITCH_PICO_HAPTICS_EXPERIMENT void perform_haptics_out(uint8_t action, uint8_t slot, bool accepted = true) { using namespace UsbConfigurationManagement; next_out_payload = make_request(Operation::kHapticsExperiment, {action, slot}); tusb_control_request_t request = setup_request( Operation::kHapticsExperiment, TUSB_DIR_OUT, static_cast(next_out_payload.size())); require(usb_configuration_management_vendor_control( 0, CONTROL_STAGE_SETUP, &request), "experiment OUT setup was rejected"); require(usb_configuration_management_vendor_control( 0, CONTROL_STAGE_DATA, &request) == accepted, "experiment must reject invalid or busy requests before status ACK"); const uint32_t requests_after_data = haptics_request_count; if (accepted) { require(!usb_configuration_management_vendor_control( 0, CONTROL_STAGE_DATA, &request) && haptics_request_count == requests_after_data, "duplicate DATA replayed experiment control"); } require(usb_configuration_management_vendor_control( 0, CONTROL_STAGE_ACK, &request) == accepted, "experiment ACK did not preserve DATA-stage result"); require(!usb_configuration_management_vendor_control( 0, CONTROL_STAGE_ACK, &request) && haptics_request_count == requests_after_data, "ACK replayed experiment control"); } #endif void test_haptics_experiment_requests() { using namespace UsbConfigurationManagement; for (uint16_t payload_size : {0, 1, 3}) { tusb_control_request_t request = setup_request( Operation::kHapticsExperiment, TUSB_DIR_OUT, kRequestHeaderSize + payload_size); require(!usb_configuration_management_vendor_control( 0, CONTROL_STAGE_SETUP, &request), "malformed experiment control size was accepted"); } #ifndef SWITCH_PICO_HAPTICS_EXPERIMENT const auto unsupported = read_haptics_payload(); require(unsupported[68] == 6 && unsupported[69] == 0xff, "disabled firmware must expose an unsupported unbound snapshot"); for (uint8_t action : {0, 1, 2}) { next_out_payload = make_request(Operation::kHapticsExperiment, {action, 0}); tusb_control_request_t request = setup_request( Operation::kHapticsExperiment, TUSB_DIR_OUT, static_cast(next_out_payload.size())); require(!usb_configuration_management_vendor_control( 0, CONTROL_STAGE_SETUP, &request), "disabled firmware accepted experiment control"); } #else std::vector expected(84, 0); expected[73] = 32; perform_haptics_out(3, 0, false); perform_haptics_out(1, 4, false); perform_haptics_out(0, 0xff, false); require(haptics_request_count == 0, "malformed control reached the experiment service"); next_out_payload = make_request(Operation::kHapticsExperiment, {1, 2}); next_out_payload.back() ^= 1; tusb_control_request_t request = setup_request( Operation::kHapticsExperiment, TUSB_DIR_OUT, static_cast(next_out_payload.size())); require(usb_configuration_management_vendor_control( 0, CONTROL_STAGE_SETUP, &request) && !usb_configuration_management_vendor_control( 0, CONTROL_STAGE_DATA, &request), "bad experiment request CRC was accepted"); require(haptics_request_count == 0, "bad CRC control reached the experiment service"); perform_haptics_out(2, 2); auto payload = read_haptics_payload(); require(payload[68] == 1 && payload[69] == 2 && payload[72] == 1 && read_u32(payload, 0) == 1 && read_u32(payload, 16) == 0, "USB acceptance must remain pending until the service starts"); perform_haptics_out(1, 1, false); perform_haptics_out(2, 1, false); payload = read_haptics_payload(); require(payload[68] == 1 && payload[69] == 2 && payload[72] == 1 && read_u32(payload, 0) == 1, "busy start overwrote the accepted run"); // Model the independently progressing Core 1 service, not a USB echo. current_haptics = {}; current_haptics.run_id = 1; current_haptics.connection_generation = 0x11223344; current_haptics.start_us = 0xffff0000; current_haptics.generated_packets = 103; current_haptics.sent_packets = 101; current_haptics.skipped_packets = 2; current_haptics.send_failures = 3; current_haptics.can_send_requests = 106; current_haptics.synchronous_callbacks = 4; current_haptics.max_generate_us = 123; current_haptics.max_send_gap_us = 22000; current_haptics.max_lateness_us = 11001; current_haptics.max_request_wait_us = 9876; current_haptics.first_tone_due_us = 0xfffffff0; current_haptics.first_tone_sent_us = 0x30; current_haptics.last_sent_us = 0x76543210; current_haptics.last_pcm_end_us = 0xfedcba98; // Internal cue coverage is not wire data. current_haptics.elapsed_us = 1100000; current_haptics.state = HapticsExperimentState::kRunning; current_haptics.slot = 2; current_haptics.mode = 1; current_haptics.host_updates = 0x89abcdef; current_haptics.dropped_updates = 0x12345678; const uint32_t fields[] = { 1, 0x11223344, 0xffff0000, 103, 101, 2, 3, 106, 4, 123, 22000, 11001, 9876, 0xfffffff0, 0x30, 0x76543210, 1100000, }; for (size_t index = 0; index < 17; ++index) { write_u32(&expected, index * 4, fields[index]); } expected[68] = 2; expected[69] = 2; expected[72] = 1; write_u32(&expected, 76, 0x89abcdef); write_u32(&expected, 80, 0x12345678); require(read_haptics_payload() == expected, "schema-3 timing and gameplay mode fields are not in wire order"); perform_haptics_out(0, 2); payload = read_haptics_payload(); require(payload[68] == 2 && payload[72] == 1 && read_u32(payload, 0) == 1, "USB stop ACK must not fabricate terminal completion"); current_haptics.state = HapticsExperimentState::kStopped; payload = read_haptics_payload(); require(payload[68] == 4 && payload[72] == 1, "service stop transition was not observable"); next_out_payload = make_request(Operation::kHapticsExperiment, {1, 3}); request = setup_request( Operation::kHapticsExperiment, TUSB_DIR_OUT, static_cast(next_out_payload.size())); const uint32_t requests_before_cancel = haptics_request_count; require(usb_configuration_management_vendor_control( 0, CONTROL_STAGE_SETUP, &request), "canceled experiment setup was rejected"); read_haptics_payload(); // A new SETUP cancels the previous OUT transfer. require(!usb_configuration_management_vendor_control( 0, CONTROL_STAGE_DATA, &request) && !usb_configuration_management_vendor_control( 0, CONTROL_STAGE_ACK, &request) && haptics_request_count == requests_before_cancel, "canceled experiment request reused stale payload"); perform_haptics_out(1, 3); current_haptics.state = HapticsExperimentState::kDisconnected; current_haptics.last_error = 3; payload = read_haptics_payload(); require(payload[68] == 5 && payload[69] == 3 && payload[70] == 3 && read_u32(payload, 0) == 2 && payload[72] == 0, "asynchronous connection failure lost request correlation"); #endif } void test_haptics_transport_probe_requests() { using namespace UsbConfigurationManagement; for (uint16_t length : {0, 16, 18, 120}) { tusb_control_request_t request = setup_request( Operation::kHapticsTransportProbe, TUSB_DIR_OUT, length); for (uint8_t stage : { CONTROL_STAGE_SETUP, CONTROL_STAGE_DATA, CONTROL_STAGE_ACK}) { require(!usb_configuration_management_vendor_control( 0, stage, &request), "IN-only transport probe accepted an OUT transfer"); } } #ifdef SWITCH_PICO_HAPTICS_EXPERIMENT current_transport.run_id = 0x10203040; current_transport.connection_generation = 0x50607080; current_transport.connection_handle = 0xffff; current_transport.timer_wakes = 4; current_transport.max_timer_lateness_us = 5; current_transport.total_timer_lateness_us = 6; current_transport.send_calls = 7; current_transport.max_send_us = 8; current_transport.total_send_us = 9; current_transport.write_calls = 10; current_transport.max_write_us = 11; current_transport.total_write_us = 12; current_transport.read_calls = 13; current_transport.read_packets = 14; current_transport.max_read_us = 15; current_transport.total_read_us = 16; current_transport.poll_calls = 17; current_transport.max_poll_us = 18; current_transport.total_poll_us = 19; current_transport.completion_events = 20; current_transport.completed_packets = 21; current_transport.max_completion_gap_us = 22; current_transport.max_outstanding_acl = 23; current_transport.min_free_acl = 24; current_transport.first_tone_send_return_us = 0xfffffff0; current_transport.active = 1; current_transport.max_permission_wait_us = 27; current_transport.total_permission_wait_us = 0xffffffff; current_transport.permission_callbacks = 29; current_transport.max_poll_gap_us = 30; current_transport.controller_acl_packet_bytes = 1021; current_transport.controller_acl_packet_count = 10; #endif tusb_control_request_t request = setup_request( Operation::kHapticsTransportProbe, TUSB_DIR_IN, kMaximumResponseSize); require(usb_configuration_management_vendor_control( 0, CONTROL_STAGE_SETUP, &request), "transport probe IN was rejected"); require(control_payload.size() >= kResponseHeaderSize, "transport probe response header is truncated"); require(control_payload[5] == 0x41 && control_payload[7] == 0 && read_u16(control_payload, 10) == 3, "transport probe operation, flags, or schema are invalid"); const std::vector payload( control_payload.begin() + kResponseHeaderSize, control_payload.end()); require(read_u32(control_payload, 16) == configuration_crc32(payload.data(), payload.size()), "transport probe response CRC is invalid"); #ifdef SWITCH_PICO_HAPTICS_EXPERIMENT require(control_payload.size() == kResponseHeaderSize + 176 && read_u16(control_payload, 8) == 176 && control_payload[6] == static_cast(Status::kOk) && read_u32(control_payload, 12) == 0x10203040, "transport probe schema-3 envelope is invalid"); const uint32_t fields[] = { 0x10203040, 0x50607080, 0xffff, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 0xfffffff0, 1, 27, 0xffffffff, 29, 30, 1021, 10, }; std::vector expected(176); for (size_t index = 0; index < 32; ++index) { write_u32(&expected, index * 4, fields[index]); } require(payload == expected, "transport probe fields are not in explicit little-endian wire order"); #else require(control_payload.size() == kResponseHeaderSize && read_u16(control_payload, 8) == 0 && read_u32(control_payload, 12) == 0 && control_payload[6] == static_cast(Status::kUnsupportedSchema), "disabled firmware must report the transport probe as unsupported"); #endif } } // namespace uint32_t configuration_crc32(const uint8_t* data, size_t size) { uint32_t crc = 0xffffffffu; for (size_t index = 0; index < size; ++index) { crc ^= data[index]; for (uint8_t bit = 0; bit < 8; ++bit) { const uint32_t mask = 0u - (crc & 1u); crc = (crc >> 1) ^ (0xedb88320u & mask); } } return ~crc; } void configuration_service_snapshot(ConfigurationServiceSnapshot* output) { *output = current_configuration; } ConfigurationTransactionStatus configuration_service_begin( uint32_t transaction_id, uint16_t, size_t, uint32_t) { begin_transaction_id = transaction_id; return ConfigurationTransactionStatus::kReceiving; } ConfigurationTransactionStatus configuration_service_append( uint32_t transaction_id, size_t offset, const uint8_t* data, size_t size) { append_transaction_id = transaction_id; append_offset = offset; appended_bytes.assign(data, data + size); return ConfigurationTransactionStatus::kReceiving; } ConfigurationTransactionStatus configuration_service_commit( uint32_t transaction_id) { commit_transaction_id = transaction_id; return ConfigurationTransactionStatus::kPending; } ConfigurationTransactionStatus configuration_service_reset(uint32_t) { return ConfigurationTransactionStatus::kPending; } const AdapterModeAvailability& adapter_usb_mode_availability() { ++mode_availability_query_count; return runtime_mode_availability; } ConfigurationTransactionStatus configuration_service_set_mode( uint32_t transaction_id, AdapterRequestedMode requested_mode, const AdapterModeAvailability& availability) { mode_set_transaction_id = transaction_id; mode_set_requested_mode = requested_mode; mode_set_availability = availability; ++mode_set_call_count; if (!adapter_requested_mode_available(requested_mode, availability)) { return ConfigurationTransactionStatus::kUnsupportedSchema; } return mode_set_result; } AdapterUsbMode usb_output_driver_mode() { return current_active_mode; } uint8_t usb_output_driver_capabilities() { return current_capabilities; } bool adapter_reboot_for_mode_transaction(uint32_t transaction_id) { reboot_transaction_id = transaction_id; ++reboot_call_count; return transaction_id == correlated_reboot_transaction_id; } ConfigurationTransactionStatus profile_service_select( const ControllerIdentity& identity, uint8_t selected_profile) { profile_identity = identity; profile_index = selected_profile; return ConfigurationTransactionStatus::kPending; } ConfigurationTransactionStatus profile_service_set_metadata( uint32_t transaction_id, const ControllerIdentity& identity, uint8_t selected_profile, const char* value, size_t value_size) { profile_metadata_requested = true; profile_metadata_transaction_id = transaction_id; profile_identity = identity; profile_metadata_index = selected_profile; profile_metadata_value.assign(value, value + value_size); return ConfigurationTransactionStatus::kPending; } ConfigurationTransactionStatus profile_service_begin( uint32_t transaction_id, const ControllerIdentity& identity, uint8_t selected_profile, uint16_t schema_version, size_t payload_size, uint32_t payload_crc) { begin_transaction_id = transaction_id; profile_identity = identity; profile_index = selected_profile; profile_schema = schema_version; profile_size = payload_size; profile_crc = payload_crc; return ConfigurationTransactionStatus::kReceiving; } ConfigurationTransactionStatus profile_service_append( uint32_t transaction_id, size_t offset, const uint8_t* data, size_t size) { append_transaction_id = transaction_id; append_offset = offset; appended_bytes.assign(data, data + size); return ConfigurationTransactionStatus::kReceiving; } ConfigurationTransactionStatus profile_service_commit( uint32_t transaction_id) { profile_commit_transaction_id = transaction_id; return ConfigurationTransactionStatus::kPending; } void profile_service_metadata_snapshot( ProfileServiceMetadataSnapshot* output) { *output = current_profile_metadata; } bool bluepad32_input_backend_identify( const ControllerIdentity&) { identify_requested = true; return true; } bool bluepad32_input_backend_set_wii_orientation( const ControllerIdentity&, uint32_t, bool) { return true; } ConfigurationTransactionStatus profile_service_reset( uint32_t transaction_id, const ControllerIdentity& identity, uint8_t selected_profile) { profile_reset_transaction_id = transaction_id; profile_identity = identity; profile_index = selected_profile; profile_reset_requested = true; return ConfigurationTransactionStatus::kPending; } ConfigurationTransactionStatus profile_service_activate( uint32_t transaction_id, const ControllerIdentity& identity, uint8_t selected_profile) { profile_activate_transaction_id = transaction_id; profile_identity = identity; profile_index = selected_profile; profile_activate_requested = true; return ConfigurationTransactionStatus::kPending; } void profile_service_list_snapshot(ProfileServiceListSnapshot* output) { *output = current_profile_list; } void profile_service_selected_snapshot( ProfileServiceSelectedSnapshot* output) { *output = current_profile_selected; } void profile_service_transaction_snapshot( ProfileServiceTransactionSnapshot* output) { *output = current_profile_transaction; } namespace { ControllerMacroCapture capture_fixture; } bool bluepad32_input_backend_capture_start( uint8_t slot, uint32_t generation, const CaptureOptions& options) { return capture_fixture.start(slot, generation, options, 0, controller_neutral_state()); } bool bluepad32_input_backend_capture_stop(uint32_t run_id) { if (run_id == 0 || run_id != capture_fixture.run_id()) return false; capture_fixture.stop(100000); return true; } bool bluepad32_input_backend_capture_page( uint32_t run_id, uint16_t first, Bluepad32CaptureSnapshot* output) { if (output == nullptr || (run_id && run_id != capture_fixture.run_id()) || first > capture_fixture.event_count()) return false; *output = {}; output->run_id = capture_fixture.run_id(); output->connection_generation = capture_fixture.generation(); output->elapsed_us = capture_fixture.elapsed_us(100000); output->slot = capture_fixture.slot(); output->state = capture_fixture.state(); output->options = capture_fixture.options(); output->total_events = capture_fixture.event_count(); output->first_index = first; while (output->event_count < BLUEPAD32_CAPTURE_PAGE_EVENTS && capture_fixture.event(first + output->event_count, &output->events[output->event_count])) { ++output->event_count; } return true; } bool bluepad32_input_backend_request_switch2_wake(uint32_t) { ++wake_request_calls; return accept_wake_request; } void bluepad32_input_backend_switch2_wake_snapshot(Bluepad32Switch2WakeStatus* output) { *output = current_wake_status; } void bluepad32_input_backend_request_pairing_snapshot() { refresh_requested = true; } uint32_t bluepad32_input_backend_clear_pairings() { clear_requested = true; return 1; } void bluepad32_input_backend_pairing_snapshot( Bluepad32PairingSnapshot* out) { *out = current_pairings; } void bluepad32_input_backend_playtest_snapshot( uint8_t slot, Bluepad32PlaytestSnapshot* out) { *out = current_playtest[slot]; } void bluepad32_input_backend_diagnostics( Bluepad32BackendDiagnostics* out) { *out = current_diagnostics; } #ifdef SWITCH_PICO_HAPTICS_EXPERIMENT bool haptics_experiment_request(uint8_t action, uint8_t slot) { ++haptics_request_count; if ((action == 1 || action == 2) && (current_haptics.state == HapticsExperimentState::kPending || current_haptics.state == HapticsExperimentState::kRunning)) { return false; } if (action == 1 || action == 2) { const uint32_t run_id = current_haptics.run_id + 1; current_haptics = {}; current_haptics.run_id = run_id; current_haptics.slot = slot; current_haptics.state = HapticsExperimentState::kPending; current_haptics.mode = action == 2 ? 1 : 0; } return true; } void haptics_experiment_snapshot(HapticsExperimentDiagnostics* output) { *output = current_haptics; } void haptics_transport_probe_snapshot(HapticsTransportProbe* output) { *output = current_transport; } #endif bool adapter_reboot_to_bootsel() { bootsel_reboot_requested = true; return true; } bool adapter_host_probe_vendor_control( uint8_t, uint8_t, const tusb_control_request_t*) { return false; } bool tud_control_xfer(uint8_t, const tusb_control_request_t* request, void* buffer, uint16_t length) { if (request->bmRequestType_bit.direction == TUSB_DIR_OUT) { if (next_out_payload.size() != length) { return false; } memcpy(buffer, next_out_payload.data(), length); } else { const auto* bytes = static_cast(buffer); control_payload.assign(bytes, bytes + length); } return true; } bool tud_control_status(uint8_t, const tusb_control_request_t*) { return true; } #include "configuration/adapter_configuration.cpp" #include "core/controller_identity.cpp" #include "profile/controller_profile.cpp" #include "usb/usb_configuration_management.cpp" int main() { current_configuration.state = ConfigurationServiceState::kReady; current_configuration.configuration = adapter_configuration_default(); test_envelope_encoding(); test_pairing_encoding(); test_vendor_requests(); test_mode_vendor_requests(); test_profile_vendor_requests(); test_switch2_wake_requests(); test_haptics_experiment_requests(); test_haptics_transport_probe_requests(); return 0; }