From 1bcee309d38a22ee7da7a45332e2d18e4e069a7b Mon Sep 17 00:00:00 2001 From: Ken Sanislo Date: Sun, 19 Apr 2026 10:08:28 -0700 Subject: [PATCH] Fall back to FrameEnd 0x01; log _absent cache skips MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FrameEnd 0x02 (persistent) returns LOGITECH_INTERNAL (0x05) on the G522 even after a successful SetHostModeState + SetRgbZonesSingleValue sequence. Persistent commit probably requires onboard profile state we haven't mapped. Use 0x01 (transient) so the LEDs at least refresh visually; sort out persistence once we have a wireshark capture of the LGHUB commit sequence. check_feature_settings now logs at INFO when it skips a setting cached in the persister's _absent list despite the feature being present on the device. Without this, a previous run's failed build() silently suppresses the setting forever — users see no panel and no diagnostic. --- lib/logitech_receiver/settings_templates.py | 87 +++++++++++---------- 1 file changed, 47 insertions(+), 40 deletions(-) diff --git a/lib/logitech_receiver/settings_templates.py b/lib/logitech_receiver/settings_templates.py index d126c3d1..f2a72ad3 100644 --- a/lib/logitech_receiver/settings_templates.py +++ b/lib/logitech_receiver/settings_templates.py @@ -2052,19 +2052,14 @@ class HeadsetRGBColor(settings.Setting): resp.hex() if resp else resp, ) # FrameEnd: commit the frame. - # Byte 0 is frame_type: 0x01 = transient commit, 0x02 = persistent - # (saves to onboard NVS as baseline; survives the firmware's - # host-mode self-release window). 0x00 is silently discarded by - # firmware — canonical protocol doc was wrong. See - # HEADSET_RGB_HOSTMODE_WIRE_PROTOCOL.md. - # - # Use 0x02 for user-visible color picks so the color sticks even - # after the firmware releases host mode. Use 0x01 when setting - # black (lights off) — mirroring LGHUB's turn_off_lighting so we - # don't overwrite the NVS baseline with all-zeros. - is_off = r == 0 and g == 0 and b == 0 - frame_type = 0x01 if is_off else 0x02 - resp = device.feature_request(_F.HEADSET_RGB_HOSTMODE, 0x60, bytes([frame_type, 0x00, 0x00, 0x00])) + # Byte 0 is frame_type: 0x01 = transient commit, 0x02 = persistent. + # G522 firmware rejects 0x02 with LOGITECH_INTERNAL (0x05) — the + # persistent commit path may require additional state (e.g. onboard + # profile activation) we haven't mapped yet. Stick with 0x01 for + # now so the LEDs at least refresh visually; we can revisit 0x02 + # once a wireshark capture of the LGHUB sequence is available. + resp = device.feature_request(_F.HEADSET_RGB_HOSTMODE, 0x60, bytes([0x01, 0x00, 0x00, 0x00])) + frame_type = 0x01 logger.info( "HeadsetRGBColor: FrameEnd frame_type=0x%02X resp=%s", frame_type, @@ -2809,34 +2804,46 @@ def check_feature_settings(device, already_known) -> bool: for sclass in SETTINGS: if sclass.feature: known_present = device.persister and sclass.name in device.persister - if not any(s.name == sclass.name for s in already_known) and (known_present or sclass.name not in absent): - try: - setting = check_feature(device, sclass) - except Exception as err: - # on an internal HID++ error, assume offline and stop further checking - if ( - isinstance(err, exceptions.FeatureCallError) - and err.error == hidpp20_constants.ErrorCode.LOGITECH_ERROR - ): - logger.warning(f"HID++ internal error checking feature {sclass.name}: make device not present") - device.online = False - device.present = False - return False - else: - logger.warning(f"ignore feature {sclass.name} because of error {err}") + already = any(s.name == sclass.name for s in already_known) + if already: + continue + if not known_present and sclass.name in absent: + # Silent-skip cache from an earlier run's failed build(). Log at + # INFO so field diagnostics reveal when a setting is suppressed + # this way — users can clear stale entries by deleting their + # solaar config or we can add retry logic later. + if sclass.feature in device.features: + logger.info( + "check_feature_settings: skipping %s — cached in _absent despite feature %s being present; " + "delete the setting from ~/.config/solaar/config.yaml to retry", + sclass.name, + sclass.feature, + ) + continue + try: + setting = check_feature(device, sclass) + except Exception as err: + # on an internal HID++ error, assume offline and stop further checking + if isinstance(err, exceptions.FeatureCallError) and err.error == hidpp20_constants.ErrorCode.LOGITECH_ERROR: + logger.warning(f"HID++ internal error checking feature {sclass.name}: make device not present") + device.online = False + device.present = False + return False + else: + logger.warning(f"ignore feature {sclass.name} because of error {err}") - if isinstance(setting, list): - for s in setting: - already_known.append(s) - if sclass.name in new_absent: - new_absent.remove(sclass.name) - elif setting: - already_known.append(setting) - if sclass.name in new_absent: - new_absent.remove(sclass.name) - elif setting is None: - if sclass.name not in new_absent and sclass.name not in absent and sclass.name not in device.persister: - new_absent.append(sclass.name) + if isinstance(setting, list): + for s in setting: + already_known.append(s) + if sclass.name in new_absent: + new_absent.remove(sclass.name) + elif setting: + already_known.append(setting) + if sclass.name in new_absent: + new_absent.remove(sclass.name) + elif setting is None: + if sclass.name not in new_absent and sclass.name not in absent and sclass.name not in device.persister: + new_absent.append(sclass.name) if device.persister and new_absent: absent.extend(new_absent) device.persister["_absent"] = absent