diff --git a/docs/features.md b/docs/features.md index 2d2981fd..74590631 100644 --- a/docs/features.md +++ b/docs/features.md @@ -75,8 +75,8 @@ Feature | ID | Status | Notes `KEYBOARD_LAYOUT` | `0x4520` | :x: | read only `KEYBOARD_DISABLE_KEYS` | `0x4521` | :heavy_check_mark: | `_feature_disable_keyboard_keys` `KEYBOARD_DISABLE_BY_USAGE` | `0x4522` | :x: | -`DUALPLATFORM` | `0x4530` | :x: | :wrench: -`MULTIPLATFORM` | `0x4531` | :x: | :wrench: +`DUALPLATFORM` | `0x4530` | :heavy_check_mark: | `_feature_dualplatform`, untested +`MULTIPLATFORM` | `0x4531` | :heavy_check_mark: | `_feature_multiplatform` `KEYBOARD_LAYOUT_2` | `0x4540` | :x: | read only `CROWN` | `0x4600` | :x: | `TOUCHPAD_FW_ITEMS` | `0x6010` | :x: | diff --git a/lib/logitech_receiver/settings.py b/lib/logitech_receiver/settings.py index ad14d957..034cf502 100644 --- a/lib/logitech_receiver/settings.py +++ b/lib/logitech_receiver/settings.py @@ -45,7 +45,7 @@ class Setting(object): Needs to be instantiated for each specific device.""" __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): + def __init__(self, name, rw, validator, kind=None, label=None, description=None, device_kind=None, feature=None, **kwargs): assert name self.name = name self.label = label or name @@ -434,7 +434,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): + def __init__(self, feature, read_fnid=default_read_fnid, write_fnid=default_write_fnid, **kwargs): assert isinstance(feature, _NamedInt) self.feature = feature self.read_fnid = read_fnid @@ -453,23 +453,30 @@ class FeatureRWMap(FeatureRW): kind = _NamedInt(0x02, 'feature') default_read_fnid = 0x00 default_write_fnid = 0x10 - default_key_bytes = 1 + default_key_bytes_count = 1 - def __init__(self, feature, read_fnid=default_read_fnid, write_fnid=default_write_fnid, key_bytes=default_key_bytes): + def __init__( + self, + feature, + read_fnid=default_read_fnid, + write_fnid=default_write_fnid, + key_bytes_count=default_key_bytes_count, + **kwargs + ): assert isinstance(feature, _NamedInt) self.feature = feature self.read_fnid = read_fnid self.write_fnid = write_fnid - self.key_bytes = key_bytes + self.key_bytes_count = key_bytes_count def read(self, device, key): assert self.feature is not None - key_bytes = _int2bytes(key, self.key_bytes) + key_bytes = _int2bytes(key, self.key_bytes_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) + key_bytes = _int2bytes(key, self.key_bytes_count) return device.feature_request(self.feature, self.write_fnid, key_bytes, data_bytes) @@ -488,7 +495,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): + def __init__(self, true_value=default_true, false_value=default_false, mask=default_mask, **kwargs): if isinstance(true_value, int): assert isinstance(false_value, int) if mask is None: @@ -593,7 +600,7 @@ class BitFieldValidator(object): kind = KIND.multiple_toggle - def __init__(self, options, byte_count=None): + def __init__(self, options, byte_count=None, **kwargs): assert (isinstance(options, list)) self.options = options self.byte_count = (max(x.bit_length() for x in options) + 7) // 8 @@ -621,14 +628,12 @@ class BitFieldValidator(object): class ChoicesValidator(object): - __slots__ = ('choices', 'flag', '_bytes_count', 'needs_current_value') - 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 will be calculated from the choices.""" - def __init__(self, choices, bytes_count=None): + def __init__(self, choices, bytes_count=None, read_skip_bytes_count=None, write_prefix_bytes=b'', **_ignore): assert choices is not None assert isinstance(choices, _NamedInts) assert len(choices) > 2 @@ -641,9 +646,13 @@ class ChoicesValidator(object): 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._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 def validate_read(self, reply_bytes): - reply_value = _bytes2int(reply_bytes[:self._bytes_count]) + reply_value = _bytes2int(reply_bytes[self._read_skip_bytes_count:self._read_skip_bytes_count + self._bytes_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 @@ -664,13 +673,22 @@ class ChoicesValidator(object): if choice is None: raise ValueError('invalid choice %r' % new_value) assert isinstance(choice, _NamedInt) - return choice.bytes(self._bytes_count) + return self._write_prefix_bytes + choice.bytes(self._bytes_count) 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): + def __init__( + self, + choices_map, + key_bytes_count=None, + bytes_count=None, + read_skip_bytes_count=0, + write_prefix_bytes=b'', + extra_default=None, + **kwargs + ): assert choices_map is not None assert isinstance(choices_map, dict) max_key_bits = 0 @@ -682,24 +700,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.choices = choices_map - self.needs_current_value = False - self.extra_default = extra_default - 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._value_bytes_count = (max_value_bits + 7) // 8 - if value_bytes_count: - assert self._value_bytes_count <= value_bytes_count - self._value_bytes_count = value_bytes_count - self._skip_bytes_count = skip_bytes_count if skip_bytes_count is not None else 0 - self._bytes_count = self._key_bytes_count + self._skip_bytes_count + self._value_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.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._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 def validate_read(self, reply_bytes, key): - start = self._key_bytes_count + self._skip_bytes_count - end = start + self._value_bytes_count + start = self._key_bytes_count + self._read_skip_bytes_count + end = start + self._bytes_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: @@ -712,7 +731,7 @@ 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 _int2bytes(new_value, self._skip_bytes_count + self._value_bytes_count) + return self._write_prefix_bytes + new_value.to_bytes(self._bytes_count, 'big') class RangeValidator(object): @@ -724,7 +743,7 @@ class RangeValidator(object): :param max_value: maximum accepted value (inclusive) :param bytes_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): + def __init__(self, min_value, max_value, bytes_count=None, **kwargs): assert max_value > min_value self.min_value = min_value self.max_value = max_value diff --git a/lib/logitech_receiver/settings_templates.py b/lib/logitech_receiver/settings_templates.py index 44cbccb6..c5cfe854 100644 --- a/lib/logitech_receiver/settings_templates.py +++ b/lib/logitech_receiver/settings_templates.py @@ -82,8 +82,8 @@ def register_choices(name, register, choices, kind=_KIND.choice, label=None, des def feature_toggle( name, feature, - read_function_id=_FeatureRW.default_read_fnid, - write_function_id=_FeatureRW.default_write_fnid, + 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, @@ -92,7 +92,7 @@ def feature_toggle( device_kind=None ): validator = _BooleanV(true_value=true_value, false_value=false_value, mask=mask) - rw = _FeatureRW(feature, read_function_id, write_function_id) + rw = _FeatureRW(feature, read_fnid=read_fnid, write_fnid=write_fnid) return _Setting(name, rw, validator, feature=feature, label=label, description=description, device_kind=device_kind) @@ -100,15 +100,15 @@ def feature_bitfield_toggle( name, feature, options, - read_function_id=_FeatureRW.default_read_fnid, - write_function_id=_FeatureRW.default_write_fnid, + read_fnid=_FeatureRW.default_read_fnid, + write_fnid=_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) + rw = _FeatureRW(feature, read_fnid=read_fnid, write_fnid=write_fnid) return _BitFieldSetting( name, rw, validator, feature=feature, label=label, description=description, device_kind=device_kind ) @@ -118,8 +118,8 @@ def feature_bitfield_toggle_dynamic( name, feature, options_callback, - read_function_id=_FeatureRW.default_read_fnid, - write_function_id=_FeatureRW.default_write_fnid, + read_fnid=_FeatureRW.default_read_fnid, + write_fnid=_FeatureRW.default_write_fnid, label=None, description=None, device_kind=None @@ -130,8 +130,8 @@ def feature_bitfield_toggle_dynamic( name, feature, options, - read_function_id=read_function_id, - write_function_id=write_function_id, + read_fnid=read_fnid, + write_fnid=write_fnid, label=label, description=description, device_kind=device_kind @@ -142,51 +142,21 @@ def feature_bitfield_toggle_dynamic( return instantiate -def feature_choices( - name, - feature, - choices, - read_function_id, - write_function_id, - bytes_count=None, - label=None, - description=None, - device_kind=None -): +def feature_choices(name, feature, choices, **kwargs): assert choices - validator = _ChoicesV(choices, bytes_count=bytes_count) - rw = _FeatureRW(feature, read_function_id, write_function_id) - return _Setting( - name, rw, validator, feature=feature, kind=_KIND.choice, label=label, description=description, device_kind=device_kind - ) + validator = _ChoicesV(choices, **kwargs) + rw = _FeatureRW(feature, **kwargs) + return _Setting(name, rw, validator, kind=_KIND.choice, **kwargs) -def feature_choices_dynamic( - name, - feature, - choices_callback, - read_function_id, - write_function_id, - bytes_count=None, - label=None, - description=None, - device_kind=None -): +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) - setting = feature_choices( - name, - feature, - choices, - read_function_id, - write_function_id, - bytes_count=bytes_count, - label=label, - description=description, - device_kind=device_kind - ) + 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 @@ -195,75 +165,20 @@ def feature_choices_dynamic( # 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 -# extra_default is an extra value that comes from the device that also means the default -def feature_map_choices( - name, - feature, - choicesmap, - read_function_id, - write_function_id, - key_bytes_count=None, - skip_bytes_count=None, - value_bytes_count=None, - label=None, - description=None, - device_kind=None, - extra_default=None -): +def feature_map_choices(name, feature, choicesmap, **kwargs): assert choicesmap - validator = _ChoicesMapV( - choicesmap, - key_bytes_count=key_bytes_count, - skip_bytes_count=skip_bytes_count, - value_bytes_count=value_bytes_count, - extra_default=extra_default - ) - rw = _FeatureRWMap(feature, read_function_id, write_function_id, key_bytes=key_bytes_count) - return _Settings( - name, - rw, - validator, - feature=feature, - kind=_KIND.map_choice, - label=label, - description=description, - device_kind=device_kind - ) + validator = _ChoicesMapV(choicesmap, **kwargs) + rw = _FeatureRWMap(feature, **kwargs) + return _Settings(name, rw, validator, kind=_KIND.map_choice, **kwargs) -def feature_map_choices_dynamic( - name, - feature, - choices_callback, - read_function_id, - write_function_id, - key_bytes_count=None, - skip_bytes_count=None, - value_bytes_count=None, - label=None, - description=None, - device_kind=None, - extra_default=None -): +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, - read_function_id, - write_function_id, - 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 - ) + setting = feature_map_choices(name, feature, choices, **kwargs) return setting(device) instantiate._rw_kind = _FeatureRWMap.kind @@ -275,8 +190,8 @@ def feature_range( feature, min_value, max_value, - read_function_id=_FeatureRW.default_read_fnid, - write_function_id=_FeatureRW.default_write_fnid, + read_fnid=_FeatureRW.default_read_fnid, + write_fnid=_FeatureRW.default_write_fnid, rw=None, bytes_count=None, label=None, @@ -285,7 +200,7 @@ def feature_range( ): validator = _RangeV(min_value, max_value, bytes_count=bytes_count) if rw is None: - rw = _FeatureRW(feature, read_function_id, write_function_id) + rw = _FeatureRW(feature, read_fnid=read_fnid, write_fnid=write_fnid) return _Setting( name, rw, validator, feature=feature, kind=_KIND.range, label=label, description=description, device_kind=device_kind ) @@ -342,10 +257,22 @@ _REPROGRAMMABLE_KEYS = ( _('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.')) # -# -# +# Keyword arguments for setting template functions: +# label, description - label and tooltip to be shown in GUI +# device_kind - the kinds of devices that setting is suitable for (NOT CURRENTLY USED) +# read_fnid, write_fnid - default 0x00 and 0x10 function numbers (times 16) to read and write setting +# bytes_count - default 1 - number of bytes for the data (ignoring the key) +# only for boolean settings +# true_value, false_value, mask - integer or byte strings for boolean settings +# only for map choices +# key_bytes_count - default 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 - default 0 - number of bytes to ignore before the data when reading +# write_prefix_bytes - default None - bytes to put before the data writing def _register_hand_detection( @@ -463,8 +390,8 @@ def _feature_hires_smooth_invert(): return feature_toggle( _HIRES_INV[0], _F.HIRES_WHEEL, - read_function_id=0x10, - write_function_id=0x20, + read_fnid=0x10, + write_fnid=0x20, true_value=0x04, mask=0x04, label=_HIRES_INV[1], @@ -477,8 +404,8 @@ def _feature_hires_smooth_resolution(): return feature_toggle( _HIRES_RES[0], _F.HIRES_WHEEL, - read_function_id=0x10, - write_function_id=0x20, + read_fnid=0x10, + write_fnid=0x20, true_value=0x02, mask=0x02, label=_HIRES_RES[1], @@ -562,8 +489,8 @@ def _feature_adjustable_dpi(): _DPI[0], _F.ADJUSTABLE_DPI, _feature_adjustable_dpi_choices, - read_function_id=0x20, - write_function_id=0x30, + read_fnid=0x20, + write_fnid=0x30, bytes_count=3, label=_DPI[1], description=_DPI[2], @@ -579,8 +506,8 @@ def _feature_pointer_speed(): _F.POINTER_SPEED, 0x002e, 0x01ff, - read_function_id=0x0, - write_function_id=0x10, + read_fnid=0x0, + write_fnid=0x10, bytes_count=2, label=_POINTER_SPEED[1], description=_POINTER_SPEED[2], @@ -624,15 +551,16 @@ def _feature_reprogrammable_keys(): _REPROGRAMMABLE_KEYS[0], _F.REPROG_CONTROLS_V4, _feature_reprogrammable_keys_choices, - read_function_id=0x20, - write_function_id=0x30, + read_fnid=0x20, + write_fnid=0x30, key_bytes_count=2, - skip_bytes_count=1, - value_bytes_count=2, + bytes_count=2, + read_skip_bytes_count=1, + write_prefix_bytes=b'\x00', + extra_default=0, label=_REPROGRAMMABLE_KEYS[1], description=_REPROGRAMMABLE_KEYS[2], device_kind=(_DK.keyboard, ), - extra_default=0 ) @@ -647,14 +575,83 @@ def _feature_disable_keyboard_keys(): _DISABLE_KEYS[0], _F.KEYBOARD_DISABLE_KEYS, _feature_disable_keyboard_keys_key_list, - read_function_id=0x10, - write_function_id=0x20, + read_fnid=0x10, + write_fnid=0x20, label=_DISABLE_KEYS[1], description=_DISABLE_KEYS[2], device_kind=(_DK.keyboard, ) ) +# muultiplatform OS bits +OSS = [('Linux', 0x0400), ('MacOS', 0x2000), ('Windows', 0x0100), ('iOS', 0x4000), ('Android', 0x1000), ('WebOS', 0x8000), + ('Chrome', 0x0800), ('WinEmb', 0x0200), ('Tizen', 0x0001)] + + +def _feature_multiplatform_choices(device): + def _str_os_versions(low, high): + def _str_os_version(version): + if version == 0: + return '' + elif version & 0xFF: + return str(version >> 8) + '.' + str(version & 0xFF) + else: + return str(version >> 8) + + return '' if low == 0 and high == 0 else ' ' + _str_os_version(low) + '-' + _str_os_version(high) + + infos = device.feature_request(_F.MULTIPLATFORM) + assert infos, 'Oops, multiplatform count cannot be retrieved!' + flags, _ignore, num_descriptors = _unpack('!BBB', infos[:3]) + if not (flags & 0x02): # can't set platform so don't create setting + return [] + descriptors = [] + for index in range(0, num_descriptors): + descriptor = device.feature_request(_F.MULTIPLATFORM, 0x10, index) + platform, _ignore, os_flags, low, high = _unpack('!BBHHH', descriptor[:8]) + descriptors.append((platform, os_flags, low, high)) + choices = _NamedInts() + for os_name, os_bit in OSS: + for platform, os_flags, low, high in descriptors: + 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 + + +def _feature_multiplatform(): + return feature_choices_dynamic( + _PLATFORM[0], + _F.MULTIPLATFORM, + _feature_multiplatform_choices, + read_fnid=0x00, + read_skip_bytes_count=6, + write_fnid=0x30, + write_prefix_bytes=b'\xff', + label=_PLATFORM[1], + description=_PLATFORM[2], + device_kind=(_DK.keyboard, ) + ) + + +PLATFORMS = _NamedInts() +PLATFORMS[0x00] = 'iOS, MacOS' +PLATFORMS[0x01] = 'Android, Windows' + + +def _feature_dualplatform(): + return feature_choices( + _PLATFORM[0], + _F.DUALPLATFORM, + PLATFORMS, + read_fnid=0x10, + write_fnid=0x20, + label=_PLATFORM[1], + description=_PLATFORM[2], + device_kind=(_DK.keyboard, ) + ) + + # # # @@ -681,6 +678,8 @@ _SETTINGS_TABLE = [ _S(_BACKLIGHT[0], _F.BACKLIGHT2, _feature_backlight2), _S(_REPROGRAMMABLE_KEYS[0], _F.REPROG_CONTROLS_V4, _feature_reprogrammable_keys), _S(_DISABLE_KEYS[0], _F.KEYBOARD_DISABLE_KEYS, _feature_disable_keyboard_keys), + _S(_PLATFORM[0], _F.MULTIPLATFORM, _feature_multiplatform), + _S(_PLATFORM[0], _F.DUALPLATFORM, _feature_dualplatform, identifier='dualplatform') ] _SETTINGS_LIST = namedtuple('_SETTINGS_LIST', [s[4] for s in _SETTINGS_TABLE]) diff --git a/lib/solaar/ui/config_panel.py b/lib/solaar/ui/config_panel.py index df52442c..a737518f 100644 --- a/lib/solaar/ui/config_panel.py +++ b/lib/solaar/ui/config_panel.py @@ -90,7 +90,7 @@ def _create_choice_control(setting): c = Gtk.ComboBoxText() # TODO i18n text entries for entry in setting.choices: - c.append(str(entry), str(entry)) + c.append(str(int(entry)), str(entry)) c.connect('changed', _combo_notify, setting) return c @@ -114,7 +114,7 @@ def _create_map_choice_control(setting): def _map_populate_value_box(valueBox, setting, key_choice): choices = None choices = setting.choices[key_choice] - current = setting._value.get(str(key_choice)) # just in case the persisted value is missing some keys + current = setting._value.get(str(key_choice)) if setting._value else None if choices: # TODO i18n text entries for choice in choices: