diff --git a/bluepad32_config/parser/uni_hid_parser_imu.h b/bluepad32_config/parser/uni_hid_parser_imu.h new file mode 100644 index 0000000..4e4cf73 --- /dev/null +++ b/bluepad32_config/parser/uni_hid_parser_imu.h @@ -0,0 +1,242 @@ +#pragma once + +#include +#include +#include +#include +#include + +#define UNI_IMU_ACCEL_RES_PER_G 8192 +#define UNI_IMU_GYRO_RES_PER_DEG_S 1024 +#define UNI_PSMOVE_CALIBRATION_REPORT_SIZE 49 +#define UNI_PSMOVE_ZCM1_CALIBRATION_SIZE 143 +#define UNI_PSMOVE_ZCM2_CALIBRATION_SIZE 96 + +typedef enum { + UNI_PSMOVE_IMU_MODEL_UNKNOWN = 0, + UNI_PSMOVE_IMU_MODEL_ZCM1, + UNI_PSMOVE_IMU_MODEL_ZCM2, +} uni_psmove_imu_model_t; + +typedef enum { + UNI_PSMOVE_CALIBRATION_INVALID = 0, + UNI_PSMOVE_CALIBRATION_INCOMPLETE, + UNI_PSMOVE_CALIBRATION_COMPLETE, +} uni_psmove_calibration_result_t; + +typedef struct { + int32_t accel[3]; + int32_t gyro[3]; +} uni_imu_fixed_sample_t; + +typedef struct { + uint8_t data[UNI_PSMOVE_ZCM1_CALIBRATION_SIZE]; + uni_psmove_imu_model_t model; + uint8_t received_blocks; + bool complete; +} uni_psmove_imu_calibration_t; + +static inline int32_t uni_imu_clamp_i64(int64_t value) { + if (value > INT32_MAX) { + return INT32_MAX; + } + if (value < INT32_MIN) { + return INT32_MIN; + } + return (int32_t)value; +} + +static inline int32_t uni_imu_scale(int32_t value, int32_t span, + int32_t full_scale) { + if (span <= 0) { + return 0; + } + return uni_imu_clamp_i64((int64_t)value * full_scale / span); +} + +static inline int32_t uni_psmove_scale_gyro(int32_t raw, int32_t bias, + int32_t span, + int32_t full_scale) { + if (span <= 0) { + return 0; + } + return uni_imu_clamp_i64( + ((int64_t)raw - bias) * full_scale / span); +} + +static inline int32_t uni_psmove_decode_value( + uni_psmove_imu_model_t model, uint16_t value) { + if (model == UNI_PSMOVE_IMU_MODEL_ZCM1) { + return (int32_t)value - 0x8000; + } + return (int16_t)value; +} + +static inline int32_t uni_psmove_read_calibration_value( + const uint8_t* data, uni_psmove_imu_model_t model, uint8_t offset) { + const uint16_t value = + (uint16_t)(data[offset] | ((uint16_t)data[offset + 1] << 8)); + return uni_psmove_decode_value(model, value); +} + +static inline uni_psmove_calibration_result_t +uni_psmove_add_calibration_report(uni_psmove_imu_calibration_t* calibration, + uni_psmove_imu_model_t model, + const uint8_t* report, uint16_t length) { + if (calibration == NULL || report == NULL || + length != UNI_PSMOVE_CALIBRATION_REPORT_SIZE || report[0] != 0x10 || + (model != UNI_PSMOVE_IMU_MODEL_ZCM1 && + model != UNI_PSMOVE_IMU_MODEL_ZCM2)) { + return UNI_PSMOVE_CALIBRATION_INVALID; + } + if (calibration->model != UNI_PSMOVE_IMU_MODEL_UNKNOWN && + calibration->model != model) { + return UNI_PSMOVE_CALIBRATION_INVALID; + } + calibration->model = model; + + size_t offset; + size_t source_offset; + uint8_t block_mask; + switch (report[1]) { + case 0x00: + offset = 0; + source_offset = 0; + block_mask = 0x01; + break; + case 0x01: + if (model != UNI_PSMOVE_IMU_MODEL_ZCM1) { + return UNI_PSMOVE_CALIBRATION_INVALID; + } + offset = UNI_PSMOVE_CALIBRATION_REPORT_SIZE; + source_offset = 2; + block_mask = 0x02; + break; + case 0x81: + if (model != UNI_PSMOVE_IMU_MODEL_ZCM2) { + return UNI_PSMOVE_CALIBRATION_INVALID; + } + offset = UNI_PSMOVE_CALIBRATION_REPORT_SIZE; + source_offset = 2; + block_mask = 0x02; + break; + case 0x82: + if (model != UNI_PSMOVE_IMU_MODEL_ZCM1) { + return UNI_PSMOVE_CALIBRATION_INVALID; + } + offset = 2 * UNI_PSMOVE_CALIBRATION_REPORT_SIZE - 2; + source_offset = 2; + block_mask = 0x04; + break; + default: + return UNI_PSMOVE_CALIBRATION_INVALID; + } + + const size_t copy_size = length - source_offset; + const size_t calibration_size = + model == UNI_PSMOVE_IMU_MODEL_ZCM1 + ? UNI_PSMOVE_ZCM1_CALIBRATION_SIZE + : UNI_PSMOVE_ZCM2_CALIBRATION_SIZE; + if (offset + copy_size > calibration_size) { + return UNI_PSMOVE_CALIBRATION_INVALID; + } + memcpy(&calibration->data[offset], &report[source_offset], copy_size); + calibration->received_blocks |= block_mask; + + const uint8_t required_blocks = + model == UNI_PSMOVE_IMU_MODEL_ZCM1 ? 0x07 : 0x03; + calibration->complete = + (calibration->received_blocks & required_blocks) == required_blocks; + return calibration->complete ? UNI_PSMOVE_CALIBRATION_COMPLETE + : UNI_PSMOVE_CALIBRATION_INCOMPLETE; +} + +static inline bool uni_psmove_normalize_imu( + uni_psmove_imu_model_t model, + const uni_psmove_imu_calibration_t* calibration, + const uint16_t accel_first[3], const uint16_t accel_second[3], + const uint16_t gyro_first[3], const uint16_t gyro_second[3], + uni_imu_fixed_sample_t* output) { + if (output == NULL) { + return false; + } + memset(output, 0, sizeof(*output)); + if (calibration == NULL || !calibration->complete || + calibration->model != model || accel_first == NULL || + accel_second == NULL || gyro_first == NULL || gyro_second == NULL) { + return false; + } + + static const uint8_t zcm1_accel_low[] = {0x0a, 0x24, 0x14}; + static const uint8_t zcm1_accel_high[] = {0x16, 0x1e, 0x08}; + static const uint8_t zcm2_accel_low[] = {0x08, 0x16, 0x24}; + static const uint8_t zcm2_accel_high[] = {0x02, 0x10, 0x1e}; + static const uint8_t zcm1_gyro_bias[] = {0x2a, 0x2c, 0x2e}; + static const uint8_t zcm1_gyro_high[] = {0x46, 0x50, 0x5a}; + static const uint8_t zcm2_gyro_bias[] = {0x26, 0x28, 0x2a}; + static const uint8_t zcm2_gyro_low[] = {0x42, 0x4a, 0x52}; + static const uint8_t zcm2_gyro_high[] = {0x30, 0x38, 0x40}; + + const uint8_t* accel_low = + model == UNI_PSMOVE_IMU_MODEL_ZCM1 ? zcm1_accel_low : zcm2_accel_low; + const uint8_t* accel_high = + model == UNI_PSMOVE_IMU_MODEL_ZCM1 ? zcm1_accel_high : zcm2_accel_high; + const uint8_t* gyro_bias = + model == UNI_PSMOVE_IMU_MODEL_ZCM1 ? zcm1_gyro_bias : zcm2_gyro_bias; + const uint8_t* gyro_high = + model == UNI_PSMOVE_IMU_MODEL_ZCM1 ? zcm1_gyro_high : zcm2_gyro_high; + const int32_t gyro_full_scale = + (model == UNI_PSMOVE_IMU_MODEL_ZCM1 ? 480 : 540) * + UNI_IMU_GYRO_RES_PER_DEG_S; + + for (uint8_t axis = 0; axis < 3; ++axis) { + const int32_t accel_low_value = uni_psmove_read_calibration_value( + calibration->data, model, accel_low[axis]); + const int32_t accel_high_value = uni_psmove_read_calibration_value( + calibration->data, model, accel_high[axis]); + const int32_t accel_center = + (accel_low_value + accel_high_value) / 2; + const int32_t accel_raw = + (uni_psmove_decode_value(model, accel_first[axis]) + + uni_psmove_decode_value(model, accel_second[axis])) / + 2; + const int32_t accel_delta = accel_raw - accel_center; + const int32_t accel_span = + accel_delta < 0 ? accel_center - accel_low_value + : accel_high_value - accel_center; + output->accel[axis] = + uni_imu_scale(accel_delta, accel_span, UNI_IMU_ACCEL_RES_PER_G); + + const int32_t gyro_bias_value = uni_psmove_read_calibration_value( + calibration->data, model, gyro_bias[axis]); + const int32_t gyro_raw = + (uni_psmove_decode_value(model, gyro_first[axis]) + + uni_psmove_decode_value(model, gyro_second[axis])) / + 2; + int32_t gyro_span; + if (model == UNI_PSMOVE_IMU_MODEL_ZCM1 || + gyro_raw >= gyro_bias_value) { + gyro_span = uni_psmove_read_calibration_value( + calibration->data, model, gyro_high[axis]) - + gyro_bias_value; + } else { + gyro_span = gyro_bias_value - uni_psmove_read_calibration_value( + calibration->data, model, + zcm2_gyro_low[axis]); + } + output->gyro[axis] = uni_psmove_scale_gyro( + gyro_raw, gyro_bias_value, gyro_span, gyro_full_scale); + } + return true; +} + +static inline void uni_imu_normalize_wii_accel(int32_t x, int32_t y, + int32_t z, + int32_t output[3]) { + if (output == NULL) { + return; + } + output[0] = uni_imu_scale(-x, 100, UNI_IMU_ACCEL_RES_PER_G); + output[1] = uni_imu_scale(z, 100, UNI_IMU_ACCEL_RES_PER_G); + output[2] = uni_imu_scale(y, 100, UNI_IMU_ACCEL_RES_PER_G); +} diff --git a/bluepad32_input_backend.cpp b/bluepad32_input_backend.cpp index 1e84c6a..12d4f29 100644 --- a/bluepad32_input_backend.cpp +++ b/bluepad32_input_backend.cpp @@ -20,7 +20,14 @@ constexpr int32_t kAxisMinimum = -512; constexpr int32_t kAxisMaximum = 511; constexpr int32_t kTriggerMaximum = 1023; constexpr int32_t kTriggerThreshold = (kTriggerMaximum * 35) / 100; +#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY +// XInput vibration is stateful: it remains active until XInputSetState sends +// a new magnitude. Use the longest Bluepad32 duration and stop explicitly on +// the zero-magnitude packet. +constexpr uint16_t kRumbleDurationMs = UINT16_MAX; +#else constexpr uint16_t kRumbleDurationMs = 50; +#endif constexpr uint32_t kRumblePollIntervalMs = 5; constexpr uint8_t kSlotCount = BLUEPAD32_INPUT_BACKEND_SLOT_COUNT; constexpr uint32_t kPairingWindowDurationMs = 60000; @@ -725,8 +732,11 @@ void process_rumble_timer(btstack_timer_source_t* timer) { feedback.weak_magnitude, feedback.strong_magnitude); } else if (host_dispatch && device->report_parser.play_dual_rumble != nullptr) { + const bool stop = + envelope.rumble.low_frequency_magnitude == 0 && + envelope.rumble.high_frequency_magnitude == 0; device->report_parser.play_dual_rumble( - device, 0, kRumbleDurationMs, + device, 0, stop ? 0 : kRumbleDurationMs, envelope.rumble.high_frequency_magnitude, envelope.rumble.low_frequency_magnitude); } diff --git a/firmware/switch-pico-adapter-feasibility.uf2 b/firmware/switch-pico-adapter-feasibility.uf2 new file mode 100644 index 0000000..ec4a41e Binary files /dev/null and b/firmware/switch-pico-adapter-feasibility.uf2 differ diff --git a/switch_pro_driver.cpp b/switch_pro_driver.cpp index 964a682..9cc7490 100644 --- a/switch_pro_driver.cpp +++ b/switch_pro_driver.cpp @@ -1090,8 +1090,10 @@ uint8_t const* tud_descriptor_device_cb(void) { if (adapter_host_probe_mode() == AdapterUsbMode::kXInput) { return XInputFeasibility::kDeviceDescriptor; } -#endif + return XInputFeasibility::kSwitchProbeDeviceDescriptor; +#else return switch_pro_device_descriptor; +#endif } uint8_t const* tud_descriptor_configuration_cb(uint8_t index) { diff --git a/tests/bluepad32_backend_lifecycle_test.cpp b/tests/bluepad32_backend_lifecycle_test.cpp index cca33a1..db668ed 100644 --- a/tests/bluepad32_backend_lifecycle_test.cpp +++ b/tests/bluepad32_backend_lifecycle_test.cpp @@ -575,10 +575,18 @@ void test_independent_lifecycle() { for (int slot = 0; slot < kSlotCount; ++slot) { require(devices[slot].rumble_calls == 1 && devices[slot].last_low == 11 + slot && - devices[slot].last_high == 21 + slot, + devices[slot].last_high == 21 + slot && + devices[slot].last_rumble_duration_ms == + kRumbleDurationMs, "each slot rumble must reach only its indexed controller"); } + bluepad32_input_backend_queue_rumble(0, SwitchRumbleOutput{0, 0}); + process_rumble_timer(&g_rumble_timer); + require(devices[0].rumble_calls == 2 && + devices[0].last_rumble_duration_ms == 0, + "zero XInput magnitude must stop rumble immediately"); + bluepad32_input_backend_queue_rumble(3, SwitchRumbleOutput{55, 66}); const uint32_t disconnected_generation = g_slots[3].connection_generation; diff --git a/tests/test_bluepad32_backend_lifecycle_native.py b/tests/test_bluepad32_backend_lifecycle_native.py index a583717..65c0285 100644 --- a/tests/test_bluepad32_backend_lifecycle_native.py +++ b/tests/test_bluepad32_backend_lifecycle_native.py @@ -10,9 +10,10 @@ def test_bluepad32_backend_lifecycle_native(tmp_path: Path) -> None: compiler = shutil.which("c++") or shutil.which("g++") assert compiler is not None, "a host C++ compiler is required" - executable = tmp_path / "bluepad32_backend_lifecycle_test" - subprocess.run( - [ + for adapter_feasibility in (False, True): + suffix = "_adapter" if adapter_feasibility else "" + executable = tmp_path / f"bluepad32_backend_lifecycle_test{suffix}" + command = [ compiler, "-std=c++17", "-Wall", @@ -20,27 +21,31 @@ def test_bluepad32_backend_lifecycle_native(tmp_path: Path) -> None: "-Werror", "-pedantic", "-DSWITCH_PICO_HID_INSTANCE_COUNT=4", - f"-I{root / 'tests' / 'bluepad32_native_stubs'}", - f"-I{root}", - str(root / "tests" / "bluepad32_backend_lifecycle_test.cpp"), - "-o", - str(executable), - ], - check=True, - cwd=root, - ) + ] + if adapter_feasibility: + command.append("-DSWITCH_PICO_ADAPTER_FEASIBILITY=1") + command.extend( + [ + f"-I{root / 'tests' / 'bluepad32_native_stubs'}", + f"-I{root}", + str(root / "tests" / "bluepad32_backend_lifecycle_test.cpp"), + "-o", + str(executable), + ] + ) + subprocess.run(command, check=True, cwd=root) - for scenario in ( - "ready-forward", - "ready-reverse", - "rejections", - "lifecycle", - "pairing-policy", - "slot-lighting", - "abxy-hotkey", - "motion-hotkey", - "clear-pairings", - "flash-core-start", - "flash-core-failure", - ): - subprocess.run([str(executable), scenario], check=True, cwd=root) + for scenario in ( + "ready-forward", + "ready-reverse", + "rejections", + "lifecycle", + "pairing-policy", + "slot-lighting", + "abxy-hotkey", + "motion-hotkey", + "clear-pairings", + "flash-core-start", + "flash-core-failure", + ): + subprocess.run([str(executable), scenario], check=True, cwd=root) diff --git a/tests/test_bluepad32_imu_normalization_native.py b/tests/test_bluepad32_imu_normalization_native.py index 9995079..54a465b 100644 --- a/tests/test_bluepad32_imu_normalization_native.py +++ b/tests/test_bluepad32_imu_normalization_native.py @@ -19,6 +19,7 @@ def test_bluepad32_imu_normalization_native(tmp_path: Path) -> None: "-Wextra", "-Werror", "-pedantic", + f"-I{root / 'bluepad32_config'}", f"-I{root / 'external' / 'bluepad32' / 'src' / 'components' / 'bluepad32' / 'include'}", str(root / "tests" / "bluepad32_imu_normalization_test.cpp"), "-o", diff --git a/tests/xinput_feasibility_test.cpp b/tests/xinput_feasibility_test.cpp index 88873b4..c56131a 100644 --- a/tests/xinput_feasibility_test.cpp +++ b/tests/xinput_feasibility_test.cpp @@ -32,10 +32,27 @@ uint32_t read_le32(const uint8_t *data) { void test_device_and_configuration_descriptors() { using namespace XInputFeasibility; + expect(read_le16(&kSwitchProbeDeviceDescriptor[8]) == + kSwitchProbeVendorId, + "Switch probe VID mismatch"); + expect(read_le16(&kSwitchProbeDeviceDescriptor[10]) == + kSwitchProbeProductId, + "Switch probe PID mismatch"); + expect(read_le16(&kSwitchProbeDeviceDescriptor[12]) == + kSwitchProbeDeviceRevision, + "Switch probe revision mismatch"); + expect(kSwitchProbeDeviceRevision != 0x0210, + "Switch probe reuses the genuine controller cache identity"); expect(read_le16(&kDeviceDescriptor[8]) == kPrototypeVendorId, "prototype VID mismatch"); expect(read_le16(&kDeviceDescriptor[10]) == kPrototypeProductId, "prototype PID mismatch"); + expect(read_le16(&kDeviceDescriptor[12]) == + kPrototypeDeviceRevision, + "prototype revision mismatch"); + expect(kDeviceDescriptor[4] == 0 && kDeviceDescriptor[5] == 0 && + kDeviceDescriptor[6] == 0, + "multi-interface prototype is not a composite USB device"); expect(kPrototypeVendorId != 0x045e, "prototype must not impersonate Microsoft's VID"); expect(read_le16(&kConfigurationDescriptor[2]) == diff --git a/tools/Test-AdapterFeasibility.ps1 b/tools/Test-AdapterFeasibility.ps1 index 0b071de..5e94f03 100644 --- a/tools/Test-AdapterFeasibility.ps1 +++ b/tools/Test-AdapterFeasibility.ps1 @@ -87,10 +87,10 @@ function Get-UsbIdentityDevices { [Parameter(Mandatory = $true)] [string]$Vid, [Parameter(Mandatory = $true)] - [string]$Pid + [string]$ProductId ) - $pattern = "VID_{0}&PID_{1}" -f $Vid.ToUpperInvariant(), $Pid.ToUpperInvariant() + $pattern = "VID_{0}&PID_{1}" -f $Vid.ToUpperInvariant(), $ProductId.ToUpperInvariant() if (Get-Command Get-PnpDevice -ErrorAction SilentlyContinue) { return @(Get-PnpDevice -PresentOnly -ErrorAction SilentlyContinue | Where-Object { $_.InstanceId -like "*$pattern*" }) @@ -165,7 +165,7 @@ Write-Host "Switch Pico Windows XInput feasibility test" -ForegroundColor Cyan Write-Host "Prototype identities: Switch probe 057E:2009 -> XInput CAFE:4010" Write-Host "Disconnect other XInput controllers before continuing." -$baseline = Get-ConnectedXInputStates +$baseline = @(Get-ConnectedXInputStates) Add-Result -Name "Clean XInput baseline" -Passed ($baseline.Count -eq 0) ` -Detail ("{0} XInput controller(s) connected before the Pico" -f $baseline.Count) @@ -176,8 +176,8 @@ if (-not $SkipInteractive) { $disconnectDeadline = (Get-Date).AddSeconds(10) do { - $switchPresent = (Get-UsbIdentityDevices -Vid "057E" -Pid "2009").Count -gt 0 - $xinputPresent = (Get-UsbIdentityDevices -Vid "CAFE" -Pid "4010").Count -gt 0 + $switchPresent = @(Get-UsbIdentityDevices -Vid "057E" -ProductId "2009").Count -gt 0 + $xinputPresent = @(Get-UsbIdentityDevices -Vid "CAFE" -ProductId "4010").Count -gt 0 if (-not $switchPresent -and -not $xinputPresent) { break } Start-Sleep -Milliseconds 100 } while ((Get-Date) -lt $disconnectDeadline) @@ -199,13 +199,13 @@ $switchSeenAt = $null $xinputSeenAt = $null while ((Get-Date) -lt $deadline) { - if (-not $seenSwitch -and (Get-UsbIdentityDevices -Vid "057E" -Pid "2009").Count -gt 0) { + if (-not $seenSwitch -and @(Get-UsbIdentityDevices -Vid "057E" -ProductId "2009").Count -gt 0) { $seenSwitch = $true $switchSeenAt = (Get-Date) Write-Host "Observed Switch probe identity 057E:2009" -ForegroundColor DarkCyan } - if ((Get-UsbIdentityDevices -Vid "CAFE" -Pid "4010").Count -gt 0) { + if (@(Get-UsbIdentityDevices -Vid "CAFE" -ProductId "4010").Count -gt 0) { $seenXInput = $true $xinputSeenAt = (Get-Date) Write-Host "Observed XInput identity CAFE:4010" -ForegroundColor DarkCyan @@ -226,7 +226,7 @@ if ($SkipInteractive -and $seenXInput) { -Detail ("SwitchSeen={0}, XInputSeen={1}, transition={2} ms" -f $seenSwitch, $seenXInput, $transitionMs) } -$xinputPnp = @(Get-UsbIdentityDevices -Vid "CAFE" -Pid "4010") +$xinputPnp = @(Get-UsbIdentityDevices -Vid "CAFE" -ProductId "4010") Add-Result -Name "XInput PnP identity" -Passed ($xinputPnp.Count -gt 0) ` -Detail ("Found {0} present PnP node(s) for CAFE:4010" -f $xinputPnp.Count) @@ -237,7 +237,7 @@ Add-Result -Name "PnP device health" -Passed ($problemDevices.Count -eq 0) ` -Detail ("{0} unhealthy PnP node(s)" -f $problemDevices.Count) Start-Sleep -Seconds 2 -$connectedStates = Get-ConnectedXInputStates +$connectedStates = @(Get-ConnectedXInputStates) Add-Result -Name "Four XInput API slots" -Passed ($connectedStates.Count -eq 4) ` -Detail ("XInputGetState reports {0}/4 connected slots" -f $connectedStates.Count) @@ -344,7 +344,11 @@ $report = [ordered]@{ Results = @($script:Results) } -$report | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $OutputPath -Encoding UTF8 +$reportJson = $report | ConvertTo-Json -Depth 8 +[System.IO.File]::WriteAllText( + $OutputPath, + $reportJson, + [System.Text.UTF8Encoding]::new($false)) Write-Host "" Write-Host ("Wrote report: {0}" -f (Resolve-Path $OutputPath)) -ForegroundColor Cyan diff --git a/xinput_feasibility_descriptors.h b/xinput_feasibility_descriptors.h index 244bbff..785c8d1 100644 --- a/xinput_feasibility_descriptors.h +++ b/xinput_feasibility_descriptors.h @@ -15,6 +15,7 @@ namespace XInputFeasibility { constexpr uint16_t kPrototypeVendorId = 0xcafe; constexpr uint16_t kPrototypeProductId = 0x4010; +constexpr uint16_t kPrototypeDeviceRevision = 0x0101; constexpr uint8_t kInterfaceDescriptorSize = 39; constexpr uint16_t kConfigurationDescriptorSize = 9 + SWITCH_PICO_HID_INSTANCE_COUNT * kInterfaceDescriptorSize; @@ -23,21 +24,49 @@ constexpr uint16_t kMsCompatIdDescriptorSize = constexpr uint8_t kMsVendorRequest = 0x20; constexpr uint16_t kMsCompatIdIndex = 0x0004; +constexpr uint16_t kSwitchProbeVendorId = 0x057e; +constexpr uint16_t kSwitchProbeProductId = 0x2009; +// Windows caches Microsoft OS descriptor support by VID, PID, and bcdDevice. +// Use a revision distinct from genuine Pro Controllers so their cached +// "unsupported" result cannot suppress this probe's 0xEE request. +constexpr uint16_t kSwitchProbeDeviceRevision = 0x0211; + +static const uint8_t kSwitchProbeDeviceDescriptor[] = { + 0x12, + 0x01, // Device descriptor + 0x00, + 0x02, // USB 2.0 + 0x00, + 0x00, + 0x00, // Class information comes from the interface descriptor + 0x40, // Endpoint zero packet size + static_cast(kSwitchProbeVendorId & 0xff), + static_cast(kSwitchProbeVendorId >> 8), + static_cast(kSwitchProbeProductId & 0xff), + static_cast(kSwitchProbeProductId >> 8), + static_cast(kSwitchProbeDeviceRevision & 0xff), + static_cast(kSwitchProbeDeviceRevision >> 8), + 0x01, + 0x02, + 0x03, // Manufacturer, product, serial strings + 0x01, // One configuration +}; + static const uint8_t kDeviceDescriptor[] = { 0x12, 0x01, // Device descriptor 0x00, 0x02, // USB 2.0 - 0xff, - 0xff, - 0xff, // Vendor-specific device + 0x00, + 0x00, + 0x00, // Composite device; each interface binds to the XUSB driver 0x40, // Endpoint zero packet size static_cast(kPrototypeVendorId & 0xff), static_cast(kPrototypeVendorId >> 8), static_cast(kPrototypeProductId & 0xff), static_cast(kPrototypeProductId >> 8), - 0x00, - 0x01, // Prototype revision 1.00 + static_cast(kPrototypeDeviceRevision & 0xff), + static_cast(kPrototypeDeviceRevision >> 8), 0x01, 0x02, 0x03, // Manufacturer, product, serial strings @@ -116,6 +145,7 @@ static const uint8_t kProbeMsCompatIdDescriptor[] = { }; static_assert(sizeof(kDeviceDescriptor) == 18); +static_assert(sizeof(kSwitchProbeDeviceDescriptor) == 18); static_assert(sizeof(kConfigurationDescriptor) == kConfigurationDescriptorSize); static_assert(sizeof(kMsCompatIdDescriptor) == kMsCompatIdDescriptorSize); static_assert(sizeof(kProbeMsCompatIdDescriptor) == 16);