device: clean up data for LED effects

This commit is contained in:
Peter F. Patel-Schneider 2024-02-08 18:21:41 -05:00
parent 7c441cc652
commit 23517048d4
5 changed files with 56 additions and 96 deletions

View File

@ -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:

View File

@ -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):

View File

@ -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?

View File

@ -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])

View File

@ -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):