Unify imports in logitech package

Related #2273
This commit is contained in:
MattHag 2024-05-16 20:49:01 +02:00 committed by Peter F. Patel-Schneider
parent 90b0db6c3b
commit 815dce07be
15 changed files with 871 additions and 891 deletions

View File

@ -20,32 +20,28 @@
from __future__ import annotations from __future__ import annotations
import logging import logging
import threading as _threading import struct
import threading
from collections import namedtuple from collections import namedtuple
from contextlib import contextmanager from contextlib import contextmanager
from random import getrandbits as _random_bits from random import getrandbits
from struct import pack as _pack from time import time
from time import time as _timestamp
import hidapi as _hid import hidapi
from . import base_usb
from . import common
from . import descriptors
from . import exceptions from . import exceptions
from . import hidpp10_constants as _hidpp10_constants from . import hidpp10_constants
from . import hidpp20 from . import hidpp20
from . import hidpp20_constants as _hidpp20_constants from . import hidpp20_constants
from .base_usb import ALL as _RECEIVER_USB_IDS
from .common import strhex as _strhex
from .descriptors import DEVICES as _DEVICES
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
_hidpp20 = hidpp20.Hidpp20() _hidpp20 = hidpp20.Hidpp20()
#
#
#
def _wired_device(product_id, interface): def _wired_device(product_id, interface):
return {"vendor_id": 1133, "product_id": product_id, "bus_id": 3, "usb_interface": interface, "isDevice": True} 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 = [] DEVICE_IDS = []
for _ignore, d in _DEVICES.items(): for _ignore, d in descriptors.DEVICES.items():
if d.usbid: if d.usbid:
DEVICE_IDS.append(_wired_device(d.usbid, d.interface if d.interface else 2)) DEVICE_IDS.append(_wired_device(d.usbid, d.interface if d.interface else 2))
if d.btid: if d.btid:
@ -81,7 +77,7 @@ def product_information(usb_id: int | str) -> dict:
if isinstance(usb_id, str): if isinstance(usb_id, str):
usb_id = int(usb_id, 16) 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"): if usb_id == r.get("product_id"):
return r return r
return {} 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): 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""" """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): if match(record, bus_id, vendor_id, product_id):
return record return record
if vendor_id == 0x046D and 0xC500 <= product_id <= 0xC5FF: # unknown receiver 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(): def receivers():
"""Enumerate all the receivers attached to the machine.""" """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): 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(): def receivers_and_devices():
"""Enumerate all the receivers and devices directly attached to the machine.""" """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): def notify_on_receivers_glib(callback):
"""Watch for matching devices and notifies the callback on the GLib thread.""" """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 :returns: an open receiver handle if this is the right Linux device, or
``None``. ``None``.
""" """
return _hid.open_path(path) return hidapi.open_path(path)
def open(): def open():
@ -204,13 +200,11 @@ def close(handle):
if handle: if handle:
try: try:
if isinstance(handle, int): if isinstance(handle, int):
_hid.close(handle) hidapi.close(handle)
else: else:
handle.close() handle.close()
# logger.info("closed receiver handle %r", handle)
return True return True
except Exception: except Exception:
# logger.exception("closing receiver handle %r", handle)
pass pass
return False return False
@ -234,14 +228,21 @@ def write(handle, devnumber, data, long_message=False):
assert isinstance(data, bytes), (repr(data), type(data)) assert isinstance(data, bytes), (repr(data), type(data))
if long_message or len(data) > _SHORT_MESSAGE_SIZE - 2 or data[:1] == b"\x82": 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: 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): 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: try:
_hid.write(int(handle), wdata) hidapi.write(int(handle), wdata)
except Exception as reason: except Exception as reason:
logger.error("write failed, assuming handle %r no longer available", handle) logger.error("write failed, assuming handle %r no longer available", handle)
close(handle) close(handle)
@ -274,7 +275,7 @@ def check_message(data):
if report_lengths.get(report_id) == len(data): if report_lengths.get(report_id) == len(data):
return True return True
else: 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 return False
@ -290,7 +291,7 @@ def _read(handle, timeout):
try: try:
# convert timeout to milliseconds, the hidapi expects it # convert timeout to milliseconds, the hidapi expects it
timeout = int(timeout * 1000) 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: except Exception as reason:
logger.warning("read failed, assuming handle %r no longer available", handle) logger.warning("read failed, assuming handle %r no longer available", handle)
close(handle) close(handle)
@ -303,7 +304,9 @@ def _read(handle, timeout):
if logger.isEnabledFor(logging.DEBUG) and ( if logger.isEnabledFor(logging.DEBUG) and (
report_id != DJ_MESSAGE_ID or ord(data[2:3]) > 0x10 report_id != DJ_MESSAGE_ID or ord(data[2:3]) > 0x10
): # ignore DJ input messages ): # 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:] return report_id, devnumber, data[2:]
@ -322,7 +325,7 @@ def _skip_incoming(handle, ihandle, notifications_hook):
while True: while True:
try: try:
# read whatever is already in the buffer, if any # 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: except Exception as reason:
logger.error("read failed, assuming receiver %s no longer available", handle) logger.error("read failed, assuming receiver %s no longer available", handle)
close(handle) close(handle)
@ -380,14 +383,10 @@ _HIDPP_Notification.__str__ = lambda self: "Notification(%02x,%d,%02X,%02X,%s)"
self.devnumber, self.devnumber,
self.sub_id, self.sub_id,
self.address, 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 = {} handles_lock = {}
@ -396,7 +395,7 @@ def handle_lock(handle):
if handles_lock.get(handle) is None: if handles_lock.get(handle) is None:
if logger.isEnabledFor(logging.INFO): if logger.isEnabledFor(logging.INFO):
logger.info("New lock %s", repr(handle)) 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] 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. :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 :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): with acquire_timeout(handle_lock(handle), handle, 10.0):
assert isinstance(request_id, int) assert isinstance(request_id, int)
if (devnumber != 0xFF or protocol >= 2.0) and request_id < 0x8000: 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 # most significant bit (8) in SoftwareId, to make notifications easier
# to distinguish from request replies. # to distinguish from request replies.
# This only applies to peripheral requests, ofc. # 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 timeout = _RECEIVER_REQUEST_TIMEOUT if devnumber == 0xFF else _DEVICE_REQUEST_TIMEOUT
# be extra patient on long register read # 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 timeout *= 2
if params: 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: else:
params = b"" params = b""
# if logger.isEnabledFor(logging.DEBUG): request_data = struct.pack("!H", request_id) + params
# logger.debug("(%s) device %d request_id {%04X} params [%s]", handle, devnumber, request_id, _strhex(params))
request_data = _pack("!H", request_id) + params
ihandle = int(handle) ihandle = int(handle)
notifications_hook = getattr(handle, "notifications_hook", None) 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 return None
# we consider timeout from this point # we consider timeout from this point
request_started = _timestamp() request_started = time()
delta = 0 delta = 0
while delta < timeout: 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 reply_devnumber == devnumber or reply_devnumber == devnumber ^ 0xFF: # BT device returning 0x00
if ( if (
report_id == HIDPP_SHORT_MESSAGE_ID 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] and reply_data[1:3] == request_data[:2]
): ):
error = ord(reply_data[3:4]) error = ord(reply_data[3:4])
@ -485,10 +478,10 @@ def request(handle, devnumber, request_id, *params, no_reply=False, return_error
devnumber, devnumber,
request_id, request_id,
error, error,
_hidpp10_constants.ERROR[error], hidpp10_constants.ERROR[error],
) )
return _hidpp10_constants.ERROR[error] if return_error else None return hidpp10_constants.ERROR[error] if return_error else None
if reply_data[:1] == b"\xFF" and reply_data[1:3] == request_data[:2]: if reply_data[:1] == b"\xff" and reply_data[1:3] == request_data[:2]:
# a HID++ 2.0 feature call returned with an error # a HID++ 2.0 feature call returned with an error
error = ord(reply_data[3:4]) error = ord(reply_data[3:4])
logger.error( logger.error(
@ -497,7 +490,7 @@ def request(handle, devnumber, request_id, *params, no_reply=False, return_error
devnumber, devnumber,
request_id, request_id,
error, error,
_hidpp20_constants.ERROR[error], hidpp20_constants.ERROR[error],
) )
raise exceptions.FeatureCallError(number=devnumber, request=request_id, error=error, params=params) 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: else:
# a reply was received, but did not match our request in any way # a reply was received, but did not match our request in any way
# reset the timeout starting point # reset the timeout starting point
request_started = _timestamp() request_started = time()
if notifications_hook: if notifications_hook:
n = make_notification(report_id, reply_devnumber, reply_data) n = make_notification(report_id, reply_devnumber, reply_data)
if n: if n:
notifications_hook(n) notifications_hook(n)
# elif logger.isEnabledFor(logging.DEBUG): delta = time() - request_started
# 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)
logger.warning( logger.warning(
"timeout (%0.2f/%0.2f) on device %d request {%04X} params [%s]", "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, timeout,
devnumber, devnumber,
request_id, request_id,
_strhex(params), common.strhex(params),
) )
# raise DeviceUnreachable(number=devnumber, request=request_id) # 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 # 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 # reply, and set most significant (0x8) bit in SoftwareId so that the reply
# is always distinguishable from notifications # is always distinguishable from notifications
request_id = 0x0018 | _random_bits(3) request_id = 0x0018 | getrandbits(3)
request_data = _pack("!HBBB", request_id, 0, 0, _random_bits(8)) request_data = struct.pack("!HBBB", request_id, 0, 0, getrandbits(8))
write(int(handle), devnumber, request_data, long_message) 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 delta = 0
while delta < _PING_TIMEOUT: while delta < _PING_TIMEOUT:
reply = _read(handle, _PING_TIMEOUT) reply = _read(handle, _PING_TIMEOUT)
@ -577,18 +563,18 @@ def ping(handle, devnumber, long_message=False):
if ( if (
report_id == HIDPP_SHORT_MESSAGE_ID 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] and reply_data[1:3] == request_data[:2]
): # error response ): # error response
error = ord(reply_data[3:4]) 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 return 1.0
if ( if (
error == _hidpp10_constants.ERROR.resource_error error == hidpp10_constants.ERROR.resource_error
or error == _hidpp10_constants.ERROR.connection_request_failed or error == hidpp10_constants.ERROR.connection_request_failed
): ):
return # device unreachable 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) logger.error("(%s) device %d error on ping request: unknown device", handle, devnumber)
raise exceptions.NoSuchDevice(number=devnumber, request=request_id) 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) n = make_notification(report_id, reply_devnumber, reply_data)
if n: if n:
notifications_hook(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) logger.warning("(%s) timeout (%0.2f/%0.2f) on device %d ping", handle, delta, _PING_TIMEOUT, devnumber)

View File

@ -14,16 +14,16 @@
## You should have received a copy of the GNU General Public License along ## 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., ## with this program; if not, write to the Free Software Foundation, Inc.,
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ## 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 enum import IntEnum
from typing import Optional from typing import Optional
from typing import Union from typing import Union
import yaml as _yaml import yaml
from solaar.i18n import _ from solaar.i18n import _
@ -348,8 +348,8 @@ class NamedInt(int):
return dumper.represent_mapping("!NamedInt", {"value": int(data), "name": data.name}, flow_style=True) return dumper.represent_mapping("!NamedInt", {"value": int(data), "name": data.name}, flow_style=True)
_yaml.SafeLoader.add_constructor("!NamedInt", NamedInt.from_yaml) yaml.SafeLoader.add_constructor("!NamedInt", NamedInt.from_yaml)
_yaml.add_representer(NamedInt, NamedInt.to_yaml) yaml.add_representer(NamedInt, NamedInt.to_yaml)
class NamedInts: class NamedInts:
@ -512,7 +512,7 @@ class UnsortedNamedInts(NamedInts):
def strhex(x): def strhex(x):
assert x is not None assert x is not None
"""Produce a hex-string representation of a sequence of bytes.""" """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): def bytes2int(x, signed=False):
@ -541,7 +541,7 @@ class KwException(Exception):
return self.args[0].get(k) # was self.args[0][k] return self.args[0].get(k) # was self.args[0][k]
@dataclass @dataclasses.dataclass
class FirmwareInfo: class FirmwareInfo:
kind: str kind: str
name: str name: str
@ -567,7 +567,7 @@ class BatteryLevelApproximation(IntEnum):
FULL = 90 FULL = 90
@dataclass @dataclasses.dataclass
class Battery: class Battery:
"""Information about the current state of a battery""" """Information about the current state of a battery"""

View File

@ -22,12 +22,8 @@
# - the device uses a USB interface other than 2 # - the device uses a USB interface other than 2
# - the name or codename should be different from what the device reports # - the name or codename should be different from what the device reports
from .hidpp10_constants import DEVICE_KIND as _DK from .hidpp10_constants import DEVICE_KIND
from .hidpp10_constants import REGISTERS as _R from .hidpp10_constants import REGISTERS as REG
#
#
#
class _DeviceDescriptor: class _DeviceDescriptor:
@ -73,15 +69,15 @@ def _D(
): ):
if kind is None: if kind is None:
kind = ( kind = (
_DK.mouse DEVICE_KIND.mouse
if "Mouse" in name if "Mouse" in name
else _DK.keyboard else DEVICE_KIND.keyboard
if "Keyboard" in name if "Keyboard" in name
else _DK.numpad else DEVICE_KIND.numpad
if "Number Pad" in name if "Number Pad" in name
else _DK.touchpad else DEVICE_KIND.touchpad
if "Touchpad" in name if "Touchpad" in name
else _DK.trackball else DEVICE_KIND.trackball
if "Trackball" in name if "Trackball" in name
else None else None
) )
@ -94,9 +90,12 @@ def _D(
assert w[0:1] == "4", f"{name} has protocol {protocol:0.1f}, wpid {w}" assert w[0:1] == "4", f"{name} has protocol {protocol:0.1f}, wpid {w}"
else: else:
if w[0:1] == "1": 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": 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( device_descriptor = _DeviceDescriptor(
name=name, name=name,
@ -192,24 +191,24 @@ def get_btid(btid):
# Keyboards # Keyboards
_D("Wireless Keyboard EX110", codename="EX110", protocol=1.0, wpid="0055", 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=(_R.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=(_R.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=(_R.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=(_R.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=(_R.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=(_R.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=(_R.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=(_R.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=(_R.battery_status,)) _D("Wireless Keyboard MK320", codename="MK320", protocol=1.0, wpid="200F", registers=(REG.battery_status,))
_D( _D(
"Wireless Illuminated Keyboard K800", "Wireless Illuminated Keyboard K800",
codename="K800", codename="K800",
protocol=1.0, protocol=1.0,
wpid="2010", 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 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 K270 (unifying)", codename="K270", protocol=2.0, wpid="4003")
_D("Wireless Keyboard K360", codename="K360", protocol=2.0, wpid="4004") _D("Wireless Keyboard K360", codename="K360", protocol=2.0, wpid="4004")
@ -234,51 +233,57 @@ _D("K845 Mechanical Keyboard", codename="K845", usbid=0xC341, interface=3)
# Mice # Mice
_D("LX5 Cordless Mouse", codename="LX5", protocol=1.0, wpid="0036", 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=(_R.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=(_R.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=(_R.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=(_R.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=(_R.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=(_R.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=(_R.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=(_R.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=(_R.battery_status,)) _D("V450 Laser Cordless Mouse", codename="V450", protocol=1.0, wpid="1005", registers=(REG.battery_status,))
_D( _D(
"VX Revolution", "VX Revolution",
codename="VX Revolution", codename="VX Revolution",
kind=_DK.mouse, kind=DEVICE_KIND.mouse,
protocol=1.0, protocol=1.0,
wpid=("1006", "100D", "0612"), 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( _D(
"MX Revolution", "MX Revolution",
codename="MX Revolution", codename="MX Revolution",
protocol=1.0, protocol=1.0,
kind=_DK.mouse, kind=DEVICE_KIND.mouse,
wpid=("1008", "100C"), 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("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=(_R.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=(_R.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=(_R.battery_charge,)) _D("V550 Nano Cordless Laser Mouse", codename="V550 Nano", protocol=1.0, wpid="1013", registers=(REG.battery_charge,))
_D( _D(
"MX 1100 Cordless Laser Mouse", "MX 1100 Cordless Laser Mouse",
codename="MX 1100", codename="MX 1100",
protocol=1.0, protocol=1.0,
kind=_DK.mouse, kind=DEVICE_KIND.mouse,
wpid="1014", 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("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=(_R.battery_status, _R.three_leds)) _D(
_D("Marathon Mouse M705 (M-R0009)", codename="M705 (M-R0009)", protocol=1.0, wpid="101B", registers=(_R.battery_charge,)) "Performance Mouse MX",
_D("Wireless Mouse M350", codename="M350", protocol=1.0, wpid="101C", registers=(_R.battery_charge,)) codename="Performance MX",
_D("Wireless Mouse M505", codename="M505/B605", protocol=1.0, wpid="101D", registers=(_R.battery_charge,)) protocol=1.0,
_D("Wireless Mouse M305", codename="M305", protocol=1.0, wpid="101F", registers=(_R.battery_status,)) 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("Wireless Mouse M215", codename="M215", protocol=1.0, wpid="1020")
_D( _D(
"G700 Gaming Mouse", "G700 Gaming Mouse",
@ -288,12 +293,12 @@ _D(
usbid=0xC06B, usbid=0xC06B,
interface=1, interface=1,
registers=( registers=(
_R.battery_status, REG.battery_status,
_R.three_leds, REG.three_leds,
), ),
) )
_D("Wireless Mouse M310", codename="M310", protocol=1.0, wpid="1024", 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=(_R.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("Fujitsu Sonic Mouse", codename="Sonic", protocol=1.0, wpid="1029")
_D( _D(
"G700s Gaming Mouse", "G700s Gaming Mouse",
@ -303,8 +308,8 @@ _D(
usbid=0xC07C, usbid=0xC07C,
interface=1, interface=1,
registers=( registers=(
_R.battery_status, REG.battery_status,
_R.three_leds, REG.three_leds,
), ),
) )
_D("Couch Mouse M515", codename="M515", protocol=2.0, wpid="4007") _D("Couch Mouse M515", codename="M515", protocol=2.0, wpid="4007")
@ -348,7 +353,7 @@ _D("G502 Lightspeed Gaming Mouse", codename="G502 Lightspeed", usbid=0xC08D)
_D("MX518 Gaming Mouse", codename="MX518", usbid=0xC08E, interface=1) _D("MX518 Gaming Mouse", codename="MX518", usbid=0xC08E, interface=1)
_D("G703 Hero Gaming Mouse", codename="G703 Hero", usbid=0xC090) _D("G703 Hero Gaming Mouse", codename="G703 Hero", usbid=0xC090)
_D("G903 Hero Gaming Mouse", codename="G903 Hero", usbid=0xC091) _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("M500S Mouse", codename="M500S", usbid=0xC093, interface=1)
# _D('G600 Gaming Mouse', codename='G600 Gaming', usbid=0xc24a, interface=1) # not an HID++ device # _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) _D("G500s Gaming Mouse", codename="G500s Gaming", usbid=0xC24E, interface=1, protocol=1.0)
@ -365,13 +370,15 @@ _D("Wireless Trackball M570", codename="M570")
_D("Wireless Touchpad", codename="Wireless Touch", protocol=2.0, wpid="4011") _D("Wireless Touchpad", codename="Wireless Touch", protocol=2.0, wpid="4011")
_D("Wireless Rechargeable Touchpad T650", codename="T650", protocol=2.0, wpid="4101") _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 # Headset
_D("G533 Gaming Headset", codename="G533 Headset", protocol=2.0, interface=3, kind=_DK.headset, usbid=0x0A66) _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=_DK.headset, usbid=0x0AC4) _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=_DK.headset, usbid=0x0A87) _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=_DK.headset, usbid=0x0AB5) _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=_DK.headset, usbid=0x0AFE) _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=_DK.headset, usbid=0x0ABA) _D("PRO X Wireless Gaming Headset", codename="PRO Headset", protocol=2.0, interface=3, kind=DEVICE_KIND.headset, usbid=0x0ABA)

View File

@ -14,17 +14,17 @@
## You should have received a copy of the GNU General Public License along ## 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., ## with this program; if not, write to the Free Software Foundation, Inc.,
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
import errno
import errno as _errno
import logging import logging
import threading as _threading import threading
import time import time
from typing import Callable from typing import Callable
from typing import Optional from typing import Optional
import hidapi as _hid import hidapi
import solaar.configuration as _configuration
from solaar import configuration
from . import base from . import base
from . import descriptors from . import descriptors
@ -34,9 +34,9 @@ from . import hidpp10_constants
from . import hidpp20 from . import hidpp20
from . import hidpp20_constants from . import hidpp20_constants
from . import settings from . import settings
from . import settings_templates
from .common import Alert from .common import Alert
from .common import Battery from .common import Battery
from .settings_templates import check_feature_settings as _check_feature_settings
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -59,7 +59,7 @@ class DeviceFactory:
return Device(None, None, None, handle=handle, device_info=device_info, setting_callback=setting_callback) return Device(None, None, None, handle=handle, device_info=device_info, setting_callback=setting_callback)
except OSError as e: except OSError as e:
logger.exception("open %s", device_info) logger.exception("open %s", device_info)
if e.errno == _errno.EACCES: if e.errno == errno.EACCES:
raise raise
except Exception: except Exception:
logger.exception("open %s", device_info) logger.exception("open %s", device_info)
@ -70,7 +70,16 @@ class Device:
read_register: Callable = hidpp10.read_register read_register: Callable = hidpp10.read_register
write_register: Callable = hidpp10.write_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 assert receiver or device_info
if receiver: if receiver:
assert 0 < number <= 15 # some receivers have devices past their max # of devices assert 0 < number <= 15 # some receivers have devices past their max # of devices
@ -110,14 +119,14 @@ class Device:
self._active = None # lags self.online - is used to help determine when to setup devices self._active = None # lags self.online - is used to help determine when to setup devices
self._feature_settings_checked = False self._feature_settings_checked = False
self._gestures_lock = _threading.Lock() self._gestures_lock = threading.Lock()
self._settings_lock = _threading.Lock() self._settings_lock = threading.Lock()
self._persister_lock = _threading.Lock() self._persister_lock = threading.Lock()
self._notification_handlers = {} # See `add_notification_handler` self._notification_handlers = {} # See `add_notification_handler`
self.cleanups = [] # functions to run on the device when it is closed self.cleanups = [] # functions to run on the device when it is closed
if not self.path: 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: if not self.handle:
try: try:
self.handle = base.open_path(self.path) if self.path else None self.handle = base.open_path(self.path) if self.path else None
@ -302,9 +311,9 @@ class Device:
self._profiles = _hidpp20.get_profiles(self) self._profiles = _hidpp20.get_profiles(self)
return self._profiles 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: 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): def reset(self, no_reply=False):
self.set_configuration(0, no_reply) self.set_configuration(0, no_reply)
@ -314,7 +323,7 @@ class Device:
if not self._persister: if not self._persister:
with self._persister_lock: with self._persister_lock:
if not self._persister: if not self._persister:
self._persister = _configuration.persister(self) self._persister = configuration.persister(self)
return self._persister return self._persister
@property @property
@ -337,7 +346,7 @@ class Device:
if not self._feature_settings_checked: if not self._feature_settings_checked:
with self._settings_lock: with self._settings_lock:
if not self._feature_settings_checked: 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 return self._settings
def battery(self): # None or level, next, status, voltage def battery(self): # None or level, next, status, voltage

View File

@ -14,43 +14,37 @@
## with this program; if not, write to the Free Software Foundation, Inc., ## with this program; if not, write to the Free Software Foundation, Inc.,
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
import ctypes as _ctypes import ctypes
import logging import logging
import math import math
import numbers import numbers
import os as _os import os
import os.path as _path import platform
import platform as _platform
import socket import socket
import struct
import subprocess import subprocess
import sys as _sys import sys
import time as _time import time
from typing import Dict from typing import Dict
from typing import Tuple from typing import Tuple
import gi import gi
import psutil import psutil
import yaml
from keysyms import keysymdef from keysyms import keysymdef
# There is no evdev on macOS or Windows. Diversion will not work without # There is no evdev on macOS or Windows. Diversion will not work without
# it but other Solaar functionality is available. # it but other Solaar functionality is available.
if _platform.system() in ("Darwin", "Windows"): if platform.system() in ("Darwin", "Windows"):
evdev = None evdev = None
else: else:
import evdev 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 .common import NamedInt
from .hidpp20 import FEATURE as _F from .hidpp20 import FEATURE
from .special_keys import CONTROL as _CONTROL from .special_keys import CONTROL
gi.require_version("Gdk", "3.0") # isort:skip gi.require_version("Gdk", "3.0") # isort:skip
from gi.repository import Gdk, GLib # NOQA: E402 # isort:skip from gi.repository import Gdk, GLib # NOQA: E402 # isort:skip
@ -102,7 +96,7 @@ gkeymap = Gdk.Keymap.get_for_display(gdisplay) if gdisplay else None
if logger.isEnabledFor(logging.INFO): if logger.isEnabledFor(logging.INFO):
logger.info("GDK Keymap %sset up", "" if gkeymap else "not ") 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: if wayland:
logger.warning( logger.warning(
"rules cannot access modifier keys in Wayland, " "rules cannot access modifier keys in Wayland, "
@ -137,26 +131,26 @@ thumb_wheel_displacement = 0
_dbus_interface = None _dbus_interface = None
class XkbDisplay(_ctypes.Structure): class XkbDisplay(ctypes.Structure):
"""opaque struct""" """opaque struct"""
class XkbStateRec(_ctypes.Structure): class XkbStateRec(ctypes.Structure):
_fields_ = [ _fields_ = [
("group", _ctypes.c_ubyte), ("group", ctypes.c_ubyte),
("locked_group", _ctypes.c_ubyte), ("locked_group", ctypes.c_ubyte),
("base_group", _ctypes.c_ushort), ("base_group", ctypes.c_ushort),
("latched_group", _ctypes.c_ushort), ("latched_group", ctypes.c_ushort),
("mods", _ctypes.c_ubyte), ("mods", ctypes.c_ubyte),
("base_mods", _ctypes.c_ubyte), ("base_mods", ctypes.c_ubyte),
("latched_mods", _ctypes.c_ubyte), ("latched_mods", ctypes.c_ubyte),
("locked_mods", _ctypes.c_ubyte), ("locked_mods", ctypes.c_ubyte),
("compat_state", _ctypes.c_ubyte), ("compat_state", ctypes.c_ubyte),
("grab_mods", _ctypes.c_ubyte), ("grab_mods", ctypes.c_ubyte),
("compat_grab_mods", _ctypes.c_ubyte), ("compat_grab_mods", ctypes.c_ubyte),
("lookup_mods", _ctypes.c_ubyte), ("lookup_mods", ctypes.c_ubyte),
("compat_lookup_mods", _ctypes.c_ubyte), ("compat_lookup_mods", ctypes.c_ubyte),
("ptr_buttons", _ctypes.c_ushort), ("ptr_buttons", ctypes.c_ushort),
] # something strange is happening here but it is not being used ] # something strange is happening here but it is not being used
@ -176,7 +170,7 @@ def x11_setup():
if logger.isEnabledFor(logging.INFO): if logger.isEnabledFor(logging.INFO):
logger.info("X11 library loaded and display set up") logger.info("X11 library loaded and display set up")
except Exception: 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 _x11 = False
xtest_available = False xtest_available = False
return _x11 return _x11
@ -193,7 +187,7 @@ def gnome_dbus_interface_setup():
remote_object = bus.get_object("org.gnome.Shell", "/io/github/pwr_solaar/solaar") remote_object = bus.get_object("org.gnome.Shell", "/io/github/pwr_solaar/solaar")
_dbus_interface = dbus.Interface(remote_object, "io.github.pwr_solaar.solaar") _dbus_interface = dbus.Interface(remote_object, "io.github.pwr_solaar.solaar")
except dbus.exceptions.DBusException: 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 _dbus_interface = False
return _dbus_interface return _dbus_interface
@ -203,14 +197,14 @@ def xkb_setup():
if Xkbdisplay is not None: if Xkbdisplay is not None:
return Xkbdisplay return Xkbdisplay
try: # set up to get keyboard state using ctypes interface to libx11 try: # set up to get keyboard state using ctypes interface to libx11
X11Lib = _ctypes.cdll.LoadLibrary("libX11.so") X11Lib = ctypes.cdll.LoadLibrary("libX11.so")
X11Lib.XOpenDisplay.restype = _ctypes.POINTER(XkbDisplay) X11Lib.XOpenDisplay.restype = ctypes.POINTER(XkbDisplay)
X11Lib.XkbGetState.argtypes = [_ctypes.POINTER(XkbDisplay), _ctypes.c_uint, _ctypes.POINTER(XkbStateRec)] X11Lib.XkbGetState.argtypes = [ctypes.POINTER(XkbDisplay), ctypes.c_uint, ctypes.POINTER(XkbStateRec)]
Xkbdisplay = X11Lib.XOpenDisplay(None) Xkbdisplay = X11Lib.XOpenDisplay(None)
if logger.isEnabledFor(logging.INFO): if logger.isEnabledFor(logging.INFO):
logger.info("XKB display set up") logger.info("XKB display set up")
except Exception: 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 Xkbdisplay = False
return Xkbdisplay return Xkbdisplay
@ -262,7 +256,7 @@ if wayland: # Wayland can't use xtest so may as well set up uinput now
def kbdgroup(): def kbdgroup():
if xkb_setup(): if xkb_setup():
state = XkbStateRec() state = XkbStateRec()
X11Lib.XkbGetState(Xkbdisplay, XkbUseCoreKbd, _ctypes.pointer(state)) X11Lib.XkbGetState(Xkbdisplay, XkbUseCoreKbd, ctypes.pointer(state))
return state.group return state.group
else: else:
return None return None
@ -282,7 +276,7 @@ def signed(bytes_: bytes) -> int:
def xy_direction(_x, _y): def xy_direction(_x, _y):
# normalize x and y # normalize x and y
m = _sqrt((_x * _x) + (_y * _y)) m = math.sqrt((_x * _x) + (_y * _y))
if m == 0: if m == 0:
return "noop" return "noop"
x = round(_x / m) x = round(_x / m)
@ -419,7 +413,7 @@ def simulate_scroll(dx, dy):
def thumb_wheel_up(f, r, d, a): def thumb_wheel_up(f, r, d, a):
global thumb_wheel_displacement global thumb_wheel_displacement
if f != _F.THUMB_WHEEL or r != 0: if f != FEATURE.THUMB_WHEEL or r != 0:
return False return False
if a is None: if a is None:
return signed(d[0:2]) < 0 and signed(d[0:2]) 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): def thumb_wheel_down(f, r, d, a):
global thumb_wheel_displacement global thumb_wheel_displacement
if f != _F.THUMB_WHEEL or r != 0: if f != FEATURE.THUMB_WHEEL or r != 0:
return False return False
if a is None: if a is None:
return signed(d[0:2]) > 0 and signed(d[0:2]) 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): def charging(f, r, d, _a):
if ( if (
(f == _F.BATTERY_STATUS and r == 0 and 1 <= d[2] <= 4) (f == FEATURE.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 == FEATURE.BATTERY_VOLTAGE and r == 0 and d[2] & (1 << 7))
or (f == _F.UNIFIED_BATTERY and r == 0 and 1 <= d[2] <= 3) or (f == FEATURE.UNIFIED_BATTERY and r == 0 and 1 <= d[2] <= 3)
): ):
return 1 return 1
else: else:
@ -455,20 +449,32 @@ def charging(f, r, d, _a):
TESTS = { TESTS = {
"crown_right": [lambda f, r, d, a: f == _F.CROWN and r == 0 and d[1] < 128 and d[1], 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 == _F.CROWN and r == 0 and d[1] >= 128 and 256 - 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 == _F.CROWN and r == 0 and d[2] < 128 and d[2], 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 == _F.CROWN and r == 0 and d[2] >= 128 and 256 - 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 == _F.CROWN and r == 0 and d[5] == 0x01 and d[5], 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 == _F.CROWN and r == 0 and d[6] == 0x01 and d[6], 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 == _F.CROWN and r == 0 and d[6] == 0x05 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 == _F.CROWN and r == 0 and d[6] >= 0x01 and d[6] <= 0x04 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_up": [thumb_wheel_up, True],
"thumb_wheel_down": [thumb_wheel_down, 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_up": [
"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], lambda f, r, d, a: f == FEATURE.LOWRES_WHEEL and r == 0 and signed(d[0:1]) > 0 and signed(d[0:1]),
"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], 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_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], "charging": [charging, False],
"False": [lambda f, r, d, a: False, False], "False": [lambda f, r, d, a: False, False],
"True": [lambda f, r, d, a: True, False], "True": [lambda f, r, d, a: True, False],
@ -714,11 +720,11 @@ class MouseProcess(Condition):
class Feature(Condition): class Feature(Condition):
def __init__(self, feature, warn=True): 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: if warn:
logger.warning("rule Feature argument not name of a feature: %s", feature) logger.warning("rule Feature argument not name of a feature: %s", feature)
self.feature = None self.feature = None
self.feature = _F[feature] self.feature = FEATURE[feature]
def __str__(self): def __str__(self):
return "Feature: " + str(self.feature) return "Feature: " + str(self.feature)
@ -857,8 +863,8 @@ class Key(Condition):
elif len(args) >= 2: elif len(args) >= 2:
key, action = args[:2] key, action = args[:2]
if isinstance(key, str) and key in _CONTROL: if isinstance(key, str) and key in CONTROL:
self.key = _CONTROL[key] self.key = CONTROL[key]
else: else:
if warn: if warn:
logger.warning(f"rule Key key name not name of a Logitech key: {key}") logger.warning(f"rule Key key name not name of a Logitech key: {key}")
@ -896,8 +902,8 @@ class KeyIsDown(Condition):
elif isinstance(args, str): elif isinstance(args, str):
key = args key = args
if isinstance(key, str) and key in _CONTROL: if isinstance(key, str) and key in CONTROL:
self.key = _CONTROL[key] self.key = CONTROL[key]
else: else:
if warn: if warn:
logger.warning(f"rule Key key name not name of a Logitech key: {key}") logger.warning(f"rule Key key name not name of a Logitech key: {key}")
@ -1013,7 +1019,7 @@ class MouseGesture(Condition):
if isinstance(movements, str): if isinstance(movements, str):
movements = [movements] movements = [movements]
for x in 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: if warn:
logger.warning("rule Mouse Gesture argument not direction or name of a Logitech key: %s", x) logger.warning("rule Mouse Gesture argument not direction or name of a Logitech key: %s", x)
self.movements = movements self.movements = movements
@ -1024,14 +1030,14 @@ class MouseGesture(Condition):
def evaluate(self, feature, notification, device, last_result): def evaluate(self, feature, notification, device, last_result):
if logger.isEnabledFor(logging.DEBUG): if logger.isEnabledFor(logging.DEBUG):
logger.debug("evaluate condition: %s", self) logger.debug("evaluate condition: %s", self)
if feature == _F.MOUSE_GESTURE: if feature == FEATURE.MOUSE_GESTURE:
d = notification.data d = notification.data
data = _unpack("!" + (int(len(d) / 2) * "h"), d) data = struct.unpack("!" + (int(len(d) / 2) * "h"), d)
data_offset = 1 data_offset = 1
movement_offset = 0 movement_offset = 0
if self.movements and self.movements[0] not in self.MOVEMENTS: # matching against initiating key if self.movements and self.movements[0] not in self.MOVEMENTS: # matching against initiating key
movement_offset = 1 movement_offset = 1
if self.movements[0] != str(_CONTROL[data[0]]): if self.movements[0] != str(CONTROL[data[0]]):
return False return False
for m in self.movements[movement_offset:]: for m in self.movements[movement_offset:]:
if data_offset >= len(data): if data_offset >= len(data):
@ -1042,7 +1048,7 @@ class MouseGesture(Condition):
return False return False
data_offset += 3 data_offset += 3
elif data[data_offset] == 1: elif data[data_offset] == 1:
if m != str(_CONTROL[data[data_offset + 1]]): if m != str(CONTROL[data[data_offset + 1]]):
return False return False
data_offset += 2 data_offset += 2
return data_offset == len(data) return data_offset == len(data)
@ -1214,7 +1220,7 @@ class KeyPress(Action):
self.keyDown(self.key_symbols, current) self.keyDown(self.key_symbols, current)
if self.action != DEPRESS: if self.action != DEPRESS:
self.keyUp(reversed(self.key_symbols), current) self.keyUp(reversed(self.key_symbols), current)
_time.sleep(0.01) time.sleep(0.01)
else: else:
logger.warning("no keymap so cannot determine which keycode to send") logger.warning("no keymap so cannot determine which keycode to send")
return None return None
@ -1253,7 +1259,7 @@ class MouseScroll(Action):
logger.info("MouseScroll action: %s %s %s", self.amounts, last_result, amounts) logger.info("MouseScroll action: %s %s %s", self.amounts, last_result, amounts)
dx, dy = amounts dx, dy = amounts
simulate_scroll(dx, dy) simulate_scroll(dx, dy)
_time.sleep(0.01) time.sleep(0.01)
return None return None
def data(self): def data(self):
@ -1289,7 +1295,7 @@ class MouseClick(Action):
logger.info(f"MouseClick action: {int(self.count)} {self.button}") logger.info(f"MouseClick action: {int(self.count)} {self.button}")
if self.button and self.count: if self.button and self.count:
click(buttons[self.button], self.count) click(buttons[self.button], self.count)
_time.sleep(0.01) time.sleep(0.01)
return None return None
def data(self): def data(self):
@ -1438,12 +1444,12 @@ if True:
def key_is_down(key): def key_is_down(key):
if key == _CONTROL.MR: if key == CONTROL.MR:
return mr_key_down return mr_key_down
elif _CONTROL.M1 <= key <= _CONTROL.M8: elif CONTROL.M1 <= key <= CONTROL.M8:
return bool(m_keys_down & (0x01 << (key - _CONTROL.M1))) return bool(m_keys_down & (0x01 << (key - CONTROL.M1)))
elif _CONTROL.G1 <= key <= _CONTROL.G32: elif CONTROL.G1 <= key <= CONTROL.G32:
return bool(g_keys_down & (0x01 << (key - _CONTROL.G1))) return bool(g_keys_down & (0x01 << (key - CONTROL.G1)))
else: else:
return key in keys_down 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 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 key_down, key_up = None, None
# need to keep track of keys that are down to find a new key down # 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: if feature == FEATURE.REPROG_CONTROLS_V4 and notification.address == 0x00:
new_keys_down = _unpack("!4H", notification.data[:8]) new_keys_down = struct.unpack("!4H", notification.data[:8])
for key in new_keys_down: for key in new_keys_down:
if key and key not in keys_down: if key and key not in keys_down:
key_down = key key_down = key
@ -1469,33 +1475,33 @@ def process_notification(device, notification, feature):
key_up = key key_up = key
keys_down = new_keys_down keys_down = new_keys_down
# and also G keys down # and also G keys down
elif feature == _F.GKEY and notification.address == 0x00: elif feature == FEATURE.GKEY and notification.address == 0x00:
new_g_keys_down = _unpack("<I", notification.data[:4])[0] new_g_keys_down = struct.unpack("<I", notification.data[:4])[0]
for i in range(32): for i in range(32):
if new_g_keys_down & (0x01 << i) and not g_keys_down & (0x01 << i): if new_g_keys_down & (0x01 << i) and not g_keys_down & (0x01 << i):
key_down = _CONTROL["G" + str(i + 1)] key_down = CONTROL["G" + str(i + 1)]
if g_keys_down & (0x01 << i) and not new_g_keys_down & (0x01 << i): if g_keys_down & (0x01 << i) and not new_g_keys_down & (0x01 << i):
key_up = _CONTROL["G" + str(i + 1)] key_up = CONTROL["G" + str(i + 1)]
g_keys_down = new_g_keys_down g_keys_down = new_g_keys_down
# and also M keys down # and also M keys down
elif feature == _F.MKEYS and notification.address == 0x00: elif feature == FEATURE.MKEYS and notification.address == 0x00:
new_m_keys_down = _unpack("!1B", notification.data[:1])[0] new_m_keys_down = struct.unpack("!1B", notification.data[:1])[0]
for i in range(1, 9): for i in range(1, 9):
if new_m_keys_down & (0x01 << (i - 1)) and not m_keys_down & (0x01 << (i - 1)): if new_m_keys_down & (0x01 << (i - 1)) and not m_keys_down & (0x01 << (i - 1)):
key_down = _CONTROL["M" + str(i)] key_down = CONTROL["M" + str(i)]
if m_keys_down & (0x01 << (i - 1)) and not new_m_keys_down & (0x01 << (i - 1)): if m_keys_down & (0x01 << (i - 1)) and not new_m_keys_down & (0x01 << (i - 1)):
key_up = _CONTROL["M" + str(i)] key_up = CONTROL["M" + str(i)]
m_keys_down = new_m_keys_down m_keys_down = new_m_keys_down
# and also MR key # and also MR key
elif feature == _F.MR and notification.address == 0x00: elif feature == FEATURE.MR and notification.address == 0x00:
new_mr_key_down = _unpack("!1B", notification.data[:1])[0] new_mr_key_down = struct.unpack("!1B", notification.data[:1])[0]
if not mr_key_down and new_mr_key_down: if not mr_key_down and new_mr_key_down:
key_down = _CONTROL["MR"] key_down = CONTROL["MR"]
if mr_key_down and not new_mr_key_down: if mr_key_down and not new_mr_key_down:
key_up = _CONTROL["MR"] key_up = CONTROL["MR"]
mr_key_down = new_mr_key_down mr_key_down = new_mr_key_down
# keep track of thumb wheel movment # keep track of thumb wheel movment
elif feature == _F.THUMB_WHEEL and notification.address == 0x00: elif feature == FEATURE.THUMB_WHEEL and notification.address == 0x00:
if notification.data[4] <= 0x01: # when wheel starts, zero out last movement if notification.data[4] <= 0x01: # when wheel starts, zero out last movement
thumb_wheel_displacement = 0 thumb_wheel_displacement = 0
thumb_wheel_displacement += signed(notification.data[0:2]) thumb_wheel_displacement += signed(notification.data[0:2])
@ -1503,8 +1509,8 @@ def process_notification(device, notification, feature):
GLib.idle_add(evaluate_rules, feature, notification, device) GLib.idle_add(evaluate_rules, feature, notification, device)
_XDG_CONFIG_HOME = _os.environ.get("XDG_CONFIG_HOME") or _path.expanduser(_path.join("~", ".config")) _XDG_CONFIG_HOME = os.environ.get("XDG_CONFIG_HOME") or os.path.expanduser(os.path.join("~", ".config"))
_file_path = _path.join(_XDG_CONFIG_HOME, "solaar", "rules.yaml") _file_path = os.path.join(_XDG_CONFIG_HOME, "solaar", "rules.yaml")
rules = built_in_rules rules = built_in_rules
@ -1517,7 +1523,7 @@ def _save_config_rule_file(file_name=_file_path):
def blockseq_rep(dumper, data): def blockseq_rep(dumper, data):
return dumper.represent_sequence("tag:yaml.org,2002:seq", data, flow_style=True) return dumper.represent_sequence("tag:yaml.org,2002:seq", data, flow_style=True)
_yaml_add_representer(inline_list, blockseq_rep) yaml.add_representer(inline_list, blockseq_rep)
def convert(elem): def convert(elem):
if isinstance(elem, list): if isinstance(elem, list):
@ -1550,7 +1556,7 @@ def _save_config_rule_file(file_name=_file_path):
with open(file_name, "w") as f: with open(file_name, "w") as f:
if rules_to_save: if rules_to_save:
f.write("%YAML 1.3\n") # Write version manually f.write("%YAML 1.3\n") # Write version manually
_yaml_dump_all(convert([r["Rule"] for r in rules_to_save]), f, **dump_settings) yaml.dump_all(convert([r["Rule"] for r in rules_to_save]), f, **dump_settings)
except Exception as e: except Exception as e:
logger.error("failed to save to %s\n%s", file_name, e) logger.error("failed to save to %s\n%s", file_name, e)
return False return False
@ -1561,7 +1567,7 @@ def load_config_rule_file():
"""Loads user configured rules.""" """Loads user configured rules."""
global rules global rules
if _path.isfile(_file_path): if os.path.isfile(_file_path):
rules = _load_rule_config(_file_path) rules = _load_rule_config(_file_path)
@ -1570,7 +1576,7 @@ def _load_rule_config(file_path: str) -> Rule:
try: try:
with open(file_path) as config_file: with open(file_path) as config_file:
loaded_rules = [] 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) rule = Rule(loaded_rule, source=file_path)
if logger.isEnabledFor(logging.DEBUG): if logger.isEnabledFor(logging.DEBUG):
logger.debug("load rule: %s", rule) logger.debug("load rule: %s", rule)

View File

@ -15,14 +15,14 @@
## with this program; if not, write to the Free Software Foundation, Inc., ## with this program; if not, write to the Free Software Foundation, Inc.,
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ## 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. # 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 """Raised when trying to talk through a previously open handle, when the
receiver is no longer available. Should only happen if the receiver is receiver is no longer available. Should only happen if the receiver is
physically disconnected from the machine, or its kernel driver module is physically disconnected from the machine, or its kernel driver module is
@ -31,25 +31,25 @@ class NoReceiver(_KwException):
pass pass
class NoSuchDevice(_KwException): class NoSuchDevice(KwException):
"""Raised when trying to reach a device number not paired to the receiver.""" """Raised when trying to reach a device number not paired to the receiver."""
pass pass
class DeviceUnreachable(_KwException): class DeviceUnreachable(KwException):
"""Raised when a request is made to an unreachable (turned off) device.""" """Raised when a request is made to an unreachable (turned off) device."""
pass pass
class FeatureNotSupported(_KwException): class FeatureNotSupported(KwException):
"""Raised when trying to request a feature not supported by the device.""" """Raised when trying to request a feature not supported by the device."""
pass pass
class FeatureCallError(_KwException): class FeatureCallError(KwException):
"""Raised if the device replied to a feature call with an error.""" """Raised if the device replied to a feature call with an error."""
pass pass

View File

@ -21,13 +21,10 @@ from typing import Any
from typing_extensions import Protocol from typing_extensions import Protocol
from . import common
from .common import Battery from .common import Battery
from .common import BatteryLevelApproximation from .common import BatteryLevelApproximation
from .common import BatteryStatus 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 .hidpp10_constants import REGISTERS
from .hidpp20_constants import FIRMWARE_KIND from .hidpp20_constants import FIRMWARE_KIND
@ -123,26 +120,26 @@ class Hidpp10:
# won't be able to read any of it now... # won't be able to read any of it now...
return 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]}" fw_version = f"{fw_version[0:2]}.{fw_version[2:4]}"
reply = read_register(device, REGISTERS.firmware, 0x02) reply = read_register(device, REGISTERS.firmware, 0x02)
if reply: if reply:
fw_version += ".B" + _strhex(reply[1:3]) fw_version += ".B" + common.strhex(reply[1:3])
fw = _FirmwareInfo(FIRMWARE_KIND.Firmware, "", fw_version, None) fw = common.FirmwareInfo(FIRMWARE_KIND.Firmware, "", fw_version, None)
firmware[0] = fw firmware[0] = fw
reply = read_register(device, REGISTERS.firmware, 0x04) reply = read_register(device, REGISTERS.firmware, 0x04)
if reply: 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_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 firmware[1] = bl
reply = read_register(device, REGISTERS.firmware, 0x03) reply = read_register(device, REGISTERS.firmware, 0x03)
if reply: 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_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 firmware[2] = o
if any(firmware): if any(firmware):
@ -205,7 +202,7 @@ class Hidpp10:
flag_bits = sum(int(b) for b in flag_bits) flag_bits = sum(int(b) for b in flag_bits)
assert flag_bits & 0x00FFFFFF == 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 return result is not None
def get_device_features(self, device: Device): def get_device_features(self, device: Device):
@ -224,7 +221,7 @@ class Hidpp10:
flags = read_register(device, register) flags = read_register(device, register)
if flags is not None: if flags is not None:
assert len(flags) == 3 assert len(flags) == 3
return _bytes2int(flags) return common.bytes2int(flags)
def parse_battery_status(register, reply) -> Battery | None: def parse_battery_status(register, reply) -> Battery | None:

View File

@ -17,33 +17,27 @@
import logging import logging
import socket 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 Any
from typing import List from typing import List
from typing import Optional from typing import Optional
from typing import Tuple from typing import Tuple
import yaml as _yaml import yaml
from solaar.i18n import _ from solaar.i18n import _
from typing_extensions import Protocol from typing_extensions import Protocol
from . import common
from . import exceptions from . import exceptions
from . import hidpp10_constants as _hidpp10_constants from . import hidpp10_constants
from . import special_keys from . import special_keys
from .common import Battery from .common import Battery
from .common import BatteryLevelApproximation from .common import BatteryLevelApproximation
from .common import BatteryStatus from .common import BatteryStatus
from .common import FirmwareInfo as _FirmwareInfo from .common import NamedInt
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 .hidpp20_constants import CHARGE_LEVEL from .hidpp20_constants import CHARGE_LEVEL
from .hidpp20_constants import CHARGE_STATUS from .hidpp20_constants import CHARGE_STATUS
from .hidpp20_constants import CHARGE_TYPE from .hidpp20_constants import CHARGE_TYPE
@ -57,7 +51,7 @@ logger = logging.getLogger(__name__)
FixedBytes5 = bytes 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): class Device(Protocol):
@ -103,7 +97,7 @@ class FeaturesArray(dict):
return False return False
if self.count > 0: if self.count > 0:
return True 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: if reply is not None:
fs_index = reply[0] fs_index = reply[0]
if fs_index: if fs_index:
@ -120,7 +114,7 @@ class FeaturesArray(dict):
self.supported = False self.supported = False
return False return False
def get_feature(self, index: int) -> Optional[_NamedInt]: def get_feature(self, index: int) -> Optional[NamedInt]:
feature = self.inverse.get(index) feature = self.inverse.get(index)
if feature is not None: if feature is not None:
return feature return feature
@ -130,7 +124,7 @@ class FeaturesArray(dict):
return feature return feature
response = self.device.feature_request(FEATURE.FEATURE_SET, 0x10, index) response = self.device.feature_request(FEATURE.FEATURE_SET, 0x10, index)
if response: if response:
feature = FEATURE[_unpack("!H", response[:2])[0]] feature = FEATURE[struct.unpack("!H", response[:2])[0]]
self[feature] = index self[feature] = index
self.version[feature] = response[3] self.version[feature] = response[3]
return feature return feature
@ -141,15 +135,15 @@ class FeaturesArray(dict):
feature = self.get_feature(index) feature = self.get_feature(index)
yield 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]: if self[feature]:
return self.version.get(feature, 0) return self.version.get(feature, 0)
def __contains__(self, feature: _NamedInt) -> bool: def __contains__(self, feature: NamedInt) -> bool:
index = self.__getitem__(feature) index = self.__getitem__(feature)
return index is not None and index is not False 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) index = super().get(feature)
if index is not None: if index is not None:
return index return index
@ -157,7 +151,7 @@ class FeaturesArray(dict):
index = super().get(feature) index = super().get(feature)
if index is not None: if index is not None:
return index return index
response = self.device.request(0x0000, _pack("!H", feature)) response = self.device.request(0x0000, struct.pack("!H", feature))
if response: if response:
index = response[0] index = response[0]
self[feature] = index if index else False self[feature] = index if index else False
@ -185,8 +179,8 @@ class ReprogrammableKey:
Ref: https://drive.google.com/file/d/0BxbRzx7vEV7eU3VfMnRuRXktZ3M/view Ref: https://drive.google.com/file/d/0BxbRzx7vEV7eU3VfMnRuRXktZ3M/view
Read-only properties: Read-only properties:
- index {int} -- index in the control ID table - index {int} -- index in the control ID table
- key {_NamedInt} -- the name of this control - key {NamedInt} -- the name of this control
- default_task {_NamedInt} -- the native function of this control - default_task {NamedInt} -- the native function of this control
- flags {List[str]} -- capabilities and desired software handling of the control - flags {List[str]} -- capabilities and desired software handling of the control
""" """
@ -198,17 +192,17 @@ class ReprogrammableKey:
self._flags = flags self._flags = flags
@property @property
def key(self) -> _NamedInt: def key(self) -> NamedInt:
return special_keys.CONTROL[self._cid] return special_keys.CONTROL[self._cid]
@property @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 """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 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 than presenting details of controls vs tasks in the interface. The same
convention applies to `mapped_to`, `remappable_to`, `remap` in `ReprogrammableKeyV4`.""" convention applies to `mapped_to`, `remappable_to`, `remap` in `ReprogrammableKeyV4`."""
task = str(special_keys.TASK[self._tid]) task = str(special_keys.TASK[self._tid])
return _NamedInt(self._cid, task) return NamedInt(self._cid, task)
@property @property
def flags(self) -> List[str]: def flags(self) -> List[str]:
@ -227,8 +221,8 @@ class ReprogrammableKeyV4(ReprogrammableKey):
- group {int} -- the group this control belongs to; other controls with this group in their - 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` can be remapped to this control
- group_mask {List[str]} -- this control can be remapped to any control ID in these groups - 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 - 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 - remappable_to {List[NamedInt]} -- list of actions which this control can be remapped to
- mapping_flags {List[str]} -- mapping flags set on the control - mapping_flags {List[str]} -- mapping flags set on the control
""" """
@ -245,24 +239,24 @@ class ReprogrammableKeyV4(ReprogrammableKey):
return special_keys.CID_GROUP_BIT.flag_names(self._gmask) return special_keys.CID_GROUP_BIT.flag_names(self._gmask)
@property @property
def mapped_to(self) -> _NamedInt: def mapped_to(self) -> NamedInt:
if self._mapped_to is None: if self._mapped_to is None:
self._getCidReporting() self._getCidReporting()
self._device.keys._ensure_all_keys_queried() self._device.keys._ensure_all_keys_queried()
task = str(special_keys.TASK[self._device.keys.cid_to_tid[self._mapped_to]]) 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 @property
def remappable_to(self) -> _NamedInts: def remappable_to(self) -> common.NamedInts:
self._device.keys._ensure_all_keys_queried() 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 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 ret[self.default_task] = self.default_task # it should always be possible to map the key to itself
for g in self.group_mask: for g in self.group_mask:
g = special_keys.CID_GROUP[str(g)] g = special_keys.CID_GROUP[str(g)]
for tgt_cid in self._device.keys.group_cids[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 = 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 if tgt_task != self.default_task: # don't put itself in twice
ret[tgt_task] = tgt_task ret[tgt_task] = tgt_task
return ret return ret
@ -288,15 +282,15 @@ class ReprogrammableKeyV4(ReprogrammableKey):
flags = {special_keys.MAPPING_FLAG.raw_XY_diverted: value} flags = {special_keys.MAPPING_FLAG.raw_XY_diverted: value}
self._setCidReporting(flags=flags) self._setCidReporting(flags=flags)
def remap(self, to: _NamedInt): def remap(self, to: NamedInt):
"""Temporarily remaps this control to another action.""" """Temporarily remaps this control to another action."""
self._setCidReporting(remap=int(to)) self._setCidReporting(remap=int(to))
def _getCidReporting(self): def _getCidReporting(self):
try: 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: 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): if cid != self._cid and logger.isEnabledFor(logging.WARNING):
logger.warning( logger.warning(
f"REPROG_CONTROLS_V4 endpoint getCidReporting on device {self._device} replied " f"REPROG_CONTROLS_V4 endpoint getCidReporting on device {self._device} replied "
@ -304,7 +298,7 @@ class ReprogrammableKeyV4(ReprogrammableKey):
) )
self._mapped_to = mapped_to if mapped_to != 0 else self._cid self._mapped_to = mapped_to if mapped_to != 0 else self._cid
if len(mapped_data) > 5: 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: else:
mapping_flags_2 = 0 mapping_flags_2 = 0
self._mapping_flags = mapping_flags_1 | (mapping_flags_2 << 8) self._mapping_flags = mapping_flags_1 | (mapping_flags_2 << 8)
@ -322,7 +316,7 @@ class ReprogrammableKeyV4(ReprogrammableKey):
def _setCidReporting(self, flags=None, remap=0): def _setCidReporting(self, flags=None, remap=0):
"""Sends a `setCidReporting` request with the given parameters. Raises an exception if the parameters are invalid. """Sends a `setCidReporting` request with the given parameters. Raises an exception if the parameters are invalid.
Parameters: 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 - remap {int} -- which control ID to remap to; or 0 to keep current mapping
""" """
flags = flags if flags else {} # See flake8 B006 flags = flags if flags else {} # See flake8 B006
@ -365,11 +359,11 @@ class ReprogrammableKeyV4(ReprogrammableKey):
if remap != 0: # update mapping if changing (even if not already read) if remap != 0: # update mapping if changing (even if not already read)
self._mapped_to = remap 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. # 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. # 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) 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.") logger.debug(f"REPROG_CONTROLS_v4 setCidReporting on device {self._device} didn't echo request packet.")
@ -384,11 +378,11 @@ class PersistentRemappableAction:
self.cidStatus = cidStatus self.cidStatus = cidStatus
@property @property
def key(self) -> _NamedInt: def key(self) -> NamedInt:
return special_keys.CONTROL[self._cid] return special_keys.CONTROL[self._cid]
@property @property
def actionType(self) -> _NamedInt: def actionType(self) -> NamedInt:
return special_keys.ACTIONID[self.actionId] return special_keys.ACTIONID[self.actionId]
@property @property
@ -422,16 +416,18 @@ class PersistentRemappableAction:
@property @property
def data_bytes(self): 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): def remap(self, data_bytes):
cid = _int2bytes(self._cid, 2) cid = common.int2bytes(self._cid, 2)
if _bytes2int(data_bytes) == special_keys.KEYS_Default: # map back to default 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.feature_request(FEATURE.PERSISTENT_REMAPPABLE_ACTION, 0x50, cid, 0xFF)
self._device.remap_keys._query_key(self.index) self._device.remap_keys._query_key(self.index)
return self._device.remap_keys.keys[self.index].data_bytes return self._device.remap_keys.keys[self.index].data_bytes
else: 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.cidStatus = 0x01
self._device.feature_request(FEATURE.PERSISTENT_REMAPPABLE_ACTION, 0x40, cid, 0xFF, data_bytes) self._device.feature_request(FEATURE.PERSISTENT_REMAPPABLE_ACTION, 0x40, cid, 0xFF, data_bytes)
return True return True
@ -443,7 +439,7 @@ class KeysArray:
def __init__(self, device, count, version): def __init__(self, device, count, version):
assert device is not None assert device is not None
self.device = device self.device = device
self.lock = _threading.Lock() self.lock = threading.Lock()
if FEATURE.REPROG_CONTROLS_V4 in self.device.features: if FEATURE.REPROG_CONTROLS_V4 in self.device.features:
self.keyversion = FEATURE.REPROG_CONTROLS_V4 self.keyversion = FEATURE.REPROG_CONTROLS_V4
elif FEATURE.REPROG_CONTROLS_V2 in self.device.features: elif FEATURE.REPROG_CONTROLS_V2 in self.device.features:
@ -510,7 +506,7 @@ class KeysArrayV2(KeysArray):
raise IndexError(index) raise IndexError(index)
keydata = self.device.feature_request(FEATURE.REPROG_CONTROLS, 0x10, index) keydata = self.device.feature_request(FEATURE.REPROG_CONTROLS, 0x10, index)
if keydata: 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.keys[index] = ReprogrammableKey(self.device, index, cid, tid, flags)
self.cid_to_tid[cid] = tid self.cid_to_tid[cid] = tid
elif logger.isEnabledFor(logging.WARNING): elif logger.isEnabledFor(logging.WARNING):
@ -526,7 +522,7 @@ class KeysArrayV4(KeysArrayV2):
raise IndexError(index) raise IndexError(index)
keydata = self.device.feature_request(FEATURE.REPROG_CONTROLS_V4, 0x10, index) keydata = self.device.feature_request(FEATURE.REPROG_CONTROLS_V4, 0x10, index)
if keydata: 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) flags = flags1 | (flags2 << 8)
self.keys[index] = ReprogrammableKeyV4(self.device, index, cid, tid, flags, pos, group, gmask) self.keys[index] = ReprogrammableKeyV4(self.device, index, cid, tid, flags, pos, group, gmask)
self.cid_to_tid[cid] = tid self.cid_to_tid[cid] = tid
@ -547,7 +543,7 @@ class KeysArrayPersistent(KeysArray):
if self._capabilities is None and self.device.online: if self._capabilities is None and self.device.online:
capabilities = self.device.feature_request(FEATURE.PERSISTENT_REMAPPABLE_ACTION, 0x00) capabilities = self.device.feature_request(FEATURE.PERSISTENT_REMAPPABLE_ACTION, 0x00)
assert capabilities, "Oops, persistent remappable key capabilities cannot be retrieved!" 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 return self._capabilities
def _query_key(self, index: int): def _query_key(self, index: int):
@ -555,10 +551,10 @@ class KeysArrayPersistent(KeysArray):
raise IndexError(index) raise IndexError(index)
keydata = self.device.feature_request(FEATURE.PERSISTENT_REMAPPABLE_ACTION, 0x20, index, 0xFF) keydata = self.device.feature_request(FEATURE.PERSISTENT_REMAPPABLE_ACTION, 0x20, index, 0xFF)
if keydata: 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) mapped_data = self.device.feature_request(FEATURE.PERSISTENT_REMAPPABLE_ACTION, 0x30, key >> 8, key & 0xFF, 0xFF)
if mapped_data: 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: else:
actionId = remapped = modifiers = status = 0 actionId = remapped = modifiers = status = 0
actionId = special_keys.ACTIONID[actionId] actionId = special_keys.ACTIONID[actionId]
@ -578,7 +574,7 @@ class KeysArrayPersistent(KeysArray):
# Param Ids for feature GESTURE_2 # Param Ids for feature GESTURE_2
PARAM = _NamedInts( PARAM = common.NamedInts(
ExtraCapabilities=1, # not suitable for use ExtraCapabilities=1, # not suitable for use
PixelZone=2, # 4 2-byte integers, left, bottom, width, height; pixels PixelZone=2, # 4 2-byte integers, left, bottom, width, height; pixels
RatioZone=3, # 4 bytes, left, bottom, width, height; unit 1/240 pad size RatioZone=3, # 4 bytes, left, bottom, width, height; unit 1/240 pad size
@ -622,7 +618,7 @@ SUB_PARAM = { # (byte count, minimum, maximum)
} }
# Spec Ids for feature GESTURE_2 # Spec Ids for feature GESTURE_2
SPEC = _NamedInts( SPEC = common.NamedInts(
DVI_field_width=1, DVI_field_width=1,
field_widths=2, field_widths=2,
period_unit=3, period_unit=3,
@ -637,7 +633,7 @@ SPEC = _NamedInts(
SPEC._fallback = lambda x: f"unknown:{x:04X}" SPEC._fallback = lambda x: f"unknown:{x:04X}"
# Action Ids for feature GESTURE_2 # Action Ids for feature GESTURE_2
ACTION_ID = _NamedInts( ACTION_ID = common.NamedInts(
MovePointer=1, MovePointer=1,
ScrollHorizontal=2, ScrollHorizontal=2,
WheelScrolling=3, WheelScrolling=3,
@ -750,7 +746,7 @@ class Param:
def read(self): # returns the bytes for the parameter def read(self): # returns the bytes for the parameter
result = self._device.feature_request(FEATURE.GESTURE_2, 0x70, self.index, 0xFF) result = self._device.feature_request(FEATURE.GESTURE_2, 0x70, self.index, 0xFF)
if result: if result:
self._value = _bytes2int(result[: self.size]) self._value = common.bytes2int(result[: self.size])
return self._value return self._value
@property @property
@ -762,7 +758,7 @@ class Param:
def _read_default(self): def _read_default(self):
result = self._device.feature_request(FEATURE.GESTURE_2, 0x60, self.index, 0xFF) result = self._device.feature_request(FEATURE.GESTURE_2, 0x60, self.index, 0xFF)
if result: if result:
self._default_value = _bytes2int(result[: self.size]) self._default_value = common.bytes2int(result[: self.size])
return self._default_value return self._default_value
def write(self, bytes): def write(self, bytes):
@ -799,7 +795,7 @@ class Spec:
f"Feature Call Error reading Gesture Spec on device {self._device} for spec {self.id} - use None" f"Feature Call Error reading Gesture Spec on device {self._device} for spec {self.id} - use None"
) )
return None return None
return _bytes2int(value[: self.byte_count]) return common.bytes2int(value[: self.byte_count])
def __repr__(self): def __repr__(self):
return f"[{self.spec}={self.value}]" return f"[{self.spec}={self.value}]"
@ -883,7 +879,7 @@ class Backlight:
if not response: if not response:
raise exceptions.FeatureCallError(msg="No reply from device.") raise exceptions.FeatureCallError(msg="No reply from device.")
self.device = 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(
"<BBBHBHHH", response[:12] "<BBBHBHHH", response[:12]
) )
self.auto_supported = supported & 0x08 self.auto_supported = supported & 0x08
@ -894,7 +890,7 @@ class Backlight:
def write(self): def write(self):
self.options = (self.options & 0x07) | (self.mode << 3) self.options = (self.options & 0x07) | (self.mode << 3)
level = self.level if self.mode == 0x3 else 0 level = self.level if self.mode == 0x3 else 0
data_bytes = _pack("<BBBBHHH", self.enabled, self.options, 0xFF, level, self.dho, self.dhi, self.dpow) data_bytes = struct.pack("<BBBBHHH", self.enabled, self.options, 0xFF, level, self.dho, self.dhi, self.dpow)
return self.device.feature_request(FEATURE.BACKLIGHT2, 0x10, data_bytes) return self.device.feature_request(FEATURE.BACKLIGHT2, 0x10, data_bytes)
@ -908,8 +904,8 @@ class LEDParam:
saturation = "saturation" saturation = "saturation"
LEDRampChoices = _NamedInts(default=0, yes=1, no=2) LEDRampChoices = common.NamedInts(default=0, yes=1, no=2)
LEDFormChoices = _NamedInts(default=0, sine=1, square=2, triangle=3, sawtooth=4, sharkfin=5, exponential=6) LEDFormChoices = common.NamedInts(default=0, sine=1, square=2, triangle=3, sawtooth=4, sharkfin=5, exponential=6)
LEDParamSize = { LEDParamSize = {
LEDParam.color: 3, LEDParam.color: 3,
LEDParam.speed: 1, LEDParam.speed: 1,
@ -922,18 +918,18 @@ LEDParamSize = {
# not implemented from x8070 Wave=4, Stars=5, Press=6, Audio=7 # not implemented from x8070 Wave=4, Stars=5, Press=6, Audio=7
# not implemented from x8071 Custom=12, Kitt=13, HSVPulsing=20, WaveC=22, RippleC=23, SignatureActive=24, SignaturePassive=25 # not implemented from x8071 Custom=12, Kitt=13, HSVPulsing=20, WaveC=22, RippleC=23, SignatureActive=24, SignaturePassive=25
LEDEffects = { LEDEffects = {
0x00: [_NamedInt(0x00, _("Disabled")), {}], 0x00: [NamedInt(0x00, _("Disabled")), {}],
0x01: [_NamedInt(0x01, _("Static")), {LEDParam.color: 0, LEDParam.ramp: 3}], 0x01: [NamedInt(0x01, _("Static")), {LEDParam.color: 0, LEDParam.ramp: 3}],
0x02: [_NamedInt(0x02, _("Pulse")), {LEDParam.color: 0, LEDParam.speed: 3}], 0x02: [NamedInt(0x02, _("Pulse")), {LEDParam.color: 0, LEDParam.speed: 3}],
0x03: [_NamedInt(0x03, _("Cycle")), {LEDParam.period: 5, LEDParam.intensity: 7}], 0x03: [NamedInt(0x03, _("Cycle")), {LEDParam.period: 5, LEDParam.intensity: 7}],
0x08: [_NamedInt(0x08, _("Boot")), {}], 0x08: [NamedInt(0x08, _("Boot")), {}],
0x09: [_NamedInt(0x09, _("Demo")), {}], 0x09: [NamedInt(0x09, _("Demo")), {}],
0x0A: [_NamedInt(0x0A, _("Breathe")), {LEDParam.color: 0, LEDParam.period: 3, LEDParam.form: 5, LEDParam.intensity: 6}], 0x0A: [NamedInt(0x0A, _("Breathe")), {LEDParam.color: 0, LEDParam.period: 3, LEDParam.form: 5, LEDParam.intensity: 6}],
0x0B: [_NamedInt(0x0B, _("Ripple")), {LEDParam.color: 0, LEDParam.period: 4}], 0x0B: [NamedInt(0x0B, _("Ripple")), {LEDParam.color: 0, LEDParam.period: 4}],
0x0E: [_NamedInt(0x0E, _("Decomposition")), {LEDParam.period: 6, LEDParam.intensity: 8}], 0x0E: [NamedInt(0x0E, _("Decomposition")), {LEDParam.period: 6, LEDParam.intensity: 8}],
0x0F: [_NamedInt(0x0F, _("Signature1")), {LEDParam.period: 5, LEDParam.intensity: 7}], 0x0F: [NamedInt(0x0F, _("Signature1")), {LEDParam.period: 5, LEDParam.intensity: 7}],
0x10: [_NamedInt(0x10, _("Signature2")), {LEDParam.period: 5, LEDParam.intensity: 7}], 0x10: [NamedInt(0x10, _("Signature2")), {LEDParam.period: 5, LEDParam.intensity: 7}],
0x15: [_NamedInt(0x15, _("CycleS")), {LEDParam.saturation: 1, LEDParam.period: 6, LEDParam.intensity: 8}], 0x15: [NamedInt(0x15, _("CycleS")), {LEDParam.saturation: 1, LEDParam.period: 6, LEDParam.intensity: 8}],
} }
@ -950,7 +946,7 @@ class LEDEffectSetting: # an effect plus its parameters
args = {"ID": effect[0] if effect else None} args = {"ID": effect[0] if effect else None}
if effect: if effect:
for p, b in effect[1].items(): for p, b in effect[1].items():
args[str(p)] = _bytes2int(bytes[1 + b : 1 + b + LEDParamSize[p]]) args[str(p)] = common.bytes2int(bytes[1 + b : 1 + b + LEDParamSize[p]])
else: else:
args["bytes"] = bytes args["bytes"] = bytes
return cls(**args) return cls(**args)
@ -962,10 +958,10 @@ class LEDEffectSetting: # an effect plus its parameters
else: else:
bs = [0] * 10 bs = [0] * 10
for p, b in LEDEffects[ID][1].items(): for p, b in LEDEffects[ID][1].items():
bs[b : b + LEDParamSize[p]] = _int2bytes(getattr(self, str(p), 0), LEDParamSize[p]) bs[b : b + LEDParamSize[p]] = common.int2bytes(getattr(self, str(p), 0), LEDParamSize[p])
if options is not None: if options is not None:
ID = next((ze.index for ze in options if ze.ID == ID), None) ID = next((ze.index for ze in options if ze.ID == ID), None)
result = _int2bytes(ID, 1) + bytes(bs) result = common.int2bytes(ID, 1) + bytes(bs)
return result return result
@classmethod @classmethod
@ -980,23 +976,23 @@ class LEDEffectSetting: # an effect plus its parameters
return type(self) == type(other) and self.to_bytes() == other.to_bytes() return type(self) == type(other) and self.to_bytes() == other.to_bytes()
def __str__(self): def __str__(self):
return _yaml.dump(self, width=float("inf")).rstrip("\n") return yaml.dump(self, width=float("inf")).rstrip("\n")
_yaml.SafeLoader.add_constructor("!LEDEffectSetting", LEDEffectSetting.from_yaml) yaml.SafeLoader.add_constructor("!LEDEffectSetting", LEDEffectSetting.from_yaml)
_yaml.add_representer(LEDEffectSetting, LEDEffectSetting.to_yaml) yaml.add_representer(LEDEffectSetting, LEDEffectSetting.to_yaml)
class LEDEffectInfo: # an effect that a zone can do class LEDEffectInfo: # an effect that a zone can do
def __init__(self, feature, function, device, zindex, eindex): def __init__(self, feature, function, device, zindex, eindex):
info = device.feature_request(feature, function, zindex, eindex, 0x00) info = device.feature_request(feature, function, zindex, eindex, 0x00)
self.zindex, self.index, self.ID, self.capabilities, self.period = _unpack("!BBHHH", info[0:8]) self.zindex, self.index, self.ID, self.capabilities, self.period = struct.unpack("!BBHHH", info[0:8])
def __str__(self): def __str__(self):
return f"LEDEffectInfo({self.zindex}, {self.index}, {self.ID}, {self.capabilities: x}, {self.period})" return f"LEDEffectInfo({self.zindex}, {self.index}, {self.ID}, {self.capabilities: x}, {self.period})"
LEDZoneLocations = _NamedInts() LEDZoneLocations = common.NamedInts()
LEDZoneLocations[0x00] = _("Unknown Location") LEDZoneLocations[0x00] = _("Unknown Location")
LEDZoneLocations[0x01] = _("Primary") LEDZoneLocations[0x01] = _("Primary")
LEDZoneLocations[0x02] = _("Logo") LEDZoneLocations[0x02] = _("Logo")
@ -1014,7 +1010,7 @@ LEDZoneLocations[0x0B] = _("Primary 6")
class LEDZoneInfo: # effects that a zone can do class LEDZoneInfo: # effects that a zone can do
def __init__(self, feature, function, offset, effect_function, device, index): def __init__(self, feature, function, offset, effect_function, device, index):
info = device.feature_request(feature, function, index, 0xFF, 0x00) info = device.feature_request(feature, function, index, 0xFF, 0x00)
self.location, self.count = _unpack("!HB", info[1 + offset : 4 + offset]) self.location, self.count = struct.unpack("!HB", info[1 + offset : 4 + offset])
self.index = index self.index = index
self.location = LEDZoneLocations[self.location] if LEDZoneLocations[self.location] else self.location self.location = LEDZoneLocations[self.location] if LEDZoneLocations[self.location] else self.location
self.effects = [] self.effects = []
@ -1025,7 +1021,7 @@ class LEDZoneInfo: # effects that a zone can do
for i in range(0, len(self.effects)): for i in range(0, len(self.effects)):
e = self.effects[i] e = self.effects[i]
if e.ID == setting.ID: if e.ID == setting.ID:
return _int2bytes(self.index, 1) + _int2bytes(i, 1) + setting.to_bytes()[1:] return common.int2bytes(self.index, 1) + common.int2bytes(i, 1) + setting.to_bytes()[1:]
return None return None
def __str__(self): def __str__(self):
@ -1036,7 +1032,7 @@ class LEDEffectsInfo: # effects that the LEDs can do, using COLOR_LED_EFFECTS
def __init__(self, device): def __init__(self, device):
self.device = device self.device = device
info = device.feature_request(FEATURE.COLOR_LED_EFFECTS, 0x00) info = device.feature_request(FEATURE.COLOR_LED_EFFECTS, 0x00)
self.count, _, capabilities = _unpack("!BHH", info[0:5]) self.count, _, capabilities = struct.unpack("!BHH", info[0:5])
self.readable = capabilities & 0x1 self.readable = capabilities & 0x1
self.zones = [] self.zones = []
for i in range(0, self.count): for i in range(0, self.count):
@ -1054,16 +1050,16 @@ class RGBEffectsInfo(LEDEffectsInfo): # effects that the LEDs can do using RGB_
def __init__(self, device): def __init__(self, device):
self.device = device self.device = device
info = device.feature_request(FEATURE.RGB_EFFECTS, 0x00, 0xFF, 0xFF, 0x00) info = device.feature_request(FEATURE.RGB_EFFECTS, 0x00, 0xFF, 0xFF, 0x00)
_, _, self.count, _, capabilities = _unpack("!BBBHH", info[0:7]) _, _, self.count, _, capabilities = struct.unpack("!BBBHH", info[0:7])
self.readable = capabilities & 0x1 self.readable = capabilities & 0x1
self.zones = [] self.zones = []
for i in range(0, self.count): for i in range(0, self.count):
self.zones.append(LEDZoneInfo(FEATURE.RGB_EFFECTS, 0x00, 1, 0x00, device, i)) self.zones.append(LEDZoneInfo(FEATURE.RGB_EFFECTS, 0x00, 1, 0x00, device, i))
ButtonBehaviors = _NamedInts(MacroExecute=0x0, MacroStop=0x1, MacroStopAll=0x2, Send=0x8, Function=0x9) ButtonBehaviors = common.NamedInts(MacroExecute=0x0, MacroStop=0x1, MacroStopAll=0x2, Send=0x8, Function=0x9)
ButtonMappingTypes = _NamedInts(No_Action=0x0, Button=0x1, Modifier_And_Key=0x2, Consumer_Key=0x3) ButtonMappingTypes = common.NamedInts(No_Action=0x0, Button=0x1, Modifier_And_Key=0x2, Consumer_Key=0x3)
ButtonFunctions = _NamedInts( ButtonFunctions = common.NamedInts(
No_Action=0x0, No_Action=0x0,
Tilt_Left=0x1, Tilt_Left=0x1,
Tilt_Right=0x2, Tilt_Right=0x2,
@ -1136,22 +1132,22 @@ class Button:
return result return result
def to_bytes(self): def to_bytes(self):
bytes = _int2bytes(self.behavior << 4, 1) if self.behavior is not None else None bytes = common.int2bytes(self.behavior << 4, 1) if self.behavior is not None else None
if self.behavior == ButtonBehaviors.MacroExecute or self.behavior == ButtonBehaviors.MacroStop: if self.behavior == ButtonBehaviors.MacroExecute or self.behavior == ButtonBehaviors.MacroStop:
bytes = _int2bytes((self.behavior << 12) + self.sector, 2) + _int2bytes(self.address, 2) bytes = common.int2bytes((self.behavior << 12) + self.sector, 2) + common.int2bytes(self.address, 2)
elif self.behavior == ButtonBehaviors.Send: elif self.behavior == ButtonBehaviors.Send:
bytes += _int2bytes(self.type, 1) bytes += common.int2bytes(self.type, 1)
if self.type == ButtonMappingTypes.Button: if self.type == ButtonMappingTypes.Button:
bytes += _int2bytes(self.value, 2) bytes += common.int2bytes(self.value, 2)
elif self.type == ButtonMappingTypes.Modifier_And_Key: elif self.type == ButtonMappingTypes.Modifier_And_Key:
bytes += _int2bytes(self.modifiers, 1) bytes += common.int2bytes(self.modifiers, 1)
bytes += _int2bytes(self.value, 1) bytes += common.int2bytes(self.value, 1)
elif self.type == ButtonMappingTypes.Consumer_Key: elif self.type == ButtonMappingTypes.Consumer_Key:
bytes += _int2bytes(self.value, 2) bytes += common.int2bytes(self.value, 2)
elif self.type == ButtonMappingTypes.No_Action: elif self.type == ButtonMappingTypes.No_Action:
bytes += b"\xff\xff" bytes += b"\xff\xff"
elif self.behavior == ButtonBehaviors.Function: elif self.behavior == ButtonBehaviors.Function:
bytes += _int2bytes(self.value, 1) + b"\xff" + (_int2bytes(self.data, 1) if self.data else b"\x00") bytes += common.int2bytes(self.value, 1) + b"\xff" + (common.int2bytes(self.data, 1) if self.data else b"\x00")
else: else:
bytes = self.bytes if self.bytes else b"\xff\xff\xff\xff" bytes = self.bytes if self.bytes else b"\xff\xff\xff\xff"
return bytes return bytes
@ -1163,8 +1159,8 @@ class Button:
) )
_yaml.SafeLoader.add_constructor("!Button", Button.from_yaml) yaml.SafeLoader.add_constructor("!Button", Button.from_yaml)
_yaml.add_representer(Button, Button.to_yaml) yaml.add_representer(Button, Button.to_yaml)
class OnboardProfile: class OnboardProfile:
@ -1191,19 +1187,19 @@ class OnboardProfile:
report_rate=bytes[0], report_rate=bytes[0],
resolution_default_index=bytes[1], resolution_default_index=bytes[1],
resolution_shift_index=bytes[2], resolution_shift_index=bytes[2],
resolutions=[_unpack("<H", bytes[i * 2 + 3 : i * 2 + 5])[0] for i in range(0, 5)], resolutions=[struct.unpack("<H", bytes[i * 2 + 3 : i * 2 + 5])[0] for i in range(0, 5)],
red=bytes[13], red=bytes[13],
green=bytes[14], green=bytes[14],
blue=bytes[15], blue=bytes[15],
power_mode=bytes[16], power_mode=bytes[16],
angle_snap=bytes[17], angle_snap=bytes[17],
write_count=_unpack("<H", bytes[18:20])[0], write_count=struct.unpack("<H", bytes[18:20])[0],
reserved=bytes[20:28], reserved=bytes[20:28],
ps_timeout=_unpack("<H", bytes[28:30])[0], ps_timeout=struct.unpack("<H", bytes[28:30])[0],
po_timeout=_unpack("<H", bytes[30:32])[0], po_timeout=struct.unpack("<H", bytes[30:32])[0],
buttons=[Button.from_bytes(bytes[32 + i * 4 : 32 + i * 4 + 4]) for i in range(0, buttons)], buttons=[Button.from_bytes(bytes[32 + i * 4 : 32 + i * 4 + 4]) for i in range(0, buttons)],
gbuttons=[Button.from_bytes(bytes[96 + i * 4 : 96 + i * 4 + 4]) for i in range(0, gbuttons)], gbuttons=[Button.from_bytes(bytes[96 + i * 4 : 96 + i * 4 + 4]) for i in range(0, gbuttons)],
name=bytes[160:208].decode("utf-16le").rstrip("\x00").rstrip("\uFFFF"), name=bytes[160:208].decode("utf-16le").rstrip("\x00").rstrip("\uffff"),
lighting=[LEDEffectSetting.from_bytes(bytes[208 + i * 11 : 219 + i * 11]) for i in range(0, 4)], lighting=[LEDEffectSetting.from_bytes(bytes[208 + i * 11 : 219 + i * 11]) for i in range(0, 4)],
) )
@ -1213,11 +1209,11 @@ class OnboardProfile:
return cls.from_bytes(sector, enabled, buttons, gbuttons, bytes) return cls.from_bytes(sector, enabled, buttons, gbuttons, bytes)
def to_bytes(self, length): def to_bytes(self, length):
bytes = _int2bytes(self.report_rate, 1) bytes = common.int2bytes(self.report_rate, 1)
bytes += _int2bytes(self.resolution_default_index, 1) + _int2bytes(self.resolution_shift_index, 1) bytes += common.int2bytes(self.resolution_default_index, 1) + common.int2bytes(self.resolution_shift_index, 1)
bytes += b"".join([self.resolutions[i].to_bytes(2, "little") for i in range(0, 5)]) bytes += b"".join([self.resolutions[i].to_bytes(2, "little") for i in range(0, 5)])
bytes += _int2bytes(self.red, 1) + _int2bytes(self.green, 1) + _int2bytes(self.blue, 1) bytes += common.int2bytes(self.red, 1) + common.int2bytes(self.green, 1) + common.int2bytes(self.blue, 1)
bytes += _int2bytes(self.power_mode, 1) + _int2bytes(self.angle_snap, 1) bytes += common.int2bytes(self.power_mode, 1) + common.int2bytes(self.angle_snap, 1)
bytes += self.write_count.to_bytes(2, "little") + self.reserved bytes += self.write_count.to_bytes(2, "little") + self.reserved
bytes += self.ps_timeout.to_bytes(2, "little") + self.po_timeout.to_bytes(2, "little") bytes += self.ps_timeout.to_bytes(2, "little") + self.po_timeout.to_bytes(2, "little")
for i in range(0, 16): for i in range(0, 16):
@ -1232,7 +1228,7 @@ class OnboardProfile:
bytes += self.lighting[i].to_bytes() bytes += self.lighting[i].to_bytes()
while len(bytes) < length - 2: while len(bytes) < length - 2:
bytes += b"\xff" bytes += b"\xff"
bytes += _int2bytes(_crc16(bytes), 2) bytes += common.int2bytes(common.crc16(bytes), 2)
return bytes return bytes
def dump(self): def dump(self):
@ -1250,8 +1246,8 @@ class OnboardProfile:
print(" G-BUTTON", i + 1, self.gbuttons[i]) print(" G-BUTTON", i + 1, self.gbuttons[i])
_yaml.SafeLoader.add_constructor("!OnboardProfile", OnboardProfile.from_yaml) yaml.SafeLoader.add_constructor("!OnboardProfile", OnboardProfile.from_yaml)
_yaml.add_representer(OnboardProfile, OnboardProfile.to_yaml) yaml.add_representer(OnboardProfile, OnboardProfile.to_yaml)
OnboardProfilesVersion = 3 OnboardProfilesVersion = 3
@ -1279,11 +1275,11 @@ class OnboardProfiles:
headers = [] headers = []
chunk = device.feature_request(FEATURE.ONBOARD_PROFILES, 0x50, 0, 0, 0, i) chunk = device.feature_request(FEATURE.ONBOARD_PROFILES, 0x50, 0, 0, 0, i)
s = 0x00 s = 0x00
if chunk[0:4] == b"\x00\x00\x00\x00" or chunk[0:4] == b"\xFF\xFF\xFF\xFF": # look in ROM instead if chunk[0:4] == b"\x00\x00\x00\x00" or chunk[0:4] == b"\xff\xff\xff\xff": # look in ROM instead
chunk = device.feature_request(FEATURE.ONBOARD_PROFILES, 0x50, 0x01, 0, 0, i) chunk = device.feature_request(FEATURE.ONBOARD_PROFILES, 0x50, 0x01, 0, 0, i)
s = 0x01 s = 0x01
while chunk[0:2] != b"\xff\xff": while chunk[0:2] != b"\xff\xff":
sector, enabled = _unpack("!HB", chunk[0:3]) sector, enabled = struct.unpack("!HB", chunk[0:3])
headers.append((sector, enabled)) headers.append((sector, enabled))
i += 1 i += 1
chunk = device.feature_request(FEATURE.ONBOARD_PROFILES, 0x50, s, 0, 0, i * 4) chunk = device.feature_request(FEATURE.ONBOARD_PROFILES, 0x50, s, 0, 0, i * 4)
@ -1294,10 +1290,10 @@ class OnboardProfiles:
if not device.online: # wake the device up if necessary if not device.online: # wake the device up if necessary
device.ping() device.ping()
response = device.feature_request(FEATURE.ONBOARD_PROFILES, 0x00) response = device.feature_request(FEATURE.ONBOARD_PROFILES, 0x00)
memory, profile, _macro = _unpack("!BBB", response[0:3]) memory, profile, _macro = struct.unpack("!BBB", response[0:3])
if memory != 0x01 or profile > 0x04: if memory != 0x01 or profile > 0x04:
return 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 gbuttons = buttons if (shift & 0x3 == 0x2) else 0
headers = OnboardProfiles.get_profile_headers(device) headers = OnboardProfiles.get_profile_headers(device)
profiles = {} profiles = {}
@ -1319,11 +1315,11 @@ class OnboardProfiles:
def to_bytes(self): def to_bytes(self):
bytes = b"" bytes = b""
for i in range(1, len(self.profiles) + 1): 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 bytes += b"\xff\xff\x00\x00" # marker after last profile
while len(bytes) < self.size - 2: # leave room for CRC while len(bytes) < self.size - 2: # leave room for CRC
bytes += b"\xff" bytes += b"\xff"
bytes += _int2bytes(_crc16(bytes), 2) bytes += common.int2bytes(common.crc16(bytes), 2)
return bytes return bytes
@classmethod @classmethod
@ -1368,11 +1364,11 @@ class OnboardProfiles:
return written return written
def show(self): def show(self):
print(_yaml.dump(self)) print(yaml.dump(self))
_yaml.SafeLoader.add_constructor("!OnboardProfiles", OnboardProfiles.from_yaml) yaml.SafeLoader.add_constructor("!OnboardProfiles", OnboardProfiles.from_yaml)
_yaml.add_representer(OnboardProfiles, OnboardProfiles.to_yaml) yaml.add_representer(OnboardProfiles, OnboardProfiles.to_yaml)
def feature_request(device, feature, function=0x00, *params, no_reply=False): def feature_request(device, feature, function=0x00, *params, no_reply=False):
@ -1417,20 +1413,18 @@ class Hidpp20:
if fw_info: if fw_info:
level = ord(fw_info[:1]) & 0x0F level = ord(fw_info[:1]) & 0x0F
if level == 0 or level == 1: 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}" version = f"{version_major:02X}.{version_minor:02X}"
if build: if build:
version += f".B{build:04X}" version += f".B{build:04X}"
extras = fw_info[9:].rstrip(b"\x00") or None 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: 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: else:
fw_info = _FirmwareInfo(FIRMWARE_KIND.Other, "", "", None) fw_info = common.FirmwareInfo(FIRMWARE_KIND.Other, "", "", None)
fw.append(fw_info) fw.append(fw_info)
# if logger.isEnabledFor(logging.DEBUG):
# logger.debug("device %d firmware %s", devnumber, fw_info)
return tuple(fw) return tuple(fw)
def get_ids(self, device): def get_ids(self, device):
@ -1458,8 +1452,6 @@ class Hidpp20:
kind = device.feature_request(FEATURE.DEVICE_NAME, 0x20) kind = device.feature_request(FEATURE.DEVICE_NAME, 0x20)
if kind: if kind:
kind = ord(kind[:1]) 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]] return KIND_MAP[DEVICE_KIND[kind]]
def get_name(self, device: Device): def get_name(self, device: Device):
@ -1581,7 +1573,7 @@ class Hidpp20:
def get_mouse_pointer_info(self, device: Device): def get_mouse_pointer_info(self, device: Device):
pointer_info = device.feature_request(FEATURE.MOUSE_POINTER) pointer_info = device.feature_request(FEATURE.MOUSE_POINTER)
if pointer_info: 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] acceleration = ("none", "low", "med", "high")[flags & 0x3]
suggest_os_ballistics = (flags & 0x04) != 0 suggest_os_ballistics = (flags & 0x04) != 0
suggest_vertical_orientation = (flags & 0x08) != 0 suggest_vertical_orientation = (flags & 0x08) != 0
@ -1595,7 +1587,7 @@ class Hidpp20:
def get_vertical_scrolling_info(self, device: Device): def get_vertical_scrolling_info(self, device: Device):
vertical_scrolling_info = device.feature_request(FEATURE.VERTICAL_SCROLLING) vertical_scrolling_info = device.feature_request(FEATURE.VERTICAL_SCROLLING)
if vertical_scrolling_info: 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 = ( roller_type = (
"reserved", "reserved",
"standard", "standard",
@ -1611,13 +1603,13 @@ class Hidpp20:
def get_hi_res_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) hi_res_scrolling_info = device.feature_request(FEATURE.HI_RES_SCROLLING)
if hi_res_scrolling_info: 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 return mode, resolution
def get_pointer_speed_info(self, device: Device): def get_pointer_speed_info(self, device: Device):
pointer_speed_info = device.feature_request(FEATURE.POINTER_SPEED) pointer_speed_info = device.feature_request(FEATURE.POINTER_SPEED)
if pointer_speed_info: 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: # if pointer_speed_lo > 0:
# pointer_speed_lo = pointer_speed_lo # pointer_speed_lo = pointer_speed_lo
return pointer_speed_hi + pointer_speed_lo / 256 return pointer_speed_hi + pointer_speed_lo / 256
@ -1625,7 +1617,7 @@ class Hidpp20:
def get_lowres_wheel_status(self, device: Device): def get_lowres_wheel_status(self, device: Device):
lowres_wheel_status = device.feature_request(FEATURE.LOWRES_WHEEL) lowres_wheel_status = device.feature_request(FEATURE.LOWRES_WHEEL)
if lowres_wheel_status: 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] wheel_reporting = ("HID", "HID++")[wheel_flag & 0x01]
return wheel_reporting return wheel_reporting
@ -1636,20 +1628,20 @@ class Hidpp20:
if caps and mode and ratchet: if caps and mode and ratchet:
# Parse caps # Parse caps
multi, flags = _unpack("!BB", caps[:2]) multi, flags = struct.unpack("!BB", caps[:2])
has_invert = (flags & 0x08) != 0 has_invert = (flags & 0x08) != 0
has_ratchet = (flags & 0x04) != 0 has_ratchet = (flags & 0x04) != 0
# Parse mode # Parse mode
wheel_mode, reserved = _unpack("!BB", mode[:2]) wheel_mode, reserved = struct.unpack("!BB", mode[:2])
target = (wheel_mode & 0x01) != 0 target = (wheel_mode & 0x01) != 0
res = (wheel_mode & 0x02) != 0 res = (wheel_mode & 0x02) != 0
inv = (wheel_mode & 0x04) != 0 inv = (wheel_mode & 0x04) != 0
# Parse Ratchet switch # Parse Ratchet switch
ratchet_mode, reserved = _unpack("!BB", ratchet[:2]) ratchet_mode, reserved = struct.unpack("!BB", ratchet[:2])
ratchet = (ratchet_mode & 0x01) != 0 ratchet = (ratchet_mode & 0x01) != 0
@ -1658,7 +1650,7 @@ class Hidpp20:
def get_new_fn_inversion(self, device: Device): def get_new_fn_inversion(self, device: Device):
state = device.feature_request(FEATURE.NEW_FN_INVERSION, 0x00) state = device.feature_request(FEATURE.NEW_FN_INVERSION, 0x00)
if state: if state:
inverted, default_inverted = _unpack("!BB", state[:2]) inverted, default_inverted = struct.unpack("!BB", state[:2])
inverted = (inverted & 0x01) != 0 inverted = (inverted & 0x01) != 0
default_inverted = (default_inverted & 0x01) != 0 default_inverted = (default_inverted & 0x01) != 0
return inverted, default_inverted return inverted, default_inverted
@ -1667,11 +1659,11 @@ class Hidpp20:
state = device.feature_request(FEATURE.HOSTS_INFO, 0x00) state = device.feature_request(FEATURE.HOSTS_INFO, 0x00)
host_names = {} host_names = {}
if state: 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 if capability_flags & 0x01: # device can get host names
for host in range(0, numHosts): for host in range(0, numHosts):
hostinfo = device.feature_request(FEATURE.HOSTS_INFO, 0x10, host) 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 = "" name = ""
remaining = nameLen remaining = nameLen
while remaining > 0: while remaining > 0:
@ -1696,10 +1688,10 @@ class Hidpp20:
logger.info("Setting host name to %s", name) logger.info("Setting host name to %s", name)
state = device.feature_request(FEATURE.HOSTS_INFO, 0x00) state = device.feature_request(FEATURE.HOSTS_INFO, 0x00)
if state: if state:
flags, _ignore, _ignore, currentHost = _unpack("!BBBB", state[:4]) flags, _ignore, _ignore, currentHost = struct.unpack("!BBBB", state[:4])
if flags & 0x02: if flags & 0x02:
hostinfo = device.feature_request(FEATURE.HOSTS_INFO, 0x10, currentHost) 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: if name[:maxNameLen] == currentName[:maxNameLen] and False:
return True return True
length = min(maxNameLen, len(name)) length = min(maxNameLen, len(name))
@ -1715,7 +1707,7 @@ class Hidpp20:
state = device.feature_request(FEATURE.ONBOARD_PROFILES, 0x20) state = device.feature_request(FEATURE.ONBOARD_PROFILES, 0x20)
if state: if state:
mode = _unpack("!B", state[:1])[0] mode = struct.unpack("!B", state[:1])[0]
return mode return mode
def set_onboard_mode(self, device: Device, mode): def set_onboard_mode(self, device: Device, mode):
@ -1725,19 +1717,19 @@ class Hidpp20:
def get_polling_rate(self, device: Device): def get_polling_rate(self, device: Device):
state = device.feature_request(FEATURE.REPORT_RATE, 0x10) state = device.feature_request(FEATURE.REPORT_RATE, 0x10)
if state: if state:
rate = _unpack("!B", state[:1])[0] rate = struct.unpack("!B", state[:1])[0]
return str(rate) + "ms" return str(rate) + "ms"
else: else:
rates = ["8ms", "4ms", "2ms", "1ms", "500us", "250us", "125us"] rates = ["8ms", "4ms", "2ms", "1ms", "500us", "250us", "125us"]
state = device.feature_request(FEATURE.EXTENDED_ADJUSTABLE_REPORT_RATE, 0x20) state = device.feature_request(FEATURE.EXTENDED_ADJUSTABLE_REPORT_RATE, 0x20)
if state: if state:
rate = _unpack("!B", state[:1])[0] rate = struct.unpack("!B", state[:1])[0]
return rates[rate] return rates[rate]
def get_remaining_pairing(self, device: Device): def get_remaining_pairing(self, device: Device):
result = device.feature_request(FEATURE.REMAINING_PAIRING, 0x0) result = device.feature_request(FEATURE.REMAINING_PAIRING, 0x0)
if result: if result:
result = _unpack("!B", result[:1])[0] result = struct.unpack("!B", result[:1])[0]
FEATURE._fallback = lambda x: f"unknown:{x:04X}" FEATURE._fallback = lambda x: f"unknown:{x:04X}"
return result return result
@ -1754,7 +1746,7 @@ battery_functions = {
def decipher_battery_status(report: FixedBytes5) -> Tuple[Any, Battery]: 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: if battery_discharge_level == 0:
battery_discharge_level = None battery_discharge_level = None
try: try:
@ -1770,7 +1762,7 @@ def decipher_battery_status(report: FixedBytes5) -> Tuple[Any, Battery]:
def decipher_battery_voltage(report): def decipher_battery_voltage(report):
voltage, flags = _unpack(">HB", report[:3]) voltage, flags = struct.unpack(">HB", report[:3])
status = BatteryStatus.DISCHARGING status = BatteryStatus.DISCHARGING
charge_sts = ERROR.unknown charge_sts = ERROR.unknown
charge_lvl = CHARGE_LEVEL.average charge_lvl = CHARGE_LEVEL.average
@ -1808,7 +1800,7 @@ def decipher_battery_voltage(report):
def decipher_battery_unified(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: try:
status = BatteryStatus(status_byte) status = BatteryStatus(status_byte)
except ValueError: except ValueError:
@ -1833,7 +1825,7 @@ def decipher_battery_unified(report):
def decipher_adc_measurement(report): def decipher_adc_measurement(report):
# partial implementation - needs mapping to levels # 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: for level in battery_voltage_remaining:
if level[0] < adc: if level[0] < adc:
charge_level = level[1] charge_level = level[1]

View File

@ -16,10 +16,10 @@
# Translation support for the Logitech receivers library # Translation support for the Logitech receivers library
import gettext as _gettext import gettext
_ = _gettext.gettext _ = gettext.gettext
ngettext = _gettext.ngettext ngettext = gettext.ngettext
# A few common strings, not always accessible as such in the code. # A few common strings, not always accessible as such in the code.

View File

@ -18,31 +18,31 @@
# Handles incoming events from the receiver/devices, updating the object as appropriate. # Handles incoming events from the receiver/devices, updating the object as appropriate.
import logging import logging
import threading as _threading import struct
import threading
from struct import unpack as _unpack
from solaar.i18n import _ from solaar.i18n import _
from . import diversion as _diversion from . import base
from . import common
from . import diversion
from . import hidpp10 from . import hidpp10
from . import hidpp10_constants as _hidpp10_constants from . import hidpp10_constants
from . import hidpp20 from . import hidpp20
from . import hidpp20_constants as _hidpp20_constants from . import hidpp20_constants
from . import settings_templates as _st from . import settings_templates
from .base import DJ_MESSAGE_ID as _DJ_MESSAGE_ID
from .common import Alert from .common import Alert
from .common import Battery as _Battery
from .common import BatteryStatus from .common import BatteryStatus
from .common import strhex as _strhex
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
_R = _hidpp10_constants.REGISTERS _hidpp10 = hidpp10.Hidpp10()
_F = _hidpp20_constants.FEATURE _hidpp20 = hidpp20.Hidpp20()
_R = hidpp10_constants.REGISTERS
_F = hidpp20_constants.FEATURE
notification_lock = _threading.Lock() notification_lock = threading.Lock()
def process(device, notification): def process(device, notification):
@ -68,7 +68,7 @@ def _process_receiver_notification(receiver, n):
receiver.pairing.new_device = None receiver.pairing.new_device = None
pair_error = ord(n.data[:1]) pair_error = ord(n.data[:1])
if pair_error: 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 receiver.pairing.new_device = None
logger.warning("pairing error %d: %s", pair_error, error_string) logger.warning("pairing error %d: %s", pair_error, error_string)
receiver.changed(reason=reason) receiver.changed(reason=reason)
@ -87,7 +87,7 @@ def _process_receiver_notification(receiver, n):
receiver.pairing.device_passkey = None receiver.pairing.device_passkey = None
discover_error = ord(n.data[:1]) discover_error = ord(n.data[:1])
if discover_error: 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) logger.warning("bolt discovering error %d: %s", discover_error, discover_string)
receiver.changed(reason=reason) receiver.changed(reason=reason)
return True return True
@ -126,7 +126,7 @@ def _process_receiver_notification(receiver, n):
elif n.address == 0x02 and not pair_error: elif n.address == 0x02 and not pair_error:
receiver.pairing.new_device = receiver.register_new_device(n.data[7]) receiver.pairing.new_device = receiver.register_new_device(n.data[7])
if pair_error: 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 receiver.pairing.new_device = None
logger.warning("pairing error %d: %s", pair_error, error_string) logger.warning("pairing error %d: %s", pair_error, error_string)
receiver.changed(reason=reason) 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 # 0x40 to 0x7F appear to be HID++ 1.0 or DJ notifications
if n.sub_id >= 0x40: 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) return _process_dj_notification(device, n)
else: else:
return _process_hidpp10_notification(device, n) 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) if n.sub_id == 0x41: # device connection (and disconnection)
flags = ord(n.data[:1]) & 0xF0 flags = ord(n.data[:1]) & 0xF0
if n.address == 0x02: # very old 27 MHz protocol 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_established = True
link_encrypted = bool(flags & 0x80) link_encrypted = bool(flags & 0x80)
elif n.address > 0x00: # all other protocols are supposed to be almost the same 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_established = not (flags & 0x40)
link_encrypted = bool(flags & 0x20) or n.address == 0x10 # Bolt protocol always encrypted link_encrypted = bool(flags & 0x20) or n.address == 0x10 # Bolt protocol always encrypted
else: else:
@ -288,7 +288,9 @@ def _process_hidpp10_notification(device, n):
def _process_feature_notification(device, n, feature): def _process_feature_notification(device, n, feature):
if logger.isEnabledFor(logging.DEBUG): 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 feature == _F.BATTERY_STATUS:
if n.address == 0x00: if n.address == 0x00:
@ -323,16 +325,16 @@ def _process_feature_notification(device, n, feature):
elif feature == _F.SOLAR_DASHBOARD: elif feature == _F.SOLAR_DASHBOARD:
if n.data[5:9] == b"GOOD": 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' # guesstimate the battery voltage, emphasis on 'guess'
# status_text = '%1.2fV' % (adc * 2.67793237653 / 0x0672) # status_text = '%1.2fV' % (adc * 2.67793237653 / 0x0672)
status_text = BatteryStatus.DISCHARGING status_text = BatteryStatus.DISCHARGING
if n.address == 0x00: 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: elif n.address == 0x10:
if lux > 200: if lux > 200:
status_text = BatteryStatus.RECHARGING 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: elif n.address == 0x20:
if logger.isEnabledFor(logging.DEBUG): if logger.isEnabledFor(logging.DEBUG):
logger.debug("%s: Light Check button pressed", device) logger.debug("%s: Light Check button pressed", device)
@ -382,18 +384,18 @@ def _process_feature_notification(device, n, feature):
elif feature == _F.BACKLIGHT2: elif feature == _F.BACKLIGHT2:
if n.address == 0x00: 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: 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: elif feature == _F.REPROG_CONTROLS_V4:
if n.address == 0x00: if n.address == 0x00:
if logger.isEnabledFor(logging.DEBUG): 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) logger.debug("%s: diverted controls pressed: 0x%x, 0x%x, 0x%x, 0x%x", device, cid1, cid2, cid3, cid4)
elif n.address == 0x10: elif n.address == 0x10:
if logger.isEnabledFor(logging.DEBUG): 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) logger.debug("%s: rawXY dx=%i dy=%i", device, dx, dy)
elif n.address == 0x20: elif n.address == 0x20:
if logger.isEnabledFor(logging.DEBUG): if logger.isEnabledFor(logging.DEBUG):
@ -404,7 +406,7 @@ def _process_feature_notification(device, n, feature):
elif feature == _F.HIRES_WHEEL: elif feature == _F.HIRES_WHEEL:
if n.address == 0x00: if n.address == 0x00:
if logger.isEnabledFor(logging.INFO): 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 high_res = (flags & 0x10) != 0
periods = flags & 0x0F periods = flags & 0x0F
logger.info("%s: WHEEL: res: %d periods: %d delta V:%-3d", device, high_res, periods, delta_v) 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) logger.info("%s: WHEEL: ratchet: %d", device, ratchet)
if ratchet < 2: # don't process messages with unusual ratchet values if ratchet < 2: # don't process messages with unusual ratchet values
if device.setting_callback: 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: else:
if logger.isEnabledFor(logging.INFO): if logger.isEnabledFor(logging.INFO):
logger.info("%s: unknown WHEEL %s", device, n) 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) logger.info("%s: unknown ONBOARD PROFILES %s", device, n)
else: else:
if n.address == 0x00: if n.address == 0x00:
profile_sector = _unpack("!H", n.data[:2])[0] profile_sector = struct.unpack("!H", n.data[:2])[0]
if profile_sector: if profile_sector:
_st.profile_change(device, profile_sector) settings_templates.profile_change(device, profile_sector)
elif n.address == 0x10: elif n.address == 0x10:
resolution_index = _unpack("!B", n.data[:1])[0] resolution_index = struct.unpack("!B", n.data[:1])[0]
profile_sector = _unpack("!H", device.feature_request(_F.ONBOARD_PROFILES, 0x40)[:2])[0] profile_sector = struct.unpack("!H", device.feature_request(_F.ONBOARD_PROFILES, 0x40)[:2])[0]
if device.setting_callback: if device.setting_callback:
for profile in device.profiles.profiles.values() if device.profiles else []: for profile in device.profiles.profiles.values() if device.profiles else []:
if profile.sector == profile_sector: 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 break
elif feature == _F.BRIGHTNESS_CONTROL: 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) logger.info("%s: unknown BRIGHTNESS CONTROL %s", device, n)
else: else:
if n.address == 0x00: if n.address == 0x00:
brightness = _unpack("!H", n.data[:2])[0] brightness = struct.unpack("!H", n.data[:2])[0]
device.setting_callback(device, _st.BrightnessControl, [brightness]) device.setting_callback(device, settings_templates.BrightnessControl, [brightness])
elif n.address == 0x10: elif n.address == 0x10:
brightness = n.data[0] & 0x01 brightness = n.data[0] & 0x01
if brightness: if brightness:
brightness = _unpack("!H", device.feature_request(_F.BRIGHTNESS_CONTROL, 0x10)[:2])[0] brightness = struct.unpack("!H", device.feature_request(_F.BRIGHTNESS_CONTROL, 0x10)[:2])[0]
device.setting_callback(device, _st.BrightnessControl, [brightness]) device.setting_callback(device, settings_templates.BrightnessControl, [brightness])
_diversion.process_notification(device, n, feature) diversion.process_notification(device, n, feature)
return True return True

View File

@ -15,7 +15,7 @@
## with this program; if not, write to the Free Software Foundation, Inc., ## with this program; if not, write to the Free Software Foundation, Inc.,
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
import errno as _errno import errno
import logging import logging
import time import time
@ -23,12 +23,12 @@ from dataclasses import dataclass
from typing import Callable from typing import Callable
from typing import Optional from typing import Optional
import hidapi as _hid import hidapi
from solaar.i18n import _ from solaar.i18n import _
from solaar.i18n import ngettext from solaar.i18n import ngettext
from . import base as _base from . import base
from . import exceptions from . import exceptions
from . import hidpp10 from . import hidpp10
from . import hidpp10_constants from . import hidpp10_constants
@ -108,7 +108,7 @@ class Receiver:
if d: if d:
d.close() d.close()
self._devices.clear() self._devices.clear()
return handle and _base.close(handle) return handle and base.close(handle)
def __del__(self): def __del__(self):
self.close() self.close()
@ -253,7 +253,7 @@ class Receiver:
def request(self, request_id, *params): def request(self, request_id, *params):
if bool(self): 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): def reset_pairing(self):
self.pairing = Pairing() self.pairing = Pairing()
@ -451,7 +451,7 @@ class Ex100Receiver(Receiver):
return online, encrypted, wpid, kind return online, encrypted, wpid, kind
def device_pairing_information(self, number: int) -> dict: 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: if not wpid:
logger.error("Unable to get wpid from udev for device %d of %s", number, self) 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") raise exceptions.NoSuchDevice(number=number, receiver=self, error="Not present 27Mhz device")
@ -491,9 +491,9 @@ class ReceiverFactory:
"""Opens a Logitech Receiver found attached to the machine, by Linux device path.""" """Opens a Logitech Receiver found attached to the machine, by Linux device path."""
try: try:
handle = _base.open_path(device_info.path) handle = base.open_path(device_info.path)
if handle: if handle:
product_info = _base.product_information(device_info.product_id) product_info = base.product_information(device_info.product_id)
if not product_info: if not product_info:
logger.warning("Unknown receiver type: %s", device_info.product_id) logger.warning("Unknown receiver type: %s", device_info.product_id)
product_info = {} product_info = {}
@ -502,7 +502,7 @@ class ReceiverFactory:
return rclass(kind, product_info, handle, device_info.path, device_info.product_id, setting_callback) return rclass(kind, product_info, handle, device_info.path, device_info.product_id, setting_callback)
except OSError as e: except OSError as e:
logger.exception("open %s", device_info) logger.exception("open %s", device_info)
if e.errno == _errno.EACCES: if e.errno == errno.EACCES:
raise raise
except Exception: except Exception:
logger.exception("open %s", device_info) logger.exception("open %s", device_info)

View File

@ -16,26 +16,21 @@
import logging import logging
import math import math
import struct
from struct import unpack as _unpack import time
from time import sleep as _sleep
from solaar.i18n import _ from solaar.i18n import _
from . import hidpp20_constants as _hidpp20_constants from . import common
from .common import NamedInt as _NamedInt from . import hidpp20_constants
from .common import NamedInts as _NamedInts from .common import NamedInt
from .common import bytes2int as _bytes2int from .common import NamedInts
from .common import int2bytes as _int2bytes
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
#
#
#
SENSITIVITY_IGNORE = "ignore" SENSITIVITY_IGNORE = "ignore"
KIND = _NamedInts( KIND = NamedInts(
toggle=0x01, toggle=0x01,
choice=0x02, choice=0x02,
range=0x04, range=0x04,
@ -613,7 +608,7 @@ class RangeFieldSetting(Setting):
class RegisterRW: class RegisterRW:
__slots__ = ("register",) __slots__ = ("register",)
kind = _NamedInt(0x01, _("register")) kind = NamedInt(0x01, _("register"))
def __init__(self, register): def __init__(self, register):
assert isinstance(register, int) assert isinstance(register, int)
@ -627,12 +622,12 @@ class RegisterRW:
class FeatureRW: class FeatureRW:
kind = _NamedInt(0x02, _("feature")) kind = NamedInt(0x02, _("feature"))
default_read_fnid = 0x00 default_read_fnid = 0x00
default_write_fnid = 0x10 default_write_fnid = 0x10
def __init__(self, feature, read_fnid=0x00, write_fnid=0x10, prefix=b"", suffix=b"", read_prefix=b"", no_reply=False): 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.feature = feature
self.read_fnid = read_fnid self.read_fnid = read_fnid
self.write_fnid = write_fnid self.write_fnid = write_fnid
@ -653,7 +648,7 @@ class FeatureRW:
class FeatureRWMap(FeatureRW): class FeatureRWMap(FeatureRW):
kind = _NamedInt(0x02, _("feature")) kind = NamedInt(0x02, _("feature"))
default_read_fnid = 0x00 default_read_fnid = 0x00
default_write_fnid = 0x10 default_write_fnid = 0x10
default_key_byte_count = 1 default_key_byte_count = 1
@ -666,7 +661,7 @@ class FeatureRWMap(FeatureRW):
key_byte_count=default_key_byte_count, key_byte_count=default_key_byte_count,
no_reply=False, no_reply=False,
): ):
assert isinstance(feature, _NamedInt) assert isinstance(feature, NamedInt)
self.feature = feature self.feature = feature
self.read_fnid = read_fnid self.read_fnid = read_fnid
self.write_fnid = write_fnid self.write_fnid = write_fnid
@ -675,12 +670,12 @@ class FeatureRWMap(FeatureRW):
def read(self, device, key): def read(self, device, key):
assert self.feature is not None 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) return device.feature_request(self.feature, self.read_fnid, key_bytes)
def write(self, device, key, data_bytes): def write(self, device, key, data_bytes):
assert self.feature is not None 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) 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 return reply if not self.no_reply else True
@ -733,13 +728,13 @@ class BooleanValidator(Validator):
else: else:
assert isinstance(false_value, bytes) assert isinstance(false_value, bytes)
if mask is None or mask == self.default_mask: if mask is None or mask == self.default_mask:
mask = b"\xFF" * len(true_value) mask = b"\xff" * len(true_value)
else: else:
assert isinstance(mask, bytes) assert isinstance(mask, bytes)
assert len(mask) == len(true_value) == len(false_value) assert len(mask) == len(true_value) == len(false_value)
tv = _bytes2int(true_value) tv = common.bytes2int(true_value)
fv = _bytes2int(false_value) fv = common.bytes2int(false_value)
mv = _bytes2int(mask) mv = common.bytes2int(mask)
assert tv != fv # true and false might be something other than bit values assert tv != fv # true and false might be something other than bit values
assert tv & mv == tv assert tv & mv == tv
assert fv & mv == fv assert fv & mv == fv
@ -773,14 +768,14 @@ class BooleanValidator(Validator):
return False return False
count = len(self.mask) count = len(self.mask)
mask = _bytes2int(self.mask) mask = common.bytes2int(self.mask)
reply_value = _bytes2int(reply_bytes[:count]) & 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: if reply_value == true_value:
return True return True
false_value = _bytes2int(self.false_value) false_value = common.bytes2int(self.false_value)
if reply_value == false_value: if reply_value == false_value:
return False return False
@ -852,7 +847,7 @@ class BitFieldValidator(Validator):
return "{" + ", ".join([element_to_string(k, value[k]) for k in value]) + "}" return "{" + ", ".join([element_to_string(k, value[k]) for k in value]) + "}"
def validate_read(self, reply_bytes): 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} value = {int(k): False for k in self.options}
m = 1 m = 1
for _ignore in range(8 * self.byte_count): for _ignore in range(8 * self.byte_count):
@ -867,7 +862,7 @@ class BitFieldValidator(Validator):
for k, v in new_value.items(): for k, v in new_value.items():
if v: if v:
w |= int(k) w |= int(k)
return _int2bytes(w, self.byte_count) return common.int2bytes(w, self.byte_count)
def get_options(self): def get_options(self):
return self.options return self.options
@ -931,7 +926,7 @@ class BitFieldWithOffsetAndMaskValidator(Validator):
for offset, mask in self._mask_from_offset.items(): for offset, mask in self._mask_from_offset.items():
b = offset << (8 * (self.byte_count + 1)) b = offset << (8 * (self.byte_count + 1))
b |= (self.sep << (8 * self.byte_count)) | mask 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 return r
def prepare_read_key(self, key): def prepare_read_key(self, key):
@ -941,14 +936,14 @@ class BitFieldWithOffsetAndMaskValidator(Validator):
offset, mask = option.om_method(option) offset, mask = option.om_method(option)
b = offset << (8 * (self.byte_count + 1)) b = offset << (8 * (self.byte_count + 1))
b |= (self.sep << (8 * self.byte_count)) | mask 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): def validate_read(self, reply_bytes_dict):
values = {int(k): False for k in self.options} values = {int(k): False for k in self.options}
for query, b in reply_bytes_dict.items(): 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" 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, {}) mask_to_opt = self._option_from_offset_mask.get(offset, {})
m = 1 m = 1
for _ignore in range(8 * self.byte_count): for _ignore in range(8 * self.byte_count):
@ -968,7 +963,7 @@ class BitFieldWithOffsetAndMaskValidator(Validator):
if v: if v:
w[offset] |= mask w[offset] |= mask
return [ return [
_int2bytes( common.int2bytes(
(offset << (8 * (2 * self.byte_count + 1))) (offset << (8 * (2 * self.byte_count + 1)))
| (self.sep << (16 * self.byte_count)) | (self.sep << (16 * self.byte_count))
| (self._mask_from_offset[offset] << (8 * 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""): def __init__(self, choices=None, byte_count=None, read_skip_byte_count=0, write_prefix_bytes=b""):
assert choices is not None assert choices is not None
assert isinstance(choices, _NamedInts) assert isinstance(choices, NamedInts)
assert len(choices) > 1 assert len(choices) > 1
self.choices = choices self.choices = choices
self.needs_current_value = False self.needs_current_value = False
@ -1029,7 +1024,7 @@ class ChoicesValidator(Validator):
return str(self.choices[value]) if isinstance(value, int) else str(value) return str(self.choices[value]) if isinstance(value, int) else str(value)
def validate_read(self, reply_bytes): 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] valid_value = self.choices[reply_value]
assert valid_value is not None, f"{self.__class__.__name__}: failed to validate read value {reply_value:02X}" assert valid_value is not None, f"{self.__class__.__name__}: failed to validate read value {reply_value:02X}"
return valid_value return valid_value
@ -1041,7 +1036,7 @@ class ChoicesValidator(Validator):
value = self.choice(new_value) value = self.choice(new_value)
if value is None: if value is None:
raise ValueError(f"invalid choice {new_value!r}") 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) return self._write_prefix_bytes + value.bytes(self._byte_count)
def choice(self, value): def choice(self, value):
@ -1083,11 +1078,11 @@ class ChoicesMapValidator(ChoicesValidator):
max_key_bits = 0 max_key_bits = 0
max_value_bits = 0 max_value_bits = 0
for key, choices in choices_map.items(): for key, choices in choices_map.items():
assert isinstance(key, _NamedInt) assert isinstance(key, NamedInt)
assert isinstance(choices, _NamedInts) assert isinstance(choices, NamedInts)
max_key_bits = max(max_key_bits, key.bit_length()) max_key_bits = max(max_key_bits, key.bit_length())
for key_value in choices: 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()) max_value_bits = max(max_value_bits, key_value.bit_length())
self._key_byte_count = (max_key_bits + 7) // 8 self._key_byte_count = (max_key_bits + 7) // 8
if key_byte_count: if key_byte_count:
@ -1119,7 +1114,7 @@ class ChoicesMapValidator(ChoicesValidator):
def validate_read(self, reply_bytes, key): def validate_read(self, reply_bytes, key):
start = self._key_byte_count + self._read_skip_byte_count start = self._key_byte_count + self._read_skip_byte_count
end = start + self._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 # 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: if self.extra_default is not None and self.extra_default == reply_value:
return int(self.choices[key][0]) return int(self.choices[key][0])
@ -1188,7 +1183,7 @@ class RangeValidator(Validator):
assert self._byte_count < 8 assert self._byte_count < 8
def validate_read(self, reply_bytes): 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.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}" assert reply_value <= self.max_value, f"{self.__class__.__name__}: failed to validate read value {reply_value:02X}"
return reply_value return reply_value
@ -1197,7 +1192,7 @@ class RangeValidator(Validator):
if new_value < self.min_value or new_value > self.max_value: if new_value < self.min_value or new_value > self.max_value:
raise ValueError(f"invalid choice {new_value!r}") raise ValueError(f"invalid choice {new_value!r}")
current_value = self.validate_read(current_value) if current_value is not None else None 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 # 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 return None if current_value is not None and current_value == new_value else to_write
@ -1270,7 +1265,7 @@ class PackedRangeValidator(Validator):
def validate_read(self, reply_bytes): def validate_read(self, reply_bytes):
rvs = { 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)
} }
for n in range(self.count): for n in range(self.count):
@ -1284,7 +1279,9 @@ class PackedRangeValidator(Validator):
for new_value in new_values.values(): for new_value in new_values.values():
if new_value < self.min_value or new_value > self.max_value: if new_value < self.min_value or new_value > self.max_value:
raise ValueError(f"invalid value {new_value!r}") 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 return bytes
def acceptable(self, args, current): def acceptable(self, args, current):
@ -1305,12 +1302,12 @@ class MultipleRangeValidator(Validator):
assert isinstance(sub_items, dict) assert isinstance(sub_items, dict)
# sub_items: items -> class with .minimum, .maximum, .length (in bytes), .id (a string) and .widget (e.g. 'Scale') # sub_items: items -> class with .minimum, .maximum, .length (in bytes), .id (a string) and .widget (e.g. 'Scale')
self.items = items 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._item_from_id = {int(k): k for k in items}
self.sub_items = sub_items self.sub_items = sub_items
def prepare_read_item(self, item): 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): def validate_read_item(self, reply_bytes, item):
item = self._item_from_id[int(item)] item = self._item_from_id[int(item)]
@ -1320,7 +1317,7 @@ class MultipleRangeValidator(Validator):
r = reply_bytes[start : start + sub_item.length] r = reply_bytes[start : start + sub_item.length]
if len(r) < sub_item.length: if len(r) < sub_item.length:
r += b"\x00" * (sub_item.length - len(value)) r += b"\x00" * (sub_item.length - len(value))
v = _bytes2int(r) v = common.bytes2int(r)
if not (sub_item.minimum < v < sub_item.maximum): if not (sub_item.minimum < v < sub_item.maximum):
logger.warning( logger.warning(
f"{self.__class__.__name__}: failed to validate read value for {item}.{sub_item}: " f"{self.__class__.__name__}: failed to validate read value for {item}.{sub_item}: "
@ -1335,7 +1332,7 @@ class MultipleRangeValidator(Validator):
w = b"" w = b""
for item in value.keys(): for item in value.keys():
_item = self._item_from_id[int(item)] _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]: for sub_item in self.sub_items[_item]:
try: try:
v = value[int(item)][str(sub_item)] v = value[int(item)][str(sub_item)]
@ -1345,17 +1342,17 @@ class MultipleRangeValidator(Validator):
raise ValueError( raise ValueError(
f"invalid choice for {item}.{sub_item}: {v} not in [{sub_item.minimum}..{sub_item.maximum}]" 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: if len(w) + len(b) > 15:
seq.append(b + b"\xFF") seq.append(b + b"\xff")
w = b"" w = b""
w += b w += b
seq.append(w + b"\xFF") seq.append(w + b"\xff")
return seq return seq
def prepare_write_item(self, item, value): def prepare_write_item(self, item, value):
_item = self._item_from_id[int(item)] _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]: for sub_item in self.sub_items[_item]:
try: try:
v = value[str(sub_item)] v = value[str(sub_item)]
@ -1363,8 +1360,8 @@ class MultipleRangeValidator(Validator):
return None return None
if not (sub_item.minimum <= v <= sub_item.maximum): 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}]") raise ValueError(f"invalid choice for {item}.{sub_item}: {v} not in [{sub_item.minimum}..{sub_item.maximum}]")
w += _int2bytes(v, sub_item.length) w += common.int2bytes(v, sub_item.length)
return w + b"\xFF" return w + b"\xff"
def acceptable(self, args, current): def acceptable(self, args, current):
# just one item, with at least one sub-item # just one item, with at least one sub-item
@ -1418,13 +1415,13 @@ class ActionSettingRW:
pass pass
def read(self, device): # need to return bytes, as if read from device 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 write(self, device, data_bytes):
def handler(device, n): # Called on notification events from the device 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: 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 if not self.pressed and int(self.key.key) in cids: # trigger key pressed
self.pressed = True self.pressed = True
self.press_action() self.press_action()
@ -1438,7 +1435,7 @@ class ActionSettingRW:
self.key_action(key) self.key_action(key)
elif n.address == 0x10: elif n.address == 0x10:
if self.pressed: if self.pressed:
dx, dy = _unpack("!hh", n.data[:4]) dx, dy = struct.unpack("!hh", n.data[:4])
self.move_action(dx, dy) self.move_action(dx, dy)
divertSetting = next(filter(lambda s: s.name == self.divert_setting_name, device.settings), None) divertSetting = next(filter(lambda s: s.name == self.divert_setting_name, device.settings), None)
@ -1446,7 +1443,7 @@ class ActionSettingRW:
logger.warning("setting %s not found on %s", self.divert_setting_name, device.name) logger.warning("setting %s not found on %s", self.divert_setting_name, device.name)
return None return None
self.device = device self.device = device
key = _bytes2int(data_bytes) key = common.bytes2int(data_bytes)
if key: # Enable if key: # Enable
self.key = next((k for k in device.keys if k.key == key), None) self.key = next((k for k in device.keys if k.key == key), None)
if self.key: if self.key:
@ -1484,13 +1481,13 @@ class RawXYProcessing:
self.keys = [] # the keys that can initiate processing self.keys = [] # the keys that can initiate processing
self.initiating_key = None # the key that did initiate processing self.initiating_key = None # the key that did initiate processing
self.active = False 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 assert self.feature_offset is not False
def handler(self, device, n): # Called on notification events from the device 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: if n.address == 0x00:
cids = _unpack("!HHHH", n.data[:8]) cids = struct.unpack("!HHHH", n.data[:8])
## generalize to list of keys ## generalize to list of keys
if not self.initiating_key: # no initiating key pressed if not self.initiating_key: # no initiating key pressed
for k in self.keys: for k in self.keys:
@ -1508,7 +1505,7 @@ class RawXYProcessing:
self.key_action(key) self.key_action(key)
elif n.address == 0x10: elif n.address == 0x10:
if self.initiating_key: if self.initiating_key:
dx, dy = _unpack("!hh", n.data[:4]) dx, dy = struct.unpack("!hh", n.data[:4])
self.move_action(dx, dy) self.move_action(dx, dy)
def start(self, key): def start(self, key):
@ -1556,8 +1553,8 @@ class RawXYProcessing:
def apply_all_settings(device): def apply_all_settings(device):
if device.features and _hidpp20_constants.FEATURE.HIRES_WHEEL in device.features: if device.features and hidpp20_constants.FEATURE.HIRES_WHEEL in device.features:
_sleep(0.2) # delay to try to get out of race condition with Linux HID++ driver time.sleep(0.2) # delay to try to get out of race condition with Linux HID++ driver
persister = getattr(device, "persister", None) persister = getattr(device, "persister", None)
sensitives = persister.get("_sensitive", {}) if persister else {} sensitives = persister.get("_sensitive", {}) if persister else {}
for s in device.settings: for s in device.settings:

File diff suppressed because it is too large Load Diff

View File

@ -15,20 +15,20 @@
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
# Reprogrammable keys information # 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 NamedInts
from .common import UnsortedNamedInts as _UnsortedNamedInts from .common import UnsortedNamedInts
_XDG_CONFIG_HOME = _os.environ.get("XDG_CONFIG_HOME") or _os.path.expanduser(_os.path.join("~", ".config")) _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") _keys_file_path = os.path.join(_XDG_CONFIG_HOME, "solaar", "keys.yaml")
# <controls.xml awk -F\" '/<Control /{sub(/^LD_FINFO_(CTRLID_)?/, "", $2);printf("\t%s=0x%04X,\n", $2, $4)}' | sort -t= -k2 # <controls.xml awk -F\" '/<Control /{sub(/^LD_FINFO_(CTRLID_)?/, "", $2);printf("\t%s=0x%04X,\n", $2, $4)}' | sort -t= -k2
CONTROL = _NamedInts( CONTROL = NamedInts(
{ {
"Volume_Up": 0x0001, "Volume_Up": 0x0001,
"Volume_Down": 0x0002, "Volume_Down": 0x0002,
@ -322,7 +322,7 @@ CONTROL[0x1200] = "MR" # add in MR key - this is not really a Logitech Control
CONTROL._fallback = lambda x: f"unknown:{x:04X}" CONTROL._fallback = lambda x: f"unknown:{x:04X}"
# <tasks.xml awk -F\" '/<Task /{gsub(/ /, "_", $6); printf("\t%s=0x%04X,\n", $6, $4)}' # <tasks.xml awk -F\" '/<Task /{gsub(/ /, "_", $6); printf("\t%s=0x%04X,\n", $6, $4)}'
TASK = _NamedInts( TASK = NamedInts(
Volume_Up=0x0001, Volume_Up=0x0001,
Volume_Down=0x0002, Volume_Down=0x0002,
Mute=0x0003, Mute=0x0003,
@ -573,7 +573,7 @@ TASK._fallback = lambda x: f"unknown:{x:04X}"
# Capabilities and desired software handling for a control # Capabilities and desired software handling for a control
# Ref: https://drive.google.com/file/d/10imcbmoxTJ1N510poGdsviEhoFfB_Ua4/view # Ref: https://drive.google.com/file/d/10imcbmoxTJ1N510poGdsviEhoFfB_Ua4/view
# We treat bytes 4 and 8 of `getCidInfo` as a single bitfield # We treat bytes 4 and 8 of `getCidInfo` as a single bitfield
KEY_FLAG = _NamedInts( KEY_FLAG = NamedInts(
analytics_key_events=0x400, analytics_key_events=0x400,
force_raw_XY=0x200, force_raw_XY=0x200,
raw_XY=0x100, raw_XY=0x100,
@ -588,16 +588,16 @@ KEY_FLAG = _NamedInts(
) )
# Flags describing the reporting method of a control # Flags describing the reporting method of a control
# We treat bytes 2 and 5 of `get/setCidReporting` as a single bitfield # We treat bytes 2 and 5 of `get/setCidReporting` as a single bitfield
MAPPING_FLAG = _NamedInts( MAPPING_FLAG = NamedInts(
analytics_key_events_reporting=0x100, analytics_key_events_reporting=0x100,
force_raw_XY_diverted=0x40, force_raw_XY_diverted=0x40,
raw_XY_diverted=0x10, raw_XY_diverted=0x10,
persistently_diverted=0x04, persistently_diverted=0x04,
diverted=0x01, diverted=0x01,
) )
CID_GROUP_BIT = _NamedInts(g8=0x80, g7=0x40, g6=0x20, g5=0x10, g4=0x08, g3=0x04, g2=0x02, g1=0x01) CID_GROUP_BIT = NamedInts(g8=0x80, g7=0x40, g6=0x20, g5=0x10, g4=0x08, g3=0x04, g2=0x02, g1=0x01)
CID_GROUP = _NamedInts(g8=8, g7=7, g6=6, g5=5, g4=4, g3=3, g2=2, g1=1) CID_GROUP = NamedInts(g8=8, g7=7, g6=6, g5=5, g4=4, g3=3, g2=2, g1=1)
DISABLE = _NamedInts( DISABLE = NamedInts(
Caps_Lock=0x01, Caps_Lock=0x01,
Num_Lock=0x02, Num_Lock=0x02,
Scroll_Lock=0x04, Scroll_Lock=0x04,
@ -608,7 +608,7 @@ DISABLE._fallback = lambda x: f"unknown:{x:02X}"
# HID USB Keycodes from https://www.usb.org/sites/default/files/documents/hut1_12v2.pdf # HID USB Keycodes from https://www.usb.org/sites/default/files/documents/hut1_12v2.pdf
# Modified by information from Linux HID driver linux/drivers/hid/hid-input.c # Modified by information from Linux HID driver linux/drivers/hid/hid-input.c
USB_HID_KEYCODES = _NamedInts( USB_HID_KEYCODES = NamedInts(
A=0x04, A=0x04,
B=0x05, B=0x05,
C=0x06, C=0x06,
@ -780,7 +780,7 @@ USB_HID_KEYCODES[0x26] = "9"
USB_HID_KEYCODES[0x27] = "0" USB_HID_KEYCODES[0x27] = "0"
USB_HID_KEYCODES[0x64] = "102ND" USB_HID_KEYCODES[0x64] = "102ND"
HID_CONSUMERCODES = _NamedInts( HID_CONSUMERCODES = NamedInts(
{ {
# Unassigned=0x00, # Unassigned=0x00,
# Consumer_Control=0x01, # Consumer_Control=0x01,
@ -1167,9 +1167,9 @@ HID_CONSUMERCODES._fallback = lambda x: f"unknown:{x:04X}"
## Information for x1c00 Persistent from https://drive.google.com/drive/folders/0BxbRzx7vEV7eWmgwazJ3NUFfQ28 ## Information for x1c00 Persistent from https://drive.google.com/drive/folders/0BxbRzx7vEV7eWmgwazJ3NUFfQ28
KEYMOD = _NamedInts(CTRL=0x01, SHIFT=0x02, ALT=0x04, META=0x08, RCTRL=0x10, RSHIFT=0x20, RALT=0x40, RMETA=0x80) KEYMOD = NamedInts(CTRL=0x01, SHIFT=0x02, ALT=0x04, META=0x08, RCTRL=0x10, RSHIFT=0x20, RALT=0x40, RMETA=0x80)
ACTIONID = _NamedInts( ACTIONID = NamedInts(
Empty=0x00, Empty=0x00,
Key=0x01, Key=0x01,
Mouse=0x02, Mouse=0x02,
@ -1182,7 +1182,7 @@ ACTIONID = _NamedInts(
Power=0x09, Power=0x09,
) )
MOUSE_BUTTONS = _NamedInts( MOUSE_BUTTONS = NamedInts(
Mouse_Button_Left=0x0001, Mouse_Button_Left=0x0001,
Mouse_Button_Right=0x0002, Mouse_Button_Right=0x0002,
Mouse_Button_Middle=0x0004, Mouse_Button_Middle=0x0004,
@ -1202,14 +1202,14 @@ MOUSE_BUTTONS = _NamedInts(
) )
MOUSE_BUTTONS._fallback = lambda x: f"unknown mouse button:{x:04X}" MOUSE_BUTTONS._fallback = lambda x: f"unknown mouse button:{x:04X}"
HORIZONTAL_SCROLL = _NamedInts( HORIZONTAL_SCROLL = NamedInts(
Horizontal_Scroll_Left=0x4000, Horizontal_Scroll_Left=0x4000,
Horizontal_Scroll_Right=0x8000, Horizontal_Scroll_Right=0x8000,
) )
HORIZONTAL_SCROLL._fallback = lambda x: f"unknown horizontal scroll:{x:04X}" HORIZONTAL_SCROLL._fallback = lambda x: f"unknown horizontal scroll:{x:04X}"
# Construct universe for Persistent Remappable Keys setting (only for supported values) # Construct universe for Persistent Remappable Keys setting (only for supported values)
KEYS = _UnsortedNamedInts() KEYS = UnsortedNamedInts()
KEYS_Default = 0x7FFFFFFF # Special value to reset key to default - has to be different from all others KEYS_Default = 0x7FFFFFFF # Special value to reset key to default - has to be different from all others
KEYS[KEYS_Default] = "Default" # Value to reset to default KEYS[KEYS_Default] = "Default" # Value to reset to default
KEYS[0] = "None" # Value for no output KEYS[0] = "None" # Value for no output
@ -1247,7 +1247,7 @@ for code in HORIZONTAL_SCROLL:
# Construct subsets for known devices # Construct subsets for known devices
def persistent_keys(action_ids): def persistent_keys(action_ids):
keys = _UnsortedNamedInts() keys = UnsortedNamedInts()
keys[KEYS_Default] = "Default" # Value to reset to default keys[KEYS_Default] = "Default" # Value to reset to default
keys[0] = "No Output (only as default)" keys[0] = "No Output (only as default)"
for key in KEYS: for key in KEYS:
@ -1259,7 +1259,7 @@ def persistent_keys(action_ids):
KEYS_KEYS_CONSUMER = persistent_keys([ACTIONID.Key, ACTIONID.Consumer]) KEYS_KEYS_CONSUMER = persistent_keys([ACTIONID.Key, ACTIONID.Consumer])
KEYS_KEYS_MOUSE_HSCROLL = persistent_keys([ACTIONID.Key, ACTIONID.Mouse, ACTIONID.Hscroll]) KEYS_KEYS_MOUSE_HSCROLL = persistent_keys([ACTIONID.Key, ACTIONID.Mouse, ACTIONID.Hscroll])
COLORS = _UnsortedNamedInts( COLORS = UnsortedNamedInts(
{ {
# from Xorg rgb.txt,v 1.3 2000/08/17 # from Xorg rgb.txt,v 1.3 2000/08/17
"red": 0xFF0000, "red": 0xFF0000,
@ -1400,11 +1400,11 @@ COLORS = _UnsortedNamedInts(
} }
) )
COLORSPLUS = _UnsortedNamedInts({"No change": -1}) COLORSPLUS = UnsortedNamedInts({"No change": -1})
for i in COLORS: for i in COLORS:
COLORSPLUS[int(i)] = str(i) COLORSPLUS[int(i)] = str(i)
KEYCODES = _NamedInts( KEYCODES = NamedInts(
{ {
"A": 1, "A": 1,
"B": 2, "B": 2,
@ -1529,11 +1529,11 @@ KEYCODES = _NamedInts(
# load in override dictionary for KEYCODES # load in override dictionary for KEYCODES
try: try:
if _os.path.isfile(_keys_file_path): if os.path.isfile(_keys_file_path):
with open(_keys_file_path) as keys_file: with open(_keys_file_path) as keys_file:
keys = _yaml.safe_load(keys_file) keys = yaml.safe_load(keys_file)
if isinstance(keys, dict): if isinstance(keys, dict):
keys = _NamedInts(**keys) keys = NamedInts(**keys)
for k in KEYCODES: for k in KEYCODES:
if int(k) not in keys and str(k) not in keys: if int(k) not in keys and str(k) not in keys:
keys[int(k)] = str(k) keys[int(k)] = str(k)

View File

@ -20,9 +20,9 @@ import logging
import os as _os import os as _os
import threading import threading
import yaml as _yaml import yaml
from logitech_receiver.common import NamedInt as _NamedInt from logitech_receiver.common import NamedInt
from solaar import __version__ from solaar import __version__
@ -49,7 +49,7 @@ def _load():
path = _yaml_file_path path = _yaml_file_path
try: try:
with open(_yaml_file_path) as config_file: with open(_yaml_file_path) as config_file:
loaded_config = _yaml.safe_load(config_file) loaded_config = yaml.safe_load(config_file)
except Exception as e: except Exception as e:
logger.error("failed to load from %s: %s", _yaml_file_path, e) logger.error("failed to load from %s: %s", _yaml_file_path, e)
elif _os.path.isfile(_json_file_path): elif _os.path.isfile(_json_file_path):
@ -153,7 +153,7 @@ def do_save():
save_timer = None save_timer = None
try: try:
with open(_yaml_file_path, "w") as config_file: with open(_yaml_file_path, "w") as config_file:
_yaml.dump(_config, config_file, default_flow_style=None, width=150) yaml.dump(_config, config_file, default_flow_style=None, width=150)
if logger.isEnabledFor(logging.INFO): if logger.isEnabledFor(logging.INFO):
logger.info("saved %s to %s", _config, _yaml_file_path) logger.info("saved %s to %s", _config, _yaml_file_path)
except Exception as e: except Exception as e:
@ -216,14 +216,14 @@ def device_representer(dumper, data):
return dumper.represent_mapping("tag:yaml.org,2002:map", data) return dumper.represent_mapping("tag:yaml.org,2002:map", data)
_yaml.add_representer(_DeviceEntry, device_representer) yaml.add_representer(_DeviceEntry, device_representer)
def named_int_representer(dumper, data): def named_int_representer(dumper, data):
return dumper.represent_scalar("tag:yaml.org,2002:int", str(int(data))) return dumper.represent_scalar("tag:yaml.org,2002:int", str(int(data)))
_yaml.add_representer(_NamedInt, named_int_representer) yaml.add_representer(NamedInt, named_int_representer)
# A device can be identified by a combination of WPID and serial number (for receiver-connected devices) # A device can be identified by a combination of WPID and serial number (for receiver-connected devices)