Apply ruff format

Run ruff auto formatting using:
ruff format .

Related #2295
This commit is contained in:
Matthias Hagmann 2024-02-20 20:48:26 +01:00 committed by Peter F. Patel-Schneider
parent 35f63edcd8
commit 7774569971
56 changed files with 6955 additions and 6566 deletions

View File

@ -32,16 +32,16 @@ def init_paths():
except UnicodeError: except UnicodeError:
sys.stderr.write( sys.stderr.write(
'ERROR: Solaar cannot recognize encoding of filesystem path, ' "ERROR: Solaar cannot recognize encoding of filesystem path, "
'this may happen because non UTF-8 characters in the pathname.\n' "this may happen because non UTF-8 characters in the pathname.\n"
) )
sys.exit(1) sys.exit(1)
prefix = _path.normpath(_path.join(_path.realpath(decoded_path), '..')) prefix = _path.normpath(_path.join(_path.realpath(decoded_path), ".."))
src_lib = _path.join(prefix, 'lib') src_lib = _path.join(prefix, "lib")
share_lib = _path.join(prefix, 'share', 'solaar', 'lib') share_lib = _path.join(prefix, "share", "solaar", "lib")
for location in src_lib, share_lib: for location in src_lib, share_lib:
init_py = _path.join(location, 'solaar', '__init__.py') init_py = _path.join(location, "solaar", "__init__.py")
# print ("sys.path[0]: checking", init_py) # print ("sys.path[0]: checking", init_py)
if _path.exists(init_py): if _path.exists(init_py):
# print ("sys.path[0]: found", location, "replacing", sys.path[0]) # print ("sys.path[0]: found", location, "replacing", sys.path[0])
@ -49,7 +49,8 @@ def init_paths():
break break
if __name__ == '__main__': if __name__ == "__main__":
init_paths() init_paths()
import solaar.gtk import solaar.gtk
solaar.gtk.main() solaar.gtk.main()

View File

@ -19,31 +19,35 @@
import platform as _platform import platform as _platform
if _platform.system() in ('Darwin', 'Windows'): if _platform.system() in ("Darwin", "Windows"):
from hidapi.hidapi import close # noqa: F401 from hidapi.hidapi import (
from hidapi.hidapi import enumerate # noqa: F401 close, # noqa: F401
from hidapi.hidapi import find_paired_node # noqa: F401 enumerate, # noqa: F401
from hidapi.hidapi import find_paired_node_wpid # noqa: F401 find_paired_node, # noqa: F401
from hidapi.hidapi import get_manufacturer # noqa: F401 find_paired_node_wpid, # noqa: F401
from hidapi.hidapi import get_product # noqa: F401 get_manufacturer, # noqa: F401
from hidapi.hidapi import get_serial # noqa: F401 get_product, # noqa: F401
from hidapi.hidapi import monitor_glib # noqa: F401 get_serial, # noqa: F401
from hidapi.hidapi import open # noqa: F401 monitor_glib, # noqa: F401
from hidapi.hidapi import open_path # noqa: F401 open, # noqa: F401
from hidapi.hidapi import read # noqa: F401 open_path, # noqa: F401
from hidapi.hidapi import write # noqa: F401 read, # noqa: F401
write, # noqa: F401
)
else: else:
from hidapi.udev import close # noqa: F401 from hidapi.udev import (
from hidapi.udev import enumerate # noqa: F401 close, # noqa: F401
from hidapi.udev import find_paired_node # noqa: F401 enumerate, # noqa: F401
from hidapi.udev import find_paired_node_wpid # noqa: F401 find_paired_node, # noqa: F401
from hidapi.udev import get_manufacturer # noqa: F401 find_paired_node_wpid, # noqa: F401
from hidapi.udev import get_product # noqa: F401 get_manufacturer, # noqa: F401
from hidapi.udev import get_serial # noqa: F401 get_product, # noqa: F401
from hidapi.udev import monitor_glib # noqa: F401 get_serial, # noqa: F401
from hidapi.udev import open # noqa: F401 monitor_glib, # noqa: F401
from hidapi.udev import open_path # noqa: F401 open, # noqa: F401
from hidapi.udev import read # noqa: F401 open_path, # noqa: F401
from hidapi.udev import write # noqa: F401 read, # noqa: F401
write, # noqa: F401
)
__version__ = '0.9' __version__ = "0.9"

View File

@ -35,30 +35,31 @@ from time import sleep
import gi import gi
gi.require_version('Gdk', '3.0') gi.require_version("Gdk", "3.0")
from gi.repository import GLib # NOQA: E402 from gi.repository import GLib # NOQA: E402
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
native_implementation = 'hidapi' native_implementation = "hidapi"
# Device info as expected by Solaar # Device info as expected by Solaar
DeviceInfo = namedtuple( DeviceInfo = namedtuple(
'DeviceInfo', [ "DeviceInfo",
'path', [
'bus_id', "path",
'vendor_id', "bus_id",
'product_id', "vendor_id",
'interface', "product_id",
'driver', "interface",
'manufacturer', "driver",
'product', "manufacturer",
'serial', "product",
'release', "serial",
'isDevice', "release",
'hidpp_short', "isDevice",
'hidpp_long', "hidpp_short",
] "hidpp_long",
],
) )
del namedtuple del namedtuple
@ -67,8 +68,15 @@ _hidapi = None
# hidapi binary names for various platforms # hidapi binary names for various platforms
_library_paths = ( _library_paths = (
'libhidapi-hidraw.so', 'libhidapi-hidraw.so.0', 'libhidapi-libusb.so', 'libhidapi-libusb.so.0', "libhidapi-hidraw.so",
'libhidapi-iohidmanager.so', 'libhidapi-iohidmanager.so.0', 'libhidapi.dylib', 'hidapi.dll', 'libhidapi-0.dll' "libhidapi-hidraw.so.0",
"libhidapi-libusb.so",
"libhidapi-libusb.so.0",
"libhidapi-iohidmanager.so",
"libhidapi-iohidmanager.so.0",
"libhidapi.dylib",
"hidapi.dll",
"libhidapi-0.dll",
) )
for lib in _library_paths: for lib in _library_paths:
@ -84,9 +92,9 @@ else:
# Retrieve version of hdiapi library # Retrieve version of hdiapi library
class _cHidApiVersion(ctypes.Structure): class _cHidApiVersion(ctypes.Structure):
_fields_ = [ _fields_ = [
('major', ctypes.c_int), ("major", ctypes.c_int),
('minor', ctypes.c_int), ("minor", ctypes.c_int),
('patch', ctypes.c_int), ("patch", ctypes.c_int),
] ]
@ -97,28 +105,27 @@ _hid_version = _hidapi.hid_version()
# Construct device info struct based on API version # Construct device info struct based on API version
class _cDeviceInfo(ctypes.Structure): class _cDeviceInfo(ctypes.Structure):
def as_dict(self): def as_dict(self):
return {name: getattr(self, name) for name, _t in self._fields_ if name != 'next'} return {name: getattr(self, name) for name, _t in self._fields_ if name != "next"}
# Low level hdiapi device info struct # Low level hdiapi device info struct
# See https://github.com/libusb/hidapi/blob/master/hidapi/hidapi.h#L143 # See https://github.com/libusb/hidapi/blob/master/hidapi/hidapi.h#L143
_cDeviceInfo_fields = [ _cDeviceInfo_fields = [
('path', ctypes.c_char_p), ("path", ctypes.c_char_p),
('vendor_id', ctypes.c_ushort), ("vendor_id", ctypes.c_ushort),
('product_id', ctypes.c_ushort), ("product_id", ctypes.c_ushort),
('serial_number', ctypes.c_wchar_p), ("serial_number", ctypes.c_wchar_p),
('release_number', ctypes.c_ushort), ("release_number", ctypes.c_ushort),
('manufacturer_string', ctypes.c_wchar_p), ("manufacturer_string", ctypes.c_wchar_p),
('product_string', ctypes.c_wchar_p), ("product_string", ctypes.c_wchar_p),
('usage_page', ctypes.c_ushort), ("usage_page", ctypes.c_ushort),
('usage', ctypes.c_ushort), ("usage", ctypes.c_ushort),
('interface_number', ctypes.c_int), ("interface_number", ctypes.c_int),
('next', ctypes.POINTER(_cDeviceInfo)), ("next", ctypes.POINTER(_cDeviceInfo)),
] ]
if _hid_version.contents.major >= 0 and _hid_version.contents.minor >= 13: if _hid_version.contents.major >= 0 and _hid_version.contents.minor >= 13:
_cDeviceInfo_fields.append(('bus_type', ctypes.c_int)) _cDeviceInfo_fields.append(("bus_type", ctypes.c_int))
_cDeviceInfo._fields_ = _cDeviceInfo_fields _cDeviceInfo._fields_ = _cDeviceInfo_fields
# Set up hidapi functions # Set up hidapi functions
@ -168,7 +175,7 @@ atexit.register(_hidapi.hid_exit)
# Solaar opens the same device more than once which will fail unless we # Solaar opens the same device more than once which will fail unless we
# allow non-exclusive opening. On windows opening with shared access is # allow non-exclusive opening. On windows opening with shared access is
# the default, for macOS we need to set it explicitly. # the default, for macOS we need to set it explicitly.
if _platform.system() == 'Darwin': if _platform.system() == "Darwin":
_hidapi.hid_darwin_set_open_exclusive.argtypes = [ctypes.c_int] _hidapi.hid_darwin_set_open_exclusive.argtypes = [ctypes.c_int]
_hidapi.hid_darwin_set_open_exclusive.restype = None _hidapi.hid_darwin_set_open_exclusive.restype = None
_hidapi.hid_darwin_set_open_exclusive(0) _hidapi.hid_darwin_set_open_exclusive(0)
@ -179,7 +186,7 @@ class HIDError(Exception):
def _enumerate_devices(): def _enumerate_devices():
""" Returns all HID devices which are potentially useful to us """ """Returns all HID devices which are potentially useful to us"""
devices = [] devices = []
c_devices = _hidapi.hid_enumerate(0, 0) c_devices = _hidapi.hid_enumerate(0, 0)
p = c_devices p = c_devices
@ -188,19 +195,19 @@ def _enumerate_devices():
p = p.contents.next p = p.contents.next
_hidapi.hid_free_enumeration(c_devices) _hidapi.hid_free_enumeration(c_devices)
keyboard_or_mouse = {d['path'] for d in devices if d['usage_page'] == 1 and d['usage'] in (6, 2)} keyboard_or_mouse = {d["path"] for d in devices if d["usage_page"] == 1 and d["usage"] in (6, 2)}
unique_devices = {} unique_devices = {}
for device in devices: for device in devices:
# On macOS we cannot access keyboard or mouse devices without special permissions. Since # On macOS we cannot access keyboard or mouse devices without special permissions. Since
# we don't need them anyway we remove them so opening them doesn't cause errors later. # we don't need them anyway we remove them so opening them doesn't cause errors later.
if device['path'] in keyboard_or_mouse: if device["path"] in keyboard_or_mouse:
# print(f"Ignoring keyboard or mouse device: {device}") # print(f"Ignoring keyboard or mouse device: {device}")
continue continue
# hidapi returns separate entries for each usage page of a device. # hidapi returns separate entries for each usage page of a device.
# Deduplicate by path to only keep one device entry. # Deduplicate by path to only keep one device entry.
if device['path'] not in unique_devices: if device["path"] not in unique_devices:
unique_devices[device['path']] = device unique_devices[device["path"]] = device
unique_devices = unique_devices.values() unique_devices = unique_devices.values()
# print("Unique devices:\n" + '\n'.join([f"{dev}" for dev in unique_devices])) # print("Unique devices:\n" + '\n'.join([f"{dev}" for dev in unique_devices]))
@ -209,7 +216,6 @@ def _enumerate_devices():
# Use a separate thread to check if devices have been removed or connected # Use a separate thread to check if devices have been removed or connected
class _DeviceMonitor(Thread): class _DeviceMonitor(Thread):
def __init__(self, device_callback, polling_delay=5.0): def __init__(self, device_callback, polling_delay=5.0):
self.device_callback = device_callback self.device_callback = device_callback
self.polling_delay = polling_delay self.polling_delay = polling_delay
@ -225,10 +231,10 @@ class _DeviceMonitor(Thread):
current_devices = {tuple(dev.items()): dev for dev in _enumerate_devices()} current_devices = {tuple(dev.items()): dev for dev in _enumerate_devices()}
for key, device in self.prev_devices.items(): for key, device in self.prev_devices.items():
if key not in current_devices: if key not in current_devices:
self.device_callback('remove', device) self.device_callback("remove", device)
for key, device in current_devices.items(): for key, device in current_devices.items():
if key not in self.prev_devices: if key not in self.prev_devices:
self.device_callback('add', device) self.device_callback("add", device)
self.prev_devices = current_devices self.prev_devices = current_devices
sleep(self.polling_delay) sleep(self.polling_delay)
@ -237,29 +243,29 @@ class _DeviceMonitor(Thread):
# It is given the bus id, vendor id, and product id and returns a dictionary # It is given the bus id, vendor id, and product id and returns a dictionary
# with the required hid_driver and usb_interface and whether this is a receiver or device. # with the required hid_driver and usb_interface and whether this is a receiver or device.
def _match(action, device, filterfn): def _match(action, device, filterfn):
vid = device['vendor_id'] vid = device["vendor_id"]
pid = device['product_id'] pid = device["product_id"]
# Translate hidapi bus_type to the bus_id values Solaar expects # Translate hidapi bus_type to the bus_id values Solaar expects
if device.get('bus_type') == 0x01: if device.get("bus_type") == 0x01:
bus_id = 0x03 # USB bus_id = 0x03 # USB
elif device.get('bus_type') == 0x02: elif device.get("bus_type") == 0x02:
bus_id = 0x05 # Bluetooth bus_id = 0x05 # Bluetooth
else: else:
bus_id = None bus_id = None
# Check for hidpp support # Check for hidpp support
device['hidpp_short'] = False device["hidpp_short"] = False
device['hidpp_long'] = False device["hidpp_long"] = False
device_handle = None device_handle = None
try: try:
device_handle = open_path(device['path']) device_handle = open_path(device["path"])
report = get_input_report(device_handle, 0x10, 32) report = get_input_report(device_handle, 0x10, 32)
if len(report) == 1 + 6 and report[0] == 0x10: if len(report) == 1 + 6 and report[0] == 0x10:
device['hidpp_short'] = True device["hidpp_short"] = True
report = get_input_report(device_handle, 0x11, 32) report = get_input_report(device_handle, 0x11, 32)
if len(report) == 1 + 19 and report[0] == 0x11: if len(report) == 1 + 19 and report[0] == 0x11:
device['hidpp_long'] = True device["hidpp_long"] = True
except HIDError as e: # noqa: F841 except HIDError as e: # noqa: F841
if logger.isEnabledFor(logging.INFO): if logger.isEnabledFor(logging.INFO):
logger.info(f"Error opening device {device['path']} ({bus_id}/{vid:04X}/{pid:04X}) for hidpp check: {e}") # noqa logger.info(f"Error opening device {device['path']} ({bus_id}/{vid:04X}/{pid:04X}) for hidpp check: {e}") # noqa
@ -269,41 +275,41 @@ def _match(action, device, filterfn):
if logger.isEnabledFor(logging.INFO): if logger.isEnabledFor(logging.INFO):
logger.info( logger.info(
'Found device BID %s VID %04X PID %04X HID++ %s %s', bus_id, vid, pid, device['hidpp_short'], device['hidpp_long'] "Found device BID %s VID %04X PID %04X HID++ %s %s", bus_id, vid, pid, device["hidpp_short"], device["hidpp_long"]
) )
if not device['hidpp_short'] and not device['hidpp_long']: if not device["hidpp_short"] and not device["hidpp_long"]:
return None return None
filter = filterfn(bus_id, vid, pid, device['hidpp_short'], device['hidpp_long']) filter = filterfn(bus_id, vid, pid, device["hidpp_short"], device["hidpp_long"])
if not filter: if not filter:
return return
isDevice = filter.get('isDevice') isDevice = filter.get("isDevice")
if action == 'add': if action == "add":
d_info = DeviceInfo( d_info = DeviceInfo(
path=device['path'].decode(), path=device["path"].decode(),
bus_id=bus_id, bus_id=bus_id,
vendor_id=f'{vid:04X}', # noqa vendor_id=f"{vid:04X}", # noqa
product_id=f'{pid:04X}', # noqa product_id=f"{pid:04X}", # noqa
interface=None, interface=None,
driver=None, driver=None,
manufacturer=device['manufacturer_string'], manufacturer=device["manufacturer_string"],
product=device['product_string'], product=device["product_string"],
serial=device['serial_number'], serial=device["serial_number"],
release=device['release_number'], release=device["release_number"],
isDevice=isDevice, isDevice=isDevice,
hidpp_short=device['hidpp_short'], hidpp_short=device["hidpp_short"],
hidpp_long=device['hidpp_long'], hidpp_long=device["hidpp_long"],
) )
return d_info return d_info
elif action == 'remove': elif action == "remove":
d_info = DeviceInfo( d_info = DeviceInfo(
path=device['path'].decode(), path=device["path"].decode(),
bus_id=None, bus_id=None,
vendor_id=f'{vid:04X}', # noqa vendor_id=f"{vid:04X}", # noqa
product_id=f'{pid:04X}', # noqa product_id=f"{pid:04X}", # noqa
interface=None, interface=None,
driver=None, driver=None,
manufacturer=None, manufacturer=None,
@ -328,14 +334,13 @@ def find_paired_node_wpid(receiver_path, index):
def monitor_glib(callback, filterfn): def monitor_glib(callback, filterfn):
def device_callback(action, device): def device_callback(action, device):
# print(f"device_callback({action}): {device}") # print(f"device_callback({action}): {device}")
if action == 'add': if action == "add":
d_info = _match(action, device, filterfn) d_info = _match(action, device, filterfn)
if d_info: if d_info:
GLib.idle_add(callback, action, d_info) GLib.idle_add(callback, action, d_info)
elif action == 'remove': elif action == "remove":
# Removed devices will be detected by Solaar directly # Removed devices will be detected by Solaar directly
pass pass
@ -352,7 +357,7 @@ def enumerate(filterfn):
:returns: a list of matching ``DeviceInfo`` tuples. :returns: a list of matching ``DeviceInfo`` tuples.
""" """
for device in _enumerate_devices(): for device in _enumerate_devices():
d_info = _match('add', device, filterfn) d_info = _match("add", device, filterfn)
if d_info: if d_info:
yield d_info yield d_info
@ -463,7 +468,7 @@ def read(device_handle, bytes_count, timeout_ms=None):
def get_input_report(device_handle, report_id, size): def get_input_report(device_handle, report_id, size):
assert device_handle assert device_handle
data = ctypes.create_string_buffer(size) data = ctypes.create_string_buffer(size)
data[0] = bytearray((report_id, )) data[0] = bytearray((report_id,))
size = _hidapi.hid_get_input_report(device_handle, data, size) size = _hidapi.hid_get_input_report(device_handle, data, size)
if size < 0: if size < 0:
raise HIDError(_hidapi.hid_error(device_handle)) raise HIDError(_hidapi.hid_error(device_handle))
@ -475,7 +480,7 @@ def _readstring(device_handle, func, max_length=255):
buf = ctypes.create_unicode_buffer(max_length) buf = ctypes.create_unicode_buffer(max_length)
ret = func(device_handle, buf, max_length) ret = func(device_handle, buf, max_length)
if ret < 0: if ret < 0:
raise HIDError('Error reading device property') raise HIDError("Error reading device property")
return buf.value return buf.value

View File

@ -40,10 +40,10 @@ except NameError:
read_packet = input read_packet = input
interactive = os.isatty(0) interactive = os.isatty(0)
prompt = '?? Input: ' if interactive else '' prompt = "?? Input: " if interactive else ""
start_time = time.time() start_time = time.time()
strhex = lambda d: hexlify(d).decode('ascii').upper() strhex = lambda d: hexlify(d).decode("ascii").upper()
# #
# #
@ -56,10 +56,10 @@ del Lock
def _print(marker, data, scroll=False): def _print(marker, data, scroll=False):
t = time.time() - start_time t = time.time() - start_time
if isinstance(data, str): if isinstance(data, str):
s = marker + ' ' + data s = marker + " " + data
else: else:
hexs = strhex(data) hexs = strhex(data)
s = '%s (% 8.3f) [%s %s %s %s] %s' % (marker, t, hexs[0:2], hexs[2:4], hexs[4:8], hexs[8:], repr(data)) s = "%s (% 8.3f) [%s %s %s %s] %s" % (marker, t, hexs[0:2], hexs[2:4], hexs[4:8], hexs[8:], repr(data))
with print_lock: with print_lock:
# allow only one thread at a time to write to the console, otherwise # allow only one thread at a time to write to the console, otherwise
@ -68,18 +68,18 @@ def _print(marker, data, scroll=False):
if interactive and scroll: if interactive and scroll:
# scroll the entire screen above the current line up by 1 line # scroll the entire screen above the current line up by 1 line
sys.stdout.write( sys.stdout.write(
'\033[s' # save cursor position "\033[s" # save cursor position
'\033[S' # scroll up "\033[S" # scroll up
'\033[A' # cursor up "\033[A" # cursor up
'\033[L' # insert 1 line "\033[L" # insert 1 line
'\033[G' "\033[G"
) # move cursor to column 1 ) # move cursor to column 1
sys.stdout.write(s) sys.stdout.write(s)
if interactive and scroll: if interactive and scroll:
# restore cursor position # restore cursor position
sys.stdout.write('\033[u') sys.stdout.write("\033[u")
else: else:
sys.stdout.write('\n') sys.stdout.write("\n")
# flush stdout manually... # flush stdout manually...
# because trying to open stdin/out unbuffered programmatically # because trying to open stdin/out unbuffered programmatically
@ -88,7 +88,7 @@ def _print(marker, data, scroll=False):
def _error(text, scroll=False): def _error(text, scroll=False):
_print('!!', text, scroll) _print("!!", text, scroll)
def _continuous_read(handle, timeout=2000): def _continuous_read(handle, timeout=2000):
@ -96,79 +96,78 @@ def _continuous_read(handle, timeout=2000):
try: try:
reply = _hid.read(handle, 128, timeout) reply = _hid.read(handle, 128, timeout)
except OSError as e: except OSError as e:
_error('Read failed, aborting: ' + str(e), True) _error("Read failed, aborting: " + str(e), True)
break break
assert reply is not None assert reply is not None
if reply: if reply:
_print('>>', reply, True) _print(">>", reply, True)
def _validate_input(line, hidpp=False): def _validate_input(line, hidpp=False):
try: try:
data = unhexlify(line.encode('ascii')) data = unhexlify(line.encode("ascii"))
except Exception as e: except Exception as e:
_error('Invalid input: ' + str(e)) _error("Invalid input: " + str(e))
return None return None
if hidpp: if hidpp:
if len(data) < 4: if len(data) < 4:
_error('Invalid HID++ request: need at least 4 bytes') _error("Invalid HID++ request: need at least 4 bytes")
return None return None
if data[:1] not in b'\x10\x11': if data[:1] not in b"\x10\x11":
_error('Invalid HID++ request: first byte must be 0x10 or 0x11') _error("Invalid HID++ request: first byte must be 0x10 or 0x11")
return None return None
if data[1:2] not in b'\xFF\x00\x01\x02\x03\x04\x05\x06\x07': if data[1:2] not in b"\xFF\x00\x01\x02\x03\x04\x05\x06\x07":
_error('Invalid HID++ request: second byte must be 0xFF or one of 0x00..0x07') _error("Invalid HID++ request: second byte must be 0xFF or one of 0x00..0x07")
return None return None
if data[:1] == b'\x10': if data[:1] == b"\x10":
if len(data) > 7: if len(data) > 7:
_error('Invalid HID++ request: maximum length of a 0x10 request is 7 bytes') _error("Invalid HID++ request: maximum length of a 0x10 request is 7 bytes")
return None return None
while len(data) < 7: while len(data) < 7:
data = (data + b'\x00' * 7)[:7] data = (data + b"\x00" * 7)[:7]
elif data[:1] == b'\x11': elif data[:1] == b"\x11":
if len(data) > 20: if len(data) > 20:
_error('Invalid HID++ request: maximum length of a 0x11 request is 20 bytes') _error("Invalid HID++ request: maximum length of a 0x11 request is 20 bytes")
return None return None
while len(data) < 20: while len(data) < 20:
data = (data + b'\x00' * 20)[:20] data = (data + b"\x00" * 20)[:20]
return data return data
def _open(args): def _open(args):
def matchfn(bid, vid, pid, _a, _b): def matchfn(bid, vid, pid, _a, _b):
if vid == 0x046d: if vid == 0x046D:
return {'vid': 0x046d} return {"vid": 0x046D}
device = args.device device = args.device
if args.hidpp and not device: if args.hidpp and not device:
for d in _hid.enumerate(matchfn): for d in _hid.enumerate(matchfn):
if d.driver == 'logitech-djreceiver': if d.driver == "logitech-djreceiver":
device = d.path device = d.path
break break
if not device: if not device:
sys.exit('!! No HID++ receiver found.') sys.exit("!! No HID++ receiver found.")
if not device: if not device:
sys.exit('!! Device path required.') sys.exit("!! Device path required.")
print('.. Opening device', device) print(".. Opening device", device)
handle = _hid.open_path(device) handle = _hid.open_path(device)
if not handle: if not handle:
sys.exit('!! Failed to open %s, aborting.' % device) sys.exit("!! Failed to open %s, aborting." % device)
print( print(
'.. Opened handle %r, vendor %r product %r serial %r.' % ".. Opened handle %r, vendor %r product %r serial %r."
(handle, _hid.get_manufacturer(handle), _hid.get_product(handle), _hid.get_serial(handle)) % (handle, _hid.get_manufacturer(handle), _hid.get_product(handle), _hid.get_serial(handle))
) )
if args.hidpp: if args.hidpp:
if _hid.get_manufacturer(handle) is not None and _hid.get_manufacturer(handle) != b'Logitech': if _hid.get_manufacturer(handle) is not None and _hid.get_manufacturer(handle) != b"Logitech":
sys.exit('!! Only Logitech devices support the HID++ protocol.') sys.exit("!! Only Logitech devices support the HID++ protocol.")
print('.. HID++ validation enabled.') print(".. HID++ validation enabled.")
else: else:
if (_hid.get_manufacturer(handle) == b'Logitech' and b'Receiver' in _hid.get_product(handle)): if _hid.get_manufacturer(handle) == b"Logitech" and b"Receiver" in _hid.get_product(handle):
args.hidpp = True args.hidpp = True
print('.. Logitech receiver detected, HID++ validation enabled.') print(".. Logitech receiver detected, HID++ validation enabled.")
return handle return handle
@ -180,13 +179,13 @@ def _open(args):
def _parse_arguments(): def _parse_arguments():
arg_parser = argparse.ArgumentParser() arg_parser = argparse.ArgumentParser()
arg_parser.add_argument('--history', help='history file (default ~/.hidconsole-history)') arg_parser.add_argument("--history", help="history file (default ~/.hidconsole-history)")
arg_parser.add_argument('--hidpp', action='store_true', help='ensure input data is a valid HID++ request') arg_parser.add_argument("--hidpp", action="store_true", help="ensure input data is a valid HID++ request")
arg_parser.add_argument( arg_parser.add_argument(
'device', "device",
nargs='?', nargs="?",
help='linux device to connect to (/dev/hidrawX); ' help="linux device to connect to (/dev/hidrawX); "
'may be omitted if --hidpp is given, in which case it looks for the first Logitech receiver' "may be omitted if --hidpp is given, in which case it looks for the first Logitech receiver",
) )
return arg_parser.parse_args() return arg_parser.parse_args()
@ -196,10 +195,10 @@ def main():
handle = _open(args) handle = _open(args)
if interactive: if interactive:
print('.. Press ^C/^D to exit, or type hex bytes to write to the device.') print(".. Press ^C/^D to exit, or type hex bytes to write to the device.")
if args.history is None: if args.history is None:
args.history = os.path.join(os.path.expanduser('~'), '.hidconsole-history') args.history = os.path.join(os.path.expanduser("~"), ".hidconsole-history")
try: try:
readline.read_history_file(args.history) readline.read_history_file(args.history)
except Exception: except Exception:
@ -207,17 +206,17 @@ def main():
pass pass
try: try:
t = Thread(target=_continuous_read, args=(handle, )) t = Thread(target=_continuous_read, args=(handle,))
t.daemon = True t.daemon = True
t.start() t.start()
if interactive: if interactive:
# move the cursor at the bottom of the screen # move the cursor at the bottom of the screen
sys.stdout.write('\033[300B') # move cusor at most 300 lines down, don't scroll sys.stdout.write("\033[300B") # move cusor at most 300 lines down, don't scroll
while t.is_alive(): while t.is_alive():
line = read_packet(prompt) line = read_packet(prompt)
line = line.strip().replace(' ', '') line = line.strip().replace(" ", "")
# print ("line", line) # print ("line", line)
if not line: if not line:
continue continue
@ -226,12 +225,12 @@ def main():
if data is None: if data is None:
continue continue
_print('<<', data) _print("<<", data)
_hid.write(handle, data) _hid.write(handle, data)
# wait for some kind of reply # wait for some kind of reply
if args.hidpp and not interactive: if args.hidpp and not interactive:
rlist, wlist, xlist = _select([handle], [], [], 1) rlist, wlist, xlist = _select([handle], [], [], 1)
if data[1:2] == b'\xFF': if data[1:2] == b"\xFF":
# the receiver will reply very fast, in a few milliseconds # the receiver will reply very fast, in a few milliseconds
time.sleep(0.010) time.sleep(0.010)
else: else:
@ -239,16 +238,16 @@ def main():
time.sleep(0.700) time.sleep(0.700)
except EOFError: except EOFError:
if interactive: if interactive:
print('') print("")
else: else:
time.sleep(1) time.sleep(1)
finally: finally:
print('.. Closing handle %r' % handle) print(".. Closing handle %r" % handle)
_hid.close(handle) _hid.close(handle)
if interactive: if interactive:
readline.write_history_file(args.history) readline.write_history_file(args.history)
if __name__ == '__main__': if __name__ == "__main__":
main() main()

View File

@ -29,6 +29,7 @@ import logging
import os as _os import os as _os
import warnings as _warnings import warnings as _warnings
# the tuple object we'll expose when enumerating devices # the tuple object we'll expose when enumerating devices
from collections import namedtuple from collections import namedtuple
from select import select as _select from select import select as _select
@ -44,30 +45,31 @@ from pyudev import DeviceNotFoundError
from pyudev import Devices as _Devices from pyudev import Devices as _Devices
from pyudev import Monitor as _Monitor from pyudev import Monitor as _Monitor
gi.require_version('Gdk', '3.0') gi.require_version("Gdk", "3.0")
from gi.repository import GLib # NOQA: E402 from gi.repository import GLib # NOQA: E402
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
native_implementation = 'udev' native_implementation = "udev"
fileopen = open fileopen = open
DeviceInfo = namedtuple( DeviceInfo = namedtuple(
'DeviceInfo', [ "DeviceInfo",
'path', [
'bus_id', "path",
'vendor_id', "bus_id",
'product_id', "vendor_id",
'interface', "product_id",
'driver', "interface",
'manufacturer', "driver",
'product', "manufacturer",
'serial', "product",
'release', "serial",
'isDevice', "release",
'hidpp_short', "isDevice",
'hidpp_long', "hidpp_short",
] "hidpp_long",
],
) )
del namedtuple del namedtuple
@ -100,24 +102,24 @@ def exit():
# with the required hid_driver and usb_interface and whether this is a receiver or device. # with the required hid_driver and usb_interface and whether this is a receiver or device.
def _match(action, device, filterfn): def _match(action, device, filterfn):
if logger.isEnabledFor(logging.DEBUG): if logger.isEnabledFor(logging.DEBUG):
logger.debug(f'Dbus event {action} {device}') logger.debug(f"Dbus event {action} {device}")
hid_device = device.find_parent('hid') hid_device = device.find_parent("hid")
if not hid_device: # only HID devices are of interest to Solaar if not hid_device: # only HID devices are of interest to Solaar
return return
hid_id = hid_device.get('HID_ID') hid_id = hid_device.get("HID_ID")
if not hid_id: if not hid_id:
return # there are reports that sometimes the id isn't set up right so be defensive return # there are reports that sometimes the id isn't set up right so be defensive
bid, vid, pid = hid_id.split(':') bid, vid, pid = hid_id.split(":")
hid_hid_device = hid_device.find_parent('hid') hid_hid_device = hid_device.find_parent("hid")
if hid_hid_device: if hid_hid_device:
return # these are devices connected through a receiver so don't pick them up here return # these are devices connected through a receiver so don't pick them up here
try: # if report descriptor does not indicate HID++ capabilities then this device is not of interest to Solaar try: # if report descriptor does not indicate HID++ capabilities then this device is not of interest to Solaar
hidpp_short = hidpp_long = False hidpp_short = hidpp_long = False
devfile = '/sys' + hid_device.get('DEVPATH') + '/report_descriptor' devfile = "/sys" + hid_device.get("DEVPATH") + "/report_descriptor"
with fileopen(devfile, 'rb') as fd: with fileopen(devfile, "rb") as fd:
with _warnings.catch_warnings(): with _warnings.catch_warnings():
_warnings.simplefilter('ignore') _warnings.simplefilter("ignore")
rd = _ReportDescriptor(fd.read()) rd = _ReportDescriptor(fd.read())
hidpp_short = 0x10 in rd.input_report_ids and 6 * 8 == int(rd.get_input_report_size(0x10)) hidpp_short = 0x10 in rd.input_report_ids and 6 * 8 == int(rd.get_input_report_size(0x10))
# and _Usage(0xFF00, 0x0001) in rd.get_input_items(0x10)[0].usages # be more permissive # and _Usage(0xFF00, 0x0001) in rd.get_input_items(0x10)[0].usages # be more permissive
@ -128,18 +130,18 @@ def _match(action, device, filterfn):
except Exception as e: # if can't process report descriptor fall back to old scheme except Exception as e: # if can't process report descriptor fall back to old scheme
hidpp_short = hidpp_long = None hidpp_short = hidpp_long = None
logger.info( logger.info(
'Report Descriptor not processed for DEVICE %s BID %s VID %s PID %s: %s', device.device_node, bid, vid, pid, e "Report Descriptor not processed for DEVICE %s BID %s VID %s PID %s: %s", device.device_node, bid, vid, pid, e
) )
filter = filterfn(int(bid, 16), int(vid, 16), int(pid, 16), hidpp_short, hidpp_long) filter = filterfn(int(bid, 16), int(vid, 16), int(pid, 16), hidpp_short, hidpp_long)
if not filter: if not filter:
return return
hid_driver = filter.get('hid_driver') hid_driver = filter.get("hid_driver")
interface_number = filter.get('usb_interface') interface_number = filter.get("usb_interface")
isDevice = filter.get('isDevice') isDevice = filter.get("isDevice")
if action == 'add': if action == "add":
hid_driver_name = hid_device.get('DRIVER') hid_driver_name = hid_device.get("DRIVER")
# print ("** found hid", action, device, "hid:", hid_device, hid_driver_name) # print ("** found hid", action, device, "hid:", hid_device, hid_driver_name)
if hid_driver: if hid_driver:
if isinstance(hid_driver, tuple): if isinstance(hid_driver, tuple):
@ -148,13 +150,20 @@ def _match(action, device, filterfn):
elif hid_driver_name != hid_driver: elif hid_driver_name != hid_driver:
return return
intf_device = device.find_parent('usb', 'usb_interface') intf_device = device.find_parent("usb", "usb_interface")
usb_interface = None if intf_device is None else intf_device.attributes.asint('bInterfaceNumber') usb_interface = None if intf_device is None else intf_device.attributes.asint("bInterfaceNumber")
# print('*** usb interface', action, device, 'usb_interface:', intf_device, usb_interface, interface_number) # print('*** usb interface', action, device, 'usb_interface:', intf_device, usb_interface, interface_number)
if logger.isEnabledFor(logging.INFO): if logger.isEnabledFor(logging.INFO):
logger.info( logger.info(
'Found device %s BID %s VID %s PID %s HID++ %s %s USB %s %s', device.device_node, bid, vid, pid, hidpp_short, "Found device %s BID %s VID %s PID %s HID++ %s %s USB %s %s",
hidpp_long, usb_interface, interface_number device.device_node,
bid,
vid,
pid,
hidpp_short,
hidpp_long,
usb_interface,
interface_number,
) )
if not (hidpp_short or hidpp_long or interface_number is None or interface_number == usb_interface): if not (hidpp_short or hidpp_long or interface_number is None or interface_number == usb_interface):
return return
@ -167,17 +176,17 @@ def _match(action, device, filterfn):
product_id=pid[-4:], product_id=pid[-4:],
interface=usb_interface, interface=usb_interface,
driver=hid_driver_name, driver=hid_driver_name,
manufacturer=attrs.get('manufacturer') if attrs else None, manufacturer=attrs.get("manufacturer") if attrs else None,
product=attrs.get('product') if attrs else None, product=attrs.get("product") if attrs else None,
serial=hid_device.get('HID_UNIQ'), serial=hid_device.get("HID_UNIQ"),
release=attrs.get('bcdDevice') if attrs else None, release=attrs.get("bcdDevice") if attrs else None,
isDevice=isDevice, isDevice=isDevice,
hidpp_short=hidpp_short, hidpp_short=hidpp_short,
hidpp_long=hidpp_long, hidpp_long=hidpp_long,
) )
return d_info return d_info
elif action == 'remove': elif action == "remove":
# print (dict(device), dict(usb_device)) # print (dict(device), dict(usb_device))
d_info = DeviceInfo( d_info = DeviceInfo(
@ -201,17 +210,17 @@ def _match(action, device, filterfn):
def find_paired_node(receiver_path, index, timeout): def find_paired_node(receiver_path, index, timeout):
"""Find the node of a device paired with a receiver""" """Find the node of a device paired with a receiver"""
context = _Context() context = _Context()
receiver_phys = _Devices.from_device_file(context, receiver_path).find_parent('hid').get('HID_PHYS') receiver_phys = _Devices.from_device_file(context, receiver_path).find_parent("hid").get("HID_PHYS")
if not receiver_phys: if not receiver_phys:
return None return None
phys = f'{receiver_phys}:{index}' # noqa: E231 phys = f"{receiver_phys}:{index}" # noqa: E231
timeout += _timestamp() timeout += _timestamp()
delta = _timestamp() delta = _timestamp()
while delta < timeout: while delta < timeout:
for dev in context.list_devices(subsystem='hidraw'): for dev in context.list_devices(subsystem="hidraw"):
dev_phys = dev.find_parent('hid').get('HID_PHYS') dev_phys = dev.find_parent("hid").get("HID_PHYS")
if dev_phys and dev_phys == phys: if dev_phys and dev_phys == phys:
return dev.device_node return dev.device_node
delta = _timestamp() delta = _timestamp()
@ -222,17 +231,17 @@ def find_paired_node(receiver_path, index, timeout):
def find_paired_node_wpid(receiver_path, index): def find_paired_node_wpid(receiver_path, index):
"""Find the node of a device paired with a receiver, get wpid from udev""" """Find the node of a device paired with a receiver, get wpid from udev"""
context = _Context() context = _Context()
receiver_phys = _Devices.from_device_file(context, receiver_path).find_parent('hid').get('HID_PHYS') receiver_phys = _Devices.from_device_file(context, receiver_path).find_parent("hid").get("HID_PHYS")
if not receiver_phys: if not receiver_phys:
return None return None
phys = f'{receiver_phys}:{index}' # noqa: E231 phys = f"{receiver_phys}:{index}" # noqa: E231
for dev in context.list_devices(subsystem='hidraw'): for dev in context.list_devices(subsystem="hidraw"):
dev_phys = dev.find_parent('hid').get('HID_PHYS') dev_phys = dev.find_parent("hid").get("HID_PHYS")
if dev_phys and dev_phys == phys: if dev_phys and dev_phys == phys:
# get hid id like 0003:0000046D:00000065 # get hid id like 0003:0000046D:00000065
hid_id = dev.find_parent('hid').get('HID_ID') hid_id = dev.find_parent("hid").get("HID_ID")
# get wpid - last 4 symbols # get wpid - last 4 symbols
udev_wpid = hid_id[-4:] udev_wpid = hid_id[-4:]
return udev_wpid return udev_wpid
@ -253,7 +262,7 @@ def monitor_glib(callback, filterfn):
# break # break
m = _Monitor.from_netlink(c) m = _Monitor.from_netlink(c)
m.filter_by(subsystem='hidraw') m.filter_by(subsystem="hidraw")
def _process_udev_event(monitor, condition, cb, filterfn): def _process_udev_event(monitor, condition, cb, filterfn):
if condition == GLib.IO_IN: if condition == GLib.IO_IN:
@ -261,11 +270,11 @@ def monitor_glib(callback, filterfn):
if event: if event:
action, device = event action, device = event
# print ("***", action, device) # print ("***", action, device)
if action == 'add': if action == "add":
d_info = _match(action, device, filterfn) d_info = _match(action, device, filterfn)
if d_info: if d_info:
GLib.idle_add(cb, action, d_info) GLib.idle_add(cb, action, d_info)
elif action == 'remove': elif action == "remove":
# the GLib notification does _not_ match! # the GLib notification does _not_ match!
pass pass
return True return True
@ -284,7 +293,7 @@ def monitor_glib(callback, filterfn):
# print ("did io_add_watch") # print ("did io_add_watch")
if logger.isEnabledFor(logging.DEBUG): if logger.isEnabledFor(logging.DEBUG):
logger.debug('Starting dbus monitoring') logger.debug("Starting dbus monitoring")
m.start() m.start()
@ -298,9 +307,9 @@ def enumerate(filterfn):
""" """
if logger.isEnabledFor(logging.DEBUG): if logger.isEnabledFor(logging.DEBUG):
logger.debug('Starting dbus enumeration') logger.debug("Starting dbus enumeration")
for dev in _Context().list_devices(subsystem='hidraw'): for dev in _Context().list_devices(subsystem="hidraw"):
dev_info = _match('add', dev, filterfn) dev_info = _match("add", dev, filterfn)
if dev_info: if dev_info:
yield dev_info yield dev_info
@ -329,16 +338,16 @@ def open_path(device_path):
:returns: an opaque device handle, or ``None``. :returns: an opaque device handle, or ``None``.
""" """
assert device_path assert device_path
assert device_path.startswith('/dev/hidraw') assert device_path.startswith("/dev/hidraw")
logger.info('OPEN PATH %s', device_path) logger.info("OPEN PATH %s", device_path)
retrycount = 0 retrycount = 0
while (retrycount < 3): while retrycount < 3:
retrycount += 1 retrycount += 1
try: try:
return _os.open(device_path, _os.O_RDWR | _os.O_SYNC) return _os.open(device_path, _os.O_RDWR | _os.O_SYNC)
except OSError as e: except OSError as e:
logger.info('OPEN PATH FAILED %s ERROR %s %s', device_path, e.errno, e) logger.info("OPEN PATH FAILED %s ERROR %s %s", device_path, e.errno, e)
if e.errno == _errno.EACCES: if e.errno == _errno.EACCES:
sleep(0.1) sleep(0.1)
else: else:
@ -380,7 +389,7 @@ def write(device_handle, data):
assert isinstance(data, bytes), (repr(data), type(data)) assert isinstance(data, bytes), (repr(data), type(data))
retrycount = 0 retrycount = 0
bytes_written = 0 bytes_written = 0
while (retrycount < 3): while retrycount < 3:
try: try:
retrycount += 1 retrycount += 1
bytes_written = _os.write(device_handle, data) bytes_written = _os.write(device_handle, data)
@ -390,7 +399,7 @@ def write(device_handle, data):
else: else:
break break
if bytes_written != len(data): if bytes_written != len(data):
raise OSError(_errno.EIO, 'written %d bytes out of expected %d' % (bytes_written, len(data))) raise OSError(_errno.EIO, "written %d bytes out of expected %d" % (bytes_written, len(data)))
def read(device_handle, bytes_count, timeout_ms=-1): def read(device_handle, bytes_count, timeout_ms=-1):
@ -415,7 +424,7 @@ def read(device_handle, bytes_count, timeout_ms=-1):
if xlist: if xlist:
assert xlist == [device_handle] assert xlist == [device_handle]
raise OSError(_errno.EIO, 'exception on file descriptor %d' % device_handle) raise OSError(_errno.EIO, "exception on file descriptor %d" % device_handle)
if rlist: if rlist:
assert rlist == [device_handle] assert rlist == [device_handle]
@ -424,13 +433,13 @@ def read(device_handle, bytes_count, timeout_ms=-1):
assert isinstance(data, bytes), (repr(data), type(data)) assert isinstance(data, bytes), (repr(data), type(data))
return data return data
else: else:
return b'' return b""
_DEVICE_STRINGS = { _DEVICE_STRINGS = {
0: 'manufacturer', 0: "manufacturer",
1: 'product', 1: "product",
2: 'serial', 2: "serial",
} }
@ -477,20 +486,20 @@ def get_indexed_string(device_handle, index):
assert device_handle assert device_handle
stat = _os.fstat(device_handle) stat = _os.fstat(device_handle)
try: try:
dev = _Device.from_device_number(_Context(), 'char', stat.st_rdev) dev = _Device.from_device_number(_Context(), "char", stat.st_rdev)
except (DeviceNotFoundError, ValueError): except (DeviceNotFoundError, ValueError):
return None return None
hid_dev = dev.find_parent('hid') hid_dev = dev.find_parent("hid")
if hid_dev: if hid_dev:
assert 'HID_ID' in hid_dev assert "HID_ID" in hid_dev
bus, _ignore, _ignore = hid_dev['HID_ID'].split(':') bus, _ignore, _ignore = hid_dev["HID_ID"].split(":")
if bus == '0003': # USB if bus == "0003": # USB
usb_dev = dev.find_parent('usb', 'usb_device') usb_dev = dev.find_parent("usb", "usb_device")
assert usb_dev assert usb_dev
return usb_dev.attributes.get(key) return usb_dev.attributes.get(key)
elif bus == '0005': # BLUETOOTH elif bus == "0005": # BLUETOOTH
# TODO # TODO
pass pass

View File

@ -5,39 +5,39 @@ from re import findall
from subprocess import run from subprocess import run
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
repo = 'https://github.com/freedesktop/xorg-proto-x11proto.git' repo = "https://github.com/freedesktop/xorg-proto-x11proto.git"
xx = 'https://gitlab.freedesktop.org/xorg/proto/xorgproto/-/tree/master/include/X11/' xx = "https://gitlab.freedesktop.org/xorg/proto/xorgproto/-/tree/master/include/X11/"
repo = 'https://gitlab.freedesktop.org/xorg/proto/xorgproto.git' repo = "https://gitlab.freedesktop.org/xorg/proto/xorgproto.git"
pattern = r'#define XK_(\w+)\s+0x(\w+)(?:\s+/\*\s+U\+(\w+))?' pattern = r"#define XK_(\w+)\s+0x(\w+)(?:\s+/\*\s+U\+(\w+))?"
xf86pattern = r'#define XF86XK_(\w+)\s+0x(\w+)(?:\s+/\*\s+U\+(\w+))?' xf86pattern = r"#define XF86XK_(\w+)\s+0x(\w+)(?:\s+/\*\s+U\+(\w+))?"
def main(): def main():
keysymdef = {} keysymdef = {}
with TemporaryDirectory() as temp: with TemporaryDirectory() as temp:
run(['git', 'clone', repo, '.'], cwd=temp) run(["git", "clone", repo, "."], cwd=temp)
# text = Path(temp, 'keysymdef.h').read_text() # text = Path(temp, 'keysymdef.h').read_text()
text = Path(temp, 'include/X11/keysymdef.h').read_text() text = Path(temp, "include/X11/keysymdef.h").read_text()
for name, sym, uni in findall(pattern, text): for name, sym, uni in findall(pattern, text):
sym = int(sym, 16) sym = int(sym, 16)
uni = int(uni, 16) if uni else None uni = int(uni, 16) if uni else None
if keysymdef.get(name, None): if keysymdef.get(name, None):
print('KEY DUP', name) print("KEY DUP", name)
keysymdef[name] = sym keysymdef[name] = sym
# text = Path(temp, 'keysymdef.h').read_text() # text = Path(temp, 'keysymdef.h').read_text()
text = Path(temp, 'include/X11/XF86keysym.h').read_text() text = Path(temp, "include/X11/XF86keysym.h").read_text()
for name, sym, uni in findall(xf86pattern, text): for name, sym, uni in findall(xf86pattern, text):
sym = int(sym, 16) sym = int(sym, 16)
uni = int(uni, 16) if uni else None uni = int(uni, 16) if uni else None
if keysymdef.get('XF86_' + name, None): if keysymdef.get("XF86_" + name, None):
print('KEY DUP', 'XF86_' + name) print("KEY DUP", "XF86_" + name)
keysymdef['XF86_' + name] = sym keysymdef["XF86_" + name] = sym
with open('keysymdef.py', 'w') as f: with open("keysymdef.py", "w") as f:
f.write('# flake8: noqa\nkeysymdef = \\\n') f.write("# flake8: noqa\nkeysymdef = \\\n")
pprint(keysymdef, f) pprint(keysymdef, f)
if __name__ == '__main__': if __name__ == "__main__":
main() main()

File diff suppressed because it is too large Load Diff

View File

@ -38,4 +38,4 @@ logger.setLevel(logging.root.level)
del logging del logging
__version__ = '0.9' __version__ = "0.9"

View File

@ -45,14 +45,14 @@ logger = logging.getLogger(__name__)
# #
_wired_device = lambda product_id, interface: { _wired_device = lambda product_id, interface: {
'vendor_id': 0x046d, "vendor_id": 0x046D,
'product_id': product_id, "product_id": product_id,
'bus_id': 0x3, "bus_id": 0x3,
'usb_interface': interface, "usb_interface": interface,
'isDevice': True "isDevice": True,
} }
_bt_device = lambda product_id: {'vendor_id': 0x046d, 'product_id': product_id, 'bus_id': 0x5, 'isDevice': True} _bt_device = lambda product_id: {"vendor_id": 0x046D, "product_id": product_id, "bus_id": 0x5, "isDevice": True}
DEVICE_IDS = [] DEVICE_IDS = []
@ -66,13 +66,13 @@ for _ignore, d in _DEVICES.items():
def other_device_check(bus_id, vendor_id, product_id): def other_device_check(bus_id, vendor_id, product_id):
"""Check whether product is a Logitech USB-connected or Bluetooth device based on bus, vendor, and product IDs """Check whether product is a Logitech USB-connected or Bluetooth device based on bus, vendor, and product IDs
This allows Solaar to support receiverless HID++ 2.0 devices that it knows nothing about""" This allows Solaar to support receiverless HID++ 2.0 devices that it knows nothing about"""
if vendor_id != 0x46d: # Logitech if vendor_id != 0x46D: # Logitech
return return
if bus_id == 0x3: # USB if bus_id == 0x3: # USB
if (product_id >= 0xC07D and product_id <= 0xC094 or product_id >= 0xC32B and product_id <= 0xC344): if product_id >= 0xC07D and product_id <= 0xC094 or product_id >= 0xC32B and product_id <= 0xC344:
return _wired_device(product_id, 2) return _wired_device(product_id, 2)
elif bus_id == 0x5: # Bluetooth elif bus_id == 0x5: # Bluetooth
if (product_id >= 0xB012 and product_id <= 0xB0FF or product_id >= 0xB317 and product_id <= 0xB3FF): if product_id >= 0xB012 and product_id <= 0xB0FF or product_id >= 0xB317 and product_id <= 0xB3FF:
return _bt_device(product_id) return _bt_device(product_id)
@ -80,7 +80,7 @@ def product_information(usb_id):
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 _RECEIVER_USB_IDS:
if usb_id == r.get('product_id'): if usb_id == r.get("product_id"):
return r return r
return {} return {}
@ -103,7 +103,7 @@ report_lengths = {
HIDPP_SHORT_MESSAGE_ID: _SHORT_MESSAGE_SIZE, HIDPP_SHORT_MESSAGE_ID: _SHORT_MESSAGE_SIZE,
HIDPP_LONG_MESSAGE_ID: _LONG_MESSAGE_SIZE, HIDPP_LONG_MESSAGE_ID: _LONG_MESSAGE_SIZE,
DJ_MESSAGE_ID: _MEDIUM_MESSAGE_SIZE, DJ_MESSAGE_ID: _MEDIUM_MESSAGE_SIZE,
0x21: _MAX_READ_SIZE 0x21: _MAX_READ_SIZE,
} }
"""Default timeout on read (in seconds).""" """Default timeout on read (in seconds)."""
DEFAULT_TIMEOUT = 4 DEFAULT_TIMEOUT = 4
@ -120,9 +120,11 @@ _PING_TIMEOUT = DEFAULT_TIMEOUT
def match(record, bus_id, vendor_id, product_id): def match(record, bus_id, vendor_id, product_id):
return ((record.get('bus_id') is None or record.get('bus_id') == bus_id) return (
and (record.get('vendor_id') is None or record.get('vendor_id') == vendor_id) (record.get("bus_id") is None or record.get("bus_id") == bus_id)
and (record.get('product_id') is None or record.get('product_id') == product_id)) and (record.get("vendor_id") is None or record.get("vendor_id") == vendor_id)
and (record.get("product_id") is None or record.get("product_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):
@ -131,7 +133,7 @@ def filter_receivers(bus_id, vendor_id, product_id, hidpp_short=False, hidpp_lon
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
return {'vendor_id': vendor_id, 'product_id': product_id, 'bus_id': bus_id, 'isDevice': False} return {"vendor_id": vendor_id, "product_id": product_id, "bus_id": bus_id, "isDevice": False}
def receivers(): def receivers():
@ -148,7 +150,7 @@ def filter(bus_id, vendor_id, product_id, hidpp_short=False, hidpp_long=False):
if match(record, bus_id, vendor_id, product_id): if match(record, bus_id, vendor_id, product_id):
return record return record
if hidpp_short or hidpp_long: # unknown devices that use HID++ if hidpp_short or hidpp_long: # unknown devices that use HID++
return {'vendor_id': vendor_id, 'product_id': product_id, 'bus_id': bus_id, 'isDevice': True} return {"vendor_id": vendor_id, "product_id": product_id, "bus_id": bus_id, "isDevice": True}
elif hidpp_short is None and hidpp_long is None: # unknown devices in correct range of IDs elif hidpp_short is None and hidpp_long is None: # unknown devices in correct range of IDs
return other_device_check(bus_id, vendor_id, product_id) return other_device_check(bus_id, vendor_id, product_id)
@ -229,17 +231,17 @@ def write(handle, devnumber, data, long_message=False):
assert data is not None assert data is not None
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 = _pack("!BB18s", HIDPP_LONG_MESSAGE_ID, devnumber, data)
else: else:
wdata = _pack('!BB5s', HIDPP_SHORT_MESSAGE_ID, devnumber, data) wdata = _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, _strhex(wdata[2:4]), _strhex(wdata[4:]))
try: try:
_hid.write(int(handle), wdata) _hid.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)
raise exceptions.NoReceiver(reason=reason) raise exceptions.NoReceiver(reason=reason)
@ -270,7 +272,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('unexpected message size: report_id %02X message %s' % (report_id, _strhex(data))) logger.warning("unexpected message size: report_id %02X message %s" % (report_id, _strhex(data)))
return False return False
@ -288,7 +290,7 @@ def _read(handle, timeout):
timeout = int(timeout * 1000) timeout = int(timeout * 1000)
data = _hid.read(int(handle), _MAX_READ_SIZE, timeout) data = _hid.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)
raise exceptions.NoReceiver(reason=reason) raise exceptions.NoReceiver(reason=reason)
@ -296,9 +298,10 @@ def _read(handle, timeout):
report_id = ord(data[:1]) report_id = ord(data[:1])
devnumber = ord(data[1:2]) devnumber = ord(data[1:2])
if logger.isEnabledFor(logging.DEBUG if logger.isEnabledFor(logging.DEBUG) and (
) and (report_id != DJ_MESSAGE_ID or ord(data[2:3]) > 0x10): # ignore DJ input messages report_id != DJ_MESSAGE_ID or ord(data[2:3]) > 0x10
logger.debug('(%s) => r[%02X %02X %s %s]', handle, report_id, devnumber, _strhex(data[2:4]), _strhex(data[4:])) ): # ignore DJ input messages
logger.debug("(%s) => r[%02X %02X %s %s]", handle, report_id, devnumber, _strhex(data[2:4]), _strhex(data[4:]))
return report_id, devnumber, data[2:] return report_id, devnumber, data[2:]
@ -319,7 +322,7 @@ def _skip_incoming(handle, ihandle, notifications_hook):
# 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 = _hid.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)
raise exceptions.NoReceiver(reason=reason) raise exceptions.NoReceiver(reason=reason)
@ -355,20 +358,27 @@ def make_notification(report_id, devnumber, data):
if ( if (
# standard HID++ 1.0 notification, SubId may be 0x40 - 0x7F # standard HID++ 1.0 notification, SubId may be 0x40 - 0x7F
(sub_id >= 0x40) or # noqa: E131 (sub_id >= 0x40) # noqa: E131
or
# custom HID++1.0 battery events, where SubId is 0x07/0x0D # custom HID++1.0 battery events, where SubId is 0x07/0x0D
(sub_id in (0x07, 0x0D) and len(data) == 5 and data[4:5] == b'\x00') or (sub_id in (0x07, 0x0D) and len(data) == 5 and data[4:5] == b"\x00")
or
# custom HID++1.0 illumination event, where SubId is 0x17 # custom HID++1.0 illumination event, where SubId is 0x17
(sub_id == 0x17 and len(data) == 5) or (sub_id == 0x17 and len(data) == 5)
or
# HID++ 2.0 feature notifications have the SoftwareID 0 # HID++ 2.0 feature notifications have the SoftwareID 0
(address & 0x0F == 0x00) (address & 0x0F == 0x00)
): # noqa: E129 ): # noqa: E129
return _HIDPP_Notification(report_id, devnumber, sub_id, address, data[2:]) return _HIDPP_Notification(report_id, devnumber, sub_id, address, data[2:])
_HIDPP_Notification = namedtuple('_HIDPP_Notification', ('report_id', 'devnumber', 'sub_id', 'address', 'data')) _HIDPP_Notification = namedtuple("_HIDPP_Notification", ("report_id", "devnumber", "sub_id", "address", "data"))
_HIDPP_Notification.__str__ = lambda self: 'Notification(%02x,%d,%02X,%02X,%s)' % ( _HIDPP_Notification.__str__ = lambda self: "Notification(%02x,%d,%02X,%02X,%s)" % (
self.report_id, self.devnumber, self.sub_id, self.address, _strhex(self.data) self.report_id,
self.devnumber,
self.sub_id,
self.address,
_strhex(self.data),
) )
del namedtuple del namedtuple
@ -384,7 +394,7 @@ def handle_lock(handle):
with request_lock: with request_lock:
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]
@ -395,7 +405,7 @@ def acquire_timeout(lock, handle, timeout):
result = lock.acquire(timeout=timeout) result = lock.acquire(timeout=timeout)
try: try:
if not result: if not result:
logger.error('lock on handle %d not acquired, probably due to timeout', int(handle)) logger.error("lock on handle %d not acquired, probably due to timeout", int(handle))
yield result yield result
finally: finally:
if result: if result:
@ -415,7 +425,7 @@ def request(handle, devnumber, request_id, *params, no_reply=False, return_error
# import inspect as _inspect # import inspect as _inspect
# print ('\n '.join(str(s) for s in _inspect.stack())) # print ('\n '.join(str(s) for s in _inspect.stack()))
with acquire_timeout(handle_lock(handle), handle, 10.): 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:
# For HID++ 2.0 feature requests, randomize the SoftwareId to make it # For HID++ 2.0 feature requests, randomize the SoftwareId to make it
@ -431,19 +441,19 @@ 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(_pack("B", p) if isinstance(p, int) else p for p in params)
else: else:
params = b'' params = b""
# if logger.isEnabledFor(logging.DEBUG): # if logger.isEnabledFor(logging.DEBUG):
# logger.debug("(%s) device %d request_id {%04X} params [%s]", handle, devnumber, request_id, _strhex(params)) # logger.debug("(%s) device %d request_id {%04X} params [%s]", handle, devnumber, request_id, _strhex(params))
request_data = _pack('!H', request_id) + params request_data = _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)
try: try:
_skip_incoming(handle, ihandle, notifications_hook) _skip_incoming(handle, ihandle, notifications_hook)
except exceptions.NoReceiver: except exceptions.NoReceiver:
logger.warning('device or receiver disconnected') logger.warning("device or receiver disconnected")
return None return None
write(ihandle, devnumber, request_data, long_message) write(ihandle, devnumber, request_data, long_message)
@ -459,23 +469,34 @@ def request(handle, devnumber, request_id, *params, no_reply=False, return_error
if reply: if reply:
report_id, reply_devnumber, reply_data = reply report_id, reply_devnumber, reply_data = reply
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 report_id == HIDPP_SHORT_MESSAGE_ID and reply_data[:1] == b'\x8F' and reply_data[1:3] == request_data[:2 if (
]: report_id == HIDPP_SHORT_MESSAGE_ID
and reply_data[:1] == b"\x8F"
and reply_data[1:3] == request_data[:2]
):
error = ord(reply_data[3:4]) error = ord(reply_data[3:4])
if logger.isEnabledFor(logging.DEBUG): if logger.isEnabledFor(logging.DEBUG):
logger.debug( logger.debug(
'(%s) device 0x%02X error on request {%04X}: %d = %s', handle, devnumber, request_id, error, "(%s) device 0x%02X error on request {%04X}: %d = %s",
_hidpp10_constants.ERROR[error] handle,
devnumber,
request_id,
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(
'(%s) device %d error on feature request {%04X}: %d = %s', handle, devnumber, request_id, error, "(%s) device %d error on feature request {%04X}: %d = %s",
_hidpp20_constants.ERROR[error] handle,
devnumber,
request_id,
error,
_hidpp20_constants.ERROR[error],
) )
raise _hidpp20.FeatureCallError(number=devnumber, request=request_id, error=error, params=params) raise _hidpp20.FeatureCallError(number=devnumber, request=request_id, error=error, params=params)
@ -511,8 +532,12 @@ def request(handle, devnumber, request_id, *params, no_reply=False, return_error
# logger.debug("(%s) still waiting for reply, delta %f", handle, delta) # 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]', delta, timeout, devnumber, request_id, "timeout (%0.2f/%0.2f) on device %d request {%04X} params [%s]",
_strhex(params) delta,
timeout,
devnumber,
request_id,
_strhex(params),
) )
# raise DeviceUnreachable(number=devnumber, request=request_id) # raise DeviceUnreachable(number=devnumber, request=request_id)
@ -522,20 +547,20 @@ def ping(handle, devnumber, long_message=False):
:returns: The HID protocol supported by the device, as a floating point number, if the device is active. :returns: The HID protocol supported by the device, as a floating point number, if the device is active.
""" """
if logger.isEnabledFor(logging.DEBUG): if logger.isEnabledFor(logging.DEBUG):
logger.debug('(%s) pinging device %d', handle, devnumber) logger.debug("(%s) pinging device %d", handle, devnumber)
with acquire_timeout(handle_lock(handle), handle, 10.): with acquire_timeout(handle_lock(handle), handle, 10.0):
notifications_hook = getattr(handle, 'notifications_hook', None) notifications_hook = getattr(handle, "notifications_hook", None)
try: try:
_skip_incoming(handle, int(handle), notifications_hook) _skip_incoming(handle, int(handle), notifications_hook)
except exceptions.NoReceiver: except exceptions.NoReceiver:
logger.warning('device or receiver disconnected') logger.warning("device or receiver disconnected")
return return
# 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 | _random_bits(3)
request_data = _pack('!HBBB', request_id, 0, 0, _random_bits(8)) request_data = _pack("!HBBB", request_id, 0, 0, _random_bits(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 = _timestamp() # we consider timeout from this point
@ -544,21 +569,26 @@ def ping(handle, devnumber, long_message=False):
reply = _read(handle, _PING_TIMEOUT) reply = _read(handle, _PING_TIMEOUT)
if reply: if reply:
report_id, reply_devnumber, reply_data = reply report_id, reply_devnumber, reply_data = reply
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 reply_data[:2] == request_data[:2] and reply_data[4:5] == request_data[-1:]: if reply_data[:2] == request_data[:2] and reply_data[4:5] == request_data[-1:]:
# HID++ 2.0+ device, currently connected # HID++ 2.0+ device, currently connected
return ord(reply_data[2:3]) + ord(reply_data[3:4]) / 10.0 return ord(reply_data[2:3]) + ord(reply_data[3:4]) / 10.0
if report_id == HIDPP_SHORT_MESSAGE_ID and reply_data[:1] == b'\x8F' and \ if (
reply_data[1:3] == request_data[:2]: # error response report_id == HIDPP_SHORT_MESSAGE_ID
and reply_data[:1] == b"\x8F"
and reply_data[1:3] == request_data[:2]
): # error response
error = ord(reply_data[3:4]) 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 error == _hidpp10_constants.ERROR.resource_error or \ if (
error == _hidpp10_constants.ERROR.connection_request_failed: error == _hidpp10_constants.ERROR.resource_error
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)
if notifications_hook: if notifications_hook:
@ -570,4 +600,4 @@ def ping(handle, devnumber, long_message=False):
delta = _timestamp() - request_started delta = _timestamp() - 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

@ -35,105 +35,105 @@ from .i18n import _
# re_pairs determines whether a receiver pairs by replacing existing pairings, default to False # re_pairs determines whether a receiver pairs by replacing existing pairings, default to False
## currently only one receiver is so marked - should there be more? ## currently only one receiver is so marked - should there be more?
_DRIVER = ('hid-generic', 'generic-usb', 'logitech-djreceiver') _DRIVER = ("hid-generic", "generic-usb", "logitech-djreceiver")
_bolt_receiver = lambda product_id: { _bolt_receiver = lambda product_id: {
'vendor_id': 0x046d, "vendor_id": 0x046D,
'product_id': product_id, "product_id": product_id,
'usb_interface': 2, "usb_interface": 2,
'hid_driver': _DRIVER, # noqa: F821 "hid_driver": _DRIVER, # noqa: F821
'name': _('Bolt Receiver'), "name": _("Bolt Receiver"),
'receiver_kind': 'bolt', "receiver_kind": "bolt",
'max_devices': 6, "max_devices": 6,
'may_unpair': True "may_unpair": True,
} }
_unifying_receiver = lambda product_id: { _unifying_receiver = lambda product_id: {
'vendor_id': 0x046d, "vendor_id": 0x046D,
'product_id': product_id, "product_id": product_id,
'usb_interface': 2, "usb_interface": 2,
'hid_driver': _DRIVER, # noqa: F821 "hid_driver": _DRIVER, # noqa: F821
'name': _('Unifying Receiver'), "name": _("Unifying Receiver"),
'receiver_kind': 'unifying', "receiver_kind": "unifying",
'may_unpair': True "may_unpair": True,
} }
_nano_receiver = lambda product_id: { _nano_receiver = lambda product_id: {
'vendor_id': 0x046d, "vendor_id": 0x046D,
'product_id': product_id, "product_id": product_id,
'usb_interface': 1, "usb_interface": 1,
'hid_driver': _DRIVER, # noqa: F821 "hid_driver": _DRIVER, # noqa: F821
'name': _('Nano Receiver'), "name": _("Nano Receiver"),
'receiver_kind': 'nano', "receiver_kind": "nano",
'may_unpair': False, "may_unpair": False,
're_pairs': True "re_pairs": True,
} }
_nano_receiver_no_unpair = lambda product_id: { _nano_receiver_no_unpair = lambda product_id: {
'vendor_id': 0x046d, "vendor_id": 0x046D,
'product_id': product_id, "product_id": product_id,
'usb_interface': 1, "usb_interface": 1,
'hid_driver': _DRIVER, # noqa: F821 "hid_driver": _DRIVER, # noqa: F821
'name': _('Nano Receiver'), "name": _("Nano Receiver"),
'receiver_kind': 'nano', "receiver_kind": "nano",
'may_unpair': False, "may_unpair": False,
'unpair': False, "unpair": False,
're_pairs': True "re_pairs": True,
} }
_nano_receiver_max2 = lambda product_id: { _nano_receiver_max2 = lambda product_id: {
'vendor_id': 0x046d, "vendor_id": 0x046D,
'product_id': product_id, "product_id": product_id,
'usb_interface': 1, "usb_interface": 1,
'hid_driver': _DRIVER, # noqa: F821 "hid_driver": _DRIVER, # noqa: F821
'name': _('Nano Receiver'), "name": _("Nano Receiver"),
'receiver_kind': 'nano', "receiver_kind": "nano",
'max_devices': 2, "max_devices": 2,
'may_unpair': False, "may_unpair": False,
're_pairs': True "re_pairs": True,
} }
_nano_receiver_maxn = lambda product_id, max: { _nano_receiver_maxn = lambda product_id, max: {
'vendor_id': 0x046d, "vendor_id": 0x046D,
'product_id': product_id, "product_id": product_id,
'usb_interface': 1, "usb_interface": 1,
'hid_driver': _DRIVER, # noqa: F821 "hid_driver": _DRIVER, # noqa: F821
'name': _('Nano Receiver'), "name": _("Nano Receiver"),
'receiver_kind': 'nano', "receiver_kind": "nano",
'max_devices': max, "max_devices": max,
'may_unpair': False, "may_unpair": False,
're_pairs': True "re_pairs": True,
} }
_lenovo_receiver = lambda product_id: { _lenovo_receiver = lambda product_id: {
'vendor_id': 0x17ef, "vendor_id": 0x17EF,
'product_id': product_id, "product_id": product_id,
'usb_interface': 1, "usb_interface": 1,
'hid_driver': _DRIVER, # noqa: F821 "hid_driver": _DRIVER, # noqa: F821
'name': _('Nano Receiver'), "name": _("Nano Receiver"),
'receiver_kind': 'nano', "receiver_kind": "nano",
'may_unpair': False "may_unpair": False,
} }
_lightspeed_receiver = lambda product_id: { _lightspeed_receiver = lambda product_id: {
'vendor_id': 0x046d, "vendor_id": 0x046D,
'product_id': product_id, "product_id": product_id,
'usb_interface': 2, "usb_interface": 2,
'hid_driver': _DRIVER, # noqa: F821 "hid_driver": _DRIVER, # noqa: F821
'name': _('Lightspeed Receiver'), "name": _("Lightspeed Receiver"),
'may_unpair': False "may_unpair": False,
} }
_ex100_receiver = lambda product_id: { _ex100_receiver = lambda product_id: {
'vendor_id': 0x046d, "vendor_id": 0x046D,
'product_id': product_id, "product_id": product_id,
'usb_interface': 1, "usb_interface": 1,
'hid_driver': _DRIVER, # noqa: F821 "hid_driver": _DRIVER, # noqa: F821
'name': _('EX100 Receiver 27 Mhz'), "name": _("EX100 Receiver 27 Mhz"),
'receiver_kind': '27Mhz', "receiver_kind": "27Mhz",
'max_devices': 4, "max_devices": 4,
'may_unpair': False, "may_unpair": False,
're_pairs': True "re_pairs": True,
} }
# Receivers added here should also be listed in # Receivers added here should also be listed in
@ -141,40 +141,40 @@ _ex100_receiver = lambda product_id: {
# Look in https://github.com/torvalds/linux/blob/master/drivers/hid/hid-ids.h # Look in https://github.com/torvalds/linux/blob/master/drivers/hid/hid-ids.h
# Bolt receivers (marked with the yellow lightning bolt logo) # Bolt receivers (marked with the yellow lightning bolt logo)
BOLT_RECEIVER_C548 = _bolt_receiver(0xc548) BOLT_RECEIVER_C548 = _bolt_receiver(0xC548)
# standard Unifying receivers (marked with the orange Unifying logo) # standard Unifying receivers (marked with the orange Unifying logo)
UNIFYING_RECEIVER_C52B = _unifying_receiver(0xc52b) UNIFYING_RECEIVER_C52B = _unifying_receiver(0xC52B)
UNIFYING_RECEIVER_C532 = _unifying_receiver(0xc532) UNIFYING_RECEIVER_C532 = _unifying_receiver(0xC532)
# Nano receivers (usually sold with low-end devices) # Nano receivers (usually sold with low-end devices)
NANO_RECEIVER_ADVANCED = _nano_receiver_no_unpair(0xc52f) NANO_RECEIVER_ADVANCED = _nano_receiver_no_unpair(0xC52F)
NANO_RECEIVER_C518 = _nano_receiver(0xc518) NANO_RECEIVER_C518 = _nano_receiver(0xC518)
NANO_RECEIVER_C51A = _nano_receiver(0xc51a) NANO_RECEIVER_C51A = _nano_receiver(0xC51A)
NANO_RECEIVER_C51B = _nano_receiver(0xc51b) NANO_RECEIVER_C51B = _nano_receiver(0xC51B)
NANO_RECEIVER_C521 = _nano_receiver(0xc521) NANO_RECEIVER_C521 = _nano_receiver(0xC521)
NANO_RECEIVER_C525 = _nano_receiver(0xc525) NANO_RECEIVER_C525 = _nano_receiver(0xC525)
NANO_RECEIVER_C526 = _nano_receiver(0xc526) NANO_RECEIVER_C526 = _nano_receiver(0xC526)
NANO_RECEIVER_C52E = _nano_receiver_no_unpair(0xc52e) NANO_RECEIVER_C52E = _nano_receiver_no_unpair(0xC52E)
NANO_RECEIVER_C531 = _nano_receiver(0xc531) NANO_RECEIVER_C531 = _nano_receiver(0xC531)
NANO_RECEIVER_C534 = _nano_receiver_max2(0xc534) NANO_RECEIVER_C534 = _nano_receiver_max2(0xC534)
NANO_RECEIVER_C535 = _nano_receiver(0xc535) # branded as Dell NANO_RECEIVER_C535 = _nano_receiver(0xC535) # branded as Dell
NANO_RECEIVER_C537 = _nano_receiver(0xc537) NANO_RECEIVER_C537 = _nano_receiver(0xC537)
# NANO_RECEIVER_C542 = _nano_receiver(0xc542) # does not use HID++ # NANO_RECEIVER_C542 = _nano_receiver(0xc542) # does not use HID++
NANO_RECEIVER_6042 = _lenovo_receiver(0x6042) NANO_RECEIVER_6042 = _lenovo_receiver(0x6042)
# Lightspeed receivers (usually sold with gaming devices) # Lightspeed receivers (usually sold with gaming devices)
LIGHTSPEED_RECEIVER_C539 = _lightspeed_receiver(0xc539) LIGHTSPEED_RECEIVER_C539 = _lightspeed_receiver(0xC539)
LIGHTSPEED_RECEIVER_C53A = _lightspeed_receiver(0xc53a) LIGHTSPEED_RECEIVER_C53A = _lightspeed_receiver(0xC53A)
LIGHTSPEED_RECEIVER_C53D = _lightspeed_receiver(0xc53d) LIGHTSPEED_RECEIVER_C53D = _lightspeed_receiver(0xC53D)
LIGHTSPEED_RECEIVER_C53F = _lightspeed_receiver(0xc53f) LIGHTSPEED_RECEIVER_C53F = _lightspeed_receiver(0xC53F)
LIGHTSPEED_RECEIVER_C541 = _lightspeed_receiver(0xc541) LIGHTSPEED_RECEIVER_C541 = _lightspeed_receiver(0xC541)
LIGHTSPEED_RECEIVER_C545 = _lightspeed_receiver(0xc545) LIGHTSPEED_RECEIVER_C545 = _lightspeed_receiver(0xC545)
LIGHTSPEED_RECEIVER_C547 = _lightspeed_receiver(0xc547) LIGHTSPEED_RECEIVER_C547 = _lightspeed_receiver(0xC547)
# EX100 old style receiver pre-unifying protocol # EX100 old style receiver pre-unifying protocol
# EX100_27MHZ_RECEIVER_C50C = _ex100_receiver(0xc50C) # in hid/hid-ids.h # EX100_27MHZ_RECEIVER_C50C = _ex100_receiver(0xc50C) # in hid/hid-ids.h
EX100_27MHZ_RECEIVER_C517 = _ex100_receiver(0xc517) EX100_27MHZ_RECEIVER_C517 = _ex100_receiver(0xC517)
# EX100_27MHZ_RECEIVER_C51B = _ex100_receiver(0xc51B) # in hid/hid-ids.h # EX100_27MHZ_RECEIVER_C51B = _ex100_receiver(0xc51B) # in hid/hid-ids.h
ALL = ( ALL = (

View File

@ -31,28 +31,266 @@ is_string = lambda d: isinstance(d, str)
def crc16(data: bytes): def crc16(data: bytes):
''' """
CRC-16 (CCITT) implemented with a precomputed lookup table CRC-16 (CCITT) implemented with a precomputed lookup table
''' """
table = [ table = [
0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50A5, 0x60C6, 0x70E7, 0x8108, 0x9129, 0xA14A, 0xB16B, 0xC18C, 0xD1AD, 0xE1CE, 0x0000,
0xF1EF, 0x1231, 0x0210, 0x3273, 0x2252, 0x52B5, 0x4294, 0x72F7, 0x62D6, 0x9339, 0x8318, 0xB37B, 0xA35A, 0xD3BD, 0xC39C, 0x1021,
0xF3FF, 0xE3DE, 0x2462, 0x3443, 0x0420, 0x1401, 0x64E6, 0x74C7, 0x44A4, 0x5485, 0xA56A, 0xB54B, 0x8528, 0x9509, 0xE5EE, 0x2042,
0xF5CF, 0xC5AC, 0xD58D, 0x3653, 0x2672, 0x1611, 0x0630, 0x76D7, 0x66F6, 0x5695, 0x46B4, 0xB75B, 0xA77A, 0x9719, 0x8738, 0x3063,
0xF7DF, 0xE7FE, 0xD79D, 0xC7BC, 0x48C4, 0x58E5, 0x6886, 0x78A7, 0x0840, 0x1861, 0x2802, 0x3823, 0xC9CC, 0xD9ED, 0xE98E, 0x4084,
0xF9AF, 0x8948, 0x9969, 0xA90A, 0xB92B, 0x5AF5, 0x4AD4, 0x7AB7, 0x6A96, 0x1A71, 0x0A50, 0x3A33, 0x2A12, 0xDBFD, 0xCBDC, 0x50A5,
0xFBBF, 0xEB9E, 0x9B79, 0x8B58, 0xBB3B, 0xAB1A, 0x6CA6, 0x7C87, 0x4CE4, 0x5CC5, 0x2C22, 0x3C03, 0x0C60, 0x1C41, 0xEDAE, 0x60C6,
0xFD8F, 0xCDEC, 0xDDCD, 0xAD2A, 0xBD0B, 0x8D68, 0x9D49, 0x7E97, 0x6EB6, 0x5ED5, 0x4EF4, 0x3E13, 0x2E32, 0x1E51, 0x0E70, 0x70E7,
0xFF9F, 0xEFBE, 0xDFDD, 0xCFFC, 0xBF1B, 0xAF3A, 0x9F59, 0x8F78, 0x9188, 0x81A9, 0xB1CA, 0xA1EB, 0xD10C, 0xC12D, 0xF14E, 0x8108,
0xE16F, 0x1080, 0x00A1, 0x30C2, 0x20E3, 0x5004, 0x4025, 0x7046, 0x6067, 0x83B9, 0x9398, 0xA3FB, 0xB3DA, 0xC33D, 0xD31C, 0x9129,
0xE37F, 0xF35E, 0x02B1, 0x1290, 0x22F3, 0x32D2, 0x4235, 0x5214, 0x6277, 0x7256, 0xB5EA, 0xA5CB, 0x95A8, 0x8589, 0xF56E, 0xA14A,
0xE54F, 0xD52C, 0xC50D, 0x34E2, 0x24C3, 0x14A0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, 0xA7DB, 0xB7FA, 0x8799, 0x97B8, 0xB16B,
0xE75F, 0xF77E, 0xC71D, 0xD73C, 0x26D3, 0x36F2, 0x0691, 0x16B0, 0x6657, 0x7676, 0x4615, 0x5634, 0xD94C, 0xC96D, 0xF90E, 0xC18C,
0xE92F, 0x99C8, 0x89E9, 0xB98A, 0xA9AB, 0x5844, 0x4865, 0x7806, 0x6827, 0x18C0, 0x08E1, 0x3882, 0x28A3, 0xCB7D, 0xDB5C, 0xD1AD,
0xEB3F, 0xFB1E, 0x8BF9, 0x9BD8, 0xABBB, 0xBB9A, 0x4A75, 0x5A54, 0x6A37, 0x7A16, 0x0AF1, 0x1AD0, 0x2AB3, 0x3A92, 0xFD2E, 0xE1CE,
0xED0F, 0xDD6C, 0xCD4D, 0xBDAA, 0xAD8B, 0x9DE8, 0x8DC9, 0x7C26, 0x6C07, 0x5C64, 0x4C45, 0x3CA2, 0x2C83, 0x1CE0, 0x0CC1, 0xF1EF,
0xEF1F, 0xFF3E, 0xCF5D, 0xDF7C, 0xAF9B, 0xBFBA, 0x8FD9, 0x9FF8, 0x6E17, 0x7E36, 0x4E55, 0x5E74, 0x2E93, 0x3EB2, 0x0ED1, 0x1231,
0x1EF0 0x0210,
0x3273,
0x2252,
0x52B5,
0x4294,
0x72F7,
0x62D6,
0x9339,
0x8318,
0xB37B,
0xA35A,
0xD3BD,
0xC39C,
0xF3FF,
0xE3DE,
0x2462,
0x3443,
0x0420,
0x1401,
0x64E6,
0x74C7,
0x44A4,
0x5485,
0xA56A,
0xB54B,
0x8528,
0x9509,
0xE5EE,
0xF5CF,
0xC5AC,
0xD58D,
0x3653,
0x2672,
0x1611,
0x0630,
0x76D7,
0x66F6,
0x5695,
0x46B4,
0xB75B,
0xA77A,
0x9719,
0x8738,
0xF7DF,
0xE7FE,
0xD79D,
0xC7BC,
0x48C4,
0x58E5,
0x6886,
0x78A7,
0x0840,
0x1861,
0x2802,
0x3823,
0xC9CC,
0xD9ED,
0xE98E,
0xF9AF,
0x8948,
0x9969,
0xA90A,
0xB92B,
0x5AF5,
0x4AD4,
0x7AB7,
0x6A96,
0x1A71,
0x0A50,
0x3A33,
0x2A12,
0xDBFD,
0xCBDC,
0xFBBF,
0xEB9E,
0x9B79,
0x8B58,
0xBB3B,
0xAB1A,
0x6CA6,
0x7C87,
0x4CE4,
0x5CC5,
0x2C22,
0x3C03,
0x0C60,
0x1C41,
0xEDAE,
0xFD8F,
0xCDEC,
0xDDCD,
0xAD2A,
0xBD0B,
0x8D68,
0x9D49,
0x7E97,
0x6EB6,
0x5ED5,
0x4EF4,
0x3E13,
0x2E32,
0x1E51,
0x0E70,
0xFF9F,
0xEFBE,
0xDFDD,
0xCFFC,
0xBF1B,
0xAF3A,
0x9F59,
0x8F78,
0x9188,
0x81A9,
0xB1CA,
0xA1EB,
0xD10C,
0xC12D,
0xF14E,
0xE16F,
0x1080,
0x00A1,
0x30C2,
0x20E3,
0x5004,
0x4025,
0x7046,
0x6067,
0x83B9,
0x9398,
0xA3FB,
0xB3DA,
0xC33D,
0xD31C,
0xE37F,
0xF35E,
0x02B1,
0x1290,
0x22F3,
0x32D2,
0x4235,
0x5214,
0x6277,
0x7256,
0xB5EA,
0xA5CB,
0x95A8,
0x8589,
0xF56E,
0xE54F,
0xD52C,
0xC50D,
0x34E2,
0x24C3,
0x14A0,
0x0481,
0x7466,
0x6447,
0x5424,
0x4405,
0xA7DB,
0xB7FA,
0x8799,
0x97B8,
0xE75F,
0xF77E,
0xC71D,
0xD73C,
0x26D3,
0x36F2,
0x0691,
0x16B0,
0x6657,
0x7676,
0x4615,
0x5634,
0xD94C,
0xC96D,
0xF90E,
0xE92F,
0x99C8,
0x89E9,
0xB98A,
0xA9AB,
0x5844,
0x4865,
0x7806,
0x6827,
0x18C0,
0x08E1,
0x3882,
0x28A3,
0xCB7D,
0xDB5C,
0xEB3F,
0xFB1E,
0x8BF9,
0x9BD8,
0xABBB,
0xBB9A,
0x4A75,
0x5A54,
0x6A37,
0x7A16,
0x0AF1,
0x1AD0,
0x2AB3,
0x3A92,
0xFD2E,
0xED0F,
0xDD6C,
0xCD4D,
0xBDAA,
0xAD8B,
0x9DE8,
0x8DC9,
0x7C26,
0x6C07,
0x5C64,
0x4C45,
0x3CA2,
0x2C83,
0x1CE0,
0x0CC1,
0xEF1F,
0xFF3E,
0xCF5D,
0xDF7C,
0xAF9B,
0xBFBA,
0x8FD9,
0x9FF8,
0x6E17,
0x7E36,
0x4E55,
0x5E74,
0x2E93,
0x3EB2,
0x0ED1,
0x1EF0,
] ]
crc = 0xFFFF crc = 0xFFFF
@ -88,7 +326,7 @@ class NamedInt(int):
return self.name.lower() == other.lower() return self.name.lower() == other.lower()
# this should catch comparisons with bytes in Py3 # this should catch comparisons with bytes in Py3
if other is not None: if other is not None:
raise TypeError('Unsupported type ' + str(type(other))) raise TypeError("Unsupported type " + str(type(other)))
def __ne__(self, other): def __ne__(self, other):
return not self.__eq__(other) return not self.__eq__(other)
@ -100,19 +338,19 @@ class NamedInt(int):
return self.name return self.name
def __repr__(self): def __repr__(self):
return 'NamedInt(%d, %r)' % (int(self), self.name) return "NamedInt(%d, %r)" % (int(self), self.name)
@classmethod @classmethod
def from_yaml(cls, loader, node): def from_yaml(cls, loader, node):
args = loader.construct_mapping(node) args = loader.construct_mapping(node)
return cls(value=args['value'], name=args['name']) return cls(value=args["value"], name=args["name"])
@classmethod @classmethod
def to_yaml(cls, dumper, data): def to_yaml(cls, dumper, data):
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)
@ -129,14 +367,14 @@ class NamedInts:
if the value already exists in the set (int or string), ValueError will be if the value already exists in the set (int or string), ValueError will be
raised. raised.
""" """
__slots__ = ('__dict__', '_values', '_indexed', '_fallback', '_is_sorted')
__slots__ = ("__dict__", "_values", "_indexed", "_fallback", "_is_sorted")
def __init__(self, dict=None, **kwargs): def __init__(self, dict=None, **kwargs):
def _readable_name(n): def _readable_name(n):
if not is_string(n): if not is_string(n):
raise TypeError('expected string, got ' + str(type(n))) raise TypeError("expected string, got " + str(type(n)))
return n.replace('__', '/').replace('_', ' ') return n.replace("__", "/").replace("_", " ")
# print (repr(kwargs)) # print (repr(kwargs))
elements = dict if dict else kwargs elements = dict if dict else kwargs
@ -163,13 +401,13 @@ class NamedInts:
def flag_names(self, value): def flag_names(self, value):
unknown_bits = value unknown_bits = value
for k in self._indexed: for k in self._indexed:
assert bin(k).count('1') == 1 assert bin(k).count("1") == 1
if k & value == k: if k & value == k:
unknown_bits &= ~k unknown_bits &= ~k
yield str(self._indexed[k]) yield str(self._indexed[k])
if unknown_bits: if unknown_bits:
yield 'unknown:%06X' % unknown_bits yield "unknown:%06X" % unknown_bits
def _sort_values(self): def _sort_values(self):
self._values = sorted(self._values) self._values = sorted(self._values)
@ -190,7 +428,7 @@ class NamedInts:
elif is_string(index): elif is_string(index):
if index in self.__dict__: if index in self.__dict__:
return self.__dict__[index] return self.__dict__[index]
return (next((x for x in self._values if str(x) == index), None)) return next((x for x in self._values if str(x) == index), None)
elif isinstance(index, slice): elif isinstance(index, slice):
values = self._values if self._is_sorted else sorted(self._values) values = self._values if self._is_sorted else sorted(self._values)
@ -224,17 +462,17 @@ class NamedInts:
def __setitem__(self, index, name): def __setitem__(self, index, name):
assert isinstance(index, int), type(index) assert isinstance(index, int), type(index)
if isinstance(name, NamedInt): if isinstance(name, NamedInt):
assert int(index) == int(name), repr(index) + ' ' + repr(name) assert int(index) == int(name), repr(index) + " " + repr(name)
value = name value = name
elif is_string(name): elif is_string(name):
value = NamedInt(index, name) value = NamedInt(index, name)
else: else:
raise TypeError('name must be a string') raise TypeError("name must be a string")
if str(value) in self.__dict__: if str(value) in self.__dict__:
raise ValueError('%s (%d) already known' % (value, int(value))) raise ValueError("%s (%d) already known" % (value, int(value)))
if int(value) in self._indexed: if int(value) in self._indexed:
raise ValueError('%d (%s) already known' % (int(value), value)) raise ValueError("%d (%s) already known" % (int(value), value))
self._values.append(value) self._values.append(value)
self._is_sorted = False self._is_sorted = False
@ -257,14 +495,13 @@ class NamedInts:
return len(self._values) return len(self._values)
def __repr__(self): def __repr__(self):
return 'NamedInts(%s)' % ', '.join(repr(v) for v in self._values) return "NamedInts(%s)" % ", ".join(repr(v) for v in self._values)
def __or__(self, other): def __or__(self, other):
return NamedInts(**self.__dict__, **other.__dict__) return NamedInts(**self.__dict__, **other.__dict__)
class UnsortedNamedInts(NamedInts): class UnsortedNamedInts(NamedInts):
def _sort_values(self): def _sort_values(self):
pass pass
@ -276,18 +513,18 @@ 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 _hexlify(x).decode("ascii").upper()
def bytes2int(x, signed=False): def bytes2int(x, signed=False):
return int.from_bytes(x, signed=signed, byteorder='big') return int.from_bytes(x, signed=signed, byteorder="big")
def int2bytes(x, count=None, signed=False): def int2bytes(x, count=None, signed=False):
if count: if count:
return x.to_bytes(length=count, byteorder='big', signed=signed) return x.to_bytes(length=count, byteorder="big", signed=signed)
else: else:
return x.to_bytes(length=8, byteorder='big', signed=signed).lstrip(b'\x00') return x.to_bytes(length=8, byteorder="big", signed=signed).lstrip(b"\x00")
class KwException(Exception): class KwException(Exception):
@ -306,7 +543,7 @@ class KwException(Exception):
"""Firmware information.""" """Firmware information."""
FirmwareInfo = namedtuple('FirmwareInfo', ['kind', 'name', 'version', 'extras']) FirmwareInfo = namedtuple("FirmwareInfo", ["kind", "name", "version", "extras"])
BATTERY_APPROX = NamedInts(empty=0, critical=5, low=20, good=50, full=90) BATTERY_APPROX = NamedInts(empty=0, critical=5, low=20, good=50, full=90)

View File

@ -33,7 +33,6 @@ from .hidpp10_constants import REGISTERS as _R
class _DeviceDescriptor: class _DeviceDescriptor:
def __init__( def __init__(
self, self,
name=None, name=None,
@ -44,7 +43,7 @@ class _DeviceDescriptor:
registers=None, registers=None,
usbid=None, usbid=None,
interface=None, interface=None,
btid=None btid=None,
): ):
self.name = name self.name = name
self.kind = kind self.kind = kind
@ -76,21 +75,30 @@ def _D(
): ):
if kind is None: if kind is None:
kind = ( kind = (
_DK.mouse if 'Mouse' in name else _DK.keyboard if 'Keyboard' in name else _DK.numpad _DK.mouse
if 'Number Pad' in name else _DK.touchpad if 'Touchpad' in name else _DK.trackball if 'Trackball' in name else None if "Mouse" in name
else _DK.keyboard
if "Keyboard" in name
else _DK.numpad
if "Number Pad" in name
else _DK.touchpad
if "Touchpad" in name
else _DK.trackball
if "Trackball" in name
else None
) )
assert kind is not None, 'descriptor for %s does not have kind set' % name assert kind is not None, "descriptor for %s does not have kind set" % name
if protocol is not None: if protocol is not None:
if wpid: if wpid:
for w in wpid if isinstance(wpid, tuple) else (wpid, ): for w in wpid if isinstance(wpid, tuple) else (wpid,):
if protocol > 1.0: if protocol > 1.0:
assert w[0:1] == '4', '%s has protocol %0.1f, wpid %s' % (name, protocol, w) assert w[0:1] == "4", "%s has protocol %0.1f, wpid %s" % (name, protocol, w)
else: else:
if w[0:1] == '1': if w[0:1] == "1":
assert kind == _DK.mouse, '%s has protocol %0.1f, wpid %s' % (name, protocol, w) assert kind == _DK.mouse, "%s has protocol %0.1f, wpid %s" % (name, protocol, w)
elif w[0:1] == '2': elif w[0:1] == "2":
assert kind in (_DK.keyboard, _DK.numpad), '%s has protocol %0.1f, wpid %s' % (name, protocol, w) assert kind in (_DK.keyboard, _DK.numpad), "%s has protocol %0.1f, wpid %s" % (name, protocol, w)
device_descriptor = _DeviceDescriptor( device_descriptor = _DeviceDescriptor(
name=name, name=name,
@ -101,23 +109,23 @@ def _D(
registers=registers, registers=registers,
usbid=usbid, usbid=usbid,
interface=interface, interface=interface,
btid=btid btid=btid,
) )
if usbid: if usbid:
found = get_usbid(usbid) found = get_usbid(usbid)
assert found is None, 'duplicate usbid in device descriptors: %s' % (found, ) assert found is None, "duplicate usbid in device descriptors: %s" % (found,)
if btid: if btid:
found = get_btid(btid) found = get_btid(btid)
assert found is None, 'duplicate btid in device descriptors: %s' % (found, ) assert found is None, "duplicate btid in device descriptors: %s" % (found,)
assert codename not in DEVICES, 'duplicate codename in device descriptors: %s' % (DEVICES[codename], ) assert codename not in DEVICES, "duplicate codename in device descriptors: %s" % (DEVICES[codename],)
if codename: if codename:
DEVICES[codename] = device_descriptor DEVICES[codename] = device_descriptor
if wpid: if wpid:
for w in wpid if isinstance(wpid, tuple) else (wpid, ): for w in wpid if isinstance(wpid, tuple) else (wpid,):
assert w not in DEVICES_WPID, 'duplicate wpid in device descriptors: %s' % (DEVICES_WPID[w], ) assert w not in DEVICES_WPID, "duplicate wpid in device descriptors: %s" % (DEVICES_WPID[w],)
DEVICES_WPID[w] = device_descriptor DEVICES_WPID[w] = device_descriptor

View File

@ -64,7 +64,7 @@ class Device:
long=None, long=None,
product_id=None, product_id=None,
bus_id=None, bus_id=None,
setting_callback=None setting_callback=None,
): ):
assert receiver or handle assert receiver or handle
Device.instances.append(self) Device.instances.append(self)
@ -125,15 +125,15 @@ class Device:
# assert link_notification.address == (0x04 if unifying else 0x03) # assert link_notification.address == (0x04 if unifying else 0x03)
kind = ord(link_notification.data[0:1]) & 0x0F kind = ord(link_notification.data[0:1]) & 0x0F
# get 27Mhz wpid and set kind based on index # get 27Mhz wpid and set kind based on index
if receiver.receiver_kind == '27Mhz': # 27 Mhz receiver if receiver.receiver_kind == "27Mhz": # 27 Mhz receiver
self.wpid = '00' + _strhex(link_notification.data[2:3]) self.wpid = "00" + _strhex(link_notification.data[2:3])
kind = receiver.get_kind_from_index(number) kind = receiver.get_kind_from_index(number)
self._kind = _hidpp10_constants.DEVICE_KIND[kind] self._kind = _hidpp10_constants.DEVICE_KIND[kind]
elif receiver.receiver_kind == '27Mhz': # 27 Mhz receiver doesn't have pairing registers elif receiver.receiver_kind == "27Mhz": # 27 Mhz receiver doesn't have pairing registers
self.wpid = _hid.find_paired_node_wpid(receiver.path, number) self.wpid = _hid.find_paired_node_wpid(receiver.path, number)
if not self.wpid: if not self.wpid:
logger.error('Unable to get wpid from udev for device %d of %s', number, receiver) logger.error("Unable to get wpid from udev for device %d of %s", number, receiver)
raise exceptions.NoSuchDevice(number=number, receiver=receiver, error='Not present 27Mhz device') raise exceptions.NoSuchDevice(number=number, receiver=receiver, error="Not present 27Mhz device")
kind = receiver.get_kind_from_index(number) kind = receiver.get_kind_from_index(number)
self._kind = _hidpp10_constants.DEVICE_KIND[kind] self._kind = _hidpp10_constants.DEVICE_KIND[kind]
else: # get information from pairing registers else: # get information from pairing registers
@ -141,10 +141,10 @@ class Device:
self.update_pairing_information() self.update_pairing_information()
self.update_extended_pairing_information() self.update_extended_pairing_information()
if not self.wpid and not self._serial: # if neither then the device almost certainly wasn't found if not self.wpid and not self._serial: # if neither then the device almost certainly wasn't found
raise exceptions.NoSuchDevice(number=number, receiver=receiver, error='no wpid or serial') raise exceptions.NoSuchDevice(number=number, receiver=receiver, error="no wpid or serial")
# the wpid is set to None on this object when the device is unpaired # the wpid is set to None on this object when the device is unpaired
assert self.wpid is not None, 'failed to read wpid: device %d of %s' % (number, receiver) assert self.wpid is not None, "failed to read wpid: device %d of %s" % (number, receiver)
self.descriptor = _descriptors.get_wpid(self.wpid) self.descriptor = _descriptors.get_wpid(self.wpid)
if self.descriptor is None: if self.descriptor is None:
@ -155,8 +155,9 @@ class Device:
self.descriptor = _descriptors.get_codename(self._codename) self.descriptor = _descriptors.get_codename(self._codename)
else: else:
self.online = None # a direct connected device might not be online (as reported by user) self.online = None # a direct connected device might not be online (as reported by user)
self.descriptor = _descriptors.get_btid(self.product_id) if self.bluetooth else \ self.descriptor = (
_descriptors.get_usbid(self.product_id) _descriptors.get_btid(self.product_id) if self.bluetooth else _descriptors.get_usbid(self.product_id)
)
if self.number is None: # for direct-connected devices get 'number' from descriptor protocol else use 0xFF if self.number is None: # for direct-connected devices get 'number' from descriptor protocol else use 0xFF
self.number = 0x00 if self.descriptor and self.descriptor.protocol and self.descriptor.protocol < 2.0 else 0xFF self.number = 0x00 if self.descriptor and self.descriptor.protocol and self.descriptor.protocol < 2.0 else 0xFF
@ -176,7 +177,7 @@ class Device:
self.features = _hidpp20.FeaturesArray(self) self.features = _hidpp20.FeaturesArray(self)
def find(self, serial): # find a device by serial number or unit ID def find(self, serial): # find a device by serial number or unit ID
assert serial, 'need serial number or unit ID to find a device' assert serial, "need serial number or unit ID to find a device"
result = None result = None
for device in Device.instances: for device in Device.instances:
if device.online and (device.unitId == serial or device.serial == serial): if device.online and (device.unitId == serial or device.serial == serial):
@ -197,14 +198,14 @@ class Device:
if self.online and self.protocol >= 2.0: if self.online and self.protocol >= 2.0:
self._codename = _hidpp20.get_friendly_name(self) self._codename = _hidpp20.get_friendly_name(self)
if not self._codename: if not self._codename:
self._codename = self.name.split(' ', 1)[0] if self.name else None self._codename = self.name.split(" ", 1)[0] if self.name else None
if not self._codename and self.receiver: if not self._codename and self.receiver:
codename = self.receiver.device_codename(self.number) codename = self.receiver.device_codename(self.number)
if codename: if codename:
self._codename = codename self._codename = codename
elif self.protocol < 2.0: elif self.protocol < 2.0:
self._codename = '? (%s)' % (self.wpid or self.product_id) self._codename = "? (%s)" % (self.wpid or self.product_id)
return self._codename or '?? (%s)' % (self.wpid or self.product_id) return self._codename or "?? (%s)" % (self.wpid or self.product_id)
@property @property
def name(self): def name(self):
@ -216,14 +217,14 @@ class Device:
pass pass
if self.online and self.protocol >= 2.0: if self.online and self.protocol >= 2.0:
self._name = _hidpp20.get_name(self) self._name = _hidpp20.get_name(self)
return self._name or self._codename or ('Unknown device %s' % (self.wpid or self.product_id)) return self._name or self._codename or ("Unknown device %s" % (self.wpid or self.product_id))
def get_ids(self): def get_ids(self):
ids = _hidpp20.get_ids(self) ids = _hidpp20.get_ids(self)
if ids: if ids:
self._unitId, self._modelId, self._tid_map = ids self._unitId, self._modelId, self._tid_map = ids
if logger.isEnabledFor(logging.INFO) and self._serial and self._serial != self._unitId: if logger.isEnabledFor(logging.INFO) and self._serial and self._serial != self._unitId:
logger.info('%s: unitId %s does not match serial %s', self, self._unitId, self._serial) logger.info("%s: unitId %s does not match serial %s", self, self._unitId, self._serial)
@property @property
def unitId(self): def unitId(self):
@ -251,7 +252,7 @@ class Device:
if not self._kind: if not self._kind:
self._kind = kind self._kind = kind
if not self._polling_rate: if not self._polling_rate:
self._polling_rate = str(polling_rate) + 'ms' self._polling_rate = str(polling_rate) + "ms"
def update_extended_pairing_information(self): def update_extended_pairing_information(self):
if self.receiver: if self.receiver:
@ -268,7 +269,7 @@ class Device:
if not self._kind and self.protocol >= 2.0: if not self._kind and self.protocol >= 2.0:
kind = _hidpp20.get_kind(self) kind = _hidpp20.get_kind(self)
self._kind = KIND_MAP[kind] if kind else None self._kind = KIND_MAP[kind] if kind else None
return self._kind or '?' return self._kind or "?"
@property @property
def firmware(self): def firmware(self):
@ -283,13 +284,13 @@ class Device:
def serial(self): def serial(self):
if not self._serial: if not self._serial:
self.update_extended_pairing_information() self.update_extended_pairing_information()
return self._serial or '' return self._serial or ""
@property @property
def id(self): def id(self):
if not self.serial: if not self.serial:
if self.persister and self.persister.get('_serial', None): if self.persister and self.persister.get("_serial", None):
self._serial = self.persister.get('_serial', None) self._serial = self.persister.get("_serial", None)
return self.unitId or self.serial return self.unitId or self.serial
@property @property
@ -399,17 +400,17 @@ class Device:
if self.protocol < 2.0: if self.protocol < 2.0:
return _hidpp10.get_battery(self) return _hidpp10.get_battery(self)
else: else:
battery_feature = self.persister.get('_battery', None) if self.persister else None battery_feature = self.persister.get("_battery", None) if self.persister else None
if battery_feature != 0: if battery_feature != 0:
result = _hidpp20.get_battery(self, battery_feature) result = _hidpp20.get_battery(self, battery_feature)
try: try:
feature, level, next, status, voltage = result feature, level, next, status, voltage = result
if self.persister and battery_feature is None: if self.persister and battery_feature is None:
self.persister['_battery'] = feature self.persister["_battery"] = feature
return level, next, status, voltage return level, next, status, voltage
except Exception: except Exception:
if self.persister and battery_feature is None: if self.persister and battery_feature is None:
self.persister['_battery'] = result self.persister["_battery"] = result
def enable_connection_notifications(self, enable=True): def enable_connection_notifications(self, enable=True):
"""Enable or disable device (dis)connection notifications on this """Enable or disable device (dis)connection notifications on this
@ -428,12 +429,12 @@ class Device:
set_flag_bits = 0 set_flag_bits = 0
ok = _hidpp10.set_notification_flags(self, set_flag_bits) ok = _hidpp10.set_notification_flags(self, set_flag_bits)
if not ok: if not ok:
logger.warning('%s: failed to %s device notifications', self, 'enable' if enable else 'disable') logger.warning("%s: failed to %s device notifications", self, "enable" if enable else "disable")
flag_bits = _hidpp10.get_notification_flags(self) flag_bits = _hidpp10.get_notification_flags(self)
flag_names = None if flag_bits is None else tuple(_hidpp10_constants.NOTIFICATION_FLAG.flag_names(flag_bits)) flag_names = None if flag_bits is None else tuple(_hidpp10_constants.NOTIFICATION_FLAG.flag_names(flag_bits))
if logger.isEnabledFor(logging.INFO): if logger.isEnabledFor(logging.INFO):
logger.info('%s: device notifications %s %s', self, 'enabled' if enable else 'disabled', flag_names) logger.info("%s: device notifications %s %s", self, "enabled" if enable else "disabled", flag_names)
return flag_bits if ok else None return flag_bits if ok else None
def add_notification_handler(self, id: str, fn): def add_notification_handler(self, id: str, fn):
@ -454,7 +455,7 @@ class Device:
"""Unregisters the notification handler under name `id`.""" """Unregisters the notification handler under name `id`."""
if id not in self._notification_handlers and logger.isEnabledFor(logging.INFO): if id not in self._notification_handlers and logger.isEnabledFor(logging.INFO):
logger.info(f'Tried to remove nonexistent notification handler {id} from device {self}.') logger.info(f"Tried to remove nonexistent notification handler {id} from device {self}.")
else: else:
del self._notification_handlers[id] del self._notification_handlers[id]
@ -477,7 +478,7 @@ class Device:
*params, *params,
no_reply=no_reply, no_reply=no_reply,
long_message=long, long_message=long,
protocol=self.protocol protocol=self.protocol,
) )
def feature_request(self, feature, function=0x00, *params, no_reply=False): def feature_request(self, feature, function=0x00, *params, no_reply=False):
@ -517,10 +518,10 @@ class Device:
def __str__(self): def __str__(self):
try: try:
name = self.name or self.codename or '?' name = self.name or self.codename or "?"
except exceptions.NoSuchDevice: except exceptions.NoSuchDevice:
name = 'name not available' name = "name not available"
return '<Device(%d,%s,%s,%s)>' % (self.number, self.wpid or self.product_id, name, self.serial) return "<Device(%d,%s,%s,%s)>" % (self.number, self.wpid or self.product_id, name, self.serial)
__repr__ = __str__ __repr__ = __str__
@ -544,20 +545,20 @@ class Device:
long=device_info.hidpp_long, long=device_info.hidpp_long,
product_id=device_info.product_id, product_id=device_info.product_id,
bus_id=device_info.bus_id, bus_id=device_info.bus_id,
setting_callback=setting_callback 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)
def close(self): def close(self):
handle, self.handle = self.handle, None handle, self.handle = self.handle, None
if self in Device.instances: if self in Device.instances:
Device.instances.remove(self) Device.instances.remove(self)
return (handle and _base.close(handle)) return handle and _base.close(handle)
def __del__(self): def __del__(self):
self.close() self.close()

File diff suppressed because it is too large Load Diff

View File

@ -10,24 +10,29 @@ class NoReceiver(_KwException):
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
unloaded.""" unloaded."""
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

@ -34,14 +34,14 @@ logger = logging.getLogger(__name__)
def read_register(device, register_number, *params): def read_register(device, register_number, *params):
assert device is not None, 'tried to read register %02X from invalid device %s' % (register_number, device) assert device is not None, "tried to read register %02X from invalid device %s" % (register_number, device)
# support long registers by adding a 2 in front of the register number # support long registers by adding a 2 in front of the register number
request_id = 0x8100 | (int(register_number) & 0x2FF) request_id = 0x8100 | (int(register_number) & 0x2FF)
return device.request(request_id, *params) return device.request(request_id, *params)
def write_register(device, register_number, *value): def write_register(device, register_number, *value):
assert device is not None, 'tried to write register %02X to invalid device %s' % (register_number, device) assert device is not None, "tried to write register %02X to invalid device %s" % (register_number, device)
# support long registers by adding a 2 in front of the register number # support long registers by adding a 2 in front of the register number
request_id = 0x8000 | (int(register_number) & 0x2FF) request_id = 0x8000 | (int(register_number) & 0x2FF)
return device.request(request_id, *value) return device.request(request_id, *value)
@ -83,18 +83,27 @@ def parse_battery_status(register, reply):
charge = ord(reply[:1]) charge = ord(reply[:1])
status_byte = ord(reply[2:3]) & 0xF0 status_byte = ord(reply[2:3]) & 0xF0
status_text = ( status_text = (
BATTERY_STATUS.discharging if status_byte == 0x30 else BATTERY_STATUS.discharging
BATTERY_STATUS.recharging if status_byte == 0x50 else BATTERY_STATUS.full if status_byte == 0x90 else None if status_byte == 0x30
else BATTERY_STATUS.recharging
if status_byte == 0x50
else BATTERY_STATUS.full
if status_byte == 0x90
else None
) )
return charge, None, status_text, None return charge, None, status_text, None
if register == REGISTERS.battery_status: if register == REGISTERS.battery_status:
status_byte = ord(reply[:1]) status_byte = ord(reply[:1])
charge = ( charge = (
_BATTERY_APPROX.full if status_byte == 7 # full _BATTERY_APPROX.full
else _BATTERY_APPROX.good if status_byte == 5 # good if status_byte == 7 # full
else _BATTERY_APPROX.low if status_byte == 3 # low else _BATTERY_APPROX.good
else _BATTERY_APPROX.critical if status_byte == 1 # critical if status_byte == 5 # good
else _BATTERY_APPROX.low
if status_byte == 3 # low
else _BATTERY_APPROX.critical
if status_byte == 1 # critical
# pure 'charging' notifications may come without a status # pure 'charging' notifications may come without a status
else _BATTERY_APPROX.empty else _BATTERY_APPROX.empty
) )
@ -107,7 +116,7 @@ def parse_battery_status(register, reply):
elif charging_byte & 0x22 == 0x22: elif charging_byte & 0x22 == 0x22:
status_text = BATTERY_STATUS.full status_text = BATTERY_STATUS.full
else: else:
logger.warning('could not parse 0x07 battery status: %02X (level %02X)', charging_byte, status_byte) logger.warning("could not parse 0x07 battery status: %02X (level %02X)", charging_byte, status_byte)
status_text = None status_text = None
if charging_byte & 0x03 and status_byte == 0: if charging_byte & 0x03 and status_byte == 0:
@ -129,25 +138,25 @@ def get_firmware(device):
return return
fw_version = _strhex(reply[1:3]) fw_version = _strhex(reply[1:3])
fw_version = '%s.%s' % (fw_version[0:2], fw_version[2:4]) fw_version = "%s.%s" % (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" + _strhex(reply[1:3])
fw = _FirmwareInfo(FIRMWARE_KIND.Firmware, '', fw_version, None) fw = _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 = _strhex(reply[1:3])
bl_version = '%s.%s' % (bl_version[0:2], bl_version[2:4]) bl_version = "%s.%s" % (bl_version[0:2], bl_version[2:4])
bl = _FirmwareInfo(FIRMWARE_KIND.Bootloader, '', bl_version, None) bl = _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 = _strhex(reply[1:3])
o_version = '%s.%s' % (o_version[0:2], o_version[2:4]) o_version = "%s.%s" % (o_version[0:2], o_version[2:4])
o = _FirmwareInfo(FIRMWARE_KIND.Other, '', o_version, None) o = _FirmwareInfo(FIRMWARE_KIND.Other, "", o_version, None)
firmware[2] = o firmware[2] = o
if any(firmware): if any(firmware):
@ -182,8 +191,8 @@ def set_3leds(device, battery_level=None, charging=None, warning=None):
v1, v2 = 0x20, 0x22 v1, v2 = 0x20, 0x22
if warning: if warning:
# set the blinking flag for the leds already set # set the blinking flag for the leds already set
v1 |= (v1 >> 1) v1 |= v1 >> 1
v2 |= (v2 >> 1) v2 |= v2 >> 1
elif charging: elif charging:
# blink all green # blink all green
v1, v2 = 0x30, 0x33 v1, v2 = 0x30, 0x33

View File

@ -16,7 +16,7 @@ DEVICE_KIND = NamedInts(
touchpad=0x09, touchpad=0x09,
headset=0x0D, # not from Logitech documentation headset=0x0D, # not from Logitech documentation
remote_control=0x0E, # for compatibility with HID++ 2.0 remote_control=0x0E, # for compatibility with HID++ 2.0
receiver=0x0F # for compatibility with HID++ 2.0 receiver=0x0F, # for compatibility with HID++ 2.0
) )
POWER_SWITCH_LOCATION = NamedInts( POWER_SWITCH_LOCATION = NamedInts(
@ -30,7 +30,7 @@ POWER_SWITCH_LOCATION = NamedInts(
top_edge=0x09, top_edge=0x09,
right_edge=0x0A, right_edge=0x0A,
left_edge=0x0B, left_edge=0x0B,
bottom_edge=0x0C bottom_edge=0x0C,
) )
# Some flags are used both by devices and receivers. The Logitech documentation # Some flags are used both by devices and receivers. The Logitech documentation
@ -79,7 +79,7 @@ ERROR = NamedInts(
resource_error=0x09, resource_error=0x09,
request_unavailable=0x0A, request_unavailable=0x0A,
unsupported_parameter_value=0x0B, unsupported_parameter_value=0x0B,
wrong_pin_code=0x0C wrong_pin_code=0x0C,
) )
PAIRING_ERRORS = NamedInts(device_timeout=0x01, device_not_supported=0x02, too_many_devices=0x03, sequence_timeout=0x06) PAIRING_ERRORS = NamedInts(device_timeout=0x01, device_not_supported=0x02, too_many_devices=0x03, sequence_timeout=0x06)
@ -96,7 +96,6 @@ REGISTERS = NamedInts(
bolt_device_discovery=0xC0, bolt_device_discovery=0xC0,
bolt_pairing=0x2C1, bolt_pairing=0x2C1,
bolt_uniqueId=0x02FB, bolt_uniqueId=0x02FB,
# only apply to devices # only apply to devices
mouse_button_flags=0x01, mouse_button_flags=0x01,
keyboard_hand_detection=0x01, keyboard_hand_detection=0x01,
@ -106,11 +105,9 @@ REGISTERS = NamedInts(
keyboard_illumination=0x17, keyboard_illumination=0x17,
three_leds=0x51, three_leds=0x51,
mouse_dpi=0x63, mouse_dpi=0x63,
# apply to both # apply to both
notifications=0x00, notifications=0x00,
firmware=0xF1, firmware=0xF1,
# notifications # notifications
passkey_request_notification=0x4D, passkey_request_notification=0x4D,
passkey_pressed_notification=0x4E, passkey_pressed_notification=0x4E,

File diff suppressed because it is too large Load Diff

View File

@ -128,7 +128,7 @@ FEATURE = NamedInts(
# Fake features for Solaar internal use # Fake features for Solaar internal use
MOUSE_GESTURE=0xFE00, MOUSE_GESTURE=0xFE00,
) )
FEATURE._fallback = lambda x: 'unknown:%04X' % x FEATURE._fallback = lambda x: "unknown:%04X" % x
FEATURE_FLAG = NamedInts(internal=0x20, hidden=0x40, obsolete=0x80) FEATURE_FLAG = NamedInts(internal=0x20, hidden=0x40, obsolete=0x80)
@ -150,7 +150,7 @@ BATTERY_STATUS = NamedInts(
full=0x03, full=0x03,
slow_recharge=0x04, slow_recharge=0x04,
invalid_battery=0x05, invalid_battery=0x05,
thermal_error=0x06 thermal_error=0x06,
) )
ONBOARD_MODES = NamedInts(MODE_NO_CHANGE=0x00, MODE_ONBOARD=0x01, MODE_HOST=0x02) ONBOARD_MODES = NamedInts(MODE_NO_CHANGE=0x00, MODE_ONBOARD=0x01, MODE_HOST=0x02)
@ -170,7 +170,7 @@ ERROR = NamedInts(
invalid_feature_index=0x06, invalid_feature_index=0x06,
invalid_function=0x07, invalid_function=0x07,
busy=0x08, busy=0x08,
unsupported=0x09 unsupported=0x09,
) )
# Gesture Ids for feature GESTURE_2 # Gesture Ids for feature GESTURE_2
@ -236,7 +236,7 @@ GESTURE = NamedInts(
Finger10=99, Finger10=99,
DeviceSpecificRawData=100, DeviceSpecificRawData=100,
) )
GESTURE._fallback = lambda x: 'unknown:%04X' % x GESTURE._fallback = lambda x: "unknown:%04X" % x
# Param Ids for feature GESTURE_2 # Param Ids for feature GESTURE_2
PARAM = NamedInts( PARAM = NamedInts(
@ -245,4 +245,4 @@ PARAM = NamedInts(
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
ScaleFactor=4, # 2-byte integer, with 256 as normal scale ScaleFactor=4, # 2-byte integer, with 256 as normal scale
) )
PARAM._fallback = lambda x: 'unknown:%04X' % x PARAM._fallback = lambda x: "unknown:%04X" % x

View File

@ -27,61 +27,56 @@ ngettext = _gettext.ngettext
_DUMMY = ( _DUMMY = (
# approximative battery levels # approximative battery levels
_('empty'), _("empty"),
_('critical'), _("critical"),
_('low'), _("low"),
_('average'), _("average"),
_('good'), _("good"),
_('full'), _("full"),
# battery charging statuses # battery charging statuses
_('discharging'), _("discharging"),
_('recharging'), _("recharging"),
_('charging'), _("charging"),
_('not charging'), _("not charging"),
_('almost full'), _("almost full"),
_('charged'), _("charged"),
_('slow recharge'), _("slow recharge"),
_('invalid battery'), _("invalid battery"),
_('thermal error'), _("thermal error"),
_('error'), _("error"),
_('standard'), _("standard"),
_('fast'), _("fast"),
_('slow'), _("slow"),
# pairing errors # pairing errors
_('device timeout'), _("device timeout"),
_('device not supported'), _("device not supported"),
_('too many devices'), _("too many devices"),
_('sequence timeout'), _("sequence timeout"),
# firmware kinds # firmware kinds
_('Firmware'), _("Firmware"),
_('Bootloader'), _("Bootloader"),
_('Hardware'), _("Hardware"),
_('Other'), _("Other"),
# common button and task names (from special_keys.py) # common button and task names (from special_keys.py)
_('Left Button'), _("Left Button"),
_('Right Button'), _("Right Button"),
_('Middle Button'), _("Middle Button"),
_('Back Button'), _("Back Button"),
_('Forward Button'), _("Forward Button"),
_('Mouse Gesture Button'), _("Mouse Gesture Button"),
_('Smart Shift'), _("Smart Shift"),
_('DPI Switch'), _("DPI Switch"),
_('Left Tilt'), _("Left Tilt"),
_('Right Tilt'), _("Right Tilt"),
_('Left Click'), _("Left Click"),
_('Right Click'), _("Right Click"),
_('Mouse Middle Button'), _("Mouse Middle Button"),
_('Mouse Back Button'), _("Mouse Back Button"),
_('Mouse Forward Button'), _("Mouse Forward Button"),
_('Gesture Button Navigation'), _("Gesture Button Navigation"),
_('Mouse Scroll Left Button'), _("Mouse Scroll Left Button"),
_('Mouse Scroll Right Button'), _("Mouse Scroll Right Button"),
# key/button statuses # key/button statuses
_('pressed'), _("pressed"),
_('released'), _("released"),
) )

View File

@ -43,7 +43,7 @@ class _ThreadedHandle:
Closing a ThreadedHandle will close all handles. Closing a ThreadedHandle will close all handles.
""" """
__slots__ = ('path', '_local', '_handles', '_listener') __slots__ = ("path", "_local", "_handles", "_listener")
def __init__(self, listener, path, handle): def __init__(self, listener, path, handle):
assert listener is not None assert listener is not None
@ -61,7 +61,7 @@ class _ThreadedHandle:
def _open(self): def _open(self):
handle = _base.open_path(self.path) handle = _base.open_path(self.path)
if handle is None: if handle is None:
logger.error('%r failed to open new handle', self) logger.error("%r failed to open new handle", self)
else: else:
# if logger.isEnabledFor(logging.DEBUG): # if logger.isEnabledFor(logging.DEBUG):
# logger.debug("%r opened new handle %d", self, handle) # logger.debug("%r opened new handle %d", self, handle)
@ -74,7 +74,7 @@ class _ThreadedHandle:
self._local = None self._local = None
handles, self._handles = self._handles, [] handles, self._handles = self._handles, []
if logger.isEnabledFor(logging.DEBUG): if logger.isEnabledFor(logging.DEBUG):
logger.debug('%r closing %s', self, handles) logger.debug("%r closing %s", self, handles)
for h in handles: for h in handles:
_base.close(h) _base.close(h)
@ -105,7 +105,7 @@ class _ThreadedHandle:
return str(int(self)) return str(int(self))
def __repr__(self): def __repr__(self):
return '<_ThreadedHandle(%s)>' % self.path return "<_ThreadedHandle(%s)>" % self.path
def __bool__(self): def __bool__(self):
return bool(self._local) return bool(self._local)
@ -123,7 +123,7 @@ class _ThreadedHandle:
# a while for it to acknowledge it. # a while for it to acknowledge it.
# Forcibly closing the file handle on another thread does _not_ interrupt the # Forcibly closing the file handle on another thread does _not_ interrupt the
# read on Linux systems. # read on Linux systems.
_EVENT_READ_TIMEOUT = 1. # in seconds _EVENT_READ_TIMEOUT = 1.0 # in seconds
# After this many reads that did not produce a packet, call the tick() method. # After this many reads that did not produce a packet, call the tick() method.
# This only happens if tick_period is enabled (>0) for the Listener instance. # This only happens if tick_period is enabled (>0) for the Listener instance.
@ -138,10 +138,10 @@ class EventsListener(_threading.Thread):
def __init__(self, receiver, notifications_callback): def __init__(self, receiver, notifications_callback):
try: try:
path_name = receiver.path.split('/')[2] path_name = receiver.path.split("/")[2]
except IndexError: except IndexError:
path_name = receiver.path path_name = receiver.path
super().__init__(name=self.__class__.__name__ + ':' + path_name) super().__init__(name=self.__class__.__name__ + ":" + path_name)
self.daemon = True self.daemon = True
self._active = False self._active = False
self.receiver = receiver self.receiver = receiver
@ -153,19 +153,19 @@ class EventsListener(_threading.Thread):
# replace the handle with a threaded one # replace the handle with a threaded one
self.receiver.handle = _ThreadedHandle(self, self.receiver.path, self.receiver.handle) self.receiver.handle = _ThreadedHandle(self, self.receiver.path, self.receiver.handle)
if logger.isEnabledFor(logging.INFO): if logger.isEnabledFor(logging.INFO):
logger.info('started with %s (%d)', self.receiver, int(self.receiver.handle)) logger.info("started with %s (%d)", self.receiver, int(self.receiver.handle))
self.has_started() self.has_started()
if self.receiver.isDevice: # ping (wired or BT) devices to see if they are really online if self.receiver.isDevice: # ping (wired or BT) devices to see if they are really online
if self.receiver.ping(): if self.receiver.ping():
self.receiver.status.changed(True, reason='initialization') self.receiver.status.changed(True, reason="initialization")
while self._active: while self._active:
if self._queued_notifications.empty(): if self._queued_notifications.empty():
try: try:
n = _base.read(self.receiver.handle, _EVENT_READ_TIMEOUT) n = _base.read(self.receiver.handle, _EVENT_READ_TIMEOUT)
except exceptions.NoReceiver: except exceptions.NoReceiver:
logger.warning('%s disconnected', self.receiver.name) logger.warning("%s disconnected", self.receiver.name)
self.receiver.close() self.receiver.close()
break break
if n: if n:
@ -176,7 +176,7 @@ class EventsListener(_threading.Thread):
try: try:
self._notifications_callback(n) self._notifications_callback(n)
except Exception: except Exception:
logger.exception('processing %s', n) logger.exception("processing %s", n)
del self._queued_notifications del self._queued_notifications
self.has_stopped() self.has_stopped()

View File

@ -49,7 +49,7 @@ def process(device, notification):
assert device assert device
assert notification assert notification
assert hasattr(device, 'status') assert hasattr(device, "status")
status = device.status status = device.status
assert status is not None assert status is not None
@ -70,9 +70,9 @@ def _process_receiver_notification(receiver, status, n):
if n.sub_id == 0x4A: # pairing lock notification if n.sub_id == 0x4A: # pairing lock notification
status.lock_open = bool(n.address & 0x01) status.lock_open = bool(n.address & 0x01)
reason = (_('pairing lock is open') if status.lock_open else _('pairing lock is closed')) reason = _("pairing lock is open") if status.lock_open else _("pairing lock is closed")
if logger.isEnabledFor(logging.INFO): if logger.isEnabledFor(logging.INFO):
logger.info('%s: %s', receiver, reason) logger.info("%s: %s", receiver, reason)
status[_K.ERROR] = None status[_K.ERROR] = None
if status.lock_open: if status.lock_open:
status.new_device = None status.new_device = None
@ -80,16 +80,16 @@ def _process_receiver_notification(receiver, status, n):
if pair_error: if pair_error:
status[_K.ERROR] = error_string = _hidpp10.PAIRING_ERRORS[pair_error] status[_K.ERROR] = error_string = _hidpp10.PAIRING_ERRORS[pair_error]
status.new_device = None status.new_device = None
logger.warning('pairing error %d: %s', pair_error, error_string) logger.warning("pairing error %d: %s", pair_error, error_string)
status.changed(reason=reason) status.changed(reason=reason)
return True return True
elif n.sub_id == _R.discovery_status_notification: # Bolt pairing elif n.sub_id == _R.discovery_status_notification: # Bolt pairing
with notification_lock: with notification_lock:
status.discovering = n.address == 0x00 status.discovering = n.address == 0x00
reason = (_('discovery lock is open') if status.discovering else _('discovery lock is closed')) reason = _("discovery lock is open") if status.discovering else _("discovery lock is closed")
if logger.isEnabledFor(logging.INFO): if logger.isEnabledFor(logging.INFO):
logger.info('%s: %s', receiver, reason) logger.info("%s: %s", receiver, reason)
status[_K.ERROR] = None status[_K.ERROR] = None
if status.discovering: if status.discovering:
status.counter = status.device_address = status.device_authentication = status.device_name = None status.counter = status.device_address = status.device_authentication = status.device_name = None
@ -97,7 +97,7 @@ def _process_receiver_notification(receiver, status, n):
discover_error = ord(n.data[:1]) discover_error = ord(n.data[:1])
if discover_error: if discover_error:
status[_K.ERROR] = discover_string = _hidpp10.BOLT_PAIRING_ERRORS[discover_error] status[_K.ERROR] = discover_string = _hidpp10.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)
status.changed(reason=reason) status.changed(reason=reason)
return True return True
@ -114,16 +114,16 @@ def _process_receiver_notification(receiver, status, n):
status.device_address = n.data[6:12] status.device_address = n.data[6:12]
status.device_authentication = n.data[14] status.device_authentication = n.data[14]
elif n.data[1] == 1: elif n.data[1] == 1:
status.device_name = n.data[3:3 + n.data[2]].decode('utf-8') status.device_name = n.data[3 : 3 + n.data[2]].decode("utf-8")
return True return True
elif n.sub_id == _R.pairing_status_notification: # Bolt pairing elif n.sub_id == _R.pairing_status_notification: # Bolt pairing
with notification_lock: with notification_lock:
status.device_passkey = None status.device_passkey = None
status.lock_open = n.address == 0x00 status.lock_open = n.address == 0x00
reason = (_('pairing lock is open') if status.lock_open else _('pairing lock is closed')) reason = _("pairing lock is open") if status.lock_open else _("pairing lock is closed")
if logger.isEnabledFor(logging.INFO): if logger.isEnabledFor(logging.INFO):
logger.info('%s: %s', receiver, reason) logger.info("%s: %s", receiver, reason)
status[_K.ERROR] = None status[_K.ERROR] = None
if not status.lock_open: if not status.lock_open:
status.counter = status.device_address = status.device_authentication = status.device_name = None status.counter = status.device_address = status.device_authentication = status.device_name = None
@ -135,19 +135,19 @@ def _process_receiver_notification(receiver, status, n):
if pair_error: if pair_error:
status[_K.ERROR] = error_string = _hidpp10.BOLT_PAIRING_ERRORS[pair_error] status[_K.ERROR] = error_string = _hidpp10.BOLT_PAIRING_ERRORS[pair_error]
status.new_device = None status.new_device = None
logger.warning('pairing error %d: %s', pair_error, error_string) logger.warning("pairing error %d: %s", pair_error, error_string)
status.changed(reason=reason) status.changed(reason=reason)
return True return True
elif n.sub_id == _R.passkey_request_notification: # Bolt pairing elif n.sub_id == _R.passkey_request_notification: # Bolt pairing
with notification_lock: with notification_lock:
status.device_passkey = n.data[0:6].decode('utf-8') status.device_passkey = n.data[0:6].decode("utf-8")
return True return True
elif n.sub_id == _R.passkey_pressed_notification: # Bolt pairing elif n.sub_id == _R.passkey_pressed_notification: # Bolt pairing
return True return True
logger.warning('%s: unhandled notification %s', receiver, n) logger.warning("%s: unhandled notification %s", receiver, n)
# #
@ -188,12 +188,12 @@ def _process_device_notification(device, status, n):
# assuming 0x00 to 0x3F are feature (HID++ 2.0) notifications # assuming 0x00 to 0x3F are feature (HID++ 2.0) notifications
if not device.features: if not device.features:
logger.warning('%s: feature notification but features not set up: %02X %s', device, n.sub_id, n) logger.warning("%s: feature notification but features not set up: %02X %s", device, n.sub_id, n)
return False return False
try: try:
feature = device.features.get_feature(n.sub_id) feature = device.features.get_feature(n.sub_id)
except IndexError: except IndexError:
logger.warning('%s: notification from invalid feature index %02X: %s', device, n.sub_id, n) logger.warning("%s: notification from invalid feature index %02X: %s", device, n.sub_id, n)
return False return False
return _process_feature_notification(device, status, n, feature) return _process_feature_notification(device, status, n, feature)
@ -201,37 +201,37 @@ def _process_device_notification(device, status, n):
def _process_dj_notification(device, status, n): def _process_dj_notification(device, status, n):
if logger.isEnabledFor(logging.DEBUG): if logger.isEnabledFor(logging.DEBUG):
logger.debug('%s (%s) DJ %s', device, device.protocol, n) logger.debug("%s (%s) DJ %s", device, device.protocol, n)
if n.sub_id == 0x40: if n.sub_id == 0x40:
# do all DJ paired notifications also show up as HID++ 1.0 notifications? # do all DJ paired notifications also show up as HID++ 1.0 notifications?
if logger.isEnabledFor(logging.INFO): if logger.isEnabledFor(logging.INFO):
logger.info('%s: ignoring DJ unpaired: %s', device, n) logger.info("%s: ignoring DJ unpaired: %s", device, n)
return True return True
if n.sub_id == 0x41: if n.sub_id == 0x41:
# do all DJ paired notifications also show up as HID++ 1.0 notifications? # do all DJ paired notifications also show up as HID++ 1.0 notifications?
if logger.isEnabledFor(logging.INFO): if logger.isEnabledFor(logging.INFO):
logger.info('%s: ignoring DJ paired: %s', device, n) logger.info("%s: ignoring DJ paired: %s", device, n)
return True return True
if n.sub_id == 0x42: if n.sub_id == 0x42:
connected = not n.address & 0x01 connected = not n.address & 0x01
if logger.isEnabledFor(logging.INFO): if logger.isEnabledFor(logging.INFO):
logger.info('%s: DJ connection: %s %s', device, connected, n) logger.info("%s: DJ connection: %s %s", device, connected, n)
status.changed(active=connected, alert=_ALERT.NONE, reason=_('connected') if connected else _('disconnected')) status.changed(active=connected, alert=_ALERT.NONE, reason=_("connected") if connected else _("disconnected"))
return True return True
logger.warning('%s: unrecognized DJ %s', device, n) logger.warning("%s: unrecognized DJ %s", device, n)
def _process_hidpp10_custom_notification(device, status, n): def _process_hidpp10_custom_notification(device, status, n):
if logger.isEnabledFor(logging.DEBUG): if logger.isEnabledFor(logging.DEBUG):
logger.debug('%s (%s) custom notification %s', device, device.protocol, n) logger.debug("%s (%s) custom notification %s", device, device.protocol, n)
if n.sub_id in (_R.battery_status, _R.battery_charge): if n.sub_id in (_R.battery_status, _R.battery_charge):
# message layout: 10 ix <register> <xx> <yy> <zz> <00> # message layout: 10 ix <register> <xx> <yy> <zz> <00>
assert n.data[-1:] == b'\x00' assert n.data[-1:] == b"\x00"
data = chr(n.address).encode() + n.data data = chr(n.address).encode() + n.data
charge, next_charge, status_text, voltage = _hidpp10.parse_battery_status(n.sub_id, data) charge, next_charge, status_text, voltage = _hidpp10.parse_battery_status(n.sub_id, data)
status.set_battery_info(charge, next_charge, status_text, voltage) status.set_battery_info(charge, next_charge, status_text, voltage)
@ -241,10 +241,10 @@ def _process_hidpp10_custom_notification(device, status, n):
# message layout: 10 ix 17("address") <??> <?> <??> <light level 1=off..5=max> # message layout: 10 ix 17("address") <??> <?> <??> <light level 1=off..5=max>
# TODO anything we can do with this? # TODO anything we can do with this?
if logger.isEnabledFor(logging.INFO): if logger.isEnabledFor(logging.INFO):
logger.info('illumination event: %s', n) logger.info("illumination event: %s", n)
return True return True
logger.warning('%s: unrecognized %s', device, n) logger.warning("%s: unrecognized %s", device, n)
def _process_hidpp10_notification(device, status, n): def _process_hidpp10_notification(device, status, n):
@ -257,16 +257,16 @@ def _process_hidpp10_notification(device, status, n):
device.status = None device.status = None
if device.number in device.receiver: if device.number in device.receiver:
del device.receiver[device.number] del device.receiver[device.number]
status.changed(active=False, alert=_ALERT.ALL, reason=_('unpaired')) status.changed(active=False, alert=_ALERT.ALL, reason=_("unpaired"))
else: else:
logger.warning('%s: disconnection with unknown type %02X: %s', device, n.address, n) logger.warning("%s: disconnection with unknown type %02X: %s", device, n.address, n)
return True return True
# device connection (and disconnection) # device connection (and disconnection)
if n.sub_id == 0x41: if n.sub_id == 0x41:
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" + _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
@ -274,14 +274,19 @@ def _process_hidpp10_notification(device, status, n):
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:
logger.warning('%s: connection notification with unknown protocol %02X: %s', device.number, n.address, n) logger.warning("%s: connection notification with unknown protocol %02X: %s", device.number, n.address, n)
return True return True
if wpid != device.wpid: if wpid != device.wpid:
logger.warning('%s wpid mismatch, got %s', device, wpid) logger.warning("%s wpid mismatch, got %s", device, wpid)
if logger.isEnabledFor(logging.DEBUG): if logger.isEnabledFor(logging.DEBUG):
logger.debug( logger.debug(
'%s: protocol %s connection notification: software=%s, encrypted=%s, link=%s, payload=%s', device, n.address, "%s: protocol %s connection notification: software=%s, encrypted=%s, link=%s, payload=%s",
bool(flags & 0x10), link_encrypted, link_established, bool(flags & 0x80) device,
n.address,
bool(flags & 0x10),
link_encrypted,
link_established,
bool(flags & 0x80),
) )
status[_K.LINK_ENCRYPTED] = link_encrypted status[_K.LINK_ENCRYPTED] = link_encrypted
status.changed(active=link_established) status.changed(active=link_established)
@ -298,19 +303,19 @@ def _process_hidpp10_notification(device, status, n):
if n.sub_id == 0x4B: if n.sub_id == 0x4B:
if n.address == 0x01: if n.address == 0x01:
if logger.isEnabledFor(logging.DEBUG): if logger.isEnabledFor(logging.DEBUG):
logger.debug('%s: device powered on', device) logger.debug("%s: device powered on", device)
reason = status.to_string() or _('powered on') reason = status.to_string() or _("powered on")
status.changed(active=True, alert=_ALERT.NOTIFICATION, reason=reason) status.changed(active=True, alert=_ALERT.NOTIFICATION, reason=reason)
else: else:
logger.warning('%s: unknown %s', device, n) logger.warning("%s: unknown %s", device, n)
return True return True
logger.warning('%s: unrecognized %s', device, n) logger.warning("%s: unrecognized %s", device, n)
def _process_feature_notification(device, status, n, feature): def _process_feature_notification(device, status, 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, _strhex(n.data))
if feature == _F.BATTERY_STATUS: if feature == _F.BATTERY_STATUS:
if n.address == 0x00: if n.address == 0x00:
@ -318,23 +323,23 @@ def _process_feature_notification(device, status, n, feature):
status.set_battery_info(discharge_level, discharge_next_level, battery_status, voltage) status.set_battery_info(discharge_level, discharge_next_level, battery_status, voltage)
elif n.address == 0x10: elif n.address == 0x10:
if logger.isEnabledFor(logging.INFO): if logger.isEnabledFor(logging.INFO):
logger.info('%s: spurious BATTERY status %s', device, n) logger.info("%s: spurious BATTERY status %s", device, n)
else: else:
logger.warning('%s: unknown BATTERY %s', device, n) logger.warning("%s: unknown BATTERY %s", device, n)
elif feature == _F.BATTERY_VOLTAGE: elif feature == _F.BATTERY_VOLTAGE:
if n.address == 0x00: if n.address == 0x00:
_ignore, level, nextl, battery_status, voltage = _hidpp20.decipher_battery_voltage(n.data) _ignore, level, nextl, battery_status, voltage = _hidpp20.decipher_battery_voltage(n.data)
status.set_battery_info(level, nextl, battery_status, voltage) status.set_battery_info(level, nextl, battery_status, voltage)
else: else:
logger.warning('%s: unknown VOLTAGE %s', device, n) logger.warning("%s: unknown VOLTAGE %s", device, n)
elif feature == _F.UNIFIED_BATTERY: elif feature == _F.UNIFIED_BATTERY:
if n.address == 0x00: if n.address == 0x00:
_ignore, level, nextl, battery_status, voltage = _hidpp20.decipher_battery_unified(n.data) _ignore, level, nextl, battery_status, voltage = _hidpp20.decipher_battery_unified(n.data)
status.set_battery_info(level, nextl, battery_status, voltage) status.set_battery_info(level, nextl, battery_status, voltage)
else: else:
logger.warning('%s: unknown UNIFIED BATTERY %s', device, n) logger.warning("%s: unknown UNIFIED BATTERY %s", device, n)
elif feature == _F.ADC_MEASUREMENT: elif feature == _F.ADC_MEASUREMENT:
if n.address == 0x00: if n.address == 0x00:
@ -345,11 +350,11 @@ def _process_feature_notification(device, status, n, feature):
else: # this feature is used to signal device becoming inactive else: # this feature is used to signal device becoming inactive
status.changed(active=False) status.changed(active=False)
else: else:
logger.warning('%s: unknown ADC MEASUREMENT %s', device, n) logger.warning("%s: unknown ADC MEASUREMENT %s", device, n)
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 = _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 = _hidpp20_constants.BATTERY_STATUS.discharging status_text = _hidpp20_constants.BATTERY_STATUS.discharging
@ -363,7 +368,7 @@ def _process_feature_notification(device, status, n, feature):
status.set_battery_info(charge, None, status_text, None) status.set_battery_info(charge, None, status_text, None)
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)
status.changed(alert=_ALERT.SHOW_WINDOW) status.changed(alert=_ALERT.SHOW_WINDOW)
# first cancel any reporting # first cancel any reporting
# device.feature_request(_F.SOLAR_DASHBOARD) # device.feature_request(_F.SOLAR_DASHBOARD)
@ -372,93 +377,93 @@ def _process_feature_notification(device, status, n, feature):
reports_period = 2 # seconds reports_period = 2 # seconds
device.feature_request(_F.SOLAR_DASHBOARD, 0x00, reports_count, reports_period) device.feature_request(_F.SOLAR_DASHBOARD, 0x00, reports_count, reports_period)
else: else:
logger.warning('%s: unknown SOLAR CHARGE %s', device, n) logger.warning("%s: unknown SOLAR CHARGE %s", device, n)
else: else:
logger.warning('%s: SOLAR CHARGE not GOOD? %s', device, n) logger.warning("%s: SOLAR CHARGE not GOOD? %s", device, n)
elif feature == _F.WIRELESS_DEVICE_STATUS: elif feature == _F.WIRELESS_DEVICE_STATUS:
if n.address == 0x00: if n.address == 0x00:
if logger.isEnabledFor(logging.DEBUG): if logger.isEnabledFor(logging.DEBUG):
logger.debug('wireless status: %s', n) logger.debug("wireless status: %s", n)
reason = 'powered on' if n.data[2] == 1 else None reason = "powered on" if n.data[2] == 1 else None
if n.data[1] == 1: # device is asking for software reconfiguration so need to change status if n.data[1] == 1: # device is asking for software reconfiguration so need to change status
alert = _ALERT.NONE alert = _ALERT.NONE
status.changed(active=True, alert=alert, reason=reason, push=True) status.changed(active=True, alert=alert, reason=reason, push=True)
else: else:
logger.warning('%s: unknown WIRELESS %s', device, n) logger.warning("%s: unknown WIRELESS %s", device, n)
elif feature == _F.TOUCHMOUSE_RAW_POINTS: elif feature == _F.TOUCHMOUSE_RAW_POINTS:
if n.address == 0x00: if n.address == 0x00:
if logger.isEnabledFor(logging.INFO): if logger.isEnabledFor(logging.INFO):
logger.info('%s: TOUCH MOUSE points %s', device, n) logger.info("%s: TOUCH MOUSE points %s", device, n)
elif n.address == 0x10: elif n.address == 0x10:
touch = ord(n.data[:1]) touch = ord(n.data[:1])
button_down = bool(touch & 0x02) button_down = bool(touch & 0x02)
mouse_lifted = bool(touch & 0x01) mouse_lifted = bool(touch & 0x01)
if logger.isEnabledFor(logging.INFO): if logger.isEnabledFor(logging.INFO):
logger.info('%s: TOUCH MOUSE status: button_down=%s mouse_lifted=%s', device, button_down, mouse_lifted) logger.info("%s: TOUCH MOUSE status: button_down=%s mouse_lifted=%s", device, button_down, mouse_lifted)
else: else:
logger.warning('%s: unknown TOUCH MOUSE %s', device, n) logger.warning("%s: unknown TOUCH MOUSE %s", device, n)
# TODO: what are REPROG_CONTROLS_V{2,3}? # TODO: what are REPROG_CONTROLS_V{2,3}?
elif feature == _F.REPROG_CONTROLS: elif feature == _F.REPROG_CONTROLS:
if n.address == 0x00: if n.address == 0x00:
if logger.isEnabledFor(logging.INFO): if logger.isEnabledFor(logging.INFO):
logger.info('%s: reprogrammable key: %s', device, n) logger.info("%s: reprogrammable key: %s", device, n)
else: else:
logger.warning('%s: unknown REPROG_CONTROLS %s', device, n) logger.warning("%s: unknown REPROG_CONTROLS %s", device, n)
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 = _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, _st.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 = _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 = _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):
logger.debug('%s: received analyticsKeyEvents', device) logger.debug("%s: received analyticsKeyEvents", device)
elif logger.isEnabledFor(logging.INFO): elif logger.isEnabledFor(logging.INFO):
logger.info('%s: unknown REPROG_CONTROLS_V4 %s', device, n) logger.info("%s: unknown REPROG_CONTROLS_V4 %s", device, n)
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 = _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)
elif (n.address == 0x10): elif n.address == 0x10:
ratchet = n.data[0] ratchet = n.data[0]
if logger.isEnabledFor(logging.INFO): if logger.isEnabledFor(logging.INFO):
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, _st.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)
elif feature == _F.ONBOARD_PROFILES: elif feature == _F.ONBOARD_PROFILES:
if (n.address > 0x10): if n.address > 0x10:
if logger.isEnabledFor(logging.INFO): if logger.isEnabledFor(logging.INFO):
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 = _unpack("!H", n.data[:2])[0]
if profile_sector: if profile_sector:
_st.profile_change(device, profile_sector) _st.profile_change(device, profile_sector)
elif (n.address == 0x10): elif n.address == 0x10:
resolution_index = _unpack('!B', n.data[:1])[0] resolution_index = _unpack("!B", n.data[:1])[0]
profile_sector = _unpack('!H', device.feature_request(_F.ONBOARD_PROFILES, 0x40)[:2])[0] profile_sector = _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:

View File

@ -22,9 +22,11 @@ logger = logging.getLogger(__name__)
try: try:
import gi import gi
gi.require_version('Notify', '0.7')
gi.require_version('Gtk', '3.0') gi.require_version("Notify", "0.7")
gi.require_version("Gtk", "3.0")
from gi.repository import GLib, Gtk, Notify # this import is allowed to fail making the entire feature unavailable from gi.repository import GLib, Gtk, Notify # this import is allowed to fail making the entire feature unavailable
available = True available = True
except (ValueError, ImportError): except (ValueError, ImportError):
available = False available = False
@ -39,18 +41,18 @@ if available:
if available: if available:
if not Notify.is_initted(): if not Notify.is_initted():
if logger.isEnabledFor(logging.INFO): if logger.isEnabledFor(logging.INFO):
logger.info('starting desktop notifications') logger.info("starting desktop notifications")
try: try:
return Notify.init('solaar') # replace with better name later return Notify.init("solaar") # replace with better name later
except Exception: except Exception:
logger.exception('initializing desktop notifications') logger.exception("initializing desktop notifications")
available = False available = False
return available and Notify.is_initted() return available and Notify.is_initted()
def uninit(): def uninit():
if available and Notify.is_initted(): if available and Notify.is_initted():
if logger.isEnabledFor(logging.INFO): if logger.isEnabledFor(logging.INFO):
logger.info('stopping desktop notifications') logger.info("stopping desktop notifications")
_notifications.clear() _notifications.clear()
Notify.uninit() Notify.uninit()
@ -64,32 +66,32 @@ if available:
icon_name = device_icon_name(dev.name, dev.kind) if icon is None else icon icon_name = device_icon_name(dev.name, dev.kind) if icon is None else icon
n.update(summary, message, icon_name) n.update(summary, message, icon_name)
n.set_urgency(Notify.Urgency.NORMAL) n.set_urgency(Notify.Urgency.NORMAL)
n.set_hint('desktop-entry', GLib.Variant('s', 'solaar')) # replace with better name late n.set_hint("desktop-entry", GLib.Variant("s", "solaar")) # replace with better name late
try: try:
# if logger.isEnabledFor(logging.DEBUG): # if logger.isEnabledFor(logging.DEBUG):
# logger.debug("showing %s", n) # logger.debug("showing %s", n)
n.show() n.show()
except Exception: except Exception:
logger.exception('showing %s', n) logger.exception("showing %s", n)
_ICON_LISTS = {} _ICON_LISTS = {}
def device_icon_list(name='_', kind=None): def device_icon_list(name="_", kind=None):
icon_list = _ICON_LISTS.get(name) icon_list = _ICON_LISTS.get(name)
if icon_list is None: if icon_list is None:
# names of possible icons, in reverse order of likelihood # names of possible icons, in reverse order of likelihood
# the theme will hopefully pick up the most appropriate # the theme will hopefully pick up the most appropriate
icon_list = ['preferences-desktop-peripherals'] icon_list = ["preferences-desktop-peripherals"]
if kind: if kind:
if str(kind) == 'numpad': if str(kind) == "numpad":
icon_list += ('input-keyboard', 'input-dialpad') icon_list += ("input-keyboard", "input-dialpad")
elif str(kind) == 'touchpad': elif str(kind) == "touchpad":
icon_list += ('input-mouse', 'input-tablet') icon_list += ("input-mouse", "input-tablet")
elif str(kind) == 'trackball': elif str(kind) == "trackball":
icon_list += ('input-mouse', ) icon_list += ("input-mouse",)
elif str(kind) == 'headset': elif str(kind) == "headset":
icon_list += ('audio-headphones', 'audio-headset') icon_list += ("audio-headphones", "audio-headset")
icon_list += ('input-' + str(kind), ) icon_list += ("input-" + str(kind),)
_ICON_LISTS[name] = icon_list _ICON_LISTS[name] = icon_list
return icon_list return icon_list

View File

@ -44,6 +44,7 @@ class Receiver:
The paired devices are available through the sequence interface. The paired devices are available through the sequence interface.
""" """
number = 0xFF number = 0xFF
kind = None kind = None
@ -56,33 +57,36 @@ class Receiver:
self.setting_callback = setting_callback self.setting_callback = setting_callback
product_info = _product_information(self.product_id) product_info = _product_information(self.product_id)
if not product_info: if not product_info:
logger.warning('Unknown receiver type: %s', self.product_id) logger.warning("Unknown receiver type: %s", self.product_id)
product_info = {} product_info = {}
self.receiver_kind = product_info.get('receiver_kind', 'unknown') self.receiver_kind = product_info.get("receiver_kind", "unknown")
# read the serial immediately, so we can find out max_devices # read the serial immediately, so we can find out max_devices
if self.receiver_kind == 'bolt': if self.receiver_kind == "bolt":
serial_reply = self.read_register(_R.bolt_uniqueId) serial_reply = self.read_register(_R.bolt_uniqueId)
self.serial = _strhex(serial_reply) self.serial = _strhex(serial_reply)
self.max_devices = product_info.get('max_devices', 1) self.max_devices = product_info.get("max_devices", 1)
self.may_unpair = product_info.get('may_unpair', False) self.may_unpair = product_info.get("may_unpair", False)
else: else:
serial_reply = self.read_register(_R.receiver_info, _IR.receiver_information) serial_reply = self.read_register(_R.receiver_info, _IR.receiver_information)
if serial_reply: if serial_reply:
self.serial = _strhex(serial_reply[1:5]) self.serial = _strhex(serial_reply[1:5])
self.max_devices = ord(serial_reply[6:7]) self.max_devices = ord(serial_reply[6:7])
if self.max_devices <= 0 or self.max_devices > 6: if self.max_devices <= 0 or self.max_devices > 6:
self.max_devices = product_info.get('max_devices', 1) self.max_devices = product_info.get("max_devices", 1)
self.may_unpair = product_info.get('may_unpair', False) self.may_unpair = product_info.get("may_unpair", False)
else: # handle receivers that don't have a serial number specially (i.e., c534 and Bolt receivers) else: # handle receivers that don't have a serial number specially (i.e., c534 and Bolt receivers)
self.serial = None self.serial = None
self.max_devices = product_info.get('max_devices', 1) self.max_devices = product_info.get("max_devices", 1)
self.may_unpair = product_info.get('may_unpair', False) self.may_unpair = product_info.get("may_unpair", False)
self.name = product_info.get('name', 'Receiver') self.name = product_info.get("name", "Receiver")
self.re_pairs = product_info.get('re_pairs', False) self.re_pairs = product_info.get("re_pairs", False)
self._str = '<%s(%s,%s%s)>' % ( self._str = "<%s(%s,%s%s)>" % (
self.name.replace(' ', ''), self.path, '' if isinstance(self.handle, int) else 'T', self.handle self.name.replace(" ", ""),
self.path,
"" if isinstance(self.handle, int) else "T",
self.handle,
) )
self._firmware = None self._firmware = None
@ -95,7 +99,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()
@ -131,36 +135,36 @@ class Receiver:
set_flag_bits = 0 set_flag_bits = 0
ok = _hidpp10.set_notification_flags(self, set_flag_bits) ok = _hidpp10.set_notification_flags(self, set_flag_bits)
if ok is None: if ok is None:
logger.warning('%s: failed to %s receiver notifications', self, 'enable' if enable else 'disable') logger.warning("%s: failed to %s receiver notifications", self, "enable" if enable else "disable")
return None return None
flag_bits = _hidpp10.get_notification_flags(self) flag_bits = _hidpp10.get_notification_flags(self)
flag_names = None if flag_bits is None else tuple(_hidpp10_constants.NOTIFICATION_FLAG.flag_names(flag_bits)) flag_names = None if flag_bits is None else tuple(_hidpp10_constants.NOTIFICATION_FLAG.flag_names(flag_bits))
if logger.isEnabledFor(logging.INFO): if logger.isEnabledFor(logging.INFO):
logger.info('%s: receiver notifications %s => %s', self, 'enabled' if enable else 'disabled', flag_names) logger.info("%s: receiver notifications %s => %s", self, "enabled" if enable else "disabled", flag_names)
return flag_bits return flag_bits
def device_codename(self, n): def device_codename(self, n):
if self.receiver_kind == 'bolt': if self.receiver_kind == "bolt":
codename = self.read_register(_R.receiver_info, _IR.bolt_device_name + n, 0x01) codename = self.read_register(_R.receiver_info, _IR.bolt_device_name + n, 0x01)
if codename: if codename:
codename = codename[3:3 + min(14, ord(codename[2:3]))] codename = codename[3 : 3 + min(14, ord(codename[2:3]))]
return codename.decode('ascii') return codename.decode("ascii")
return return
codename = self.read_register(_R.receiver_info, _IR.device_name + n - 1) codename = self.read_register(_R.receiver_info, _IR.device_name + n - 1)
if codename: if codename:
codename = codename[2:2 + ord(codename[1:2])] codename = codename[2 : 2 + ord(codename[1:2])]
return codename.decode('ascii') return codename.decode("ascii")
def device_pairing_information(self, n): def device_pairing_information(self, n):
if self.receiver_kind == 'bolt': if self.receiver_kind == "bolt":
pair_info = self.read_register(_R.receiver_info, _IR.bolt_pairing_information + n) pair_info = self.read_register(_R.receiver_info, _IR.bolt_pairing_information + n)
if pair_info: if pair_info:
wpid = _strhex(pair_info[3:4]) + _strhex(pair_info[2:3]) wpid = _strhex(pair_info[3:4]) + _strhex(pair_info[2:3])
kind = _hidpp10_constants.DEVICE_KIND[ord(pair_info[1:2]) & 0x0F] kind = _hidpp10_constants.DEVICE_KIND[ord(pair_info[1:2]) & 0x0F]
return wpid, kind, 0 return wpid, kind, 0
else: else:
raise exceptions.NoSuchDevice(number=n, receiver=self, error='read Bolt wpid') raise exceptions.NoSuchDevice(number=n, receiver=self, error="read Bolt wpid")
wpid = 0 wpid = 0
kind = None kind = None
polling_rate = None polling_rate = None
@ -168,35 +172,35 @@ class Receiver:
if pair_info: # may be either a Unifying receiver, or an Unifying-ready receiver if pair_info: # may be either a Unifying receiver, or an Unifying-ready receiver
wpid = _strhex(pair_info[3:5]) wpid = _strhex(pair_info[3:5])
kind = _hidpp10_constants.DEVICE_KIND[ord(pair_info[7:8]) & 0x0F] kind = _hidpp10_constants.DEVICE_KIND[ord(pair_info[7:8]) & 0x0F]
polling_rate = str(ord(pair_info[2:3])) + 'ms' polling_rate = str(ord(pair_info[2:3])) + "ms"
elif self.receiver_kind == '27Mz': # 27Mhz receiver, fill extracting WPID from udev path elif self.receiver_kind == "27Mz": # 27Mhz receiver, fill extracting WPID from udev path
wpid = _hid.find_paired_node_wpid(self.path, n) wpid = _hid.find_paired_node_wpid(self.path, n)
if not wpid: if not wpid:
logger.error('Unable to get wpid from udev for device %d of %s', n, self) logger.error("Unable to get wpid from udev for device %d of %s", n, self)
raise exceptions.NoSuchDevice(number=n, receiver=self, error='Not present 27Mhz device') raise exceptions.NoSuchDevice(number=n, receiver=self, error="Not present 27Mhz device")
kind = _hidpp10_constants.DEVICE_KIND[self.get_kind_from_index(n)] kind = _hidpp10_constants.DEVICE_KIND[self.get_kind_from_index(n)]
elif not self.receiver_kind == 'unifying': # unifying protocol not supported, may be an old Nano receiver elif not self.receiver_kind == "unifying": # unifying protocol not supported, may be an old Nano receiver
device_info = self.read_register(_R.receiver_info, 0x04) device_info = self.read_register(_R.receiver_info, 0x04)
if device_info: if device_info:
wpid = _strhex(device_info[3:5]) wpid = _strhex(device_info[3:5])
kind = _hidpp10_constants.DEVICE_KIND[0x00] # unknown kind kind = _hidpp10_constants.DEVICE_KIND[0x00] # unknown kind
else: else:
raise exceptions.NoSuchDevice(number=n, receiver=self, error='read pairing information - non-unifying') raise exceptions.NoSuchDevice(number=n, receiver=self, error="read pairing information - non-unifying")
else: else:
raise exceptions.NoSuchDevice(number=n, receiver=self, error='read pairing information') raise exceptions.NoSuchDevice(number=n, receiver=self, error="read pairing information")
return wpid, kind, polling_rate return wpid, kind, polling_rate
def device_extended_pairing_information(self, n): def device_extended_pairing_information(self, n):
serial = None serial = None
power_switch = '(unknown)' power_switch = "(unknown)"
if self.receiver_kind == 'bolt': if self.receiver_kind == "bolt":
pair_info = self.read_register(_R.receiver_info, _IR.bolt_pairing_information + n) pair_info = self.read_register(_R.receiver_info, _IR.bolt_pairing_information + n)
if pair_info: if pair_info:
serial = _strhex(pair_info[4:8]) serial = _strhex(pair_info[4:8])
return serial, power_switch return serial, power_switch
else: else:
return '?', power_switch return "?", power_switch
pair_info = self.read_register(_R.receiver_info, _IR.extended_pairing_information + n - 1) pair_info = self.read_register(_R.receiver_info, _IR.extended_pairing_information + n - 1)
if pair_info: if pair_info:
power_switch = _hidpp10_constants.POWER_SWITCH_LOCATION[ord(pair_info[9:10]) & 0x0F] power_switch = _hidpp10_constants.POWER_SWITCH_LOCATION[ord(pair_info[9:10]) & 0x0F]
@ -220,19 +224,19 @@ class Receiver:
elif index == 4: # numpad elif index == 4: # numpad
kind = 3 kind = 3
else: # unknown device number on 27Mhz receiver else: # unknown device number on 27Mhz receiver
logger.error('failed to calculate device kind for device %d of %s', index, self) logger.error("failed to calculate device kind for device %d of %s", index, self)
raise exceptions.NoSuchDevice(number=index, receiver=self, error='Unknown 27Mhz device number') raise exceptions.NoSuchDevice(number=index, receiver=self, error="Unknown 27Mhz device number")
return kind return kind
def notify_devices(self): def notify_devices(self):
"""Scan all devices.""" """Scan all devices."""
if self.handle: if self.handle:
if not self.write_register(_R.receiver_connection, 0x02): if not self.write_register(_R.receiver_connection, 0x02):
logger.warning('%s: failed to trigger device link notifications', self) logger.warning("%s: failed to trigger device link notifications", self)
def register_new_device(self, number, notification=None): def register_new_device(self, number, notification=None):
if self._devices.get(number) is not None: if self._devices.get(number) is not None:
raise IndexError('%s: device number %d already registered' % (self, number)) raise IndexError("%s: device number %d already registered" % (self, number))
assert notification is None or notification.devnumber == number assert notification is None or notification.devnumber == number
assert notification is None or notification.sub_id == 0x41 assert notification is None or notification.sub_id == 0x41
@ -240,13 +244,13 @@ class Receiver:
try: try:
dev = Device(self, number, notification, setting_callback=self.setting_callback) dev = Device(self, number, notification, setting_callback=self.setting_callback)
if logger.isEnabledFor(logging.INFO): if logger.isEnabledFor(logging.INFO):
logger.info('%s: found new device %d (%s)', self, number, dev.wpid) logger.info("%s: found new device %d (%s)", self, number, dev.wpid)
self._devices[number] = dev self._devices[number] = dev
return dev return dev
except exceptions.NoSuchDevice as e: except exceptions.NoSuchDevice as e:
logger.warning('register new device failed for %s device %d error %s', e.receiver, e.number, e.error) logger.warning("register new device failed for %s device %d error %s", e.receiver, e.number, e.error)
logger.warning('%s: looked for device %d, not found', self, number) logger.warning("%s: looked for device %d, not found", self, number)
self._devices[number] = None self._devices[number] = None
def set_lock(self, lock_closed=True, device=0, timeout=0): def set_lock(self, lock_closed=True, device=0, timeout=0):
@ -255,25 +259,25 @@ class Receiver:
reply = self.write_register(_R.receiver_pairing, action, device, timeout) reply = self.write_register(_R.receiver_pairing, action, device, timeout)
if reply: if reply:
return True return True
logger.warning('%s: failed to %s the receiver lock', self, 'close' if lock_closed else 'open') logger.warning("%s: failed to %s the receiver lock", self, "close" if lock_closed else "open")
def discover(self, cancel=False, timeout=30): # Bolt device discovery def discover(self, cancel=False, timeout=30): # Bolt device discovery
assert self.receiver_kind == 'bolt' assert self.receiver_kind == "bolt"
if self.handle: if self.handle:
action = 0x02 if cancel else 0x01 action = 0x02 if cancel else 0x01
reply = self.write_register(_R.bolt_device_discovery, timeout, action) reply = self.write_register(_R.bolt_device_discovery, timeout, action)
if reply: if reply:
return True return True
logger.warning('%s: failed to %s device discovery', self, 'cancel' if cancel else 'start') logger.warning("%s: failed to %s device discovery", self, "cancel" if cancel else "start")
def pair_device(self, pair=True, slot=0, address=b'\0\0\0\0\0\0', authentication=0x00, entropy=20): # Bolt pairing def pair_device(self, pair=True, slot=0, address=b"\0\0\0\0\0\0", authentication=0x00, entropy=20): # Bolt pairing
assert self.receiver_kind == 'bolt' assert self.receiver_kind == "bolt"
if self.handle: if self.handle:
action = 0x01 if pair is True else 0x03 if pair is False else 0x02 action = 0x01 if pair is True else 0x03 if pair is False else 0x02
reply = self.write_register(_R.bolt_pairing, action, slot, address, authentication, entropy) reply = self.write_register(_R.bolt_pairing, action, slot, address, authentication, entropy)
if reply: if reply:
return True return True
logger.warning('%s: failed to %s device %s', self, 'pair' if pair else 'unpair', address) logger.warning("%s: failed to %s device %s", self, "pair" if pair else "unpair", address)
def count(self): def count(self):
count = self.read_register(_R.receiver_connection) count = self.read_register(_R.receiver_connection)
@ -312,7 +316,7 @@ class Receiver:
return dev return dev
if not isinstance(key, int): if not isinstance(key, int):
raise TypeError('key must be an integer') raise TypeError("key must be an integer")
if key < 1 or key > 15: # some receivers have devices past their max # devices if key < 1 or key > 15: # some receivers have devices past their max # devices
raise IndexError(key) raise IndexError(key)
@ -339,9 +343,9 @@ class Receiver:
dev.wpid = None dev.wpid = None
if key in self._devices: if key in self._devices:
del self._devices[key] del self._devices[key]
logger.warning('%s removed device %s', self, dev) logger.warning("%s removed device %s", self, dev)
else: else:
if self.receiver_kind == 'bolt': if self.receiver_kind == "bolt":
reply = self.write_register(_R.bolt_pairing, 0x03, key) reply = self.write_register(_R.bolt_pairing, 0x03, key)
else: else:
reply = self.write_register(_R.receiver_pairing, 0x03, key) reply = self.write_register(_R.receiver_pairing, 0x03, key)
@ -352,10 +356,10 @@ class Receiver:
if key in self._devices: if key in self._devices:
del self._devices[key] del self._devices[key]
if logger.isEnabledFor(logging.INFO): if logger.isEnabledFor(logging.INFO):
logger.info('%s unpaired device %s', self, dev) logger.info("%s unpaired device %s", self, dev)
else: else:
logger.error('%s failed to unpair device %s', self, dev) logger.error("%s failed to unpair device %s", self, dev)
raise Exception('failed to unpair device %s: %s' % (dev.name, key)) raise Exception("failed to unpair device %s: %s" % (dev.name, key))
def __len__(self): def __len__(self):
return len([d for d in self._devices.values() if d is not None]) return len([d for d in self._devices.values() if d is not None])
@ -393,8 +397,8 @@ class Receiver:
if handle: if handle:
return Receiver(handle, device_info.path, device_info.product_id, setting_callback) return Receiver(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)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -58,7 +58,7 @@ def attach_to(device, changed_callback):
assert device assert device
assert changed_callback assert changed_callback
if not hasattr(device, 'status') or device.status is None: if not hasattr(device, "status") or device.status is None:
if not device.isDevice: if not device.isDevice:
device.status = ReceiverStatus(device, changed_callback) device.status = ReceiverStatus(device, changed_callback)
else: else:
@ -97,10 +97,9 @@ class ReceiverStatus(dict):
def to_string(self): def to_string(self):
count = len(self._receiver) count = len(self._receiver)
return ( return (
_('No paired devices.') _("No paired devices.")
if count == 0 else ngettext('%(count)s paired device.', '%(count)s paired devices.', count) % { if count == 0
'count': count else ngettext("%(count)s paired device.", "%(count)s paired devices.", count) % {"count": count}
}
) )
def __str__(self): def __str__(self):
@ -129,21 +128,20 @@ class DeviceStatus(dict):
self._active = None # is the device active? self._active = None # is the device active?
def to_string(self): def to_string(self):
status = ""
status = ''
battery_level = self.get(KEYS.BATTERY_LEVEL) battery_level = self.get(KEYS.BATTERY_LEVEL)
if battery_level is not None: if battery_level is not None:
if isinstance(battery_level, _NamedInt): if isinstance(battery_level, _NamedInt):
status = _('Battery: %(level)s') % {'level': _(str(battery_level))} status = _("Battery: %(level)s") % {"level": _(str(battery_level))}
else: else:
status = _('Battery: %(percent)d%%') % {'percent': battery_level} status = _("Battery: %(percent)d%%") % {"percent": battery_level}
battery_status = self.get(KEYS.BATTERY_STATUS) battery_status = self.get(KEYS.BATTERY_STATUS)
if battery_status is not None: if battery_status is not None:
status += ' (%s)' % _(str(battery_status)) status += " (%s)" % _(str(battery_status))
return status return status
def __repr__(self): def __repr__(self):
return '{' + ', '.join('\'%s\': %r' % (k, v) for k, v in self.items()) + '}' return "{" + ", ".join("'%s': %r" % (k, v) for k, v in self.items()) + "}"
def __bool__(self): def __bool__(self):
return bool(self._active) return bool(self._active)
@ -152,7 +150,7 @@ class DeviceStatus(dict):
def set_battery_info(self, level, nextLevel, status, voltage): def set_battery_info(self, level, nextLevel, status, voltage):
if logger.isEnabledFor(logging.DEBUG): if logger.isEnabledFor(logging.DEBUG):
logger.debug('%s: battery %s, %s', self._device, level, status) logger.debug("%s: battery %s, %s", self._device, level, status)
if level is None: if level is None:
# Some notifications may come with no battery level info, just # Some notifications may come with no battery level info, just
@ -176,8 +174,10 @@ class DeviceStatus(dict):
old_voltage, self[KEYS.BATTERY_VOLTAGE] = self.get(KEYS.BATTERY_VOLTAGE), voltage old_voltage, self[KEYS.BATTERY_VOLTAGE] = self.get(KEYS.BATTERY_VOLTAGE), voltage
charging = status in ( charging = status in (
_hidpp20_constants.BATTERY_STATUS.recharging, _hidpp20_constants.BATTERY_STATUS.almost_full, _hidpp20_constants.BATTERY_STATUS.recharging,
_hidpp20_constants.BATTERY_STATUS.full, _hidpp20_constants.BATTERY_STATUS.slow_recharge _hidpp20_constants.BATTERY_STATUS.almost_full,
_hidpp20_constants.BATTERY_STATUS.full,
_hidpp20_constants.BATTERY_STATUS.slow_recharge,
) )
old_charging, self[KEYS.BATTERY_CHARGING] = self.get(KEYS.BATTERY_CHARGING), charging old_charging, self[KEYS.BATTERY_CHARGING] = self.get(KEYS.BATTERY_CHARGING), charging
@ -187,15 +187,15 @@ class DeviceStatus(dict):
if _hidpp20_constants.BATTERY_OK(status) and (level is None or level > _BATTERY_ATTENTION_LEVEL): if _hidpp20_constants.BATTERY_OK(status) and (level is None or level > _BATTERY_ATTENTION_LEVEL):
self[KEYS.ERROR] = None self[KEYS.ERROR] = None
else: else:
logger.warning('%s: battery %d%%, ALERT %s', self._device, level, status) logger.warning("%s: battery %d%%, ALERT %s", self._device, level, status)
if self.get(KEYS.ERROR) != status: if self.get(KEYS.ERROR) != status:
self[KEYS.ERROR] = status self[KEYS.ERROR] = status
# only show the notification once # only show the notification once
alert = ALERT.NOTIFICATION | ALERT.ATTENTION alert = ALERT.NOTIFICATION | ALERT.ATTENTION
if isinstance(level, _NamedInt): if isinstance(level, _NamedInt):
reason = _('Battery: %(level)s (%(status)s)') % {'level': _(level), 'status': _(status)} reason = _("Battery: %(level)s (%(status)s)") % {"level": _(level), "status": _(status)}
else: else:
reason = _('Battery: %(percent)d%% (%(status)s)') % {'percent': level, 'status': status.name} reason = _("Battery: %(percent)d%% (%(status)s)") % {"percent": level, "status": status.name}
if changed or reason or not self._active: # a battery response means device is active if changed or reason or not self._active: # a battery response means device is active
# update the leds on the device, if any # update the leds on the device, if any
@ -241,11 +241,14 @@ class DeviceStatus(dict):
# Push settings for new devices (was_active is None), # Push settings for new devices (was_active is None),
# when devices request software reconfiguration # when devices request software reconfiguration
# and when devices become active if they don't have wireless device status feature, # and when devices become active if they don't have wireless device status feature,
if was_active is None or push or not was_active and ( if (
not d.features or _hidpp20_constants.FEATURE.WIRELESS_DEVICE_STATUS not in d.features was_active is None
or push
or not was_active
and (not d.features or _hidpp20_constants.FEATURE.WIRELESS_DEVICE_STATUS not in d.features)
): ):
if logger.isEnabledFor(logging.INFO): if logger.isEnabledFor(logging.INFO):
logger.info('%s pushing device settings %s', d, d.settings) logger.info("%s pushing device settings %s", d, d.settings)
_settings.apply_all_settings(d) _settings.apply_all_settings(d)
else: else:

View File

@ -20,13 +20,16 @@ import pkgutil as _pkgutil
import subprocess as _subprocess import subprocess as _subprocess
import sys as _sys import sys as _sys
NAME = 'solaar' NAME = "solaar"
try: try:
__version__ = _subprocess.check_output(['git', 'describe', '--always'], cwd=_sys.path[0], __version__ = (
stderr=_subprocess.DEVNULL).strip().decode() _subprocess.check_output(["git", "describe", "--always"], cwd=_sys.path[0], stderr=_subprocess.DEVNULL)
.strip()
.decode()
)
except Exception: except Exception:
try: try:
__version__ = _pkgutil.get_data('solaar', 'commit').strip().decode() __version__ = _pkgutil.get_data("solaar", "commit").strip().decode()
except Exception: except Exception:
__version__ = _pkgutil.get_data('solaar', 'version').strip().decode() __version__ = _pkgutil.get_data("solaar", "version").strip().decode()

View File

@ -27,6 +27,7 @@ import logitech_receiver.device as _device
import logitech_receiver.receiver as _receiver import logitech_receiver.receiver as _receiver
from logitech_receiver.base import receivers, receivers_and_devices from logitech_receiver.base import receivers, receivers_and_devices
from solaar import NAME from solaar import NAME
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -38,70 +39,66 @@ logger = logging.getLogger(__name__)
def _create_parser(): def _create_parser():
parser = _argparse.ArgumentParser( parser = _argparse.ArgumentParser(
prog=NAME.lower(), prog=NAME.lower(), add_help=False, epilog="For details on individual actions, run `%s <action> --help`." % NAME.lower()
add_help=False,
epilog='For details on individual actions, run `%s <action> --help`.' % NAME.lower()
) )
subparsers = parser.add_subparsers(title='actions', help='optional action to perform') subparsers = parser.add_subparsers(title="actions", help="optional action to perform")
sp = subparsers.add_parser('show', help='show information about devices') sp = subparsers.add_parser("show", help="show information about devices")
sp.add_argument( sp.add_argument(
'device', "device",
nargs='?', nargs="?",
default='all', default="all",
help='device to show information about; may be a device number (1..6), a serial number, ' help="device to show information about; may be a device number (1..6), a serial number, "
'a substring of a device\'s name, or "all" (the default)' 'a substring of a device\'s name, or "all" (the default)',
) )
sp.set_defaults(action='show') sp.set_defaults(action="show")
sp = subparsers.add_parser('probe', help='probe a receiver (debugging use only)') sp = subparsers.add_parser("probe", help="probe a receiver (debugging use only)")
sp.add_argument( sp.add_argument(
'receiver', nargs='?', help='select receiver by name substring or serial number when more than one is present' "receiver", nargs="?", help="select receiver by name substring or serial number when more than one is present"
) )
sp.set_defaults(action='probe') sp.set_defaults(action="probe")
sp = subparsers.add_parser('profiles', help='read or write onboard profiles', epilog='Only works on active devices.') sp = subparsers.add_parser("profiles", help="read or write onboard profiles", epilog="Only works on active devices.")
sp.add_argument( sp.add_argument(
'device', "device",
help='device to read or write profiles of; may be a device number (1..6), a serial number, ' help="device to read or write profiles of; may be a device number (1..6), a serial number, "
'a substring of a device\'s name' "a substring of a device's name",
) )
sp.add_argument('profiles', nargs='?', help='file containing YAML dump of profiles') sp.add_argument("profiles", nargs="?", help="file containing YAML dump of profiles")
sp.set_defaults(action='profiles') sp.set_defaults(action="profiles")
sp = subparsers.add_parser( sp = subparsers.add_parser(
'config', "config",
help='read/write device-specific settings', help="read/write device-specific settings",
epilog='Please note that configuration only works on active devices.' epilog="Please note that configuration only works on active devices.",
) )
sp.add_argument( sp.add_argument(
'device', "device",
help='device to configure; may be a device number (1..6), a serial number, ' help="device to configure; may be a device number (1..6), a serial number, " "or a substring of a device's name",
'or a substring of a device\'s name'
) )
sp.add_argument('setting', nargs='?', help='device-specific setting; leave empty to list available settings') sp.add_argument("setting", nargs="?", help="device-specific setting; leave empty to list available settings")
sp.add_argument('value_key', nargs='?', help='new value for the setting or key for keyed settings') sp.add_argument("value_key", nargs="?", help="new value for the setting or key for keyed settings")
sp.add_argument('extra_subkey', nargs='?', help='value for keyed or subkey for subkeyed settings') sp.add_argument("extra_subkey", nargs="?", help="value for keyed or subkey for subkeyed settings")
sp.add_argument('extra2', nargs='?', help='value for subkeyed settings') sp.add_argument("extra2", nargs="?", help="value for subkeyed settings")
sp.set_defaults(action='config') sp.set_defaults(action="config")
sp = subparsers.add_parser( sp = subparsers.add_parser(
'pair', "pair",
help='pair a new device', help="pair a new device",
epilog='The Logitech Unifying Receiver supports up to 6 paired devices at the same time.' epilog="The Logitech Unifying Receiver supports up to 6 paired devices at the same time.",
) )
sp.add_argument( sp.add_argument(
'receiver', nargs='?', help='select receiver by name substring or serial number when more than one is present' "receiver", nargs="?", help="select receiver by name substring or serial number when more than one is present"
) )
sp.set_defaults(action='pair') sp.set_defaults(action="pair")
sp = subparsers.add_parser('unpair', help='unpair a device') sp = subparsers.add_parser("unpair", help="unpair a device")
sp.add_argument( sp.add_argument(
'device', "device",
help='device to unpair; may be a device number (1..6), a serial number, ' help="device to unpair; may be a device number (1..6), a serial number, " "or a substring of a device's name.",
'or a substring of a device\'s name.'
) )
sp.set_defaults(action='unpair') sp.set_defaults(action="unpair")
return parser, subparsers.choices return parser, subparsers.choices
@ -117,12 +114,12 @@ def _receivers(dev_path=None):
try: try:
r = _receiver.Receiver.open(dev_info) r = _receiver.Receiver.open(dev_info)
if logger.isEnabledFor(logging.DEBUG): if logger.isEnabledFor(logging.DEBUG):
logger.debug('[%s] => %s', dev_info.path, r) logger.debug("[%s] => %s", dev_info.path, r)
if r: if r:
yield r yield r
except Exception as e: except Exception as e:
logger.exception('opening ' + str(dev_info)) logger.exception("opening " + str(dev_info))
_sys.exit('%s: error: %s' % (NAME, str(e))) _sys.exit("%s: error: %s" % (NAME, str(e)))
def _receivers_and_devices(dev_path=None): def _receivers_and_devices(dev_path=None):
@ -132,12 +129,12 @@ def _receivers_and_devices(dev_path=None):
try: try:
d = _device.Device.open(dev_info) if dev_info.isDevice else _receiver.Receiver.open(dev_info) d = _device.Device.open(dev_info) if dev_info.isDevice else _receiver.Receiver.open(dev_info)
if logger.isEnabledFor(logging.DEBUG): if logger.isEnabledFor(logging.DEBUG):
logger.debug('[%s] => %s', dev_info.path, d) logger.debug("[%s] => %s", dev_info.path, d)
if d is not None: if d is not None:
yield d yield d
except Exception as e: except Exception as e:
logger.exception('opening ' + str(dev_info)) logger.exception("opening " + str(dev_info))
_sys.exit('%s: error: %s' % (NAME, str(e))) _sys.exit("%s: error: %s" % (NAME, str(e)))
def _find_receiver(receivers, name): def _find_receiver(receivers, name):
@ -178,7 +175,9 @@ def _find_device(receivers, name):
for dev in r: for dev in r:
if ( if (
name == dev.serial.lower() or name == dev.codename.lower() or name == str(dev.kind).lower() name == dev.serial.lower()
or name == dev.codename.lower()
or name == str(dev.kind).lower()
or name in dev.name.lower() or name in dev.name.lower()
): ):
yield dev yield dev
@ -191,7 +190,6 @@ def _find_device(receivers, name):
def run(cli_args=None, hidraw_path=None): def run(cli_args=None, hidraw_path=None):
if cli_args: if cli_args:
action = cli_args[0] action = cli_args[0]
args = _cli_parser.parse_args(cli_args) args = _cli_parser.parse_args(cli_args)
@ -199,15 +197,15 @@ def run(cli_args=None, hidraw_path=None):
args = _cli_parser.parse_args() args = _cli_parser.parse_args()
# Python 3 has an undocumented 'feature' that breaks parsing empty args # Python 3 has an undocumented 'feature' that breaks parsing empty args
# http://bugs.python.org/issue16308 # http://bugs.python.org/issue16308
if 'cmd' not in args: if "cmd" not in args:
_cli_parser.print_usage(_sys.stderr) _cli_parser.print_usage(_sys.stderr)
_sys.stderr.write('%s: error: too few arguments\n' % NAME.lower()) _sys.stderr.write("%s: error: too few arguments\n" % NAME.lower())
_sys.exit(2) _sys.exit(2)
action = args.action action = args.action
assert action in actions assert action in actions
try: try:
if action == 'show' or action == 'probe' or action == 'config' or action == 'profiles': if action == "show" or action == "probe" or action == "config" or action == "profiles":
c = list(_receivers_and_devices(hidraw_path)) c = list(_receivers_and_devices(hidraw_path))
else: else:
c = list(_receivers(hidraw_path)) c = list(_receivers(hidraw_path))
@ -215,10 +213,10 @@ def run(cli_args=None, hidraw_path=None):
raise Exception( raise Exception(
'No supported device found. Use "lsusb" and "bluetoothctl devices Connected" to list connected devices.' 'No supported device found. Use "lsusb" and "bluetoothctl devices Connected" to list connected devices.'
) )
m = import_module('.' + action, package=__name__) m = import_module("." + action, package=__name__)
m.run(c, args, _find_receiver, _find_device) m.run(c, args, _find_receiver, _find_device)
except AssertionError: except AssertionError:
tb_last = extract_tb(_sys.exc_info()[2])[-1] tb_last = extract_tb(_sys.exc_info()[2])[-1]
_sys.exit('%s: assertion failed: %s line %d' % (NAME.lower(), tb_last[0], tb_last[1])) _sys.exit("%s: assertion failed: %s line %d" % (NAME.lower(), tb_last[0], tb_last[1]))
except Exception: except Exception:
_sys.exit('%s: error: %s' % (NAME.lower(), format_exc())) _sys.exit("%s: error: %s" % (NAME.lower(), format_exc()))

View File

@ -21,56 +21,58 @@ import yaml as _yaml
from logitech_receiver import settings as _settings from logitech_receiver import settings as _settings
from logitech_receiver import settings_templates as _settings_templates from logitech_receiver import settings_templates as _settings_templates
from logitech_receiver.common import NamedInts as _NamedInts from logitech_receiver.common import NamedInts as _NamedInts
from solaar import configuration as _configuration from solaar import configuration as _configuration
def _print_setting(s, verbose=True): def _print_setting(s, verbose=True):
print('#', s.label) print("#", s.label)
if verbose: if verbose:
if s.description: if s.description:
print('#', s.description.replace('\n', ' ')) print("#", s.description.replace("\n", " "))
if s.kind == _settings.KIND.toggle: if s.kind == _settings.KIND.toggle:
print('# possible values: on/true/t/yes/y/1 or off/false/f/no/n/0 or Toggle/~') print("# possible values: on/true/t/yes/y/1 or off/false/f/no/n/0 or Toggle/~")
elif s.kind == _settings.KIND.choice: elif s.kind == _settings.KIND.choice:
print( print(
'# possible values: one of [', ', '.join(str(v) for v in s.choices), "# possible values: one of [",
'], or higher/lower/highest/max/lowest/min' ", ".join(str(v) for v in s.choices),
"], or higher/lower/highest/max/lowest/min",
) )
else: else:
# wtf? # wtf?
pass pass
value = s.read(cached=False) value = s.read(cached=False)
if value is None: if value is None:
print(s.name, '= ? (failed to read from device)') print(s.name, "= ? (failed to read from device)")
else: else:
print(s.name, '=', s.val_to_string(value)) print(s.name, "=", s.val_to_string(value))
def _print_setting_keyed(s, key, verbose=True): def _print_setting_keyed(s, key, verbose=True):
print('#', s.label) print("#", s.label)
if verbose: if verbose:
if s.description: if s.description:
print('#', s.description.replace('\n', ' ')) print("#", s.description.replace("\n", " "))
if s.kind == _settings.KIND.multiple_toggle: if s.kind == _settings.KIND.multiple_toggle:
k = next((k for k in s._labels if key == k), None) k = next((k for k in s._labels if key == k), None)
if k is None: if k is None:
print(s.name, '=? (key not found)') print(s.name, "=? (key not found)")
else: else:
print('# possible values: on/true/t/yes/y/1 or off/false/f/no/n/0 or Toggle/~') print("# possible values: on/true/t/yes/y/1 or off/false/f/no/n/0 or Toggle/~")
value = s.read(cached=False) value = s.read(cached=False)
if value is None: if value is None:
print(s.name, '= ? (failed to read from device)') print(s.name, "= ? (failed to read from device)")
else: else:
print(s.name, s.val_to_string({k: value[str(int(k))]})) print(s.name, s.val_to_string({k: value[str(int(k))]}))
elif s.kind == _settings.KIND.map_choice: elif s.kind == _settings.KIND.map_choice:
k = next((k for k in s.choices.keys() if key == k), None) k = next((k for k in s.choices.keys() if key == k), None)
if k is None: if k is None:
print(s.name, '=? (key not found)') print(s.name, "=? (key not found)")
else: else:
print('# possible values: one of [', ', '.join(str(v) for v in s.choices[k]), ']') print("# possible values: one of [", ", ".join(str(v) for v in s.choices[k]), "]")
value = s.read(cached=False) value = s.read(cached=False)
if value is None: if value is None:
print(s.name, '= ? (failed to read from device)') print(s.name, "= ? (failed to read from device)")
else: else:
print(s.name, s.val_to_string({k: value[int(k)]})) print(s.name, s.val_to_string({k: value[int(k)]}))
@ -94,35 +96,35 @@ def select_choice(value, choices, setting, key):
value = val value = val
elif ivalue is not None and ivalue >= 1 and ivalue <= len(choices): elif ivalue is not None and ivalue >= 1 and ivalue <= len(choices):
value = choices[ivalue - 1] value = choices[ivalue - 1]
elif lvalue in ('higher', 'lower'): elif lvalue in ("higher", "lower"):
old_value = setting.read() if key is None else setting.read_key(key) old_value = setting.read() if key is None else setting.read_key(key)
if old_value is None: if old_value is None:
raise Exception('%s: could not read current value' % setting.name) raise Exception("%s: could not read current value" % setting.name)
if lvalue == 'lower': if lvalue == "lower":
lower_values = choices[:old_value] lower_values = choices[:old_value]
value = lower_values[-1] if lower_values else choices[:][0] value = lower_values[-1] if lower_values else choices[:][0]
elif lvalue == 'higher': elif lvalue == "higher":
higher_values = choices[old_value + 1:] higher_values = choices[old_value + 1 :]
value = higher_values[0] if higher_values else choices[:][-1] value = higher_values[0] if higher_values else choices[:][-1]
elif lvalue in ('highest', 'max', 'first'): elif lvalue in ("highest", "max", "first"):
value = choices[:][-1] value = choices[:][-1]
elif lvalue in ('lowest', 'min', 'last'): elif lvalue in ("lowest", "min", "last"):
value = choices[:][0] value = choices[:][0]
else: else:
raise Exception('%s: possible values are [%s]' % (setting.name, ', '.join(str(v) for v in choices))) raise Exception("%s: possible values are [%s]" % (setting.name, ", ".join(str(v) for v in choices)))
return value return value
def select_toggle(value, setting, key=None): def select_toggle(value, setting, key=None):
if value.lower() in ('toggle', '~'): if value.lower() in ("toggle", "~"):
value = not (setting.read() if key is None else setting.read()[str(int(key))]) value = not (setting.read() if key is None else setting.read()[str(int(key))])
else: else:
try: try:
value = bool(int(value)) value = bool(int(value))
except Exception: except Exception:
if value.lower() in ('true', 'yes', 'on', 't', 'y'): if value.lower() in ("true", "yes", "on", "t", "y"):
value = True value = True
elif value.lower() in ('false', 'no', 'off', 'f', 'n'): elif value.lower() in ("false", "no", "off", "f", "n"):
value = False value = False
else: else:
raise Exception("%s: don't know how to interpret '%s' as boolean" % (setting.name, value)) raise Exception("%s: don't know how to interpret '%s' as boolean" % (setting.name, value))
@ -157,12 +159,12 @@ def run(receivers, args, find_receiver, find_device):
if not args.setting: # print all settings, so first set them all up if not args.setting: # print all settings, so first set them all up
if not dev.settings: if not dev.settings:
raise Exception('no settings for %s' % dev.name) raise Exception("no settings for %s" % dev.name)
_configuration.attach_to(dev) _configuration.attach_to(dev)
# _settings.apply_all_settings(dev) # _settings.apply_all_settings(dev)
print(dev.name, '(%s) [%s:%s]' % (dev.codename, dev.wpid, dev.serial)) print(dev.name, "(%s) [%s:%s]" % (dev.codename, dev.wpid, dev.serial))
for s in dev.settings: for s in dev.settings:
print('') print("")
_print_setting(s) _print_setting(s)
return return
@ -186,10 +188,12 @@ def run(receivers, args, find_receiver, find_device):
remote = False remote = False
try: try:
import gi import gi
gi.require_version('Gtk', '3.0')
gi.require_version("Gtk", "3.0")
from gi.repository import Gio, Gtk from gi.repository import Gio, Gtk
if Gtk.init_check()[0]: # can Gtk be initialized? if Gtk.init_check()[0]: # can Gtk be initialized?
APP_ID = 'io.github.pwr_solaar.solaar' APP_ID = "io.github.pwr_solaar.solaar"
application = Gtk.Application.new(APP_ID, Gio.ApplicationFlags.HANDLES_COMMAND_LINE) application = Gtk.Application.new(APP_ID, Gio.ApplicationFlags.HANDLES_COMMAND_LINE)
application.register() application.register()
remote = application.get_is_remote() remote = application.get_is_remote()
@ -205,7 +209,7 @@ def run(receivers, args, find_receiver, find_device):
# if the Solaar UI is running tell it to also perform the set, otherwise save the change in the configuration file # if the Solaar UI is running tell it to also perform the set, otherwise save the change in the configuration file
if remote: if remote:
argl = ['config', dev.serial or dev.unitId, setting.name] argl = ["config", dev.serial or dev.unitId, setting.name]
argl.extend([a for a in [args.value_key, args.extra_subkey, args.extra2] if a is not None]) argl.extend([a for a in [args.value_key, args.extra_subkey, args.extra2] if a is not None])
application.run(_yaml.safe_dump(argl)) application.run(_yaml.safe_dump(argl))
else: else:
@ -217,19 +221,19 @@ def set(dev, setting, args, save):
if setting.kind == _settings.KIND.toggle: if setting.kind == _settings.KIND.toggle:
value = select_toggle(args.value_key, setting) value = select_toggle(args.value_key, setting)
args.value_key = value args.value_key = value
message = 'Setting %s of %s to %s' % (setting.name, dev.name, value) message = "Setting %s of %s to %s" % (setting.name, dev.name, value)
result = setting.write(value, save=save) result = setting.write(value, save=save)
elif setting.kind == _settings.KIND.range: elif setting.kind == _settings.KIND.range:
value = select_range(args.value_key, setting) value = select_range(args.value_key, setting)
args.value_key = value args.value_key = value
message = 'Setting %s of %s to %s' % (setting.name, dev.name, value) message = "Setting %s of %s to %s" % (setting.name, dev.name, value)
result = setting.write(value, save=save) result = setting.write(value, save=save)
elif setting.kind == _settings.KIND.choice: elif setting.kind == _settings.KIND.choice:
value = select_choice(args.value_key, setting.choices, setting, None) value = select_choice(args.value_key, setting.choices, setting, None)
args.value_key = int(value) args.value_key = int(value)
message = 'Setting %s of %s to %s' % (setting.name, dev.name, value) message = "Setting %s of %s to %s" % (setting.name, dev.name, value)
result = setting.write(value, save=save) result = setting.write(value, save=save)
elif setting.kind == _settings.KIND.map_choice: elif setting.kind == _settings.KIND.map_choice:
@ -247,7 +251,7 @@ def set(dev, setting, args, save):
args.value_key = str(int(k)) args.value_key = str(int(k))
else: else:
raise Exception("%s: key '%s' not in setting" % (setting.name, key)) raise Exception("%s: key '%s' not in setting" % (setting.name, key))
message = 'Setting %s of %s key %r to %r' % (setting.name, dev.name, k, value) message = "Setting %s of %s key %r to %r" % (setting.name, dev.name, k, value)
result = setting.write_key_value(int(k), value, save=save) result = setting.write_key_value(int(k), value, save=save)
elif setting.kind == _settings.KIND.multiple_toggle: elif setting.kind == _settings.KIND.multiple_toggle:
@ -255,7 +259,7 @@ def set(dev, setting, args, save):
_print_setting_keyed(setting, args.value_key) _print_setting_keyed(setting, args.value_key)
return (None, None, None) return (None, None, None)
key = args.value_key key = args.value_key
all_keys = getattr(setting, 'choices_universe', None) all_keys = getattr(setting, "choices_universe", None)
ikey = all_keys[int(key) if key.isdigit() else key] if isinstance(all_keys, _NamedInts) else to_int(key) ikey = all_keys[int(key) if key.isdigit() else key] if isinstance(all_keys, _NamedInts) else to_int(key)
k = next((k for k in setting._labels if key == k), None) k = next((k for k in setting._labels if key == k), None)
if k is None and ikey is not None: if k is None and ikey is not None:
@ -266,17 +270,17 @@ def set(dev, setting, args, save):
args.value_key = str(int(k)) args.value_key = str(int(k))
else: else:
raise Exception("%s: key '%s' not in setting" % (setting.name, key)) raise Exception("%s: key '%s' not in setting" % (setting.name, key))
message = 'Setting %s key %r to %r' % (setting.name, k, value) message = "Setting %s key %r to %r" % (setting.name, k, value)
result = setting.write_key_value(str(int(k)), value, save=save) result = setting.write_key_value(str(int(k)), value, save=save)
elif setting.kind == _settings.KIND.multiple_range: elif setting.kind == _settings.KIND.multiple_range:
if args.extra_subkey is None: if args.extra_subkey is None:
raise Exception('%s: setting needs both key and value to set' % (setting.name)) raise Exception("%s: setting needs both key and value to set" % (setting.name))
key = args.value_key key = args.value_key
all_keys = getattr(setting, 'choices_universe', None) all_keys = getattr(setting, "choices_universe", None)
ikey = all_keys[int(key) if key.isdigit() else key] if isinstance(all_keys, _NamedInts) else to_int(key) ikey = all_keys[int(key) if key.isdigit() else key] if isinstance(all_keys, _NamedInts) else to_int(key)
if args.extra2 is None or to_int(args.extra2) is None: if args.extra2 is None or to_int(args.extra2) is None:
raise Exception('%s: setting needs an integer value, not %s' % (setting.name, args.extra2)) raise Exception("%s: setting needs an integer value, not %s" % (setting.name, args.extra2))
if not setting._value: # ensure that there are values to look through if not setting._value: # ensure that there are values to look through
setting.read() setting.read()
k = next((k for k in setting._value if key == ikey or key.isdigit() and ikey == int(key)), None) k = next((k for k in setting._value if key == ikey or key.isdigit() and ikey == int(key)), None)
@ -288,11 +292,11 @@ def set(dev, setting, args, save):
args.value_key = str(int(k)) args.value_key = str(int(k))
else: else:
raise Exception("%s: key '%s' not in setting" % (setting.name, key)) raise Exception("%s: key '%s' not in setting" % (setting.name, key))
message = 'Setting %s key %s parameter %s to %r' % (setting.name, k, args.extra_subkey, item[args.extra_subkey]) message = "Setting %s key %s parameter %s to %r" % (setting.name, k, args.extra_subkey, item[args.extra_subkey])
result = setting.write_key_value(int(k), item, save=save) result = setting.write_key_value(int(k), item, save=save)
value = item value = item
else: else:
raise Exception('NotImplemented') raise Exception("NotImplemented")
return result, message, value return result, message, value

View File

@ -50,7 +50,6 @@ def run(receivers, args, find_receiver, _ignore):
known_devices = [dev.number for dev in receiver] known_devices = [dev.number for dev in receiver]
class _HandleWithNotificationHook(int): class _HandleWithNotificationHook(int):
def notifications_hook(self, n): def notifications_hook(self, n):
nonlocal known_devices nonlocal known_devices
assert n assert n
@ -68,9 +67,9 @@ def run(receivers, args, find_receiver, _ignore):
timeout = 30 # seconds timeout = 30 # seconds
receiver.handle = _HandleWithNotificationHook(receiver.handle) receiver.handle = _HandleWithNotificationHook(receiver.handle)
if receiver.receiver_kind == 'bolt': # Bolt receivers require authentication to pair a device if receiver.receiver_kind == "bolt": # Bolt receivers require authentication to pair a device
receiver.discover(timeout=timeout) receiver.discover(timeout=timeout)
print('Bolt Pairing: long-press the pairing key or button on your device (timing out in', timeout, 'seconds).') print("Bolt Pairing: long-press the pairing key or button on your device (timing out in", timeout, "seconds).")
pairing_start = _timestamp() pairing_start = _timestamp()
patience = 5 # the discovering notification may come slightly later, so be patient patience = 5 # the discovering notification may come slightly later, so be patient
while receiver.status.discovering or _timestamp() - pairing_start < patience: while receiver.status.discovering or _timestamp() - pairing_start < patience:
@ -84,11 +83,11 @@ def run(receivers, args, find_receiver, _ignore):
name = receiver.status.device_name name = receiver.status.device_name
authentication = receiver.status.device_authentication authentication = receiver.status.device_authentication
kind = receiver.status.device_kind kind = receiver.status.device_kind
print(f'Bolt Pairing: discovered {name}') print(f"Bolt Pairing: discovered {name}")
receiver.pair_device( receiver.pair_device(
address=address, address=address,
authentication=authentication, authentication=authentication,
entropy=20 if kind == _hidpp10_constants.DEVICE_KIND.keyboard else 10 entropy=20 if kind == _hidpp10_constants.DEVICE_KIND.keyboard else 10,
) )
pairing_start = _timestamp() pairing_start = _timestamp()
patience = 5 # the discovering notification may come slightly later, so be patient patience = 5 # the discovering notification may come slightly later, so be patient
@ -100,12 +99,12 @@ def run(receivers, args, find_receiver, _ignore):
if n: if n:
receiver.handle.notifications_hook(n) receiver.handle.notifications_hook(n)
if authentication & 0x01: if authentication & 0x01:
print(f'Bolt Pairing: type passkey {receiver.status.device_passkey} and then press the enter key') print(f"Bolt Pairing: type passkey {receiver.status.device_passkey} and then press the enter key")
else: else:
passkey = f'{int(receiver.status.device_passkey):010b}' passkey = f"{int(receiver.status.device_passkey):010b}"
passkey = ', '.join(['right' if bit == '1' else 'left' for bit in passkey]) passkey = ", ".join(["right" if bit == "1" else "left" for bit in passkey])
print(f'Bolt Pairing: press {passkey}') print(f"Bolt Pairing: press {passkey}")
print('and then press left and right buttons simultaneously') print("and then press left and right buttons simultaneously")
while receiver.status.lock_open: while receiver.status.lock_open:
n = _base.read(receiver.handle) n = _base.read(receiver.handle)
n = _base.make_notification(*n) if n else None n = _base.make_notification(*n) if n else None
@ -114,7 +113,7 @@ def run(receivers, args, find_receiver, _ignore):
else: else:
receiver.set_lock(False, timeout=timeout) receiver.set_lock(False, timeout=timeout)
print('Pairing: turn your new device on (timing out in', timeout, 'seconds).') print("Pairing: turn your new device on (timing out in", timeout, "seconds).")
pairing_start = _timestamp() pairing_start = _timestamp()
patience = 5 # the lock-open notification may come slightly later, wait for it a bit patience = 5 # the lock-open notification may come slightly later, wait for it a bit
while receiver.status.lock_open or _timestamp() - pairing_start < patience: while receiver.status.lock_open or _timestamp() - pairing_start < patience:
@ -131,10 +130,10 @@ def run(receivers, args, find_receiver, _ignore):
if receiver.status.new_device: if receiver.status.new_device:
dev = receiver.status.new_device dev = receiver.status.new_device
print('Paired device %d: %s (%s) [%s:%s]' % (dev.number, dev.name, dev.codename, dev.wpid, dev.serial)) print("Paired device %d: %s (%s) [%s:%s]" % (dev.number, dev.name, dev.codename, dev.wpid, dev.serial))
else: else:
error = receiver.status.get(_status.KEYS.ERROR) error = receiver.status.get(_status.KEYS.ERROR)
if error: if error:
raise Exception('pairing failed: %s' % error) raise Exception("pairing failed: %s" % error)
else: else:
print('Paired device') # this is better than an error print("Paired device") # this is better than an error

View File

@ -19,6 +19,7 @@
from logitech_receiver import base as _base from logitech_receiver import base as _base
from logitech_receiver import hidpp10_constants as _hidpp10_constants from logitech_receiver import hidpp10_constants as _hidpp10_constants
from logitech_receiver.common import strhex as _strhex from logitech_receiver.common import strhex as _strhex
from solaar.cli.show import _print_device, _print_receiver from solaar.cli.show import _print_device, _print_receiver
_R = _hidpp10_constants.REGISTERS _R = _hidpp10_constants.REGISTERS
@ -43,49 +44,53 @@ def run(receivers, args, find_receiver, _ignore):
_print_receiver(receiver) _print_receiver(receiver)
print('') print("")
print(' Register Dump') print(" Register Dump")
rgst = receiver.read_register(_R.notifications) rgst = receiver.read_register(_R.notifications)
print(' Notifications %#04x: %s' % (_R.notifications % 0x100, '0x' + _strhex(rgst) if rgst else 'None')) print(" Notifications %#04x: %s" % (_R.notifications % 0x100, "0x" + _strhex(rgst) if rgst else "None"))
rgst = receiver.read_register(_R.receiver_connection) rgst = receiver.read_register(_R.receiver_connection)
print(' Connection State %#04x: %s' % (_R.receiver_connection % 0x100, '0x' + _strhex(rgst) if rgst else 'None')) print(" Connection State %#04x: %s" % (_R.receiver_connection % 0x100, "0x" + _strhex(rgst) if rgst else "None"))
rgst = receiver.read_register(_R.devices_activity) rgst = receiver.read_register(_R.devices_activity)
print(' Device Activity %#04x: %s' % (_R.devices_activity % 0x100, '0x' + _strhex(rgst) if rgst else 'None')) print(" Device Activity %#04x: %s" % (_R.devices_activity % 0x100, "0x" + _strhex(rgst) if rgst else "None"))
for sub_reg in range(0, 16): for sub_reg in range(0, 16):
rgst = receiver.read_register(_R.receiver_info, sub_reg) rgst = receiver.read_register(_R.receiver_info, sub_reg)
print( print(
' Pairing Register %#04x %#04x: %s' % " Pairing Register %#04x %#04x: %s"
(_R.receiver_info % 0x100, sub_reg, '0x' + _strhex(rgst) if rgst else 'None') % (_R.receiver_info % 0x100, sub_reg, "0x" + _strhex(rgst) if rgst else "None")
) )
for device in range(0, 7): for device in range(0, 7):
for sub_reg in [0x10, 0x20, 0x30, 0x50]: for sub_reg in [0x10, 0x20, 0x30, 0x50]:
rgst = receiver.read_register(_R.receiver_info, sub_reg + device) rgst = receiver.read_register(_R.receiver_info, sub_reg + device)
print( print(
' Pairing Register %#04x %#04x: %s' % " Pairing Register %#04x %#04x: %s"
(_R.receiver_info % 0x100, sub_reg + device, '0x' + _strhex(rgst) if rgst else 'None') % (_R.receiver_info % 0x100, sub_reg + device, "0x" + _strhex(rgst) if rgst else "None")
) )
rgst = receiver.read_register(_R.receiver_info, 0x40 + device) rgst = receiver.read_register(_R.receiver_info, 0x40 + device)
print( print(
' Pairing Name %#04x %#02x: %s' % " Pairing Name %#04x %#02x: %s"
(_R.receiver_info % 0x100, 0x40 + device, rgst[2:2 + ord(rgst[1:2])] if rgst else 'None') % (_R.receiver_info % 0x100, 0x40 + device, rgst[2 : 2 + ord(rgst[1:2])] if rgst else "None")
) )
for part in range(1, 4): for part in range(1, 4):
rgst = receiver.read_register(_R.receiver_info, 0x60 + device, part) rgst = receiver.read_register(_R.receiver_info, 0x60 + device, part)
print( print(
' Pairing Name %#04x %#02x %#02x: %2d %s' % ( " Pairing Name %#04x %#02x %#02x: %2d %s"
_R.receiver_info % 0x100, 0x60 + device, part, ord(rgst[2:3]) if rgst else 0, % (
rgst[3:3 + ord(rgst[2:3])] if rgst else 'None' _R.receiver_info % 0x100,
0x60 + device,
part,
ord(rgst[2:3]) if rgst else 0,
rgst[3 : 3 + ord(rgst[2:3])] if rgst else "None",
) )
) )
for sub_reg in range(0, 5): for sub_reg in range(0, 5):
rgst = receiver.read_register(_R.firmware, sub_reg) rgst = receiver.read_register(_R.firmware, sub_reg)
print( print(
' Firmware %#04x %#04x: %s' % " Firmware %#04x %#04x: %s"
(_R.firmware % 0x100, sub_reg, '0x' + _strhex(rgst) if rgst is not None else 'None') % (_R.firmware % 0x100, sub_reg, "0x" + _strhex(rgst) if rgst is not None else "None")
) )
print('') print("")
for reg in range(0, 0xFF): for reg in range(0, 0xFF):
last = None last = None
for sub in range(0, 0xFF): for sub in range(0, 0xFF):
@ -97,8 +102,8 @@ def run(receivers, args, find_receiver, _ignore):
else: else:
if not isinstance(last, bytes) or not isinstance(rgst, bytes) or last != rgst: if not isinstance(last, bytes) or not isinstance(rgst, bytes) or last != rgst:
print( print(
' Register Short %#04x %#04x: %s' % " Register Short %#04x %#04x: %s"
(reg, sub, '0x' + _strhex(rgst) if isinstance(rgst, bytes) else str(rgst)) % (reg, sub, "0x" + _strhex(rgst) if isinstance(rgst, bytes) else str(rgst))
) )
last = rgst last = rgst
last = None last = None
@ -111,7 +116,7 @@ def run(receivers, args, find_receiver, _ignore):
else: else:
if not isinstance(last, bytes) or not isinstance(rgst, bytes) or last != rgst: if not isinstance(last, bytes) or not isinstance(rgst, bytes) or last != rgst:
print( print(
' Register Long %#04x %#04x: %s' % " Register Long %#04x %#04x: %s"
(reg, sub, '0x' + _strhex(rgst) if isinstance(rgst, bytes) else str(rgst)) % (reg, sub, "0x" + _strhex(rgst) if isinstance(rgst, bytes) else str(rgst))
) )
last = rgst last = rgst

View File

@ -41,27 +41,27 @@ def run(receivers, args, find_receiver, find_device):
raise Exception("no online device found matching '%s'" % device_name) raise Exception("no online device found matching '%s'" % device_name)
if not (dev.online and dev.profiles): if not (dev.online and dev.profiles):
print(f'Device {dev.name} is either offline or has no onboard profiles') print(f"Device {dev.name} is either offline or has no onboard profiles")
elif not profiles_file: elif not profiles_file:
print(f'#Dumping profiles from {dev.name}') print(f"#Dumping profiles from {dev.name}")
print(_yaml.dump(dev.profiles)) print(_yaml.dump(dev.profiles))
else: else:
try: try:
with open(profiles_file, 'r') as f: with open(profiles_file, "r") as f:
print(f'Reading profiles from {profiles_file}') print(f"Reading profiles from {profiles_file}")
profiles = _yaml.safe_load(f) profiles = _yaml.safe_load(f)
if not isinstance(profiles, _OnboardProfiles): if not isinstance(profiles, _OnboardProfiles):
print('Profiles file does not contain current onboard profiles') print("Profiles file does not contain current onboard profiles")
elif getattr(profiles, 'version', None) != _OnboardProfilesVersion: elif getattr(profiles, "version", None) != _OnboardProfilesVersion:
version = getattr(profiles, 'version', None) version = getattr(profiles, "version", None)
print(f'Missing or incorrect profile version {version} in loaded profile') print(f"Missing or incorrect profile version {version} in loaded profile")
elif getattr(profiles, 'name', None) != dev.name: elif getattr(profiles, "name", None) != dev.name:
name = getattr(profiles, 'name', None) name = getattr(profiles, "name", None)
print(f'Different device name {name} in loaded profile') print(f"Different device name {name} in loaded profile")
else: else:
print(f'Loading profiles into {dev.name}') print(f"Loading profiles into {dev.name}")
written = profiles.write(dev) written = profiles.write(dev)
print(f'Wrote {written} sectors to {dev.name}') print(f"Wrote {written} sectors to {dev.name}")
except Exception as exc: except Exception as exc:
print('Profiles not written:', exc) print("Profiles not written:", exc)
print(_traceback.format_exc()) print(_traceback.format_exc())

View File

@ -25,6 +25,7 @@ from logitech_receiver import receiver as _receiver
from logitech_receiver import settings_templates as _settings_templates from logitech_receiver import settings_templates as _settings_templates
from logitech_receiver.common import NamedInt as _NamedInt from logitech_receiver.common import NamedInt as _NamedInt
from logitech_receiver.common import strhex as _strhex from logitech_receiver.common import strhex as _strhex
from solaar import NAME, __version__ from solaar import NAME, __version__
_F = _hidpp20_constants.FEATURE _F = _hidpp20_constants.FEATURE
@ -34,39 +35,39 @@ def _print_receiver(receiver):
paired_count = receiver.count() paired_count = receiver.count()
print(receiver.name) print(receiver.name)
print(' Device path :', receiver.path) print(" Device path :", receiver.path)
print(' USB id : 046d:%s' % receiver.product_id) print(" USB id : 046d:%s" % receiver.product_id)
print(' Serial :', receiver.serial) print(" Serial :", receiver.serial)
if receiver.firmware: if receiver.firmware:
for f in receiver.firmware: for f in receiver.firmware:
print(' %-11s: %s' % (f.kind, f.version)) print(" %-11s: %s" % (f.kind, f.version))
print(' Has', paired_count, 'paired device(s) out of a maximum of %d.' % receiver.max_devices) print(" Has", paired_count, "paired device(s) out of a maximum of %d." % receiver.max_devices)
if receiver.remaining_pairings() and receiver.remaining_pairings() >= 0: if receiver.remaining_pairings() and receiver.remaining_pairings() >= 0:
print(' Has %d successful pairing(s) remaining.' % receiver.remaining_pairings()) print(" Has %d successful pairing(s) remaining." % receiver.remaining_pairings())
notification_flags = _hidpp10.get_notification_flags(receiver) notification_flags = _hidpp10.get_notification_flags(receiver)
if notification_flags is not None: if notification_flags is not None:
if notification_flags: if notification_flags:
notification_names = _hidpp10_constants.NOTIFICATION_FLAG.flag_names(notification_flags) notification_names = _hidpp10_constants.NOTIFICATION_FLAG.flag_names(notification_flags)
print(' Notifications: %s (0x%06X)' % (', '.join(notification_names), notification_flags)) print(" Notifications: %s (0x%06X)" % (", ".join(notification_names), notification_flags))
else: else:
print(' Notifications: (none)') print(" Notifications: (none)")
activity = receiver.read_register(_hidpp10_constants.REGISTERS.devices_activity) activity = receiver.read_register(_hidpp10_constants.REGISTERS.devices_activity)
if activity: if activity:
activity = [(d, ord(activity[d - 1:d])) for d in range(1, receiver.max_devices)] activity = [(d, ord(activity[d - 1 : d])) for d in range(1, receiver.max_devices)]
activity_text = ', '.join(('%d=%d' % (d, a)) for d, a in activity if a > 0) activity_text = ", ".join(("%d=%d" % (d, a)) for d, a in activity if a > 0)
print(' Device activity counters:', activity_text or '(empty)') print(" Device activity counters:", activity_text or "(empty)")
def _battery_text(level): def _battery_text(level):
if level is None: if level is None:
return 'N/A' return "N/A"
elif isinstance(level, _NamedInt): elif isinstance(level, _NamedInt):
return str(level) return str(level)
else: else:
return '%d%%' % level return "%d%%" % level
def _battery_line(dev): def _battery_line(dev):
@ -75,11 +76,11 @@ def _battery_line(dev):
level, nextLevel, status, voltage = battery level, nextLevel, status, voltage = battery
text = _battery_text(level) text = _battery_text(level)
if voltage is not None: if voltage is not None:
text = text + (' %smV ' % voltage) text = text + (" %smV " % voltage)
nextText = '' if nextLevel is None else ', next level ' + _battery_text(nextLevel) nextText = "" if nextLevel is None else ", next level " + _battery_text(nextLevel)
print(' Battery: %s, %s%s.' % (text, status, nextText)) print(" Battery: %s, %s%s." % (text, status, nextText))
else: else:
print(' Battery status unavailable.') print(" Battery status unavailable.")
def _print_device(dev, num=None): def _print_device(dev, num=None):
@ -88,56 +89,56 @@ def _print_device(dev, num=None):
try: try:
dev.ping() dev.ping()
except exceptions.NoSuchDevice: except exceptions.NoSuchDevice:
print(' %s: Device not found' % num or dev.number) print(" %s: Device not found" % num or dev.number)
return return
if num or dev.number < 8: if num or dev.number < 8:
print(' %d: %s' % (num or dev.number, dev.name)) print(" %d: %s" % (num or dev.number, dev.name))
else: else:
print('%s' % dev.name) print("%s" % dev.name)
print(' Device path :', dev.path) print(" Device path :", dev.path)
if dev.wpid: if dev.wpid:
print(' WPID : %s' % dev.wpid) print(" WPID : %s" % dev.wpid)
if dev.product_id: if dev.product_id:
print(' USB id : 046d:%s' % dev.product_id) print(" USB id : 046d:%s" % dev.product_id)
print(' Codename :', dev.codename) print(" Codename :", dev.codename)
print(' Kind :', dev.kind) print(" Kind :", dev.kind)
if dev.protocol: if dev.protocol:
print(' Protocol : HID++ %1.1f' % dev.protocol) print(" Protocol : HID++ %1.1f" % dev.protocol)
else: else:
print(' Protocol : unknown (device is offline)') print(" Protocol : unknown (device is offline)")
if dev.polling_rate: if dev.polling_rate:
print(' Report Rate :', dev.polling_rate) print(" Report Rate :", dev.polling_rate)
print(' Serial number:', dev.serial) print(" Serial number:", dev.serial)
if dev.modelId: if dev.modelId:
print(' Model ID: ', dev.modelId) print(" Model ID: ", dev.modelId)
if dev.unitId: if dev.unitId:
print(' Unit ID: ', dev.unitId) print(" Unit ID: ", dev.unitId)
if dev.firmware: if dev.firmware:
for fw in dev.firmware: for fw in dev.firmware:
print(' %11s:' % fw.kind, (fw.name + ' ' + fw.version).strip()) print(" %11s:" % fw.kind, (fw.name + " " + fw.version).strip())
if dev.power_switch_location: if dev.power_switch_location:
print(' The power switch is located on the %s.' % dev.power_switch_location) print(" The power switch is located on the %s." % dev.power_switch_location)
if dev.online: if dev.online:
notification_flags = _hidpp10.get_notification_flags(dev) notification_flags = _hidpp10.get_notification_flags(dev)
if notification_flags is not None: if notification_flags is not None:
if notification_flags: if notification_flags:
notification_names = _hidpp10_constants.NOTIFICATION_FLAG.flag_names(notification_flags) notification_names = _hidpp10_constants.NOTIFICATION_FLAG.flag_names(notification_flags)
print(' Notifications: %s (0x%06X).' % (', '.join(notification_names), notification_flags)) print(" Notifications: %s (0x%06X)." % (", ".join(notification_names), notification_flags))
else: else:
print(' Notifications: (none).') print(" Notifications: (none).")
device_features = _hidpp10.get_device_features(dev) device_features = _hidpp10.get_device_features(dev)
if device_features is not None: if device_features is not None:
if device_features: if device_features:
device_features_names = _hidpp10_constants.DEVICE_FEATURES.flag_names(device_features) device_features_names = _hidpp10_constants.DEVICE_FEATURES.flag_names(device_features)
print(' Features: %s (0x%06X)' % (', '.join(device_features_names), device_features)) print(" Features: %s (0x%06X)" % (", ".join(device_features_names), device_features))
else: else:
print(' Features: (none)') print(" Features: (none)")
if dev.online and dev.features: if dev.online and dev.features:
print(' Supports %d HID++ 2.0 features:' % len(dev.features)) print(" Supports %d HID++ 2.0 features:" % len(dev.features))
dev_settings = [] dev_settings = []
_settings_templates.check_feature_settings(dev, dev_settings) _settings_templates.check_feature_settings(dev, dev_settings)
for feature, index in dev.features.enumerate(): for feature, index in dev.features.enumerate():
@ -146,172 +147,177 @@ def _print_device(dev, num=None):
flags = _hidpp20_constants.FEATURE_FLAG.flag_names(flags) flags = _hidpp20_constants.FEATURE_FLAG.flag_names(flags)
version = dev.features.get_feature_version(int(feature)) version = dev.features.get_feature_version(int(feature))
version = version if version else 0 version = version if version else 0
print(' %2d: %-22s {%04X} V%s %s ' % (index, feature, feature, version, ', '.join(flags))) print(" %2d: %-22s {%04X} V%s %s " % (index, feature, feature, version, ", ".join(flags)))
if feature == _hidpp20_constants.FEATURE.HIRES_WHEEL: if feature == _hidpp20_constants.FEATURE.HIRES_WHEEL:
wheel = _hidpp20.get_hires_wheel(dev) wheel = _hidpp20.get_hires_wheel(dev)
if wheel: if wheel:
multi, has_invert, has_switch, inv, res, target, ratchet = wheel multi, has_invert, has_switch, inv, res, target, ratchet = wheel
print(' Multiplier: %s' % multi) print(" Multiplier: %s" % multi)
if has_invert: if has_invert:
print(' Has invert:', 'Inverse wheel motion' if inv else 'Normal wheel motion') print(" Has invert:", "Inverse wheel motion" if inv else "Normal wheel motion")
if has_switch: if has_switch:
print(' Has ratchet switch:', 'Normal wheel mode' if ratchet else 'Free wheel mode') print(" Has ratchet switch:", "Normal wheel mode" if ratchet else "Free wheel mode")
if res: if res:
print(' High resolution mode') print(" High resolution mode")
else: else:
print(' Low resolution mode') print(" Low resolution mode")
if target: if target:
print(' HID++ notification') print(" HID++ notification")
else: else:
print(' HID notification') print(" HID notification")
elif feature == _hidpp20_constants.FEATURE.MOUSE_POINTER: elif feature == _hidpp20_constants.FEATURE.MOUSE_POINTER:
mouse_pointer = _hidpp20.get_mouse_pointer_info(dev) mouse_pointer = _hidpp20.get_mouse_pointer_info(dev)
if mouse_pointer: if mouse_pointer:
print(' DPI: %s' % mouse_pointer['dpi']) print(" DPI: %s" % mouse_pointer["dpi"])
print(' Acceleration: %s' % mouse_pointer['acceleration']) print(" Acceleration: %s" % mouse_pointer["acceleration"])
if mouse_pointer['suggest_os_ballistics']: if mouse_pointer["suggest_os_ballistics"]:
print(' Use OS ballistics') print(" Use OS ballistics")
else: else:
print(' Override OS ballistics') print(" Override OS ballistics")
if mouse_pointer['suggest_vertical_orientation']: if mouse_pointer["suggest_vertical_orientation"]:
print(' Provide vertical tuning, trackball') print(" Provide vertical tuning, trackball")
else: else:
print(' No vertical tuning, standard mice') print(" No vertical tuning, standard mice")
elif feature == _hidpp20_constants.FEATURE.VERTICAL_SCROLLING: elif feature == _hidpp20_constants.FEATURE.VERTICAL_SCROLLING:
vertical_scrolling_info = _hidpp20.get_vertical_scrolling_info(dev) vertical_scrolling_info = _hidpp20.get_vertical_scrolling_info(dev)
if vertical_scrolling_info: if vertical_scrolling_info:
print(' Roller type: %s' % vertical_scrolling_info['roller']) print(" Roller type: %s" % vertical_scrolling_info["roller"])
print(' Ratchet per turn: %s' % vertical_scrolling_info['ratchet']) print(" Ratchet per turn: %s" % vertical_scrolling_info["ratchet"])
print(' Scroll lines: %s' % vertical_scrolling_info['lines']) print(" Scroll lines: %s" % vertical_scrolling_info["lines"])
elif feature == _hidpp20_constants.FEATURE.HI_RES_SCROLLING: elif feature == _hidpp20_constants.FEATURE.HI_RES_SCROLLING:
scrolling_mode, scrolling_resolution = _hidpp20.get_hi_res_scrolling_info(dev) scrolling_mode, scrolling_resolution = _hidpp20.get_hi_res_scrolling_info(dev)
if scrolling_mode: if scrolling_mode:
print(' Hi-res scrolling enabled') print(" Hi-res scrolling enabled")
else: else:
print(' Hi-res scrolling disabled') print(" Hi-res scrolling disabled")
if scrolling_resolution: if scrolling_resolution:
print(' Hi-res scrolling multiplier: %s' % scrolling_resolution) print(" Hi-res scrolling multiplier: %s" % scrolling_resolution)
elif feature == _hidpp20_constants.FEATURE.POINTER_SPEED: elif feature == _hidpp20_constants.FEATURE.POINTER_SPEED:
pointer_speed = _hidpp20.get_pointer_speed_info(dev) pointer_speed = _hidpp20.get_pointer_speed_info(dev)
if pointer_speed: if pointer_speed:
print(' Pointer Speed: %s' % pointer_speed) print(" Pointer Speed: %s" % pointer_speed)
elif feature == _hidpp20_constants.FEATURE.LOWRES_WHEEL: elif feature == _hidpp20_constants.FEATURE.LOWRES_WHEEL:
wheel_status = _hidpp20.get_lowres_wheel_status(dev) wheel_status = _hidpp20.get_lowres_wheel_status(dev)
if wheel_status: if wheel_status:
print(' Wheel Reports: %s' % wheel_status) print(" Wheel Reports: %s" % wheel_status)
elif feature == _hidpp20_constants.FEATURE.NEW_FN_INVERSION: elif feature == _hidpp20_constants.FEATURE.NEW_FN_INVERSION:
inversion = _hidpp20.get_new_fn_inversion(dev) inversion = _hidpp20.get_new_fn_inversion(dev)
if inversion: if inversion:
inverted, default_inverted = inversion inverted, default_inverted = inversion
print(' Fn-swap:', 'enabled' if inverted else 'disabled') print(" Fn-swap:", "enabled" if inverted else "disabled")
print(' Fn-swap default:', 'enabled' if default_inverted else 'disabled') print(" Fn-swap default:", "enabled" if default_inverted else "disabled")
elif feature == _hidpp20_constants.FEATURE.HOSTS_INFO: elif feature == _hidpp20_constants.FEATURE.HOSTS_INFO:
host_names = _hidpp20.get_host_names(dev) host_names = _hidpp20.get_host_names(dev)
for host, (paired, name) in host_names.items(): for host, (paired, name) in host_names.items():
print(' Host %s (%s): %s' % (host, 'paired' if paired else 'unpaired', name)) print(" Host %s (%s): %s" % (host, "paired" if paired else "unpaired", name))
elif feature == _hidpp20_constants.FEATURE.DEVICE_NAME: elif feature == _hidpp20_constants.FEATURE.DEVICE_NAME:
print(' Name: %s' % _hidpp20.get_name(dev)) print(" Name: %s" % _hidpp20.get_name(dev))
print(' Kind: %s' % _hidpp20.get_kind(dev)) print(" Kind: %s" % _hidpp20.get_kind(dev))
elif feature == _hidpp20_constants.FEATURE.DEVICE_FRIENDLY_NAME: elif feature == _hidpp20_constants.FEATURE.DEVICE_FRIENDLY_NAME:
print(' Friendly Name: %s' % _hidpp20.get_friendly_name(dev)) print(" Friendly Name: %s" % _hidpp20.get_friendly_name(dev))
elif feature == _hidpp20_constants.FEATURE.DEVICE_FW_VERSION: elif feature == _hidpp20_constants.FEATURE.DEVICE_FW_VERSION:
for fw in _hidpp20.get_firmware(dev): for fw in _hidpp20.get_firmware(dev):
extras = _strhex(fw.extras) if fw.extras else '' extras = _strhex(fw.extras) if fw.extras else ""
print(' Firmware: %s %s %s %s' % (fw.kind, fw.name, fw.version, extras)) print(" Firmware: %s %s %s %s" % (fw.kind, fw.name, fw.version, extras))
ids = _hidpp20.get_ids(dev) ids = _hidpp20.get_ids(dev)
if ids: if ids:
unitId, modelId, tid_map = ids unitId, modelId, tid_map = ids
print(' Unit ID: %s Model ID: %s Transport IDs: %s' % (unitId, modelId, tid_map)) print(" Unit ID: %s Model ID: %s Transport IDs: %s" % (unitId, modelId, tid_map))
elif feature == _hidpp20_constants.FEATURE.REPORT_RATE or \ elif (
feature == _hidpp20_constants.FEATURE.EXTENDED_ADJUSTABLE_REPORT_RATE: feature == _hidpp20_constants.FEATURE.REPORT_RATE
print(' Report Rate: %s' % _hidpp20.get_polling_rate(dev)) or feature == _hidpp20_constants.FEATURE.EXTENDED_ADJUSTABLE_REPORT_RATE
):
print(" Report Rate: %s" % _hidpp20.get_polling_rate(dev))
elif feature == _hidpp20_constants.FEATURE.REMAINING_PAIRING: elif feature == _hidpp20_constants.FEATURE.REMAINING_PAIRING:
print(' Remaining Pairings: %d' % _hidpp20.get_remaining_pairing(dev)) print(" Remaining Pairings: %d" % _hidpp20.get_remaining_pairing(dev))
elif feature == _hidpp20_constants.FEATURE.ONBOARD_PROFILES: elif feature == _hidpp20_constants.FEATURE.ONBOARD_PROFILES:
if _hidpp20.get_onboard_mode(dev) == _hidpp20_constants.ONBOARD_MODES.MODE_HOST: if _hidpp20.get_onboard_mode(dev) == _hidpp20_constants.ONBOARD_MODES.MODE_HOST:
mode = 'Host' mode = "Host"
else: else:
mode = 'On-Board' mode = "On-Board"
print(' Device Mode: %s' % mode) print(" Device Mode: %s" % mode)
elif _hidpp20.battery_functions.get(feature, None): elif _hidpp20.battery_functions.get(feature, None):
print('', end=' ') print("", end=" ")
_battery_line(dev) _battery_line(dev)
for setting in dev_settings: for setting in dev_settings:
if setting.feature == feature: if setting.feature == feature:
if setting._device and getattr(setting._device, 'persister', None) and \ if (
setting._device.persister.get(setting.name) is not None: setting._device
and getattr(setting._device, "persister", None)
and setting._device.persister.get(setting.name) is not None
):
v = setting.val_to_string(setting._device.persister.get(setting.name)) v = setting.val_to_string(setting._device.persister.get(setting.name))
print(' %s (saved): %s' % (setting.label, v)) print(" %s (saved): %s" % (setting.label, v))
try: try:
v = setting.val_to_string(setting.read(False)) v = setting.val_to_string(setting.read(False))
except _hidpp20.FeatureCallError as e: except _hidpp20.FeatureCallError as e:
v = 'HID++ error ' + str(e) v = "HID++ error " + str(e)
except AssertionError as e: except AssertionError as e:
v = 'AssertionError ' + str(e) v = "AssertionError " + str(e)
print(' %s : %s' % (setting.label, v)) print(" %s : %s" % (setting.label, v))
if dev.online and dev.keys: if dev.online and dev.keys:
print(' Has %d reprogrammable keys:' % len(dev.keys)) print(" Has %d reprogrammable keys:" % len(dev.keys))
for k in dev.keys: for k in dev.keys:
# TODO: add here additional variants for other REPROG_CONTROLS # TODO: add here additional variants for other REPROG_CONTROLS
if dev.keys.keyversion == _hidpp20_constants.FEATURE.REPROG_CONTROLS_V2: if dev.keys.keyversion == _hidpp20_constants.FEATURE.REPROG_CONTROLS_V2:
print(' %2d: %-26s => %-27s %s' % (k.index, k.key, k.default_task, ', '.join(k.flags))) print(" %2d: %-26s => %-27s %s" % (k.index, k.key, k.default_task, ", ".join(k.flags)))
if dev.keys.keyversion == _hidpp20_constants.FEATURE.REPROG_CONTROLS_V4: if dev.keys.keyversion == _hidpp20_constants.FEATURE.REPROG_CONTROLS_V4:
print(' %2d: %-26s, default: %-27s => %-26s' % (k.index, k.key, k.default_task, k.mapped_to)) print(" %2d: %-26s, default: %-27s => %-26s" % (k.index, k.key, k.default_task, k.mapped_to))
gmask_fmt = ','.join(k.group_mask) gmask_fmt = ",".join(k.group_mask)
gmask_fmt = gmask_fmt if gmask_fmt else 'empty' gmask_fmt = gmask_fmt if gmask_fmt else "empty"
print(' %s, pos:%d, group:%1d, group mask:%s' % (', '.join(k.flags), k.pos, k.group, gmask_fmt)) print(" %s, pos:%d, group:%1d, group mask:%s" % (", ".join(k.flags), k.pos, k.group, gmask_fmt))
report_fmt = ', '.join(k.mapping_flags) report_fmt = ", ".join(k.mapping_flags)
report_fmt = report_fmt if report_fmt else 'default' report_fmt = report_fmt if report_fmt else "default"
print(' reporting: %s' % (report_fmt)) print(" reporting: %s" % (report_fmt))
if dev.online and dev.remap_keys: if dev.online and dev.remap_keys:
print(' Has %d persistent remappable keys:' % len(dev.remap_keys)) print(" Has %d persistent remappable keys:" % len(dev.remap_keys))
for k in dev.remap_keys: for k in dev.remap_keys:
print(' %2d: %-26s => %s%s' % (k.index, k.key, k.action, ' (remapped)' if k.cidStatus else '')) print(" %2d: %-26s => %s%s" % (k.index, k.key, k.action, " (remapped)" if k.cidStatus else ""))
if dev.online and dev.gestures: if dev.online and dev.gestures:
print( print(
' Has %d gesture(s), %d param(s) and %d spec(s):' % " Has %d gesture(s), %d param(s) and %d spec(s):"
(len(dev.gestures.gestures), len(dev.gestures.params), len(dev.gestures.specs)) % (len(dev.gestures.gestures), len(dev.gestures.params), len(dev.gestures.specs))
) )
for k in dev.gestures.gestures.values(): for k in dev.gestures.gestures.values():
print( print(
' %-26s Enabled(%4s): %-5s Diverted:(%4s) %s' % " %-26s Enabled(%4s): %-5s Diverted:(%4s) %s"
(k.gesture, k.index, k.enabled(), k.diversion_index, k.diverted()) % (k.gesture, k.index, k.enabled(), k.diversion_index, k.diverted())
) )
for k in dev.gestures.params.values(): for k in dev.gestures.params.values():
print(' %-26s Value (%4s): %s [Default: %s]' % (k.param, k.index, k.value, k.default_value)) print(" %-26s Value (%4s): %s [Default: %s]" % (k.param, k.index, k.value, k.default_value))
for k in dev.gestures.specs.values(): for k in dev.gestures.specs.values():
print(' %-26s Spec (%4s): %s' % (k.spec, k.id, k.value)) print(" %-26s Spec (%4s): %s" % (k.spec, k.id, k.value))
if dev.online: if dev.online:
_battery_line(dev) _battery_line(dev)
else: else:
print(' Battery: unknown (device is offline).') print(" Battery: unknown (device is offline).")
def run(devices, args, find_receiver, find_device): def run(devices, args, find_receiver, find_device):
assert devices assert devices
assert args.device assert args.device
print('%s version %s' % (NAME, __version__)) print("%s version %s" % (NAME, __version__))
print('') print("")
device_name = args.device.lower() device_name = args.device.lower()
if device_name == 'all': if device_name == "all":
for d in devices: for d in devices:
if isinstance(d, _receiver.Receiver): if isinstance(d, _receiver.Receiver):
_print_receiver(d) _print_receiver(d)
count = d.count() count = d.count()
if count: if count:
for dev in d: for dev in d:
print('') print("")
_print_device(dev, dev.number) _print_device(dev, dev.number)
count -= 1 count -= 1
if not count: if not count:
break break
print('') print("")
else: else:
print('') print("")
_print_device(d) _print_device(d)
return return

View File

@ -28,13 +28,13 @@ def run(receivers, args, find_receiver, find_device):
if not dev.receiver.may_unpair: if not dev.receiver.may_unpair:
print( print(
'Receiver with USB id %s for %s [%s:%s] does not unpair, but attempting anyway.' % "Receiver with USB id %s for %s [%s:%s] does not unpair, but attempting anyway."
(dev.receiver.product_id, dev.name, dev.wpid, dev.serial) % (dev.receiver.product_id, dev.name, dev.wpid, dev.serial)
) )
try: try:
# query these now, it's last chance to get them # query these now, it's last chance to get them
number, codename, wpid, serial = dev.number, dev.codename, dev.wpid, dev.serial number, codename, wpid, serial = dev.number, dev.codename, dev.wpid, dev.serial
dev.receiver._unpair_device(number, True) # force an unpair dev.receiver._unpair_device(number, True) # force an unpair
print('Unpaired %d: %s (%s) [%s:%s]' % (number, dev.name, codename, wpid, serial)) print("Unpaired %d: %s (%s) [%s:%s]" % (number, dev.name, codename, wpid, serial))
except Exception as e: except Exception as e:
raise e raise e

View File

@ -28,22 +28,23 @@ import yaml as _yaml
from gi.repository import GLib from gi.repository import GLib
from logitech_receiver.common import NamedInt as _NamedInt from logitech_receiver.common import NamedInt as _NamedInt
from solaar import __version__ from solaar import __version__
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
_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 _path.expanduser(_path.join("~", ".config"))
_yaml_file_path = _path.join(_XDG_CONFIG_HOME, 'solaar', 'config.yaml') _yaml_file_path = _path.join(_XDG_CONFIG_HOME, "solaar", "config.yaml")
_json_file_path = _path.join(_XDG_CONFIG_HOME, 'solaar', 'config.json') _json_file_path = _path.join(_XDG_CONFIG_HOME, "solaar", "config.json")
_KEY_VERSION = '_version' _KEY_VERSION = "_version"
_KEY_NAME = '_NAME' _KEY_NAME = "_NAME"
_KEY_WPID = '_wpid' _KEY_WPID = "_wpid"
_KEY_SERIAL = '_serial' _KEY_SERIAL = "_serial"
_KEY_MODEL_ID = '_modelId' _KEY_MODEL_ID = "_modelId"
_KEY_UNIT_ID = '_unitId' _KEY_UNIT_ID = "_unitId"
_KEY_ABSENT = '_absent' _KEY_ABSENT = "_absent"
_KEY_SENSITIVE = '_sensitive' _KEY_SENSITIVE = "_sensitive"
_config = [] _config = []
@ -55,19 +56,19 @@ def _load():
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 _path.isfile(_json_file_path): elif _path.isfile(_json_file_path):
path = _json_file_path path = _json_file_path
try: try:
with open(_json_file_path) as config_file: with open(_json_file_path) as config_file:
loaded_config = _json.load(config_file) loaded_config = _json.load(config_file)
except Exception as e: except Exception as e:
logger.error('failed to load from %s: %s', _json_file_path, e) logger.error("failed to load from %s: %s", _json_file_path, e)
loaded_config = _convert_json(loaded_config) loaded_config = _convert_json(loaded_config)
else: else:
path = None path = None
if logger.isEnabledFor(logging.DEBUG): if logger.isEnabledFor(logging.DEBUG):
logger.debug('load => %s', loaded_config) logger.debug("load => %s", loaded_config)
global _config global _config
_config = _parse_config(loaded_config, path) _config = _parse_config(loaded_config, path)
@ -84,41 +85,43 @@ def _parse_config(loaded_config, config_path):
if discard_derived_properties: if discard_derived_properties:
if logger.isEnabledFor(logging.INFO): if logger.isEnabledFor(logging.INFO):
logger.info( logger.info(
'config file \'%s\' was generated by another version of solaar ' "config file '%s' was generated by another version of solaar "
'(config: %s, current: %s). refreshing detected device capabilities', config_path, loaded_version, "(config: %s, current: %s). refreshing detected device capabilities",
current_version config_path,
loaded_version,
current_version,
) )
for device in loaded_config[1:]: for device in loaded_config[1:]:
assert isinstance(device, dict) assert isinstance(device, dict)
parsed_config.append(_device_entry_from_config_dict(device, discard_derived_properties)) parsed_config.append(_device_entry_from_config_dict(device, discard_derived_properties))
except Exception as e: except Exception as e:
logger.warning('Exception processing config file \'%s\', ignoring contents: %s', config_path, e) logger.warning("Exception processing config file '%s', ignoring contents: %s", config_path, e)
return parsed_config return parsed_config
def _device_entry_from_config_dict(data, discard_derived_properties): def _device_entry_from_config_dict(data, discard_derived_properties):
divert = data.get('divert-keys') divert = data.get("divert-keys")
if divert: if divert:
sliding = data.get('dpi-sliding') sliding = data.get("dpi-sliding")
if sliding: # convert old-style dpi-sliding setting to divert-keys entry if sliding: # convert old-style dpi-sliding setting to divert-keys entry
divert[int(sliding)] = 3 divert[int(sliding)] = 3
data.pop('dpi-sliding', None) data.pop("dpi-sliding", None)
gestures = data.get('mouse-gestures') gestures = data.get("mouse-gestures")
if gestures: # convert old-style mouse-gestures setting to divert-keys entry if gestures: # convert old-style mouse-gestures setting to divert-keys entry
divert[int(gestures)] = 2 divert[int(gestures)] = 2
data.pop('mouse-gestures', None) data.pop("mouse-gestures", None)
# remove any string entries (from bad conversions) # remove any string entries (from bad conversions)
data['divert-keys'] = {k: v for k, v in divert.items() if isinstance(k, int)} data["divert-keys"] = {k: v for k, v in divert.items() if isinstance(k, int)}
if data.get('_sensitive', None) is None: # make scroll wheel settings default to ignore if data.get("_sensitive", None) is None: # make scroll wheel settings default to ignore
data['_sensitive'] = { data["_sensitive"] = {
'hires-smooth-resolution': 'ignore', "hires-smooth-resolution": "ignore",
'hires-smooth-invert': 'ignore', "hires-smooth-invert": "ignore",
'hires-scroll-mode': 'ignore' "hires-scroll-mode": "ignore",
} }
if discard_derived_properties: if discard_derived_properties:
data.pop('_absent', None) data.pop("_absent", None)
data.pop('_battery', None) data.pop("_battery", None)
return _DeviceEntry(**data) return _DeviceEntry(**data)
@ -136,7 +139,7 @@ def save(defer=False):
try: try:
_os.makedirs(dirname) _os.makedirs(dirname)
except Exception: except Exception:
logger.error('failed to create %s', dirname) logger.error("failed to create %s", dirname)
return return
if not defer or not defer_saves: if not defer or not defer_saves:
do_save() do_save()
@ -154,38 +157,37 @@ def do_save():
save_timer.cancel() save_timer.cancel()
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:
logger.error('failed to save to %s: %s', _yaml_file_path, e) logger.error("failed to save to %s: %s", _yaml_file_path, e)
def _convert_json(json_dict): def _convert_json(json_dict):
config = [json_dict.get(_KEY_VERSION)] config = [json_dict.get(_KEY_VERSION)]
for key, dev in json_dict.items(): for key, dev in json_dict.items():
key = key.split(':') key = key.split(":")
if len(key) == 2: if len(key) == 2:
dev[_KEY_WPID] = dev.get(_KEY_WPID) if dev.get(_KEY_WPID) else key[0] dev[_KEY_WPID] = dev.get(_KEY_WPID) if dev.get(_KEY_WPID) else key[0]
dev[_KEY_SERIAL] = dev.get(_KEY_SERIAL) if dev.get(_KEY_SERIAL) else key[1] dev[_KEY_SERIAL] = dev.get(_KEY_SERIAL) if dev.get(_KEY_SERIAL) else key[1]
for k, v in dev.items(): for k, v in dev.items():
if type(k) == str and not k.startswith('_') and type(v) == dict: # convert string keys to ints if type(k) == str and not k.startswith("_") and type(v) == dict: # convert string keys to ints
v = {int(dk) if type(dk) == str else dk: dv for dk, dv in v.items()} v = {int(dk) if type(dk) == str else dk: dv for dk, dv in v.items()}
dev[k] = v dev[k] = v
for k in ['mouse-gestures', 'dpi-sliding']: for k in ["mouse-gestures", "dpi-sliding"]:
v = dev.get(k, None) v = dev.get(k, None)
if v is True or v is False: if v is True or v is False:
dev.pop(k) dev.pop(k)
if '_name' in dev: if "_name" in dev:
dev[_KEY_NAME] = dev['_name'] dev[_KEY_NAME] = dev["_name"]
dev.pop('_name') dev.pop("_name")
config.append(dev) config.append(dev)
return config return config
class _DeviceEntry(dict): class _DeviceEntry(dict):
def __init__(self, **kwargs): def __init__(self, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
@ -198,7 +200,7 @@ class _DeviceEntry(dict):
super().__setitem__(_KEY_NAME, device.name) super().__setitem__(_KEY_NAME, device.name)
if device.wpid and device.wpid != self.get(_KEY_WPID): if device.wpid and device.wpid != self.get(_KEY_WPID):
super().__setitem__(_KEY_WPID, device.wpid) super().__setitem__(_KEY_WPID, device.wpid)
if device.serial and device.serial != '?' and device.serial != self.get(_KEY_SERIAL): if device.serial and device.serial != "?" and device.serial != self.get(_KEY_SERIAL):
super().__setitem__(_KEY_SERIAL, device.serial) super().__setitem__(_KEY_SERIAL, device.serial)
if modelId and modelId != self.get(_KEY_MODEL_ID): if modelId and modelId != self.get(_KEY_MODEL_ID):
super().__setitem__(_KEY_MODEL_ID, modelId) super().__setitem__(_KEY_MODEL_ID, modelId)
@ -216,14 +218,14 @@ class _DeviceEntry(dict):
def device_representer(dumper, data): 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)
@ -236,17 +238,19 @@ _yaml.add_representer(_NamedInt, named_int_representer)
# that is directly connected. Here there is no way to realize that the two devices are the same. # that is directly connected. Here there is no way to realize that the two devices are the same.
# So new entries are not created for unseen off-line receiver-connected devices except for those with protocol 1.0 # So new entries are not created for unseen off-line receiver-connected devices except for those with protocol 1.0
def persister(device): def persister(device):
def match(wpid, serial, modelId, unitId, c): def match(wpid, serial, modelId, unitId, c):
return ((wpid and wpid == c.get(_KEY_WPID) and serial and serial == c.get(_KEY_SERIAL)) or ( return (wpid and wpid == c.get(_KEY_WPID) and serial and serial == c.get(_KEY_SERIAL)) or (
modelId and modelId != '000000000000' and modelId == c.get(_KEY_MODEL_ID) and unitId modelId
and modelId != "000000000000"
and modelId == c.get(_KEY_MODEL_ID)
and unitId
and unitId == c.get(_KEY_UNIT_ID) and unitId == c.get(_KEY_UNIT_ID)
)) )
if not _config: if not _config:
_load() _load()
entry = None entry = None
modelId = device.modelId if device.modelId != '000000000000' else device.name if device.modelId else None modelId = device.modelId if device.modelId != "000000000000" else device.name if device.modelId else None
for c in _config: for c in _config:
if isinstance(c, _DeviceEntry) and match(device.wpid, device.serial, modelId, device.unitId, c): if isinstance(c, _DeviceEntry) and match(device.wpid, device.serial, modelId, device.unitId, c):
entry = c entry = c
@ -254,10 +258,10 @@ def persister(device):
if not entry: if not entry:
if not device.online and not device.serial: # don't create entry for offline devices without serial number if not device.online and not device.serial: # don't create entry for offline devices without serial number
if logger.isEnabledFor(logging.INFO): if logger.isEnabledFor(logging.INFO):
logger.info('not setting up persister for offline device %s with missing serial number', device.name) logger.info("not setting up persister for offline device %s with missing serial number", device.name)
return return
if logger.isEnabledFor(logging.INFO): if logger.isEnabledFor(logging.INFO):
logger.info('setting up persister for device %s', device.name) logger.info("setting up persister for device %s", device.name)
entry = _DeviceEntry() entry = _DeviceEntry()
_config.append(entry) _config.append(entry)
entry.update(device, modelId) entry.update(device, modelId)

View File

@ -53,46 +53,46 @@ def _require(module, os_package, gi=None, gi_package=None, gi_version=None):
gi.require_version(gi_package, gi_version) gi.require_version(gi_package, gi_version)
return importlib.import_module(module) return importlib.import_module(module)
except (ImportError, ValueError): except (ImportError, ValueError):
sys.exit('%s: missing required system package %s' % (NAME, os_package)) sys.exit("%s: missing required system package %s" % (NAME, os_package))
battery_icons_style = 'regular' battery_icons_style = "regular"
temp = tempfile.NamedTemporaryFile(prefix='Solaar_', mode='w', delete=True) temp = tempfile.NamedTemporaryFile(prefix="Solaar_", mode="w", delete=True)
def _parse_arguments(): def _parse_arguments():
arg_parser = argparse.ArgumentParser( arg_parser = argparse.ArgumentParser(
prog=NAME.lower(), epilog='For more information see https://pwr-solaar.github.io/Solaar' prog=NAME.lower(), epilog="For more information see https://pwr-solaar.github.io/Solaar"
) )
arg_parser.add_argument( arg_parser.add_argument(
'-d', "-d",
'--debug', "--debug",
action='count', action="count",
default=0, default=0,
help='print logging messages, for debugging purposes (may be repeated for extra verbosity)' help="print logging messages, for debugging purposes (may be repeated for extra verbosity)",
) )
arg_parser.add_argument( arg_parser.add_argument(
'-D', "-D",
'--hidraw', "--hidraw",
action='store', action="store",
dest='hidraw_path', dest="hidraw_path",
metavar='PATH', metavar="PATH",
help='unifying receiver to use; the first detected receiver if unspecified. Example: /dev/hidraw2' help="unifying receiver to use; the first detected receiver if unspecified. Example: /dev/hidraw2",
) )
arg_parser.add_argument('--restart-on-wake-up', action='store_true', help='restart Solaar on sleep wake-up (experimental)') arg_parser.add_argument("--restart-on-wake-up", action="store_true", help="restart Solaar on sleep wake-up (experimental)")
arg_parser.add_argument( arg_parser.add_argument(
'-w', '--window', choices=('show', 'hide', 'only'), help='start with window showing / hidden / only (no tray icon)' "-w", "--window", choices=("show", "hide", "only"), help="start with window showing / hidden / only (no tray icon)"
) )
arg_parser.add_argument( arg_parser.add_argument(
'-b', "-b",
'--battery-icons', "--battery-icons",
choices=('regular', 'symbolic', 'solaar'), choices=("regular", "symbolic", "solaar"),
help='prefer regular battery / symbolic battery / solaar icons' help="prefer regular battery / symbolic battery / solaar icons",
) )
arg_parser.add_argument('--tray-icon-size', type=int, help='explicit size for tray icons') arg_parser.add_argument("--tray-icon-size", type=int, help="explicit size for tray icons")
arg_parser.add_argument('-V', '--version', action='version', version='%(prog)s ' + __version__) arg_parser.add_argument("-V", "--version", action="version", version="%(prog)s " + __version__)
arg_parser.add_argument('--help-actions', action='store_true', help='print help for the optional actions') arg_parser.add_argument("--help-actions", action="store_true", help="print help for the optional actions")
arg_parser.add_argument('action', nargs=argparse.REMAINDER, choices=_cli.actions, help='optional actions to perform') arg_parser.add_argument("action", nargs=argparse.REMAINDER, choices=_cli.actions, help="optional actions to perform")
args = arg_parser.parse_args() args = arg_parser.parse_args()
@ -101,29 +101,29 @@ def _parse_arguments():
return return
if args.window is None: if args.window is None:
args.window = 'show' # default behaviour is to show main window args.window = "show" # default behaviour is to show main window
global battery_icons_style global battery_icons_style
battery_icons_style = args.battery_icons if args.battery_icons is not None else 'regular' battery_icons_style = args.battery_icons if args.battery_icons is not None else "regular"
global tray_icon_size global tray_icon_size
tray_icon_size = args.tray_icon_size tray_icon_size = args.tray_icon_size
log_format = '%(asctime)s,%(msecs)03d %(levelname)8s [%(threadName)s] %(name)s: %(message)s' log_format = "%(asctime)s,%(msecs)03d %(levelname)8s [%(threadName)s] %(name)s: %(message)s"
log_level = logging.ERROR - 10 * args.debug log_level = logging.ERROR - 10 * args.debug
logging.getLogger('').setLevel(min(log_level, logging.WARNING)) logging.getLogger("").setLevel(min(log_level, logging.WARNING))
file_handler = logging.StreamHandler(temp) file_handler = logging.StreamHandler(temp)
file_handler.setLevel(max(min(log_level, logging.WARNING), logging.INFO)) file_handler.setLevel(max(min(log_level, logging.WARNING), logging.INFO))
file_handler.setFormatter(logging.Formatter(log_format)) file_handler.setFormatter(logging.Formatter(log_format))
logging.getLogger('').addHandler(file_handler) logging.getLogger("").addHandler(file_handler)
if args.debug > 0: if args.debug > 0:
stream_handler = logging.StreamHandler() stream_handler = logging.StreamHandler()
stream_handler.setFormatter(logging.Formatter(log_format)) stream_handler.setFormatter(logging.Formatter(log_format))
stream_handler.setLevel(log_level) stream_handler.setLevel(log_level)
logging.getLogger('').addHandler(stream_handler) logging.getLogger("").addHandler(stream_handler)
if not args.action: if not args.action:
if logger.isEnabledFor(logging.INFO): if logger.isEnabledFor(logging.INFO):
logger.info('version %s, language %s (%s)', __version__, _i18n.language, _i18n.encoding) logger.info("version %s, language %s (%s)", __version__, _i18n.language, _i18n.encoding)
return args return args
@ -136,14 +136,14 @@ def _handlesig(signl, stack):
if signl == int(signal.SIGINT): if signl == int(signal.SIGINT):
if logger.isEnabledFor(logging.INFO): if logger.isEnabledFor(logging.INFO):
faulthandler.dump_traceback() faulthandler.dump_traceback()
sys.exit('%s: exit due to keyboard interrupt' % (NAME.lower())) sys.exit("%s: exit due to keyboard interrupt" % (NAME.lower()))
else: else:
sys.exit(0) sys.exit(0)
def main(): def main():
if platform.system() not in ('Darwin', 'Windows'): if platform.system() not in ("Darwin", "Windows"):
_require('pyudev', 'python3-pyudev') _require("pyudev", "python3-pyudev")
args = _parse_arguments() args = _parse_arguments()
if not args: if not args:
@ -152,21 +152,23 @@ def main():
# if any argument, run comandline and exit # if any argument, run comandline and exit
return _cli.run(args.action, args.hidraw_path) return _cli.run(args.action, args.hidraw_path)
gi = _require('gi', 'python3-gi (in Ubuntu) or python3-gobject (in Fedora)') gi = _require("gi", "python3-gi (in Ubuntu) or python3-gobject (in Fedora)")
_require('gi.repository.Gtk', 'gir1.2-gtk-3.0', gi, 'Gtk', '3.0') _require("gi.repository.Gtk", "gir1.2-gtk-3.0", gi, "Gtk", "3.0")
# handle ^C in console # handle ^C in console
signal.signal(signal.SIGINT, signal.SIG_DFL) signal.signal(signal.SIGINT, signal.SIG_DFL)
signal.signal(signal.SIGINT, _handlesig) signal.signal(signal.SIGINT, _handlesig)
signal.signal(signal.SIGTERM, _handlesig) signal.signal(signal.SIGTERM, _handlesig)
udev_file = '42-logitech-unify-permissions.rules' udev_file = "42-logitech-unify-permissions.rules"
if logger.isEnabledFor(logging.WARNING) \ if (
and not os.path.isfile('/etc/udev/rules.d/' + udev_file) \ logger.isEnabledFor(logging.WARNING)
and not os.path.isfile('/usr/lib/udev/rules.d/' + udev_file) \ and not os.path.isfile("/etc/udev/rules.d/" + udev_file)
and not os.path.isfile('/usr/local/lib/udev/rules.d/' + udev_file): and not os.path.isfile("/usr/lib/udev/rules.d/" + udev_file)
logger.warning('Solaar udev file not found in expected location') and not os.path.isfile("/usr/local/lib/udev/rules.d/" + udev_file)
logger.warning('See https://pwr-solaar.github.io/Solaar/installation for more information') ):
logger.warning("Solaar udev file not found in expected location")
logger.warning("See https://pwr-solaar.github.io/Solaar/installation for more information")
try: try:
_listener.setup_scanner(_ui.status_changed, _ui.setting_changed, _common.error_dialog) _listener.setup_scanner(_ui.status_changed, _ui.setting_changed, _common.error_dialog)
@ -178,12 +180,12 @@ def main():
_configuration.defer_saves = True # allow configuration saves to be deferred _configuration.defer_saves = True # allow configuration saves to be deferred
# main UI event loop # main UI event loop
_ui.run_loop(_listener.start_all, _listener.stop_all, args.window != 'only', args.window != 'hide') _ui.run_loop(_listener.start_all, _listener.stop_all, args.window != "only", args.window != "hide")
except Exception: except Exception:
sys.exit('%s: error: %s' % (NAME.lower(), format_exc())) sys.exit("%s: error: %s" % (NAME.lower(), format_exc()))
temp.close() temp.close()
if __name__ == '__main__': if __name__ == "__main__":
main() main()

View File

@ -31,19 +31,19 @@ from solaar import NAME as _NAME
def _find_locale_path(lc_domain): def _find_locale_path(lc_domain):
prefix_share = _path.normpath(_path.join(_path.realpath(_sys.path[0]), '..')) prefix_share = _path.normpath(_path.join(_path.realpath(_sys.path[0]), ".."))
src_share = _path.normpath(_path.join(_path.realpath(_sys.path[0]), '..', 'share')) src_share = _path.normpath(_path.join(_path.realpath(_sys.path[0]), "..", "share"))
for location in prefix_share, src_share: for location in prefix_share, src_share:
mo_files = _glob(_path.join(location, 'locale', '*', 'LC_MESSAGES', lc_domain + '.mo')) mo_files = _glob(_path.join(location, "locale", "*", "LC_MESSAGES", lc_domain + ".mo"))
if mo_files: if mo_files:
return _path.join(location, 'locale') return _path.join(location, "locale")
# del _path # del _path
try: try:
locale.setlocale(locale.LC_ALL, '') locale.setlocale(locale.LC_ALL, "")
except Exception: except Exception:
pass pass

View File

@ -36,7 +36,7 @@ from logitech_receiver import status as _status
from . import configuration from . import configuration
gi.require_version('Gtk', '3.0') # NOQA: E402 gi.require_version("Gtk", "3.0") # NOQA: E402
from gi.repository import GLib # NOQA: E402 # isort:skip from gi.repository import GLib # NOQA: E402 # isort:skip
# from solaar.i18n import _ # from solaar.i18n import _
@ -50,7 +50,7 @@ _IR = _hidpp10_constants.INFO_SUBREGISTERS
# #
# #
_GHOST_DEVICE = namedtuple('_GHOST_DEVICE', ('receiver', 'number', 'name', 'kind', 'status', 'online')) _GHOST_DEVICE = namedtuple("_GHOST_DEVICE", ("receiver", "number", "name", "kind", "status", "online"))
_GHOST_DEVICE.__bool__ = lambda self: False _GHOST_DEVICE.__bool__ = lambda self: False
_GHOST_DEVICE.__nonzero__ = _GHOST_DEVICE.__bool__ _GHOST_DEVICE.__nonzero__ = _GHOST_DEVICE.__bool__
del namedtuple del namedtuple
@ -72,8 +72,7 @@ def _ghost(device):
class ReceiverListener(_listener.EventsListener): class ReceiverListener(_listener.EventsListener):
"""Keeps the status of a Receiver. """Keeps the status of a Receiver."""
"""
def __init__(self, receiver, status_changed_callback): def __init__(self, receiver, status_changed_callback):
super().__init__(receiver, self._notifications_handler) super().__init__(receiver, self._notifications_handler)
@ -87,13 +86,13 @@ class ReceiverListener(_listener.EventsListener):
def has_started(self): def has_started(self):
if logger.isEnabledFor(logging.INFO): if logger.isEnabledFor(logging.INFO):
logger.info('%s: notifications listener has started (%s)', self.receiver, self.receiver.handle) logger.info("%s: notifications listener has started (%s)", self.receiver, self.receiver.handle)
nfs = self.receiver.enable_connection_notifications() nfs = self.receiver.enable_connection_notifications()
if logger.isEnabledFor(logging.WARNING): if logger.isEnabledFor(logging.WARNING):
if not self.receiver.isDevice and not ((nfs if nfs else 0) & _hidpp10_constants.NOTIFICATION_FLAG.wireless): if not self.receiver.isDevice and not ((nfs if nfs else 0) & _hidpp10_constants.NOTIFICATION_FLAG.wireless):
logger.warning( logger.warning(
'Receiver on %s might not support connection notifications, GUI might not show its devices', "Receiver on %s might not support connection notifications, GUI might not show its devices",
self.receiver.path self.receiver.path,
) )
self.receiver.status[_status.KEYS.NOTIFICATION_FLAGS] = nfs self.receiver.status[_status.KEYS.NOTIFICATION_FLAGS] = nfs
self.receiver.notify_devices() self.receiver.notify_devices()
@ -103,7 +102,7 @@ class ReceiverListener(_listener.EventsListener):
r, self.receiver = self.receiver, None r, self.receiver = self.receiver, None
assert r is not None assert r is not None
if logger.isEnabledFor(logging.INFO): if logger.isEnabledFor(logging.INFO):
logger.info('%s: notifications listener has stopped', r) logger.info("%s: notifications listener has stopped", r)
# because udev is not notifying us about device removal, # because udev is not notifying us about device removal,
# make sure to clean up in _all_listeners # make sure to clean up in _all_listeners
@ -114,7 +113,7 @@ class ReceiverListener(_listener.EventsListener):
try: try:
r.close() r.close()
except Exception: except Exception:
logger.exception('closing receiver %s' % r.path) logger.exception("closing receiver %s" % r.path)
self.status_changed_callback(r) # , _status.ALERT.NOTIFICATION) self.status_changed_callback(r) # , _status.ALERT.NOTIFICATION)
# def tick(self, timestamp): # def tick(self, timestamp):
@ -161,16 +160,25 @@ class ReceiverListener(_listener.EventsListener):
device.ping() device.ping()
if device.kind is None: if device.kind is None:
logger.info( logger.info(
'status_changed %r: %s, %s (%X) %s', device, 'present' if bool(device) else 'removed', device.status, "status_changed %r: %s, %s (%X) %s",
alert, reason or '' device,
"present" if bool(device) else "removed",
device.status,
alert,
reason or "",
) )
else: else:
logger.info( logger.info(
'status_changed %r: %s %s, %s (%X) %s', device, 'paired' if bool(device) else 'unpaired', "status_changed %r: %s %s, %s (%X) %s",
'online' if device.online else 'offline', device.status, alert, reason or '' device,
"paired" if bool(device) else "unpaired",
"online" if device.online else "offline",
device.status,
alert,
reason or "",
) )
except Exception: except Exception:
logger.info('status_changed for unknown device') logger.info("status_changed for unknown device")
if device.kind is None: if device.kind is None:
assert device == self.receiver assert device == self.receiver
@ -184,7 +192,7 @@ class ReceiverListener(_listener.EventsListener):
# We replace it with a ghost so that the UI has something to work # We replace it with a ghost so that the UI has something to work
# with while cleaning up. # with while cleaning up.
if logger.isEnabledFor(logging.INFO): if logger.isEnabledFor(logging.INFO):
logger.info('device %s was unpaired, ghosting', device) logger.info("device %s was unpaired, ghosting", device)
device = _ghost(device) device = _ghost(device)
self.status_changed_callback(device, alert, reason) self.status_changed_callback(device, alert, reason)
@ -206,19 +214,19 @@ class ReceiverListener(_listener.EventsListener):
# a notification that came in to the device listener - strange, but nothing needs to be done here # a notification that came in to the device listener - strange, but nothing needs to be done here
if self.receiver.isDevice: if self.receiver.isDevice:
if logger.isEnabledFor(logging.DEBUG): if logger.isEnabledFor(logging.DEBUG):
logger.debug('Notification %s via device %s being ignored.', n, self.receiver) logger.debug("Notification %s via device %s being ignored.", n, self.receiver)
return return
# DJ pairing notification - ignore - hid++ 1.0 pairing notification is all that is needed # DJ pairing notification - ignore - hid++ 1.0 pairing notification is all that is needed
if n.sub_id == 0x41 and n.report_id == _base.DJ_MESSAGE_ID: if n.sub_id == 0x41 and n.report_id == _base.DJ_MESSAGE_ID:
if logger.isEnabledFor(logging.INFO): if logger.isEnabledFor(logging.INFO):
logger.info('ignoring DJ pairing notification %s', n) logger.info("ignoring DJ pairing notification %s", n)
return return
# a device notification # a device notification
if not (0 < n.devnumber <= 16): # some receivers have devices past their max # devices if not (0 < n.devnumber <= 16): # some receivers have devices past their max # devices
if logger.isEnabledFor(logging.WARNING): if logger.isEnabledFor(logging.WARNING):
logger.warning('Unexpected device number (%s) in notification %s.', n.devnumber, n) logger.warning("Unexpected device number (%s) in notification %s.", n.devnumber, n)
return return
already_known = n.devnumber in self.receiver already_known = n.devnumber in self.receiver
@ -237,7 +245,7 @@ class ReceiverListener(_listener.EventsListener):
if n.sub_id == 0x41: if n.sub_id == 0x41:
if not already_known: if not already_known:
if n.address == 0x0A and not self.receiver.receiver_kind == 'bolt': if n.address == 0x0A and not self.receiver.receiver_kind == "bolt":
# some Nanos send a notification even if no new pairing - check that there really is a device there # some Nanos send a notification even if no new pairing - check that there really is a device there
if self.receiver.read_register(_R.receiver_info, _IR.pairing_information + n.devnumber - 1) is None: if self.receiver.read_register(_R.receiver_info, _IR.pairing_information + n.devnumber - 1) is None:
return return
@ -254,7 +262,7 @@ class ReceiverListener(_listener.EventsListener):
dev = self.receiver[n.devnumber] dev = self.receiver[n.devnumber]
if not dev: if not dev:
logger.warning('%s: received %s for invalid device %d: %r', self.receiver, n, n.devnumber, dev) logger.warning("%s: received %s for invalid device %d: %r", self.receiver, n, n.devnumber, dev)
return return
# Apply settings every time the device connects # Apply settings every time the device connects
@ -262,9 +270,9 @@ class ReceiverListener(_listener.EventsListener):
if logger.isEnabledFor(logging.INFO): if logger.isEnabledFor(logging.INFO):
try: try:
dev.ping() dev.ping()
logger.info('connection %s for %r', n, dev) logger.info("connection %s for %r", n, dev)
except Exception: except Exception:
logger.info('connection %s for unknown device, number %s', n, n.devnumber) logger.info("connection %s for unknown device, number %s", n, n.devnumber)
# If there are saved configs, bring the device's settings up-to-date. # If there are saved configs, bring the device's settings up-to-date.
# They will be applied when the device is marked as online. # They will be applied when the device is marked as online.
configuration.attach_to(dev) configuration.attach_to(dev)
@ -272,23 +280,23 @@ class ReceiverListener(_listener.EventsListener):
# the receiver changed status as well # the receiver changed status as well
self._status_changed(self.receiver) self._status_changed(self.receiver)
if not hasattr(dev, 'status') or dev.status is None: if not hasattr(dev, "status") or dev.status is None:
# notification before device status set up - don't process it # notification before device status set up - don't process it
logger.warning('%s before device %s has status', n, dev) logger.warning("%s before device %s has status", n, dev)
else: else:
_notifications.process(dev, n) _notifications.process(dev, n)
if self.receiver.status.lock_open and not already_known: if self.receiver.status.lock_open and not already_known:
# this should be the first notification after a device was paired # this should be the first notification after a device was paired
assert n.sub_id == 0x41, 'first notification was not a connection notification' assert n.sub_id == 0x41, "first notification was not a connection notification"
if logger.isEnabledFor(logging.INFO): if logger.isEnabledFor(logging.INFO):
logger.info('%s: pairing detected new device', self.receiver) logger.info("%s: pairing detected new device", self.receiver)
self.receiver.status.new_device = dev self.receiver.status.new_device = dev
elif dev.online is None: elif dev.online is None:
dev.ping() dev.ping()
def __str__(self): def __str__(self):
return '<ReceiverListener(%s,%s)>' % (self.receiver.path, self.receiver.handle) return "<ReceiverListener(%s,%s)>" % (self.receiver.path, self.receiver.handle)
# #
@ -315,7 +323,7 @@ def _start(device_info):
_all_listeners[device_info.path] = rl _all_listeners[device_info.path] = rl
return rl return rl
logger.warning('failed to open %s', device_info) logger.warning("failed to open %s", device_info)
def start_all(): def start_all():
@ -323,9 +331,9 @@ def start_all():
stop_all() stop_all()
if logger.isEnabledFor(logging.INFO): if logger.isEnabledFor(logging.INFO):
logger.info('starting receiver listening threads') logger.info("starting receiver listening threads")
for device_info in _base.receivers_and_devices(): for device_info in _base.receivers_and_devices():
_process_receiver_event('add', device_info) _process_receiver_event("add", device_info)
def stop_all(): def stop_all():
@ -334,7 +342,7 @@ def stop_all():
if listeners: if listeners:
if logger.isEnabledFor(logging.INFO): if logger.isEnabledFor(logging.INFO):
logger.info('stopping receiver listening threads %s', listeners) logger.info("stopping receiver listening threads %s", listeners)
for l in listeners: for l in listeners:
l.stop() l.stop()
@ -351,10 +359,10 @@ def stop_all():
# so mark its saved status to ensure that the status is pushed to the device when it comes back # so mark its saved status to ensure that the status is pushed to the device when it comes back
def ping_all(resuming=False): def ping_all(resuming=False):
if logger.isEnabledFor(logging.INFO): if logger.isEnabledFor(logging.INFO):
logger.info('ping all devices%s', ' when resuming' if resuming else '') logger.info("ping all devices%s", " when resuming" if resuming else "")
for l in _all_listeners.values(): for l in _all_listeners.values():
if l.receiver.isDevice: if l.receiver.isDevice:
if resuming and hasattr(l.receiver, 'status'): if resuming and hasattr(l.receiver, "status"):
l.receiver.status._active = None # ensure that settings are pushed l.receiver.status._active = None # ensure that settings are pushed
if l.receiver.ping(): if l.receiver.ping():
l.receiver.status.changed(active=True, push=True) l.receiver.status.changed(active=True, push=True)
@ -363,7 +371,7 @@ def ping_all(resuming=False):
count = l.receiver.count() count = l.receiver.count()
if count: if count:
for dev in l.receiver: for dev in l.receiver:
if resuming and hasattr(dev, 'status'): if resuming and hasattr(dev, "status"):
dev.status._active = None # ensure that settings are pushed dev.status._active = None # ensure that settings are pushed
if dev.ping(): if dev.ping():
dev.status.changed(active=True, push=True) dev.status.changed(active=True, push=True)
@ -380,7 +388,7 @@ _error_callback = None
def setup_scanner(status_changed_callback, setting_changed_callback, error_callback): def setup_scanner(status_changed_callback, setting_changed_callback, error_callback):
global _status_callback, _error_callback, _setting_callback global _status_callback, _error_callback, _setting_callback
assert _status_callback is None, 'scanner was already set-up' assert _status_callback is None, "scanner was already set-up"
_status_callback = status_changed_callback _status_callback = status_changed_callback
_setting_callback = setting_changed_callback _setting_callback = setting_changed_callback
@ -395,19 +403,19 @@ def _process_add(device_info, retry):
except OSError as e: except OSError as e:
if e.errno == _errno.EACCES: if e.errno == _errno.EACCES:
try: try:
output = subprocess.check_output(['/usr/bin/getfacl', '-p', device_info.path], text=True) output = subprocess.check_output(["/usr/bin/getfacl", "-p", device_info.path], text=True)
if logger.isEnabledFor(logging.WARNING): if logger.isEnabledFor(logging.WARNING):
logger.warning('Missing permissions on %s\n%s.', device_info.path, output) logger.warning("Missing permissions on %s\n%s.", device_info.path, output)
except Exception: except Exception:
pass pass
if retry: if retry:
GLib.timeout_add(2000.0, _process_add, device_info, retry - 1) GLib.timeout_add(2000.0, _process_add, device_info, retry - 1)
else: else:
_error_callback('permissions', device_info.path) _error_callback("permissions", device_info.path)
else: else:
_error_callback('nodevice', device_info.path) _error_callback("nodevice", device_info.path)
except exceptions.NoReceiver: except exceptions.NoReceiver:
_error_callback('nodevice', device_info.path) _error_callback("nodevice", device_info.path)
# receiver add/remove events will start/stop listener threads # receiver add/remove events will start/stop listener threads
@ -417,7 +425,7 @@ def _process_receiver_event(action, device_info):
assert _error_callback assert _error_callback
if logger.isEnabledFor(logging.INFO): if logger.isEnabledFor(logging.INFO):
logger.info('receiver event %s %s', action, device_info) logger.info("receiver event %s %s", action, device_info)
# whatever the action, stop any previous receivers at this path # whatever the action, stop any previous receivers at this path
l = _all_listeners.pop(device_info.path, None) l = _all_listeners.pop(device_info.path, None)
@ -425,7 +433,7 @@ def _process_receiver_event(action, device_info):
assert isinstance(l, ReceiverListener) assert isinstance(l, ReceiverListener)
l.stop() l.stop()
if action == 'add': # a new device was detected if action == "add": # a new device was detected
_process_add(device_info, 3) _process_add(device_info, 3)
return False return False

View File

@ -35,7 +35,6 @@ except ImportError:
class TaskRunner(_Thread): class TaskRunner(_Thread):
def __init__(self, name): def __init__(self, name):
super().__init__(name=name) super().__init__(name=name)
self.daemon = True self.daemon = True
@ -54,7 +53,7 @@ class TaskRunner(_Thread):
self.alive = True self.alive = True
if logger.isEnabledFor(logging.DEBUG): if logger.isEnabledFor(logging.DEBUG):
logger.debug('started') logger.debug("started")
while self.alive: while self.alive:
task = self.queue.get() task = self.queue.get()
@ -64,7 +63,7 @@ class TaskRunner(_Thread):
try: try:
function(*args, **kwargs) function(*args, **kwargs)
except Exception: except Exception:
logger.exception('calling %s', function) logger.exception("calling %s", function)
if logger.isEnabledFor(logging.DEBUG): if logger.isEnabledFor(logging.DEBUG):
logger.debug('stopped') logger.debug("stopped")

View File

@ -22,13 +22,14 @@ import gi
import yaml as _yaml import yaml as _yaml
from logitech_receiver.status import ALERT from logitech_receiver.status import ALERT
from solaar.i18n import _ from solaar.i18n import _
from solaar.ui.config_panel import change_setting, record_setting from solaar.ui.config_panel import change_setting, record_setting
from solaar.ui.window import find_device from solaar.ui.window import find_device
from . import common, diversion_rules, notify, tray, window from . import common, diversion_rules, notify, tray, window
gi.require_version('Gtk', '3.0') gi.require_version("Gtk", "3.0")
from gi.repository import Gio, GLib, Gtk # NOQA: E402 from gi.repository import Gio, GLib, Gtk # NOQA: E402
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -37,7 +38,7 @@ logger = logging.getLogger(__name__)
# #
# #
assert Gtk.get_major_version() > 2, 'Solaar requires Gtk 3 python bindings' assert Gtk.get_major_version() > 2, "Solaar requires Gtk 3 python bindings"
GLib.threads_init() GLib.threads_init()
@ -48,7 +49,7 @@ GLib.threads_init()
def _startup(app, startup_hook, use_tray, show_window): def _startup(app, startup_hook, use_tray, show_window):
if logger.isEnabledFor(logging.DEBUG): if logger.isEnabledFor(logging.DEBUG):
logger.debug('startup registered=%s, remote=%s', app.get_is_registered(), app.get_is_remote()) logger.debug("startup registered=%s, remote=%s", app.get_is_registered(), app.get_is_remote())
common.start_async() common.start_async()
notify.init() notify.init()
if use_tray: if use_tray:
@ -59,7 +60,7 @@ def _startup(app, startup_hook, use_tray, show_window):
def _activate(app): def _activate(app):
if logger.isEnabledFor(logging.DEBUG): if logger.isEnabledFor(logging.DEBUG):
logger.debug('activate') logger.debug("activate")
if app.get_windows(): if app.get_windows():
window.popup() window.popup()
else: else:
@ -68,12 +69,12 @@ def _activate(app):
def _command_line(app, command_line): def _command_line(app, command_line):
args = command_line.get_arguments() args = command_line.get_arguments()
args = _yaml.safe_load(''.join(args)) if args else args args = _yaml.safe_load("".join(args)) if args else args
if not args: if not args:
_activate(app) _activate(app)
elif args[0] == 'config': # config call from remote instance elif args[0] == "config": # config call from remote instance
if logger.isEnabledFor(logging.INFO): if logger.isEnabledFor(logging.INFO):
logger.info('remote command line %s', args) logger.info("remote command line %s", args)
dev = find_device(args[1]) dev = find_device(args[1])
if dev: if dev:
setting = next((s for s in dev.settings if s.name == args[2]), None) setting = next((s for s in dev.settings if s.name == args[2]), None)
@ -84,7 +85,7 @@ def _command_line(app, command_line):
def _shutdown(app, shutdown_hook): def _shutdown(app, shutdown_hook):
if logger.isEnabledFor(logging.DEBUG): if logger.isEnabledFor(logging.DEBUG):
logger.debug('shutdown') logger.debug("shutdown")
shutdown_hook() shutdown_hook()
common.stop_async() common.stop_async()
tray.destroy() tray.destroy()
@ -92,18 +93,18 @@ def _shutdown(app, shutdown_hook):
def run_loop(startup_hook, shutdown_hook, use_tray, show_window): def run_loop(startup_hook, shutdown_hook, use_tray, show_window):
assert use_tray or show_window, 'need either tray or visible window' assert use_tray or show_window, "need either tray or visible window"
APP_ID = 'io.github.pwr_solaar.solaar' APP_ID = "io.github.pwr_solaar.solaar"
application = Gtk.Application.new(APP_ID, Gio.ApplicationFlags.HANDLES_COMMAND_LINE) application = Gtk.Application.new(APP_ID, Gio.ApplicationFlags.HANDLES_COMMAND_LINE)
application.connect('startup', lambda app, startup_hook: _startup(app, startup_hook, use_tray, show_window), startup_hook) application.connect("startup", lambda app, startup_hook: _startup(app, startup_hook, use_tray, show_window), startup_hook)
application.connect('command-line', _command_line) application.connect("command-line", _command_line)
application.connect('activate', _activate) application.connect("activate", _activate)
application.connect('shutdown', _shutdown, shutdown_hook) application.connect("shutdown", _shutdown, shutdown_hook)
application.register() application.register()
if application.get_is_remote(): if application.get_is_remote():
print(_('Another Solaar process is already running so just expose its window')) print(_("Another Solaar process is already running so just expose its window"))
application.run() application.run()
@ -115,7 +116,7 @@ def run_loop(startup_hook, shutdown_hook, use_tray, show_window):
def _status_changed(device, alert, reason, refresh=False): def _status_changed(device, alert, reason, refresh=False):
assert device is not None assert device is not None
if logger.isEnabledFor(logging.DEBUG): if logger.isEnabledFor(logging.DEBUG):
logger.debug('status changed: %s (%s) %s', device, alert, reason) logger.debug("status changed: %s (%s) %s", device, alert, reason)
tray.update(device) tray.update(device)
if alert & ALERT.ATTENTION: if alert & ALERT.ATTENTION:

View File

@ -20,6 +20,7 @@
import logging import logging
from gi.repository import Gtk from gi.repository import Gtk
from solaar import NAME, __version__ from solaar import NAME, __version__
from solaar.i18n import _ from solaar.i18n import _
@ -35,60 +36,64 @@ def _create():
about.set_program_name(NAME) about.set_program_name(NAME)
about.set_version(__version__) about.set_version(__version__)
about.set_comments(_('Manages Logitech receivers,\nkeyboards, mice, and tablets.')) about.set_comments(_("Manages Logitech receivers,\nkeyboards, mice, and tablets."))
about.set_icon_name(NAME.lower()) about.set_icon_name(NAME.lower())
about.set_copyright('© 2012-2023 Daniel Pavel and contributors to the Solaar project') about.set_copyright("© 2012-2023 Daniel Pavel and contributors to the Solaar project")
about.set_license_type(Gtk.License.GPL_2_0) about.set_license_type(Gtk.License.GPL_2_0)
about.set_authors(('Daniel Pavel http://github.com/pwr', )) about.set_authors(("Daniel Pavel http://github.com/pwr",))
try: try:
about.add_credit_section(_('Additional Programming'), ('Filipe Laíns', 'Peter F. Patel-Schneider')) about.add_credit_section(_("Additional Programming"), ("Filipe Laíns", "Peter F. Patel-Schneider"))
about.add_credit_section(_('GUI design'), ('Julien Gascard', 'Daniel Pavel')) about.add_credit_section(_("GUI design"), ("Julien Gascard", "Daniel Pavel"))
about.add_credit_section( about.add_credit_section(
_('Testing'), ( _("Testing"),
'Douglas Wagner', (
'Julien Gascard', "Douglas Wagner",
'Peter Wu http://www.lekensteyn.nl/logitech-unifying.html', "Julien Gascard",
) "Peter Wu http://www.lekensteyn.nl/logitech-unifying.html",
),
) )
about.add_credit_section( about.add_credit_section(
_('Logitech documentation'), ( _("Logitech documentation"),
'Julien Danjou http://julien.danjou.info/blog/2012/logitech-unifying-upower', (
'Nestor Lopez Casado http://drive.google.com/folderview?id=0BxbRzx7vEV7eWmgwazJ3NUFfQ28', "Julien Danjou http://julien.danjou.info/blog/2012/logitech-unifying-upower",
) "Nestor Lopez Casado http://drive.google.com/folderview?id=0BxbRzx7vEV7eWmgwazJ3NUFfQ28",
),
) )
except TypeError: except TypeError:
# gtk3 < ~3.6.4 has incorrect gi bindings # gtk3 < ~3.6.4 has incorrect gi bindings
logging.exception('failed to fully create the about dialog') logging.exception("failed to fully create the about dialog")
except Exception: except Exception:
# the Gtk3 version may be too old, and the function does not exist # the Gtk3 version may be too old, and the function does not exist
logging.exception('failed to fully create the about dialog') logging.exception("failed to fully create the about dialog")
about.set_translator_credits( about.set_translator_credits(
'\n'.join(( "\n".join(
'gogo (croatian)', (
'Papoteur, David Geiger, Damien Lallement (français)', "gogo (croatian)",
'Michele Olivo (italiano)', "Papoteur, David Geiger, Damien Lallement (français)",
'Adrian Piotrowicz (polski)', "Michele Olivo (italiano)",
'Drovetto, JrBenito (Portuguese-BR)', "Adrian Piotrowicz (polski)",
'Daniel Pavel (română)', "Drovetto, JrBenito (Portuguese-BR)",
'Daniel Zippert, Emelie Snecker (svensk)', "Daniel Pavel (română)",
'Dimitriy Ryazantcev (Russian)', "Daniel Zippert, Emelie Snecker (svensk)",
'El Jinete Sin Cabeza (Español)', "Dimitriy Ryazantcev (Russian)",
)) "El Jinete Sin Cabeza (Español)",
)
)
) )
about.set_website('https://pwr-solaar.github.io/Solaar') about.set_website("https://pwr-solaar.github.io/Solaar")
about.set_website_label(NAME) about.set_website_label(NAME)
about.connect('response', lambda x, y: x.hide()) about.connect("response", lambda x, y: x.hide())
def _hide(dialog, event): def _hide(dialog, event):
dialog.hide() dialog.hide()
return True return True
about.connect('delete-event', _hide) about.connect("delete-event", _hide)
return about return about

View File

@ -17,6 +17,7 @@
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
from gi.repository import Gdk, Gtk from gi.repository import Gdk, Gtk
from solaar.i18n import _ from solaar.i18n import _
from . import pair_window from . import pair_window
@ -36,7 +37,7 @@ def make(name, label, function, stock_id=None, *args):
if stock_id is not None: if stock_id is not None:
action.set_stock_id(stock_id) action.set_stock_id(stock_id)
if function: if function:
action.connect('activate', function, *args) action.connect("activate", function, *args)
return action return action
@ -45,7 +46,7 @@ def make_toggle(name, label, function, stock_id=None, *args):
action.set_icon_name(name) action.set_icon_name(name)
if stock_id is not None: if stock_id is not None:
action.set_stock_id(stock_id) action.set_stock_id(stock_id)
action.connect('activate', function, *args) action.connect("activate", function, *args)
return action return action
@ -80,12 +81,11 @@ def unpair(window, device):
assert device.kind is not None assert device.kind is not None
qdialog = Gtk.MessageDialog( qdialog = Gtk.MessageDialog(
window, 0, Gtk.MessageType.QUESTION, Gtk.ButtonsType.NONE, window, 0, Gtk.MessageType.QUESTION, Gtk.ButtonsType.NONE, _("Unpair") + " " + device.name + " ?"
_('Unpair') + ' ' + device.name + ' ?'
) )
qdialog.set_icon_name('remove') qdialog.set_icon_name("remove")
qdialog.add_button(_('Cancel'), Gtk.ResponseType.CANCEL) qdialog.add_button(_("Cancel"), Gtk.ResponseType.CANCEL)
qdialog.add_button(_('Unpair'), Gtk.ResponseType.ACCEPT) qdialog.add_button(_("Unpair"), Gtk.ResponseType.ACCEPT)
choice = qdialog.run() choice = qdialog.run()
qdialog.destroy() qdialog.destroy()
if choice == Gtk.ResponseType.ACCEPT: if choice == Gtk.ResponseType.ACCEPT:
@ -97,4 +97,4 @@ def unpair(window, device):
del receiver[device_number] del receiver[device_number]
except Exception: except Exception:
# logger.exception("unpairing %s", device) # logger.exception("unpairing %s", device)
error_dialog('unpair', device) error_dialog("unpair", device)

View File

@ -23,32 +23,35 @@ import gi
from solaar.i18n import _ from solaar.i18n import _
from solaar.tasks import TaskRunner as _TaskRunner from solaar.tasks import TaskRunner as _TaskRunner
gi.require_version('Gtk', '3.0') gi.require_version("Gtk", "3.0")
from gi.repository import GLib, Gtk # NOQA: E402 from gi.repository import GLib, Gtk # NOQA: E402
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def _error_dialog(reason, object): def _error_dialog(reason, object):
logger.error('error: %s %s', reason, object) logger.error("error: %s %s", reason, object)
if reason == 'permissions': if reason == "permissions":
title = _('Permissions error') title = _("Permissions error")
text = ( text = (
_('Found a Logitech receiver or device (%s), but did not have permission to open it.') % object + '\n\n' + _("Found a Logitech receiver or device (%s), but did not have permission to open it.") % object
_("If you've just installed Solaar, try disconnecting the receiver or device and then reconnecting it.") + "\n\n"
+ _("If you've just installed Solaar, try disconnecting the receiver or device and then reconnecting it.")
) )
elif reason == 'nodevice': elif reason == "nodevice":
title = _('Cannot connect to device error') title = _("Cannot connect to device error")
text = ( text = (
_('Found a Logitech receiver or device at %s, but encountered an error connecting to it.') % object + '\n\n' + _("Found a Logitech receiver or device at %s, but encountered an error connecting to it.") % object
_('Try disconnecting the device and then reconnecting it or turning it off and then on.') + "\n\n"
+ _("Try disconnecting the device and then reconnecting it or turning it off and then on.")
) )
elif reason == 'unpair': elif reason == "unpair":
title = _('Unpairing failed') title = _("Unpairing failed")
text = ( text = (
_('Failed to unpair %{device} from %{receiver}.').format(device=object.name, receiver=object.receiver.name) + _("Failed to unpair %{device} from %{receiver}.").format(device=object.name, receiver=object.receiver.name)
'\n\n' + _('The receiver returned an error, with no further details.') + "\n\n"
+ _("The receiver returned an error, with no further details.")
) )
else: else:
raise Exception("ui.error_dialog: don't know how to handle (%s, %s)", reason, object) raise Exception("ui.error_dialog: don't know how to handle (%s, %s)", reason, object)
@ -76,7 +79,7 @@ _task_runner = None
def start_async(): def start_async():
global _task_runner global _task_runner
_task_runner = _TaskRunner('AsyncUI') _task_runner = _TaskRunner("AsyncUI")
_task_runner.start() _task_runner.start()

View File

@ -26,11 +26,12 @@ import gi
from logitech_receiver.hidpp20 import LEDEffectSetting as _LEDEffectSetting from logitech_receiver.hidpp20 import LEDEffectSetting as _LEDEffectSetting
from logitech_receiver.settings import KIND as _SETTING_KIND from logitech_receiver.settings import KIND as _SETTING_KIND
from logitech_receiver.settings import SENSITIVITY_IGNORE as _SENSITIVITY_IGNORE from logitech_receiver.settings import SENSITIVITY_IGNORE as _SENSITIVITY_IGNORE
from solaar.i18n import _, ngettext from solaar.i18n import _, ngettext
from .common import ui_async as _ui_async from .common import ui_async as _ui_async
gi.require_version('Gtk', '3.0') gi.require_version("Gtk", "3.0")
from gi.repository import Gdk, GLib, Gtk # NOQA: E402 from gi.repository import Gdk, GLib, Gtk # NOQA: E402
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -41,7 +42,6 @@ logger = logging.getLogger(__name__)
def _read_async(setting, force_read, sbox, device_is_online, sensitive): def _read_async(setting, force_read, sbox, device_is_online, sensitive):
def _do_read(s, force, sb, online, sensitive): def _do_read(s, force, sb, online, sensitive):
v = s.read(not force) v = s.read(not force)
GLib.idle_add(_update_setting_item, sb, v, online, sensitive, True, priority=99) GLib.idle_add(_update_setting_item, sb, v, online, sensitive, True, priority=99)
@ -50,7 +50,6 @@ def _read_async(setting, force_read, sbox, device_is_online, sensitive):
def _write_async(setting, value, sbox, sensitive=True, key=None): def _write_async(setting, value, sbox, sensitive=True, key=None):
def _do_write(s, v, sb, key): def _do_write(s, v, sb, key):
try: try:
if key is None: if key is None:
@ -78,7 +77,6 @@ def _write_async(setting, value, sbox, sensitive=True, key=None):
class ComboBoxText(Gtk.ComboBoxText): class ComboBoxText(Gtk.ComboBoxText):
def get_value(self): def get_value(self):
return int(self.get_active_id()) return int(self.get_active_id())
@ -87,13 +85,11 @@ class ComboBoxText(Gtk.ComboBoxText):
class Scale(Gtk.Scale): class Scale(Gtk.Scale):
def get_value(self): def get_value(self):
return int(super().get_value()) return int(super().get_value())
class Control(): class Control:
def __init__(**kwargs): def __init__(**kwargs):
pass pass
@ -119,11 +115,10 @@ class Control():
class ToggleControl(Gtk.Switch, Control): class ToggleControl(Gtk.Switch, Control):
def __init__(self, sbox, delegate=None): def __init__(self, sbox, delegate=None):
super().__init__(halign=Gtk.Align.CENTER, valign=Gtk.Align.CENTER) super().__init__(halign=Gtk.Align.CENTER, valign=Gtk.Align.CENTER)
self.init(sbox, delegate) self.init(sbox, delegate)
self.connect('notify::active', self.changed) self.connect("notify::active", self.changed)
def set_value(self, value): def set_value(self, value):
if value is not None: if value is not None:
@ -134,7 +129,6 @@ class ToggleControl(Gtk.Switch, Control):
class SliderControl(Gtk.Scale, Control): class SliderControl(Gtk.Scale, Control):
def __init__(self, sbox, delegate=None): def __init__(self, sbox, delegate=None):
super().__init__(halign=Gtk.Align.FILL) super().__init__(halign=Gtk.Align.FILL)
self.init(sbox, delegate) self.init(sbox, delegate)
@ -143,7 +137,7 @@ class SliderControl(Gtk.Scale, Control):
self.set_round_digits(0) self.set_round_digits(0)
self.set_digits(0) self.set_digits(0)
self.set_increments(1, 5) self.set_increments(1, 5)
self.connect('value-changed', self.changed) self.connect("value-changed", self.changed)
def get_value(self): def get_value(self):
return int(super().get_value()) return int(super().get_value())
@ -169,14 +163,13 @@ def _create_choice_control(sbox, delegate=None, choices=None):
# GTK boxes have property lists, but the keys must be strings # GTK boxes have property lists, but the keys must be strings
class ChoiceControlLittle(Gtk.ComboBoxText, Control): class ChoiceControlLittle(Gtk.ComboBoxText, Control):
def __init__(self, sbox, delegate=None, choices=None): def __init__(self, sbox, delegate=None, choices=None):
super().__init__(halign=Gtk.Align.FILL) super().__init__(halign=Gtk.Align.FILL)
self.init(sbox, delegate) self.init(sbox, delegate)
self.choices = choices if choices is not None else sbox.setting.choices self.choices = choices if choices is not None else sbox.setting.choices
for entry in self.choices: for entry in self.choices:
self.append(str(int(entry)), str(entry)) self.append(str(int(entry)), str(entry))
self.connect('changed', self.changed) self.connect("changed", self.changed)
def get_value(self): def get_value(self):
return int(self.get_active_id()) if self.get_active_id() is not None else None return int(self.get_active_id()) if self.get_active_id() is not None else None
@ -196,7 +189,6 @@ class ChoiceControlLittle(Gtk.ComboBoxText, Control):
class ChoiceControlBig(Gtk.Entry, Control): class ChoiceControlBig(Gtk.Entry, Control):
def __init__(self, sbox, delegate=None, choices=None): def __init__(self, sbox, delegate=None, choices=None):
super().__init__(halign=Gtk.Align.FILL) super().__init__(halign=Gtk.Align.FILL)
self.init(sbox, delegate) self.init(sbox, delegate)
@ -208,13 +200,13 @@ class ChoiceControlBig(Gtk.Entry, Control):
liststore.append((int(v), str(v))) liststore.append((int(v), str(v)))
completion = Gtk.EntryCompletion() completion = Gtk.EntryCompletion()
completion.set_model(liststore) completion.set_model(liststore)
norm = lambda s: s.replace('_', '').replace(' ', '').lower() norm = lambda s: s.replace("_", "").replace(" ", "").lower()
completion.set_match_func(lambda completion, key, it: norm(key) in norm(completion.get_model()[it][1])) completion.set_match_func(lambda completion, key, it: norm(key) in norm(completion.get_model()[it][1]))
completion.set_text_column(1) completion.set_text_column(1)
self.set_completion(completion) self.set_completion(completion)
self.connect('changed', self.changed) self.connect("changed", self.changed)
self.connect('activate', self.activate) self.connect("activate", self.activate)
completion.connect('match_selected', self.select) completion.connect("match_selected", self.select)
def get_value(self): def get_value(self):
choice = self.get_choice() choice = self.get_choice()
@ -230,25 +222,24 @@ class ChoiceControlBig(Gtk.Entry, Control):
def changed(self, *args): def changed(self, *args):
self.value = self.get_choice() self.value = self.get_choice()
icon = 'dialog-warning' if self.value is None else 'dialog-question' if self.get_sensitive() else '' icon = "dialog-warning" if self.value is None else "dialog-question" if self.get_sensitive() else ""
self.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, icon) self.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, icon)
tooltip = _('Incomplete') if self.value is None else _('Complete - ENTER to change') tooltip = _("Incomplete") if self.value is None else _("Complete - ENTER to change")
self.set_icon_tooltip_text(Gtk.EntryIconPosition.SECONDARY, tooltip) self.set_icon_tooltip_text(Gtk.EntryIconPosition.SECONDARY, tooltip)
def activate(self, *args): def activate(self, *args):
if self.value is not None and self.get_sensitive(): if self.value is not None and self.get_sensitive():
self.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, '') self.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, "")
self.delegate.update() self.delegate.update()
def select(self, completion, model, iter): def select(self, completion, model, iter):
self.set_value(model.get(iter, 0)[0]) self.set_value(model.get(iter, 0)[0])
if self.value and self.get_sensitive(): if self.value and self.get_sensitive():
self.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, '') self.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, "")
self.delegate.update() self.delegate.update()
class MapChoiceControl(Gtk.HBox, Control): class MapChoiceControl(Gtk.HBox, Control):
def __init__(self, sbox, delegate=None): def __init__(self, sbox, delegate=None):
super().__init__(homogeneous=False, spacing=6) super().__init__(homogeneous=False, spacing=6)
self.init(sbox, delegate) self.init(sbox, delegate)
@ -261,7 +252,7 @@ class MapChoiceControl(Gtk.HBox, Control):
self.valueBox = _create_choice_control(sbox.setting, choices=self.value_choices, delegate=self) self.valueBox = _create_choice_control(sbox.setting, choices=self.value_choices, delegate=self)
self.pack_start(self.keyBox, False, False, 0) self.pack_start(self.keyBox, False, False, 0)
self.pack_end(self.valueBox, False, False, 0) self.pack_end(self.valueBox, False, False, 0)
self.keyBox.connect('changed', self.map_value_notify_key) self.keyBox.connect("changed", self.map_value_notify_key)
def get_value(self): def get_value(self):
key_choice = int(self.keyBox.get_active_id()) key_choice = int(self.keyBox.get_active_id())
@ -301,8 +292,7 @@ class MapChoiceControl(Gtk.HBox, Control):
class MultipleControl(Gtk.ListBox, Control): class MultipleControl(Gtk.ListBox, Control):
def __init__(self, sbox, change, button_label="...", delegate=None):
def __init__(self, sbox, change, button_label='...', delegate=None):
super().__init__() super().__init__()
self.init(sbox, delegate) self.init(sbox, delegate)
self.set_selection_mode(Gtk.SelectionMode.NONE) self.set_selection_mode(Gtk.SelectionMode.NONE)
@ -311,7 +301,7 @@ class MultipleControl(Gtk.ListBox, Control):
self.setup(sbox.setting) # set up the data and boxes for the sub-controls self.setup(sbox.setting) # set up the data and boxes for the sub-controls
btn = Gtk.Button(button_label) btn = Gtk.Button(button_label)
btn.set_alignment(1.0, 0.5) btn.set_alignment(1.0, 0.5)
btn.connect('clicked', self.toggle_display) btn.connect("clicked", self.toggle_display)
self._button = btn self._button = btn
hbox = Gtk.HBox(homogeneous=False, spacing=6) hbox = Gtk.HBox(homogeneous=False, spacing=6)
hbox.pack_end(change, False, False, 0) hbox.pack_end(change, False, False, 0)
@ -345,22 +335,21 @@ class MultipleControl(Gtk.ListBox, Control):
class MultipleToggleControl(MultipleControl): class MultipleToggleControl(MultipleControl):
def setup(self, setting): def setup(self, setting):
self._label_control_pairs = [] self._label_control_pairs = []
for k in setting._validator.get_options(): for k in setting._validator.get_options():
h = Gtk.HBox(homogeneous=False, spacing=0) h = Gtk.HBox(homogeneous=False, spacing=0)
lbl_text = str(k) lbl_text = str(k)
lbl_tooltip = None lbl_tooltip = None
if hasattr(setting, '_labels'): if hasattr(setting, "_labels"):
l1, l2 = setting._labels.get(k, (None, None)) l1, l2 = setting._labels.get(k, (None, None))
lbl_text = l1 if l1 else lbl_text lbl_text = l1 if l1 else lbl_text
lbl_tooltip = l2 if l2 else lbl_tooltip lbl_tooltip = l2 if l2 else lbl_tooltip
lbl = Gtk.Label(lbl_text) lbl = Gtk.Label(lbl_text)
h.set_tooltip_text(lbl_tooltip or ' ') h.set_tooltip_text(lbl_tooltip or " ")
control = Gtk.Switch() control = Gtk.Switch()
control._setting_key = int(k) control._setting_key = int(k)
control.connect('notify::active', self.toggle_notify) control.connect("notify::active", self.toggle_notify)
h.pack_start(lbl, False, False, 0) h.pack_start(lbl, False, False, 0)
h.pack_end(control, False, False, 0) h.pack_end(control, False, False, 0)
lbl.set_alignment(0.0, 0.5) lbl.set_alignment(0.0, 0.5)
@ -388,26 +377,25 @@ class MultipleToggleControl(MultipleControl):
elem.set_state(v) elem.set_state(v)
if elem.get_state(): if elem.get_state():
active += 1 active += 1
to_join.append(lbl.get_text() + ': ' + str(elem.get_state())) to_join.append(lbl.get_text() + ": " + str(elem.get_state()))
b = ', '.join(to_join) b = ", ".join(to_join)
self._button.set_label(f'{active} / {total}') self._button.set_label(f"{active} / {total}")
self._button.set_tooltip_text(b) self._button.set_tooltip_text(b)
class MultipleRangeControl(MultipleControl): class MultipleRangeControl(MultipleControl):
def setup(self, setting): def setup(self, setting):
self._items = [] self._items = []
for item in setting._validator.items: for item in setting._validator.items:
lbl_text = str(item) lbl_text = str(item)
lbl_tooltip = None lbl_tooltip = None
if hasattr(setting, '_labels'): if hasattr(setting, "_labels"):
l1, l2 = setting._labels.get(int(item), (None, None)) l1, l2 = setting._labels.get(int(item), (None, None))
lbl_text = l1 if l1 else lbl_text lbl_text = l1 if l1 else lbl_text
lbl_tooltip = l2 if l2 else lbl_tooltip lbl_tooltip = l2 if l2 else lbl_tooltip
item_lbl = Gtk.Label(lbl_text) item_lbl = Gtk.Label(lbl_text)
self.add(item_lbl) self.add(item_lbl)
self.set_tooltip_text(lbl_tooltip or ' ') self.set_tooltip_text(lbl_tooltip or " ")
item_lb = Gtk.ListBox() item_lb = Gtk.ListBox()
item_lb.set_selection_mode(Gtk.SelectionMode.NONE) item_lb.set_selection_mode(Gtk.SelectionMode.NONE)
item_lb._sub_items = [] item_lb._sub_items = []
@ -415,27 +403,27 @@ class MultipleRangeControl(MultipleControl):
h = Gtk.HBox(homogeneous=False, spacing=20) h = Gtk.HBox(homogeneous=False, spacing=20)
lbl_text = str(sub_item) lbl_text = str(sub_item)
lbl_tooltip = None lbl_tooltip = None
if hasattr(setting, '_labels_sub'): if hasattr(setting, "_labels_sub"):
l1, l2 = setting._labels_sub.get(str(sub_item), (None, None)) l1, l2 = setting._labels_sub.get(str(sub_item), (None, None))
lbl_text = l1 if l1 else lbl_text lbl_text = l1 if l1 else lbl_text
lbl_tooltip = l2 if l2 else lbl_tooltip lbl_tooltip = l2 if l2 else lbl_tooltip
sub_item_lbl = Gtk.Label(lbl_text) sub_item_lbl = Gtk.Label(lbl_text)
h.set_tooltip_text(lbl_tooltip or ' ') h.set_tooltip_text(lbl_tooltip or " ")
h.pack_start(sub_item_lbl, False, False, 0) h.pack_start(sub_item_lbl, False, False, 0)
sub_item_lbl.set_margin_left(30) sub_item_lbl.set_margin_left(30)
sub_item_lbl.set_alignment(0.0, 0.5) sub_item_lbl.set_alignment(0.0, 0.5)
if sub_item.widget == 'Scale': if sub_item.widget == "Scale":
control = Gtk.Scale.new_with_range(Gtk.Orientation.HORIZONTAL, sub_item.minimum, sub_item.maximum, 1) control = Gtk.Scale.new_with_range(Gtk.Orientation.HORIZONTAL, sub_item.minimum, sub_item.maximum, 1)
control.set_round_digits(0) control.set_round_digits(0)
control.set_digits(0) control.set_digits(0)
h.pack_end(control, True, True, 0) h.pack_end(control, True, True, 0)
elif sub_item.widget == 'SpinButton': elif sub_item.widget == "SpinButton":
control = Gtk.SpinButton.new_with_range(sub_item.minimum, sub_item.maximum, 1) control = Gtk.SpinButton.new_with_range(sub_item.minimum, sub_item.maximum, 1)
control.set_digits(0) control.set_digits(0)
h.pack_end(control, False, False, 0) h.pack_end(control, False, False, 0)
else: else:
raise NotImplementedError raise NotImplementedError
control.connect('value-changed', self.changed, item, sub_item) control.connect("value-changed", self.changed, item, sub_item)
item_lb.add(h) item_lb.add(h)
h._setting_sub_item = sub_item h._setting_sub_item = sub_item
h._label, h._control = sub_item_lbl, control h._label, h._control = sub_item_lbl, control
@ -447,14 +435,14 @@ class MultipleRangeControl(MultipleControl):
def changed(self, control, item, sub_item): def changed(self, control, item, sub_item):
if control.get_sensitive(): if control.get_sensitive():
if hasattr(control, '_timer'): if hasattr(control, "_timer"):
control._timer.cancel() control._timer.cancel()
control._timer = _Timer(0.5, lambda: GLib.idle_add(self._write, control, item, sub_item)) control._timer = _Timer(0.5, lambda: GLib.idle_add(self._write, control, item, sub_item))
control._timer.start() control._timer.start()
def _write(self, control, item, sub_item): def _write(self, control, item, sub_item):
control._timer.cancel() control._timer.cancel()
delattr(control, '_timer') delattr(control, "_timer")
new_state = int(control.get_value()) new_state = int(control.get_value())
if self.sbox.setting._value[int(item)][str(sub_item)] != new_state: if self.sbox.setting._value[int(item)][str(sub_item)] != new_state:
self.sbox.setting._value[int(item)][str(sub_item)] = new_state self.sbox.setting._value[int(item)][str(sub_item)] = new_state
@ -463,13 +451,13 @@ class MultipleRangeControl(MultipleControl):
def set_value(self, value): def set_value(self, value):
if value is None: if value is None:
return return
b = '' b = ""
n = 0 n = 0
for ch in self._items: for ch in self._items:
item = ch._setting_item item = ch._setting_item
v = value.get(int(item), None) v = value.get(int(item), None)
if v is not None: if v is not None:
b += str(item) + ': (' b += str(item) + ": ("
to_join = [] to_join = []
for c in ch._sub_items: for c in ch._sub_items:
sub_item = c._setting_sub_item sub_item = c._setting_sub_item
@ -479,15 +467,14 @@ class MultipleRangeControl(MultipleControl):
sub_item_value = c._control.get_value() sub_item_value = c._control.get_value()
c._control.set_value(sub_item_value) c._control.set_value(sub_item_value)
n += 1 n += 1
to_join.append(str(sub_item) + f'={sub_item_value}') to_join.append(str(sub_item) + f"={sub_item_value}")
b += ', '.join(to_join) + ') ' b += ", ".join(to_join) + ") "
lbl_text = ngettext('%d value', '%d values', n) % n lbl_text = ngettext("%d value", "%d values", n) % n
self._button.set_label(lbl_text) self._button.set_label(lbl_text)
self._button.set_tooltip_text(b) self._button.set_tooltip_text(b)
class PackedRangeControl(MultipleRangeControl): class PackedRangeControl(MultipleRangeControl):
def setup(self, setting): def setup(self, setting):
validator = setting._validator validator = setting._validator
self._items = [] self._items = []
@ -497,7 +484,7 @@ class PackedRangeControl(MultipleRangeControl):
control = Gtk.Scale.new_with_range(Gtk.Orientation.HORIZONTAL, validator.min_value, validator.max_value, 1) control = Gtk.Scale.new_with_range(Gtk.Orientation.HORIZONTAL, validator.min_value, validator.max_value, 1)
control.set_round_digits(0) control.set_round_digits(0)
control.set_digits(0) control.set_digits(0)
control.connect('value-changed', self.changed, validator.keys[item]) control.connect("value-changed", self.changed, validator.keys[item])
h.pack_start(lbl, False, False, 0) h.pack_start(lbl, False, False, 0)
h.pack_end(control, True, True, 0) h.pack_end(control, True, True, 0)
h._setting_item = validator.keys[item] h._setting_item = validator.keys[item]
@ -509,14 +496,14 @@ class PackedRangeControl(MultipleRangeControl):
def changed(self, control, item): def changed(self, control, item):
if control.get_sensitive(): if control.get_sensitive():
if hasattr(control, '_timer'): if hasattr(control, "_timer"):
control._timer.cancel() control._timer.cancel()
control._timer = _Timer(0.5, lambda: GLib.idle_add(self._write, control, item)) control._timer = _Timer(0.5, lambda: GLib.idle_add(self._write, control, item))
control._timer.start() control._timer.start()
def _write(self, control, item): def _write(self, control, item):
control._timer.cancel() control._timer.cancel()
delattr(control, '_timer') delattr(control, "_timer")
new_state = int(control.get_value()) new_state = int(control.get_value())
if self.sbox.setting._value[int(item)] != new_state: if self.sbox.setting._value[int(item)] != new_state:
self.sbox.setting._value[int(item)] = new_state self.sbox.setting._value[int(item)] = new_state
@ -525,7 +512,7 @@ class PackedRangeControl(MultipleRangeControl):
def set_value(self, value): def set_value(self, value):
if value is None: if value is None:
return return
b = '' b = ""
n = len(self._items) n = len(self._items)
for h in self._items: for h in self._items:
item = h._setting_item item = h._setting_item
@ -534,43 +521,42 @@ class PackedRangeControl(MultipleRangeControl):
h.control.set_value(v) h.control.set_value(v)
else: else:
v = self.sbox.setting._value[int(item)] v = self.sbox.setting._value[int(item)]
b += str(item) + ': (' + str(v) + ') ' b += str(item) + ": (" + str(v) + ") "
lbl_text = ngettext('%d value', '%d values', n) % n lbl_text = ngettext("%d value", "%d values", n) % n
self._button.set_label(lbl_text) self._button.set_label(lbl_text)
self._button.set_tooltip_text(b) self._button.set_tooltip_text(b)
# control with an ID key that determines what else to show # control with an ID key that determines what else to show
class HeteroKeyControl(Gtk.HBox, Control): class HeteroKeyControl(Gtk.HBox, Control):
def __init__(self, sbox, delegate=None): def __init__(self, sbox, delegate=None):
super().__init__(homogeneous=False, spacing=6) super().__init__(homogeneous=False, spacing=6)
self.init(sbox, delegate) self.init(sbox, delegate)
self._items = {} self._items = {}
for item in sbox.setting.possible_fields: for item in sbox.setting.possible_fields:
if item['label']: if item["label"]:
item_lblbox = Gtk.Label(item['label']) item_lblbox = Gtk.Label(item["label"])
self.pack_start(item_lblbox, False, False, 0) self.pack_start(item_lblbox, False, False, 0)
item_lblbox.set_visible(False) item_lblbox.set_visible(False)
else: else:
item_lblbox = None item_lblbox = None
if item['kind'] == _SETTING_KIND.choice: if item["kind"] == _SETTING_KIND.choice:
item_box = ComboBoxText() item_box = ComboBoxText()
for entry in item['choices']: for entry in item["choices"]:
item_box.append(str(int(entry)), str(entry)) item_box.append(str(int(entry)), str(entry))
item_box.set_active(0) item_box.set_active(0)
item_box.connect('changed', self.changed) item_box.connect("changed", self.changed)
self.pack_start(item_box, False, False, 0) self.pack_start(item_box, False, False, 0)
elif item['kind'] == _SETTING_KIND.range: elif item["kind"] == _SETTING_KIND.range:
item_box = Scale() item_box = Scale()
item_box.set_range(item['min'], item['max']) item_box.set_range(item["min"], item["max"])
item_box.set_round_digits(0) item_box.set_round_digits(0)
item_box.set_digits(0) item_box.set_digits(0)
item_box.set_increments(1, 5) item_box.set_increments(1, 5)
item_box.connect('value-changed', self.changed) item_box.connect("value-changed", self.changed)
self.pack_start(item_box, True, True, 0) self.pack_start(item_box, True, True, 0)
item_box.set_visible(False) item_box.set_visible(False)
self._items[str(item['name'])] = (item_lblbox, item_box) self._items[str(item["name"])] = (item_lblbox, item_box)
def get_value(self): def get_value(self):
result = {} result = {}
@ -591,23 +577,23 @@ class HeteroKeyControl(Gtk.HBox, Control):
def setup_visibles(self, ID): def setup_visibles(self, ID):
fields = self.sbox.setting.fields_map[ID][1] if ID in self.sbox.setting.fields_map else {} fields = self.sbox.setting.fields_map[ID][1] if ID in self.sbox.setting.fields_map else {}
for name, (lblbox, box) in self._items.items(): for name, (lblbox, box) in self._items.items():
visible = name in fields or name == 'ID' visible = name in fields or name == "ID"
if lblbox: if lblbox:
lblbox.set_visible(visible) lblbox.set_visible(visible)
box.set_visible(visible) box.set_visible(visible)
def changed(self, control): def changed(self, control):
if self.get_sensitive() and control.get_sensitive(): if self.get_sensitive() and control.get_sensitive():
if 'ID' in self._items and control == self._items['ID'][1]: if "ID" in self._items and control == self._items["ID"][1]:
self.setup_visibles(int(self._items['ID'][1].get_value())) self.setup_visibles(int(self._items["ID"][1].get_value()))
if hasattr(control, '_timer'): if hasattr(control, "_timer"):
control._timer.cancel() control._timer.cancel()
control._timer = _Timer(0.3, lambda: GLib.idle_add(self._write, control)) control._timer = _Timer(0.3, lambda: GLib.idle_add(self._write, control))
control._timer.start() control._timer.start()
def _write(self, control): def _write(self, control):
control._timer.cancel() control._timer.cancel()
delattr(control, '_timer') delattr(control, "_timer")
new_state = self.get_value() new_state = self.get_value()
if self.sbox.setting._value != new_state: if self.sbox.setting._value != new_state:
_write_async(self.sbox.setting, new_state, self.sbox) _write_async(self.sbox.setting, new_state, self.sbox)
@ -617,11 +603,11 @@ class HeteroKeyControl(Gtk.HBox, Control):
# #
# #
_allowables_icons = {True: 'changes-allow', False: 'changes-prevent', _SENSITIVITY_IGNORE: 'dialog-error'} _allowables_icons = {True: "changes-allow", False: "changes-prevent", _SENSITIVITY_IGNORE: "dialog-error"}
_allowables_tooltips = { _allowables_tooltips = {
True: _('Changes allowed'), True: _("Changes allowed"),
False: _('No changes allowed'), False: _("No changes allowed"),
_SENSITIVITY_IGNORE: _('Ignore this setting') _SENSITIVITY_IGNORE: _("Ignore this setting"),
} }
_next_allowable = {True: False, False: _SENSITIVITY_IGNORE, _SENSITIVITY_IGNORE: True} _next_allowable = {True: False, False: _SENSITIVITY_IGNORE, _SENSITIVITY_IGNORE: True}
_icons_allowables = {v: k for k, v in _allowables_icons.items()} _icons_allowables = {v: k for k, v in _allowables_icons.items()}
@ -666,19 +652,19 @@ def _create_sbox(s, device):
label = Gtk.EventBox() label = Gtk.EventBox()
label.add(lbl) label.add(lbl)
spinner = Gtk.Spinner() spinner = Gtk.Spinner()
spinner.set_tooltip_text(_('Working') + '...') spinner.set_tooltip_text(_("Working") + "...")
sbox._spinner = spinner sbox._spinner = spinner
failed = Gtk.Image.new_from_icon_name('dialog-warning', Gtk.IconSize.SMALL_TOOLBAR) failed = Gtk.Image.new_from_icon_name("dialog-warning", Gtk.IconSize.SMALL_TOOLBAR)
failed.set_tooltip_text(_('Read/write operation failed.')) failed.set_tooltip_text(_("Read/write operation failed."))
sbox._failed = failed sbox._failed = failed
change_icon = Gtk.Image.new_from_icon_name('changes-prevent', Gtk.IconSize.LARGE_TOOLBAR) change_icon = Gtk.Image.new_from_icon_name("changes-prevent", Gtk.IconSize.LARGE_TOOLBAR)
sbox._change_icon = change_icon sbox._change_icon = change_icon
_change_icon(False, change_icon) _change_icon(False, change_icon)
change = Gtk.Button() change = Gtk.Button()
change.set_relief(Gtk.ReliefStyle.NONE) change.set_relief(Gtk.ReliefStyle.NONE)
change.add(change_icon) change.add(change_icon)
change.set_sensitive(True) change.set_sensitive(True)
change.connect('clicked', _change_click, sbox) change.connect("clicked", _change_click, sbox)
if s.kind == _SETTING_KIND.toggle: if s.kind == _SETTING_KIND.toggle:
control = ToggleControl(sbox) control = ToggleControl(sbox)
@ -698,7 +684,7 @@ def _create_sbox(s, device):
control = HeteroKeyControl(sbox, change) control = HeteroKeyControl(sbox, change)
else: else:
if logger.isEnabledFor(logging.WARNING): if logger.isEnabledFor(logging.WARNING):
logger.warning('setting %s display not implemented', s.label) logger.warning("setting %s display not implemented", s.label)
return None return None
control.set_sensitive(False) # the first read will enable it control.set_sensitive(False) # the first read will enable it
@ -728,7 +714,7 @@ def _update_setting_item(sbox, value, is_online=True, sensitive=True, nullOK=Fal
def _disable_listbox_highlight_bg(lb): def _disable_listbox_highlight_bg(lb):
colour = Gdk.RGBA() colour = Gdk.RGBA()
colour.parse('rgba(0,0,0,0)') colour.parse("rgba(0,0,0,0)")
for child in lb.get_children(): for child in lb.get_children():
child.override_background_color(Gtk.StateFlags.PRELIGHT, colour) child.override_background_color(Gtk.StateFlags.PRELIGHT, colour)
@ -830,10 +816,10 @@ def record_setting(device, setting, values):
def _record_setting(device, setting_class, values): def _record_setting(device, setting_class, values):
if logger.isEnabledFor(logging.DEBUG): if logger.isEnabledFor(logging.DEBUG):
logger.debug('on %s changing setting %s to %s', device, setting_class.name, values) logger.debug("on %s changing setting %s to %s", device, setting_class.name, values)
setting = next((s for s in device.settings if s.name == setting_class.name), None) setting = next((s for s in device.settings if s.name == setting_class.name), None)
if setting is None and logger.isEnabledFor(logging.DEBUG): if setting is None and logger.isEnabledFor(logging.DEBUG):
logger.debug('No setting for %s found on %s when trying to record a change made elsewhere', setting_class.name, device) logger.debug("No setting for %s found on %s when trying to record a change made elsewhere", setting_class.name, device)
if setting: if setting:
assert device == setting._device assert device == setting._device
if len(values) > 1: if len(values) > 1:

File diff suppressed because it is too large Load Diff

View File

@ -18,10 +18,10 @@
import logging import logging
import solaar.gtk as gtk
from gi.repository import Gtk from gi.repository import Gtk
import solaar.gtk as gtk
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# #
@ -29,12 +29,12 @@ logger = logging.getLogger(__name__)
# #
_LARGE_SIZE = 64 _LARGE_SIZE = 64
Gtk.IconSize.LARGE = Gtk.icon_size_register('large', _LARGE_SIZE, _LARGE_SIZE) Gtk.IconSize.LARGE = Gtk.icon_size_register("large", _LARGE_SIZE, _LARGE_SIZE)
# Gtk.IconSize.XLARGE = Gtk.icon_size_register('x-large', _LARGE_SIZE * 2, _LARGE_SIZE * 2) # Gtk.IconSize.XLARGE = Gtk.icon_size_register('x-large', _LARGE_SIZE * 2, _LARGE_SIZE * 2)
TRAY_INIT = 'solaar-init' TRAY_INIT = "solaar-init"
TRAY_OKAY = 'solaar' TRAY_OKAY = "solaar"
TRAY_ATTENTION = 'solaar-attention' TRAY_ATTENTION = "solaar-attention"
_default_theme = None _default_theme = None
@ -46,18 +46,18 @@ def _init_icon_paths():
_default_theme = Gtk.IconTheme.get_default() _default_theme = Gtk.IconTheme.get_default()
if logger.isEnabledFor(logging.DEBUG): if logger.isEnabledFor(logging.DEBUG):
logger.debug('icon theme paths: %s', _default_theme.get_search_path()) logger.debug("icon theme paths: %s", _default_theme.get_search_path())
if gtk.battery_icons_style == 'symbolic': if gtk.battery_icons_style == "symbolic":
global TRAY_OKAY global TRAY_OKAY
TRAY_OKAY = TRAY_INIT # use monochrome tray icon TRAY_OKAY = TRAY_INIT # use monochrome tray icon
if not _default_theme.has_icon('battery-good-symbolic'): if not _default_theme.has_icon("battery-good-symbolic"):
logger.warning('failed to detect symbolic icons') logger.warning("failed to detect symbolic icons")
gtk.battery_icons_style = 'regular' gtk.battery_icons_style = "regular"
if gtk.battery_icons_style == 'regular': if gtk.battery_icons_style == "regular":
if not _default_theme.has_icon('battery-good'): if not _default_theme.has_icon("battery-good"):
logger.warning('failed to detect icons') logger.warning("failed to detect icons")
gtk.battery_icons_style = 'solaar' gtk.battery_icons_style = "solaar"
# #
@ -68,10 +68,10 @@ def _init_icon_paths():
def battery(level=None, charging=False): def battery(level=None, charging=False):
icon_name = _battery_icon_name(level, charging) icon_name = _battery_icon_name(level, charging)
if not _default_theme.has_icon(icon_name): if not _default_theme.has_icon(icon_name):
logger.warning('icon %s not found in current theme', icon_name) logger.warning("icon %s not found in current theme", icon_name)
return TRAY_OKAY # use Solaar icon if battery icon not available return TRAY_OKAY # use Solaar icon if battery icon not available
elif logger.isEnabledFor(logging.DEBUG): elif logger.isEnabledFor(logging.DEBUG):
logger.debug('battery icon for %s:%s = %s', level, charging, icon_name) logger.debug("battery icon for %s:%s = %s", level, charging, icon_name)
return icon_name return icon_name
@ -85,11 +85,13 @@ def _battery_icon_name(level, charging):
_init_icon_paths() _init_icon_paths()
if level is None or level < 0: if level is None or level < 0:
return 'battery-missing' + ('-symbolic' if gtk.battery_icons_style == 'symbolic' else '') return "battery-missing" + ("-symbolic" if gtk.battery_icons_style == "symbolic" else "")
level_name = _first_res(level, ((90, 'full'), (30, 'good'), (20, 'low'), (5, 'caution'), (0, 'empty'))) level_name = _first_res(level, ((90, "full"), (30, "good"), (20, "low"), (5, "caution"), (0, "empty")))
return 'battery-%s%s%s' % ( return "battery-%s%s%s" % (
level_name, '-charging' if charging else '', '-symbolic' if gtk.battery_icons_style == 'symbolic' else '' level_name,
"-charging" if charging else "",
"-symbolic" if gtk.battery_icons_style == "symbolic" else "",
) )
@ -100,8 +102,8 @@ def _battery_icon_name(level, charging):
def lux(level=None): def lux(level=None):
if level is None or level < 0: if level is None or level < 0:
return 'light_unknown' return "light_unknown"
return 'solaar-light_%03d' % (20 * ((level + 50) // 100)) return "solaar-light_%03d" % (20 * ((level + 50) // 100))
# #
@ -111,7 +113,7 @@ def lux(level=None):
_ICON_SETS = {} _ICON_SETS = {}
def device_icon_set(name='_', kind=None): def device_icon_set(name="_", kind=None):
icon_set = _ICON_SETS.get(name) icon_set = _ICON_SETS.get(name)
if icon_set is None: if icon_set is None:
icon_set = Gtk.IconSet.new() icon_set = Gtk.IconSet.new()
@ -119,17 +121,17 @@ def device_icon_set(name='_', kind=None):
# names of possible icons, in reverse order of likelihood # names of possible icons, in reverse order of likelihood
# the theme will hopefully pick up the most appropriate # the theme will hopefully pick up the most appropriate
names = ['preferences-desktop-peripherals'] names = ["preferences-desktop-peripherals"]
if kind: if kind:
if str(kind) == 'numpad': if str(kind) == "numpad":
names += ('input-keyboard', 'input-dialpad') names += ("input-keyboard", "input-dialpad")
elif str(kind) == 'touchpad': elif str(kind) == "touchpad":
names += ('input-mouse', 'input-tablet') names += ("input-mouse", "input-tablet")
elif str(kind) == 'trackball': elif str(kind) == "trackball":
names += ('input-mouse', ) names += ("input-mouse",)
elif str(kind) == 'headset': elif str(kind) == "headset":
names += ('audio-headphones', 'audio-headset') names += ("audio-headphones", "audio-headset")
names += ('input-' + str(kind), ) names += ("input-" + str(kind),)
# names += (name.replace(' ', '-'),) # names += (name.replace(' ', '-'),)
source = Gtk.IconSource.new() source = Gtk.IconSource.new()
@ -174,4 +176,4 @@ def icon_file(name, size=_LARGE_SIZE):
# logger.debug("icon %s(%d) => %s", name, size, file_name) # logger.debug("icon %s(%d) => %s", name, size, file_name)
return file_name return file_name
logger.warning('icon %s(%d) not found in current theme', name, size) logger.warning("icon %s(%d) not found in current theme", name, size)

View File

@ -33,7 +33,8 @@ logger = logging.getLogger(__name__)
try: try:
import gi import gi
gi.require_version('Notify', '0.7')
gi.require_version("Notify", "0.7")
# this import is allowed to fail, in which case the entire feature is unavailable # this import is allowed to fail, in which case the entire feature is unavailable
from gi.repository import GLib, Notify from gi.repository import GLib, Notify
@ -44,7 +45,6 @@ except (ValueError, ImportError):
available = False available = False
if available: if available:
# cache references to shown notifications here, so if another status comes # cache references to shown notifications here, so if another status comes
# while its notification is still visible we don't create another one # while its notification is still visible we don't create another one
_notifications = {} _notifications = {}
@ -55,18 +55,18 @@ if available:
if available: if available:
if not Notify.is_initted(): if not Notify.is_initted():
if logger.isEnabledFor(logging.INFO): if logger.isEnabledFor(logging.INFO):
logger.info('starting desktop notifications') logger.info("starting desktop notifications")
try: try:
return Notify.init(NAME) return Notify.init(NAME)
except Exception: except Exception:
logger.exception('initializing desktop notifications') logger.exception("initializing desktop notifications")
available = False available = False
return available and Notify.is_initted() return available and Notify.is_initted()
def uninit(): def uninit():
if available and Notify.is_initted(): if available and Notify.is_initted():
if logger.isEnabledFor(logging.INFO): if logger.isEnabledFor(logging.INFO):
logger.info('stopping desktop notifications') logger.info("stopping desktop notifications")
_notifications.clear() _notifications.clear()
Notify.uninit() Notify.uninit()
@ -92,14 +92,14 @@ if available:
n.update(NAME, reason, icon_file) n.update(NAME, reason, icon_file)
n.set_urgency(Notify.Urgency.NORMAL) n.set_urgency(Notify.Urgency.NORMAL)
n.set_hint('desktop-entry', GLib.Variant('s', NAME.lower())) n.set_hint("desktop-entry", GLib.Variant("s", NAME.lower()))
try: try:
# if logger.isEnabledFor(logging.DEBUG): # if logger.isEnabledFor(logging.DEBUG):
# logger.debug("showing %s", n) # logger.debug("showing %s", n)
n.show() n.show()
except Exception: except Exception:
logger.exception('showing %s', n) logger.exception("showing %s", n)
def show(dev, reason=None, icon=None, progress=None): def show(dev, reason=None, icon=None, progress=None):
"""Show a notification with title and text. """Show a notification with title and text.
@ -116,11 +116,11 @@ if available:
if reason: if reason:
message = reason message = reason
elif dev.status is None: elif dev.status is None:
message = _('unpaired') message = _("unpaired")
elif bool(dev.status): elif bool(dev.status):
message = dev.status.to_string() or _('connected') message = dev.status.to_string() or _("connected")
else: else:
message = _('offline') message = _("offline")
# we need to use the filename here because the notifications daemon # we need to use the filename here because the notifications daemon
# is an external application that does not know about our icon sets # is an external application that does not know about our icon sets
@ -129,16 +129,16 @@ if available:
n.update(summary, message, icon_file) n.update(summary, message, icon_file)
urgency = Notify.Urgency.LOW if dev.status else Notify.Urgency.NORMAL urgency = Notify.Urgency.LOW if dev.status else Notify.Urgency.NORMAL
n.set_urgency(urgency) n.set_urgency(urgency)
n.set_hint('desktop-entry', GLib.Variant('s', NAME.lower())) n.set_hint("desktop-entry", GLib.Variant("s", NAME.lower()))
if progress: if progress:
n.set_hint('value', GLib.Variant('i', progress)) n.set_hint("value", GLib.Variant("i", progress))
try: try:
# if logger.isEnabledFor(logging.DEBUG): # if logger.isEnabledFor(logging.DEBUG):
# logger.debug("showing %s", n) # logger.debug("showing %s", n)
n.show() n.show()
except Exception: except Exception:
logger.exception('showing %s', n) logger.exception("showing %s", n)
else: else:
init = lambda: False init = lambda: False

View File

@ -21,6 +21,7 @@ import logging
from gi.repository import GLib, Gtk from gi.repository import GLib, Gtk
from logitech_receiver import hidpp10 as _hidpp10 from logitech_receiver import hidpp10 as _hidpp10
from logitech_receiver.status import KEYS as _K from logitech_receiver.status import KEYS as _K
from solaar.i18n import _, ngettext from solaar.i18n import _, ngettext
from . import icons as _icons from . import icons as _icons
@ -70,7 +71,7 @@ def _check_lock_state(assistant, receiver, count=2):
if not assistant.is_drawable(): if not assistant.is_drawable():
if logger.isEnabledFor(logging.DEBUG): if logger.isEnabledFor(logging.DEBUG):
logger.debug('assistant %s destroyed, bailing out', assistant) logger.debug("assistant %s destroyed, bailing out", assistant)
return False return False
if receiver.status.get(_K.ERROR): if receiver.status.get(_K.ERROR):
@ -94,7 +95,7 @@ def _check_lock_state(assistant, receiver, count=2):
): ):
return True return True
else: else:
_pairing_failed(assistant, receiver, 'failed to open pairing lock') _pairing_failed(assistant, receiver, "failed to open pairing lock")
return False return False
elif address and receiver.status.device_passkey and not passcode: elif address and receiver.status.device_passkey and not passcode:
passcode = receiver.status.device_passkey passcode = receiver.status.device_passkey
@ -106,7 +107,7 @@ def _check_lock_state(assistant, receiver, count=2):
# the actual device notification may arrive later so have a little patience # the actual device notification may arrive later so have a little patience
GLib.timeout_add(_STATUS_CHECK, _check_lock_state, assistant, receiver, count - 1) GLib.timeout_add(_STATUS_CHECK, _check_lock_state, assistant, receiver, count - 1)
else: else:
_pairing_failed(assistant, receiver, 'failed to open pairing lock') _pairing_failed(assistant, receiver, "failed to open pairing lock")
return False return False
return True return True
@ -114,20 +115,20 @@ def _check_lock_state(assistant, receiver, count=2):
def _show_passcode(assistant, receiver, passkey): def _show_passcode(assistant, receiver, passkey):
if logger.isEnabledFor(logging.DEBUG): if logger.isEnabledFor(logging.DEBUG):
logger.debug('%s show passkey: %s', receiver, passkey) logger.debug("%s show passkey: %s", receiver, passkey)
name = receiver.status.device_name name = receiver.status.device_name
authentication = receiver.status.device_authentication authentication = receiver.status.device_authentication
intro_text = _('%(receiver_name)s: pair new device') % {'receiver_name': receiver.name} intro_text = _("%(receiver_name)s: pair new device") % {"receiver_name": receiver.name}
page_text = _('Enter passcode on %(name)s.') % {'name': name} page_text = _("Enter passcode on %(name)s.") % {"name": name}
page_text += '\n' page_text += "\n"
if authentication & 0x01: if authentication & 0x01:
page_text += _('Type %(passcode)s and then press the enter key.') % {'passcode': receiver.status.device_passkey} page_text += _("Type %(passcode)s and then press the enter key.") % {"passcode": receiver.status.device_passkey}
else: else:
passcode = ', '.join([ passcode = ", ".join(
_('right') if bit == '1' else _('left') for bit in f'{int(receiver.status.device_passkey):010b}' [_("right") if bit == "1" else _("left") for bit in f"{int(receiver.status.device_passkey):010b}"]
]) )
page_text += _('Press %(code)s\nand then press left and right buttons simultaneously.') % {'code': passcode} page_text += _("Press %(code)s\nand then press left and right buttons simultaneously.") % {"code": passcode}
page = _create_page(assistant, Gtk.AssistantPageType.PROGRESS, intro_text, 'preferences-desktop-peripherals', page_text) page = _create_page(assistant, Gtk.AssistantPageType.PROGRESS, intro_text, "preferences-desktop-peripherals", page_text)
assistant.set_page_complete(page, True) assistant.set_page_complete(page, True)
assistant.next_page() assistant.next_page()
@ -135,10 +136,10 @@ def _show_passcode(assistant, receiver, passkey):
def _prepare(assistant, page, receiver): def _prepare(assistant, page, receiver):
index = assistant.get_current_page() index = assistant.get_current_page()
if logger.isEnabledFor(logging.DEBUG): if logger.isEnabledFor(logging.DEBUG):
logger.debug('prepare %s %d %s', assistant, index, page) logger.debug("prepare %s %d %s", assistant, index, page)
if index == 0: if index == 0:
if receiver.receiver_kind == 'bolt': if receiver.receiver_kind == "bolt":
if receiver.discover(timeout=_PAIRING_TIMEOUT): if receiver.discover(timeout=_PAIRING_TIMEOUT):
assert receiver.status.new_device is None assert receiver.status.new_device is None
assert receiver.status.get(_K.ERROR) is None assert receiver.status.get(_K.ERROR) is None
@ -147,7 +148,7 @@ def _prepare(assistant, page, receiver):
GLib.timeout_add(_STATUS_CHECK, _check_lock_state, assistant, receiver) GLib.timeout_add(_STATUS_CHECK, _check_lock_state, assistant, receiver)
assistant.set_page_complete(page, True) assistant.set_page_complete(page, True)
else: else:
GLib.idle_add(_pairing_failed, assistant, receiver, 'discovery did not start') GLib.idle_add(_pairing_failed, assistant, receiver, "discovery did not start")
elif receiver.set_lock(False, timeout=_PAIRING_TIMEOUT): elif receiver.set_lock(False, timeout=_PAIRING_TIMEOUT):
assert receiver.status.new_device is None assert receiver.status.new_device is None
assert receiver.status.get(_K.ERROR) is None assert receiver.status.get(_K.ERROR) is None
@ -156,19 +157,19 @@ def _prepare(assistant, page, receiver):
GLib.timeout_add(_STATUS_CHECK, _check_lock_state, assistant, receiver) GLib.timeout_add(_STATUS_CHECK, _check_lock_state, assistant, receiver)
assistant.set_page_complete(page, True) assistant.set_page_complete(page, True)
else: else:
GLib.idle_add(_pairing_failed, assistant, receiver, 'the pairing lock did not open') GLib.idle_add(_pairing_failed, assistant, receiver, "the pairing lock did not open")
else: else:
assistant.remove_page(0) assistant.remove_page(0)
def _finish(assistant, receiver): def _finish(assistant, receiver):
if logger.isEnabledFor(logging.DEBUG): if logger.isEnabledFor(logging.DEBUG):
logger.debug('finish %s', assistant) logger.debug("finish %s", assistant)
assistant.destroy() assistant.destroy()
receiver.status.new_device = None receiver.status.new_device = None
if receiver.status.lock_open: if receiver.status.lock_open:
if receiver.receiver_kind == 'bolt': if receiver.receiver_kind == "bolt":
receiver.pair_device('cancel') receiver.pair_device("cancel")
else: else:
receiver.set_lock() receiver.set_lock()
if receiver.status.discovering: if receiver.status.discovering:
@ -179,20 +180,20 @@ def _finish(assistant, receiver):
def _pairing_failed(assistant, receiver, error): def _pairing_failed(assistant, receiver, error):
if logger.isEnabledFor(logging.DEBUG): if logger.isEnabledFor(logging.DEBUG):
logger.debug('%s fail: %s', receiver, error) logger.debug("%s fail: %s", receiver, error)
assistant.commit() assistant.commit()
header = _('Pairing failed') + ': ' + _(str(error)) + '.' header = _("Pairing failed") + ": " + _(str(error)) + "."
if 'timeout' in str(error): if "timeout" in str(error):
text = _('Make sure your device is within range, and has a decent battery charge.') text = _("Make sure your device is within range, and has a decent battery charge.")
elif str(error) == 'device not supported': elif str(error) == "device not supported":
text = _('A new device was detected, but it is not compatible with this receiver.') text = _("A new device was detected, but it is not compatible with this receiver.")
elif 'many' in str(error): elif "many" in str(error):
text = _('More paired devices than receiver can support.') text = _("More paired devices than receiver can support.")
else: else:
text = _('No further details are available about the error.') text = _("No further details are available about the error.")
_create_page(assistant, Gtk.AssistantPageType.SUMMARY, header, 'dialog-error', text) _create_page(assistant, Gtk.AssistantPageType.SUMMARY, header, "dialog-error", text)
assistant.next_page() assistant.next_page()
assistant.commit() assistant.commit()
@ -201,11 +202,11 @@ def _pairing_failed(assistant, receiver, error):
def _pairing_succeeded(assistant, receiver, device): def _pairing_succeeded(assistant, receiver, device):
assert device assert device
if logger.isEnabledFor(logging.DEBUG): if logger.isEnabledFor(logging.DEBUG):
logger.debug('%s success: %s', receiver, device) logger.debug("%s success: %s", receiver, device)
page = _create_page(assistant, Gtk.AssistantPageType.SUMMARY) page = _create_page(assistant, Gtk.AssistantPageType.SUMMARY)
header = Gtk.Label(_('Found a new device:')) header = Gtk.Label(_("Found a new device:"))
header.set_alignment(0.5, 0) header.set_alignment(0.5, 0)
page.pack_start(header, False, False, 0) page.pack_start(header, False, False, 0)
@ -216,21 +217,21 @@ def _pairing_succeeded(assistant, receiver, device):
page.pack_start(device_icon, True, True, 0) page.pack_start(device_icon, True, True, 0)
device_label = Gtk.Label() device_label = Gtk.Label()
device_label.set_markup('<b>%s</b>' % device.name) device_label.set_markup("<b>%s</b>" % device.name)
device_label.set_alignment(0.5, 0) device_label.set_alignment(0.5, 0)
page.pack_start(device_label, True, True, 0) page.pack_start(device_label, True, True, 0)
hbox = Gtk.HBox(False, 8) hbox = Gtk.HBox(False, 8)
hbox.pack_start(Gtk.Label(' '), False, False, 0) hbox.pack_start(Gtk.Label(" "), False, False, 0)
hbox.set_property('expand', False) hbox.set_property("expand", False)
hbox.set_property('halign', Gtk.Align.CENTER) hbox.set_property("halign", Gtk.Align.CENTER)
page.pack_start(hbox, False, False, 0) page.pack_start(hbox, False, False, 0)
def _check_encrypted(dev): def _check_encrypted(dev):
if assistant.is_drawable(): if assistant.is_drawable():
if device.status.get(_K.LINK_ENCRYPTED) is False: if device.status.get(_K.LINK_ENCRYPTED) is False:
hbox.pack_start(Gtk.Image.new_from_icon_name('security-low', Gtk.IconSize.MENU), False, False, 0) hbox.pack_start(Gtk.Image.new_from_icon_name("security-low", Gtk.IconSize.MENU), False, False, 0)
hbox.pack_start(Gtk.Label(_('The wireless link is not encrypted') + '!'), False, False, 0) hbox.pack_start(Gtk.Label(_("The wireless link is not encrypted") + "!"), False, False, 0)
hbox.show_all() hbox.show_all()
else: else:
return True return True
@ -251,49 +252,53 @@ def create(receiver):
address = name = kind = authentication = passcode = None address = name = kind = authentication = passcode = None
assistant = Gtk.Assistant() assistant = Gtk.Assistant()
assistant.set_title(_('%(receiver_name)s: pair new device') % {'receiver_name': receiver.name}) assistant.set_title(_("%(receiver_name)s: pair new device") % {"receiver_name": receiver.name})
assistant.set_icon_name('list-add') assistant.set_icon_name("list-add")
assistant.set_size_request(400, 240) assistant.set_size_request(400, 240)
assistant.set_resizable(False) assistant.set_resizable(False)
assistant.set_role('pair-device') assistant.set_role("pair-device")
if receiver.receiver_kind == 'unifying': if receiver.receiver_kind == "unifying":
page_text = _('Unifying receivers are only compatible with Unifying devices.') page_text = _("Unifying receivers are only compatible with Unifying devices.")
elif receiver.receiver_kind == 'bolt': elif receiver.receiver_kind == "bolt":
page_text = _('Bolt receivers are only compatible with Bolt devices.') page_text = _("Bolt receivers are only compatible with Bolt devices.")
else: else:
page_text = _('Other receivers are only compatible with a few devices.') page_text = _("Other receivers are only compatible with a few devices.")
page_text += '\n' page_text += "\n"
page_text += _('The device must not be paired with a nearby powered-on receiver.') page_text += _("The device must not be paired with a nearby powered-on receiver.")
page_text += '\n\n' page_text += "\n\n"
if receiver.receiver_kind == 'bolt': if receiver.receiver_kind == "bolt":
page_text += _('Press a pairing button or key until the pairing light flashes quickly.') page_text += _("Press a pairing button or key until the pairing light flashes quickly.")
page_text += '\n' page_text += "\n"
page_text += _('You may have to first turn the device off and on again.') page_text += _("You may have to first turn the device off and on again.")
else: else:
page_text += _('Turn on the device you want to pair.') page_text += _("Turn on the device you want to pair.")
page_text += '\n' page_text += "\n"
page_text += _('If the device is already turned on, turn it off and on again.') page_text += _("If the device is already turned on, turn it off and on again.")
if receiver.remaining_pairings() and receiver.remaining_pairings() >= 0: if receiver.remaining_pairings() and receiver.remaining_pairings() >= 0:
page_text += ngettext( page_text += (
'\n\nThis receiver has %d pairing remaining.', '\n\nThis receiver has %d pairings remaining.', ngettext(
receiver.remaining_pairings() "\n\nThis receiver has %d pairing remaining.",
) % receiver.remaining_pairings() "\n\nThis receiver has %d pairings remaining.",
page_text += _('\nCancelling at this point will not use up a pairing.') receiver.remaining_pairings(),
)
% receiver.remaining_pairings()
)
page_text += _("\nCancelling at this point will not use up a pairing.")
intro_text = _('%(receiver_name)s: pair new device') % {'receiver_name': receiver.name} intro_text = _("%(receiver_name)s: pair new device") % {"receiver_name": receiver.name}
page_intro = _create_page( page_intro = _create_page(
assistant, Gtk.AssistantPageType.PROGRESS, intro_text, 'preferences-desktop-peripherals', page_text assistant, Gtk.AssistantPageType.PROGRESS, intro_text, "preferences-desktop-peripherals", page_text
) )
spinner = Gtk.Spinner() spinner = Gtk.Spinner()
spinner.set_visible(True) spinner.set_visible(True)
page_intro.pack_end(spinner, True, True, 24) page_intro.pack_end(spinner, True, True, 24)
assistant.connect('prepare', _prepare, receiver) assistant.connect("prepare", _prepare, receiver)
assistant.connect('cancel', _finish, receiver) assistant.connect("cancel", _finish, receiver)
assistant.connect('close', _finish, receiver) assistant.connect("close", _finish, receiver)
return assistant return assistant

View File

@ -22,11 +22,13 @@ import os
from time import time as _timestamp from time import time as _timestamp
import gi import gi
import solaar.gtk as gtk
from gi.repository import GLib, Gtk from gi.repository import GLib, Gtk
from gi.repository.Gdk import ScrollDirection from gi.repository.Gdk import ScrollDirection
from logitech_receiver.status import KEYS as _K from logitech_receiver.status import KEYS as _K
import solaar.gtk as gtk
from solaar import NAME from solaar import NAME
from solaar.i18n import _ from solaar.i18n import _
@ -55,13 +57,13 @@ def _create_menu(quit_handler):
# per-device menu entries will be generated as-needed # per-device menu entries will be generated as-needed
no_receiver = Gtk.MenuItem.new_with_label(_('No supported device found')) no_receiver = Gtk.MenuItem.new_with_label(_("No supported device found"))
no_receiver.set_sensitive(False) no_receiver.set_sensitive(False)
menu.append(no_receiver) menu.append(no_receiver)
menu.append(Gtk.SeparatorMenuItem.new()) menu.append(Gtk.SeparatorMenuItem.new())
menu.append(_make('help-about', _('About %s') % NAME, _show_about_window, stock_id='help-about').create_menu_item()) menu.append(_make("help-about", _("About %s") % NAME, _show_about_window, stock_id="help-about").create_menu_item())
menu.append(_make('application-exit', _('Quit %s') % NAME, quit_handler, stock_id='application-exit').create_menu_item()) menu.append(_make("application-exit", _("Quit %s") % NAME, quit_handler, stock_id="application-exit").create_menu_item())
menu.show_all() menu.show_all()
@ -140,26 +142,28 @@ def _scroll(tray_icon, event, direction=None):
_picked_device = candidate or _picked_device _picked_device = candidate or _picked_device
if logger.isEnabledFor(logging.DEBUG): if logger.isEnabledFor(logging.DEBUG):
logger.debug('scroll: picked %s', _picked_device) logger.debug("scroll: picked %s", _picked_device)
_update_tray_icon() _update_tray_icon()
try: try:
try: try:
gi.require_version('AyatanaAppIndicator3', '0.1') gi.require_version("AyatanaAppIndicator3", "0.1")
from gi.repository import AyatanaAppIndicator3 as AppIndicator3 from gi.repository import AyatanaAppIndicator3 as AppIndicator3
ayatana_appindicator_found = True ayatana_appindicator_found = True
except ValueError: except ValueError:
try: try:
gi.require_version('AppIndicator3', '0.1') gi.require_version("AppIndicator3", "0.1")
from gi.repository import AppIndicator3 from gi.repository import AppIndicator3
ayatana_appindicator_found = False ayatana_appindicator_found = False
except ValueError: except ValueError:
# treat unavailable versions the same as unavailable packages # treat unavailable versions the same as unavailable packages
raise ImportError raise ImportError
if logger.isEnabledFor(logging.DEBUG): if logger.isEnabledFor(logging.DEBUG):
logger.debug('using %sAppIndicator3' % ('Ayatana ' if ayatana_appindicator_found else '')) logger.debug("using %sAppIndicator3" % ("Ayatana " if ayatana_appindicator_found else ""))
# Defense against AppIndicator3 bug that treats files in current directory as icon files # Defense against AppIndicator3 bug that treats files in current directory as icon files
# https://bugs.launchpad.net/ubuntu/+source/libappindicator/+bug/1363277 # https://bugs.launchpad.net/ubuntu/+source/libappindicator/+bug/1363277
@ -175,7 +179,7 @@ try:
def _create(menu): def _create(menu):
_icons._init_icon_paths() _icons._init_icon_paths()
ind = AppIndicator3.Indicator.new( ind = AppIndicator3.Indicator.new(
'indicator-solaar', _icon_file(_icons.TRAY_INIT), AppIndicator3.IndicatorCategory.HARDWARE "indicator-solaar", _icon_file(_icons.TRAY_INIT), AppIndicator3.IndicatorCategory.HARDWARE
) )
ind.set_title(NAME) ind.set_title(NAME)
ind.set_status(AppIndicator3.IndicatorStatus.ACTIVE) ind.set_status(AppIndicator3.IndicatorStatus.ACTIVE)
@ -183,7 +187,7 @@ try:
# ind.set_label(NAME, NAME) # ind.set_label(NAME, NAME)
ind.set_menu(menu) ind.set_menu(menu)
ind.connect('scroll-event', _scroll) ind.connect("scroll-event", _scroll)
return ind return ind
@ -194,19 +198,19 @@ try:
indicator.set_status(AppIndicator3.IndicatorStatus.ACTIVE) indicator.set_status(AppIndicator3.IndicatorStatus.ACTIVE)
def _update_tray_icon(): def _update_tray_icon():
if _picked_device and gtk.battery_icons_style != 'solaar': if _picked_device and gtk.battery_icons_style != "solaar":
_ignore, _ignore, name, device_status = _picked_device _ignore, _ignore, name, device_status = _picked_device
battery_level = device_status.get(_K.BATTERY_LEVEL) battery_level = device_status.get(_K.BATTERY_LEVEL)
battery_charging = device_status.get(_K.BATTERY_CHARGING) battery_charging = device_status.get(_K.BATTERY_CHARGING)
tray_icon_name = _icons.battery(battery_level, battery_charging) tray_icon_name = _icons.battery(battery_level, battery_charging)
description = '%s: %s' % (name, device_status.to_string()) description = "%s: %s" % (name, device_status.to_string())
else: else:
# there may be a receiver, but no peripherals # there may be a receiver, but no peripherals
tray_icon_name = _icons.TRAY_OKAY if _devices_info else _icons.TRAY_INIT tray_icon_name = _icons.TRAY_OKAY if _devices_info else _icons.TRAY_INIT
description_lines = _generate_description_lines() description_lines = _generate_description_lines()
description = '\n'.join(description_lines).rstrip('\n') description = "\n".join(description_lines).rstrip("\n")
# icon_file = _icons.icon_file(icon_name, _TRAY_ICON_SIZE) # icon_file = _icons.icon_file(icon_name, _TRAY_ICON_SIZE)
_icon.set_icon_full(_icon_file(tray_icon_name), description) _icon.set_icon_full(_icon_file(tray_icon_name), description)
@ -224,18 +228,17 @@ try:
GLib.timeout_add(10 * 1000, _icon.set_status, AppIndicator3.IndicatorStatus.ACTIVE) GLib.timeout_add(10 * 1000, _icon.set_status, AppIndicator3.IndicatorStatus.ACTIVE)
except ImportError: except ImportError:
if logger.isEnabledFor(logging.DEBUG): if logger.isEnabledFor(logging.DEBUG):
logger.debug('using StatusIcon') logger.debug("using StatusIcon")
def _create(menu): def _create(menu):
icon = Gtk.StatusIcon.new_from_icon_name(_icons.TRAY_INIT) icon = Gtk.StatusIcon.new_from_icon_name(_icons.TRAY_INIT)
icon.set_name(NAME) icon.set_name(NAME)
icon.set_title(NAME) icon.set_title(NAME)
icon.set_tooltip_text(NAME) icon.set_tooltip_text(NAME)
icon.connect('activate', _window_toggle) icon.connect("activate", _window_toggle)
icon.connect('scroll-event', _scroll) icon.connect("scroll-event", _scroll)
icon.connect('popup-menu', lambda icon, button, time: menu.popup(None, None, icon.position_menu, icon, button, time)) icon.connect("popup-menu", lambda icon, button, time: menu.popup(None, None, icon.position_menu, icon, button, time))
return icon return icon
@ -247,10 +250,10 @@ except ImportError:
def _update_tray_icon(): def _update_tray_icon():
tooltip_lines = _generate_tooltip_lines() tooltip_lines = _generate_tooltip_lines()
tooltip = '\n'.join(tooltip_lines).rstrip('\n') tooltip = "\n".join(tooltip_lines).rstrip("\n")
_icon.set_tooltip_markup(tooltip) _icon.set_tooltip_markup(tooltip)
if _picked_device and gtk.battery_icons_style != 'solaar': if _picked_device and gtk.battery_icons_style != "solaar":
_ignore, _ignore, name, device_status = _picked_device _ignore, _ignore, name, device_status = _picked_device
battery_level = device_status.get(_K.BATTERY_LEVEL) battery_level = device_status.get(_K.BATTERY_LEVEL)
battery_charging = device_status.get(_K.BATTERY_CHARGING) battery_charging = device_status.get(_K.BATTERY_CHARGING)
@ -291,7 +294,7 @@ except ImportError:
def _generate_tooltip_lines(): def _generate_tooltip_lines():
if not _devices_info: if not _devices_info:
yield '<b>%s</b>: ' % NAME + _('no receiver') yield "<b>%s</b>: " % NAME + _("no receiver")
return return
yield from _generate_description_lines() yield from _generate_description_lines()
@ -299,7 +302,7 @@ def _generate_tooltip_lines():
def _generate_description_lines(): def _generate_description_lines():
if not _devices_info: if not _devices_info:
yield _('no receiver') yield _("no receiver")
return return
for _ignore, number, name, status in _devices_info: for _ignore, number, name, status in _devices_info:
@ -308,16 +311,16 @@ def _generate_description_lines():
p = status.to_string() p = status.to_string()
if p: # does it have any properties to print? if p: # does it have any properties to print?
yield '<b>%s</b>' % name yield "<b>%s</b>" % name
if status: if status:
yield '\t%s' % p yield "\t%s" % p
else: else:
yield '\t%s <small>(' % p + _('offline') + ')</small>' yield "\t%s <small>(" % p + _("offline") + ")</small>"
else: else:
if status: if status:
yield '<b>%s</b> <small>(' % name + _('no status') + ')</small>' yield "<b>%s</b> <small>(" % name + _("no status") + ")</small>"
else: else:
yield '<b>%s</b> <small>(' % name + _('offline') + ')</small>' yield "<b>%s</b> <small>(" % name + _("offline") + ")</small>"
def _pick_device_with_lowest_battery(): def _pick_device_with_lowest_battery():
@ -337,7 +340,7 @@ def _pick_device_with_lowest_battery():
picked_level = level or 0 picked_level = level or 0
if logger.isEnabledFor(logging.DEBUG): if logger.isEnabledFor(logging.DEBUG):
logger.debug('picked device with lowest battery: %s', picked) logger.debug("picked device with lowest battery: %s", picked)
return picked return picked
@ -369,11 +372,11 @@ def _add_device(device):
new_device_info = (receiver_path, device.number, device.name, device.status) new_device_info = (receiver_path, device.number, device.name, device.status)
_devices_info.insert(index, new_device_info) _devices_info.insert(index, new_device_info)
label_prefix = ' ' label_prefix = " "
new_menu_item = Gtk.ImageMenuItem.new_with_label((label_prefix if device.number else '') + device.name) new_menu_item = Gtk.ImageMenuItem.new_with_label((label_prefix if device.number else "") + device.name)
new_menu_item.set_image(Gtk.Image()) new_menu_item.set_image(Gtk.Image())
new_menu_item.show_all() new_menu_item.show_all()
new_menu_item.connect('activate', _window_popup, receiver_path, device.number) new_menu_item.connect("activate", _window_popup, receiver_path, device.number)
_menu.insert(new_menu_item, index) _menu.insert(new_menu_item, index)
return index return index
@ -402,7 +405,7 @@ def _add_receiver(receiver):
icon_set = _icons.device_icon_set(receiver.name) icon_set = _icons.device_icon_set(receiver.name)
new_menu_item.set_image(Gtk.Image().new_from_icon_name(icon_set.names[0], _MENU_ICON_SIZE)) new_menu_item.set_image(Gtk.Image().new_from_icon_name(icon_set.names[0], _MENU_ICON_SIZE))
new_menu_item.show_all() new_menu_item.show_all()
new_menu_item.connect('activate', _window_popup, receiver.path) new_menu_item.connect("activate", _window_popup, receiver.path)
_menu.insert(new_menu_item, index) _menu.insert(new_menu_item, index)
return 0 return 0
@ -421,7 +424,7 @@ def _remove_receiver(receiver):
def _update_menu_item(index, device): def _update_menu_item(index, device):
if device is None or device.status is None: if device is None or device.status is None:
logger.warning('updating an inactive device %s, assuming disconnected', device) logger.warning("updating an inactive device %s, assuming disconnected", device)
return None return None
menu_items = _menu.get_children() menu_items = _menu.get_children()
@ -431,7 +434,7 @@ def _update_menu_item(index, device):
charging = device.status.get(_K.BATTERY_CHARGING) charging = device.status.get(_K.BATTERY_CHARGING)
icon_name = _icons.battery(level, charging) icon_name = _icons.battery(level, charging)
menu_item.set_label((' ' if 0 < device.number <= 6 else '') + device.name + ': ' + device.status.to_string()) menu_item.set_label((" " if 0 < device.number <= 6 else "") + device.name + ": " + device.status.to_string())
image_widget = menu_item.get_image() image_widget = menu_item.get_image()
image_widget.set_sensitive(bool(device.online)) image_widget.set_sensitive(bool(device.online))
_update_menu_icon(image_widget, icon_name) _update_menu_icon(image_widget, icon_name)

View File

@ -25,6 +25,7 @@ from logitech_receiver import hidpp10 as _hidpp10
from logitech_receiver.common import NamedInt as _NamedInt from logitech_receiver.common import NamedInt as _NamedInt
from logitech_receiver.common import NamedInts as _NamedInts from logitech_receiver.common import NamedInts as _NamedInts
from logitech_receiver.status import KEYS as _K from logitech_receiver.status import KEYS as _K
from solaar import NAME from solaar import NAME
from solaar.i18n import _, ngettext from solaar.i18n import _, ngettext
@ -37,7 +38,7 @@ from .diversion_rules import show_window as _show_diversion_window
# from solaar import __version__ as VERSION # from solaar import __version__ as VERSION
gi.require_version('Gdk', '3.0') gi.require_version("Gdk", "3.0")
from gi.repository import Gdk, GLib, Gtk # NOQA: E402 from gi.repository import Gdk, GLib, Gtk # NOQA: E402
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -52,10 +53,10 @@ _TREE_ICON_SIZE = Gtk.IconSize.BUTTON
_INFO_ICON_SIZE = Gtk.IconSize.LARGE_TOOLBAR _INFO_ICON_SIZE = Gtk.IconSize.LARGE_TOOLBAR
_DEVICE_ICON_SIZE = Gtk.IconSize.DND _DEVICE_ICON_SIZE = Gtk.IconSize.DND
try: try:
gi.check_version('3.7.4') gi.check_version("3.7.4")
_CAN_SET_ROW_NONE = None _CAN_SET_ROW_NONE = None
except (ValueError, AttributeError): except (ValueError, AttributeError):
_CAN_SET_ROW_NONE = '' _CAN_SET_ROW_NONE = ""
# tree model columns # tree model columns
_COLUMN = _NamedInts(PATH=0, NUMBER=1, ACTIVE=2, NAME=3, ICON=4, STATUS_TEXT=5, STATUS_ICON=6, DEVICE=7) _COLUMN = _NamedInts(PATH=0, NUMBER=1, ACTIVE=2, NAME=3, ICON=4, STATUS_TEXT=5, STATUS_ICON=6, DEVICE=7)
@ -78,7 +79,7 @@ def _new_button(label, icon_name=None, icon_size=_NORMAL_BUTTON_ICON_SIZE, toolt
c.pack_start(Gtk.Label(label), True, True, 0) c.pack_start(Gtk.Label(label), True, True, 0)
b.add(c) b.add(c)
if clicked is not None: if clicked is not None:
b.connect('clicked', clicked) b.connect("clicked", clicked)
if tooltip: if tooltip:
b.set_tooltip_text(tooltip) b.set_tooltip_text(tooltip)
if not label and icon_size < _NORMAL_BUTTON_ICON_SIZE: if not label and icon_size < _NORMAL_BUTTON_ICON_SIZE:
@ -95,11 +96,11 @@ def _create_receiver_panel():
p._count.set_alignment(0, 0.5) p._count.set_alignment(0, 0.5)
p.pack_start(p._count, True, True, 0) p.pack_start(p._count, True, True, 0)
p._scanning = Gtk.Label(_('Scanning') + '...') p._scanning = Gtk.Label(_("Scanning") + "...")
p._spinner = Gtk.Spinner() p._spinner = Gtk.Spinner()
bp = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 8) bp = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 8)
bp.pack_start(Gtk.Label(' '), True, True, 0) bp.pack_start(Gtk.Label(" "), True, True, 0)
bp.pack_start(p._scanning, False, False, 0) bp.pack_start(p._scanning, False, False, 0)
bp.pack_end(p._spinner, False, False, 0) bp.pack_end(p._spinner, False, False, 0)
p.pack_end(bp, False, False, 0) p.pack_end(bp, False, False, 0)
@ -128,14 +129,14 @@ def _create_device_panel():
return b return b
p._battery = _status_line(_('Battery')) p._battery = _status_line(_("Battery"))
p.pack_start(p._battery, False, False, 0) p.pack_start(p._battery, False, False, 0)
p._secure = _status_line(_('Wireless Link')) p._secure = _status_line(_("Wireless Link"))
p._secure._icon.set_from_icon_name('dialog-warning', _INFO_ICON_SIZE) p._secure._icon.set_from_icon_name("dialog-warning", _INFO_ICON_SIZE)
p.pack_start(p._secure, False, False, 0) p.pack_start(p._secure, False, False, 0)
p._lux = _status_line(_('Lighting')) p._lux = _status_line(_("Lighting"))
p.pack_start(p._lux, False, False, 0) p.pack_start(p._lux, False, False, 0)
p.pack_start(Gtk.Separator.new(Gtk.Orientation.HORIZONTAL), False, False, 0) # spacer p.pack_start(Gtk.Separator.new(Gtk.Orientation.HORIZONTAL), False, False, 0) # spacer
@ -167,11 +168,11 @@ def _create_buttons_box():
bb._details = _new_button( bb._details = _new_button(
None, None,
'dialog-information', "dialog-information",
_SMALL_BUTTON_ICON_SIZE, _SMALL_BUTTON_ICON_SIZE,
tooltip=_('Show Technical Details'), tooltip=_("Show Technical Details"),
toggle=True, toggle=True,
clicked=_update_details clicked=_update_details,
) )
bb.add(bb._details) bb.add(bb._details)
bb.set_child_secondary(bb._details, True) bb.set_child_secondary(bb._details, True)
@ -185,7 +186,7 @@ def _create_buttons_box():
assert receiver.kind is None assert receiver.kind is None
_action.pair(_window, receiver) _action.pair(_window, receiver)
bb._pair = _new_button(_('Pair new device'), 'list-add', clicked=_pair_new_device) bb._pair = _new_button(_("Pair new device"), "list-add", clicked=_pair_new_device)
bb.add(bb._pair) bb.add(bb._pair)
def _unpair_current_device(trigger): def _unpair_current_device(trigger):
@ -196,7 +197,7 @@ def _create_buttons_box():
assert device.kind is not None assert device.kind is not None
_action.unpair(_window, device) _action.unpair(_window, device)
bb._unpair = _new_button(_('Unpair'), 'edit-delete', clicked=_unpair_current_device) bb._unpair = _new_button(_("Unpair"), "edit-delete", clicked=_unpair_current_device)
bb.add(bb._unpair) bb.add(bb._unpair)
return bb return bb
@ -204,7 +205,7 @@ def _create_buttons_box():
def _create_empty_panel(): def _create_empty_panel():
p = Gtk.Label() p = Gtk.Label()
p.set_markup('<small>' + _('Select a device') + '</small>') p.set_markup("<small>" + _("Select a device") + "</small>")
p.set_sensitive(False) p.set_sensitive(False)
return p return p
@ -213,7 +214,7 @@ def _create_empty_panel():
def _create_info_panel(): def _create_info_panel():
p = Gtk.Box.new(Gtk.Orientation.VERTICAL, 4) p = Gtk.Box.new(Gtk.Orientation.VERTICAL, 4)
p._title = Gtk.Label(' ') p._title = Gtk.Label(" ")
p._title.set_alignment(0, 0.5) p._title.set_alignment(0, 0.5)
p._icon = Gtk.Image() p._icon = Gtk.Image()
@ -256,34 +257,34 @@ def _create_tree(model):
tree.set_row_separator_func(_is_separator, None) tree.set_row_separator_func(_is_separator, None)
icon_cell_renderer = Gtk.CellRendererPixbuf() icon_cell_renderer = Gtk.CellRendererPixbuf()
icon_cell_renderer.set_property('stock-size', _TREE_ICON_SIZE) icon_cell_renderer.set_property("stock-size", _TREE_ICON_SIZE)
icon_column = Gtk.TreeViewColumn('Icon', icon_cell_renderer) icon_column = Gtk.TreeViewColumn("Icon", icon_cell_renderer)
icon_column.add_attribute(icon_cell_renderer, 'sensitive', _COLUMN.ACTIVE) icon_column.add_attribute(icon_cell_renderer, "sensitive", _COLUMN.ACTIVE)
icon_column.add_attribute(icon_cell_renderer, 'icon-name', _COLUMN.ICON) icon_column.add_attribute(icon_cell_renderer, "icon-name", _COLUMN.ICON)
tree.append_column(icon_column) tree.append_column(icon_column)
name_cell_renderer = Gtk.CellRendererText() name_cell_renderer = Gtk.CellRendererText()
name_column = Gtk.TreeViewColumn('device name', name_cell_renderer) name_column = Gtk.TreeViewColumn("device name", name_cell_renderer)
name_column.add_attribute(name_cell_renderer, 'sensitive', _COLUMN.ACTIVE) name_column.add_attribute(name_cell_renderer, "sensitive", _COLUMN.ACTIVE)
name_column.add_attribute(name_cell_renderer, 'text', _COLUMN.NAME) name_column.add_attribute(name_cell_renderer, "text", _COLUMN.NAME)
name_column.set_expand(True) name_column.set_expand(True)
tree.append_column(name_column) tree.append_column(name_column)
tree.set_expander_column(name_column) tree.set_expander_column(name_column)
status_cell_renderer = Gtk.CellRendererText() status_cell_renderer = Gtk.CellRendererText()
status_cell_renderer.set_property('scale', 0.85) status_cell_renderer.set_property("scale", 0.85)
status_cell_renderer.set_property('xalign', 1) status_cell_renderer.set_property("xalign", 1)
status_column = Gtk.TreeViewColumn('status text', status_cell_renderer) status_column = Gtk.TreeViewColumn("status text", status_cell_renderer)
status_column.add_attribute(status_cell_renderer, 'sensitive', _COLUMN.ACTIVE) status_column.add_attribute(status_cell_renderer, "sensitive", _COLUMN.ACTIVE)
status_column.add_attribute(status_cell_renderer, 'text', _COLUMN.STATUS_TEXT) status_column.add_attribute(status_cell_renderer, "text", _COLUMN.STATUS_TEXT)
status_column.set_expand(True) status_column.set_expand(True)
tree.append_column(status_column) tree.append_column(status_column)
battery_cell_renderer = Gtk.CellRendererPixbuf() battery_cell_renderer = Gtk.CellRendererPixbuf()
battery_cell_renderer.set_property('stock-size', _TREE_ICON_SIZE) battery_cell_renderer.set_property("stock-size", _TREE_ICON_SIZE)
battery_column = Gtk.TreeViewColumn('status icon', battery_cell_renderer) battery_column = Gtk.TreeViewColumn("status icon", battery_cell_renderer)
battery_column.add_attribute(battery_cell_renderer, 'sensitive', _COLUMN.ACTIVE) battery_column.add_attribute(battery_cell_renderer, "sensitive", _COLUMN.ACTIVE)
battery_column.add_attribute(battery_cell_renderer, 'icon-name', _COLUMN.STATUS_ICON) battery_column.add_attribute(battery_cell_renderer, "icon-name", _COLUMN.STATUS_ICON)
tree.append_column(battery_column) tree.append_column(battery_column)
return tree return tree
@ -296,7 +297,7 @@ def _create_window_layout():
assert _empty is not None assert _empty is not None
assert _tree.get_selection().get_mode() == Gtk.SelectionMode.SINGLE assert _tree.get_selection().get_mode() == Gtk.SelectionMode.SINGLE
_tree.get_selection().connect('changed', _device_selected) _tree.get_selection().connect("changed", _device_selected)
tree_scroll = Gtk.ScrolledWindow() tree_scroll = Gtk.ScrolledWindow()
tree_scroll.add(_tree) tree_scroll.add(_tree)
@ -316,12 +317,12 @@ def _create_window_layout():
bottom_buttons_box = Gtk.ButtonBox(Gtk.Orientation.HORIZONTAL) bottom_buttons_box = Gtk.ButtonBox(Gtk.Orientation.HORIZONTAL)
bottom_buttons_box.set_layout(Gtk.ButtonBoxStyle.START) bottom_buttons_box.set_layout(Gtk.ButtonBoxStyle.START)
bottom_buttons_box.set_spacing(20) bottom_buttons_box.set_spacing(20)
quit_button = _new_button(_('Quit %s') % NAME, 'application-exit', _SMALL_BUTTON_ICON_SIZE, clicked=destroy) quit_button = _new_button(_("Quit %s") % NAME, "application-exit", _SMALL_BUTTON_ICON_SIZE, clicked=destroy)
bottom_buttons_box.add(quit_button) bottom_buttons_box.add(quit_button)
about_button = _new_button(_('About %s') % NAME, 'help-about', _SMALL_BUTTON_ICON_SIZE, clicked=_show_about_window) about_button = _new_button(_("About %s") % NAME, "help-about", _SMALL_BUTTON_ICON_SIZE, clicked=_show_about_window)
bottom_buttons_box.add(about_button) bottom_buttons_box.add(about_button)
diversion_button = _new_button( diversion_button = _new_button(
_('Rule Editor'), '', _SMALL_BUTTON_ICON_SIZE, clicked=lambda *_trigger: _show_diversion_window(_model) _("Rule Editor"), "", _SMALL_BUTTON_ICON_SIZE, clicked=lambda *_trigger: _show_diversion_window(_model)
) )
bottom_buttons_box.add(diversion_button) bottom_buttons_box.add(diversion_button)
bottom_buttons_box.set_child_secondary(diversion_button, True) bottom_buttons_box.set_child_secondary(diversion_button, True)
@ -345,12 +346,12 @@ def _create_window_layout():
def _create(delete_action): def _create(delete_action):
window = Gtk.Window() window = Gtk.Window()
window.set_title(NAME) window.set_title(NAME)
window.set_role('status-window') window.set_role("status-window")
# window.set_type_hint(Gdk.WindowTypeHint.UTILITY) # window.set_type_hint(Gdk.WindowTypeHint.UTILITY)
# window.set_skip_taskbar_hint(True) # window.set_skip_taskbar_hint(True)
# window.set_skip_pager_hint(True) # window.set_skip_pager_hint(True)
window.connect('delete-event', delete_action) window.connect("delete-event", delete_action)
vbox = _create_window_layout() vbox = _create_window_layout()
window.add(vbox) window.add(vbox)
@ -362,7 +363,7 @@ def _create(delete_action):
window.set_position(Gtk.WindowPosition.CENTER) window.set_position(Gtk.WindowPosition.CENTER)
style = window.get_style_context() style = window.get_style_context()
style.add_class('solaar') style.add_class("solaar")
return window return window
@ -421,7 +422,7 @@ def _receiver_row(receiver_path, receiver=None):
row_data = (receiver_path, 0, True, receiver.name, icon_name, status_text, status_icon, receiver) row_data = (receiver_path, 0, True, receiver.name, icon_name, status_text, status_icon, receiver)
assert len(row_data) == len(_TREE_SEPATATOR) assert len(row_data) == len(_TREE_SEPATATOR)
if logger.isEnabledFor(logging.DEBUG): if logger.isEnabledFor(logging.DEBUG):
logger.debug('new receiver row %s', row_data) logger.debug("new receiver row %s", row_data)
item = _model.append(None, row_data) item = _model.append(None, row_data)
if _TREE_SEPATATOR: if _TREE_SEPATATOR:
_model.append(None, _TREE_SEPATATOR) _model.append(None, _TREE_SEPATATOR)
@ -446,8 +447,9 @@ def _device_row(receiver_path, device_number, device=None):
while item: while item:
if _model.get_value(item, _COLUMN.PATH) != receiver_path: if _model.get_value(item, _COLUMN.PATH) != receiver_path:
logger.warning( logger.warning(
'path for device row %s different from path for receiver %s', _model.get_value(item, _COLUMN.PATH), "path for device row %s different from path for receiver %s",
receiver_path _model.get_value(item, _COLUMN.PATH),
receiver_path,
) )
item_number = _model.get_value(item, _COLUMN.NUMBER) item_number = _model.get_value(item, _COLUMN.NUMBER)
if item_number == device_number: if item_number == device_number:
@ -463,11 +465,18 @@ def _device_row(receiver_path, device_number, device=None):
status_text = None status_text = None
status_icon = None status_icon = None
row_data = ( row_data = (
receiver_path, device_number, bool(device.online), device.codename, icon_name, status_text, status_icon, device receiver_path,
device_number,
bool(device.online),
device.codename,
icon_name,
status_text,
status_icon,
device,
) )
assert len(row_data) == len(_TREE_SEPATATOR) assert len(row_data) == len(_TREE_SEPATATOR)
if logger.isEnabledFor(logging.DEBUG): if logger.isEnabledFor(logging.DEBUG):
logger.debug('new device row %s at index %d', row_data, new_child_index) logger.debug("new device row %s at index %d", row_data, new_child_index)
item = _model.insert(receiver_row, new_child_index, row_data) item = _model.insert(receiver_row, new_child_index, row_data)
return item or None return item or None
@ -489,7 +498,7 @@ def select(receiver_path, device_number=None):
selection = _tree.get_selection() selection = _tree.get_selection()
selection.select_iter(item) selection.select_iter(item)
else: else:
logger.warning('select(%s, %s) failed to find an item', receiver_path, device_number) logger.warning("select(%s, %s) failed to find an item", receiver_path, device_number)
def _hide(w, _ignore=None): def _hide(w, _ignore=None):
@ -532,57 +541,57 @@ def _update_details(button):
# If read_all is False, only return stuff that is ~100% already # If read_all is False, only return stuff that is ~100% already
# cached, and involves no HID++ calls. # cached, and involves no HID++ calls.
yield (_('Path'), device.path) yield (_("Path"), device.path)
if device.kind is None: if device.kind is None:
# 046d is the Logitech vendor id # 046d is the Logitech vendor id
yield (_('USB ID'), '046d:' + device.product_id) yield (_("USB ID"), "046d:" + device.product_id)
if read_all: if read_all:
yield (_('Serial'), device.serial) yield (_("Serial"), device.serial)
else: else:
yield (_('Serial'), '...') yield (_("Serial"), "...")
else: else:
# yield ('Codename', device.codename) # yield ('Codename', device.codename)
yield (_('Index'), device.number) yield (_("Index"), device.number)
if device.wpid: if device.wpid:
yield (_('Wireless PID'), device.wpid) yield (_("Wireless PID"), device.wpid)
if device.product_id: if device.product_id:
yield (_('Product ID'), '046d:' + device.product_id) yield (_("Product ID"), "046d:" + device.product_id)
hid_version = device.protocol hid_version = device.protocol
yield (_('Protocol'), 'HID++ %1.1f' % hid_version if hid_version else _('Unknown')) yield (_("Protocol"), "HID++ %1.1f" % hid_version if hid_version else _("Unknown"))
if read_all and device.polling_rate: if read_all and device.polling_rate:
yield (_('Polling rate'), device.polling_rate) yield (_("Polling rate"), device.polling_rate)
if read_all or not device.online: if read_all or not device.online:
yield (_('Serial'), device.serial) yield (_("Serial"), device.serial)
else: else:
yield (_('Serial'), '...') yield (_("Serial"), "...")
if read_all and device.unitId and device.unitId != device.serial: if read_all and device.unitId and device.unitId != device.serial:
yield (_('Unit ID'), device.unitId) yield (_("Unit ID"), device.unitId)
if read_all: if read_all:
if device.firmware: if device.firmware:
for fw in list(device.firmware): for fw in list(device.firmware):
yield (' ' + _(str(fw.kind)), (fw.name + ' ' + fw.version).strip()) yield (" " + _(str(fw.kind)), (fw.name + " " + fw.version).strip())
elif device.kind is None or device.online: elif device.kind is None or device.online:
yield (' %s' % _('Firmware'), '...') yield (" %s" % _("Firmware"), "...")
flag_bits = device.status.get(_K.NOTIFICATION_FLAGS) flag_bits = device.status.get(_K.NOTIFICATION_FLAGS)
if flag_bits is not None: if flag_bits is not None:
flag_names = ('(%s)' % _('none'), ) if flag_bits == 0 else _hidpp10.NOTIFICATION_FLAG.flag_names(flag_bits) flag_names = ("(%s)" % _("none"),) if flag_bits == 0 else _hidpp10.NOTIFICATION_FLAG.flag_names(flag_bits)
yield (_('Notifications'), ('\n%15s' % ' ').join(flag_names)) yield (_("Notifications"), ("\n%15s" % " ").join(flag_names))
def _set_details(text): def _set_details(text):
_details._text.set_markup(text) _details._text.set_markup(text)
def _make_text(items): def _make_text(items):
text = '\n'.join('%-13s: %s' % (name, value) for name, value in items) text = "\n".join("%-13s: %s" % (name, value) for name, value in items)
return '<small><tt>' + text + '</tt></small>' return "<small><tt>" + text + "</tt></small>"
def _displayable_items(items): def _displayable_items(items):
for name, value in items: for name, value in items:
value = GLib.markup_escape_text(str(value).replace('\x00', '')).strip() value = GLib.markup_escape_text(str(value).replace("\x00", "")).strip()
if value: if value:
yield name, value yield name, value
@ -614,26 +623,29 @@ def _update_receiver_panel(receiver, panel, buttons, full=False):
devices_count = len(receiver) devices_count = len(receiver)
paired_text = _( paired_text = (
_('No device paired.') _(_("No device paired."))
) if devices_count == 0 else ngettext('%(count)s paired device.', '%(count)s paired devices.', devices_count) % { if devices_count == 0
'count': devices_count else ngettext("%(count)s paired device.", "%(count)s paired devices.", devices_count) % {"count": devices_count}
} )
if (receiver.max_devices > 0): if receiver.max_devices > 0:
paired_text += '\n\n<small>%s</small>' % ngettext( paired_text += (
'Up to %(max_count)s device can be paired to this receiver.', "\n\n<small>%s</small>"
'Up to %(max_count)s devices can be paired to this receiver.', receiver.max_devices % ngettext(
) % { "Up to %(max_count)s device can be paired to this receiver.",
'max_count': receiver.max_devices "Up to %(max_count)s devices can be paired to this receiver.",
} receiver.max_devices,
)
% {"max_count": receiver.max_devices}
)
elif devices_count > 0: elif devices_count > 0:
paired_text += '\n\n<small>%s</small>' % _('Only one device can be paired to this receiver.') paired_text += "\n\n<small>%s</small>" % _("Only one device can be paired to this receiver.")
pairings = receiver.remaining_pairings() pairings = receiver.remaining_pairings()
if (pairings is not None and pairings >= 0): if pairings is not None and pairings >= 0:
paired_text += '\n<small>%s</small>' % ( paired_text += "\n<small>%s</small>" % (
ngettext('This receiver has %d pairing remaining.', 'This receiver has %d pairings remaining.', pairings) % ngettext("This receiver has %d pairing remaining.", "This receiver has %d pairings remaining.", pairings)
pairings % pairings
) )
panel._count.set_markup(paired_text) panel._count.set_markup(paired_text)
@ -655,8 +667,11 @@ def _update_receiver_panel(receiver, panel, buttons, full=False):
# b._insecure.set_visible(False) # b._insecure.set_visible(False)
buttons._unpair.set_visible(False) buttons._unpair.set_visible(False)
if not is_pairing and (receiver.remaining_pairings() is None or receiver.remaining_pairings() != 0) and \ if (
(receiver.re_pairs or devices_count < receiver.max_devices): not is_pairing
and (receiver.remaining_pairings() is None or receiver.remaining_pairings() != 0)
and (receiver.re_pairs or devices_count < receiver.max_devices)
):
buttons._pair.set_sensitive(True) buttons._pair.set_sensitive(True)
else: else:
buttons._pair.set_sensitive(False) buttons._pair.set_sensitive(False)
@ -686,28 +701,28 @@ def _update_device_panel(device, panel, buttons, full=False):
panel._battery._text.set_sensitive(is_online) panel._battery._text.set_sensitive(is_online)
if battery_voltage is not None: if battery_voltage is not None:
panel._battery._label.set_text(_('Battery Voltage')) panel._battery._label.set_text(_("Battery Voltage"))
text = '%dmV' % battery_voltage text = "%dmV" % battery_voltage
tooltip_text = _('Voltage reported by battery') tooltip_text = _("Voltage reported by battery")
else: else:
panel._battery._label.set_text(_('Battery Level')) panel._battery._label.set_text(_("Battery Level"))
text = '' text = ""
tooltip_text = _('Approximate level reported by battery') tooltip_text = _("Approximate level reported by battery")
if battery_voltage is not None and battery_level is not None: if battery_voltage is not None and battery_level is not None:
text += ', ' text += ", "
if battery_level is not None: if battery_level is not None:
text += _(str(battery_level)) if isinstance(battery_level, _NamedInt) else '%d%%' % battery_level text += _(str(battery_level)) if isinstance(battery_level, _NamedInt) else "%d%%" % battery_level
if battery_next_level is not None and not charging: if battery_next_level is not None and not charging:
if isinstance(battery_next_level, _NamedInt): if isinstance(battery_next_level, _NamedInt):
text += '<small> (' + _('next reported ') + _(str(battery_next_level)) + ')</small>' text += "<small> (" + _("next reported ") + _(str(battery_next_level)) + ")</small>"
else: else:
text += '<small> (' + _('next reported ') + ('%d%%' % battery_next_level) + ')</small>' text += "<small> (" + _("next reported ") + ("%d%%" % battery_next_level) + ")</small>"
tooltip_text = tooltip_text + _(' and next level to be reported.') tooltip_text = tooltip_text + _(" and next level to be reported.")
if is_online: if is_online:
if charging: if charging:
text += ' <small>(%s)</small>' % _('charging') text += " <small>(%s)</small>" % _("charging")
else: else:
text += ' <small>(%s)</small>' % _('last known') text += " <small>(%s)</small>" % _("last known")
panel._battery._text.set_markup(text) panel._battery._text.set_markup(text)
panel._battery.set_tooltip_text(tooltip_text) panel._battery.set_tooltip_text(tooltip_text)
@ -718,23 +733,23 @@ def _update_device_panel(device, panel, buttons, full=False):
panel._secure.set_visible(True) panel._secure.set_visible(True)
panel._secure._icon.set_visible(True) panel._secure._icon.set_visible(True)
if device.status.get(_K.LINK_ENCRYPTED) is True: if device.status.get(_K.LINK_ENCRYPTED) is True:
panel._secure._text.set_text(_('encrypted')) panel._secure._text.set_text(_("encrypted"))
panel._secure._icon.set_from_icon_name('security-high', _INFO_ICON_SIZE) panel._secure._icon.set_from_icon_name("security-high", _INFO_ICON_SIZE)
panel._secure.set_tooltip_text(_('The wireless link between this device and its receiver is encrypted.')) panel._secure.set_tooltip_text(_("The wireless link between this device and its receiver is encrypted."))
else: else:
panel._secure._text.set_text(_('not encrypted')) panel._secure._text.set_text(_("not encrypted"))
panel._secure._icon.set_from_icon_name('security-low', _INFO_ICON_SIZE) panel._secure._icon.set_from_icon_name("security-low", _INFO_ICON_SIZE)
panel._secure.set_tooltip_text( panel._secure.set_tooltip_text(
_( _(
'The wireless link between this device and its receiver is not encrypted.\n' "The wireless link between this device and its receiver is not encrypted.\n"
'This is a security issue for pointing devices, and a major security issue for text-input devices.' "This is a security issue for pointing devices, and a major security issue for text-input devices."
) )
) )
else: else:
panel._secure.set_visible(True) panel._secure.set_visible(True)
panel._secure._icon.set_visible(False) panel._secure._icon.set_visible(False)
panel._secure._text.set_markup('<small>%s</small>' % _('offline')) panel._secure._text.set_markup("<small>%s</small>" % _("offline"))
panel._secure.set_tooltip_text('') panel._secure.set_tooltip_text("")
if is_online: if is_online:
light_level = device.status.get(_K.LIGHT_LEVEL) light_level = device.status.get(_K.LIGHT_LEVEL)
@ -742,7 +757,7 @@ def _update_device_panel(device, panel, buttons, full=False):
panel._lux.set_visible(False) panel._lux.set_visible(False)
else: else:
panel._lux._icon.set_from_icon_name(_icons.lux(light_level), _INFO_ICON_SIZE) panel._lux._icon.set_from_icon_name(_icons.lux(light_level), _INFO_ICON_SIZE)
panel._lux._text.set_text(_('%(light_level)d lux') % {'light_level': light_level}) panel._lux._text.set_text(_("%(light_level)d lux") % {"light_level": light_level})
panel._lux.set_visible(True) panel._lux.set_visible(True)
else: else:
panel._lux.set_visible(False) panel._lux.set_visible(False)
@ -769,7 +784,7 @@ def _update_info_panel(device, full=False):
# a device must be paired # a device must be paired
assert device assert device
_info._title.set_markup('<b>%s</b>' % device.name) _info._title.set_markup("<b>%s</b>" % device.name)
icon_name = _icons.device_icon_name(device.name, device.kind) icon_name = _icons.device_icon_name(device.name, device.kind)
_info._icon.set_from_icon_name(icon_name, _DEVICE_ICON_SIZE) _info._icon.set_from_icon_name(icon_name, _DEVICE_ICON_SIZE)
@ -860,7 +875,7 @@ def update(device, need_popup=False, refresh=False):
if is_alive and item: if is_alive and item:
was_pairing = bool(_model.get_value(item, _COLUMN.STATUS_ICON)) was_pairing = bool(_model.get_value(item, _COLUMN.STATUS_ICON))
is_pairing = (not device.isDevice) and bool(device.status.lock_open) is_pairing = (not device.isDevice) and bool(device.status.lock_open)
_model.set_value(item, _COLUMN.STATUS_ICON, 'network-wireless' if is_pairing else _CAN_SET_ROW_NONE) _model.set_value(item, _COLUMN.STATUS_ICON, "network-wireless" if is_pairing else _CAN_SET_ROW_NONE)
if selected_device_id == (device.path, 0): if selected_device_id == (device.path, 0):
full_update = need_popup or was_pairing != is_pairing full_update = need_popup or was_pairing != is_pairing
@ -875,7 +890,7 @@ def update(device, need_popup=False, refresh=False):
else: else:
path = device.receiver.path if device.receiver is not None else device.path path = device.receiver.path if device.receiver is not None else device.path
assert device.number is not None and device.number >= 0, 'invalid device number' + str(device.number) assert device.number is not None and device.number >= 0, "invalid device number" + str(device.number)
item = _device_row(path, device.number, device if bool(device) else None) item = _device_row(path, device.number, device if bool(device) else None)
if bool(device) and item: if bool(device) and item:
@ -900,11 +915,11 @@ def update_device(device, item, selected_device_id, need_popup, full=False):
_model.set_value(item, _COLUMN.STATUS_ICON, _CAN_SET_ROW_NONE) _model.set_value(item, _COLUMN.STATUS_ICON, _CAN_SET_ROW_NONE)
else: else:
if battery_voltage is not None and False: # Use levels instead of voltage here if battery_voltage is not None and False: # Use levels instead of voltage here
status_text = '%(battery_voltage)dmV' % {'battery_voltage': battery_voltage} status_text = "%(battery_voltage)dmV" % {"battery_voltage": battery_voltage}
elif isinstance(battery_level, _NamedInt): elif isinstance(battery_level, _NamedInt):
status_text = _(str(battery_level)) status_text = _(str(battery_level))
else: else:
status_text = '%(battery_percent)d%%' % {'battery_percent': battery_level} status_text = "%(battery_percent)d%%" % {"battery_percent": battery_level}
_model.set_value(item, _COLUMN.STATUS_TEXT, status_text) _model.set_value(item, _COLUMN.STATUS_TEXT, status_text)
charging = device.status.get(_K.BATTERY_CHARGING) charging = device.status.get(_K.BATTERY_CHARGING)
@ -921,7 +936,7 @@ def update_device(device, item, selected_device_id, need_popup, full=False):
def find_device(serial): def find_device(serial):
assert serial, 'need serial number or unit ID to find a device' assert serial, "need serial number or unit ID to find a device"
result = None result = None
def check(_store, _treepath, row): def check(_store, _treepath, row):

View File

@ -30,7 +30,7 @@ _suspend_callback = None
def _suspend(): def _suspend():
if _suspend_callback: if _suspend_callback:
if logger.isEnabledFor(logging.INFO): if logger.isEnabledFor(logging.INFO):
logger.info('received suspend event') logger.info("received suspend event")
_suspend_callback() _suspend_callback()
@ -40,7 +40,7 @@ _resume_callback = None
def _resume(): def _resume():
if _resume_callback: if _resume_callback:
if logger.isEnabledFor(logging.INFO): if logger.isEnabledFor(logging.INFO):
logger.info('received resume event') logger.info("received resume event")
_resume_callback() _resume_callback()
@ -59,24 +59,25 @@ def watch(on_resume_callback=None, on_suspend_callback=None):
try: try:
import dbus import dbus
_LOGIND_BUS = 'org.freedesktop.login1' _LOGIND_BUS = "org.freedesktop.login1"
_LOGIND_INTERFACE = 'org.freedesktop.login1.Manager' _LOGIND_INTERFACE = "org.freedesktop.login1.Manager"
# integration into the main GLib loop # integration into the main GLib loop
from dbus.mainloop.glib import DBusGMainLoop from dbus.mainloop.glib import DBusGMainLoop
DBusGMainLoop(set_as_default=True) DBusGMainLoop(set_as_default=True)
bus = dbus.SystemBus() bus = dbus.SystemBus()
assert bus assert bus
bus.add_signal_receiver(_suspend_or_resume, 'PrepareForSleep', dbus_interface=_LOGIND_INTERFACE, bus_name=_LOGIND_BUS) bus.add_signal_receiver(_suspend_or_resume, "PrepareForSleep", dbus_interface=_LOGIND_INTERFACE, bus_name=_LOGIND_BUS)
if logger.isEnabledFor(logging.INFO): if logger.isEnabledFor(logging.INFO):
logger.info('connected to system dbus, watching for suspend/resume events') logger.info("connected to system dbus, watching for suspend/resume events")
except Exception: except Exception:
# Either: # Either:
# - the dbus library is not available # - the dbus library is not available
# - the system dbus is not running # - the system dbus is not running
logger.warning('failed to register suspend/resume callbacks') logger.warning("failed to register suspend/resume callbacks")
pass pass

View File

@ -9,88 +9,89 @@ try:
except ImportError: except ImportError:
from distutils.core import setup from distutils.core import setup
NAME = 'Solaar' NAME = "Solaar"
with open('lib/solaar/version', 'r') as vfile: with open("lib/solaar/version", "r") as vfile:
version = vfile.read().strip() version = vfile.read().strip()
try: # get commit from git describe try: # get commit from git describe
commit = subprocess.check_output(['git', 'describe', '--always'], stderr=subprocess.DEVNULL).strip().decode() commit = subprocess.check_output(["git", "describe", "--always"], stderr=subprocess.DEVNULL).strip().decode()
with open('lib/solaar/commit', 'w') as vfile: with open("lib/solaar/commit", "w") as vfile:
vfile.write(f'{commit}\n') vfile.write(f"{commit}\n")
except Exception: # get commit from Ubuntu dpkg-parsechangelog except Exception: # get commit from Ubuntu dpkg-parsechangelog
try: try:
commit = subprocess.check_output(['dpkg-parsechangelog', '--show-field', 'Version'], commit = (
stderr=subprocess.DEVNULL).strip().decode() subprocess.check_output(["dpkg-parsechangelog", "--show-field", "Version"], stderr=subprocess.DEVNULL)
commit = commit.split('~') .strip()
with open('lib/solaar/commit', 'w') as vfile: .decode()
vfile.write(f'{commit[0]}\n') )
commit = commit.split("~")
with open("lib/solaar/commit", "w") as vfile:
vfile.write(f"{commit[0]}\n")
except Exception as e: except Exception as e:
print('Exception using dpkg-parsechangelog', e) print("Exception using dpkg-parsechangelog", e)
def _data_files(): def _data_files():
yield "share/icons/hicolor/scalable/apps", _glob("share/solaar/icons/solaar*.svg")
yield "share/icons/hicolor/32x32/apps", _glob("share/solaar/icons/solaar-light_*.png")
yield 'share/icons/hicolor/scalable/apps', _glob('share/solaar/icons/solaar*.svg') for mo in _glob("share/locale/*/LC_MESSAGES/solaar.mo"):
yield 'share/icons/hicolor/32x32/apps', _glob('share/solaar/icons/solaar-light_*.png')
for mo in _glob('share/locale/*/LC_MESSAGES/solaar.mo'):
yield _dirname(mo), [mo] yield _dirname(mo), [mo]
yield 'share/applications', ['share/applications/solaar.desktop'] yield "share/applications", ["share/applications/solaar.desktop"]
yield 'lib/udev/rules.d', ['rules.d/42-logitech-unify-permissions.rules'] yield "lib/udev/rules.d", ["rules.d/42-logitech-unify-permissions.rules"]
yield 'share/metainfo', ['share/solaar/io.github.pwr_solaar.solaar.metainfo.xml'] yield "share/metainfo", ["share/solaar/io.github.pwr_solaar.solaar.metainfo.xml"]
setup( setup(
name=NAME.lower(), name=NAME.lower(),
version=version, version=version,
description='Linux device manager for Logitech receivers, keyboards, mice, and tablets.', description="Linux device manager for Logitech receivers, keyboards, mice, and tablets.",
long_description=''' long_description="""
Solaar is a Linux device manager for many Logitech peripherals that connect through Solaar is a Linux device manager for many Logitech peripherals that connect through
Unifying and other receivers or via USB or Bluetooth. Unifying and other receivers or via USB or Bluetooth.
Solaar is able to pair/unpair devices with receivers and show and modify some of the Solaar is able to pair/unpair devices with receivers and show and modify some of the
modifiable features of devices. modifiable features of devices.
For instructions on installing Solaar see https://pwr-solaar.github.io/Solaar/installation'''.strip(), For instructions on installing Solaar see https://pwr-solaar.github.io/Solaar/installation""".strip(),
author='Daniel Pavel', author="Daniel Pavel",
license='GPLv2', license="GPLv2",
url='http://pwr-solaar.github.io/Solaar/', url="http://pwr-solaar.github.io/Solaar/",
classifiers=[ classifiers=[
'Development Status :: 4 - Beta', "Development Status :: 4 - Beta",
'Environment :: X11 Applications :: GTK', "Environment :: X11 Applications :: GTK",
'Environment :: Console', "Environment :: Console",
'Intended Audience :: End Users/Desktop', "Intended Audience :: End Users/Desktop",
'License :: DFSG approved', "License :: DFSG approved",
'License :: OSI Approved :: GNU General Public License v2 (GPLv2)', "License :: OSI Approved :: GNU General Public License v2 (GPLv2)",
'Natural Language :: English', "Natural Language :: English",
'Programming Language :: Python :: 3 :: Only', "Programming Language :: Python :: 3 :: Only",
'Operating System :: POSIX :: Linux', "Operating System :: POSIX :: Linux",
'Topic :: Utilities', "Topic :: Utilities",
], ],
platforms=['linux'], platforms=["linux"],
# sudo apt install python-gi python3-gi \ # sudo apt install python-gi python3-gi \
# gir1.2-gtk-3.0 gir1.2-notify-0.7 gir1.2-ayatanaappindicator3-0.1 # gir1.2-gtk-3.0 gir1.2-notify-0.7 gir1.2-ayatanaappindicator3-0.1
# os_requires=['gi.repository.GObject (>= 2.0)', 'gi.repository.Gtk (>= 3.0)'], # os_requires=['gi.repository.GObject (>= 2.0)', 'gi.repository.Gtk (>= 3.0)'],
python_requires='>=3.7', python_requires=">=3.7",
install_requires=[ install_requires=[
'evdev (>= 1.1.2) ; platform_system=="Linux"', 'evdev (>= 1.1.2) ; platform_system=="Linux"',
'pyudev (>= 0.13)', "pyudev (>= 0.13)",
'PyYAML (>= 3.12)', "PyYAML (>= 3.12)",
'python-xlib (>= 0.27)', "python-xlib (>= 0.27)",
'psutil (>= 5.4.3)', "psutil (>= 5.4.3)",
'dbus-python ; platform_system=="Linux"', 'dbus-python ; platform_system=="Linux"',
], ],
extras_require={ extras_require={
'report-descriptor': ['hid-parser'], "report-descriptor": ["hid-parser"],
'desktop-notifications': ['Notify (>= 0.7)'], "desktop-notifications": ["Notify (>= 0.7)"],
'git-commit': ['python-git-info'], "git-commit": ["python-git-info"],
'test': ['pytest', 'pytest-cov'], "test": ["pytest", "pytest-cov"],
'dev': ['ruff'], "dev": ["ruff"],
}, },
package_dir={'': 'lib'}, package_dir={"": "lib"},
packages=['keysyms', 'hidapi', 'logitech_receiver', 'solaar', 'solaar.ui', 'solaar.cli'], packages=["keysyms", "hidapi", "logitech_receiver", "solaar", "solaar.ui", "solaar.cli"],
data_files=list(_data_files()), data_files=list(_data_files()),
include_package_data=True, include_package_data=True,
scripts=_glob('bin/*'), scripts=_glob("bin/*"),
) )

View File

@ -4,7 +4,7 @@ from lib.logitech_receiver import common
def test_crc16(): def test_crc16():
value = b'123456789' value = b"123456789"
expected = 0x29B1 expected = 0x29B1
result = common.crc16(value) result = common.crc16(value)
@ -13,21 +13,21 @@ def test_crc16():
def test_named_int(): def test_named_int():
named_int = common.NamedInt(0x2, 'pulse') named_int = common.NamedInt(0x2, "pulse")
assert named_int.name == 'pulse' assert named_int.name == "pulse"
assert named_int == 2 assert named_int == 2
def test_named_int_comparison(): def test_named_int_comparison():
default_value = 0 default_value = 0
default_name = 'entry' default_name = "entry"
named_int = common.NamedInt(default_value, default_name) named_int = common.NamedInt(default_value, default_name)
named_int_equal = common.NamedInt(default_value, default_name) named_int_equal = common.NamedInt(default_value, default_name)
named_int_unequal_name = common.NamedInt(default_value, 'unequal') named_int_unequal_name = common.NamedInt(default_value, "unequal")
named_int_unequal_value = common.NamedInt(5, default_name) named_int_unequal_value = common.NamedInt(5, default_name)
named_int_unequal = common.NamedInt(2, 'unequal') named_int_unequal = common.NamedInt(2, "unequal")
assert named_int == named_int_equal assert named_int == named_int_equal
assert named_int != named_int_unequal_name assert named_int != named_int_unequal_name
@ -42,23 +42,26 @@ def named_ints():
def test_named_ints(named_ints): def test_named_ints(named_ints):
assert named_ints.empty == 0 assert named_ints.empty == 0
assert named_ints.empty.name == 'empty' assert named_ints.empty.name == "empty"
assert named_ints.critical == 5 assert named_ints.critical == 5
assert named_ints.critical.name == 'critical' assert named_ints.critical.name == "critical"
assert named_ints.low == 20 assert named_ints.low == 20
assert named_ints.low.name == 'low' assert named_ints.low.name == "low"
assert named_ints.good == 50 assert named_ints.good == 50
assert named_ints.good.name == 'good' assert named_ints.good.name == "good"
assert named_ints.full == 90 assert named_ints.full == 90
assert named_ints.full.name == 'full' assert named_ints.full.name == "full"
assert len(named_ints) == 5 assert len(named_ints) == 5
@pytest.mark.parametrize('bytes_input, expected_output', [ @pytest.mark.parametrize(
(b'\x01\x02\x03\x04', '01020304'), "bytes_input, expected_output",
(b'', ''), [
]) (b"\x01\x02\x03\x04", "01020304"),
(b"", ""),
],
)
def test_strhex(bytes_input, expected_output): def test_strhex(bytes_input, expected_output):
result = common.strhex(bytes_input) result = common.strhex(bytes_input)
@ -66,7 +69,7 @@ def test_strhex(bytes_input, expected_output):
def test_bytest2int(): def test_bytest2int():
value = b'\x12\x34\x56\x78' value = b"\x12\x34\x56\x78"
expected = 0x12345678 expected = 0x12345678
result = common.bytes2int(value) result = common.bytes2int(value)
@ -76,7 +79,7 @@ def test_bytest2int():
def test_int2bytes(): def test_int2bytes():
value = 0x12345678 value = 0x12345678
expected = b'\x12\x34\x56\x78' expected = b"\x12\x34\x56\x78"
result = common.int2bytes(value) result = common.int2bytes(value)

View File

@ -10,13 +10,14 @@ def init_paths():
import os.path as _path import os.path as _path
import sys import sys
src_lib = _path.normpath(_path.join(_path.realpath(sys.path[0]), '..', 'lib')) src_lib = _path.normpath(_path.join(_path.realpath(sys.path[0]), "..", "lib"))
init_py = _path.join(src_lib, 'hidapi', '__init__.py') init_py = _path.join(src_lib, "hidapi", "__init__.py")
if _path.exists(init_py): if _path.exists(init_py):
sys.path[0] = src_lib sys.path[0] = src_lib
if __name__ == '__main__': if __name__ == "__main__":
init_paths() init_paths()
from hidapi import hidconsole from hidapi import hidconsole
hidconsole.main() hidconsole.main()