feat(native-usb): add 32-frame HD gameplay rumble and USB flight diagnostics

Preserve native frequency/amplitude timelines for DualSense PCM output,
remove the unsupported 64-frame path, and retain compatibility rumble for
other controllers. Use the validated 300 MHz sampling phase and restrict
non-bondable Classic discovery autoconnect to previously paired peers.

Buffer native-hub UART stdout and release USB IRQs around port-reset
callbacks. Add observer liveness and pre-SETUP root-response observations
without changing recovery behavior. Cover transport and haptics boundaries.

Record the user-accepted 0.108 trial: controls remained responsive and
rumble felt fine. Instrumentation changes timing; the disconnect root
cause and long-term reliability remain unqualified.

Validation: 624 tests, 11 affected firmware/probe builds, and on-device
concurrent USB and HD-auto-start checks. Private captures, generated
images, and unrelated working-tree files are intentionally excluded.
This commit is contained in:
Joey Yakimowich-Payne 2026-09-19 17:03:46 -06:00
commit 80b788099b
46 changed files with 2873 additions and 412 deletions

View file

@ -681,10 +681,21 @@ static void test_indexed_memory(void) {
}
static void expect_invalid_rumble(uint8_t report_id, const uint8_t* data, size_t length) {
probe_rumble_frame output = {.count = 3, .magnitude = {17, 93, 241}};
NativeHapticsActuatorFrame output;
memset(&output, 0xa5, sizeof(output));
NativeHapticsActuatorFrame before;
memcpy(&before, &output, sizeof(before));
assert(!probe_protocol_decode_rumble(report_id, data, length, &output));
assert(output.count == 3);
assert(output.magnitude[0] == 17 && output.magnitude[1] == 93 && output.magnitude[2] == 241);
assert(memcmp(&output, &before, sizeof(output)) == 0);
}
static void expect_wave(const NativeHapticsSample* sample, uint16_t low_frequency,
uint16_t high_frequency, uint16_t low_amplitude,
uint16_t high_amplitude) {
assert(sample->low_frequency_code == low_frequency);
assert(sample->high_frequency_code == high_frequency);
assert(sample->low_amplitude == low_amplitude);
assert(sample->high_amplitude == high_amplitude);
}
static void test_native_rumble(void) {
@ -695,49 +706,52 @@ static void test_native_rumble(void) {
{0x50, 0x81, 0x01, 0x10, 0x1e, 0x00},
{0x52, 0x9f, 0x19, 0xe0, 0x9d, 0x00},
};
probe_rumble_frame output;
NativeHapticsActuatorFrame output;
uint8_t wire[65];
for (unsigned i = 0; i < 2; ++i) {
memset(wire, 0xa5, sizeof(wire));
wire[0] = 0x01;
memcpy(wire + 1, captured_blocks[i], sizeof(captured_blocks[i]));
assert(probe_protocol_decode_rumble(0, wire, 64, &output));
assert(output.count == 1 && output.magnitude[0] == i);
assert(output.sample_count == 1);
expect_wave(&output.samples[0], i ? 415 : 385, i ? 478 : 481, i ? 6 : 0, i ? 2 : 0);
assert(probe_protocol_decode_rumble(1, wire + 1, 63, &output));
assert(output.count == 1 && output.magnitude[0] == i);
assert(output.sample_count == 1);
expect_wave(&output.samples[0], i ? 415 : 385, i ? 478 : 481, i ? 6 : 0, i ? 2 : 0);
}
// Manually specified byte boundaries, not an encoder/decoder roundtrip.
// Frequencies are both 1023: they must not leak into either amplitude,
// nor be rejected merely because this compatibility decoder ignores them.
// All four fields cross byte boundaries. The decoder preserves codes even
// outside a particular output actuator's renderable frequency range.
static const struct {
uint8_t sample[5];
uint8_t expected;
uint16_t low, high;
} boundaries[] = {
{{0xff, 0x03, 0xf0, 0x3f, 0x00}, 0}, // amplitudes 0, 0
{{0xff, 0x0b, 0xf0, 0x3f, 0x00}, 0}, // 2, 0 rounds down
{{0xff, 0x03, 0xf0, 0xff, 0x00}, 1}, // 0, 3 rounds up
{{0xff, 0xff, 0xf0, 0x3f, 0x00}, 16}, // 63, 0
{{0xff, 0x03, 0xf1, 0x3f, 0x00}, 16}, // 64, 0
{{0xff, 0xff, 0xf7, 0x3f, 0x80}, 128}, // 511, 512
{{0xff, 0x03, 0xf8, 0xff, 0x7f}, 128}, // 512, 511
{{0xff, 0xff, 0xff, 0x3f, 0x00}, 255}, // 1023, 0
{{0xff, 0x03, 0xf0, 0xff, 0xff}, 255}, // 0, 1023
{{0xff, 0x03, 0xf0, 0x3f, 0x00}, 0, 0},
{{0xff, 0x0b, 0xf0, 0x3f, 0x00}, 2, 0},
{{0xff, 0x03, 0xf0, 0xff, 0x00}, 0, 3},
{{0xff, 0xff, 0xf0, 0x3f, 0x00}, 63, 0},
{{0xff, 0x03, 0xf1, 0x3f, 0x00}, 64, 0},
{{0xff, 0xff, 0xf7, 0x3f, 0x80}, 511, 512},
{{0xff, 0x03, 0xf8, 0xff, 0x7f}, 512, 511},
{{0xff, 0xff, 0xff, 0x3f, 0x00}, 1023, 0},
{{0xff, 0x03, 0xf0, 0xff, 0xff}, 0, 1023},
};
wire[1] = 0x5f;
for (unsigned i = 0; i < sizeof(boundaries) / sizeof(boundaries[0]); ++i) {
memcpy(wire + 2, boundaries[i].sample, 5);
assert(probe_protocol_decode_rumble(0, wire, 17, &output));
assert(output.count == 1 && output.magnitude[0] == boundaries[i].expected);
assert(output.sample_count == 1);
expect_wave(&output.samples[0], 1023, 1023, boundaries[i].low, boundaries[i].high);
}
// Three distinguishable samples retain wire order; a shorter count ignores
// stale later samples. Both callback envelopes accept minimal/compact/USB sizes.
static const uint8_t ordered[16] = {
0x70,
0x00, 0xfc, 0x0f, 0x00, 0x00, // amplitudes 1023, 0 -> 255
0x00, 0x00, 0x00, 0xc0, 0x3f, // amplitudes 0, 255 -> 64
0xff, 0xff, 0xf7, 0x3f, 0x80, // amplitudes 511, 512 -> 128
0x81, 0x05, 0x18, 0x5e, 0x40, // codes 385/481, amplitudes 513/257
0x82, 0x05, 0x18, 0x5e, 0x40, // adjacent frequency survives, no 7-bit quantization
0x83, 0x05, 0x18, 0x5e, 0x40,
};
static const size_t lengths[] = {17, 42, 64};
memcpy(wire + 1, ordered, sizeof(ordered));
@ -748,9 +762,9 @@ static void test_native_rumble(void) {
for (unsigned form = 0; form < 2; ++form) {
assert(probe_protocol_decode_rumble((uint8_t)form, wire + form,
lengths[i] - form, &output));
assert(output.count == count && output.magnitude[0] == 255);
if (count >= 2) assert(output.magnitude[1] == 64);
if (count == 3) assert(output.magnitude[2] == 128);
assert(output.sample_count == count);
for (unsigned sample = 0; sample < count; ++sample)
expect_wave(&output.samples[sample], (uint16_t)(385 + sample), 481, 513, 257);
}
}
}
@ -758,9 +772,9 @@ static void test_native_rumble(void) {
wire[1] = 0x4f; // HOLD: nonzero stale samples must not become a stop/update.
assert(probe_protocol_decode_rumble(0, wire, 17, &output));
assert(output.count == 0);
assert(output.sample_count == 0);
assert(probe_protocol_decode_rumble(1, wire + 1, 16, &output));
assert(output.count == 0);
assert(output.sample_count == 0);
// Complete 16-byte block required even for HOLD or a one-sample update.
for (unsigned count = 0; count <= 3; ++count) {