Commit Graph

10 Commits

Author SHA1 Message Date
Ken Sanislo 593223d6eb AdvancedParaEQ: cache getEQInfos result on the device
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.
2026-05-10 16:52:13 -07:00
Ken Sanislo 2e07b112ff G522 bug batch from log df178225
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.
2026-05-10 15:39:17 -07:00
Ken Sanislo 59e3dcb736 AdvancedParaEQ V2: handle G522 wire layout
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.
2026-05-10 15:39:17 -07:00
Ken Sanislo 3ea47b09ed Audit diagnostic logging added during G522 bring-up
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.
2026-05-10 15:39:17 -07:00
Ken Sanislo 76b2ee8270 Remove references to internal RE docs and lghub_agent from code comments 2026-05-10 15:39:17 -07:00
Ken Sanislo a1089cd8c5 AdvancedParaEQ V2: correct stride + real Hz labels
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.
2026-05-10 15:39:17 -07:00
Ken Sanislo 0520ab40ba 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.
2026-05-10 15:39:17 -07:00
Ken Sanislo 41db76bc81 AdvancedParaEQ: add V2 wire-format support; keep V0/V1
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.
2026-05-10 15:39:17 -07:00
Ken Sanislo c45449b212 Add diagnostic logging for EQ setting build failures
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.
2026-05-10 15:39:17 -07:00
Ken Sanislo bde3c3bc86 Add read-only HeadsetAdvancedEQ (0x020D) display
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.
2026-05-10 15:39:17 -07:00