From 1018f5514bda045dadcfc427d468a209322fd580 Mon Sep 17 00:00:00 2001 From: Matthias Hagmann <16444067+MattHag@users.noreply.github.com> Date: Wed, 14 Feb 2024 20:37:41 +0100 Subject: [PATCH 01/10] logitech_receiver: Move exceptions into own module Related #1097 --- lib/logitech_receiver/__init__.py | 2 -- lib/logitech_receiver/base.py | 52 +++++++++-------------------- lib/logitech_receiver/device.py | 9 ++--- lib/logitech_receiver/exceptions.py | 33 ++++++++++++++++++ lib/logitech_receiver/hidpp20.py | 32 +++++------------- lib/logitech_receiver/listener.py | 3 +- lib/logitech_receiver/receiver.py | 32 +++++++++--------- lib/solaar/cli/show.py | 4 +-- lib/solaar/listener.py | 11 +++--- 9 files changed, 88 insertions(+), 90 deletions(-) create mode 100644 lib/logitech_receiver/exceptions.py diff --git a/lib/logitech_receiver/__init__.py b/lib/logitech_receiver/__init__.py index e31407edac..e1a193ee04 100644 --- a/lib/logitech_receiver/__init__.py +++ b/lib/logitech_receiver/__init__.py @@ -31,10 +31,8 @@ import logging from . import listener, status # noqa: F401 -from .base import DeviceUnreachable, NoReceiver, NoSuchDevice # noqa: F401 from .common import strhex # noqa: F401 from .device import Device # noqa: F401 -from .hidpp20 import FeatureCallError, FeatureNotSupported # noqa: F401 from .receiver import Receiver # noqa: F401 logger = logging.getLogger(__name__) diff --git a/lib/logitech_receiver/base.py b/lib/logitech_receiver/base.py index 0024b021ec..61fa12fb71 100644 --- a/lib/logitech_receiver/base.py +++ b/lib/logitech_receiver/base.py @@ -30,12 +30,13 @@ import hidapi as _hid -from . import hidpp10 as _hidpp10 +from . import exceptions +from . import hidpp10_constants as _hidpp10_constants from . import hidpp20 as _hidpp20 +from . import hidpp20_constants as _hidpp20_constants from .base_usb import ALL as _RECEIVER_USB_IDS from .base_usb import DEVICES as _DEVICE_IDS from .base_usb import other_device_check as _other_device_check -from .common import KwException as _KwException from .common import strhex as _strhex logger = logging.getLogger(__name__) @@ -69,29 +70,6 @@ # when pinging, be extra patient (no longer) _PING_TIMEOUT = DEFAULT_TIMEOUT -# -# Exceptions that may be raised by this API. -# - - -class NoReceiver(_KwException): - """Raised when trying to talk through a previously open handle, when the - receiver is no longer available. Should only happen if the receiver is - physically disconnected from the machine, or its kernel driver module is - unloaded.""" - pass - - -class NoSuchDevice(_KwException): - """Raised when trying to reach a device number not paired to the receiver.""" - pass - - -class DeviceUnreachable(_KwException): - """Raised when a request is made to an unreachable (turned off) device.""" - pass - - # # # @@ -219,7 +197,7 @@ def write(handle, devnumber, data, long_message=False): except Exception as reason: logger.error('write failed, assuming handle %r no longer available', handle) close(handle) - raise NoReceiver(reason=reason) + raise exceptions.NoReceiver(reason=reason) def read(handle, timeout=DEFAULT_TIMEOUT): @@ -268,7 +246,7 @@ def _read(handle, timeout): except Exception as reason: logger.warning('read failed, assuming handle %r no longer available', handle) close(handle) - raise NoReceiver(reason=reason) + raise exceptions.NoReceiver(reason=reason) if data and check_message(data): # ignore messages that fail check report_id = ord(data[:1]) @@ -299,7 +277,7 @@ def _skip_incoming(handle, ihandle, notifications_hook): except Exception as reason: logger.error('read failed, assuming receiver %s no longer available', handle) close(handle) - raise NoReceiver(reason=reason) + raise exceptions.NoReceiver(reason=reason) if data: if check_message(data): # only process messages that pass check @@ -420,7 +398,7 @@ def request(handle, devnumber, request_id, *params, no_reply=False, return_error notifications_hook = getattr(handle, 'notifications_hook', None) try: _skip_incoming(handle, ihandle, notifications_hook) - except NoReceiver: + except exceptions.NoReceiver: logger.warning('device or receiver disconnected') return None write(ihandle, devnumber, request_data, long_message) @@ -445,15 +423,15 @@ def request(handle, devnumber, request_id, *params, no_reply=False, return_error if logger.isEnabledFor(logging.DEBUG): logger.debug( '(%s) device 0x%02X error on request {%04X}: %d = %s', handle, devnumber, request_id, error, - _hidpp10.ERROR[error] + _hidpp10_constants.ERROR[error] ) - return _hidpp10.ERROR[error] if return_error else None + return _hidpp10_constants.ERROR[error] if return_error else None if reply_data[:1] == b'\xFF' and reply_data[1:3] == request_data[:2]: # a HID++ 2.0 feature call returned with an error error = ord(reply_data[3:4]) logger.error( '(%s) device %d error on feature request {%04X}: %d = %s', handle, devnumber, request_id, error, - _hidpp20.ERROR[error] + _hidpp20_constants.ERROR[error] ) raise _hidpp20.FeatureCallError(number=devnumber, request=request_id, error=error, params=params) @@ -505,7 +483,7 @@ def ping(handle, devnumber, long_message=False): notifications_hook = getattr(handle, 'notifications_hook', None) try: _skip_incoming(handle, int(handle), notifications_hook) - except NoReceiver: + except exceptions.NoReceiver: logger.warning('device or receiver disconnected') return @@ -530,13 +508,13 @@ def ping(handle, devnumber, long_message=False): if report_id == HIDPP_SHORT_MESSAGE_ID and reply_data[:1] == b'\x8F' and \ reply_data[1:3] == request_data[:2]: # error response error = ord(reply_data[3:4]) - if error == _hidpp10.ERROR.invalid_SubID__command: # a valid reply from a HID++ 1.0 device + if error == _hidpp10_constants.ERROR.invalid_SubID__command: # a valid reply from a HID++ 1.0 device return 1.0 - if error == _hidpp10.ERROR.resource_error or error == _hidpp10.ERROR.connection_request_failed: + if error == _hidpp10_constants.ERROR.resource_error or error == _hidpp10_constants.ERROR.connection_request_failed: return # device unreachable - if error == _hidpp10.ERROR.unknown_device: # no paired device with that number + if error == _hidpp10_constants.ERROR.unknown_device: # no paired device with that number logger.error('(%s) device %d error on ping request: unknown device', handle, devnumber) - raise NoSuchDevice(number=devnumber, request=request_id) + raise exceptions.NoSuchDevice(number=devnumber, request=request_id) if notifications_hook: n = make_notification(report_id, reply_devnumber, reply_data) diff --git a/lib/logitech_receiver/device.py b/lib/logitech_receiver/device.py index 18799f8959..a0cdb51652 100644 --- a/lib/logitech_receiver/device.py +++ b/lib/logitech_receiver/device.py @@ -27,6 +27,7 @@ from . import base as _base from . import descriptors as _descriptors +from . import exceptions from . import hidpp10 as _hidpp10 from . import hidpp20 as _hidpp20 from .common import strhex as _strhex @@ -128,7 +129,7 @@ def __init__( self.wpid = _hid.find_paired_node_wpid(receiver.path, number) if not self.wpid: logger.error('Unable to get wpid from udev for device %d of %s', number, receiver) - raise _base.NoSuchDevice(number=number, receiver=receiver, error='Not present 27Mhz device') + raise exceptions.NoSuchDevice(number=number, receiver=receiver, error='Not present 27Mhz device') kind = receiver.get_kind_from_index(number) self._kind = _hidpp10.DEVICE_KIND[kind] else: # get information from pairing registers @@ -136,7 +137,7 @@ def __init__( self.update_pairing_information() self.update_extended_pairing_information() if not self.wpid and not self._serial: # if neither then the device almost certainly wasn't found - raise _base.NoSuchDevice(number=number, receiver=receiver, error='no wpid or serial') + raise exceptions.NoSuchDevice(number=number, receiver=receiver, error='no wpid or serial') # the wpid is set to None on this object when the device is unpaired assert self.wpid is not None, 'failed to read wpid: device %d of %s' % (number, receiver) @@ -208,7 +209,7 @@ def name(self): if not self.online: # be very defensive try: self.ping() - except _base.NoSuchDevice: + except exceptions.NoSuchDevice: pass if self.online and self.protocol >= 2.0: self._name = _hidpp20.get_name(self) @@ -514,7 +515,7 @@ def __bool__(self): def __str__(self): try: name = self.name or self.codename or '?' - except _base.NoSuchDevice: + except exceptions.NoSuchDevice: name = 'name not available' return '' % (self.number, self.wpid or self.product_id, name, self.serial) diff --git a/lib/logitech_receiver/exceptions.py b/lib/logitech_receiver/exceptions.py new file mode 100644 index 0000000000..5ae67c5975 --- /dev/null +++ b/lib/logitech_receiver/exceptions.py @@ -0,0 +1,33 @@ +from .common import KwException as _KwException + +# +# Exceptions that may be raised by this API. +# + + +class NoReceiver(_KwException): + """Raised when trying to talk through a previously open handle, when the + receiver is no longer available. Should only happen if the receiver is + physically disconnected from the machine, or its kernel driver module is + unloaded.""" + pass + + +class NoSuchDevice(_KwException): + """Raised when trying to reach a device number not paired to the receiver.""" + pass + + +class DeviceUnreachable(_KwException): + """Raised when a request is made to an unreachable (turned off) device.""" + pass + + +class FeatureNotSupported(_KwException): + """Raised when trying to request a feature not supported by the device.""" + pass + + +class FeatureCallError(_KwException): + """Raised if the device replied to a feature call with an error.""" + pass diff --git a/lib/logitech_receiver/hidpp20.py b/lib/logitech_receiver/hidpp20.py index 4032d1c96c..0d4b0c37a8 100644 --- a/lib/logitech_receiver/hidpp20.py +++ b/lib/logitech_receiver/hidpp20.py @@ -27,10 +27,9 @@ import yaml as _yaml -from . import special_keys +from . import exceptions, special_keys from .common import BATTERY_APPROX as _BATTERY_APPROX from .common import FirmwareInfo as _FirmwareInfo -from .common import KwException as _KwException from .common import NamedInt as _NamedInt from .common import NamedInts as _NamedInts from .common import UnsortedNamedInts as _UnsortedNamedInts @@ -227,21 +226,6 @@ def hexint_presenter(dumper, data): # -class FeatureNotSupported(_KwException): - """Raised when trying to request a feature not supported by the device.""" - pass - - -class FeatureCallError(_KwException): - """Raised if the device replied to a feature call with an error.""" - pass - - -# -# -# - - class FeaturesArray(dict): def __init__(self, device): @@ -462,8 +446,8 @@ def _getCidReporting(self): mapping_flags_2 = 0 self._mapping_flags = mapping_flags_1 | (mapping_flags_2 << 8) else: - raise FeatureCallError(msg='No reply from device.') - except FeatureCallError: # if the key hasn't ever been configured then the read may fail so only produce a warning + raise exceptions.FeatureCallError(msg='No reply from device.') + except exceptions.FeatureCallError: # if the key hasn't ever been configured then the read may fail so only produce a warning if logger.isEnabledFor(logging.WARNING): logger.warn( f'Feature Call Error in _getCidReporting on device {self._device} for cid {self._cid} - use defaults' @@ -498,7 +482,7 @@ def _setCidReporting(self, flags=None, remap=0): bfield = 0 for f, v in flags.items(): if v and FLAG_TO_CAPABILITY[f] not in self.flags: - raise FeatureNotSupported( + raise exceptions.FeatureNotSupported( msg=f'Tried to set mapping flag "{f}" on control "{self.key}" ' + f'which does not support "{FLAG_TO_CAPABILITY[f]}" on device {self._device}.' ) @@ -511,7 +495,7 @@ def _setCidReporting(self, flags=None, remap=0): self._mapping_flags &= ~int(f) if remap != 0 and remap not in self.remappable_to: - raise FeatureNotSupported( + raise exceptions.FeatureNotSupported( msg=f'Tried to remap control "{self.key}" to a control ID {remap} which it is not remappable to ' + f'on device {self._device}.' ) @@ -1044,7 +1028,7 @@ def value(self): def read(self): try: value = feature_request(self._device, FEATURE.GESTURE_2, 0x50, self.id, 0xFF) - except FeatureCallError: # some calls produce an error (notably spec 5 multiplier on K400Plus) + except exceptions.FeatureCallError: # some calls produce an error (notably spec 5 multiplier on K400Plus) if logger.isEnabledFor(logging.WARNING): logger.warn(f'Feature Call Error reading Gesture Spec on device {self._device} for spec {self.id} - use None') return None @@ -1129,7 +1113,7 @@ class Backlight: def __init__(self, device): response = device.feature_request(FEATURE.BACKLIGHT2, 0x00) if not response: - raise FeatureCallError(msg='No reply from device.') + raise exceptions.FeatureCallError(msg='No reply from device.') self.device = device self.enabled, self.options, supported, effects, self.level, self.dho, self.dhi, self.dpow = _unpack( ' %s', self, 'enabled' if enable else 'disabled', flag_names) return flag_bits @@ -154,33 +156,33 @@ def device_pairing_information(self, n): pair_info = self.read_register(_R.receiver_info, _IR.bolt_pairing_information + n) if pair_info: wpid = _strhex(pair_info[3:4]) + _strhex(pair_info[2:3]) - kind = _hidpp10.DEVICE_KIND[ord(pair_info[1:2]) & 0x0F] + kind = _hidpp10_constants.DEVICE_KIND[ord(pair_info[1:2]) & 0x0F] return wpid, kind, 0 else: - raise _base.NoSuchDevice(number=n, receiver=self, error='read Bolt wpid') + raise exceptions.NoSuchDevice(number=n, receiver=self, error='read Bolt wpid') wpid = 0 kind = None polling_rate = None pair_info = self.read_register(_R.receiver_info, _IR.pairing_information + n - 1) if pair_info: # may be either a Unifying receiver, or an Unifying-ready receiver wpid = _strhex(pair_info[3:5]) - kind = _hidpp10.DEVICE_KIND[ord(pair_info[7:8]) & 0x0F] + kind = _hidpp10_constants.DEVICE_KIND[ord(pair_info[7:8]) & 0x0F] polling_rate = str(ord(pair_info[2:3])) + 'ms' elif self.receiver_kind == '27Mz': # 27Mhz receiver, fill extracting WPID from udev path wpid = _hid.find_paired_node_wpid(self.path, n) if not wpid: logger.error('Unable to get wpid from udev for device %d of %s', n, self) - raise _base.NoSuchDevice(number=n, receiver=self, error='Not present 27Mhz device') + raise exceptions.NoSuchDevice(number=n, receiver=self, error='Not present 27Mhz device') kind = _hidpp10.DEVICE_KIND[self.get_kind_from_index(n)] elif not self.receiver_kind == 'unifying': # unifying protocol not supported, may be an old Nano receiver device_info = self.read_register(_R.receiver_info, 0x04) if device_info: wpid = _strhex(device_info[3:5]) - kind = _hidpp10.DEVICE_KIND[0x00] # unknown kind + kind = _hidpp10_constants.DEVICE_KIND[0x00] # unknown kind else: - raise _base.NoSuchDevice(number=n, receiver=self, error='read pairing information - non-unifying') + raise exceptions.NoSuchDevice(number=n, receiver=self, error='read pairing information - non-unifying') else: - raise _base.NoSuchDevice(number=n, receiver=self, error='read pairing information') + raise exceptions.NoSuchDevice(number=n, receiver=self, error='read pairing information') return wpid, kind, polling_rate def device_extended_pairing_information(self, n): @@ -217,7 +219,7 @@ def get_kind_from_index(self, index): kind = 3 else: # unknown device number on 27Mhz receiver logger.error('failed to calculate device kind for device %d of %s', index, self) - raise _base.NoSuchDevice(number=index, receiver=self, error='Unknown 27Mhz device number') + raise exceptions.NoSuchDevice(number=index, receiver=self, error='Unknown 27Mhz device number') return kind def notify_devices(self): @@ -239,7 +241,7 @@ def register_new_device(self, number, notification=None): logger.info('%s: found new device %d (%s)', self, number, dev.wpid) self._devices[number] = dev return dev - except _base.NoSuchDevice as e: + except exceptions.NoSuchDevice as e: logger.warning('register new device failed for %s device %d error %s', e.receiver, e.number, e.error) logger.warning('%s: looked for device %d, not found', self, number) diff --git a/lib/solaar/cli/show.py b/lib/solaar/cli/show.py index 9b204d0ab1..307366ffe1 100644 --- a/lib/solaar/cli/show.py +++ b/lib/solaar/cli/show.py @@ -16,7 +16,7 @@ ## with this program; if not, write to the Free Software Foundation, Inc., ## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -from logitech_receiver import base as _base +from logitech_receiver import exceptions from logitech_receiver import hidpp10 as _hidpp10 from logitech_receiver import hidpp20 as _hidpp20 from logitech_receiver import receiver as _receiver @@ -85,7 +85,7 @@ def _print_device(dev, num=None): # try to ping the device to see if it actually exists and to wake it up try: dev.ping() - except _base.NoSuchDevice: + except exceptions.NoSuchDevice: print(' %s: Device not found' % num or dev.number) return diff --git a/lib/solaar/listener.py b/lib/solaar/listener.py index 3ddba6d5be..c3d7580d2f 100644 --- a/lib/solaar/listener.py +++ b/lib/solaar/listener.py @@ -26,7 +26,8 @@ from logitech_receiver import Device, Receiver from logitech_receiver import base as _base -from logitech_receiver import hidpp10 as _hidpp10 +from logitech_receiver import exceptions +from logitech_receiver import hidpp10_constants as _hidpp10_constants from logitech_receiver import listener as _listener from logitech_receiver import notifications as _notifications from logitech_receiver import status as _status @@ -40,8 +41,8 @@ logger = logging.getLogger(__name__) -_R = _hidpp10.REGISTERS -_IR = _hidpp10.INFO_SUBREGISTERS +_R = _hidpp10_constants.REGISTERS +_IR = _hidpp10_constants.INFO_SUBREGISTERS # # @@ -87,7 +88,7 @@ def has_started(self): logger.info('%s: notifications listener has started (%s)', self.receiver, self.receiver.handle) nfs = self.receiver.enable_connection_notifications() if logger.isEnabledFor(logging.WARNING): - if not self.receiver.isDevice and not ((nfs if nfs else 0) & _hidpp10.NOTIFICATION_FLAG.wireless): + if not self.receiver.isDevice and not ((nfs if nfs else 0) & _hidpp10_constants.NOTIFICATION_FLAG.wireless): logger.warning( 'Receiver on %s might not support connection notifications, GUI might not show its devices', self.receiver.path @@ -402,7 +403,7 @@ def _process_add(device_info, retry): _error_callback('permissions', device_info.path) else: _error_callback('nodevice', device_info.path) - except _base.NoReceiver: + except exceptions.NoReceiver: _error_callback('nodevice', device_info.path) From 3513d6e604144e2b7786ec3be28d08ab26cba326 Mon Sep 17 00:00:00 2001 From: Matthias Hagmann <16444067+MattHag@users.noreply.github.com> Date: Wed, 14 Feb 2024 20:36:42 +0100 Subject: [PATCH 02/10] logitech_receiver: Move hidpp10 constants into new module Related #1097 --- lib/logitech_receiver/descriptors.py | 4 +- lib/logitech_receiver/device.py | 22 +-- lib/logitech_receiver/hidpp10.py | 152 +------------------- lib/logitech_receiver/hidpp10_constants.py | 151 +++++++++++++++++++ lib/logitech_receiver/receiver.py | 5 +- lib/logitech_receiver/settings_templates.py | 6 +- lib/logitech_receiver/status.py | 19 +-- lib/solaar/cli/pair.py | 13 +- lib/solaar/cli/probe.py | 12 +- lib/solaar/cli/show.py | 9 +- 10 files changed, 201 insertions(+), 192 deletions(-) create mode 100644 lib/logitech_receiver/hidpp10_constants.py diff --git a/lib/logitech_receiver/descriptors.py b/lib/logitech_receiver/descriptors.py index 6c709ad987..bae4e7bfc5 100644 --- a/lib/logitech_receiver/descriptors.py +++ b/lib/logitech_receiver/descriptors.py @@ -28,8 +28,8 @@ from . import settings_templates as _ST from .common import NamedInts as _NamedInts -from .hidpp10 import DEVICE_KIND as _DK -from .hidpp10 import REGISTERS as _R +from .hidpp10_constants import DEVICE_KIND as _DK +from .hidpp10_constants import REGISTERS as _R # # diff --git a/lib/logitech_receiver/device.py b/lib/logitech_receiver/device.py index a0cdb51652..73dbd73896 100644 --- a/lib/logitech_receiver/device.py +++ b/lib/logitech_receiver/device.py @@ -29,16 +29,18 @@ from . import descriptors as _descriptors from . import exceptions from . import hidpp10 as _hidpp10 +from . import hidpp10_constants as _hidpp10_constants from . import hidpp20 as _hidpp20 +from . import hidpp20_constants as _hidpp20_constants from .common import strhex as _strhex from .settings_templates import check_feature_settings as _check_feature_settings logger = logging.getLogger(__name__) -_R = _hidpp10.REGISTERS -_IR = _hidpp10.INFO_SUBREGISTERS +_R = _hidpp10_constants.REGISTERS +_IR = _hidpp10_constants.INFO_SUBREGISTERS -KIND_MAP = {kind: _hidpp10.DEVICE_KIND[str(kind)] for kind in _hidpp20.DEVICE_KIND} +KIND_MAP = {kind: _hidpp10_constants.DEVICE_KIND[str(kind)] for kind in _hidpp20_constants.DEVICE_KIND} # # @@ -124,14 +126,14 @@ def __init__( if receiver.receiver_kind == '27Mhz': # 27 Mhz receiver self.wpid = '00' + _strhex(link_notification.data[2:3]) kind = receiver.get_kind_from_index(number) - self._kind = _hidpp10.DEVICE_KIND[kind] + self._kind = _hidpp10_constants.DEVICE_KIND[kind] elif receiver.receiver_kind == '27Mhz': # 27 Mhz receiver doesn't have pairing registers self.wpid = _hid.find_paired_node_wpid(receiver.path, number) if not self.wpid: logger.error('Unable to get wpid from udev for device %d of %s', number, receiver) raise exceptions.NoSuchDevice(number=number, receiver=receiver, error='Not present 27Mhz device') kind = receiver.get_kind_from_index(number) - self._kind = _hidpp10.DEVICE_KIND[kind] + self._kind = _hidpp10_constants.DEVICE_KIND[kind] else: # get information from pairing registers self.online = True self.update_pairing_information() @@ -416,10 +418,10 @@ def enable_connection_notifications(self, enable=True): if enable: set_flag_bits = ( - _hidpp10.NOTIFICATION_FLAG.battery_status - | _hidpp10.NOTIFICATION_FLAG.keyboard_illumination - | _hidpp10.NOTIFICATION_FLAG.wireless - | _hidpp10.NOTIFICATION_FLAG.software_present + _hidpp10_constants.NOTIFICATION_FLAG.battery_status + | _hidpp10_constants.NOTIFICATION_FLAG.keyboard_illumination + | _hidpp10_constants.NOTIFICATION_FLAG.wireless + | _hidpp10_constants.NOTIFICATION_FLAG.software_present ) else: set_flag_bits = 0 @@ -428,7 +430,7 @@ def enable_connection_notifications(self, enable=True): logger.warning('%s: failed to %s device notifications', self, 'enable' if enable else 'disable') flag_bits = _hidpp10.get_notification_flags(self) - flag_names = None if flag_bits is None else tuple(_hidpp10.NOTIFICATION_FLAG.flag_names(flag_bits)) + flag_names = None if flag_bits is None else tuple(_hidpp10_constants.NOTIFICATION_FLAG.flag_names(flag_bits)) if logger.isEnabledFor(logging.INFO): logger.info('%s: device notifications %s %s', self, 'enabled' if enable else 'disabled', flag_names) return flag_bits if ok else None diff --git a/lib/logitech_receiver/hidpp10.py b/lib/logitech_receiver/hidpp10.py index 51de5c2870..2a6873d1e3 100644 --- a/lib/logitech_receiver/hidpp10.py +++ b/lib/logitech_receiver/hidpp10.py @@ -20,164 +20,14 @@ from .common import BATTERY_APPROX as _BATTERY_APPROX from .common import FirmwareInfo as _FirmwareInfo -from .common import NamedInts as _NamedInts from .common import bytes2int as _bytes2int from .common import int2bytes as _int2bytes from .common import strhex as _strhex from .hidpp20 import BATTERY_STATUS, FIRMWARE_KIND +from .hidpp10_constants import REGISTERS logger = logging.getLogger(__name__) -# -# Constants - most of them as defined by the official Logitech HID++ 1.0 -# documentation, some of them guessed. -# - -DEVICE_KIND = _NamedInts( - unknown=0x00, - keyboard=0x01, - mouse=0x02, - numpad=0x03, - presenter=0x04, - remote=0x07, - trackball=0x08, - touchpad=0x09, - headset=0x0D, # not from Logitech documentation - remote_control=0x0E, # for compatibility with HID++ 2.0 - receiver=0x0F # for compatibility with HID++ 2.0 -) - -POWER_SWITCH_LOCATION = _NamedInts( - base=0x01, - top_case=0x02, - edge_of_top_right_corner=0x03, - top_left_corner=0x05, - bottom_left_corner=0x06, - top_right_corner=0x07, - bottom_right_corner=0x08, - top_edge=0x09, - right_edge=0x0A, - left_edge=0x0B, - bottom_edge=0x0C -) - -# Some flags are used both by devices and receivers. The Logitech documentation -# mentions that the first and last (third) byte are used for devices while the -# second is used for the receiver. In practise, the second byte is also used for -# some device-specific notifications (keyboard illumination level). Do not -# simply set all notification bits if the software does not support it. For -# example, enabling keyboard_sleep_raw makes the Sleep key a no-operation unless -# the software is updated to handle that event. -# Observations: -# - wireless and software present were seen on receivers, reserved_r1b4 as well -# - the rest work only on devices as far as we can tell right now -# In the future would be useful to have separate enums for receiver and device notification flags, -# but right now we don't know enough. -# additional flags taken from https://drive.google.com/file/d/0BxbRzx7vEV7eNDBheWY0UHM5dEU/view?usp=sharing -NOTIFICATION_FLAG = _NamedInts( - numpad_numerical_keys=0x800000, - f_lock_status=0x400000, - roller_H=0x200000, - battery_status=0x100000, # send battery charge notifications (0x07 or 0x0D) - mouse_extra_buttons=0x080000, - roller_V=0x040000, - keyboard_sleep_raw=0x020000, # system control keys such as Sleep - keyboard_multimedia_raw=0x010000, # consumer controls such as Mute and Calculator - # reserved_r1b4= 0x001000, # unknown, seen on a unifying receiver - reserved5=0x008000, - reserved4=0x004000, - reserved3=0x002000, - reserved2=0x001000, - software_present=0x000800, # .. no idea - reserved1=0x000400, - keyboard_illumination=0x000200, # illumination brightness level changes (by pressing keys) - wireless=0x000100, # notify when the device wireless goes on/off-line - mx_air_3d_gesture=0x000001, -) - -ERROR = _NamedInts( - invalid_SubID__command=0x01, - invalid_address=0x02, - invalid_value=0x03, - connection_request_failed=0x04, - too_many_devices=0x05, - already_exists=0x06, - busy=0x07, - unknown_device=0x08, - resource_error=0x09, - request_unavailable=0x0A, - unsupported_parameter_value=0x0B, - wrong_pin_code=0x0C -) - -PAIRING_ERRORS = _NamedInts(device_timeout=0x01, device_not_supported=0x02, too_many_devices=0x03, sequence_timeout=0x06) -BOLT_PAIRING_ERRORS = _NamedInts(device_timeout=0x01, failed=0x02) -"""Known registers. -Devices usually have a (small) sub-set of these. Some registers are only -applicable to certain device kinds (e.g. smooth_scroll only applies to mice.""" -REGISTERS = _NamedInts( - # only apply to receivers - receiver_connection=0x02, - receiver_pairing=0xB2, - devices_activity=0x2B3, - receiver_info=0x2B5, - bolt_device_discovery=0xC0, - bolt_pairing=0x2C1, - bolt_uniqueId=0x02FB, - - # only apply to devices - mouse_button_flags=0x01, - keyboard_hand_detection=0x01, - battery_status=0x07, - keyboard_fn_swap=0x09, - battery_charge=0x0D, - keyboard_illumination=0x17, - three_leds=0x51, - mouse_dpi=0x63, - - # apply to both - notifications=0x00, - firmware=0xF1, - - # notifications - passkey_request_notification=0x4D, - passkey_pressed_notification=0x4E, - device_discovery_notification=0x4F, - discovery_status_notification=0x53, - pairing_status_notification=0x54, -) -# Subregisters for receiver_info register -INFO_SUBREGISTERS = _NamedInts( - serial_number=0x01, # not found on many receivers - fw_version=0x02, - receiver_information=0x03, - pairing_information=0x20, # 0x2N, by connected device - extended_pairing_information=0x30, # 0x3N, by connected device - device_name=0x40, # 0x4N, by connected device - bolt_pairing_information=0x50, # 0x5N, by connected device - bolt_device_name=0x60, # 0x6N01, by connected device, -) - -# Flags taken from https://drive.google.com/file/d/0BxbRzx7vEV7eNDBheWY0UHM5dEU/view?usp=sharing -DEVICE_FEATURES = _NamedInts( - reserved1=0x010000, - special_buttons=0x020000, - enhanced_key_usage=0x040000, - fast_fw_rev=0x080000, - reserved2=0x100000, - reserved3=0x200000, - scroll_accel=0x400000, - buttons_control_resolution=0x800000, - inhibit_lock_key_sound=0x000001, - reserved4=0x000002, - mx_air_3d_engine=0x000004, - host_control_leds=0x000008, - reserved5=0x000010, - reserved6=0x000020, - reserved7=0x000040, - reserved8=0x000080, -) - # # functions # diff --git a/lib/logitech_receiver/hidpp10_constants.py b/lib/logitech_receiver/hidpp10_constants.py new file mode 100644 index 0000000000..eb1c3ad807 --- /dev/null +++ b/lib/logitech_receiver/hidpp10_constants.py @@ -0,0 +1,151 @@ +from .common import NamedInts + +# +# Constants - most of them as defined by the official Logitech HID++ 1.0 +# documentation, some of them guessed. +# + +DEVICE_KIND = NamedInts( + unknown=0x00, + keyboard=0x01, + mouse=0x02, + numpad=0x03, + presenter=0x04, + remote=0x07, + trackball=0x08, + touchpad=0x09, + headset=0x0D, # not from Logitech documentation + remote_control=0x0E, # for compatibility with HID++ 2.0 + receiver=0x0F # for compatibility with HID++ 2.0 +) + +POWER_SWITCH_LOCATION = NamedInts( + base=0x01, + top_case=0x02, + edge_of_top_right_corner=0x03, + top_left_corner=0x05, + bottom_left_corner=0x06, + top_right_corner=0x07, + bottom_right_corner=0x08, + top_edge=0x09, + right_edge=0x0A, + left_edge=0x0B, + bottom_edge=0x0C +) + +# Some flags are used both by devices and receivers. The Logitech documentation +# mentions that the first and last (third) byte are used for devices while the +# second is used for the receiver. In practise, the second byte is also used for +# some device-specific notifications (keyboard illumination level). Do not +# simply set all notification bits if the software does not support it. For +# example, enabling keyboard_sleep_raw makes the Sleep key a no-operation unless +# the software is updated to handle that event. +# Observations: +# - wireless and software present were seen on receivers, reserved_r1b4 as well +# - the rest work only on devices as far as we can tell right now +# In the future would be useful to have separate enums for receiver and device notification flags, +# but right now we don't know enough. +# additional flags taken from https://drive.google.com/file/d/0BxbRzx7vEV7eNDBheWY0UHM5dEU/view?usp=sharing +NOTIFICATION_FLAG = NamedInts( + numpad_numerical_keys=0x800000, + f_lock_status=0x400000, + roller_H=0x200000, + battery_status=0x100000, # send battery charge notifications (0x07 or 0x0D) + mouse_extra_buttons=0x080000, + roller_V=0x040000, + keyboard_sleep_raw=0x020000, # system control keys such as Sleep + keyboard_multimedia_raw=0x010000, # consumer controls such as Mute and Calculator + # reserved_r1b4= 0x001000, # unknown, seen on a unifying receiver + reserved5=0x008000, + reserved4=0x004000, + reserved3=0x002000, + reserved2=0x001000, + software_present=0x000800, # .. no idea + reserved1=0x000400, + keyboard_illumination=0x000200, # illumination brightness level changes (by pressing keys) + wireless=0x000100, # notify when the device wireless goes on/off-line + mx_air_3d_gesture=0x000001, +) + +ERROR = NamedInts( + invalid_SubID__command=0x01, + invalid_address=0x02, + invalid_value=0x03, + connection_request_failed=0x04, + too_many_devices=0x05, + already_exists=0x06, + busy=0x07, + unknown_device=0x08, + resource_error=0x09, + request_unavailable=0x0A, + unsupported_parameter_value=0x0B, + wrong_pin_code=0x0C +) + +PAIRING_ERRORS = NamedInts(device_timeout=0x01, device_not_supported=0x02, too_many_devices=0x03, sequence_timeout=0x06) +BOLT_PAIRING_ERRORS = NamedInts(device_timeout=0x01, failed=0x02) +"""Known registers. +Devices usually have a (small) sub-set of these. Some registers are only +applicable to certain device kinds (e.g. smooth_scroll only applies to mice.""" +REGISTERS = NamedInts( + # only apply to receivers + receiver_connection=0x02, + receiver_pairing=0xB2, + devices_activity=0x2B3, + receiver_info=0x2B5, + bolt_device_discovery=0xC0, + bolt_pairing=0x2C1, + bolt_uniqueId=0x02FB, + + # only apply to devices + mouse_button_flags=0x01, + keyboard_hand_detection=0x01, + battery_status=0x07, + keyboard_fn_swap=0x09, + battery_charge=0x0D, + keyboard_illumination=0x17, + three_leds=0x51, + mouse_dpi=0x63, + + # apply to both + notifications=0x00, + firmware=0xF1, + + # notifications + passkey_request_notification=0x4D, + passkey_pressed_notification=0x4E, + device_discovery_notification=0x4F, + discovery_status_notification=0x53, + pairing_status_notification=0x54, +) +# Subregisters for receiver_info register +INFO_SUBREGISTERS = NamedInts( + serial_number=0x01, # not found on many receivers + fw_version=0x02, + receiver_information=0x03, + pairing_information=0x20, # 0x2N, by connected device + extended_pairing_information=0x30, # 0x3N, by connected device + device_name=0x40, # 0x4N, by connected device + bolt_pairing_information=0x50, # 0x5N, by connected device + bolt_device_name=0x60, # 0x6N01, by connected device, +) + +# Flags taken from https://drive.google.com/file/d/0BxbRzx7vEV7eNDBheWY0UHM5dEU/view?usp=sharing +DEVICE_FEATURES = NamedInts( + reserved1=0x010000, + special_buttons=0x020000, + enhanced_key_usage=0x040000, + fast_fw_rev=0x080000, + reserved2=0x100000, + reserved3=0x200000, + scroll_accel=0x400000, + buttons_control_resolution=0x800000, + inhibit_lock_key_sound=0x000001, + reserved4=0x000002, + mx_air_3d_engine=0x000004, + host_control_leds=0x000008, + reserved5=0x000010, + reserved6=0x000020, + reserved7=0x000040, + reserved8=0x000080, +) diff --git a/lib/logitech_receiver/receiver.py b/lib/logitech_receiver/receiver.py index fabe3e65d4..c1f6ebfcdd 100644 --- a/lib/logitech_receiver/receiver.py +++ b/lib/logitech_receiver/receiver.py @@ -173,7 +173,8 @@ def device_pairing_information(self, n): if not wpid: logger.error('Unable to get wpid from udev for device %d of %s', n, self) raise exceptions.NoSuchDevice(number=n, receiver=self, error='Not present 27Mhz device') - kind = _hidpp10.DEVICE_KIND[self.get_kind_from_index(n)] + kind = _hidpp10_constants.DEVICE_KIND[self.get_kind_from_index(n)] + elif not self.receiver_kind == 'unifying': # unifying protocol not supported, may be an old Nano receiver device_info = self.read_register(_R.receiver_info, 0x04) if device_info: @@ -197,7 +198,7 @@ def device_extended_pairing_information(self, n): return '?', power_switch pair_info = self.read_register(_R.receiver_info, _IR.extended_pairing_information + n - 1) if pair_info: - power_switch = _hidpp10.POWER_SWITCH_LOCATION[ord(pair_info[9:10]) & 0x0F] + power_switch = _hidpp10_constants.POWER_SWITCH_LOCATION[ord(pair_info[9:10]) & 0x0F] else: # some Nano receivers? pair_info = self.read_register(0x2D5) if pair_info: diff --git a/lib/logitech_receiver/settings_templates.py b/lib/logitech_receiver/settings_templates.py index 15b65c5d62..2dd600604b 100644 --- a/lib/logitech_receiver/settings_templates.py +++ b/lib/logitech_receiver/settings_templates.py @@ -23,7 +23,7 @@ from struct import unpack as _unpack from time import time as _time -from . import hidpp10 as _hidpp10 +from . import hidpp10_constants as _hidpp10_constants from . import hidpp20 as _hidpp20 from . import special_keys as _special_keys from .common import NamedInt as _NamedInt @@ -53,8 +53,8 @@ logger = logging.getLogger(__name__) -_DK = _hidpp10.DEVICE_KIND -_R = _hidpp10.REGISTERS +_DK = _hidpp10_constants.DEVICE_KIND +_R = _hidpp10_constants.REGISTERS _F = _hidpp20.FEATURE _GG = _hidpp20.GESTURE diff --git a/lib/logitech_receiver/status.py b/lib/logitech_receiver/status.py index 60d5837f6c..324a5a09d0 100644 --- a/lib/logitech_receiver/status.py +++ b/lib/logitech_receiver/status.py @@ -21,7 +21,8 @@ from time import time as _timestamp from . import hidpp10 as _hidpp10 -from . import hidpp20 as _hidpp20 +from . import hidpp10_constants as _hidpp10_constants +from . import hidpp20_constants as _hidpp20_constants from . import settings as _settings from .common import BATTERY_APPROX as _BATTERY_APPROX from .common import NamedInt as _NamedInt @@ -30,7 +31,7 @@ logger = logging.getLogger(__name__) -_R = _hidpp10.REGISTERS +_R = _hidpp10_constants.REGISTERS # # @@ -199,11 +200,11 @@ def set_battery_info(self, level, nextLevel, status, voltage, timestamp=None): # Some notifications may come with no battery level info, just # charging state info, so do our best to infer a level (even if it is just the last level) # It is not always possible to do this well - if status == _hidpp20.BATTERY_STATUS.full: + if status == _hidpp20_constants.BATTERY_STATUS.full: level = _BATTERY_APPROX.full - elif status in (_hidpp20.BATTERY_STATUS.almost_full, _hidpp20.BATTERY_STATUS.recharging): + elif status in (_hidpp20_constants.BATTERY_STATUS.almost_full, _hidpp20_constants.BATTERY_STATUS.recharging): level = _BATTERY_APPROX.good - elif status == _hidpp20.BATTERY_STATUS.slow_recharge: + elif status == _hidpp20_constants.BATTERY_STATUS.slow_recharge: level = _BATTERY_APPROX.low else: level = self.get(KEYS.BATTERY_LEVEL) @@ -217,15 +218,15 @@ def set_battery_info(self, level, nextLevel, status, voltage, timestamp=None): old_voltage, self[KEYS.BATTERY_VOLTAGE] = self.get(KEYS.BATTERY_VOLTAGE), voltage charging = status in ( - _hidpp20.BATTERY_STATUS.recharging, _hidpp20.BATTERY_STATUS.almost_full, _hidpp20.BATTERY_STATUS.full, - _hidpp20.BATTERY_STATUS.slow_recharge + _hidpp20_constants.BATTERY_STATUS.recharging, _hidpp20_constants.BATTERY_STATUS.almost_full, _hidpp20_constants.BATTERY_STATUS.full, + _hidpp20_constants.BATTERY_STATUS.slow_recharge ) old_charging, self[KEYS.BATTERY_CHARGING] = self.get(KEYS.BATTERY_CHARGING), charging changed = old_level != level or old_status != status or old_charging != charging or old_voltage != voltage alert, reason = ALERT.NONE, None - if _hidpp20.BATTERY_OK(status) and (level is None or level > _BATTERY_ATTENTION_LEVEL): + if _hidpp20_constants.BATTERY_OK(status) and (level is None or level > _BATTERY_ATTENTION_LEVEL): self[KEYS.ERROR] = None else: logger.warning('%s: battery %d%%, ALERT %s', self._device, level, status) @@ -284,7 +285,7 @@ def changed(self, active=None, alert=ALERT.NONE, reason=None, push=False, timest # when devices request software reconfiguration # and when devices become active if they don't have wireless device status feature, if was_active is None or push or not was_active and ( - not d.features or _hidpp20.FEATURE.WIRELESS_DEVICE_STATUS not in d.features + not d.features or _hidpp20_constants.FEATURE.WIRELESS_DEVICE_STATUS not in d.features ): if logger.isEnabledFor(logging.INFO): logger.info('%s pushing device settings %s', d, d.settings) diff --git a/lib/solaar/cli/pair.py b/lib/solaar/cli/pair.py index b63953ff96..a12ae555f6 100644 --- a/lib/solaar/cli/pair.py +++ b/lib/solaar/cli/pair.py @@ -20,10 +20,11 @@ from logitech_receiver import base as _base from logitech_receiver import hidpp10 as _hidpp10 +from logitech_receiver import hidpp10_constants as _hidpp10_constants from logitech_receiver import notifications as _notifications from logitech_receiver import status as _status -_R = _hidpp10.REGISTERS +_R = _hidpp10_constants.REGISTERS def run(receivers, args, find_receiver, _ignore): @@ -42,8 +43,8 @@ def run(receivers, args, find_receiver, _ignore): # check if it's necessary to set the notification flags old_notification_flags = _hidpp10.get_notification_flags(receiver) or 0 - if not (old_notification_flags & _hidpp10.NOTIFICATION_FLAG.wireless): - _hidpp10.set_notification_flags(receiver, old_notification_flags | _hidpp10.NOTIFICATION_FLAG.wireless) + if not (old_notification_flags & _hidpp10_constants.NOTIFICATION_FLAG.wireless): + _hidpp10.set_notification_flags(receiver, old_notification_flags | _hidpp10_constants.NOTIFICATION_FLAG.wireless) # get all current devices known_devices = [dev.number for dev in receiver] @@ -85,7 +86,9 @@ def notifications_hook(self, n): kind = receiver.status.device_kind print(f'Bolt Pairing: discovered {name}') receiver.pair_device( - address=address, authentication=authentication, entropy=20 if kind == _hidpp10.DEVICE_KIND.keyboard else 10 + address=address, + authentication=authentication, + entropy=20 if kind == _hidpp10_constants.DEVICE_KIND.keyboard else 10 ) pairing_start = _timestamp() patience = 5 # the discovering notification may come slightly later, so be patient @@ -121,7 +124,7 @@ def notifications_hook(self, n): if n: receiver.handle.notifications_hook(n) - if not (old_notification_flags & _hidpp10.NOTIFICATION_FLAG.wireless): + if not (old_notification_flags & _hidpp10_constants.NOTIFICATION_FLAG.wireless): # only clear the flags if they weren't set before, otherwise a # concurrently running Solaar app might stop working properly _hidpp10.set_notification_flags(receiver, old_notification_flags) diff --git a/lib/solaar/cli/probe.py b/lib/solaar/cli/probe.py index 859eb8ff3f..977a87f554 100644 --- a/lib/solaar/cli/probe.py +++ b/lib/solaar/cli/probe.py @@ -17,11 +17,11 @@ ## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. from logitech_receiver import base as _base -from logitech_receiver import hidpp10 as _hidpp10 +from logitech_receiver import hidpp10_constants as _hidpp10_constants from logitech_receiver.common import strhex as _strhex from solaar.cli.show import _print_device, _print_receiver -_R = _hidpp10.REGISTERS +_R = _hidpp10_constants.REGISTERS def run(receivers, args, find_receiver, _ignore): @@ -90,9 +90,9 @@ def run(receivers, args, find_receiver, _ignore): last = None for sub in range(0, 0xFF): rgst = _base.request(receiver.handle, 0xFF, 0x8100 | reg, sub, return_error=True) - if isinstance(rgst, int) and rgst == _hidpp10.ERROR.invalid_address: + if isinstance(rgst, int) and rgst == _hidpp10_constants.ERROR.invalid_address: break - elif isinstance(rgst, int) and rgst == _hidpp10.ERROR.invalid_value: + elif isinstance(rgst, int) and rgst == _hidpp10_constants.ERROR.invalid_value: continue else: if not isinstance(last, bytes) or not isinstance(rgst, bytes) or last != rgst: @@ -104,9 +104,9 @@ def run(receivers, args, find_receiver, _ignore): last = None for sub in range(0, 0xFF): rgst = _base.request(receiver.handle, 0xFF, 0x8100 | (0x200 + reg), sub, return_error=True) - if isinstance(rgst, int) and rgst == _hidpp10.ERROR.invalid_address: + if isinstance(rgst, int) and rgst == _hidpp10_constants.ERROR.invalid_address: break - elif isinstance(rgst, int) and rgst == _hidpp10.ERROR.invalid_value: + elif isinstance(rgst, int) and rgst == _hidpp10_constants.ERROR.invalid_value: continue else: if not isinstance(last, bytes) or not isinstance(rgst, bytes) or last != rgst: diff --git a/lib/solaar/cli/show.py b/lib/solaar/cli/show.py index 307366ffe1..fad81e9fc6 100644 --- a/lib/solaar/cli/show.py +++ b/lib/solaar/cli/show.py @@ -18,6 +18,7 @@ from logitech_receiver import exceptions from logitech_receiver import hidpp10 as _hidpp10 +from logitech_receiver import hidpp10_constants as _hidpp10_constants from logitech_receiver import hidpp20 as _hidpp20 from logitech_receiver import receiver as _receiver from logitech_receiver import settings_templates as _settings_templates @@ -46,12 +47,12 @@ def _print_receiver(receiver): notification_flags = _hidpp10.get_notification_flags(receiver) if notification_flags is not None: if notification_flags: - notification_names = _hidpp10.NOTIFICATION_FLAG.flag_names(notification_flags) + notification_names = _hidpp10_constants.NOTIFICATION_FLAG.flag_names(notification_flags) print(' Notifications: %s (0x%06X)' % (', '.join(notification_names), notification_flags)) else: print(' Notifications: (none)') - activity = receiver.read_register(_hidpp10.REGISTERS.devices_activity) + activity = receiver.read_register(_hidpp10_constants.REGISTERS.devices_activity) if activity: activity = [(d, ord(activity[d - 1:d])) for d in range(1, receiver.max_devices)] activity_text = ', '.join(('%d=%d' % (d, a)) for d, a in activity if a > 0) @@ -122,14 +123,14 @@ def _print_device(dev, num=None): notification_flags = _hidpp10.get_notification_flags(dev) if notification_flags is not None: if notification_flags: - notification_names = _hidpp10.NOTIFICATION_FLAG.flag_names(notification_flags) + notification_names = _hidpp10_constants.NOTIFICATION_FLAG.flag_names(notification_flags) print(' Notifications: %s (0x%06X).' % (', '.join(notification_names), notification_flags)) else: print(' Notifications: (none).') device_features = _hidpp10.get_device_features(dev) if device_features is not None: if device_features: - device_features_names = _hidpp10.DEVICE_FEATURES.flag_names(device_features) + device_features_names = _hidpp10_constants.DEVICE_FEATURES.flag_names(device_features) print(' Features: %s (0x%06X)' % (', '.join(device_features_names), device_features)) else: print(' Features: (none)') From db8741563b1603062fd6064de7ec8ad2ce6e7a0d Mon Sep 17 00:00:00 2001 From: Matthias Hagmann <16444067+MattHag@users.noreply.github.com> Date: Wed, 14 Feb 2024 20:41:49 +0100 Subject: [PATCH 03/10] logitech_receiver: Move hidpp20 constants into new module Related #1097 --- lib/logitech_receiver/hidpp10.py | 2 +- lib/logitech_receiver/hidpp20.py | 247 +------------------ lib/logitech_receiver/hidpp20_constants.py | 248 ++++++++++++++++++++ lib/logitech_receiver/notifications.py | 7 +- lib/logitech_receiver/settings.py | 10 +- lib/logitech_receiver/settings_templates.py | 19 +- lib/logitech_receiver/status.py | 4 +- lib/solaar/cli/show.py | 39 +-- 8 files changed, 296 insertions(+), 280 deletions(-) create mode 100644 lib/logitech_receiver/hidpp20_constants.py diff --git a/lib/logitech_receiver/hidpp10.py b/lib/logitech_receiver/hidpp10.py index 2a6873d1e3..55484e8e78 100644 --- a/lib/logitech_receiver/hidpp10.py +++ b/lib/logitech_receiver/hidpp10.py @@ -23,8 +23,8 @@ from .common import bytes2int as _bytes2int from .common import int2bytes as _int2bytes from .common import strhex as _strhex -from .hidpp20 import BATTERY_STATUS, FIRMWARE_KIND from .hidpp10_constants import REGISTERS +from .hidpp20_constants import BATTERY_STATUS, FIRMWARE_KIND logger = logging.getLogger(__name__) diff --git a/lib/logitech_receiver/hidpp20.py b/lib/logitech_receiver/hidpp20.py index 0d4b0c37a8..bd12ef18a5 100644 --- a/lib/logitech_receiver/hidpp20.py +++ b/lib/logitech_receiver/hidpp20.py @@ -36,6 +36,8 @@ from .common import bytes2int as _bytes2int from .common import crc16 as _crc16 from .common import int2bytes as _int2bytes +from .hidpp20_constants import (BATTERY_STATUS, CHARGE_LEVEL, CHARGE_STATUS, CHARGE_TYPE, DEVICE_KIND, ERROR, FEATURE, + FIRMWARE_KIND, GESTURE) from .i18n import _ logger = logging.getLogger(__name__) @@ -51,180 +53,6 @@ def hexint_presenter(dumper, data): # # -# /,/p}' | sort -t= -k2 -# additional features names taken from https://github.com/cvuchener/hidpp and -# https://github.com/Logitech/cpg-docs/tree/master/hidpp20 -"""Possible features available on a Logitech device. - -A particular device might not support all these features, and may support other -unknown features as well. -""" -FEATURE = _NamedInts( - ROOT=0x0000, - FEATURE_SET=0x0001, - FEATURE_INFO=0x0002, - # Common - DEVICE_FW_VERSION=0x0003, - DEVICE_UNIT_ID=0x0004, - DEVICE_NAME=0x0005, - DEVICE_GROUPS=0x0006, - DEVICE_FRIENDLY_NAME=0x0007, - KEEP_ALIVE=0x0008, - CONFIG_CHANGE=0x0020, - CRYPTO_ID=0x0021, - TARGET_SOFTWARE=0x0030, - WIRELESS_SIGNAL_STRENGTH=0x0080, - DFUCONTROL_LEGACY=0x00C0, - DFUCONTROL_UNSIGNED=0x00C1, - DFUCONTROL_SIGNED=0x00C2, - DFUCONTROL=0x00C3, - DFU=0x00D0, - BATTERY_STATUS=0x1000, - BATTERY_VOLTAGE=0x1001, - UNIFIED_BATTERY=0x1004, - CHARGING_CONTROL=0x1010, - LED_CONTROL=0x1300, - FORCE_PAIRING=0x1500, - GENERIC_TEST=0x1800, - DEVICE_RESET=0x1802, - OOBSTATE=0x1805, - CONFIG_DEVICE_PROPS=0x1806, - CHANGE_HOST=0x1814, - HOSTS_INFO=0x1815, - BACKLIGHT=0x1981, - BACKLIGHT2=0x1982, - BACKLIGHT3=0x1983, - ILLUMINATION=0x1990, - PRESENTER_CONTROL=0x1A00, - SENSOR_3D=0x1A01, - REPROG_CONTROLS=0x1B00, - REPROG_CONTROLS_V2=0x1B01, - REPROG_CONTROLS_V2_2=0x1B02, # LogiOptions 2.10.73 features.xml - REPROG_CONTROLS_V3=0x1B03, - REPROG_CONTROLS_V4=0x1B04, - REPORT_HID_USAGE=0x1BC0, - PERSISTENT_REMAPPABLE_ACTION=0x1C00, - WIRELESS_DEVICE_STATUS=0x1D4B, - REMAINING_PAIRING=0x1DF0, - FIRMWARE_PROPERTIES=0x1F1F, - ADC_MEASUREMENT=0x1F20, - # Mouse - LEFT_RIGHT_SWAP=0x2001, - SWAP_BUTTON_CANCEL=0x2005, - POINTER_AXIS_ORIENTATION=0x2006, - VERTICAL_SCROLLING=0x2100, - SMART_SHIFT=0x2110, - SMART_SHIFT_ENHANCED=0x2111, - HI_RES_SCROLLING=0x2120, - HIRES_WHEEL=0x2121, - LOWRES_WHEEL=0x2130, - THUMB_WHEEL=0x2150, - MOUSE_POINTER=0x2200, - ADJUSTABLE_DPI=0x2201, - EXTENDED_ADJUSTABLE_DPI=0x2202, - POINTER_SPEED=0x2205, - ANGLE_SNAPPING=0x2230, - SURFACE_TUNING=0x2240, - XY_STATS=0x2250, - WHEEL_STATS=0x2251, - HYBRID_TRACKING=0x2400, - # Keyboard - FN_INVERSION=0x40A0, - NEW_FN_INVERSION=0x40A2, - K375S_FN_INVERSION=0x40A3, - ENCRYPTION=0x4100, - LOCK_KEY_STATE=0x4220, - SOLAR_DASHBOARD=0x4301, - KEYBOARD_LAYOUT=0x4520, - KEYBOARD_DISABLE_KEYS=0x4521, - KEYBOARD_DISABLE_BY_USAGE=0x4522, - DUALPLATFORM=0x4530, - MULTIPLATFORM=0x4531, - KEYBOARD_LAYOUT_2=0x4540, - CROWN=0x4600, - # Touchpad - TOUCHPAD_FW_ITEMS=0x6010, - TOUCHPAD_SW_ITEMS=0x6011, - TOUCHPAD_WIN8_FW_ITEMS=0x6012, - TAP_ENABLE=0x6020, - TAP_ENABLE_EXTENDED=0x6021, - CURSOR_BALLISTIC=0x6030, - TOUCHPAD_RESOLUTION=0x6040, - TOUCHPAD_RAW_XY=0x6100, - TOUCHMOUSE_RAW_POINTS=0x6110, - TOUCHMOUSE_6120=0x6120, - GESTURE=0x6500, - GESTURE_2=0x6501, - # Gaming Devices - GKEY=0x8010, - MKEYS=0x8020, - MR=0x8030, - BRIGHTNESS_CONTROL=0x8040, - REPORT_RATE=0x8060, - EXTENDED_ADJUSTABLE_REPORT_RATE=0x8061, - COLOR_LED_EFFECTS=0x8070, - RGB_EFFECTS=0x8071, - PER_KEY_LIGHTING=0x8080, - PER_KEY_LIGHTING_V2=0x8081, - MODE_STATUS=0x8090, - ONBOARD_PROFILES=0x8100, - MOUSE_BUTTON_SPY=0x8110, - LATENCY_MONITORING=0x8111, - GAMING_ATTACHMENTS=0x8120, - FORCE_FEEDBACK=0x8123, - # Headsets - SIDETONE=0x8300, - EQUALIZER=0x8310, - HEADSET_OUT=0x8320, - # Fake features for Solaar internal use - MOUSE_GESTURE=0xFE00, -) -FEATURE._fallback = lambda x: 'unknown:%04X' % x - -FEATURE_FLAG = _NamedInts(internal=0x20, hidden=0x40, obsolete=0x80) - -DEVICE_KIND = _NamedInts( - keyboard=0x00, remote_control=0x01, numpad=0x02, mouse=0x03, touchpad=0x04, trackball=0x05, presenter=0x06, receiver=0x07 -) - -FIRMWARE_KIND = _NamedInts(Firmware=0x00, Bootloader=0x01, Hardware=0x02, Other=0x03) - -BATTERY_OK = lambda status: status not in (BATTERY_STATUS.invalid_battery, BATTERY_STATUS.thermal_error) - -BATTERY_STATUS = _NamedInts( - discharging=0x00, - recharging=0x01, - almost_full=0x02, - full=0x03, - slow_recharge=0x04, - invalid_battery=0x05, - thermal_error=0x06 -) - -ONBOARD_MODES = _NamedInts(MODE_NO_CHANGE=0x00, MODE_ONBOARD=0x01, MODE_HOST=0x02) - -CHARGE_STATUS = _NamedInts(charging=0x00, full=0x01, not_charging=0x02, error=0x07) - -CHARGE_LEVEL = _NamedInts(average=50, full=90, critical=5) - -CHARGE_TYPE = _NamedInts(standard=0x00, fast=0x01, slow=0x02) - -ERROR = _NamedInts( - unknown=0x01, - invalid_argument=0x02, - out_of_range=0x03, - hardware_error=0x04, - logitech_internal=0x05, - invalid_feature_index=0x06, - invalid_function=0x07, - busy=0x08, - unsupported=0x09 -) - -# -# -# - class FeaturesArray(dict): @@ -745,71 +573,6 @@ def _query_key(self, index: int): logger.warn(f"Key with index {index} was expected to exist but device doesn't report it.") -# Gesture Ids for feature GESTURE_2 -GESTURE = _NamedInts( - Tap1Finger=1, # task Left_Click - Tap2Finger=2, # task Right_Click - Tap3Finger=3, - Click1Finger=4, # task Left_Click - Click2Finger=5, # task Right_Click - Click3Finger=6, - DoubleTap1Finger=10, - DoubleTap2Finger=11, - DoubleTap3Finger=12, - Track1Finger=20, # action MovePointer - TrackingAcceleration=21, - TapDrag1Finger=30, # action Drag - TapDrag2Finger=31, # action SecondaryDrag - Drag3Finger=32, - TapGestures=33, # group all tap gestures under a single UI setting - FnClickGestureSuppression=34, # suppresses Tap and Edge gestures, toggled by Fn+Click - Scroll1Finger=40, # action ScrollOrPageXY / ScrollHorizontal - Scroll2Finger=41, # action ScrollOrPageXY / ScrollHorizontal - Scroll2FingerHoriz=42, # action ScrollHorizontal - Scroll2FingerVert=43, # action WheelScrolling - Scroll2FingerStateless=44, - NaturalScrolling=45, # affects native HID wheel reporting by gestures, not when diverted - Thumbwheel=46, # action WheelScrolling - VScrollInertia=48, - VScrollBallistics=49, - Swipe2FingerHoriz=50, # action PageScreen - Swipe3FingerHoriz=51, # action PageScreen - Swipe4FingerHoriz=52, # action PageScreen - Swipe3FingerVert=53, - Swipe4FingerVert=54, - LeftEdgeSwipe1Finger=60, - RightEdgeSwipe1Finger=61, - BottomEdgeSwipe1Finger=62, - TopEdgeSwipe1Finger=63, - LeftEdgeSwipe1Finger2=64, # task HorzScrollNoRepeatSet - RightEdgeSwipe1Finger2=65, # task 122 ?? - BottomEdgeSwipe1Finger2=66, # - TopEdgeSwipe1Finger2=67, # task 121 ?? - LeftEdgeSwipe2Finger=70, - RightEdgeSwipe2Finger=71, - BottomEdgeSwipe2Finger=72, - TopEdgeSwipe2Finger=73, - Zoom2Finger=80, # action Zoom - Zoom2FingerPinch=81, # ZoomBtnInSet - Zoom2FingerSpread=82, # ZoomBtnOutSet - Zoom3Finger=83, - Zoom2FingerStateless=84, # action Zoom - TwoFingersPresent=85, - Rotate2Finger=87, - Finger1=90, - Finger2=91, - Finger3=92, - Finger4=93, - Finger5=94, - Finger6=95, - Finger7=96, - Finger8=97, - Finger9=98, - Finger10=99, - DeviceSpecificRawData=100, -) -GESTURE._fallback = lambda x: 'unknown:%04X' % x - # Param Ids for feature GESTURE_2 PARAM = _NamedInts( ExtraCapabilities=1, # not suitable for use @@ -911,9 +674,11 @@ def _offset_mask(self, index): # offset and mask else: return (None, None) - enable_offset_mask = lambda gesture: gesture._offset_mask(gesture.index) + def enable_offset_mask(gesture): + return gesture._offset_mask(gesture.index) - diversion_offset_mask = lambda gesture: gesture._offset_mask(gesture.diversion_index) + def diversion_offset_mask(gesture): + return gesture._offset_mask(gesture.diversion_index) def enabled(self): # is the gesture enabled? if self._enabled is None and self.index is not None: diff --git a/lib/logitech_receiver/hidpp20_constants.py b/lib/logitech_receiver/hidpp20_constants.py new file mode 100644 index 0000000000..72cc5e272b --- /dev/null +++ b/lib/logitech_receiver/hidpp20_constants.py @@ -0,0 +1,248 @@ +from .common import NamedInts + +# /,/p}' | sort -t= -k2 +# additional features names taken from https://github.com/cvuchener/hidpp and +# https://github.com/Logitech/cpg-docs/tree/master/hidpp20 +"""Possible features available on a Logitech device. + +A particular device might not support all these features, and may support other +unknown features as well. +""" +FEATURE = NamedInts( + ROOT=0x0000, + FEATURE_SET=0x0001, + FEATURE_INFO=0x0002, + # Common + DEVICE_FW_VERSION=0x0003, + DEVICE_UNIT_ID=0x0004, + DEVICE_NAME=0x0005, + DEVICE_GROUPS=0x0006, + DEVICE_FRIENDLY_NAME=0x0007, + KEEP_ALIVE=0x0008, + CONFIG_CHANGE=0x0020, + CRYPTO_ID=0x0021, + TARGET_SOFTWARE=0x0030, + WIRELESS_SIGNAL_STRENGTH=0x0080, + DFUCONTROL_LEGACY=0x00C0, + DFUCONTROL_UNSIGNED=0x00C1, + DFUCONTROL_SIGNED=0x00C2, + DFUCONTROL=0x00C3, + DFU=0x00D0, + BATTERY_STATUS=0x1000, + BATTERY_VOLTAGE=0x1001, + UNIFIED_BATTERY=0x1004, + CHARGING_CONTROL=0x1010, + LED_CONTROL=0x1300, + FORCE_PAIRING=0x1500, + GENERIC_TEST=0x1800, + DEVICE_RESET=0x1802, + OOBSTATE=0x1805, + CONFIG_DEVICE_PROPS=0x1806, + CHANGE_HOST=0x1814, + HOSTS_INFO=0x1815, + BACKLIGHT=0x1981, + BACKLIGHT2=0x1982, + BACKLIGHT3=0x1983, + ILLUMINATION=0x1990, + PRESENTER_CONTROL=0x1A00, + SENSOR_3D=0x1A01, + REPROG_CONTROLS=0x1B00, + REPROG_CONTROLS_V2=0x1B01, + REPROG_CONTROLS_V2_2=0x1B02, # LogiOptions 2.10.73 features.xml + REPROG_CONTROLS_V3=0x1B03, + REPROG_CONTROLS_V4=0x1B04, + REPORT_HID_USAGE=0x1BC0, + PERSISTENT_REMAPPABLE_ACTION=0x1C00, + WIRELESS_DEVICE_STATUS=0x1D4B, + REMAINING_PAIRING=0x1DF0, + FIRMWARE_PROPERTIES=0x1F1F, + ADC_MEASUREMENT=0x1F20, + # Mouse + LEFT_RIGHT_SWAP=0x2001, + SWAP_BUTTON_CANCEL=0x2005, + POINTER_AXIS_ORIENTATION=0x2006, + VERTICAL_SCROLLING=0x2100, + SMART_SHIFT=0x2110, + SMART_SHIFT_ENHANCED=0x2111, + HI_RES_SCROLLING=0x2120, + HIRES_WHEEL=0x2121, + LOWRES_WHEEL=0x2130, + THUMB_WHEEL=0x2150, + MOUSE_POINTER=0x2200, + ADJUSTABLE_DPI=0x2201, + EXTENDED_ADJUSTABLE_DPI=0x2202, + POINTER_SPEED=0x2205, + ANGLE_SNAPPING=0x2230, + SURFACE_TUNING=0x2240, + XY_STATS=0x2250, + WHEEL_STATS=0x2251, + HYBRID_TRACKING=0x2400, + # Keyboard + FN_INVERSION=0x40A0, + NEW_FN_INVERSION=0x40A2, + K375S_FN_INVERSION=0x40A3, + ENCRYPTION=0x4100, + LOCK_KEY_STATE=0x4220, + SOLAR_DASHBOARD=0x4301, + KEYBOARD_LAYOUT=0x4520, + KEYBOARD_DISABLE_KEYS=0x4521, + KEYBOARD_DISABLE_BY_USAGE=0x4522, + DUALPLATFORM=0x4530, + MULTIPLATFORM=0x4531, + KEYBOARD_LAYOUT_2=0x4540, + CROWN=0x4600, + # Touchpad + TOUCHPAD_FW_ITEMS=0x6010, + TOUCHPAD_SW_ITEMS=0x6011, + TOUCHPAD_WIN8_FW_ITEMS=0x6012, + TAP_ENABLE=0x6020, + TAP_ENABLE_EXTENDED=0x6021, + CURSOR_BALLISTIC=0x6030, + TOUCHPAD_RESOLUTION=0x6040, + TOUCHPAD_RAW_XY=0x6100, + TOUCHMOUSE_RAW_POINTS=0x6110, + TOUCHMOUSE_6120=0x6120, + GESTURE=0x6500, + GESTURE_2=0x6501, + # Gaming Devices + GKEY=0x8010, + MKEYS=0x8020, + MR=0x8030, + BRIGHTNESS_CONTROL=0x8040, + REPORT_RATE=0x8060, + EXTENDED_ADJUSTABLE_REPORT_RATE=0x8061, + COLOR_LED_EFFECTS=0x8070, + RGB_EFFECTS=0x8071, + PER_KEY_LIGHTING=0x8080, + PER_KEY_LIGHTING_V2=0x8081, + MODE_STATUS=0x8090, + ONBOARD_PROFILES=0x8100, + MOUSE_BUTTON_SPY=0x8110, + LATENCY_MONITORING=0x8111, + GAMING_ATTACHMENTS=0x8120, + FORCE_FEEDBACK=0x8123, + # Headsets + SIDETONE=0x8300, + EQUALIZER=0x8310, + HEADSET_OUT=0x8320, + # Fake features for Solaar internal use + MOUSE_GESTURE=0xFE00, +) +FEATURE._fallback = lambda x: 'unknown:%04X' % x + +FEATURE_FLAG = NamedInts(internal=0x20, hidden=0x40, obsolete=0x80) + +DEVICE_KIND = NamedInts( + keyboard=0x00, remote_control=0x01, numpad=0x02, mouse=0x03, touchpad=0x04, trackball=0x05, presenter=0x06, receiver=0x07 +) + +FIRMWARE_KIND = NamedInts(Firmware=0x00, Bootloader=0x01, Hardware=0x02, Other=0x03) + + +def BATTERY_OK(status): + return status not in (BATTERY_STATUS.invalid_battery, BATTERY_STATUS.thermal_error) + + +BATTERY_STATUS = NamedInts( + discharging=0x00, + recharging=0x01, + almost_full=0x02, + full=0x03, + slow_recharge=0x04, + invalid_battery=0x05, + thermal_error=0x06 +) + +ONBOARD_MODES = NamedInts(MODE_NO_CHANGE=0x00, MODE_ONBOARD=0x01, MODE_HOST=0x02) + +CHARGE_STATUS = NamedInts(charging=0x00, full=0x01, not_charging=0x02, error=0x07) + +CHARGE_LEVEL = NamedInts(average=50, full=90, critical=5) + +CHARGE_TYPE = NamedInts(standard=0x00, fast=0x01, slow=0x02) + +ERROR = NamedInts( + unknown=0x01, + invalid_argument=0x02, + out_of_range=0x03, + hardware_error=0x04, + logitech_internal=0x05, + invalid_feature_index=0x06, + invalid_function=0x07, + busy=0x08, + unsupported=0x09 +) + +# Gesture Ids for feature GESTURE_2 +GESTURE = NamedInts( + Tap1Finger=1, # task Left_Click + Tap2Finger=2, # task Right_Click + Tap3Finger=3, + Click1Finger=4, # task Left_Click + Click2Finger=5, # task Right_Click + Click3Finger=6, + DoubleTap1Finger=10, + DoubleTap2Finger=11, + DoubleTap3Finger=12, + Track1Finger=20, # action MovePointer + TrackingAcceleration=21, + TapDrag1Finger=30, # action Drag + TapDrag2Finger=31, # action SecondaryDrag + Drag3Finger=32, + TapGestures=33, # group all tap gestures under a single UI setting + FnClickGestureSuppression=34, # suppresses Tap and Edge gestures, toggled by Fn+Click + Scroll1Finger=40, # action ScrollOrPageXY / ScrollHorizontal + Scroll2Finger=41, # action ScrollOrPageXY / ScrollHorizontal + Scroll2FingerHoriz=42, # action ScrollHorizontal + Scroll2FingerVert=43, # action WheelScrolling + Scroll2FingerStateless=44, + NaturalScrolling=45, # affects native HID wheel reporting by gestures, not when diverted + Thumbwheel=46, # action WheelScrolling + VScrollInertia=48, + VScrollBallistics=49, + Swipe2FingerHoriz=50, # action PageScreen + Swipe3FingerHoriz=51, # action PageScreen + Swipe4FingerHoriz=52, # action PageScreen + Swipe3FingerVert=53, + Swipe4FingerVert=54, + LeftEdgeSwipe1Finger=60, + RightEdgeSwipe1Finger=61, + BottomEdgeSwipe1Finger=62, + TopEdgeSwipe1Finger=63, + LeftEdgeSwipe1Finger2=64, # task HorzScrollNoRepeatSet + RightEdgeSwipe1Finger2=65, # task 122 ?? + BottomEdgeSwipe1Finger2=66, # + TopEdgeSwipe1Finger2=67, # task 121 ?? + LeftEdgeSwipe2Finger=70, + RightEdgeSwipe2Finger=71, + BottomEdgeSwipe2Finger=72, + TopEdgeSwipe2Finger=73, + Zoom2Finger=80, # action Zoom + Zoom2FingerPinch=81, # ZoomBtnInSet + Zoom2FingerSpread=82, # ZoomBtnOutSet + Zoom3Finger=83, + Zoom2FingerStateless=84, # action Zoom + TwoFingersPresent=85, + Rotate2Finger=87, + Finger1=90, + Finger2=91, + Finger3=92, + Finger4=93, + Finger5=94, + Finger6=95, + Finger7=96, + Finger8=97, + Finger9=98, + Finger10=99, + DeviceSpecificRawData=100, +) +GESTURE._fallback = lambda x: 'unknown:%04X' % x + +# Param Ids for feature GESTURE_2 +PARAM = NamedInts( + ExtraCapabilities=1, # not suitable for use + PixelZone=2, # 4 2-byte integers, left, bottom, width, height; pixels + RatioZone=3, # 4 bytes, left, bottom, width, height; unit 1/240 pad size + ScaleFactor=4, # 2-byte integer, with 256 as normal scale +) +PARAM._fallback = lambda x: 'unknown:%04X' % x diff --git a/lib/logitech_receiver/notifications.py b/lib/logitech_receiver/notifications.py index 4dbb3db375..da94bb11d6 100644 --- a/lib/logitech_receiver/notifications.py +++ b/lib/logitech_receiver/notifications.py @@ -25,6 +25,7 @@ from . import diversion as _diversion from . import hidpp10 as _hidpp10 from . import hidpp20 as _hidpp20 +from . import hidpp20_constants as _hidpp20_constants from . import settings_templates as _st from .base import DJ_MESSAGE_ID as _DJ_MESSAGE_ID from .common import strhex as _strhex @@ -35,7 +36,7 @@ logger = logging.getLogger(__name__) _R = _hidpp10.REGISTERS -_F = _hidpp20.FEATURE +_F = _hidpp20_constants.FEATURE # # @@ -351,14 +352,14 @@ def _process_feature_notification(device, status, n, feature): charge, lux, adc = _unpack('!BHH', n.data[:5]) # guesstimate the battery voltage, emphasis on 'guess' # status_text = '%1.2fV' % (adc * 2.67793237653 / 0x0672) - status_text = _hidpp20.BATTERY_STATUS.discharging + status_text = _hidpp20_constants.BATTERY_STATUS.discharging if n.address == 0x00: status[_K.LIGHT_LEVEL] = None status.set_battery_info(charge, None, status_text, None) elif n.address == 0x10: status[_K.LIGHT_LEVEL] = lux if lux > 200: - status_text = _hidpp20.BATTERY_STATUS.recharging + status_text = _hidpp20_constants.BATTERY_STATUS.recharging status.set_battery_info(charge, None, status_text, None) elif n.address == 0x20: if logger.isEnabledFor(logging.DEBUG): diff --git a/lib/logitech_receiver/settings.py b/lib/logitech_receiver/settings.py index 5bc136b389..23f17d7334 100644 --- a/lib/logitech_receiver/settings.py +++ b/lib/logitech_receiver/settings.py @@ -22,7 +22,7 @@ from struct import unpack as _unpack from time import sleep as _sleep -from . import hidpp20 as _hidpp20 +from . import hidpp20_constants as _hidpp20_constants from .common import NamedInt as _NamedInt from .common import NamedInts as _NamedInts from .common import bytes2int as _bytes2int @@ -1416,7 +1416,7 @@ def read(self, device): # need to return bytes, as if read from device def write(self, device, data_bytes): def handler(device, n): # Called on notification events from the device - if n.sub_id < 0x40 and device.features.get_feature(n.sub_id) == _hidpp20.FEATURE.REPROG_CONTROLS_V4: + if n.sub_id < 0x40 and device.features.get_feature(n.sub_id) == _hidpp20_constants.FEATURE.REPROG_CONTROLS_V4: if n.address == 0x00: cids = _unpack('!HHHH', n.data[:8]) if not self.pressed and int(self.key.key) in cids: # trigger key pressed @@ -1478,11 +1478,11 @@ def __init__(self, device, name=''): self.keys = [] # the keys that can initiate processing self.initiating_key = None # the key that did initiate processing self.active = False - self.feature_offset = device.features[_hidpp20.FEATURE.REPROG_CONTROLS_V4] + self.feature_offset = device.features[_hidpp20_constants.FEATURE.REPROG_CONTROLS_V4] assert self.feature_offset is not False def handler(self, device, n): # Called on notification events from the device - if n.sub_id < 0x40 and device.features.get_feature(n.sub_id) == _hidpp20.FEATURE.REPROG_CONTROLS_V4: + if n.sub_id < 0x40 and device.features.get_feature(n.sub_id) == _hidpp20_constants.FEATURE.REPROG_CONTROLS_V4: if n.address == 0x00: cids = _unpack('!HHHH', n.data[:8]) ## generalize to list of keys @@ -1550,7 +1550,7 @@ def key_action(self, key): # acction to take when some other diverted key is pr def apply_all_settings(device): - if device.features and _hidpp20.FEATURE.HIRES_WHEEL in device.features: + if device.features and _hidpp20_constants.FEATURE.HIRES_WHEEL in device.features: _sleep(0.2) # delay to try to get out of race condition with Linux HID++ driver persister = getattr(device, 'persister', None) sensitives = persister.get('_sensitive', {}) if persister else {} diff --git a/lib/logitech_receiver/settings_templates.py b/lib/logitech_receiver/settings_templates.py index 2dd600604b..e6144fe191 100644 --- a/lib/logitech_receiver/settings_templates.py +++ b/lib/logitech_receiver/settings_templates.py @@ -25,6 +25,7 @@ from . import hidpp10_constants as _hidpp10_constants from . import hidpp20 as _hidpp20 +from . import hidpp20_constants as _hidpp20_constants from . import special_keys as _special_keys from .common import NamedInt as _NamedInt from .common import NamedInts as _NamedInts @@ -55,9 +56,9 @@ _DK = _hidpp10_constants.DEVICE_KIND _R = _hidpp10_constants.REGISTERS -_F = _hidpp20.FEATURE +_F = _hidpp20_constants.FEATURE -_GG = _hidpp20.GESTURE +_GG = _hidpp20_constants.GESTURE _GP = _hidpp20.PARAM # Setting classes are used to control the settings that the Solaar GUI shows and manipulates. @@ -510,8 +511,8 @@ class _rw_class(_FeatureRW): # no longer needed - set Onboard Profiles to disab def write(self, device, data_bytes): # Host mode is required for report rate to be adjustable - if _hidpp20.get_onboard_mode(device) != _hidpp20.ONBOARD_MODES.MODE_HOST: - _hidpp20.set_onboard_mode(device, _hidpp20.ONBOARD_MODES.MODE_HOST) + 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): @@ -556,8 +557,8 @@ def read(self, device, data_bytes=b''): def write(self, device, data_bytes): # Host mode is required for report rate to be adjustable - if _hidpp20.get_onboard_mode(device) != _hidpp20.ONBOARD_MODES.MODE_HOST: - _hidpp20.set_onboard_mode(device, _hidpp20.ONBOARD_MODES.MODE_HOST) + 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): @@ -805,7 +806,7 @@ def release_action(self): 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, _hidpp20.FEATURE.MOUSE_GESTURE) + _process_notification(self.device, self.device.status, notification, _F.MOUSE_GESTURE) self.fsmState = 'idle' def move_action(self, dx, dy): @@ -1195,7 +1196,7 @@ class Gesture2Gestures(_BitFieldOMSetting): feature = _F.GESTURE_2 rw_options = {'read_fnid': 0x10, 'write_fnid': 0x20} validator_options = {'om_method': _hidpp20.Gesture.enable_offset_mask} - choices_universe = _hidpp20.GESTURE + choices_universe = _hidpp20_constants.GESTURE _labels = _GESTURE2_GESTURES_LABELS class validator_class(_BitFieldOMV): @@ -1213,7 +1214,7 @@ class Gesture2Divert(_BitFieldOMSetting): feature = _F.GESTURE_2 rw_options = {'read_fnid': 0x30, 'write_fnid': 0x40} validator_options = {'om_method': _hidpp20.Gesture.diversion_offset_mask} - choices_universe = _hidpp20.GESTURE + choices_universe = _hidpp20_constants.GESTURE _labels = _GESTURE2_GESTURES_LABELS class validator_class(_BitFieldOMV): diff --git a/lib/logitech_receiver/status.py b/lib/logitech_receiver/status.py index 324a5a09d0..03e4d2ff4e 100644 --- a/lib/logitech_receiver/status.py +++ b/lib/logitech_receiver/status.py @@ -218,8 +218,8 @@ def set_battery_info(self, level, nextLevel, status, voltage, timestamp=None): old_voltage, self[KEYS.BATTERY_VOLTAGE] = self.get(KEYS.BATTERY_VOLTAGE), voltage charging = status in ( - _hidpp20_constants.BATTERY_STATUS.recharging, _hidpp20_constants.BATTERY_STATUS.almost_full, _hidpp20_constants.BATTERY_STATUS.full, - _hidpp20_constants.BATTERY_STATUS.slow_recharge + _hidpp20_constants.BATTERY_STATUS.recharging, _hidpp20_constants.BATTERY_STATUS.almost_full, + _hidpp20_constants.BATTERY_STATUS.full, _hidpp20_constants.BATTERY_STATUS.slow_recharge ) old_charging, self[KEYS.BATTERY_CHARGING] = self.get(KEYS.BATTERY_CHARGING), charging diff --git a/lib/solaar/cli/show.py b/lib/solaar/cli/show.py index fad81e9fc6..5500e1e783 100644 --- a/lib/solaar/cli/show.py +++ b/lib/solaar/cli/show.py @@ -20,13 +20,14 @@ from logitech_receiver import hidpp10 as _hidpp10 from logitech_receiver import hidpp10_constants as _hidpp10_constants from logitech_receiver import hidpp20 as _hidpp20 +from logitech_receiver import hidpp20_constants as _hidpp20_constants from logitech_receiver import receiver as _receiver from logitech_receiver import settings_templates as _settings_templates from logitech_receiver.common import NamedInt as _NamedInt from logitech_receiver.common import strhex as _strhex from solaar import NAME, __version__ -_F = _hidpp20.FEATURE +_F = _hidpp20_constants.FEATURE def _print_receiver(receiver): @@ -142,11 +143,11 @@ def _print_device(dev, num=None): for feature, index in dev.features.enumerate(): flags = dev.request(0x0000, feature.bytes(2)) flags = 0 if flags is None else ord(flags[1:2]) - flags = _hidpp20.FEATURE_FLAG.flag_names(flags) + flags = _hidpp20_constants.FEATURE_FLAG.flag_names(flags) version = dev.features.get_feature_version(int(feature)) version = version if version else 0 print(' %2d: %-22s {%04X} V%s %s ' % (index, feature, feature, version, ', '.join(flags))) - if feature == _hidpp20.FEATURE.HIRES_WHEEL: + if feature == _hidpp20_constants.FEATURE.HIRES_WHEEL: wheel = _hidpp20.get_hires_wheel(dev) if wheel: multi, has_invert, has_switch, inv, res, target, ratchet = wheel @@ -163,7 +164,7 @@ def _print_device(dev, num=None): print(' HID++ notification') else: print(' HID notification') - elif feature == _hidpp20.FEATURE.MOUSE_POINTER: + elif feature == _hidpp20_constants.FEATURE.MOUSE_POINTER: mouse_pointer = _hidpp20.get_mouse_pointer_info(dev) if mouse_pointer: print(' DPI: %s' % mouse_pointer['dpi']) @@ -176,13 +177,13 @@ def _print_device(dev, num=None): print(' Provide vertical tuning, trackball') else: print(' No vertical tuning, standard mice') - elif feature == _hidpp20.FEATURE.VERTICAL_SCROLLING: + elif feature == _hidpp20_constants.FEATURE.VERTICAL_SCROLLING: vertical_scrolling_info = _hidpp20.get_vertical_scrolling_info(dev) if vertical_scrolling_info: print(' Roller type: %s' % vertical_scrolling_info['roller']) print(' Ratchet per turn: %s' % vertical_scrolling_info['ratchet']) print(' Scroll lines: %s' % vertical_scrolling_info['lines']) - elif feature == _hidpp20.FEATURE.HI_RES_SCROLLING: + elif feature == _hidpp20_constants.FEATURE.HI_RES_SCROLLING: scrolling_mode, scrolling_resolution = _hidpp20.get_hi_res_scrolling_info(dev) if scrolling_mode: print(' Hi-res scrolling enabled') @@ -190,30 +191,30 @@ def _print_device(dev, num=None): print(' Hi-res scrolling disabled') if scrolling_resolution: print(' Hi-res scrolling multiplier: %s' % scrolling_resolution) - elif feature == _hidpp20.FEATURE.POINTER_SPEED: + elif feature == _hidpp20_constants.FEATURE.POINTER_SPEED: pointer_speed = _hidpp20.get_pointer_speed_info(dev) if pointer_speed: print(' Pointer Speed: %s' % pointer_speed) - elif feature == _hidpp20.FEATURE.LOWRES_WHEEL: + elif feature == _hidpp20_constants.FEATURE.LOWRES_WHEEL: wheel_status = _hidpp20.get_lowres_wheel_status(dev) if wheel_status: print(' Wheel Reports: %s' % wheel_status) - elif feature == _hidpp20.FEATURE.NEW_FN_INVERSION: + elif feature == _hidpp20_constants.FEATURE.NEW_FN_INVERSION: inversion = _hidpp20.get_new_fn_inversion(dev) if inversion: inverted, default_inverted = inversion print(' Fn-swap:', 'enabled' if inverted else 'disabled') print(' Fn-swap default:', 'enabled' if default_inverted else 'disabled') - elif feature == _hidpp20.FEATURE.HOSTS_INFO: + elif feature == _hidpp20_constants.FEATURE.HOSTS_INFO: host_names = _hidpp20.get_host_names(dev) for host, (paired, name) in host_names.items(): print(' Host %s (%s): %s' % (host, 'paired' if paired else 'unpaired', name)) - elif feature == _hidpp20.FEATURE.DEVICE_NAME: + elif feature == _hidpp20_constants.FEATURE.DEVICE_NAME: print(' Name: %s' % _hidpp20.get_name(dev)) print(' Kind: %s' % _hidpp20.get_kind(dev)) - elif feature == _hidpp20.FEATURE.DEVICE_FRIENDLY_NAME: + elif feature == _hidpp20_constants.FEATURE.DEVICE_FRIENDLY_NAME: print(' Friendly Name: %s' % _hidpp20.get_friendly_name(dev)) - elif feature == _hidpp20.FEATURE.DEVICE_FW_VERSION: + elif feature == _hidpp20_constants.FEATURE.DEVICE_FW_VERSION: for fw in _hidpp20.get_firmware(dev): extras = _strhex(fw.extras) if fw.extras else '' print(' Firmware: %s %s %s %s' % (fw.kind, fw.name, fw.version, extras)) @@ -221,12 +222,12 @@ def _print_device(dev, num=None): if ids: unitId, modelId, tid_map = ids print(' Unit ID: %s Model ID: %s Transport IDs: %s' % (unitId, modelId, tid_map)) - elif feature == _hidpp20.FEATURE.REPORT_RATE or feature == _hidpp20.FEATURE.EXTENDED_ADJUSTABLE_REPORT_RATE: + elif feature == _hidpp20_constants.FEATURE.REPORT_RATE or feature == _hidpp20_constants.FEATURE.EXTENDED_ADJUSTABLE_REPORT_RATE: print(' Report Rate: %s' % _hidpp20.get_polling_rate(dev)) - elif feature == _hidpp20.FEATURE.REMAINING_PAIRING: + elif feature == _hidpp20_constants.FEATURE.REMAINING_PAIRING: print(' Remaining Pairings: %d' % _hidpp20.get_remaining_pairing(dev)) - elif feature == _hidpp20.FEATURE.ONBOARD_PROFILES: - if _hidpp20.get_onboard_mode(dev) == _hidpp20.ONBOARD_MODES.MODE_HOST: + elif feature == _hidpp20_constants.FEATURE.ONBOARD_PROFILES: + if _hidpp20.get_onboard_mode(dev) == _hidpp20_constants.ONBOARD_MODES.MODE_HOST: mode = 'Host' else: mode = 'On-Board' @@ -252,9 +253,9 @@ def _print_device(dev, num=None): print(' Has %d reprogrammable keys:' % len(dev.keys)) for k in dev.keys: # TODO: add here additional variants for other REPROG_CONTROLS - if dev.keys.keyversion == _hidpp20.FEATURE.REPROG_CONTROLS_V2: + if dev.keys.keyversion == _hidpp20_constants.FEATURE.REPROG_CONTROLS_V2: print(' %2d: %-26s => %-27s %s' % (k.index, k.key, k.default_task, ', '.join(k.flags))) - if dev.keys.keyversion == _hidpp20.FEATURE.REPROG_CONTROLS_V4: + if dev.keys.keyversion == _hidpp20_constants.FEATURE.REPROG_CONTROLS_V4: print(' %2d: %-26s, default: %-27s => %-26s' % (k.index, k.key, k.default_task, k.mapped_to)) gmask_fmt = ','.join(k.group_mask) gmask_fmt = gmask_fmt if gmask_fmt else 'empty' From f3bb6c50e359e3e3b040ce3b63265d4498c373ce Mon Sep 17 00:00:00 2001 From: "Peter F. Patel-Schneider" Date: Tue, 20 Feb 2024 05:02:59 -0500 Subject: [PATCH 04/10] Update base.py --- lib/logitech_receiver/base.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/logitech_receiver/base.py b/lib/logitech_receiver/base.py index 24234e7d90..c3f2726c7e 100644 --- a/lib/logitech_receiver/base.py +++ b/lib/logitech_receiver/base.py @@ -554,7 +554,8 @@ def ping(handle, devnumber, long_message=False): error = ord(reply_data[3:4]) if error == _hidpp10_constants.ERROR.invalid_SubID__command: # a valid reply from a HID++ 1.0 device return 1.0 - if error == _hidpp10_constants.ERROR.resource_error or error == _hidpp10_constants.ERROR.connection_request_failed: + if error == _hidpp10_constants.ERROR.resource_error or + error == _hidpp10_constants.ERROR.connection_request_failed: return # device unreachable if error == _hidpp10_constants.ERROR.unknown_device: # no paired device with that number logger.error('(%s) device %d error on ping request: unknown device', handle, devnumber) From 4faa50f0ee2735829f295b5fed07852f588fb160 Mon Sep 17 00:00:00 2001 From: "Peter F. Patel-Schneider" Date: Tue, 20 Feb 2024 05:04:17 -0500 Subject: [PATCH 05/10] Update hidpp20.py --- lib/logitech_receiver/hidpp20.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/logitech_receiver/hidpp20.py b/lib/logitech_receiver/hidpp20.py index 919557bc56..201be55f08 100644 --- a/lib/logitech_receiver/hidpp20.py +++ b/lib/logitech_receiver/hidpp20.py @@ -276,7 +276,7 @@ def _getCidReporting(self): self._mapping_flags = mapping_flags_1 | (mapping_flags_2 << 8) else: raise exceptions.FeatureCallError(msg='No reply from device.') - except exceptions.FeatureCallError: # if the key hasn't ever been configured then the read may fail so only produce a warning + except exceptions.FeatureCallError: # if the key hasn't ever been configured then the read may fail if logger.isEnabledFor(logging.WARNING): logger.warn( f'Feature Call Error in _getCidReporting on device {self._device} for cid {self._cid} - use defaults' From 2d04af90c429a24e809181269dbd56e4bd4f338a Mon Sep 17 00:00:00 2001 From: "Peter F. Patel-Schneider" Date: Tue, 20 Feb 2024 05:06:40 -0500 Subject: [PATCH 06/10] Update show.py --- lib/solaar/cli/show.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/solaar/cli/show.py b/lib/solaar/cli/show.py index 5500e1e783..cb4b8e0888 100644 --- a/lib/solaar/cli/show.py +++ b/lib/solaar/cli/show.py @@ -222,7 +222,8 @@ def _print_device(dev, num=None): if ids: unitId, modelId, tid_map = ids print(' Unit ID: %s Model ID: %s Transport IDs: %s' % (unitId, modelId, tid_map)) - elif feature == _hidpp20_constants.FEATURE.REPORT_RATE or feature == _hidpp20_constants.FEATURE.EXTENDED_ADJUSTABLE_REPORT_RATE: + elif feature == _hidpp20_constants.FEATURE.REPORT_RATE or \ + feature == _hidpp20_constants.FEATURE.EXTENDED_ADJUSTABLE_REPORT_RATE: print(' Report Rate: %s' % _hidpp20.get_polling_rate(dev)) elif feature == _hidpp20_constants.FEATURE.REMAINING_PAIRING: print(' Remaining Pairings: %d' % _hidpp20.get_remaining_pairing(dev)) From d44aaa9d9aa215d2aea74a551ccb28be4eb2cd4e Mon Sep 17 00:00:00 2001 From: "Peter F. Patel-Schneider" Date: Tue, 20 Feb 2024 05:09:27 -0500 Subject: [PATCH 07/10] Update base.py --- lib/logitech_receiver/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/logitech_receiver/base.py b/lib/logitech_receiver/base.py index c3f2726c7e..803a272448 100644 --- a/lib/logitech_receiver/base.py +++ b/lib/logitech_receiver/base.py @@ -554,7 +554,7 @@ def ping(handle, devnumber, long_message=False): error = ord(reply_data[3:4]) if error == _hidpp10_constants.ERROR.invalid_SubID__command: # a valid reply from a HID++ 1.0 device return 1.0 - if error == _hidpp10_constants.ERROR.resource_error or + if error == _hidpp10_constants.ERROR.resource_error or \ error == _hidpp10_constants.ERROR.connection_request_failed: return # device unreachable if error == _hidpp10_constants.ERROR.unknown_device: # no paired device with that number From 6a28336d55e3d899ff3f7ea42da1b07344d27ee7 Mon Sep 17 00:00:00 2001 From: "Peter F. Patel-Schneider" Date: Tue, 20 Feb 2024 05:11:19 -0500 Subject: [PATCH 08/10] Update show.py --- lib/solaar/cli/show.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/solaar/cli/show.py b/lib/solaar/cli/show.py index cb4b8e0888..38ee0c7d8b 100644 --- a/lib/solaar/cli/show.py +++ b/lib/solaar/cli/show.py @@ -223,7 +223,7 @@ def _print_device(dev, num=None): unitId, modelId, tid_map = ids print(' Unit ID: %s Model ID: %s Transport IDs: %s' % (unitId, modelId, tid_map)) elif feature == _hidpp20_constants.FEATURE.REPORT_RATE or \ - feature == _hidpp20_constants.FEATURE.EXTENDED_ADJUSTABLE_REPORT_RATE: + feature == _hidpp20_constants.FEATURE.EXTENDED_ADJUSTABLE_REPORT_RATE: print(' Report Rate: %s' % _hidpp20.get_polling_rate(dev)) elif feature == _hidpp20_constants.FEATURE.REMAINING_PAIRING: print(' Remaining Pairings: %d' % _hidpp20.get_remaining_pairing(dev)) From a55520fb3b313210616bbd96393025760cdc1f3d Mon Sep 17 00:00:00 2001 From: "Peter F. Patel-Schneider" Date: Tue, 20 Feb 2024 05:21:24 -0500 Subject: [PATCH 09/10] Update show.py --- lib/solaar/cli/show.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/solaar/cli/show.py b/lib/solaar/cli/show.py index 38ee0c7d8b..c69e91d41b 100644 --- a/lib/solaar/cli/show.py +++ b/lib/solaar/cli/show.py @@ -223,7 +223,7 @@ def _print_device(dev, num=None): unitId, modelId, tid_map = ids print(' Unit ID: %s Model ID: %s Transport IDs: %s' % (unitId, modelId, tid_map)) elif feature == _hidpp20_constants.FEATURE.REPORT_RATE or \ - feature == _hidpp20_constants.FEATURE.EXTENDED_ADJUSTABLE_REPORT_RATE: + feature == _hidpp20_constants.FEATURE.EXTENDED_ADJUSTABLE_REPORT_RATE: print(' Report Rate: %s' % _hidpp20.get_polling_rate(dev)) elif feature == _hidpp20_constants.FEATURE.REMAINING_PAIRING: print(' Remaining Pairings: %d' % _hidpp20.get_remaining_pairing(dev)) From c6e5b8c1ed72b03aab66786c8c6dcb75c55cf71c Mon Sep 17 00:00:00 2001 From: "Peter F. Patel-Schneider" Date: Tue, 20 Feb 2024 05:22:54 -0500 Subject: [PATCH 10/10] Update __init__.py --- lib/logitech_receiver/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/logitech_receiver/__init__.py b/lib/logitech_receiver/__init__.py index e5863e16df..46aa1f0a20 100644 --- a/lib/logitech_receiver/__init__.py +++ b/lib/logitech_receiver/__init__.py @@ -30,7 +30,6 @@ import logging - logger = logging.getLogger(__name__) logger.setLevel(logging.root.level) # if logging.root.level > logging.DEBUG: