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:
# ? 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:
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:
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:
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',
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',
registers=(_R.battery_status, ),

View File

@ -115,7 +115,7 @@ FEATURE = _NamedInts(
LOCK_KEY_STATE=0x4220,
SOLAR_DASHBOARD=0x4301,
KEYBOARD_LAYOUT=0x4520,
KEYBOARD_DISABLE=0x4521,
KEYBOARD_DISABLE_KEYS=0x4521,
KEYBOARD_DISABLE_BY_USAGE=0x4522,
DUALPLATFORM=0x4530,
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):
"""A setting descriptor.
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')
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
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
#
@ -469,6 +583,38 @@ class BooleanValidator(object):
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):
__slots__ = ('choices', 'flag', '_bytes_count', 'needs_current_value')
@ -516,7 +662,7 @@ class ChoicesValidator(object):
assert isinstance(choice, _NamedInt)
return choice.bytes(self._bytes_count)
class ChoicesMapValidator(ChoicesValidator):
class ChoicesMapValidator(ChoicesValidator):
kind = KIND.map_choice
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 (
KIND as _KIND,
Setting as _Setting,
BitFieldSetting as _BitFieldSetting,
Settings as _Settings,
RegisterRW as _RegisterRW,
FeatureRW as _FeatureRW,
FeatureRWMap as _FeatureRWMap,
BooleanValidator as _BooleanV,
BitFieldValidator as _BitFieldV,
ChoicesValidator as _ChoicesV,
ChoicesMapValidator as _ChoicesMapV,
RangeValidator as _RangeV,
@ -86,6 +88,30 @@ def feature_toggle(name, feature,
rw = _FeatureRW(feature, read_function_id, write_function_id)
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,
read_function_id, write_function_id,
bytes_count=None,
@ -108,6 +134,7 @@ def feature_choices_dynamic(name, feature, choices_callback,
bytes_count=bytes_count,
label=label, description=description, device_kind=device_kind)
return setting(device)
instantiate._rw_kind = _FeatureRW.kind
return instantiate
# 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,
label=label, description=description, device_kind=device_kind, extra_default=extra_default)
return setting(device)
instantiate._rw_kind = _FeatureRWMap.kind
return instantiate
def feature_range(name, feature, min_value, max_value,
@ -175,6 +203,7 @@ _FN_SWAP = ('fn-swap', _("Swap Fx function"),
+ '\n\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."))
_DISABLE_KEYS = ('disable-keyboard-keys', _("Disable keys"), _("Disable specific keyboard keys."))
_HAND_DETECTION = ('hand-detection', _("Hand Detection"),
_("Turn on illumination when the hands hover over the keyboard."))
_BACKLIGHT = ('backlight', _("Backlight"),
@ -246,6 +275,7 @@ def _feature_lowres_smooth_scroll():
return feature_toggle(_LOW_RES_SCROLL[0], _F.LOWRES_WHEEL,
label=_LOW_RES_SCROLL[1], description=_LOW_RES_SCROLL[2],
device_kind=(_DK.mouse, _DK.trackball))
def _feature_hires_smooth_invert():
return feature_toggle(_HIRES_INV[0], _F.HIRES_WHEEL,
read_function_id=0x10,
@ -379,6 +409,18 @@ def _feature_reprogrammable_keys():
label=_REPROGRAMMABLE_KEYS[1], description=_REPROGRAMMABLE_KEYS[2],
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',
'smart_shift',
'reprogrammable_keys',
'disable_keyboard_keys',
])
del namedtuple
@ -421,6 +464,7 @@ RegisterSettings = _SETTINGS_LIST(
typing_illumination=None,
smart_shift=None,
reprogrammable_keys=None,
disable_keyboard_keys=None,
)
FeatureSettings = _SETTINGS_LIST(
fn_swap=_feature_fn_swap,
@ -439,6 +483,7 @@ FeatureSettings = _SETTINGS_LIST(
typing_illumination=None,
smart_shift=_feature_smart_shift,
reprogrammable_keys=_feature_reprogrammable_keys,
disable_keyboard_keys=_feature_disable_keyboard_keys,
)
del _SETTINGS_LIST
@ -479,6 +524,7 @@ def check_feature_settings(device, already_known):
if detected:
already_known.append(detected)
except Exception as reason:
raise reason
_log.error("check_feature[%s] inconsistent feature %s", featureId, reason)
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(_BACKLIGHT[0], _F.BACKLIGHT2)
check_feature(_REPROGRAMMABLE_KEYS[0], _F.REPROG_CONTROLS_V4)
check_feature(_DISABLE_KEYS[0], _F.KEYBOARD_DISABLE_KEYS, 'disable_keyboard_keys')
return True

View File

@ -377,3 +377,12 @@ KEY_FLAG = _NamedInts(
is_FN=0x02,
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:
control = _create_map_choice_control(s)
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:
raise Exception("NotImplemented")