1855 lines
		
	
	
		
			80 KiB
		
	
	
	
		
			Python
		
	
	
	
			
		
		
	
	
			1855 lines
		
	
	
		
			80 KiB
		
	
	
	
		
			Python
		
	
	
	
| ## Copyright (C) 2012-2013  Daniel Pavel
 | |
| ##
 | |
| ## This program is free software; you can redistribute it and/or modify
 | |
| ## it under the terms of the GNU General Public License as published by
 | |
| ## the Free Software Foundation; either version 2 of the License, or
 | |
| ## (at your option) any later version.
 | |
| ##
 | |
| ## This program is distributed in the hope that it will be useful,
 | |
| ## but WITHOUT ANY WARRANTY; without even the implied warranty of
 | |
| ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | |
| ## GNU General Public License for more details.
 | |
| ##
 | |
| ## You should have received a copy of the GNU General Public License along
 | |
| ## with this program; if not, write to the Free Software Foundation, Inc.,
 | |
| ## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 | |
| from __future__ import annotations
 | |
| 
 | |
| import enum
 | |
| import logging
 | |
| import socket
 | |
| import struct
 | |
| import traceback
 | |
| 
 | |
| from time import time
 | |
| from typing import Any
 | |
| from typing import Callable
 | |
| 
 | |
| from solaar.i18n import _
 | |
| 
 | |
| from . import base
 | |
| from . import common
 | |
| from . import descriptors
 | |
| from . import desktop_notifications
 | |
| from . import diversion
 | |
| from . import hidpp10_constants
 | |
| from . import hidpp20
 | |
| from . import hidpp20_constants
 | |
| from . import settings
 | |
| from . import special_keys
 | |
| from .hidpp10_constants import Registers
 | |
| from .hidpp20_constants import GestureId
 | |
| from .hidpp20_constants import ParamId
 | |
| 
 | |
| logger = logging.getLogger(__name__)
 | |
| 
 | |
| _hidpp20 = hidpp20.Hidpp20()
 | |
| _DK = hidpp10_constants.DEVICE_KIND
 | |
| _F = hidpp20_constants.SupportedFeature
 | |
| 
 | |
| 
 | |
| class State(enum.Enum):
 | |
|     IDLE = "idle"
 | |
|     PRESSED = "pressed"
 | |
|     MOVED = "moved"
 | |
| 
 | |
| 
 | |
| # Setting classes are used to control the settings that the Solaar GUI shows and manipulates.
 | |
| # Each setting class has to several class variables:
 | |
| # name, which is used as a key when storing information about the setting,
 | |
| #   setting classes can have the same name, as long as devices only have one setting with the same name;
 | |
| # label, which is shown in the Solaar main window;
 | |
| # description, which is shown when the mouse hovers over the setting in the main window;
 | |
| # either register or feature, the register or feature that the setting uses;
 | |
| # rw_class, the class of the reader/writer (if it is not the standard one),
 | |
| # rw_options, a dictionary of options for the reader/writer.
 | |
| # validator_class, the class of the validator (default settings.BooleanValidator)
 | |
| # validator_options, a dictionary of options for the validator
 | |
| # persist (inherited True), which is whether to store the value and apply it when setting up the device.
 | |
| #
 | |
| # The different setting classes imported from settings.py are for different numbers and kinds of arguments.
 | |
| # Setting is for settings with a single value (boolean, number in a range, and symbolic choice).
 | |
| # Settings is for settings that are maps from keys to values
 | |
| #    and permit reading or writing the entire map or just one key/value pair.
 | |
| # The BitFieldSetting class is for settings that have multiple boolean values packed into a bit field.
 | |
| # BitFieldWithOffsetAndMaskSetting is similar.
 | |
| # The RangeFieldSetting class is for settings that have multiple ranges packed into a byte string.
 | |
| # LongSettings is for settings that have an even more complex structure.
 | |
| #
 | |
| # When settings are created a reader/writer and a validator are created.
 | |
| 
 | |
| # If the setting class has a value for rw_class then an instance of that class is created.
 | |
| # Otherwise if the setting has a register then an instance of RegisterRW is created.
 | |
| # and if the setting has a feature then an instance of FeatureRW is created.
 | |
| # The instance is created with the register or feature as the first argument and rw_options as keyword arguments.
 | |
| # RegisterRW doesn't use any options.
 | |
| # FeatureRW options include
 | |
| #   read_fnid - the feature function (times 16) to read the value (default 0x00),
 | |
| #   write_fnid - the feature function (times 16) to write the value (default 0x10),
 | |
| #   prefix - a prefix to add to the data being written and the read request (default b''), used for features
 | |
| #     that provide and set multiple settings (e.g., to read and write function key inversion for current host)
 | |
| #   no_reply - whether to wait for a reply (default false) (USE WITH EXTREME CAUTION).
 | |
| #
 | |
| # There are three simple validator classes - BooleanV, RangeValidator, and ChoicesValidator
 | |
| # BooleanV is for boolean values and is the default.  It takes
 | |
| #   true_value is the raw value for true (default 0x01), this can be an integer or a byte string,
 | |
| #   false_value is the raw value for false (default 0x00), this can be an integer or a byte string,
 | |
| #   mask is used to keep only some bits from a sequence of bits, this can be an integer or a byte string,
 | |
| #   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).
 | |
| # RangeValidator is for an integer in a range.  It takes
 | |
| #   byte_count is number of bytes that the value is stored in (defaults to size of max_value).
 | |
| # RangeValidator uses min_value and max_value from the setting class as minimum and maximum.
 | |
| 
 | |
| # ChoicesValidator is for symbolic choices.  It takes one positional and three keyword arguments:
 | |
| #   choices 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 as for BooleanV,
 | |
| #   write_prefix_bytes is as for BooleanV.
 | |
| # Settings that use ChoicesValidator should have a choices_universe class variable of the potential choices,
 | |
| # or None for no limitation and optionally a choices_extra class variable with an extra choice.
 | |
| # The choices_extra is so that there is no need to specially extend a large existing NamedInts.
 | |
| # ChoicesMapValidator validator is for map settings that map onto symbolic choices.   It takes
 | |
| #   choices_map is a map from keys to possible values
 | |
| #   byte_count is as for ChoicesValidator,
 | |
| #   read_skip_byte_count is as for ChoicesValidator,
 | |
| #   write_prefix_bytes is as for ChoicesValidator,
 | |
| #   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).
 | |
| # Settings that use ChoicesValidator should have keys_universe and choices_universe class variable of
 | |
| # the potential keys and potential choices or None for no limitation.
 | |
| 
 | |
| # BitFieldValidator 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)
 | |
| #
 | |
| # A few settings work very differently.  They divert a key, which is then used to start and stop some special action.
 | |
| # These settings have reader/writer classes that perform special processing instead of sending commands to the device.
 | |
| 
 | |
| 
 | |
| class FnSwapVirtual(settings.Setting):  # virtual setting to hold fn swap strings
 | |
|     name = "fn-swap"
 | |
|     label = _("Swap Fx function")
 | |
|     description = (
 | |
|         _(
 | |
|             "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."
 | |
|         )
 | |
|     )
 | |
| 
 | |
| 
 | |
| class RegisterHandDetection(settings.Setting):
 | |
|     name = "hand-detection"
 | |
|     label = _("Hand Detection")
 | |
|     description = _("Turn on illumination when the hands hover over the keyboard.")
 | |
|     register = Registers.KEYBOARD_HAND_DETECTION
 | |
|     validator_options = {"true_value": b"\x00\x00\x00", "false_value": b"\x00\x00\x30", "mask": b"\x00\x00\xff"}
 | |
| 
 | |
| 
 | |
| class RegisterSmoothScroll(settings.Setting):
 | |
|     name = "smooth-scroll"
 | |
|     label = _("Scroll Wheel Smooth Scrolling")
 | |
|     description = _("High-sensitivity mode for vertical scroll with the wheel.")
 | |
|     register = Registers.MOUSE_BUTTON_FLAGS
 | |
|     validator_options = {"true_value": 0x40, "mask": 0x40}
 | |
| 
 | |
| 
 | |
| class RegisterSideScroll(settings.Setting):
 | |
|     name = "side-scroll"
 | |
|     label = _("Side Scrolling")
 | |
|     description = _(
 | |
|         "When disabled, pushing the wheel sideways sends custom button events\n"
 | |
|         "instead of the standard side-scrolling events."
 | |
|     )
 | |
|     register = Registers.MOUSE_BUTTON_FLAGS
 | |
|     validator_options = {"true_value": 0x02, "mask": 0x02}
 | |
| 
 | |
| 
 | |
| # different devices have different sets of permissible dpis, so this should be subclassed
 | |
| class RegisterDpi(settings.Setting):
 | |
|     name = "dpi-old"
 | |
|     label = _("Sensitivity (DPI - older mice)")
 | |
|     description = _("Mouse movement sensitivity")
 | |
|     register = Registers.MOUSE_DPI
 | |
|     choices_universe = common.NamedInts.range(0x81, 0x8F, lambda x: str((x - 0x80) * 100))
 | |
|     validator_class = settings.ChoicesValidator
 | |
|     validator_options = {"choices": choices_universe}
 | |
| 
 | |
| 
 | |
| class RegisterFnSwap(FnSwapVirtual):
 | |
|     register = Registers.KEYBOARD_FN_SWAP
 | |
|     validator_options = {"true_value": b"\x00\x01", "mask": b"\x00\x01"}
 | |
| 
 | |
| 
 | |
| class _PerformanceMXDpi(RegisterDpi):
 | |
|     choices_universe = common.NamedInts.range(0x81, 0x8F, lambda x: str((x - 0x80) * 100))
 | |
|     validator_options = {"choices": choices_universe}
 | |
| 
 | |
| 
 | |
| # set up register settings for devices - this is done here to break up an import loop
 | |
| descriptors.get_wpid("0060").settings = [RegisterFnSwap]
 | |
| descriptors.get_wpid("2008").settings = [RegisterFnSwap]
 | |
| descriptors.get_wpid("2010").settings = [RegisterFnSwap, RegisterHandDetection]
 | |
| descriptors.get_wpid("2011").settings = [RegisterFnSwap]
 | |
| descriptors.get_usbid(0xC318).settings = [RegisterFnSwap]
 | |
| descriptors.get_wpid("C714").settings = [RegisterFnSwap]
 | |
| descriptors.get_wpid("100B").settings = [RegisterSmoothScroll, RegisterSideScroll]
 | |
| descriptors.get_wpid("100F").settings = [RegisterSmoothScroll, RegisterSideScroll]
 | |
| descriptors.get_wpid("1013").settings = [RegisterSmoothScroll, RegisterSideScroll]
 | |
| descriptors.get_wpid("1014").settings = [RegisterSmoothScroll, RegisterSideScroll]
 | |
| descriptors.get_wpid("1017").settings = [RegisterSmoothScroll, RegisterSideScroll]
 | |
| descriptors.get_wpid("1023").settings = [RegisterSmoothScroll, RegisterSideScroll]
 | |
| descriptors.get_wpid("4004").settings = [_PerformanceMXDpi, RegisterSmoothScroll, RegisterSideScroll]
 | |
| descriptors.get_wpid("101A").settings = [_PerformanceMXDpi, RegisterSmoothScroll, RegisterSideScroll]
 | |
| descriptors.get_wpid("101B").settings = [RegisterSmoothScroll, RegisterSideScroll]
 | |
| descriptors.get_wpid("101D").settings = [RegisterSmoothScroll, RegisterSideScroll]
 | |
| descriptors.get_wpid("101F").settings = [RegisterSideScroll]
 | |
| descriptors.get_usbid(0xC06B).settings = [RegisterSmoothScroll, RegisterSideScroll]
 | |
| descriptors.get_wpid("1025").settings = [RegisterSideScroll]
 | |
| descriptors.get_wpid("102A").settings = [RegisterSmoothScroll, RegisterSideScroll]
 | |
| descriptors.get_usbid(0xC048).settings = [_PerformanceMXDpi, RegisterSmoothScroll, RegisterSideScroll]
 | |
| descriptors.get_usbid(0xC066).settings = [_PerformanceMXDpi, RegisterSmoothScroll, RegisterSideScroll]
 | |
| 
 | |
| 
 | |
| # ignore the capabilities part of the feature - all devices should be able to swap Fn state
 | |
| # can't just use the first byte = 0xFF (for current host) because of a bug in the firmware of the MX Keys S
 | |
| class K375sFnSwap(FnSwapVirtual):
 | |
|     feature = _F.K375S_FN_INVERSION
 | |
|     validator_options = {"true_value": b"\x01", "false_value": b"\x00", "read_skip_byte_count": 1}
 | |
| 
 | |
|     class rw_class(settings.FeatureRW):
 | |
|         def find_current_host(self, device):
 | |
|             if not self.prefix:
 | |
|                 response = device.feature_request(_F.HOSTS_INFO, 0x00)
 | |
|                 self.prefix = response[3:4] if response else b"\xff"
 | |
| 
 | |
|         def read(self, device, data_bytes=b""):
 | |
|             self.find_current_host(device)
 | |
|             return super().read(device, data_bytes)
 | |
| 
 | |
|         def write(self, device, data_bytes):
 | |
|             self.find_current_host(device)
 | |
|             return super().write(device, data_bytes)
 | |
| 
 | |
| 
 | |
| class FnSwap(FnSwapVirtual):
 | |
|     feature = _F.FN_INVERSION
 | |
| 
 | |
| 
 | |
| class NewFnSwap(FnSwapVirtual):
 | |
|     feature = _F.NEW_FN_INVERSION
 | |
| 
 | |
| 
 | |
| class Backlight(settings.Setting):
 | |
|     name = "backlight-qualitative"
 | |
|     label = _("Backlight Timed")
 | |
|     description = _("Set illumination time for keyboard.")
 | |
|     feature = _F.BACKLIGHT
 | |
|     choices_universe = common.NamedInts(Off=0, Varying=2, VeryShort=5, Short=10, Medium=20, Long=60, VeryLong=180)
 | |
|     validator_class = settings.ChoicesValidator
 | |
|     validator_options = {"choices": choices_universe}
 | |
| 
 | |
| 
 | |
| # MX Keys S requires some extra values, as in 11 02 0c1a 000dff000b000b003c00000000000000
 | |
| # on/off options (from current) effect (FF-no change) level (from current) durations[6] (from current)
 | |
| class Backlight2(settings.Setting):
 | |
|     name = "backlight"
 | |
|     label = _("Backlight")
 | |
|     description = _("Illumination level on keyboard.  Changes made are only applied in Manual mode.")
 | |
|     feature = _F.BACKLIGHT2
 | |
|     choices_universe = common.NamedInts(Disabled=0xFF, Enabled=0x00, Automatic=0x01, Manual=0x02)
 | |
|     min_version = 0
 | |
| 
 | |
|     class rw_class:
 | |
|         def __init__(self, feature):
 | |
|             self.feature = feature
 | |
|             self.kind = settings.FeatureRW.kind
 | |
| 
 | |
|         def read(self, device):
 | |
|             backlight = device.backlight
 | |
|             if not backlight.enabled:
 | |
|                 return b"\xff"
 | |
|             else:
 | |
|                 return common.int2bytes(backlight.mode, 1)
 | |
| 
 | |
|         def write(self, device, data_bytes):
 | |
|             backlight = device.backlight
 | |
|             backlight.enabled = data_bytes[0] != 0xFF
 | |
|             if data_bytes[0] != 0xFF:
 | |
|                 backlight.mode = data_bytes[0]
 | |
|             backlight.write()
 | |
|             return True
 | |
| 
 | |
|     class validator_class(settings.ChoicesValidator):
 | |
|         @classmethod
 | |
|         def build(cls, setting_class, device):
 | |
|             backlight = device.backlight
 | |
|             choices = common.NamedInts()
 | |
|             choices[0xFF] = _("Disabled")
 | |
|             if backlight.auto_supported:
 | |
|                 choices[0x1] = _("Automatic")
 | |
|             if backlight.perm_supported:
 | |
|                 choices[0x3] = _("Manual")
 | |
|             if not (backlight.auto_supported or backlight.temp_supported or backlight.perm_supported):
 | |
|                 choices[0x0] = _("Enabled")
 | |
|             return cls(choices=choices, byte_count=1)
 | |
| 
 | |
| 
 | |
| class Backlight2Level(settings.Setting):
 | |
|     name = "backlight_level"
 | |
|     label = _("Backlight Level")
 | |
|     description = _("Illumination level on keyboard when in Manual mode.")
 | |
|     feature = _F.BACKLIGHT2
 | |
|     min_version = 3
 | |
| 
 | |
|     class rw_class:
 | |
|         def __init__(self, feature):
 | |
|             self.feature = feature
 | |
|             self.kind = settings.FeatureRW.kind
 | |
| 
 | |
|         def read(self, device):
 | |
|             backlight = device.backlight
 | |
|             return common.int2bytes(backlight.level, 1)
 | |
| 
 | |
|         def write(self, device, data_bytes):
 | |
|             if device.backlight.level != common.bytes2int(data_bytes):
 | |
|                 device.backlight.level = common.bytes2int(data_bytes)
 | |
|                 device.backlight.write()
 | |
|             return True
 | |
| 
 | |
|     class validator_class(settings.RangeValidator):
 | |
|         @classmethod
 | |
|         def build(cls, setting_class, device):
 | |
|             reply = device.feature_request(_F.BACKLIGHT2, 0x20)
 | |
|             assert reply, "Oops, backlight range cannot be retrieved!"
 | |
|             if reply[0] > 1:
 | |
|                 return cls(min_value=0, max_value=reply[0] - 1, byte_count=1)
 | |
| 
 | |
| 
 | |
| class Backlight2Duration(settings.Setting):
 | |
|     feature = _F.BACKLIGHT2
 | |
|     min_version = 3
 | |
|     validator_class = settings.RangeValidator
 | |
|     min_value = 1
 | |
|     max_value = 600  # 10 minutes - actual maximum is 2 hours
 | |
|     validator_options = {"byte_count": 2}
 | |
| 
 | |
|     class rw_class:
 | |
|         def __init__(self, feature, field):
 | |
|             self.feature = feature
 | |
|             self.kind = settings.FeatureRW.kind
 | |
|             self.field = field
 | |
| 
 | |
|         def read(self, device):
 | |
|             backlight = device.backlight
 | |
|             return common.int2bytes(getattr(backlight, self.field) * 5, 2)  # use seconds instead of 5-second units
 | |
| 
 | |
|         def write(self, device, data_bytes):
 | |
|             backlight = device.backlight
 | |
|             new_duration = (int.from_bytes(data_bytes, byteorder="big") + 4) // 5  # use ceiling in 5-second units
 | |
|             if new_duration != getattr(backlight, self.field):
 | |
|                 setattr(backlight, self.field, new_duration)
 | |
|                 backlight.write()
 | |
|             return True
 | |
| 
 | |
| 
 | |
| class Backlight2DurationHandsOut(Backlight2Duration):
 | |
|     name = "backlight_duration_hands_out"
 | |
|     label = _("Backlight Delay Hands Out")
 | |
|     description = _("Delay in seconds until backlight fades out with hands away from keyboard.")
 | |
|     feature = _F.BACKLIGHT2
 | |
|     validator_class = settings.RangeValidator
 | |
|     rw_options = {"field": "dho"}
 | |
| 
 | |
| 
 | |
| class Backlight2DurationHandsIn(Backlight2Duration):
 | |
|     name = "backlight_duration_hands_in"
 | |
|     label = _("Backlight Delay Hands In")
 | |
|     description = _("Delay in seconds until backlight fades out with hands near keyboard.")
 | |
|     feature = _F.BACKLIGHT2
 | |
|     validator_class = settings.RangeValidator
 | |
|     rw_options = {"field": "dhi"}
 | |
| 
 | |
| 
 | |
| class Backlight2DurationPowered(Backlight2Duration):
 | |
|     name = "backlight_duration_powered"
 | |
|     label = _("Backlight Delay Powered")
 | |
|     description = _("Delay in seconds until backlight fades out with external power.")
 | |
|     feature = _F.BACKLIGHT2
 | |
|     validator_class = settings.RangeValidator
 | |
|     rw_options = {"field": "dpow"}
 | |
| 
 | |
| 
 | |
| class Backlight3(settings.Setting):
 | |
|     name = "backlight-timed"
 | |
|     label = _("Backlight (Seconds)")
 | |
|     description = _("Set illumination time for keyboard.")
 | |
|     feature = _F.BACKLIGHT3
 | |
|     rw_options = {"read_fnid": 0x10, "write_fnid": 0x20, "suffix": b"\x09"}
 | |
|     validator_class = settings.RangeValidator
 | |
|     min_value = 0
 | |
|     max_value = 1000
 | |
|     validator_options = {"byte_count": 2}
 | |
| 
 | |
| 
 | |
| class HiResScroll(settings.Setting):
 | |
|     name = "hi-res-scroll"
 | |
|     label = _("Scroll Wheel High Resolution")
 | |
|     description = (
 | |
|         _("High-sensitivity mode for vertical scroll with the wheel.")
 | |
|         + "\n"
 | |
|         + _("Set to ignore if scrolling is abnormally fast or slow")
 | |
|     )
 | |
|     feature = _F.HI_RES_SCROLLING
 | |
| 
 | |
| 
 | |
| class LowresMode(settings.Setting):
 | |
|     name = "lowres-scroll-mode"
 | |
|     label = _("Scroll Wheel Diversion")
 | |
|     description = _(
 | |
|         "Make scroll wheel send LOWRES_WHEEL HID++ notifications (which trigger Solaar rules but are otherwise ignored)."
 | |
|     )
 | |
|     feature = _F.LOWRES_WHEEL
 | |
| 
 | |
| 
 | |
| class HiresSmoothInvert(settings.Setting):
 | |
|     name = "hires-smooth-invert"
 | |
|     label = _("Scroll Wheel Direction")
 | |
|     description = _("Invert direction for vertical scroll with wheel.")
 | |
|     feature = _F.HIRES_WHEEL
 | |
|     rw_options = {"read_fnid": 0x10, "write_fnid": 0x20}
 | |
|     validator_options = {"true_value": 0x04, "mask": 0x04}
 | |
| 
 | |
| 
 | |
| class HiresSmoothResolution(settings.Setting):
 | |
|     name = "hires-smooth-resolution"
 | |
|     label = _("Scroll Wheel Resolution")
 | |
|     description = (
 | |
|         _("High-sensitivity mode for vertical scroll with the wheel.")
 | |
|         + "\n"
 | |
|         + _("Set to ignore if scrolling is abnormally fast or slow")
 | |
|     )
 | |
|     feature = _F.HIRES_WHEEL
 | |
|     rw_options = {"read_fnid": 0x10, "write_fnid": 0x20}
 | |
|     validator_options = {"true_value": 0x02, "mask": 0x02}
 | |
| 
 | |
| 
 | |
| class HiresMode(settings.Setting):
 | |
|     name = "hires-scroll-mode"
 | |
|     label = _("Scroll Wheel Diversion")
 | |
|     description = _(
 | |
|         "Make scroll wheel send HIRES_WHEEL HID++ notifications (which trigger Solaar rules but are otherwise ignored)."
 | |
|     )
 | |
|     feature = _F.HIRES_WHEEL
 | |
|     rw_options = {"read_fnid": 0x10, "write_fnid": 0x20}
 | |
|     validator_options = {"true_value": 0x01, "mask": 0x01}
 | |
| 
 | |
| 
 | |
| class PointerSpeed(settings.Setting):
 | |
|     name = "pointer_speed"
 | |
|     label = _("Sensitivity (Pointer Speed)")
 | |
|     description = _("Speed multiplier for mouse (256 is normal multiplier).")
 | |
|     feature = _F.POINTER_SPEED
 | |
|     validator_class = settings.RangeValidator
 | |
|     min_value = 0x002E
 | |
|     max_value = 0x01FF
 | |
|     validator_options = {"byte_count": 2}
 | |
| 
 | |
| 
 | |
| class ThumbMode(settings.Setting):
 | |
|     name = "thumb-scroll-mode"
 | |
|     label = _("Thumb Wheel Diversion")
 | |
|     description = _(
 | |
|         "Make thumb wheel send THUMB_WHEEL HID++ notifications (which trigger Solaar rules but are otherwise ignored)."
 | |
|     )
 | |
|     feature = _F.THUMB_WHEEL
 | |
|     rw_options = {"read_fnid": 0x10, "write_fnid": 0x20}
 | |
|     validator_options = {"true_value": b"\x01\x00", "false_value": b"\x00\x00", "mask": b"\x01\x00"}
 | |
| 
 | |
| 
 | |
| class ThumbInvert(settings.Setting):
 | |
|     name = "thumb-scroll-invert"
 | |
|     label = _("Thumb Wheel Direction")
 | |
|     description = _("Invert thumb wheel scroll direction.")
 | |
|     feature = _F.THUMB_WHEEL
 | |
|     rw_options = {"read_fnid": 0x10, "write_fnid": 0x20}
 | |
|     validator_options = {"true_value": b"\x00\x01", "false_value": b"\x00\x00", "mask": b"\x00\x01"}
 | |
| 
 | |
| 
 | |
| # change UI to show result of onboard profile change
 | |
| def profile_change(device, profile_sector):
 | |
|     if device.setting_callback:
 | |
|         device.setting_callback(device, OnboardProfiles, [profile_sector])
 | |
|         for profile in device.profiles.profiles.values() if device.profiles else []:
 | |
|             if profile.sector == profile_sector:
 | |
|                 resolution_index = profile.resolution_default_index
 | |
|                 device.setting_callback(device, AdjustableDpi, [profile.resolutions[resolution_index]])
 | |
|                 device.setting_callback(device, ReportRate, [profile.report_rate])
 | |
|                 break
 | |
| 
 | |
| 
 | |
| class OnboardProfiles(settings.Setting):
 | |
|     name = "onboard_profiles"
 | |
|     label = _("Onboard Profiles")
 | |
|     description = _("Enable an onboard profile, which controls report rate, sensitivity, and button actions")
 | |
|     feature = _F.ONBOARD_PROFILES
 | |
|     choices_universe = common.NamedInts(Disabled=0)
 | |
|     for i in range(1, 16):
 | |
|         choices_universe[i] = f"Profile {i}"
 | |
|         choices_universe[i + 0x100] = f"Read-Only Profile {i}"
 | |
|     validator_class = settings.ChoicesValidator
 | |
| 
 | |
|     class rw_class:
 | |
|         def __init__(self, feature):
 | |
|             self.feature = feature
 | |
|             self.kind = settings.FeatureRW.kind
 | |
| 
 | |
|         def read(self, device):
 | |
|             enabled = device.feature_request(_F.ONBOARD_PROFILES, 0x20)[0]
 | |
|             if enabled == 0x01:
 | |
|                 active = device.feature_request(_F.ONBOARD_PROFILES, 0x40)
 | |
|                 return active[:2]
 | |
|             else:
 | |
|                 return b"\x00\x00"
 | |
| 
 | |
|         def write(self, device, data_bytes):
 | |
|             if data_bytes == b"\x00\x00":
 | |
|                 result = device.feature_request(_F.ONBOARD_PROFILES, 0x10, b"\x02")
 | |
|             else:
 | |
|                 device.feature_request(_F.ONBOARD_PROFILES, 0x10, b"\x01")
 | |
|                 result = device.feature_request(_F.ONBOARD_PROFILES, 0x30, data_bytes)
 | |
|                 profile_change(device, common.bytes2int(data_bytes))
 | |
|             return result
 | |
| 
 | |
|     class validator_class(settings.ChoicesValidator):
 | |
|         @classmethod
 | |
|         def build(cls, setting_class, device):
 | |
|             headers = hidpp20.OnboardProfiles.get_profile_headers(device)
 | |
|             profiles_list = [setting_class.choices_universe[0]]
 | |
|             if headers:
 | |
|                 for sector, enabled in headers:
 | |
|                     if enabled and setting_class.choices_universe[sector]:
 | |
|                         profiles_list.append(setting_class.choices_universe[sector])
 | |
|             return cls(choices=common.NamedInts.list(profiles_list), byte_count=2) if len(profiles_list) > 1 else None
 | |
| 
 | |
| 
 | |
| class ReportRate(settings.Setting):
 | |
|     name = "report_rate"
 | |
|     label = _("Report Rate")
 | |
|     description = (
 | |
|         _("Frequency of device movement reports") + "\n" + _("May need Onboard Profiles set to Disable to be effective.")
 | |
|     )
 | |
|     feature = _F.REPORT_RATE
 | |
|     rw_options = {"read_fnid": 0x10, "write_fnid": 0x20}
 | |
|     choices_universe = common.NamedInts()
 | |
|     choices_universe[1] = "1ms"
 | |
|     choices_universe[2] = "2ms"
 | |
|     choices_universe[3] = "3ms"
 | |
|     choices_universe[4] = "4ms"
 | |
|     choices_universe[5] = "5ms"
 | |
|     choices_universe[6] = "6ms"
 | |
|     choices_universe[7] = "7ms"
 | |
|     choices_universe[8] = "8ms"
 | |
| 
 | |
|     class validator_class(settings.ChoicesValidator):
 | |
|         @classmethod
 | |
|         def build(cls, setting_class, device):
 | |
|             # if device.wpid == '408E':
 | |
|             #    return None  # host mode borks the function keys on the G915 TKL keyboard
 | |
|             reply = device.feature_request(_F.REPORT_RATE, 0x00)
 | |
|             assert reply, "Oops, report rate choices cannot be retrieved!"
 | |
|             rate_list = []
 | |
|             rate_flags = common.bytes2int(reply[0:1])
 | |
|             for i in range(0, 8):
 | |
|                 if (rate_flags >> i) & 0x01:
 | |
|                     rate_list.append(setting_class.choices_universe[i + 1])
 | |
|             return cls(choices=common.NamedInts.list(rate_list), byte_count=1) if rate_list else None
 | |
| 
 | |
| 
 | |
| class ExtendedReportRate(settings.Setting):
 | |
|     name = "report_rate_extended"
 | |
|     label = _("Report Rate")
 | |
|     description = (
 | |
|         _("Frequency of device movement reports") + "\n" + _("May need Onboard Profiles set to Disable to be effective.")
 | |
|     )
 | |
|     feature = _F.EXTENDED_ADJUSTABLE_REPORT_RATE
 | |
|     rw_options = {"read_fnid": 0x20, "write_fnid": 0x30}
 | |
|     choices_universe = common.NamedInts()
 | |
|     choices_universe[0] = "8ms"
 | |
|     choices_universe[1] = "4ms"
 | |
|     choices_universe[2] = "2ms"
 | |
|     choices_universe[3] = "1ms"
 | |
|     choices_universe[4] = "500us"
 | |
|     choices_universe[5] = "250us"
 | |
|     choices_universe[6] = "125us"
 | |
| 
 | |
|     class validator_class(settings.ChoicesValidator):
 | |
|         @classmethod
 | |
|         def build(cls, setting_class, device):
 | |
|             reply = device.feature_request(_F.EXTENDED_ADJUSTABLE_REPORT_RATE, 0x10)
 | |
|             assert reply, "Oops, report rate choices cannot be retrieved!"
 | |
|             rate_list = []
 | |
|             rate_flags = common.bytes2int(reply[0:2])
 | |
|             for i in range(0, 7):
 | |
|                 if rate_flags & (0x01 << i):
 | |
|                     rate_list.append(setting_class.choices_universe[i])
 | |
|             return cls(choices=common.NamedInts.list(rate_list), byte_count=1) if rate_list else None
 | |
| 
 | |
| 
 | |
| class DivertCrown(settings.Setting):
 | |
|     name = "divert-crown"
 | |
|     label = _("Divert crown events")
 | |
|     description = _("Make crown send CROWN HID++ notifications (which trigger Solaar rules but are otherwise ignored).")
 | |
|     feature = _F.CROWN
 | |
|     rw_options = {"read_fnid": 0x10, "write_fnid": 0x20}
 | |
|     validator_options = {"true_value": 0x02, "false_value": 0x01, "mask": 0xFF}
 | |
| 
 | |
| 
 | |
| class CrownSmooth(settings.Setting):
 | |
|     name = "crown-smooth"
 | |
|     label = _("Crown smooth scroll")
 | |
|     description = _("Set crown smooth scroll")
 | |
|     feature = _F.CROWN
 | |
|     rw_options = {"read_fnid": 0x10, "write_fnid": 0x20}
 | |
|     validator_options = {"true_value": 0x01, "false_value": 0x02, "read_skip_byte_count": 1, "write_prefix_bytes": b"\x00"}
 | |
| 
 | |
| 
 | |
| class DivertGkeys(settings.Setting):
 | |
|     name = "divert-gkeys"
 | |
|     label = _("Divert G and M Keys")
 | |
|     description = _("Make G and M keys send HID++ notifications (which trigger Solaar rules but are otherwise ignored).")
 | |
|     feature = _F.GKEY
 | |
|     validator_options = {"true_value": 0x01, "false_value": 0x00, "mask": 0xFF}
 | |
| 
 | |
|     class rw_class(settings.FeatureRW):
 | |
|         def __init__(self, feature):
 | |
|             super().__init__(feature, write_fnid=0x20)
 | |
| 
 | |
|         def read(self, device):  # no way to read, so just assume not diverted
 | |
|             return b"\x00"
 | |
| 
 | |
| 
 | |
| class ScrollRatchet(settings.Setting):
 | |
|     name = "scroll-ratchet"
 | |
|     label = _("Scroll Wheel Ratcheted")
 | |
|     description = _("Switch the mouse wheel between speed-controlled ratcheting and always freespin.")
 | |
|     feature = _F.SMART_SHIFT
 | |
|     choices_universe = common.NamedInts(**{_("Freespinning"): 1, _("Ratcheted"): 2})
 | |
|     validator_class = settings.ChoicesValidator
 | |
|     validator_options = {"choices": choices_universe}
 | |
| 
 | |
| 
 | |
| class SmartShift(settings.Setting):
 | |
|     name = "smart-shift"
 | |
|     label = _("Scroll Wheel Ratchet Speed")
 | |
|     description = _(
 | |
|         "Use the mouse wheel speed to switch between ratcheted and freespinning.\n"
 | |
|         "The mouse wheel is always ratcheted at 50."
 | |
|     )
 | |
|     feature = _F.SMART_SHIFT
 | |
|     rw_options = {"read_fnid": 0x00, "write_fnid": 0x10}
 | |
| 
 | |
|     class rw_class(settings.FeatureRW):
 | |
|         MIN_VALUE = 1
 | |
|         MAX_VALUE = 50
 | |
| 
 | |
|         def __init__(self, feature, read_fnid, write_fnid):
 | |
|             super().__init__(feature, read_fnid, write_fnid)
 | |
| 
 | |
|         def read(self, device):
 | |
|             value = super().read(device)
 | |
|             if common.bytes2int(value[0:1]) == 1:
 | |
|                 # Mode = Freespin, map to minimum
 | |
|                 return common.int2bytes(self.MIN_VALUE, count=1)
 | |
|             else:
 | |
|                 # Mode = smart shift, map to the value, capped at maximum
 | |
|                 threshold = min(common.bytes2int(value[1:2]), self.MAX_VALUE)
 | |
|                 return common.int2bytes(threshold, count=1)
 | |
| 
 | |
|         def write(self, device, data_bytes):
 | |
|             threshold = common.bytes2int(data_bytes)
 | |
|             # Freespin at minimum
 | |
|             mode = 0  # 1 if threshold <= self.MIN_VALUE else 2
 | |
|             # Ratchet at maximum
 | |
|             if threshold >= self.MAX_VALUE:
 | |
|                 threshold = 255
 | |
|             data = common.int2bytes(mode, count=1) + common.int2bytes(max(0, threshold), count=1)
 | |
|             return super().write(device, data)
 | |
| 
 | |
|     min_value = rw_class.MIN_VALUE
 | |
|     max_value = rw_class.MAX_VALUE
 | |
|     validator_class = settings.RangeValidator
 | |
| 
 | |
| 
 | |
| class SmartShiftEnhanced(SmartShift):
 | |
|     feature = _F.SMART_SHIFT_ENHANCED
 | |
|     rw_options = {"read_fnid": 0x10, "write_fnid": 0x20}
 | |
| 
 | |
| 
 | |
| # 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
 | |
| class ReprogrammableKeys(settings.Settings):
 | |
|     name = "reprogrammable-keys"
 | |
|     label = _("Key/Button Actions")
 | |
|     description = (
 | |
|         _("Change the action for the key or button.")
 | |
|         + "  "
 | |
|         + _("Overridden by diversion.")
 | |
|         + "\n"
 | |
|         + _("Changing important actions (such as for the left mouse button) can result in an unusable system.")
 | |
|     )
 | |
|     feature = _F.REPROG_CONTROLS_V4
 | |
|     keys_universe = special_keys.CONTROL
 | |
|     choices_universe = special_keys.CONTROL
 | |
| 
 | |
|     class rw_class:
 | |
|         def __init__(self, feature):
 | |
|             self.feature = feature
 | |
|             self.kind = settings.FeatureRW.kind
 | |
| 
 | |
|         def read(self, device, key):
 | |
|             key_index = device.keys.index(key)
 | |
|             key_struct = device.keys[key_index]
 | |
|             return b"\x00\x00" + common.int2bytes(int(key_struct.mapped_to), 2)
 | |
| 
 | |
|         def write(self, device, key, data_bytes):
 | |
|             key_index = device.keys.index(key)
 | |
|             key_struct = device.keys[key_index]
 | |
|             key_struct.remap(special_keys.CONTROL[common.bytes2int(data_bytes)])
 | |
|             return True
 | |
| 
 | |
|     class validator_class(settings.ChoicesMapValidator):
 | |
|         @classmethod
 | |
|         def build(cls, setting_class, device):
 | |
|             choices = {}
 | |
|             if device.keys:
 | |
|                 for k in device.keys:
 | |
|                     tgts = k.remappable_to
 | |
|                     if len(tgts) > 1:
 | |
|                         choices[k.key] = tgts
 | |
|             return cls(choices, key_byte_count=2, byte_count=2, extra_default=0) if choices else None
 | |
| 
 | |
| 
 | |
| class DpiSlidingXY(settings.RawXYProcessing):
 | |
|     def __init__(
 | |
|         self,
 | |
|         *args,
 | |
|         show_notification: Callable[[str, str], bool],
 | |
|         **kwargs,
 | |
|     ):
 | |
|         super().__init__(*args, **kwargs)
 | |
|         self.fsmState = None
 | |
|         self._show_notification = show_notification
 | |
| 
 | |
|     def activate_action(self):
 | |
|         self.dpiSetting = next(filter(lambda s: s.name == "dpi" or s.name == "dpi_extended", self.device.settings), None)
 | |
|         self.dpiChoices = list(self.dpiSetting.choices)
 | |
|         self.otherDpiIdx = self.device.persister.get("_dpi-sliding", -1) if self.device.persister else -1
 | |
|         if not isinstance(self.otherDpiIdx, int) or self.otherDpiIdx < 0 or self.otherDpiIdx >= len(self.dpiChoices):
 | |
|             self.otherDpiIdx = self.dpiChoices.index(self.dpiSetting.read())
 | |
|         self.fsmState = State.IDLE
 | |
|         self.dx = 0.0
 | |
|         self.movingDpiIdx = None
 | |
| 
 | |
|     def setNewDpi(self, newDpiIdx):
 | |
|         newDpi = self.dpiChoices[newDpiIdx]
 | |
|         self.dpiSetting.write(newDpi)
 | |
|         if self.device.setting_callback:
 | |
|             self.device.setting_callback(self.device, type(self.dpiSetting), [newDpi])
 | |
| 
 | |
|     def displayNewDpi(self, newDpiIdx):
 | |
|         selected_dpi = self.dpiChoices[newDpiIdx]
 | |
|         min_dpi = self.dpiChoices[0]
 | |
|         max_dpi = self.dpiChoices[-1]
 | |
|         reason = f"DPI {selected_dpi} [min {min_dpi}, max {max_dpi}]"
 | |
|         self._show_notification(self.device, reason)
 | |
| 
 | |
|     def press_action(self, key):  # start tracking
 | |
|         self.starting = True
 | |
|         if self.fsmState == State.IDLE:
 | |
|             self.fsmState = State.PRESSED
 | |
|             self.dx = 0.0
 | |
|             # While in 'moved' state, the index into 'dpiChoices' of the currently selected DPI setting
 | |
|             self.movingDpiIdx = None
 | |
| 
 | |
|     def release_action(self):  # adjust DPI and stop tracking
 | |
|         if self.fsmState == State.PRESSED:  # Swap with other DPI
 | |
|             thisIdx = self.dpiChoices.index(self.dpiSetting.read())
 | |
|             newDpiIdx, self.otherDpiIdx = self.otherDpiIdx, thisIdx
 | |
|             if self.device.persister:
 | |
|                 self.device.persister["_dpi-sliding"] = self.otherDpiIdx
 | |
|             self.setNewDpi(newDpiIdx)
 | |
|             self.displayNewDpi(newDpiIdx)
 | |
|         elif self.fsmState == State.MOVED:  # Set DPI according to displacement
 | |
|             self.setNewDpi(self.movingDpiIdx)
 | |
|         self.fsmState = State.IDLE
 | |
| 
 | |
|     def move_action(self, dx, dy):
 | |
|         if self.device.features.get_feature_version(_F.REPROG_CONTROLS_V4) >= 5 and self.starting:
 | |
|             self.starting = False  # hack to ignore strange first movement report from MX Master 3S
 | |
|             return
 | |
|         currDpi = self.dpiSetting.read()
 | |
|         self.dx += float(dx) / float(currDpi) * 15.0  # yields a more-or-less DPI-independent dx of about 5/cm
 | |
|         if self.fsmState == State.PRESSED:
 | |
|             if abs(self.dx) >= 1.0:
 | |
|                 self.fsmState = State.MOVED
 | |
|                 self.movingDpiIdx = self.dpiChoices.index(currDpi)
 | |
|         elif self.fsmState == State.MOVED:
 | |
|             currIdx = self.dpiChoices.index(self.dpiSetting.read())
 | |
|             newMovingDpiIdx = min(max(currIdx + int(self.dx), 0), len(self.dpiChoices) - 1)
 | |
|             if newMovingDpiIdx != self.movingDpiIdx:
 | |
|                 self.movingDpiIdx = newMovingDpiIdx
 | |
|                 self.displayNewDpi(newMovingDpiIdx)
 | |
| 
 | |
| 
 | |
| class MouseGesturesXY(settings.RawXYProcessing):
 | |
|     def activate_action(self):
 | |
|         self.dpiSetting = next(filter(lambda s: s.name == "dpi" or s.name == "dpi_extended", self.device.settings), None)
 | |
|         self.fsmState = State.IDLE
 | |
|         self.initialize_data()
 | |
| 
 | |
|     def initialize_data(self):
 | |
|         self.dx = 0.0
 | |
|         self.dy = 0.0
 | |
|         self.lastEv = None
 | |
|         self.data = []
 | |
| 
 | |
|     def press_action(self, key):
 | |
|         self.starting = True
 | |
|         if self.fsmState == State.IDLE:
 | |
|             self.fsmState = State.PRESSED
 | |
|             self.initialize_data()
 | |
|             self.data = [key.key]
 | |
| 
 | |
|     def release_action(self):
 | |
|         if self.fsmState == State.PRESSED:
 | |
|             # emit mouse gesture notification
 | |
|             self.push_mouse_event()
 | |
|             if logger.isEnabledFor(logging.INFO):
 | |
|                 logger.info("mouse gesture notification %s", self.data)
 | |
|             payload = struct.pack("!" + (len(self.data) * "h"), *self.data)
 | |
|             notification = base.HIDPPNotification(0, 0, 0, 0, payload)
 | |
|             diversion.process_notification(self.device, notification, _F.MOUSE_GESTURE)
 | |
|             self.fsmState = State.IDLE
 | |
| 
 | |
|     def move_action(self, dx, dy):
 | |
|         if self.fsmState == State.PRESSED:
 | |
|             now = time() * 1000  # time_ns() / 1e6
 | |
|             if self.device.features.get_feature_version(_F.REPROG_CONTROLS_V4) >= 5 and self.starting:
 | |
|                 self.starting = False  # hack to ignore strange first movement report from MX Master 3S
 | |
|                 return
 | |
|             if self.lastEv is not None and now - self.lastEv > 200.0:
 | |
|                 self.push_mouse_event()
 | |
|             dpi = self.dpiSetting.read() if self.dpiSetting else 1000
 | |
|             dx = float(dx) / float(dpi) * 15.0  # This multiplier yields a more-or-less DPI-independent dx of about 5/cm
 | |
|             self.dx += dx
 | |
|             dy = float(dy) / float(dpi) * 15.0  # This multiplier yields a more-or-less DPI-independent dx of about 5/cm
 | |
|             self.dy += dy
 | |
|             self.lastEv = now
 | |
| 
 | |
|     def key_action(self, key):
 | |
|         self.push_mouse_event()
 | |
|         self.data.append(1)
 | |
|         self.data.append(key)
 | |
|         self.lastEv = time() * 1000  # time_ns() / 1e6
 | |
|         if logger.isEnabledFor(logging.DEBUG):
 | |
|             logger.debug("mouse gesture key event %d %s", key, self.data)
 | |
| 
 | |
|     def push_mouse_event(self):
 | |
|         x = int(self.dx)
 | |
|         y = int(self.dy)
 | |
|         if x == 0 and y == 0:
 | |
|             return
 | |
|         self.data.append(0)
 | |
|         self.data.append(x)
 | |
|         self.data.append(y)
 | |
|         self.dx = 0.0
 | |
|         self.dy = 0.0
 | |
|         if logger.isEnabledFor(logging.DEBUG):
 | |
|             logger.debug("mouse gesture move event %d %d %s", x, y, self.data)
 | |
| 
 | |
| 
 | |
| class DivertKeys(settings.Settings):
 | |
|     name = "divert-keys"
 | |
|     label = _("Key/Button Diversion")
 | |
|     description = _("Make the key or button send HID++ notifications (Diverted) or initiate Mouse Gestures or Sliding DPI")
 | |
|     feature = _F.REPROG_CONTROLS_V4
 | |
|     keys_universe = special_keys.CONTROL
 | |
|     choices_universe = common.NamedInts(**{_("Regular"): 0, _("Diverted"): 1, _("Mouse Gestures"): 2, _("Sliding DPI"): 3})
 | |
|     choices_gesture = common.NamedInts(**{_("Regular"): 0, _("Diverted"): 1, _("Mouse Gestures"): 2})
 | |
|     choices_divert = common.NamedInts(**{_("Regular"): 0, _("Diverted"): 1})
 | |
| 
 | |
|     class rw_class:
 | |
|         def __init__(self, feature):
 | |
|             self.feature = feature
 | |
|             self.kind = settings.FeatureRW.kind
 | |
| 
 | |
|         def read(self, device, key):
 | |
|             key_index = device.keys.index(key)
 | |
|             key_struct = device.keys[key_index]
 | |
|             return b"\x00\x00\x01" if "diverted" in key_struct.mapping_flags else b"\x00\x00\x00"
 | |
| 
 | |
|         def write(self, device, key, data_bytes):
 | |
|             key_index = device.keys.index(key)
 | |
|             key_struct = device.keys[key_index]
 | |
|             key_struct.set_diverted(common.bytes2int(data_bytes) != 0)  # not regular
 | |
|             return True
 | |
| 
 | |
|     class validator_class(settings.ChoicesMapValidator):
 | |
|         def __init__(self, choices, key_byte_count=2, byte_count=1, mask=0x01):
 | |
|             super().__init__(choices, key_byte_count, byte_count, mask)
 | |
| 
 | |
|         def prepare_write(self, key, new_value):
 | |
|             if self.gestures and new_value != 2:  # mouse gestures
 | |
|                 self.gestures.stop(key)
 | |
|             if self.sliding and new_value != 3:  # sliding DPI
 | |
|                 self.sliding.stop(key)
 | |
|             if self.gestures and new_value == 2:  # mouse gestures
 | |
|                 self.gestures.start(key)
 | |
|             if self.sliding and new_value == 3:  # sliding DPI
 | |
|                 self.sliding.start(key)
 | |
|             return super().prepare_write(key, new_value)
 | |
| 
 | |
|         @classmethod
 | |
|         def build(cls, setting_class, device):
 | |
|             sliding = gestures = None
 | |
|             choices = {}
 | |
|             if device.keys:
 | |
|                 for k in device.keys:
 | |
|                     if "divertable" in k.flags and "virtual" not in k.flags:
 | |
|                         if "raw XY" in k.flags:
 | |
|                             choices[k.key] = setting_class.choices_gesture
 | |
|                             if gestures is None:
 | |
|                                 gestures = MouseGesturesXY(device, name="MouseGestures")
 | |
|                             if _F.ADJUSTABLE_DPI in device.features:
 | |
|                                 choices[k.key] = setting_class.choices_universe
 | |
|                                 if sliding is None:
 | |
|                                     sliding = DpiSlidingXY(
 | |
|                                         device, name="DpiSliding", show_notification=desktop_notifications.show
 | |
|                                     )
 | |
|                         else:
 | |
|                             choices[k.key] = setting_class.choices_divert
 | |
|             if not choices:
 | |
|                 return None
 | |
|             validator = cls(choices, key_byte_count=2, byte_count=1, mask=0x01)
 | |
|             validator.sliding = sliding
 | |
|             validator.gestures = gestures
 | |
|             return validator
 | |
| 
 | |
| 
 | |
| def produce_dpi_list(feature, function, ignore, device, direction):
 | |
|     dpi_bytes = b""
 | |
|     for i in range(0, 0x100):  # there will be only a very few iterations performed
 | |
|         reply = device.feature_request(feature, function, 0x00, direction, i)
 | |
|         assert reply, "Oops, DPI list cannot be retrieved!"
 | |
|         dpi_bytes += reply[ignore:]
 | |
|         if dpi_bytes[-2:] == b"\x00\x00":
 | |
|             break
 | |
|     dpi_list = []
 | |
|     i = 0
 | |
|     while i < len(dpi_bytes):
 | |
|         val = common.bytes2int(dpi_bytes[i : i + 2])
 | |
|         if val == 0:
 | |
|             break
 | |
|         if val >> 13 == 0b111:
 | |
|             step = val & 0x1FFF
 | |
|             last = common.bytes2int(dpi_bytes[i + 2 : i + 4])
 | |
|             assert len(dpi_list) > 0 and last > dpi_list[-1], f"Invalid DPI list item: {val!r}"
 | |
|             dpi_list += range(dpi_list[-1] + step, last + 1, step)
 | |
|             i += 4
 | |
|         else:
 | |
|             dpi_list.append(val)
 | |
|             i += 2
 | |
|     return dpi_list
 | |
| 
 | |
| 
 | |
| class AdjustableDpi(settings.Setting):
 | |
|     name = "dpi"
 | |
|     label = _("Sensitivity (DPI)")
 | |
|     description = _("Mouse movement sensitivity")
 | |
|     feature = _F.ADJUSTABLE_DPI
 | |
|     rw_options = {"read_fnid": 0x20, "write_fnid": 0x30}
 | |
|     choices_universe = common.NamedInts.range(100, 4000, str, 50)
 | |
| 
 | |
|     class validator_class(settings.ChoicesValidator):
 | |
|         @classmethod
 | |
|         def build(cls, setting_class, device):
 | |
|             dpilist = produce_dpi_list(setting_class.feature, 0x10, 1, device, 0)
 | |
|             setting = (
 | |
|                 cls(choices=common.NamedInts.list(dpilist), byte_count=2, write_prefix_bytes=b"\x00") if dpilist else None
 | |
|             )
 | |
|             setting.dpilist = dpilist
 | |
|             return setting
 | |
| 
 | |
|         def validate_read(self, reply_bytes):  # special validator to use default DPI if needed
 | |
|             reply_value = common.bytes2int(reply_bytes[1:3])
 | |
|             if reply_value == 0:  # use default value instead
 | |
|                 reply_value = common.bytes2int(reply_bytes[3:5])
 | |
|             valid_value = self.choices[reply_value]
 | |
|             assert valid_value is not None, f"{self.__class__.__name__}: failed to validate read value {reply_value:02X}"
 | |
|             return valid_value
 | |
| 
 | |
| 
 | |
| class ExtendedAdjustableDpi(settings.Setting):
 | |
|     # the extended version allows for two dimensions, longer dpi descriptions, but still assume only one sensor
 | |
|     name = "dpi_extended"
 | |
|     label = _("Sensitivity (DPI)")
 | |
|     description = _("Mouse movement sensitivity") + "\n" + _("May need Onboard Profiles set to Disable to be effective.")
 | |
|     feature = _F.EXTENDED_ADJUSTABLE_DPI
 | |
|     rw_options = {"read_fnid": 0x50, "write_fnid": 0x60}
 | |
|     keys_universe = common.NamedInts(X=0, Y=1, LOD=2)
 | |
|     choices_universe = common.NamedInts.range(100, 4000, str, 50)
 | |
|     choices_universe[0] = "LOW"
 | |
|     choices_universe[1] = "MEDIUM"
 | |
|     choices_universe[2] = "HIGH"
 | |
|     keys = common.NamedInts(X=0, Y=1, LOD=2)
 | |
| 
 | |
|     def write_key_value(self, key, value, save=True):
 | |
|         if isinstance(self._value, dict):
 | |
|             self._value[key] = value
 | |
|         else:
 | |
|             self._value = {key: value}
 | |
|         result = self.write(self._value, save)
 | |
|         return result[key] if isinstance(result, dict) else result
 | |
| 
 | |
|     class validator_class(settings.ChoicesMapValidator):
 | |
|         @classmethod
 | |
|         def build(cls, setting_class, device):
 | |
|             reply = device.feature_request(setting_class.feature, 0x10, 0x00)
 | |
|             y = bool(reply[2] & 0x01)
 | |
|             lod = bool(reply[2] & 0x02)
 | |
|             choices_map = {}
 | |
|             dpilist_x = produce_dpi_list(setting_class.feature, 0x20, 3, device, 0)
 | |
|             choices_map[setting_class.keys["X"]] = common.NamedInts.list(dpilist_x)
 | |
|             if y:
 | |
|                 dpilist_y = produce_dpi_list(setting_class.feature, 0x20, 3, device, 1)
 | |
|                 choices_map[setting_class.keys["Y"]] = common.NamedInts.list(dpilist_y)
 | |
|             if lod:
 | |
|                 choices_map[setting_class.keys["LOD"]] = common.NamedInts(LOW=0, MEDIUM=1, HIGH=2)
 | |
|             validator = cls(choices_map=choices_map, byte_count=2, write_prefix_bytes=b"\x00")
 | |
|             validator.y = y
 | |
|             validator.lod = lod
 | |
|             validator.keys = setting_class.keys
 | |
|             return validator
 | |
| 
 | |
|         def validate_read(self, reply_bytes):  # special validator to read entire setting
 | |
|             dpi_x = common.bytes2int(reply_bytes[3:5]) if reply_bytes[1:3] == 0 else common.bytes2int(reply_bytes[1:3])
 | |
|             assert dpi_x in self.choices[0], f"{self.__class__.__name__}: failed to validate dpi_x value {dpi_x:04X}"
 | |
|             value = {self.keys["X"]: dpi_x}
 | |
|             if self.y:
 | |
|                 dpi_y = common.bytes2int(reply_bytes[7:9]) if reply_bytes[5:7] == 0 else common.bytes2int(reply_bytes[5:7])
 | |
|                 assert dpi_y in self.choices[1], f"{self.__class__.__name__}: failed to validate dpi_y value {dpi_y:04X}"
 | |
|                 value[self.keys["Y"]] = dpi_y
 | |
|             if self.lod:
 | |
|                 lod = reply_bytes[9]
 | |
|                 assert lod in self.choices[2], f"{self.__class__.__name__}: failed to validate lod value {lod:02X}"
 | |
|                 value[self.keys["LOD"]] = lod
 | |
|             return value
 | |
| 
 | |
|         def prepare_write(self, new_value, current_value=None):  # special preparer to write entire setting
 | |
|             data_bytes = self._write_prefix_bytes
 | |
|             if new_value[self.keys["X"]] not in self.choices[self.keys["X"]]:
 | |
|                 raise ValueError(f"invalid value {new_value!r}")
 | |
|             data_bytes += common.int2bytes(new_value[0], 2)
 | |
|             if self.y:
 | |
|                 if new_value[self.keys["Y"]] not in self.choices[self.keys["Y"]]:
 | |
|                     raise ValueError(f"invalid value {new_value!r}")
 | |
|                 data_bytes += common.int2bytes(new_value[self.keys["Y"]], 2)
 | |
|             else:
 | |
|                 data_bytes += b"\x00\x00"
 | |
|             if self.lod:
 | |
|                 if new_value[self.keys["LOD"]] not in self.choices[self.keys["LOD"]]:
 | |
|                     raise ValueError(f"invalid value {new_value!r}")
 | |
|                 data_bytes += common.int2bytes(new_value[self.keys["LOD"]], 1)
 | |
|             else:
 | |
|                 data_bytes += b"\x00"
 | |
|             return data_bytes
 | |
| 
 | |
| 
 | |
| class SpeedChange(settings.Setting):
 | |
|     """Implements the ability to switch Sensitivity by clicking on the DPI_Change button."""
 | |
| 
 | |
|     name = "speed-change"
 | |
|     label = _("Sensitivity Switching")
 | |
|     description = _(
 | |
|         "Switch the current sensitivity and the remembered sensitivity when the key or button is pressed.\n"
 | |
|         "If there is no remembered sensitivity, just remember the current sensitivity"
 | |
|     )
 | |
|     choices_universe = special_keys.CONTROL
 | |
|     choices_extra = common.NamedInt(0, _("Off"))
 | |
|     feature = _F.POINTER_SPEED
 | |
|     rw_options = {"name": "speed change"}
 | |
| 
 | |
|     class rw_class(settings.ActionSettingRW):
 | |
|         def press_action(self):  # switch sensitivity
 | |
|             currentSpeed = self.device.persister.get("pointer_speed", None) if self.device.persister else None
 | |
|             newSpeed = self.device.persister.get("_speed-change", None) if self.device.persister else None
 | |
|             speed_setting = next(filter(lambda s: s.name == "pointer_speed", self.device.settings), None)
 | |
|             if newSpeed is not None:
 | |
|                 if speed_setting:
 | |
|                     speed_setting.write(newSpeed)
 | |
|                     if self.device.setting_callback:
 | |
|                         self.device.setting_callback(self.device, type(speed_setting), [newSpeed])
 | |
|                 else:
 | |
|                     logger.error("cannot save sensitivity setting on %s", self.device)
 | |
|             if self.device.persister:
 | |
|                 self.device.persister["_speed-change"] = currentSpeed
 | |
| 
 | |
|     class validator_class(settings.ChoicesValidator):
 | |
|         @classmethod
 | |
|         def build(cls, setting_class, device):
 | |
|             key_index = device.keys.index(special_keys.CONTROL.DPI_Change)
 | |
|             key = device.keys[key_index] if key_index is not None else None
 | |
|             if key is not None and "divertable" in key.flags:
 | |
|                 keys = [setting_class.choices_extra, key.key]
 | |
|                 return cls(choices=common.NamedInts.list(keys), byte_count=2)
 | |
| 
 | |
| 
 | |
| class DisableKeyboardKeys(settings.BitFieldSetting):
 | |
|     name = "disable-keyboard-keys"
 | |
|     label = _("Disable keys")
 | |
|     description = _("Disable specific keyboard keys.")
 | |
|     feature = _F.KEYBOARD_DISABLE_KEYS
 | |
|     rw_options = {"read_fnid": 0x10, "write_fnid": 0x20}
 | |
|     _labels = {k: (None, _("Disables the %s key.") % k) for k in special_keys.DISABLE}
 | |
|     choices_universe = special_keys.DISABLE
 | |
| 
 | |
|     class validator_class(settings.BitFieldValidator):
 | |
|         @classmethod
 | |
|         def build(cls, setting_class, device):
 | |
|             mask = device.feature_request(_F.KEYBOARD_DISABLE_KEYS, 0x00)[0]
 | |
|             options = [special_keys.DISABLE[1 << i] for i in range(8) if mask & (1 << i)]
 | |
|             return cls(options) if options else None
 | |
| 
 | |
| 
 | |
| class Multiplatform(settings.Setting):
 | |
|     name = "multiplatform"
 | |
|     label = _("Set OS")
 | |
|     description = _("Change keys to match OS.")
 | |
|     feature = _F.MULTIPLATFORM
 | |
|     rw_options = {"read_fnid": 0x00, "write_fnid": 0x30}
 | |
|     choices_universe = common.NamedInts(**{"OS " + str(i + 1): i for i in range(8)})
 | |
| 
 | |
|     # multiplatform OS bits
 | |
|     OSS = [
 | |
|         ("Linux", 0x0400),
 | |
|         ("MacOS", 0x2000),
 | |
|         ("Windows", 0x0100),
 | |
|         ("iOS", 0x4000),
 | |
|         ("Android", 0x1000),
 | |
|         ("WebOS", 0x8000),
 | |
|         ("Chrome", 0x0800),
 | |
|         ("WinEmb", 0x0200),
 | |
|         ("Tizen", 0x0001),
 | |
|     ]
 | |
| 
 | |
|     # the problem here is how to construct the right values for the rules Set GUI,
 | |
|     # as, for example, the integer value for 'Windows' can be different on different devices
 | |
| 
 | |
|     class validator_class(settings.ChoicesValidator):
 | |
|         @classmethod
 | |
|         def build(cls, setting_class, 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 = struct.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 = struct.unpack("!BBHHH", descriptor[:8])
 | |
|                 descriptors.append((platform, os_flags, low, high))
 | |
|             choices = common.NamedInts()
 | |
|             for os_name, os_bit in setting_class.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 cls(choices=choices, read_skip_byte_count=6, write_prefix_bytes=b"\xff") if choices else None
 | |
| 
 | |
| 
 | |
| class DualPlatform(settings.Setting):
 | |
|     name = "multiplatform"
 | |
|     label = _("Set OS")
 | |
|     description = _("Change keys to match OS.")
 | |
|     choices_universe = common.NamedInts()
 | |
|     choices_universe[0x00] = "iOS, MacOS"
 | |
|     choices_universe[0x01] = "Android, Windows"
 | |
|     feature = _F.DUALPLATFORM
 | |
|     rw_options = {"read_fnid": 0x00, "write_fnid": 0x20}
 | |
|     validator_class = settings.ChoicesValidator
 | |
|     validator_options = {"choices": choices_universe}
 | |
| 
 | |
| 
 | |
| class ChangeHost(settings.Setting):
 | |
|     name = "change-host"
 | |
|     label = _("Change Host")
 | |
|     description = _("Switch connection to a different host")
 | |
|     persist = False  # persisting this setting is harmful
 | |
|     feature = _F.CHANGE_HOST
 | |
|     rw_options = {"read_fnid": 0x00, "write_fnid": 0x10, "no_reply": True}
 | |
|     choices_universe = common.NamedInts(**{"Host " + str(i + 1): i for i in range(3)})
 | |
| 
 | |
|     class validator_class(settings.ChoicesValidator):
 | |
|         @classmethod
 | |
|         def build(cls, setting_class, device):
 | |
|             infos = device.feature_request(_F.CHANGE_HOST)
 | |
|             assert infos, "Oops, host count cannot be retrieved!"
 | |
|             numHosts, currentHost = struct.unpack("!BB", infos[:2])
 | |
|             hostNames = _hidpp20.get_host_names(device)
 | |
|             hostNames = hostNames if hostNames is not None else {}
 | |
|             if currentHost not in hostNames or hostNames[currentHost][1] == "":
 | |
|                 hostNames[currentHost] = (True, socket.gethostname().partition(".")[0])
 | |
|             choices = common.NamedInts()
 | |
|             for host in range(0, numHosts):
 | |
|                 paired, hostName = hostNames.get(host, (True, ""))
 | |
|                 choices[host] = str(host + 1) + ":" + hostName if hostName else str(host + 1)
 | |
|             return cls(choices=choices, read_skip_byte_count=1) if choices and len(choices) > 1 else None
 | |
| 
 | |
| 
 | |
| _GESTURE2_GESTURES_LABELS = {
 | |
|     GestureId.TAP_1_FINGER: (_("Single tap"), _("Performs a left click.")),
 | |
|     GestureId.TAP_2_FINGER: (_("Single tap with two fingers"), _("Performs a right click.")),
 | |
|     GestureId.TAP_3_FINGER: (_("Single tap with three fingers"), None),
 | |
|     GestureId.CLICK_1_FINGER: (None, None),
 | |
|     GestureId.CLICK_2_FINGER: (None, None),
 | |
|     GestureId.CLICK_3_FINGER: (None, None),
 | |
|     GestureId.DOUBLE_TAP_1_FINGER: (_("Double tap"), _("Performs a double click.")),
 | |
|     GestureId.DOUBLE_TAP_2_FINGER: (_("Double tap with two fingers"), None),
 | |
|     GestureId.DOUBLE_TAP_3_FINGER: (_("Double tap with three fingers"), None),
 | |
|     GestureId.TRACK_1_FINGER: (None, None),
 | |
|     GestureId.TRACKING_ACCELERATION: (None, None),
 | |
|     GestureId.TAP_DRAG_1_FINGER: (_("Tap and drag"), _("Drags items by dragging the finger after double tapping.")),
 | |
|     GestureId.TAP_DRAG_2_FINGER: (
 | |
|         _("Tap and drag with two fingers"),
 | |
|         _("Drags items by dragging the fingers after double tapping."),
 | |
|     ),
 | |
|     GestureId.DRAG_3_FINGER: (_("Tap and drag with three fingers"), None),
 | |
|     GestureId.TAP_GESTURES: (None, None),
 | |
|     GestureId.FN_CLICK_GESTURE_SUPPRESSION: (
 | |
|         _("Suppress tap and edge gestures"),
 | |
|         _("Disables tap and edge gestures (equivalent to pressing Fn+LeftClick)."),
 | |
|     ),
 | |
|     GestureId.SCROLL_1_FINGER: (_("Scroll with one finger"), _("Scrolls.")),
 | |
|     GestureId.SCROLL_2_FINGER: (_("Scroll with two fingers"), _("Scrolls.")),
 | |
|     GestureId.SCROLL_2_FINGER_HORIZONTAL: (_("Scroll horizontally with two fingers"), _("Scrolls horizontally.")),
 | |
|     GestureId.SCROLL_2_FINGER_VERTICAL: (_("Scroll vertically with two fingers"), _("Scrolls vertically.")),
 | |
|     GestureId.SCROLL_2_FINGER_STATELESS: (_("Scroll with two fingers"), _("Scrolls.")),
 | |
|     GestureId.NATURAL_SCROLLING: (_("Natural scrolling"), _("Inverts the scrolling direction.")),
 | |
|     GestureId.THUMBWHEEL: (_("Thumbwheel"), _("Enables the thumbwheel.")),
 | |
|     GestureId.V_SCROLL_INTERTIA: (None, None),
 | |
|     GestureId.V_SCROLL_BALLISTICS: (None, None),
 | |
|     GestureId.SWIPE_2_FINGER_HORIZONTAL: (None, None),
 | |
|     GestureId.SWIPE_3_FINGER_HORIZONTAL: (None, None),
 | |
|     GestureId.SWIPE_4_FINGER_HORIZONTAL: (None, None),
 | |
|     GestureId.SWIPE_3_FINGER_VERTICAL: (None, None),
 | |
|     GestureId.SWIPE_4_FINGER_VERTICAL: (None, None),
 | |
|     GestureId.LEFT_EDGE_SWIPE_1_FINGER: (None, None),
 | |
|     GestureId.RIGHT_EDGE_SWIPE_1_FINGER: (None, None),
 | |
|     GestureId.BOTTOM_EDGE_SWIPE_1_FINGER: (None, None),
 | |
|     GestureId.TOP_EDGE_SWIPE_1_FINGER: (_("Swipe from the top edge"), None),
 | |
|     GestureId.LEFT_EDGE_SWIPE_1_FINGER_2: (_("Swipe from the left edge"), None),
 | |
|     GestureId.RIGHT_EDGE_SWIPE_1_FINGER_2: (_("Swipe from the right edge"), None),
 | |
|     GestureId.BOTTOM_EDGE_SWIPE_1_FINGER_2: (_("Swipe from the bottom edge"), None),
 | |
|     GestureId.TOP_EDGE_SWIPE_1_FINGER_2: (_("Swipe from the top edge"), None),
 | |
|     GestureId.LEFT_EDGE_SWIPE_2_FINGER: (_("Swipe two fingers from the left edge"), None),
 | |
|     GestureId.RIGHT_EDGE_SWIPE_2_FINGER: (_("Swipe two fingers from the right edge"), None),
 | |
|     GestureId.BOTTOM_EDGE_SWIPE_2_FINGER: (_("Swipe two fingers from the bottom edge"), None),
 | |
|     GestureId.TOP_EDGE_SWIPE_2_FINGER: (_("Swipe two fingers from the top edge"), None),
 | |
|     GestureId.ZOOM_2_FINGER: (_("Zoom with two fingers."), _("Pinch to zoom out; spread to zoom in.")),
 | |
|     GestureId.ZOOM_2_FINGER_PINCH: (_("Pinch to zoom out."), _("Pinch to zoom out.")),
 | |
|     GestureId.ZOOM_2_FINGER_SPREAD: (_("Spread to zoom in."), _("Spread to zoom in.")),
 | |
|     GestureId.ZOOM_3_FINGER: (_("Zoom with three fingers."), None),
 | |
|     GestureId.ZOOM_2_FINGER_STATELESS: (_("Zoom with two fingers"), _("Pinch to zoom out; spread to zoom in.")),
 | |
|     GestureId.TWO_FINGERS_PRESENT: (None, None),
 | |
|     GestureId.ROTATE_2_FINGER: (None, None),
 | |
|     GestureId.FINGER_1: (None, None),
 | |
|     GestureId.FINGER_2: (None, None),
 | |
|     GestureId.FINGER_3: (None, None),
 | |
|     GestureId.FINGER_4: (None, None),
 | |
|     GestureId.FINGER_5: (None, None),
 | |
|     GestureId.FINGER_6: (None, None),
 | |
|     GestureId.FINGER_7: (None, None),
 | |
|     GestureId.FINGER_8: (None, None),
 | |
|     GestureId.FINGER_9: (None, None),
 | |
|     GestureId.FINGER_10: (None, None),
 | |
|     GestureId.DEVICE_SPECIFIC_RAW_DATA: (None, None),
 | |
| }
 | |
| 
 | |
| _GESTURE2_PARAMS_LABELS = {
 | |
|     ParamId.EXTRA_CAPABILITIES: (None, None),  # not supported
 | |
|     ParamId.PIXEL_ZONE: (_("Pixel zone"), None),  # TO DO: replace None with a short description
 | |
|     ParamId.RATIO_ZONE: (_("Ratio zone"), None),  # TO DO: replace None with a short description
 | |
|     ParamId.SCALE_FACTOR: (_("Scale factor"), _("Sets the cursor speed.")),
 | |
| }
 | |
| 
 | |
| _GESTURE2_PARAMS_LABELS_SUB = {
 | |
|     "left": (_("Left"), _("Left-most coordinate.")),
 | |
|     "bottom": (_("Bottom"), _("Bottom coordinate.")),
 | |
|     "width": (_("Width"), _("Width.")),
 | |
|     "height": (_("Height"), _("Height.")),
 | |
|     "scale": (_("Scale"), _("Cursor speed.")),
 | |
| }
 | |
| 
 | |
| 
 | |
| class Gesture2Gestures(settings.BitFieldWithOffsetAndMaskSetting):
 | |
|     name = "gesture2-gestures"
 | |
|     label = _("Gestures")
 | |
|     description = _("Tweak the mouse/touchpad behaviour.")
 | |
|     feature = _F.GESTURE_2
 | |
|     rw_options = {"read_fnid": 0x10, "write_fnid": 0x20}
 | |
|     validator_options = {"om_method": hidpp20.Gesture.enable_offset_mask}
 | |
|     choices_universe = hidpp20_constants.GestureId
 | |
|     _labels = _GESTURE2_GESTURES_LABELS
 | |
| 
 | |
|     class validator_class(settings.BitFieldWithOffsetAndMaskValidator):
 | |
|         @classmethod
 | |
|         def build(cls, setting_class, device, om_method=None):
 | |
|             options = [g for g in device.gestures.gestures.values() if g.can_be_enabled or g.default_enabled]
 | |
|             return cls(options, om_method=om_method) if options else None
 | |
| 
 | |
| 
 | |
| class Gesture2Divert(settings.BitFieldWithOffsetAndMaskSetting):
 | |
|     name = "gesture2-divert"
 | |
|     label = _("Gestures Diversion")
 | |
|     description = _("Divert mouse/touchpad gestures.")
 | |
|     feature = _F.GESTURE_2
 | |
|     rw_options = {"read_fnid": 0x30, "write_fnid": 0x40}
 | |
|     validator_options = {"om_method": hidpp20.Gesture.diversion_offset_mask}
 | |
|     choices_universe = hidpp20_constants.GestureId
 | |
|     _labels = _GESTURE2_GESTURES_LABELS
 | |
| 
 | |
|     class validator_class(settings.BitFieldWithOffsetAndMaskValidator):
 | |
|         @classmethod
 | |
|         def build(cls, setting_class, device, om_method=None):
 | |
|             options = [g for g in device.gestures.gestures.values() if g.can_be_diverted]
 | |
|             return cls(options, om_method=om_method) if options else None
 | |
| 
 | |
| 
 | |
| class Gesture2Params(settings.LongSettings):
 | |
|     name = "gesture2-params"
 | |
|     label = _("Gesture params")
 | |
|     description = _("Change numerical parameters of a mouse/touchpad.")
 | |
|     feature = _F.GESTURE_2
 | |
|     rw_options = {"read_fnid": 0x70, "write_fnid": 0x80}
 | |
|     choices_universe = hidpp20_constants.ParamId
 | |
|     sub_items_universe = hidpp20.SUB_PARAM
 | |
|     # item (NamedInt) -> list/tuple of objects that have the following attributes
 | |
|     # .id (sub-item text), .length (in bytes), .minimum and .maximum
 | |
| 
 | |
|     _labels = _GESTURE2_PARAMS_LABELS
 | |
|     _labels_sub = _GESTURE2_PARAMS_LABELS_SUB
 | |
| 
 | |
|     class validator_class(settings.MultipleRangeValidator):
 | |
|         @classmethod
 | |
|         def build(cls, setting_class, device):
 | |
|             params = _hidpp20.get_gestures(device).params.values()
 | |
|             items = [i for i in params if i.sub_params]
 | |
|             if not items:
 | |
|                 return None
 | |
|             sub_items = {i: i.sub_params for i in items}
 | |
|             return cls(items, sub_items)
 | |
| 
 | |
| 
 | |
| class MKeyLEDs(settings.BitFieldSetting):
 | |
|     name = "m-key-leds"
 | |
|     label = _("M-Key LEDs")
 | |
|     description = (
 | |
|         _("Control the M-Key LEDs.")
 | |
|         + "\n"
 | |
|         + _("May need Onboard Profiles set to Disable to be effective.")
 | |
|         + "\n"
 | |
|         + _("May need G Keys diverted to be effective.")
 | |
|     )
 | |
|     feature = _F.MKEYS
 | |
|     choices_universe = common.NamedInts()
 | |
|     for i in range(8):
 | |
|         choices_universe[1 << i] = "M" + str(i + 1)
 | |
|     _labels = {k: (None, _("Lights up the %s key.") % k) for k in choices_universe}
 | |
| 
 | |
|     class rw_class(settings.FeatureRW):
 | |
|         def __init__(self, feature):
 | |
|             super().__init__(feature, write_fnid=0x10)
 | |
| 
 | |
|         def read(self, device):  # no way to read, so just assume off
 | |
|             return b"\x00"
 | |
| 
 | |
|     class validator_class(settings.BitFieldValidator):
 | |
|         @classmethod
 | |
|         def build(cls, setting_class, device):
 | |
|             number = device.feature_request(setting_class.feature, 0x00)[0]
 | |
|             options = [setting_class.choices_universe[1 << i] for i in range(number)]
 | |
|             return cls(options) if options else None
 | |
| 
 | |
| 
 | |
| class MRKeyLED(settings.Setting):
 | |
|     name = "mr-key-led"
 | |
|     label = _("MR-Key LED")
 | |
|     description = (
 | |
|         _("Control the MR-Key LED.")
 | |
|         + "\n"
 | |
|         + _("May need Onboard Profiles set to Disable to be effective.")
 | |
|         + "\n"
 | |
|         + _("May need G Keys diverted to be effective.")
 | |
|     )
 | |
|     feature = _F.MR
 | |
| 
 | |
|     class rw_class(settings.FeatureRW):
 | |
|         def __init__(self, feature):
 | |
|             super().__init__(feature, write_fnid=0x00)
 | |
| 
 | |
|         def read(self, device):  # no way to read, so just assume off
 | |
|             return b"\x00"
 | |
| 
 | |
| 
 | |
| ## Only implemented for devices that can produce Key and Consumer Codes (e.g., Craft)
 | |
| ## and devices that can produce Key, Mouse, and Horizontal Scroll (e.g., M720)
 | |
| ## Only interested in current host, so use 0xFF for it
 | |
| class PersistentRemappableAction(settings.Settings):
 | |
|     name = "persistent-remappable-keys"
 | |
|     label = _("Persistent Key/Button Mapping")
 | |
|     description = (
 | |
|         _("Permanently change the mapping for the key or button.")
 | |
|         + "\n"
 | |
|         + _("Changing important keys or buttons (such as for the left mouse button) can result in an unusable system.")
 | |
|     )
 | |
|     persist = False  # This setting is persistent in the device so no need to persist it here
 | |
|     feature = _F.PERSISTENT_REMAPPABLE_ACTION
 | |
|     keys_universe = special_keys.CONTROL
 | |
|     choices_universe = special_keys.KEYS
 | |
| 
 | |
|     class rw_class:
 | |
|         def __init__(self, feature):
 | |
|             self.feature = feature
 | |
|             self.kind = settings.FeatureRW.kind
 | |
| 
 | |
|         def read(self, device, key):
 | |
|             ks = device.remap_keys[device.remap_keys.index(key)]
 | |
|             return b"\x00\x00" + ks.data_bytes
 | |
| 
 | |
|         def write(self, device, key, data_bytes):
 | |
|             ks = device.remap_keys[device.remap_keys.index(key)]
 | |
|             v = ks.remap(data_bytes)
 | |
|             return v
 | |
| 
 | |
|     class validator_class(settings.ChoicesMapValidator):
 | |
|         @classmethod
 | |
|         def build(cls, setting_class, device):
 | |
|             remap_keys = device.remap_keys
 | |
|             if not remap_keys:
 | |
|                 return None
 | |
|             capabilities = device.remap_keys.capabilities
 | |
|             if capabilities & 0x0041 == 0x0041:  # Key and Consumer Codes
 | |
|                 keys = special_keys.KEYS_KEYS_CONSUMER
 | |
|             elif capabilities & 0x0023 == 0x0023:  # Key, Mouse, and HScroll Codes
 | |
|                 keys = special_keys.KEYS_KEYS_MOUSE_HSCROLL
 | |
|             else:
 | |
|                 if logger.isEnabledFor(logging.WARNING):
 | |
|                     logger.warning("%s: unimplemented Persistent Remappable capability %s", device.name, hex(capabilities))
 | |
|                 return None
 | |
|             choices = {}
 | |
|             for k in remap_keys:
 | |
|                 if k is not None:
 | |
|                     key = special_keys.CONTROL[k.key]
 | |
|                     choices[key] = keys  # TO RECOVER FROM BAD VALUES use special_keys.KEYS
 | |
|             return cls(choices, key_byte_count=2, byte_count=4) if choices else None
 | |
| 
 | |
|         def validate_read(self, reply_bytes, key):
 | |
|             start = self._key_byte_count + self._read_skip_byte_count
 | |
|             end = start + self._byte_count
 | |
|             reply_value = common.bytes2int(reply_bytes[start:end]) & self.mask
 | |
|             # Craft keyboard has a value that isn't valid so fudge these values
 | |
|             if reply_value not in self.choices[key]:
 | |
|                 if logger.isEnabledFor(logging.WARNING):
 | |
|                     logger.warning("unusual persistent remappable action mapping %x: use Default", reply_value)
 | |
|                 reply_value = special_keys.KEYS_Default
 | |
|             return reply_value
 | |
| 
 | |
| 
 | |
| class Sidetone(settings.Setting):
 | |
|     name = "sidetone"
 | |
|     label = _("Sidetone")
 | |
|     description = _("Set sidetone level.")
 | |
|     feature = _F.SIDETONE
 | |
|     validator_class = settings.RangeValidator
 | |
|     min_value = 0
 | |
|     max_value = 100
 | |
| 
 | |
| 
 | |
| class Equalizer(settings.RangeFieldSetting):
 | |
|     name = "equalizer"
 | |
|     label = _("Equalizer")
 | |
|     description = _("Set equalizer levels.")
 | |
|     feature = _F.EQUALIZER
 | |
|     rw_options = {"read_fnid": 0x20, "write_fnid": 0x30, "read_prefix": b"\x00"}
 | |
|     keys_universe = []
 | |
| 
 | |
|     class validator_class(settings.PackedRangeValidator):
 | |
|         @classmethod
 | |
|         def build(cls, setting_class, device):
 | |
|             data = device.feature_request(_F.EQUALIZER, 0x00)
 | |
|             if not data:
 | |
|                 return None
 | |
|             count, dbRange, _x, dbMin, dbMax = struct.unpack("!BBBBB", data[:5])
 | |
|             if dbMin == 0:
 | |
|                 dbMin = -dbRange
 | |
|             if dbMax == 0:
 | |
|                 dbMax = dbRange
 | |
|             map = common.NamedInts()
 | |
|             for g in range((count + 6) // 7):
 | |
|                 freqs = device.feature_request(_F.EQUALIZER, 0x10, g * 7)
 | |
|                 for b in range(7):
 | |
|                     if g * 7 + b >= count:
 | |
|                         break
 | |
|                     map[g * 7 + b] = str(int.from_bytes(freqs[2 * b + 1 : 2 * b + 3], "big")) + _("Hz")
 | |
|             return cls(map, min_value=dbMin, max_value=dbMax, count=count, write_prefix_bytes=b"\x02")
 | |
| 
 | |
| 
 | |
| class ADCPower(settings.Setting):
 | |
|     name = "adc_power_management"
 | |
|     label = _("Power Management")
 | |
|     description = _("Power off in minutes (0 for never).")
 | |
|     feature = _F.ADC_MEASUREMENT
 | |
|     rw_options = {"read_fnid": 0x10, "write_fnid": 0x20}
 | |
|     validator_class = settings.RangeValidator
 | |
|     min_value = 0x00
 | |
|     max_value = 0xFF
 | |
|     validator_options = {"byte_count": 1}
 | |
| 
 | |
| 
 | |
| class BrightnessControl(settings.Setting):
 | |
|     name = "brightness_control"
 | |
|     label = _("Brightness Control")
 | |
|     description = _("Control overall brightness")
 | |
|     feature = _F.BRIGHTNESS_CONTROL
 | |
|     rw_options = {"read_fnid": 0x10, "write_fnid": 0x20}
 | |
|     validator_class = settings.RangeValidator
 | |
| 
 | |
|     def __init__(self, device, rw, validator):
 | |
|         super().__init__(device, rw, validator)
 | |
|         rw.on_off = validator.on_off
 | |
|         rw.min_nonzero_value = validator.min_value
 | |
|         validator.min_value = 0 if validator.on_off else validator.min_value
 | |
| 
 | |
|     class rw_class(settings.FeatureRW):
 | |
|         def read(self, device, data_bytes=b""):
 | |
|             if self.on_off:
 | |
|                 reply = device.feature_request(self.feature, 0x30)
 | |
|                 if not reply[0] & 0x01:
 | |
|                     return b"\x00\x00"
 | |
|             return super().read(device, data_bytes)
 | |
| 
 | |
|         def write(self, device, data_bytes):
 | |
|             if self.on_off:
 | |
|                 off = int.from_bytes(data_bytes, byteorder="big") < self.min_nonzero_value
 | |
|                 reply = device.feature_request(self.feature, 0x40, b"\x00" if off else b"\x01", no_reply=False)
 | |
|                 if off:
 | |
|                     return reply
 | |
|             return super().write(device, data_bytes)
 | |
| 
 | |
|     class validator_class(settings.RangeValidator):
 | |
|         @classmethod
 | |
|         def build(cls, setting_class, device):
 | |
|             reply = device.feature_request(_F.BRIGHTNESS_CONTROL)
 | |
|             assert reply, "Oops, brightness range cannot be retrieved!"
 | |
|             if reply:
 | |
|                 max_value = int.from_bytes(reply[0:2], byteorder="big")
 | |
|                 min_value = int.from_bytes(reply[4:6], byteorder="big")
 | |
|                 on_off = bool(reply[3] & 0x04)  # separate on/off control
 | |
|                 validator = cls(min_value=min_value, max_value=max_value, byte_count=2)
 | |
|                 validator.on_off = on_off
 | |
|                 return validator
 | |
| 
 | |
| 
 | |
| class LEDControl(settings.Setting):
 | |
|     name = "led_control"
 | |
|     label = _("LED Control")
 | |
|     description = _("Switch control of LED zones between device and Solaar")
 | |
|     feature = _F.COLOR_LED_EFFECTS
 | |
|     rw_options = {"read_fnid": 0x70, "write_fnid": 0x80}
 | |
|     choices_universe = common.NamedInts(Device=0, Solaar=1)
 | |
|     validator_class = settings.ChoicesValidator
 | |
|     validator_options = {"choices": choices_universe}
 | |
| 
 | |
| 
 | |
| colors = special_keys.COLORS
 | |
| _LEDP = hidpp20.LEDParam
 | |
| 
 | |
| 
 | |
| # an LED Zone has an index, a set of possible LED effects, and an LED effect setting
 | |
| class LEDZoneSetting(settings.Setting):
 | |
|     name = "led_zone_"
 | |
|     label = _("LED Zone Effects")
 | |
|     description = _("Set effect for LED Zone") + "\n" + _("LED Control needs to be set to Solaar to be effective.")
 | |
|     feature = _F.COLOR_LED_EFFECTS
 | |
|     color_field = {"name": _LEDP.color, "kind": settings.KIND.choice, "label": None, "choices": colors}
 | |
|     speed_field = {"name": _LEDP.speed, "kind": settings.KIND.range, "label": _("Speed"), "min": 0, "max": 255}
 | |
|     period_field = {"name": _LEDP.period, "kind": settings.KIND.range, "label": _("Period"), "min": 100, "max": 5000}
 | |
|     intensity_field = {"name": _LEDP.intensity, "kind": settings.KIND.range, "label": _("Intensity"), "min": 0, "max": 100}
 | |
|     ramp_field = {"name": _LEDP.ramp, "kind": settings.KIND.choice, "label": _("Ramp"), "choices": hidpp20.LEDRampChoices}
 | |
|     # form_field = {"name": _LEDP.form, "kind": settings.KIND.choice, "label": _("Form"), "choices": _hidpp20.LEDFormChoices}
 | |
|     possible_fields = [color_field, speed_field, period_field, intensity_field, ramp_field]
 | |
| 
 | |
|     @classmethod
 | |
|     def setup(cls, device, read_fnid, write_fnid, suffix):
 | |
|         infos = device.led_effects
 | |
|         settings_ = []
 | |
|         for zone in infos.zones:
 | |
|             prefix = common.int2bytes(zone.index, 1)
 | |
|             rw = settings.FeatureRW(cls.feature, read_fnid, write_fnid, prefix=prefix, suffix=suffix)
 | |
|             validator = settings.HeteroValidator(
 | |
|                 data_class=hidpp20.LEDEffectSetting, options=zone.effects, readable=infos.readable
 | |
|             )
 | |
|             setting = cls(device, rw, validator)
 | |
|             setting.name = cls.name + str(int(zone.location))
 | |
|             setting.label = _("LEDs") + " " + str(hidpp20.LEDZoneLocations[zone.location])
 | |
|             choices = [hidpp20.LEDEffects[e.ID][0] for e in zone.effects if e.ID in hidpp20.LEDEffects]
 | |
|             ID_field = {"name": "ID", "kind": settings.KIND.choice, "label": None, "choices": choices}
 | |
|             setting.possible_fields = [ID_field] + cls.possible_fields
 | |
|             setting.fields_map = hidpp20.LEDEffects
 | |
|             settings_.append(setting)
 | |
|         return settings_
 | |
| 
 | |
|     @classmethod
 | |
|     def build(cls, device):
 | |
|         return cls.setup(device, 0xE0, 0x30, b"")
 | |
| 
 | |
| 
 | |
| class RGBControl(settings.Setting):
 | |
|     name = "rgb_control"
 | |
|     label = _("LED Control")
 | |
|     description = _("Switch control of LED zones between device and Solaar")
 | |
|     feature = _F.RGB_EFFECTS
 | |
|     rw_options = {"read_fnid": 0x50, "write_fnid": 0x50}
 | |
|     choices_universe = common.NamedInts(Device=0, Solaar=1)
 | |
|     validator_class = settings.ChoicesValidator
 | |
|     validator_options = {"choices": choices_universe, "write_prefix_bytes": b"\x01", "read_skip_byte_count": 1}
 | |
| 
 | |
| 
 | |
| class RGBEffectSetting(LEDZoneSetting):
 | |
|     name = "rgb_zone_"
 | |
|     label = _("LED Zone Effects")
 | |
|     description = _("Set effect for LED Zone") + "\n" + _("LED Control needs to be set to Solaar to be effective.")
 | |
|     feature = _F.RGB_EFFECTS
 | |
| 
 | |
|     @classmethod
 | |
|     def build(cls, device):
 | |
|         return cls.setup(device, 0xE0, 0x10, b"\x01")
 | |
| 
 | |
| 
 | |
| class PerKeyLighting(settings.Settings):
 | |
|     name = "per-key-lighting"
 | |
|     label = _("Per-key Lighting")
 | |
|     description = _("Control per-key lighting.")
 | |
|     feature = _F.PER_KEY_LIGHTING_V2
 | |
|     keys_universe = special_keys.KEYCODES
 | |
|     choices_universe = special_keys.COLORSPLUS
 | |
| 
 | |
|     def read(self, cached=True):
 | |
|         self._pre_read(cached)
 | |
|         if cached and self._value is not None:
 | |
|             return self._value
 | |
|         reply_map = {}
 | |
|         for key in self._validator.choices:
 | |
|             reply_map[int(key)] = special_keys.COLORSPLUS["No change"]  # this signals no change
 | |
|         self._value = reply_map
 | |
|         return reply_map
 | |
| 
 | |
|     def write(self, map, save=True):
 | |
|         if self._device.online:
 | |
|             self.update(map, save)
 | |
|             table = {}
 | |
|             for key, value in map.items():
 | |
|                 if value in table:
 | |
|                     table[value].append(key)  # keys will be in order from small to large
 | |
|                 else:
 | |
|                     table[value] = [key]
 | |
|             if len(table) == 1:  # use range update
 | |
|                 for value, keys in table.items():  # only one, of course
 | |
|                     if value != special_keys.COLORSPLUS["No change"]:  # this signals no change, so don't update at all
 | |
|                         data_bytes = keys[0].to_bytes(1, "big") + keys[-1].to_bytes(1, "big") + value.to_bytes(3, "big")
 | |
|                         self._device.feature_request(self.feature, 0x50, data_bytes)  # range update command to update all keys
 | |
|                         self._device.feature_request(self.feature, 0x70, 0x00)  # signal device to make the changes
 | |
|             else:
 | |
|                 data_bytes = b""
 | |
|                 for value, keys in table.items():  # only one, of course
 | |
|                     if value != special_keys.COLORSPLUS["No change"]:  # this signals no change, so ignore it
 | |
|                         while len(keys) > 3:  # use an optimized update command that can update up to 13 keys
 | |
|                             data = value.to_bytes(3, "big") + b"".join([key.to_bytes(1, "big") for key in keys[0:13]])
 | |
|                             self._device.feature_request(self.feature, 0x60, data)  # single-value multiple-keys update
 | |
|                             keys = keys[13:]
 | |
|                         for key in keys:
 | |
|                             data_bytes += key.to_bytes(1, "big") + value.to_bytes(3, "big")
 | |
|                             if len(data_bytes) >= 16:  # up to four values are packed into a regular update
 | |
|                                 self._device.feature_request(self.feature, 0x10, data_bytes)
 | |
|                                 data_bytes = b""
 | |
|                 if len(data_bytes) > 0:  # update any remaining keys
 | |
|                     self._device.feature_request(self.feature, 0x10, data_bytes)
 | |
|                 self._device.feature_request(self.feature, 0x70, 0x00)  # signal device to make the changes
 | |
|         return map
 | |
| 
 | |
|     def write_key_value(self, key, value, save=True):
 | |
|         if value != special_keys.COLORSPLUS["No change"]:  # this signals no change
 | |
|             result = super().write_key_value(int(key), value, save)
 | |
|             if self._device.online:
 | |
|                 self._device.feature_request(self.feature, 0x70, 0x00)  # signal device to make the change
 | |
|             return result
 | |
|         else:
 | |
|             return True
 | |
| 
 | |
|     class rw_class(settings.FeatureRWMap):
 | |
|         pass
 | |
| 
 | |
|     class validator_class(settings.ChoicesMapValidator):
 | |
|         @classmethod
 | |
|         def build(cls, setting_class, device):
 | |
|             choices_map = {}
 | |
|             key_bitmap = device.feature_request(setting_class.feature, 0x00, 0x00, 0x00)[2:]
 | |
|             key_bitmap += device.feature_request(setting_class.feature, 0x00, 0x00, 0x01)[2:]
 | |
|             key_bitmap += device.feature_request(setting_class.feature, 0x00, 0x00, 0x02)[2:]
 | |
|             for i in range(1, 255):
 | |
|                 if (key_bitmap[i // 8] >> i % 8) & 0x01:
 | |
|                     key = (
 | |
|                         setting_class.keys_universe[i]
 | |
|                         if i in setting_class.keys_universe
 | |
|                         else common.NamedInt(i, "KEY " + str(i))
 | |
|                     )
 | |
|                     choices_map[key] = setting_class.choices_universe
 | |
|             result = cls(choices_map) if choices_map else None
 | |
|             return result
 | |
| 
 | |
| 
 | |
| SETTINGS = [
 | |
|     RegisterHandDetection,  # simple
 | |
|     RegisterSmoothScroll,  # simple
 | |
|     RegisterSideScroll,  # simple
 | |
|     RegisterDpi,
 | |
|     RegisterFnSwap,  # working
 | |
|     HiResScroll,  # simple
 | |
|     LowresMode,  # simple
 | |
|     HiresSmoothInvert,  # working
 | |
|     HiresSmoothResolution,  # working
 | |
|     HiresMode,  # simple
 | |
|     ScrollRatchet,  # simple
 | |
|     SmartShift,  # working
 | |
|     SmartShiftEnhanced,  # simple
 | |
|     ThumbInvert,  # working
 | |
|     ThumbMode,  # working
 | |
|     OnboardProfiles,
 | |
|     ReportRate,  # working
 | |
|     ExtendedReportRate,
 | |
|     PointerSpeed,  # simple
 | |
|     AdjustableDpi,  # working
 | |
|     ExtendedAdjustableDpi,
 | |
|     SpeedChange,
 | |
|     #    Backlight,  # not working - disabled temporarily
 | |
|     Backlight2,  # working
 | |
|     Backlight2Level,
 | |
|     Backlight2DurationHandsOut,
 | |
|     Backlight2DurationHandsIn,
 | |
|     Backlight2DurationPowered,
 | |
|     Backlight3,
 | |
|     LEDControl,
 | |
|     LEDZoneSetting,
 | |
|     RGBControl,
 | |
|     RGBEffectSetting,
 | |
|     BrightnessControl,
 | |
|     PerKeyLighting,
 | |
|     FnSwap,  # simple
 | |
|     NewFnSwap,  # simple
 | |
|     K375sFnSwap,  # working
 | |
|     ReprogrammableKeys,  # working
 | |
|     PersistentRemappableAction,
 | |
|     DivertKeys,  # working
 | |
|     DisableKeyboardKeys,  # working
 | |
|     CrownSmooth,  # working
 | |
|     DivertCrown,  # working
 | |
|     DivertGkeys,  # working
 | |
|     MKeyLEDs,  # working
 | |
|     MRKeyLED,  # working
 | |
|     Multiplatform,  # working
 | |
|     DualPlatform,  # simple
 | |
|     ChangeHost,  # working
 | |
|     Gesture2Gestures,  # working
 | |
|     Gesture2Divert,
 | |
|     Gesture2Params,  # working
 | |
|     Sidetone,
 | |
|     Equalizer,
 | |
|     ADCPower,
 | |
| ]
 | |
| 
 | |
| 
 | |
| def check_feature(device, settings_class: settings.Setting) -> None | bool | Any:
 | |
|     if settings_class.feature not in device.features:
 | |
|         return
 | |
|     if settings_class.min_version > device.features.get_feature_version(settings_class.feature):
 | |
|         return
 | |
|     try:
 | |
|         detected = settings_class.build(device)
 | |
|         if logger.isEnabledFor(logging.DEBUG):
 | |
|             logger.debug("check_feature %s [%s] detected %s", settings_class.name, settings_class.feature, detected)
 | |
|         return detected
 | |
|     except Exception as e:
 | |
|         logger.error(
 | |
|             "check_feature %s [%s] error %s\n%s", settings_class.name, settings_class.feature, e, traceback.format_exc()
 | |
|         )
 | |
|         return False  # differentiate from an error-free determination that the setting is not supported
 | |
| 
 | |
| 
 | |
| # Returns True if device was queried to find features, False otherwise
 | |
| def check_feature_settings(device, already_known):
 | |
|     """Auto-detect device settings by the HID++ 2.0 features they have."""
 | |
|     if not device.features or not device.online:
 | |
|         return False
 | |
|     if device.protocol and device.protocol < 2.0:
 | |
|         return False
 | |
|     absent = device.persister.get("_absent", []) if device.persister else []
 | |
|     newAbsent = []
 | |
|     for sclass in SETTINGS:
 | |
|         if sclass.feature:
 | |
|             known_present = device.persister and sclass.name in device.persister
 | |
|             if not any(s.name == sclass.name for s in already_known) and (known_present or sclass.name not in absent):
 | |
|                 setting = check_feature(device, sclass)
 | |
|                 if isinstance(setting, list):
 | |
|                     for s in setting:
 | |
|                         already_known.append(s)
 | |
|                     if sclass.name in newAbsent:
 | |
|                         newAbsent.remove(sclass.name)
 | |
|                 elif setting:
 | |
|                     already_known.append(setting)
 | |
|                     if sclass.name in newAbsent:
 | |
|                         newAbsent.remove(sclass.name)
 | |
|                 elif setting is None:
 | |
|                     if sclass.name not in newAbsent and sclass.name not in absent and sclass.name not in device.persister:
 | |
|                         newAbsent.append(sclass.name)
 | |
|     if device.persister and newAbsent:
 | |
|         absent.extend(newAbsent)
 | |
|         device.persister["_absent"] = absent
 | |
|     return True
 | |
| 
 | |
| 
 | |
| def check_feature_setting(device, setting_name):
 | |
|     for sclass in SETTINGS:
 | |
|         if sclass.feature and sclass.name == setting_name and device.features:
 | |
|             setting = check_feature(device, sclass)
 | |
|             if setting:
 | |
|                 return setting
 |