receiver: simplify settings interface

This commit is contained in:
Peter F. Patel-Schneider 2020-08-16 05:38:59 -04:00
parent 1516daa6d0
commit 718c2d4039
3 changed files with 258 additions and 437 deletions

View File

@ -119,7 +119,7 @@ A “read only” note means the feature is a read-only feature and cannot be ch
Features are implemented as settable features in
lib/logitech_receiver/settings_templates.py
Some features also have direct implementation in
some features also have direct implementation in
lib/logitech_receiver/hidpp20.py
In most cases it should suffice to only implement the settable feature
@ -143,62 +143,72 @@ _POINTER_SPEED = ('pointer_speed',
_("How fast the pointer moves"))
```
Implement a register interface for the setting (if you are very brave and
some devices have a register interface for the setting).
Next implement an interface for the setting by creating
a reader/writer, a validator, and a setting instance for it.
Most settings use device features and thus need feature interfaces.
Some settings use device register and thus need register interfaces.
Only implement a register interface for the setting if you are very brave and
you have access to a device that has a register interface for the setting.
Register interfaces cannot be auto-discovered and need to be stated in descriptors.py
for each device with the register interface.
Implement a feature interface for the setting. There are several possible kinds of
feature interfaces, ranging from simple toggles, to ranges, to fixed lists, to
dynamic choices, to maps of dynamic choices, each created by a macro function.
Pointer speed is a setting
whose values are integers in a range so `feature_range` is used.
The arguments to this macro are
the name of the setting (use the name from the common strings tuple),
the HID++ 2.0 feature ID for the setting (from the FEATURE structure in hidpp20.py),
the minimum and maximum values for the setting,
the HID++ 2.0 function IDs to read and write the setting (left-shifted four bits),
the byte size of the setting value,
a label and description for the setting (from the common strings tuple),
and which kinds of devices can have this setting.
(This last is no longer used because keyboards with integrated pointers only
The reader/writer instance is responsible for reading raw values
from the device and writing values to it.
There are different classes for feature interfaces and register interfaces.
Pointer speed is a feature so the _FeatureRW reader/writer is used.
Reader/writers take the register or feature ID and the command numbers for reading and writing,
plus other arguments for complex interfaces.
The validator instance is responsible for turning read raw values into Python data
and Python data into raw values to be written and validating that the Python data is
acceptable for the setting.
There are several possible kinds of Python data for setting interfaces,
ranging from simple toggles, to ranges, to fixed lists, to
dynamic choices, to maps of dynamic choices.
Pointer speed is a setting whose values are integers in a range so a _RangeV validator is used.
The arguments to this class are the
the minimum and maximum values for the value
and the byte size of the value on the device.
Settings that are toggles or choices work similarly,
but their validators have different arguments.
Map settings have more complicated validators.
The setting instance keeps everything together and provides control.
It takes the strings for the setting, the reader/writer, the validator, and
which kinds of devices can have this setting.
(This last is no longer used because keyboards with integrated trackpads only
report that they are keyboards.)
The values to be used need to be determined from documentation of the
feature or from reverse-engineering behavior of Logitech software under
Windows or MacOS.
```python
def _feature_pointer_speed():
"""Pointer Speed feature"""
return feature_range(_POINTER_SPEED[0],
_F.POINTER_SPEED,
0x002e,
0x01ff,
read_function_id=0x0,
write_function_id=0x10,
bytes_count=2,
label=_POINTER_SPEED[1],
description=_POINTER_SPEED[2],
device_kind=(_DK.mouse, _DK.trackball))
"""Pointer Speed feature"""
# min and max values taken from usb traces of Win software
validator = _RangeV(0x002e, 0x01ff, 2)
rw = _FeatureRW(_F.POINTER_SPEED)
return _Setting(_POINTER_SPEED, rw, validator, device_kind=(_DK.mouse, _DK.trackball))
```
Settings that are toggles or choices work very similarly.
Settings where the choices are determined from the device
Settings where the acceptable values are determined from the device
need an auxiliary function to receive and decipher the permissible choices.
See `_feature_adjustable_dpi_choices` for an example.
Add an element to _SETTINGS_TABLE with
the setting name (from the common strings),
Finally, add an element to _SETTINGS_TABLE with
the common strings for the setting,
the feature ID (if any),
the feature implementation (if any),
the register implementation (if any).
and
the identifier for the setting implementation if different from the setting name.
The identifier is used in descriptors.py to say that a device has the register or feature implementation.
The identifier can be the same as the setting name if there is only one implementation for the setting.
This table is used to generate the data structures for describing devices in descriptors.py
and is also used to auto-discover feature implementations.
```python
_S( _POINTER_SPEED[0], _F.POINTER_SPEED, _feature_pointer_speed ),
_S( _POINTER_SPEED, _F.POINTER_SPEED, _feature_pointer_speed ),
```
The values to be used need to be determined from documentation of the
feature or from reverse-engineering behavior of Logitech software under
Windows or MacOS.
For more information on implementing feature settings
see the comments in lib/logitech_receiver/settings_templates.py.

View File

@ -44,23 +44,25 @@ class Setting(object):
"""A setting descriptor.
Needs to be instantiated for each specific device."""
__slots__ = (
'name', 'label', 'description', 'kind', 'device_kind', 'feature', 'persist', '_rw', '_validator', '_device', '_value'
'name', 'label', 'description', 'kind', 'device_kind', 'feature', 'persist', '_rw', '_validator', '_callback',
'_device', '_value'
)
def __init__(self, name, rw, validator, kind=None, device_kind=None, feature=None, persist=True, **kwargs):
def __init__(self, name, rw, validator=None, callback=None, kind=None, device_kind=None, feature=None, persist=True):
assert name
self.name = name[0]
self.label = name[1]
self.description = name[2]
self.device_kind = device_kind
self.feature = feature
self.feature = getattr(rw, 'feature', None)
self.persist = persist
self._rw = rw
assert (validator and not callback) or (not validator and callback)
self._validator = validator
self._callback = callback
assert kind is None or kind & validator.kind != 0
self.kind = kind or validator.kind
assert kind is None or validator is None or kind & validator.kind != 0
self.kind = kind or getattr(validator, 'kind', None)
def __call__(self, device):
assert not hasattr(self, '_value')
@ -73,8 +75,13 @@ class Setting(object):
elif p >= 2.0:
# HID++ 2.0 devices do not support registers
assert self._rw.kind == FeatureRW.kind
o = _copy(self)
if o._callback:
o._validator = o._callback(device)
if o._validator is None:
return None
assert o.kind is None or o.kind & o._validator.kind != 0
o.kind = o.kind or o._validator.kind
o._value = None
o._device = device
return o
@ -84,7 +91,7 @@ class Setting(object):
assert hasattr(self, '_value')
assert hasattr(self, '_device')
return self._validator.choices if self._validator.kind & KIND.choice else None
return self._validator.choices if self._validator and self._validator.kind & KIND.choice else None
@property
def range(self):
@ -171,9 +178,9 @@ class Setting(object):
if hasattr(self, '_value'):
assert hasattr(self, '_device')
return '<Setting([%s:%s] %s:%s=%s)>' % (
self._rw.kind, self._validator.kind, self._device.codename, self.name, self._value
self._rw.kind, self._validator.kind if self._validator else None, self._device.codename, self.name, self._value
)
return '<Setting([%s:%s] %s)>' % (self._rw.kind, self._validator.kind, self.name)
return '<Setting([%s:%s] %s)>' % (self._rw.kind, self._validator.kind if self._validator else None, self.name)
__unicode__ = __repr__ = __str__
@ -438,7 +445,7 @@ class FeatureRW(object):
default_read_fnid = 0x00
default_write_fnid = 0x10
def __init__(self, feature, read_fnid=default_read_fnid, write_fnid=default_write_fnid, no_reply=False, **kwargs):
def __init__(self, feature, read_fnid=default_read_fnid, write_fnid=default_write_fnid, no_reply=False):
assert isinstance(feature, _NamedInt)
self.feature = feature
self.read_fnid = read_fnid
@ -459,32 +466,31 @@ class FeatureRWMap(FeatureRW):
kind = _NamedInt(0x02, 'feature')
default_read_fnid = 0x00
default_write_fnid = 0x10
default_key_bytes_count = 1
default_key_byte_count = 1
def __init__(
self,
feature,
read_fnid=default_read_fnid,
write_fnid=default_write_fnid,
key_bytes_count=default_key_bytes_count,
no_reply=False,
**_ignore
key_byte_count=default_key_byte_count,
no_reply=False
):
assert isinstance(feature, _NamedInt)
self.feature = feature
self.read_fnid = read_fnid
self.write_fnid = write_fnid
self.key_bytes_count = key_bytes_count
self.key_byte_count = key_byte_count
self.no_reply = no_reply
def read(self, device, key):
assert self.feature is not None
key_bytes = _int2bytes(key, self.key_bytes_count)
key_bytes = _int2bytes(key, self.key_byte_count)
return device.feature_request(self.feature, self.read_fnid, key_bytes)
def write(self, device, key, data_bytes):
assert self.feature is not None
key_bytes = _int2bytes(key, self.key_bytes_count)
key_bytes = _int2bytes(key, self.key_byte_count)
reply = device.feature_request(self.feature, self.write_fnid, key_bytes, data_bytes, no_reply=self.no_reply)
return reply if not self.no_reply else True
@ -504,7 +510,7 @@ class BooleanValidator(object):
# mask specifies all the affected bits in the value
default_mask = 0xFF
def __init__(self, true_value=default_true, false_value=default_false, mask=default_mask, **kwargs):
def __init__(self, true_value=default_true, false_value=default_false, mask=default_mask):
if isinstance(true_value, int):
assert isinstance(false_value, int)
if mask is None:
@ -609,7 +615,7 @@ class BitFieldValidator(object):
kind = KIND.multiple_toggle
def __init__(self, options, byte_count=None, **kwargs):
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
@ -640,9 +646,9 @@ class ChoicesValidator(object):
kind = KIND.choice
"""Translates between NamedInts and a byte sequence.
:param choices: a list of NamedInts
:param bytes_count: the size of the derived byte sequence. If None, it
:param byte_count: the size of the derived byte sequence. If None, it
will be calculated from the choices."""
def __init__(self, choices, bytes_count=None, read_skip_bytes_count=None, write_prefix_bytes=b'', **_ignore):
def __init__(self, choices, byte_count=None, read_skip_byte_count=None, write_prefix_bytes=b''):
assert choices is not None
assert isinstance(choices, _NamedInts)
assert len(choices) > 1
@ -650,18 +656,18 @@ class ChoicesValidator(object):
self.needs_current_value = False
max_bits = max(x.bit_length() for x in choices)
self._bytes_count = (max_bits // 8) + (1 if max_bits % 8 else 0)
if bytes_count:
assert self._bytes_count <= bytes_count
self._bytes_count = bytes_count
assert self._bytes_count < 8
self._read_skip_bytes_count = read_skip_bytes_count if read_skip_bytes_count else 0
self._byte_count = (max_bits // 8) + (1 if max_bits % 8 else 0)
if byte_count:
assert self._byte_count <= byte_count
self._byte_count = byte_count
assert self._byte_count < 8
self._read_skip_byte_count = read_skip_byte_count if read_skip_byte_count else 0
self._write_prefix_bytes = write_prefix_bytes if write_prefix_bytes else b''
assert self._bytes_count + self._read_skip_bytes_count <= 14
assert self._bytes_count + len(self._write_prefix_bytes) <= 14
assert self._byte_count + self._read_skip_byte_count <= 14
assert self._byte_count + len(self._write_prefix_bytes) <= 14
def validate_read(self, reply_bytes):
reply_value = _bytes2int(reply_bytes[self._read_skip_bytes_count:self._read_skip_bytes_count + self._bytes_count])
reply_value = _bytes2int(reply_bytes[self._read_skip_byte_count:self._read_skip_byte_count + self._byte_count])
valid_value = self.choices[reply_value]
assert valid_value is not None, '%s: failed to validate read value %02X' % (self.__class__.__name__, reply_value)
return valid_value
@ -682,7 +688,7 @@ class ChoicesValidator(object):
if choice is None:
raise ValueError('invalid choice %r' % new_value)
assert isinstance(choice, _NamedInt)
return self._write_prefix_bytes + choice.bytes(self._bytes_count)
return self._write_prefix_bytes + choice.bytes(self._byte_count)
class ChoicesMapValidator(ChoicesValidator):
@ -691,12 +697,11 @@ class ChoicesMapValidator(ChoicesValidator):
def __init__(
self,
choices_map,
key_bytes_count=None,
bytes_count=None,
read_skip_bytes_count=0,
key_byte_count=None,
byte_count=None,
read_skip_byte_count=0,
write_prefix_bytes=b'',
extra_default=None,
**kwargs
extra_default=None
):
assert choices_map is not None
assert isinstance(choices_map, dict)
@ -709,25 +714,25 @@ class ChoicesMapValidator(ChoicesValidator):
for key_value in choices:
assert isinstance(key_value, _NamedInt)
max_value_bits = max(max_value_bits, key_value.bit_length())
self._key_bytes_count = (max_key_bits + 7) // 8
if key_bytes_count:
assert self._key_bytes_count <= key_bytes_count
self._key_bytes_count = key_bytes_count
self._bytes_count = (max_value_bits + 7) // 8
if bytes_count:
assert self._bytes_count <= bytes_count
self._bytes_count = bytes_count
self._key_byte_count = (max_key_bits + 7) // 8
if key_byte_count:
assert self._key_byte_count <= key_byte_count
self._key_byte_count = key_byte_count
self._byte_count = (max_value_bits + 7) // 8
if byte_count:
assert self._byte_count <= byte_count
self._byte_count = byte_count
self.choices = choices_map
self.needs_current_value = False
self.extra_default = extra_default
self._read_skip_bytes_count = read_skip_bytes_count if read_skip_bytes_count else 0
self._read_skip_byte_count = read_skip_byte_count if read_skip_byte_count else 0
self._write_prefix_bytes = write_prefix_bytes if write_prefix_bytes else b''
assert self._bytes_count + self._read_skip_bytes_count + self._key_bytes_count <= 14
assert self._bytes_count + len(self._write_prefix_bytes) + self._key_bytes_count <= 14
assert self._byte_count + self._read_skip_byte_count + self._key_byte_count <= 14
assert self._byte_count + len(self._write_prefix_bytes) + self._key_byte_count <= 14
def validate_read(self, reply_bytes, key):
start = self._key_bytes_count + self._read_skip_bytes_count
end = start + self._bytes_count
start = self._key_byte_count + self._read_skip_byte_count
end = start + self._byte_count
reply_value = _bytes2int(reply_bytes[start:end])
# reprogrammable keys starts out as 0, which is not a choice, so don't use assert here
if self.extra_default is not None and self.extra_default == reply_value:
@ -740,32 +745,32 @@ class ChoicesMapValidator(ChoicesValidator):
choices = self.choices[key]
if new_value not in choices and new_value != self.extra_default:
raise ValueError('invalid choice %r' % new_value)
return self._write_prefix_bytes + new_value.to_bytes(self._bytes_count, 'big')
return self._write_prefix_bytes + new_value.to_bytes(self._byte_count, 'big')
class RangeValidator(object):
__slots__ = ('min_value', 'max_value', 'flag', '_bytes_count', 'needs_current_value')
__slots__ = ('min_value', 'max_value', 'flag', '_byte_count', 'needs_current_value')
kind = KIND.range
"""Translates between integers and a byte sequence.
:param min_value: minimum accepted value (inclusive)
:param max_value: maximum accepted value (inclusive)
:param bytes_count: the size of the derived byte sequence. If None, it
:param byte_count: the size of the derived byte sequence. If None, it
will be calculated from the range."""
def __init__(self, min_value, max_value, bytes_count=None, **kwargs):
def __init__(self, min_value, max_value, byte_count=None):
assert max_value > min_value
self.min_value = min_value
self.max_value = max_value
self.needs_current_value = False
self._bytes_count = math.ceil(math.log(max_value + 1, 256))
if bytes_count:
assert self._bytes_count <= bytes_count
self._bytes_count = bytes_count
assert self._bytes_count < 8
self._byte_count = math.ceil(math.log(max_value + 1, 256))
if byte_count:
assert self._byte_count <= byte_count
self._byte_count = byte_count
assert self._byte_count < 8
def validate_read(self, reply_bytes):
reply_value = _bytes2int(reply_bytes[:self._bytes_count])
reply_value = _bytes2int(reply_bytes[:self._byte_count])
assert reply_value >= self.min_value, '%s: failed to validate read value %02X' % (self.__class__.__name__, reply_value)
assert reply_value <= self.max_value, '%s: failed to validate read value %02X' % (self.__class__.__name__, reply_value)
return reply_value
@ -773,4 +778,4 @@ class RangeValidator(object):
def prepare_write(self, new_value, current_value=None):
if new_value < self.min_value or new_value > self.max_value:
raise ValueError('invalid choice %r' % new_value)
return _int2bytes(new_value, self._bytes_count)
return _int2bytes(new_value, self._byte_count)

View File

@ -31,7 +31,6 @@ from .common import bytes2int as _bytes2int
from .common import int2bytes as _int2bytes
from .common import unpack as _unpack
from .i18n import _
from .settings import KIND as _KIND
from .settings import BitFieldSetting as _BitFieldSetting
from .settings import BitFieldValidator as _BitFieldV
from .settings import BooleanValidator as _BooleanV
@ -51,292 +50,165 @@ _DK = _hidpp10.DEVICE_KIND
_R = _hidpp10.REGISTERS
_F = _hidpp20.FEATURE
#
# pre-defined basic setting descriptors
#
def register_toggle(
name,
register,
true_value=_BooleanV.default_true,
false_value=_BooleanV.default_false,
mask=_BooleanV.default_mask,
device_kind=None
):
validator = _BooleanV(true_value=true_value, false_value=false_value, mask=mask)
rw = _RegisterRW(register)
return _Setting(name, rw, validator, device_kind=device_kind)
def register_choices(name, register, choices, kind=_KIND.choice, device_kind=None):
assert choices
validator = _ChoicesV(choices)
rw = _RegisterRW(register)
return _Setting(name, rw, validator, kind=kind, device_kind=device_kind)
def feature_toggle(
name,
feature,
read_fnid=_FeatureRW.default_read_fnid,
write_fnid=_FeatureRW.default_write_fnid,
true_value=_BooleanV.default_true,
false_value=_BooleanV.default_false,
mask=_BooleanV.default_mask,
device_kind=None
):
validator = _BooleanV(true_value=true_value, false_value=false_value, mask=mask)
rw = _FeatureRW(feature, read_fnid=read_fnid, write_fnid=write_fnid)
return _Setting(name, rw, validator, feature=feature, device_kind=device_kind)
def feature_bitfield_toggle(
name,
feature,
options,
read_fnid=_FeatureRW.default_read_fnid,
write_fnid=_FeatureRW.default_write_fnid,
device_kind=None
):
assert options
validator = _BitFieldV(options)
rw = _FeatureRW(feature, read_fnid=read_fnid, write_fnid=write_fnid)
return _BitFieldSetting(name, rw, validator, feature=feature, device_kind=device_kind)
def feature_bitfield_toggle_dynamic(
name,
feature,
options_callback,
read_fnid=_FeatureRW.default_read_fnid,
write_fnid=_FeatureRW.default_write_fnid,
device_kind=None
):
def instantiate(device):
options = options_callback(device)
setting = feature_bitfield_toggle(
name, feature, options, read_fnid=read_fnid, write_fnid=write_fnid, device_kind=device_kind
)
return setting(device)
instantiate._rw_kind = _FeatureRW.kind
return instantiate
def feature_choices(name, feature, choices, **kwargs):
assert choices
validator = _ChoicesV(choices, **kwargs)
rw = _FeatureRW(feature, **kwargs)
return _Setting(name, rw, validator, feature=feature, kind=_KIND.choice, **kwargs)
def feature_choices_dynamic(name, feature, choices_callback, **kwargs):
# Proxy that obtains choices dynamically from a device
def instantiate(device):
# Obtain choices for this feature
choices = choices_callback(device)
if not choices: # no choices, so don't create a setting
return None
setting = feature_choices(name, feature, choices, **kwargs)
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
# the setting is stored as a JSON-compatible object mapping the key int (as a string) to the choice int
def feature_map_choices(name, feature, choicesmap, **kwargs):
assert choicesmap
validator = _ChoicesMapV(choicesmap, **kwargs)
rw = _FeatureRWMap(feature, **kwargs)
return _Settings(name, rw, validator, feature=feature, kind=_KIND.map_choice, **kwargs)
def feature_map_choices_dynamic(name, feature, choices_callback, **kwargs):
# Proxy that obtains choices dynamically from a device
def instantiate(device):
choices = choices_callback(device)
if not choices: # no choices, so don't create a Setting
return None
setting = feature_map_choices(name, feature, choices, **kwargs)
return setting(device)
instantiate._rw_kind = _FeatureRWMap.kind
return instantiate
def feature_range(
name,
feature,
min_value,
max_value,
read_fnid=_FeatureRW.default_read_fnid,
write_fnid=_FeatureRW.default_write_fnid,
rw=None,
bytes_count=None,
device_kind=None
):
validator = _RangeV(min_value, max_value, bytes_count=bytes_count)
if rw is None:
rw = _FeatureRW(feature, read_fnid=read_fnid, write_fnid=write_fnid)
return _Setting(name, rw, validator, feature=feature, kind=_KIND.range, device_kind=device_kind)
#
# common strings for settings - name, string to display in main window, tool tip for main window
#
# yapf: disable
_HAND_DETECTION = ('hand-detection', _('Hand Detection'), _('Turn on illumination when the hands hover over the keyboard.'))
_SMOOTH_SCROLL = ('smooth-scroll', _('Smooth Scrolling'), _('High-sensitivity mode for vertical scroll with the wheel.'))
_SIDE_SCROLL = (
'side-scroll', _('Side Scrolling'),
_(
'When disabled, pushing the wheel sideways sends custom button events\n'
'instead of the standard side-scrolling events.'
)
)
_HI_RES_SCROLL = (
'hi-res-scroll', _('High Resolution Scrolling'), _('High-sensitivity mode for vertical scroll with the wheel.')
)
_LOW_RES_SCROLL = (
'lowres-smooth-scroll', _('HID++ Scrolling'),
_('HID++ mode for vertical scroll with the wheel.') + '\n' + _('Effectively turns off wheel scrolling in Linux.')
)
_HIRES_INV = (
'hires-smooth-invert', _('High Resolution Wheel Invert'),
_('High-sensitivity wheel invert direction for vertical scroll.')
)
_HIRES_RES = ('hires-smooth-resolution', _('Wheel Resolution'), _('High-sensitivity mode for vertical scroll with the wheel.'))
_FN_SWAP = (
'fn-swap', _('Swap Fx function'),
_(
'When set, the F1..F12 keys will activate their special function,\n'
'and you must hold the FN key to activate their standard 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.'
)
)
_SIDE_SCROLL = ('side-scroll', _('Side Scrolling'),
_('When disabled, pushing the wheel sideways sends custom button events\n'
'instead of the standard side-scrolling events.'))
_HI_RES_SCROLL = ('hi-res-scroll', _('High Resolution Scrolling'),
_('High-sensitivity mode for vertical scroll with the wheel.'))
_LOW_RES_SCROLL = ('lowres-smooth-scroll', _('HID++ Scrolling'),
_('HID++ mode for vertical scroll with the wheel.') + '\n' +
_('Effectively turns off wheel scrolling in Linux.'))
_HIRES_INV = ('hires-smooth-invert', _('High Resolution Wheel Invert'),
_('High-sensitivity wheel invert direction for vertical scroll.'))
_HIRES_RES = ('hires-smooth-resolution', _('Wheel Resolution'),
_('High-sensitivity mode for vertical scroll with the wheel.'))
_FN_SWAP = ('fn-swap', _('Swap Fx function'),
_('When set, the F1..F12 keys will activate their special function,\n'
'and you must hold the FN key to activate their standard 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.'))
_DPI = ('dpi', _('Sensitivity (DPI)'), None)
_POINTER_SPEED = (
'pointer_speed', _('Sensitivity (Pointer Speed)'), _('Speed multiplier for mouse (256 is normal multiplier).')
)
_SMART_SHIFT = (
'smart-shift', _('Smart Shift'),
_(
'Automatically switch the mouse wheel between ratchet and freespin mode.\n'
'The mouse wheel is always free at 0, and always locked at 50'
)
)
_POINTER_SPEED = ('pointer_speed', _('Sensitivity (Pointer Speed)'),
_('Speed multiplier for mouse (256 is normal multiplier).'))
_SMART_SHIFT = ('smart-shift', _('Smart Shift'),
_('Automatically switch the mouse wheel between ratchet and freespin mode.\n'
'The mouse wheel is always free at 0, and always locked at 50'))
_BACKLIGHT = ('backlight', _('Backlight'), _('Turn illumination on or off on keyboard.'))
_REPROGRAMMABLE_KEYS = (
'reprogrammable-keys', _('Actions'), _('Change the action for the key or button.') + '\n' +
_('Changing important actions (such as for the left mouse button) can result in an unusable system.')
)
_REPROGRAMMABLE_KEYS = ('reprogrammable-keys', _('Actions'),
_('Change the action for the key or button.') + '\n' +
_('Changing important actions (such as for the left mouse button) can result in an unusable system.'))
_DISABLE_KEYS = ('disable-keyboard-keys', _('Disable keys'), _('Disable specific keyboard keys.'))
_PLATFORM = ('multiplatform', _('Set OS'), _('Change keys to match OS.'))
_CHANGE_HOST = ('change-host', _('Change Host'), _('Switch connection to a different host'))
_THUMB_SCROLL_MODE = (
'thumb-scroll-mode', _('HID++ Thumb Scrolling'),
_('HID++ mode for horizontal scroll with the thumb wheel.') + '\n' + _('Effectively turns off thumb scrolling in Linux.')
)
_THUMB_SCROLL_MODE = ('thumb-scroll-mode', _('HID++ Thumb Scrolling'),
_('HID++ mode for horizontal scroll with the thumb wheel.') + '\n' +
_('Effectively turns off thumb scrolling in Linux.'))
_THUMB_SCROLL_INVERT = ('thumb-scroll-invert', _('Thumb Scroll Invert'), _('Invert thumb scroll direction.'))
# yapf: enable
# Setting template functions need to set up the setting itself, the validator, and the reader/writer.
# The reader/writer is responsible for reading raw values from the device and writing values to it.
# The validator is responsible for turning read raw values into Python data and Python data into raw values to be written.
# The setting keeps everything together and provides control.
#
# Keyword arguments for setting template functions:
# persist=True - whether to store the values and reapply them from now on
# device_kind - the kinds of devices that setting is suitable for (NOT CURRENTLY USED)
# read_fnid=0x00, write_fnid=0x10 - default 0x00 and 0x10 function numbers (times 16) to read and write setting
# bytes_count=1 - number of bytes for the data (ignoring the key, if any)
# only for boolean settings
# true_value=0x01, false_value=0x00, mask=0xFF - integer or byte strings for boolean settings
# only for map choices
# key_bytes_count=1 - number of bytes in the key
# extra_default - extra value that cannot be set but means same as default value
# only for choices and map choices
# read_skip_bytes_count=0 - number of bytes to ignore before the data when reading
# write_prefix_bytes=b'' - bytes to put before the data writing
# The _Setting class is for settings with simple values (booleans, numbers in a range, and symbolic choices).
# Its positional arguments are the strings for the setting and the reader/writer.
# The validator keyword (or third) argument is the validator, if the validator does not depend on information from the device.
# The callback keyword argument is a function that given a device as argument returns the validator or None.
# If the callback function returns None the setting is not created for the device.
# Either a validator or callback must be specified, but not both.
# The device_kind keyword argument (default None) says what kinds of devices can use the setting.
# (This argument is currently not used because keyboards with integrated trackpads break its abstraction.)
# The persist keyword argument (default True) says whether to store the value and apply it when setting up the device.
#
# There are two simple reader/writers - _RegisterRW and _FeatureRW.
# _RegisterRW is for register-based settings and takes the register name as argument.
# _FeatureRW is for feature-based settings and takes the feature name as positional argument plus the following:
# read_fnid is the feature function (times 16) to read the value (default 0x00),
# write_fnid is the feature function (times 16) to write the value (default 0x10),
# no_reply is whether to wait for a reply (default false) (USE WITH EXTREME CAUTION).
#
# There are three simple validators - _BooleanV, _RangeV, and _ChoicesV
# _BooleanV is for boolean values. It takes three keyword arguments that can be integers or byte strings:
# true_value is the raw value for true (default 0x01),
# false_value is the raw value for false (default 0x00),
# mask is used to keep only some bits from a sequence of bits.
# _RangeV is for an integer in a range. It takes three keyword arguments:
# min_value is the minimum value for the setting,
# max_value is the maximum value for the setting,
# byte_count is number of bytes that the value is stored in (defaults to size of max_value).
# _ChoicesV is for symbolic choices. It takes one positional and three keyword arguments:
# the positional argument is a list of named integers that are the valid choices,
# byte_count is the number of bytes for the integer (default size of largest choice integer),
# read_skip_byte_count is the number of bytes to ignore at the beginning of the read value (default 0),
# write_prefix_bytes is a byte string to write before the value (default empty).
#
# The _Settings class is for settings that are maps from keys to values.
# The _BitFieldSetting class is for settings that have multiple boolean values packed into a bit field.
# They have has same arguments as the _Setting class.
#
# _ChoicesMapV validator is for map settings that map onto symbolic choices. It takes four keyword arguments:
# the positional argument is the choices map
# byte_count is as for _ChoicesV,
# read_skip_byte_count is as for _ChoicesV,
# write_prefix_bytes is as for _ChoicesV,
# key_byte_count is the number of bytes for the key integer (default size of largest key),
# extra_default is an extra raw value that is used as a default value (default None).
# _BitFieldV validator is for bit field settings. It takes one positional and one keyword argument
# the positional argument is the number of bits in the bit field
# byte_count is the size of the bit field (default size of the bit field)
def _register_hand_detection(
register=_R.keyboard_hand_detection, true_value=b'\x00\x00\x00', false_value=b'\x00\x00\x30', mask=b'\x00\x00\xFF'
):
return register_toggle(
_HAND_DETECTION, register, true_value=true_value, false_value=false_value, device_kind=(_DK.keyboard, )
)
def _register_hand_detection():
validator = _BooleanV(true_value=b'\x00\x00\x00', false_value=b'\x00\x00\x30', mask=b'\x00\x00\xFF')
return _Setting(_HAND_DETECTION, _RegisterRW(_R.keyboard_hand_detection), validator, device_kind=(_DK.keyboard, ))
def _register_fn_swap(register=_R.keyboard_fn_swap, true_value=b'\x00\x01', mask=b'\x00\x01'):
return register_toggle(_FN_SWAP, register, true_value=true_value, mask=mask, device_kind=(_DK.keyboard, ))
def _register_fn_swap():
validator = _BooleanV(true_value=b'\x00\x01', mask=b'\x00\x01')
return _Setting(_FN_SWAP, _RegisterRW(_R.keyboard_fn_swap), validator, device_kind=(_DK.keyboard, ))
def _register_smooth_scroll(register=_R.mouse_button_flags, true_value=0x40, mask=0x40):
return register_toggle(_SMOOTH_SCROLL, register, true_value=true_value, mask=mask, device_kind=(_DK.mouse, _DK.trackball))
def _register_smooth_scroll():
validator = _BooleanV(true_value=0x40, mask=0x40)
return _Setting(_SMOOTH_SCROLL, _RegisterRW(_R.mouse_button_flags), validator, device_kind=(_DK.mouse, _DK.trackball))
def _register_side_scroll(register=_R.mouse_button_flags, true_value=0x02, mask=0x02):
return register_toggle(_SIDE_SCROLL, register, true_value=true_value, mask=mask, device_kind=(_DK.mouse, _DK.trackball))
def _register_side_scroll():
validator = _BooleanV(true_value=0x02, mask=0x02)
return _Setting(_SIDE_SCROLL, _RegisterRW(_R.mouse_button_flags), validator, device_kind=(_DK.mouse, _DK.trackball))
def _register_dpi(register=_R.mouse_dpi, choices=None):
return register_choices(_DPI, register, choices, device_kind=(_DK.mouse, _DK.trackball))
def _register_dpi(choices=None):
return _Setting(_DPI, _RegisterRW(_R.mouse_dpi), _ChoicesV(choices), device_kind=(_DK.mouse, _DK.trackball))
def _feature_fn_swap():
return feature_toggle(_FN_SWAP, _F.FN_INVERSION, device_kind=(_DK.keyboard, ))
return _Setting(_FN_SWAP, _FeatureRW(_F.FN_INVERSION), _BooleanV(), device_kind=(_DK.keyboard, ))
# this might not be correct for this feature
def _feature_new_fn_swap():
return feature_toggle(_FN_SWAP, _F.NEW_FN_INVERSION, device_kind=(_DK.keyboard, ))
return _Setting(_FN_SWAP, _FeatureRW(_F.NEW_FN_INVERSION), _BooleanV(), device_kind=(_DK.keyboard, ))
# ignore the capabilities part of the feature - all devices should be able to swap Fn state
# just use the current host (first byte = 0xFF) part of the feature to read and set the Fn state
def _feature_k375s_fn_swap():
return feature_toggle(
_FN_SWAP, _F.K375S_FN_INVERSION, true_value=b'\xFF\x01', false_value=b'\xFF\x00', device_kind=(_DK.keyboard, )
)
validator = _BooleanV(true_value=b'\xFF\x01', false_value=b'\xFF\x00')
return _Setting(_FN_SWAP, _FeatureRW(_F.K375S_FN_INVERSION), validator, device_kind=(_DK.keyboard, ))
# FIXME: This will enable all supported backlight settings,
# we should allow the users to select which settings they want to enable.
def _feature_backlight2():
return feature_toggle(_BACKLIGHT, _F.BACKLIGHT2, device_kind=(_DK.keyboard, ))
return _Setting(_BACKLIGHT, _FeatureRW(_F.BACKLIGHT2), _BooleanV(), device_kind=(_DK.keyboard, ))
def _feature_hi_res_scroll():
return feature_toggle(_HI_RES_SCROLL, _F.HI_RES_SCROLLING, device_kind=(_DK.mouse, _DK.trackball))
return _Setting(_HI_RES_SCROLL, _FeatureRW(_F.HI_RES_SCROLLING), _BooleanV(), device_kind=(_DK.mouse, _DK.trackball))
def _feature_lowres_smooth_scroll():
return feature_toggle(_LOW_RES_SCROLL, _F.LOWRES_WHEEL, device_kind=(_DK.mouse, _DK.trackball))
return _Setting(_LOW_RES_SCROLL, _FeatureRW(_F.LOWRES_WHEEL), _BooleanV(), device_kind=(_DK.mouse, _DK.trackball))
def _feature_hires_smooth_invert():
return feature_toggle(
_HIRES_INV,
_F.HIRES_WHEEL,
read_fnid=0x10,
write_fnid=0x20,
true_value=0x04,
mask=0x04,
device_kind=(_DK.mouse, _DK.trackball)
)
rw = _FeatureRW(_F.HIRES_WHEEL, read_fnid=0x10, write_fnid=0x20)
validator = _BooleanV(true_value=0x04, mask=0x04)
return _Setting(_HIRES_INV, rw, validator, device_kind=(_DK.mouse, _DK.trackball))
def _feature_hires_smooth_resolution():
return feature_toggle(
_HIRES_RES,
_F.HIRES_WHEEL,
read_fnid=0x10,
write_fnid=0x20,
true_value=0x02,
mask=0x02,
device_kind=(_DK.mouse, _DK.trackball)
)
rw = _FeatureRW(_F.HIRES_WHEEL, read_fnid=0x10, write_fnid=0x20)
validator = _BooleanV(true_value=0x02, mask=0x02)
return _Setting(_HIRES_RES, rw, validator, device_kind=(_DK.mouse, _DK.trackball))
def _feature_smart_shift():
@ -361,26 +233,17 @@ def _feature_smart_shift():
threshold = _bytes2int(data_bytes)
# Freespin at minimum
mode = 1 if threshold == _MIN_SMART_SHIFT_VALUE else 2
# Ratchet at maximum
if threshold == _MAX_SMART_SHIFT_VALUE:
threshold = 255
data = _int2bytes(mode, count=1) + _int2bytes(threshold, count=1) * 2
return super(_SmartShiftRW, self).write(device, data)
return feature_range(
_SMART_SHIFT,
_F.SMART_SHIFT,
_MIN_SMART_SHIFT_VALUE,
_MAX_SMART_SHIFT_VALUE,
bytes_count=1,
rw=_SmartShiftRW(_F.SMART_SHIFT),
device_kind=(_DK.mouse, _DK.trackball)
)
validator = _RangeV(_MIN_SMART_SHIFT_VALUE, _MAX_SMART_SHIFT_VALUE, 1)
return _Setting(_SMART_SHIFT, _SmartShiftRW(_F.SMART_SHIFT), validator, device_kind=(_DK.mouse, _DK.trackball))
def _feature_adjustable_dpi_choices(device):
def _feature_adjustable_dpi_callback(device):
# [1] getSensorDpiList(sensorIdx)
reply = device.feature_request(_F.ADJUSTABLE_DPI, 0x10)
# Should not happen, but might happen when the user unplugs device while the
@ -400,7 +263,7 @@ def _feature_adjustable_dpi_choices(device):
if step:
assert len(dpi_list) == 2, 'Invalid DPI list range: %r' % dpi_list
dpi_list = range(dpi_list[0], dpi_list[1] + 1, step)
return _NamedInts.list(dpi_list)
return _ChoicesV(_NamedInts.list(dpi_list), byte_count=3) if dpi_list else None
def _feature_adjustable_dpi():
@ -408,77 +271,49 @@ def _feature_adjustable_dpi():
# Assume sensorIdx 0 (there is only one sensor)
# [2] getSensorDpi(sensorIdx) -> sensorIdx, dpiMSB, dpiLSB
# [3] setSensorDpi(sensorIdx, dpi)
return feature_choices_dynamic(
_DPI,
_F.ADJUSTABLE_DPI,
_feature_adjustable_dpi_choices,
read_fnid=0x20,
write_fnid=0x30,
bytes_count=3,
device_kind=(_DK.mouse, _DK.trackball)
)
rw = _FeatureRW(_F.ADJUSTABLE_DPI, read_fnid=0x20, write_fnid=0x30)
return _Setting(_DPI, rw, callback=_feature_adjustable_dpi_callback, device_kind=(_DK.mouse, _DK.trackball))
def _feature_pointer_speed():
"""Pointer Speed feature"""
# min and max values taken from usb traces of Win software
return feature_range(
_POINTER_SPEED,
_F.POINTER_SPEED,
0x002e,
0x01ff,
read_fnid=0x0,
write_fnid=0x10,
bytes_count=2,
device_kind=(_DK.mouse, _DK.trackball)
)
validator = _RangeV(0x002e, 0x01ff, 2)
rw = _FeatureRW(_F.POINTER_SPEED)
return _Setting(_POINTER_SPEED, rw, validator, device_kind=(_DK.mouse, _DK.trackball))
# the keys for the choice map are Logitech controls (from special_keys)
# each choice value is a NamedInt with the string from a task (to be shown to the user)
# and the integer being the control number for that task (to be written to the device)
# Solaar only remaps keys (controlled by key gmask and group), not other key reprogramming
def _feature_reprogrammable_keys_choices(device):
def _feature_reprogrammable_keys_callback(device):
choices = {}
for k in device.keys:
tgts = k.remappable_to
if len(tgts) > 1:
choices[k.key] = tgts
return choices
if not choices:
return None
return _ChoicesMapV(
choices, key_byte_count=2, byte_count=2, read_skip_byte_count=1, write_prefix_bytes=b'\x00', extra_default=0
)
def _feature_reprogrammable_keys():
return feature_map_choices_dynamic(
_REPROGRAMMABLE_KEYS,
_F.REPROG_CONTROLS_V4,
_feature_reprogrammable_keys_choices,
read_fnid=0x20,
write_fnid=0x30,
key_bytes_count=2,
bytes_count=2,
read_skip_bytes_count=1,
write_prefix_bytes=b'\x00',
extra_default=0,
device_kind=(_DK.keyboard, ),
)
rw = _FeatureRWMap(_F.REPROG_CONTROLS_V4, read_fnid=0x20, write_fnid=0x30, key_byte_count=2)
return _Settings(_REPROGRAMMABLE_KEYS, rw, callback=_feature_reprogrammable_keys_callback, device_kind=(_DK.keyboard, ))
def _feature_disable_keyboard_keys_key_list(device):
def _feature_disable_keyboard_keys_callback(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
return _BitFieldV(options) if options else None
def _feature_disable_keyboard_keys():
return feature_bitfield_toggle_dynamic(
_DISABLE_KEYS,
_F.KEYBOARD_DISABLE_KEYS,
_feature_disable_keyboard_keys_key_list,
read_fnid=0x10,
write_fnid=0x20,
device_kind=(_DK.keyboard, )
)
rw = _FeatureRW(_F.KEYBOARD_DISABLE_KEYS, read_fnid=0x10, write_fnid=0x20)
return _BitFieldSetting(_DISABLE_KEYS, rw, callback=_feature_disable_keyboard_keys_callback, device_kind=(_DK.keyboard, ))
# muultiplatform OS bits
@ -486,7 +321,7 @@ OSS = [('Linux', 0x0400), ('MacOS', 0x2000), ('Windows', 0x0100), ('iOS', 0x4000
('Chrome', 0x0800), ('WinEmb', 0x0200), ('Tizen', 0x0001)]
def _feature_multiplatform_choices(device):
def _feature_multiplatform_callback(device):
def _str_os_versions(low, high):
def _str_os_version(version):
if version == 0:
@ -514,19 +349,12 @@ def _feature_multiplatform_choices(device):
os = os_name + _str_os_versions(low, high)
if os_bit & os_flags and platform not in choices and os not in choices:
choices[platform] = os
return choices
return _ChoicesV(choices, read_skip_byte_count=6, write_prefix_bytes=b'\xff') if choices else None
def _feature_multiplatform():
return feature_choices_dynamic(
_PLATFORM,
_F.MULTIPLATFORM,
_feature_multiplatform_choices,
read_fnid=0x00,
read_skip_bytes_count=6,
write_fnid=0x30,
write_prefix_bytes=b'\xff'
)
rw = _FeatureRW(_F.MULTIPLATFORM, read_fnid=0x00, write_fnid=0x30)
return _Setting(_PLATFORM, rw, callback=_feature_multiplatform_callback)
PLATFORMS = _NamedInts()
@ -535,12 +363,12 @@ PLATFORMS[0x01] = 'Android, Windows'
def _feature_dualplatform():
return feature_choices(
_PLATFORM, _F.DUALPLATFORM, PLATFORMS, read_fnid=0x10, write_fnid=0x20, device_kind=(_DK.keyboard, )
)
validator = _ChoicesV(PLATFORMS)
rw = _FeatureRW(_F.DUALPLATFORM, read_fnid=0x00, write_fnid=0x20)
return _Setting(_PLATFORM, rw, validator)
def _feature_change_host_choices(device):
def _feature_change_host_callback(device):
infos = device.feature_request(_F.CHANGE_HOST)
assert infos, 'Oops, host count cannot be retrieved!'
numHosts, currentHost = _unpack('!BB', infos[:2])
@ -553,46 +381,24 @@ def _feature_change_host_choices(device):
for host in range(0, numHosts):
_ignore, hostName = hostNames.get(host, (False, ''))
choices[host] = str(host + 1) + ':' + hostName if hostName else str(host + 1)
return choices
return _ChoicesV(choices, read_skip_byte_count=1) if choices else None
def _feature_change_host():
return feature_choices_dynamic(
_CHANGE_HOST,
_F.CHANGE_HOST,
_feature_change_host_choices,
persist=False,
no_reply=True,
read_fnid=0x00,
read_skip_bytes_count=1,
write_fnid=0x10
)
rw = _FeatureRW(_F.CHANGE_HOST, read_fnid=0x00, write_fnid=0x10, no_reply=True)
return _Setting(_CHANGE_HOST, rw, callback=_feature_change_host_callback, persist=False)
def _feature_thumb_mode():
return feature_toggle(
_THUMB_SCROLL_MODE,
_F.THUMB_WHEEL,
read_fnid=0x10,
write_fnid=0x20,
true_value=b'\x01\x00',
false_value=b'\x00\x00',
mask=b'\x01\x00',
device_kind=(_DK.mouse, _DK.trackball)
)
rw = _FeatureRW(_F.THUMB_WHEEL, read_fnid=0x10, write_fnid=0x20)
validator = _BooleanV(true_value=b'\x01\x00', false_value=b'\x00\x00', mask=b'\x01\x00')
return _Setting(_THUMB_SCROLL_MODE, rw, validator, device_kind=(_DK.mouse, _DK.trackball))
def _feature_thumb_invert():
return feature_toggle(
_THUMB_SCROLL_INVERT,
_F.THUMB_WHEEL,
read_fnid=0x10,
write_fnid=0x20,
true_value=b'\x00\x01',
false_value=b'\x00\x00',
mask=b'\x00\x01',
device_kind=(_DK.mouse, _DK.trackball)
)
rw = _FeatureRW(_F.THUMB_WHEEL, read_fnid=0x10, write_fnid=0x20)
validator = _BooleanV(true_value=b'\x00\x01', false_value=b'\x00\x00', mask=b'\x00\x01')
return _Setting(_THUMB_SCROLL_INVERT, rw, validator, device_kind=(_DK.mouse, _DK.trackball))
#