device: add settings for LED Zone control

This commit is contained in:
Peter F. Patel-Schneider 2024-02-07 10:35:09 -05:00
parent 15e14c2d48
commit 3328a6085f
3 changed files with 127 additions and 7 deletions

View File

@ -1218,14 +1218,15 @@ class LEDEffectSetting: # an effect plus its parameters
args['bytes'] = bytes args['bytes'] = bytes
return cls(**args) return cls(**args)
def to_bytes(self): def to_bytes(self, ID=None):
if self.ID is None: ID = self.ID if ID is None else ID
if ID is None:
return self.bytes if self.bytes else b'\xff' * 11 return self.bytes if self.bytes else b'\xff' * 11
else: else:
bs = [0] * 10 bs = [0] * 10
for p, b in LEDEffectsParams[self.ID].items(): for p, b in LEDEffectsParams[self.ID].items():
bs[b:b + LEDParamSize[p]] = _int2bytes(getattr(self, str(p), 0), LEDParamSize[p]) 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 @classmethod
def from_yaml(cls, loader, node): def from_yaml(cls, loader, node):
@ -1236,11 +1237,52 @@ class LEDEffectSetting: # an effect plus its parameters
def to_yaml(cls, dumper, data): def to_yaml(cls, dumper, data):
return dumper.represent_mapping('!LEDEffectSetting', data.__dict__, flow_style=True) 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.SafeLoader.add_constructor('!LEDEffectSetting', LEDEffectSetting.from_yaml)
_yaml.add_representer(LEDEffectSetting, LEDEffectSetting.to_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 class LEDEffectInfo: # an effect that a zone can do
def __init__(self, device, zindex, eindex): def __init__(self, device, zindex, eindex):

View File

@ -40,7 +40,14 @@ del getLogger
SENSITIVITY_IGNORE = 'ignore' SENSITIVITY_IGNORE = 'ignore'
KIND = _NamedInts( 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 @@ class Setting:
if self.persist and value is not None: # If setting doesn't persist no need to write value just read if self.persist and value is not None: # If setting doesn't persist no need to write value just read
try: try:
self.write(value, save=False) self.write(value, save=False)
except Exception: except Exception as e:
if _log.isEnabledFor(_WARNING): 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): def __str__(self):
if hasattr(self, '_value'): if hasattr(self, '_value'):
@ -1200,6 +1209,38 @@ class RangeValidator(Validator):
return False 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): class PackedRangeValidator(Validator):
kind = KIND.packed_range kind = KIND.packed_range
"""Several range values, all the same size, all the same min and max""" """Several range values, all the same size, all the same min and max"""

View File

@ -40,6 +40,7 @@ from .settings import BitFieldWithOffsetAndMaskValidator as _BitFieldOMV
from .settings import ChoicesMapValidator as _ChoicesMapV from .settings import ChoicesMapValidator as _ChoicesMapV
from .settings import ChoicesValidator as _ChoicesV from .settings import ChoicesValidator as _ChoicesV
from .settings import FeatureRW as _FeatureRW from .settings import FeatureRW as _FeatureRW
from .settings import HeteroValidator as _HeteroV
from .settings import LongSettings as _LongSettings from .settings import LongSettings as _LongSettings
from .settings import MultipleRangeValidator as _MultipleRangeV from .settings import MultipleRangeValidator as _MultipleRangeV
from .settings import PackedRangeValidator as _PackedRangeV from .settings import PackedRangeValidator as _PackedRangeV
@ -1429,6 +1430,36 @@ class LEDControl(_Setting):
validator_options = {'choices': choices_universe} 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 = [ SETTINGS = [
RegisterHandDetection, # simple RegisterHandDetection, # simple
RegisterSmoothScroll, # simple RegisterSmoothScroll, # simple
@ -1459,6 +1490,7 @@ SETTINGS = [
Backlight2DurationPowered, Backlight2DurationPowered,
Backlight3, Backlight3,
LEDControl, LEDControl,
LEDZoneSetting,
FnSwap, # simple FnSwap, # simple
NewFnSwap, # simple NewFnSwap, # simple
K375sFnSwap, # working K375sFnSwap, # working
@ -1517,7 +1549,12 @@ def check_feature_settings(device, already_known):
known_present = device.persister and sclass.name in device.persister 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): 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) 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) already_known.append(setting)
if sclass.name in newAbsent: if sclass.name in newAbsent:
newAbsent.remove(sclass.name) newAbsent.remove(sclass.name)