From 2214065b67c07d39d4b9d3e568682918d61534f3 Mon Sep 17 00:00:00 2001 From: "Peter F. Patel-Schneider" Date: Wed, 7 Feb 2024 10:35:09 -0500 Subject: [PATCH] device: add settings for LED Zone control --- lib/logitech_receiver/hidpp20.py | 48 +++++++++++++++++++-- lib/logitech_receiver/settings.py | 47 ++++++++++++++++++-- lib/logitech_receiver/settings_templates.py | 39 ++++++++++++++++- 3 files changed, 127 insertions(+), 7 deletions(-) diff --git a/lib/logitech_receiver/hidpp20.py b/lib/logitech_receiver/hidpp20.py index d226d756fd..187832416a 100644 --- a/lib/logitech_receiver/hidpp20.py +++ b/lib/logitech_receiver/hidpp20.py @@ -1218,14 +1218,15 @@ def from_bytes(cls, bytes): args['bytes'] = bytes return cls(**args) - def to_bytes(self): - if self.ID is None: + def to_bytes(self, ID=None): + ID = self.ID if ID is None else ID + if ID is None: return self.bytes if self.bytes else b'\xff' * 11 else: bs = [0] * 10 for p, b in LEDEffectsParams[self.ID].items(): bs[b:b + LEDParamSize[p]] = _int2bytes(getattr(self, str(p), 0), LEDParamSize[p]) - return _int2bytes(self.ID, 1) + bytes(bs) + return _int2bytes(ID, 1) + bytes(bs) @classmethod def from_yaml(cls, loader, node): @@ -1236,11 +1237,52 @@ def from_yaml(cls, loader, node): def to_yaml(cls, dumper, data): return dumper.represent_mapping('!LEDEffectSetting', data.__dict__, flow_style=True) + def __str__(self): + return _yaml.dump(self).rstrip('\n') + _yaml.SafeLoader.add_constructor('!LEDEffectSetting', LEDEffectSetting.from_yaml) _yaml.add_representer(LEDEffectSetting, LEDEffectSetting.to_yaml) +class LEDEffectIndexed(LEDEffectSetting): # an effect plus its parameters, using the effect indices from an effect zone + + @classmethod + def from_bytes(cls, bytes, options=None): + if options: + args = {'ID': next((ze.ID for ze in options if ze.index == bytes[0]), None)} + else: + args = {'ID': None} + if args['ID'] in LEDEffectsParams: + for p, b in LEDEffectsParams[args['ID']].items(): + args[str(p)] = _bytes2int(bytes[1 + b:1 + b + LEDParamSize[p]]) + else: + args['bytes'] = bytes + args['options'] = options + return cls(**args) + + def to_bytes(self): # needs zone information + ID = next((ze.index for ze in self.options if ze.ID == self.ID), None) + if ID is None: + return self.bytes if hasattr(self, 'bytes') else b'\xff' * 11 + else: + return super().to_bytes(ID) + + @classmethod + def to_yaml(cls, dumper, data): + options = getattr(data, 'options', None) + if hasattr(data, 'options'): + delattr(data, 'options') + result = dumper.represent_mapping('!LEDEffectIndexed', data.__dict__, flow_style=True) + if options is not None: + data.options = options + return result + + +_yaml.SafeLoader.add_constructor('!LEDEffectIndexed', LEDEffectIndexed.from_yaml) +_yaml.add_representer(LEDEffectIndexed, LEDEffectIndexed.to_yaml) + + class LEDEffectInfo: # an effect that a zone can do def __init__(self, device, zindex, eindex): diff --git a/lib/logitech_receiver/settings.py b/lib/logitech_receiver/settings.py index aa81c144bb..5a2d20313c 100644 --- a/lib/logitech_receiver/settings.py +++ b/lib/logitech_receiver/settings.py @@ -40,7 +40,14 @@ SENSITIVITY_IGNORE = 'ignore' KIND = _NamedInts( - toggle=0x01, choice=0x02, range=0x04, map_choice=0x0A, multiple_toggle=0x10, packed_range=0x20, multiple_range=0x40 + toggle=0x01, + choice=0x02, + range=0x04, + map_choice=0x0A, + multiple_toggle=0x10, + packed_range=0x20, + multiple_range=0x40, + hetero=0x80 ) @@ -344,9 +351,11 @@ def apply(self): if self.persist and value is not None: # If setting doesn't persist no need to write value just read try: self.write(value, save=False) - except Exception: + except Exception as e: if _log.isEnabledFor(_WARNING): - _log.warn('%s: error applying value %s so ignore it (%s)', self.name, self._value, self._device) + _log.warn( + '%s: error applying value %s so ignore it (%s): %s', self.name, self._value, self._device, repr(e) + ) def __str__(self): if hasattr(self, '_value'): @@ -1200,6 +1209,38 @@ def compare(self, args, current): return False +class HeteroValidator(Validator): + kind = KIND.hetero + + @classmethod + def build(cls, setting_class, device, **kwargs): + return cls(**kwargs) + + def __init__(self, data_class=None, options=None): + assert data_class is not None and options is not None + self.data_class = data_class + self.options = options + self.needs_current_value = False + + def validate_read(self, reply_bytes): + reply_value = self.data_class.from_bytes(reply_bytes, options=self.options) + return reply_value + + def prepare_write(self, new_value, current_value=None): + new_value.options = self.options + to_write = new_value.to_bytes() + return to_write + + def acceptable(self, args, current): # FIXME + if len(args) != 2: + return None + item = self.items[args[0]] if args[0] in self.items else None + if item.kind == KIND.range: + return None if args[1] < item.min_value or args[1] > item.max_value else args + elif item.kind == KIND.choice: + return args if args[1] in item.choices else None + + class PackedRangeValidator(Validator): kind = KIND.packed_range """Several range values, all the same size, all the same min and max""" diff --git a/lib/logitech_receiver/settings_templates.py b/lib/logitech_receiver/settings_templates.py index 329fedba36..91d018c127 100644 --- a/lib/logitech_receiver/settings_templates.py +++ b/lib/logitech_receiver/settings_templates.py @@ -40,6 +40,7 @@ from .settings import ChoicesMapValidator as _ChoicesMapV from .settings import ChoicesValidator as _ChoicesV from .settings import FeatureRW as _FeatureRW +from .settings import HeteroValidator as _HeteroV from .settings import LongSettings as _LongSettings from .settings import MultipleRangeValidator as _MultipleRangeV from .settings import PackedRangeValidator as _PackedRangeV @@ -1429,6 +1430,36 @@ class LEDControl(_Setting): validator_options = {'choices': choices_universe} +# an LED Zone has an index, a set of possible LED effects, and an LED effect setting with parameters +# the parameters are different for each effect +# reading the current setting for a zone returns zeros on some devices +class LEDZoneSetting(_Setting): + name = 'led_zone_' + label = _('LED Zone Effects') + description = _('Set effect for LED Zone') + feature = _F.COLOR_LED_EFFECTS + + class validator_class(_HeteroV): + + @classmethod + def build(cls, setting_class, device, effects): + return cls(data_class=_hidpp20.LEDEffectIndexed, options=effects) + + @classmethod + def build(cls, device): + zone_infos = _hidpp20.LEDEffectsInfo(device).zones + settings = [] + for zone in zone_infos: + prefix = zone.index.to_bytes(1) + rw = _FeatureRW(_F.COLOR_LED_EFFECTS, read_fnid=0xE0, write_fnid=0x30, prefix=prefix) + validator = cls.validator_class.build(cls, device, zone.effects) + setting = cls(device, rw, validator) + setting.name = cls.name + str(int(zone.location)) + setting.label = _('LEDs: ') + str(_hidpp20.LEDZoneLocations[zone.location]) + settings.append(setting) + return settings + + SETTINGS = [ RegisterHandDetection, # simple RegisterSmoothScroll, # simple @@ -1459,6 +1490,7 @@ class LEDControl(_Setting): Backlight2DurationPowered, Backlight3, LEDControl, + LEDZoneSetting, FnSwap, # simple NewFnSwap, # simple K375sFnSwap, # working @@ -1517,7 +1549,12 @@ def check_feature_settings(device, already_known): 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): setting = check_feature(device, sclass) - if setting: + if isinstance(setting, list): + for s in setting: + already_known.append(s) + if sclass.name in newAbsent: + newAbsent.remove(sclass.name) + elif setting: already_known.append(setting) if sclass.name in newAbsent: newAbsent.remove(sclass.name)