HeadsetAutoSleep: read-modify-write a single uint8 timer slot
HID++ 0x0108 is not a single integer: V3 carries two uint8 timer slots, V4+ carries three. The old code decoded the response as one big-endian integer and wrote the user's value into byte[0] with the other slots zeroed — which the firmware rejects, so writes silently never round-tripped. _AutoSleepRangeValidator now decodes only the user-facing slot (byte[0] on V<3 and V4+, byte[1] on V3 per LGHUB) and builds the wire payload by reading the current bytes and mutating only that slot. Range becomes the real uint8 0–255 (not the arbitrary 240), and the description drops the "minutes" claim since per-slot units are device-specific. See ~/ghub/solaar_0x0108_autosleep_impl_guide.md for the RE notes.
This commit is contained in:
parent
5357ef955d
commit
e06c4ce93a
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue