Compare commits

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 08:45:49 -06:00
11 changed files with 457 additions and 47 deletions
+23
View File
@@ -10,6 +10,29 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Version
--- ---
## [0.6.11-oled-edition] — 2026-06-02
Headline: **adaptive triggers now actually work through the dongle** (#6) — host trigger force-feedback was being forwarded with its enable bits cleared, so the DualSense silently discarded it; this is the fix. Also adds an opt-in **`CtrlWake`** setting so the OLED can sleep while you play (#8/#9), persists the **OLED brightness** choice across power cycles (#9), and ships an in-progress **speaker-audio retiming** pass aimed at the periodic crackle (#7). UF2s attached to [the GitHub release](https://github.com/MarcelineVPQ/DS5Dongle-OLED-Edition/releases/tag/v0.6.11-oled-edition) (built by `.github/workflows/release.yml`).
### Added
- **`CtrlWake` setting — let the OLED sleep while the controller is in use (issues #8, #9).** The idle power-ladder (dim → off) previously never fired during gameplay, because every controller input bumped the activity timer and kept the panel awake — so a configured dim/off timeout effectively did nothing while playing. New Settings item `CtrlWake` (default **on**, preserving the old behavior); set it **off** and controller input no longer wakes the screen — only the OLED's KEY0/KEY1 do — so the dim/off timers count down during play and the panel can sleep, requiring a button press to wake.
### Changed
- **Speaker audio path retimed onto a true 10 ms / 100 Hz grid (issue #7, experimental).** The old path resampled 512→480 samples and shipped a 480-sample Opus frame on the haptic-gated cadence (~every 10.667 ms ≈ 45 kHz into the DS5's free-running 48 kHz DAC), a ~6.25 % underrun that produced a periodic gap/crackle. The resampler is removed and the `0x36` frame now carries exactly one native 10 ms / 48 kHz Opus frame (`SAMPLE_SIZE` 64→60, 480-sample buffer), with the speaker sub-report offset computed from the haptic block length rather than hard-coded. This is an in-progress change aimed at the long-standing crackle; the audio path also carries debug counters (inert in the production UF2, visible only with `ENABLE_SERIAL`).
- **`audio_buffer_length` now defaults to 16** (was 64) — a lower buffer avoids the DS5's periodic re-buffer gap. Existing saved configs keep their value; this only affects fresh flashes / Reset-to-defaults.
- **`auto_haptics_enable` now defaults to Off** (was Fallback) — the speaker-derived rumble fallback could produce erratic haptics, so it is opt-in.
- **OLED SPI flush is now chunked / non-blocking.** The framebuffer is sent in row-chunks across main-loop iterations instead of one ~1.1 ms blocking transfer, so the OLED no longer stalls `tud_task` / `audio_loop` / BTstack polling while it refreshes (the OLED-on side of the #7 audio distortion).
- **Battery percentage shows the mid-point of each 10 % band** (5, 15, …, 95, 100 %), matching the kernel `hid-playstation` driver and Steam, instead of the band floor.
### Fixed
- **Adaptive trigger effects now work through the dongle (issue #6).** Host-sent trigger force-feedback was silently dropped: `state_update()` copied the 11-byte `RightTriggerFFB`/`LeftTriggerFFB` parameter blocks into the outgoing controller-state but never set the `AllowRightTriggerFFB`/`AllowLeftTriggerFFB` "apply" bits in byte 0, so the DualSense received the data with the enable flags cleared and discarded it. Affected **both** output paths (standalone `0x31` and the `0x36` audio-frame fold), which is why triggers felt absent through the dongle but worked on direct USB. The on-dongle Trigger Test screen always set those bits directly (`0x0C`), which is why it worked. Now the host's two allow-flags are mirrored into the state alongside the FFB data, like the rumble flags already were.
- **OLED brightness now persists across a power cycle (issue #9).** The KEY1-long-press brightness level was a runtime-only value that reset to full on every boot; it is now stored in config and restored at init.
---
## [0.6.10-oled-edition] — 2026-05-25 ## [0.6.10-oled-edition] — 2026-05-25
Headline: the **Gyro Tilt screen** is now actually usable — it applies the controller's per-unit factory **IMU calibration**, the dot **centres when the controller lies flat**, and tilt **tracks the direction** you move it. Also adds an **L3 / R3 stick-click indicator** on the Status screen and makes the **charge-ETA** robust so it no longer over-reports off a single slow charge step. Everything here is on-dongle display only — **what games receive is unchanged** (the full gyro/accel stream is still forwarded byte-for-byte). UF2s attached to [the GitHub release](https://github.com/MarcelineVPQ/DS5Dongle-OLED-Edition/releases/tag/v0.6.10-oled-edition) (built by `.github/workflows/release.yml`). Headline: the **Gyro Tilt screen** is now actually usable — it applies the controller's per-unit factory **IMU calibration**, the dot **centres when the controller lies flat**, and tilt **tracks the direction** you move it. Also adds an **L3 / R3 stick-click indicator** on the Status screen and makes the **charge-ETA** robust so it no longer over-reports off a single slow charge step. Everything here is on-dongle display only — **what games receive is unchanged** (the full gyro/accel stream is still forwarded byte-for-byte). UF2s attached to [the GitHub release](https://github.com/MarcelineVPQ/DS5Dongle-OLED-Edition/releases/tag/v0.6.10-oled-edition) (built by `.github/workflows/release.yml`).
+2 -2
View File
@@ -264,7 +264,7 @@ scripts/mic_diag.sh bt-trace
#### 1. 状态 #### 1. 状态
连接状态、已配对 DualSense 的蓝牙地址、带条形的电量百分比(`+` 充电中 / `*` 已充满 / `!` 错误)、实时摇杆位置、方向键、面板按键(△ ◯ ✕ □)、L1/R1,以及 L2/R2 模拟扳机填充条。链路指示与电量使用小像素图标。 连接状态、已配对 DualSense 的蓝牙地址、带条形的电量百分比(`+` 充电中 / `*` 已充满 / `!` 错误)、实时摇杆位置(按下 **L3 / R3** 时,对应的摇杆框会反色闪烁 —— 白底黑点 —— 直到松开)、方向键、面板按键(△ ◯ ✕ □)、L1/R1,以及 L2/R2 模拟扳机填充条。链路指示与电量使用小像素图标。充电时,电池旁会显示到 100% 的预计时间 `~Nm`。
<img src="./assets/oled/oled_sc01.jpg" alt="Status screen on the OLED" width="420"> <img src="./assets/oled/oled_sc01.jpg" alt="Status screen on the OLED" width="420">
@@ -299,7 +299,7 @@ scripts/mic_diag.sh bt-trace
#### 5. 陀螺仪倾斜 #### 5. 陀螺仪倾斜
实时 X/Y/Z 加速度计数值,配 40×40 十字准线框。倾斜手柄,点会实时跟随。 实时 X/Y/Z 加速度计数值,配 40×40 十字准线框。点会实时跟随手柄的倾斜,并在手柄**平放时居中** —— 它使用手柄自带的逐台出厂 IMU 校准(从特性报告 `0x05` 解析),因此每个手柄的静止位置与增益都准确。向左/右、向前/后倾斜时,点会朝相应方向移动
<img src="./assets/oled/oled_sc05.jpg" alt="Gyro Tilt screen on the OLED" width="420"> <img src="./assets/oled/oled_sc05.jpg" alt="Gyro Tilt screen on the OLED" width="420">
+2 -2
View File
@@ -265,7 +265,7 @@ Every screen also paints **`>`** at the top-left edge (next to KEY0) and **`<`**
#### 1. Status #### 1. Status
Connection state, paired DualSense BD address, battery % with bar (`+` charging / `*` complete / `!` error), live analog stick positions, D-pad, face buttons (△ ◯ ✕ □), L1/R1, and L2/R2 analog trigger fill bars. The link indicator and battery use small pixel icons. Connection state, paired DualSense BD address, battery % with bar (`+` charging / `*` complete / `!` error), live analog stick positions (each stick box flashes inverse — a black dot on a white box — while its **L3 / R3** is clicked in), D-pad, face buttons (△ ◯ ✕ □), L1/R1, and L2/R2 analog trigger fill bars. The link indicator and battery use small pixel icons. While charging, a `~Nm` estimate of the time to 100% appears next to the battery.
<img src="./assets/oled/oled_sc01.jpg" alt="Status screen on the OLED" width="420"> <img src="./assets/oled/oled_sc01.jpg" alt="Status screen on the OLED" width="420">
@@ -300,7 +300,7 @@ Cycle order: **Off → Feedback → Weapon → Vibration → Bow → Gallop →
#### 5. Gyro Tilt #### 5. Gyro Tilt
Live X/Y/Z accelerometer values with a 40×40 crosshair box. Tilt the controller and the dot tracks in real time. Live X/Y/Z accelerometer values with a 40×40 crosshair box. The dot tracks the controller's tilt in real time and **sits centered when the controller lies flat** — it's driven by the controller's own per-unit factory IMU calibration (parsed from feature report `0x05`), so the rest position and gain are correct on every controller. Tilting left/right and forward/back moves the dot in the matching direction.
<img src="./assets/oled/oled_sc05.jpg" alt="Gyro Tilt screen on the OLED" width="420"> <img src="./assets/oled/oled_sc05.jpg" alt="Gyro Tilt screen on the OLED" width="420">
+73
View File
@@ -0,0 +1,73 @@
#!/usr/bin/env python3
"""Decode raw Opus frames captured from firmware serial output.
Reads [OPUS_FRAME_N] hex lines from stdin or a file, decodes each frame
with libopus, and writes the result as a WAV file + prints a summary
(peak amplitude, zero-crossing rate) to diagnose encoder output quality.
"""
import ctypes
import struct
import sys
import wave
import re
lib = ctypes.cdll.LoadLibrary("libopus.so.0")
SAMPLE_RATE = 48000
CHANNELS = 2
FRAME_SIZE = 480 # 10ms at 48kHz
# Create decoder
err = ctypes.c_int(0)
decoder = lib.opus_decoder_create(SAMPLE_RATE, CHANNELS, ctypes.byref(err))
if err.value != 0:
print(f"opus_decoder_create failed: {err.value}", file=sys.stderr)
sys.exit(1)
infile = sys.argv[1] if len(sys.argv) > 1 else "/tmp/ds5_opus.log"
with open(infile) as f:
lines = f.readlines()
frames = []
for line in lines:
m = re.match(r'\[OPUS_FRAME_\d+\]\s+([0-9a-fA-F]+)', line.strip())
if m:
frames.append(bytes.fromhex(m.group(1)))
if not frames:
print("No [OPUS_FRAME_N] lines found in input.", file=sys.stderr)
sys.exit(1)
print(f"Found {len(frames)} Opus frames, decoding...")
all_pcm = b""
for i, frame_data in enumerate(frames):
pcm = (ctypes.c_int16 * (FRAME_SIZE * CHANNELS))()
ret = lib.opus_decode(
decoder,
frame_data, len(frame_data),
pcm, FRAME_SIZE,
0 # no FEC
)
if ret < 0:
errstr = lib.opus_strerror(ret)
print(f" Frame {i+1}: DECODE ERROR {ret} ({ctypes.string_at(errstr).decode()})")
continue
samples = list(pcm)
peak = max(abs(s) for s in samples)
nonzero = sum(1 for s in samples if s != 0)
print(f" Frame {i+1}: {ret} samples decoded, peak={peak}, nonzero={nonzero}/{len(samples)}")
print(f" TOC=0x{frame_data[0]:02x} first 8 bytes: {frame_data[:8].hex()}")
all_pcm += struct.pack(f"<{ret * CHANNELS}h", *samples[:ret * CHANNELS])
outpath = "/tmp/ds5_opus_decoded.wav"
with wave.open(outpath, "w") as wf:
wf.setnchannels(CHANNELS)
wf.setsampwidth(2)
wf.setframerate(SAMPLE_RATE)
wf.writeframes(all_pcm)
print(f"\nDecoded audio written to {outpath}")
print(f"Play with: aplay {outpath}")
print(f"View spectrogram: sox {outpath} -n spectrogram -o /tmp/ds5_opus_spectrogram.png")
+146
View File
@@ -0,0 +1,146 @@
#!/usr/bin/env python3
"""Send a 440 Hz sine to the DualSense speaker over Bluetooth from a Linux host
(Raspberry Pi), via raw /dev/hidraw writes — replicating the DS5Dongle firmware's
0x36 audio report BYTE-FOR-BYTE (same Opus settings, same SetStateData, same CRC).
Purpose: test whether a NON-tunneled BT path (Pi's BlueZ over a real UART) delivers
clean audio to the DS5 speaker, vs the Pico 2 W's BT-over-gSPI tunnel. If clean here,
the Pico's tunnel is the crackle culprit.
Pair the DS5 first (bluetoothctl), then run as root (hidraw needs RW):
sudo python3 pi_ds5_audio_test.py [seconds] [--headset] [--audbuf N]
"""
import ctypes, struct, math, sys, os, glob, time
# ---- libopus via ctypes (same loader style as decode_opus_dump.py) ----
opus = ctypes.cdll.LoadLibrary("libopus.so.0")
opus.opus_encoder_create.restype = ctypes.c_void_p
opus.opus_encoder_create.argtypes = [ctypes.c_int32, ctypes.c_int, ctypes.c_int,
ctypes.POINTER(ctypes.c_int)]
opus.opus_encode_float.restype = ctypes.c_int32
opus.opus_encode_float.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_float),
ctypes.c_int, ctypes.POINTER(ctypes.c_ubyte),
ctypes.c_int32]
# opus_encoder_ctl is variadic; leave argtypes unset and pass c_int values.
OPUS_APPLICATION_AUDIO = 2049
OPUS_SET_BITRATE_REQUEST = 4002
OPUS_SET_VBR_REQUEST = 4006
OPUS_SET_COMPLEXITY_REQUEST = 4010
RATE, CH, FRAME, OPUS_BYTES = 48000, 2, 480, 200 # 480 = 10 ms; 200 B = 160 kbps CBR
def make_encoder():
err = ctypes.c_int(0)
enc = opus.opus_encoder_create(RATE, CH, OPUS_APPLICATION_AUDIO, ctypes.byref(err))
if err.value != 0 or not enc:
sys.exit(f"opus_encoder_create failed: {err.value}")
enc_p = ctypes.c_void_p(enc)
# match firmware core1_entry(): 160 kbps, CBR, complexity 0
opus.opus_encoder_ctl(enc_p, OPUS_SET_BITRATE_REQUEST, ctypes.c_int(OPUS_BYTES*8*100))
opus.opus_encoder_ctl(enc_p, OPUS_SET_VBR_REQUEST, ctypes.c_int(0))
opus.opus_encoder_ctl(enc_p, OPUS_SET_COMPLEXITY_REQUEST, ctypes.c_int(0))
return enc_p
# ---- 63-byte SetStateData, verbatim from firmware src/state_mgr.cpp ----
STATE_DATA = bytes([
0xfd, 0xf7, 0x00, 0x00,
0x7f, 0x64, # VolHeadphonesMax, VolSpeaker
0x40, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a,
0x07, 0x00, 0x00, 0x02, 0x01,
0x00,
0xff, 0xd7, 0x00, # RGB
]) + bytes(16) # trailing zeros -> 63 total
assert len(STATE_DATA) == 63
# ---- CRC-32, 0xA2-seeded, verbatim from firmware src/utils.h ----
def ds_crc32(data):
crc = (~0xEADA2D49) & 0xFFFFFFFF
for b in data:
crc ^= b
for _ in range(8):
if crc & 1: crc = (crc >> 1) ^ 0xEDB88320
else: crc >>= 1
return (~crc) & 0xFFFFFFFF
REPORT_SIZE, SAMPLE_SIZE = 398, 64 # original firmware layout
def build_packet(payload, seq, counter, headset, audbuf):
pkt = bytearray(REPORT_SIZE)
pkt[0] = 0x36
pkt[1] = (seq & 0x0F) << 4
pkt[2] = 0x11 | 0x80 # 0x91
pkt[3] = 7
pkt[4] = 0xFE # audio enable, mic OFF
for i in range(5, 10): pkt[i] = audbuf & 0xFF
pkt[10] = counter & 0xFF
pkt[11] = 0x10 | 0x80 # 0x90 SetStateData
pkt[12] = 63
pkt[13:76] = STATE_DATA
pkt[76] = 0x12 | 0x80 # 0x92 haptic
pkt[77] = SAMPLE_SIZE # haptic data pkt[78..141] = 0 (silent)
pkt[142] = (0x16 if headset else 0x13) | 0x80 # 0x96 headset / 0x93 speaker
pkt[143] = OPUS_BYTES
pkt[144:144+OPUS_BYTES] = payload
struct.pack_into("<I", pkt, REPORT_SIZE-4, ds_crc32(pkt[:REPORT_SIZE-4]))
return bytes(pkt)
def find_ds5_hidraw():
for ue in glob.glob("/sys/class/hidraw/hidraw*/device/uevent"):
txt = open(ue).read().upper()
if "054C" in txt and ("0CE6" in txt or "0DF2" in txt):
return "/dev/" + ue.split("/")[4]
return None
def main():
headset = "--headset" in sys.argv
audbuf = int(sys.argv[sys.argv.index("--audbuf")+1]) if "--audbuf" in sys.argv else 64
pos = [a for a in sys.argv[1:] if not a.startswith("--") and a.isdigit()]
secs = int(pos[0]) if pos else 15
node = find_ds5_hidraw()
if not node:
sys.exit("DualSense hidraw node not found — paired & connected? (bluetoothctl)")
print(f"DS5 hidraw: {node} | output: {'HEADSET' if headset else 'SPEAKER'} | "
f"AudBuf={audbuf} | {secs}s")
enc = make_encoder()
pcm = (ctypes.c_float * (FRAME*CH))()
out = (ctypes.c_ubyte * OPUS_BYTES)()
# Pre-build every packet so the timed send loop is pure I/O (no compute jitter).
n_frames = secs * 100
packets, seq, counter, samp = [], 0, 0, 0
first_nb = None
for _ in range(n_frames):
for i in range(FRAME):
v = 0.5 * math.sin(2*math.pi*440.0*samp/RATE)
pcm[i*2] = v; pcm[i*2+1] = v; samp += 1
nb = opus.opus_encode_float(enc, pcm, FRAME, out, OPUS_BYTES)
if nb < 0: sys.exit(f"opus_encode_float error {nb}")
if first_nb is None: first_nb = nb
payload = bytes(out[:nb]) + bytes(OPUS_BYTES - nb) if nb < OPUS_BYTES else bytes(out[:OPUS_BYTES])
packets.append(build_packet(payload, seq, counter, headset, audbuf))
seq = (seq+1) & 0x0F; counter = (counter+1) & 0xFF
print(f"Encoded {len(packets)} frames (first frame {first_nb} B — expect ~200 for CBR). Streaming...")
fd = os.open(node, os.O_RDWR)
short = 0
t0 = time.monotonic()
for n, pkt in enumerate(packets):
try:
w = os.write(fd, pkt)
except OSError as e:
print(f"write failed at frame {n}: {e}"); break
if w != REPORT_SIZE: short += 1
target = t0 + (n+1)*0.01 # 10 ms grid -> 100 pkt/s
dt = target - time.monotonic()
if dt > 0: time.sleep(dt)
os.close(fd)
print(f"Done — {n+1} frames sent (~{(n+1)/100:.1f}s).", end="")
print(f" WARNING: {short} short writes (hidraw truncating the 398 B report!)" if short else " All writes full-length.")
if __name__ == "__main__":
main()
+45
View File
@@ -0,0 +1,45 @@
#!/usr/bin/env python3
"""Send a 440 Hz sine wave on channels 1+2 only (speaker), silence on 3+4 (haptic).
Usage: python3 scripts/sine_ch12.py [seconds]
"""
import subprocess, struct, math, sys, re
RATE = 48000
FREQ = 440
DURATION = int(sys.argv[1]) if len(sys.argv) > 1 else 5
CHANNELS = 4
SAMPLES = RATE * DURATION
# Generate 4-channel S16_LE: sine on ch1+ch2, silence on ch3+ch4
data = bytearray()
for i in range(SAMPLES):
val = int(6000 * math.sin(2 * math.pi * FREQ * i / RATE)) # ~-15 dBFS (was 32767 / 0 dBFS) — clipping test
s = struct.pack('<h', val)
data += s + s + b'\x00\x00' + b'\x00\x00' # L, R, hapL=0, hapR=0
# Auto-detect the dongle's ALSA card — it enumerates as "DualSense Wireless
# Controller", and the card number shifts across reboots, so never hardcode it.
def find_dualsense_card():
out = subprocess.check_output(['aplay', '-l'], text=True)
for line in out.splitlines():
m = re.match(r'card (\d+):', line)
if m and 'DualSense' in line:
return int(m.group(1))
return None
card = find_dualsense_card()
if card is None:
print("DualSense dongle not found in `aplay -l` — paired and enumerated?", file=sys.stderr)
sys.exit(1)
device = f'hw:{card},0'
print(f"Playing to {device} (DualSense dongle)")
proc = subprocess.Popen(
['aplay', '-D', device, '-f', 'S16_LE', '-c', '4', '-r', '48000', '-'],
stdin=subprocess.PIPE
)
proc.stdin.write(data)
proc.stdin.close()
proc.wait()
print(f"Played {DURATION}s of {FREQ} Hz sine on ch1+ch2 (speaker only)")
+51 -25
View File
@@ -20,7 +20,7 @@
#define INPUT_CHANNELS 4 #define INPUT_CHANNELS 4
#define OUTPUT_CHANNELS 2 #define OUTPUT_CHANNELS 2
#define SAMPLE_SIZE 64 #define SAMPLE_SIZE 60 // 60 B = 30 haptic frames @ 3 kHz = 10.00 ms/packet → true 100 Hz cadence
#define REPORT_SIZE 398 #define REPORT_SIZE 398
#define REPORT_ID 0x36 #define REPORT_ID 0x36
// #define VOLUME_GAIN 2 // #define VOLUME_GAIN 2
@@ -80,7 +80,7 @@ static volatile uint32_t g_mic_plc_frames = 0; // concealed frames genera
uint32_t audio_mic_plc_frames() { return g_mic_plc_frames; } uint32_t audio_mic_plc_frames() { return g_mic_plc_frames; }
struct audio_raw_element { struct audio_raw_element {
float data[512 * 2]; float data[480 * 2]; // exactly one 10 ms Opus frame (480 stereo samples)
}; };
void set_headset(bool state) { void set_headset(bool state) {
@@ -96,6 +96,10 @@ uint32_t opus_fifo_drops() { return 0; }
// emulator's USB / BT rate display. Updated below. // emulator's USB / BT rate display. Updated below.
static volatile uint32_t g_usb_frames = 0; static volatile uint32_t g_usb_frames = 0;
static volatile uint32_t g_bt_packets = 0; static volatile uint32_t g_bt_packets = 0;
static volatile int32_t g_opus_last_ret = 0;
static volatile uint32_t g_fifo_drops = 0;
static volatile uint32_t g_opus_encodes = 0;
static volatile bool g_opus_ready = false;
uint32_t audio_usb_frames() { return g_usb_frames; } uint32_t audio_usb_frames() { return g_usb_frames; }
uint32_t audio_bt_packets() { return g_bt_packets; } uint32_t audio_bt_packets() { return g_bt_packets; }
@@ -258,7 +262,7 @@ void audio_loop() {
} }
g_usb_frames += (uint32_t)frames; g_usb_frames += (uint32_t)frames;
static float audio_buf[512 * 2]; static float audio_buf[480 * 2];
static uint audio_buf_pos = 0; static uint audio_buf_pos = 0;
// 2. 从4ch中提取ch3/ch4,转换为float输入重采样器 // 2. 从4ch中提取ch3/ch4,转换为float输入重采样器
WDL_ResampleSample *in_buf; WDL_ResampleSample *in_buf;
@@ -306,11 +310,12 @@ void audio_loop() {
#if !DISABLE_SPEAKER_PROC #if !DISABLE_SPEAKER_PROC
audio_buf[audio_buf_pos++] = raw[i * INPUT_CHANNELS] / 32768.0f * audio_gain; audio_buf[audio_buf_pos++] = raw[i * INPUT_CHANNELS] / 32768.0f * audio_gain;
audio_buf[audio_buf_pos++] = raw[i * INPUT_CHANNELS + 1] / 32768.0f * audio_gain; audio_buf[audio_buf_pos++] = raw[i * INPUT_CHANNELS + 1] / 32768.0f * audio_gain;
if (audio_buf_pos == 512 * 2) { if (audio_buf_pos == 480 * 2) {
static audio_raw_element element{}; static audio_raw_element element{};
memcpy(element.data, audio_buf, 512 * 2 * 4); memcpy(element.data, audio_buf, 480 * 2 * 4);
if (queue_is_full(&audio_fifo)) { if (queue_is_full(&audio_fifo)) {
queue_try_remove(&audio_fifo,NULL); queue_try_remove(&audio_fifo,NULL);
g_fifo_drops++;
} }
if (!queue_try_add(&audio_fifo, &element)) { if (!queue_try_add(&audio_fifo, &element)) {
printf("[Audio] Warning: audio_fifo add failed\n"); printf("[Audio] Warning: audio_fifo add failed\n");
@@ -402,20 +407,45 @@ void audio_loop() {
pkt[77] = SAMPLE_SIZE; pkt[77] = SAMPLE_SIZE;
memcpy(pkt + 78, haptic_buf, SAMPLE_SIZE); memcpy(pkt + 78, haptic_buf, SAMPLE_SIZE);
#if !DISABLE_SPEAKER_PROC #if !DISABLE_SPEAKER_PROC
// Speaker Audio Data // Speaker Audio Data — MUST immediately follow the haptic block. The DS5
pkt[142] = (plug_headset ? 0x16 : 0x13) | 0 << 6 | 1 << 7; // Speaker: 0x13 // parses sub-reports sequentially (header + len + data), so this offset is
// 78 + SAMPLE_SIZE, NOT a fixed 142. At SAMPLE_SIZE 64 that worked out to
// 142; shrinking the haptic block to 60 without moving this is what
// silenced the speaker (controller couldn't locate the speaker sub-report).
constexpr int kSpkOff = 78 + SAMPLE_SIZE; // = 138 at SAMPLE_SIZE 60
pkt[kSpkOff] = (plug_headset ? 0x16 : 0x13) | 0 << 6 | 1 << 7; // Speaker: 0x13
// L Headset Mono: 0x14 // L Headset Mono: 0x14
// L Headset R Speaker: 0x15 // L Headset R Speaker: 0x15
// Headset: 0x16 // Headset: 0x16
pkt[143] = 200; pkt[kSpkOff + 1] = 200;
critical_section_enter_blocking(&opus_cs); critical_section_enter_blocking(&opus_cs);
memcpy(pkt + 144, opus_buf, 200); memcpy(pkt + kSpkOff + 2, opus_buf, 200);
critical_section_exit(&opus_cs); critical_section_exit(&opus_cs);
#endif #endif
bt_write(pkt, sizeof(pkt)); bt_write(pkt, sizeof(pkt));
g_bt_packets++; g_bt_packets++;
haptic_buf_pos = 0; haptic_buf_pos = 0;
// Debug: dump 5 consecutive Opus frames (skip first 10 to let encoder settle)
{
static int dump_count = 0;
if (g_bt_packets > 10 && dump_count < 5) {
dump_count++;
printf("[OPUS_FRAME_%d] ", dump_count);
critical_section_enter_blocking(&opus_cs);
for (int di = 0; di < 200; di++) printf("%02x", opus_buf[di]);
critical_section_exit(&opus_cs);
printf("\n");
}
if ((g_bt_packets % 94) == 0) {
printf("[AUD] usb=%lu enc=%lu bt=%lu opus_ret=%ld fifo_drop=%lu hs=%d\n",
(unsigned long)g_usb_frames, (unsigned long)g_opus_encodes,
(unsigned long)g_bt_packets, (long)g_opus_last_ret,
(unsigned long)g_fifo_drops,
plug_headset ? 1 : 0);
}
}
} }
} }
@@ -443,7 +473,6 @@ void audio_init() {
} }
static OpusEncoder *encoder; static OpusEncoder *encoder;
static WDL_Resampler resampler_audio;
void core1_entry() { void core1_entry() {
int error = 0; int error = 0;
@@ -455,28 +484,25 @@ void core1_entry() {
opus_encoder_ctl(encoder,OPUS_SET_EXPERT_FRAME_DURATION(OPUS_FRAMESIZE_10_MS)); opus_encoder_ctl(encoder,OPUS_SET_EXPERT_FRAME_DURATION(OPUS_FRAMESIZE_10_MS));
opus_encoder_ctl(encoder,OPUS_SET_BITRATE(200 * 8 * 100)); opus_encoder_ctl(encoder,OPUS_SET_BITRATE(200 * 8 * 100));
opus_encoder_ctl(encoder,OPUS_SET_VBR(false)); opus_encoder_ctl(encoder,OPUS_SET_VBR(false));
opus_encoder_ctl(encoder,OPUS_SET_COMPLEXITY(0)); // max 4 opus_encoder_ctl(encoder,OPUS_SET_COMPLEXITY(0)); // 5 overloaded core1 (stale frames -> worse); 0 keeps up
resampler_audio.SetMode(true, 0, false);
resampler_audio.SetRates(51200, 48000);
resampler_audio.SetFeedMode(true);
resampler_audio.Prealloc(2, 512, 480);
while (true) { while (true) {
static audio_raw_element audio_element{}; static audio_raw_element audio_element{};
queue_remove_blocking(&audio_fifo, &audio_element); queue_remove_blocking(&audio_fifo, &audio_element);
// 将 512 frames 重采样成 480 frames 以解决噪音问题。感谢 @Junhoo // audio_element is exactly 480 stereo frames (10 ms @ 48 kHz) = one native
WDL_ResampleSample *in_buf; // Opus frame, so encode it directly. The old 512→480 (51200→48000) resample
int nframes = resampler_audio.ResamplePrepare(512, 2, &in_buf); // only existed to coerce a 512-sample buffer into a legal Opus frame size; it
for (int i = 0; i < nframes * 2; i++) { // shipped 480 samples every 10.667 ms (haptic-gated cadence) = 45 kHz into the
in_buf[i] = audio_element.data[i]; // DS5's free-running 48 kHz DAC → ~6.25% underrun = the periodic gaps/crackle.
} // SAMPLE_SIZE 60 + a 480-sample buffer put the whole 0x36 frame on a true
static WDL_ResampleSample out_buf[480 * 2]; // 10 ms / 100 Hz grid: 100 × 480 = 48000 samples/s, matched, no gaps.
resampler_audio.ResampleOut(out_buf, nframes, 480, 2);
static uint8_t out[200]; static uint8_t out[200];
(void) opus_encode_float(encoder, out_buf, 480, out, 200); int enc_ret = opus_encode_float(encoder, audio_element.data, 480, out, 200);
g_opus_last_ret = enc_ret;
g_opus_encodes++;
critical_section_enter_blocking(&opus_cs); critical_section_enter_blocking(&opus_cs);
memcpy(opus_buf, out, 200); memcpy(opus_buf, out, 200);
critical_section_exit(&opus_cs); critical_section_exit(&opus_cs);
g_opus_ready = true;
} }
} }
+11 -3
View File
@@ -72,7 +72,7 @@ void config_valid() {
printf("[Config] polling_rate_mode is invalid\n"); printf("[Config] polling_rate_mode is invalid\n");
} }
if (body->audio_buffer_length < 16 || body->audio_buffer_length > 128) { if (body->audio_buffer_length < 16 || body->audio_buffer_length > 128) {
body->audio_buffer_length = 64; body->audio_buffer_length = 16; // low buffer avoids the DS5's periodic re-buffer gap
printf("[Config] haptics_buffer_length is invalid\n"); printf("[Config] haptics_buffer_length is invalid\n");
} }
if (body->controller_mode > 2) { if (body->controller_mode > 2) {
@@ -84,8 +84,8 @@ void config_valid() {
printf("[Config] current_slot is invalid\n"); printf("[Config] current_slot is invalid\n");
} }
if (body->auto_haptics_enable > 3) { if (body->auto_haptics_enable > 3) {
body->auto_haptics_enable = 1; // Fallback default body->auto_haptics_enable = 0; // default OFF (was 1/Fallback) — it derives erratic rumble from the speaker
printf("[Config] auto_haptics_enable invalid, defaulting to 1 (Fallback)\n"); printf("[Config] auto_haptics_enable invalid, defaulting to 0 (Off)\n");
} }
if (body->auto_haptics_gain > 200) { if (body->auto_haptics_gain > 200) {
body->auto_haptics_gain = 100; body->auto_haptics_gain = 100;
@@ -113,6 +113,14 @@ void config_valid() {
body->bt_mic_enable = 1; body->bt_mic_enable = 1;
printf("[Config] bt_mic_enable invalid, defaulting to 1 (on)\n"); printf("[Config] bt_mic_enable invalid, defaulting to 1 (on)\n");
} }
if (body->screen_brightness > 3) { // kBrightLevels has 4 entries (0..3)
body->screen_brightness = 0; // full brightness
printf("[Config] screen_brightness invalid, defaulting to 0 (full)\n");
}
if (body->controller_wakes_display > 1) { // 0xFF erased / upgrade → default ON
body->controller_wakes_display = 1;
printf("[Config] controller_wakes_display 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");
+10
View File
@@ -44,6 +44,16 @@ struct __attribute__((packed)) Config_body {
// over BT and the dongle decodes it to the USB capture endpoint. Costs extra // over BT and the dongle decodes it to the USB capture endpoint. Costs extra
// DS5 battery (keeps its audio subsystem awake), hence the toggle. // DS5 battery (keeps its audio subsystem awake), hence the toggle.
uint8_t bt_mic_enable; uint8_t bt_mic_enable;
// OLED brightness, as an index into kBrightLevels[] (src/oled.cpp). Persisted
// so the KEY1-long-press brightness choice survives a power cycle. Erased
// flash (0xFF) → clamped to 0 (full brightness) by config_valid. Issue #9.
uint8_t screen_brightness;
// When 0, controller input no longer keeps the OLED awake — only the OLED's
// own KEY0/KEY1 do — so the dim/off timers actually count down during
// gameplay and the panel can sleep while the controller is in use. Default 1
// preserves the original "any controller activity wakes the screen"
// behavior. Issues #8 (dim timeout never fired during play) and #9.
uint8_t controller_wakes_display;
}; };
struct __attribute__((packed)) Config { struct __attribute__((packed)) Config {
+84 -15
View File
@@ -52,6 +52,10 @@ constexpr int kRowBytes = kW / 8;
constexpr int kFbBytes = kRowBytes * kH; constexpr int kFbBytes = kRowBytes * kH;
uint8_t fb[kFbBytes]; uint8_t fb[kFbBytes];
uint8_t fb_tx[kFbBytes];
uint8_t reverse_lut[256];
int flush_progress = -1;
constexpr int kFlushChunkRows = 8;
uint32_t last_render_us = 0; uint32_t last_render_us = 0;
constexpr uint32_t kFrameUs = 100000; constexpr uint32_t kFrameUs = 100000;
@@ -126,15 +130,16 @@ constexpr int kLbModeHost = 8;
constexpr int kNumLbModes = 9; constexpr int kNumLbModes = 9;
// Settings screen state // Settings screen state
constexpr int kNumSettingsItems = 16; // 8 fields + 3 auto-haptic + 2 screen-timeout + BT mic + Reset + Wipe constexpr int kNumSettingsItems = 17; // 8 fields + 3 auto-haptic + 2 screen-timeout + BT mic + Ctrl-wake + 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 kSettingsBtMicIdx = 13; constexpr int kSettingsBtMicIdx = 13;
constexpr int kSettingsResetIdx = 14; constexpr int kSettingsCtrlWakeIdx = 14;
constexpr int kSettingsWipeSlotsIdx = 15; constexpr int kSettingsResetIdx = 15;
constexpr int kSettingsWipeSlotsIdx = 16;
Config_body settings_local{}; Config_body settings_local{};
int settings_sel = 0; int settings_sel = 0;
bool settings_dirty = false; bool settings_dirty = false;
@@ -236,7 +241,9 @@ void sh1107_init() {
// lives near the other text-drawing helpers below. // lives near the other text-drawing helpers below.
void draw_button_chrome(); void draw_button_chrome();
void flush_fb_raw() { // Blocking SPI flush — used only during boot splash before the main loop
// starts (no audio/BT to service yet, so blocking is fine).
void flush_fb_raw_blocking() {
cmd(0xB0); cmd(0xB0);
for (int j = 0; j < kH; j++) { for (int j = 0; j < kH; j++) {
const uint8_t col = kH - 1 - j; const uint8_t col = kH - 1 - j;
@@ -248,11 +255,45 @@ void flush_fb_raw() {
} }
} }
void flush_fb() { // Prepare a chunked (non-blocking) SPI flush. Pre-reverses the framebuffer
draw_button_chrome(); // into fb_tx via the LUT and sets flush_progress = 0. The actual SPI
flush_fb_raw(); // transfer is driven by flush_chunk(), called from oled_loop() on
// subsequent iterations — each chunk flushes kFlushChunkRows rows (~130 µs)
// then yields back to the main loop so audio_loop / tud_task /
// cyw43_arch_poll stay serviced. This eliminates the ~1.1 ms blocking
// window that caused audio distortion when the OLED was active (issue #7).
void flush_prepare(bool chrome) {
if (chrome) draw_button_chrome();
for (int i = 0; i < kFbBytes; i++) fb_tx[i] = reverse_lut[fb[i]];
flush_progress = 0;
} }
bool flush_chunk() {
if (flush_progress < 0) return true;
if (flush_progress == 0) cmd(0xB0);
const int end = (flush_progress + kFlushChunkRows < kH)
? flush_progress + kFlushChunkRows : kH;
for (int j = flush_progress; j < end; j++) {
const uint8_t col = kH - 1 - j;
cmd(0x00 + (col & 0x0F));
cmd(0x10 + (col >> 4));
gpio_put(kPinDC, 1);
gpio_put(kPinCS, 0);
spi_write_blocking(spi1, &fb_tx[j * kRowBytes], kRowBytes);
gpio_put(kPinCS, 1);
}
flush_progress = end;
if (flush_progress >= kH) {
flush_progress = -1;
return true;
}
return false;
}
void flush_fb_raw() { flush_prepare(false); }
void flush_fb() { flush_prepare(true); }
void fb_clear() { memset(fb, 0, sizeof(fb)); } void fb_clear() { memset(fb, 0, sizeof(fb)); }
void px(int x, int y, bool on) { void px(int x, int y, bool on) {
@@ -521,6 +562,14 @@ void handle_buttons() {
last_activity_us = time_us_64(); last_activity_us = time_us_64();
if (held > kLongPressUs) { if (held > kLongPressUs) {
bright_idx = (bright_idx + 1) % kNumBrightLevels; bright_idx = (bright_idx + 1) % kNumBrightLevels;
// Persist so the choice survives a power cycle (issue #9). Keep
// settings_local in sync too, so a later Settings-screen save can't
// clobber screen_brightness with its stale snapshot.
Config_body b = get_config();
b.screen_brightness = (uint8_t)bright_idx;
set_config(b);
config_save();
settings_local.screen_brightness = (uint8_t)bright_idx;
} else { } else {
current_screen = (current_screen - 1 + kNumScreens) % kNumScreens; current_screen = (current_screen - 1 + kNumScreens) % kNumScreens;
last_render_us = 0; last_render_us = 0;
@@ -533,8 +582,10 @@ void handle_buttons() {
// --- Charge ETA tracker -------------------------------------------------- // --- Charge ETA tracker --------------------------------------------------
// The DS5 only reports battery in 10% steps (interrupt_in_data[52] low // The DS5 only reports battery in 10% steps (interrupt_in_data[52] low
// nibble, 0..10; high nibble is power-state, 1 == charging). We can't read a // nibble, 0..10; high nibble is power-state, 1 == charging). We display the
// finer percentage over BT, so a smooth countdown is impossible. Instead we // midpoint of each band (+5), matching the kernel hid-playstation driver and
// Steam: 5, 15, 25, … 95, 100%. We can't read a finer percentage over BT,
// so a smooth countdown is impossible. Instead we
// time how long each 10% step takes while charging and extrapolate the // time how long each 10% step takes while charging and extrapolate the
// remaining steps. Sampled once per frame from oled_loop (continuously, so // remaining steps. Sampled once per frame from oled_loop (continuously, so
// the estimate stays current even while the panel is dimmed/off and even when // the estimate stays current even while the panel is dimmed/off and even when
@@ -692,8 +743,8 @@ __attribute__((noinline)) void render_screen() {
draw_text(kContentX, 9, buf); draw_text(kContentX, 9, buf);
const uint8_t pwr = interrupt_in_data[52]; const uint8_t pwr = interrupt_in_data[52];
int pct = (pwr & 0x0F) * 10; int raw = pwr & 0x0F;
if (pct > 100) pct = 100; int pct = (raw >= 10) ? 100 : raw * 10 + 5;
const uint8_t pstate = pwr >> 4; const uint8_t pstate = pwr >> 4;
char marker = ' '; char marker = ' ';
if (pstate == 1) marker = '+'; // Charging if (pstate == 1) marker = '+'; // Charging
@@ -1547,6 +1598,7 @@ void settings_adjust(int delta) {
break; break;
} }
case 13: c.bt_mic_enable ^= 1; break; // BT mic on/off case 13: c.bt_mic_enable ^= 1; break; // BT mic on/off
case 14: c.controller_wakes_display ^= 1; break; // controller activity wakes OLED on/off
} }
} }
@@ -1649,8 +1701,9 @@ __attribute__((noinline)) void format_settings_item(int idx, char* line, size_t
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 BT Mic %s", cur, c.bt_mic_enable ? "on" : "off"); break; case 13: snprintf(line, n, "%s BT Mic %s", cur, c.bt_mic_enable ? "on" : "off"); break;
case 14: snprintf(line, n, "%s Reset to defaults", cur); break; case 14: snprintf(line, n, "%s CtrlWake %s", cur, c.controller_wakes_display ? "on" : "off"); break;
case 15: snprintf(line, n, "%s Wipe all slots", cur); break; case 15: snprintf(line, n, "%s Reset to defaults", cur); break;
case 16: snprintf(line, n, "%s Wipe all slots", cur); break;
} }
} }
@@ -1796,13 +1849,15 @@ void boot_splash() {
draw_text(cx_for(l1), 16, l1); draw_text(cx_for(l1), 16, l1);
draw_text(cx_for(l2), 30, l2); draw_text(cx_for(l2), 30, l2);
draw_text(cx_for(l3), 44, l3); draw_text(cx_for(l3), 44, l3);
flush_fb(); draw_button_chrome();
flush_fb_raw_blocking();
sleep_ms(1500); sleep_ms(1500);
} }
} // namespace } // namespace
void oled_init() { void oled_init() {
for (int i = 0; i < 256; i++) reverse_lut[i] = reverse_byte((uint8_t)i);
spi_init(spi1, 10 * 1000 * 1000); spi_init(spi1, 10 * 1000 * 1000);
gpio_set_function(kPinCLK, GPIO_FUNC_SPI); gpio_set_function(kPinCLK, GPIO_FUNC_SPI);
gpio_set_function(kPinMOSI, GPIO_FUNC_SPI); gpio_set_function(kPinMOSI, GPIO_FUNC_SPI);
@@ -1822,6 +1877,11 @@ void oled_init() {
// Restore the persisted lightbar mode + favorites (config_load() already ran // Restore the persisted lightbar mode + favorites (config_load() already ran
// in main() before this). Defaults to HOST passthrough on a fresh flash. // in main() before this). Defaults to HOST passthrough on a fresh flash.
lightbar_load_config(); lightbar_load_config();
// Restore the persisted OLED brightness (KEY1-long-press choice). config_valid
// clamps screen_brightness to a legal kBrightLevels index, so this is safe to
// use directly. Fresh flash → 0 (full brightness). Issue #9.
bright_idx = get_config().screen_brightness;
} }
// Dim-tier renderer: blank the panel and draw a tiny "I'm alive" dot that // Dim-tier renderer: blank the panel and draw a tiny "I'm alive" dot that
@@ -1858,6 +1918,10 @@ void oled_loop() {
handle_buttons(); handle_buttons();
const uint32_t now = time_us_32(); const uint32_t now = time_us_32();
rumble_burst_tick(now); rumble_burst_tick(now);
if (flush_progress >= 0) {
flush_chunk();
return;
}
if ((now - last_render_us) < kFrameUs) return; if ((now - last_render_us) < kFrameUs) return;
last_render_us = now; last_render_us = now;
// Track charge progress every frame — before the power-ladder early-returns // Track charge progress every frame — before the power-ladder early-returns
@@ -1885,7 +1949,12 @@ void oled_loop() {
} }
if (hash != last_input_hash) { if (hash != last_input_hash) {
last_input_hash = hash; last_input_hash = hash;
last_activity_us = time_us_64(); // Controller input only keeps the panel awake when the user has left
// "CtrlWake" on (the default). With it off, the dim/off timers count
// down during gameplay and only KEY0/KEY1 wake the screen — see
// handle_buttons(), which bumps last_activity_us unconditionally.
// Issues #8 / #9.
if (get_config().controller_wakes_display) last_activity_us = time_us_64();
} }
// Rising-edge: BT-connect itself counts as activity, so the screen wakes // Rising-edge: BT-connect itself counts as activity, so the screen wakes
// the moment a controller pairs rather than waiting for the first input. // the moment a controller pairs rather than waiting for the first input.
+10
View File
@@ -82,6 +82,16 @@ void state_update(const uint8_t *data, const uint8_t size) {
set_bit(state[0], 0, update.EnableRumbleEmulation); set_bit(state[0], 0, update.EnableRumbleEmulation);
set_bit(state[0], 1, update.UseRumbleNotHaptics); set_bit(state[0], 1, update.UseRumbleNotHaptics);
// Mirror the host's adaptive-trigger "apply" flags into the outgoing state.
// Without these, copy_if_allowed below writes the 11-byte FFB params into
// state[] but the DS5 receives them with AllowRight/LeftTriggerFFB cleared
// and discards them — so adaptive triggers were dead through the dongle on
// BOTH the standalone 0x31 path and the 0x36 audio fold, while direct USB
// (which carries the host's flags verbatim) worked. The on-device Trigger
// Test screen sets these same bits (0x0C) directly, which is why it worked.
// (issue #6)
set_bit(state[0], 2, update.AllowRightTriggerFFB);
set_bit(state[0], 3, update.AllowLeftTriggerFFB);
set_bit(state[38], 2, update.EnableImprovedRumbleEmulation); set_bit(state[38], 2, update.EnableImprovedRumbleEmulation);
copy_if_allowed( copy_if_allowed(
update.UseRumbleNotHaptics || update.EnableRumbleEmulation, update.UseRumbleNotHaptics || update.EnableRumbleEmulation,