Previously the editor and palette listened to Gtk.Settings
notify::gtk-theme-name (and notify::gtk-application-prefer-dark-theme)
to re-render their themed icons on theme switch. Two problems:
1. The initial icon load happened during widget construction, before
the buttons were attached to the toolbar — so the style context
resolved to a default (often white) foreground rather than the
actual theme text color. Icons showed up white until the first
theme-change event.
2. Settings notify fires *before* GTK's CSS engine re-resolves styles
for the new theme. Reading the style context's foreground from
that handler returned the previous theme's color, so toggling
light <-> dark left both states settling on the same shade.
Move both responsibilities into a new attach_themed_icon helper in
_icons.py: it does the initial load, connects to the *button's own*
style-updated signal, and rebuilds the icon on each emission. That
signal fires *after* CSS resolution (both on first realize and on
runtime theme switches), so the foreground we read is always the
current one.
A per-button color-key guard skips the rebuild when the resolved
foreground hasn't changed, so unrelated style-updated emissions
(hover, focus, active) don't trigger needless re-renders.
The handler is connected to the button itself, so GTK cleans it up
when the button is destroyed; both editor.py and palette.py drop
their bespoke Gtk.Settings handler bookkeeping.
The "no change" hash overlay used a single black-or-white stripe
color picked by the base luminance, so the perceived average of an
unset cell was uniformly biased toward black or white instead of
sitting on the actual base color. That made dark base cells look
darker than they really are on the keyboard, and light ones lighter.
Draw two interleaved stripe sets at base ± offset (per channel),
spaced by half-period so the dark and light stripes alternate
evenly across the cell. Equal coverage of the two stripe colors
keeps the perceived average at base.
When a channel is too close to 0 or 1 to fit the full offset
(±0.22), halve the offset on the constrained side. The cell's
average then drifts at the limits but stays centered on base
everywhere else — verified visually across mid-tones, primaries,
and near-black/white bases.
The "no zone base color known" path keeps the previous neutral-grey
look unchanged; the average-preservation property only applies when
there is a base color to preserve.
The "Unset" toggle button next to the color picker used a custom
HashSwatch drawing area that mirrored the canvas's diagonal-hash
pattern for unset cells. The pattern was visually noisy at button
size and tied the button's appearance to the current zone base
color (the swatch had to be told the base color via
set_zone_base_color so it could redraw).
Use the Tabler "palette-off" symbolic icon instead. It reads as
"clear / no paint" at a glance, is independent of the zone base
color, and matches the icon style of the tool buttons on the other
end of the toolbar.
Extract the icon loader (themed_icon_image, ensure_icon_path) from
editor.py into a new private module _icons.py so palette.py can
reuse it; the new palette tracks its own Gtk.Settings notify signals
to re-render the icon on theme switches, and disconnects them via a
new Palette.shutdown() called from the editor's shutdown.
Adds MIT-licensed Tabler palette-off icon under share/solaar/icons/,
see THIRD_PARTY.md.
Tool icon pixbufs are baked at construction time using the button's
current style context foreground color. Without this, switching the
desktop theme (e.g. light <-> dark) at runtime leaves the editor's
icons stuck in the previous color while the rest of the UI updates.
Subscribe to Gtk.Settings notify::gtk-theme-name and
notify::gtk-application-prefer-dark-theme; on either, replace each
themed button's child Gtk.Image with a freshly-recolored one.
Disconnect both signals from PerKeyEditor.shutdown so the editor
doesn't leak handlers across openings.
The Brush / Rect / Fill tool buttons in the per-key color editor now
show icons instead of text labels, with the original labels retained
as tooltips and accessible names.
Icons are recolored at load time to match the active GTK theme's text
foreground (light grey on dark themes, dark on light), by reading the
button's style context and substituting currentColor in the SVG before
loading the pixbuf. GTK's stock symbolic loader is bypassed because it
only recolors fill palette stand-ins and ignores stroke="currentColor",
which is what the source SVGs use.
Adds MIT-licensed Tabler icons under share/solaar/icons/, see
THIRD_PARTY.md.
24-bit RGB values stored in PerKeyLighting per-key maps and in
LEDEffectSetting `.color` fields currently dump as decimal integers in
both the YAML config file and `solaar show` output:
per-key-lighting: {1: -1, 2: 16733440, 3: 16755200, ...}
LEDs Keys: {1:-1, 2:16733440, 3:16755200, ...}
Hex is the canonical RGB representation. Render colors as `0xrrggbb`
everywhere: solaar show output, repr, and YAML config dumps. Both new
values and legacy values from pre-existing YAML configs migrate
transparently.
Implementation: a `ColorInt(int)` subclass in `common.py`.
- `str(c)` / `repr(c)` → `'0xrrggbb'` for 0..0xFFFFFF; falls back to
decimal for out-of-range values so sentinels like
`COLORSPLUS["No change"] = -1` still display naturally.
- Constructor accepts ints AND hex strings (`'0xrrggbb'` or
`'#rrggbb'`) so pre-existing configs that wrote decimal continue
to load.
- YAML representer emits a hex int literal (`tag:yaml.org,2002:int`
with style `'0xrrggbb'`). YAML 1.1 parses hex int literals back as
plain ints with no custom loader registration — values round-trip
cleanly without a custom YAML tag.
Wiring:
- `Range` gains a `value_type=int` field; `MapRangeValidator.validate_read`
wraps results through `rng.value_type(...)`. PerKeyLighting's
`_COLOR_RANGE` sets `value_type=ColorInt`; other Range users keep
the int default with no behavior change.
- PerKeyLighting overrides `update()` and `update_key_value()` to wrap
raw ints in `ColorInt` at write time. `type(v) is int` (exact
match, not isinstance) deliberately excludes NamedInt sentinels and
avoids re-wrapping existing ColorInts.
- `LEDEffectSetting.__init__` wraps the `color` param in `ColorInt`
with the same guard, so zone-effect color round-trips as hex
through `yaml.dump(setting)` / `val_to_string`.
- `MapRangeValidator.to_string` re-wraps raw ints loaded from YAML
(which `yaml.safe_load` returns as plain Python ints regardless of
the choice's `value_type`) through `rng.value_type` before
formatting. Without this, `solaar show` would render legacy saved
values as decimal even after the rest of the pipeline is hex-aware.
Coverage: 12 new unit tests across `test_common.py` and
`test_settings_validator.py`:
- ColorInt str/repr, equality with plain int, hex-string constructor
(0x / 0X / # prefixes), out-of-range fallback to decimal, YAML
dump format, plain-int round-trip on load, dict-value formatting.
- MapRangeValidator.to_string: plain-int re-wrap via value_type,
pass-through for already-wrapped ColorInts, NamedInt sentinel
preservation, and no behavior change for int-typed Ranges.
Existing test fixtures updated: _PERKEY_COLOR_RANGE now carries
value_type=ColorInt to match runtime _COLOR_RANGE.
702 tests pass; pre-commit clean.
Print each zone's effect slot (index, wire ID, name from LEDEffects
table, decoded capabilities bits, default period, and the param
keys the host would expose as widgets) for any device with 0x8070
or 0x8071. Unknown wire IDs print as 'Unknown(0xXX)' rather than
being skipped, so probing surfaces effects the table doesn't yet
model.
Capabilities are decoded against the documented bits (color, fade,
period, direction, fw-handled) with any remaining bits printed as
a residual hex value, since LGHUB sets additional bits on newer
effects whose semantics aren't yet known.
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.
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.
The AnalogButtons feature packs each tunable in bits 7..2 of its byte
(wire = logical << 2); byte 2 bit 0 is a firmware-managed sensitivity
flag, the rest of the low bits are reserved-zero. Solaar 1.1.19 sent
the slider value verbatim, so any logical 1/2/3 produced wire bytes
0x01/0x02/0x03 — non-zero reserved bits and below the logical minimum,
hence INVALID_ARGUMENT (issue #3202). Only multiples of 4 happened to
land on a valid wire byte.
Decode bytes 1/2/3 of getConfig and caps[2..4] of getCapabilities by
right-shifting 2; left-shift the user value by 2 on setConfig and OR
back the prior sensitivity bit on rapid-trigger writes. Defaults
fallbacks updated to logical mid-points and the validator maxima now
reflect the real ranges (actuation 1..10, rapid trigger 1..5, haptics
0..5).
Persisted values from 1.1.19 were raw wire bytes (e.g. 40), which now
exceed the new max and would fail apply()'s prepare_write. A new
_AnalogButtonSetting subclass migrates such values in _pre_read by
dividing by 4 when the result lands inside the new valid range, and
rewrites the persister so the migration is one-shot.
* Add CLI --slot unpair for Lightspeed receivers
Adds Receiver.force_unpair_slot(), a low-level method that writes the
RECEIVER_PAIRING unpair register (action 0x03) for a given slot regardless
of cache state, bypassing may_unpair and re_pairs gates. Intended for
clearing stale pairings on Lightspeed receivers where Solaar cannot read
the slot's pairing info or the device is no longer reachable on RF.
Extends the `solaar unpair` CLI with three new flags:
--receiver <name> select which receiver to target
--slot <N> target a specific slot number directly
--dry-run print what would happen without issuing the write
The --slot path is gated to Lightspeed receivers only (by receiver_kind)
so Unifying/Bolt/Nano behavior is unchanged. It populates the cache first
and prints the current slot contents so the user can confirm what is
about to be cleared, but does not refuse based on active/offline state —
the explicit --slot N is treated as sufficient intent.
Verified end-to-end on a C547 dual-slot Lightspeed receiver: stale slot
cleared, RECEIVER_INFO sub-registers 0x21/0x31 went to None, connection
count register dropped from 2 to 1, running solaar daemon picked up the
change in real time via the existing DJ pairing notification hook.
Covered by 5 unit tests against a mocked Receiver: empty slot, stale
sentinel, active device invalidation, register write failure, closed
handle.
* Enable GUI unpair for Lightspeed receivers
Flip _lightspeed_receiver() to may_unpair: True so the GUI unpair button
becomes sensitive for Lightspeed-paired devices, and route the GUI unpair
action through _unpair_device(n, force=True) so the unpair register write
actually fires instead of short-circuiting to cache invalidation.
The previous GUI path called `del receiver[n]`, which dispatches to
Receiver.__delitem__ → _unpair_device(n, force=False). On receivers with
re_pairs=True (Lightspeed, Nano) that hits the cache-invalidation branch
at receiver.py:391 and never writes the unpair register — a "fake unpair"
that would have left the slot bound on the hardware even after the button
was enabled.
With force=True, the GUI now issues RECEIVER_PAIRING action 0x03 for the
selected slot, matching the CLI unpair path (cli/unpair.py:39) which has
always used force=True. Lightspeed and Unifying unpair behavior are now
symmetric: the button is enabled, the confirmation dialog is shown, and
the register write is performed.
The pair/add flow is untouched: it still uses set_lock(device=0) which
lets the receiver firmware pick an empty slot, re_pairs remains True so
the listener's silent-replace branch continues to handle re-pair into an
occupied devnumber. Verified on dual-slot C547 hardware that pairing into
an empty slot preserves the occupant of the other slot.
Stale pairings where Solaar can't enumerate the slot (no cached device
row to right-click) still require the --slot CLI from the preceding
commit — that path is orthogonal to this GUI enablement.
* Apply suggestion from @pfps
Lightspeed receivers don't appear to re-pair.
---------
Co-authored-by: Peter F. Patel-Schneider <pfpschneider@gmail.com>
* Handle composite IntFlag.name on Python < 3.11
On Python < 3.11, IntFlag.name returns None for composite flags
(e.g. NotificationFlag(0x000900) = SOFTWARE_PRESENT|WIRELESS).
The previous fix (#3184) returns an empty list in this case, losing
the flag names. This commit decomposes composite flags manually by
iterating over enum members, producing correct results on all
supported Python versions (>= 3.8 as declared in setup.py).
Add regression tests covering composite flags and None input.
Tested on:
- Python 3.9.2 (Debian 11): 56/56 passed
- Python 3.11.15 (pyenv): 56/56 passed
Refs #3184
* Fix line length style violation (E501)
Move comment above the parametrize entry to stay under 127 chars.
---------
Co-authored-by: avercelli <avercelli@vulog.com>
* Treat empty hidraw read as device removal (EOF)
When select() reports a hidraw fd as readable but os.read() returns
empty bytes, that's EOF per POSIX — the device has been removed.
Previously this was silently treated as no data, causing the listener
to loop indefinitely on a gone device instead of cleaning up.
* Fix test_ping_errors missing mocks for _read_input_buffer and write
The test was not mocking _read_input_buffer or write, so ping() would
call into real hidapi.read() with a fake handle (fd 1). The empty-read
EOF detection added in the previous commit made this consistently fail
by raising OSError → NoReceiver before reaching the mocked _read path.
Add the same mocks used by the adjacent test_request_errors.
---------
Co-authored-by: Peter F. Patel-Schneider <pfpschneider@gmail.com>
* 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>
flag_names() crashes with AttributeError when NotificationFlag returns
a value whose .name attribute is None. This occurs with certain receiver
firmware versions (Nano C52F, Unifying C52B, Bolt C548).
Return an empty list instead, consistent with the List[str] return type.
Fixes#3184
Co-authored-by: avercelli <avercelli@vulog.com>
* feat: PRO X 2 Superstrike support with click haptics, actuation point and rapid trigger config support
* feat: PRO X 2 Superstrike docs
* docs: document PRO X 2 Superstrike features, device entry, and capabilities
* fix: code review points
# Conflicts:
# lib/logitech_receiver/hidpp20_constants.py
* Fix the write_key_value for dpi_extended
I was playing with the branch from the MR and I wanted to fix the cli stuff, it now properly sets when I use:
solaar config 1 dpi_extended X 400
Should be enough
Signed-off-by: Shane Fagan <mail@shanefagan.com>
* Fix ruff style check
---------
Signed-off-by: Shane Fagan <mail@shanefagan.com>
Co-authored-by: Shane Fagan <mail@shanefagan.com>
* Bulgarian PO files
Български превод на приложението
* Delete bg.mo
* Delete bg.po
* Delete bg.po~
* Bulgarian Translation
Български превод на приложението.
* Update i18n.md
* Delete po/bg.mo
* Delete po/bg.po~
* Add installation guide for Solaar on RHEL 10
Document steps to install and run Solaar on RHEL 10, including environment setup and troubleshooting.
* Add interactive RHEL installer script
Replace scripts=glob("bin/*") with entry_points console_scripts
to make solaar installable via pipx. pipx requires packages to
define console_scripts entry points to detect command-line apps.
* Skip Logitech webcams (PID 0x0800 to 0x09FF) to prevent them from locking up during hidpp checks #3145
* Skip Logitech webcams (PID 0x0800 to 0x09FF) to prevent them from locking up during hidpp checks #3145 - format comment
* Skip Logitech webcams (PID 0x0800 to 0x09FF) to prevent them from locking up during hidpp checks #3145 - format hex
* Skip Logitech webcams from hidpp checks #3145 - local constant for LOGITECH_VENDOR_ID as per code review