From d1e2a08d8f74a82f24ff19ae9531217fa571f682 Mon Sep 17 00:00:00 2001 From: Ken Sanislo Date: Fri, 24 Apr 2026 20:38:20 -0700 Subject: [PATCH] RGB effects probe: dump sub-device feature table MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _log_feature_table only walked FeaturesArray's parent inverse map, so the corpus dump showed the dongle's 5-6 parent features and `?` for indexes 6+ — which gave the wrong impression that the headset had nothing else exposed. The actual headset features (0x0620 RGB hostmode, 0x0621/0x0622 RGB effects, the LogiVoice 0x0901-0x0907 set, 0x0636 onboard EQ, 0x0601 mic mute, …) live behind the Centurion bridge in FeaturesArray.sub_inverse, keyed by sub-device feature index. Enumerate sub_inverse separately and emit a second log line so the next probe run captures the full sub-device feature list. Also include the SupportedFeature enum name when available so the analyst doesn't have to keep cross-referencing IDs. --- lib/logitech_receiver/rgb_effects_probe.py | 31 +++++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/lib/logitech_receiver/rgb_effects_probe.py b/lib/logitech_receiver/rgb_effects_probe.py index fa4d642f..ea0f6229 100644 --- a/lib/logitech_receiver/rgb_effects_probe.py +++ b/lib/logitech_receiver/rgb_effects_probe.py @@ -21,15 +21,38 @@ def _hex_or_none(data) -> str | None: return data.hex() if data else None +def _format_feature(feat) -> str: + """Render a feature for the log: 0x{id:04X}{:NAME} when known, else raw.""" + if feat is None: + return "?" + try: + return f"0x{int(feat):04X}:{feat.name}" + except (AttributeError, TypeError): + return f"0x{int(feat):04X}" if feat is not None else "?" + + def _log_feature_table(device) -> None: if not device.features: return try: - pairs = [] + # Parent features live in FeaturesArray.inverse, indexed by their + # parent feature-set position. On Centurion devices these are the + # ones the dongle itself exposes (typically 5-6 entries). + parent = [] for idx in range(len(device.features)): - feat = device.features[idx] - pairs.append(f"{idx}:0x{int(feat):04X}" if feat is not None else f"{idx}:?") - logger.info("RGB probe: feature table for %s: %s", device, ", ".join(pairs)) + parent.append(f"{idx}:{_format_feature(device.features[idx])}") + logger.info("RGB probe: parent features for %s: %s", device, ", ".join(parent)) + + # Centurion sub-device features live in FeaturesArray.sub_inverse, + # keyed by sub-device feature index. These are where the actual + # headset features (0x0620/0x0621/0x0622, LogiVoice, EQ, mic mute, + # …) live — without dumping them the log shows only the dongle's + # parent features and gives the wrong impression that the device + # has nothing else. + sub_inverse = getattr(device.features, "sub_inverse", None) + if sub_inverse: + sub = [f"{idx}:{_format_feature(feat)}" for idx, feat in sorted(sub_inverse.items())] + logger.info("RGB probe: sub-device features for %s: %s", device, ", ".join(sub)) except Exception as e: logger.info("RGB probe: feature-table dump failed: %s", e)