Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1c4e34f8da | ||
|
|
20b41d80a1 |
+24
-64
@@ -1,77 +1,37 @@
|
|||||||
# Bluetooth microphone investigation — current status
|
# Bluetooth microphone — SOLVED
|
||||||
|
|
||||||
**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"*).
|
**TL;DR:** The DualSense's built-in microphone **works over this dongle's Bluetooth pairing** as of v0.6.8. It is decoded and presented to the host as the standard DualSense USB capture device. Earlier versions of this document concluded the opposite — that it was a Sony-side limitation, probably encrypted, a dead end. **That conclusion was wrong.** The whole thing hinged on a single enable bit. Full credit to **[awalol](https://github.com/awalol/DS5Dongle)** (upstream `mic` branch) for identifying it.
|
||||||
|
|
||||||
This file is a hand-off / research log for the next person who tries.
|
## How it works
|
||||||
|
|
||||||
## What does work
|
1. **Enable bit (dongle → DS5).** In the outbound `0x36` BT audio report, the audio-control sub-report's first flag byte (`pkt[4]`) has **bit 0 = mic-enable**. Setting it (`0b11111110` → `0b11111111`; bits 1–7 were already the speaker/haptic enables) tells the DS5 to start streaming its mic. See `src/audio.cpp`.
|
||||||
|
2. **Mic frames (DS5 → dongle).** Once enabled, the DS5 tags certain `0x31` BT input reports as mic frames by setting **bit 1 of byte 2** (`(data[2] >> 1) & 1`); the payload is a **71-byte Opus packet at offset +4**. `src/main.cpp:on_bt_data()` routes those to `mic_add_queue()`.
|
||||||
|
3. **Decode + present.** `src/audio.cpp` Opus-decodes each frame to mono 48 kHz (480-sample / 10 ms frames), duplicates mono → stereo, and `tud_audio_write`s it to the UAC1 capture endpoint. The host sees a normal DualSense mic.
|
||||||
|
4. **Sticky.** Once the DS5 starts streaming it **keeps going for the rest of the session** even after the enable bit / audio output stops (verified: mic stays live with no further `0x36` frames).
|
||||||
|
5. **Always-on.** Because the enable normally only rides the audio-gated `0x36` frames, the dongle sends a **control-only `0x36` keep-alive** (enable bit + `SetStateData` + silent haptic, no speaker payload) at ~4 Hz *only until mic frames start arriving*, then stops (sticky takes over). So the mic works with no game audio playing, at minimal extra BT traffic. See `mic_enable_keepalive()` in `src/audio.cpp`.
|
||||||
|
6. **Toggle.** Gated by `Config_body.bt_mic_enable` (default on) — OLED **Settings → BT Mic** and the web config tool. Off = no enable sent, no keep-alive, and inbound mic frames are not routed (so it's off host-side even if a previously-enabled DS5 is still streaming). Off by toggle saves DS5 battery (always-on keeps its audio subsystem awake).
|
||||||
|
|
||||||
- **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).
|
## Why the original conclusion was wrong (the lesson)
|
||||||
- **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 earlier investigation ported awalol's *receive* side (the `(data[2]>>1)&1` trigger + Opus decode) and then watched for that bit — but **never sent the enable bit**, because this fork's `audio.cpp` had diverged (the pre-`3a31bd7` SetStateData revert) and sat at `pkt[4] = 0b11111110`. With nothing telling the DS5 to start, it never streamed, so bit 1 of byte 2 never set — which got misread as "the trigger never fires → the channel must be encrypted / it's a dead end."
|
||||||
|
|
||||||
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).
|
It was never encrypted. It was one un-set bit on the *transmit* side. Lesson: when porting a two-sided protocol, confirm **both** halves (enable *and* receive) before concluding the device "can't" do something.
|
||||||
|
|
||||||
What we tried:
|
## What we use (host-side)
|
||||||
|
|
||||||
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.
|
- **`scripts/mic_diag.sh`** — `status` / `capture [secs]` / `watch` / `bt-trace`. `capture` arecords the DualSense mic card and reports peak/RMS/non-zero, the fastest way to confirm real audio.
|
||||||
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.
|
- The DualSense capture card's **`Headset` capture control defaults low** — raise it (`amixer -c <card> sset 'Headset' 90%`) or captures look silent.
|
||||||
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.
|
- **OLED Diagnostics** `Mic in:` (~100/s when streaming) + `Mic dec=` (480 = good Opus decode) are the on-device confirmation.
|
||||||
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.
|
- Vendor HID feature reports `0xFD` / `0xFE` and the BT counters remain useful general audio-debug infra.
|
||||||
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):
|
## Open follow-ups
|
||||||
|
|
||||||
- **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.
|
- **No documented "stop" command.** Disabling mid-session relies on gating the receive side; the DS5 keeps streaming until reconnect. If a real stop/disable bit is found, wire it into the toggle to stop the DS5-side battery drain immediately.
|
||||||
- **Open additional L2CAP channels** (proprietary audio PSM if found, or standard ones like A2DP=0x19 / RFCOMM=0x03) and watch for unsolicited inbound data.
|
- **Mono only.** Decoded mono is duplicated to the stereo endpoint; the DS5 mic is mono so this is fine, but the descriptor could be made truly mono (as awalol's branch does) to halve endpoint bandwidth.
|
||||||
- **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.)
|
- **Name the `pkt[4]` bits precisely.** `daidr/dualsense-tester`'s `outputStruct.ts` documents the *standard* output report flags but not the `0x36` BT-audio sub-report; the bit meanings beyond bit 0 are inferred from the working speaker/haptic path.
|
||||||
- **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
|
## 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).
|
- Upstream `awalol/DS5Dongle` branch `mic` (commits `9c197fc`, `3829163`) — the source of the enable bit (`pkt[4]` bit 0) and the receive-side decode. awalol confirmed bit 0 is the key.
|
||||||
- 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.
|
- Linux kernel `drivers/hid/hid-playstation.c` (~line 1509, *"Bluetooth audio is currently not supported"*) — still true for the kernel driver; not for this dongle.
|
||||||
- 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.
|
- `daidr/dualsense-tester` — `src/router/DualSense/views/_OutputPanel/outputStruct.ts` for the standard output-report flag layout.
|
||||||
- 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,24 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Version
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## [0.6.8-oled-edition] — 2026-05-24
|
||||||
|
|
||||||
|
Two big items: **DualSense microphone over Bluetooth** (long believed impossible — turned out to be a single enable bit; credit [awalol](https://github.com/awalol/DS5Dongle) upstream) and a **USB 3.0 connection-interference watchdog**. UF2s attached to [the GitHub release](https://github.com/MarcelineVPQ/DS5Dongle-OLED-Edition/releases/tag/v0.6.8-oled-edition) (built by `.github/workflows/release.yml`). The companion `DS5Dongle-OLED-Config-Web` config tool gains a **BT microphone** toggle.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **DualSense microphone over Bluetooth.** The controller's built-in mic now works over the dongle's BT pairing — decoded from the DS5's Opus stream and presented to the host as the standard DualSense USB capture device, usable by any app (Discord, OBS, in-game voice). This fork had previously documented BT mic as a hard Sony-firmware limitation (likely encrypted); that conclusion was **wrong** — it hinged on a single enable bit (`pkt[4]` bit 0 in the outbound `0x36` audio report). Credit to **[awalol](https://github.com/awalol/DS5Dongle)** (upstream) for identifying it. The DS5 streams mic as 71-byte Opus packets tagged in `0x31` reports (`(data[2]>>1)&1`); `src/audio.cpp` decodes mono→stereo to the UAC1 endpoint. **Always-on:** the enable is sticky once streaming, so a control-only `0x36` keep-alive asserts it at ~4 Hz only until frames arrive, then backs off — mic works with no game audio, at minimal BT traffic. **Toggle:** new `bt_mic_enable` config field (default on; off saves DS5 battery since always-on keeps its audio subsystem awake) — OLED **Settings → BT Mic** and the web config tool's **BT microphone** switch. `BLUETOOTH_AUDIO_NOTES.md` rewritten from "dead end" to the working mechanism; README gains a user-facing **"DualSense Microphone over Bluetooth"** section.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **Connection no longer hangs permanently on the amber lightbar (USB 3.0 interference recovery).** Users reported the DualSense getting stuck mid-connect (solid amber/yellow, never enumerates) on USB 3.0 host ports while USB 2.0 worked — caused by USB 3.0's broadband ~2.4 GHz RF noise desensitizing the CYW43 Bluetooth radio. The firmware's connection flow had dead-end states (ACL-fail and auth-fail re-inquiry were commented out; the controller-type feature-packet wait had no timeout), so a single lost packet stalled forever until a replug. Added a **connection-attempt watchdog** (`src/bt.cpp`): a 10 s timeout armed when a connection commits to a device and cleared when it reaches USB enumeration; on expiry it tears down via the existing `HCI_EVENT_DISCONNECTION_COMPLETE` path and restarts inquiry, so a stalled connect auto-retries instead of hanging. Re-enabled the ACL-fail / create-connection-reject / auth-fail recovery paths for faster recovery when the controller *does* report a failure. The watchdog is inert during a healthy established session (no effect on normal play, slot-switching, or idle-disconnect). Helps any marginal-RF setup, not just USB 3.0.
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- New README section **"USB 3.0 ports & Bluetooth interference"** + a Known Issues bullet: explains the 2.4 GHz RFI cause (referencing Intel's white paper) and lists mitigations (USB 2.0 port, short USB 2.0 extension cable, powered USB 2.0 hub, ferrite bead, distance/line-of-sight).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [0.6.7-oled-edition] — 2026-05-23
|
## [0.6.7-oled-edition] — 2026-05-23
|
||||||
|
|
||||||
UF2s attached to [the GitHub release](https://github.com/MarcelineVPQ/DS5Dongle-OLED-Edition/releases/tag/v0.6.7-oled-edition) (built by `.github/workflows/release.yml`).
|
UF2s attached to [the GitHub release](https://github.com/MarcelineVPQ/DS5Dongle-OLED-Edition/releases/tag/v0.6.7-oled-edition) (built by `.github/workflows/release.yml`).
|
||||||
|
|||||||
@@ -140,11 +140,43 @@ When the connected DualSense reports its battery at or below 10% (and it is not
|
|||||||
|
|
||||||
To opt out at build time, configure with `-DENABLE_BATT_LED=OFF`. Default is ON.
|
To opt out at build time, configure with `-DENABLE_BATT_LED=OFF`. Default is ON.
|
||||||
|
|
||||||
|
## DualSense Microphone over Bluetooth
|
||||||
|
|
||||||
|
**The DualSense's built-in microphone works over the dongle's Bluetooth pairing** (since v0.6.8). The controller streams its mic as Opus audio; the dongle decodes it and presents it to the host as the standard DualSense USB capture device, so any app (Discord, OBS, in-game voice) can use it like a normal microphone.
|
||||||
|
|
||||||
|
This was long believed impossible — earlier versions of this fork documented it as a hard Sony-firmware limitation (and the Linux `hid-playstation` kernel driver still doesn't support it). It turned out to hinge on a single enable bit in the dongle's outbound audio report. **Full credit to [awalol](https://github.com/awalol/DS5Dongle) (upstream) for identifying it.** The corrected investigation log lives in [BLUETOOTH_AUDIO_NOTES.md](./BLUETOOTH_AUDIO_NOTES.md).
|
||||||
|
|
||||||
|
**Using it:**
|
||||||
|
|
||||||
|
- **On by default.** Pair the controller and the mic begins streaming within a second or two — no game audio required (the dongle keeps the stream alive on its own).
|
||||||
|
- **Raise the capture volume on the host** — it defaults low. On Linux, find the card with `arecord -l`, then e.g. `amixer -c <DualSense card> sset 'Headset' 90%`. Verify capture with `scripts/mic_diag.sh capture`.
|
||||||
|
- **Toggle it off to save controller battery** — OLED **Settings → BT Mic**, or the **BT microphone** switch in the [web config tool](#web-config-tool). Always-on mic keeps the DS5's audio subsystem awake, which drains its battery noticeably faster, so disable it if you don't use voice.
|
||||||
|
|
||||||
|
**Caveats:**
|
||||||
|
|
||||||
|
- Mic audio is **mono** (decoded mono, duplicated across the stereo capture endpoint).
|
||||||
|
- Toggling off mid-session stops the host feed immediately, but the controller keeps streaming until it next reconnects (there's no known "stop" command); connecting fresh with the toggle off never enables it.
|
||||||
|
- The OLED **Diagnostics** screen's `Mic in:` counter reads ~100/s while the mic is streaming — a quick way to confirm it's live.
|
||||||
|
|
||||||
## Known Issues
|
## Known Issues
|
||||||
|
|
||||||
- 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.
|
- 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.
|
- 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.
|
- **USB 3.0 ports can disrupt pairing** — the controller may get stuck on a solid amber/yellow lightbar and never connect, while the same dongle works fine on a USB 2.0 port. This is RF interference, not a firmware bug; see [USB 3.0 ports & Bluetooth interference](#usb-30-ports--bluetooth-interference) below. (As of v0.6.8 the firmware auto-retries a stalled connection instead of hanging, which recovers many — but not all — marginal cases.)
|
||||||
|
|
||||||
|
## USB 3.0 ports & Bluetooth interference
|
||||||
|
|
||||||
|
If the dongle works on one port but not another, **try a USB 2.0 port first.** USB 3.0 ports and (especially) USB 3.0 extension cables emit broadband RF noise centered near 2.4 GHz — the same band the dongle's Bluetooth radio uses to talk to the controller. This is a well-documented industry issue (Intel, *"USB 3.0 Radio Frequency Interference Impact on 2.4 GHz Wireless Devices"*), not specific to this firmware. The noise desensitizes the dongle's BT receiver, so the controller can start connecting (amber lightbar) but the link is too noisy to complete — it hangs on yellow.
|
||||||
|
|
||||||
|
Mitigations, roughly in order of effectiveness:
|
||||||
|
|
||||||
|
1. **Plug the dongle into a USB 2.0 port** (often the simplest fix — many motherboards/cases have both).
|
||||||
|
2. **Use a short USB 2.0 extension cable** to get the dongle a few inches away from the USB 3.0 ports / metal chassis, improving line-of-sight to the controller. Avoid USB 3.0 extension cables specifically.
|
||||||
|
3. **Use a powered USB 2.0 hub** plugged into the USB 3.0 port — the hub downshifts the link and adds distance.
|
||||||
|
4. **Clip a ferrite bead** onto the cable near the dongle.
|
||||||
|
5. **Keep the controller closer / in line of sight** of the dongle during pairing.
|
||||||
|
|
||||||
|
The firmware will keep retrying a stalled connection on its own, so leaving it plugged in for ~10–20 s after the lightbar goes amber may let it recover without a replug.
|
||||||
|
|
||||||
## Performance / Overclocking
|
## Performance / Overclocking
|
||||||
|
|
||||||
|
|||||||
+55
-2
@@ -13,6 +13,7 @@
|
|||||||
#include "utils.h"
|
#include "utils.h"
|
||||||
#include "pico/multicore.h"
|
#include "pico/multicore.h"
|
||||||
#include "pico/util/queue.h"
|
#include "pico/util/queue.h"
|
||||||
|
#include "pico/time.h"
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "state_mgr.h"
|
#include "state_mgr.h"
|
||||||
#include "usb.h"
|
#include "usb.h"
|
||||||
@@ -114,6 +115,46 @@ void mic_add_queue(const uint8_t *data) {
|
|||||||
queue_try_add(&mic_fifo, &packet);
|
queue_try_add(&mic_fifo, &packet);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Re-assert the DS5 mic-enable (pkt[4] bit 0) so the controller streams its mic
|
||||||
|
// even when no audio is being output to it. Normally the enable only rides the
|
||||||
|
// 0x36 audio frames, which are gated on active USB audio — so without this, mic
|
||||||
|
// only works while a game plays sound. The enable is sticky (the DS5 keeps
|
||||||
|
// streaming once it starts), so we send a control-only 0x36 (enable + the
|
||||||
|
// load-bearing SetStateData sub-report + a silent haptic block, no speaker
|
||||||
|
// payload → makes no sound) at ~4 Hz ONLY until mic frames start arriving, then
|
||||||
|
// stop — minimizing BT traffic and DS5 battery. Resumes if the stream stalls.
|
||||||
|
static void mic_enable_keepalive() {
|
||||||
|
if (!bt_is_connected() || !get_config().bt_mic_enable) return;
|
||||||
|
const uint64_t now = time_us_64();
|
||||||
|
static uint32_t last_frames = 0;
|
||||||
|
static uint64_t last_frame_us = 0;
|
||||||
|
static uint64_t last_send_us = 0;
|
||||||
|
const uint32_t frames = g_mic_frames;
|
||||||
|
if (frames != last_frames) { last_frames = frames; last_frame_us = now; }
|
||||||
|
if (last_frame_us != 0 && (now - last_frame_us) < 1000000ULL) return; // streaming → sticky, no resend
|
||||||
|
if (last_send_us != 0 && (now - last_send_us) < 250000ULL) return; // throttle to ~4 Hz while arming
|
||||||
|
last_send_us = now;
|
||||||
|
|
||||||
|
uint8_t pkt[REPORT_SIZE]{};
|
||||||
|
pkt[0] = REPORT_ID;
|
||||||
|
pkt[1] = reportSeqCounter << 4;
|
||||||
|
reportSeqCounter = (reportSeqCounter + 1) & 0x0F;
|
||||||
|
pkt[2] = 0x11 | 1 << 7;
|
||||||
|
pkt[3] = 7;
|
||||||
|
pkt[4] = 0b11111111; // mic-enable (bit 0)
|
||||||
|
const auto buf_len = get_config().audio_buffer_length;
|
||||||
|
pkt[5] = pkt[6] = pkt[7] = pkt[8] = pkt[9] = buf_len;
|
||||||
|
pkt[10] = packetCounter++;
|
||||||
|
pkt[11] = 0x10 | 1 << 7; // SetStateData sub-report (load-bearing — keeps actuators alive)
|
||||||
|
pkt[12] = 63;
|
||||||
|
state_set(pkt + 13, 63);
|
||||||
|
pkt[76] = 0x12 | 1 << 7; // haptic sub-report; samples left zero = silent
|
||||||
|
pkt[77] = SAMPLE_SIZE;
|
||||||
|
// no speaker sub-report (pkt[142..] stays zero) → control-only, no audio out
|
||||||
|
bt_write(pkt, sizeof(pkt));
|
||||||
|
g_bt_packets++;
|
||||||
|
}
|
||||||
|
|
||||||
void audio_loop() {
|
void audio_loop() {
|
||||||
// Mic-in path: pull one Opus packet from the BT-side FIFO, decode to
|
// 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),
|
// mono PCM, duplicate to stereo (our UAC1 endpoint declares 2 channels),
|
||||||
@@ -142,7 +183,16 @@ void audio_loop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 1. 读取 USB 音频数据
|
// 1. 读取 USB 音频数据
|
||||||
if (!tud_audio_available()) return;
|
if (!tud_audio_available()) {
|
||||||
|
// Keep the DS5 mic streaming even without output audio — but ONLY once
|
||||||
|
// the host has enumerated us (tud_mounted). Running it during the
|
||||||
|
// fresh-pair feature handshake floods BT TX and delays controller-type
|
||||||
|
// detection past the connection watchdog's timeout, which then tears the
|
||||||
|
// link down (~10-15s "shutdown" on fresh pair). After enumeration the
|
||||||
|
// handshake is done, so it's safe — and always-on mic still works.
|
||||||
|
if (tud_mounted()) mic_enable_keepalive();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
int16_t raw[192];
|
int16_t raw[192];
|
||||||
uint32_t bytes_read = tud_audio_read(raw, sizeof(raw)); // 每次读入 384 bytes
|
uint32_t bytes_read = tud_audio_read(raw, sizeof(raw)); // 每次读入 384 bytes
|
||||||
@@ -276,7 +326,10 @@ void audio_loop() {
|
|||||||
reportSeqCounter = (reportSeqCounter + 1) & 0x0F;
|
reportSeqCounter = (reportSeqCounter + 1) & 0x0F;
|
||||||
pkt[2] = 0x11 | 0 << 6 | 1 << 7;
|
pkt[2] = 0x11 | 0 << 6 | 1 << 7;
|
||||||
pkt[3] = 7;
|
pkt[3] = 7;
|
||||||
pkt[4] = 0b11111110;
|
// bit 0 = mic-enable: tells the DS5 to stream its mic over BT (awalol
|
||||||
|
// confirmed this is the key). Bits 1-7 are the pre-existing speaker/
|
||||||
|
// haptic audio-enable flags. Gated on the bt_mic_enable config toggle.
|
||||||
|
pkt[4] = get_config().bt_mic_enable ? 0b11111111 : 0b11111110;
|
||||||
const auto buf_len = get_config().audio_buffer_length;
|
const auto buf_len = get_config().audio_buffer_length;
|
||||||
pkt[5] = buf_len;
|
pkt[5] = buf_len;
|
||||||
pkt[6] = buf_len;
|
pkt[6] = buf_len;
|
||||||
|
|||||||
+58
-3
@@ -26,6 +26,15 @@
|
|||||||
#define MTU_CONTROL 672
|
#define MTU_CONTROL 672
|
||||||
#define MTU_INTERRUPT 672
|
#define MTU_INTERRUPT 672
|
||||||
|
|
||||||
|
// Connection-attempt watchdog: if a connection commits to a device (inquiry
|
||||||
|
// found one / incoming request accepted) but doesn't reach USB-enumeration
|
||||||
|
// within this window, tear down and retry. Catches the silent stalls caused by
|
||||||
|
// USB 3.0 2.4 GHz RF interference on the CYW43 BT radio (DualSense stuck on the
|
||||||
|
// amber init lightbar, never enumerates) — see README troubleshooting. A
|
||||||
|
// healthy or slow re-pair finishes well under 6 s, so 10 s never trips a real
|
||||||
|
// connection but heals before the user reaches to replug.
|
||||||
|
#define CONNECT_WATCHDOG_TIMEOUT_US (10 * 1000 * 1000)
|
||||||
|
|
||||||
using std::unordered_map;
|
using std::unordered_map;
|
||||||
using std::vector;
|
using std::vector;
|
||||||
using std::queue;
|
using std::queue;
|
||||||
@@ -54,6 +63,12 @@ struct send_element {
|
|||||||
|
|
||||||
absolute_time_t inactive_time = 0; // 手柄长时间静默
|
absolute_time_t inactive_time = 0; // 手柄长时间静默
|
||||||
|
|
||||||
|
// Connection-attempt watchdog timestamp. 0 == not armed; armed == a connection
|
||||||
|
// attempt is in flight (committed to a device, not yet USB-enumerating). Set
|
||||||
|
// when an attempt begins, cleared the instant the controller type is identified
|
||||||
|
// (USB connects) and on every teardown. Checked by bt_connection_watchdog_tick().
|
||||||
|
static absolute_time_t connect_attempt_started = 0;
|
||||||
|
|
||||||
// Multi-slot pairing state. Modeled on zurce/DS5Dongle-OLED.
|
// Multi-slot pairing state. Modeled on zurce/DS5Dongle-OLED.
|
||||||
static int g_current_slot = 0;
|
static int g_current_slot = 0;
|
||||||
|
|
||||||
@@ -166,6 +181,35 @@ bool bt_disconnect() {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Called every main-loop iteration. If a connection attempt has stalled past
|
||||||
|
// the timeout, tear it down so the state machine retries instead of hanging
|
||||||
|
// (e.g. on the amber lightbar under USB 3.0 RF interference). Inert unless a
|
||||||
|
// connection attempt is in flight, so it never touches a healthy session.
|
||||||
|
void bt_connection_watchdog_tick() {
|
||||||
|
if (connect_attempt_started == 0) return; // not armed
|
||||||
|
if (absolute_time_diff_us(connect_attempt_started, get_absolute_time())
|
||||||
|
< CONNECT_WATCHDOG_TIMEOUT_US) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
printf("[BT] Connection watchdog: attempt stalled, recovering\n");
|
||||||
|
connect_attempt_started = 0; // disarm; the next attempt re-arms
|
||||||
|
|
||||||
|
if (acl_handle != HCI_CON_HANDLE_INVALID) {
|
||||||
|
// ACL is up but setup stalled (auth/encryption/L2CAP/feature-wait).
|
||||||
|
// Route through the proven HCI_EVENT_DISCONNECTION_COMPLETE teardown.
|
||||||
|
bt_disconnect();
|
||||||
|
} else {
|
||||||
|
// No ACL yet (stalled before/at create-connection) — reset by hand
|
||||||
|
// and kick a fresh inquiry.
|
||||||
|
device_found = false;
|
||||||
|
new_pair = false;
|
||||||
|
gap_inquiry_stop();
|
||||||
|
gap_inquiry_start(30);
|
||||||
|
gap_connectable_control(1);
|
||||||
|
update_discoverable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void bt_get_signal_strength(int8_t *rssi) {
|
void bt_get_signal_strength(int8_t *rssi) {
|
||||||
// gap_read_rssi() completes asynchronously, so this function can only
|
// gap_read_rssi() completes asynchronously, so this function can only
|
||||||
// return the last cached RSSI value. Trigger a refresh afterwards so a
|
// return the last cached RSSI value. Trigger a refresh afterwards so a
|
||||||
@@ -298,6 +342,7 @@ static void hci_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *p
|
|||||||
if (device_found) {
|
if (device_found) {
|
||||||
printf("[HCI] Connecting to %s...\n", bd_addr_to_str(current_device_addr));
|
printf("[HCI] Connecting to %s...\n", bd_addr_to_str(current_device_addr));
|
||||||
new_pair = true;
|
new_pair = true;
|
||||||
|
connect_attempt_started = get_absolute_time(); // arm connection watchdog
|
||||||
hci_send_cmd(&hci_create_connection, current_device_addr,
|
hci_send_cmd(&hci_create_connection, current_device_addr,
|
||||||
hci_usable_acl_packet_types(), 0, 0, 0, 1);
|
hci_usable_acl_packet_types(), 0, 0, 0, 1);
|
||||||
break;
|
break;
|
||||||
@@ -317,8 +362,9 @@ static void hci_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *p
|
|||||||
if (opcode == HCI_OPCODE_HCI_CREATE_CONNECTION && status != ERROR_CODE_SUCCESS) {
|
if (opcode == HCI_OPCODE_HCI_CREATE_CONNECTION && status != ERROR_CODE_SUCCESS) {
|
||||||
device_found = false;
|
device_found = false;
|
||||||
new_pair = false;
|
new_pair = false;
|
||||||
|
connect_attempt_started = 0; // disarm; failed before an ACL existed
|
||||||
printf("[HCI] Create connection rejected, restart inquiry\n");
|
printf("[HCI] Create connection rejected, restart inquiry\n");
|
||||||
// gap_inquiry_start(30);
|
gap_inquiry_start(30);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -350,8 +396,9 @@ static void hci_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *p
|
|||||||
} else {
|
} else {
|
||||||
device_found = false;
|
device_found = false;
|
||||||
new_pair = false;
|
new_pair = false;
|
||||||
|
connect_attempt_started = 0; // disarm; no ACL was established
|
||||||
printf("[HCI] ACL connect failed status=0x%02X, restart inquiry\n", status);
|
printf("[HCI] ACL connect failed status=0x%02X, restart inquiry\n", status);
|
||||||
// gap_inquiry_start(30);
|
gap_inquiry_start(30);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -401,7 +448,11 @@ static void hci_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *p
|
|||||||
if (status != ERROR_CODE_SUCCESS) {
|
if (status != ERROR_CODE_SUCCESS) {
|
||||||
printf("[HCI] Authentication failed, drop stored key for %s\n", bd_addr_to_str(current_device_addr));
|
printf("[HCI] Authentication failed, drop stored key for %s\n", bd_addr_to_str(current_device_addr));
|
||||||
gap_drop_link_key_for_bd_addr(current_device_addr);
|
gap_drop_link_key_for_bd_addr(current_device_addr);
|
||||||
// gap_inquiry_start(30);
|
connect_attempt_started = 0; // disarm; teardown below re-inquires
|
||||||
|
// ACL is still up — route through the clean disconnect path
|
||||||
|
// (HCI_EVENT_DISCONNECTION_COMPLETE restarts inquiry) rather
|
||||||
|
// than leaving a half-open ACL.
|
||||||
|
bt_disconnect();
|
||||||
} else {
|
} else {
|
||||||
hci_send_cmd(&hci_set_connection_encryption, handle, 1);
|
hci_send_cmd(&hci_set_connection_encryption, handle, 1);
|
||||||
}
|
}
|
||||||
@@ -438,6 +489,7 @@ static void hci_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *p
|
|||||||
bd_addr_copy(current_device_addr, addr);
|
bd_addr_copy(current_device_addr, addr);
|
||||||
gap_inquiry_stop();
|
gap_inquiry_stop();
|
||||||
hci_send_cmd(&hci_accept_connection_request, addr, 0x01);
|
hci_send_cmd(&hci_accept_connection_request, addr, 0x01);
|
||||||
|
connect_attempt_started = get_absolute_time(); // arm watchdog (incoming path)
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -451,6 +503,7 @@ static void hci_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *p
|
|||||||
const uint8_t reason = hci_event_disconnection_complete_get_reason(packet);
|
const uint8_t reason = hci_event_disconnection_complete_get_reason(packet);
|
||||||
device_found = false;
|
device_found = false;
|
||||||
new_pair = false;
|
new_pair = false;
|
||||||
|
connect_attempt_started = 0; // disarm — every teardown clears here
|
||||||
acl_handle = HCI_CON_HANDLE_INVALID;
|
acl_handle = HCI_CON_HANDLE_INVALID;
|
||||||
bt_rssi = 0;
|
bt_rssi = 0;
|
||||||
hid_control_cid = 0;
|
hid_control_cid = 0;
|
||||||
@@ -508,6 +561,7 @@ static void l2cap_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t
|
|||||||
printf("Connected DSE Controller\n");
|
printf("Connected DSE Controller\n");
|
||||||
check_dse = false;
|
check_dse = false;
|
||||||
is_dse = true;
|
is_dse = true;
|
||||||
|
connect_attempt_started = 0; // fully up — disarm watchdog
|
||||||
#if !ENABLE_SERIAL
|
#if !ENABLE_SERIAL
|
||||||
tud_connect();
|
tud_connect();
|
||||||
#endif
|
#endif
|
||||||
@@ -515,6 +569,7 @@ static void l2cap_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t
|
|||||||
printf("Connected DS5 Controller\n");
|
printf("Connected DS5 Controller\n");
|
||||||
check_dse = false;
|
check_dse = false;
|
||||||
is_dse = false;
|
is_dse = false;
|
||||||
|
connect_attempt_started = 0; // fully up — disarm watchdog
|
||||||
#if !ENABLE_SERIAL
|
#if !ENABLE_SERIAL
|
||||||
tud_connect();
|
tud_connect();
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -25,6 +25,11 @@ std::vector<uint8_t> get_feature_data(uint8_t reportId,uint16_t len);
|
|||||||
void init_feature();
|
void init_feature();
|
||||||
void set_feature_data(uint8_t reportId, uint8_t* data,uint16_t len);
|
void set_feature_data(uint8_t reportId, uint8_t* data,uint16_t len);
|
||||||
|
|
||||||
|
// Connection-attempt watchdog: call once per main-loop iteration. Recovers a
|
||||||
|
// stalled connection (auto re-inquiry) so a transient RF glitch — e.g. USB 3.0
|
||||||
|
// 2.4 GHz interference — doesn't hang the dongle on the amber lightbar.
|
||||||
|
void bt_connection_watchdog_tick();
|
||||||
|
|
||||||
// OLED add-on accessors.
|
// OLED add-on accessors.
|
||||||
bool bt_is_connected();
|
bool bt_is_connected();
|
||||||
void bt_get_addr(uint8_t out[6]);
|
void bt_get_addr(uint8_t out[6]);
|
||||||
|
|||||||
@@ -109,6 +109,10 @@ void config_valid() {
|
|||||||
body->screen_off_timeout = 15; // mirrors the original 15-min off tier
|
body->screen_off_timeout = 15; // mirrors the original 15-min off tier
|
||||||
printf("[Config] screen_off_timeout invalid, defaulting to 15 min\n");
|
printf("[Config] screen_off_timeout invalid, defaulting to 15 min\n");
|
||||||
}
|
}
|
||||||
|
if (body->bt_mic_enable > 1) { // 0xFF erased / upgrade → default ON
|
||||||
|
body->bt_mic_enable = 1;
|
||||||
|
printf("[Config] bt_mic_enable invalid, defaulting to 1 (on)\n");
|
||||||
|
}
|
||||||
if (body->config_version != CONFIG_VERSION) {
|
if (body->config_version != CONFIG_VERSION) {
|
||||||
body->config_version = CONFIG_VERSION;
|
body->config_version = CONFIG_VERSION;
|
||||||
printf("[Config] Warning: Config may breaking change\n");
|
printf("[Config] Warning: Config may breaking change\n");
|
||||||
|
|||||||
@@ -39,6 +39,11 @@ struct __attribute__((packed)) Config_body {
|
|||||||
// idle timer is 64-bit µs so the full range is representable. Issue #5.
|
// idle timer is 64-bit µs so the full range is representable. Issue #5.
|
||||||
uint8_t screen_dim_timeout;
|
uint8_t screen_dim_timeout;
|
||||||
uint8_t screen_off_timeout;
|
uint8_t screen_off_timeout;
|
||||||
|
// DualSense mic over Bluetooth (Phase I). 0 = off, 1 = on (default). When on,
|
||||||
|
// the dongle asserts the DS5 mic-enable bit so the controller streams its mic
|
||||||
|
// over BT and the dongle decodes it to the USB capture endpoint. Costs extra
|
||||||
|
// DS5 battery (keeps its audio subsystem awake), hence the toggle.
|
||||||
|
uint8_t bt_mic_enable;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct __attribute__((packed)) Config {
|
struct __attribute__((packed)) Config {
|
||||||
|
|||||||
+19
-5
@@ -189,11 +189,24 @@ void on_bt_data(CHANNEL_TYPE channel, uint8_t *data, uint16_t len) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mic-add tap DISABLED — was decoding standard input (button/stick
|
// Mic-in tap (TEST): once the dongle asserts the mic-enable bit in the
|
||||||
// bytes) as Opus and producing INT16_MIN garbage on the USB IN
|
// outgoing 0x36 audio report (pkt[4] bit 0, see audio.cpp — awalol
|
||||||
// endpoint. Re-enable once we identify the actual mic transport.
|
// confirmed this is the key), the DS5 streams its mic as a 71-byte Opus
|
||||||
// (Standard input handling below resumes — Status screen + HID
|
// packet at data+4 of a 0x31 report with bit 1 of data[2] set. Route those
|
||||||
// reports to host need this.)
|
// to the mic decoder instead of treating them as a standard input report.
|
||||||
|
// The length guard (4-byte header + 71-byte Opus) keeps a stray short
|
||||||
|
// frame from over-reading. The diagnostic counters above still observe
|
||||||
|
// these frames, so the Diag screen's data[2] OR-mask will show bit 1 set
|
||||||
|
// once the enable bit takes effect.
|
||||||
|
// A mic-tagged 0x31 frame carries Opus audio at data+4, NOT a standard input
|
||||||
|
// report — so it must ALWAYS be diverted here (decoded when mic is on, dropped
|
||||||
|
// when off), never fall through to the input handler below. Letting it through
|
||||||
|
// would copy Opus bytes into interrupt_in_data and corrupt sticks/buttons.
|
||||||
|
if (channel == INTERRUPT && data[1] == 0x31 && ((data[2] >> 1) & 1)
|
||||||
|
&& len >= 75) {
|
||||||
|
if (get_config().bt_mic_enable) mic_add_queue(data + 4);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (channel == INTERRUPT && data[1] == 0x31) {
|
if (channel == INTERRUPT && data[1] == 0x31) {
|
||||||
if ((data[56] & 1) != (interrupt_in_data[53] & 1)) {
|
if ((data[56] & 1) != (interrupt_in_data[53] & 1)) {
|
||||||
@@ -381,6 +394,7 @@ int main() {
|
|||||||
watchdog_update();
|
watchdog_update();
|
||||||
#endif
|
#endif
|
||||||
cyw43_arch_poll();
|
cyw43_arch_poll();
|
||||||
|
bt_connection_watchdog_tick();
|
||||||
tud_task();
|
tud_task();
|
||||||
audio_loop();
|
audio_loop();
|
||||||
interrupt_loop();
|
interrupt_loop();
|
||||||
|
|||||||
+8
-5
@@ -126,14 +126,15 @@ constexpr int kLbModeHost = 8;
|
|||||||
constexpr int kNumLbModes = 9;
|
constexpr int kNumLbModes = 9;
|
||||||
|
|
||||||
// Settings screen state
|
// Settings screen state
|
||||||
constexpr int kNumSettingsItems = 15; // 8 fields + 3 auto-haptic + 2 screen-timeout + Reset + Wipe
|
constexpr int kNumSettingsItems = 16; // 8 fields + 3 auto-haptic + 2 screen-timeout + BT mic + Reset + Wipe
|
||||||
constexpr int kSettingsAutoHapEnaIdx = 8;
|
constexpr int kSettingsAutoHapEnaIdx = 8;
|
||||||
constexpr int kSettingsAutoHapGainIdx = 9;
|
constexpr int kSettingsAutoHapGainIdx = 9;
|
||||||
constexpr int kSettingsAutoHapLpIdx = 10;
|
constexpr int kSettingsAutoHapLpIdx = 10;
|
||||||
constexpr int kSettingsScrDimIdx = 11;
|
constexpr int kSettingsScrDimIdx = 11;
|
||||||
constexpr int kSettingsScrOffIdx = 12;
|
constexpr int kSettingsScrOffIdx = 12;
|
||||||
constexpr int kSettingsResetIdx = 13;
|
constexpr int kSettingsBtMicIdx = 13;
|
||||||
constexpr int kSettingsWipeSlotsIdx = 14;
|
constexpr int kSettingsResetIdx = 14;
|
||||||
|
constexpr int kSettingsWipeSlotsIdx = 15;
|
||||||
Config_body settings_local{};
|
Config_body settings_local{};
|
||||||
int settings_sel = 0;
|
int settings_sel = 0;
|
||||||
bool settings_dirty = false;
|
bool settings_dirty = false;
|
||||||
@@ -1416,6 +1417,7 @@ void settings_adjust(int delta) {
|
|||||||
c.screen_off_timeout = (uint8_t)v;
|
c.screen_off_timeout = (uint8_t)v;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 13: c.bt_mic_enable ^= 1; break; // BT mic on/off
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1517,8 +1519,9 @@ __attribute__((noinline)) void format_settings_item(int idx, char* line, size_t
|
|||||||
if (c.screen_off_timeout == 0) snprintf(line, n, "%s ScrOff off", cur);
|
if (c.screen_off_timeout == 0) snprintf(line, n, "%s ScrOff off", cur);
|
||||||
else snprintf(line, n, "%s ScrOff %umin", cur, c.screen_off_timeout);
|
else snprintf(line, n, "%s ScrOff %umin", cur, c.screen_off_timeout);
|
||||||
break;
|
break;
|
||||||
case 13: snprintf(line, n, "%s Reset to defaults", cur); break;
|
case 13: snprintf(line, n, "%s BT Mic %s", cur, c.bt_mic_enable ? "on" : "off"); break;
|
||||||
case 14: snprintf(line, n, "%s Wipe all slots", cur); break;
|
case 14: snprintf(line, n, "%s Reset to defaults", cur); break;
|
||||||
|
case 15: snprintf(line, n, "%s Wipe all slots", cur); break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user