Commit Graph

497 Commits

Author SHA1 Message Date
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 51532252df ui: better handling of missing devices 2026-03-13 13:41:59 -04:00
NaviMen 75aadc706c Add Ukrainian credit to the about model
​Hi! I have contributed to the Ukrainian translation and would like to be added to the translators list in the "About" section.
2026-02-28 09:50:44 -05:00
Peter F. Patel-Schneider 97311bed5f ui: handle missing receiver_path more gracefully 2026-01-08 12:38:14 -05: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 97b6b958c8 settings: expand new settings type 2025-11-12 14:33:34 -05:00
Peter F. Patel-Schneider b96d0bbe0b rules: fix debug messages for MouseClick rule 2025-11-02 21:01:33 +09:00
ian-jeong 68514d83c1 fix: center labels and remove buggy entry resizing logic 2025-09-30 10:42:25 -04:00
ian-jeong 6409fc2832 fix: correct spelling of 'completion' in diversion_rules.py 2025-09-30 10:42:25 -04:00
Peter F. Patel-Schneider 64943c90d9 ui: fix error when updating ChoiceControlBig box 2025-02-26 16:08:23 -05:00
Peter F. Patel-Schneider d8f321a5e9 ui: correctly handle selection of [empty] in rule editor 2025-02-11 17:37:21 -05:00
Peter F. Patel-Schneider 7d4f787344 ui: guard against typeerror when setting the value of a control box 2025-02-04 10:22:28 -05:00
DomHeadroom 90ab457ebe Rewrote string concatenation/format with f strings 2025-01-29 08:40:14 -05:00
daviddavid 297ccb9cc1 Fix logo not showing in about dialog box 2025-01-29 08:35:53 -05:00
Peter F. Patel-Schneider 5a03433f86 tests: fix ui desktop notifications test 2025-01-02 15:04:41 -05:00
Peter F. Patel-Schneider 3192fa1a34 testing: upgrade desktop notifications tests to take notifications availability into account 2025-01-02 10:47:53 -05:00
MattHag f5d80c30fa solaar/ui: Remove outdated logger enabled checks
Logger enabled checks clutter the code unnecessarily. The checks are
now handled in a custom logger class. Eventually they can be completely
removed in the future.

Related #2664
2025-01-02 09:26:31 -05:00
MattHag e9a58fb3e0 Introduce GTK signal types
Related #2273
2025-01-02 08:29:32 -05:00
MattHag ab52c4a7c0 Introduce error types
Related #2273
2025-01-02 08:29:32 -05:00
MattHag 72c9dfc50c Remove NamedInts: Convert NotificationFlag to flag
Related #2273
2025-01-01 10:46:04 -05:00
MattHag 571cdb5f2d Prepare refactoring of NotificationFlag
Ensure behavior stays the same.

Related #2273
2025-01-01 10:46:04 -05:00
MattHag b74e789715 Remove NamedInts: Convert Column to enum
Related #2273
2025-01-01 10:46:04 -05:00
MattHag 8bc42d20fb Enforce rules on RuleComponentUI subclasses
Enforce create_widgets and collect_values.

Related #2273
2025-01-01 10:46:04 -05:00
MattHag dd13993ff3 Simplify settings UI class
Classes shouldn't don't need to know about other settings classes.

Related #2273
2025-01-01 10:46:04 -05:00
MattHag cdaffce463 Refactor: Remove diversion alias
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
John Erling Blad 8518604155
i18n: Updated Norwegian Nynorsk (nn) (#2655)
Co-authored-by: John Erling Blad <jeblad@google.com>
2024-10-31 05:27:09 -04:00
John Erling Blad de033267fa i18n: Updated Norwegian Bokmål (nb) 2024-10-31 04:19:57 -04:00
Peter F. Patel-Schneider 2185a8390c ui: fix initialization bug for PackedRangeControl 2024-10-22 14:31:13 -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 194c385824 RuleComponentUI: Type hints methods 2024-10-11 07:42:38 -04:00
MattHag d1f9b9ca3d diversion_rules: Add type hints 2024-10-11 07:42:38 -04:00
MattHag 97d1e90ceb Fix signature of show
Fix diverged signature of RuleComponentUI subclasses.
2024-10-11 07:42:38 -04:00
MattHag 9f57955142 Action menu: Move context menu into own class
Reduce complexity of diversion dialog.
2024-10-11 07:42:38 -04:00
MattHag 3277015ab6 diversion: Add type hints 2024-10-11 07:42:38 -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 0f4d1aebcd ui/common: Introduce tests 2024-10-11 07:42:38 -04:00
MattHag a7ad625023 Fix about dialog 2024-10-11 07:42:38 -04:00
MattHag a75c4b9679 ui/about: Use Model-View-Presenter pattern for testability
Split model and view, and enable view mocks for unit tests without GDK.
2024-10-11 07:42:38 -04:00
Peter F. Patel-Schneider 4aada31b21 ui: augment pairing message for devices with multiple channels 2024-10-08 15:15:35 -04:00
MattHag 128ec43d70 solaar: Add type hints 2024-10-08 14:35:16 -04:00
MattHag d5af19be8a Make ui/desktop_notifications testable
Introduce unit tests.

Related #2273
2024-10-08 14:35:16 -04:00
MattHag e8ef262433 Rename ui/notify module to desktop notifications
Related #2273
2024-10-08 14:35:16 -04:00
MattHag 67829c5807
Clean up imports (#2537)
* Remove import as _ in solaar startup

Related #2273

* Remove import as _ in listener

Related #2273

* Remove import as _ in cli init

Related #2273

* Remove import as _ in gtk

Related #2273

* Remove import as _ in show

Related #2273

* Remove import as _ in tray

Related #2273

* Remove import as _ in profiles

Related #2273

* Remove import as _ in config

Related #2273

* Remove import as _ in config panel

Related #2273

* Remove import as _ in window

Related #2273

* Remove import as _ in pair

Related #2273

* Remove import as _ in pair window

Related #2273

* Remove import as _ in cli package

Related #2273

* Remove import as _ in ui package

Related #2273

* Remove commented out code

Related #2273

* Use constant for Logitech ID
2024-07-15 08:37:18 -04:00
Anderson Silva 8ab8cb0225
docs: Update about.py (#2535)
Update copyright date in about page to reflect current year (2024)

Follow-up to #2074
2024-06-23 17:56:52 -04:00
MattHag 5a63e44d58 Remove empty comment lines
Remove hashtags solely used for structuring.

Related #2273
2024-06-02 09:54:21 -04:00
Matthias Hagmann c9dc232951 Refactor: Use dataclasses and enums
Replace unnecessary NamedInts in favour of default data types.
Simplify interfaces by reducing possible input from strings to members
of an enum.
2024-05-22 21:14:41 -04:00
Matthias Hagmann 7d171b1d09 Refactor: Use dataclasses and enums
Replace NamedTuples with more flexible dataclass, which also support
type hints. Introduce enums to replace constant strings, which need to
be kept in sync. Also enhances interfaces by limiting it to the enum
values. Remove unused variables.
2024-05-22 21:14:41 -04:00