* 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> |
||
|---|---|---|
| .github | ||
| bin | ||
| docs | ||
| lib | ||
| po | ||
| rules.d | ||
| rules.d-uinput | ||
| share | ||
| tests | ||
| tools | ||
| .coveragerc | ||
| .git-blame-ignore-revs | ||
| .gitignore | ||
| .pre-commit-config.yaml | ||
| .python-version | ||
| CHANGELOG.md | ||
| COPYRIGHT | ||
| LICENSE.txt | ||
| MANIFEST.in | ||
| Makefile | ||
| README.md | ||
| RELEASE.md | ||
| RHEL.md | ||
| Release_Notes.md | ||
| mkdocs.yml | ||
| pyproject.toml | ||
| release.sh | ||
| setup.py | ||
README.md
Solaar
Solaar is a Linux manager for many Logitech keyboards, mice, and other devices that connect wirelessly to a Unifying, Bolt, Lightspeed or Nano receiver as well as many Logitech devices that connect via a USB cable or Bluetooth. Solaar is not a device driver and responds only to special messages from devices that are otherwise ignored by the Linux input system.
More Information - Usage - Capabilities - Rules - Manual Installation - Known Issues
Solaar supports:
- pairing/unpairing of devices with receivers
- configuring device settings
- custom button configuration
- running rules in response to special messages from devices
For more information see the main Solaar documentation page. -
Installation Packages
Up-to-date prebuilt packages are available for some Linux distros (e.g., Fedora) in their standard repositories. If a recent version of Solaar is not available from the standard repositories for your distribution, you can try one of these packages:
- Arch solaar package in the extra repository
- Ubuntu/Kubuntu package in Solaar stable ppa
- NixOS Flake package in Svenum/Solaar-Flake
Solaar is available from some other repositories but may be several versions behind the current version:
- a Debian package, courtesy of Stephen Kitt
- a Ubuntu package is available from universe repository
- a Gentoo package, courtesy of Carlos Silva and Tim Harder
- a Mageia package, courtesy of David Geiger