Commit Graph

196 Commits

Author SHA1 Message Date
Ken Sanislo be047fd949 HeadsetAdvancedEQ: enable slot-0 band writes
V2 setCustomEQ wire format is pcap-verified: request payload is
[dir, slot, pad=0] + N × [freq_hi, freq_lo, filter, gain_hi, gain_lo].
Gain is offset-binary against the [gain_min, gain_max] / gain_steps
grid from getEQInfos. Freq and filter type aren't user-editable today;
they're sourced from the build-time cache.

Changes:
- rw_class.write: injects [dir, slot=active, pad=0] before band bytes
  and re-queries the active slot each call (matches the read path).
- validator.prepare_write: encodes the band payload from the dict the
  UI hands us, converting int dB → raw u16 against the gain grid.
- Setting.write: overrides RangeFieldSetting.write to treat empty-bytes
  replies as success (HID++ set ops ACK with b""), only None as failure.
- Drop the read-only override and stale docstring.
2026-05-10 17:26:33 -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 9bb0fc0205 Hide mic-mute toggle on G522; strip trailing whitespace in get_feature comment
G522 advertises HEADSET_MIC_MUTE (0x0601) in its FeatureSet at index 21
but the firmware returns 0x0A UNSUPPORTED for both GetState (fn 0x10)
and SetState (fn 0x00) — the physical mute switch on the headset doesn't
drive this feature, and G HUB silently swallows the failures. Hide the
broken toggle on G522 PIDs (0x0B18 wireless, 0x0B19 wired) by returning
None from build() so it never reaches the UI.

Also strip the trailing space on the sub-device comment in get_feature
that pre-commit's whitespace hook flags in CI.
2026-05-10 15:39:17 -07:00
Ken Sanislo f6a6315a7d Update hidpp20.py
We probably should add a comment here so this makes sense later on. Otherwise, this looks good.
2026-05-10 15:39:17 -07:00
Schmiddy f1864fd211 fix(centurion): route sub-device notifications via sub_inverse in get_feature
Bridge MessageEvents arrive with sub_feat_idx + 0x100 as the notification
sub_id. get_feature() only checked self.inverse (parent features), so sub-
device feature notifications (including CENTURION_BATTERY_SOC charging
changes) were silently dropped with 'invalid feature index'.

Check sub_inverse at index - 0x100 for any index >= 0x100 so spontaneous
notifications from the headset (e.g. charging status change) are correctly
dispatched to the feature notification handler.
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 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 fa205436f1 Record feature versions in Centurion Phase A (parent/direct) too
The previous commit fixed version recording for bridge-routed sub-device
features (Phase B). This applies the same fix to Phase A, which is used
for parent features on a wireless dongle AND for the whole feature set
on direct-USB Centurion devices like the wired G522 (PID 0x0B19).

Without this, a wired Centurion headset would enumerate all features
with version=0 and version-gated settings (sidetone 3-byte format,
auto-sleep 2/3-byte timer) would send the wrong payload and get
OUT_OF_RANGE rejections — same symptom the wireless G522 had before.
2026-05-10 15:39:17 -07:00
Ken Sanislo ef439d87fc Record Centurion sub-device feature versions + version-gate AutoSleep
The Centurion sub-device discovery ignored the type/version bytes in
the per-index getFeatureId response and defaulted every feature's
version to 0. That made version-gated settings (sidetone, auto-sleep)
send V0 payload formats on features that may actually be V2/V3/V4 —
which the G522 rejects with OUT_OF_RANGE (error 0x03).

Evidence from tester's log (version acfd02ab):
  bridge sub-device error: orig_feat_idx=13 orig_func=0x1B error=0x03
  bridge sub-device error: orig_feat_idx=20 orig_func=0x1F error=0x03

feat_idx 13 is HeadsetAudioSidetone (0x0604); 20 is CenturionAutoSleep
(0x0108). Both have version-gated payload formats per the protocol doc.

Fixes:
1. In _discover_sub_device_features, extract response[3] (type) and
   response[4] (version) and store them in self.version / self.flags,
   so get_feature_version() returns the real version for downstream
   callers.

2. Add HeadsetAutoSleep.build() that picks byte_count and max_value
   based on the reported version (V<3=1 byte, V=3=2 bytes, V>=4=3).
   HeadsetSidetone.build() already had version gating — it just wasn't
   getting the real version before.

Does NOT address the mic-mute (error 0x0A) and mic-gain (error 0x0B)
rejections — those are non-standard error codes likely meaning the G522
either gates those behind the physical mic button (mic-mute) or rejects
writes for other reasons (mic-gain). Needs separate investigation once
the simpler version fix lands.
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
Ken Sanislo cfc69431d1 Use per-index queries for Centurion sub-device feature enumeration
The previous "bulk" query (CenturionFeatureSet.GetFeatureId with
start_index, per-batch response) was limited by the 64-byte Centurion
frame size to ~13 features per response, and the device apparently
does not fragment MessageEvent responses — so start_index=14 returned
empty and we missed the remaining features.

Evidence: wired G522 exposes 30 features directly but wireless via
bridge only ever reports 13. Same sub-device, different access path,
same underlying feature set.

Switch to per-index queries matching how _discover_dongle_features
already works on the parent:
  1. CenturionFeatureSet.GetCount (func 0) -> total feature count
  2. CenturionFeatureSet.GetFeatureId (func 1) per index -> one feature
     per response

Slower (N round-trips instead of 1) but handles any feature count
without fragmentation. Expected to reveal RGB, advanced EQ, sidetone,
etc. on the wireless G522.

Updated test fixture with per-index response format.
2026-05-10 15:39:17 -07:00
Ken Sanislo e7ee34f132 Loop Centurion sub-device feature discovery across batches
CenturionFeatureSet bulk query only returns as many features as fit in
a single ~60-byte Centurion frame (~13-14 features per response). The
single-call implementation silently truncated devices with more
features, which may explain missing RGB/audio features on the G522.

Loop the query with increasing start_index, stopping on empty batch,
short batch, or 16-batch safety bound (224 feature max).

Also log the final discovered count at INFO level so we can see it in
field test logs.
2026-05-10 15:39:17 -07:00
Ken Sanislo 7851acb850 Tighten Centurion probe and silence FeaturesArray query spam
Three cleanup fixes after field testing confirmed the brute-force probe
finds device_addr=0x23 on the G522 (no special addresses at 0x00/0xFF):

1. Short-circuit the probe on first hit instead of sweeping all 256. The
   diagnostic full-sweep served its purpose — only 0x23 responds.
2. Drop per-candidate read timeout from 20ms to 5ms. USB round-trip is
   <1ms, so 5ms is 5x margin. Worst case: 1.3s. Typical: 180ms.
3. Short-circuit FeaturesArray.__getitem__ for Centurion devices. All
   parent + sub-device features are enumerated upfront by
   _check_centurion(). If a feature isn't in the dict after _check(),
   it genuinely doesn't exist — skip the raw ROOT.GetFeature query that
   the dongle rejects with LOGITECH_ERROR. Eliminates cycling
   {0002}..{000F} error log spam during settings init and speeds up
   check_feature_settings() by ~225 round trips per device.
2026-05-10 15:39:17 -07:00
Ken Sanislo 1952e9ce98 Add regional keyboard layouts (ISO_QWERTY, QWERTZ, AZERTY, JIS) and fix copyright
Read HID++ feature 0x4540 KeyboardLayout to detect the device's country
code, then route the per-key painter to a matching regional layout.

Changes:

- lib/logitech_receiver/hidpp20.py: new get_keyboard_layout() returning the
  HID Usage Table country code from feature 0x4540's first response byte.
- lib/logitech_receiver/device.py: lazy device.keyboard_layout property,
  guarded by feature presence so devices without 0x4540 don't pay a query
  cost on access.
- lib/solaar/ui/perkey/control.py: thread the country code into the editor
  hint dict.
- lib/solaar/ui/perkey/layouts/_keyboard_base.py (new): factor out the
  function row, nav cluster, and numpad block as shared building blocks.
  Two main-block variants (ANSI with row 2 col 13 backslash, ISO without)
  cover all five regions. build_layout() applies per-zone label overrides
  on top of either main block.
- lib/solaar/ui/perkey/layouts/keyboard_ansi.py: refactored to use the
  builder; same LAYOUT_FULL/LAYOUT_TKL exports.
- lib/solaar/ui/perkey/layouts/keyboard_iso_qwerty.py (new): UK English
  ISO. Same shape as DE/FR/JIS but no label overrides.
- lib/solaar/ui/perkey/layouts/keyboard_iso_qwertz.py (new): DE/Swiss --
  Y/Z swap, Ü/Ö/Ä/ß placement.
- lib/solaar/ui/perkey/layouts/keyboard_iso_azerty.py (new): FR -- A↔Q,
  W↔Z, French digit-row symbols, M repositioning.
- lib/solaar/ui/perkey/layouts/keyboard_jis.py (new): JP -- @ / [ / :
  bracket-row relabels.
- lib/solaar/ui/perkey/layouts/__init__.py: country-code-aware matchers,
  five families × two sizes (full/TKL). Defaults to ANSI when 0x4540 is
  unsupported or returns an unknown code.

POUND, ISO_BACKSLASH, and the L-shape Enter top half (zone 46) are
intentionally omitted from the ISO layouts -- same coverage as OpenRGB.
ABNT2 (Brazilian) deferred until a confirmed Logitech BR RGB device shows
up; adding it later is one new layout file plus a country-code entry.

Also fix copyright headers on all new lib/solaar/ui/perkey/ files: the
files were created in 2026, not 2024 as the headers said.
2026-05-10 17:52:55 -04:00
Ken Sanislo 12aabf029b
centurion: support PRO X 2 LIGHTSPEED headphones Centurion features (#3150)
* Add Centurion transport and PRO X 2 LIGHTSPEED headset support

Adds support for the Logitech PRO X 2 LIGHTSPEED Gaming Headset (PID 0x0AF7)
which uses the Centurion transport protocol (report ID 0x51 on USB usage page
0xFFA0) instead of standard HID++ report IDs.

Changes:
- HID enumeration: detect Centurion devices via report descriptor parsing
  (usage page 0xFFA0, report ID 0x51, 63-byte frames)
- Centurion transport: wrap/unwrap HID++ 2.0 frames in Centurion framing
  for write, read, and ping operations
- Feature discovery: enumerate features individually on Centurion devices
  (different response format: [remaining_count, feat_hi, feat_lo])
- Device descriptor for PRO X 2 LIGHTSPEED Gaming Headset
- New feature enum entries for Centurion-era headset features (0x06xx)
- CenturionRawRW class for write-only headset settings controlled via
  raw Centurion commands reverse-engineered from HeadsetControl
- HeadsetSidetone setting (0-100 range, persisted locally)

Known limitations:
- Only sidetone control is implemented; other features need RE work
- Settings are write-only (no read-back from device)
- Headset features (0x06xx) not discoverable via IRoot; registered manually

* Remove static PRO X 2 descriptor; fully probe Centurion devices at runtime

Replace the hardcoded descriptor entry with dynamic discovery of all device
properties via the Centurion protocol. The headset name, kind, serial,
firmware, and battery are now probed at runtime — matching how the device
actually presents itself rather than relying on static data.

Key changes:
- Discover sub-device features via CentPPBridge and route requests through
  the bridge automatically
- Infer device kind from feature IDs (0x06xx = headset) for both wireless
  and direct USB connections
- Read device name from USB product string with protocol probe fallback
- Parse bridge error responses (sub_feat_idx=0xFF) instead of timing out
- Handle unknown HID++ error codes gracefully in base.py
- Fix firmware deduplication for Centurion parent devices
- Prefer sub-device serial/firmware over parent (non-printable) values
- Add Centurion-aware display in solaar show with parent/sub-device sections
- Support both wireless (0AF7 dongle) and direct USB (0AF8) connections

* Display Centurion dongle as receiver with headset as child device

- Add CenturionReceiver class that provides the Receiver UI interface so
  the dongle appears as a parent with the headset indented underneath,
  matching how Lightspeed/Unifying receivers display
- Independently probe dongle features via feature_request() on the
  CenturionReceiver, separate from headset features via bridge
- Fix bridge notification dispatch: remove incorrect sub_cpl=0xFF filter
  that was silently dropping all battery and other notifications
- Fix battery status decoding: charging status is at byte 2 (not byte 1)
  of the CENTURION_BATTERY_SOC response
- Detect wired vs wireless by checking for CentPPBridge in discovered
  features; wired headsets fall back to standalone Device
- Name the dongle "Centurion Receiver" to distinguish from the headset
- Filter unprintable dongle serial (control characters 0x14-0x1F)
- Update CLI show output with proper receiver/child hierarchy and spacing

* Fix headset setting validators and code formatting

- Add signed int8 support to RangeValidator for HeadsetMicGain (0x0611)
- Make HeadsetSidetone version-aware: v1 uses 2-byte skip, v2+ uses
  3-byte skip with 0xFF separator per protocol spec
- Fix ruff formatting in device.py, listener.py, udev_impl.py
- Update CenturionReceiver test for renamed receiver

* Use ConnectionStateChangedEvent for headset online/offline detection

Replace ad-hoc heuristics with proper bridge event function dispatch:
- Function 0 (ConnectionStateChangedEvent): parse sub-device list length
  to determine connect (len>0) vs disconnect (len=0)
- Function 1 (MessageEvent): fallback online detection if headset sends
  a message while marked offline (handles cold-start power-on)

Remove CPL sub_id>=0x80 fallback in listener that misidentified HID++
error replies as disconnect events. Skip HID++ 1.0
set_configuration_pending_flags for CenturionReceiver (not supported).

Also adds OnboardEQ (0x0636) support, bridge multi-fragment sends,
bridge-based headset ping probe, and CLI offline display.

* Update PRO X 2 LIGHTSPEED device doc with current solaar show output

* Fix Centurion protocol version display (1.16 not 2.6)

The HID++ ping math (major + minor/10.0) produced a bogus "2.6" for
Centurion devices whose ProtocolCapabilities returns major=1, minor=0x10.
Store the raw (major, minor) bytes from the ping response and display
them correctly as "Centurion 1.16" in both CLI and GUI.

* Add OnboardEQ (0x0636) support for Centurion headsets

Implement host-computed biquad EQ coefficient generation and multi-fragment
bridge writes for the PRO X 2 LIGHTSPEED headset's 5-band parametric EQ.

The coefficient algorithm uses standard Audio EQ Cookbook peaking EQ formulas
with a simplified rescale normalization (max_b0 × 1.19 headroom). This is our
own implementation — not an exact replica of LGHUB's ~350-line per-band cascade
normalization — but it produces functionally correct results. The DSP
compensates via the rescale factor, and the EQ changes are audible and working
on real hardware.

Wire format verified against 38 LGHUB pcap writes:
- 4-byte LE section headers, LE uint16 coefficient words
- Mixed Q1.31/Q2.30 fixed-point with 24-bit precision
- Only b-coefficients divided by rescale; a-coefficients unchanged
- Two sections: 48kHz playback + 16kHz mic
- No trailing padding, no extra words between sections

Changes:
- base.py: Add flags parameter to write_centurion_cpl() for multi-fragment CPL
- device.py: Rewrite multi-fragment bridge send — proper CPL fragmentation with
  fragment 0 carrying bridge prefix/hdr and continuations carrying raw sub_msg,
  all fragments sent back-to-back without intermediate ACKs
- hidpp20.py: Replace placeholder coefficient code with full biquad math,
  mixed Q-format quantization, rescale normalization, and dual-section output
- settings_templates.py: Persist EQ to slot 0x80 after writing to slot 0x00
  so settings survive power cycle
- tests: Update expected SetEQParameters payloads for new coefficient format

* Extract Centurion protocol into separate modules

Move CenturionReceiver class, factory function, and Centurion protocol
queries (firmware, serial, hardware info, battery, name) from device.py
and hidpp20.py into new centurion.py module. Move OnboardEQ biquad math
and payload builders from hidpp20.py into new onboard_eq.py module.
Move _read_usb_product_string() to common.py to avoid circular imports.

Re-exports preserve backward compatibility for all existing callers.

* Add vertical graphic EQ slider widget for headset equalizer

Replace horizontal slider rows with a traditional graphic EQ layout
using vertical sliders side-by-side, with dB value display and
frequency labels per band.

* Fix device online state clobbered by debug ping in _status_changed

The INFO-level logging guard in _status_changed() called device.ping()
before logging, purely to show accurate online status. But ping() has
side effects — it sets device.online based on the result. When a
ConnectionStateChangedEvent correctly marked a device online, the
subsequent _status_changed() callback would re-ping. If the device
wasn't ready yet (e.g. Centurion headset still booting), the ping
timed out and set online back to False, requiring 2-3 power cycles
to sync state.

Remove the unnecessary ping — the log message already reads
device.online which reflects the state set by the event handler.

* Sort feature constants by ID and add PROFILE_MANAGEMENT

Move RPM_INDICATOR/RPM_LED_PATTERN (0x807A-B) before PER_KEY_LIGHTING
(0x8080-81), sort five Centurion-era headset entries into their correct
positions by feature ID, and add missing PROFILE_MANAGEMENT = 0x8101.

* Add CenturionCoreFeature enum for colliding feature IDs

Centurion transport reuses HID++ 2.0 feature IDs 0x0000, 0x0001,
0x0003, 0x0005, 0x0007 with different meanings. Since SupportedFeature
(IntEnum) requires unique values, create a separate CenturionCoreFeature
enum and resolve_feature() helper for transport-aware lookup.

Also replace the +0x100 offset hack in FeaturesArray.inverse with a
dedicated sub_inverse dict for sub-device feature indexing.

* Fix ruff I001 import sorting in centurion.py and hidpp20.py

* Add 9 missing centurion/headset feature names

Add feature constants split out from the HID++ 2.0 names PR (#3153):
CENTURION_LED_BRIGHTNESS (0x0110), CENTURION_EU_POWER_MODE (0x0115),
CENTURION_DEVICE_BOOL_STATE (0x0116), HEADSET_ADVANCED_PARA_EQ (0x020D),
HEADSET_MIC_TEST (0x020E), HEADSET_EQ_STYLES (0x0213),
BT_HOST_INFO (0x0305), LIGHTSPEED_PAIRING (0x0309),
BT_GAMING_MODE (0x030A).

* Extract _record_ping_protocol helper so all ping paths capture Centurion version

The raw Centurion (major, minor) pickup was only in the Centurion-child
dongle branch of Device.ping(). Wired Centurion variants (e.g. PRO X 2
LIGHTSPEED 046d:0AF8) go through the generic fallback branch and never
recorded the raw version, so they displayed "Centurion 2.6" instead of
"Centurion 1.16".

Extract the protocol + centurion version recording into a helper and
call it from both branches.

---------

Co-authored-by: Peter F. Patel-Schneider <pfpschneider@gmail.com>
2026-04-14 11:43:23 -04:00
Peter F. Patel-Schneider 7520c9cc28 hidpp20: be defensive about no device features 2026-03-13 16:21:51 -04:00
Peter F. Patel-Schneider 94e94c1254 hidpp: add feature x1b04 flag sent by M510 4004 2026-03-13 16:21:51 -04:00
Peter F. Patel-Schneider 50fed28b3b device: permit onboard profiles data version 5 2025-12-04 04:04:27 -05:00
Peter F. Patel-Schneider 97b6b958c8 settings: expand new settings type 2025-11-12 14:33:34 -05:00
Peter F. Patel-Schneider f739331dc2 settings: add new settings type for structure-backed setting 2025-11-12 14:33:34 -05:00
Peter F. Patel-Schneider 2cb5fa4b97 settings: ignore hidden features 2025-10-27 15:27:13 -04:00
Peter F. Patel-Schneider 51630421b2 device: add new flags for reprogrammable keys feature 2025-10-22 16:46:40 -04:00
Peter F. Patel-Schneider 783bd5e4da device: fix bug with unknown tasks 2025-10-05 08:05:15 -04:00
DomHeadroom 90ab457ebe Rewrote string concatenation/format with f strings 2025-01-29 08:40:14 -05:00
MattHag 81567a98df hidpp20: Remove dependency to NamedInts
Replace ButtonBehaviors and ButtonMappingTypes with IntEnum.

Related #2273
2025-01-02 11:06:04 -05:00
MattHag bd00cc97ad
Estimate accurate battery level for some rechargable devices (#2745)
* battery: Extract battery level estimation into function

Test battery level estimation with sharp edges based on predefined
steps. Rename variable for clarity and add type hints.

Related #2744

* battery: Interpolate battery level for some rechargeable devices in percent

Estimate remaining battery based on measured battery voltage. Use linear
interpolation to achieve a smooth line instead of 10 percent jumps.

Fixes #2744
2025-01-02 10:58:07 -05:00
MattHag 207be464a5 Test notifications
Fixes #2711
2025-01-01 13:48:14 -05:00
MattHag 5f5c7cdcce Fixes on top of refactoring 2025-01-01 10:46:04 -05:00
MattHag ad3916e1b8 Fix KeyFlag conversion 2025-01-01 10:46:04 -05:00
MattHag 6903eeefcd Remove NamedInts: Convert LedFormChoices to enum
Related #2273
2025-01-01 10:46:04 -05:00
MattHag c9d7d7234a charge status: Refactor to enum and move to module of use
The charge status is solely used in the hiddpp20 module, thus put it
into this module.

Related #2273
2025-01-01 10:46:04 -05:00
MattHag c34fd3c2b0 Remove NamedInts: Convert LedRampChoice to flag
Related #2273
2025-01-01 10:46:04 -05:00
MattHag b19c886426 Remove NamedInts: Convert HorizontalScroll to enum
Related #2273
2025-01-01 10:46:04 -05:00
MattHag d27f7285e0 Remove NamedInts: Convert MappingFlag to flag
Related #2273
2025-01-01 10:46:04 -05:00
MattHag 5c736e9154 mapping flag: Move to module of use
The mapping flags are solely used in hiddpp20 module, thus put them into
this module.

Related #2273
2025-01-01 10:46:04 -05:00
MattHag 7c91d0b2db Remove NamedInts: Convert ActionId to enum
This data is not in use currently.

Related #2273
2025-01-01 10:46:04 -05:00
MattHag 5ca9c0a6ba Remove NamedInts: Convert Spec to enum
Related #2273
2025-01-01 10:46:04 -05:00
MattHag f54eeb7998 Remove NamedInts: Convert KeyFlag to Flag
Related #2273
2025-01-01 10:46:04 -05:00
MattHag 267b0a723d key flags: Move to module of use
The key flags are solely used in hiddpp20 module, thus put them into the
module.

Related #2273
2025-01-01 10:46:04 -05:00
MattHag 4c160d1723 Remove NamedInts: Convert Task to enum
Refactor code related to task and task ID.

Related #2273
2025-01-01 10:46:04 -05:00
MattHag 862cef1f77 hidpp20_constants: Refactor Gesture into enum
Replace Gesture NamedInts with enum.

Related #2273
2024-11-03 14:41:07 -05:00
MattHag c90146df31
Refactor: hidpp20 to use enum (#2647)
* Remove duplicated Param definition

Use constants from hidpp20 constants

Related #2273

* hidpp20/Param: Refactor to use IntEnum

Related #2273

* hidpp20_constants: Refactor to use IntEnum

Related #2273
2024-11-02 08:33:58 -04:00
MattHag 1afcfe4b57
refactor: use IntEnum for firmware and cidgroup constances
* Refactor: test_named_ints_flag_names

Shorten test and clarify behavior using binary numbers.

* Introduce plain flag_names function

This replicates the NamedInts functionality as plain function.

* Refactor FeatureFlag to use IntFlag

Replace NamedInts implementation with IntFlag enum and plain flag_names
function.

Related #2273

* Refactor FirmwareKind to use IntEnum

- Move general FirmwareKind to common module.
- Replace NamedInts implementation with IntEnum.
- Harden related HIDPP 1.0 get_firmware test.

Related #2273

* Refactor CID_GROUP, CID_GROUP_BIT to use IntEnum

Related #2273
2024-10-23 16:25:35 -04:00
MattHag 0cd9c0c9b5 Refactor: Introduce Feature enum
Convert Feature NamedInts to SupportedFeature integer enum.

Related #2273
2024-10-14 07:28:09 -04:00
MattHag 46366b2430 Fix warnings from automatic code inspections
Warnings found by automatic code inspection and partially tackled
- Drop distuitls inf favour of setuptools
- Replace deprecated pyudev.Device.from_device_number
- Remove unnecessary brackets
- Avoid access to private variables etc.
- Shadows built-in name
- Line length >120 characters
- Not a module level variable
- Simplify clause
and more
2024-10-11 07:42:38 -04:00
Peter F. Patel-Schneider c07c30baef device: handle error return on root feature 2024-08-23 19:41:10 -04:00
Peter F. Patel-Schneider d9d67ed738 device: handle unknown device kinds 2024-07-02 10:59:16 -04:00