Skip to content

Commit

Permalink
device: add settings for LED Zone control
Browse files Browse the repository at this point in the history
  • Loading branch information
pfps committed Feb 7, 2024
1 parent 1752906 commit 2214065
Show file tree
Hide file tree
Showing 3 changed files with 127 additions and 7 deletions.
48 changes: 45 additions & 3 deletions lib/logitech_receiver/hidpp20.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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):
Expand Down
47 changes: 44 additions & 3 deletions lib/logitech_receiver/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
)


Expand Down Expand Up @@ -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'):
Expand Down Expand Up @@ -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"""
Expand Down
39 changes: 38 additions & 1 deletion lib/logitech_receiver/settings_templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -1459,6 +1490,7 @@ class LEDControl(_Setting):
Backlight2DurationPowered,
Backlight3,
LEDControl,
LEDZoneSetting,
FnSwap, # simple
NewFnSwap, # simple
K375sFnSwap, # working
Expand Down Expand Up @@ -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)
Expand Down

0 comments on commit 2214065

Please sign in to comment.