Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
333201e1ec | ||
|
|
878a742bfc | ||
|
|
84393c00e8 | ||
|
|
71cead401d | ||
|
|
8fc369b1af |
@@ -8,6 +8,38 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Version
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Changed
|
||||
|
||||
- **`polling_rate_mode` now defaults to realtime / 1000 Hz** (was 250 Hz) so a fresh flash matches a wired DualSense's 1 ms input latency instead of polling 4× slower. Mode 2 also bypasses the report-throttling used by the 250/500 Hz modes, forwarding every BT input report immediately. Existing saved configs keep their value; this only affects fresh flashes / Reset-to-defaults. (Out-of-box "acts like USB" pass.)
|
||||
- **`bt_mic_enable` now defaults to Off** (was On) — the DualSense BT microphone path has a confirmed playback-rate bug (issue #10: recordings come out ~2× too fast / half-duration). Until that's root-caused on hardware, the mic is opt-in: fresh flashes / Reset-to-defaults boot with it off, and it can be turned on from the OLED **Settings** screen (`BT Mic`) or the web config. Existing saved configs keep their current value. (Web config default should be flipped to match in `DS5Dongle-OLED-Config-Web`.)
|
||||
|
||||
### Known issues
|
||||
|
||||
- **BT microphone records ~2× too fast (#10).** The mic playout stage is wall-clock paced at 10 ms / 480 samples into a 48 kHz endpoint, which should be real-time, so the 2× points at the on-wire frame size/rate differing from the assumed 480 samples. Needs the OLED **Diagnostics** `Mic dec=` (samples per `opus_decode`) and `Mic in/s` (arrival rate) read on hardware to make the fix deterministic — `Mic dec=240` would confirm a half-rate stream. Mic is opt-in (off by default) in the meantime.
|
||||
|
||||
---
|
||||
|
||||
## [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
|
||||
|
||||
+2
-2
@@ -264,7 +264,7 @@ scripts/mic_diag.sh bt-trace
|
||||
|
||||
#### 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">
|
||||
|
||||
@@ -299,7 +299,7 @@ scripts/mic_diag.sh bt-trace
|
||||
|
||||
#### 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">
|
||||
|
||||
|
||||
@@ -265,7 +265,7 @@ Every screen also paints **`>`** at the top-left edge (next to KEY0) and **`<`**
|
||||
|
||||
#### 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">
|
||||
|
||||
@@ -300,7 +300,7 @@ Cycle order: **Off → Feedback → Weapon → Vibration → Bow → Gallop →
|
||||
|
||||
#### 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">
|
||||
|
||||
|
||||
@@ -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")
|
||||
@@ -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()
|
||||
@@ -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
@@ -20,7 +20,7 @@
|
||||
|
||||
#define INPUT_CHANNELS 4
|
||||
#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_ID 0x36
|
||||
// #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; }
|
||||
|
||||
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) {
|
||||
@@ -96,6 +96,10 @@ uint32_t opus_fifo_drops() { return 0; }
|
||||
// emulator's USB / BT rate display. Updated below.
|
||||
static volatile uint32_t g_usb_frames = 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_bt_packets() { return g_bt_packets; }
|
||||
|
||||
@@ -258,7 +262,7 @@ void audio_loop() {
|
||||
}
|
||||
g_usb_frames += (uint32_t)frames;
|
||||
|
||||
static float audio_buf[512 * 2];
|
||||
static float audio_buf[480 * 2];
|
||||
static uint audio_buf_pos = 0;
|
||||
// 2. 从4ch中提取ch3/ch4,转换为float输入重采样器
|
||||
WDL_ResampleSample *in_buf;
|
||||
@@ -306,11 +310,12 @@ void audio_loop() {
|
||||
#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 + 1] / 32768.0f * audio_gain;
|
||||
if (audio_buf_pos == 512 * 2) {
|
||||
if (audio_buf_pos == 480 * 2) {
|
||||
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)) {
|
||||
queue_try_remove(&audio_fifo,NULL);
|
||||
g_fifo_drops++;
|
||||
}
|
||||
if (!queue_try_add(&audio_fifo, &element)) {
|
||||
printf("[Audio] Warning: audio_fifo add failed\n");
|
||||
@@ -402,20 +407,45 @@ void audio_loop() {
|
||||
pkt[77] = SAMPLE_SIZE;
|
||||
memcpy(pkt + 78, haptic_buf, SAMPLE_SIZE);
|
||||
#if !DISABLE_SPEAKER_PROC
|
||||
// Speaker Audio Data
|
||||
pkt[142] = (plug_headset ? 0x16 : 0x13) | 0 << 6 | 1 << 7; // Speaker: 0x13
|
||||
// Speaker Audio Data — MUST immediately follow the haptic block. The DS5
|
||||
// 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 R Speaker: 0x15
|
||||
// Headset: 0x16
|
||||
pkt[143] = 200;
|
||||
pkt[kSpkOff + 1] = 200;
|
||||
critical_section_enter_blocking(&opus_cs);
|
||||
memcpy(pkt + 144, opus_buf, 200);
|
||||
memcpy(pkt + kSpkOff + 2, opus_buf, 200);
|
||||
critical_section_exit(&opus_cs);
|
||||
#endif
|
||||
|
||||
bt_write(pkt, sizeof(pkt));
|
||||
g_bt_packets++;
|
||||
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 WDL_Resampler resampler_audio;
|
||||
|
||||
void core1_entry() {
|
||||
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_BITRATE(200 * 8 * 100));
|
||||
opus_encoder_ctl(encoder,OPUS_SET_VBR(false));
|
||||
opus_encoder_ctl(encoder,OPUS_SET_COMPLEXITY(0)); // max 4
|
||||
resampler_audio.SetMode(true, 0, false);
|
||||
resampler_audio.SetRates(51200, 48000);
|
||||
resampler_audio.SetFeedMode(true);
|
||||
resampler_audio.Prealloc(2, 512, 480);
|
||||
opus_encoder_ctl(encoder,OPUS_SET_COMPLEXITY(0)); // 5 overloaded core1 (stale frames -> worse); 0 keeps up
|
||||
|
||||
while (true) {
|
||||
static audio_raw_element audio_element{};
|
||||
queue_remove_blocking(&audio_fifo, &audio_element);
|
||||
// 将 512 frames 重采样成 480 frames 以解决噪音问题。感谢 @Junhoo
|
||||
WDL_ResampleSample *in_buf;
|
||||
int nframes = resampler_audio.ResamplePrepare(512, 2, &in_buf);
|
||||
for (int i = 0; i < nframes * 2; i++) {
|
||||
in_buf[i] = audio_element.data[i];
|
||||
}
|
||||
static WDL_ResampleSample out_buf[480 * 2];
|
||||
resampler_audio.ResampleOut(out_buf, nframes, 480, 2);
|
||||
|
||||
// audio_element is exactly 480 stereo frames (10 ms @ 48 kHz) = one native
|
||||
// Opus frame, so encode it directly. The old 512→480 (51200→48000) resample
|
||||
// only existed to coerce a 512-sample buffer into a legal Opus frame size; it
|
||||
// shipped 480 samples every 10.667 ms (haptic-gated cadence) = 45 kHz into the
|
||||
// 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
|
||||
// 10 ms / 100 Hz grid: 100 × 480 = 48000 samples/s, matched, no gaps.
|
||||
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);
|
||||
memcpy(opus_buf, out, 200);
|
||||
critical_section_exit(&opus_cs);
|
||||
g_opus_ready = true;
|
||||
}
|
||||
}
|
||||
|
||||
+23
-9
@@ -67,12 +67,14 @@ void config_valid() {
|
||||
body->disable_pico_led = 0;
|
||||
printf("[Config] disable_pico_led is invalid\n");
|
||||
}
|
||||
if (body->polling_rate_mode > 2) {
|
||||
body->polling_rate_mode = 0;
|
||||
printf("[Config] polling_rate_mode is invalid\n");
|
||||
if (body->polling_rate_mode > 2) { // 0xFF erased / out of range → default
|
||||
body->polling_rate_mode = 2; // realtime / 1000 Hz — match a wired DS5's
|
||||
// 1 ms polling (was 0 / 250 Hz). Existing
|
||||
// saved configs keep their value.
|
||||
printf("[Config] polling_rate_mode invalid, defaulting to 2 (1000 Hz)\n");
|
||||
}
|
||||
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");
|
||||
}
|
||||
if (body->controller_mode > 2) {
|
||||
@@ -84,8 +86,8 @@ void config_valid() {
|
||||
printf("[Config] current_slot is invalid\n");
|
||||
}
|
||||
if (body->auto_haptics_enable > 3) {
|
||||
body->auto_haptics_enable = 1; // Fallback default
|
||||
printf("[Config] auto_haptics_enable invalid, defaulting to 1 (Fallback)\n");
|
||||
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 0 (Off)\n");
|
||||
}
|
||||
if (body->auto_haptics_gain > 200) {
|
||||
body->auto_haptics_gain = 100;
|
||||
@@ -109,9 +111,21 @@ void config_valid() {
|
||||
body->screen_off_timeout = 15; // mirrors the original 15-min off tier
|
||||
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->bt_mic_enable > 1) { // 0xFF erased / upgrade → default OFF
|
||||
body->bt_mic_enable = 0; // opt-in: the BT mic path has a known
|
||||
// 2x-playback-rate bug (#10), so it is
|
||||
// off until explicitly enabled (Settings
|
||||
// screen / web config). Existing saved
|
||||
// configs keep their value.
|
||||
printf("[Config] bt_mic_enable invalid, defaulting to 0 (off)\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) {
|
||||
body->config_version = CONFIG_VERSION;
|
||||
|
||||
@@ -44,6 +44,16 @@ struct __attribute__((packed)) Config_body {
|
||||
// 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;
|
||||
// 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 {
|
||||
|
||||
+84
-15
@@ -52,6 +52,10 @@ constexpr int kRowBytes = kW / 8;
|
||||
constexpr int kFbBytes = kRowBytes * kH;
|
||||
|
||||
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;
|
||||
constexpr uint32_t kFrameUs = 100000;
|
||||
@@ -126,15 +130,16 @@ constexpr int kLbModeHost = 8;
|
||||
constexpr int kNumLbModes = 9;
|
||||
|
||||
// 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 kSettingsAutoHapGainIdx = 9;
|
||||
constexpr int kSettingsAutoHapLpIdx = 10;
|
||||
constexpr int kSettingsScrDimIdx = 11;
|
||||
constexpr int kSettingsScrOffIdx = 12;
|
||||
constexpr int kSettingsBtMicIdx = 13;
|
||||
constexpr int kSettingsResetIdx = 14;
|
||||
constexpr int kSettingsWipeSlotsIdx = 15;
|
||||
constexpr int kSettingsCtrlWakeIdx = 14;
|
||||
constexpr int kSettingsResetIdx = 15;
|
||||
constexpr int kSettingsWipeSlotsIdx = 16;
|
||||
Config_body settings_local{};
|
||||
int settings_sel = 0;
|
||||
bool settings_dirty = false;
|
||||
@@ -236,7 +241,9 @@ void sh1107_init() {
|
||||
// lives near the other text-drawing helpers below.
|
||||
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);
|
||||
for (int j = 0; j < kH; j++) {
|
||||
const uint8_t col = kH - 1 - j;
|
||||
@@ -248,11 +255,45 @@ void flush_fb_raw() {
|
||||
}
|
||||
}
|
||||
|
||||
void flush_fb() {
|
||||
draw_button_chrome();
|
||||
flush_fb_raw();
|
||||
// Prepare a chunked (non-blocking) SPI flush. Pre-reverses the framebuffer
|
||||
// into fb_tx via the LUT and sets flush_progress = 0. The actual SPI
|
||||
// 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 px(int x, int y, bool on) {
|
||||
@@ -521,6 +562,14 @@ void handle_buttons() {
|
||||
last_activity_us = time_us_64();
|
||||
if (held > kLongPressUs) {
|
||||
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 {
|
||||
current_screen = (current_screen - 1 + kNumScreens) % kNumScreens;
|
||||
last_render_us = 0;
|
||||
@@ -533,8 +582,10 @@ void handle_buttons() {
|
||||
|
||||
// --- Charge ETA tracker --------------------------------------------------
|
||||
// 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
|
||||
// finer percentage over BT, so a smooth countdown is impossible. Instead we
|
||||
// nibble, 0..10; high nibble is power-state, 1 == charging). We display the
|
||||
// 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
|
||||
// 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
|
||||
@@ -692,8 +743,8 @@ __attribute__((noinline)) void render_screen() {
|
||||
draw_text(kContentX, 9, buf);
|
||||
|
||||
const uint8_t pwr = interrupt_in_data[52];
|
||||
int pct = (pwr & 0x0F) * 10;
|
||||
if (pct > 100) pct = 100;
|
||||
int raw = pwr & 0x0F;
|
||||
int pct = (raw >= 10) ? 100 : raw * 10 + 5;
|
||||
const uint8_t pstate = pwr >> 4;
|
||||
char marker = ' ';
|
||||
if (pstate == 1) marker = '+'; // Charging
|
||||
@@ -1547,6 +1598,7 @@ void settings_adjust(int delta) {
|
||||
break;
|
||||
}
|
||||
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);
|
||||
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 15: snprintf(line, n, "%s Wipe all slots", cur); break;
|
||||
case 14: snprintf(line, n, "%s CtrlWake %s", cur, c.controller_wakes_display ? "on" : "off"); 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(l2), 30, l2);
|
||||
draw_text(cx_for(l3), 44, l3);
|
||||
flush_fb();
|
||||
draw_button_chrome();
|
||||
flush_fb_raw_blocking();
|
||||
sleep_ms(1500);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void oled_init() {
|
||||
for (int i = 0; i < 256; i++) reverse_lut[i] = reverse_byte((uint8_t)i);
|
||||
spi_init(spi1, 10 * 1000 * 1000);
|
||||
gpio_set_function(kPinCLK, 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
|
||||
// in main() before this). Defaults to HOST passthrough on a fresh flash.
|
||||
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
|
||||
@@ -1858,6 +1918,10 @@ void oled_loop() {
|
||||
handle_buttons();
|
||||
const uint32_t now = time_us_32();
|
||||
rumble_burst_tick(now);
|
||||
if (flush_progress >= 0) {
|
||||
flush_chunk();
|
||||
return;
|
||||
}
|
||||
if ((now - last_render_us) < kFrameUs) return;
|
||||
last_render_us = now;
|
||||
// Track charge progress every frame — before the power-ladder early-returns
|
||||
@@ -1885,7 +1949,12 @@ void oled_loop() {
|
||||
}
|
||||
if (hash != last_input_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
|
||||
// the moment a controller pairs rather than waiting for the first input.
|
||||
|
||||
@@ -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], 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);
|
||||
copy_if_allowed(
|
||||
update.UseRumbleNotHaptics || update.EnableRumbleEmulation,
|
||||
|
||||
Reference in New Issue
Block a user