Commit Graph

119 Commits

Author SHA1 Message Date
Ken Sanislo ac7add6297 G522 LIGHTSPEED headphones support
End-to-end support for the G522 LIGHTSPEED gaming headset (and the
infrastructure that comes with it for related Centurion-bridged headsets).

Hardware/protocol layer:

- centurion.py — CenturionReceiver: lightweight receiver-like wrapper
  for Centurion-bridged dongles (PRO X 2 LIGHTSPEED already, G522 added
  here). Handles deferred init for 0x50 devices that don't respond to
  the initial probe, sub-device feature discovery via bridge, online
  detection via ping.

- base.py — Centurion handle state, device_addr probe (256-candidate
  scan), 0x50/0x51 frame routing, bridge TX/RX framing.

- hidpp20.py — Centurion sub-device feature query and bridge request
  routing. CENTURION_DEVICE_INFO / CENTURION_BATTERY_SOC / CENTURION_*
  query helpers in centurion.py for parent + sub paths.

- device.py — Centurion device creation path, USB product-string
  fallback for naming, bridge sub-device error handling.

- notifications.py — HEADSET_ADVANCED_PARA_EQ band-change events,
  HEADSET_MIC_MUTE state-change events; both route to the relevant
  setting via setting_callback.

Headset settings:

- HeadsetEcoMode, HeadsetDoNotDisturb, HeadsetMicMute, HeadsetMicSNR,
  HeadsetAINR / HeadsetAINRLevel, HeadsetSidetone, HeadsetMicGain,
  HeadsetMixBalance, HeadsetAutoSleep — bool / range / choice settings
  for the headset-specific feature pages.

- HeadsetOnboardEQ + HeadsetActiveEQPreset — onboard EQ slot picker
  with active-preset tracking via the EQ change event subscription.

- HeadsetAdvancedEQ — multi-band parametric EQ (advanced_para_eq.py
  module handles getEQInfos / getCustomEQ / band-change subscription;
  setting builds the per-band sliders).

- HeadsetLEDControl + HeadsetLedsPrimary + HeadsetPerZoneLighting —
  RGB feature set (0x0620 HEADSET_RGB_HOSTMODE) with shared zone-write
  helper (headset_rgb.py) and the G522 layout for the per-key painter
  (perkey/layouts/headset_g522.py).

- LogiVoice family (12 settings: NR / NG / Compressor / De-esser /
  Depopper / Limiter / HPF, each with state + parameters) — voice
  processing pipeline. logivoice.py module handles probe + parsing.

Probes:

- rgb_effects_probe.py — runtime probe for headset RGB feature
  variants (0x0621 onboard-effects, 0x0622 signature-effects, 0x0623).

Tests:

- test_base.py / test_device.py / test_hidpp20_complex.py — Centurion
  framing, device-creation path, sub-device feature parsing.

- test_setting_templates.py — fixtures for the new headset settings.

Closes pwr-Solaar/Solaar#3181.
2026-05-21 10:13:00 -04:00
Ken Sanislo ad96dd395b PerKeyLighting: drop misleading live-read output in solaar show
The 0x8081 PerKeyLighting v2 protocol has no GetIndividualRgbZones
function — there's no way to ask the device what colors are currently
on the per-key buffer over HID++. PerKeyLighting.read() papered over
this gap by fabricating an all-"No change" sentinel map whenever the
caller asked for a live (uncached) read, which produced misleading
solaar show output like:

    Per-key Lighting (saved): {1:0xc01c28, 2:0xc52d26, ...}
    Per-key Lighting        : {1:No change, 2:No change, ...}

— even with the keyboard clearly running the saved colors.

Two-part fix:

1. PerKeyLighting.read() now returns self._value if populated (from
   prior write or persister) regardless of `cached`. Only fabricates
   the all-"No change" map when there's truly no state — fresh device,
   no prior write, nothing persisted. This is the right starting state
   in that case because per-key writes are additive over a buffer of
   unknown content.

   Other callers benefit too: solaar config calls read(cached=False) on
   per-key settings to display current values, and prior to this
   change it would get fabricated sentinels back instead of the
   in-memory map.

2. Add a `live_readable = True` class attribute to Setting (default
   preserves existing behavior). Override to False on PerKeyLighting.
   solaar show gates its live-read print on this flag, so non-readable
   settings show only the (saved) line — which is the authoritative
   record of what was last written to a setting whose live state can't
   be read back.

After both fixes solaar show prints just:

    Per-key Lighting (saved): {1:0xc01c28, 2:0xc52d26, ...}

— honest about what we know.

690 tests pass; pre-commit clean.
2026-05-12 12:51:36 -04:00
Ken Sanislo d8422d78d1 Add per-key RGB color painter and replace MAP_CHOICE color validator
Replace the per-key dropdown UI (MapChoiceControl) with a Cairo-rendered
keyboard canvas where users can paint colors directly onto keys.

Editor (lib/solaar/ui/perkey/):
  - Cairo DrawingArea renders cells from a Layout dataclass; bound cells
    take their painted color, unset cells show a diagonal hash whose base
    color matches the device's rgb_zone_* setting.
  - Tools: brush, drag-rectangle, flood-fill (4-adjacent, Paint-style),
    and a directional gradient (line A->B projected across the matrix
    with cells past the endpoints clamped to the endpoint colors).
  - GradientSwatch is the single source of truth for the gradient's two
    colors; the canvas reads from it on each gradient stroke.
  - Palette: GTK ColorButton plus an unset toggle that paints the
    "no change" sentinel (-1).
  - PerKeyEditorDialog auto-sizes from the canvas's size_request, so a
    104-key keyboard opens wide and a 8-LED mouse opens compact.
  - Editor consumes only a narrow PerKeyColorSink protocol; never imports
    from lib/logitech_receiver, preserving the FE/BE seam.
  - Per-device palette state (active + previous color) persists via the
    existing persister under a _palette: prefixed key.

Layouts:
  - ANSI 104-key full-size and TKL keyboard layouts.
  - G502 X family mouse layout (zones 1-8 -> labels A-H).
  - Generic registry: register_layout(feature, matcher, layout). A
    _name_contains() helper builds case-insensitive substring matchers
    against device codename / name. Unknown devices fall back to a flat
    strip of all reported zones.

Validator (open value space):
  - New Range dataclass and MapRangeValidator extending Validator
    directly (kind=MAP_CHOICE for dispatch compatibility). Replaces the
    ChoicesMapValidator on PerKeyLighting -- the named-color universe
    (COLORSPLUS) was rejecting any picker color outside its ~20 entries.
    Other MAP_CHOICE settings are untouched.

Integration:
  - Setting base gains an editor_class string attribute. config_panel's
    _create_sbox resolves it via importlib before the kind dispatch, so
    PerKeyLighting routes to the new editor without a new Kind value.
  - CLI gains a hex/dec parser for open-value MAP_CHOICE settings:
      solaar config <dev> per-key-lighting A 0xFF00FF
  - Diversion rule editor skips Range-valued MAP_CHOICE settings'
    value-selector instead of crashing on the open value space.
  - pycairo declared in install_requires; transitively present on most
    systems but now explicit for pip-from-source installs.

Tests in test_setting_templates.py updated for the new validator.
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
danicc097 817c90e561
replace color picker (#3028)
* replace color picker
2025-11-15 11:05:59 -05:00
Peter F. Patel-Schneider 0fd262424e settings: add setting for HAPTIC feature 2025-11-12 14:50:46 -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 198067519d settings: better support RGB Effects - not readable 2025-03-03 14:11:09 -05:00
MattHag 5a9725ee17 Add type hints
Related #2273
2025-01-01 10:46:04 -05:00
MattHag 3636ed78bb Refactor: Convert Kind to IntEnum
Related #2273
2025-01-01 10:46:04 -05:00
MattHag 03de6fb276 Split up huge settings module
- Move validators into their own module.
- Convert Kind to IntEnum

Related #2273
2025-01-01 10:46:04 -05: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
MattHag 89233957dc settings: Add tests 2024-10-11 07:42:38 -04:00
MattHag be83dac209
hid: Convert definition of HID registers to enum
* Refactor HID Register definitions

Use enums for distinct type hints, easy discovery of registers.
Make constants uppercase and benefit from enum auto-completion.

Related #2273

* Improve type hints: Registers
2024-06-02 10:34:00 -04:00
MattHag 815dce07be Unify imports in logitech package
Related #2273
2024-05-23 16:42:18 -04:00
MistificaT0r 4225fce8d7
po: update Russion translation and have all strings translated
* update Russian translation

* Fixed translation display in GUI

* fix checks / Fixed translation display in GUI
2024-04-21 11:36:39 -04:00
Peter F. Patel-Schneider d12575b47d tests: test GESTURES settings 2024-04-13 18:38:44 -04:00
Peter F. Patel-Schneider afe04b9804 settings: remove unused code and fix but in EQUALIZER setting 2024-03-27 11:15:15 -04:00
Peter F. Patel-Schneider 1a874c39d7 settings: permit continuing when a read during pushing fails 2024-03-24 15:44:15 -04:00
Peter F. Patel-Schneider 17bbc9c4ea settings: simple change to improve testability 2024-03-23 10:11:30 -04:00
Matthias Hagmann e92f1a8a0b Automatically upgrade strings to f-string
Used flynt to convert strings to f-strings, where safely possible.

Usage:
flynt .

Related #2372
2024-03-13 11:02:50 -04:00
Matthias Hagmann 7ef3059b69 clean up: Remove editor specific marks
Related #2273
2024-02-29 10:10:46 -05:00
Peter F. Patel-Schneider 67be689866 device: reorder code in settings 2024-02-25 07:23:03 -05:00
Matthias Hagmann eb937fcc3a Manually fix linter issues
Related #2295
2024-02-20 15:41:10 -05:00
Matthias Hagmann 7774569971 Apply ruff format
Run ruff auto formatting using:
ruff format .

Related #2295
2024-02-20 15:41:10 -05:00
Peter F. Patel-Schneider ed248c62b9 device: add callback to call when changing a setting 2024-02-20 06:19:23 -05:00
Matthias Hagmann 5f487dd3b2 logitech_receiver: Move hidpp20 constants into new module
Related #1097
2024-02-20 05:58:33 -05:00
MattHag 87658fb189
logging: Simplify logger instantiation
* logging: Simplify logger instantiation

Relates #2254

* logging: Remove aliases

Relates #2254

* logging: Replace deprecated warn with warning

Related #2254

* logging: Fix mistake

Related #2257
2024-02-10 13:55:27 -05:00
Peter F. Patel-Schneider 23517048d4 device: clean up data for LED effects 2024-02-09 08:36:36 -05:00
Peter F. Patel-Schneider 7c441cc652 ui: better startup behavior for LED effect settings 2024-02-09 08:36:36 -05:00
Peter F. Patel-Schneider 73d091c86f ui: add UI for LED Zone control 2024-02-09 08:36:36 -05:00
Peter F. Patel-Schneider 3328a6085f device: add settings for LED Zone control 2024-02-09 08:36:36 -05:00
Peter F. Patel-Schneider 72c5860a1e device: support backlight levels and duration 2024-02-06 12:49:08 -05:00
Peter F. Patel-Schneider 79de531858 settings: read and check before write for range settings 2023-01-09 13:22:45 -05:00
Peter F. Patel-Schneider 99f9c8762e settings: only record changes of scroll wheel ratchet 2022-12-09 06:59:37 -05:00
ejsc1 5656f90cdb
misc: update yapf version
* misc: update yapf version v2

* Update .pre-commit-config.yaml

Co-authored-by: Peter F. Patel-Schneider <pfpschneider@gmail.com>
2022-11-26 22:37:32 -05:00
Peter F. Patel-Schneider 67c1d7e778 settings: don't add non-existant key in raw xy processing 2022-10-13 16:00:15 -04:00
Peter F. Patel-Schneider 55cc8ed5dc settings: add optional save argument to write_key_value methods 2022-10-06 22:00:38 -04:00
Peter F. Patel-Schneider 5ed4c05db8 device: add conditional delay to apply all to get around race with Linux driver 2022-09-15 09:51:24 -04:00
Peter F. Patel-Schneider cacf94b6f7 settings: allow multiple keys for mouse gestures and dpi sliding 2022-07-23 06:44:57 -04:00
Peter F. Patel-Schneider c741b372e1 settings: remove unused class variables 2022-06-29 09:56:14 -04:00
Peter F. Patel-Schneider 5dc9be0a04 settings: differentiate debug messages for different setting types 2022-06-21 18:39:37 -04:00
Peter F. Patel-Schneider 777a7138c1 settings: add packed ranges setting 2022-06-21 18:39:37 -04:00
Peter F. Patel-Schneider f1e2a0c449 receiver: use built-ins for conversions between int and byte string 2022-06-21 18:39:37 -04:00
Peter F. Patel-Schneider 85bcfe7310 device: fix bug in feature lookup 2022-04-29 07:24:26 -04:00
Peter F. Patel-Schneider fb74e3b657 settings: store None in persister for non-persisting settings 2022-04-26 09:03:20 -04:00
Peter F. Patel-Schneider 3ca97cccd1 settings: fix determination of whether to read before writing boolean setting 2022-04-15 09:21:08 -04:00
Kian-Meng Ang 15b92a0f40
misc: Fix typos 2022-04-12 17:18:11 -04:00
Peter F. Patel-Schneider bb20631a27 misc: remove support for Python 2 unicode 2022-04-11 13:20:42 -04:00