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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
- 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>
- 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>
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>
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>
CPU/Clock OLED diagnostics screen (PR #2) + battery_led disconnect
fix, on top of v0.6.0-oled-edition. Promotes the [Unreleased] section
to [0.6.1-oled-edition] with today's date.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Follow-up to #2 — the PR added a CPU/Clock OLED screen and bumped
kNumScreens from 10 to 11, but README still advertised 10 screens
in two places (header blurb + Features bullet).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Future releases get:
- A 'Checksums' section appended to the release body with sha256sum -c
-compatible 'HASH filename' lines for each .uf2 attached
- A SHA256SUMS.txt file uploaded alongside the .uf2 artifacts so users
can verify with 'sha256sum -c SHA256SUMS.txt' after downloading
Web-config flasher already compares this against its own SHA-256 on the
'Use latest release' path, but the hashes on the release page let people
manually verify the bundled site UF2 too.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a repository_dispatch step to the release workflow that pings the
DS5Dongle-OLED-Config-Web repo with event-type 'firmware-released' after
the UF2s are uploaded. The web repo's deploy.yml listens for that event
and re-runs its build, which re-fetches the latest release UF2 and
bundles it into the GH Pages site.
Requires a fine-grained PAT stored as WEB_REPO_DISPATCH_PAT secret on
this repo, scoped to:
- Repository: MarcelineVPQ/DS5Dongle-OLED-Config-Web
- Permissions: Actions: Write, Metadata: Read
If the secret isn't set, the step fails 401 and continue-on-error
swallows it — release upload still succeeds.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds an [Unreleased] block for the fb68ea5 disconnect fix (cross-references
the upstream PR awalol/DS5Dongle#101), and a [0.6.0-oled-edition] entry
covering the GitHub release / tag that wraps the existing 0.6.0-rebase code.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The low-battery blink would stay frozen mid-cycle (or briefly resume
during reconnect retries) after a controller died and lost connection,
because the stale-report check stopped toggling but never turned the
LED off, and the cached interrupt_in_data[52] still read low.
New `battery_led_on_disconnect()` clears blink state, forces the LED
off, and zeros last_report_us so the stale-check early-return blocks
any new blink until a fresh 0x31 report arrives on the next connection.
Called from bt.cpp's HCI_EVENT_DISCONNECTION_COMPLETE handler.
Stale-check in the tick also now forces LED off when it fires while a
blink was in progress (defense in depth for unclean disconnects).
Same bug exists in upstream awalol/DS5Dongle (their battery_led.cpp is
byte-identical to ours pre-fix) — Sura Academy reported it in their
Discord. Plan to send this back as a PR after we validate it locally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
README: checkout from our master since our content (10-screen
docs, web-config link, multi-slot docs, Auto Haptics docs,
Acknowledgements, etc.) is the source of truth for the fork; upstream's
v0.6.0 README updates are mostly about their new web config tool which
we don't surface (we link our own fork's web config instead).
CHANGELOG: new [0.6.0-rebase] entry on top documenting (a) the
state_mgr adoption + speaker-fix hack drop, (b) the discoverable-rule
loosening for multi-slot, (c) the iSerialNumber restore that fixes
#100, and (d) verification results from the mid-rebase flash.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Build now compiles. audio.cpp on top of upstream's state_mgr-based
audio_loop adds three additive pieces:
1. VU peak meters for ch0/1 (spk_max) and ch2/3 (hap_max) tracked
per-sample in the existing loop; written back to g_peak_spk /
g_peak_hap after. Accessors decay 12.5% per read so the OLED
VU bars fall back naturally over a few frames.
2. Monotonic byte-flow counters: g_usb_frames++ after tud_audio_read
(the OLED Diagnostics screen + web emulator compute USB-aud/s
and BT-0x32/s as delta over time). g_bt_packets++ after each
0x36 bt_write.
3. Audio Auto Haptics DSP — 1-pole LP + envelope follower +
modulation + soft-clip per loteran/DS5Dongle 5d6bc2f, with our
added 4th "Fallback" mode that fires only after the game's
native haptic path has been silent for ~1 s (NATIVE_SILENT_TIMEOUT
= 100 audio_loop calls). Preserves native HD haptics (Spider-Man
Remastered) while filling in for games that send no haptic data
(Ghost of Tsushima). Modes: 0=Off, 1=Fallback (default), 2=Mix,
3=Replace.
Crucially the state_mgr-based state_set(pkt+13, 63) call is left
untouched — that's upstream's correct replacement for our prior
state_data restore hack, and it's load-bearing for speaker output.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bt.h: add OLED accessors (bt_is_connected, bt_get_addr, bt_hci_err_count)
and 6 multi-slot accessors (bt_get_slot, bt_set_slot, bt_forget_slot,
bt_wipe_all_slots, bt_slot_occupied, bt_slot_get_addr).
bt.cpp:
- #include "slots.h"; add g_current_slot + update_discoverable()
helper that gates gap_discoverable_control on slots_any_empty().
- 3 OLED accessor impls + 6 slot accessor impls.
- bt_init: slots_load() + g_current_slot from Config_body.current_slot,
then update_discoverable() instead of bare gap_discoverable_control(1).
- HCI_EVENT_INQUIRY_RESULT: slot-ownership filter (skip devices owned
by other slots; if our slot is occupied, only accept matching addr).
- L2CAP_EVENT_CHANNEL_OPENED HID_CONTROL: slot_assign on empty slot
(Phase G auto-bond pattern).
- L2CAP_EVENT_CHANNEL_OPENED HID_INTERRUPT: replace upstream's
gap_*_control(false) pair with update_discoverable() — they wanted
"non-discoverable once connected"; we want "non-discoverable once
all 4 slots are full". Strictly looser, which is correct since
empty slots need to accept new pairings.
- Inquiry-complete + disconnection-complete handlers similarly
swapped to update_discoverable().
Upstream's state_mgr hooks (state_init / state_set / state_update
calls in L2CAP_EVENT_CHANNEL_OPENED and elsewhere) are preserved
as-is — they're the upstream rumble-fix infrastructure.
Build still fails on oled.cpp references to audio_peak_* / counter
accessors that Phase E adds.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three additions on top of upstream's v0.6.0-hotfix main.cpp:
- #include "oled.h" + oled_init() after audio_init/state_init,
oled_loop() in the main while(1).
- PS + Mute hold-2-seconds combo at the top of interrupt_loop()
for headless soft-reboot via watchdog_reboot(0,0,0). Bit pattern
(interrupt_in_data[9] & 0x05) is rare in normal gameplay so the
long hold prevents accidental triggers.
Upstream's state_init() call sits right between audio_init() and our
new oled_init() — preserved as-is. The state_mgr → 0x31 output report
flow in tud_hid_set_report_cb is upstream's rumble path; not touched.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Build-system, config, usb, and CI wiring on top of upstream's
v0.6.0-hotfix tree. Build will still fail until Phase C+D land bt.h
accessors that oled.cpp references; that's expected mid-rebase.
CMakeLists.txt: add src/oled.cpp + src/slots.cpp to add_executable,
link hardware_spi, restore OUTPUT_NAME ds5-bridge-oled.
src/config.h: append the 4 OLED Edition Config_body fields
(current_slot + auto_haptics_enable/gain/lowpass). Preserves
upstream's field order for the first 8 fields.
src/config.cpp:
- speaker_volume validity default -100 dB → 0 dB (footgun fix)
- 4 new validity guards for the appended fields with sensible
defaults (Fallback / 100 % / 160 Hz / slot 0)
- config_default() impl (was declared in upstream's config.h since
v0.5.4 but never defined; the OLED Edition's "Reset to defaults"
Settings item calls it)
src/usb.cpp: drop the UAC1 SET_CUR Volume → flash-config sync that
let PipeWire/Pulse silently override the user's saved Spk Vol on
every device reconnect. Borrowed from loteran/DS5Dongle commit
03fa1e4.
src/usb_descriptors.cpp: restore iSerialNumber = STRID_SERIAL.
Upstream's commit e79c762 zeroed it to work around SpecialK (#32),
but that broke Windows device identity (#100) — users lost
per-device volume / app preferences when moving USB ports. The
OLED Edition prioritizes Windows device-identity stability for
the broader user base over SpecialK compat for a narrower one.
.github/workflows/{build,release}.yml: artifact filenames use
ds5-bridge-oled.uf2 (and -debug / -picow variants) per the
OUTPUT_NAME rebrand.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>