Skip to content

fix(haptics): no click after Switch HD rumble, retry lost PCM frames - #15

Merged
ripps818 merged 1 commit into
mainfrom
fix/switch-hd-rumble-click
Oct 11, 2026
Merged

ripps818 merged 1 commit into
mainfrom
fix/switch-hd-rumble-click

Conversation

@ripps818

@ripps818 ripps818 commented Oct 11, 2026 •

Copy link
Copy Markdown
Owner

Summary

Fixes #14 (Switch HD rumble issues), both symptoms.

Click after the rumble ends. When an HD rumble went silent, the grip stream sent 500 ms of silence and then hdStop sent two -128 dB 0x83 cuts to both pads and a 0x86 op 1. Sent to idle actuators, these play as a click of their own, at a strength no rumble setting changes (the report says it is the same at 10% and 500%). The DualSense wave stream ends its hang by just stopping frames and has no such click.

  • hdStop cuts the pads only while a pad tone is still on.
  • hdStop trims the grip stream with op 1 only if it sounded within the last 150 ms (HD_PCM_TAIL_MS: ~40 ms pre-buffer plus 60-85 ms play-out, PROTOCOL.md §9.1).
  • An abrupt stop mid-effect (rumble or relay off, USB suspend, blocked or lost link) still sends both.

Jitter on bad channels. A PCM stream keeps a fixed fill once it starts, so a lost frame takes its samples out of that cushion for good, and two or three losses run it dry. A 0x86-0x89 frame that is still unanswered after its same-PID resend now goes back to the front of the ring for up to two later cycles (PCM_RELAY_RETRIES). The same cycle's backlog flush is skipped after a requeue. This path is shared, so DualSense wave and Steam-mode 0x87-0x89 relay get the retry too. If only the ACK was lost, the retry lands a duplicate frame: ~8 ms of extra fill until the stream next stops.

Changes

  • OpenPuck/haptics.cpp: hdStop, hdPcmStop(slot, trim), RelayMsg.tries, relayRequeue, retry in rfConnFlushRelay.
  • OpenPuck/haptics.h, OpenPuck/rf_link.cpp: rfConnFlushRelay(..., bool *requeued).
  • tests/hd_rumble/run.py: hang expiry sends nothing; new abrupt-stop and rumble-off-during-hang cases.
  • docs/PROTOCOL.md §9.1: the retry, and why stop messages go only to actuators that may still be playing.

Testing

  • python3 tests/hd_rumble/run.py (ASan/UBSan, -Werror) and every other tests/*/run.py pass.
  • make check (clang-format 18.1.8) and tools/check-trailing-comments.py pass.
  • make build (arduino-cli, adafruit:nrf52 1.7.0) compiles and links: 283,656 bytes (34% of flash), 54,432 bytes of RAM. Not run on hardware. To check on a puck in Switch Pro mode: press Test rumble at 10% and 500% and listen for a click about 0.5 s after the buzz; toggle rumble off mid-buzz to confirm an abrupt stop still ends cleanly. For jitter, use a noisy channel and a long rumble.

The upstream PR (safijari#238) is not touched; this is fork main only.

🤖 Generated with Claude Code

When an HD rumble went silent, the grip stream ran 500 ms of silence and
then hdStop sent two -128 dB pad cuts and a 0x86 op 1. Sent to idle
actuators, these play as a click of their own, at a strength no rumble
setting changes. hdStop now cuts the pads only while a tone is on, and
trims the grip stream only if it played within the last 150 ms. A
silent stream just stops sending frames, like the DualSense wave
stream.

A PCM stream keeps a fixed fill once it starts, so a frame lost on a
bad channel takes its samples out of that cushion for good, and two or
three losses run the stream dry. A 0x86-0x89 frame still unanswered
after its same-PID resend now goes back to the front of the ring for up
to two later cycles. The same cycle's backlog flush is skipped after a
requeue.

Fixes #14

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ripps818
ripps818 merged commit 8217036 into main Oct 11, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Switch HD rumble issues

1 participant