diff --git a/lib/logitech_receiver/__init__.py b/lib/logitech_receiver/__init__.py index 46aa1f0a20..6defad1ca1 100644 --- a/lib/logitech_receiver/__init__.py +++ b/lib/logitech_receiver/__init__.py @@ -15,27 +15,12 @@ ## 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. -"""Low-level interface for devices connected through a Logitech Universal -Receiver (UR). +"""Low-level interface for devices using the Logitech HID++ protocols. Uses the HID api exposed through hidapi.py, a Python thin layer over a native implementation. -Incomplete. Based on a bit of documentation, trial-and-error, and guesswork. - References: http://julien.danjou.info/blog/2012/logitech-k750-linux-support http://6xq.net/git/lars/lshidpp.git/plain/doc/ """ - -import logging - -logger = logging.getLogger(__name__) -logger.setLevel(logging.root.level) -# if logging.root.level > logging.DEBUG: -# logger.addHandler(logging.NullHandler()) -# logger.propagate = 0 - -del logging - -__version__ = '0.9' diff --git a/lib/logitech_receiver/base.py b/lib/logitech_receiver/base.py index 05903bb507..dcc34f5653 100644 --- a/lib/logitech_receiver/base.py +++ b/lib/logitech_receiver/base.py @@ -30,7 +30,7 @@ import hidapi as _hid -from . import hidpp10 as _hidpp10 +from . import hidpp as _hidpp from . import hidpp20 as _hidpp20 from .base_usb import ALL as _RECEIVER_USB_IDS from .common import KwException as _KwException @@ -489,15 +489,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] + _hidpp.HIDPP_ERROR[error] ) - return _hidpp10.ERROR[error] if return_error else None + return _hidpp.HIDPP_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] + _hidpp.HIDPP20_ERROR[error] ) raise _hidpp20.FeatureCallError(number=devnumber, request=request_id, error=error, params=params) @@ -574,11 +574,12 @@ 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 == _hidpp.HIDPP_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 == _hidpp.HIDPP_ERROR.resource_error or \ + error == _hidpp.HIDPP_ERROR.connection_request_failed: return # device unreachable - if error == _hidpp10.ERROR.unknown_device: # no paired device with that number + if error == _hidpp.HIDPP_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) diff --git a/lib/logitech_receiver/descriptors.py b/lib/logitech_receiver/descriptors.py index b6537f2f8a..8b42ac8cde 100644 --- a/lib/logitech_receiver/descriptors.py +++ b/lib/logitech_receiver/descriptors.py @@ -24,8 +24,8 @@ # - the device uses a USB interface other than 2 # - the name or codename should be different from what the device reports -from .hidpp10 import DEVICE_KIND as _DK -from .hidpp10 import REGISTERS as _R +from .hidpp import DEVICE_KIND as _DK +from .hidpp import REGISTERS as _R # # diff --git a/lib/logitech_receiver/device.py b/lib/logitech_receiver/device.py index c8a6dc1bb1..8a56433636 100644 --- a/lib/logitech_receiver/device.py +++ b/lib/logitech_receiver/device.py @@ -28,6 +28,7 @@ from . import base as _base from . import descriptors as _descriptors +from . import hidpp as _hidpp from . import hidpp10 as _hidpp10 from . import hidpp20 as _hidpp20 from .common import strhex as _strhex @@ -35,10 +36,8 @@ logger = logging.getLogger(__name__) -_R = _hidpp10.REGISTERS -_IR = _hidpp10.INFO_SUBREGISTERS - -KIND_MAP = {kind: _hidpp10.DEVICE_KIND[str(kind)] for kind in _hidpp20.DEVICE_KIND} +_R = _hidpp.REGISTERS +_IR = _hidpp.INFO_SUBREGISTERS # # @@ -80,7 +79,7 @@ def __init__( self.online = self.descriptor = None self.wpid = None # the Wireless PID is unique per device model - self._kind = None # mouse, keyboard, etc (see _hidpp10.DEVICE_KIND) + self._kind = None # mouse, keyboard, etc (see _hidpp.DEVICE_KIND) self._codename = None # Unifying peripherals report a codename. self._name = None # the full name of the model self._protocol = None # HID++ protocol version, 1.0 or 2.0 @@ -123,14 +122,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 = _hidpp.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 _base.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 = _hidpp.DEVICE_KIND[kind] else: # get information from pairing registers self.online = True self.update_pairing_information() @@ -261,8 +260,7 @@ def kind(self): if not self._kind: self.update_pairing_information() if not self._kind and self.protocol >= 2.0: - kind = _hidpp20.get_kind(self) - self._kind = KIND_MAP[kind] if kind else None + self._kind = _hidpp20.get_kind(self) return self._kind or '?' @property @@ -414,10 +412,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 + _hidpp.NOTIFICATION_FLAG.battery_status + | _hidpp.NOTIFICATION_FLAG.keyboard_illumination + | _hidpp.NOTIFICATION_FLAG.wireless + | _hidpp.NOTIFICATION_FLAG.software_present ) else: set_flag_bits = 0 @@ -426,7 +424,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(_hidpp.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/diversion.py b/lib/logitech_receiver/diversion.py index 3da2510f90..720e30c2eb 100644 --- a/lib/logitech_receiver/diversion.py +++ b/lib/logitech_receiver/diversion.py @@ -48,7 +48,7 @@ from yaml import safe_load_all as _yaml_safe_load_all from .common import NamedInt -from .hidpp20 import FEATURE as _F +from .hidpp import FEATURE as _F from .special_keys import CONTROL as _CONTROL gi.require_version('Gdk', '3.0') # isort:skip diff --git a/lib/logitech_receiver/hidpp.py b/lib/logitech_receiver/hidpp.py new file mode 100644 index 0000000000..60b1ebc573 --- /dev/null +++ b/lib/logitech_receiver/hidpp.py @@ -0,0 +1,346 @@ +# -*- python-mode -*- + +## Copyright (C) 2012-2013 Daniel Pavel +## Copyright (C) 2024 Peter F. Patel-Schneider and other Solaar contributors +## +## 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. + +# Constants used in HID++ version 1.0 or 2.0+ + +from .common import NamedInts as _NamedInts + +# +# From HID++ 1.0 with some HID++ 2.0+ additions may also be used in HID++ 2.0+ +# Most as defined by the official Logitech HID++ documentation, some of them guessed. +# + +HIDPP_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, + unknown=0xFF # for HID++ 2.0 unknown +) + +HIDPP20_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 +) + +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) + +DEVICE_KIND = _NamedInts( + unknown=0x00, + keyboard=0x01, + mouse=0x02, + numpad=0x03, + presenter=0x04, + remote=0x07, + trackball=0x08, + touchpad=0x09, + headset=0x0D, + remote_control=0x0E, + receiver=0x0F +) + +HIDPP20_DEVICE_KIND = _NamedInts( + keyboard=0x00, remote_control=0x01, numpad=0x02, mouse=0x03, touchpad=0x04, trackball=0x05, presenter=0x06, receiver=0x07 +) + +# map HID++ 2.0 kinds in to HID++ 1.0 kinds +KIND_MAP = {kind: DEVICE_KIND[str(kind)] for kind in HIDPP20_DEVICE_KIND} + +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, +) +"""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, +) + +# +# From HID++ 2.0 +# Most as defined by the official Logitech HID++ documentation, some of them guessed. +# +"""Features available on a Logitech device. +No device supports all these features. Devices 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) + +FIRMWARE_KIND = _NamedInts(Firmware=0x00, Bootloader=0x01, Hardware=0x02, Other=0x03) + +BATTERY_STATUS = _NamedInts( + discharging=0x00, + recharging=0x01, + almost_full=0x02, + full=0x03, + slow_recharge=0x04, + invalid_battery=0x05, + thermal_error=0x06 +) + +BATTERY_OK = lambda status: status not in (BATTERY_STATUS.invalid_battery, BATTERY_STATUS.thermal_error) + +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) + +ONBOARD_MODES = _NamedInts(MODE_NO_CHANGE=0x00, MODE_ONBOARD=0x01, MODE_HOST=0x02) diff --git a/lib/logitech_receiver/hidpp10.py b/lib/logitech_receiver/hidpp10.py index 51de5c2870..862a5b279e 100644 --- a/lib/logitech_receiver/hidpp10.py +++ b/lib/logitech_receiver/hidpp10.py @@ -20,164 +20,13 @@ 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 .hidpp import BATTERY_STATUS, FIRMWARE_KIND, 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/hidpp20.py b/lib/logitech_receiver/hidpp20.py index 0a9978e790..62592f69c4 100644 --- a/lib/logitech_receiver/hidpp20.py +++ b/lib/logitech_receiver/hidpp20.py @@ -38,6 +38,8 @@ from .common import bytes2int as _bytes2int from .common import crc16 as _crc16 from .common import int2bytes as _int2bytes +from .hidpp import BATTERY_STATUS as BATTERY_STATUS +from .hidpp import CHARGE_LEVEL, CHARGE_STATUS, CHARGE_TYPE, DEVICE_KIND, FEATURE, FIRMWARE_KIND, HIDPP_ERROR, KIND_MAP from .i18n import _ logger = logging.getLogger(__name__) @@ -53,180 +55,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 FeatureNotSupported(_KwException): """Raised when trying to request a feature not supported by the device.""" @@ -1663,15 +1491,14 @@ def get_ids(device): def get_kind(device): """Reads a device's type. - :see DEVICE_KIND: + :see hidpp.HIDPP20_DEVICE_KIND: :returns: a string describing the device type, or ``None`` if the device is not available or does not support the ``DEVICE_NAME`` feature. """ kind = feature_request(device, FEATURE.DEVICE_NAME, 0x20) if kind: kind = ord(kind[:1]) - # if logger.isEnabledFor(logging.DEBUG): - # logger.debug("device %d type %d = %s", devnumber, kind, DEVICE_KIND[kind]) + kind = KIND_MAP[kind] return DEVICE_KIND[kind] @@ -1784,14 +1611,14 @@ def get_battery_voltage(device): def decipher_battery_voltage(report): voltage, flags = _unpack('>HB', report[:3]) status = BATTERY_STATUS.discharging - charge_sts = ERROR.unknown + charge_sts = HIDPP_ERROR.unknown charge_lvl = CHARGE_LEVEL.average charge_type = CHARGE_TYPE.standard if flags & (1 << 7): status = BATTERY_STATUS.recharging charge_sts = CHARGE_STATUS[flags & 0x03] if charge_sts is None: - charge_sts = ERROR.unknown + charge_sts = HIDPP_ERROR.unknown elif charge_sts == CHARGE_STATUS.full: charge_lvl = CHARGE_LEVEL.full status = BATTERY_STATUS.full diff --git a/lib/logitech_receiver/notifications.py b/lib/logitech_receiver/notifications.py index 1d23c1fa6c..7dafebcdee 100644 --- a/lib/logitech_receiver/notifications.py +++ b/lib/logitech_receiver/notifications.py @@ -25,6 +25,7 @@ from solaar.ui.config_panel import record_setting from . import diversion as _diversion +from . import hidpp as _hidpp from . import hidpp10 as _hidpp10 from . import hidpp20 as _hidpp20 from . import settings_templates as _st @@ -36,8 +37,8 @@ logger = logging.getLogger(__name__) -_R = _hidpp10.REGISTERS -_F = _hidpp20.FEATURE +_R = _hidpp.REGISTERS +_F = _hidpp.FEATURE # # @@ -79,7 +80,7 @@ def _process_receiver_notification(receiver, status, n): status.new_device = None pair_error = ord(n.data[:1]) if pair_error: - status[_K.ERROR] = error_string = _hidpp10.PAIRING_ERRORS[pair_error] + status[_K.ERROR] = error_string = _hidpp.PAIRING_ERRORS[pair_error] status.new_device = None logger.warning('pairing error %d: %s', pair_error, error_string) status.changed(reason=reason) @@ -97,7 +98,7 @@ def _process_receiver_notification(receiver, status, n): status.device_passkey = None discover_error = ord(n.data[:1]) if discover_error: - status[_K.ERROR] = discover_string = _hidpp10.BOLT_PAIRING_ERRORS[discover_error] + status[_K.ERROR] = discover_string = _hidpp.BOLT_PAIRING_ERRORS[discover_error] logger.warning('bolt discovering error %d: %s', discover_error, discover_string) status.changed(reason=reason) return True @@ -134,7 +135,7 @@ def _process_receiver_notification(receiver, status, n): elif n.address == 0x02 and not pair_error: status.new_device = receiver.register_new_device(n.data[7]) if pair_error: - status[_K.ERROR] = error_string = _hidpp10.BOLT_PAIRING_ERRORS[pair_error] + status[_K.ERROR] = error_string = _hidpp.BOLT_PAIRING_ERRORS[pair_error] status.new_device = None logger.warning('pairing error %d: %s', pair_error, error_string) status.changed(reason=reason) @@ -353,14 +354,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 = _hidpp.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 = _hidpp.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/receiver.py b/lib/logitech_receiver/receiver.py index b600c768d6..0bdf1b7e49 100644 --- a/lib/logitech_receiver/receiver.py +++ b/lib/logitech_receiver/receiver.py @@ -22,6 +22,7 @@ import hidapi as _hid from . import base as _base +from . import hidpp as _hidpp from . import hidpp10 as _hidpp10 from .base import product_information as _product_information from .common import strhex as _strhex @@ -29,8 +30,8 @@ logger = logging.getLogger(__name__) -_R = _hidpp10.REGISTERS -_IR = _hidpp10.INFO_SUBREGISTERS +_R = _hidpp.REGISTERS +_IR = _hidpp.INFO_SUBREGISTERS # # @@ -120,9 +121,9 @@ def enable_connection_notifications(self, enable=True): if enable: set_flag_bits = ( - _hidpp10.NOTIFICATION_FLAG.battery_status - | _hidpp10.NOTIFICATION_FLAG.wireless - | _hidpp10.NOTIFICATION_FLAG.software_present + _hidpp.NOTIFICATION_FLAG.battery_status + | _hidpp.NOTIFICATION_FLAG.wireless + | _hidpp.NOTIFICATION_FLAG.software_present ) else: set_flag_bits = 0 @@ -132,7 +133,7 @@ def enable_connection_notifications(self, enable=True): return None 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(_hidpp.NOTIFICATION_FLAG.flag_names(flag_bits)) if logger.isEnabledFor(logging.INFO): logger.info('%s: receiver notifications %s => %s', self, 'enabled' if enable else 'disabled', flag_names) return flag_bits @@ -154,7 +155,7 @@ 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 = _hidpp.HIDPP10_DEVICE_KIND[ord(pair_info[1:2]) & 0x0F] return wpid, kind, 0 else: raise _base.NoSuchDevice(number=n, receiver=self, error='read Bolt wpid') @@ -164,19 +165,19 @@ def device_pairing_information(self, n): 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 = _hidpp.HIDPP10_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') - kind = _hidpp10.DEVICE_KIND[self.get_kind_from_index(n)] + kind = _hidpp.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 = _hidpp.HIDPP10_DEVICE_KIND[0x00] # unknown kind else: raise _base.NoSuchDevice(number=n, receiver=self, error='read pairing information - non-unifying') else: @@ -195,7 +196,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 = _hidpp.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.py b/lib/logitech_receiver/settings.py index 5bc136b389..a0ac13ea87 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 hidpp as _hidpp 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) == _hidpp.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[_hidpp.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) == _hidpp.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 _hidpp.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 8066e315fd..34be33547d 100644 --- a/lib/logitech_receiver/settings_templates.py +++ b/lib/logitech_receiver/settings_templates.py @@ -26,7 +26,7 @@ from traceback import format_exc as _format_exc from . import descriptors as _descriptors -from . import hidpp10 as _hidpp10 +from . import hidpp as _hidpp from . import hidpp20 as _hidpp20 from . import special_keys as _special_keys from .base import _HIDPP_Notification as _HIDPP_Notification @@ -58,9 +58,9 @@ logger = logging.getLogger(__name__) -_DK = _hidpp10.DEVICE_KIND -_R = _hidpp10.REGISTERS -_F = _hidpp20.FEATURE +_DK = _hidpp.DEVICE_KIND +_R = _hidpp.REGISTERS +_F = _hidpp.FEATURE _GG = _hidpp20.GESTURE _GP = _hidpp20.PARAM @@ -556,8 +556,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) != _hidpp.ONBOARD_MODES.MODE_HOST: + _hidpp20.set_onboard_mode(device, _hidpp.ONBOARD_MODES.MODE_HOST) return super().write(device, data_bytes) class validator_class(_ChoicesV): @@ -602,8 +602,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) != _hidpp.ONBOARD_MODES.MODE_HOST: + _hidpp20.set_onboard_mode(device, _hidpp.ONBOARD_MODES.MODE_HOST) return super().write(device, data_bytes) class validator_class(_ChoicesV): @@ -849,7 +849,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, _hidpp.FEATURE.MOUSE_GESTURE) self.fsmState = 'idle' def move_action(self, dx, dy): diff --git a/lib/logitech_receiver/status.py b/lib/logitech_receiver/status.py index 60d5837f6c..bb9b59875b 100644 --- a/lib/logitech_receiver/status.py +++ b/lib/logitech_receiver/status.py @@ -20,8 +20,8 @@ from time import time as _timestamp +from . import hidpp as _hidpp from . import hidpp10 as _hidpp10 -from . import hidpp20 as _hidpp20 from . import settings as _settings from .common import BATTERY_APPROX as _BATTERY_APPROX from .common import NamedInt as _NamedInt @@ -30,7 +30,7 @@ logger = logging.getLogger(__name__) -_R = _hidpp10.REGISTERS +_R = _hidpp.REGISTERS # # @@ -199,11 +199,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 == _hidpp.BATTERY_STATUS.full: level = _BATTERY_APPROX.full - elif status in (_hidpp20.BATTERY_STATUS.almost_full, _hidpp20.BATTERY_STATUS.recharging): + elif status in (_hidpp.BATTERY_STATUS.almost_full, _hidpp.BATTERY_STATUS.recharging): level = _BATTERY_APPROX.good - elif status == _hidpp20.BATTERY_STATUS.slow_recharge: + elif status == _hidpp.BATTERY_STATUS.slow_recharge: level = _BATTERY_APPROX.low else: level = self.get(KEYS.BATTERY_LEVEL) @@ -217,15 +217,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 + _hidpp.BATTERY_STATUS.recharging, _hidpp.BATTERY_STATUS.almost_full, _hidpp.BATTERY_STATUS.full, + _hidpp.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 _hidpp.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 +284,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 _hidpp.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..958ed5d220 100644 --- a/lib/solaar/cli/pair.py +++ b/lib/solaar/cli/pair.py @@ -19,11 +19,12 @@ from time import time as _timestamp from logitech_receiver import base as _base +from logitech_receiver import hidpp as _hidpp from logitech_receiver import hidpp10 as _hidpp10 from logitech_receiver import notifications as _notifications from logitech_receiver import status as _status -_R = _hidpp10.REGISTERS +_R = _hidpp.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 & _hidpp.NOTIFICATION_FLAG.wireless): + _hidpp10.set_notification_flags(receiver, old_notification_flags | _hidpp.NOTIFICATION_FLAG.wireless) # get all current devices known_devices = [dev.number for dev in receiver] @@ -85,7 +86,7 @@ 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 == _hidpp.DEVICE_KIND.keyboard else 10 ) pairing_start = _timestamp() patience = 5 # the discovering notification may come slightly later, so be patient @@ -99,7 +100,7 @@ def notifications_hook(self, n): if authentication & 0x01: print(f'Bolt Pairing: type passkey {receiver.status.device_passkey} and then press the enter key') else: - passkey = f'{int(receiver.status.device_passkey):010b}' + passkey = f'{int(receiver.status.device_passkey):010b}' # NOQA passkey = ', '.join(['right' if bit == '1' else 'left' for bit in passkey]) print(f'Bolt Pairing: press {passkey}') print('and then press left and right buttons simultaneously') @@ -121,7 +122,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 & _hidpp.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..da896478d7 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 hidpp as _hidpp from logitech_receiver.common import strhex as _strhex from solaar.cli.show import _print_device, _print_receiver -_R = _hidpp10.REGISTERS +_R = _hidpp.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 == _hidpp.HIDPP_ERROR.invalid_address: break - elif isinstance(rgst, int) and rgst == _hidpp10.ERROR.invalid_value: + elif isinstance(rgst, int) and rgst == _hidpp.HIDPP_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 == _hidpp.HIDPP_ERROR.invalid_address: break - elif isinstance(rgst, int) and rgst == _hidpp10.ERROR.invalid_value: + elif isinstance(rgst, int) and rgst == _hidpp.HIDPP_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 9b204d0ab1..62610a5757 100644 --- a/lib/solaar/cli/show.py +++ b/lib/solaar/cli/show.py @@ -17,6 +17,7 @@ ## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. from logitech_receiver import base as _base +from logitech_receiver import hidpp as _hidpp from logitech_receiver import hidpp10 as _hidpp10 from logitech_receiver import hidpp20 as _hidpp20 from logitech_receiver import receiver as _receiver @@ -25,7 +26,7 @@ from logitech_receiver.common import strhex as _strhex from solaar import NAME, __version__ -_F = _hidpp20.FEATURE +_F = _hidpp.FEATURE def _print_receiver(receiver): @@ -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 = _hidpp.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(_hidpp.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 = _hidpp.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 = _hidpp.DEVICE_FEATURES.flag_names(device_features) print(' Features: %s (0x%06X)' % (', '.join(device_features_names), device_features)) else: print(' Features: (none)') @@ -141,11 +142,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 = _hidpp.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 == _hidpp.FEATURE.HIRES_WHEEL: wheel = _hidpp20.get_hires_wheel(dev) if wheel: multi, has_invert, has_switch, inv, res, target, ratchet = wheel @@ -162,7 +163,7 @@ def _print_device(dev, num=None): print(' HID++ notification') else: print(' HID notification') - elif feature == _hidpp20.FEATURE.MOUSE_POINTER: + elif feature == _hidpp.FEATURE.MOUSE_POINTER: mouse_pointer = _hidpp20.get_mouse_pointer_info(dev) if mouse_pointer: print(' DPI: %s' % mouse_pointer['dpi']) @@ -175,13 +176,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 == _hidpp.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 == _hidpp.FEATURE.HI_RES_SCROLLING: scrolling_mode, scrolling_resolution = _hidpp20.get_hi_res_scrolling_info(dev) if scrolling_mode: print(' Hi-res scrolling enabled') @@ -189,30 +190,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 == _hidpp.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 == _hidpp.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 == _hidpp.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 == _hidpp.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 == _hidpp.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 == _hidpp.FEATURE.DEVICE_FRIENDLY_NAME: print(' Friendly Name: %s' % _hidpp20.get_friendly_name(dev)) - elif feature == _hidpp20.FEATURE.DEVICE_FW_VERSION: + elif feature == _hidpp.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)) @@ -220,12 +221,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 == _hidpp.FEATURE.REPORT_RATE or feature == _hidpp.FEATURE.EXTENDED_ADJUSTABLE_REPORT_RATE: print(' Report Rate: %s' % _hidpp20.get_polling_rate(dev)) - elif feature == _hidpp20.FEATURE.REMAINING_PAIRING: + elif feature == _hidpp.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 == _hidpp.FEATURE.ONBOARD_PROFILES: + if _hidpp20.get_onboard_mode(dev) == _hidpp.ONBOARD_MODES.MODE_HOST: mode = 'Host' else: mode = 'On-Board' @@ -251,9 +252,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 == _hidpp.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 == _hidpp.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' diff --git a/lib/solaar/listener.py b/lib/solaar/listener.py index 0e4c027c6a..89cfc6878b 100644 --- a/lib/solaar/listener.py +++ b/lib/solaar/listener.py @@ -28,7 +28,7 @@ import logitech_receiver.receiver as _receiver from logitech_receiver import base as _base -from logitech_receiver import hidpp10 as _hidpp10 +from logitech_receiver import hidpp as _hidpp from logitech_receiver import listener as _listener from logitech_receiver import notifications as _notifications from logitech_receiver import status as _status @@ -42,8 +42,8 @@ logger = logging.getLogger(__name__) -_R = _hidpp10.REGISTERS -_IR = _hidpp10.INFO_SUBREGISTERS +_R = _hidpp.REGISTERS +_IR = _hidpp.INFO_SUBREGISTERS # # @@ -89,7 +89,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) & _hidpp.NOTIFICATION_FLAG.wireless): logger.warning( 'Receiver on %s might not support connection notifications, GUI might not show its devices', self.receiver.path diff --git a/lib/solaar/ui/__init__.py b/lib/solaar/ui/__init__.py index b8673ffb14..aafa702da8 100644 --- a/lib/solaar/ui/__init__.py +++ b/lib/solaar/ui/__init__.py @@ -23,11 +23,10 @@ from logitech_receiver.status import ALERT from solaar.i18n import _ -from solaar.tasks import TaskRunner as _TaskRunner from solaar.ui.config_panel import change_setting from solaar.ui.window import find_device -from . import diversion_rules, notify, tray, window +from . import common, diversion_rules, notify, tray, window gi.require_version('Gtk', '3.0') from gi.repository import Gio, GLib, Gtk # NOQA: E402 @@ -51,9 +50,7 @@ def _startup(app, startup_hook, use_tray, show_window): if logger.isEnabledFor(logging.DEBUG): logger.debug('startup registered=%s, remote=%s', app.get_is_registered(), app.get_is_remote()) - global _task_runner - _task_runner = _TaskRunner('AsyncUI') - _task_runner.start() + common.start_async() notify.init() if use_tray: @@ -94,10 +91,7 @@ def _shutdown(app, shutdown_hook): shutdown_hook() - # stop the async UI processor - global _task_runner - _task_runner.stop() - _task_runner = None + common.stop_async() tray.destroy() notify.uninit() diff --git a/lib/solaar/ui/common.py b/lib/solaar/ui/common.py index 22440f885a..a7e3dbaca4 100644 --- a/lib/solaar/ui/common.py +++ b/lib/solaar/ui/common.py @@ -21,6 +21,7 @@ import gi from solaar.i18n import _ +from solaar.tasks import TaskRunner as _TaskRunner gi.require_version('Gtk', '3.0') from gi.repository import GLib, Gtk # NOQA: E402 @@ -70,9 +71,21 @@ def error_dialog(reason, object): # # -task_runner = None +_task_runner = None def ui_async(function, *args, **kwargs): - if task_runner: - task_runner(function, *args, **kwargs) + if _task_runner: + _task_runner(function, *args, **kwargs) + + +def start_async(): + global _task_runner + _task_runner = _TaskRunner('AsyncUI') + _task_runner.start() + + +def stop_async(): + global _task_runner + _task_runner.stop() + _task_runner = None diff --git a/lib/solaar/ui/diversion_rules.py b/lib/solaar/ui/diversion_rules.py index fc71a0ea78..88fce02174 100644 --- a/lib/solaar/ui/diversion_rules.py +++ b/lib/solaar/ui/diversion_rules.py @@ -33,7 +33,7 @@ from logitech_receiver.diversion import XK_KEYS as _XK_KEYS from logitech_receiver.diversion import Key as _Key from logitech_receiver.diversion import buttons as _buttons -from logitech_receiver.hidpp20 import FEATURE as _ALL_FEATURES +from logitech_receiver.hidpp import FEATURE as _ALL_FEATURES from logitech_receiver.settings import KIND as _SKIND from logitech_receiver.settings import Setting as _Setting from logitech_receiver.settings_templates import SETTINGS as _SETTINGS @@ -1959,7 +1959,7 @@ def left_label(cls, component): @classmethod def right_label(cls, component): - return f'{component.button} ({"x" if isinstance(component.count, int) else ""}{component.count})' + return f"{component.button} ({'x' if isinstance(component.count, int) else ''}{component.count})" class ExecuteUI(ActionUI): diff --git a/lib/solaar/ui/pair_window.py b/lib/solaar/ui/pair_window.py index 4664a8d5de..4d134c0135 100644 --- a/lib/solaar/ui/pair_window.py +++ b/lib/solaar/ui/pair_window.py @@ -19,7 +19,7 @@ import logging from gi.repository import GLib, Gtk -from logitech_receiver import hidpp10 as _hidpp10 +from logitech_receiver import hidpp as _hidpp from logitech_receiver.status import KEYS as _K from solaar.i18n import _, ngettext @@ -90,7 +90,7 @@ def _check_lock_state(assistant, receiver, count=2): authentication = receiver.status.device_authentication name = receiver.status.device_name if 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 == _hidpp.DEVICE_KIND.keyboard else 10 ): return True else: @@ -124,7 +124,7 @@ def _show_passcode(assistant, receiver, passkey): page_text += _('Type %(passcode)s and then press the enter key.') % {'passcode': receiver.status.device_passkey} else: passcode = ', '.join([ - _('right') if bit == '1' else _('left') for bit in f'{int(receiver.status.device_passkey):010b}' + _('right') if bit == '1' else _('left') for bit in f'{int(receiver.status.device_passkey):010b}' # NOQA ]) page_text += _('Press %(code)s\nand then press left and right buttons simultaneously.') % {'code': passcode} page = _create_page(assistant, Gtk.AssistantPageType.PROGRESS, intro_text, 'preferences-desktop-peripherals', page_text) diff --git a/lib/solaar/ui/window.py b/lib/solaar/ui/window.py index 5728e23444..55fa80b100 100644 --- a/lib/solaar/ui/window.py +++ b/lib/solaar/ui/window.py @@ -21,7 +21,7 @@ import gi from gi.repository.GObject import TYPE_PYOBJECT -from logitech_receiver import hidpp10 as _hidpp10 +from logitech_receiver import hidpp as _hidpp from logitech_receiver.common import NamedInt as _NamedInt from logitech_receiver.common import NamedInts as _NamedInts from logitech_receiver.status import KEYS as _K @@ -570,7 +570,7 @@ def _details_items(device, read_all=False): flag_bits = device.status.get(_K.NOTIFICATION_FLAGS) if flag_bits is not None: - flag_names = ('(%s)' % _('none'), ) if flag_bits == 0 else _hidpp10.NOTIFICATION_FLAG.flag_names(flag_bits) + flag_names = ('(%s)' % _('none'), ) if flag_bits == 0 else _hidpp.NOTIFICATION_FLAG.flag_names(flag_bits) yield (_('Notifications'), ('\n%15s' % ' ').join(flag_names)) def _set_details(text):