receiver: implement KEYBOARD_DISABLE_KEYS feature

(the UI needs some improvement)
This commit is contained in:
Vinícius 2020-06-18 00:27:15 -03:00 committed by Peter F. Patel-Schneider
parent c99f470dd5
commit ef54a750dc
6 changed files with 227 additions and 8 deletions

View File

@ -54,11 +54,12 @@ def _D(name, codename=None, kind=None, wpid=None, protocol=None, registers=None,
if protocol is not None: if protocol is not None:
# ? 2.0 devices should not have any registers # ? 2.0 devices should not have any registers
_kind = lambda s : s._rw.kind if hasattr(s, '_rw') else s._rw_kind
if protocol < 2.0: if protocol < 2.0:
assert settings is None or all(s._rw.kind == 1 for s in settings) assert settings is None or all(_kind(s) == 1 for s in settings)
else: else:
assert registers is None assert registers is None
assert settings is None or all(s._rw.kind == 2 for s in settings) assert settings is None or all(_kind(s) == 2 for s in settings)
if wpid: if wpid:
for w in wpid if isinstance(wpid, tuple) else (wpid, ): for w in wpid if isinstance(wpid, tuple) else (wpid, ):
@ -183,8 +184,10 @@ _D('Wireless Touch Keyboard K400', protocol=2.0, wpid=('400E', '4024'),
) )
_D('Wireless Touch Keyboard K400 Plus', codename='K400 Plus', protocol=2.0, wpid='404D', _D('Wireless Touch Keyboard K400 Plus', codename='K400 Plus', protocol=2.0, wpid='404D',
settings=[ settings=[
_FS.new_fn_swap() _FS.new_fn_swap(),
], _FS.reprogrammable_keys(),
_FS.disable_keyboard_keys(),
],
) )
_D('Wireless Keyboard K520', protocol=1.0, wpid='2011', _D('Wireless Keyboard K520', protocol=1.0, wpid='2011',
registers=(_R.battery_status, ), registers=(_R.battery_status, ),

View File

@ -115,7 +115,7 @@ FEATURE = _NamedInts(
LOCK_KEY_STATE=0x4220, LOCK_KEY_STATE=0x4220,
SOLAR_DASHBOARD=0x4301, SOLAR_DASHBOARD=0x4301,
KEYBOARD_LAYOUT=0x4520, KEYBOARD_LAYOUT=0x4520,
KEYBOARD_DISABLE=0x4521, KEYBOARD_DISABLE_KEYS=0x4521,
KEYBOARD_DISABLE_BY_USAGE=0x4522, KEYBOARD_DISABLE_BY_USAGE=0x4522,
DUALPLATFORM=0x4530, DUALPLATFORM=0x4530,
MULTIPLATFORM=0x4531, MULTIPLATFORM=0x4531,

View File

@ -37,12 +37,12 @@ from .common import (
# #
# #
KIND = _NamedInts(toggle=0x01, choice=0x02, range=0x04, map_choice=0x0A) KIND = _NamedInts(toggle=0x01, choice=0x02, range=0x04, map_choice=0x0A, multiple_toggle=0x10)
class Setting(object): class Setting(object):
"""A setting descriptor. """A setting descriptor.
Needs to be instantiated for each specific device.""" Needs to be instantiated for each specific device."""
__slots__ = ('name', 'label', 'description', 'kind', 'device_kind', 'feature', __slots__ = ('name', 'label', 'description', 'kind', 'device_kind', 'feature',
'_rw', '_validator', '_device', '_value') '_rw', '_validator', '_device', '_value')
def __init__(self, name, rw, validator, kind=None, label=None, description=None, device_kind=None, feature=None): def __init__(self, name, rw, validator, kind=None, label=None, description=None, device_kind=None, feature=None):
@ -290,6 +290,120 @@ class Settings(Setting):
return value return value
class BitFieldSetting(Setting):
"""A setting descriptor for a set of choices represented by one bit each, being a map from options to booleans.
Needs to be instantiated for each specific device."""
def read(self, cached=True):
assert hasattr(self, '_value')
assert hasattr(self, '_device')
if _log.isEnabledFor(_DEBUG):
_log.debug("%s: settings read %r from %s", self.name, self._value, self._device)
if self._value is None and getattr(self._device,'persister',None):
# We haven't read a value from the device yet,
# maybe we have something in the configuration.
self._value = self._device.persister.get(self.name)
if cached and self._value is not None:
if getattr(self._device,'persister',None) and self.name not in self._device.persister:
# If this is a new device (or a new setting for an old device),
# make sure to save its current value for the next time.
self._device.persister[self.name] = self._value
return self._value
if self._device.online:
reply_map = dict()
reply = self._rw.read(self._device)
if reply:
# keys are ints, because that is what the device uses,
# encoded into strings because JSON requires strings as keys
reply_map = self._validator.validate_read(reply)
self._value = reply_map
if getattr(self._device,'persister',None) and self.name not in self._device.persister:
# Don't update the persister if it already has a value,
# otherwise the first read might overwrite the value we wanted.
self._device.persister[self.name] = self._value
return self._value
def read_key(self, key, cached=True):
assert hasattr(self, '_value')
assert hasattr(self, '_device')
assert key is not None
if _log.isEnabledFor(_DEBUG):
_log.debug("%s: settings read %r key %r from %s", self.name, self._value, key, self._device)
if self._value is None and getattr(self._device,'persister',None):
self._value = self._device.persister.get(self.name)
if cached and self._value is not None:
if getattr(self._device,'persister',None) and self.name not in self._device.persister:
self._device.persister[self.name] = self._value
return self._value[str(int(key))]
if self._device.online:
reply = self._rw.read(self._device, key)
if reply:
self._value = self._validator.validate_read(reply)
if getattr(self._device,'persister',None) and self.name not in self._device.persister:
self._device.persister[self.name] = self._value
return self._value[str(int(key))]
def write(self, map):
assert hasattr(self, '_value')
assert hasattr(self, '_device')
assert map is not None
if _log.isEnabledFor(_DEBUG):
_log.debug("%s: settings write %r to %s", self.name, map, self._device)
if self._device.online:
# Remember the value we're trying to set, even if the write fails.
# This way even if the device is offline or some other error occurs,
# the last value we've tried to write is remembered in the configuration.
self._value = map
if self._device.persister:
self._device.persister[self.name] = map
data_bytes = self._validator.prepare_write(self._value)
if data_bytes is not None:
if _log.isEnabledFor(_DEBUG):
_log.debug("%s: settings prepare map write(%s) => %r", self.name, self._value, data_bytes)
reply = self._rw.write(self._device, data_bytes)
if not reply:
return None
return map
def write_key_value(self, key, value):
assert hasattr(self, '_value')
assert hasattr(self, '_device')
assert key is not None
assert value is not None
if _log.isEnabledFor(_DEBUG):
_log.debug("%s: settings write key %r value %r to %s", self.name, key, value, self._device)
if self._device.online:
# Remember the value we're trying to set, even if the write fails.
# This way even if the device is offline or some other error occurs,
# the last value we've tried to write is remembered in the configuration.
value = bool(value)
self._value[str(key)] = value
if self._device.persister:
self._device.persister[self.name] = self._value
data_bytes = self._validator.prepare_write(self._value)
if data_bytes is not None:
if _log.isEnabledFor(_DEBUG):
_log.debug("%s: settings prepare key value write(%s,%s) => %r", self.name, key, str(value), data_bytes)
reply = self._rw.write(self._device, data_bytes)
if not reply:
# tell whomever is calling that the write failed
return None
return value
# #
# read/write low-level operators # read/write low-level operators
# #
@ -469,6 +583,38 @@ class BooleanValidator(object):
return to_write return to_write
class BitFieldValidator(object):
__slots__ = ('byte_count', 'options')
kind = KIND.multiple_toggle
def __init__(self, options, byte_count=None):
assert(isinstance(options, list))
self.options = options
self.byte_count = (max(x.bit_length() for x in options) + 7) // 8
if byte_count:
assert(isinstance(byte_count, int) and byte_count >= self.byte_count)
self.byte_count = byte_count
def validate_read(self, reply_bytes):
r = _bytes2int(reply_bytes[:self.byte_count])
value = {str(int(k)) : False for k in self.options}
m = 1
for i in range(8 * self.byte_count):
if m in self.options:
value[str(int(m))] = bool(r & m)
m <<= 1
return value
def prepare_write(self, new_value):
assert(isinstance(new_value, dict))
w = 0
for k, v in new_value.items():
if v:
w |= int(k)
return _int2bytes(w, self.byte_count)
class ChoicesValidator(object): class ChoicesValidator(object):
__slots__ = ('choices', 'flag', '_bytes_count', 'needs_current_value') __slots__ = ('choices', 'flag', '_bytes_count', 'needs_current_value')
@ -516,7 +662,7 @@ class ChoicesValidator(object):
assert isinstance(choice, _NamedInt) assert isinstance(choice, _NamedInt)
return choice.bytes(self._bytes_count) return choice.bytes(self._bytes_count)
class ChoicesMapValidator(ChoicesValidator): class ChoicesMapValidator(ChoicesValidator):
kind = KIND.map_choice kind = KIND.map_choice
def __init__(self, choices_map, key_bytes_count=None, skip_bytes_count=None, value_bytes_count=None, extra_default=None): def __init__(self, choices_map, key_bytes_count=None, skip_bytes_count=None, value_bytes_count=None, extra_default=None):

View File

@ -38,11 +38,13 @@ from .common import (
from .settings import ( from .settings import (
KIND as _KIND, KIND as _KIND,
Setting as _Setting, Setting as _Setting,
BitFieldSetting as _BitFieldSetting,
Settings as _Settings, Settings as _Settings,
RegisterRW as _RegisterRW, RegisterRW as _RegisterRW,
FeatureRW as _FeatureRW, FeatureRW as _FeatureRW,
FeatureRWMap as _FeatureRWMap, FeatureRWMap as _FeatureRWMap,
BooleanValidator as _BooleanV, BooleanValidator as _BooleanV,
BitFieldValidator as _BitFieldV,
ChoicesValidator as _ChoicesV, ChoicesValidator as _ChoicesV,
ChoicesMapValidator as _ChoicesMapV, ChoicesMapValidator as _ChoicesMapV,
RangeValidator as _RangeV, RangeValidator as _RangeV,
@ -86,6 +88,30 @@ def feature_toggle(name, feature,
rw = _FeatureRW(feature, read_function_id, write_function_id) rw = _FeatureRW(feature, read_function_id, write_function_id)
return _Setting(name, rw, validator, feature=feature, label=label, description=description, device_kind=device_kind) return _Setting(name, rw, validator, feature=feature, label=label, description=description, device_kind=device_kind)
def feature_bitfield_toggle(name, feature, options,
read_function_id=_FeatureRW.default_read_fnid,
write_function_id=_FeatureRW.default_write_fnid,
label=None, description=None, device_kind=None):
assert options
validator = _BitFieldV(options)
rw = _FeatureRW(feature, read_function_id, write_function_id)
return _BitFieldSetting(name, rw, validator, feature=feature, label=label, description=description, device_kind=device_kind)
def feature_bitfield_toggle_dynamic(name, feature, options_callback,
read_function_id=_FeatureRW.default_read_fnid,
write_function_id=_FeatureRW.default_write_fnid,
label=None, description=None, device_kind=None):
def instantiate(device):
options = options_callback(device)
setting = feature_bitfield_toggle(name, feature, options,
read_function_id=read_function_id,
write_function_id=write_function_id,
label=label,
description=description, device_kind=device_kind)
return setting(device)
instantiate._rw_kind = _FeatureRW.kind
return instantiate
def feature_choices(name, feature, choices, def feature_choices(name, feature, choices,
read_function_id, write_function_id, read_function_id, write_function_id,
bytes_count=None, bytes_count=None,
@ -108,6 +134,7 @@ def feature_choices_dynamic(name, feature, choices_callback,
bytes_count=bytes_count, bytes_count=bytes_count,
label=label, description=description, device_kind=device_kind) label=label, description=description, device_kind=device_kind)
return setting(device) return setting(device)
instantiate._rw_kind = _FeatureRW.kind
return instantiate return instantiate
# maintain a mapping from keys (NamedInts) to one of a list of choices (NamedInts), default is first one # maintain a mapping from keys (NamedInts) to one of a list of choices (NamedInts), default is first one
@ -136,6 +163,7 @@ def feature_map_choices_dynamic(name, feature, choices_callback,
key_bytes_count=key_bytes_count, skip_bytes_count=skip_bytes_count, value_bytes_count=value_bytes_count, key_bytes_count=key_bytes_count, skip_bytes_count=skip_bytes_count, value_bytes_count=value_bytes_count,
label=label, description=description, device_kind=device_kind, extra_default=extra_default) label=label, description=description, device_kind=device_kind, extra_default=extra_default)
return setting(device) return setting(device)
instantiate._rw_kind = _FeatureRWMap.kind
return instantiate return instantiate
def feature_range(name, feature, min_value, max_value, def feature_range(name, feature, min_value, max_value,
@ -175,6 +203,7 @@ _FN_SWAP = ('fn-swap', _("Swap Fx function"),
+ '\n\n' + + '\n\n' +
_("When unset, the F1..F12 keys will activate their standard function,\n" _("When unset, the F1..F12 keys will activate their standard function,\n"
"and you must hold the FN key to activate their special function.")) "and you must hold the FN key to activate their special function."))
_DISABLE_KEYS = ('disable-keyboard-keys', _("Disable keys"), _("Disable specific keyboard keys."))
_HAND_DETECTION = ('hand-detection', _("Hand Detection"), _HAND_DETECTION = ('hand-detection', _("Hand Detection"),
_("Turn on illumination when the hands hover over the keyboard.")) _("Turn on illumination when the hands hover over the keyboard."))
_BACKLIGHT = ('backlight', _("Backlight"), _BACKLIGHT = ('backlight', _("Backlight"),
@ -246,6 +275,7 @@ def _feature_lowres_smooth_scroll():
return feature_toggle(_LOW_RES_SCROLL[0], _F.LOWRES_WHEEL, return feature_toggle(_LOW_RES_SCROLL[0], _F.LOWRES_WHEEL,
label=_LOW_RES_SCROLL[1], description=_LOW_RES_SCROLL[2], label=_LOW_RES_SCROLL[1], description=_LOW_RES_SCROLL[2],
device_kind=(_DK.mouse, _DK.trackball)) device_kind=(_DK.mouse, _DK.trackball))
def _feature_hires_smooth_invert(): def _feature_hires_smooth_invert():
return feature_toggle(_HIRES_INV[0], _F.HIRES_WHEEL, return feature_toggle(_HIRES_INV[0], _F.HIRES_WHEEL,
read_function_id=0x10, read_function_id=0x10,
@ -379,6 +409,18 @@ def _feature_reprogrammable_keys():
label=_REPROGRAMMABLE_KEYS[1], description=_REPROGRAMMABLE_KEYS[2], label=_REPROGRAMMABLE_KEYS[1], description=_REPROGRAMMABLE_KEYS[2],
device_kind=(_DK.keyboard,), extra_default=0) device_kind=(_DK.keyboard,), extra_default=0)
def _feature_disable_keyboard_keys_key_list(device):
mask = device.feature_request(_F.KEYBOARD_DISABLE_KEYS)[0]
options = [_special_keys.DISABLE[1 << i] for i in range(8) if mask & (1 << i)]
return options
def _feature_disable_keyboard_keys():
return feature_bitfield_toggle_dynamic(_DISABLE_KEYS[0], _F.KEYBOARD_DISABLE_KEYS,
_feature_disable_keyboard_keys_key_list,
read_function_id=0x10, write_function_id=0x20,
label=_DISABLE_KEYS[1], description=_DISABLE_KEYS[2], device_kind=(_DK.keyboard,))
# #
# #
# #
@ -401,6 +443,7 @@ _SETTINGS_LIST = namedtuple('_SETTINGS_LIST', [
'typing_illumination', 'typing_illumination',
'smart_shift', 'smart_shift',
'reprogrammable_keys', 'reprogrammable_keys',
'disable_keyboard_keys',
]) ])
del namedtuple del namedtuple
@ -421,6 +464,7 @@ RegisterSettings = _SETTINGS_LIST(
typing_illumination=None, typing_illumination=None,
smart_shift=None, smart_shift=None,
reprogrammable_keys=None, reprogrammable_keys=None,
disable_keyboard_keys=None,
) )
FeatureSettings = _SETTINGS_LIST( FeatureSettings = _SETTINGS_LIST(
fn_swap=_feature_fn_swap, fn_swap=_feature_fn_swap,
@ -439,6 +483,7 @@ FeatureSettings = _SETTINGS_LIST(
typing_illumination=None, typing_illumination=None,
smart_shift=_feature_smart_shift, smart_shift=_feature_smart_shift,
reprogrammable_keys=_feature_reprogrammable_keys, reprogrammable_keys=_feature_reprogrammable_keys,
disable_keyboard_keys=_feature_disable_keyboard_keys,
) )
del _SETTINGS_LIST del _SETTINGS_LIST
@ -479,6 +524,7 @@ def check_feature_settings(device, already_known):
if detected: if detected:
already_known.append(detected) already_known.append(detected)
except Exception as reason: except Exception as reason:
raise reason
_log.error("check_feature[%s] inconsistent feature %s", featureId, reason) _log.error("check_feature[%s] inconsistent feature %s", featureId, reason)
check_feature(_HI_RES_SCROLL[0], _F.HI_RES_SCROLLING) check_feature(_HI_RES_SCROLL[0], _F.HI_RES_SCROLLING)
@ -493,4 +539,5 @@ def check_feature_settings(device, already_known):
check_feature(_SMART_SHIFT[0], _F.SMART_SHIFT) check_feature(_SMART_SHIFT[0], _F.SMART_SHIFT)
check_feature(_BACKLIGHT[0], _F.BACKLIGHT2) check_feature(_BACKLIGHT[0], _F.BACKLIGHT2)
check_feature(_REPROGRAMMABLE_KEYS[0], _F.REPROG_CONTROLS_V4) check_feature(_REPROGRAMMABLE_KEYS[0], _F.REPROG_CONTROLS_V4)
check_feature(_DISABLE_KEYS[0], _F.KEYBOARD_DISABLE_KEYS, 'disable_keyboard_keys')
return True return True

View File

@ -377,3 +377,12 @@ KEY_FLAG = _NamedInts(
is_FN=0x02, is_FN=0x02,
mse=0x01 mse=0x01
) )
DISABLE = _NamedInts(
Caps_Lock=0x01,
Num_Lock=0x02,
Scroll_Lock=0x04,
Insert=0x08,
Win=0x10, # aka Super
)
DISABLE._fallback = lambda x: 'unknown:%02X' % x

View File

@ -190,6 +190,20 @@ def _create_sbox(s):
elif s.kind == _SETTING_KIND.map_choice: elif s.kind == _SETTING_KIND.map_choice:
control = _create_map_choice_control(s) control = _create_map_choice_control(s)
sbox.pack_end(control, True, True, 0) sbox.pack_end(control, True, True, 0)
elif s.kind == _SETTING_KIND.multiple_toggle:
# ugly temporary hack!
choices = {k : [False, True] for k in s._validator.options}
class X:
def __init__(self, obj, ext):
self.obj = obj
self.ext = ext
def __getattr__(self, attr):
try:
return self.ext[attr]
except KeyError:
return getattr(self.obj, attr)
control = _create_map_choice_control(X(s, {'choices': choices}))
sbox.pack_end(control, True, True, 0)
else: else:
raise Exception("NotImplemented") raise Exception("NotImplemented")