diff --git a/lib/logitech_receiver/base.py b/lib/logitech_receiver/base.py index 53c49dabc8..c8ae17993e 100644 --- a/lib/logitech_receiver/base.py +++ b/lib/logitech_receiver/base.py @@ -20,32 +20,28 @@ from __future__ import annotations import logging -import threading as _threading +import struct +import threading from collections import namedtuple from contextlib import contextmanager -from random import getrandbits as _random_bits -from struct import pack as _pack -from time import time as _timestamp +from random import getrandbits +from time import time -import hidapi as _hid +import hidapi +from . import base_usb +from . import common +from . import descriptors from . import exceptions -from . import hidpp10_constants as _hidpp10_constants +from . import hidpp10_constants from . import hidpp20 -from . import hidpp20_constants as _hidpp20_constants -from .base_usb import ALL as _RECEIVER_USB_IDS -from .common import strhex as _strhex -from .descriptors import DEVICES as _DEVICES +from . import hidpp20_constants logger = logging.getLogger(__name__) _hidpp20 = hidpp20.Hidpp20() -# -# -# - def _wired_device(product_id, interface): return {"vendor_id": 1133, "product_id": product_id, "bus_id": 3, "usb_interface": interface, "isDevice": True} @@ -57,7 +53,7 @@ def _bt_device(product_id): DEVICE_IDS = [] -for _ignore, d in _DEVICES.items(): +for _ignore, d in descriptors.DEVICES.items(): if d.usbid: DEVICE_IDS.append(_wired_device(d.usbid, d.interface if d.interface else 2)) if d.btid: @@ -81,7 +77,7 @@ def product_information(usb_id: int | str) -> dict: if isinstance(usb_id, str): usb_id = int(usb_id, 16) - for r in _RECEIVER_USB_IDS: + for r in base_usb.ALL: if usb_id == r.get("product_id"): return r return {} @@ -131,7 +127,7 @@ def match(record, bus_id, vendor_id, product_id): def filter_receivers(bus_id, vendor_id, product_id, hidpp_short=False, hidpp_long=False): """Check that this product is a Logitech receiver and if so return the receiver record for further checking""" - for record in _RECEIVER_USB_IDS: # known receivers + for record in base_usb.ALL: # known receivers if match(record, bus_id, vendor_id, product_id): return record if vendor_id == 0x046D and 0xC500 <= product_id <= 0xC5FF: # unknown receiver @@ -140,7 +136,7 @@ def filter_receivers(bus_id, vendor_id, product_id, hidpp_short=False, hidpp_lon def receivers(): """Enumerate all the receivers attached to the machine.""" - yield from _hid.enumerate(filter_receivers) + yield from hidapi.enumerate(filter_receivers) def filter(bus_id, vendor_id, product_id, hidpp_short=False, hidpp_long=False): @@ -159,12 +155,12 @@ def filter(bus_id, vendor_id, product_id, hidpp_short=False, hidpp_long=False): def receivers_and_devices(): """Enumerate all the receivers and devices directly attached to the machine.""" - yield from _hid.enumerate(filter) + yield from hidapi.enumerate(filter) def notify_on_receivers_glib(callback): """Watch for matching devices and notifies the callback on the GLib thread.""" - return _hid.monitor_glib(callback, filter) + return hidapi.monitor_glib(callback, filter) # @@ -185,7 +181,7 @@ def open_path(path): :returns: an open receiver handle if this is the right Linux device, or ``None``. """ - return _hid.open_path(path) + return hidapi.open_path(path) def open(): @@ -204,13 +200,11 @@ def close(handle): if handle: try: if isinstance(handle, int): - _hid.close(handle) + hidapi.close(handle) else: handle.close() - # logger.info("closed receiver handle %r", handle) return True except Exception: - # logger.exception("closing receiver handle %r", handle) pass return False @@ -234,14 +228,21 @@ def write(handle, devnumber, data, long_message=False): assert isinstance(data, bytes), (repr(data), type(data)) if long_message or len(data) > _SHORT_MESSAGE_SIZE - 2 or data[:1] == b"\x82": - wdata = _pack("!BB18s", HIDPP_LONG_MESSAGE_ID, devnumber, data) + wdata = struct.pack("!BB18s", HIDPP_LONG_MESSAGE_ID, devnumber, data) else: - wdata = _pack("!BB5s", HIDPP_SHORT_MESSAGE_ID, devnumber, data) + wdata = struct.pack("!BB5s", HIDPP_SHORT_MESSAGE_ID, devnumber, data) if logger.isEnabledFor(logging.DEBUG): - logger.debug("(%s) <= w[%02X %02X %s %s]", handle, ord(wdata[:1]), devnumber, _strhex(wdata[2:4]), _strhex(wdata[4:])) + logger.debug( + "(%s) <= w[%02X %02X %s %s]", + handle, + ord(wdata[:1]), + devnumber, + common.strhex(wdata[2:4]), + common.strhex(wdata[4:]), + ) try: - _hid.write(int(handle), wdata) + hidapi.write(int(handle), wdata) except Exception as reason: logger.error("write failed, assuming handle %r no longer available", handle) close(handle) @@ -274,7 +275,7 @@ def check_message(data): if report_lengths.get(report_id) == len(data): return True else: - logger.warning(f"unexpected message size: report_id {report_id:02X} message {_strhex(data)}") + logger.warning(f"unexpected message size: report_id {report_id:02X} message {common.strhex(data)}") return False @@ -290,7 +291,7 @@ def _read(handle, timeout): try: # convert timeout to milliseconds, the hidapi expects it timeout = int(timeout * 1000) - data = _hid.read(int(handle), _MAX_READ_SIZE, timeout) + data = hidapi.read(int(handle), _MAX_READ_SIZE, timeout) except Exception as reason: logger.warning("read failed, assuming handle %r no longer available", handle) close(handle) @@ -303,7 +304,9 @@ def _read(handle, timeout): if logger.isEnabledFor(logging.DEBUG) and ( report_id != DJ_MESSAGE_ID or ord(data[2:3]) > 0x10 ): # ignore DJ input messages - logger.debug("(%s) => r[%02X %02X %s %s]", handle, report_id, devnumber, _strhex(data[2:4]), _strhex(data[4:])) + logger.debug( + "(%s) => r[%02X %02X %s %s]", handle, report_id, devnumber, common.strhex(data[2:4]), common.strhex(data[4:]) + ) return report_id, devnumber, data[2:] @@ -322,7 +325,7 @@ def _skip_incoming(handle, ihandle, notifications_hook): while True: try: # read whatever is already in the buffer, if any - data = _hid.read(ihandle, _MAX_READ_SIZE, 0) + data = hidapi.read(ihandle, _MAX_READ_SIZE, 0) except Exception as reason: logger.error("read failed, assuming receiver %s no longer available", handle) close(handle) @@ -380,14 +383,10 @@ def make_notification(report_id, devnumber, data): self.devnumber, self.sub_id, self.address, - _strhex(self.data), + common.strhex(self.data), ) -# -# -# - -request_lock = _threading.Lock() # serialize all requests +request_lock = threading.Lock() # serialize all requests handles_lock = {} @@ -396,7 +395,7 @@ def handle_lock(handle): if handles_lock.get(handle) is None: if logger.isEnabledFor(logging.INFO): logger.info("New lock %s", repr(handle)) - handles_lock[handle] = _threading.Lock() # Serialize requests on the handle + handles_lock[handle] = threading.Lock() # Serialize requests on the handle return handles_lock[handle] @@ -422,10 +421,6 @@ def request(handle, devnumber, request_id, *params, no_reply=False, return_error :param params: parameters for the feature call, 3 to 16 bytes. :returns: the reply data, or ``None`` if some error occurred. or no reply expected """ - - # import inspect as _inspect - # print ('\n '.join(str(s) for s in _inspect.stack())) - with acquire_timeout(handle_lock(handle), handle, 10.0): assert isinstance(request_id, int) if (devnumber != 0xFF or protocol >= 2.0) and request_id < 0x8000: @@ -434,7 +429,7 @@ def request(handle, devnumber, request_id, *params, no_reply=False, return_error # most significant bit (8) in SoftwareId, to make notifications easier # to distinguish from request replies. # This only applies to peripheral requests, ofc. - request_id = (request_id & 0xFFF0) | 0x08 | _random_bits(3) + request_id = (request_id & 0xFFF0) | 0x08 | getrandbits(3) timeout = _RECEIVER_REQUEST_TIMEOUT if devnumber == 0xFF else _DEVICE_REQUEST_TIMEOUT # be extra patient on long register read @@ -442,12 +437,10 @@ def request(handle, devnumber, request_id, *params, no_reply=False, return_error timeout *= 2 if params: - params = b"".join(_pack("B", p) if isinstance(p, int) else p for p in params) + params = b"".join(struct.pack("B", p) if isinstance(p, int) else p for p in params) else: params = b"" - # if logger.isEnabledFor(logging.DEBUG): - # logger.debug("(%s) device %d request_id {%04X} params [%s]", handle, devnumber, request_id, _strhex(params)) - request_data = _pack("!H", request_id) + params + request_data = struct.pack("!H", request_id) + params ihandle = int(handle) notifications_hook = getattr(handle, "notifications_hook", None) @@ -462,7 +455,7 @@ def request(handle, devnumber, request_id, *params, no_reply=False, return_error return None # we consider timeout from this point - request_started = _timestamp() + request_started = time() delta = 0 while delta < timeout: @@ -473,7 +466,7 @@ def request(handle, devnumber, request_id, *params, no_reply=False, return_error if reply_devnumber == devnumber or reply_devnumber == devnumber ^ 0xFF: # BT device returning 0x00 if ( report_id == HIDPP_SHORT_MESSAGE_ID - and reply_data[:1] == b"\x8F" + and reply_data[:1] == b"\x8f" and reply_data[1:3] == request_data[:2] ): error = ord(reply_data[3:4]) @@ -485,10 +478,10 @@ def request(handle, devnumber, request_id, *params, no_reply=False, return_error devnumber, request_id, error, - _hidpp10_constants.ERROR[error], + hidpp10_constants.ERROR[error], ) - return _hidpp10_constants.ERROR[error] if return_error else None - if reply_data[:1] == b"\xFF" and reply_data[1:3] == request_data[:2]: + 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( @@ -497,7 +490,7 @@ def request(handle, devnumber, request_id, *params, no_reply=False, return_error devnumber, request_id, error, - _hidpp20_constants.ERROR[error], + hidpp20_constants.ERROR[error], ) raise exceptions.FeatureCallError(number=devnumber, request=request_id, error=error, params=params) @@ -517,20 +510,13 @@ def request(handle, devnumber, request_id, *params, no_reply=False, return_error else: # a reply was received, but did not match our request in any way # reset the timeout starting point - request_started = _timestamp() + request_started = time() if notifications_hook: n = make_notification(report_id, reply_devnumber, reply_data) if n: notifications_hook(n) - # elif logger.isEnabledFor(logging.DEBUG): - # logger.debug("(%s) ignoring reply %02X [%s]", handle, reply_devnumber, _strhex(reply_data)) - # elif logger.isEnabledFor(logging.DEBUG): - # logger.debug("(%s) ignoring reply %02X [%s]", handle, reply_devnumber, _strhex(reply_data)) - - delta = _timestamp() - request_started - # if logger.isEnabledFor(logging.DEBUG): - # logger.debug("(%s) still waiting for reply, delta %f", handle, delta) + delta = time() - request_started logger.warning( "timeout (%0.2f/%0.2f) on device %d request {%04X} params [%s]", @@ -538,7 +524,7 @@ def request(handle, devnumber, request_id, *params, no_reply=False, return_error timeout, devnumber, request_id, - _strhex(params), + common.strhex(params), ) # raise DeviceUnreachable(number=devnumber, request=request_id) @@ -560,11 +546,11 @@ def ping(handle, devnumber, long_message=False): # randomize the SoftwareId and mark byte to be able to identify the ping # reply, and set most significant (0x8) bit in SoftwareId so that the reply # is always distinguishable from notifications - request_id = 0x0018 | _random_bits(3) - request_data = _pack("!HBBB", request_id, 0, 0, _random_bits(8)) + request_id = 0x0018 | getrandbits(3) + request_data = struct.pack("!HBBB", request_id, 0, 0, getrandbits(8)) write(int(handle), devnumber, request_data, long_message) - request_started = _timestamp() # we consider timeout from this point + request_started = time() # we consider timeout from this point delta = 0 while delta < _PING_TIMEOUT: reply = _read(handle, _PING_TIMEOUT) @@ -577,18 +563,18 @@ def ping(handle, devnumber, long_message=False): if ( report_id == HIDPP_SHORT_MESSAGE_ID - and reply_data[:1] == b"\x8F" + 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_constants.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_constants.ERROR.resource_error - or error == _hidpp10_constants.ERROR.connection_request_failed + 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 + 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 exceptions.NoSuchDevice(number=devnumber, request=request_id) @@ -596,9 +582,7 @@ def ping(handle, devnumber, long_message=False): n = make_notification(report_id, reply_devnumber, reply_data) if n: notifications_hook(n) - # elif logger.isEnabledFor(logging.DEBUG): - # logger.debug("(%s) ignoring reply %02X [%s]", handle, reply_devnumber, _strhex(reply_data)) - delta = _timestamp() - request_started + delta = time() - request_started logger.warning("(%s) timeout (%0.2f/%0.2f) on device %d ping", handle, delta, _PING_TIMEOUT, devnumber) diff --git a/lib/logitech_receiver/common.py b/lib/logitech_receiver/common.py index d770a63b3b..6fce9c603f 100644 --- a/lib/logitech_receiver/common.py +++ b/lib/logitech_receiver/common.py @@ -14,16 +14,16 @@ ## You should have received a copy of the GNU General Public License along ## with this program; if not, write to the Free Software Foundation, Inc., ## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +from __future__ import annotations -# Some common functions and types. +import binascii +import dataclasses -from binascii import hexlify as _hexlify -from dataclasses import dataclass from enum import IntEnum from typing import Optional from typing import Union -import yaml as _yaml +import yaml from solaar.i18n import _ @@ -348,8 +348,8 @@ def to_yaml(cls, dumper, data): return dumper.represent_mapping("!NamedInt", {"value": int(data), "name": data.name}, flow_style=True) -_yaml.SafeLoader.add_constructor("!NamedInt", NamedInt.from_yaml) -_yaml.add_representer(NamedInt, NamedInt.to_yaml) +yaml.SafeLoader.add_constructor("!NamedInt", NamedInt.from_yaml) +yaml.add_representer(NamedInt, NamedInt.to_yaml) class NamedInts: @@ -512,7 +512,7 @@ def __or__(self, other): def strhex(x): assert x is not None """Produce a hex-string representation of a sequence of bytes.""" - return _hexlify(x).decode("ascii").upper() + return binascii.hexlify(x).decode("ascii").upper() def bytes2int(x, signed=False): @@ -541,7 +541,7 @@ def __getattr__(self, k): return self.args[0].get(k) # was self.args[0][k] -@dataclass +@dataclasses.dataclass class FirmwareInfo: kind: str name: str @@ -567,7 +567,7 @@ class BatteryLevelApproximation(IntEnum): FULL = 90 -@dataclass +@dataclasses.dataclass class Battery: """Information about the current state of a battery""" diff --git a/lib/logitech_receiver/descriptors.py b/lib/logitech_receiver/descriptors.py index 3548b2fd76..fb60f51c3a 100644 --- a/lib/logitech_receiver/descriptors.py +++ b/lib/logitech_receiver/descriptors.py @@ -22,12 +22,8 @@ # - the device uses a USB interface other than 2 # - the name or codename should be different from what the device reports -from .hidpp10_constants import DEVICE_KIND as _DK -from .hidpp10_constants import REGISTERS as _R - -# -# -# +from .hidpp10_constants import DEVICE_KIND +from .hidpp10_constants import REGISTERS as REG class _DeviceDescriptor: @@ -73,15 +69,15 @@ def _D( ): if kind is None: kind = ( - _DK.mouse + DEVICE_KIND.mouse if "Mouse" in name - else _DK.keyboard + else DEVICE_KIND.keyboard if "Keyboard" in name - else _DK.numpad + else DEVICE_KIND.numpad if "Number Pad" in name - else _DK.touchpad + else DEVICE_KIND.touchpad if "Touchpad" in name - else _DK.trackball + else DEVICE_KIND.trackball if "Trackball" in name else None ) @@ -94,9 +90,12 @@ def _D( assert w[0:1] == "4", f"{name} has protocol {protocol:0.1f}, wpid {w}" else: if w[0:1] == "1": - assert kind == _DK.mouse, f"{name} has protocol {protocol:0.1f}, wpid {w}" + assert kind == DEVICE_KIND.mouse, f"{name} has protocol {protocol:0.1f}, wpid {w}" elif w[0:1] == "2": - assert kind in (_DK.keyboard, _DK.numpad), f"{name} has protocol {protocol:0.1f}, wpid {w}" + assert kind in ( + DEVICE_KIND.keyboard, + DEVICE_KIND.numpad, + ), f"{name} has protocol {protocol:0.1f}, wpid {w}" device_descriptor = _DeviceDescriptor( name=name, @@ -192,24 +191,24 @@ def get_btid(btid): # Keyboards -_D("Wireless Keyboard EX110", codename="EX110", protocol=1.0, wpid="0055", registers=(_R.battery_status,)) -_D("Wireless Keyboard S510", codename="S510", protocol=1.0, wpid="0056", registers=(_R.battery_status,)) -_D("Wireless Wave Keyboard K550", codename="K550", protocol=1.0, wpid="0060", registers=(_R.battery_status,)) -_D("Wireless Keyboard EX100", codename="EX100", protocol=1.0, wpid="0065", registers=(_R.battery_status,)) -_D("Wireless Keyboard MK300", codename="MK300", protocol=1.0, wpid="0068", registers=(_R.battery_status,)) -_D("Number Pad N545", codename="N545", protocol=1.0, wpid="2006", registers=(_R.battery_status,)) -_D("Wireless Compact Keyboard K340", codename="K340", protocol=1.0, wpid="2007", registers=(_R.battery_status,)) -_D("Wireless Keyboard MK700", codename="MK700", protocol=1.0, wpid="2008", registers=(_R.battery_status,)) -_D("Wireless Wave Keyboard K350", codename="K350", protocol=1.0, wpid="200A", registers=(_R.battery_status,)) -_D("Wireless Keyboard MK320", codename="MK320", protocol=1.0, wpid="200F", registers=(_R.battery_status,)) +_D("Wireless Keyboard EX110", codename="EX110", protocol=1.0, wpid="0055", registers=(REG.battery_status,)) +_D("Wireless Keyboard S510", codename="S510", protocol=1.0, wpid="0056", registers=(REG.battery_status,)) +_D("Wireless Wave Keyboard K550", codename="K550", protocol=1.0, wpid="0060", registers=(REG.battery_status,)) +_D("Wireless Keyboard EX100", codename="EX100", protocol=1.0, wpid="0065", registers=(REG.battery_status,)) +_D("Wireless Keyboard MK300", codename="MK300", protocol=1.0, wpid="0068", registers=(REG.battery_status,)) +_D("Number Pad N545", codename="N545", protocol=1.0, wpid="2006", registers=(REG.battery_status,)) +_D("Wireless Compact Keyboard K340", codename="K340", protocol=1.0, wpid="2007", registers=(REG.battery_status,)) +_D("Wireless Keyboard MK700", codename="MK700", protocol=1.0, wpid="2008", registers=(REG.battery_status,)) +_D("Wireless Wave Keyboard K350", codename="K350", protocol=1.0, wpid="200A", registers=(REG.battery_status,)) +_D("Wireless Keyboard MK320", codename="MK320", protocol=1.0, wpid="200F", registers=(REG.battery_status,)) _D( "Wireless Illuminated Keyboard K800", codename="K800", protocol=1.0, wpid="2010", - registers=(_R.battery_status, _R.three_leds), + registers=(REG.battery_status, REG.three_leds), ) -_D("Wireless Keyboard K520", codename="K520", protocol=1.0, wpid="2011", registers=(_R.battery_status,)) +_D("Wireless Keyboard K520", codename="K520", protocol=1.0, wpid="2011", registers=(REG.battery_status,)) _D("Wireless Solar Keyboard K750", codename="K750", protocol=2.0, wpid="4002") _D("Wireless Keyboard K270 (unifying)", codename="K270", protocol=2.0, wpid="4003") _D("Wireless Keyboard K360", codename="K360", protocol=2.0, wpid="4004") @@ -234,51 +233,57 @@ def get_btid(btid): # Mice -_D("LX5 Cordless Mouse", codename="LX5", protocol=1.0, wpid="0036", registers=(_R.battery_status,)) -_D("LX7 Cordless Laser Mouse", codename="LX7", protocol=1.0, wpid="0039", registers=(_R.battery_status,)) -_D("Wireless Wave Mouse M550", codename="M550", protocol=1.0, wpid="003C", registers=(_R.battery_status,)) -_D("Wireless Mouse EX100", codename="EX100m", protocol=1.0, wpid="003F", registers=(_R.battery_status,)) -_D("Wireless Mouse M30", codename="M30", protocol=1.0, wpid="0085", registers=(_R.battery_status,)) -_D("MX610 Laser Cordless Mouse", codename="MX610", protocol=1.0, wpid="1001", registers=(_R.battery_status,)) -_D("G7 Cordless Laser Mouse", codename="G7", protocol=1.0, wpid="1002", registers=(_R.battery_status,)) -_D("V400 Laser Cordless Mouse", codename="V400", protocol=1.0, wpid="1003", registers=(_R.battery_status,)) -_D("MX610 Left-Handled Mouse", codename="MX610L", protocol=1.0, wpid="1004", registers=(_R.battery_status,)) -_D("V450 Laser Cordless Mouse", codename="V450", protocol=1.0, wpid="1005", registers=(_R.battery_status,)) +_D("LX5 Cordless Mouse", codename="LX5", protocol=1.0, wpid="0036", registers=(REG.battery_status,)) +_D("LX7 Cordless Laser Mouse", codename="LX7", protocol=1.0, wpid="0039", registers=(REG.battery_status,)) +_D("Wireless Wave Mouse M550", codename="M550", protocol=1.0, wpid="003C", registers=(REG.battery_status,)) +_D("Wireless Mouse EX100", codename="EX100m", protocol=1.0, wpid="003F", registers=(REG.battery_status,)) +_D("Wireless Mouse M30", codename="M30", protocol=1.0, wpid="0085", registers=(REG.battery_status,)) +_D("MX610 Laser Cordless Mouse", codename="MX610", protocol=1.0, wpid="1001", registers=(REG.battery_status,)) +_D("G7 Cordless Laser Mouse", codename="G7", protocol=1.0, wpid="1002", registers=(REG.battery_status,)) +_D("V400 Laser Cordless Mouse", codename="V400", protocol=1.0, wpid="1003", registers=(REG.battery_status,)) +_D("MX610 Left-Handled Mouse", codename="MX610L", protocol=1.0, wpid="1004", registers=(REG.battery_status,)) +_D("V450 Laser Cordless Mouse", codename="V450", protocol=1.0, wpid="1005", registers=(REG.battery_status,)) _D( "VX Revolution", codename="VX Revolution", - kind=_DK.mouse, + kind=DEVICE_KIND.mouse, protocol=1.0, wpid=("1006", "100D", "0612"), - registers=(_R.battery_charge,), + registers=(REG.battery_charge,), ) -_D("MX Air", codename="MX Air", protocol=1.0, kind=_DK.mouse, wpid=("1007", "100E"), registers=(_R.battery_charge,)) +_D("MX Air", codename="MX Air", protocol=1.0, kind=DEVICE_KIND.mouse, wpid=("1007", "100E"), registers=(REG.battery_charge,)) _D( "MX Revolution", codename="MX Revolution", protocol=1.0, - kind=_DK.mouse, + kind=DEVICE_KIND.mouse, wpid=("1008", "100C"), - registers=(_R.battery_charge,), + registers=(REG.battery_charge,), ) -_D("MX620 Laser Cordless Mouse", codename="MX620", protocol=1.0, wpid=("100A", "1016"), registers=(_R.battery_charge,)) -_D("VX Nano Cordless Laser Mouse", codename="VX Nano", protocol=1.0, wpid=("100B", "100F"), registers=(_R.battery_charge,)) -_D("V450 Nano Cordless Laser Mouse", codename="V450 Nano", protocol=1.0, wpid="1011", registers=(_R.battery_charge,)) -_D("V550 Nano Cordless Laser Mouse", codename="V550 Nano", protocol=1.0, wpid="1013", registers=(_R.battery_charge,)) +_D("MX620 Laser Cordless Mouse", codename="MX620", protocol=1.0, wpid=("100A", "1016"), registers=(REG.battery_charge,)) +_D("VX Nano Cordless Laser Mouse", codename="VX Nano", protocol=1.0, wpid=("100B", "100F"), registers=(REG.battery_charge,)) +_D("V450 Nano Cordless Laser Mouse", codename="V450 Nano", protocol=1.0, wpid="1011", registers=(REG.battery_charge,)) +_D("V550 Nano Cordless Laser Mouse", codename="V550 Nano", protocol=1.0, wpid="1013", registers=(REG.battery_charge,)) _D( "MX 1100 Cordless Laser Mouse", codename="MX 1100", protocol=1.0, - kind=_DK.mouse, + kind=DEVICE_KIND.mouse, wpid="1014", - registers=(_R.battery_charge,), + registers=(REG.battery_charge,), ) -_D("Anywhere Mouse MX", codename="Anywhere MX", protocol=1.0, wpid="1017", registers=(_R.battery_charge,)) -_D("Performance Mouse MX", codename="Performance MX", protocol=1.0, wpid="101A", registers=(_R.battery_status, _R.three_leds)) -_D("Marathon Mouse M705 (M-R0009)", codename="M705 (M-R0009)", protocol=1.0, wpid="101B", registers=(_R.battery_charge,)) -_D("Wireless Mouse M350", codename="M350", protocol=1.0, wpid="101C", registers=(_R.battery_charge,)) -_D("Wireless Mouse M505", codename="M505/B605", protocol=1.0, wpid="101D", registers=(_R.battery_charge,)) -_D("Wireless Mouse M305", codename="M305", protocol=1.0, wpid="101F", registers=(_R.battery_status,)) +_D("Anywhere Mouse MX", codename="Anywhere MX", protocol=1.0, wpid="1017", registers=(REG.battery_charge,)) +_D( + "Performance Mouse MX", + codename="Performance MX", + protocol=1.0, + wpid="101A", + registers=(REG.battery_status, REG.three_leds), +) +_D("Marathon Mouse M705 (M-R0009)", codename="M705 (M-R0009)", protocol=1.0, wpid="101B", registers=(REG.battery_charge,)) +_D("Wireless Mouse M350", codename="M350", protocol=1.0, wpid="101C", registers=(REG.battery_charge,)) +_D("Wireless Mouse M505", codename="M505/B605", protocol=1.0, wpid="101D", registers=(REG.battery_charge,)) +_D("Wireless Mouse M305", codename="M305", protocol=1.0, wpid="101F", registers=(REG.battery_status,)) _D("Wireless Mouse M215", codename="M215", protocol=1.0, wpid="1020") _D( "G700 Gaming Mouse", @@ -288,12 +293,12 @@ def get_btid(btid): usbid=0xC06B, interface=1, registers=( - _R.battery_status, - _R.three_leds, + REG.battery_status, + REG.three_leds, ), ) -_D("Wireless Mouse M310", codename="M310", protocol=1.0, wpid="1024", registers=(_R.battery_status,)) -_D("Wireless Mouse M510", codename="M510", protocol=1.0, wpid="1025", registers=(_R.battery_status,)) +_D("Wireless Mouse M310", codename="M310", protocol=1.0, wpid="1024", registers=(REG.battery_status,)) +_D("Wireless Mouse M510", codename="M510", protocol=1.0, wpid="1025", registers=(REG.battery_status,)) _D("Fujitsu Sonic Mouse", codename="Sonic", protocol=1.0, wpid="1029") _D( "G700s Gaming Mouse", @@ -303,8 +308,8 @@ def get_btid(btid): usbid=0xC07C, interface=1, registers=( - _R.battery_status, - _R.three_leds, + REG.battery_status, + REG.three_leds, ), ) _D("Couch Mouse M515", codename="M515", protocol=2.0, wpid="4007") @@ -348,7 +353,7 @@ def get_btid(btid): _D("MX518 Gaming Mouse", codename="MX518", usbid=0xC08E, interface=1) _D("G703 Hero Gaming Mouse", codename="G703 Hero", usbid=0xC090) _D("G903 Hero Gaming Mouse", codename="G903 Hero", usbid=0xC091) -_D(None, kind=_DK.mouse, usbid=0xC092, interface=1) # two mice share this ID +_D(None, kind=DEVICE_KIND.mouse, usbid=0xC092, interface=1) # two mice share this ID _D("M500S Mouse", codename="M500S", usbid=0xC093, interface=1) # _D('G600 Gaming Mouse', codename='G600 Gaming', usbid=0xc24a, interface=1) # not an HID++ device _D("G500s Gaming Mouse", codename="G500s Gaming", usbid=0xC24E, interface=1, protocol=1.0) @@ -365,13 +370,15 @@ def get_btid(btid): _D("Wireless Touchpad", codename="Wireless Touch", protocol=2.0, wpid="4011") _D("Wireless Rechargeable Touchpad T650", codename="T650", protocol=2.0, wpid="4101") -_D("G Powerplay", codename="Powerplay", protocol=2.0, kind=_DK.touchpad, wpid="405F") # To override self-identification +_D( + "G Powerplay", codename="Powerplay", protocol=2.0, kind=DEVICE_KIND.touchpad, wpid="405F" +) # To override self-identification # Headset -_D("G533 Gaming Headset", codename="G533 Headset", protocol=2.0, interface=3, kind=_DK.headset, usbid=0x0A66) -_D("G535 Gaming Headset", codename="G535 Headset", protocol=2.0, interface=3, kind=_DK.headset, usbid=0x0AC4) -_D("G935 Gaming Headset", codename="G935 Headset", protocol=2.0, interface=3, kind=_DK.headset, usbid=0x0A87) -_D("G733 Gaming Headset", codename="G733 Headset", protocol=2.0, interface=3, kind=_DK.headset, usbid=0x0AB5) -_D("G733 Gaming Headset", codename="G733 Headset New", protocol=2.0, interface=3, kind=_DK.headset, usbid=0x0AFE) -_D("PRO X Wireless Gaming Headset", codename="PRO Headset", protocol=2.0, interface=3, kind=_DK.headset, usbid=0x0ABA) +_D("G533 Gaming Headset", codename="G533 Headset", protocol=2.0, interface=3, kind=DEVICE_KIND.headset, usbid=0x0A66) +_D("G535 Gaming Headset", codename="G535 Headset", protocol=2.0, interface=3, kind=DEVICE_KIND.headset, usbid=0x0AC4) +_D("G935 Gaming Headset", codename="G935 Headset", protocol=2.0, interface=3, kind=DEVICE_KIND.headset, usbid=0x0A87) +_D("G733 Gaming Headset", codename="G733 Headset", protocol=2.0, interface=3, kind=DEVICE_KIND.headset, usbid=0x0AB5) +_D("G733 Gaming Headset", codename="G733 Headset New", protocol=2.0, interface=3, kind=DEVICE_KIND.headset, usbid=0x0AFE) +_D("PRO X Wireless Gaming Headset", codename="PRO Headset", protocol=2.0, interface=3, kind=DEVICE_KIND.headset, usbid=0x0ABA) diff --git a/lib/logitech_receiver/device.py b/lib/logitech_receiver/device.py index fb261e9ad4..8624aca1dd 100644 --- a/lib/logitech_receiver/device.py +++ b/lib/logitech_receiver/device.py @@ -14,17 +14,17 @@ ## You should have received a copy of the GNU General Public License along ## with this program; if not, write to the Free Software Foundation, Inc., ## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -import errno as _errno +import errno import logging -import threading as _threading +import threading import time from typing import Callable from typing import Optional -import hidapi as _hid -import solaar.configuration as _configuration +import hidapi + +from solaar import configuration from . import base from . import descriptors @@ -34,9 +34,9 @@ from . import hidpp20 from . import hidpp20_constants from . import settings +from . import settings_templates from .common import Alert from .common import Battery -from .settings_templates import check_feature_settings as _check_feature_settings logger = logging.getLogger(__name__) @@ -59,7 +59,7 @@ def create_device(device_info, setting_callback=None): return Device(None, None, None, handle=handle, device_info=device_info, setting_callback=setting_callback) except OSError as e: logger.exception("open %s", device_info) - if e.errno == _errno.EACCES: + if e.errno == errno.EACCES: raise except Exception: logger.exception("open %s", device_info) @@ -70,7 +70,16 @@ class Device: read_register: Callable = hidpp10.read_register write_register: Callable = hidpp10.write_register - def __init__(self, receiver, number, online, pairing_info=None, handle=None, device_info=None, setting_callback=None): + def __init__( + self, + receiver, + number, + online, + pairing_info=None, + handle=None, + device_info=None, + setting_callback=None, + ): assert receiver or device_info if receiver: assert 0 < number <= 15 # some receivers have devices past their max # of devices @@ -110,14 +119,14 @@ def __init__(self, receiver, number, online, pairing_info=None, handle=None, dev self._active = None # lags self.online - is used to help determine when to setup devices self._feature_settings_checked = False - self._gestures_lock = _threading.Lock() - self._settings_lock = _threading.Lock() - self._persister_lock = _threading.Lock() + self._gestures_lock = threading.Lock() + self._settings_lock = threading.Lock() + self._persister_lock = threading.Lock() self._notification_handlers = {} # See `add_notification_handler` self.cleanups = [] # functions to run on the device when it is closed if not self.path: - self.path = _hid.find_paired_node(receiver.path, number, 1) if receiver else None + self.path = hidapi.find_paired_node(receiver.path, number, 1) if receiver else None if not self.handle: try: self.handle = base.open_path(self.path) if self.path else None @@ -302,9 +311,9 @@ def profiles(self): self._profiles = _hidpp20.get_profiles(self) return self._profiles - def set_configuration(self, configuration, no_reply=False): + def set_configuration(self, configuration_, no_reply=False): if self.online and self.protocol >= 2.0: - _hidpp20.config_change(self, configuration, no_reply=no_reply) + _hidpp20.config_change(self, configuration_, no_reply=no_reply) def reset(self, no_reply=False): self.set_configuration(0, no_reply) @@ -314,7 +323,7 @@ def persister(self): if not self._persister: with self._persister_lock: if not self._persister: - self._persister = _configuration.persister(self) + self._persister = configuration.persister(self) return self._persister @property @@ -337,7 +346,7 @@ def settings(self): if not self._feature_settings_checked: with self._settings_lock: if not self._feature_settings_checked: - self._feature_settings_checked = _check_feature_settings(self, self._settings) + self._feature_settings_checked = settings_templates.check_feature_settings(self, self._settings) return self._settings def battery(self): # None or level, next, status, voltage diff --git a/lib/logitech_receiver/diversion.py b/lib/logitech_receiver/diversion.py index 912e22da38..f504950ab5 100644 --- a/lib/logitech_receiver/diversion.py +++ b/lib/logitech_receiver/diversion.py @@ -14,43 +14,37 @@ ## with this program; if not, write to the Free Software Foundation, Inc., ## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -import ctypes as _ctypes +import ctypes import logging import math import numbers -import os as _os -import os.path as _path -import platform as _platform +import os +import platform import socket +import struct import subprocess -import sys as _sys -import time as _time +import sys +import time from typing import Dict from typing import Tuple import gi import psutil +import yaml from keysyms import keysymdef # There is no evdev on macOS or Windows. Diversion will not work without # it but other Solaar functionality is available. -if _platform.system() in ("Darwin", "Windows"): +if platform.system() in ("Darwin", "Windows"): evdev = None else: import evdev -from math import sqrt as _sqrt -from struct import unpack as _unpack - -from yaml import add_representer as _yaml_add_representer -from yaml import dump_all as _yaml_dump_all -from yaml import safe_load_all as _yaml_safe_load_all - from .common import NamedInt -from .hidpp20 import FEATURE as _F -from .special_keys import CONTROL as _CONTROL +from .hidpp20 import FEATURE +from .special_keys import CONTROL gi.require_version("Gdk", "3.0") # isort:skip from gi.repository import Gdk, GLib # NOQA: E402 # isort:skip @@ -102,7 +96,7 @@ if logger.isEnabledFor(logging.INFO): logger.info("GDK Keymap %sset up", "" if gkeymap else "not ") -wayland = _os.getenv("WAYLAND_DISPLAY") # is this Wayland? +wayland = os.getenv("WAYLAND_DISPLAY") # is this Wayland? if wayland: logger.warning( "rules cannot access modifier keys in Wayland, " @@ -137,26 +131,26 @@ _dbus_interface = None -class XkbDisplay(_ctypes.Structure): +class XkbDisplay(ctypes.Structure): """opaque struct""" -class XkbStateRec(_ctypes.Structure): +class XkbStateRec(ctypes.Structure): _fields_ = [ - ("group", _ctypes.c_ubyte), - ("locked_group", _ctypes.c_ubyte), - ("base_group", _ctypes.c_ushort), - ("latched_group", _ctypes.c_ushort), - ("mods", _ctypes.c_ubyte), - ("base_mods", _ctypes.c_ubyte), - ("latched_mods", _ctypes.c_ubyte), - ("locked_mods", _ctypes.c_ubyte), - ("compat_state", _ctypes.c_ubyte), - ("grab_mods", _ctypes.c_ubyte), - ("compat_grab_mods", _ctypes.c_ubyte), - ("lookup_mods", _ctypes.c_ubyte), - ("compat_lookup_mods", _ctypes.c_ubyte), - ("ptr_buttons", _ctypes.c_ushort), + ("group", ctypes.c_ubyte), + ("locked_group", ctypes.c_ubyte), + ("base_group", ctypes.c_ushort), + ("latched_group", ctypes.c_ushort), + ("mods", ctypes.c_ubyte), + ("base_mods", ctypes.c_ubyte), + ("latched_mods", ctypes.c_ubyte), + ("locked_mods", ctypes.c_ubyte), + ("compat_state", ctypes.c_ubyte), + ("grab_mods", ctypes.c_ubyte), + ("compat_grab_mods", ctypes.c_ubyte), + ("lookup_mods", ctypes.c_ubyte), + ("compat_lookup_mods", ctypes.c_ubyte), + ("ptr_buttons", ctypes.c_ushort), ] # something strange is happening here but it is not being used @@ -176,7 +170,7 @@ def x11_setup(): if logger.isEnabledFor(logging.INFO): logger.info("X11 library loaded and display set up") except Exception: - logger.warning("X11 not available - some rule capabilities inoperable", exc_info=_sys.exc_info()) + logger.warning("X11 not available - some rule capabilities inoperable", exc_info=sys.exc_info()) _x11 = False xtest_available = False return _x11 @@ -193,7 +187,7 @@ def gnome_dbus_interface_setup(): remote_object = bus.get_object("org.gnome.Shell", "/io/github/pwr_solaar/solaar") _dbus_interface = dbus.Interface(remote_object, "io.github.pwr_solaar.solaar") except dbus.exceptions.DBusException: - logger.warning("Solaar Gnome extension not installed - some rule capabilities inoperable", exc_info=_sys.exc_info()) + logger.warning("Solaar Gnome extension not installed - some rule capabilities inoperable", exc_info=sys.exc_info()) _dbus_interface = False return _dbus_interface @@ -203,14 +197,14 @@ def xkb_setup(): if Xkbdisplay is not None: return Xkbdisplay try: # set up to get keyboard state using ctypes interface to libx11 - X11Lib = _ctypes.cdll.LoadLibrary("libX11.so") - X11Lib.XOpenDisplay.restype = _ctypes.POINTER(XkbDisplay) - X11Lib.XkbGetState.argtypes = [_ctypes.POINTER(XkbDisplay), _ctypes.c_uint, _ctypes.POINTER(XkbStateRec)] + X11Lib = ctypes.cdll.LoadLibrary("libX11.so") + X11Lib.XOpenDisplay.restype = ctypes.POINTER(XkbDisplay) + X11Lib.XkbGetState.argtypes = [ctypes.POINTER(XkbDisplay), ctypes.c_uint, ctypes.POINTER(XkbStateRec)] Xkbdisplay = X11Lib.XOpenDisplay(None) if logger.isEnabledFor(logging.INFO): logger.info("XKB display set up") except Exception: - logger.warning("XKB display not available - rules cannot access keyboard group", exc_info=_sys.exc_info()) + logger.warning("XKB display not available - rules cannot access keyboard group", exc_info=sys.exc_info()) Xkbdisplay = False return Xkbdisplay @@ -262,7 +256,7 @@ def setup_uinput(): def kbdgroup(): if xkb_setup(): state = XkbStateRec() - X11Lib.XkbGetState(Xkbdisplay, XkbUseCoreKbd, _ctypes.pointer(state)) + X11Lib.XkbGetState(Xkbdisplay, XkbUseCoreKbd, ctypes.pointer(state)) return state.group else: return None @@ -282,7 +276,7 @@ def signed(bytes_: bytes) -> int: def xy_direction(_x, _y): # normalize x and y - m = _sqrt((_x * _x) + (_y * _y)) + m = math.sqrt((_x * _x) + (_y * _y)) if m == 0: return "noop" x = round(_x / m) @@ -419,7 +413,7 @@ def simulate_scroll(dx, dy): def thumb_wheel_up(f, r, d, a): global thumb_wheel_displacement - if f != _F.THUMB_WHEEL or r != 0: + if f != FEATURE.THUMB_WHEEL or r != 0: return False if a is None: return signed(d[0:2]) < 0 and signed(d[0:2]) @@ -432,7 +426,7 @@ def thumb_wheel_up(f, r, d, a): def thumb_wheel_down(f, r, d, a): global thumb_wheel_displacement - if f != _F.THUMB_WHEEL or r != 0: + if f != FEATURE.THUMB_WHEEL or r != 0: return False if a is None: return signed(d[0:2]) > 0 and signed(d[0:2]) @@ -445,9 +439,9 @@ def thumb_wheel_down(f, r, d, a): def charging(f, r, d, _a): if ( - (f == _F.BATTERY_STATUS and r == 0 and 1 <= d[2] <= 4) - or (f == _F.BATTERY_VOLTAGE and r == 0 and d[2] & (1 << 7)) - or (f == _F.UNIFIED_BATTERY and r == 0 and 1 <= d[2] <= 3) + (f == FEATURE.BATTERY_STATUS and r == 0 and 1 <= d[2] <= 4) + or (f == FEATURE.BATTERY_VOLTAGE and r == 0 and d[2] & (1 << 7)) + or (f == FEATURE.UNIFIED_BATTERY and r == 0 and 1 <= d[2] <= 3) ): return 1 else: @@ -455,20 +449,32 @@ def charging(f, r, d, _a): TESTS = { - "crown_right": [lambda f, r, d, a: f == _F.CROWN and r == 0 and d[1] < 128 and d[1], False], - "crown_left": [lambda f, r, d, a: f == _F.CROWN and r == 0 and d[1] >= 128 and 256 - d[1], False], - "crown_right_ratchet": [lambda f, r, d, a: f == _F.CROWN and r == 0 and d[2] < 128 and d[2], False], - "crown_left_ratchet": [lambda f, r, d, a: f == _F.CROWN and r == 0 and d[2] >= 128 and 256 - d[2], False], - "crown_tap": [lambda f, r, d, a: f == _F.CROWN and r == 0 and d[5] == 0x01 and d[5], False], - "crown_start_press": [lambda f, r, d, a: f == _F.CROWN and r == 0 and d[6] == 0x01 and d[6], False], - "crown_end_press": [lambda f, r, d, a: f == _F.CROWN and r == 0 and d[6] == 0x05 and d[6], False], - "crown_pressed": [lambda f, r, d, a: f == _F.CROWN and r == 0 and d[6] >= 0x01 and d[6] <= 0x04 and d[6], False], + "crown_right": [lambda f, r, d, a: f == FEATURE.CROWN and r == 0 and d[1] < 128 and d[1], False], + "crown_left": [lambda f, r, d, a: f == FEATURE.CROWN and r == 0 and d[1] >= 128 and 256 - d[1], False], + "crown_right_ratchet": [lambda f, r, d, a: f == FEATURE.CROWN and r == 0 and d[2] < 128 and d[2], False], + "crown_left_ratchet": [lambda f, r, d, a: f == FEATURE.CROWN and r == 0 and d[2] >= 128 and 256 - d[2], False], + "crown_tap": [lambda f, r, d, a: f == FEATURE.CROWN and r == 0 and d[5] == 0x01 and d[5], False], + "crown_start_press": [lambda f, r, d, a: f == FEATURE.CROWN and r == 0 and d[6] == 0x01 and d[6], False], + "crown_end_press": [lambda f, r, d, a: f == FEATURE.CROWN and r == 0 and d[6] == 0x05 and d[6], False], + "crown_pressed": [lambda f, r, d, a: f == FEATURE.CROWN and r == 0 and d[6] >= 0x01 and d[6] <= 0x04 and d[6], False], "thumb_wheel_up": [thumb_wheel_up, True], "thumb_wheel_down": [thumb_wheel_down, True], - "lowres_wheel_up": [lambda f, r, d, a: f == _F.LOWRES_WHEEL and r == 0 and signed(d[0:1]) > 0 and signed(d[0:1]), False], - "lowres_wheel_down": [lambda f, r, d, a: f == _F.LOWRES_WHEEL and r == 0 and signed(d[0:1]) < 0 and signed(d[0:1]), False], - "hires_wheel_up": [lambda f, r, d, a: f == _F.HIRES_WHEEL and r == 0 and signed(d[1:3]) > 0 and signed(d[1:3]), False], - "hires_wheel_down": [lambda f, r, d, a: f == _F.HIRES_WHEEL and r == 0 and signed(d[1:3]) < 0 and signed(d[1:3]), False], + "lowres_wheel_up": [ + lambda f, r, d, a: f == FEATURE.LOWRES_WHEEL and r == 0 and signed(d[0:1]) > 0 and signed(d[0:1]), + False, + ], + "lowres_wheel_down": [ + lambda f, r, d, a: f == FEATURE.LOWRES_WHEEL and r == 0 and signed(d[0:1]) < 0 and signed(d[0:1]), + False, + ], + "hires_wheel_up": [ + lambda f, r, d, a: f == FEATURE.HIRES_WHEEL and r == 0 and signed(d[1:3]) > 0 and signed(d[1:3]), + False, + ], + "hires_wheel_down": [ + lambda f, r, d, a: f == FEATURE.HIRES_WHEEL and r == 0 and signed(d[1:3]) < 0 and signed(d[1:3]), + False, + ], "charging": [charging, False], "False": [lambda f, r, d, a: False, False], "True": [lambda f, r, d, a: True, False], @@ -714,11 +720,11 @@ def data(self): class Feature(Condition): def __init__(self, feature, warn=True): - if not (isinstance(feature, str) and feature in _F): + if not (isinstance(feature, str) and feature in FEATURE): if warn: logger.warning("rule Feature argument not name of a feature: %s", feature) self.feature = None - self.feature = _F[feature] + self.feature = FEATURE[feature] def __str__(self): return "Feature: " + str(self.feature) @@ -857,8 +863,8 @@ def __init__(self, args, warn=True): elif len(args) >= 2: key, action = args[:2] - if isinstance(key, str) and key in _CONTROL: - self.key = _CONTROL[key] + if isinstance(key, str) and key in CONTROL: + self.key = CONTROL[key] else: if warn: logger.warning(f"rule Key key name not name of a Logitech key: {key}") @@ -896,8 +902,8 @@ def __init__(self, args, warn=True): elif isinstance(args, str): key = args - if isinstance(key, str) and key in _CONTROL: - self.key = _CONTROL[key] + if isinstance(key, str) and key in CONTROL: + self.key = CONTROL[key] else: if warn: logger.warning(f"rule Key key name not name of a Logitech key: {key}") @@ -1013,7 +1019,7 @@ def __init__(self, movements, warn=True): if isinstance(movements, str): movements = [movements] for x in movements: - if x not in self.MOVEMENTS and x not in _CONTROL: + if x not in self.MOVEMENTS and x not in CONTROL: if warn: logger.warning("rule Mouse Gesture argument not direction or name of a Logitech key: %s", x) self.movements = movements @@ -1024,14 +1030,14 @@ def __str__(self): def evaluate(self, feature, notification, device, last_result): if logger.isEnabledFor(logging.DEBUG): logger.debug("evaluate condition: %s", self) - if feature == _F.MOUSE_GESTURE: + if feature == FEATURE.MOUSE_GESTURE: d = notification.data - data = _unpack("!" + (int(len(d) / 2) * "h"), d) + data = struct.unpack("!" + (int(len(d) / 2) * "h"), d) data_offset = 1 movement_offset = 0 if self.movements and self.movements[0] not in self.MOVEMENTS: # matching against initiating key movement_offset = 1 - if self.movements[0] != str(_CONTROL[data[0]]): + if self.movements[0] != str(CONTROL[data[0]]): return False for m in self.movements[movement_offset:]: if data_offset >= len(data): @@ -1042,7 +1048,7 @@ def evaluate(self, feature, notification, device, last_result): return False data_offset += 3 elif data[data_offset] == 1: - if m != str(_CONTROL[data[data_offset + 1]]): + if m != str(CONTROL[data[data_offset + 1]]): return False data_offset += 2 return data_offset == len(data) @@ -1214,7 +1220,7 @@ def evaluate(self, feature, notification, device, last_result): self.keyDown(self.key_symbols, current) if self.action != DEPRESS: self.keyUp(reversed(self.key_symbols), current) - _time.sleep(0.01) + time.sleep(0.01) else: logger.warning("no keymap so cannot determine which keycode to send") return None @@ -1253,7 +1259,7 @@ def evaluate(self, feature, notification, device, last_result): logger.info("MouseScroll action: %s %s %s", self.amounts, last_result, amounts) dx, dy = amounts simulate_scroll(dx, dy) - _time.sleep(0.01) + time.sleep(0.01) return None def data(self): @@ -1289,7 +1295,7 @@ def evaluate(self, feature, notification, device, last_result): logger.info(f"MouseClick action: {int(self.count)} {self.button}") if self.button and self.count: click(buttons[self.button], self.count) - _time.sleep(0.01) + time.sleep(0.01) return None def data(self): @@ -1438,12 +1444,12 @@ def data(self): def key_is_down(key): - if key == _CONTROL.MR: + if key == CONTROL.MR: return mr_key_down - elif _CONTROL.M1 <= key <= _CONTROL.M8: - return bool(m_keys_down & (0x01 << (key - _CONTROL.M1))) - elif _CONTROL.G1 <= key <= _CONTROL.G32: - return bool(g_keys_down & (0x01 << (key - _CONTROL.G1))) + elif CONTROL.M1 <= key <= CONTROL.M8: + return bool(m_keys_down & (0x01 << (key - CONTROL.M1))) + elif CONTROL.G1 <= key <= CONTROL.G32: + return bool(g_keys_down & (0x01 << (key - CONTROL.G1))) else: return key in keys_down @@ -1459,8 +1465,8 @@ def process_notification(device, notification, feature): global keys_down, g_keys_down, m_keys_down, mr_key_down, key_down, key_up, thumb_wheel_displacement key_down, key_up = None, None # need to keep track of keys that are down to find a new key down - if feature == _F.REPROG_CONTROLS_V4 and notification.address == 0x00: - new_keys_down = _unpack("!4H", notification.data[:8]) + if feature == FEATURE.REPROG_CONTROLS_V4 and notification.address == 0x00: + new_keys_down = struct.unpack("!4H", notification.data[:8]) for key in new_keys_down: if key and key not in keys_down: key_down = key @@ -1469,33 +1475,33 @@ def process_notification(device, notification, feature): key_up = key keys_down = new_keys_down # and also G keys down - elif feature == _F.GKEY and notification.address == 0x00: - new_g_keys_down = _unpack(" Rule: try: with open(file_path) as config_file: loaded_rules = [] - for loaded_rule in _yaml_safe_load_all(config_file): + for loaded_rule in yaml.safe_load_all(config_file): rule = Rule(loaded_rule, source=file_path) if logger.isEnabledFor(logging.DEBUG): logger.debug("load rule: %s", rule) diff --git a/lib/logitech_receiver/exceptions.py b/lib/logitech_receiver/exceptions.py index 27989aa1d0..0f96276e51 100644 --- a/lib/logitech_receiver/exceptions.py +++ b/lib/logitech_receiver/exceptions.py @@ -15,14 +15,14 @@ ## with this program; if not, write to the Free Software Foundation, Inc., ## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -from .common import KwException as _KwException +from .common import KwException # # Exceptions that may be raised by this API. # -class NoReceiver(_KwException): +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 @@ -31,25 +31,25 @@ class NoReceiver(_KwException): pass -class NoSuchDevice(_KwException): +class NoSuchDevice(KwException): """Raised when trying to reach a device number not paired to the receiver.""" pass -class DeviceUnreachable(_KwException): +class DeviceUnreachable(KwException): """Raised when a request is made to an unreachable (turned off) device.""" pass -class FeatureNotSupported(_KwException): +class FeatureNotSupported(KwException): """Raised when trying to request a feature not supported by the device.""" pass -class FeatureCallError(_KwException): +class FeatureCallError(KwException): """Raised if the device replied to a feature call with an error.""" pass diff --git a/lib/logitech_receiver/hidpp10.py b/lib/logitech_receiver/hidpp10.py index 461dcd6162..7529a65ab9 100644 --- a/lib/logitech_receiver/hidpp10.py +++ b/lib/logitech_receiver/hidpp10.py @@ -21,13 +21,10 @@ from typing_extensions import Protocol +from . import common from .common import Battery from .common import BatteryLevelApproximation from .common import BatteryStatus -from .common import FirmwareInfo as _FirmwareInfo -from .common import bytes2int as _bytes2int -from .common import int2bytes as _int2bytes -from .common import strhex as _strhex from .hidpp10_constants import REGISTERS from .hidpp20_constants import FIRMWARE_KIND @@ -35,24 +32,19 @@ class Device(Protocol): - def request(self, request_id, *params): - ... + def request(self, request_id, *params): ... @property - def kind(self) -> Any: - ... + def kind(self) -> Any: ... @property - def online(self) -> bool: - ... + def online(self) -> bool: ... @property - def protocol(self) -> Any: - ... + def protocol(self) -> Any: ... @property - def registers(self) -> list: - ... + def registers(self) -> list: ... def read_register(device: Device, register_number, *params): @@ -123,26 +115,26 @@ def get_firmware(self, device: Device): # won't be able to read any of it now... return - fw_version = _strhex(reply[1:3]) + fw_version = common.strhex(reply[1:3]) fw_version = f"{fw_version[0:2]}.{fw_version[2:4]}" reply = read_register(device, REGISTERS.firmware, 0x02) if reply: - fw_version += ".B" + _strhex(reply[1:3]) - fw = _FirmwareInfo(FIRMWARE_KIND.Firmware, "", fw_version, None) + fw_version += ".B" + common.strhex(reply[1:3]) + fw = common.FirmwareInfo(FIRMWARE_KIND.Firmware, "", fw_version, None) firmware[0] = fw reply = read_register(device, REGISTERS.firmware, 0x04) if reply: - bl_version = _strhex(reply[1:3]) + bl_version = common.strhex(reply[1:3]) bl_version = f"{bl_version[0:2]}.{bl_version[2:4]}" - bl = _FirmwareInfo(FIRMWARE_KIND.Bootloader, "", bl_version, None) + bl = common.FirmwareInfo(FIRMWARE_KIND.Bootloader, "", bl_version, None) firmware[1] = bl reply = read_register(device, REGISTERS.firmware, 0x03) if reply: - o_version = _strhex(reply[1:3]) + o_version = common.strhex(reply[1:3]) o_version = f"{o_version[0:2]}.{o_version[2:4]}" - o = _FirmwareInfo(FIRMWARE_KIND.Other, "", o_version, None) + o = common.FirmwareInfo(FIRMWARE_KIND.Other, "", o_version, None) firmware[2] = o if any(firmware): @@ -205,7 +197,7 @@ def set_notification_flags(self, device: Device, *flag_bits): flag_bits = sum(int(b) for b in flag_bits) assert flag_bits & 0x00FFFFFF == flag_bits - result = write_register(device, REGISTERS.notifications, _int2bytes(flag_bits, 3)) + result = write_register(device, REGISTERS.notifications, common.int2bytes(flag_bits, 3)) return result is not None def get_device_features(self, device: Device): @@ -224,7 +216,7 @@ def _get_register(self, device: Device, register): flags = read_register(device, register) if flags is not None: assert len(flags) == 3 - return _bytes2int(flags) + return common.bytes2int(flags) def parse_battery_status(register, reply) -> Battery | None: diff --git a/lib/logitech_receiver/hidpp20.py b/lib/logitech_receiver/hidpp20.py index 1efa6e6dbc..2e3e8faf8c 100644 --- a/lib/logitech_receiver/hidpp20.py +++ b/lib/logitech_receiver/hidpp20.py @@ -17,33 +17,27 @@ import logging import socket -import threading as _threading +import struct +import threading -from struct import pack as _pack -from struct import unpack as _unpack from typing import Any from typing import List from typing import Optional from typing import Tuple -import yaml as _yaml +import yaml from solaar.i18n import _ from typing_extensions import Protocol +from . import common from . import exceptions -from . import hidpp10_constants as _hidpp10_constants +from . import hidpp10_constants from . import special_keys from .common import Battery from .common import BatteryLevelApproximation from .common import BatteryStatus -from .common import FirmwareInfo as _FirmwareInfo -from .common import NamedInt as _NamedInt -from .common import NamedInts as _NamedInts -from .common import UnsortedNamedInts as _UnsortedNamedInts -from .common import bytes2int as _bytes2int -from .common import crc16 as _crc16 -from .common import int2bytes as _int2bytes +from .common import NamedInt from .hidpp20_constants import CHARGE_LEVEL from .hidpp20_constants import CHARGE_STATUS from .hidpp20_constants import CHARGE_TYPE @@ -57,31 +51,25 @@ FixedBytes5 = bytes -KIND_MAP = {kind: _hidpp10_constants.DEVICE_KIND[str(kind)] for kind in DEVICE_KIND} +KIND_MAP = {kind: hidpp10_constants.DEVICE_KIND[str(kind)] for kind in DEVICE_KIND} class Device(Protocol): - def feature_request(self, feature: FEATURE) -> Any: - ... + def feature_request(self, feature: FEATURE) -> Any: ... - def request(self) -> Any: - ... + def request(self) -> Any: ... @property - def features(self) -> Any: - ... + def features(self) -> Any: ... @property - def _gestures(self) -> Any: - ... + def _gestures(self) -> Any: ... @property - def _backlight(self) -> Any: - ... + def _backlight(self) -> Any: ... @property - def _profiles(self) -> Any: - ... + def _profiles(self) -> Any: ... class FeaturesArray(dict): @@ -103,7 +91,7 @@ def _check(self) -> bool: return False if self.count > 0: return True - reply = self.device.request(0x0000, _pack("!H", FEATURE.FEATURE_SET)) + reply = self.device.request(0x0000, struct.pack("!H", FEATURE.FEATURE_SET)) if reply is not None: fs_index = reply[0] if fs_index: @@ -120,7 +108,7 @@ def _check(self) -> bool: self.supported = False return False - def get_feature(self, index: int) -> Optional[_NamedInt]: + def get_feature(self, index: int) -> Optional[NamedInt]: feature = self.inverse.get(index) if feature is not None: return feature @@ -130,7 +118,7 @@ def get_feature(self, index: int) -> Optional[_NamedInt]: return feature response = self.device.feature_request(FEATURE.FEATURE_SET, 0x10, index) if response: - feature = FEATURE[_unpack("!H", response[:2])[0]] + feature = FEATURE[struct.unpack("!H", response[:2])[0]] self[feature] = index self.version[feature] = response[3] return feature @@ -141,15 +129,15 @@ def enumerate(self): # return all features and their index, ordered by index feature = self.get_feature(index) yield feature, index - def get_feature_version(self, feature: _NamedInt) -> Optional[int]: + def get_feature_version(self, feature: NamedInt) -> Optional[int]: if self[feature]: return self.version.get(feature, 0) - def __contains__(self, feature: _NamedInt) -> bool: + def __contains__(self, feature: NamedInt) -> bool: index = self.__getitem__(feature) return index is not None and index is not False - def __getitem__(self, feature: _NamedInt) -> Optional[int]: + def __getitem__(self, feature: NamedInt) -> Optional[int]: index = super().get(feature) if index is not None: return index @@ -157,7 +145,7 @@ def __getitem__(self, feature: _NamedInt) -> Optional[int]: index = super().get(feature) if index is not None: return index - response = self.device.request(0x0000, _pack("!H", feature)) + response = self.device.request(0x0000, struct.pack("!H", feature)) if response: index = response[0] self[feature] = index if index else False @@ -185,8 +173,8 @@ class ReprogrammableKey: Ref: https://drive.google.com/file/d/0BxbRzx7vEV7eU3VfMnRuRXktZ3M/view Read-only properties: - index {int} -- index in the control ID table - - key {_NamedInt} -- the name of this control - - default_task {_NamedInt} -- the native function of this control + - key {NamedInt} -- the name of this control + - default_task {NamedInt} -- the native function of this control - flags {List[str]} -- capabilities and desired software handling of the control """ @@ -198,17 +186,17 @@ def __init__(self, device: Device, index, cid, tid, flags): self._flags = flags @property - def key(self) -> _NamedInt: + def key(self) -> NamedInt: return special_keys.CONTROL[self._cid] @property - def default_task(self) -> _NamedInt: + def default_task(self) -> NamedInt: """NOTE: This NamedInt is a bit mixed up, because its value is the Control ID while the name is the Control ID's native task. But this makes more sense than presenting details of controls vs tasks in the interface. The same convention applies to `mapped_to`, `remappable_to`, `remap` in `ReprogrammableKeyV4`.""" task = str(special_keys.TASK[self._tid]) - return _NamedInt(self._cid, task) + return NamedInt(self._cid, task) @property def flags(self) -> List[str]: @@ -227,8 +215,8 @@ class ReprogrammableKeyV4(ReprogrammableKey): - group {int} -- the group this control belongs to; other controls with this group in their `group_mask` can be remapped to this control - group_mask {List[str]} -- this control can be remapped to any control ID in these groups - - mapped_to {_NamedInt} -- which action this control is mapped to; usually itself - - remappable_to {List[_NamedInt]} -- list of actions which this control can be remapped to + - mapped_to {NamedInt} -- which action this control is mapped to; usually itself + - remappable_to {List[NamedInt]} -- list of actions which this control can be remapped to - mapping_flags {List[str]} -- mapping flags set on the control """ @@ -245,24 +233,24 @@ def group_mask(self): return special_keys.CID_GROUP_BIT.flag_names(self._gmask) @property - def mapped_to(self) -> _NamedInt: + def mapped_to(self) -> NamedInt: if self._mapped_to is None: self._getCidReporting() self._device.keys._ensure_all_keys_queried() task = str(special_keys.TASK[self._device.keys.cid_to_tid[self._mapped_to]]) - return _NamedInt(self._mapped_to, task) + return NamedInt(self._mapped_to, task) @property - def remappable_to(self) -> _NamedInts: + def remappable_to(self) -> common.NamedInts: self._device.keys._ensure_all_keys_queried() - ret = _UnsortedNamedInts() + ret = common.UnsortedNamedInts() if self.group_mask != []: # only keys with a non-zero gmask are remappable ret[self.default_task] = self.default_task # it should always be possible to map the key to itself for g in self.group_mask: g = special_keys.CID_GROUP[str(g)] for tgt_cid in self._device.keys.group_cids[g]: tgt_task = str(special_keys.TASK[self._device.keys.cid_to_tid[tgt_cid]]) - tgt_task = _NamedInt(tgt_cid, tgt_task) + tgt_task = NamedInt(tgt_cid, tgt_task) if tgt_task != self.default_task: # don't put itself in twice ret[tgt_task] = tgt_task return ret @@ -288,15 +276,15 @@ def set_rawXY_reporting(self, value: bool): flags = {special_keys.MAPPING_FLAG.raw_XY_diverted: value} self._setCidReporting(flags=flags) - def remap(self, to: _NamedInt): + def remap(self, to: NamedInt): """Temporarily remaps this control to another action.""" self._setCidReporting(remap=int(to)) def _getCidReporting(self): try: - mapped_data = self._device.feature_request(FEATURE.REPROG_CONTROLS_V4, 0x20, *tuple(_pack("!H", self._cid))) + mapped_data = self._device.feature_request(FEATURE.REPROG_CONTROLS_V4, 0x20, *tuple(struct.pack("!H", self._cid))) if mapped_data: - cid, mapping_flags_1, mapped_to = _unpack("!HBH", mapped_data[:5]) + cid, mapping_flags_1, mapped_to = struct.unpack("!HBH", mapped_data[:5]) if cid != self._cid and logger.isEnabledFor(logging.WARNING): logger.warning( f"REPROG_CONTROLS_V4 endpoint getCidReporting on device {self._device} replied " @@ -304,7 +292,7 @@ def _getCidReporting(self): ) self._mapped_to = mapped_to if mapped_to != 0 else self._cid if len(mapped_data) > 5: - (mapping_flags_2,) = _unpack("!B", mapped_data[5:6]) + (mapping_flags_2,) = struct.unpack("!B", mapped_data[5:6]) else: mapping_flags_2 = 0 self._mapping_flags = mapping_flags_1 | (mapping_flags_2 << 8) @@ -322,7 +310,7 @@ def _getCidReporting(self): def _setCidReporting(self, flags=None, remap=0): """Sends a `setCidReporting` request with the given parameters. Raises an exception if the parameters are invalid. Parameters: - - flags {Dict[_NamedInt,bool]} -- a dictionary of which mapping flags to set/unset + - flags {Dict[NamedInt,bool]} -- a dictionary of which mapping flags to set/unset - remap {int} -- which control ID to remap to; or 0 to keep current mapping """ flags = flags if flags else {} # See flake8 B006 @@ -365,11 +353,11 @@ def _setCidReporting(self, flags=None, remap=0): if remap != 0: # update mapping if changing (even if not already read) self._mapped_to = remap - pkt = tuple(_pack("!HBH", self._cid, bfield & 0xFF, remap)) + pkt = tuple(struct.pack("!HBH", self._cid, bfield & 0xFF, remap)) # TODO: to fully support version 4 of REPROG_CONTROLS_V4, append `(bfield >> 8) & 0xff` here. # But older devices might behave oddly given that byte, so we don't send it. ret = self._device.feature_request(FEATURE.REPROG_CONTROLS_V4, 0x30, *pkt) - if ret is None or _unpack("!BBBBB", ret[:5]) != pkt and logger.isEnabledFor(logging.DEBUG): + if ret is None or struct.unpack("!BBBBB", ret[:5]) != pkt and logger.isEnabledFor(logging.DEBUG): logger.debug(f"REPROG_CONTROLS_v4 setCidReporting on device {self._device} didn't echo request packet.") @@ -384,11 +372,11 @@ def __init__(self, device, index, cid, actionId, remapped, modifierMask, cidStat self.cidStatus = cidStatus @property - def key(self) -> _NamedInt: + def key(self) -> NamedInt: return special_keys.CONTROL[self._cid] @property - def actionType(self) -> _NamedInt: + def actionType(self) -> NamedInt: return special_keys.ACTIONID[self.actionId] @property @@ -422,16 +410,18 @@ def modifiers(self): @property def data_bytes(self): - return _int2bytes(self.actionId, 1) + _int2bytes(self.remapped, 2) + _int2bytes(self._modifierMask, 1) + return ( + common.int2bytes(self.actionId, 1) + common.int2bytes(self.remapped, 2) + common.int2bytes(self._modifierMask, 1) + ) def remap(self, data_bytes): - cid = _int2bytes(self._cid, 2) - if _bytes2int(data_bytes) == special_keys.KEYS_Default: # map back to default + cid = common.int2bytes(self._cid, 2) + if common.bytes2int(data_bytes) == special_keys.KEYS_Default: # map back to default self._device.feature_request(FEATURE.PERSISTENT_REMAPPABLE_ACTION, 0x50, cid, 0xFF) self._device.remap_keys._query_key(self.index) return self._device.remap_keys.keys[self.index].data_bytes else: - self.actionId, self.remapped, self._modifierMask = _unpack("!BHB", data_bytes) + self.actionId, self.remapped, self._modifierMask = struct.unpack("!BHB", data_bytes) self.cidStatus = 0x01 self._device.feature_request(FEATURE.PERSISTENT_REMAPPABLE_ACTION, 0x40, cid, 0xFF, data_bytes) return True @@ -443,7 +433,7 @@ class KeysArray: def __init__(self, device, count, version): assert device is not None self.device = device - self.lock = _threading.Lock() + self.lock = threading.Lock() if FEATURE.REPROG_CONTROLS_V4 in self.device.features: self.keyversion = FEATURE.REPROG_CONTROLS_V4 elif FEATURE.REPROG_CONTROLS_V2 in self.device.features: @@ -510,7 +500,7 @@ def _query_key(self, index: int): raise IndexError(index) keydata = self.device.feature_request(FEATURE.REPROG_CONTROLS, 0x10, index) if keydata: - cid, tid, flags = _unpack("!HHB", keydata[:5]) + cid, tid, flags = struct.unpack("!HHB", keydata[:5]) self.keys[index] = ReprogrammableKey(self.device, index, cid, tid, flags) self.cid_to_tid[cid] = tid elif logger.isEnabledFor(logging.WARNING): @@ -526,7 +516,7 @@ def _query_key(self, index: int): raise IndexError(index) keydata = self.device.feature_request(FEATURE.REPROG_CONTROLS_V4, 0x10, index) if keydata: - cid, tid, flags1, pos, group, gmask, flags2 = _unpack("!HHBBBBB", keydata[:9]) + cid, tid, flags1, pos, group, gmask, flags2 = struct.unpack("!HHBBBBB", keydata[:9]) flags = flags1 | (flags2 << 8) self.keys[index] = ReprogrammableKeyV4(self.device, index, cid, tid, flags, pos, group, gmask) self.cid_to_tid[cid] = tid @@ -547,7 +537,7 @@ def capabilities(self): if self._capabilities is None and self.device.online: capabilities = self.device.feature_request(FEATURE.PERSISTENT_REMAPPABLE_ACTION, 0x00) assert capabilities, "Oops, persistent remappable key capabilities cannot be retrieved!" - self._capabilities = _unpack("!H", capabilities[:2])[0] # flags saying what the mappings are possible + self._capabilities = struct.unpack("!H", capabilities[:2])[0] # flags saying what the mappings are possible return self._capabilities def _query_key(self, index: int): @@ -555,10 +545,10 @@ def _query_key(self, index: int): raise IndexError(index) keydata = self.device.feature_request(FEATURE.PERSISTENT_REMAPPABLE_ACTION, 0x20, index, 0xFF) if keydata: - key = _unpack("!H", keydata[:2])[0] + key = struct.unpack("!H", keydata[:2])[0] mapped_data = self.device.feature_request(FEATURE.PERSISTENT_REMAPPABLE_ACTION, 0x30, key >> 8, key & 0xFF, 0xFF) if mapped_data: - _ignore, _ignore, actionId, remapped, modifiers, status = _unpack("!HBBHBB", mapped_data[:8]) + _ignore, _ignore, actionId, remapped, modifiers, status = struct.unpack("!HBBHBB", mapped_data[:8]) else: actionId = remapped = modifiers = status = 0 actionId = special_keys.ACTIONID[actionId] @@ -578,7 +568,7 @@ def _query_key(self, index: int): # Param Ids for feature GESTURE_2 -PARAM = _NamedInts( +PARAM = common.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 @@ -622,7 +612,7 @@ def __repr__(self): } # Spec Ids for feature GESTURE_2 -SPEC = _NamedInts( +SPEC = common.NamedInts( DVI_field_width=1, field_widths=2, period_unit=3, @@ -637,7 +627,7 @@ def __repr__(self): SPEC._fallback = lambda x: f"unknown:{x:04X}" # Action Ids for feature GESTURE_2 -ACTION_ID = _NamedInts( +ACTION_ID = common.NamedInts( MovePointer=1, ScrollHorizontal=2, WheelScrolling=3, @@ -750,7 +740,7 @@ def value(self): def read(self): # returns the bytes for the parameter result = self._device.feature_request(FEATURE.GESTURE_2, 0x70, self.index, 0xFF) if result: - self._value = _bytes2int(result[: self.size]) + self._value = common.bytes2int(result[: self.size]) return self._value @property @@ -762,7 +752,7 @@ def default_value(self): def _read_default(self): result = self._device.feature_request(FEATURE.GESTURE_2, 0x60, self.index, 0xFF) if result: - self._default_value = _bytes2int(result[: self.size]) + self._default_value = common.bytes2int(result[: self.size]) return self._default_value def write(self, bytes): @@ -799,7 +789,7 @@ def read(self): f"Feature Call Error reading Gesture Spec on device {self._device} for spec {self.id} - use None" ) return None - return _bytes2int(value[: self.byte_count]) + return common.bytes2int(value[: self.byte_count]) def __repr__(self): return f"[{self.spec}={self.value}]" @@ -883,7 +873,7 @@ def __init__(self, device): if not response: 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( + self.enabled, self.options, supported, effects, self.level, self.dho, self.dhi, self.dpow = struct.unpack( " 0x04: return - count, oob, buttons, sectors, size, shift = _unpack("!BBBBHB", response[3:10]) + count, oob, buttons, sectors, size, shift = struct.unpack("!BBBBHB", response[3:10]) gbuttons = buttons if (shift & 0x3 == 0x2) else 0 headers = OnboardProfiles.get_profile_headers(device) profiles = {} @@ -1319,11 +1309,11 @@ def from_device(cls, device): def to_bytes(self): bytes = b"" for i in range(1, len(self.profiles) + 1): - bytes += _int2bytes(self.profiles[i].sector, 2) + _int2bytes(self.profiles[i].enabled, 1) + b"\x00" + bytes += common.int2bytes(self.profiles[i].sector, 2) + common.int2bytes(self.profiles[i].enabled, 1) + b"\x00" bytes += b"\xff\xff\x00\x00" # marker after last profile while len(bytes) < self.size - 2: # leave room for CRC bytes += b"\xff" - bytes += _int2bytes(_crc16(bytes), 2) + bytes += common.int2bytes(common.crc16(bytes), 2) return bytes @classmethod @@ -1368,11 +1358,11 @@ def write(self, device): return written def show(self): - print(_yaml.dump(self)) + print(yaml.dump(self)) -_yaml.SafeLoader.add_constructor("!OnboardProfiles", OnboardProfiles.from_yaml) -_yaml.add_representer(OnboardProfiles, OnboardProfiles.to_yaml) +yaml.SafeLoader.add_constructor("!OnboardProfiles", OnboardProfiles.from_yaml) +yaml.add_representer(OnboardProfiles, OnboardProfiles.to_yaml) def feature_request(device, feature, function=0x00, *params, no_reply=False): @@ -1417,20 +1407,18 @@ def get_firmware(self, device): if fw_info: level = ord(fw_info[:1]) & 0x0F if level == 0 or level == 1: - name, version_major, version_minor, build = _unpack("!3sBBH", fw_info[1:8]) + name, version_major, version_minor, build = struct.unpack("!3sBBH", fw_info[1:8]) version = f"{version_major:02X}.{version_minor:02X}" if build: version += f".B{build:04X}" extras = fw_info[9:].rstrip(b"\x00") or None - fw_info = _FirmwareInfo(FIRMWARE_KIND[level], name.decode("ascii"), version, extras) + fw_info = common.FirmwareInfo(FIRMWARE_KIND[level], name.decode("ascii"), version, extras) elif level == FIRMWARE_KIND.Hardware: - fw_info = _FirmwareInfo(FIRMWARE_KIND.Hardware, "", str(ord(fw_info[1:2])), None) + fw_info = common.FirmwareInfo(FIRMWARE_KIND.Hardware, "", str(ord(fw_info[1:2])), None) else: - fw_info = _FirmwareInfo(FIRMWARE_KIND.Other, "", "", None) + fw_info = common.FirmwareInfo(FIRMWARE_KIND.Other, "", "", None) fw.append(fw_info) - # if logger.isEnabledFor(logging.DEBUG): - # logger.debug("device %d firmware %s", devnumber, fw_info) return tuple(fw) def get_ids(self, device): @@ -1458,8 +1446,6 @@ def get_kind(self, device: Device): kind = device.feature_request(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]) return KIND_MAP[DEVICE_KIND[kind]] def get_name(self, device: Device): @@ -1581,7 +1567,7 @@ def get_profiles(self, device: Device): def get_mouse_pointer_info(self, device: Device): pointer_info = device.feature_request(FEATURE.MOUSE_POINTER) if pointer_info: - dpi, flags = _unpack("!HB", pointer_info[:3]) + dpi, flags = struct.unpack("!HB", pointer_info[:3]) acceleration = ("none", "low", "med", "high")[flags & 0x3] suggest_os_ballistics = (flags & 0x04) != 0 suggest_vertical_orientation = (flags & 0x08) != 0 @@ -1595,7 +1581,7 @@ def get_mouse_pointer_info(self, device: Device): def get_vertical_scrolling_info(self, device: Device): vertical_scrolling_info = device.feature_request(FEATURE.VERTICAL_SCROLLING) if vertical_scrolling_info: - roller, ratchet, lines = _unpack("!BBB", vertical_scrolling_info[:3]) + roller, ratchet, lines = struct.unpack("!BBB", vertical_scrolling_info[:3]) roller_type = ( "reserved", "standard", @@ -1611,13 +1597,13 @@ def get_vertical_scrolling_info(self, device: Device): def get_hi_res_scrolling_info(self, device: Device): hi_res_scrolling_info = device.feature_request(FEATURE.HI_RES_SCROLLING) if hi_res_scrolling_info: - mode, resolution = _unpack("!BB", hi_res_scrolling_info[:2]) + mode, resolution = struct.unpack("!BB", hi_res_scrolling_info[:2]) return mode, resolution def get_pointer_speed_info(self, device: Device): pointer_speed_info = device.feature_request(FEATURE.POINTER_SPEED) if pointer_speed_info: - pointer_speed_hi, pointer_speed_lo = _unpack("!BB", pointer_speed_info[:2]) + pointer_speed_hi, pointer_speed_lo = struct.unpack("!BB", pointer_speed_info[:2]) # if pointer_speed_lo > 0: # pointer_speed_lo = pointer_speed_lo return pointer_speed_hi + pointer_speed_lo / 256 @@ -1625,7 +1611,7 @@ def get_pointer_speed_info(self, device: Device): def get_lowres_wheel_status(self, device: Device): lowres_wheel_status = device.feature_request(FEATURE.LOWRES_WHEEL) if lowres_wheel_status: - wheel_flag = _unpack("!B", lowres_wheel_status[:1])[0] + wheel_flag = struct.unpack("!B", lowres_wheel_status[:1])[0] wheel_reporting = ("HID", "HID++")[wheel_flag & 0x01] return wheel_reporting @@ -1636,20 +1622,20 @@ def get_hires_wheel(self, device: Device): if caps and mode and ratchet: # Parse caps - multi, flags = _unpack("!BB", caps[:2]) + multi, flags = struct.unpack("!BB", caps[:2]) has_invert = (flags & 0x08) != 0 has_ratchet = (flags & 0x04) != 0 # Parse mode - wheel_mode, reserved = _unpack("!BB", mode[:2]) + wheel_mode, reserved = struct.unpack("!BB", mode[:2]) target = (wheel_mode & 0x01) != 0 res = (wheel_mode & 0x02) != 0 inv = (wheel_mode & 0x04) != 0 # Parse Ratchet switch - ratchet_mode, reserved = _unpack("!BB", ratchet[:2]) + ratchet_mode, reserved = struct.unpack("!BB", ratchet[:2]) ratchet = (ratchet_mode & 0x01) != 0 @@ -1658,7 +1644,7 @@ def get_hires_wheel(self, device: Device): def get_new_fn_inversion(self, device: Device): state = device.feature_request(FEATURE.NEW_FN_INVERSION, 0x00) if state: - inverted, default_inverted = _unpack("!BB", state[:2]) + inverted, default_inverted = struct.unpack("!BB", state[:2]) inverted = (inverted & 0x01) != 0 default_inverted = (default_inverted & 0x01) != 0 return inverted, default_inverted @@ -1667,11 +1653,11 @@ def get_host_names(self, device: Device): state = device.feature_request(FEATURE.HOSTS_INFO, 0x00) host_names = {} if state: - capability_flags, _ignore, numHosts, currentHost = _unpack("!BBBB", state[:4]) + capability_flags, _ignore, numHosts, currentHost = struct.unpack("!BBBB", state[:4]) if capability_flags & 0x01: # device can get host names for host in range(0, numHosts): hostinfo = device.feature_request(FEATURE.HOSTS_INFO, 0x10, host) - _ignore, status, _ignore, _ignore, nameLen, _ignore = _unpack("!BBBBBB", hostinfo[:6]) + _ignore, status, _ignore, _ignore, nameLen, _ignore = struct.unpack("!BBBBBB", hostinfo[:6]) name = "" remaining = nameLen while remaining > 0: @@ -1696,10 +1682,10 @@ def set_host_name(self, device: Device, name, currentName=""): logger.info("Setting host name to %s", name) state = device.feature_request(FEATURE.HOSTS_INFO, 0x00) if state: - flags, _ignore, _ignore, currentHost = _unpack("!BBBB", state[:4]) + flags, _ignore, _ignore, currentHost = struct.unpack("!BBBB", state[:4]) if flags & 0x02: hostinfo = device.feature_request(FEATURE.HOSTS_INFO, 0x10, currentHost) - _ignore, _ignore, _ignore, _ignore, _ignore, maxNameLen = _unpack("!BBBBBB", hostinfo[:6]) + _ignore, _ignore, _ignore, _ignore, _ignore, maxNameLen = struct.unpack("!BBBBBB", hostinfo[:6]) if name[:maxNameLen] == currentName[:maxNameLen] and False: return True length = min(maxNameLen, len(name)) @@ -1715,7 +1701,7 @@ def get_onboard_mode(self, device: Device): state = device.feature_request(FEATURE.ONBOARD_PROFILES, 0x20) if state: - mode = _unpack("!B", state[:1])[0] + mode = struct.unpack("!B", state[:1])[0] return mode def set_onboard_mode(self, device: Device, mode): @@ -1725,19 +1711,19 @@ def set_onboard_mode(self, device: Device, mode): def get_polling_rate(self, device: Device): state = device.feature_request(FEATURE.REPORT_RATE, 0x10) if state: - rate = _unpack("!B", state[:1])[0] + rate = struct.unpack("!B", state[:1])[0] return str(rate) + "ms" else: rates = ["8ms", "4ms", "2ms", "1ms", "500us", "250us", "125us"] state = device.feature_request(FEATURE.EXTENDED_ADJUSTABLE_REPORT_RATE, 0x20) if state: - rate = _unpack("!B", state[:1])[0] + rate = struct.unpack("!B", state[:1])[0] return rates[rate] def get_remaining_pairing(self, device: Device): result = device.feature_request(FEATURE.REMAINING_PAIRING, 0x0) if result: - result = _unpack("!B", result[:1])[0] + result = struct.unpack("!B", result[:1])[0] FEATURE._fallback = lambda x: f"unknown:{x:04X}" return result @@ -1754,7 +1740,7 @@ def config_change(self, device: Device, configuration, no_reply=False): def decipher_battery_status(report: FixedBytes5) -> Tuple[Any, Battery]: - battery_discharge_level, battery_discharge_next_level, battery_status = _unpack("!BBB", report[:3]) + battery_discharge_level, battery_discharge_next_level, battery_status = struct.unpack("!BBB", report[:3]) if battery_discharge_level == 0: battery_discharge_level = None try: @@ -1770,7 +1756,7 @@ def decipher_battery_status(report: FixedBytes5) -> Tuple[Any, Battery]: def decipher_battery_voltage(report): - voltage, flags = _unpack(">HB", report[:3]) + voltage, flags = struct.unpack(">HB", report[:3]) status = BatteryStatus.DISCHARGING charge_sts = ERROR.unknown charge_lvl = CHARGE_LEVEL.average @@ -1808,7 +1794,7 @@ def decipher_battery_voltage(report): def decipher_battery_unified(report): - discharge, level, status_byte, _ignore = _unpack("!BBBB", report[:4]) + discharge, level, status_byte, _ignore = struct.unpack("!BBBB", report[:4]) try: status = BatteryStatus(status_byte) except ValueError: @@ -1833,7 +1819,7 @@ def decipher_battery_unified(report): def decipher_adc_measurement(report): # partial implementation - needs mapping to levels - adc, flags = _unpack("!HB", report[:3]) + adc, flags = struct.unpack("!HB", report[:3]) for level in battery_voltage_remaining: if level[0] < adc: charge_level = level[1] diff --git a/lib/logitech_receiver/i18n.py b/lib/logitech_receiver/i18n.py index e676605112..c0bedb630c 100644 --- a/lib/logitech_receiver/i18n.py +++ b/lib/logitech_receiver/i18n.py @@ -16,10 +16,10 @@ # Translation support for the Logitech receivers library -import gettext as _gettext +import gettext -_ = _gettext.gettext -ngettext = _gettext.ngettext +_ = gettext.gettext +ngettext = gettext.ngettext # A few common strings, not always accessible as such in the code. diff --git a/lib/logitech_receiver/notifications.py b/lib/logitech_receiver/notifications.py index 15e1df31e8..6cb0f49db6 100644 --- a/lib/logitech_receiver/notifications.py +++ b/lib/logitech_receiver/notifications.py @@ -18,31 +18,31 @@ # Handles incoming events from the receiver/devices, updating the object as appropriate. import logging -import threading as _threading - -from struct import unpack as _unpack +import struct +import threading from solaar.i18n import _ -from . import diversion as _diversion +from . import base +from . import common +from . import diversion from . import hidpp10 -from . import hidpp10_constants as _hidpp10_constants +from . import hidpp10_constants from . import 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 . import hidpp20_constants +from . import settings_templates from .common import Alert -from .common import Battery as _Battery from .common import BatteryStatus -from .common import strhex as _strhex logger = logging.getLogger(__name__) -_R = _hidpp10_constants.REGISTERS -_F = _hidpp20_constants.FEATURE +_hidpp10 = hidpp10.Hidpp10() +_hidpp20 = hidpp20.Hidpp20() +_R = hidpp10_constants.REGISTERS +_F = hidpp20_constants.FEATURE -notification_lock = _threading.Lock() +notification_lock = threading.Lock() def process(device, notification): @@ -68,7 +68,7 @@ def _process_receiver_notification(receiver, n): receiver.pairing.new_device = None pair_error = ord(n.data[:1]) if pair_error: - receiver.pairing.error = error_string = _hidpp10_constants.PAIRING_ERRORS[pair_error] + receiver.pairing.error = error_string = hidpp10_constants.PAIRING_ERRORS[pair_error] receiver.pairing.new_device = None logger.warning("pairing error %d: %s", pair_error, error_string) receiver.changed(reason=reason) @@ -87,7 +87,7 @@ def _process_receiver_notification(receiver, n): receiver.pairing.device_passkey = None discover_error = ord(n.data[:1]) if discover_error: - receiver.pairing.error = discover_string = _hidpp10_constants.BOLT_PAIRING_ERRORS[discover_error] + receiver.pairing.error = discover_string = hidpp10_constants.BOLT_PAIRING_ERRORS[discover_error] logger.warning("bolt discovering error %d: %s", discover_error, discover_string) receiver.changed(reason=reason) return True @@ -117,16 +117,16 @@ def _process_receiver_notification(receiver, n): logger.info("%s: %s", receiver, reason) receiver.pairing.error = None if not receiver.pairing.lock_open: - receiver.pairing.counter = ( - receiver.pairing.device_address - ) = receiver.pairing.device_authentication = receiver.pairing.device_name = None + receiver.pairing.counter = receiver.pairing.device_address = receiver.pairing.device_authentication = ( + receiver.pairing.device_name + ) = None pair_error = n.data[0] if receiver.pairing.lock_open: receiver.pairing.new_device = None elif n.address == 0x02 and not pair_error: receiver.pairing.new_device = receiver.register_new_device(n.data[7]) if pair_error: - receiver.pairing.error = error_string = _hidpp10_constants.BOLT_PAIRING_ERRORS[pair_error] + receiver.pairing.error = error_string = hidpp10_constants.BOLT_PAIRING_ERRORS[pair_error] receiver.pairing.new_device = None logger.warning("pairing error %d: %s", pair_error, error_string) receiver.changed(reason=reason) @@ -157,7 +157,7 @@ def _process_device_notification(device, n): # 0x40 to 0x7F appear to be HID++ 1.0 or DJ notifications if n.sub_id >= 0x40: - if n.report_id == _DJ_MESSAGE_ID: + if n.report_id == base.DJ_MESSAGE_ID: return _process_dj_notification(device, n) else: return _process_hidpp10_notification(device, n) @@ -239,11 +239,11 @@ def _process_hidpp10_notification(device, n): if n.sub_id == 0x41: # device connection (and disconnection) flags = ord(n.data[:1]) & 0xF0 if n.address == 0x02: # very old 27 MHz protocol - wpid = "00" + _strhex(n.data[2:3]) + wpid = "00" + common.strhex(n.data[2:3]) link_established = True link_encrypted = bool(flags & 0x80) elif n.address > 0x00: # all other protocols are supposed to be almost the same - wpid = _strhex(n.data[2:3] + n.data[1:2]) + wpid = common.strhex(n.data[2:3] + n.data[1:2]) link_established = not (flags & 0x40) link_encrypted = bool(flags & 0x20) or n.address == 0x10 # Bolt protocol always encrypted else: @@ -288,7 +288,9 @@ def _process_hidpp10_notification(device, n): def _process_feature_notification(device, n, feature): if logger.isEnabledFor(logging.DEBUG): - logger.debug("%s: notification for feature %s, report %s, data %s", device, feature, n.address >> 4, _strhex(n.data)) + logger.debug( + "%s: notification for feature %s, report %s, data %s", device, feature, n.address >> 4, common.strhex(n.data) + ) if feature == _F.BATTERY_STATUS: if n.address == 0x00: @@ -323,16 +325,16 @@ def _process_feature_notification(device, n, feature): elif feature == _F.SOLAR_DASHBOARD: if n.data[5:9] == b"GOOD": - charge, lux, adc = _unpack("!BHH", n.data[:5]) + charge, lux, adc = struct.unpack("!BHH", n.data[:5]) # guesstimate the battery voltage, emphasis on 'guess' # status_text = '%1.2fV' % (adc * 2.67793237653 / 0x0672) status_text = BatteryStatus.DISCHARGING if n.address == 0x00: - device.set_battery_info(_Battery(charge, None, status_text, None)) + device.set_battery_info(common.Battery(charge, None, status_text, None)) elif n.address == 0x10: if lux > 200: status_text = BatteryStatus.RECHARGING - device.set_battery_info(_Battery(charge, None, status_text, None, lux)) + device.set_battery_info(common.Battery(charge, None, status_text, None, lux)) elif n.address == 0x20: if logger.isEnabledFor(logging.DEBUG): logger.debug("%s: Light Check button pressed", device) @@ -382,18 +384,18 @@ def _process_feature_notification(device, n, feature): elif feature == _F.BACKLIGHT2: if n.address == 0x00: - level = _unpack("!B", n.data[1:2])[0] + level = struct.unpack("!B", n.data[1:2])[0] if device.setting_callback: - device.setting_callback(device, _st.Backlight2Level, [level]) + device.setting_callback(device, settings_templates.Backlight2Level, [level]) elif feature == _F.REPROG_CONTROLS_V4: if n.address == 0x00: if logger.isEnabledFor(logging.DEBUG): - cid1, cid2, cid3, cid4 = _unpack("!HHHH", n.data[:8]) + cid1, cid2, cid3, cid4 = struct.unpack("!HHHH", n.data[:8]) logger.debug("%s: diverted controls pressed: 0x%x, 0x%x, 0x%x, 0x%x", device, cid1, cid2, cid3, cid4) elif n.address == 0x10: if logger.isEnabledFor(logging.DEBUG): - dx, dy = _unpack("!hh", n.data[:4]) + dx, dy = struct.unpack("!hh", n.data[:4]) logger.debug("%s: rawXY dx=%i dy=%i", device, dx, dy) elif n.address == 0x20: if logger.isEnabledFor(logging.DEBUG): @@ -404,7 +406,7 @@ def _process_feature_notification(device, n, feature): elif feature == _F.HIRES_WHEEL: if n.address == 0x00: if logger.isEnabledFor(logging.INFO): - flags, delta_v = _unpack(">bh", n.data[:3]) + flags, delta_v = struct.unpack(">bh", n.data[:3]) high_res = (flags & 0x10) != 0 periods = flags & 0x0F logger.info("%s: WHEEL: res: %d periods: %d delta V:%-3d", device, high_res, periods, delta_v) @@ -414,7 +416,7 @@ def _process_feature_notification(device, n, feature): logger.info("%s: WHEEL: ratchet: %d", device, ratchet) if ratchet < 2: # don't process messages with unusual ratchet values if device.setting_callback: - device.setting_callback(device, _st.ScrollRatchet, [2 if ratchet else 1]) + device.setting_callback(device, settings_templates.ScrollRatchet, [2 if ratchet else 1]) else: if logger.isEnabledFor(logging.INFO): logger.info("%s: unknown WHEEL %s", device, n) @@ -425,16 +427,18 @@ def _process_feature_notification(device, n, feature): logger.info("%s: unknown ONBOARD PROFILES %s", device, n) else: if n.address == 0x00: - profile_sector = _unpack("!H", n.data[:2])[0] + profile_sector = struct.unpack("!H", n.data[:2])[0] if profile_sector: - _st.profile_change(device, profile_sector) + settings_templates.profile_change(device, profile_sector) elif n.address == 0x10: - resolution_index = _unpack("!B", n.data[:1])[0] - profile_sector = _unpack("!H", device.feature_request(_F.ONBOARD_PROFILES, 0x40)[:2])[0] + resolution_index = struct.unpack("!B", n.data[:1])[0] + profile_sector = struct.unpack("!H", device.feature_request(_F.ONBOARD_PROFILES, 0x40)[:2])[0] if device.setting_callback: for profile in device.profiles.profiles.values() if device.profiles else []: if profile.sector == profile_sector: - device.setting_callback(device, _st.AdjustableDpi, [profile.resolutions[resolution_index]]) + device.setting_callback( + device, settings_templates.AdjustableDpi, [profile.resolutions[resolution_index]] + ) break elif feature == _F.BRIGHTNESS_CONTROL: @@ -443,13 +447,13 @@ def _process_feature_notification(device, n, feature): logger.info("%s: unknown BRIGHTNESS CONTROL %s", device, n) else: if n.address == 0x00: - brightness = _unpack("!H", n.data[:2])[0] - device.setting_callback(device, _st.BrightnessControl, [brightness]) + brightness = struct.unpack("!H", n.data[:2])[0] + device.setting_callback(device, settings_templates.BrightnessControl, [brightness]) elif n.address == 0x10: brightness = n.data[0] & 0x01 if brightness: - brightness = _unpack("!H", device.feature_request(_F.BRIGHTNESS_CONTROL, 0x10)[:2])[0] - device.setting_callback(device, _st.BrightnessControl, [brightness]) + brightness = struct.unpack("!H", device.feature_request(_F.BRIGHTNESS_CONTROL, 0x10)[:2])[0] + device.setting_callback(device, settings_templates.BrightnessControl, [brightness]) - _diversion.process_notification(device, n, feature) + diversion.process_notification(device, n, feature) return True diff --git a/lib/logitech_receiver/receiver.py b/lib/logitech_receiver/receiver.py index af7e0421b2..753c8687d8 100644 --- a/lib/logitech_receiver/receiver.py +++ b/lib/logitech_receiver/receiver.py @@ -15,7 +15,7 @@ ## with this program; if not, write to the Free Software Foundation, Inc., ## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -import errno as _errno +import errno import logging import time @@ -23,12 +23,12 @@ from typing import Callable from typing import Optional -import hidapi as _hid +import hidapi from solaar.i18n import _ from solaar.i18n import ngettext -from . import base as _base +from . import base from . import exceptions from . import hidpp10 from . import hidpp10_constants @@ -108,7 +108,7 @@ def close(self): if d: d.close() self._devices.clear() - return handle and _base.close(handle) + return handle and base.close(handle) def __del__(self): self.close() @@ -253,7 +253,7 @@ def count(self): def request(self, request_id, *params): if bool(self): - return _base.request(self.handle, 0xFF, request_id, *params) + return base.request(self.handle, 0xFF, request_id, *params) def reset_pairing(self): self.pairing = Pairing() @@ -451,7 +451,7 @@ def notification_information(self, number, notification): return online, encrypted, wpid, kind def device_pairing_information(self, number: int) -> dict: - wpid = _hid.find_paired_node_wpid(self.path, number) # extract WPID from udev path + wpid = hidapi.find_paired_node_wpid(self.path, number) # extract WPID from udev path if not wpid: logger.error("Unable to get wpid from udev for device %d of %s", number, self) raise exceptions.NoSuchDevice(number=number, receiver=self, error="Not present 27Mhz device") @@ -491,9 +491,9 @@ def create_receiver(device_info, setting_callback=None) -> Optional[Receiver]: """Opens a Logitech Receiver found attached to the machine, by Linux device path.""" try: - handle = _base.open_path(device_info.path) + handle = base.open_path(device_info.path) if handle: - product_info = _base.product_information(device_info.product_id) + product_info = base.product_information(device_info.product_id) if not product_info: logger.warning("Unknown receiver type: %s", device_info.product_id) product_info = {} @@ -502,7 +502,7 @@ def create_receiver(device_info, setting_callback=None) -> Optional[Receiver]: return rclass(kind, product_info, handle, device_info.path, device_info.product_id, setting_callback) except OSError as e: logger.exception("open %s", device_info) - if e.errno == _errno.EACCES: + if e.errno == errno.EACCES: raise except Exception: logger.exception("open %s", device_info) diff --git a/lib/logitech_receiver/settings.py b/lib/logitech_receiver/settings.py index b8cd447a14..4feafbbaa3 100644 --- a/lib/logitech_receiver/settings.py +++ b/lib/logitech_receiver/settings.py @@ -16,26 +16,21 @@ import logging import math - -from struct import unpack as _unpack -from time import sleep as _sleep +import struct +import time from solaar.i18n import _ -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 -from .common import int2bytes as _int2bytes +from . import common +from . import hidpp20_constants +from .common import NamedInt +from .common import NamedInts logger = logging.getLogger(__name__) -# -# -# SENSITIVITY_IGNORE = "ignore" -KIND = _NamedInts( +KIND = NamedInts( toggle=0x01, choice=0x02, range=0x04, @@ -613,7 +608,7 @@ def write_key_value(self, key, value, save=True): class RegisterRW: __slots__ = ("register",) - kind = _NamedInt(0x01, _("register")) + kind = NamedInt(0x01, _("register")) def __init__(self, register): assert isinstance(register, int) @@ -627,12 +622,12 @@ def write(self, device, data_bytes): class FeatureRW: - kind = _NamedInt(0x02, _("feature")) + kind = NamedInt(0x02, _("feature")) default_read_fnid = 0x00 default_write_fnid = 0x10 def __init__(self, feature, read_fnid=0x00, write_fnid=0x10, prefix=b"", suffix=b"", read_prefix=b"", no_reply=False): - assert isinstance(feature, _NamedInt) + assert isinstance(feature, NamedInt) self.feature = feature self.read_fnid = read_fnid self.write_fnid = write_fnid @@ -653,7 +648,7 @@ def write(self, device, data_bytes): class FeatureRWMap(FeatureRW): - kind = _NamedInt(0x02, _("feature")) + kind = NamedInt(0x02, _("feature")) default_read_fnid = 0x00 default_write_fnid = 0x10 default_key_byte_count = 1 @@ -666,7 +661,7 @@ def __init__( key_byte_count=default_key_byte_count, no_reply=False, ): - assert isinstance(feature, _NamedInt) + assert isinstance(feature, NamedInt) self.feature = feature self.read_fnid = read_fnid self.write_fnid = write_fnid @@ -675,12 +670,12 @@ def __init__( def read(self, device, key): assert self.feature is not None - key_bytes = _int2bytes(key, self.key_byte_count) + key_bytes = common.int2bytes(key, self.key_byte_count) return device.feature_request(self.feature, self.read_fnid, key_bytes) def write(self, device, key, data_bytes): assert self.feature is not None - key_bytes = _int2bytes(key, self.key_byte_count) + key_bytes = common.int2bytes(key, self.key_byte_count) reply = device.feature_request(self.feature, self.write_fnid, key_bytes, data_bytes, no_reply=self.no_reply) return reply if not self.no_reply else True @@ -733,13 +728,13 @@ def __init__( else: assert isinstance(false_value, bytes) if mask is None or mask == self.default_mask: - mask = b"\xFF" * len(true_value) + mask = b"\xff" * len(true_value) else: assert isinstance(mask, bytes) assert len(mask) == len(true_value) == len(false_value) - tv = _bytes2int(true_value) - fv = _bytes2int(false_value) - mv = _bytes2int(mask) + tv = common.bytes2int(true_value) + fv = common.bytes2int(false_value) + mv = common.bytes2int(mask) assert tv != fv # true and false might be something other than bit values assert tv & mv == tv assert fv & mv == fv @@ -773,14 +768,14 @@ def validate_read(self, reply_bytes): return False count = len(self.mask) - mask = _bytes2int(self.mask) - reply_value = _bytes2int(reply_bytes[:count]) & mask + mask = common.bytes2int(self.mask) + reply_value = common.bytes2int(reply_bytes[:count]) & mask - true_value = _bytes2int(self.true_value) + true_value = common.bytes2int(self.true_value) if reply_value == true_value: return True - false_value = _bytes2int(self.false_value) + false_value = common.bytes2int(self.false_value) if reply_value == false_value: return False @@ -852,7 +847,7 @@ def element_to_string(key, val): return "{" + ", ".join([element_to_string(k, value[k]) for k in value]) + "}" def validate_read(self, reply_bytes): - r = _bytes2int(reply_bytes[: self.byte_count]) + r = common.bytes2int(reply_bytes[: self.byte_count]) value = {int(k): False for k in self.options} m = 1 for _ignore in range(8 * self.byte_count): @@ -867,7 +862,7 @@ def prepare_write(self, new_value): for k, v in new_value.items(): if v: w |= int(k) - return _int2bytes(w, self.byte_count) + return common.int2bytes(w, self.byte_count) def get_options(self): return self.options @@ -931,7 +926,7 @@ def prepare_read(self): for offset, mask in self._mask_from_offset.items(): b = offset << (8 * (self.byte_count + 1)) b |= (self.sep << (8 * self.byte_count)) | mask - r.append(_int2bytes(b, self.byte_count + 2)) + r.append(common.int2bytes(b, self.byte_count + 2)) return r def prepare_read_key(self, key): @@ -941,14 +936,14 @@ def prepare_read_key(self, key): offset, mask = option.om_method(option) b = offset << (8 * (self.byte_count + 1)) b |= (self.sep << (8 * self.byte_count)) | mask - return _int2bytes(b, self.byte_count + 2) + return common.int2bytes(b, self.byte_count + 2) def validate_read(self, reply_bytes_dict): values = {int(k): False for k in self.options} for query, b in reply_bytes_dict.items(): - offset = _bytes2int(query[0:1]) + offset = common.bytes2int(query[0:1]) b += (self.byte_count - len(b)) * b"\x00" - value = _bytes2int(b[: self.byte_count]) + value = common.bytes2int(b[: self.byte_count]) mask_to_opt = self._option_from_offset_mask.get(offset, {}) m = 1 for _ignore in range(8 * self.byte_count): @@ -968,7 +963,7 @@ def prepare_write(self, new_value): if v: w[offset] |= mask return [ - _int2bytes( + common.int2bytes( (offset << (8 * (2 * self.byte_count + 1))) | (self.sep << (16 * self.byte_count)) | (self._mask_from_offset[offset] << (8 * self.byte_count)) @@ -1009,7 +1004,7 @@ class ChoicesValidator(Validator): def __init__(self, choices=None, byte_count=None, read_skip_byte_count=0, write_prefix_bytes=b""): assert choices is not None - assert isinstance(choices, _NamedInts) + assert isinstance(choices, NamedInts) assert len(choices) > 1 self.choices = choices self.needs_current_value = False @@ -1029,7 +1024,7 @@ def to_string(self, value): return str(self.choices[value]) if isinstance(value, int) else str(value) def validate_read(self, reply_bytes): - reply_value = _bytes2int(reply_bytes[self._read_skip_byte_count : self._read_skip_byte_count + self._byte_count]) + reply_value = common.bytes2int(reply_bytes[self._read_skip_byte_count : self._read_skip_byte_count + self._byte_count]) valid_value = self.choices[reply_value] assert valid_value is not None, f"{self.__class__.__name__}: failed to validate read value {reply_value:02X}" return valid_value @@ -1041,7 +1036,7 @@ def prepare_write(self, new_value, current_value=None): value = self.choice(new_value) if value is None: raise ValueError(f"invalid choice {new_value!r}") - assert isinstance(value, _NamedInt) + assert isinstance(value, NamedInt) return self._write_prefix_bytes + value.bytes(self._byte_count) def choice(self, value): @@ -1083,11 +1078,11 @@ def __init__( max_key_bits = 0 max_value_bits = 0 for key, choices in choices_map.items(): - assert isinstance(key, _NamedInt) - assert isinstance(choices, _NamedInts) + assert isinstance(key, NamedInt) + assert isinstance(choices, NamedInts) max_key_bits = max(max_key_bits, key.bit_length()) for key_value in choices: - assert isinstance(key_value, _NamedInt) + assert isinstance(key_value, NamedInt) max_value_bits = max(max_value_bits, key_value.bit_length()) self._key_byte_count = (max_key_bits + 7) // 8 if key_byte_count: @@ -1119,7 +1114,7 @@ def element_to_string(key, val): def validate_read(self, reply_bytes, key): start = self._key_byte_count + self._read_skip_byte_count end = start + self._byte_count - reply_value = _bytes2int(reply_bytes[start:end]) & self.mask + reply_value = common.bytes2int(reply_bytes[start:end]) & self.mask # reprogrammable keys starts out as 0, which is not a choice, so don't use assert here if self.extra_default is not None and self.extra_default == reply_value: return int(self.choices[key][0]) @@ -1188,7 +1183,7 @@ def __init__(self, min_value=0, max_value=255, byte_count=1): assert self._byte_count < 8 def validate_read(self, reply_bytes): - reply_value = _bytes2int(reply_bytes[: self._byte_count]) + reply_value = common.bytes2int(reply_bytes[: self._byte_count]) assert reply_value >= self.min_value, f"{self.__class__.__name__}: failed to validate read value {reply_value:02X}" assert reply_value <= self.max_value, f"{self.__class__.__name__}: failed to validate read value {reply_value:02X}" return reply_value @@ -1197,7 +1192,7 @@ def prepare_write(self, new_value, current_value=None): if new_value < self.min_value or new_value > self.max_value: raise ValueError(f"invalid choice {new_value!r}") current_value = self.validate_read(current_value) if current_value is not None else None - to_write = _int2bytes(new_value, self._byte_count) + to_write = common.int2bytes(new_value, self._byte_count) # current value is known and same as value to be written return None to signal not to write it return None if current_value is not None and current_value == new_value else to_write @@ -1270,7 +1265,7 @@ def __init__( def validate_read(self, reply_bytes): rvs = { - n: _bytes2int(reply_bytes[self.rsbc + n * self.bc : self.rsbc + (n + 1) * self.bc], signed=True) + n: common.bytes2int(reply_bytes[self.rsbc + n * self.bc : self.rsbc + (n + 1) * self.bc], signed=True) for n in range(self.count) } for n in range(self.count): @@ -1284,7 +1279,9 @@ def prepare_write(self, new_values): for new_value in new_values.values(): if new_value < self.min_value or new_value > self.max_value: raise ValueError(f"invalid value {new_value!r}") - bytes = self.write_prefix_bytes + b"".join(_int2bytes(new_values[n], self.bc, signed=True) for n in range(self.count)) + bytes = self.write_prefix_bytes + b"".join( + common.int2bytes(new_values[n], self.bc, signed=True) for n in range(self.count) + ) return bytes def acceptable(self, args, current): @@ -1305,12 +1302,12 @@ def __init__(self, items, sub_items): assert isinstance(sub_items, dict) # sub_items: items -> class with .minimum, .maximum, .length (in bytes), .id (a string) and .widget (e.g. 'Scale') self.items = items - self.keys = _NamedInts(**{str(item): int(item) for item in items}) + self.keys = NamedInts(**{str(item): int(item) for item in items}) self._item_from_id = {int(k): k for k in items} self.sub_items = sub_items def prepare_read_item(self, item): - return _int2bytes((self._item_from_id[int(item)].index << 1) | 0xFF, 2) + return common.int2bytes((self._item_from_id[int(item)].index << 1) | 0xFF, 2) def validate_read_item(self, reply_bytes, item): item = self._item_from_id[int(item)] @@ -1320,7 +1317,7 @@ def validate_read_item(self, reply_bytes, item): r = reply_bytes[start : start + sub_item.length] if len(r) < sub_item.length: r += b"\x00" * (sub_item.length - len(value)) - v = _bytes2int(r) + v = common.bytes2int(r) if not (sub_item.minimum < v < sub_item.maximum): logger.warning( f"{self.__class__.__name__}: failed to validate read value for {item}.{sub_item}: " @@ -1335,7 +1332,7 @@ def prepare_write(self, value): w = b"" for item in value.keys(): _item = self._item_from_id[int(item)] - b = _int2bytes(_item.index, 1) + b = common.int2bytes(_item.index, 1) for sub_item in self.sub_items[_item]: try: v = value[int(item)][str(sub_item)] @@ -1345,17 +1342,17 @@ def prepare_write(self, value): raise ValueError( f"invalid choice for {item}.{sub_item}: {v} not in [{sub_item.minimum}..{sub_item.maximum}]" ) - b += _int2bytes(v, sub_item.length) + b += common.int2bytes(v, sub_item.length) if len(w) + len(b) > 15: - seq.append(b + b"\xFF") + seq.append(b + b"\xff") w = b"" w += b - seq.append(w + b"\xFF") + seq.append(w + b"\xff") return seq def prepare_write_item(self, item, value): _item = self._item_from_id[int(item)] - w = _int2bytes(_item.index, 1) + w = common.int2bytes(_item.index, 1) for sub_item in self.sub_items[_item]: try: v = value[str(sub_item)] @@ -1363,8 +1360,8 @@ def prepare_write_item(self, item, value): return None if not (sub_item.minimum <= v <= sub_item.maximum): raise ValueError(f"invalid choice for {item}.{sub_item}: {v} not in [{sub_item.minimum}..{sub_item.maximum}]") - w += _int2bytes(v, sub_item.length) - return w + b"\xFF" + w += common.int2bytes(v, sub_item.length) + return w + b"\xff" def acceptable(self, args, current): # just one item, with at least one sub-item @@ -1418,13 +1415,13 @@ def key_action(self, key): # acction to take when some other diverted key is pr pass def read(self, device): # need to return bytes, as if read from device - return _int2bytes(self.key.key, 2) if self.active and self.key else b"\x00\x00" + return common.int2bytes(self.key.key, 2) if self.active and self.key else b"\x00\x00" 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_constants.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]) + cids = struct.unpack("!HHHH", n.data[:8]) if not self.pressed and int(self.key.key) in cids: # trigger key pressed self.pressed = True self.press_action() @@ -1438,7 +1435,7 @@ def handler(device, n): # Called on notification events from the device self.key_action(key) elif n.address == 0x10: if self.pressed: - dx, dy = _unpack("!hh", n.data[:4]) + dx, dy = struct.unpack("!hh", n.data[:4]) self.move_action(dx, dy) divertSetting = next(filter(lambda s: s.name == self.divert_setting_name, device.settings), None) @@ -1446,7 +1443,7 @@ def handler(device, n): # Called on notification events from the device logger.warning("setting %s not found on %s", self.divert_setting_name, device.name) return None self.device = device - key = _bytes2int(data_bytes) + key = common.bytes2int(data_bytes) if key: # Enable self.key = next((k for k in device.keys if k.key == key), None) if self.key: @@ -1484,13 +1481,13 @@ 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_constants.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_constants.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]) + cids = struct.unpack("!HHHH", n.data[:8]) ## generalize to list of keys if not self.initiating_key: # no initiating key pressed for k in self.keys: @@ -1508,7 +1505,7 @@ def handler(self, device, n): # Called on notification events from the device self.key_action(key) elif n.address == 0x10: if self.initiating_key: - dx, dy = _unpack("!hh", n.data[:4]) + dx, dy = struct.unpack("!hh", n.data[:4]) self.move_action(dx, dy) def start(self, key): @@ -1556,8 +1553,8 @@ 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_constants.FEATURE.HIRES_WHEEL in device.features: - _sleep(0.2) # delay to try to get out of race condition with Linux HID++ driver + if device.features and hidpp20_constants.FEATURE.HIRES_WHEEL in device.features: + time.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 {} for s in device.settings: diff --git a/lib/logitech_receiver/settings_templates.py b/lib/logitech_receiver/settings_templates.py index a7d8e1ec1d..c9903fb22e 100644 --- a/lib/logitech_receiver/settings_templates.py +++ b/lib/logitech_receiver/settings_templates.py @@ -15,58 +15,34 @@ ## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. import logging -import socket as _socket +import socket +import struct +import traceback -from logging import WARN as _WARN -from struct import pack as _pack -from struct import unpack as _unpack -from time import time as _time -from traceback import format_exc as _format_exc +from time import time from solaar.i18n import _ -from . import descriptors as _descriptors -from . import hidpp10_constants as _hidpp10_constants +from . import base +from . import common +from . import descriptors +from . import diversion +from . import hidpp10_constants from . import hidpp20 -from . import hidpp20_constants as _hidpp20_constants -from . import notify as _notify -from . import special_keys as _special_keys -from .base import _HIDPP_Notification as _HIDPP_Notification -from .common import NamedInt as _NamedInt -from .common import NamedInts as _NamedInts -from .common import bytes2int as _bytes2int -from .common import int2bytes as _int2bytes -from .diversion import process_notification as _process_notification -from .settings import KIND as _KIND -from .settings import ActionSettingRW as _ActionSettingRW -from .settings import BitFieldSetting as _BitFieldSetting -from .settings import BitFieldValidator as _BitFieldV -from .settings import BitFieldWithOffsetAndMaskSetting as _BitFieldOMSetting -from .settings import BitFieldWithOffsetAndMaskValidator as _BitFieldOMV -from .settings import ChoicesMapValidator as _ChoicesMapV -from .settings import ChoicesValidator as _ChoicesV -from .settings import FeatureRW as _FeatureRW -from .settings import FeatureRWMap as _FeatureRWMap -from .settings import HeteroValidator as _HeteroV -from .settings import LongSettings as _LongSettings -from .settings import MultipleRangeValidator as _MultipleRangeV -from .settings import PackedRangeValidator as _PackedRangeV -from .settings import RangeFieldSetting as _RangeFieldSetting -from .settings import RangeValidator as _RangeV -from .settings import RawXYProcessing as _RawXYProcessing -from .settings import Setting as _Setting -from .settings import Settings as _Settings -from .special_keys import DISABLE as _DKEY +from . import hidpp20_constants +from . import notify +from . import settings +from . import special_keys logger = logging.getLogger(__name__) _hidpp20 = hidpp20.Hidpp20() -_DK = _hidpp10_constants.DEVICE_KIND -_R = _hidpp10_constants.REGISTERS -_F = _hidpp20_constants.FEATURE +_DK = hidpp10_constants.DEVICE_KIND +_R = hidpp10_constants.REGISTERS +_F = hidpp20_constants.FEATURE -_GG = _hidpp20_constants.GESTURE -_GP = _hidpp20_constants.PARAM +_GG = hidpp20_constants.GESTURE +_GP = hidpp20_constants.PARAM # Setting classes are used to control the settings that the Solaar GUI shows and manipulates. # Each setting class has to several class variables: @@ -82,58 +58,58 @@ # persist (inherited True), which is whether to store the value and apply it when setting up the device. # # The different setting classes imported from settings.py are for different numbers and kinds of arguments. -# _Setting is for settings with a single value (boolean, number in a range, and symbolic choice). -# _Settings is for settings that are maps from keys to values +# Setting is for settings with a single value (boolean, number in a range, and symbolic choice). +# Settings is for settings that are maps from keys to values # and permit reading or writing the entire map or just one key/value pair. -# The _BitFieldSetting class is for settings that have multiple boolean values packed into a bit field. -# _BitFieldOMSetting is similar. -# The _RangeFieldSetting class is for settings that have multiple ranges packed into a byte string. -# _LongSettings is for settings that have an even more complex structure. +# The BitFieldSetting class is for settings that have multiple boolean values packed into a bit field. +# BitFieldWithOffsetAndMaskSetting is similar. +# The RangeFieldSetting class is for settings that have multiple ranges packed into a byte string. +# LongSettings is for settings that have an even more complex structure. # # When settings are created a reader/writer and a validator are created. # If the setting class has a value for rw_class then an instance of that class is created. -# Otherwise if the setting has a register then an instance of settings.RegisterRW is created. -# and if the setting has a feature then then an instance of _FeatureRW is created. +# Otherwise if the setting has a register then an instance of RegisterRW is created. +# and if the setting has a feature then an instance of FeatureRW is created. # The instance is created with the register or feature as the first argument and rw_options as keyword arguments. -# _RegisterRW doesn't use any options. -# _FeatureRW options include +# RegisterRW doesn't use any options. +# FeatureRW options include # read_fnid - the feature function (times 16) to read the value (default 0x00), # write_fnid - the feature function (times 16) to write the value (default 0x10), # prefix - a prefix to add to the data being written and the read request (default b''), used for features # that provide and set multiple settings (e.g., to read and write function key inversion for current host) # no_reply - whether to wait for a reply (default false) (USE WITH EXTREME CAUTION). # -# There are three simple validator classes - _BooleanV, _RangeV, and _ChoicesV -# _BooleanV is for boolean values and is the default. It takes +# There are three simple validator classes - BooleanV, RangeValidator, and ChoicesValidator +# BooleanV is for boolean values and is the default. It takes # true_value is the raw value for true (default 0x01), this can be an integer or a byte string, # false_value is the raw value for false (default 0x00), this can be an integer or a byte string, # mask is used to keep only some bits from a sequence of bits, this can be an integer or a byte string, # read_skip_byte_count is the number of bytes to ignore at the beginning of the read value (default 0), # write_prefix_bytes is a byte string to write before the value (default empty). -# _RangeV is for an integer in a range. It takes +# RangeValidator is for an integer in a range. It takes # byte_count is number of bytes that the value is stored in (defaults to size of max_value). -# _RangeV uses min_value and max_value from the setting class as minimum and maximum. +# RangeValidator uses min_value and max_value from the setting class as minimum and maximum. -# _ChoicesV is for symbolic choices. It takes one positional and three keyword arguments: +# ChoicesValidator is for symbolic choices. It takes one positional and three keyword arguments: # choices is a list of named integers that are the valid choices, # byte_count is the number of bytes for the integer (default size of largest choice integer), -# read_skip_byte_count is as for _BooleanV, -# write_prefix_bytes is as for _BooleanV. -# Settings that use _ChoicesV should have a choices_universe class variable of the potential choices, +# read_skip_byte_count is as for BooleanV, +# write_prefix_bytes is as for BooleanV. +# Settings that use ChoicesValidator should have a choices_universe class variable of the potential choices, # or None for no limitation and optionally a choices_extra class variable with an extra choice. # The choices_extra is so that there is no need to specially extend a large existing NamedInts. -# _ChoicesMapV validator is for map settings that map onto symbolic choices. It takes +# ChoicesMapValidator validator is for map settings that map onto symbolic choices. It takes # choices_map is a map from keys to possible values -# byte_count is as for _ChoicesV, -# read_skip_byte_count is as for _ChoicesV, -# write_prefix_bytes is as for _ChoicesV, +# byte_count is as for ChoicesValidator, +# read_skip_byte_count is as for ChoicesValidator, +# write_prefix_bytes is as for ChoicesValidator, # key_byte_count is the number of bytes for the key integer (default size of largest key), # extra_default is an extra raw value that is used as a default value (default None). -# Settings that use _ChoicesV should have keys_universe and choices_universe class variable of +# Settings that use ChoicesValidator should have keys_universe and choices_universe class variable of # the potential keys and potential choices or None for no limitation. -# _BitFieldV validator is for bit field settings. It takes one positional and one keyword argument +# BitFieldValidator validator is for bit field settings. It takes one positional and one keyword argument # the positional argument is the number of bits in the bit field # byte_count is the size of the bit field (default size of the bit field) # @@ -141,7 +117,7 @@ # These settings have reader/writer classes that perform special processing instead of sending commands to the device. -class FnSwapVirtual(_Setting): # virtual setting to hold fn swap strings +class FnSwapVirtual(settings.Setting): # virtual setting to hold fn swap strings name = "fn-swap" label = _("Swap Fx function") description = ( @@ -157,15 +133,15 @@ class FnSwapVirtual(_Setting): # virtual setting to hold fn swap strings ) -class RegisterHandDetection(_Setting): +class RegisterHandDetection(settings.Setting): name = "hand-detection" label = _("Hand Detection") description = _("Turn on illumination when the hands hover over the keyboard.") register = _R.keyboard_hand_detection - validator_options = {"true_value": b"\x00\x00\x00", "false_value": b"\x00\x00\x30", "mask": b"\x00\x00\xFF"} + validator_options = {"true_value": b"\x00\x00\x00", "false_value": b"\x00\x00\x30", "mask": b"\x00\x00\xff"} -class RegisterSmoothScroll(_Setting): +class RegisterSmoothScroll(settings.Setting): name = "smooth-scroll" label = _("Scroll Wheel Smooth Scrolling") description = _("High-sensitivity mode for vertical scroll with the wheel.") @@ -173,7 +149,7 @@ class RegisterSmoothScroll(_Setting): validator_options = {"true_value": 0x40, "mask": 0x40} -class RegisterSideScroll(_Setting): +class RegisterSideScroll(settings.Setting): name = "side-scroll" label = _("Side Scrolling") description = _( @@ -185,13 +161,13 @@ class RegisterSideScroll(_Setting): # different devices have different sets of permissible dpis, so this should be subclassed -class RegisterDpi(_Setting): +class RegisterDpi(settings.Setting): name = "dpi-old" label = _("Sensitivity (DPI - older mice)") description = _("Mouse movement sensitivity") register = _R.mouse_dpi - choices_universe = _NamedInts.range(0x81, 0x8F, lambda x: str((x - 0x80) * 100)) - validator_class = _ChoicesV + choices_universe = common.NamedInts.range(0x81, 0x8F, lambda x: str((x - 0x80) * 100)) + validator_class = settings.ChoicesValidator validator_options = {"choices": choices_universe} @@ -201,33 +177,33 @@ class RegisterFnSwap(FnSwapVirtual): class _PerformanceMXDpi(RegisterDpi): - choices_universe = _NamedInts.range(0x81, 0x8F, lambda x: str((x - 0x80) * 100)) + choices_universe = common.NamedInts.range(0x81, 0x8F, lambda x: str((x - 0x80) * 100)) validator_options = {"choices": choices_universe} # set up register settings for devices - this is done here to break up an import loop -_descriptors.get_wpid("0060").settings = [RegisterFnSwap] -_descriptors.get_wpid("2008").settings = [RegisterFnSwap] -_descriptors.get_wpid("2010").settings = [RegisterFnSwap, RegisterHandDetection] -_descriptors.get_wpid("2011").settings = [RegisterFnSwap] -_descriptors.get_usbid(0xC318).settings = [RegisterFnSwap] -_descriptors.get_wpid("C714").settings = [RegisterFnSwap] -_descriptors.get_wpid("100B").settings = [RegisterSmoothScroll, RegisterSideScroll] -_descriptors.get_wpid("100F").settings = [RegisterSmoothScroll, RegisterSideScroll] -_descriptors.get_wpid("1013").settings = [RegisterSmoothScroll, RegisterSideScroll] -_descriptors.get_wpid("1014").settings = [RegisterSmoothScroll, RegisterSideScroll] -_descriptors.get_wpid("1017").settings = [RegisterSmoothScroll, RegisterSideScroll] -_descriptors.get_wpid("1023").settings = [RegisterSmoothScroll, RegisterSideScroll] -_descriptors.get_wpid("4004").settings = [_PerformanceMXDpi, RegisterSmoothScroll, RegisterSideScroll] -_descriptors.get_wpid("101A").settings = [_PerformanceMXDpi, RegisterSmoothScroll, RegisterSideScroll] -_descriptors.get_wpid("101B").settings = [RegisterSmoothScroll, RegisterSideScroll] -_descriptors.get_wpid("101D").settings = [RegisterSmoothScroll, RegisterSideScroll] -_descriptors.get_wpid("101F").settings = [RegisterSideScroll] -_descriptors.get_usbid(0xC06B).settings = [RegisterSmoothScroll, RegisterSideScroll] -_descriptors.get_wpid("1025").settings = [RegisterSideScroll] -_descriptors.get_wpid("102A").settings = [RegisterSmoothScroll, RegisterSideScroll] -_descriptors.get_usbid(0xC048).settings = [_PerformanceMXDpi, RegisterSmoothScroll, RegisterSideScroll] -_descriptors.get_usbid(0xC066).settings = [_PerformanceMXDpi, RegisterSmoothScroll, RegisterSideScroll] +descriptors.get_wpid("0060").settings = [RegisterFnSwap] +descriptors.get_wpid("2008").settings = [RegisterFnSwap] +descriptors.get_wpid("2010").settings = [RegisterFnSwap, RegisterHandDetection] +descriptors.get_wpid("2011").settings = [RegisterFnSwap] +descriptors.get_usbid(0xC318).settings = [RegisterFnSwap] +descriptors.get_wpid("C714").settings = [RegisterFnSwap] +descriptors.get_wpid("100B").settings = [RegisterSmoothScroll, RegisterSideScroll] +descriptors.get_wpid("100F").settings = [RegisterSmoothScroll, RegisterSideScroll] +descriptors.get_wpid("1013").settings = [RegisterSmoothScroll, RegisterSideScroll] +descriptors.get_wpid("1014").settings = [RegisterSmoothScroll, RegisterSideScroll] +descriptors.get_wpid("1017").settings = [RegisterSmoothScroll, RegisterSideScroll] +descriptors.get_wpid("1023").settings = [RegisterSmoothScroll, RegisterSideScroll] +descriptors.get_wpid("4004").settings = [_PerformanceMXDpi, RegisterSmoothScroll, RegisterSideScroll] +descriptors.get_wpid("101A").settings = [_PerformanceMXDpi, RegisterSmoothScroll, RegisterSideScroll] +descriptors.get_wpid("101B").settings = [RegisterSmoothScroll, RegisterSideScroll] +descriptors.get_wpid("101D").settings = [RegisterSmoothScroll, RegisterSideScroll] +descriptors.get_wpid("101F").settings = [RegisterSideScroll] +descriptors.get_usbid(0xC06B).settings = [RegisterSmoothScroll, RegisterSideScroll] +descriptors.get_wpid("1025").settings = [RegisterSideScroll] +descriptors.get_wpid("102A").settings = [RegisterSmoothScroll, RegisterSideScroll] +descriptors.get_usbid(0xC048).settings = [_PerformanceMXDpi, RegisterSmoothScroll, RegisterSideScroll] +descriptors.get_usbid(0xC066).settings = [_PerformanceMXDpi, RegisterSmoothScroll, RegisterSideScroll] # ignore the capabilities part of the feature - all devices should be able to swap Fn state @@ -236,11 +212,11 @@ class K375sFnSwap(FnSwapVirtual): feature = _F.K375S_FN_INVERSION validator_options = {"true_value": b"\x01", "false_value": b"\x00", "read_skip_byte_count": 1} - class rw_class(_FeatureRW): + class rw_class(settings.FeatureRW): def find_current_host(self, device): if not self.prefix: response = device.feature_request(_F.HOSTS_INFO, 0x00) - self.prefix = response[3:4] if response else b"\xFF" + self.prefix = response[3:4] if response else b"\xff" def read(self, device, data_bytes=b""): self.find_current_host(device) @@ -259,37 +235,37 @@ class NewFnSwap(FnSwapVirtual): feature = _F.NEW_FN_INVERSION -class Backlight(_Setting): +class Backlight(settings.Setting): name = "backlight-qualitative" label = _("Backlight Timed") description = _("Set illumination time for keyboard.") feature = _F.BACKLIGHT - choices_universe = _NamedInts(Off=0, Varying=2, VeryShort=5, Short=10, Medium=20, Long=60, VeryLong=180) - validator_class = _ChoicesV + choices_universe = common.NamedInts(Off=0, Varying=2, VeryShort=5, Short=10, Medium=20, Long=60, VeryLong=180) + validator_class = settings.ChoicesValidator validator_options = {"choices": choices_universe} # MX Keys S requires some extra values, as in 11 02 0c1a 000dff000b000b003c00000000000000 # on/off options (from current) effect (FF-no change) level (from current) durations[6] (from current) -class Backlight2(_Setting): +class Backlight2(settings.Setting): name = "backlight" label = _("Backlight") description = _("Illumination level on keyboard. Changes made are only applied in Manual mode.") feature = _F.BACKLIGHT2 - choices_universe = _NamedInts(Disabled=0xFF, Enabled=0x00, Automatic=0x01, Manual=0x02) + choices_universe = common.NamedInts(Disabled=0xFF, Enabled=0x00, Automatic=0x01, Manual=0x02) min_version = 0 class rw_class: def __init__(self, feature): self.feature = feature - self.kind = _FeatureRW.kind + self.kind = settings.FeatureRW.kind def read(self, device): backlight = device.backlight if not backlight.enabled: - return b"\xFF" + return b"\xff" else: - return _int2bytes(backlight.mode, 1) + return common.int2bytes(backlight.mode, 1) def write(self, device, data_bytes): backlight = device.backlight @@ -299,11 +275,11 @@ def write(self, device, data_bytes): backlight.write() return True - class validator_class(_ChoicesV): + class validator_class(settings.ChoicesValidator): @classmethod def build(cls, setting_class, device): backlight = device.backlight - choices = _NamedInts() + choices = common.NamedInts() choices[0xFF] = _("Disabled") if backlight.auto_supported: choices[0x1] = _("Automatic") @@ -314,7 +290,7 @@ def build(cls, setting_class, device): return cls(choices=choices, byte_count=1) -class Backlight2Level(_Setting): +class Backlight2Level(settings.Setting): name = "backlight_level" label = _("Backlight Level") description = _("Illumination level on keyboard when in Manual mode.") @@ -324,19 +300,19 @@ class Backlight2Level(_Setting): class rw_class: def __init__(self, feature): self.feature = feature - self.kind = _FeatureRW.kind + self.kind = settings.FeatureRW.kind def read(self, device): backlight = device.backlight - return _int2bytes(backlight.level, 1) + return common.int2bytes(backlight.level, 1) def write(self, device, data_bytes): - if device.backlight.level != _bytes2int(data_bytes): - device.backlight.level = _bytes2int(data_bytes) + if device.backlight.level != common.bytes2int(data_bytes): + device.backlight.level = common.bytes2int(data_bytes) device.backlight.write() return True - class validator_class(_RangeV): + class validator_class(settings.RangeValidator): @classmethod def build(cls, setting_class, device): reply = device.feature_request(_F.BACKLIGHT2, 0x20) @@ -345,10 +321,10 @@ def build(cls, setting_class, device): return cls(min_value=0, max_value=reply[0] - 1, byte_count=1) -class Backlight2Duration(_Setting): +class Backlight2Duration(settings.Setting): feature = _F.BACKLIGHT2 min_version = 3 - validator_class = _RangeV + validator_class = settings.RangeValidator min_value = 1 max_value = 600 # 10 minutes - actual maximum is 2 hours validator_options = {"byte_count": 2} @@ -356,12 +332,12 @@ class Backlight2Duration(_Setting): class rw_class: def __init__(self, feature, field): self.feature = feature - self.kind = _FeatureRW.kind + self.kind = settings.FeatureRW.kind self.field = field def read(self, device): backlight = device.backlight - return _int2bytes(getattr(backlight, self.field) * 5, 2) # use seconds instead of 5-second units + return common.int2bytes(getattr(backlight, self.field) * 5, 2) # use seconds instead of 5-second units def write(self, device, data_bytes): backlight = device.backlight @@ -377,7 +353,7 @@ class Backlight2DurationHandsOut(Backlight2Duration): label = _("Backlight Delay Hands Out") description = _("Delay in seconds until backlight fades out with hands away from keyboard.") feature = _F.BACKLIGHT2 - validator_class = _RangeV + validator_class = settings.RangeValidator rw_options = {"field": "dho"} @@ -386,7 +362,7 @@ class Backlight2DurationHandsIn(Backlight2Duration): label = _("Backlight Delay Hands In") description = _("Delay in seconds until backlight fades out with hands near keyboard.") feature = _F.BACKLIGHT2 - validator_class = _RangeV + validator_class = settings.RangeValidator rw_options = {"field": "dhi"} @@ -395,23 +371,23 @@ class Backlight2DurationPowered(Backlight2Duration): label = _("Backlight Delay Powered") description = _("Delay in seconds until backlight fades out with external power.") feature = _F.BACKLIGHT2 - validator_class = _RangeV + validator_class = settings.RangeValidator rw_options = {"field": "dpow"} -class Backlight3(_Setting): +class Backlight3(settings.Setting): name = "backlight-timed" label = _("Backlight (Seconds)") description = _("Set illumination time for keyboard.") feature = _F.BACKLIGHT3 rw_options = {"read_fnid": 0x10, "write_fnid": 0x20, "suffix": b"\x09"} - validator_class = _RangeV + validator_class = settings.RangeValidator min_value = 0 max_value = 1000 validator_options = {"byte_count": 2} -class HiResScroll(_Setting): +class HiResScroll(settings.Setting): name = "hi-res-scroll" label = _("Scroll Wheel High Resolution") description = ( @@ -422,7 +398,7 @@ class HiResScroll(_Setting): feature = _F.HI_RES_SCROLLING -class LowresMode(_Setting): +class LowresMode(settings.Setting): name = "lowres-scroll-mode" label = _("Scroll Wheel Diversion") description = _( @@ -431,7 +407,7 @@ class LowresMode(_Setting): feature = _F.LOWRES_WHEEL -class HiresSmoothInvert(_Setting): +class HiresSmoothInvert(settings.Setting): name = "hires-smooth-invert" label = _("Scroll Wheel Direction") description = _("Invert direction for vertical scroll with wheel.") @@ -440,7 +416,7 @@ class HiresSmoothInvert(_Setting): validator_options = {"true_value": 0x04, "mask": 0x04} -class HiresSmoothResolution(_Setting): +class HiresSmoothResolution(settings.Setting): name = "hires-smooth-resolution" label = _("Scroll Wheel Resolution") description = ( @@ -453,7 +429,7 @@ class HiresSmoothResolution(_Setting): validator_options = {"true_value": 0x02, "mask": 0x02} -class HiresMode(_Setting): +class HiresMode(settings.Setting): name = "hires-scroll-mode" label = _("Scroll Wheel Diversion") description = _( @@ -464,18 +440,18 @@ class HiresMode(_Setting): validator_options = {"true_value": 0x01, "mask": 0x01} -class PointerSpeed(_Setting): +class PointerSpeed(settings.Setting): name = "pointer_speed" label = _("Sensitivity (Pointer Speed)") description = _("Speed multiplier for mouse (256 is normal multiplier).") feature = _F.POINTER_SPEED - validator_class = _RangeV + validator_class = settings.RangeValidator min_value = 0x002E max_value = 0x01FF validator_options = {"byte_count": 2} -class ThumbMode(_Setting): +class ThumbMode(settings.Setting): name = "thumb-scroll-mode" label = _("Thumb Wheel Diversion") description = _( @@ -486,7 +462,7 @@ class ThumbMode(_Setting): validator_options = {"true_value": b"\x01\x00", "false_value": b"\x00\x00", "mask": b"\x01\x00"} -class ThumbInvert(_Setting): +class ThumbInvert(settings.Setting): name = "thumb-scroll-invert" label = _("Thumb Wheel Direction") description = _("Invert thumb wheel scroll direction.") @@ -507,21 +483,21 @@ def profile_change(device, profile_sector): break -class OnboardProfiles(_Setting): +class OnboardProfiles(settings.Setting): name = "onboard_profiles" label = _("Onboard Profiles") description = _("Enable an onboard profile, which controls report rate, sensitivity, and button actions") feature = _F.ONBOARD_PROFILES - choices_universe = _NamedInts(Disabled=0) + choices_universe = common.NamedInts(Disabled=0) for i in range(1, 16): choices_universe[i] = f"Profile {i}" choices_universe[i + 0x100] = f"Read-Only Profile {i}" - validator_class = _ChoicesV + validator_class = settings.ChoicesValidator class rw_class: def __init__(self, feature): self.feature = feature - self.kind = _FeatureRW.kind + self.kind = settings.FeatureRW.kind def read(self, device): enabled = device.feature_request(_F.ONBOARD_PROFILES, 0x20)[0] @@ -537,10 +513,10 @@ def write(self, device, data_bytes): else: device.feature_request(_F.ONBOARD_PROFILES, 0x10, b"\x01") result = device.feature_request(_F.ONBOARD_PROFILES, 0x30, data_bytes) - profile_change(device, _bytes2int(data_bytes)) + profile_change(device, common.bytes2int(data_bytes)) return result - class validator_class(_ChoicesV): + class validator_class(settings.ChoicesValidator): @classmethod def build(cls, setting_class, device): headers = hidpp20.OnboardProfiles.get_profile_headers(device) @@ -549,10 +525,10 @@ def build(cls, setting_class, device): for sector, enabled in headers: if enabled and setting_class.choices_universe[sector]: profiles_list.append(setting_class.choices_universe[sector]) - return cls(choices=_NamedInts.list(profiles_list), byte_count=2) if len(profiles_list) > 1 else None + return cls(choices=common.NamedInts.list(profiles_list), byte_count=2) if len(profiles_list) > 1 else None -class ReportRate(_Setting): +class ReportRate(settings.Setting): name = "report_rate" label = _("Report Rate") description = ( @@ -560,7 +536,7 @@ class ReportRate(_Setting): ) feature = _F.REPORT_RATE rw_options = {"read_fnid": 0x10, "write_fnid": 0x20} - choices_universe = _NamedInts() + choices_universe = common.NamedInts() choices_universe[1] = "1ms" choices_universe[2] = "2ms" choices_universe[3] = "3ms" @@ -570,7 +546,7 @@ class ReportRate(_Setting): choices_universe[7] = "7ms" choices_universe[8] = "8ms" - class validator_class(_ChoicesV): + class validator_class(settings.ChoicesValidator): @classmethod def build(cls, setting_class, device): # if device.wpid == '408E': @@ -578,14 +554,14 @@ def build(cls, setting_class, device): reply = device.feature_request(_F.REPORT_RATE, 0x00) assert reply, "Oops, report rate choices cannot be retrieved!" rate_list = [] - rate_flags = _bytes2int(reply[0:1]) + rate_flags = common.bytes2int(reply[0:1]) for i in range(0, 8): if (rate_flags >> i) & 0x01: rate_list.append(setting_class.choices_universe[i + 1]) - return cls(choices=_NamedInts.list(rate_list), byte_count=1) if rate_list else None + return cls(choices=common.NamedInts.list(rate_list), byte_count=1) if rate_list else None -class ExtendedReportRate(_Setting): +class ExtendedReportRate(settings.Setting): name = "report_rate_extended" label = _("Report Rate") description = ( @@ -593,7 +569,7 @@ class ExtendedReportRate(_Setting): ) feature = _F.EXTENDED_ADJUSTABLE_REPORT_RATE rw_options = {"read_fnid": 0x20, "write_fnid": 0x30} - choices_universe = _NamedInts() + choices_universe = common.NamedInts() choices_universe[0] = "8ms" choices_universe[1] = "4ms" choices_universe[2] = "2ms" @@ -602,20 +578,20 @@ class ExtendedReportRate(_Setting): choices_universe[5] = "250us" choices_universe[6] = "125us" - class validator_class(_ChoicesV): + class validator_class(settings.ChoicesValidator): @classmethod def build(cls, setting_class, device): reply = device.feature_request(_F.EXTENDED_ADJUSTABLE_REPORT_RATE, 0x10) assert reply, "Oops, report rate choices cannot be retrieved!" rate_list = [] - rate_flags = _bytes2int(reply[0:2]) + rate_flags = common.bytes2int(reply[0:2]) for i in range(0, 6): if rate_flags & (0x01 << i): rate_list.append(setting_class.choices_universe[i]) - return cls(choices=_NamedInts.list(rate_list), byte_count=1) if rate_list else None + return cls(choices=common.NamedInts.list(rate_list), byte_count=1) if rate_list else None -class DivertCrown(_Setting): +class DivertCrown(settings.Setting): name = "divert-crown" label = _("Divert crown events") description = _("Make crown send CROWN HID++ notifications (which trigger Solaar rules but are otherwise ignored).") @@ -624,7 +600,7 @@ class DivertCrown(_Setting): validator_options = {"true_value": 0x02, "false_value": 0x01, "mask": 0xFF} -class CrownSmooth(_Setting): +class CrownSmooth(settings.Setting): name = "crown-smooth" label = _("Crown smooth scroll") description = _("Set crown smooth scroll") @@ -633,14 +609,14 @@ class CrownSmooth(_Setting): validator_options = {"true_value": 0x01, "false_value": 0x02, "read_skip_byte_count": 1, "write_prefix_bytes": b"\x00"} -class DivertGkeys(_Setting): +class DivertGkeys(settings.Setting): name = "divert-gkeys" label = _("Divert G and M Keys") description = _("Make G and M keys send HID++ notifications (which trigger Solaar rules but are otherwise ignored).") feature = _F.GKEY validator_options = {"true_value": 0x01, "false_value": 0x00, "mask": 0xFF} - class rw_class(_FeatureRW): + class rw_class(settings.FeatureRW): def __init__(self, feature): super().__init__(feature, write_fnid=0x20) @@ -648,17 +624,17 @@ def read(self, device): # no way to read, so just assume not diverted return b"\x00" -class ScrollRatchet(_Setting): +class ScrollRatchet(settings.Setting): name = "scroll-ratchet" label = _("Scroll Wheel Ratcheted") description = _("Switch the mouse wheel between speed-controlled ratcheting and always freespin.") feature = _F.SMART_SHIFT - choices_universe = _NamedInts(**{_("Freespinning"): 1, _("Ratcheted"): 2}) - validator_class = _ChoicesV + choices_universe = common.NamedInts(**{_("Freespinning"): 1, _("Ratcheted"): 2}) + validator_class = settings.ChoicesValidator validator_options = {"choices": choices_universe} -class SmartShift(_Setting): +class SmartShift(settings.Setting): name = "smart-shift" label = _("Scroll Wheel Ratchet Speed") description = _( @@ -668,7 +644,7 @@ class SmartShift(_Setting): feature = _F.SMART_SHIFT rw_options = {"read_fnid": 0x00, "write_fnid": 0x10} - class rw_class(_FeatureRW): + class rw_class(settings.FeatureRW): MIN_VALUE = 1 MAX_VALUE = 50 @@ -677,27 +653,27 @@ def __init__(self, feature, read_fnid, write_fnid): def read(self, device): value = super().read(device) - if _bytes2int(value[0:1]) == 1: + if common.bytes2int(value[0:1]) == 1: # Mode = Freespin, map to minimum - return _int2bytes(self.MIN_VALUE, count=1) + return common.int2bytes(self.MIN_VALUE, count=1) else: # Mode = smart shift, map to the value, capped at maximum - threshold = min(_bytes2int(value[1:2]), self.MAX_VALUE) - return _int2bytes(threshold, count=1) + threshold = min(common.bytes2int(value[1:2]), self.MAX_VALUE) + return common.int2bytes(threshold, count=1) def write(self, device, data_bytes): - threshold = _bytes2int(data_bytes) + threshold = common.bytes2int(data_bytes) # Freespin at minimum mode = 0 # 1 if threshold <= self.MIN_VALUE else 2 # Ratchet at maximum if threshold >= self.MAX_VALUE: threshold = 255 - data = _int2bytes(mode, count=1) + _int2bytes(max(0, threshold), count=1) + data = common.int2bytes(mode, count=1) + common.int2bytes(max(0, threshold), count=1) return super().write(device, data) min_value = rw_class.MIN_VALUE max_value = rw_class.MAX_VALUE - validator_class = _RangeV + validator_class = settings.RangeValidator class SmartShiftEnhanced(SmartShift): @@ -709,7 +685,7 @@ class SmartShiftEnhanced(SmartShift): # each choice value is a NamedInt with the string from a task (to be shown to the user) # and the integer being the control number for that task (to be written to the device) # Solaar only remaps keys (controlled by key gmask and group), not other key reprogramming -class ReprogrammableKeys(_Settings): +class ReprogrammableKeys(settings.Settings): name = "reprogrammable-keys" label = _("Key/Button Actions") description = ( @@ -720,26 +696,26 @@ class ReprogrammableKeys(_Settings): + _("Changing important actions (such as for the left mouse button) can result in an unusable system.") ) feature = _F.REPROG_CONTROLS_V4 - keys_universe = _special_keys.CONTROL - choices_universe = _special_keys.CONTROL + keys_universe = special_keys.CONTROL + choices_universe = special_keys.CONTROL class rw_class: def __init__(self, feature): self.feature = feature - self.kind = _FeatureRW.kind + self.kind = settings.FeatureRW.kind def read(self, device, key): key_index = device.keys.index(key) key_struct = device.keys[key_index] - return b"\x00\x00" + _int2bytes(int(key_struct.mapped_to), 2) + return b"\x00\x00" + common.int2bytes(int(key_struct.mapped_to), 2) def write(self, device, key, data_bytes): key_index = device.keys.index(key) key_struct = device.keys[key_index] - key_struct.remap(_special_keys.CONTROL[_bytes2int(data_bytes)]) + key_struct.remap(special_keys.CONTROL[common.bytes2int(data_bytes)]) return True - class validator_class(_ChoicesMapV): + class validator_class(settings.ChoicesMapValidator): @classmethod def build(cls, setting_class, device): choices = {} @@ -751,7 +727,7 @@ def build(cls, setting_class, device): return cls(choices, key_byte_count=2, byte_count=2, extra_default=0) if choices else None -class DpiSlidingXY(_RawXYProcessing): +class DpiSlidingXY(settings.RawXYProcessing): def activate_action(self): self.dpiSetting = next(filter(lambda s: s.name == "dpi" or s.name == "dpi_extended", self.device.settings), None) self.dpiChoices = list(self.dpiSetting.choices) @@ -769,12 +745,12 @@ def setNewDpi(self, newDpiIdx): self.device.setting_callback(self.device, type(self.dpiSetting), [newDpi]) def displayNewDpi(self, newDpiIdx): - if _notify.available: + if notify.available: selected_dpi = self.dpiChoices[newDpiIdx] min_dpi = self.dpiChoices[0] max_dpi = self.dpiChoices[-1] reason = f"DPI {selected_dpi} [min {min_dpi}, max {max_dpi}]" - _notify.show(self.device, reason) + notify.show(self.device, reason) def press_action(self, key): # start tracking self.starting = True @@ -814,7 +790,7 @@ def move_action(self, dx, dy): self.displayNewDpi(newMovingDpiIdx) -class MouseGesturesXY(_RawXYProcessing): +class MouseGesturesXY(settings.RawXYProcessing): def activate_action(self): self.dpiSetting = next(filter(lambda s: s.name == "dpi" or s.name == "dpi_extended", self.device.settings), None) self.fsmState = "idle" @@ -839,14 +815,14 @@ def release_action(self): self.push_mouse_event() if logger.isEnabledFor(logging.INFO): logger.info("mouse gesture notification %s", self.data) - payload = _pack("!" + (len(self.data) * "h"), *self.data) - notification = _HIDPP_Notification(0, 0, 0, 0, payload) - _process_notification(self.device, notification, _F.MOUSE_GESTURE) + payload = struct.pack("!" + (len(self.data) * "h"), *self.data) + notification = base._HIDPP_Notification(0, 0, 0, 0, payload) + diversion.process_notification(self.device, notification, _F.MOUSE_GESTURE) self.fsmState = "idle" def move_action(self, dx, dy): if self.fsmState == "pressed": - now = _time() * 1000 # _time_ns() / 1e6 + now = time() * 1000 # time_ns() / 1e6 if self.device.features.get_feature_version(_F.REPROG_CONTROLS_V4) >= 5 and self.starting: self.starting = False # hack to ignore strange first movement report from MX Master 3S return @@ -863,7 +839,7 @@ def key_action(self, key): self.push_mouse_event() self.data.append(1) self.data.append(key) - self.lastEv = _time() * 1000 # _time_ns() / 1e6 + self.lastEv = time() * 1000 # time_ns() / 1e6 if logger.isEnabledFor(logging.DEBUG): logger.debug("mouse gesture key event %d %s", key, self.data) @@ -881,20 +857,20 @@ def push_mouse_event(self): logger.debug("mouse gesture move event %d %d %s", x, y, self.data) -class DivertKeys(_Settings): +class DivertKeys(settings.Settings): name = "divert-keys" label = _("Key/Button Diversion") description = _("Make the key or button send HID++ notifications (Diverted) or initiate Mouse Gestures or Sliding DPI") feature = _F.REPROG_CONTROLS_V4 - keys_universe = _special_keys.CONTROL - choices_universe = _NamedInts(**{_("Regular"): 0, _("Diverted"): 1, _("Mouse Gestures"): 2, _("Sliding DPI"): 3}) - choices_gesture = _NamedInts(**{_("Regular"): 0, _("Diverted"): 1, _("Mouse Gestures"): 2}) - choices_divert = _NamedInts(**{_("Regular"): 0, _("Diverted"): 1}) + keys_universe = special_keys.CONTROL + choices_universe = common.NamedInts(**{_("Regular"): 0, _("Diverted"): 1, _("Mouse Gestures"): 2, _("Sliding DPI"): 3}) + choices_gesture = common.NamedInts(**{_("Regular"): 0, _("Diverted"): 1, _("Mouse Gestures"): 2}) + choices_divert = common.NamedInts(**{_("Regular"): 0, _("Diverted"): 1}) class rw_class: def __init__(self, feature): self.feature = feature - self.kind = _FeatureRW.kind + self.kind = settings.FeatureRW.kind def read(self, device, key): key_index = device.keys.index(key) @@ -904,10 +880,10 @@ def read(self, device, key): def write(self, device, key, data_bytes): key_index = device.keys.index(key) key_struct = device.keys[key_index] - key_struct.set_diverted(_bytes2int(data_bytes) != 0) # not regular + key_struct.set_diverted(common.bytes2int(data_bytes) != 0) # not regular return True - class validator_class(_ChoicesMapV): + class validator_class(settings.ChoicesMapValidator): def __init__(self, choices, key_byte_count=2, byte_count=1, mask=0x01): super().__init__(choices, key_byte_count, byte_count, mask) @@ -958,12 +934,12 @@ def produce_dpi_list(feature, function, ignore, device, direction): dpi_list = [] i = 0 while i < len(dpi_bytes): - val = _bytes2int(dpi_bytes[i : i + 2]) + val = common.bytes2int(dpi_bytes[i : i + 2]) if val == 0: break if val >> 13 == 0b111: step = val & 0x1FFF - last = _bytes2int(dpi_bytes[i + 2 : i + 4]) + last = common.bytes2int(dpi_bytes[i + 2 : i + 4]) assert len(dpi_list) > 0 and last > dpi_list[-1], f"Invalid DPI list item: {val!r}" dpi_list += range(dpi_list[-1] + step, last + 1, step) i += 4 @@ -973,44 +949,46 @@ def produce_dpi_list(feature, function, ignore, device, direction): return dpi_list -class AdjustableDpi(_Setting): +class AdjustableDpi(settings.Setting): name = "dpi" label = _("Sensitivity (DPI)") description = _("Mouse movement sensitivity") feature = _F.ADJUSTABLE_DPI rw_options = {"read_fnid": 0x20, "write_fnid": 0x30} - choices_universe = _NamedInts.range(100, 4000, str, 50) + choices_universe = common.NamedInts.range(100, 4000, str, 50) - class validator_class(_ChoicesV): + class validator_class(settings.ChoicesValidator): @classmethod def build(cls, setting_class, device): dpilist = produce_dpi_list(setting_class.feature, 0x10, 1, device, 0) - setting = cls(choices=_NamedInts.list(dpilist), byte_count=2, write_prefix_bytes=b"\x00") if dpilist else None + setting = ( + cls(choices=common.NamedInts.list(dpilist), byte_count=2, write_prefix_bytes=b"\x00") if dpilist else None + ) setting.dpilist = dpilist return setting def validate_read(self, reply_bytes): # special validator to use default DPI if needed - reply_value = _bytes2int(reply_bytes[1:3]) + reply_value = common.bytes2int(reply_bytes[1:3]) if reply_value == 0: # use default value instead - reply_value = _bytes2int(reply_bytes[3:5]) + reply_value = common.bytes2int(reply_bytes[3:5]) valid_value = self.choices[reply_value] assert valid_value is not None, f"{self.__class__.__name__}: failed to validate read value {reply_value:02X}" return valid_value -class ExtendedAdjustableDpi(_Setting): +class ExtendedAdjustableDpi(settings.Setting): # the extended version allows for two dimensions, longer dpi descriptions, but still assume only one sensor name = "dpi_extended" label = _("Sensitivity (DPI)") description = _("Mouse movement sensitivity") + "\n" + _("May need Onboard Profiles set to Disable to be effective.") feature = _F.EXTENDED_ADJUSTABLE_DPI rw_options = {"read_fnid": 0x50, "write_fnid": 0x60} - keys_universe = _NamedInts(X=0, Y=1, LOD=2) - choices_universe = _NamedInts.range(100, 4000, str, 50) + keys_universe = common.NamedInts(X=0, Y=1, LOD=2) + choices_universe = common.NamedInts.range(100, 4000, str, 50) choices_universe[0] = "LOW" choices_universe[1] = "MEDIUM" choices_universe[2] = "HIGH" - keys = _NamedInts(X=0, Y=1, LOD=2) + keys = common.NamedInts(X=0, Y=1, LOD=2) def write_key_value(self, key, value, save=True): if isinstance(self._value, dict): @@ -1020,7 +998,7 @@ def write_key_value(self, key, value, save=True): result = self.write(self._value, save) return result[key] if isinstance(result, dict) else result - class validator_class(_ChoicesMapV): + class validator_class(settings.ChoicesMapValidator): @classmethod def build(cls, setting_class, device): reply = device.feature_request(setting_class.feature, 0x10, 0x00) @@ -1028,12 +1006,12 @@ def build(cls, setting_class, device): lod = bool(reply[2] & 0x02) choices_map = {} dpilist_x = produce_dpi_list(setting_class.feature, 0x20, 3, device, 0) - choices_map[setting_class.keys["X"]] = _NamedInts.list(dpilist_x) + choices_map[setting_class.keys["X"]] = common.NamedInts.list(dpilist_x) if y: dpilist_y = produce_dpi_list(setting_class.feature, 0x20, 3, device, 1) - choices_map[setting_class.keys["Y"]] = _NamedInts.list(dpilist_y) + choices_map[setting_class.keys["Y"]] = common.NamedInts.list(dpilist_y) if lod: - choices_map[setting_class.keys["LOD"]] = _NamedInts(LOW=0, MEDIUM=1, HIGH=2) + choices_map[setting_class.keys["LOD"]] = common.NamedInts(LOW=0, MEDIUM=1, HIGH=2) validator = cls(choices_map=choices_map, byte_count=2, write_prefix_bytes=b"\x00") validator.y = y validator.lod = lod @@ -1041,11 +1019,11 @@ def build(cls, setting_class, device): return validator def validate_read(self, reply_bytes): # special validator to read entire setting - dpi_x = _bytes2int(reply_bytes[3:5]) if reply_bytes[1:3] == 0 else _bytes2int(reply_bytes[1:3]) + dpi_x = common.bytes2int(reply_bytes[3:5]) if reply_bytes[1:3] == 0 else common.bytes2int(reply_bytes[1:3]) assert dpi_x in self.choices[0], f"{self.__class__.__name__}: failed to validate dpi_x value {dpi_x:04X}" value = {self.keys["X"]: dpi_x} if self.y: - dpi_y = _bytes2int(reply_bytes[7:9]) if reply_bytes[5:7] == 0 else _bytes2int(reply_bytes[5:7]) + dpi_y = common.bytes2int(reply_bytes[7:9]) if reply_bytes[5:7] == 0 else common.bytes2int(reply_bytes[5:7]) assert dpi_y in self.choices[1], f"{self.__class__.__name__}: failed to validate dpi_y value {dpi_y:04X}" value[self.keys["Y"]] = dpi_y if self.lod: @@ -1058,23 +1036,23 @@ def prepare_write(self, new_value, current_value=None): # special preparer to w data_bytes = self._write_prefix_bytes if new_value[self.keys["X"]] not in self.choices[self.keys["X"]]: raise ValueError(f"invalid value {new_value!r}") - data_bytes += _int2bytes(new_value[0], 2) + data_bytes += common.int2bytes(new_value[0], 2) if self.y: if new_value[self.keys["Y"]] not in self.choices[self.keys["Y"]]: raise ValueError(f"invalid value {new_value!r}") - data_bytes += _int2bytes(new_value[self.keys["Y"]], 2) + data_bytes += common.int2bytes(new_value[self.keys["Y"]], 2) else: data_bytes += b"\x00\x00" if self.lod: if new_value[self.keys["LOD"]] not in self.choices[self.keys["LOD"]]: raise ValueError(f"invalid value {new_value!r}") - data_bytes += _int2bytes(new_value[self.keys["LOD"]], 1) + data_bytes += common.int2bytes(new_value[self.keys["LOD"]], 1) else: data_bytes += b"\x00" return data_bytes -class SpeedChange(_Setting): +class SpeedChange(settings.Setting): """Implements the ability to switch Sensitivity by clicking on the DPI_Change button.""" name = "speed-change" @@ -1083,12 +1061,12 @@ class SpeedChange(_Setting): "Switch the current sensitivity and the remembered sensitivity when the key or button is pressed.\n" "If there is no remembered sensitivity, just remember the current sensitivity" ) - choices_universe = _special_keys.CONTROL - choices_extra = _NamedInt(0, _("Off")) + choices_universe = special_keys.CONTROL + choices_extra = common.NamedInt(0, _("Off")) feature = _F.POINTER_SPEED rw_options = {"name": "speed change"} - class rw_class(_ActionSettingRW): + class rw_class(settings.ActionSettingRW): def press_action(self): # switch sensitivity currentSpeed = self.device.persister.get("pointer_speed", None) if self.device.persister else None newSpeed = self.device.persister.get("_speed-change", None) if self.device.persister else None @@ -1103,40 +1081,40 @@ def press_action(self): # switch sensitivity if self.device.persister: self.device.persister["_speed-change"] = currentSpeed - class validator_class(_ChoicesV): + class validator_class(settings.ChoicesValidator): @classmethod def build(cls, setting_class, device): - key_index = device.keys.index(_special_keys.CONTROL.DPI_Change) + key_index = device.keys.index(special_keys.CONTROL.DPI_Change) key = device.keys[key_index] if key_index is not None else None if key is not None and "divertable" in key.flags: keys = [setting_class.choices_extra, key.key] - return cls(choices=_NamedInts.list(keys), byte_count=2) + return cls(choices=common.NamedInts.list(keys), byte_count=2) -class DisableKeyboardKeys(_BitFieldSetting): +class DisableKeyboardKeys(settings.BitFieldSetting): name = "disable-keyboard-keys" label = _("Disable keys") description = _("Disable specific keyboard keys.") feature = _F.KEYBOARD_DISABLE_KEYS rw_options = {"read_fnid": 0x10, "write_fnid": 0x20} - _labels = {k: (None, _("Disables the %s key.") % k) for k in _DKEY} - choices_universe = _DKEY + _labels = {k: (None, _("Disables the %s key.") % k) for k in special_keys.DISABLE} + choices_universe = special_keys.DISABLE - class validator_class(_BitFieldV): + class validator_class(settings.BitFieldValidator): @classmethod def build(cls, setting_class, device): mask = device.feature_request(_F.KEYBOARD_DISABLE_KEYS, 0x00)[0] - options = [_DKEY[1 << i] for i in range(8) if mask & (1 << i)] + options = [special_keys.DISABLE[1 << i] for i in range(8) if mask & (1 << i)] return cls(options) if options else None -class Multiplatform(_Setting): +class Multiplatform(settings.Setting): name = "multiplatform" label = _("Set OS") description = _("Change keys to match OS.") feature = _F.MULTIPLATFORM rw_options = {"read_fnid": 0x00, "write_fnid": 0x30} - choices_universe = _NamedInts(**{"OS " + str(i + 1): i for i in range(8)}) + choices_universe = common.NamedInts(**{"OS " + str(i + 1): i for i in range(8)}) # multiplatform OS bits OSS = [ @@ -1154,7 +1132,7 @@ class Multiplatform(_Setting): # the problem here is how to construct the right values for the rules Set GUI, # as, for example, the integer value for 'Windows' can be different on different devices - class validator_class(_ChoicesV): + class validator_class(settings.ChoicesValidator): @classmethod def build(cls, setting_class, device): def _str_os_versions(low, high): @@ -1170,15 +1148,15 @@ def _str_os_version(version): infos = device.feature_request(_F.MULTIPLATFORM) assert infos, "Oops, multiplatform count cannot be retrieved!" - flags, _ignore, num_descriptors = _unpack("!BBB", infos[:3]) + flags, _ignore, num_descriptors = struct.unpack("!BBB", infos[:3]) if not (flags & 0x02): # can't set platform so don't create setting return [] descriptors = [] for index in range(0, num_descriptors): descriptor = device.feature_request(_F.MULTIPLATFORM, 0x10, index) - platform, _ignore, os_flags, low, high = _unpack("!BBHHH", descriptor[:8]) + platform, _ignore, os_flags, low, high = struct.unpack("!BBHHH", descriptor[:8]) descriptors.append((platform, os_flags, low, high)) - choices = _NamedInts() + choices = common.NamedInts() for os_name, os_bit in setting_class.OSS: for platform, os_flags, low, high in descriptors: os = os_name + _str_os_versions(low, high) @@ -1187,39 +1165,39 @@ def _str_os_version(version): return cls(choices=choices, read_skip_byte_count=6, write_prefix_bytes=b"\xff") if choices else None -class DualPlatform(_Setting): +class DualPlatform(settings.Setting): name = "multiplatform" label = _("Set OS") description = _("Change keys to match OS.") - choices_universe = _NamedInts() + choices_universe = common.NamedInts() choices_universe[0x00] = "iOS, MacOS" choices_universe[0x01] = "Android, Windows" feature = _F.DUALPLATFORM rw_options = {"read_fnid": 0x00, "write_fnid": 0x20} - validator_class = _ChoicesV + validator_class = settings.ChoicesValidator validator_options = {"choices": choices_universe} -class ChangeHost(_Setting): +class ChangeHost(settings.Setting): name = "change-host" label = _("Change Host") description = _("Switch connection to a different host") persist = False # persisting this setting is harmful feature = _F.CHANGE_HOST rw_options = {"read_fnid": 0x00, "write_fnid": 0x10, "no_reply": True} - choices_universe = _NamedInts(**{"Host " + str(i + 1): i for i in range(3)}) + choices_universe = common.NamedInts(**{"Host " + str(i + 1): i for i in range(3)}) - class validator_class(_ChoicesV): + class validator_class(settings.ChoicesValidator): @classmethod def build(cls, setting_class, device): infos = device.feature_request(_F.CHANGE_HOST) assert infos, "Oops, host count cannot be retrieved!" - numHosts, currentHost = _unpack("!BB", infos[:2]) + numHosts, currentHost = struct.unpack("!BB", infos[:2]) hostNames = _hidpp20.get_host_names(device) hostNames = hostNames if hostNames is not None else {} if currentHost not in hostNames or hostNames[currentHost][1] == "": - hostNames[currentHost] = (True, _socket.gethostname().partition(".")[0]) - choices = _NamedInts() + hostNames[currentHost] = (True, socket.gethostname().partition(".")[0]) + choices = common.NamedInts() for host in range(0, numHosts): paired, hostName = hostNames.get(host, (True, "")) choices[host] = str(host + 1) + ":" + hostName if hostName else str(host + 1) @@ -1311,41 +1289,41 @@ def build(cls, setting_class, device): } -class Gesture2Gestures(_BitFieldOMSetting): +class Gesture2Gestures(settings.BitFieldWithOffsetAndMaskSetting): name = "gesture2-gestures" label = _("Gestures") description = _("Tweak the mouse/touchpad behaviour.") feature = _F.GESTURE_2 rw_options = {"read_fnid": 0x10, "write_fnid": 0x20} validator_options = {"om_method": hidpp20.Gesture.enable_offset_mask} - choices_universe = _hidpp20_constants.GESTURE + choices_universe = hidpp20_constants.GESTURE _labels = _GESTURE2_GESTURES_LABELS - class validator_class(_BitFieldOMV): + class validator_class(settings.BitFieldWithOffsetAndMaskValidator): @classmethod def build(cls, setting_class, device, om_method=None): options = [g for g in device.gestures.gestures.values() if g.can_be_enabled or g.default_enabled] return cls(options, om_method=om_method) if options else None -class Gesture2Divert(_BitFieldOMSetting): +class Gesture2Divert(settings.BitFieldWithOffsetAndMaskSetting): name = "gesture2-divert" label = _("Gestures Diversion") description = _("Divert mouse/touchpad gestures.") feature = _F.GESTURE_2 rw_options = {"read_fnid": 0x30, "write_fnid": 0x40} validator_options = {"om_method": hidpp20.Gesture.diversion_offset_mask} - choices_universe = _hidpp20_constants.GESTURE + choices_universe = hidpp20_constants.GESTURE _labels = _GESTURE2_GESTURES_LABELS - class validator_class(_BitFieldOMV): + class validator_class(settings.BitFieldWithOffsetAndMaskValidator): @classmethod def build(cls, setting_class, device, om_method=None): options = [g for g in device.gestures.gestures.values() if g.can_be_diverted] return cls(options, om_method=om_method) if options else None -class Gesture2Params(_LongSettings): +class Gesture2Params(settings.LongSettings): name = "gesture2-params" label = _("Gesture params") description = _("Change numerical parameters of a mouse/touchpad.") @@ -1359,7 +1337,7 @@ class Gesture2Params(_LongSettings): _labels = _GESTURE2_PARAMS_LABELS _labels_sub = _GESTURE2_PARAMS_LABELS_SUB - class validator_class(_MultipleRangeV): + class validator_class(settings.MultipleRangeValidator): @classmethod def build(cls, setting_class, device): params = _hidpp20.get_gestures(device).params.values() @@ -1370,7 +1348,7 @@ def build(cls, setting_class, device): return cls(items, sub_items) -class MKeyLEDs(_BitFieldSetting): +class MKeyLEDs(settings.BitFieldSetting): name = "m-key-leds" label = _("M-Key LEDs") description = ( @@ -1381,19 +1359,19 @@ class MKeyLEDs(_BitFieldSetting): + _("May need G Keys diverted to be effective.") ) feature = _F.MKEYS - choices_universe = _NamedInts() + choices_universe = common.NamedInts() for i in range(8): choices_universe[1 << i] = "M" + str(i + 1) _labels = {k: (None, _("Lights up the %s key.") % k) for k in choices_universe} - class rw_class(_FeatureRW): + class rw_class(settings.FeatureRW): def __init__(self, feature): super().__init__(feature, write_fnid=0x10) def read(self, device): # no way to read, so just assume off return b"\x00" - class validator_class(_BitFieldV): + class validator_class(settings.BitFieldValidator): @classmethod def build(cls, setting_class, device): number = device.feature_request(setting_class.feature, 0x00)[0] @@ -1401,7 +1379,7 @@ def build(cls, setting_class, device): return cls(options) if options else None -class MRKeyLED(_Setting): +class MRKeyLED(settings.Setting): name = "mr-key-led" label = _("MR-Key LED") description = ( @@ -1413,7 +1391,7 @@ class MRKeyLED(_Setting): ) feature = _F.MR - class rw_class(_FeatureRW): + class rw_class(settings.FeatureRW): def __init__(self, feature): super().__init__(feature, write_fnid=0x00) @@ -1424,7 +1402,7 @@ def read(self, device): # no way to read, so just assume off ## Only implemented for devices that can produce Key and Consumer Codes (e.g., Craft) ## and devices that can produce Key, Mouse, and Horizontal Scroll (e.g., M720) ## Only interested in current host, so use 0xFF for it -class PersistentRemappableAction(_Settings): +class PersistentRemappableAction(settings.Settings): name = "persistent-remappable-keys" label = _("Persistent Key/Button Mapping") description = ( @@ -1434,13 +1412,13 @@ class PersistentRemappableAction(_Settings): ) persist = False # This setting is persistent in the device so no need to persist it here feature = _F.PERSISTENT_REMAPPABLE_ACTION - keys_universe = _special_keys.CONTROL - choices_universe = _special_keys.KEYS + keys_universe = special_keys.CONTROL + choices_universe = special_keys.KEYS class rw_class: def __init__(self, feature): self.feature = feature - self.kind = _FeatureRW.kind + self.kind = settings.FeatureRW.kind def read(self, device, key): ks = device.remap_keys[device.remap_keys.index(key)] @@ -1451,7 +1429,7 @@ def write(self, device, key, data_bytes): v = ks.remap(data_bytes) return v - class validator_class(_ChoicesMapV): + class validator_class(settings.ChoicesMapValidator): @classmethod def build(cls, setting_class, device): remap_keys = device.remap_keys @@ -1459,43 +1437,43 @@ def build(cls, setting_class, device): return None capabilities = device.remap_keys.capabilities if capabilities & 0x0041 == 0x0041: # Key and Consumer Codes - keys = _special_keys.KEYS_KEYS_CONSUMER + keys = special_keys.KEYS_KEYS_CONSUMER elif capabilities & 0x0023 == 0x0023: # Key, Mouse, and HScroll Codes - keys = _special_keys.KEYS_KEYS_MOUSE_HSCROLL + keys = special_keys.KEYS_KEYS_MOUSE_HSCROLL else: - if logger.isEnabledFor(_WARN): + if logger.isEnabledFor(logging.WARNING): logger.warning("%s: unimplemented Persistent Remappable capability %s", device.name, hex(capabilities)) return None choices = {} for k in remap_keys: if k is not None: - key = _special_keys.CONTROL[k.key] - choices[key] = keys # TO RECOVER FROM BAD VALUES use _special_keys.KEYS + key = special_keys.CONTROL[k.key] + choices[key] = keys # TO RECOVER FROM BAD VALUES use special_keys.KEYS return cls(choices, key_byte_count=2, byte_count=4) if choices else None def validate_read(self, reply_bytes, key): start = self._key_byte_count + self._read_skip_byte_count end = start + self._byte_count - reply_value = _bytes2int(reply_bytes[start:end]) & self.mask + reply_value = common.bytes2int(reply_bytes[start:end]) & self.mask # Craft keyboard has a value that isn't valid so fudge these values if reply_value not in self.choices[key]: - if logger.isEnabledFor(_WARN): + if logger.isEnabledFor(logging.WARNING): logger.warning("unusual persistent remappable action mapping %x: use Default", reply_value) - reply_value = _special_keys.KEYS_Default + reply_value = special_keys.KEYS_Default return reply_value -class Sidetone(_Setting): +class Sidetone(settings.Setting): name = "sidetone" label = _("Sidetone") description = _("Set sidetone level.") feature = _F.SIDETONE - validator_class = _RangeV + validator_class = settings.RangeValidator min_value = 0 max_value = 100 -class Equalizer(_RangeFieldSetting): +class Equalizer(settings.RangeFieldSetting): name = "equalizer" label = _("Equalizer") description = _("Set equalizer levels.") @@ -1503,18 +1481,18 @@ class Equalizer(_RangeFieldSetting): rw_options = {"read_fnid": 0x20, "write_fnid": 0x30, "read_prefix": b"\x00"} keys_universe = [] - class validator_class(_PackedRangeV): + class validator_class(settings.PackedRangeValidator): @classmethod def build(cls, setting_class, device): data = device.feature_request(_F.EQUALIZER, 0x00) if not data: return None - count, dbRange, _x, dbMin, dbMax = _unpack("!BBBBB", data[:5]) + count, dbRange, _x, dbMin, dbMax = struct.unpack("!BBBBB", data[:5]) if dbMin == 0: dbMin = -dbRange if dbMax == 0: dbMax = dbRange - map = _NamedInts() + map = common.NamedInts() for g in range((count + 6) // 7): freqs = device.feature_request(_F.EQUALIZER, 0x10, g * 7) for b in range(7): @@ -1524,25 +1502,25 @@ def build(cls, setting_class, device): return cls(map, min_value=dbMin, max_value=dbMax, count=count, write_prefix_bytes=b"\x02") -class ADCPower(_Setting): +class ADCPower(settings.Setting): name = "adc_power_management" label = _("Power Management") description = _("Power off in minutes (0 for never).") feature = _F.ADC_MEASUREMENT rw_options = {"read_fnid": 0x10, "write_fnid": 0x20} - validator_class = _RangeV + validator_class = settings.RangeValidator min_value = 0x00 max_value = 0xFF validator_options = {"byte_count": 1} -class BrightnessControl(_Setting): +class BrightnessControl(settings.Setting): name = "brightness_control" label = _("Brightness Control") description = _("Control overall brightness") feature = _F.BRIGHTNESS_CONTROL rw_options = {"read_fnid": 0x10, "write_fnid": 0x20} - validator_class = _RangeV + validator_class = settings.RangeValidator def __init__(self, device, rw, validator): super().__init__(device, rw, validator) @@ -1550,7 +1528,7 @@ def __init__(self, device, rw, validator): rw.min_nonzero_value = validator.min_value validator.min_value = 0 if validator.on_off else validator.min_value - class rw_class(_FeatureRW): + class rw_class(settings.FeatureRW): def read(self, device, data_bytes=b""): if self.on_off: reply = device.feature_request(self.feature, 0x30) @@ -1566,7 +1544,7 @@ def write(self, device, data_bytes): return reply return super().write(device, data_bytes) - class validator_class(_RangeV): + class validator_class(settings.RangeValidator): @classmethod def build(cls, setting_class, device): reply = device.feature_request(_F.BRIGHTNESS_CONTROL) @@ -1580,66 +1558,68 @@ def build(cls, setting_class, device): return validator -class LEDControl(_Setting): +class LEDControl(settings.Setting): name = "led_control" label = _("LED Control") description = _("Switch control of LED zones between device and Solaar") feature = _F.COLOR_LED_EFFECTS rw_options = {"read_fnid": 0x70, "write_fnid": 0x80} - choices_universe = _NamedInts(Device=0, Solaar=1) - validator_class = _ChoicesV + choices_universe = common.NamedInts(Device=0, Solaar=1) + validator_class = settings.ChoicesValidator validator_options = {"choices": choices_universe} -colors = _special_keys.COLORS +colors = special_keys.COLORS _LEDP = hidpp20.LEDParam # an LED Zone has an index, a set of possible LED effects, and an LED effect setting -class LEDZoneSetting(_Setting): +class LEDZoneSetting(settings.Setting): name = "led_zone_" label = _("LED Zone Effects") description = _("Set effect for LED Zone") + "\n" + _("LED Control needs to be set to Solaar to be effective.") feature = _F.COLOR_LED_EFFECTS - color_field = {"name": _LEDP.color, "kind": _KIND.choice, "label": None, "choices": colors} - speed_field = {"name": _LEDP.speed, "kind": _KIND.range, "label": _("Speed"), "min": 0, "max": 255} - period_field = {"name": _LEDP.period, "kind": _KIND.range, "label": _("Period"), "min": 100, "max": 5000} - intensity_field = {"name": _LEDP.intensity, "kind": _KIND.range, "label": _("Intensity"), "min": 0, "max": 100} - ramp_field = {"name": _LEDP.ramp, "kind": _KIND.choice, "label": _("Ramp"), "choices": hidpp20.LEDRampChoices} - # form_field = { 'name': _LEDP.form, 'kind': _KIND.choice, 'label': _('Form'), 'choices': _hidpp20.LEDFormChoices } + color_field = {"name": _LEDP.color, "kind": settings.KIND.choice, "label": None, "choices": colors} + speed_field = {"name": _LEDP.speed, "kind": settings.KIND.range, "label": _("Speed"), "min": 0, "max": 255} + period_field = {"name": _LEDP.period, "kind": settings.KIND.range, "label": _("Period"), "min": 100, "max": 5000} + intensity_field = {"name": _LEDP.intensity, "kind": settings.KIND.range, "label": _("Intensity"), "min": 0, "max": 100} + ramp_field = {"name": _LEDP.ramp, "kind": settings.KIND.choice, "label": _("Ramp"), "choices": hidpp20.LEDRampChoices} + # form_field = {"name": _LEDP.form, "kind": settings.KIND.choice, "label": _("Form"), "choices": _hidpp20.LEDFormChoices} possible_fields = [color_field, speed_field, period_field, intensity_field, ramp_field] @classmethod def setup(cls, device, read_fnid, write_fnid, suffix): infos = device.led_effects - settings = [] + settings_ = [] for zone in infos.zones: - prefix = _int2bytes(zone.index, 1) - rw = _FeatureRW(cls.feature, read_fnid, write_fnid, prefix=prefix, suffix=suffix) - validator = _HeteroV(data_class=hidpp20.LEDEffectSetting, options=zone.effects, readable=infos.readable) + prefix = common.int2bytes(zone.index, 1) + rw = settings.FeatureRW(cls.feature, read_fnid, write_fnid, prefix=prefix, suffix=suffix) + validator = settings.HeteroValidator( + data_class=hidpp20.LEDEffectSetting, options=zone.effects, readable=infos.readable + ) setting = cls(device, rw, validator) setting.name = cls.name + str(int(zone.location)) setting.label = _("LEDs") + " " + str(hidpp20.LEDZoneLocations[zone.location]) choices = [hidpp20.LEDEffects[e.ID][0] for e in zone.effects if e.ID in hidpp20.LEDEffects] - ID_field = {"name": "ID", "kind": _KIND.choice, "label": None, "choices": choices} + ID_field = {"name": "ID", "kind": settings.KIND.choice, "label": None, "choices": choices} setting.possible_fields = [ID_field] + cls.possible_fields setting.fields_map = hidpp20.LEDEffects - settings.append(setting) - return settings + settings_.append(setting) + return settings_ @classmethod def build(cls, device): return cls.setup(device, 0xE0, 0x30, b"") -class RGBControl(_Setting): +class RGBControl(settings.Setting): name = "rgb_control" label = _("LED Control") description = _("Switch control of LED zones between device and Solaar") feature = _F.RGB_EFFECTS rw_options = {"read_fnid": 0x50, "write_fnid": 0x50} - choices_universe = _NamedInts(Device=0, Solaar=1) - validator_class = _ChoicesV + choices_universe = common.NamedInts(Device=0, Solaar=1) + validator_class = settings.ChoicesValidator validator_options = {"choices": choices_universe, "write_prefix_bytes": b"\x01", "read_skip_byte_count": 1} @@ -1654,13 +1634,13 @@ def build(cls, device): return cls.setup(device, 0xE0, 0x10, b"\x01") -class PerKeyLighting(_Settings): +class PerKeyLighting(settings.Settings): name = "per-key-lighting" label = _("Per-key Lighting") description = _("Control per-key lighting.") feature = _F.PER_KEY_LIGHTING_V2 - keys_universe = _special_keys.KEYCODES - choices_universe = _special_keys.COLORSPLUS + keys_universe = special_keys.KEYCODES + choices_universe = special_keys.COLORSPLUS def read(self, cached=True): self._pre_read(cached) @@ -1668,7 +1648,7 @@ def read(self, cached=True): return self._value reply_map = {} for key in self._validator.choices: - reply_map[int(key)] = _special_keys.COLORSPLUS["No change"] # this signals no change + reply_map[int(key)] = special_keys.COLORSPLUS["No change"] # this signals no change self._value = reply_map return reply_map @@ -1683,14 +1663,14 @@ def write(self, map, save=True): table[value] = [key] if len(table) == 1: # use range update for value, keys in table.items(): # only one, of course - if value != _special_keys.COLORSPLUS["No change"]: # this signals no change, so don't update at all + if value != special_keys.COLORSPLUS["No change"]: # this signals no change, so don't update at all data_bytes = keys[0].to_bytes(1, "big") + keys[-1].to_bytes(1, "big") + value.to_bytes(3, "big") self._device.feature_request(self.feature, 0x50, data_bytes) # range update command to update all keys self._device.feature_request(self.feature, 0x70, 0x00) # signal device to make the changes else: data_bytes = b"" for value, keys in table.items(): # only one, of course - if value != _special_keys.COLORSPLUS["No change"]: # this signals no change, so ignore it + if value != special_keys.COLORSPLUS["No change"]: # this signals no change, so ignore it while len(keys) > 3: # use an optimized update command that can update up to 13 keys data = value.to_bytes(3, "big") + b"".join([key.to_bytes(1, "big") for key in keys[0:13]]) self._device.feature_request(self.feature, 0x60, data) # single-value multiple-keys update @@ -1706,7 +1686,7 @@ def write(self, map, save=True): return map def write_key_value(self, key, value, save=True): - if value != _special_keys.COLORSPLUS["No change"]: # this signals no change + if value != special_keys.COLORSPLUS["No change"]: # this signals no change result = super().write_key_value(int(key), value, save) if self._device.online: self._device.feature_request(self.feature, 0x70, 0x00) # signal device to make the change @@ -1714,10 +1694,10 @@ def write_key_value(self, key, value, save=True): else: return True - class rw_class(_FeatureRWMap): + class rw_class(settings.FeatureRWMap): pass - class validator_class(_ChoicesMapV): + class validator_class(settings.ChoicesMapValidator): @classmethod def build(cls, setting_class, device): choices_map = {} @@ -1726,7 +1706,11 @@ def build(cls, setting_class, device): key_bitmap += device.feature_request(setting_class.feature, 0x00, 0x00, 0x02)[2:] for i in range(1, 255): if (key_bitmap[i // 8] >> i % 8) & 0x01: - key = setting_class.keys_universe[i] if i in setting_class.keys_universe else _NamedInt(i, "KEY " + str(i)) + key = ( + setting_class.keys_universe[i] + if i in setting_class.keys_universe + else common.NamedInt(i, "KEY " + str(i)) + ) choices_map[key] = setting_class.choices_universe result = cls(choices_map) if choices_map else None return result @@ -1807,7 +1791,7 @@ def check_feature(device, sclass): logger.debug("check_feature %s [%s] detected %s", sclass.name, sclass.feature, detected) return detected except Exception as e: - logger.error("check_feature %s [%s] error %s\n%s", sclass.name, sclass.feature, e, _format_exc()) + logger.error("check_feature %s [%s] error %s\n%s", sclass.name, sclass.feature, e, traceback.format_exc()) return False # differentiate from an error-free determination that the setting is not supported diff --git a/lib/logitech_receiver/special_keys.py b/lib/logitech_receiver/special_keys.py index ca351c5163..0cce5ba2a2 100644 --- a/lib/logitech_receiver/special_keys.py +++ b/lib/logitech_receiver/special_keys.py @@ -15,20 +15,20 @@ ## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # Reprogrammable keys information -# Mostly from Logitech documentation, but with some edits for better Lunix compatibility +# Mostly from Logitech documentation, but with some edits for better Linux compatibility -import os as _os +import os -import yaml as _yaml +import yaml -from .common import NamedInts as _NamedInts -from .common import UnsortedNamedInts as _UnsortedNamedInts +from .common import NamedInts +from .common import UnsortedNamedInts -_XDG_CONFIG_HOME = _os.environ.get("XDG_CONFIG_HOME") or _os.path.expanduser(_os.path.join("~", ".config")) -_keys_file_path = _os.path.join(_XDG_CONFIG_HOME, "solaar", "keys.yaml") +_XDG_CONFIG_HOME = os.environ.get("XDG_CONFIG_HOME") or os.path.expanduser(os.path.join("~", ".config")) +_keys_file_path = os.path.join(_XDG_CONFIG_HOME, "solaar", "keys.yaml") #