Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

device: support backlight on MX Keys S #2230

Merged
merged 4 commits into from
Feb 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
31 changes: 31 additions & 0 deletions lib/logitech_receiver/hidpp20.py
Original file line number Diff line number Diff line change
Expand Up @@ -1117,6 +1117,30 @@ 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

def write(self):
self.options = (self.options & 0x07) | (self.mode << 3)
level = self.level if self.mode == 0x3 else 0
data_bytes = _pack('<BBBBHHH', self.enabled, self.options, 0xFF, level, self.dho, self.dhi, self.dpow)
self.device.feature_request(FEATURE.BACKLIGHT2, 0x00) # for testing - remove later
self.device.feature_request(FEATURE.BACKLIGHT2, 0x10, data_bytes)


#
#
#
Expand Down Expand Up @@ -1402,6 +1426,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
8 changes: 8 additions & 0 deletions lib/logitech_receiver/notifications.py
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,14 @@ def _process_feature_notification(device, status, n, feature):
else:
_log.warn('%s: unknown REPROG_CONTROLS %s', device, n)

elif feature == _F.BACKLIGHT2:
if (n.address == 0x00):
level = _unpack('!B', n.data[1:2])[0]
from solaar.ui.config_panel import record_setting # prevent circular import
setting = next((s for s in device.settings if s.name == _st.Backlight2Level.name), None)
if setting:
record_setting(device, setting, [level])

elif feature == _F.REPROG_CONTROLS_V4:
if n.address == 0x00:
if _log.isEnabledFor(_DEBUG):
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
135 changes: 134 additions & 1 deletion lib/logitech_receiver/settings_templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,11 +213,138 @@ class Backlight(_Setting):
validator_options = {'choices': choices_universe}


# 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 = _('Turn illumination on or off on keyboard.')
description = _('Illumination level on keyboard. Changes made are only applied in Manual mode.')
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.perm_supported:
choices[0x3] = _('Manual')
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 when in Manual 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):
Expand Down Expand Up @@ -1279,6 +1406,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 @@ -1311,6 +1442,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
3 changes: 3 additions & 0 deletions lib/solaar/ui/config_panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import traceback

from logging import DEBUG as _DEBUG
from logging import WARNING as _WARNING
from logging import getLogger
from threading import Timer as _Timer
Expand Down Expand Up @@ -724,6 +725,8 @@ def record_setting(device, setting, values):


def _record_setting(device, setting, values):
if _log.isEnabledFor(_DEBUG):
_log.debug('on %s changing setting %s to %s', device, setting, values)
if len(values) > 1:
setting.update_key_value(values[0], values[-1])
value = {values[0]: values[-1]}
Expand Down
Loading