HeadsetActiveEQPreset.build runs before HeadsetAdvancedEQ.build and probes
each EQ slot via getCustomEQ, which needs the step_db from getEQInfos to
decode gain values. Without the cache, the first pass decoded gains as
all 0.0dB and logged a "no cached getEQInfos — gain values will be wrong"
warning. Cache the dict on device._advanced_eq_info as soon as we have it
so every later caller sees the right step_db.
Five fixes from analysis of the latest user log:
A. HeadsetMicMute.build() now matches device.product_id as the hex
string ("0B18", "0B19") rather than ints. product_id is set as a
str by hidapi_impl.f"{pid:04X}", so the int comparison was always
False and the suppression never fired. The G522 was still being
asked to write mic-mute on each connect and erroring out 0x0A.
B. RGB-effects probe sub-device feature dump tolerates "unknown:HHHH"
string features. _format_feature now detects that shape explicitly
(and renders 0xHHHH from the suffix) instead of letting int(feat)
raise ValueError mid-iteration and abort the whole table dump.
C. HeadsetActiveEQPreset.write replaces the bare `_value = None` cache
invalidation with a synchronous read(cached=False) so _value is a
real dict before returning. Prevents a UI band-click crash with
'NoneType' object is not subscriptable when a user clicks an EQ
band before the panel re-reads after a preset switch.
D. New probe_advanced_eq_slots() iterates every advertised slot via
getCustomEQ at build time, logs which respond, and caches a list
of (slot, name, bands) on device._advanced_eq_working_slots. The
HeadsetActiveEQPreset selector builds choices only from working
slots and returns None from build() if ≤1 slot responds — G522's
firmware advertises 16 slots but only honors slot 0, so the user
no longer sees a 16-option dropdown they can't actually use.
HeadsetAdvancedEQ.build now reuses the same probe (cached) rather
than the old probe_all_presets path. The legacy alias is kept in
the hidpp20 facade for any external callers still on it.
E. (capture only — no parse change yet) get_advanced_eq_params and
get_advanced_eq_defaults now log raw=<hex> on success too. The
two functions decode the same slot to wildly different bands on
G522 (likely a different header size between getCustomEQ's reply
and getEQDefaults's reply); raw bytes will let us pin down the
exact framing difference and adjust the parser in a follow-up.
Fix path will land alongside docs/features.md notes documenting
the per-call format variation, since other 0x020D headsets may
diverge differently from G522.
Rewrite parse_v2_bands to match the wire format the G522 firmware
actually emits: 3-byte header [direction_echo, slot_echo,
band_count_max], 5-byte stride [filter_type, gain_BE, freq_BE], and a
0..2 byte trailer that's opaque. band_count_max is the device's
max-bands capacity, not the populated-band count — bands are consumed
until <5 bytes remain or freq=0 marks end-of-bands.
Gain encoding switched from signed×step_db to offset-binary against
the gain bounds reported by getEQInfos, so raw 120 with steps=241 and
gain=[-6,6] decodes to 0 dB (flat) rather than +6 dB.
Add 0x16 to FILTER_TYPE_NAMES as peaking — that's the byte every G522
band carries at the textbook ISO third-octave centers (50, 125, 250,
500, 1000, 2500, 5000, 10000, 20000 Hz).
Previously the parser bailed because the 54-byte response wasn't a
clean multiple of 5, so the EQ panel never built.
Downgrade INFO logs that served their purpose during format/bug
discovery to DEBUG (bridge TX per-call, Centurion feature enumeration
per-feature, RGB zone discovery) and drop the HeadsetRGBHostMode
diagnostic write wrapper entirely.
Keep all EQ read paths at INFO — AdvancedParaEQ work is still in the
data-collection phase, writes are gated, and at least one tester
can't surface DEBUG logs. Keep all failure paths (build failures,
_absent cache hits, bridge sub-device errors) and one-shot signals
(device_addr probe result, deferred-init completion) at INFO.
Routine -dd output shrinks notably; INFO becomes mostly actionable
events. Nothing is lost — everything is recoverable at DEBUG.
The RE pass against lghub_agent.arm64 plus the G522 live probe resolved
the V2 wire format. Key corrections to the previous implementation:
1. 5-byte stride is [filter_type, freq_hi, freq_lo, gain_hi, gain_lo].
The initial RE interpretation of [freq_hi, freq_lo, gain, q_hi, q_lo]
was wrong — the 0x78 byte is a filter-type sentinel (peaking), not
the high byte of a frequency.
2. No header before the bands. G522's default "header" was actually
band 0: a high-pass filter at 20 Hz (filter_type=0x00, freq=0x0014).
Total is 10 bands (1 HP + 9 peaking at ISO octaves), not 9.
3. Frequency is raw Hz as BE u16 — no log/ERB/bin transform. 0x4E20
is exactly 20000 Hz.
4. Gain is signed BE int16 (not int8), scaled by step_db from
getEQInfos. ±120 maps to ±6 dB at 0.05 dB/LSB on the G522.
5. No Q on the wire — firmware-fixed per filter type.
get_advanced_eq_info is unchanged (13-byte V2 decode was already right).
Parser tuple shape is now (filter_type_byte, freq_hz, gain_db) across
both V0/V1 and V2 paths; V0/V1 synthesises filter_type=peaking so the
shape is uniform. Band labels display real Hz — "HP 20Hz", "50Hz",
"125Hz", ..., "20000Hz" on G522.
Stays read-only. Will enable write once we round-trip-test with known
raw bytes.
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.
G522's 0x020D V2 uses a 5-byte band stride
[freq_hi, freq_lo, gain_i8, q_hi, q_lo] and a 13-byte getEQInfos
(gain bounds, gain_steps, format, xy, preset counts). Frequency and
Q are opaque u16 round-trip values — the u16->Hz and u16->Q mappings
need a LGHUB pcap to pin down (documented in
~/ghub/HEADSET_ADVANCED_PARA_EQ_WIRE_PROTOCOL.md).
get_advanced_eq_info now returns a dict with a `version` discriminator
and the union of V0/V1 and V2 fields; step_db is derived from the
gain_min/gain_max/gain_steps triple on V2 (0.05 dB/LSB on G522).
get_advanced_eq_params version-switches: V2 uses parse_v2_bands which
probes header length {5, 2, 0} until the tail is a clean multiple of
5, strips trailing all-zero terminator entries. V0/V1 falls through
to the legacy 3-byte stride so older devices still work.
HeadsetAdvancedEQ.build() no longer requires band_count from
getEQInfos (V2 doesn't advertise it); derives from getCustomEQ length
per the wire-protocol doc's recommendation. V2 band labels use
"Band N" since u16->Hz isn't confirmed. Read-only still — writes
stay gated until pcap confirms the encodings.
Centurion feature discovery now logs each parent + sub-device feature at
INFO with name/index/version/flags. `check_feature` logs INFO when it
skips a setting for min_version or the INTERNAL flag. HeadsetAdvancedEQ
and HeadsetOnboardEQ `build()` paths log at every failure branch. The
three AdvancedParaEQ helpers log raw response bytes.
On the G522, `HEADSET_ADVANCED_PARA_EQ` (0x020D) is present in the
feature set but no settings panel appears and current logging gives us
no way to tell which step silently returns None.
The G522 exposes AdvancedParaEQ (0x020D), a different EQ feature than
the PRO X 2's OnboardEQ (0x0636). Key differences:
- 3-byte-per-band wire format ([freq_hi, freq_lo, gain]) vs 0x0636's
4-byte-per-band ([freq_hi, freq_lo, gain, Q])
- Device handles biquad coefficient computation — no host-side DSP math
- Has explicit direction (playback/capture), multiple preset slots with
getActiveEQ/setActiveEQ for switching, preset friendly names
Adds new advanced_para_eq.py module with getInfos, getActiveEQ, and
getCustomEQ helpers (function 0, 3, 1 respectively). Re-exported from
hidpp20.py following the onboard_eq.py pattern.
HeadsetAdvancedEQ setting displays the currently-active playback EQ
using the same RangeFieldSetting + PackedRangeValidator UI pattern as
HeadsetOnboardEQ, so the graphic EQ widget looks the same.
**Writes are intentionally disabled for now** — prepare_write returns
None and write() logs "read-only mode" without sending anything. This
lets us verify the wire format matches the protocol doc against real
hardware before risking a write that could misconfigure the DSP. Once
read output is confirmed sensible, we'll wire up setCustomEQ (function
2) to enable writes.