AdvancedParaEQ: probe all factory + custom presets at build time
Adds get_advanced_eq_defaults (function 5), get_advanced_eq_friendly_name (function 6), and a probe_all_presets helper that reads every factory and custom preset slot and logs its name + band data at INFO. The G522 exposes 6 factory presets and 10 custom slots. Reading each and dumping (freq_u16, gain_db, q_u16) across a corpus of named presets (Flat, Bass Boost, Vocal, etc.) should reveal the u16->Hz and u16->Q encodings by pattern-matching — without needing a LGHUB pcap. The results land in the normal -dd log so the next tester run gives us the data. One-shot probe runs at HeadsetAdvancedEQ.build() for V2 devices only. Failures are logged at INFO and don't block panel construction.
This commit is contained in:
parent
41db76bc81
commit
0520ab40ba
|
|
@ -193,6 +193,125 @@ def parse_v2_bands(result: bytes, step_db: float):
|
|||
return bands, header_len
|
||||
|
||||
|
||||
def get_advanced_eq_defaults(device, direction=DIRECTION_PLAYBACK, slot=0):
|
||||
"""Query getEQDefaults (function 5). Same per-band layout as getCustomEQ.
|
||||
|
||||
Factory presets, read-only. Reading them across all factory slots gives
|
||||
us a corpus of (name, freq_u16[], q_u16[]) tuples that may reveal the
|
||||
u16->Hz and u16->Q scaling without needing a pcap. Returns list of
|
||||
(freq_u16, gain_db, q_u16) or None.
|
||||
"""
|
||||
version = _get_version(device)
|
||||
result = device.feature_request(SupportedFeature.HEADSET_ADVANCED_PARA_EQ, 0x50, direction, slot)
|
||||
if result is None:
|
||||
logger.info(
|
||||
"AdvancedParaEQ getEQDefaults V%d (dir=%d slot=%d): feature_request returned None",
|
||||
version,
|
||||
direction,
|
||||
slot,
|
||||
)
|
||||
return None
|
||||
if version >= 2:
|
||||
info = getattr(device, "_advanced_eq_info", None)
|
||||
step_db = info["step_db"] if info and "step_db" in info else 1.0
|
||||
bands, header_len = parse_v2_bands(result, step_db)
|
||||
if bands is None:
|
||||
logger.info(
|
||||
"AdvancedParaEQ getEQDefaults V2 (dir=%d slot=%d): couldn't locate band payload raw=%s",
|
||||
direction,
|
||||
slot,
|
||||
result.hex(),
|
||||
)
|
||||
return None
|
||||
logger.info(
|
||||
"AdvancedParaEQ getEQDefaults V2 (dir=%d slot=%d): %d band(s) header_len=%d raw=%s",
|
||||
direction,
|
||||
slot,
|
||||
len(bands),
|
||||
header_len,
|
||||
result.hex(),
|
||||
)
|
||||
return bands
|
||||
# V0/V1: 3-byte stride, gain is whole dB, no Q.
|
||||
bands = []
|
||||
offset = 0
|
||||
while offset + 3 <= len(result):
|
||||
freq = struct.unpack(">H", result[offset : offset + 2])[0]
|
||||
if freq == 0:
|
||||
break
|
||||
gain_db = struct.unpack("b", bytes([result[offset + 2]]))[0]
|
||||
bands.append((freq, float(gain_db), 0))
|
||||
offset += 3
|
||||
logger.info(
|
||||
"AdvancedParaEQ getEQDefaults V%d (dir=%d slot=%d): %d band(s) raw=%s",
|
||||
version,
|
||||
direction,
|
||||
slot,
|
||||
len(bands),
|
||||
result.hex(),
|
||||
)
|
||||
return bands
|
||||
|
||||
|
||||
def get_advanced_eq_friendly_name(device, direction=DIRECTION_PLAYBACK, slot=0):
|
||||
"""Query getEQFriendlyName (function 6). Returns the UTF-8 preset name or None."""
|
||||
result = device.feature_request(SupportedFeature.HEADSET_ADVANCED_PARA_EQ, 0x60, direction, slot)
|
||||
if result is None or len(result) < 1:
|
||||
return None
|
||||
name_len = result[0]
|
||||
if 1 + name_len > len(result):
|
||||
name_len = len(result) - 1
|
||||
try:
|
||||
name = bytes(result[1 : 1 + name_len]).decode("utf-8", errors="replace")
|
||||
except Exception:
|
||||
name = result[1 : 1 + name_len].hex()
|
||||
return name
|
||||
|
||||
|
||||
def probe_all_presets(device, direction=DIRECTION_PLAYBACK):
|
||||
"""Read every factory and custom preset slot's name + band data and log it.
|
||||
|
||||
Diagnostic probe — intended to run once at HeadsetAdvancedEQ.build() time
|
||||
so we accumulate a corpus of (name, freq_u16[], q_u16[]) tuples across
|
||||
presets. Patterns in that corpus should reveal the u16->Hz and u16->Q
|
||||
mappings without needing a live pcap from LGHUB.
|
||||
"""
|
||||
info = getattr(device, "_advanced_eq_info", None)
|
||||
if not info:
|
||||
return
|
||||
ro_count = info.get("onboard_ro_preset_count", 0)
|
||||
custom_count = info.get("onboard_custom_preset_count", 0)
|
||||
# Factory presets: read via getEQDefaults
|
||||
for slot in range(ro_count):
|
||||
name = get_advanced_eq_friendly_name(device, direction=direction, slot=slot)
|
||||
bands = get_advanced_eq_defaults(device, direction=direction, slot=slot)
|
||||
if bands is None:
|
||||
continue
|
||||
logger.info(
|
||||
"AdvancedParaEQ factory preset %d/%d (dir=%d) name=%r: %s",
|
||||
slot,
|
||||
ro_count,
|
||||
direction,
|
||||
name,
|
||||
[(f"0x{f:04X}", round(g, 3), f"0x{q:04X}") for f, g, q in bands],
|
||||
)
|
||||
# Custom preset slots: via getCustomEQ. Most will be empty/default, but
|
||||
# any user-authored slots could give additional freq/Q samples.
|
||||
for slot in range(custom_count):
|
||||
name = get_advanced_eq_friendly_name(device, direction=direction, slot=slot)
|
||||
bands = get_advanced_eq_params(device, direction=direction, slot=slot)
|
||||
if bands is None:
|
||||
continue
|
||||
logger.info(
|
||||
"AdvancedParaEQ custom preset %d/%d (dir=%d) name=%r: %s",
|
||||
slot,
|
||||
custom_count,
|
||||
direction,
|
||||
name,
|
||||
[(f"0x{f:04X}", round(g, 3), f"0x{q:04X}") for f, g, q in bands],
|
||||
)
|
||||
|
||||
|
||||
def get_advanced_eq_params(device, direction=DIRECTION_PLAYBACK, slot=0):
|
||||
"""Query getCustomEQ (function 1). Returns list of (freq, gain_db, q) or None.
|
||||
|
||||
|
|
|
|||
|
|
@ -2317,9 +2317,12 @@ class ForceSensingButtonArray(UserDict):
|
|||
# --- OnboardEQ (0x0636) — re-exported from onboard_eq.py ---
|
||||
# --- AdvancedParaEQ (0x020D) — re-exported from advanced_para_eq.py ---
|
||||
from .advanced_para_eq import get_advanced_eq_active_slot # noqa: E402, F401
|
||||
from .advanced_para_eq import get_advanced_eq_defaults # noqa: E402, F401
|
||||
from .advanced_para_eq import get_advanced_eq_friendly_name # noqa: E402, F401
|
||||
from .advanced_para_eq import get_advanced_eq_info # noqa: E402, F401
|
||||
from .advanced_para_eq import get_advanced_eq_params # noqa: E402, F401
|
||||
from .advanced_para_eq import parse_v2_bands # noqa: E402, F401
|
||||
from .advanced_para_eq import probe_all_presets as probe_advanced_eq_presets # noqa: E402, F401
|
||||
from .onboard_eq import _build_set_eq_payload # noqa: E402, F401
|
||||
from .onboard_eq import get_onboard_eq_info # noqa: E402, F401
|
||||
from .onboard_eq import get_onboard_eq_params # noqa: E402, F401
|
||||
|
|
|
|||
|
|
@ -1934,6 +1934,15 @@ class HeadsetAdvancedEQ(settings.RangeFieldSetting):
|
|||
gain_max,
|
||||
step_db,
|
||||
)
|
||||
# One-shot corpus probe: read every factory/custom preset's name
|
||||
# and band data. Comparing freq_u16 and q_u16 values across named
|
||||
# presets ("Flat" vs "Bass Boost" etc.) may reveal the u16->Hz
|
||||
# and u16->Q scalings without requiring a LGHUB pcap.
|
||||
if version >= 2:
|
||||
try:
|
||||
hidpp20.probe_advanced_eq_presets(device, direction=0)
|
||||
except Exception as e:
|
||||
logger.info("HeadsetAdvancedEQ.build: preset corpus probe failed: %s", e)
|
||||
return v
|
||||
|
||||
def validate_read(self, reply_bytes):
|
||||
|
|
|
|||
Loading…
Reference in New Issue