From 671c07d861b2f8b86b2c68fae7a6ca6dc588a30a Mon Sep 17 00:00:00 2001 From: Ken Sanislo Date: Tue, 12 May 2026 06:54:30 -0700 Subject: [PATCH] cli: enumerate LED effects under RGB_EFFECTS / COLOR_LED_EFFECTS in solaar show (#3213) 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. --- lib/solaar/cli/show.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/lib/solaar/cli/show.py b/lib/solaar/cli/show.py index ab03a5b9..0dad16b7 100644 --- a/lib/solaar/cli/show.py +++ b/lib/solaar/cli/show.py @@ -112,6 +112,17 @@ def _print_centurion_dongle_features(receiver): print(f" Hardware: model {model_id}" f" rev {hw_rev} product {product_id:04X}") +_LED_CAPS_BITS = ((0x0001, "color"), (0x0002, "fade"), (0x0004, "period"), (0x0010, "direction"), (0xC000, "fw")) + + +def _decode_led_caps(caps): + names = [name for mask, name in _LED_CAPS_BITS if caps & mask] + other = caps & ~sum(m for m, _ in _LED_CAPS_BITS) + if other: + names.append(f"+{other:#06x}") + return f"0x{caps:04x}=" + ("+".join(names) if names else "none") + + def _battery_text(level) -> str: if level is None: return "N/A" @@ -363,6 +374,24 @@ def _print_device(dev, num=None): bands = hidpp20.get_onboard_eq_params(dev) if bands: print(f" EQ: {', '.join(f'{f}Hz:{g:+d}dB' for f, g, _q in bands)}") + elif feature == SupportedFeature.RGB_EFFECTS or feature == SupportedFeature.COLOR_LED_EFFECTS: + try: + infos = dev.led_effects + except Exception as e: + print(f" Effect enumeration failed: {e}") + infos = None + if infos and infos.zones: + for zone in infos.zones: + print(f" Zone {int(zone.index)} ({zone.location}): {len(zone.effects)} effect(s)") + for e in zone.effects: + entry = hidpp20.LEDEffects.get(e.ID) + name = entry[0].name if entry else f"Unknown(0x{e.ID:02x})" + caps = _decode_led_caps(e.capabilities) + params = ", ".join(str(p) for p in entry[1]) if entry and entry[1] else "—" + print( + f" [{e.index}] 0x{e.ID:02x} {name:<14} " + f"caps {caps:<28} default {e.period}ms params: {params}" + ) elif hidpp20.battery_functions.get(feature, None): print("", end=" ") _battery_line(dev)