From 23517048d4aa3598881e91b0a8f19f34b95748c8 Mon Sep 17 00:00:00 2001 From: "Peter F. Patel-Schneider" Date: Thu, 8 Feb 2024 18:21:41 -0500 Subject: [PATCH] device: clean up data for LED effects --- lib/logitech_receiver/device.py | 36 +++++-- lib/logitech_receiver/hidpp20.py | 102 +++++--------------- lib/logitech_receiver/settings.py | 3 +- lib/logitech_receiver/settings_templates.py | 7 +- lib/solaar/ui/config_panel.py | 4 +- 5 files changed, 56 insertions(+), 96 deletions(-) diff --git a/lib/logitech_receiver/device.py b/lib/logitech_receiver/device.py index d3b8977d..177e2140 100644 --- a/lib/logitech_receiver/device.py +++ b/lib/logitech_receiver/device.py @@ -1,3 +1,21 @@ +# -*- python-mode -*- + +## Copyright (C) 2012-2013 Daniel Pavel +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation; either version 2 of the License, or +## (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License along +## with this program; if not, write to the Free Software Foundation, Inc., +## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + import errno as _errno import threading as _threading @@ -60,10 +78,9 @@ class Device: if receiver: assert number > 0 and number <= 15 # some receivers have devices past their max # of devices self.number = number # will be None at this point for directly connected devices - self.online = None + self.online = self.descriptor = None self.wpid = None # the Wireless PID is unique per device model - self.descriptor = None self._kind = None # mouse, keyboard, etc (see _hidpp10.DEVICE_KIND) self._codename = None # Unifying peripherals report a codename. self._name = None # the full name of the model @@ -74,16 +91,13 @@ class Device: self._tid_map = None # map from transports to product identifiers self._persister = None # persister holds settings - self._firmware = None - self._keys = None - self._remap_keys = None - self._gestures = None + self._firmware = self._keys = self._remap_keys = self._gestures = None + self._polling_rate = self._power_switch = self._led_effects = None + self._gestures_lock = _threading.Lock() self._profiles = self._backlight = self._registers = self._settings = None self._feature_settings_checked = False self._settings_lock = _threading.Lock() - self._polling_rate = None - self._power_switch = None # See `add_notification_handler` self._notification_handlers = {} @@ -291,6 +305,12 @@ class Device: self._polling_rate = rate if rate else self._polling_rate return self._polling_rate + @property + def led_effects(self): + if not self._led_effects and self.online and self.protocol >= 2.0: + self._led_effects = _hidpp20.LEDEffectsInfo(self) + return self._led_effects + @property def keys(self): if not self._keys: diff --git a/lib/logitech_receiver/hidpp20.py b/lib/logitech_receiver/hidpp20.py index d4808a32..d4c4555a 100644 --- a/lib/logitech_receiver/hidpp20.py +++ b/lib/logitech_receiver/hidpp20.py @@ -1163,42 +1163,19 @@ class LEDParam: LEDRampChoices = _NamedInts(default=0, yes=1, no=2) LEDFormChoices = _NamedInts(default=0, sine=1, square=2, triangle=3, sawtooth=4, sharkfin=5, exponential=6) -LEDParamSize = { - LEDParam.color: 3, - LEDParam.speed: 1, - LEDParam.period: 2, - LEDParam.intensity: 1, - LEDParam.ramp: 1, - LEDParam.form: 1 -} -LEDEffects = { +LEDParamSize = {LEDParam.color: 3, LEDParam.speed: 1, LEDParam.period: 2, + LEDParam.intensity: 1, LEDParam.ramp: 1, LEDParam.form: 1} # yapf: disable +LEDEffects = { # Wave=0x04, Stars=0x05, Press=0x06, Audio=0x07, # not implemented 0x0: [_NamedInt(0x0, _('Disabled')), {}], - 0x1: [_NamedInt(0x1, _('Static')), { - LEDParam.color: 0, - LEDParam.ramp: 3 - }], - 0x2: [_NamedInt(0x2, _('Pulse')), { - LEDParam.color: 0, - LEDParam.speed: 3 - }], - 0x3: [_NamedInt(0x3, _('Cycle')), { - LEDParam.period: 5, - LEDParam.intensity: 7 - }], + 0x1: [_NamedInt(0x1, _('Static')), {LEDParam.color: 0, LEDParam.ramp: 3}], + 0x2: [_NamedInt(0x2, _('Pulse')), {LEDParam.color: 0, LEDParam.speed: 3}], + 0x3: [_NamedInt(0x3, _('Cycle')), {LEDParam.period: 5, LEDParam.intensity: 7}], 0x8: [_NamedInt(0x8, _('Boot')), {}], 0x9: [_NamedInt(0x9, _('Demo')), {}], - 0xA: [_NamedInt(0xA, _('Breathe')), { - LEDParam.color: 0, - LEDParam.period: 3, - LEDParam.form: 5, - LEDParam.intensity: 6 - }], - 0xB: [_NamedInt(0xB, _('Ripple')), { - LEDParam.color: 0, - LEDParam.period: 4 - }] -} -# Wave=0x04, Stars=0x05, Press=0x06, Audio=0x07, # not implemented + 0xA: [_NamedInt(0xA, _('Breathe')), {LEDParam.color: 0, LEDParam.period: 3, + LEDParam.form: 5, LEDParam.intensity: 6}], + 0xB: [_NamedInt(0xB, _('Ripple')), {LEDParam.color: 0, LEDParam.period: 4}] +} # yapf: disable class LEDEffectSetting: # an effect plus its parameters @@ -1209,8 +1186,9 @@ class LEDEffectSetting: # an effect plus its parameters setattr(self, key, val) @classmethod - def from_bytes(cls, bytes): - effect = LEDEffects[bytes[0]] if bytes[0] in LEDEffects else None + def from_bytes(cls, bytes, options=None): + ID = next((ze.ID for ze in options if ze.index == bytes[0]), None) if options is not None else bytes[0] + effect = LEDEffects[ID] if ID in LEDEffects else None args = {'ID': effect[0] if effect else None} if effect: for p, b in effect[1].items(): @@ -1219,20 +1197,22 @@ class LEDEffectSetting: # an effect plus its parameters args['bytes'] = bytes return cls(**args) - def to_bytes(self, ID=None): - ID = self.ID if ID is None else ID + def to_bytes(self, options=None): + ID = self.ID if ID is None: - return self.bytes if self.bytes else b'\xff' * 11 + return self.bytes if hasattr(self, 'bytes') else b'\xff' * 11 else: bs = [0] * 10 - for p, b in LEDEffects[self.ID][1].items(): + for p, b in LEDEffects[ID][1].items(): bs[b:b + LEDParamSize[p]] = _int2bytes(getattr(self, str(p), 0), LEDParamSize[p]) - return _int2bytes(ID, 1) + bytes(bs) + if options is not None: + ID = next((ze.index for ze in options if ze.ID == ID), None) + result = _int2bytes(ID, 1) + bytes(bs) + return result @classmethod def from_yaml(cls, loader, node): - args = loader.construct_mapping(node) - return cls(**args) + return cls(**loader.construct_mapping(node)) @classmethod def to_yaml(cls, dumper, data): @@ -1246,44 +1226,6 @@ _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 LEDEffects: - for p, b in LEDEffects[args['ID']][1].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 27fe79da..abe180c6 100644 --- a/lib/logitech_receiver/settings.py +++ b/lib/logitech_receiver/settings.py @@ -1229,8 +1229,7 @@ class HeteroValidator(Validator): return reply_value def prepare_write(self, new_value, current_value=None): - new_value.options = self.options - to_write = new_value.to_bytes() + to_write = new_value.to_bytes(options=self.options) return to_write def acceptable(self, args, current): # should this actually do some checking? diff --git a/lib/logitech_receiver/settings_templates.py b/lib/logitech_receiver/settings_templates.py index 51ee8551..81caadc1 100644 --- a/lib/logitech_receiver/settings_templates.py +++ b/lib/logitech_receiver/settings_templates.py @@ -1442,7 +1442,6 @@ _LEDP = _hidpp20.LEDParam # an LED Zone has an index, a set of possible LED effects, and an LED effect setting -# reading the current setting for a zone returns zeros on some devices class LEDZoneSetting(_Setting): name = 'led_zone_' label = _('LED Zone Effects') @@ -1458,12 +1457,12 @@ class LEDZoneSetting(_Setting): @classmethod def build(cls, device): - infos = _hidpp20.LEDEffectsInfo(device) + infos = device.led_effects settings = [] for zone in infos.zones: - prefix = zone.index.to_bytes(1) + prefix = _int2bytes(zone.index, 1) rw = _FeatureRW(_F.COLOR_LED_EFFECTS, read_fnid=0xE0, write_fnid=0x30, prefix=prefix) - validator = _HeteroV(data_class=_hidpp20.LEDEffectIndexed, options=zone.effects, readable=infos.readable) + validator = _HeteroV(data_class=_hidpp20.LEDEffectSetting, options=zone.effects, readable=infos.readable) setting = cls(device, rw, validator) setting.name = cls.name + str(int(zone.location)) setting.label = _('LEDs') + ' ' + str(_hidpp20.LEDZoneLocations[zone.location]) diff --git a/lib/solaar/ui/config_panel.py b/lib/solaar/ui/config_panel.py index 823dbd64..ac9e3999 100644 --- a/lib/solaar/ui/config_panel.py +++ b/lib/solaar/ui/config_panel.py @@ -24,7 +24,7 @@ from logging import getLogger from threading import Timer as _Timer from gi.repository import Gdk, GLib, Gtk -from logitech_receiver.hidpp20 import LEDEffectIndexed as _LEDEffectIndexed +from logitech_receiver.hidpp20 import LEDEffectSetting as _LEDEffectSetting from logitech_receiver.settings import KIND as _SETTING_KIND from logitech_receiver.settings import SENSITIVITY_IGNORE as _SENSITIVITY_IGNORE from solaar.i18n import _, ngettext @@ -574,7 +574,7 @@ class HeteroKeyControl(Gtk.HBox, Control): result = {} for k, (_lblbox, box) in self._items.items(): result[str(k)] = box.get_value() - result = _LEDEffectIndexed(**result) + result = _LEDEffectSetting(**result) return result def set_value(self, value):