Skip to content

Commit

Permalink
device: support backlight levels and duration
Browse files Browse the repository at this point in the history
  • Loading branch information
pfps committed Feb 2, 2024
1 parent 3394c99 commit fdb8d53
Show file tree
Hide file tree
Showing 4 changed files with 196 additions and 1 deletion.
8 changes: 8 additions & 0 deletions lib/logitech_receiver/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ def __init__(
self._remap_keys = None
self._gestures = None
self._gestures_lock = _threading.Lock()
self._backlight = None
self._registers = None
self._settings = None
self._feature_settings_checked = False
Expand Down Expand Up @@ -315,6 +316,13 @@ def gestures(self):
self._gestures = _hidpp20.get_gestures(self) or ()
return self._gestures

@property
def backlight(self):
if self._backlight is None:
if self.online and self.protocol >= 2.0:
self._backlight = _hidpp20.get_backlight(self)
return self._backlight

@property
def registers(self):
if not self._registers:
Expand Down
44 changes: 44 additions & 0 deletions lib/logitech_receiver/hidpp20.py
Original file line number Diff line number Diff line change
Expand Up @@ -1117,6 +1117,43 @@ def set_param(self, param, value):
return g.set(self.device, value) if g else None


class Backlight:
"""Information about the current settings of x1982 Backlight2 v3, but also works for previous versions"""

def __init__(self, device):
response = device.feature_request(FEATURE.BACKLIGHT2, 0x00)
if not response:
raise FeatureCallError(msg='No reply from device.')
self.device = device
self.enabled, self.options, supported, effects, self.level, self.dho, self.dhi, self.dpow = _unpack(
'<BBBHBHHH', response[:12]
)
self.auto_supported = supported & 0x08
self.temp_supported = supported & 0x10
self.perm_supported = supported & 0x20
self.mode = (self.options >> 3) & 0x03
if _log.isEnabledFor(_DEBUG):
_log.debug(
'READ BACKLIGHT %x %x %x %x %x %x %x', self.mode, self.enabled, self.options, self.level, self.dho, self.dhi,
self.dpow
)

def write(self):
self.options = (self.options & 0x07) | (self.mode << 3)
if _log.isEnabledFor(_DEBUG):
_log.debug(
'WRITE BACKLIGHT %x %x %x %x %x %x %x', self.mode, self.enabled, self.options, self.level, self.dho, self.dhi,
self.dpow
)
data_bytes = _pack('<BBBBHHH', self.enabled, self.options, 0xFF, self.level, self.dho, self.dhi, self.dpow)
try: # for testing - remove later
self.device.feature_request(FEATURE.BACKLIGHT2, 0x10, data_bytes)
except Exception as e:
self.device.feature_request(FEATURE.BACKLIGHT2, 0x00)
raise e
self.device.feature_request(FEATURE.BACKLIGHT2, 0x00)


#
#
#
Expand Down Expand Up @@ -1402,6 +1439,13 @@ def get_gestures(device):
return Gestures(device)


def get_backlight(device):
if getattr(device, '_backlight', None) is not None:
return device._backlight
if FEATURE.BACKLIGHT2 in device.features:
return Backlight(device)


def get_mouse_pointer_info(device):
pointer_info = feature_request(device, FEATURE.MOUSE_POINTER)
if pointer_info:
Expand Down
1 change: 1 addition & 0 deletions lib/logitech_receiver/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ class Setting:
"""A setting descriptor. Needs to be instantiated for each specific device."""
name = label = description = ''
feature = register = kind = None
min_version = 0
persist = True
rw_options = {}
validator_class = BooleanValidator
Expand Down
144 changes: 143 additions & 1 deletion lib/logitech_receiver/settings_templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ class Backlight(_Setting):

# MX Keys S requires some extra values, as in 11 02 0c1a 000dff000b000b003c00000000000000
# on/off options (from current) effect (FF-no change) level (from current) durations[6] (from current)
class Backlight2(_Setting):
class Backlight2X(_Setting):
name = 'backlight'
label = _('Backlight')
description = _('Turn illumination on or off on keyboard.')
Expand All @@ -231,6 +231,142 @@ def write(self, device, data_bytes):
return super().write(device, data_bytes + self.trail)


# MX Keys S requires some extra values, as in 11 02 0c1a 000dff000b000b003c00000000000000
# on/off options (from current) effect (FF-no change) level (from current) durations[6] (from current)
class Backlight2(_Setting):
name = 'backlight'
label = _('Backlight')
description = _('Illumination mode of keyboard.')
feature = _F.BACKLIGHT2
min_version = 0

class rw_class:

def __init__(self, feature):
self.feature = feature
self.kind = _FeatureRW.kind

def read(self, device):
backlight = device.backlight
if not backlight.enabled:
return 0xFF
else:
return backlight.mode

def write(self, device, data_bytes):
backlight = device.backlight
backlight.enabled = data_bytes[0] != 0xFF
if data_bytes[0] != 0xFF:
backlight.mode = data_bytes[0]
backlight.write()
return True

class validator_class(_ChoicesV):

@classmethod
def build(cls, setting_class, device):
backlight = device.backlight
choices = _NamedInts()
choices[0xFF] = _('Disabled')
if backlight.auto_supported:
choices[0x1] = _('Automatic')
if backlight.temp_supported:
choices[0x2] = _('Keyboard')
if backlight.perm_supported:
choices[0x3] = _('Solaar')
if not (backlight.auto_supported or backlight.temp_supported or backlight.perm_supported):
choices[0x0] = _('Enabled')
return cls(choices=choices, byte_count=1)


class Backlight2Level(_Setting):
name = 'backlight_level'
label = _('Backlight Level')
description = _('Illumination level on keyboard in Solaar mode.')
feature = _F.BACKLIGHT2
min_version = 3

class rw_class:

def __init__(self, feature):
self.feature = feature
self.kind = _FeatureRW.kind

def read(self, device):
backlight = device.backlight
return _int2bytes(backlight.level, 1)

def write(self, device, data_bytes):
if device.backlight.level != _bytes2int(data_bytes):
device.backlight.level = _bytes2int(data_bytes)
device.backlight.write()
return True

class validator_class(_RangeV):

@classmethod
def build(cls, setting_class, device):
reply = device.feature_request(_F.BACKLIGHT2, 0x20)
assert reply, 'Oops, backlight range cannot be retrieved!'
if reply[0] > 1:
return cls(min_value=0, max_value=reply[0] - 1, byte_count=1)


class Backlight2Duration(_Setting):
feature = _F.BACKLIGHT2
min_version = 3
validator_class = _RangeV
min_value = 1
max_value = 120 # actual maximum is 2 hours
validator_options = {'byte_count': 2}

class rw_class:

def __init__(self, feature, field):
self.feature = feature
self.kind = _FeatureRW.kind
self.field = field

def read(self, device):
backlight = device.backlight
return _int2bytes(getattr(backlight, self.field), 2) * 5 # use seconds instead of 5-second units

def write(self, device, data_bytes):
backlight = device.backlight
new_duration = (int.from_bytes(data_bytes) + 4) // 5 # use ceiling in 5-second units
if new_duration != getattr(backlight, self.field):
setattr(backlight, self.field, new_duration)
backlight.write()
return True


class Backlight2DurationHandsOut(Backlight2Duration):
name = 'backlight_duration_hands_out'
label = _('Backlight Delay Hands Out')
description = _('Delay in seconds until backlight fades out with hands away from keyboard.')
feature = _F.BACKLIGHT2
validator_class = _RangeV
rw_options = {'field': 'dho'}


class Backlight2DurationHandsIn(Backlight2Duration):
name = 'backlight_duration_hands_in'
label = _('Backlight Delay Hands In')
description = _('Delay in seconds until backlight fades out with hands near keyboard.')
feature = _F.BACKLIGHT2
validator_class = _RangeV
rw_options = {'field': 'dhi'}


class Backlight2DurationPowered(Backlight2Duration):
name = 'backlight_duration_powered'
label = _('Backlight Delay Powered')
description = _('Delay in seconds until backlight fades out with external power.')
feature = _F.BACKLIGHT2
validator_class = _RangeV
rw_options = {'field': 'dpow'}


class Backlight3(_Setting):
name = 'backlight-timed'
label = _('Backlight')
Expand Down Expand Up @@ -1290,6 +1426,10 @@ class ADCPower(_Setting):
SpeedChange,
# Backlight, # not working - disabled temporarily
Backlight2, # working
Backlight2Level,
Backlight2DurationHandsOut,
Backlight2DurationHandsIn,
Backlight2DurationPowered,
Backlight3,
FnSwap, # simple
NewFnSwap, # simple
Expand Down Expand Up @@ -1322,6 +1462,8 @@ class ADCPower(_Setting):
def check_feature(device, sclass):
if sclass.feature not in device.features:
return
if sclass.min_version > device.features.get_feature_version(sclass.feature):
return
try:
detected = sclass.build(device)
if _log.isEnabledFor(_DEBUG):
Expand Down

0 comments on commit fdb8d53

Please sign in to comment.