AdvancedParaEQ: correct V2 band-stride and header length
The V2 parser was using stride [filter, gain_hi, gain_lo, freq_hi, freq_lo] with a 3-byte header. Pcap traces of LGHUB ↔ G522 LIGHTSPEED show the actual wire is [freq_hi, freq_lo, filter, gain_hi, gain_lo] with a 1-byte header (just dir_echo; the centurion bridge omits the slot_echo the protocol spec describes for getCustomEQ). With the old stride, slot 0's 10-band response on G522 parsed as 9 bands: the highest band (19 kHz, +2.75 dB on the active preset) was silently swallowed because byte alignment landed on what looked like a freq=0 end-sentinel. The displayed bands were also shifted — 20 Hz was missing from the front because the parser started one full band late. See ~/ghub/solaar_0x020D_slot_investigation.md for the RE notes.
This commit is contained in:
parent
298380b8d2
commit
7c73c88867
|
|
@ -23,16 +23,20 @@ V0/V1 wire format: 3-byte band stride [freq_hi, freq_lo, gain_i8],
|
||||||
gain is whole dB; getEQInfos returns 5 bytes [bandCount, dbRange,
|
gain is whole dB; getEQInfos returns 5 bytes [bandCount, dbRange,
|
||||||
caps, dbMin, dbMax].
|
caps, dbMin, dbMax].
|
||||||
|
|
||||||
V2 wire format: 3-byte header [direction_echo, slot_echo,
|
V2 wire format: 1-byte header [direction_echo], then N × 5-byte band
|
||||||
band_count_max], then N × 5-byte band stride [filter_type, gain_hi,
|
stride [freq_hi, freq_lo, filter_type, gain_hi, gain_lo], with 0..3
|
||||||
gain_lo, freq_hi, freq_lo], then 0..2 trailer bytes (opaque, ignored).
|
trailer bytes (opaque, ignored). The parser consumes 5-byte chunks
|
||||||
band_count_max is the device's max-bands capacity, NOT how many bands
|
until <5 bytes remain or a freq=0 sentinel is hit. Frequency u16 BE
|
||||||
are populated — the parser consumes 5-byte chunks until <5 bytes
|
in Hz; gain u16 BE in **offset-binary**: raw 0..(steps-1) maps
|
||||||
remain. Frequency u16 BE in Hz, with freq=0 marking end-of-bands.
|
linearly to gain_min..gain_max (so on G522 with steps=241 /
|
||||||
Gain is **offset-binary**: raw 0..(steps-1) maps linearly to
|
gain=[-6..6], raw=120 = 0 dB). getEQInfos returns 13 bytes with gain
|
||||||
gain_min..gain_max (so on G522 with steps=241 / gain=[-6..6], raw=120
|
bounds + step count, format enum, XY-support flag, and onboard preset
|
||||||
= 0 dB). getEQInfos returns 13 bytes with gain bounds + step count,
|
counts.
|
||||||
format enum, XY-support flag, and onboard preset counts.
|
|
||||||
|
(The protocol spec lists a 2-byte header [dir_echo, slot_echo] for
|
||||||
|
getCustomEQ, but G522 firmware via the centurion bridge omits the
|
||||||
|
slot_echo and emits a 1-byte header that matches getEQDefaults.
|
||||||
|
Verified against pcap traces of LGHUB ↔ G522 LIGHTSPEED traffic.)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
@ -178,9 +182,9 @@ def parse_v2_bands(result: bytes, info: dict | None):
|
||||||
"""Parse a V2 getCustomEQ / getEQDefaults response.
|
"""Parse a V2 getCustomEQ / getEQDefaults response.
|
||||||
|
|
||||||
Wire layout (see module docstring):
|
Wire layout (see module docstring):
|
||||||
[direction_echo, slot_echo, band_count_max] (3-byte header)
|
[direction_echo] (1-byte header)
|
||||||
N × [filter_type, gain_hi, gain_lo, freq_hi, freq_lo] (5 bytes)
|
N × [freq_hi, freq_lo, filter_type, gain_hi, gain_lo] (5 bytes)
|
||||||
[trailer …] (0..2 bytes, ignored)
|
[trailer …] (0..3 bytes, ignored)
|
||||||
|
|
||||||
Gain is offset-binary against `info`'s gain bounds:
|
Gain is offset-binary against `info`'s gain bounds:
|
||||||
gain_db = gain_min + (gain_max - gain_min) * raw / (steps - 1)
|
gain_db = gain_min + (gain_max - gain_min) * raw / (steps - 1)
|
||||||
|
|
@ -194,9 +198,9 @@ def parse_v2_bands(result: bytes, info: dict | None):
|
||||||
valid header returns []. Bands with freq=0 are treated as the
|
valid header returns []. Bands with freq=0 are treated as the
|
||||||
end-of-bands sentinel (matches V0/V1 behavior at lines below).
|
end-of-bands sentinel (matches V0/V1 behavior at lines below).
|
||||||
"""
|
"""
|
||||||
if result is None or len(result) < 3:
|
if result is None or len(result) < 1:
|
||||||
return None
|
return None
|
||||||
payload = result[3:] # skip [dir_echo, slot_echo, band_count_max]
|
payload = result[1:] # skip [dir_echo]
|
||||||
band_size = 5
|
band_size = 5
|
||||||
if info:
|
if info:
|
||||||
gain_min = info.get("gain_min_db", -6)
|
gain_min = info.get("gain_min_db", -6)
|
||||||
|
|
@ -207,9 +211,9 @@ def parse_v2_bands(result: bytes, info: dict | None):
|
||||||
bands = []
|
bands = []
|
||||||
for i in range(len(payload) // band_size):
|
for i in range(len(payload) // band_size):
|
||||||
e = payload[i * band_size : (i + 1) * band_size]
|
e = payload[i * band_size : (i + 1) * band_size]
|
||||||
filter_type = e[0]
|
freq_hz = (e[0] << 8) | e[1]
|
||||||
gain_raw = (e[1] << 8) | e[2]
|
filter_type = e[2]
|
||||||
freq_hz = (e[3] << 8) | e[4]
|
gain_raw = (e[3] << 8) | e[4]
|
||||||
if freq_hz == 0:
|
if freq_hz == 0:
|
||||||
break # disabled band — end-of-bands sentinel
|
break # disabled band — end-of-bands sentinel
|
||||||
if steps > 1:
|
if steps > 1:
|
||||||
|
|
@ -255,10 +259,6 @@ def get_advanced_eq_defaults(device, direction=DIRECTION_PLAYBACK, slot=0):
|
||||||
result.hex(),
|
result.hex(),
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
# Log raw=... too — getEQDefaults appears to use a different header
|
|
||||||
# framing than getCustomEQ on G522 (decoded values come out shifted
|
|
||||||
# by a byte). Capture the raw bytes so we can pin down the actual
|
|
||||||
# layout difference and adjust the parser accordingly.
|
|
||||||
logger.info(
|
logger.info(
|
||||||
"AdvancedParaEQ getEQDefaults V2 (dir=%d slot=%d): %d band(s) raw=%s %s",
|
"AdvancedParaEQ getEQDefaults V2 (dir=%d slot=%d): %d band(s) raw=%s %s",
|
||||||
direction,
|
direction,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue