From f1fad2d16b4db29967714cc237211412269b03ea Mon Sep 17 00:00:00 2001 From: Javier Torres Date: Thu, 9 Jun 2016 19:39:43 +0200 Subject: [PATCH 1/6] Add range validator --- lib/logitech_receiver/settings.py | 46 +++++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/lib/logitech_receiver/settings.py b/lib/logitech_receiver/settings.py index 6534f24d..5e4764ba 100644 --- a/lib/logitech_receiver/settings.py +++ b/lib/logitech_receiver/settings.py @@ -24,19 +24,20 @@ _log = getLogger(__name__) del getLogger from copy import copy as _copy - +import math from .common import ( NamedInt as _NamedInt, NamedInts as _NamedInts, bytes2int as _bytes2int, + int2bytes as _int2bytes, ) # # # -KIND = _NamedInts(toggle=0x01, choice=0x02, range=0x12) +KIND = _NamedInts(toggle=0x01, choice=0x02, range=0x04) class Setting(object): """A setting descriptor. @@ -81,6 +82,14 @@ class Setting(object): return self._validator.choices if self._validator.kind & KIND.choice else None + @property + def range(self): + assert hasattr(self, '_value') + assert hasattr(self, '_device') + + if self._validator.kind == KIND.range: + return (self._validator.min_value, self._validator.max_value) + def read(self, cached=True): assert hasattr(self, '_value') assert hasattr(self, '_device') @@ -356,3 +365,36 @@ class ChoicesValidator(object): raise ValueError("invalid choice %r" % new_value) assert isinstance(choice, _NamedInt) return choice.bytes(self._bytes_count) + +class RangeValidator(object): + __slots__ = ('min_value', 'max_value', 'flag', '_bytes_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 + will be calculated from the range.""" + def __init__(self, min_value, max_value, bytes_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 + + def validate_read(self, reply_bytes): + reply_value = _bytes2int(reply_bytes[:self._bytes_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 + + 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) From 2fdce2f938671bd2ad36f039015ebd905770e722 Mon Sep 17 00:00:00 2001 From: Javier Torres Date: Thu, 9 Jun 2016 19:41:31 +0200 Subject: [PATCH 2/6] Add range features --- lib/logitech_receiver/settings_templates.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/lib/logitech_receiver/settings_templates.py b/lib/logitech_receiver/settings_templates.py index a2df4a29..b9b4c298 100644 --- a/lib/logitech_receiver/settings_templates.py +++ b/lib/logitech_receiver/settings_templates.py @@ -35,6 +35,7 @@ from .settings import ( FeatureRW as _FeatureRW, BooleanValidator as _BooleanV, ChoicesValidator as _ChoicesV, + RangeValidator as _RangeV, ) _DK = _hidpp10.DEVICE_KIND @@ -99,6 +100,17 @@ def feature_choices_dynamic(name, feature, choices_callback, return setting(device) return instantiate +def feature_range(name, feature, min_value, max_value, + read_function_id=_FeatureRW.default_read_fnid, + write_function_id=_FeatureRW.default_write_fnid, + rw=None, + bytes_count=None, + label=None, description=None, device_kind=None): + validator = _RangeV(min_value, max_value, bytes_count=bytes_count) + if rw is None: + rw = _FeatureRW(feature, read_function_id, write_function_id) + return _Setting(name, rw, validator, kind=_KIND.range, label=label, description=description, device_kind=device_kind) + # # common strings for settings # From bbadd3e7554bb7a27047b98a159d0b8b98079b09 Mon Sep 17 00:00:00 2001 From: Javier Torres Date: Thu, 9 Jun 2016 19:42:03 +0200 Subject: [PATCH 3/6] Add support for range features in CLI --- lib/solaar/cli/config.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/solaar/cli/config.py b/lib/solaar/cli/config.py index c6bfed06..60e63d50 100644 --- a/lib/solaar/cli/config.py +++ b/lib/solaar/cli/config.py @@ -112,6 +112,12 @@ def run(receivers, args, find_receiver, find_device): raise Exception("possible values for '%s' are: [%s]" % (setting.name, ', '.join(str(v) for v in setting.choices))) value = setting.choices[value] + elif setting.kind == _settings.KIND.range: + try: + value = int(args.value) + except ValueError: + raise Exception("can't interpret '%s' as integer" % args.value) + else: raise NotImplemented From 2442fee3414788ae1559a4aa7303a0df3e6efc3c Mon Sep 17 00:00:00 2001 From: Javier Torres Date: Thu, 9 Jun 2016 19:42:53 +0200 Subject: [PATCH 4/6] Add support for range features in GUI --- lib/solaar/ui/config_panel.py | 54 +++++++++++++++++++++++++---------- 1 file changed, 39 insertions(+), 15 deletions(-) diff --git a/lib/solaar/ui/config_panel.py b/lib/solaar/ui/config_panel.py index 99a37120..75f9a2c3 100644 --- a/lib/solaar/ui/config_panel.py +++ b/lib/solaar/ui/config_panel.py @@ -20,7 +20,7 @@ from __future__ import absolute_import, division, print_function, unicode_literals from gi.repository import Gtk, GLib - +from threading import Timer as _Timer from solaar.i18n import _ from solaar.ui import async as _ui_async @@ -76,15 +76,37 @@ def _create_choice_control(setting): c.connect('changed', _combo_notify, setting) return c -# def _create_slider_control(setting): -# def _slider_notify(slider, s): -# if slider.get_sensitive(): -# _apply_queue.put(('write', s, slider.get_value(), slider.get_parent())) -# -# c = Gtk.Scale(setting.choices) -# c.connect('value-changed', _slider_notify, setting) -# -# return c +def _create_slider_control(setting): + class SliderControl: + __slots__ = ('gtk_range', 'timer', 'setting') + def __init__(self, setting): + self.setting = setting + self.timer = None + + self.gtk_range = Gtk.Scale() + self.gtk_range.set_range(*self.setting.range) + self.gtk_range.set_round_digits(0) + self.gtk_range.set_digits(0) + self.gtk_range.set_increments(1, 5) + self.gtk_range.connect('value-changed', + lambda _, c: c._changed(), + self) + + def _write(self): + _write_async(self.setting, + int(self.gtk_range.get_value()), + self.gtk_range.get_parent()) + self.timer.cancel() + + def _changed(self): + if self.gtk_range.get_sensitive(): + if self.timer: + self.timer.cancel() + self.timer = _Timer(0.5, lambda: GLib.idle_add(self._write)) + self.timer.start() + + control = SliderControl(setting) + return control.gtk_range # # @@ -102,15 +124,17 @@ def _create_sbox(s): if s.kind == _SETTING_KIND.toggle: control = _create_toggle_control(s) + sbox.pack_end(control, False, False, 0) elif s.kind == _SETTING_KIND.choice: control = _create_choice_control(s) - # elif s.kind == _SETTING_KIND.range: - # control = _create_slider_control(s) + sbox.pack_end(control, False, False, 0) + elif s.kind == _SETTING_KIND.range: + control = _create_slider_control(s) + sbox.pack_end(control, True, True, 0) else: raise NotImplemented control.set_sensitive(False) # the first read will enable it - sbox.pack_end(control, False, False, 0) sbox.pack_end(spinner, False, False, 0) sbox.pack_end(failed, False, False, 0) @@ -140,8 +164,8 @@ def _update_setting_item(sbox, value, is_online=True): control.set_active(value) elif isinstance(control, Gtk.ComboBoxText): control.set_active_id(str(value)) - # elif isinstance(control, Gtk.Scale): - # control.set_value(int(value)) + elif isinstance(control, Gtk.Scale): + control.set_value(int(value)) else: raise NotImplemented control.set_sensitive(True) From ba540338e2ffb755bdca3d9808787c28314f2d6c Mon Sep 17 00:00:00 2001 From: Javier Torres Date: Thu, 9 Jun 2016 19:43:45 +0200 Subject: [PATCH 5/6] Add smart shift feature for MX master --- lib/logitech_receiver/settings_templates.py | 51 +++++++++++++++++++-- 1 file changed, 47 insertions(+), 4 deletions(-) diff --git a/lib/logitech_receiver/settings_templates.py b/lib/logitech_receiver/settings_templates.py index b9b4c298..f7fbbd21 100644 --- a/lib/logitech_receiver/settings_templates.py +++ b/lib/logitech_receiver/settings_templates.py @@ -25,6 +25,7 @@ from . import hidpp10 as _hidpp10 from . import hidpp20 as _hidpp20 from .common import ( bytes2int as _bytes2int, + int2bytes as _int2bytes, NamedInts as _NamedInts, unpack as _unpack, ) @@ -129,7 +130,9 @@ _FN_SWAP = ('fn-swap', _("Swap Fx function"), "and you must hold the FN key to activate their special function.")) _HAND_DETECTION = ('hand-detection', _("Hand Detection"), _("Turn on illumination when the hands hover over the keyboard.")) - +_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")) # # # @@ -176,6 +179,42 @@ def _feature_smooth_scroll(): label=_SMOOTH_SCROLL[1], description=_SMOOTH_SCROLL[2], device_kind=_DK.mouse) +def _feature_smart_shift(): + _MIN_SMART_SHIFT_VALUE = 0 + _MAX_SMART_SHIFT_VALUE = 50 + class _SmartShiftRW(_FeatureRW): + def __init__(self, feature): + super(_SmartShiftRW, self).__init__(feature) + + def read(self, device): + value = super(_SmartShiftRW, self).read(device) + if _bytes2int(value[0:1]) == 1: + # Mode = Freespin, map to minimum + return _int2bytes(_MIN_SMART_SHIFT_VALUE, count=1) + else: + # Mode = smart shift, map to the value, capped at maximum + threshold = min(_bytes2int(value[1:2]), _MAX_SMART_SHIFT_VALUE) + return _int2bytes(threshold, count=1) + + def write(self, device, data_bytes): + 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[0], _F.SMART_SHIFT, + _MIN_SMART_SHIFT_VALUE, _MAX_SMART_SHIFT_VALUE, + bytes_count=1, + rw=_SmartShiftRW(_F.SMART_SHIFT), + label=_SMART_SHIFT[1], description=_SMART_SHIFT[2], + device_kind=_DK.mouse) + def _feature_adjustable_dpi_choices(device): # [1] getSensorDpiList(sensorIdx) reply = device.feature_request(_F.ADJUSTABLE_DPI, 0x10) @@ -224,6 +263,7 @@ _SETTINGS_LIST = namedtuple('_SETTINGS_LIST', [ 'dpi', 'hand_detection', 'typing_illumination', + 'smart_shift', ]) del namedtuple @@ -235,6 +275,7 @@ RegisterSettings = _SETTINGS_LIST( dpi=_register_dpi, hand_detection=_register_hand_detection, typing_illumination=None, + smart_shift=None, ) FeatureSettings = _SETTINGS_LIST( fn_swap=_feature_fn_swap, @@ -244,6 +285,7 @@ FeatureSettings = _SETTINGS_LIST( dpi=_feature_adjustable_dpi, hand_detection=None, typing_illumination=None, + smart_shift=_feature_smart_shift, ) del _SETTINGS_LIST @@ -278,6 +320,7 @@ def check_feature_settings(device, already_known): already_known.append(feature(device)) check_feature(_SMOOTH_SCROLL[0], _F.HI_RES_SCROLLING) - check_feature(_FN_SWAP[0], _F.FN_INVERSION) - check_feature(_FN_SWAP[0], _F.NEW_FN_INVERSION, 'new_fn_swap') - check_feature(_DPI[0], _F.ADJUSTABLE_DPI) + check_feature(_FN_SWAP[0], _F.FN_INVERSION) + check_feature(_FN_SWAP[0], _F.NEW_FN_INVERSION, 'new_fn_swap') + check_feature(_DPI[0], _F.ADJUSTABLE_DPI) + check_feature(_SMART_SHIFT[0], _F.SMART_SHIFT) From f29de0f3dd11e838071823cc1caebe13e61caaa3 Mon Sep 17 00:00:00 2001 From: Javier Torres Date: Thu, 9 Jun 2016 19:44:07 +0200 Subject: [PATCH 6/6] Update docs: mx master supports smart shift --- docs/devices.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/devices.md b/docs/devices.md index c13fde5a..02ad3965 100644 --- a/docs/devices.md +++ b/docs/devices.md @@ -89,7 +89,7 @@ Mice (Unifying): | T620 Touch | 2.0 | yes | | | | Performance MX | 1.0 | yes | R/W | smooth scrolling | | Anywhere MX | 1.0 | yes | R/W | smooth scrolling | -| MX Master | 4.5 | yes | TODO | | +| MX Master | 4.5 | yes | R/W | smart shift | | Cube | 2.0 | yes | | |