202 Commits
Author SHA1 Message Date
MarcelineVPQandClaude Opus 4.8 bc70bbd87c docs: credit SundayMoments/DS5_Bridge in Acknowledgements
The button-remap apply logic + digital-control set (src/remap.{h,cpp}) were
ported from it, and its audio architecture was the reference for the
speaker-cadence investigation. Crediting it was an oversight before.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 20:49:54 -06:00
MarcelineVPQandClaude Opus 4.8 be3736c230 Merge native-trigger firmware as v0.6.12 — supersedes the withdrawn v0.6.11
v0.6.12 = the verified-working native-adaptive-trigger firmware (off v0.6.10)
plus the two separable good v0.6.11 features (CtrlWake, brightness persistence).
The v0.6.11 regressions are intentionally NOT carried over: the state_mgr.cpp
trigger-FFB allow-bit mirror (#11 constant haptics) and the audio.cpp resampler
retiming (#12). This merge takes the branch's tree wholesale; v0.6.11 remains in
history (and its tag) but its file changes are superseded.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
v0.6.12-oled-edition
2026-06-07 18:28:31 -06:00
MarcelineVPQandClaude Opus 4.7 aef03e9633 docs(readme): document gyro-tilt rework + L3/R3 indicator (EN + CN)
Bring both READMEs up to v0.6.10's user-facing capability:
- Status screen: note the L3/R3 stick-click inverse-flash indicator and
  the ~Nm charge-time token next to the battery.
- Gyro Tilt screen: per-unit factory IMU calibration, dot centres when the
  controller lies flat, and tilt moves the dot in the matching direction.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-07 18:28:31 -06:00
MarcelineVPQandClaude Opus 4.8 cb7fffd795 docs(linux): native adaptive triggers confirmed on Steam AND Heroic, flag-free
Verified 2026-06-07: Cyberpunk on GOG/Heroic fires adaptive triggers in
real gameplay, 1:1 with Steam. Corrects the recipe across README,
CHANGELOG, and the new DUALSENSE_GAME_LIST.md:
  - no launch option needed on Wine 11 (PROTON_ENABLE_HIDRAW now legacy)
  - works on BOTH Steam and Heroic; Heroic needs Steam fully quit + no
    global PROTON_PREFER_SDL
  - XInput-only games (Ghostrunner, Control) give rumble but no triggers
    by engine design, not a dongle limit
  - OpenRGB confirmed a non-issue

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 18:25:25 -06:00
MarcelineVPQandClaude Opus 4.8 9cabdca3f5 feat(oled): fold CtrlWake + brightness persistence onto native-trigger firmware (v0.6.12)
Cherry-picks ONLY the two separable good features from the withdrawn
v0.6.11 (commit 71cead4) — the `controller_wakes_display` (CtrlWake /
OLED-sleep-while-playing) toggle and `screen_brightness` persistence —
onto the verified-working native-trigger build.

The v0.6.11 regressions are intentionally NOT carried over:
  - 71cead4's state_mgr.cpp trigger-FFB allow-bit mirror (#11 suspect)
  - all of 84393c0's audio resampler retiming + config audio-defaults
    + chunked-SPI flush (#12 suspect)

Touches only src/config.{h,cpp} and src/oled.cpp, so the trigger and
audio code paths compile byte-identical to the tested firmware and
cannot regress. Two new Config_body fields append at the struct tail
(erased flash -> clamped to defaults by config_valid, clean upgrade).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 12:23:21 -06:00
MarcelineVPQandClaude Opus 4.8 00da3d6a18 docs(readme): flag the WebHID Config/Remap tabs as non-functional on the native-trigger firmware
Inline warnings on the Config and Remap tab descriptions (in addition to the
existing caveat block) so users on the native-trigger firmware don't try the
browser config tool and hit a silent failure. The reports those tabs use
(0xF6/0xF7) are undeclared on this firmware so WebHID can't reach them; OLED
on-device config and a Linux hidraw path still work.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 11:53:22 -06:00
MarcelineVPQandClaude Opus 4.8 6482c79644 fix(audio): fold the crackle fix (RX buffer 3x->16x) into the native-trigger firmware
The speaker-crackle root cause -- USB-audio OUT endpoint software buffer overflow
at 3x the max packet size -- was fixed on the audio branch (de37e00) but never on
this branch. Bumping CFG_TUD_AUDIO_FUNC_1_EP_OUT_SW_BUF_SZ from 3x to 16x removes
the crackle; verified by A/B tone test (440 Hz) through the DS5 speaker. A faint
residual cadence "warble" remains (a separate root cause, not the buffer) and is
left for a later pass. Net: one firmware with BOTH native adaptive triggers and a
crackle-free speaker.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 11:50:37 -06:00
MarcelineVPQandClaude Opus 4.8 00822d6556 docs: document native adaptive triggers in games (Linux/Proton) + the descriptor/feature-data fixes
CHANGELOG: headline + Added/Changed/Known-tradeoff for the native-trigger work.
README: a Features bullet, a "Linux: native adaptive triggers (Proton)" setup
section with every launch option in a reference table, and a caveat that the
WebHID Config/Remap tabs are disabled on this firmware. CLAUDE.md: a load-bearing
gotcha -- the HID descriptor must stay byte-identical to a real DS5 (289 bytes)
and feature reports 0x09/0x20/0x05 must serve the real cached controller data, or
native game triggers silently break.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 10:44:31 -06:00
MarcelineVPQandClaude Opus 4.8 8d811f09cc fix(hid): serve the controller's REAL 0x09/0x20/0x05 feature data to the host
The native-recognition shortcut answered 0x09 (pairing), 0x20 (firmware) and 0x05
(calibration) with zeros + a valid crc. hid_playstation accepted that (it checks
size+crc only), but games validate the actual CONTENT: Cyberpunk GET-read 0x20/0x09
~156x in a retry storm rejecting the zeros, never completed the DualSense handshake,
and showed no controller / no adaptive triggers.

init_feature() already caches the real reports from the controller over BT, so serve
those (via get_feature_data) for 0x09/0x20/0x05, keeping the crc-valid synthetic stub
only as a fallback for the USB-enumeration probe before the BT link (so the kernel
still binds then). The dongle now returns the genuine firmware string / pairing /
calibration, and native adaptive triggers work in games through the dongle, 1:1 with
a wired DualSense.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 10:36:10 -06:00
MarcelineVPQandClaude Opus 4.8 36687ce76d fix(usb): strip web-config reports 0xF6-0xF9 so the HID descriptor byte-matches a real DS5
The OLED Edition declared four extra feature reports (0xF6-0xF9, the WebHID config
IDs) in the DualSense HID descriptor, making it 321 bytes vs a genuine DS5's 289.
Games' native DualSense parser (Cyberpunk, etc.) rejects the mismatch and falls
back to generic/Xbox, so adaptive triggers never fire in a game -- the long-standing
"triggers only work in the OLED self-test" bug.

Remove the four declarations and the runtime wDescriptorLength patch that overrode
them (0x41->0x21) so the descriptor is byte-identical to a real DS5 (289 bytes). The
firmware still handles 0xF6-0xF9; they're just undeclared now and work over hidraw on
Linux exactly like 0xFD. Trade-off: the browser WebHID config tool can't reach them
(OLED + hidraw config still work).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 10:36:10 -06:00
MarcelineVPQandClaude Opus 4.8 5e5aa71cfe fix(usb): use the controller MAC as the USB serial, matching the 0x09 report
The serial was the Pico flash unique id (board_usb_get_serial), while the 0x09
pairing-info feature report returns the controller's BT MAC. The host saw two
identities for one controller (flash-id over USB, MAC over 0x09), and Wine/Steam
device-matching choked — "recognized but no controller" in-game.

Set STRID_SERIAL to the controller MAC (live bt_get_addr, falling back to the
current slot's stored addr, then the flash id if never paired) so the dongle
presents one consistent identity like a real DualSense. Steam now recognises
the dongle as a native DualSense.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 00:39:46 -06:00
MarcelineVPQandClaude Opus 4.8 d090df6820 feat(hid): answer DS5 0x09/0x20/0x05 feature reports so Linux binds hid_playstation
Linux's hid_playstation driver reads three feature reports at probe — 0x09
(pairing info / controller MAC), 0x20 (firmware info) and 0x05 (calibration),
the latter two CRC-checked. Without valid answers the kernel never creates a
gamepad, so games outside Steam Input (Heroic/Proton/native) see no controller.

Synthesize them in tud_hid_get_report_cb: 0x09 returns the BT MAC at [0..5];
0x20/0x05 fill zeros plus a valid trailing crc32 over [0xA3 seed, report_id,
data]. Confirmed: the dongle now binds hid_playstation — a real gamepad on Linux.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 00:39:46 -06:00
MarcelineVPQandClaude Opus 4.8 878a742bfc docs(changelog): cut v0.6.11 — triggers fix + CtrlWake + audio retiming
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
v0.6.11-oled-edition
2026-06-02 19:19:16 -06:00
MarcelineVPQandClaude Opus 4.8 84393c00e8 wip: park audio-crackle investigation + haptic/OLED defaults + debug tooling
Preserves in-progress work that had been sitting uncommitted in the working
tree (predates this session) so it is protected in git history. This is NOT a
verified/shipped fix — it is parked. Audio path is unverified on hardware and
still carries debug instrumentation.

- src/audio.cpp: drop the 512->480 resampler and retime the 0x36 frame to a
  true 10ms/100Hz grid (SAMPLE_SIZE 64->60, 480-sample buffer) to chase the
  ~45kHz-vs-48kHz speaker underrun theory behind the crackle; dynamic speaker
  sub-report offset; debug printf + Opus-frame-dump instrumentation.
- src/config.cpp: default audio_buffer_length 64->16; auto_haptics_enable
  default 1 (Fallback) -> 0 (Off).
- src/oled.cpp: chunked non-blocking SPI flush (issue #7 OLED-stall angle) and
  a battery-% midpoint display tweak.
- scripts/: audio debug helpers (opus-dump decode, pi audio test, sine ch1/2).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 19:11:39 -06:00
MarcelineVPQandClaude Opus 4.8 71cead401d fix: adaptive triggers + opt-in OLED sleep + brightness persistence
Closes the three non-audio user issues (audio path untouched):

- #6: adaptive trigger FFB was silently dropped. state_update() copied the
  RightTriggerFFB/LeftTriggerFFB params into the outgoing state but never set
  the Allow{Right,Left}TriggerFFB apply-bits in byte 0, so the DS5 discarded
  them on BOTH the standalone 0x31 path and the 0x36 audio-frame fold, while
  direct USB worked. Mirror the host's two allow-flags in, like the rumble
  flags already were (matches what the on-device Trigger Test screen does).

- #8/#9: new CtrlWake setting (default on = unchanged behavior). Set off and
  controller input no longer keeps the OLED awake, so the dim/off timeouts run
  during gameplay and the panel can sleep while the controller is in use; only
  KEY0/KEY1 wake it.

- #9: OLED brightness (screen_brightness) now persists across a power cycle
  instead of resetting to full on every boot.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 18:58:33 -06:00
MarcelineVPQandClaude Opus 4.7 8fc369b1af docs(readme): document gyro-tilt rework + L3/R3 indicator (EN + CN)
Bring both READMEs up to v0.6.10's user-facing capability:
- Status screen: note the L3/R3 stick-click inverse-flash indicator and
  the ~Nm charge-time token next to the battery.
- Gyro Tilt screen: per-unit factory IMU calibration, dot centres when the
  controller lies flat, and tilt moves the dot in the matching direction.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 08:45:49 -06:00
MarcelineVPQandClaude Opus 4.7 c879b24364 docs(changelog): cut v0.6.10 — gyro tilt rework + L3/R3 indicator + charge-ETA
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v0.6.10-oled-edition
2026-05-25 07:20:30 -06:00
MarcelineVPQandClaude Opus 4.7 c7e6c5b914 feat(oled): IMU gyro calibration + centred, correct-direction tilt dot
Rework the Gyro Tilt screen (and the tilt->RGB lightbar) using the DS5's
own factory IMU calibration, and fix two long-standing tilt-dot quirks.
All three are display-only — the host input report (every gyro + accel
axis) is still forwarded byte-for-byte, so in-game motion is unaffected.

- Calibration: parse feature report 0x05 (already cached by bt.cpp) into
  per-axis bias + sensitivity and apply (raw-bias)*sens to the accel the
  tilt visuals use, keeping the +-8192 == 1g scale. Re-read per connection
  so it tracks across the 4 pairing slots; sanity-gated with raw fallback.
  New side-effect-free bt_peek_feature() accessor (never issues an L2CAP
  request, safe to poll). Parse/apply mirror SDL's SDL_hidapi_ps5.c (zlib).
- Centred when flat: drive the dot from X (roll) + Z (pitch) instead of
  X + Y. Gravity rests on Y when flat, so the old Y mapping pegged the dot
  to the bottom edge at rest; it now sits centred.
- Direction: negate both axes so the dot follows the tilt (tilt left -> dot
  left, tilt forward -> dot up) instead of mirroring it.

Also fold in this session's charge-ETA robustness fix: cap each timed 10%
step at 30 min and take the median over the last 5 steps, so one slow step
can't balloon the estimate (was reading ~222m at 70% off a single ~47-min
step). Shares src/oled.cpp with the gyro work.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 07:16:51 -06:00
MarcelineVPQandClaude Opus 4.7 7e21a1fbea docs(changelog): note L3/R3 Status-screen click indicator under [Unreleased]
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 12:39:40 -06:00
MarcelineVPQandClaude Opus 4.7 3556cdedb3 feat(oled): invert the stick box on L3/R3 click (Status screen)
The two analog-stick boxes on the Status screen had no L3/R3 (stick-click)
indicator. Add a rect_invert() helper and XOR-invert the whole box on click
so it flashes inverse — the position dot inverts with it (black dot on a
white box). Bits 0x40 (L3) / 0x80 (R3) on interrupt_in_data[8].

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 12:36:40 -06:00
MarcelineVPQandClaude Opus 4.7 a7f20cfd5a docs(cn): rewrite README.CN.md to match the OLED Edition README
The Chinese README was still the upstream stub (awalol.eu.org links, Pico W
notes, USB-wake branch) — never adapted for this fork. Replaced with a full
translation of the current README: web config tool (incl. Remap tab), button
remapping, BT mic + PLC, all 11 OLED screens, overclock, build, diagnostics,
USB 3.0 interference, and Zacksly CC BY 3.0 credit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 12:14:17 -06:00
MarcelineVPQandClaude Opus 4.7 a3881a4bda docs(changelog): cut v0.6.9 — button remapping + visual web remap editor + mic PLC
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v0.6.9-oled-edition
2026-05-24 12:02:42 -06:00
MarcelineVPQandClaude Opus 4.7 3d90ad4aa2 docs: highlight button remapping (README + CHANGELOG)
README: add a "Button remapping" feature bullet and a "Remap tab" entry
under the Web Config Tool section. CHANGELOG [Unreleased]: add a Companion
web tool note documenting the visual Remap editor + Zacksly CC BY 3.0
asset attribution.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 11:58:32 -06:00
MarcelineVPQandClaude Opus 4.7 1b1944c006 feat(remap): on-dongle button remapping over 0xF6/0xF7
16-entry source→target table in its own flash sector (-3, magic DS5\x03),
identity default, 0xFF = disabled. Applied to the OUTGOING host report copy
only — raw interrupt_in_data (OLED screens + PS+Mute reboot combo) stays
physical. Edited via the existing 0xF6/0xF7 vendor reports with a hardened
RM+version frame (no HID-descriptor change, so Windows enumeration is
unaffected) plus a revision counter the host polls to confirm a write.
Apply logic + button set ported from SundayMoments/DS5_Bridge (credit).

scripts/remap_test.py exercises the path over /dev/hidraw without the web
tool. Verified on hardware: swap applied, read back, survived reboot.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 10:42:29 -06:00
MarcelineVPQandClaude Opus 4.7 da63e2bb72 feat(audio): mic packet-loss concealment (jitter buffer + Opus PLC)
The BT mic decode path gained a decoded-frame jitter buffer (8 frames) drained
at a steady 10 ms playout cadence. Bursty BT delivery is smoothed; a dropped
mic frame during an active session is concealed with an Opus PLC frame
(opus_decode(decoder, NULL, 0, ...)) instead of leaving a hole the host hears
as a click/dropout. Playout pre-buffers 3 frames and stops after 300 ms of no
real frames so it never emits comfort noise when the mic is idle. New "Mic PLC:"
Diagnostics counter climbs only when concealment fires (a live link-quality
gauge). Verified on hardware: forced BT loss kept captured audio gap-free
(longest zero-run ~0 ms) while the counter climbed; clean link leaves it idle.
Adds ~30 ms mic latency (the pre-buffer). Design ported from
SundayMoments/DS5_Bridge (credit). Unreleased — batching with the next feature.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 09:27:11 -06:00
MarcelineVPQandClaude Opus 4.7 9920516f52 ci(release): stop publishing the debug UF2 as a release download
The debug build (ENABLE_SERIAL=ON) compiles out tud_connect/disconnect and
enumerates as a serial console, not a working HID/audio bridge — an end user
who downloads it gets a non-functional dongle. Keep it buildable on demand
(-DENABLE_SERIAL=ON) and in build.yml PR CI for compile coverage, but don't
ship it on the public release page. Releases now upload the standard UF2 only.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 01:44:32 -06:00
MarcelineVPQandClaude Opus 4.7 1c4e34f8da docs(changelog): cut v0.6.8 — BT microphone + USB 3.0 connection watchdog
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v0.6.8-oled-edition
2026-05-24 01:31:06 -06:00
MarcelineVPQandClaude Opus 4.7 20b41d80a1 feat: DualSense BT microphone + USB 3.0 connection watchdog
BT microphone over Bluetooth: the DS5 mic now works over the dongle's BT
pairing — decoded from the controller's Opus stream to the USB capture
endpoint. Hinges on pkt[4] bit 0 (mic-enable) in the outbound 0x36 audio
report; credit to awalol (upstream) for identifying it. Mic-tagged 0x31
frames ((data[2]>>1)&1) are ALWAYS diverted out of the input path (decoded
when on, dropped when off) so Opus payload can never corrupt sticks/buttons.
Always-on via a sticky-latch keep-alive that only runs post-enumeration
(tud_mounted) so it never floods the fresh-pair handshake (which otherwise
delayed controller detection past the watchdog and tore the link down).
Toggle: bt_mic_enable config field (default on) — OLED Settings + web config.
README gains a "DualSense Microphone over Bluetooth" section;
BLUETOOTH_AUDIO_NOTES.md rewritten from "dead end" to the working mechanism.

USB 3.0 connection watchdog: auto-recovers a stalled connection (re-inquiry)
instead of hanging on the amber lightbar, for USB 3.0 ~2.4 GHz RF interference
that desensitizes the CYW43 BT radio. Re-enabled the ACL-fail / auth-fail /
create-connection-reject recovery paths. README "USB 3.0 ports & Bluetooth
interference" section with mitigations.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 01:30:37 -06:00
MarcelineVPQandClaude Opus 4.7 89847ed06e docs(changelog): cut v0.6.7 — provisional charge ETA on plug-in
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v0.6.7-oled-edition
2026-05-23 20:39:35 -06:00
MarcelineVPQandClaude Opus 4.7 0eceb1d5e2 feat(oled): provisional charge ETA on plug-in — no more ~--m wait
Show a default-rate estimate "~Nm?" the moment charging starts instead of
"~--m" for the ~15-20 min until the first 10% step is timed. The "?" drops and
the number switches to the measured rate once a clean step completes. Default
~15 min per 10% step (kDefaultStepUs), taper-weighted like the measured path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 20:29:46 -06:00
MarcelineVPQandClaude Opus 4.7 d99689bf75 docs(changelog): cut v0.6.6 — configurable screen timeouts (#5) + trig-fold counter (#6)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v0.6.6-oled-edition
2026-05-23 20:03:54 -06:00
MarcelineVPQandClaude Opus 4.7 c65e0c9802 feat(oled): configurable idle-ladder thresholds (#5) + trig-fold diag counter (#6)
Issue #5 (requested by @TerryFrench): the OLED dim/off tiers are no longer
hardcoded at 2/15 min. Two new Config_body fields screen_dim_timeout /
screen_off_timeout (minutes, 0 = tier disabled, range [0,250]) are editable
on the Settings screen (ScrDim/ScrOff) and persist to flash; defaults
preserve the 2/15 ladder and upgraders read those via the config_valid clamp.
The idle timer moved from time_us_32() to 64-bit µs so the full 250-min range
is representable without the ~71-min wrap.

Issue #6: new "trig fold" Diagnostics counter — trigger-bearing 0x02 host
reports that arrived while the speaker stream was active and were folded into
the 0x36 audio frames (via state[]) rather than sent as a standalone 0x31.
Makes trig_allow == to_bt(trig share) + fold visible, confirming the apparent
trig/tx gap is audio-path folding, not dropped reports.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 20:03:45 -06:00
MarcelineVPQandClaude Opus 4.7 585a385219 docs(changelog): cut v0.6.5 — charging UX, persistent lightbar, idle-ladder fixes
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v0.6.5-oled-edition
2026-05-23 19:39:00 -06:00
MarcelineVPQandClaude Opus 4.7 4edcd20181 feat(oled): charge ETA, persistent lightbar, charging-aware idle ladder
Status screen: show an estimated time-to-full ("~43m") while charging,
self-calibrating from each 10% battery notch with Li-ion taper correction
(discard the partial plug-in step; 3-sample moving average; per-step weight
1.0/1.5/2.2 for bulk/80-90/90-100%). Shows "~--m" until the first full step.

Lightbar: the chosen mode + 4 favorites now persist to config flash and
stick across every screen (and through gameplay/audio). A single
lightbar_service() owns the LED via the persistent state[] block (new
state_set_led/state_get_led) with a host-override gate (g_lightbar_override)
so the host's AllowLedColor can't stomp a firmware-chosen mode. New HOST
passthrough mode (default) keeps the game in control of the LED out of the
box. The charging amber pulse (255,100,0) is folded in as top priority.

Idle ladder: keep the panel at the dim/dot tier (never full-off) while
charging so users stop unplugging to wake it (which reset the charge ETA).
Fix idle detection to deadzone stick jitter [120,140] + skip the counter
byte (idata[6]) so the dot tier engages with a controller connected
(mirrors bt.cpp's inactivity heuristic).

New Config_body fields: lightbar_mode + lb_fav_{r,g,b}[4].

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 19:38:51 -06:00
MarcelineVPQandClaude Opus 4.7 219a9dd58a docs(changelog): cut v0.6.4 — idle power ladder + trigger-flow diagnostics
Issue #3: screen-stays-on fixed by the idle power ladder; trigger-flow diagnostics (host02/trig/tx counters + mic_diag.sh bt-trace) added to triage the missing adaptive-trigger tension.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v0.6.4-oled-edition
2026-05-19 21:08:32 -06:00
MarcelineVPQandClaude Opus 4.7 69a0cd4ddb feat(diag): host-side trigger-flow triage via 0xFD + mic_diag.sh bt-trace
Extends the firmware's 0xFD vendor feature report from 32 to 44 bytes
to carry the three host -> dongle -> BT trigger counters added in
5da1be4 (host_out02_total / host_out02_trig_allow / host_out02_to_bt).
mic_diag.sh's bt-trace decoder reads them and prints a one-line
verdict so the user can triage issue #3 in one terminal command
instead of switching to the OLED Diagnostics screen mid-game.

Verdicts encoded:
  host02 > 0, trig == 0       -> host driver never sets the trigger
                                 Allow bits; not a firmware problem.
  trig > 0, tx < trig         -> trigger Allow bits arrive but the
                                 speaker-active gate in main.cpp
                                 swallowed them.
  full chain populated        -> dongle delivered, controller didn't
                                 actuate; Sony BT-side limit.

The 0xFD report ID stays undeclared in the HID descriptor — Linux
hidraw ioctls don't enforce that and WebHID never sees these reports
(config goes through 0xF6). bt-trace's IOCTL_SIZE bumped to 45
(1 byte for the kernel-prepended report ID + 44 bytes payload).

Docs:
  README.md   - new "Diagnostics & debug tooling" section walks
                through the four mic_diag.sh subcommands; previously
                the script was only mentioned inside
                BLUETOOTH_AUDIO_NOTES.md.
  README.md   - Diagnostics-screen description updated for the
                scrolling row-list + cross-link to bt-trace.
  CLAUDE.md   - "Where features live" gets a host-side-diagnostics
                entry explaining how to extend the 0xFD payload and
                wire it into bt-trace's decoder.
  CHANGELOG   - records both the firmware extension and the README
                discoverability fix.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 19:21:00 -06:00
MarcelineVPQandClaude Opus 4.7 c6f490ad8e docs: record idle power ladder, trigger counters, and scrolling Diag screen
CHANGELOG [Unreleased]:
- Added: OLED idle power ladder (breathing dot @ 2 min, full off @ 15 min).
- Added: host02 / trig / tx trigger-flow counters for issue #3 triage.
- Added: D-pad scrolling Diagnostics screen.
- Changed: flush_fb split into flush_fb_raw + chrome wrapper.
- Changed: Diagnostics row order re-prioritized for trigger triage.

README — replace stale "auto-dim after 5 min idle" line with the
new three-stage power ladder description.

CLAUDE.md — new "Idle power ladder" paragraph in the Architecture
section (state machine + wake events + why contrast-register dim
doesn't work on this panel), and a new entry in "Where features
live" for adding diagnostic rows.

There's a stray older `[Unreleased]` heading at CHANGELOG.md:112
that looks like leftover release content (speaker/haptic regression
fix + Audio Auto Haptics) which actually shipped in an earlier tag.
Not touched here; flagging for a future CHANGELOG hygiene pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 19:14:16 -06:00
MarcelineVPQandClaude Opus 4.7 5da1be498e feat(diag): trigger-flow counters + scroll-only Diagnostics screen
Issue #3 (Few things — Triggers and Screen) reports that adaptive
trigger tension is missing through the dongle. The send_trigger_effect
preset cycle on the OLED Trigger Test screen works fine, so the BT
forwarding path is sound — meaning the loss is either at the host
(driver / Steam Input / game not emitting trigger output) or in our
forwarding of the host's 0x02 output report. Adds counters so we can
tell which.

Counters in main.cpp's tud_hid_set_report_cb 0x02 handler:
  - host_out02_total       — every 0x02 OUT report seen
  - host_out02_trig_allow  — those where the host set
                             AllowRight/LeftTriggerFFB
                             (valid_flag0 bits 2 & 3)
  - host_out02_to_bt       — 0x02 reports forwarded as BT 0x31

Diagnosis matrix:
  total grows, trig_allow flat → host driver never sets the trigger
                                 Allow bits = host-side issue
  trig_allow grows, to_bt lags → we're dropping (audio path stealing
                                 the BT pipe while speaker is active)
  all three grow proportionally → forwarding works; controller is
                                  receiving but not actuating
                                  = Sony / DS5 firmware limit

Same flash gets a scroll-only Diagnostics screen — the old layout
was already at the 6-row ceiling and three new counters push it over.
Rows are now a single switch-formatted list (10 rows), 5 visible at
a time, D-pad up/down scrolls. Caret glyphs at the right edge mark
"more above/below". Read-only screen so no cursor (unlike Settings).

Order is by triage relevance: Uptime, BT state, host02, trig/tx,
BT31 in/s fit on the default view; audio + parked-mic counters are
below the fold.

flush_fb split earlier (flush_fb_raw + flush_fb) is unaffected.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 19:08:49 -06:00
MarcelineVPQandClaude Opus 4.7 0a2841bbfe feat(oled): idle power ladder — breathing dot at 2 min, full off at 15 min
Replaces the single-tier auto-dim (5 min, contrast 0x10) with a three-tier
ladder that actually protects the OLED:

- 0–2 min idle: full brightness per bright_idx (unchanged).
- 2 min: clear the panel and render a 2×2 "breathing" dot (1 s on / 1 s
  off) that walks through 8 evenly-spaced screen positions every ~30 s.
  Drops contrast to 0x01. The dot is the indicator that the dongle is
  alive; rotating its position prevents any single pixel from
  accumulating wear.
- 15 min: SH1107 display-off (cmd 0xAE). Panel fully dark.

Wake on KEY0/KEY1, controller pair (BT-connect rising edge), or any
controller input report change.

Why this shape: bench-tested 0x10 and 0x02 contrast values — both
looked nearly identical to full brightness because the SH1107 contrast
register has a heavily non-linear perceptual curve on this Waveshare
panel. The only reliable per-pixel "dim" available is to render fewer
pixels. The breathing dot lights ~4 of 8192 pixels half the time,
which is roughly a 1000× drop in cumulative current vs the bright
Status screen.

flush_fb split into flush_fb_raw + flush_fb so the dim-tier renderer
can push the framebuffer without the chrome arrows (no navigation
target while sleeping).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 18:34:35 -06:00
MarcelineVPQandClaude Opus 4.7 4a984c8347 docs(mic): add encrypted-channel hypothesis to BT-audio notes
Most plausible explanation for the negative-evidence pattern (kernel
gave up, public RE blocked, our matching every documented "enable" bit
got nothing back): the DS5 ↔ PS5 mic channel is encrypted with a
session key derived during pairing, on a Sony-proprietary L2CAP PSM.

Sony's incentives all align with this — voice chat data from a $40
third-party dongle routed to an attacker is a worst-case PR / GDPR
scenario, and anti-spoofing is its own threat model.

Mechanism that fits: BT-Classic SSP link key → Sony-proprietary KDF →
audio-channel session key (likely AES-CCM). PS5 firmware on both ends
knows the KDF; third parties don't. Even a BT sniffer would see only
encrypted payload blobs without the KDF.

Re-frames "we can't get mic over BT" from "we haven't tried hard
enough" to "the architecture is intentionally hardened" — a clear
answer to give users + a clear bar for anyone wanting to actually
pursue this.

Credit: hypothesis suggested by MarcelineVPQ ("its probably encrypted,
I think the microphone over bluetooth is a security concern for sony").

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 17:57:20 -06:00
MarcelineVPQandClaude Opus 4.7 e0a86a8983 fix(diag): mic_diag.sh — strip kernel-prepended report ID byte
bt-trace was reading kernel-prepended 0xFD as the low byte of bt_31
counter (and shifting all subsequent fields by 1), producing
nonsensical rates like 200000/s and frame lengths of 20224 bytes.
Slicing buf[1:] before decoding gives the firmware-supplied payload
bytes aligned to the documented 0xFD layout.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 17:56:24 -06:00
MarcelineVPQandClaude Opus 4.7 0429bd6703 docs(mic): park BT-mic investigation; document findings + USB workaround
Park the DualSense-microphone-over-Bluetooth feature. Investigation
log + diagnostic infrastructure ship as-is for a future contributor
who wants to take another crack at it.

BLUETOOTH_AUDIO_NOTES.md (new): full hand-off doc covering what we
tried, what we found, where Sony / the kernel / the public RE
community currently stand on DS5 BT mic. Highlights:

- Real DS5 mic works fine over USB; our dongle's UAC1 descriptor
  matches the DS5's exactly. The data path is correct.
- DS5 firmware on the test controller (build date Jul 4 2025) does
  not stream microphone audio over the standard BT-HID L2CAP
  channels. Confirmed by: every BT input report is a standard 79-
  byte 0x31 report with no audio appended; no alternative report
  IDs ever arrive; no bit of byte 2 correlates with mic-tagged
  frames; bidirectional-audio hypothesis (aplay + arecord) did not
  trigger streaming.
- Upstream awalol/DS5Dongle 'mic' branch's RE — 71-byte Opus at
  data + 4 of frames where (data[2] >> 1) & 1 — appears to be
  firmware-revision-specific; bit 1 of byte 2 is never set on the
  current firmware.
- Linux kernel hid-playstation.c line ~1509 explicitly says
  "Bluetooth audio is currently not supported" — the kernel
  maintainers reached the same conclusion.
- Our state config (AllowAudioControl=1, MicSelect=Internal,
  VolumeMic=0x40, MicMute=0, AudioPowerSave=0) matches exactly
  what the kernel driver sends to enable the mic over USB. The
  bytes are right; the BT-side trigger we're missing is not in
  the standard DS5 protocol.

Diagnostic infrastructure stays in firmware (mic_diag.sh,
vendor feature reports 0xFD/0xFE, OLED Diagnostics rate counters,
Opus-decoder + mic_fifo wired but gated behind `if (false)` in
on_bt_data). Zero cost when mic-add is disabled; ready to flip back
on the moment someone identifies the right trigger.

README.md: short "known issue" entry pointing users at USB-direct
when they need the mic, and at BLUETOOTH_AUDIO_NOTES.md for the
research log.

Possible next steps for the curious:
- SDP browse the DS5 to discover non-HID L2CAP services
- BT sniffer (~$50-200) between a real PS5 and a DS5 during voice
  chat — would settle the protocol question definitively
- Test with an older DS5 firmware revision (pre-2024) to see if
  the BT mic path used to work and got removed

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 17:56:00 -06:00
MarcelineVPQandClaude Opus 4.7 72f163ca50 wip(mic): BT-side mic capture infrastructure + host-side diag
In-progress work on DualSense microphone capture over BT. Mic-add tap
itself is disabled (was decoding standard input bytes as Opus and
producing INT16_MIN garbage on the USB IN endpoint) but everything
around it is wired and ready to re-enable once we identify the actual
mic transport.

Firmware:
- src/audio.cpp: Opus decoder on core0, mic_fifo queue, audio_loop
  mic-in path with decode + mono->stereo + tud_audio_write. Decoder
  init in audio_init() (creates 48kHz mono OpusDecoder).
- src/audio.h: exports mic_add_queue() + per-frame diagnostic
  accessors (audio_mic_frames, last_decoded, last_want, last_wrote,
  last_toc).
- src/main.cpp on_bt_data(): BT-side instrumentation — counts every
  INTERRUPT input report, tracks min/max length, OR mask of byte[2],
  most recent non-0x31 report ID, hex prefix of last 0x31/other/any
  frame, full content of the longest 0x31 frame seen. Mic-tap call
  itself stubbed behind `if (false)` pending the real detector.
- src/state_mgr.cpp: state_init_data byte 6 (VolumeMic) 0xFF→0x40
  (was out of range), byte 9 (MuteControl) 0x0F→0x00 (clear all
  PowerSave bits — AudioPowerSave was muting DSP).
- src/cmd.cpp: two new vendor feature reports — 0xFD returns 32-byte
  diagnostic state (counters + prefixes), 0xFE returns the longest
  0x31 frame in full (up to 80 bytes). Both queryable via
  /dev/hidraw on Linux from the host script.
- src/oled.cpp: Diagnostics screen shows TOC + decode result + USB
  wrote/want bytes for live BT-side visibility.

Host-side:
- scripts/mic_diag.sh: subcommands `status`, `capture [secs]`,
  `watch`, `bt-trace`. The bt-trace subcommand reads the 0xFD
  feature report via hidraw ioctl, decodes counters + recent
  prefixes, computes per-second rates. Drastically cuts iteration
  time — no OLED relay or per-test flash cycle needed.

Findings to date:
- Upstream/mic's mic-flag bit ((data[2] >> 1) & 1) does NOT match
  this DS5 firmware; bit 1 of byte[2] is NEVER set. Bit 0 is the
  standard input report type indicator, not a mic tag — confirmed
  by stick-bytes appearing as our supposed "Opus prefix".
- DS5 sends both report ID 0x01 and 0x31 over BT; the longest frame
  is a standard 79-byte 0x31 input report with sticks/IMU/touchpad
  but no audio bytes appended.
- Conclusion in progress: the DS5 firmware on this controller is
  not currently streaming mic over BT at all, even with
  AllowAudioControl=1, VolumeMic=0x40, AudioPowerSave=0,
  MicMute=0. Next investigation step: compare against a USB-mode
  DS5 to see what a real mic stream looks like at the UAC1 layer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 17:35:08 -06:00
MarcelineVPQandClaude Opus 4.7 2209f9b8c7 docs(changelog): cut v0.6.3-oled-edition
Small follow-up to v0.6.2. The version-centralization landed on
master after the v0.6.2 tag, so v0.6.2 UF2s still ship the stale
hardcoded "v0.6.0" on the Status header. v0.6.3 rebuilds from the
current master so the new FIRMWARE_VERSION macro path is exercised
and the Status header reflects the actual release tag.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v0.6.3-oled-edition
2026-05-18 22:25:05 -06:00
MarcelineVPQandClaude Opus 4.7 a7835ac156 chore(version): single source of truth — release tag → FIRMWARE_VERSION macro
Audit revealed the Status screen hardcoded "v0.6.0" while v0.6.1 and
v0.6.2 had already shipped — the version string was scattered and
fell behind every release. Centralizing:

- CMakeLists.txt: target_compile_definitions adds
  FIRMWARE_VERSION="${VERSION}" so the existing -DVERSION=$tag flag
  passed by release.yml propagates into C++ as a compile-time macro.
  Local builds without -DVERSION get "dev" — a deliberate "this is
  not a release build" tell on the Status header.
- src/oled.cpp render_screen() now renders
  "DS5 Bridge " FIRMWARE_VERSION via string-literal concatenation. The
  only place a version literal exists in source is now the macro at
  the link layer.
- CLAUDE.md gets a "Versioning — single source of truth" section
  documenting the release ritual (CHANGELOG bump → tag → push → gh
  release create → CI builds + propagates) and naming the
  WEB_REPO_DISPATCH_PAT follow-up that's still pending.

Web preview is updated separately to read firmware-latest.json at
runtime — same single source of truth, different distribution path
(the GitHub API).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 22:17:46 -06:00
MarcelineVPQandClaude Opus 4.7 ac035fcb25 docs(changelog): cut v0.6.2-oled-edition
OLED button-model + arrow-chrome refactor on top of v0.6.1. Strict
KEY0/KEY1 nav on every screen, KEY0+KEY1 chord reboot, per-screen
cycling moved to controller buttons (Triangle on Trigger Test, R1 on
Lightbar). Visual chrome strip with > / < arrows on the left edge.
Web preview mirrored in lockstep + orange-tint indicator for screens
whose firmware feature report Chrome WebHID can't expose.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v0.6.2-oled-edition
2026-05-18 22:11:28 -06:00
MarcelineVPQandClaude Opus 4.7 5b9df0596b docs(readme): OLED section rewrite — 11 screens, new button model, web preview screenshots
- Bump screen count 10 → 11 in the OLED Display Add-on section to
  reflect the CPU/Clock screen added in v0.6.1.
- Update the cycle order to include CPU/Clock between Diagnostics and
  BT Signal.
- Drop the per-screen ASCII mockups (they were noisy and out of date)
  and link each screen section to a consistent web-preview screenshot
  under assets/oled/. Eleven screenshots, named oled_sc01.jpg through
  oled_sc11.jpg, captured from the OLED Preview tab of the web config
  so every screen looks the same and the chrome/layout matches the
  newly-flashed firmware.
- Rewrite the "KEY1 behavior by screen" table as a single "Button
  reference" table. K0 = next, K1 = back, K1 long-press = brightness
  cycle, K0+K1 hold = reboot. The per-screen state-change actions
  (Triangle / R1 / D-pad) live on the controller now and are
  documented per screen.
- Update Soft-reboot Recovery + the top-level Features bullet to
  reflect KEY0+KEY1 chord replacing KEY0 double-click.
- Update per-screen descriptions: Lightbar uses R1 to cycle mode (not
  K1); Trigger Test uses Triangle to cycle preset (not K1); Settings
  text updated to mention the new D-pad cursor behavior on the web
  preview.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 22:11:16 -06:00
MarcelineVPQandClaude Opus 4.7 bd2f33bf0d docs(readme): web config tool section + BOOTSEL explainer + overclock clarity
- Add a dedicated "🛠️ Web Config Tool" section near the top of the
  README so the deep link to https://marcelinevpq.github.io/DS5Dongle-
  OLED-Config-Web/#config is discoverable. Lists the three tabs (Flash
  Firmware / Config / OLED Preview) and what each one does end-to-end:
  a brand-new Pico 2 W can go from "out of the box" to flashed and
  configured without touching a terminal.
- BOOTSEL mode callout explaining what the physical button is, how to
  enter the mode, and what the user sees (Pico mounts as a removable
  drive). First-time flashers were getting stuck on this step.
- Performance / Overclocking section reworded: lead with "you don't
  need to do anything — the overclock is baked into the firmware".
  Stock UF2 sets 320 MHz @ 1.20 V automatically via vreg_set_voltage
  and set_sys_clock_khz in src/main.cpp. The "if it fails to boot,
  raise voltage / lower clock" line is scoped to people compiling
  from source.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 21:52:26 -06:00
MarcelineVPQandClaude Opus 4.7 3eb0e76508 feat(oled): strict K0/K1 nav + chord reboot + arrow chrome + controller cycle
Refactor the Pico-OLED-1.3 button model and visual layout:

- KEY0 single press = "next screen" (double-click reboot path removed —
  it was easy to fire by accident while paging quickly).
- KEY1 short press = "previous screen" on every screen. The old
  contextual cycle on Trigger Test (cycle trigger preset) and Lightbar
  (cycle lb_mode) moves to controller buttons:
    * △ rising edge on Trigger Test  → cycle trigger preset
                                       (re-applies via send_trigger_effect)
    * R1 rising edge on Lightbar     → cycle lb_mode
  △ on Lightbar stays as "save current RGB to favorite slot 0" (existing
  UX), so R1 is used there instead.
- KEY1 long press = cycle brightness — unchanged.
- KEY0 + KEY1 held simultaneously for >= 1 s → watchdog_reboot. Pre-empts
  per-key handlers so a chord cancels any armed single press. DS5
  PS+Mute hold-2 s remains the headless backup.

Visual layout:

- New draw_button_chrome() paints '>' at (0, 8) and '<' at (0, 49) on
  every frame from flush_fb(), pairing the on-screen labels with the
  physical KEY0/KEY1 buttons on the left edge of the device.
- Every render_screen_*() shifts main content right by kContentX (6 px)
  to clear the chrome strip. Stick boxes, L2 column, L1 bar, and D-pad
  center on the Status screen also shift right by 6 to avoid the '<'
  glyph painting inside the live left-stick area.
- "K0=next K1=back" footers removed from all screens (the chrome strip
  is now the affordance). Trigger Test footer = "Tri=cycle"; Lightbar
  footer = "R1=mode"; Slots/Settings keep their contextual hints.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 21:51:59 -06:00
MarcelineVPQandClaude Opus 4.7 c5759dfce8 chore(gitignore): ignore build-pr/ + local scratch files
build-pr/ is a parallel CMake build tree that survives between PR
investigations. Added alongside build/ to keep it out of the index.

Also ignore four root-level files that are inherently per-user:
.claude/ (Claude Code config), .directory (KDE Dolphin metadata),
*.code-workspace (VS Code workspace files), pr-review-*.md (local
scratch review notes).

Drops VS Code's untracked-file count for a fresh clone + build run
from ~1760 to 0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 19:14:09 -06:00