Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
219a9dd58a | ||
|
|
69a0cd4ddb | ||
|
|
c6f490ad8e | ||
|
|
5da1be498e | ||
|
|
0a2841bbfe | ||
|
|
4a984c8347 | ||
|
|
e0a86a8983 | ||
|
|
0429bd6703 | ||
|
|
72f163ca50 |
@@ -0,0 +1,77 @@
|
||||
# Bluetooth microphone investigation — current status
|
||||
|
||||
**TL;DR:** The DualSense's built-in microphone does **not** work when the controller is paired to this dongle over Bluetooth. It works fine when the controller is connected directly to a host over USB. This is a Sony / DS5-firmware-side limitation we currently can't work around without reverse engineering or BT-sniffer access to PS5 ↔ DS5 traffic. The same limitation is documented in the upstream Linux kernel driver (`drivers/hid/hid-playstation.c` line ~1509: *"Bluetooth audio is currently not supported"*).
|
||||
|
||||
This file is a hand-off / research log for the next person who tries.
|
||||
|
||||
## What does work
|
||||
|
||||
- **Direct USB-C from DS5 → host:** mic enumerates as a UAC1 IN endpoint at 48 kHz / 16-bit / 2 channels on `EP 0x82`, max packet 196 bytes. ALSA recognizes it as `card N: Controller [DualSense Wireless Controller]`. `arecord` captures real audio after raising the `Headset Capture Volume` mixer control (it defaults to 0 dB).
|
||||
- **Our dongle's USB descriptor** correctly mirrors the DS5's UAC1 layout — same interfaces, same alt settings, same endpoint addresses, same packet sizes. Verified with `lsusb -v` against a real DS5.
|
||||
- **All the firmware-side decode infrastructure for BT mic is in place** (Opus decoder, `mic_fifo` queue, `tud_audio_write` to the IN endpoint, mono → stereo duplication) — see `src/audio.cpp`. It's currently gated behind `if (false)` in `src/main.cpp`'s `on_bt_data()` because we have nothing to feed it.
|
||||
|
||||
## What doesn't, and why
|
||||
|
||||
The DS5 firmware on the test controller (build date `Jul 4 2025`, queried via feature report 0x20) **does not stream microphone audio over the standard BT-HID L2CAP channels** (PSM 0x11 control + 0x13 interrupt).
|
||||
|
||||
What we tried:
|
||||
|
||||
1. **Upstream `awalol/DS5Dongle` `mic` branch as reference.** That branch claims to extract a 71-byte Opus packet at `data + 4` of any BT input report where `(data[2] >> 1) & 1` is set. On our DS5 firmware, **bit 1 of byte 2 is never set** (verified across thousands of frames via the `g_31_b2_or` OR mask). The upstream RE was likely done on a different (older) DS5 firmware revision.
|
||||
2. **Bit 0 of byte 2** matches roughly all standard input reports — confirmed by reading the supposed "mic prefix" via `0xFD` feature report and seeing live stick X/Y values (not Opus data). Not a mic flag.
|
||||
3. **Frame length sweep.** Longest BT 0x31 frame we ever see is 79 bytes — a fully-decoded standard DS5 input report (sticks + IMU + touchpad + battery + sensor timestamp + trailing zeros). No audio bytes appended anywhere.
|
||||
4. **Other report IDs.** Counted ALL incoming BT input reports by report ID. Only `0x01` (rare) and `0x31` (common). No 0x33 / 0x35 / 0x36 / 0x39 / etc. The DS5 isn't sending anything mic-shaped on a different ID.
|
||||
5. **State configuration matching the kernel.** Set `AllowAudioControl=1`, `AllowMicVolume=1`, `AllowAudioMute=1`, `MicSelect=Internal`, `VolumeMic=0x40`, `MicMute=0`, `AudioPowerSave=0` — exactly what `hid-playstation.c` sets when calling its "Enable microphone" path. DS5 still doesn't stream.
|
||||
6. **Bidirectional audio session hypothesis.** Maybe the DS5 only streams mic when there's also active speaker audio (`0x36` packets) flowing. Tested: ran `aplay /dev/zero` simultaneously with `arecord`. No change in BT-side counters, no new report IDs, no longer frames. Disproved.
|
||||
7. **State refresh on host UAC1 alt-setting change.** Considered hooking `tud_audio_set_itf_cb(itf=2, alt=1)` to send the DS5 a fresh "enable mic" state update. Not implemented — given the kernel comment and our state matching the kernel's own "enable" sequence, this wouldn't have helped.
|
||||
|
||||
What we **did not** try (real next steps if anyone picks this up):
|
||||
|
||||
- **SDP browse the DS5** over BT after pairing. Discover what L2CAP PSMs / services it advertises beyond HID. If there's a Sony proprietary audio PSM we haven't subscribed to, that's where mic traffic might live.
|
||||
- **Open additional L2CAP channels** (proprietary audio PSM if found, or standard ones like A2DP=0x19 / RFCOMM=0x03) and watch for unsolicited inbound data.
|
||||
- **Compare DS5 firmware revisions.** Test with an older DS5 (pre-2024 manufacture) and see if it streams mic over BT — that would tell us whether Sony removed the feature or just nobody documented the protocol. (We only have one DS5; can't test.)
|
||||
- **BT sniffer** between a PS5 console and a DS5 during voice chat. Tells us exactly what L2CAP channels and bytes Sony uses for mic. Equipment-intensive (~$50–200 for an Ubertooth or commercial sniffer).
|
||||
- **DS5 firmware disassembly.** Legally fraught, almost certainly EULA-violating.
|
||||
|
||||
## Strongest hypothesis: the channel is encrypted
|
||||
|
||||
The shape of all the negative evidence — kernel maintainers giving up, no public RE project succeeding, our matching every documented "enable" bit and getting nothing — strongly suggests the channel is **encrypted with a session key derived during pairing**, not just transported on an undocumented PSM. Sony's incentives line up perfectly:
|
||||
|
||||
- **PR / privacy:** a $40 third-party dongle routing a user's PS5 voice chat to a malicious host is a worst-case PR scenario. Encrypting the mic channel is the obvious defense.
|
||||
- **GDPR-class regulation:** voice biometrics from a console controller over plaintext BT is the kind of thing EU regulators ask hard questions about.
|
||||
- **Anti-spoofing:** prevents injecting fake mic data into a PS5 session, which is its own threat model.
|
||||
|
||||
Mechanism that fits the evidence:
|
||||
|
||||
- During pairing, BT Classic SSP produces a link key. The PS5 + DS5 firmware likely run a Sony-proprietary KDF on top of that link key to produce an audio-channel session key.
|
||||
- Mic audio is transported on a Sony-allocated proprietary L2CAP PSM (not in the standard BT-SIG ranges) and encrypted with that session key (AES-CCM or similar).
|
||||
- A third-party dongle could connect to the PSM if it knew the number, but without the KDF / session key the payload would be opaque encrypted blobs.
|
||||
|
||||
**Implication:** a BT sniffer might tell us the PSM and packet timing/sizes, but not the payload contents. Building a PS5-impersonating dongle that derives valid session keys would require either Sony system-software disassembly or DS5 firmware disassembly — legally fraught, and a much bigger undertaking than what this project is set up for.
|
||||
|
||||
This re-frames "we can't get mic over BT" from "we haven't tried hard enough" to "the architecture is intentionally hardened against exactly this." That's not nothing — it's a clear answer to give users who ask, and a clear bar to clear if anyone wants to actually pursue it.
|
||||
|
||||
## What we built that's useful regardless
|
||||
|
||||
These all stay shipped — they're general-purpose audio-debug infrastructure now:
|
||||
|
||||
- **`scripts/mic_diag.sh`** with subcommands `status`, `capture [secs]`, `watch`, `bt-trace`. Drives the entire diagnostic loop from the host without needing OLED-relay-through-the-user; reads vendor feature reports via `/dev/hidraw`.
|
||||
- **Vendor HID feature report `0xFD`** (32 bytes): BT input-report counter, non-0x31 counter, last seen non-0x31 report ID, OR mask of byte 2 across 0x31 frames, length range, hex prefix of last frame.
|
||||
- **Vendor HID feature report `0xFE`** (82 bytes): full content of the longest 0x31 frame seen, for byte-level inspection.
|
||||
- **OLED Diagnostics screen** carries BT31/Mic rate + recent frame prefix + opus dec/wrote bytes — useful for any future audio-path debugging at the bench.
|
||||
- **`src/audio.cpp`** mic-decode infrastructure (Opus decoder on core0, FIFO, mono → stereo duplication, `tud_audio_write` to IN endpoint). Disabled at the `mic_add_queue` call site, ready to re-enable the moment a real mic trigger is identified.
|
||||
- **`src/state_mgr.cpp`** initial state corrected — `VolumeMic` was `0xff` (out of valid range per spec; max is `0x40`), `MuteControl` had all `*PowerSave` bits set which would have power-gated the audio DSP. These corrections don't enable BT mic but they're the right defaults regardless.
|
||||
|
||||
## References
|
||||
|
||||
- Linux kernel `drivers/hid/hid-playstation.c`. Quoted lines: ~1407–1420 (mic enable/disable), ~1509 (*"Bluetooth audio is currently not supported"*). [Raw source on GitHub mirror](https://raw.githubusercontent.com/torvalds/linux/master/drivers/hid/hid-playstation.c).
|
||||
- PSDevWiki [DualSense HID Commands](https://www.psdevwiki.com/ps5/DualSense_HID_Commands) — has factory/manufacturer commands (report IDs 128, 129, 160, 164, 165) for BT patches and audio codec selection, but explicitly notes most "do not work with retail controllers". Not a path forward.
|
||||
- Upstream `awalol/DS5Dongle` branch `mic` (commits `9c197fc feat: mic work`, `3829163 mic mono channel`). RE'd a working mic path for an older DS5 firmware revision; we ported the data plumbing but the BT-side trigger differs on current firmware.
|
||||
- dualsensectl: `command_microphone on/off` sets `valid_flag0 |= DS_OUTPUT_VALID_FLAG0_AUDIO_CONTROL_ENABLE` and clears `DS_OUTPUT_POWER_SAVE_CONTROL_MIC_MUTE`. Same as what we already do on connect.
|
||||
|
||||
## For users asking about the mic
|
||||
|
||||
When users report "the mic doesn't work":
|
||||
|
||||
- **Plug the DS5 into the host via USB.** Mic works out of the box. You may need to raise the `Headset Capture Volume` mixer control if it defaults to 0 dB.
|
||||
- **Over the dongle's Bluetooth pairing, the mic is currently a known limitation** — not something a firmware update on our side can fix without further reverse engineering of the DS5's proprietary BT audio path.
|
||||
- The diagnostic tools in `scripts/mic_diag.sh` are available if you want to help reverse engineer this; PRs welcome.
|
||||
@@ -10,6 +10,25 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Version
|
||||
|
||||
---
|
||||
|
||||
## [0.6.4-oled-edition] — 2026-05-19
|
||||
|
||||
Trigger-flow diagnostics (in response to issue #3) + the OLED idle power ladder. UF2s attached to [the GitHub release](https://github.com/MarcelineVPQ/DS5Dongle-OLED-Edition/releases/tag/v0.6.4-oled-edition) (built by `.github/workflows/release.yml`).
|
||||
|
||||
### Added
|
||||
|
||||
- **OLED idle power ladder.** Replaces the single-tier 5-min auto-dim with a three-stage state machine: at 2 min idle the panel wipes black and a 2×2 "breathing dot" (1 s on / 1 s off) walks through 8 evenly-spaced positions every 30 s; at 15 min idle the SH1107 is sent `cmd(0xAE)` (display off) entirely. Wakes instantly on KEY0/KEY1, controller pair (BT-connect rising edge), or any input-report change. Why this shape: on the Waveshare panel, bench-testing `kDimContrast = 0x10` and `0x02` both produced only ~10 % perceptual reduction (SH1107's contrast register vs apparent brightness is heavily non-linear on this hardware), so the only reliable per-pixel dim available is *rendering fewer pixels*. The breathing dot lights ~4 of 8 192 pixels half the time — roughly a 1 000× drop in cumulative current — while still indicating "the dongle is alive," and the rotating position spreads OLED wear across the panel.
|
||||
- **Trigger-flow diagnostic counters on the Diagnostics screen.** `host02` (total `0x02` HID OUT reports from host) / `trig` (those where the host set `AllowRight|LeftTriggerFFB` in `valid_flag0`) / `tx` (forwarded as BT `0x31` sub-`0x10`). Added in response to issue #3 ("trigger tension missing in Death Stranding 2"). Lets the user triage in one game session whether the dongle, the host driver, or the controller is the source of the missing adaptive-trigger effect — without a UART or BT sniffer.
|
||||
- **Diagnostics screen now scrolls with the controller D-pad.** Refactored to a row-list (10 rows currently: Uptime / BT state / host02 / trig+tx / BT31 in/s / USB aud/s / BT32 out/s / Mic in/s / Mic dec=&w= / Mic prefix). 5 rows visible at a time; ▲/▼ glyphs at the right edge mark "more above/below." Read-only — no cursor, unlike Settings, since there's nothing to select.
|
||||
- **Host-side trigger-flow triage via `scripts/mic_diag.sh bt-trace`.** The firmware's `0xFD` vendor feature report grew a second section (bytes 32–43) with the trigger counters; `bt-trace`'s Python decoder now reads them and prints a one-line verdict — "host driver isn't setting Allow*TriggerFFB" / "trigger Allow bits set but speaker path stole the BT pipe" / "full chain reached the controller". Lets the user diagnose issue #3 without a UART cable or OLED-relay-per-flash.
|
||||
- **`README.md` "Diagnostics & debug tooling" section** documents `scripts/mic_diag.sh` and its subcommands. The script existed but was only mentioned inside `BLUETOOTH_AUDIO_NOTES.md` — invisible to anyone who hadn't already read the parked-mic notes.
|
||||
|
||||
### Changed
|
||||
|
||||
- **`flush_fb()` split.** Internal refactor: `flush_fb_raw()` writes just the framebuffer; `flush_fb()` is now `draw_button_chrome() + flush_fb_raw()`. Lets the dim-tier renderer push the breathing dot without the K0/K1 chrome arrows (no navigation target while the panel is asleep).
|
||||
- **Diagnostics row order re-prioritized.** The first 5 rows (always visible without scrolling) cover the most common triage path: Uptime / BT state / `host02` / `trig`+`tx` / `BT31 in/s`. Audio + parked-mic-investigation counters live below the fold.
|
||||
|
||||
---
|
||||
|
||||
## [0.6.3-oled-edition] — 2026-05-18
|
||||
|
||||
Small follow-up to v0.6.2. UF2s attached to [the GitHub release](https://github.com/MarcelineVPQ/DS5Dongle-OLED-Edition/releases/tag/v0.6.3-oled-edition) (built by `.github/workflows/release.yml`).
|
||||
|
||||
@@ -66,6 +66,8 @@ The development cadence is **one feature per UF2 + checkpoint with the user befo
|
||||
|
||||
**OLED add-on is optional and self-contained:** All 10 screens, the SH1107 SPI driver, the 5×7 font, the icon table, and the input handling live in `src/oled.cpp` (~30 KB). The only outward dependencies are read-only accessors (`bt_is_connected`, `bt_get_addr`, `audio_peak_*`, `bt_get_signal_strength`, the new slot accessors) and the global `interrupt_in_data[63]` (read-only for input visualization). When no OLED is wired, the SPI writes go nowhere — no init check needed.
|
||||
|
||||
**Idle power ladder (`oled_loop` tail):** Three-stage state machine (`OLED_ACTIVE` → `OLED_DIM` → `OLED_OFF`) driven by `last_activity_us`. `kAutoDimUs = 2 min` enters Dim — the regular per-screen render is replaced with `render_dim_pulse()`, which clears the framebuffer and walks a 2×2 dot through 8 positions every 30 s, blinking 1 s on / 1 s off. `kAutoOffUs = 15 min` enters Off — `cmd(0xAE)` puts the SH1107 to sleep and `oled_loop` returns early before rendering. Wakes on KEY0/KEY1 (via `handle_buttons` bumping `last_activity_us`), `bt_is_connected()` rising edge, or any change in the `interrupt_in_data[0..9]` hash. The dim tier uses `flush_fb_raw()` (the chrome-less variant) since there's no nav target while asleep. **Why this shape:** the SH1107 contrast register has a heavily non-linear perceptual curve on the Waveshare panel — even `0x02` looks ~90 % as bright as `0xFF`. The only reliable "dim" available is rendering fewer pixels.
|
||||
|
||||
**Multi-slot pairing (Phase G):** Storage is two-tier:
|
||||
|
||||
- **Link keys** stay in BTstack's TLV NVM (4 slots, unchanged from upstream).
|
||||
@@ -136,6 +138,8 @@ When asked to modify behavior, the *first* file to read is usually one of:
|
||||
|
||||
- New BT pairing / connection state behavior → `src/bt.cpp` (HCI + L2CAP event handlers).
|
||||
- New OLED screen or change to existing one → `src/oled.cpp`.
|
||||
- New diagnostic counter on the OLED Diagnostics screen → bump `kNumDiagRows` in `src/oled.cpp` and add a `case` to `format_diag_row()` (single switch, one row per case). The screen scrolls automatically; no D-pad wiring needed. Counters that need rate-per-second arithmetic should be sampled in `sample_diag_rates()` and read from `g_diag_rates`. Counter globals themselves typically live in `src/main.cpp` next to `g_bt_31_packets` etc. with `extern` declarations near the top of `src/oled.cpp`.
|
||||
- Host-side diagnostics → `scripts/mic_diag.sh` (Linux only, reads `/dev/hidraw` directly). Subcommands: `status` / `capture [secs]` / `watch` / `bt-trace`. `bt-trace` reads the firmware's `0xFD` vendor feature report (defined in `src/cmd.cpp`'s `tud_hid_get_report_cb`), which currently exposes BT-input counters + the host-output trigger-flow counters. To add a new counter visible to `bt-trace`: extend the `0xFD` payload in `src/cmd.cpp` (bump `want`, write at the new offset), bump `IOCTL_SIZE` in `bt_trace()` of the script, and add the field to its `decode()` dict. The `0xFD` report ID is not declared in the HID descriptor (Linux hidraw ioctls don't enforce that; WebHID would reject undeclared IDs, which is why config goes through `0xF6`).
|
||||
- New persistent config field → `src/config.h` (struct), `src/config.cpp:config_valid()` (defaults + clamping), `src/oled.cpp:format_settings_item()` (UI), `src/oled.cpp:settings_adjust()` (D-pad ▶◀ behavior). Update `CHANGELOG.md`.
|
||||
- USB descriptor or interface change → `src/usb_descriptors.cpp` + `src/tusb_config.h`.
|
||||
- Audio / haptic path → `src/audio.cpp`. **Don't** add stack arrays sized smaller than the resampler / Opus expects (this is the C1 bug that caused the long-standing "audio stuttering" issue — fix landed in upstream `5b04cbd`, but the lesson stands).
|
||||
|
||||
@@ -46,7 +46,7 @@ This project enables the Raspberry Pi Pico2W to function as a Bluetooth bridge f
|
||||
- **4-slot persistent multi-controller pairing** — bond up to four DualSenses, switch between them from the OLED, slot 0 reconnects automatically on boot
|
||||
- **Lightbar color picker** with 4 user favorite slots + breathing / rainbow / fade effect presets
|
||||
- **Persistent settings menu** for the 8 firmware config fields (haptics gain, speaker volume, polling rate, etc.) with hold-to-confirm Reset and Wipe-all-slots actions
|
||||
- **OLED brightness control + auto-dim** after 5 min idle (extends OLED life)
|
||||
- **OLED idle power ladder** — manual brightness cycle (KEY1 long-press), automatic deep-dim with a small breathing dot at 2 min idle, full display-off at 15 min idle. Wakes instantly on button, controller pair, or input. Real burn-in protection, not just a contrast tweak.
|
||||
- **Soft-reboot** without unplugging USB via DS5 `PS + Mute` hold (works headless) or **KEY0 + KEY1 held together for 1 s** on the OLED add-on (replaces the older KEY0 double-click gesture, which was easy to fire by accident while paging quickly)
|
||||
- **Audit pass on the core bridge** — critical stack-overflow fix in the audio path (resolves long-standing "audio stuttering"), security hardening, watchdog, length validation across HID/L2CAP boundaries (see [CHANGELOG.md](./CHANGELOG.md))
|
||||
|
||||
@@ -144,6 +144,7 @@ To opt out at build time, configure with `-DENABLE_BATT_LED=OFF`. Default is ON.
|
||||
|
||||
- Overclocking to 320 MHz @ 1.20 V is **required** for stable BT pairing. Dropping voltage to 1.10 V or clock to stock breaks the CYW43 PIO SPI bus and BT stops working. A small heatsink on the RP2350 is recommended for sustained gameplay.
|
||||
- HD haptics may not fire in every game on Linux + Steam; this is game-side (some titles only send HD-haptic audio under Windows-specific APIs). Tested working in Spider-Man Remastered; not delivered in Ghost of Tsushima — same firmware, same controller.
|
||||
- **DualSense microphone does not work over the Bluetooth pairing.** This is a Sony / DS5-firmware-side limitation also documented in the upstream Linux kernel driver (`drivers/hid/hid-playstation.c` line ~1509: *"Bluetooth audio is currently not supported"*). The mic works fine when the controller is connected directly to the host via USB-C. See [BLUETOOTH_AUDIO_NOTES.md](./BLUETOOTH_AUDIO_NOTES.md) for the full investigation log + what's already wired firmware-side if a future contributor cracks the BT-side trigger.
|
||||
|
||||
## Performance / Overclocking
|
||||
|
||||
@@ -185,6 +186,34 @@ Build flags worth knowing:
|
||||
- `-DENABLE_SERIAL=ON` — route printf to USB CDC for debugging (default OFF; releases UART for production builds).
|
||||
- `-DPICO_W_BUILD=ON` — build for the original Pico W (drops audio, lowers clock). Default targets Pico 2 W.
|
||||
|
||||
## Diagnostics & debug tooling
|
||||
|
||||
Two ways to triage bridge issues — on-device via the OLED Diagnostics screen, and host-side via `scripts/mic_diag.sh` (Linux). The host-side path is faster: no screen-switching, no flash cycle, runs while the controller is in active use.
|
||||
|
||||
```
|
||||
# One-shot snapshot — is the dongle on USB? Did ALSA enumerate it? Is the
|
||||
# capture stream live? Is a controller currently paired?
|
||||
scripts/mic_diag.sh status
|
||||
|
||||
# 3-second arecord on the mic IN endpoint — reports peak / RMS / non-zero
|
||||
# count so we can tell "stream is silent" from "stream is producing audio".
|
||||
scripts/mic_diag.sh capture 3
|
||||
|
||||
# Same as `status` but in a loop, prints only on state change. Useful for
|
||||
# catching the exact second pairing completes or audio streams open / close.
|
||||
scripts/mic_diag.sh watch
|
||||
|
||||
# Live read of the firmware's 0xFD vendor feature report (via /dev/hidraw):
|
||||
# BT input counts + rates, last seen non-0x31 IDs, byte prefixes, AND the
|
||||
# trigger-flow counters (host 0x02 received / with AllowTriggerFFB set /
|
||||
# forwarded to BT). bt-trace prints a verdict — "host driver isn't sending
|
||||
# trigger Allow bits" vs "forwarded but controller didn't actuate" — which
|
||||
# is what would otherwise need a USB protocol analyzer.
|
||||
scripts/mic_diag.sh bt-trace
|
||||
```
|
||||
|
||||
Originally written to triage the parked DS5 BT-microphone investigation (see [BLUETOOTH_AUDIO_NOTES.md](./BLUETOOTH_AUDIO_NOTES.md)). The `0xFD` feature report and `bt-trace` decoder now also carry the trigger-flow counters added for [issue #3](https://github.com/MarcelineVPQ/DS5Dongle-OLED-Edition/issues/3) (missing adaptive trigger tension in some games).
|
||||
|
||||
## OLED Display Add-on (optional)
|
||||
|
||||
If you plug a [Waveshare Pico-OLED-1.3](#hardware) onto the Pico2W's headers, the firmware drives it automatically as a live status display. No configuration needed — the firmware no-ops gracefully when no OLED is present.
|
||||
@@ -248,7 +277,9 @@ Live render of the touchpad surface. Dots appear at current finger positions; th
|
||||
|
||||
#### 7. Diagnostics
|
||||
|
||||
Uptime, BT state, USB-audio frames/sec, BT 0x32 packets/sec, and HCI error counter — live values for verifying the audio path is moving bytes without needing a UART cable.
|
||||
Scrollable list of live counters — uptime, BT state, host → BT trigger flow (`host02` / `trig` / `tx`), BT 0x31 input rate, USB audio frames/sec, BT 0x32 packets/sec, and parked mic-investigation counters at the bottom. Controller D-pad ▲/▼ scrolls; tiny `^` / `v` glyphs at the right edge mark "more above/below." Read-only, so no cursor. Useful for verifying the bridge is moving bytes without needing a UART cable.
|
||||
|
||||
The same counters are also exported on HID feature report `0xFD` for host-side tooling — see `scripts/mic_diag.sh bt-trace` below.
|
||||
|
||||
<img src="./assets/oled/oled_sc07.jpg" alt="Diagnostics screen on the OLED" width="420">
|
||||
|
||||
|
||||
Executable
+239
@@ -0,0 +1,239 @@
|
||||
#!/usr/bin/env bash
|
||||
# Mic-path host-side diagnostic for the DS5Dongle (OLED Edition).
|
||||
#
|
||||
# Subcommands:
|
||||
# status — one-shot snapshot of dongle USB / ALSA / capture stream state.
|
||||
# Prints whether the dongle enumerated, what ALSA card # it
|
||||
# took, the capture stream's current alt setting + sync mode,
|
||||
# and whether a paired DualSense is reachable.
|
||||
# capture — runs a 3-second arecord on the mic IN endpoint, reports
|
||||
# ALSA result code, captured byte count, and a non-silence
|
||||
# indicator (peak abs sample value via Python's wave module).
|
||||
# Tells us in one shot whether the firmware is producing
|
||||
# actual isoc-IN data and whether anything audio-like is
|
||||
# showing up.
|
||||
# watch — loops `status` every 2 seconds, prints only on changes —
|
||||
# useful for catching the moment pairing completes or the
|
||||
# arecord stream opens/closes.
|
||||
#
|
||||
# Why a script: lets the assistant query mic-path state directly from
|
||||
# the host rather than waiting for the user to relay OLED counters
|
||||
# through chat, which dominated the early Phase-3 debugging time.
|
||||
#
|
||||
# Requirements (all already installed on the user's machine):
|
||||
# - arecord (alsa-utils)
|
||||
# - lsusb (usbutils)
|
||||
# - python3 (for wave-file stats)
|
||||
|
||||
set -u
|
||||
|
||||
VID=054c
|
||||
PID=0ce6
|
||||
DEV_NAME_RE='DualSense Wireless Controller'
|
||||
|
||||
find_card() {
|
||||
arecord -l 2>/dev/null | awk -v re="$DEV_NAME_RE" '
|
||||
$0 ~ re {
|
||||
for (i = 1; i <= NF; i++) {
|
||||
if ($i == "card") { gsub(":", "", $(i+1)); print $(i+1); exit }
|
||||
}
|
||||
}'
|
||||
}
|
||||
|
||||
show_status() {
|
||||
local card
|
||||
card="$(find_card)"
|
||||
|
||||
# USB layer — is the device visible?
|
||||
if lsusb -d "${VID}:${PID}" >/dev/null 2>&1; then
|
||||
printf 'usb: present (%s:%s)\n' "$VID" "$PID"
|
||||
else
|
||||
printf 'usb: NOT FOUND — is the dongle plugged in?\n'
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ -z "$card" ]]; then
|
||||
printf 'alsa: dongle is on USB but not exposed as an audio card\n'
|
||||
return 1
|
||||
fi
|
||||
printf 'alsa: card %s\n' "$card"
|
||||
|
||||
# Capture stream details (interface 2 alt 1 mic-IN endpoint)
|
||||
if [[ -r "/proc/asound/card${card}/stream0" ]]; then
|
||||
# Grep just the Capture block so we see status + altset + endpoint
|
||||
awk '/^Capture:/,0' "/proc/asound/card${card}/stream0" | head -10 | sed 's/^/ /'
|
||||
else
|
||||
printf ' (no /proc/asound/card%s/stream0 — older kernel?)\n' "$card"
|
||||
fi
|
||||
}
|
||||
|
||||
run_capture() {
|
||||
local card secs="${1:-3}"
|
||||
card="$(find_card)"
|
||||
if [[ -z "$card" ]]; then
|
||||
printf 'no dongle capture device found\n'
|
||||
return 1
|
||||
fi
|
||||
|
||||
local tmp
|
||||
tmp="$(mktemp -t mic_diag.XXXXXX.wav)"
|
||||
printf 'capturing %ss from card %s into %s ...\n' "$secs" "$card" "$tmp"
|
||||
|
||||
local err
|
||||
err="$(arecord -q -D "plughw:${card},0" -f S16_LE -c 2 -r 48000 -d "$secs" "$tmp" 2>&1)"
|
||||
local rc=$?
|
||||
if (( rc != 0 )); then
|
||||
printf 'arecord exit=%d: %s\n' "$rc" "$err"
|
||||
rm -f "$tmp"
|
||||
return "$rc"
|
||||
fi
|
||||
|
||||
# Stats via Python — peak abs sample is enough to distinguish "stream
|
||||
# produced silence" from "stream produced actual audio".
|
||||
python3 - "$tmp" <<'PY'
|
||||
import sys, wave, struct
|
||||
path = sys.argv[1]
|
||||
with wave.open(path, 'rb') as w:
|
||||
nframes = w.getnframes()
|
||||
sw = w.getsampwidth()
|
||||
ch = w.getnchannels()
|
||||
fr = w.getframerate()
|
||||
raw = w.readframes(nframes)
|
||||
nsamples = nframes * ch
|
||||
fmt = '<' + ('h' * nsamples)
|
||||
data = struct.unpack(fmt, raw)
|
||||
peak = max(abs(s) for s in data) if data else 0
|
||||
nonzero = sum(1 for s in data if s != 0)
|
||||
rms = (sum(s*s for s in data) / max(len(data), 1)) ** 0.5
|
||||
print(f'wav: {nframes} frames, {ch} ch, {sw*8}-bit, {fr} Hz')
|
||||
print(f'samples: nonzero={nonzero}/{nsamples} peak={peak} rms={rms:.1f}')
|
||||
if peak == 0:
|
||||
print('verdict: STREAM IS SILENT — firmware not producing isoc-IN data')
|
||||
elif peak < 100:
|
||||
print('verdict: extremely quiet — possibly DC offset only')
|
||||
else:
|
||||
print('verdict: AUDIO PRESENT')
|
||||
PY
|
||||
rm -f "$tmp"
|
||||
}
|
||||
|
||||
watch_status() {
|
||||
local prev=""
|
||||
while :; do
|
||||
local now
|
||||
now="$(show_status 2>&1)"
|
||||
if [[ "$now" != "$prev" ]]; then
|
||||
printf '\n=== %s ===\n%s\n' "$(date '+%H:%M:%S')" "$now"
|
||||
prev="$now"
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
}
|
||||
|
||||
bt_trace() {
|
||||
# Query the firmware's 0xFD vendor feature report via /dev/hidraw.
|
||||
# 0xFD carries two sections:
|
||||
# Section 1 (bytes 0..31) — mic-investigation: BT 0x31 / non-0x31
|
||||
# counts, byte[2] OR mask, frame prefixes. Originally used to
|
||||
# locate the mic stream; kept for any future BT-input triage.
|
||||
# Section 2 (bytes 32..43) — host -> dongle -> BT trigger flow
|
||||
# counters (issue #3): host 0x02 OUT received total, of those
|
||||
# where AllowRight/LeftTriggerFFB was set, and of those forwarded
|
||||
# as BT 0x31 sub-0x10. Lets the user triage adaptive-trigger
|
||||
# issues without needing an OLED in the loop.
|
||||
# The ioctl buffer is 45 bytes (44 payload + 1 byte that the kernel
|
||||
# fills with the report ID).
|
||||
python3 - <<'PY'
|
||||
import fcntl, glob, struct, sys, time
|
||||
|
||||
VID, PID = 0x054c, 0x0ce6
|
||||
IOCTL_SIZE = 45 # 1 byte report ID + 44 bytes firmware payload
|
||||
|
||||
def find_dongle():
|
||||
for path in sorted(glob.glob('/dev/hidraw*')):
|
||||
try:
|
||||
f = open(path, 'rb+')
|
||||
buf = bytearray(IOCTL_SIZE); buf[0] = 0xFD
|
||||
ioctl_num = (3 << 30) | (IOCTL_SIZE << 16) | (ord('H') << 8) | 0x07
|
||||
try:
|
||||
fcntl.ioctl(f, ioctl_num, buf)
|
||||
return f
|
||||
except OSError:
|
||||
f.close()
|
||||
except (OSError, PermissionError):
|
||||
pass
|
||||
return None
|
||||
|
||||
f = find_dongle()
|
||||
if f is None:
|
||||
print('no dongle found (or no /dev/hidraw permission)')
|
||||
sys.exit(1)
|
||||
|
||||
def query():
|
||||
buf = bytearray(IOCTL_SIZE); buf[0] = 0xFD
|
||||
ioctl_num = (3 << 30) | (IOCTL_SIZE << 16) | (ord('H') << 8) | 0x07
|
||||
fcntl.ioctl(f, ioctl_num, buf)
|
||||
# Kernel prepends the report ID at byte 0; firmware payload starts at byte 1.
|
||||
return bytes(buf[1:])
|
||||
|
||||
def decode(b):
|
||||
return {
|
||||
'bt31': struct.unpack('<I', b[0:4])[0],
|
||||
'btoth': struct.unpack('<I', b[4:8])[0],
|
||||
'other_id': b[8],
|
||||
'other_or': b[9],
|
||||
'b2_or': b[10],
|
||||
'b2_last': b[11],
|
||||
'lmin': struct.unpack('<H', b[12:14])[0],
|
||||
'lmax': struct.unpack('<H', b[14:16])[0],
|
||||
'othpfx': b[16:24].hex(),
|
||||
'anypfx': b[24:32].hex(),
|
||||
'host02': struct.unpack('<I', b[32:36])[0] if len(b) >= 36 else 0,
|
||||
'host02_trig':struct.unpack('<I', b[36:40])[0] if len(b) >= 40 else 0,
|
||||
'host02_tx': struct.unpack('<I', b[40:44])[0] if len(b) >= 44 else 0,
|
||||
}
|
||||
|
||||
s1 = query(); time.sleep(1.0); s2 = query()
|
||||
d1 = decode(s1); d2 = decode(s2)
|
||||
|
||||
# Mic-investigation section
|
||||
bt31_rate = d2['bt31'] - d1['bt31']
|
||||
btoth_rate = d2['btoth'] - d1['btoth']
|
||||
print('-- BT input (mic investigation legacy) --')
|
||||
print(f'rates: 0x31={bt31_rate}/s, non-0x31={btoth_rate}/s')
|
||||
print(f'len range: {d2["lmin"]}-{d2["lmax"]} bytes')
|
||||
print(f'byte[2] OR mask across 0x31 frames: 0x{d2["b2_or"]:02X} last=0x{d2["b2_last"]:02X}')
|
||||
print(f'non-0x31 report IDs: OR mask=0x{d2["other_or"]:02X} most recent=0x{d2["other_id"]:02X}')
|
||||
print(f'last non-0x31 prefix (data[0..7]): {d2["othpfx"]}')
|
||||
print(f'last ANY frame (data[0..7]): {d2["anypfx"]}')
|
||||
|
||||
# Trigger-flow section
|
||||
o02_rate = d2['host02'] - d1['host02']
|
||||
trig_rate = d2['host02_trig'] - d1['host02_trig']
|
||||
tx_rate = d2['host02_tx'] - d1['host02_tx']
|
||||
print()
|
||||
print('-- Host -> dongle -> BT trigger flow (issue #3) --')
|
||||
print(f'host 0x02 OUT: total={d2["host02"]} ({o02_rate}/s)')
|
||||
print(f' w/ AllowTrigFFB: total={d2["host02_trig"]} ({trig_rate}/s)')
|
||||
print(f' forwarded to BT: total={d2["host02_tx"]} ({tx_rate}/s)')
|
||||
if d2['host02'] > 0 and d2['host02_trig'] == 0:
|
||||
print('verdict: host is sending 0x02 reports but never sets Allow*TriggerFFB.')
|
||||
print(' The host driver is not requesting adaptive trigger effects.')
|
||||
elif d2['host02_trig'] > 0 and d2['host02_tx'] < d2['host02_trig']:
|
||||
print('verdict: trigger Allow bits are set but some reports are not reaching BT.')
|
||||
print(' Likely the speaker-active gate in main.cpp swallowed them.')
|
||||
elif d2['host02_trig'] > 0:
|
||||
print('verdict: full chain reached the controller. Tension still missing -> Sony BT limit.')
|
||||
PY
|
||||
}
|
||||
|
||||
case "${1:-status}" in
|
||||
status) show_status ;;
|
||||
capture) shift; run_capture "${1:-3}" ;;
|
||||
watch) watch_status ;;
|
||||
bt-trace) bt_trace ;;
|
||||
*)
|
||||
printf 'usage: %s {status|capture [secs]|watch|bt-trace}\n' "$0" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
@@ -25,6 +25,14 @@
|
||||
// #define VOLUME_GAIN 2
|
||||
// #define BUFFER_LENGTH 48
|
||||
|
||||
// DualSense microphone, ported from awalol/DS5Dongle's `mic` branch.
|
||||
// The DS5 sends mic audio as Opus packets embedded in BT input report
|
||||
// 0x31 when bit 1 of byte 2 is set; payload is 71 bytes of Opus at
|
||||
// offset 4, decoded to mono 48 kHz 10 ms frames (480 samples).
|
||||
#define MIC_CHANNELS 1
|
||||
#define MIC_FRAMES 480
|
||||
#define MIC_OPUS_SIZE 71
|
||||
|
||||
using std::clamp;
|
||||
using std::max;
|
||||
|
||||
@@ -37,6 +45,21 @@ queue_t audio_fifo;
|
||||
static uint8_t opus_buf[200];
|
||||
critical_section_t opus_cs;
|
||||
|
||||
// Mic ingress queue — filled from on_bt_data() (BT poll, core0), drained
|
||||
// at the top of audio_loop() on core0. The decoder is single-threaded
|
||||
// (core0 only), so no critical section is needed around it.
|
||||
queue_t mic_fifo;
|
||||
struct mic_element { uint8_t data[MIC_OPUS_SIZE]; };
|
||||
static OpusDecoder *mic_decoder = nullptr;
|
||||
static volatile uint32_t g_mic_frames = 0;
|
||||
static volatile int32_t g_mic_last_decoded = 0; // opus_decode return value
|
||||
static volatile uint16_t g_mic_last_want = 0; // bytes we asked TinyUSB to send
|
||||
static volatile uint16_t g_mic_last_wrote = 0; // bytes TinyUSB accepted
|
||||
uint32_t audio_mic_frames() { return g_mic_frames; }
|
||||
int32_t audio_mic_last_decoded() { return g_mic_last_decoded; }
|
||||
uint16_t audio_mic_last_want() { return g_mic_last_want; }
|
||||
uint16_t audio_mic_last_wrote() { return g_mic_last_wrote; }
|
||||
|
||||
struct audio_raw_element {
|
||||
float data[512 * 2];
|
||||
};
|
||||
@@ -73,7 +96,51 @@ uint8_t audio_peak_haptic() {
|
||||
return (uint8_t)(v >> 7);
|
||||
}
|
||||
|
||||
// Most-recent Opus TOC byte (first byte of the packet). Used by the OLED
|
||||
// Diagnostics screen to decode the frame's bandwidth + duration config
|
||||
// without serial.
|
||||
static volatile uint8_t g_mic_toc = 0;
|
||||
uint8_t audio_mic_last_toc() { return g_mic_toc; }
|
||||
|
||||
// Push a 71-byte Opus mic packet from the BT handler into the mic_fifo.
|
||||
// Called from src/main.cpp's on_bt_data() when the DS5 sends a mic-tagged
|
||||
// 0x31 input report. Drops the oldest queued packet if the FIFO is full —
|
||||
// preferring fresh audio over backlog on overload.
|
||||
void mic_add_queue(const uint8_t *data) {
|
||||
static mic_element packet{};
|
||||
memcpy(packet.data, data, MIC_OPUS_SIZE);
|
||||
g_mic_toc = data[0]; // first byte of the Opus packet
|
||||
if (queue_is_full(&mic_fifo)) queue_try_remove(&mic_fifo, NULL);
|
||||
queue_try_add(&mic_fifo, &packet);
|
||||
}
|
||||
|
||||
void audio_loop() {
|
||||
// Mic-in path: pull one Opus packet from the BT-side FIFO, decode to
|
||||
// mono PCM, duplicate to stereo (our UAC1 endpoint declares 2 channels),
|
||||
// push to the host via tud_audio_write. Runs once per loop iteration so
|
||||
// it keeps up with the ~100 Hz arrival rate of mic-tagged BT frames.
|
||||
if (mic_decoder != nullptr) {
|
||||
static mic_element packet{};
|
||||
if (queue_try_remove(&mic_fifo, &packet)) {
|
||||
static int16_t mono[MIC_FRAMES];
|
||||
const int decoded = opus_decode(mic_decoder, packet.data,
|
||||
MIC_OPUS_SIZE, mono, MIC_FRAMES, 0);
|
||||
g_mic_last_decoded = decoded; // observed in OLED Diag
|
||||
if (decoded > 0) {
|
||||
static int16_t stereo[MIC_FRAMES * 2];
|
||||
for (int i = 0; i < decoded; i++) {
|
||||
stereo[i * 2] = mono[i];
|
||||
stereo[i * 2 + 1] = mono[i];
|
||||
}
|
||||
const uint16_t want = (uint16_t)(decoded * 2 * sizeof(int16_t));
|
||||
const uint16_t wrote = tud_audio_write(stereo, want);
|
||||
g_mic_last_want = want;
|
||||
g_mic_last_wrote = wrote;
|
||||
g_mic_frames++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 1. 读取 USB 音频数据
|
||||
if (!tud_audio_available()) return;
|
||||
|
||||
@@ -253,6 +320,16 @@ void audio_init() {
|
||||
critical_section_init(&opus_cs);
|
||||
multicore_launch_core1_with_stack(core1_entry, audio_core1_stack, sizeof(audio_core1_stack));
|
||||
#endif
|
||||
|
||||
// Mic path: queue + decoder live on core0 (audio_loop), separate from
|
||||
// the core1 speaker encoder. Mic Opus is mono / 48 kHz / 10 ms frames.
|
||||
queue_init(&mic_fifo, sizeof(mic_element), 2);
|
||||
int dec_error = 0;
|
||||
mic_decoder = opus_decoder_create(48000, MIC_CHANNELS, &dec_error);
|
||||
if (dec_error != 0 || mic_decoder == nullptr) {
|
||||
printf("[Audio] OpusDecoder create failed (err=%d)\n", dec_error);
|
||||
mic_decoder = nullptr; // ensure audio_loop's null-guard short-circuits
|
||||
}
|
||||
}
|
||||
|
||||
static OpusEncoder *encoder;
|
||||
|
||||
+10
@@ -21,5 +21,15 @@ uint8_t audio_peak_haptic(); // 0..255, decays on read
|
||||
// Byte-flow counters for the Diagnostics screen + web emulator.
|
||||
uint32_t audio_usb_frames();
|
||||
uint32_t audio_bt_packets();
|
||||
uint32_t audio_mic_frames(); // count of mic Opus frames decoded + written
|
||||
int32_t audio_mic_last_decoded(); // last opus_decode return — neg = error, 480 = OK
|
||||
uint16_t audio_mic_last_want(); // bytes asked of tud_audio_write
|
||||
uint16_t audio_mic_last_wrote(); // bytes TinyUSB FIFO actually accepted
|
||||
uint8_t audio_mic_last_toc(); // first byte of last Opus packet (frame config)
|
||||
|
||||
// Called from on_bt_data() in main.cpp when the DS5 sends a mic-tagged
|
||||
// 0x31 input report. Buffer must point at MIC_OPUS_SIZE (71) bytes of
|
||||
// Opus payload.
|
||||
void mic_add_queue(const uint8_t *data);
|
||||
|
||||
#endif //DS5_BRIDGE_AUDIO_H
|
||||
+82
-1
@@ -48,6 +48,25 @@ uint16_t cpu_temp_raw_smoothed() {
|
||||
return (uint16_t)(ema + 0.5f);
|
||||
}
|
||||
|
||||
// Mic-debug globals (defined in main.cpp). File-scope extern so the
|
||||
// linker resolves them once and cmd.cpp's 0xFD handler reads the same
|
||||
// memory main.cpp writes to.
|
||||
extern volatile uint32_t g_bt_31_packets;
|
||||
extern volatile uint32_t g_bt_other_packets;
|
||||
extern volatile uint8_t g_last_other_id;
|
||||
extern volatile uint8_t g_other_id_or;
|
||||
extern volatile uint8_t g_31_b2_or;
|
||||
extern volatile uint8_t g_last_31_b2;
|
||||
extern volatile uint16_t g_31_len_min;
|
||||
extern volatile uint16_t g_31_len_max;
|
||||
extern volatile uint8_t g_last_other_prefix[8];
|
||||
extern volatile uint8_t g_last_any_prefix[16];
|
||||
extern volatile uint16_t g_longest_len;
|
||||
extern volatile uint8_t g_longest_frame[80];
|
||||
extern volatile uint32_t g_host_out02_total;
|
||||
extern volatile uint32_t g_host_out02_trig_allow;
|
||||
extern volatile uint32_t g_host_out02_to_bt;
|
||||
|
||||
bool is_pico_cmd(uint8_t report_id) {
|
||||
if (report_id == 0xf6 ||
|
||||
report_id == 0xf7 ||
|
||||
@@ -55,7 +74,9 @@ bool is_pico_cmd(uint8_t report_id) {
|
||||
report_id == 0xf9 ||
|
||||
report_id == 0xfa ||
|
||||
report_id == 0xfb ||
|
||||
report_id == 0xfc
|
||||
report_id == 0xfc ||
|
||||
report_id == 0xfd || // mic-debug counters
|
||||
report_id == 0xfe // mic-debug longest-frame dump
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
@@ -161,6 +182,66 @@ uint16_t pico_cmd_get(uint8_t report_id, uint8_t *buffer, uint16_t reqlen) {
|
||||
memcpy(buffer + 9, &temp_raw, 2);
|
||||
return want;
|
||||
}
|
||||
if (report_id == 0xfd) {
|
||||
// Bridge-diagnostics feature report. 44-byte payload.
|
||||
// Section 1: mic-investigation counters (original 0..31).
|
||||
// [0..3] uint32 BT 0x31 input report count
|
||||
// [4..7] uint32 BT non-0x31 input report count
|
||||
// [8] uint8 last non-0x31 report ID seen
|
||||
// [9] uint8 OR mask of all non-0x31 report IDs seen
|
||||
// [10] uint8 OR mask of byte[2] across all 0x31 frames
|
||||
// [11] uint8 last value of byte[2] in a 0x31 frame
|
||||
// [12..13] uint16 min frame length seen
|
||||
// [14..15] uint16 max frame length seen
|
||||
// [16..23] uint8[8] first 8 bytes of last non-0x31 frame
|
||||
// [24..31] uint8[8] first 8 bytes of most recent ANY frame
|
||||
// Section 2: trigger-flow counters (issue #3 triage).
|
||||
// [32..35] uint32 host 0x02 OUT reports received total
|
||||
// [36..39] uint32 ...of those, with Allow*TriggerFFB set
|
||||
// [40..43] uint32 ...forwarded as BT 0x31 sub-0x10
|
||||
constexpr uint16_t want = 44;
|
||||
for (uint16_t i = 0; i < want && i < reqlen; i++) buffer[i] = 0;
|
||||
|
||||
const uint32_t bt31 = g_bt_31_packets;
|
||||
const uint32_t btother = g_bt_other_packets;
|
||||
const uint16_t lmin = g_31_len_min == 0xFFFF ? 0 : g_31_len_min;
|
||||
const uint16_t lmax = g_31_len_max;
|
||||
|
||||
memcpy(buffer + 0, &bt31, 4);
|
||||
memcpy(buffer + 4, &btother, 4);
|
||||
buffer[8] = g_last_other_id;
|
||||
buffer[9] = g_other_id_or;
|
||||
buffer[10] = g_31_b2_or;
|
||||
buffer[11] = g_last_31_b2;
|
||||
memcpy(buffer + 12, &lmin, 2);
|
||||
memcpy(buffer + 14, &lmax, 2);
|
||||
for (int i = 0; i < 8 && (16 + i) < reqlen; i++) buffer[16 + i] = g_last_other_prefix[i];
|
||||
for (int i = 0; i < 8 && (24 + i) < reqlen; i++) buffer[24 + i] = g_last_any_prefix[i];
|
||||
|
||||
const uint32_t out02 = g_host_out02_total;
|
||||
const uint32_t out02_t = g_host_out02_trig_allow;
|
||||
const uint32_t out02_b = g_host_out02_to_bt;
|
||||
if ((32 + 4) <= reqlen) memcpy(buffer + 32, &out02, 4);
|
||||
if ((36 + 4) <= reqlen) memcpy(buffer + 36, &out02_t, 4);
|
||||
if ((40 + 4) <= reqlen) memcpy(buffer + 40, &out02_b, 4);
|
||||
return (reqlen < want) ? reqlen : want;
|
||||
}
|
||||
if (report_id == 0xfe) {
|
||||
// 0xFE: full content of the LONGEST 0x31 frame seen. Bytes 0-1
|
||||
// = length (uint16 LE), bytes 2+ = the captured frame bytes.
|
||||
constexpr uint16_t want = 82; // 2 length + 80 frame bytes
|
||||
const uint16_t lim = (reqlen < want) ? reqlen : want;
|
||||
for (uint16_t i = 0; i < lim; i++) buffer[i] = 0;
|
||||
const uint16_t llen = g_longest_len;
|
||||
if (lim >= 2) {
|
||||
buffer[0] = (uint8_t)(llen & 0xFF);
|
||||
buffer[1] = (uint8_t)((llen >> 8) & 0xFF);
|
||||
}
|
||||
for (uint16_t i = 0; i < 80 && (i + 2) < lim; i++) {
|
||||
buffer[2 + i] = g_longest_frame[i];
|
||||
}
|
||||
return lim;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
+93
-6
@@ -30,6 +30,50 @@ int reportSeqCounter = 0;
|
||||
uint8_t packetCounter = 0;
|
||||
bool spk_active = false;
|
||||
|
||||
// Mic-debug instrumentation: count every 0x31 BT input report regardless
|
||||
// of mic-tag bit, accumulate OR-mask of every byte-2 value seen (tells us
|
||||
// which bits ever fire) and remember the last byte-2 value. Also track
|
||||
// observed frame-length range. Surfaced on the OLED Diagnostics screen.
|
||||
volatile uint32_t g_bt_31_packets = 0;
|
||||
volatile uint32_t g_bt_other_packets = 0;
|
||||
volatile uint8_t g_last_other_id = 0;
|
||||
volatile uint8_t g_other_id_or = 0;
|
||||
volatile uint8_t g_last_31_b2 = 0;
|
||||
volatile uint8_t g_31_b2_or = 0;
|
||||
volatile uint16_t g_31_len_min = 0xFFFF;
|
||||
volatile uint16_t g_31_len_max = 0;
|
||||
volatile uint8_t g_mic_prefix[6] = {0};
|
||||
volatile uint8_t g_last_other_prefix[8] = {0};
|
||||
volatile uint8_t g_last_any_prefix[16] = {0};
|
||||
volatile uint16_t g_longest_len = 0;
|
||||
volatile uint8_t g_longest_frame[80] = {0};
|
||||
uint32_t bt_31_packet_count() { return g_bt_31_packets; }
|
||||
uint8_t bt_31_last_byte2() { return g_last_31_b2; }
|
||||
uint8_t bt_31_b2_or_mask() { return g_31_b2_or; }
|
||||
uint16_t bt_31_len_min() { return g_31_len_min == 0xFFFF ? 0 : g_31_len_min; }
|
||||
uint16_t bt_31_len_max() { return g_31_len_max; }
|
||||
void bt_31_mic_prefix(uint8_t out[6]) {
|
||||
for (int i = 0; i < 6; i++) out[i] = g_mic_prefix[i];
|
||||
}
|
||||
|
||||
// Trigger-flow diagnostics. Counts host → dongle → BT path for adaptive
|
||||
// trigger effects. Lets us tell which link in the chain breaks when games
|
||||
// like Death Stranding 2 don't produce trigger tension via the dongle:
|
||||
// out02_total - every 0x02 HID OUT report received from host
|
||||
// out02_trig_allow - of those, how many set AllowRight/LeftTriggerFFB
|
||||
// (valid_flag0 bits 2 & 3) — i.e. the host actually
|
||||
// told us "apply trigger FFB"
|
||||
// out02_to_bt - 0x02 reports that we forwarded to the controller as
|
||||
// a BT 0x31 sub-0x10 packet (gated off when speaker is
|
||||
// active; audio.cpp's 0x36 path carries state then)
|
||||
// Surfaced on the OLED Diagnostics screen.
|
||||
volatile uint32_t g_host_out02_total = 0;
|
||||
volatile uint32_t g_host_out02_trig_allow = 0;
|
||||
volatile uint32_t g_host_out02_to_bt = 0;
|
||||
uint32_t host_out02_total() { return g_host_out02_total; }
|
||||
uint32_t host_out02_trig_allow() { return g_host_out02_trig_allow; }
|
||||
uint32_t host_out02_to_bt() { return g_host_out02_to_bt; }
|
||||
|
||||
uint8_t interrupt_in_data[63] = {
|
||||
0x7f, 0x7d, 0x7f, 0x7e, 0x00, 0x00, 0xa7,
|
||||
0x08, 0x00, 0x00, 0x00, 0x52, 0x43, 0x30, 0x41,
|
||||
@@ -101,6 +145,48 @@ void interrupt_loop() {
|
||||
|
||||
void on_bt_data(CHANNEL_TYPE channel, uint8_t *data, uint16_t len) {
|
||||
// printf("[Main] BT data callback: channel=%u len=%u\n", channel, len);
|
||||
// Track ALL INTERRUPT input reports, not just 0x31. The mic stream
|
||||
// may live on a different report ID — confirmed 2026-05-19 that data[2]
|
||||
// bit 0 (and bit 1) is NOT a mic flag, just the report-type indicator;
|
||||
// every "mic-tagged" frame turned out to be standard input.
|
||||
if (channel == INTERRUPT && len > 1) {
|
||||
if (data[1] == 0x31) g_bt_31_packets++;
|
||||
else {
|
||||
g_bt_other_packets++;
|
||||
g_last_other_id = data[1];
|
||||
g_other_id_or = (uint8_t)(g_other_id_or | data[1]);
|
||||
for (uint16_t i = 0; i < 8 && i < len; i++) {
|
||||
g_last_other_prefix[i] = data[i];
|
||||
}
|
||||
}
|
||||
if (len > 2) {
|
||||
g_last_31_b2 = data[2];
|
||||
g_31_b2_or = (uint8_t)(g_31_b2_or | data[2]);
|
||||
}
|
||||
if (len < g_31_len_min) g_31_len_min = len;
|
||||
if (len > g_31_len_max) g_31_len_max = len;
|
||||
for (uint16_t i = 0; i < 16 && i < len; i++) {
|
||||
g_last_any_prefix[i] = data[i];
|
||||
}
|
||||
|
||||
// Capture the entire content of the longest 0x31 frame we've
|
||||
// seen. Long frames almost certainly carry the mic audio appended
|
||||
// after the standard 63-byte input report — this lets us look
|
||||
// at the trailing bytes directly via 0xFD diagnostic.
|
||||
if (data[1] == 0x31 && len > g_longest_len) {
|
||||
g_longest_len = len;
|
||||
for (uint16_t i = 0; i < 80 && i < len; i++) {
|
||||
g_longest_frame[i] = data[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mic-add tap DISABLED — was decoding standard input (button/stick
|
||||
// bytes) as Opus and producing INT16_MIN garbage on the USB IN
|
||||
// endpoint. Re-enable once we identify the actual mic transport.
|
||||
// (Standard input handling below resumes — Status screen + HID
|
||||
// reports to host need this.)
|
||||
|
||||
if (channel == INTERRUPT && data[1] == 0x31) {
|
||||
if ((data[56] & 1) != (interrupt_in_data[53] & 1)) {
|
||||
set_headset(data[56] & 1);
|
||||
@@ -114,12 +200,6 @@ void on_bt_data(CHANNEL_TYPE channel, uint8_t *data, uint16_t len) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We add the critical section here to avoid any race conditions when writing to the interrupt_in_data buffer,
|
||||
// which is shared between the main loop and this callback.
|
||||
// The critical section ensures that only one thread can access the buffer at a time,
|
||||
// preventing data corruption and ensuring thread safety.
|
||||
// We also set the report_dirty flag to true to indicate that new data is available
|
||||
// and needs to be sent in the next interrupt report.
|
||||
critical_section_enter_blocking(&report_cs);
|
||||
memcpy(interrupt_in_data, data + 3, 63);
|
||||
report_dirty = true;
|
||||
@@ -186,6 +266,12 @@ void tud_hid_set_report_cb(uint8_t itf, uint8_t report_id, hid_report_type_t rep
|
||||
if (report_id == 0) {
|
||||
switch (buffer[0]) {
|
||||
case 0x02: {
|
||||
g_host_out02_total++;
|
||||
// valid_flag0 lives at buffer[1] (right after the 0x02 report id).
|
||||
// Bits 2 & 3 are AllowRight/LeftTriggerFFB.
|
||||
if (bufsize > 1 && (buffer[1] & 0x0C)) {
|
||||
g_host_out02_trig_allow++;
|
||||
}
|
||||
state_update(buffer + 1, bufsize - 1);
|
||||
if (spk_active) {
|
||||
break;
|
||||
@@ -200,6 +286,7 @@ void tud_hid_set_report_cb(uint8_t itf, uint8_t report_id, hid_report_type_t rep
|
||||
// memcpy(outputData + 3, buffer + 1, bufsize - 1);
|
||||
state_set(outputData + 3,sizeof(SetStateData));
|
||||
bt_write(outputData, sizeof(outputData));
|
||||
g_host_out02_to_bt++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
+207
-35
@@ -17,6 +17,17 @@
|
||||
|
||||
extern uint8_t interrupt_in_data[63]; // defined in main.cpp
|
||||
|
||||
// Mic diagnostic counters (defined in main.cpp).
|
||||
extern uint32_t bt_31_packet_count();
|
||||
extern uint32_t host_out02_total();
|
||||
extern uint32_t host_out02_trig_allow();
|
||||
extern uint32_t host_out02_to_bt();
|
||||
extern uint8_t bt_31_last_byte2();
|
||||
extern uint8_t bt_31_b2_or_mask();
|
||||
extern uint16_t bt_31_len_min();
|
||||
extern uint16_t bt_31_len_max();
|
||||
extern void bt_31_mic_prefix(uint8_t out[6]);
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr uint kPinDC = 8;
|
||||
@@ -63,11 +74,22 @@ constexpr int kNumBrightLevels = sizeof(kBrightLevels) / sizeof(kBrightLevels[0]
|
||||
int bright_idx = 0;
|
||||
uint8_t current_contrast = 0xFF;
|
||||
|
||||
// Auto-dim after idle. Tracks last button/input activity.
|
||||
// Auto-dim / auto-off after idle. Tracks last button/input activity.
|
||||
// Tier 1: Active → full brightness (bright_idx).
|
||||
// Tier 2: idle > kAutoDimUs → contrast drops to kDimContrast (deep dim).
|
||||
// Tier 3: idle > kAutoOffUs → SH1107 panel turned fully off (cmd 0xAE)
|
||||
// to prevent OLED burn-in on long unattended sits.
|
||||
// kDimContrast tuned by eye: 0x10 looked like only ~10% reduction on this
|
||||
// panel (contrast-vs-brightness is heavily non-linear near the bottom of
|
||||
// the register range). 0x02 is visibly dim while still legible up close.
|
||||
uint32_t last_activity_us = 0;
|
||||
uint32_t last_input_hash = 0;
|
||||
constexpr uint32_t kAutoDimUs = 5UL * 60UL * 1000000UL; // 5 min
|
||||
constexpr uint8_t kDimContrast = 0x10;
|
||||
constexpr uint32_t kAutoDimUs = 2UL * 60UL * 1000000UL; // 2 min — generous for pairing
|
||||
constexpr uint32_t kAutoOffUs = 15UL * 60UL * 1000000UL; // 15 min
|
||||
constexpr uint8_t kDimContrast = 0x01;
|
||||
enum OledPowerState { OLED_ACTIVE, OLED_DIM, OLED_OFF };
|
||||
OledPowerState oled_power_state = OLED_ACTIVE;
|
||||
bool prev_bt_connected = false;
|
||||
|
||||
// Screen ordering — single source of truth. Reorder by editing this block;
|
||||
// oled_loop's switch and handle_buttons' KEY1 contextual checks use these
|
||||
@@ -193,8 +215,7 @@ void sh1107_init() {
|
||||
// lives near the other text-drawing helpers below.
|
||||
void draw_button_chrome();
|
||||
|
||||
void flush_fb() {
|
||||
draw_button_chrome();
|
||||
void flush_fb_raw() {
|
||||
cmd(0xB0);
|
||||
for (int j = 0; j < kH; j++) {
|
||||
const uint8_t col = kH - 1 - j;
|
||||
@@ -206,6 +227,11 @@ void flush_fb() {
|
||||
}
|
||||
}
|
||||
|
||||
void flush_fb() {
|
||||
draw_button_chrome();
|
||||
flush_fb_raw();
|
||||
}
|
||||
|
||||
void fb_clear() { memset(fb, 0, sizeof(fb)); }
|
||||
|
||||
void px(int x, int y, bool on) {
|
||||
@@ -604,46 +630,130 @@ __attribute__((noinline)) void render_screen_rssi() {
|
||||
flush_fb();
|
||||
}
|
||||
|
||||
__attribute__((noinline)) void render_screen_diag() {
|
||||
fb_clear();
|
||||
// Diagnostics screen state. Read-only viewport that scrolls with controller
|
||||
// D-pad up/down. No cursor — there's nothing to select.
|
||||
int diag_scroll = 0;
|
||||
uint8_t diag_last_dpad = 8; // edge-trigger N/E/S/W like settings_handle_input
|
||||
|
||||
draw_text(kContentX, 0, "Diagnostics");
|
||||
// Per-second rates sampled once per render, shared across format_diag_row's
|
||||
// rate-based rows so they stay in sync.
|
||||
struct DiagRates {
|
||||
uint32_t usb_rate;
|
||||
uint32_t bt_rate;
|
||||
uint32_t mic_rate;
|
||||
uint32_t bt31_rate;
|
||||
};
|
||||
DiagRates g_diag_rates{};
|
||||
|
||||
const uint32_t uptime_s = time_us_32() / 1000000u;
|
||||
const uint32_t h = uptime_s / 3600u;
|
||||
const uint32_t m = (uptime_s / 60u) % 60u;
|
||||
const uint32_t s = uptime_s % 60u;
|
||||
char buf[24];
|
||||
snprintf(buf, sizeof(buf), "Up:%luh %02lum %02lus", (unsigned long)h, (unsigned long)m, (unsigned long)s);
|
||||
draw_text(kContentX, 9, buf);
|
||||
|
||||
// Per-second rates for the audio path counters — recompute every render.
|
||||
static uint32_t prev_us_frames = 0, prev_bt_packets = 0;
|
||||
void sample_diag_rates() {
|
||||
static uint32_t prev_us_frames = 0, prev_bt_packets = 0, prev_mic_frames = 0, prev_bt31 = 0;
|
||||
static uint32_t prev_sample_us = 0;
|
||||
const uint32_t now_us = time_us_32();
|
||||
const uint32_t cur_us_frames = audio_usb_frames();
|
||||
const uint32_t cur_us_frames = audio_usb_frames();
|
||||
const uint32_t cur_bt_packets = audio_bt_packets();
|
||||
uint32_t usb_rate = 0, bt_rate = 0;
|
||||
const uint32_t cur_mic_frames = audio_mic_frames();
|
||||
const uint32_t cur_bt31 = bt_31_packet_count();
|
||||
if (prev_sample_us != 0 && now_us > prev_sample_us) {
|
||||
const uint32_t dt_us = now_us - prev_sample_us;
|
||||
if (dt_us > 0) {
|
||||
usb_rate = (uint32_t)(((uint64_t)(cur_us_frames - prev_us_frames) * 1000000u) / dt_us);
|
||||
bt_rate = (uint32_t)(((uint64_t)(cur_bt_packets - prev_bt_packets) * 1000000u) / dt_us);
|
||||
g_diag_rates.usb_rate = (uint32_t)(((uint64_t)(cur_us_frames - prev_us_frames) * 1000000u) / dt_us);
|
||||
g_diag_rates.bt_rate = (uint32_t)(((uint64_t)(cur_bt_packets - prev_bt_packets) * 1000000u) / dt_us);
|
||||
g_diag_rates.mic_rate = (uint32_t)(((uint64_t)(cur_mic_frames - prev_mic_frames) * 1000000u) / dt_us);
|
||||
g_diag_rates.bt31_rate = (uint32_t)(((uint64_t)(cur_bt31 - prev_bt31) * 1000000u) / dt_us);
|
||||
}
|
||||
}
|
||||
prev_us_frames = cur_us_frames;
|
||||
prev_us_frames = cur_us_frames;
|
||||
prev_bt_packets = cur_bt_packets;
|
||||
prev_sample_us = now_us;
|
||||
prev_mic_frames = cur_mic_frames;
|
||||
prev_bt31 = cur_bt31;
|
||||
prev_sample_us = now_us;
|
||||
}
|
||||
|
||||
snprintf(buf, sizeof(buf), "USB aud %lu/s", (unsigned long)usb_rate);
|
||||
draw_text(kContentX, 18, buf);
|
||||
snprintf(buf, sizeof(buf), "BT 0x32 %lu/s", (unsigned long)bt_rate);
|
||||
draw_text(kContentX, 27, buf);
|
||||
snprintf(buf, sizeof(buf), "HCI errs: %lu", (unsigned long)bt_hci_err_count());
|
||||
draw_text(kContentX, 36, buf);
|
||||
// Row list ordered by relevance: always-useful at top, parked-mic-investigation
|
||||
// data at bottom. To add a row, bump kNumDiagRows and add a case.
|
||||
constexpr int kNumDiagRows = 10;
|
||||
__attribute__((noinline))
|
||||
void format_diag_row(int idx, char* line, size_t n) {
|
||||
switch (idx) {
|
||||
case 0: {
|
||||
const uint32_t s = time_us_32() / 1000000u;
|
||||
snprintf(line, n, "Up:%luh %02lum %02lus",
|
||||
(unsigned long)(s / 3600u),
|
||||
(unsigned long)((s / 60u) % 60u),
|
||||
(unsigned long)(s % 60u));
|
||||
break;
|
||||
}
|
||||
case 1:
|
||||
snprintf(line, n, "BT: %s", bt_is_connected() ? "connected" : "waiting");
|
||||
break;
|
||||
case 2:
|
||||
snprintf(line, n, "host02: %lu", (unsigned long)host_out02_total());
|
||||
break;
|
||||
case 3:
|
||||
snprintf(line, n, "trig %lu / tx %lu",
|
||||
(unsigned long)host_out02_trig_allow(),
|
||||
(unsigned long)host_out02_to_bt());
|
||||
break;
|
||||
case 4:
|
||||
snprintf(line, n, "BT31 in: %lu/s", (unsigned long)g_diag_rates.bt31_rate);
|
||||
break;
|
||||
case 5:
|
||||
snprintf(line, n, "USB aud: %lu/s", (unsigned long)g_diag_rates.usb_rate);
|
||||
break;
|
||||
case 6:
|
||||
snprintf(line, n, "BT32 out: %lu/s", (unsigned long)g_diag_rates.bt_rate);
|
||||
break;
|
||||
case 7:
|
||||
snprintf(line, n, "Mic in: %lu/s", (unsigned long)g_diag_rates.mic_rate);
|
||||
break;
|
||||
case 8:
|
||||
snprintf(line, n, "Mic dec=%ld w=%u",
|
||||
(long)audio_mic_last_decoded(),
|
||||
(unsigned)audio_mic_last_wrote());
|
||||
break;
|
||||
case 9: {
|
||||
uint8_t pfx[6]; bt_31_mic_prefix(pfx);
|
||||
snprintf(line, n, "%02X %02X %02X %02X %02X %02X",
|
||||
pfx[0], pfx[1], pfx[2], pfx[3], pfx[4], pfx[5]);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
line[0] = '\0';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
snprintf(buf, sizeof(buf), "BT: %s", bt_is_connected() ? "connected" : "waiting");
|
||||
draw_text(kContentX, 45, buf);
|
||||
void diag_handle_input(int visible) {
|
||||
if (!bt_is_connected()) return;
|
||||
const uint8_t dpad = (uint8_t)(interrupt_in_data[7] & 0x0F);
|
||||
if (dpad != diag_last_dpad && dpad != 8) {
|
||||
if (dpad == 0) diag_scroll--; // up
|
||||
else if (dpad == 4) diag_scroll++; // down
|
||||
}
|
||||
diag_last_dpad = dpad;
|
||||
const int max_top = (kNumDiagRows > visible) ? (kNumDiagRows - visible) : 0;
|
||||
if (diag_scroll < 0) diag_scroll = 0;
|
||||
if (diag_scroll > max_top) diag_scroll = max_top;
|
||||
}
|
||||
|
||||
__attribute__((noinline)) void render_screen_diag() {
|
||||
fb_clear();
|
||||
draw_text(kContentX, 0, "Diagnostics");
|
||||
|
||||
sample_diag_rates();
|
||||
constexpr int kVisible = 5;
|
||||
diag_handle_input(kVisible);
|
||||
|
||||
char line[28];
|
||||
for (int i = 0; i < kVisible && diag_scroll + i < kNumDiagRows; i++) {
|
||||
format_diag_row(diag_scroll + i, line, sizeof(line));
|
||||
draw_text(kContentX, 9 + i * 9, line);
|
||||
}
|
||||
|
||||
// Scroll indicators along the right edge, adjacent to the visible content
|
||||
// rather than in a footer — keeps the bottom row available for content.
|
||||
if (diag_scroll > 0) draw_text(120, 9, "^");
|
||||
if (diag_scroll + kVisible < kNumDiagRows) draw_text(120, 45, "v");
|
||||
|
||||
flush_fb();
|
||||
}
|
||||
@@ -1316,6 +1426,36 @@ void oled_init() {
|
||||
boot_splash();
|
||||
}
|
||||
|
||||
// Dim-tier renderer: blank the panel and draw a tiny "I'm alive" dot that
|
||||
// breathes (1s on / 1s off) and walks through 8 evenly-spaced positions every
|
||||
// ~30 s. Two goals: (1) reduce total pixel-on-time to a tiny fraction so the
|
||||
// panel barely glows even with the contrast register pinned, (2) prevent any
|
||||
// single pixel from accumulating wear. noinline keeps oled_loop's literal pool
|
||||
// in Thumb's 4 KB reach (same constraint the other render_screen_* hit).
|
||||
__attribute__((noinline))
|
||||
void render_dim_pulse(uint32_t dim_elapsed_us) {
|
||||
fb_clear();
|
||||
constexpr uint32_t kPulsePeriodUs = 2UL * 1000000UL; // 2 s blink cycle
|
||||
constexpr uint32_t kPulseOnUs = 1UL * 1000000UL; // 1 s on, 1 s off
|
||||
constexpr uint32_t kPosStepUs = 30UL * 1000000UL; // 30 s per position
|
||||
constexpr int kPositions[][2] = {
|
||||
{ 16, 8}, { 64, 8}, {112, 8},
|
||||
{112, 32},
|
||||
{112, 56}, { 64, 56}, { 16, 56},
|
||||
{ 16, 32},
|
||||
};
|
||||
constexpr int kNumPositions = sizeof(kPositions) / sizeof(kPositions[0]);
|
||||
const bool dot_on = (dim_elapsed_us % kPulsePeriodUs) < kPulseOnUs;
|
||||
if (dot_on) {
|
||||
const int idx = (int)((dim_elapsed_us / kPosStepUs) % (uint32_t)kNumPositions);
|
||||
const int cx = kPositions[idx][0];
|
||||
const int cy = kPositions[idx][1];
|
||||
// 2x2 dot — small enough to barely register, big enough to see across a desk.
|
||||
rect_filled(cx, cy, 2, 2);
|
||||
}
|
||||
flush_fb_raw(); // skip chrome arrows; nothing to navigate to from sleep
|
||||
}
|
||||
|
||||
void oled_loop() {
|
||||
handle_buttons();
|
||||
const uint32_t now = time_us_32();
|
||||
@@ -1329,16 +1469,48 @@ void oled_loop() {
|
||||
last_input_hash = hash;
|
||||
last_activity_us = now;
|
||||
}
|
||||
// Rising-edge: BT-connect itself counts as activity, so the screen wakes
|
||||
// the moment a controller pairs rather than waiting for the first input.
|
||||
const bool bt_connected_now = bt_is_connected();
|
||||
if (bt_connected_now && !prev_bt_connected) last_activity_us = now;
|
||||
prev_bt_connected = bt_connected_now;
|
||||
|
||||
// Auto-dim after idle; respect user-selected brightness otherwise
|
||||
const bool idle = (now - last_activity_us) > kAutoDimUs;
|
||||
sh1107_set_contrast(idle ? kDimContrast : kBrightLevels[bright_idx]);
|
||||
// Power-state ladder: Active → Dim (breathing dot) → Off based on idle time.
|
||||
const uint32_t idle = now - last_activity_us;
|
||||
if (idle > kAutoOffUs) {
|
||||
if (oled_power_state != OLED_OFF) {
|
||||
cmd(0xAE);
|
||||
oled_power_state = OLED_OFF;
|
||||
}
|
||||
return; // panel is off, nothing to draw
|
||||
}
|
||||
if (oled_power_state == OLED_OFF) cmd(0xAF); // wake panel before drawing
|
||||
if (idle > kAutoDimUs) {
|
||||
sh1107_set_contrast(kDimContrast);
|
||||
oled_power_state = OLED_DIM;
|
||||
render_dim_pulse(idle - kAutoDimUs);
|
||||
return; // skip the regular per-screen render path
|
||||
}
|
||||
sh1107_set_contrast(kBrightLevels[bright_idx]);
|
||||
oled_power_state = OLED_ACTIVE;
|
||||
|
||||
// True on the first render after navigating to a different screen.
|
||||
// Lets a screen do expensive one-shot work on entry (the CPU screen
|
||||
// caches its frequency-counter measurement here).
|
||||
static int last_rendered_screen = -1;
|
||||
const bool screen_entered = (current_screen != last_rendered_screen);
|
||||
|
||||
// Leaving Trigger Test in either direction → reset the adaptive
|
||||
// trigger preset to OFF and push it to the controller. Otherwise
|
||||
// the last-cycled effect (Weapon snap, Galloping pulse, etc.)
|
||||
// stays active on the DS5 indefinitely, which surprised users
|
||||
// who'd just navigated away expecting a clean slate.
|
||||
if (last_rendered_screen == kScreenTriggers
|
||||
&& current_screen != kScreenTriggers) {
|
||||
trigger_preset = 0;
|
||||
send_trigger_effect(0);
|
||||
}
|
||||
|
||||
last_rendered_screen = current_screen;
|
||||
|
||||
switch (current_screen) {
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ namespace {
|
||||
static constexpr uint8_t state_init_data[63] = {
|
||||
0xfd, 0xf7, 0x0, 0x0,
|
||||
0x7f, 0x64, // Headphones, Speaker
|
||||
0xff, 0x9, 0x0, 0x0F, 0x0, 0x0, 0x0, 0x0,
|
||||
0x40, 0x9, 0x0, 0x00, 0x0, 0x0, 0x0, 0x0, // VolumeMic=64, MuteControl all clear (no PowerSave)
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xa,
|
||||
|
||||
Reference in New Issue
Block a user