fix: keep Joy-Con links fast and stop idle rumble writes
This commit is contained in:
parent
ed8f3a7c7f
commit
79a72d811d
10 changed files with 220 additions and 84 deletions
|
|
@ -819,26 +819,40 @@ from 197 host requests without queue drops; the user confirmed both sides.
|
|||
Normal GATT-client busy responses now retain queued output instead of
|
||||
disconnecting the controller.
|
||||
|
||||
Two or more Switch 2 BLE links now select 30 ms connection intervals even
|
||||
before Classic connects; a single Switch 2 link retains 7.5 ms. Paired and
|
||||
Individual Joy-Cons use the same physical-link policy. This trades Switch 2
|
||||
delivery latency for Classic paging and streaming airtime without changing
|
||||
HD encoding or bonds. The earlier policy waited for a Classic link and passed
|
||||
one physical power-off/reconnect check, but subsequent use exposed intermittent
|
||||
DualSense connection failures with two Joy-Cons already active. Regression
|
||||
coverage now requires airtime before Classic admission and after its departure.
|
||||
The user confirmed reconnects and actuator output/clean stops at 30 ms.
|
||||
A same-workload 15/30 ms comparison at 300 MHz rejected 15 ms for native
|
||||
haptics: it timed out around four seconds, with 111 PCM sends, 38 skips and
|
||||
one send failure. At 30 ms the full 30-second run retained all three links
|
||||
and native streaming: 1,384 PCM sends, 47 skips, no send failures or host-update/
|
||||
Switch 2 ingress drops, and seven Joy-Con output-stage drops. The isolated
|
||||
DualSense fixture delivered all 288 packets without skips. Release images keep
|
||||
30 ms; long-duration reliability remains unqualified. All 349 tests and four
|
||||
affected firmware builds passed for the preconnection policy.
|
||||
A prior 30-second production mixed-rumble run kept native DualSense streaming,
|
||||
with no send failures or ingress drops, 14 Joy-Con output-stage discards and
|
||||
nine PCM skips. It is not lossless.
|
||||
Joy-Con 2 links now retain 7.5 ms BLE intervals in Paired and Individual modes,
|
||||
including when a Classic controller connects. This intentionally removes their
|
||||
automatic 30 ms slowdown while preserving the idle-neutral sending fix. Other
|
||||
Switch 2 models retain 30 ms when multiple physical Switch 2 links are present,
|
||||
otherwise 7.5 ms; unrelated BLE controllers are untouched. Lifecycle coverage
|
||||
checks modes, Classic arrival/departure, handle reuse, slow-interval correction,
|
||||
and the remaining Switch 2 Pro coexistence/retry policy.
|
||||
|
||||
Idle-neutral output ends after three successful neutral submissions/completions.
|
||||
Late non-neutral completion re-arms stops even after a logical reset; failed or
|
||||
pending writes do not consume the budget. Native subframes, expiry and normal
|
||||
active-output cadence remain unchanged. The direct lab fixtures and unqualified
|
||||
shared Classic/GATT admission experiment are excluded from this checkpoint.
|
||||
|
||||
Measured limits remain important:
|
||||
- At 30 ms, idle suppression reduced outgoing HCI ACL writes from 974 to 469 per
|
||||
ten seconds with the same 469 DualSense PCM sends. Total HCI transactions did
|
||||
not decrease because receive-credit traffic increased.
|
||||
- The optimized 30-second mixed run sent 1,431 PCM packets without skips/send
|
||||
failures, but had two Switch 2 ingress drops and nine output-stage drops.
|
||||
The user later clarified that 30 ms Joy-Con rumble was weak; activity and
|
||||
clean stops were not full quality approval. DualSense was weak/inconsistent.
|
||||
- A single right-Joy-Con direct-packet reference confirmed clear tone and good
|
||||
feel at a nominal 7.5 ms packet cadence: 267 packets per two-second burst,
|
||||
7.504 ms average submission spacing, same 320 Hz/amplitude as the weak 20 ms
|
||||
one-/three-sample trials. This is distinct from the BLE link interval.
|
||||
- Adding DualSense PCM to the fast fixture timed out in about 0.66 seconds,
|
||||
with one or both Joy-Cons vibrating (both links connected in either case).
|
||||
Both runs sent only 11 PCM packets; the two-vibrating run also had Joy-Con
|
||||
submission gaps up to 32 ms. Explicit stop writes completed after abort.
|
||||
|
||||
The requested fast-link default is not a claim of reliable mixed native PCM,
|
||||
lossless transport or qualified endurance. DualSense buffer/cadence work remains
|
||||
separate; no hidden gain or compatibility fallback change is included.
|
||||
|
||||
The native-HD checkpoint passed 289 tests and all five firmware builds; its
|
||||
40 pre-existing profiles and metadata survived. Its inventory then contained
|
||||
|
|
|
|||
96
README.md
96
README.md
|
|
@ -481,43 +481,59 @@ results do **not** establish lossless arbitrary workloads or perceptual
|
|||
equivalence. All 40 profiles, names, active selections and adapter
|
||||
configuration were preserved during this upgrade.
|
||||
|
||||
**Automatic mixed-controller scheduling:** whenever at least two physical
|
||||
Switch 2 BLE links are present, the firmware requests a 30 ms connection
|
||||
interval for those links, even before a Classic Bluetooth controller connects.
|
||||
A single Switch 2 link uses 7.5 ms. A merged Joy-Con pair counts as two links;
|
||||
Individual mode uses the same physical-link policy. Unrelated BLE controllers
|
||||
are not retimed; bonds, profiles, HD encoding and the DualSense timeout stay
|
||||
unchanged. The policy reconciles asynchronous negotiation and topology changes.
|
||||
Waiting for a Classic connection before reserving its airtime left a reconnect
|
||||
timing hole: DualSense could fail to connect with both Joy-Cons already active.
|
||||
Keeping 30 ms after Classic disconnect also leaves airtime for its next attempt.
|
||||
**Joy-Con 2 connection timing:** both halves now request **7.5 ms BLE connection
|
||||
intervals**, in Paired and Individual modes, including while Classic controllers
|
||||
are connected. This deliberately replaces their earlier automatic 30 ms slowdown
|
||||
to favor Joy-Con responsiveness and rumble quality. Other Switch 2 models retain
|
||||
the existing coexistence policy: 30 ms when at least two physical Switch 2 links
|
||||
are present, otherwise 7.5 ms. Unrelated BLE controllers are not retimed. Bonds,
|
||||
profiles, HD encoding and the DualSense timeout are unchanged. Negotiated
|
||||
intervals are reconciled after asynchronous updates and topology changes.
|
||||
|
||||
This trades additional Switch 2 input, gyro and rumble delivery latency for
|
||||
Classic radio time. Multiple reports can travel per connection event, so
|
||||
30 ms does not impose a 33-report/s limit. Two Joy-Con 2 links at 7.5 ms caused
|
||||
native DualSense PCM timeouts in hardware testing. With the automatic policy,
|
||||
both negotiated 30 ms and DualSense started without manual re-arming.
|
||||
A production-firmware 30-second simultaneous rumble run retained all three
|
||||
connections and native streaming: zero ingress drops or send failures,
|
||||
14 Joy-Con output-stage discards and nine DualSense PCM skips. This is not
|
||||
a lossless or long-duration qualification. Lifecycle regressions cover airtime
|
||||
reservation before Classic arrival in Paired and Individual modes, Classic
|
||||
departure/reconnect, a missing half, handle reuse, late negotiation completion,
|
||||
clock wrap and transient request rejection.
|
||||
The earlier policy passed one physical DualSense power-off/reconnect check,
|
||||
but subsequent use exposed intermittent connection failures. The preconnection
|
||||
airtime regression fails under that policy and passes with the current one.
|
||||
The user confirmed reconnects and correct actuator output/stops with the
|
||||
preconnection 30 ms policy. A 15 ms trial also reconnected, but its native
|
||||
DualSense stream timed out even while sending silence. Under the same
|
||||
125 Hz-per-slot held-effect/silence workload at 300 MHz, 15 ms failed around
|
||||
four seconds (111 PCM packets sent, 38 skipped, one send failure); 30 ms
|
||||
completed 30 seconds (1,384 sent, 47 skipped, no send failures). All three
|
||||
controllers remained connected. The 30 ms run had zero host-update or Switch 2
|
||||
ingress drops and seven Joy-Con output-stage drops; its separate DualSense
|
||||
fixture delivered all 288 packets without skips. This supports keeping 30 ms,
|
||||
not a lossless or long-duration claim. The 15 ms trial is not the release default.
|
||||
The preconnection policy passed all 349 tests and four affected firmware builds.
|
||||
The BLE connection interval is not the rumble packet cadence: multiple packets
|
||||
can travel per connection event. The normal active-output algorithm is unchanged
|
||||
by this checkpoint; the direct-packet lab fixtures are not release features.
|
||||
Lifecycle coverage includes Paired/Individual mode changes, Classic arrival and
|
||||
departure, handle reuse, correcting a slow negotiated Joy-Con interval, unrelated
|
||||
BLE isolation, and the retained Switch 2 Pro interval/retry policy.
|
||||
|
||||
**Idle Switch 2 rumble traffic:** the parser sends three successful neutral
|
||||
writes, then suppresses further idle output. Any successful non-neutral packet
|
||||
re-arms that stop budget, including a late completion from an older logical
|
||||
epoch. Failed or pending ATT writes do not count as completed stops. Repeated
|
||||
host stops still discard queued history without restarting settled idle traffic.
|
||||
Active-effect cadence, native subframes and watchdogs are unchanged; the parser
|
||||
timer remains available for control and expiry work.
|
||||
|
||||
**Qualification and known mixed-radio limitation:** this default does not claim
|
||||
reliable native DualSense PCM alongside two fast Joy-Con links.
|
||||
|
||||
- Earlier 15/30 ms trials favored 30 ms for transport continuity: at 15 ms the
|
||||
same 125 Hz-per-slot workload timed out around four seconds; at 30 ms it
|
||||
completed 30 seconds with 1,384 PCM sends, 47 skips and no send failures.
|
||||
All three links stayed connected, but the user later clarified that Joy-Con
|
||||
rumble felt weak at 30 ms and DualSense rumble was also weak/inconsistent.
|
||||
- Idle suppression was measured at 30 ms: outgoing HCI ACL writes fell from 974
|
||||
to 469 per ten-second idle sample, while both samples sent 469 DualSense PCM
|
||||
packets without skips. Total HCI writes did not fall because receive-credit
|
||||
traffic increased. A subsequent 30-second workload sent 1,431 PCM packets
|
||||
with zero skips/failures, two Switch 2 ingress drops and nine output-stage
|
||||
drops. This was a transport improvement, not full perceptual qualification.
|
||||
- In the isolated right-Joy-Con fixture, one- and three-sample packets at a
|
||||
20 ms packet cadence both felt weak. One-sample packets at a nominal 7.5 ms
|
||||
cadence, with the same 320 Hz frequency and amplitude, produced user-confirmed
|
||||
clear tone and good feel. Each two-second burst sent 267 packets with a
|
||||
7.504 ms average submission gap, not a measured on-air or actuator interval.
|
||||
- Adding native DualSense PCM to that fast fixture failed in about 0.66 seconds,
|
||||
both with one Joy-Con vibrating and with both vibrating. Both Joy-Cons remained
|
||||
connected in each test. DualSense sent 11 PCM packets before timing out; with
|
||||
both vibrating, Joy-Con submission gaps reached 32 ms. The fixture aborted and
|
||||
completed explicit stop writes.
|
||||
|
||||
The shared Classic/GATT scheduler trial was not qualified and is excluded from
|
||||
this checkpoint. Temporary packet fixtures are also excluded. Native DualSense
|
||||
buffer/cadence qualification remains separate; no gain increase or fallback mode
|
||||
is silently applied to conceal transport loss.
|
||||
|
||||
### Rumble per controller
|
||||
|
||||
|
|
@ -982,15 +998,15 @@ linked binary, not from the larger debug-bearing ELF or UF2 transport file:
|
|||
|
||||
| Resource | Used or reserved | Device capacity |
|
||||
|---|---:|---:|
|
||||
| Executable flash image | 833,320 bytes | 4 MiB |
|
||||
| Executable flash image | 833,672 bytes | 4 MiB |
|
||||
| Indexed profile arenas | 256 KiB | 4 MiB flash |
|
||||
| Adapter configuration | 8 KiB | 4 MiB flash |
|
||||
| BTstack bonds and Switch 2 application authorizations | 8 KiB | 4 MiB flash |
|
||||
| RP2350 terminal sector | 4 KiB | 4 MiB flash |
|
||||
| Allocated/reserved SRAM, including heap and stacks | 151,804 bytes | 520 KiB |
|
||||
| Allocated/reserved SRAM, including heap and stacks | 151,820 bytes | 520 KiB |
|
||||
|
||||
The executable plus persistent reservations consume 1,115,944 bytes of flash,
|
||||
leaving 3,078,360 bytes. Allocated SRAM sections leave 380,676 bytes of link-time
|
||||
The executable plus persistent reservations consume 1,116,296 bytes of flash,
|
||||
leaving 3,078,008 bytes. Allocated SRAM sections leave 380,660 bytes of link-time
|
||||
headroom; this is not a runtime heap high-water measurement. Core 0 has a
|
||||
4 KiB stack, and Core 1 uses a dedicated 16 KiB stack in main SRAM for nested
|
||||
catalog migration/compaction rather than overflowing its 4 KiB scratch bank.
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@
|
|||
|
||||
#define SW2_TIMEOUT_MS 2000
|
||||
#define SW2_OUTPUT_INTERVAL_MS 13
|
||||
#define SW2_NEUTRAL_WRITE_BUDGET 3
|
||||
#define SW2_HAPTICS_CAPACITY 16
|
||||
#define SW2_REPORT_SIZE 63
|
||||
#define SW2_ACK 0x78
|
||||
|
|
@ -112,6 +113,8 @@ typedef struct {
|
|||
uint32_t pending_serial, pending_epoch, pending_revision;
|
||||
uint32_t playback_until;
|
||||
uint8_t pending_guard_ms;
|
||||
uint8_t neutral_writes;
|
||||
bool pending_neutral;
|
||||
bool pending_host, pending_barrier, barrier_pending, output_urgent;
|
||||
bool feedback_active, playback_guard;
|
||||
uint32_t sensor_start, sensor_host_start, sensor_last;
|
||||
|
|
@ -1147,6 +1150,12 @@ void uni_hid_parser_switch2_reset_haptics(uni_hid_device_t* d) {
|
|||
static void sw2_rumble_complete(sw2_instance_t* ins) {
|
||||
uint32_t now = btstack_run_loop_get_time_ms();
|
||||
++ins->rumble_id; // Only a successful write consumes the physical sequence.
|
||||
// Track transport success even for an old logical epoch: a late active
|
||||
// packet invalidates prior silence and must be followed by fresh stops.
|
||||
if (!ins->pending_neutral)
|
||||
ins->neutral_writes = 0;
|
||||
else if (ins->neutral_writes < SW2_NEUTRAL_WRITE_BUDGET)
|
||||
++ins->neutral_writes;
|
||||
ins->playback_until = now + ins->pending_guard_ms;
|
||||
ins->playback_guard = true;
|
||||
sw2_update_haptics(ins, now);
|
||||
|
|
@ -1204,6 +1213,16 @@ static bool sw2_send_rumble(sw2_instance_t* ins, uint32_t now) {
|
|||
break;
|
||||
}
|
||||
}
|
||||
ins->pending_neutral = sw2_physical_stop(ins, &frame);
|
||||
if (ins->pending_neutral && !ins->pending_host &&
|
||||
ins->neutral_writes == SW2_NEUTRAL_WRITE_BUDGET) {
|
||||
// Three successful neutral writes settle idle output. Repeated host
|
||||
// stops still discard queued history, but need no further radio work.
|
||||
// Never suppress a queued native sequence, even if its endpoint is zero.
|
||||
ins->barrier_pending = false;
|
||||
ins->output_urgent = false;
|
||||
return false;
|
||||
}
|
||||
ins->pending_epoch = ins->haptics_epoch;
|
||||
ins->pending_revision = ins->output_revision;
|
||||
ins->pending_barrier = ins->barrier_pending;
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -48,7 +48,7 @@ constexpr uint32_t kDefaultPairingWindowDurationMs =
|
|||
constexpr uint32_t kPairingResetFeedbackDurationMs = 2000;
|
||||
// Bluetooth Classic units are 0.625 ms: 0x1900 = 4 seconds.
|
||||
constexpr uint16_t kClassicLinkSupervisionTimeout = 0x1900;
|
||||
// LE units are 1.25 ms. Reserve airtime for Classic paging and native PCM.
|
||||
// LE units are 1.25 ms. Joy-Cons retain the fast interval even with Classic.
|
||||
constexpr uint16_t kSwitch2FastInterval = 6;
|
||||
constexpr uint16_t kSwitch2MixedInterval = 24;
|
||||
constexpr uint32_t kSwitch2IntervalSettleMs = 1000;
|
||||
|
|
@ -591,9 +591,9 @@ void stop_background_scan() {
|
|||
g_background_scan_active = false;
|
||||
}
|
||||
}
|
||||
// Core 1 only. Count physical links, not logical players: a merged Joy-Con
|
||||
// pair still consumes two LE connections. Reserve Classic airtime before an
|
||||
// incoming connection exists; waiting for its ACL handle is too late for paging.
|
||||
// Core 1 only. Count physical links, not logical players. Joy-Cons always keep
|
||||
// their fast default; retain preconnection coexistence timing for other Switch 2
|
||||
// models when multiple physical Switch 2 links share the radio.
|
||||
void apply_radio_connection_policy() {
|
||||
unsigned switch2_links = 0;
|
||||
uni_hid_device_t* ready[kSlotCount]{};
|
||||
|
|
@ -612,9 +612,6 @@ void apply_radio_connection_policy() {
|
|||
}
|
||||
}
|
||||
}
|
||||
const uint16_t desired = switch2_links >= 2
|
||||
? kSwitch2MixedInterval
|
||||
: kSwitch2FastInterval;
|
||||
const uint32_t now_ms = btstack_run_loop_get_time_ms();
|
||||
for (uint8_t index = 0; index < kSlotCount; ++index) {
|
||||
auto& request = g_switch2_interval_requests[index];
|
||||
|
|
@ -622,6 +619,9 @@ void apply_radio_connection_policy() {
|
|||
request = {};
|
||||
continue;
|
||||
}
|
||||
const uint16_t desired = switch2_links >= 2 && joycon_side(ready[index]) == 0
|
||||
? kSwitch2MixedInterval
|
||||
: kSwitch2FastInterval;
|
||||
const auto handle = ready[index]->conn.handle;
|
||||
if (request.handle != handle) request = {};
|
||||
const uint16_t actual = gap_le_connection_interval(handle);
|
||||
|
|
|
|||
|
|
@ -1385,10 +1385,9 @@ void test_switch2_mode_roundtrip(bool right_first) {
|
|||
slot_snapshot(2).connection_generation == classic_before.connection_generation &&
|
||||
slot_snapshot(2).state.button_north &&
|
||||
classic.rumble_calls == classic_calls && classic.last_low == 71 &&
|
||||
negotiated_intervals[left.conn.handle] == 24 &&
|
||||
negotiated_intervals[right.conn.handle] == 24 &&
|
||||
interval_requests[left.conn.handle] == 1 && interval_requests[right.conn.handle] == 1,
|
||||
"mode roundtrip must preserve unrelated Classic state, haptics and physical mixed-link intervals");
|
||||
negotiated_intervals[left.conn.handle] == 6 &&
|
||||
negotiated_intervals[right.conn.handle] == 6,
|
||||
"mode roundtrip must preserve unrelated Classic state, haptics and fast Joy-Con links");
|
||||
ControllerProfile inactive{};
|
||||
require(runtime_profile_storage.get(pair_identity, 2, &inactive) == ProfileStorageResult::kOk &&
|
||||
inactive.weak_rumble_scale == 37,
|
||||
|
|
@ -3046,25 +3045,25 @@ void test_switch2_radio_policy(bool individual) {
|
|||
"a single Switch2 link must retain fast scheduling");
|
||||
ready_switch2(right);
|
||||
require(incoming_connections &&
|
||||
negotiated_intervals[left.conn.handle] == 24 &&
|
||||
negotiated_intervals[right.conn.handle] == 24,
|
||||
"two Switch2 links must reserve Classic reconnect airtime before a Classic device exists");
|
||||
negotiated_intervals[left.conn.handle] == 6 &&
|
||||
negotiated_intervals[right.conn.handle] == 6,
|
||||
"paired and individual Joy-Cons must retain fast links before Classic arrival");
|
||||
platform_on_device_connected(&other_ble);
|
||||
require(platform_on_device_ready(&other_ble) == UNI_ERROR_SUCCESS,
|
||||
"unrelated BLE controller must join");
|
||||
platform_on_device_connected(&classic);
|
||||
require(negotiated_intervals[left.conn.handle] == 24 &&
|
||||
negotiated_intervals[right.conn.handle] == 24 &&
|
||||
require(negotiated_intervals[left.conn.handle] == 6 &&
|
||||
negotiated_intervals[right.conn.handle] == 6 &&
|
||||
negotiated_intervals[other_ble.conn.handle] == 6,
|
||||
"Classic setup must retain reconnect airtime without retiming unrelated BLE");
|
||||
"Classic setup must not slow Joy-Cons or retime unrelated BLE");
|
||||
require(platform_on_device_ready(&classic) == UNI_ERROR_SUCCESS,
|
||||
"Classic controller must complete setup alongside the pair");
|
||||
const auto pair = slot_snapshot(0);
|
||||
platform_on_device_disconnected(&classic);
|
||||
require(negotiated_intervals[left.conn.handle] == 24 &&
|
||||
negotiated_intervals[right.conn.handle] == 24 &&
|
||||
require(negotiated_intervals[left.conn.handle] == 6 &&
|
||||
negotiated_intervals[right.conn.handle] == 6 &&
|
||||
slot_snapshot(0).connection_generation == pair.connection_generation,
|
||||
"Classic departure must preserve reconnect airtime without rebinding the pair");
|
||||
"Classic departure must preserve fast Joy-Con links without rebinding the pair");
|
||||
platform_on_device_connected(&classic);
|
||||
require(platform_on_device_ready(&classic) == UNI_ERROR_SUCCESS,
|
||||
"Classic reconnect must complete");
|
||||
|
|
@ -3073,16 +3072,29 @@ void test_switch2_radio_policy(bool individual) {
|
|||
"a single surviving Switch2 link must return to fast scheduling even with Classic");
|
||||
left = switch2_device(0, UNI_SW2_JOYCON_L_PID);
|
||||
ready_switch2(left);
|
||||
require(negotiated_intervals[left.conn.handle] == 24 &&
|
||||
require(negotiated_intervals[left.conn.handle] == 6 &&
|
||||
negotiated_intervals[right.conn.handle] == 6,
|
||||
"physical index and handle reuse must retain fast Joy-Con links");
|
||||
negotiated_intervals[left.conn.handle] = 24;
|
||||
negotiated_intervals[right.conn.handle] = 24;
|
||||
now_ms += 1000;
|
||||
process_configuration_timer(&g_configuration_timer);
|
||||
require(negotiated_intervals[left.conn.handle] == 6 &&
|
||||
negotiated_intervals[right.conn.handle] == 6,
|
||||
"peer-negotiated slow Joy-Con links must reconcile to the fast default");
|
||||
platform_on_device_disconnected(&right);
|
||||
right = switch2_device(1, UNI_SW2_PRO_PID);
|
||||
ready_switch2(right);
|
||||
require(negotiated_intervals[left.conn.handle] == 6 &&
|
||||
negotiated_intervals[right.conn.handle] == 24,
|
||||
"physical index and handle reuse must negotiate mixed intervals on the new connection");
|
||||
"Joy-Con preference must not remove the existing Switch2 Pro coexistence policy");
|
||||
}
|
||||
|
||||
void test_switch2_radio_settling() {
|
||||
start_backend();
|
||||
auto classic = device(2, true, UNI_BT_CONN_PROTOCOL_BR_EDR);
|
||||
auto left = switch2_device(0, UNI_SW2_JOYCON_L_PID);
|
||||
auto right = switch2_device(1, UNI_SW2_JOYCON_R_PID);
|
||||
auto left = switch2_device(0, UNI_SW2_PRO_PID);
|
||||
auto right = switch2_device(1, UNI_SW2_PRO_PID);
|
||||
platform_on_device_connected(&classic);
|
||||
require(platform_on_device_ready(&classic) == UNI_ERROR_SUCCESS,
|
||||
"Classic-first connection must become ready");
|
||||
|
|
|
|||
|
|
@ -1149,6 +1149,79 @@ static void test_immediate_connection_failure_preserves_reconnect_discovery(void
|
|||
assert(!scan_running && !peers[0].used); // Explicit stop must not be overridden.
|
||||
}
|
||||
|
||||
static void test_idle_neutral_output_quiesces_and_resumes(void) {
|
||||
struct fixture_peer* peer = ready_peer(UNI_SW2_JOYCON_L_PID, false);
|
||||
advance(100);
|
||||
assert(peer->rumbles == 3 && !rumble_active(peer));
|
||||
unsigned idle_writes = peer->rumbles;
|
||||
advance(1300);
|
||||
assert(peer->rumbles == idle_writes);
|
||||
// Repeated host stops must not restart an already completed neutral burst.
|
||||
for (unsigned i = 0; i < 20; ++i) {
|
||||
assert(uni_hid_parser_switch2_queue_rumble(&peer->device, 0, 0, 0, now_ms));
|
||||
advance(8);
|
||||
}
|
||||
assert(peer->rumbles == idle_writes);
|
||||
// Quiet output must not delay a future effect or a finite watchdog stop.
|
||||
uni_hid_parser_switch2_play_dual_rumble(&peer->device, 20, 50, 40, 80);
|
||||
advance(19);
|
||||
assert(peer->rumbles == idle_writes);
|
||||
advance(1);
|
||||
assert(rumble_active(peer));
|
||||
advance(20);
|
||||
assert(peer->rumbles > idle_writes + 1 && rumble_active(peer));
|
||||
advance(30);
|
||||
assert(!rumble_active(peer));
|
||||
advance(100);
|
||||
idle_writes = peer->rumbles;
|
||||
advance(1300);
|
||||
assert(peer->rumbles == idle_writes);
|
||||
uni_switch2_haptics_frame_t frame = native_frame(3, 0, 8);
|
||||
assert(uni_hid_parser_switch2_queue_haptics(&peer->device, &frame, now_ms));
|
||||
advance(1);
|
||||
assert_block(peer, 0, &frame.sides[0]);
|
||||
advance(100);
|
||||
assert_silent(peer, 0);
|
||||
idle_writes = peer->rumbles;
|
||||
advance(1300);
|
||||
assert(peer->rumbles == idle_writes);
|
||||
}
|
||||
|
||||
static void test_neutral_budget_waits_for_success_and_stale_active_completion(void) {
|
||||
struct fixture_peer* peer = ready_peer(UNI_SW2_JOYCON_R_PID, true);
|
||||
next_write_error = BTSTACK_ACL_BUFFERS_FULL;
|
||||
advance(1);
|
||||
assert(peer->rumbles == 0);
|
||||
advance(13);
|
||||
assert(peer->rumbles == 1 && peer->query == QUERY_WRITE);
|
||||
advance(100);
|
||||
assert(peer->rumbles == 1); // Pending ATT writes cannot count as repeated stops.
|
||||
query_done(peer, 0);
|
||||
advance(13);
|
||||
assert(peer->rumbles == 2);
|
||||
query_done(peer, 0);
|
||||
advance(13);
|
||||
assert(peer->rumbles == 3);
|
||||
query_done(peer, 0);
|
||||
advance(100);
|
||||
assert(peer->rumbles == 3 && peer->query == QUERY_NONE);
|
||||
// Reset while an active write borrows its buffer. Its stale completion must
|
||||
// invalidate the earlier idle state and require a fresh stop burst.
|
||||
uni_hid_parser_switch2_play_dual_rumble(&peer->device, 0, UINT16_MAX, 40, 80);
|
||||
advance(1);
|
||||
assert(rumble_active(peer) && peer->query == QUERY_WRITE);
|
||||
uni_hid_parser_switch2_reset_haptics(&peer->device);
|
||||
query_done(peer, 0);
|
||||
for (unsigned i = 0; i < 3; ++i) {
|
||||
advance(i ? 13 : 1);
|
||||
assert(peer->rumbles == 5 + i && peer->query == QUERY_WRITE);
|
||||
assert_silent(peer, 0);
|
||||
query_done(peer, 0);
|
||||
}
|
||||
advance(100);
|
||||
assert(peer->rumbles == 7 && peer->query == QUERY_NONE);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
test_connected_callback_rejection();
|
||||
test_advertisement_bounds_and_admission();
|
||||
|
|
@ -1171,6 +1244,8 @@ int main(void) {
|
|||
test_hold_coalescing_requires_identical_samples_and_side_masks();
|
||||
test_expired_history_does_not_starve_fresh_sequences();
|
||||
test_gatt_busy_retries_without_disconnect_or_sequence_loss();
|
||||
test_idle_neutral_output_quiesces_and_resumes();
|
||||
test_neutral_budget_waits_for_success_and_stale_active_completion();
|
||||
reset();
|
||||
puts("Switch2 protocol boundaries, setup failure, pairing, calibration, physical input, motion and rumble passed");
|
||||
return 0;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue