diff --git a/lib/logitech_receiver/settings_templates.py b/lib/logitech_receiver/settings_templates.py index 0104d4b1..f18aae7c 100644 --- a/lib/logitech_receiver/settings_templates.py +++ b/lib/logitech_receiver/settings_templates.py @@ -1749,18 +1749,56 @@ class HeadsetMixBalance(settings.Setting): validator_options = {"byte_count": 1} +class _AutoSleepRangeValidator(settings_validator.RangeValidator): + """Single-slot read-modify-write validator for HID++ 0x0108 AutoSleep. + + 0x0108 is not a single timer: V3 has two uint8 bytes, V4+ has three. Each + byte is an independent timer slot. Solaar exposes only the user-facing slot + today and preserves the others via RMW; writing zero into the other slots + causes the firmware to reject the request. + + Wire byte layout per feature version: + V<3: [timer] + V3: [reserved, timer] — preserve byte[0] + V4+: [timer_a, timer_b, timer_c] — preserve byte[1], byte[2] + """ + + def __init__(self, byte_count, **kwargs): + super().__init__(byte_count=byte_count, **kwargs) + # V3 sources the user-controllable timer from byte[1] per LGHUB. + self._slot = 1 if byte_count == 2 else 0 + + def validate_read(self, reply_bytes): + if len(reply_bytes) <= self._slot: + raise AssertionError( + f"{self.__class__.__name__}: read returned {len(reply_bytes)} bytes, expected ≥ {self._slot + 1}" + ) + return reply_bytes[self._slot] + + def prepare_write(self, new_value, current_value=None): + if new_value < self.min_value or new_value > self.max_value: + raise ValueError(f"invalid choice {new_value!r}") + if current_value is None: + payload = bytearray(self._byte_count) + else: + payload = bytearray(current_value[: self._byte_count]) + if len(payload) < self._byte_count: + payload.extend(b"\x00" * (self._byte_count - len(payload))) + if payload[self._slot] == new_value: + return None + payload[self._slot] = new_value + return bytes(payload) + + class HeadsetAutoSleep(settings.Setting): name = "headset-auto-sleep" label = _("Auto Sleep Timeout") - description = _("Idle time in minutes before the headset enters sleep mode (0 = disabled).") + description = _("Headset idle-timer slot (0 = disabled). Units are device-specific.") feature = _F.CENTURION_AUTO_SLEEP rw_options = {"read_fnid": 0x00, "write_fnid": 0x10} - validator_class = settings_validator.RangeValidator + validator_class = _AutoSleepRangeValidator min_value = 0 - # Wire format byte count depends on feature version (V<3: 1B, V=3: 2B, V>=4: 3B). - # UI slider is capped at 240 min regardless — firmware accepts larger values, but - # a 31-year slider (24-bit max) is not useful. - max_value = 240 + max_value = 255 # uint8 slot validator_options = {"byte_count": 1} @classmethod @@ -1773,7 +1811,7 @@ class HeadsetAutoSleep(settings.Setting): else: byte_count = 1 rw = settings.FeatureRW(cls.feature, **cls.rw_options) - validator = settings_validator.RangeValidator(min_value=0, max_value=cls.max_value, byte_count=byte_count) + validator = _AutoSleepRangeValidator(min_value=0, max_value=cls.max_value, byte_count=byte_count) return cls(device, rw, validator)