Fix Windows adapter enumeration and rumble

This commit is contained in:
Joey Yakimowich-Payne 2026-09-02 12:29:57 -06:00
commit db4a860cd6
10 changed files with 363 additions and 44 deletions

View file

@ -0,0 +1,242 @@
#pragma once
#include <limits.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <string.h>
#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);
}

View file

@ -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);
}

Binary file not shown.

View file

@ -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) {

View file

@ -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;

View file

@ -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)

View file

@ -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",

View file

@ -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]) ==

View file

@ -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

View file

@ -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<uint8_t>(kSwitchProbeVendorId & 0xff),
static_cast<uint8_t>(kSwitchProbeVendorId >> 8),
static_cast<uint8_t>(kSwitchProbeProductId & 0xff),
static_cast<uint8_t>(kSwitchProbeProductId >> 8),
static_cast<uint8_t>(kSwitchProbeDeviceRevision & 0xff),
static_cast<uint8_t>(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<uint8_t>(kPrototypeVendorId & 0xff),
static_cast<uint8_t>(kPrototypeVendorId >> 8),
static_cast<uint8_t>(kPrototypeProductId & 0xff),
static_cast<uint8_t>(kPrototypeProductId >> 8),
0x00,
0x01, // Prototype revision 1.00
static_cast<uint8_t>(kPrototypeDeviceRevision & 0xff),
static_cast<uint8_t>(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);