## 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. import logging import socket as _socket from logging import WARN as _WARN from struct import pack as _pack from struct import unpack as _unpack from time import time as _time from traceback import format_exc as _format_exc from . import descriptors as _descriptors from . import hidpp10_constants as _hidpp10_constants from . import hidpp20 from . import hidpp20_constants as _hidpp20_constants from . import notify as _notify from . import special_keys as _special_keys from .base import _HIDPP_Notification as _HIDPP_Notification from .common import NamedInt as _NamedInt from .common import NamedInts as _NamedInts from .common import bytes2int as _bytes2int from .common import int2bytes as _int2bytes from .diversion import process_notification as _process_notification from .i18n import _ from .settings import KIND as _KIND from .settings import ActionSettingRW as _ActionSettingRW from .settings import BitFieldSetting as _BitFieldSetting from .settings import BitFieldValidator as _BitFieldV from .settings import BitFieldWithOffsetAndMaskSetting as _BitFieldOMSetting from .settings import BitFieldWithOffsetAndMaskValidator as _BitFieldOMV from .settings import ChoicesMapValidator as _ChoicesMapV from .settings import ChoicesValidator as _ChoicesV from .settings import FeatureRW as _FeatureRW from .settings import HeteroValidator as _HeteroV from .settings import LongSettings as _LongSettings from .settings import MultipleRangeValidator as _MultipleRangeV from .settings import PackedRangeValidator as _PackedRangeV from .settings import RangeFieldSetting as _RangeFieldSetting from .settings import RangeValidator as _RangeV from .settings import RawXYProcessing as _RawXYProcessing from .settings import Setting as _Setting from .settings import Settings as _Settings from .special_keys import DISABLE as _DKEY logger = logging.getLogger(__name__) _hidpp20 = hidpp20.Hidpp20() _DK = _hidpp10_constants.DEVICE_KIND _R = _hidpp10_constants.REGISTERS _F = _hidpp20_constants.FEATURE _GG = _hidpp20_constants.GESTURE _GP = _hidpp20_constants.PARAM # 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. # _BitFieldOMSetting 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 settings.RegisterRW is created. # and if the setting has a feature then 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, _RangeV, and _ChoicesV # _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). # _RangeV 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). # _RangeV uses min_value and max_value from the setting class as minimum and maximum. # _ChoicesV 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 _ChoicesV 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. # _ChoicesMapV 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 _ChoicesV, # read_skip_byte_count is as for _ChoicesV, # write_prefix_bytes is as for _ChoicesV, # key_byte_count is the number of bytes for the key integer (default size of largest key), # extra_default is an extra raw value that is used as a default value (default None). # Settings that use _ChoicesV should have keys_universe and choices_universe class variable of # the potential keys and potential choices or None for no limitation. # _BitFieldV validator is for bit field settings. It takes one positional and one keyword argument # the positional argument is the number of bits in the bit field # byte_count is the size of the bit field (default size of the bit field) # # 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(_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(_Setting): name = "hand-detection" label = _("Hand Detection") description = _("Turn on illumination when the hands hover over the keyboard.") register = _R.keyboard_hand_detection validator_options = {"true_value": b"\x00\x00\x00", "false_value": b"\x00\x00\x30", "mask": b"\x00\x00\xFF"} class RegisterSmoothScroll(_Setting): name = "smooth-scroll" label = _("Scroll Wheel Smooth Scrolling") description = _("High-sensitivity mode for vertical scroll with the wheel.") register = _R.mouse_button_flags validator_options = {"true_value": 0x40, "mask": 0x40} class RegisterSideScroll(_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 = _R.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(_Setting): name = "dpi-old" label = _("Sensitivity (DPI - older mice)") description = _("Mouse movement sensitivity") register = _R.mouse_dpi choices_universe = _NamedInts.range(0x81, 0x8F, lambda x: str((x - 0x80) * 100)) validator_class = _ChoicesV validator_options = {"choices": choices_universe} class RegisterFnSwap(FnSwapVirtual): register = _R.keyboard_fn_swap validator_options = {"true_value": b"\x00\x01", "mask": b"\x00\x01"} class _PerformanceMXDpi(RegisterDpi): choices_universe = _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 # just use the current host (first byte = 0xFF) part of the feature to read and set the Fn state class K375sFnSwap(FnSwapVirtual): feature = _F.K375S_FN_INVERSION rw_options = {"prefix": b"\xFF"} validator_options = {"true_value": b"\x01", "false_value": b"\x00", "read_skip_byte_count": 1} class FnSwap(FnSwapVirtual): feature = _F.FN_INVERSION class NewFnSwap(FnSwapVirtual): feature = _F.NEW_FN_INVERSION class Backlight(_Setting): name = "backlight-qualitative" label = _("Backlight") description = _("Set illumination time for keyboard.") feature = _F.BACKLIGHT choices_universe = _NamedInts(Off=0, Varying=2, VeryShort=5, Short=10, Medium=20, Long=60, VeryLong=180) validator_class = _ChoicesV 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(_Setting): name = "backlight" label = _("Backlight") description = _("Illumination level on keyboard. Changes made are only applied in Manual mode.") feature = _F.BACKLIGHT2 min_version = 0 class rw_class: def __init__(self, feature): self.feature = feature self.kind = _FeatureRW.kind def read(self, device): backlight = device.backlight if not backlight.enabled: return b"\xFF" else: return _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(_ChoicesV): @classmethod def build(cls, setting_class, device): backlight = device.backlight choices = _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(_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 = _FeatureRW.kind def read(self, device): backlight = device.backlight return _int2bytes(backlight.level, 1) def write(self, device, data_bytes): if device.backlight.level != _bytes2int(data_bytes): device.backlight.level = _bytes2int(data_bytes) device.backlight.write() return True class validator_class(_RangeV): @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(_Setting): feature = _F.BACKLIGHT2 min_version = 3 validator_class = _RangeV min_value = 1 max_value = 120 # actual maximum is 2 hours validator_options = {"byte_count": 2} class rw_class: def __init__(self, feature, field): self.feature = feature self.kind = _FeatureRW.kind self.field = field def read(self, device): backlight = device.backlight return _int2bytes(getattr(backlight, self.field), 2) * 5 # use seconds instead of 5-second units def write(self, device, data_bytes): backlight = device.backlight new_duration = (int.from_bytes(data_bytes) + 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 = _RangeV 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 = _RangeV 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 = _RangeV rw_options = {"field": "dpow"} class Backlight3(_Setting): name = "backlight-timed" label = _("Backlight") description = _("Set illumination time for keyboard.") feature = _F.BACKLIGHT3 rw_options = {"read_fnid": 0x10, "write_fnid": 0x20, "suffix": 0x09} validator_class = _RangeV min_value = 0 max_value = 1000 validator_options = {"byte_count": 2} class HiResScroll(_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(_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(_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(_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(_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(_Setting): name = "pointer_speed" label = _("Sensitivity (Pointer Speed)") description = _("Speed multiplier for mouse (256 is normal multiplier).") feature = _F.POINTER_SPEED validator_class = _RangeV min_value = 0x002E max_value = 0x01FF validator_options = {"byte_count": 2} class ThumbMode(_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(_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(_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 = _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 = _ChoicesV class rw_class: def __init__(self, feature): self.feature = feature self.kind = _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, _bytes2int(data_bytes)) return result class validator_class(_ChoicesV): @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=_NamedInts.list(profiles_list), byte_count=2) if len(profiles_list) > 1 else None class ReportRate(_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 = _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 _rw_class(_FeatureRW): # no longer needed - set Onboard Profiles to disable def write(self, device, data_bytes): # Host mode is required for report rate to be adjustable if _hidpp20.get_onboard_mode(device) != _hidpp20_constants.ONBOARD_MODES.MODE_HOST: _hidpp20.set_onboard_mode(device, _hidpp20_constants.ONBOARD_MODES.MODE_HOST) return super().write(device, data_bytes) class validator_class(_ChoicesV): @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 = _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=_NamedInts.list(rate_list), byte_count=1) if rate_list else None class ExtendedReportRate(_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 = _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 _rw_class(_FeatureRW): def read(self, device, data_bytes=b""): # need connection type from device to get actual report rate self.read_prefix = b"\x00" if device.receiver else b"\x01" super().read(device, data_bytes) def write(self, device, data_bytes): # Host mode is required for report rate to be adjustable if _hidpp20.get_onboard_mode(device) != _hidpp20_constants.ONBOARD_MODES.MODE_HOST: _hidpp20.set_onboard_mode(device, _hidpp20_constants.ONBOARD_MODES.MODE_HOST) return super().write(device, data_bytes) class validator_class(_ChoicesV): @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 = _bytes2int(reply[0:2]) for i in range(0, 6): if rate_flags & (0x01 << i): rate_list.append(setting_class.choices_universe[i]) return cls(choices=_NamedInts.list(rate_list), byte_count=1) if rate_list else None class DivertCrown(_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(_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(_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(_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(_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 = _NamedInts(**{_("Freespinning"): 1, _("Ratcheted"): 2}) validator_class = _ChoicesV validator_options = {"choices": choices_universe} class SmartShift(_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(_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 _bytes2int(value[0:1]) == 1: # Mode = Freespin, map to minimum return _int2bytes(self.MIN_VALUE, count=1) else: # Mode = smart shift, map to the value, capped at maximum threshold = min(_bytes2int(value[1:2]), self.MAX_VALUE) return _int2bytes(threshold, count=1) def write(self, device, data_bytes): threshold = _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 = _int2bytes(mode, count=1) + _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 = _RangeV 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): 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 = _FeatureRW.kind def read(self, device, key): key_index = device.keys.index(key) key_struct = device.keys[key_index] return b"\x00\x00" + _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[_bytes2int(data_bytes)]) return True class validator_class(_ChoicesMapV): @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(_RawXYProcessing): def activate_action(self): self.dpiSetting = next(filter(lambda s: s.name == "dpi", 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 = "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): if _notify.available: reason = "DPI %d [min %d, max %d]" % (self.dpiChoices[newDpiIdx], self.dpiChoices[0], self.dpiChoices[-1]) _notify.show(self.device, reason) def press_action(self, key): # start tracking self.starting = True if self.fsmState == "idle": self.fsmState = "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 == "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 == "moved": # Set DPI according to displacement self.setNewDpi(self.movingDpiIdx) self.fsmState = "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 == "pressed": if abs(self.dx) >= 1.0: self.fsmState = "moved" self.movingDpiIdx = self.dpiChoices.index(currDpi) elif self.fsmState == "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(_RawXYProcessing): def activate_action(self): self.dpiSetting = next(filter(lambda s: s.name == "dpi", self.device.settings), None) self.fsmState = "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 == "idle": self.fsmState = "pressed" self.initialize_data() self.data = [key.key] def release_action(self): if self.fsmState == "pressed": # emit mouse gesture notification self.push_mouse_event() if logger.isEnabledFor(logging.INFO): logger.info("mouse gesture notification %s", self.data) payload = _pack("!" + (len(self.data) * "h"), *self.data) notification = _HIDPP_Notification(0, 0, 0, 0, payload) _process_notification(self.device, self.device.status, notification, _F.MOUSE_GESTURE) self.fsmState = "idle" def move_action(self, dx, dy): if self.fsmState == "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): 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 = _NamedInts(**{_("Regular"): 0, _("Diverted"): 1, _("Mouse Gestures"): 2, _("Sliding DPI"): 3}) choices_gesture = _NamedInts(**{_("Regular"): 0, _("Diverted"): 1, _("Mouse Gestures"): 2}) choices_divert = _NamedInts(**{_("Regular"): 0, _("Diverted"): 1}) class rw_class: def __init__(self, feature): self.feature = feature self.kind = _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(_bytes2int(data_bytes) != 0) # not regular return True class validator_class(_ChoicesMapV): 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="DpiSlding") 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 class AdjustableDpi(_Setting): """Pointer Speed feature""" # Assume sensorIdx 0 (there is only one sensor) # [2] getSensorDpi(sensorIdx) -> sensorIdx, dpiMSB, dpiLSB # [3] setSensorDpi(sensorIdx, dpi) name = "dpi" label = _("Sensitivity (DPI)") description = _("Mouse movement sensitivity") feature = _F.ADJUSTABLE_DPI rw_options = {"read_fnid": 0x20, "write_fnid": 0x30} choices_universe = _NamedInts.range(200, 4000, str, 50) class validator_class(_ChoicesV): @classmethod def build(cls, setting_class, device): # [1] getSensorDpiList(sensorIdx) reply = device.feature_request(_F.ADJUSTABLE_DPI, 0x10) assert reply, "Oops, DPI list cannot be retrieved!" dpi_list = [] step = None for val in _unpack("!7H", reply[1 : 1 + 14]): if val == 0: break if val >> 13 == 0b111: assert step is None and len(dpi_list) == 1, "Invalid DPI list item: %r" % val step = val & 0x1FFF else: dpi_list.append(val) if step: assert len(dpi_list) == 2, "Invalid DPI list range: %r" % dpi_list dpi_list = range(dpi_list[0], dpi_list[1] + 1, step) return cls(choices=_NamedInts.list(dpi_list), byte_count=3) if dpi_list else None def validate_read(self, reply_bytes): # special validator to use default DPI if needed reply_value = _bytes2int(reply_bytes[1:3]) if reply_value == 0: # use default value instead reply_value = _bytes2int(reply_bytes[3:5]) valid_value = self.choices[reply_value] assert valid_value is not None, "%s: failed to validate read value %02X" % (self.__class__.__name__, reply_value) return valid_value class SpeedChange(_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 = _NamedInt(0, _("Off")) feature = _F.POINTER_SPEED rw_options = {"name": "speed change"} class rw_class(_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(_ChoicesV): @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=_NamedInts.list(keys), byte_count=2) class DisableKeyboardKeys(_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 _DKEY} choices_universe = _DKEY class validator_class(_BitFieldV): @classmethod def build(cls, setting_class, device): mask = device.feature_request(_F.KEYBOARD_DISABLE_KEYS, 0x00)[0] options = [_DKEY[1 << i] for i in range(8) if mask & (1 << i)] return cls(options) if options else None class Multiplatform(_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 = _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(_ChoicesV): @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 = _unpack("!BBB", infos[:3]) if not (flags & 0x02): # can't set platform so don't create setting return [] descriptors = [] for index in range(0, num_descriptors): descriptor = device.feature_request(_F.MULTIPLATFORM, 0x10, index) platform, _ignore, os_flags, low, high = _unpack("!BBHHH", descriptor[:8]) descriptors.append((platform, os_flags, low, high)) choices = _NamedInts() for os_name, os_bit in 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(_Setting): name = "multiplatform" label = _("Set OS") description = _("Change keys to match OS.") choices_universe = _NamedInts() choices_universe[0x00] = "iOS, MacOS" choices_universe[0x01] = "Android, Windows" feature = _F.DUALPLATFORM rw_options = {"read_fnid": 0x00, "write_fnid": 0x20} validator_class = _ChoicesV validator_options = {"choices": choices_universe} class ChangeHost(_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 = _NamedInts(**{"Host " + str(i + 1): i for i in range(3)}) class validator_class(_ChoicesV): @classmethod def build(cls, setting_class, device): infos = device.feature_request(_F.CHANGE_HOST) assert infos, "Oops, host count cannot be retrieved!" numHosts, currentHost = _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 = _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 = { _GG["Tap1Finger"]: (_("Single tap"), _("Performs a left click.")), _GG["Tap2Finger"]: (_("Single tap with two fingers"), _("Performs a right click.")), _GG["Tap3Finger"]: (_("Single tap with three fingers"), None), _GG["Click1Finger"]: (None, None), _GG["Click2Finger"]: (None, None), _GG["Click3Finger"]: (None, None), _GG["DoubleTap1Finger"]: (_("Double tap"), _("Performs a double click.")), _GG["DoubleTap2Finger"]: (_("Double tap with two fingers"), None), _GG["DoubleTap3Finger"]: (_("Double tap with three fingers"), None), _GG["Track1Finger"]: (None, None), _GG["TrackingAcceleration"]: (None, None), _GG["TapDrag1Finger"]: (_("Tap and drag"), _("Drags items by dragging the finger after double tapping.")), _GG["TapDrag2Finger"]: ( _("Tap and drag with two fingers"), _("Drags items by dragging the fingers after double tapping."), ), _GG["Drag3Finger"]: (_("Tap and drag with three fingers"), None), _GG["TapGestures"]: (None, None), _GG["FnClickGestureSuppression"]: ( _("Suppress tap and edge gestures"), _("Disables tap and edge gestures (equivalent to pressing Fn+LeftClick)."), ), _GG["Scroll1Finger"]: (_("Scroll with one finger"), _("Scrolls.")), _GG["Scroll2Finger"]: (_("Scroll with two fingers"), _("Scrolls.")), _GG["Scroll2FingerHoriz"]: (_("Scroll horizontally with two fingers"), _("Scrolls horizontally.")), _GG["Scroll2FingerVert"]: (_("Scroll vertically with two fingers"), _("Scrolls vertically.")), _GG["Scroll2FingerStateless"]: (_("Scroll with two fingers"), _("Scrolls.")), _GG["NaturalScrolling"]: (_("Natural scrolling"), _("Inverts the scrolling direction.")), _GG["Thumbwheel"]: (_("Thumbwheel"), _("Enables the thumbwheel.")), _GG["VScrollInertia"]: (None, None), _GG["VScrollBallistics"]: (None, None), _GG["Swipe2FingerHoriz"]: (None, None), _GG["Swipe3FingerHoriz"]: (None, None), _GG["Swipe4FingerHoriz"]: (None, None), _GG["Swipe3FingerVert"]: (None, None), _GG["Swipe4FingerVert"]: (None, None), _GG["LeftEdgeSwipe1Finger"]: (None, None), _GG["RightEdgeSwipe1Finger"]: (None, None), _GG["BottomEdgeSwipe1Finger"]: (None, None), _GG["TopEdgeSwipe1Finger"]: (_("Swipe from the top edge"), None), _GG["LeftEdgeSwipe1Finger2"]: (_("Swipe from the left edge"), None), _GG["RightEdgeSwipe1Finger2"]: (_("Swipe from the right edge"), None), _GG["BottomEdgeSwipe1Finger2"]: (_("Swipe from the bottom edge"), None), _GG["TopEdgeSwipe1Finger2"]: (_("Swipe from the top edge"), None), _GG["LeftEdgeSwipe2Finger"]: (_("Swipe two fingers from the left edge"), None), _GG["RightEdgeSwipe2Finger"]: (_("Swipe two fingers from the right edge"), None), _GG["BottomEdgeSwipe2Finger"]: (_("Swipe two fingers from the bottom edge"), None), _GG["TopEdgeSwipe2Finger"]: (_("Swipe two fingers from the top edge"), None), _GG["Zoom2Finger"]: (_("Zoom with two fingers."), _("Pinch to zoom out; spread to zoom in.")), _GG["Zoom2FingerPinch"]: (_("Pinch to zoom out."), _("Pinch to zoom out.")), _GG["Zoom2FingerSpread"]: (_("Spread to zoom in."), _("Spread to zoom in.")), _GG["Zoom3Finger"]: (_("Zoom with three fingers."), None), _GG["Zoom2FingerStateless"]: (_("Zoom with two fingers"), _("Pinch to zoom out; spread to zoom in.")), _GG["TwoFingersPresent"]: (None, None), _GG["Rotate2Finger"]: (None, None), _GG["Finger1"]: (None, None), _GG["Finger2"]: (None, None), _GG["Finger3"]: (None, None), _GG["Finger4"]: (None, None), _GG["Finger5"]: (None, None), _GG["Finger6"]: (None, None), _GG["Finger7"]: (None, None), _GG["Finger8"]: (None, None), _GG["Finger9"]: (None, None), _GG["Finger10"]: (None, None), _GG["DeviceSpecificRawData"]: (None, None), } _GESTURE2_PARAMS_LABELS = { _GP["ExtraCapabilities"]: (None, None), # not supported _GP["PixelZone"]: (_("Pixel zone"), None), # TO DO: replace None with a short description _GP["RatioZone"]: (_("Ratio zone"), None), # TO DO: replace None with a short description _GP["ScaleFactor"]: (_("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(_BitFieldOMSetting): 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.GESTURE _labels = _GESTURE2_GESTURES_LABELS class validator_class(_BitFieldOMV): @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(_BitFieldOMSetting): 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.GESTURE _labels = _GESTURE2_GESTURES_LABELS class validator_class(_BitFieldOMV): @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(_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.PARAM 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(_MultipleRangeV): @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(_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 = _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(_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(_BitFieldV): @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(_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(_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): 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 = _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(_ChoicesMapV): @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(_WARN): 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 = _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(_WARN): logger.warning("unusual persistent remappable action mapping %x: use Default", reply_value) reply_value = _special_keys.KEYS_Default return reply_value class Sidetone(_Setting): name = "sidetone" label = _("Sidetone") description = _("Set sidetone level.") feature = _F.SIDETONE validator_class = _RangeV min_value = 0 max_value = 100 class Equalizer(_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(_PackedRangeV): @classmethod def build(cls, setting_class, device): data = device.feature_request(_F.EQUALIZER, 0x00) if not data: return None count, dbRange, _x, dbMin, dbMax = _unpack("!BBBBB", data[:5]) if dbMin == 0: dbMin = -dbRange if dbMax == 0: dbMax = dbRange map = _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(_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 = _RangeV min_value = 0x00 max_value = 0xFF validator_options = {"byte_count": 1} class LEDControl(_Setting): name = "led_control" label = _("LED Control") description = _("Switch control of LEDs between device and Solaar") feature = _F.COLOR_LED_EFFECTS rw_options = {"read_fnid": 0x70, "write_fnid": 0x80} choices_universe = _NamedInts(Device=0, Solaar=1) validator_class = _ChoicesV 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(_Setting): name = "led_zone_" label = _("LED Zone Effects") description = _("Set effect for LED Zone") feature = _F.COLOR_LED_EFFECTS color_field = {"name": _LEDP.color, "kind": _KIND.choice, "label": None, "choices": colors} speed_field = {"name": _LEDP.speed, "kind": _KIND.range, "label": _("Speed"), "min": 0, "max": 255} period_field = {"name": _LEDP.period, "kind": _KIND.range, "label": _("Period"), "min": 100, "max": 5000} intensity_field = {"name": _LEDP.intensity, "kind": _KIND.range, "label": _("Intensity"), "min": 0, "max": 100} ramp_field = {"name": _LEDP.ramp, "kind": _KIND.choice, "label": _("Ramp"), "choices": hidpp20.LEDRampChoices} # form_field = { 'name': _LEDP.form, 'kind': _KIND.choice, 'label': _('Form'), 'choices': _hidpp20.LEDFormChoices } possible_fields = [color_field, speed_field, period_field, intensity_field, ramp_field] @classmethod def build(cls, device): infos = device.led_effects settings = [] for zone in infos.zones: prefix = _int2bytes(zone.index, 1) rw = _FeatureRW(_F.COLOR_LED_EFFECTS, read_fnid=0xE0, write_fnid=0x30, prefix=prefix) validator = _HeteroV(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] ID_field = {"name": "ID", "kind": _KIND.choice, "label": None, "choices": choices} setting.possible_fields = [ID_field] + cls.possible_fields setting.fields_map = hidpp20.LEDEffects settings.append(setting) return settings 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 SpeedChange, # Backlight, # not working - disabled temporarily Backlight2, # working Backlight2Level, Backlight2DurationHandsOut, Backlight2DurationHandsIn, Backlight2DurationPowered, Backlight3, LEDControl, LEDZoneSetting, 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, sclass): if sclass.feature not in device.features: return if sclass.min_version > device.features.get_feature_version(sclass.feature): return try: detected = sclass.build(device) if logger.isEnabledFor(logging.DEBUG): logger.debug("check_feature %s [%s] detected %s", sclass.name, sclass.feature, detected) return detected except Exception as e: logger.error("check_feature %s [%s] error %s\n%s", sclass.name, sclass.feature, e, _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