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

View File

@ -19,31 +19,35 @@
import platform as _platform
if _platform.system() in ('Darwin', 'Windows'):
from hidapi.hidapi import close # noqa: F401
from hidapi.hidapi import enumerate # noqa: F401
from hidapi.hidapi import find_paired_node # noqa: F401
from hidapi.hidapi import find_paired_node_wpid # noqa: F401
from hidapi.hidapi import get_manufacturer # noqa: F401
from hidapi.hidapi import get_product # noqa: F401
from hidapi.hidapi import get_serial # noqa: F401
from hidapi.hidapi import monitor_glib # noqa: F401
from hidapi.hidapi import open # noqa: F401
from hidapi.hidapi import open_path # noqa: F401
from hidapi.hidapi import read # noqa: F401
from hidapi.hidapi import write # noqa: F401
if _platform.system() in ("Darwin", "Windows"):
from hidapi.hidapi import (
close, # noqa: F401
enumerate, # noqa: F401
find_paired_node, # noqa: F401
find_paired_node_wpid, # noqa: F401
get_manufacturer, # noqa: F401
get_product, # noqa: F401
get_serial, # noqa: F401
monitor_glib, # noqa: F401
open, # noqa: F401
open_path, # noqa: F401
read, # noqa: F401
write, # noqa: F401
)
else:
from hidapi.udev import close # noqa: F401
from hidapi.udev import enumerate # noqa: F401
from hidapi.udev import find_paired_node # noqa: F401
from hidapi.udev import find_paired_node_wpid # noqa: F401
from hidapi.udev import get_manufacturer # noqa: F401
from hidapi.udev import get_product # noqa: F401
from hidapi.udev import get_serial # noqa: F401
from hidapi.udev import monitor_glib # noqa: F401
from hidapi.udev import open # noqa: F401
from hidapi.udev import open_path # noqa: F401
from hidapi.udev import read # noqa: F401
from hidapi.udev import write # noqa: F401
from hidapi.udev import (
close, # noqa: F401
enumerate, # noqa: F401
find_paired_node, # noqa: F401
find_paired_node_wpid, # noqa: F401
get_manufacturer, # noqa: F401
get_product, # noqa: F401
get_serial, # noqa: F401
monitor_glib, # noqa: F401
open, # noqa: F401
open_path, # 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
gi.require_version('Gdk', '3.0')
gi.require_version("Gdk", "3.0")
from gi.repository import GLib # NOQA: E402
logger = logging.getLogger(__name__)
native_implementation = 'hidapi'
native_implementation = "hidapi"
# Device info as expected by Solaar
DeviceInfo = namedtuple(
'DeviceInfo', [
'path',
'bus_id',
'vendor_id',
'product_id',
'interface',
'driver',
'manufacturer',
'product',
'serial',
'release',
'isDevice',
'hidpp_short',
'hidpp_long',
]
"DeviceInfo",
[
"path",
"bus_id",
"vendor_id",
"product_id",
"interface",
"driver",
"manufacturer",
"product",
"serial",
"release",
"isDevice",
"hidpp_short",
"hidpp_long",
],
)
del namedtuple
@ -67,8 +68,15 @@ _hidapi = None
# hidapi binary names for various platforms
_library_paths = (
'libhidapi-hidraw.so', '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'
"libhidapi-hidraw.so",
"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:
@ -84,9 +92,9 @@ else:
# Retrieve version of hdiapi library
class _cHidApiVersion(ctypes.Structure):
_fields_ = [
('major', ctypes.c_int),
('minor', ctypes.c_int),
('patch', ctypes.c_int),
("major", ctypes.c_int),
("minor", ctypes.c_int),
("patch", ctypes.c_int),
]
@ -97,28 +105,27 @@ _hid_version = _hidapi.hid_version()
# Construct device info struct based on API version
class _cDeviceInfo(ctypes.Structure):
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
# See https://github.com/libusb/hidapi/blob/master/hidapi/hidapi.h#L143
_cDeviceInfo_fields = [
('path', ctypes.c_char_p),
('vendor_id', ctypes.c_ushort),
('product_id', ctypes.c_ushort),
('serial_number', ctypes.c_wchar_p),
('release_number', ctypes.c_ushort),
('manufacturer_string', ctypes.c_wchar_p),
('product_string', ctypes.c_wchar_p),
('usage_page', ctypes.c_ushort),
('usage', ctypes.c_ushort),
('interface_number', ctypes.c_int),
('next', ctypes.POINTER(_cDeviceInfo)),
("path", ctypes.c_char_p),
("vendor_id", ctypes.c_ushort),
("product_id", ctypes.c_ushort),
("serial_number", ctypes.c_wchar_p),
("release_number", ctypes.c_ushort),
("manufacturer_string", ctypes.c_wchar_p),
("product_string", ctypes.c_wchar_p),
("usage_page", ctypes.c_ushort),
("usage", ctypes.c_ushort),
("interface_number", ctypes.c_int),
("next", ctypes.POINTER(_cDeviceInfo)),
]
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
# 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
# allow non-exclusive opening. On windows opening with shared access is
# 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.restype = None
_hidapi.hid_darwin_set_open_exclusive(0)
@ -179,7 +186,7 @@ class HIDError(Exception):
def _enumerate_devices():
""" Returns all HID devices which are potentially useful to us """
"""Returns all HID devices which are potentially useful to us"""
devices = []
c_devices = _hidapi.hid_enumerate(0, 0)
p = c_devices
@ -188,19 +195,19 @@ def _enumerate_devices():
p = p.contents.next
_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 = {}
for device in devices:
# 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.
if device['path'] in keyboard_or_mouse:
if device["path"] in keyboard_or_mouse:
# print(f"Ignoring keyboard or mouse device: {device}")
continue
# hidapi returns separate entries for each usage page of a device.
# Deduplicate by path to only keep one device entry.
if device['path'] not in unique_devices:
unique_devices[device['path']] = device
if device["path"] not in unique_devices:
unique_devices[device["path"]] = device
unique_devices = unique_devices.values()
# 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
class _DeviceMonitor(Thread):
def __init__(self, device_callback, polling_delay=5.0):
self.device_callback = device_callback
self.polling_delay = polling_delay
@ -225,10 +231,10 @@ class _DeviceMonitor(Thread):
current_devices = {tuple(dev.items()): dev for dev in _enumerate_devices()}
for key, device in self.prev_devices.items():
if key not in current_devices:
self.device_callback('remove', device)
self.device_callback("remove", device)
for key, device in current_devices.items():
if key not in self.prev_devices:
self.device_callback('add', device)
self.device_callback("add", device)
self.prev_devices = current_devices
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
# with the required hid_driver and usb_interface and whether this is a receiver or device.
def _match(action, device, filterfn):
vid = device['vendor_id']
pid = device['product_id']
vid = device["vendor_id"]
pid = device["product_id"]
# 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
elif device.get('bus_type') == 0x02:
elif device.get("bus_type") == 0x02:
bus_id = 0x05 # Bluetooth
else:
bus_id = None
# Check for hidpp support
device['hidpp_short'] = False
device['hidpp_long'] = False
device["hidpp_short"] = False
device["hidpp_long"] = False
device_handle = None
try:
device_handle = open_path(device['path'])
device_handle = open_path(device["path"])
report = get_input_report(device_handle, 0x10, 32)
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)
if len(report) == 1 + 19 and report[0] == 0x11:
device['hidpp_long'] = True
device["hidpp_long"] = True
except HIDError as e: # noqa: F841
if logger.isEnabledFor(logging.INFO):
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):
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
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:
return
isDevice = filter.get('isDevice')
isDevice = filter.get("isDevice")
if action == 'add':
if action == "add":
d_info = DeviceInfo(
path=device['path'].decode(),
path=device["path"].decode(),
bus_id=bus_id,
vendor_id=f'{vid:04X}', # noqa
product_id=f'{pid:04X}', # noqa
vendor_id=f"{vid:04X}", # noqa
product_id=f"{pid:04X}", # noqa
interface=None,
driver=None,
manufacturer=device['manufacturer_string'],
product=device['product_string'],
serial=device['serial_number'],
release=device['release_number'],
manufacturer=device["manufacturer_string"],
product=device["product_string"],
serial=device["serial_number"],
release=device["release_number"],
isDevice=isDevice,
hidpp_short=device['hidpp_short'],
hidpp_long=device['hidpp_long'],
hidpp_short=device["hidpp_short"],
hidpp_long=device["hidpp_long"],
)
return d_info
elif action == 'remove':
elif action == "remove":
d_info = DeviceInfo(
path=device['path'].decode(),
path=device["path"].decode(),
bus_id=None,
vendor_id=f'{vid:04X}', # noqa
product_id=f'{pid:04X}', # noqa
vendor_id=f"{vid:04X}", # noqa
product_id=f"{pid:04X}", # noqa
interface=None,
driver=None,
manufacturer=None,
@ -328,14 +334,13 @@ def find_paired_node_wpid(receiver_path, index):
def monitor_glib(callback, filterfn):
def device_callback(action, device):
# print(f"device_callback({action}): {device}")
if action == 'add':
if action == "add":
d_info = _match(action, device, filterfn)
if d_info:
GLib.idle_add(callback, action, d_info)
elif action == 'remove':
elif action == "remove":
# Removed devices will be detected by Solaar directly
pass
@ -352,7 +357,7 @@ def enumerate(filterfn):
:returns: a list of matching ``DeviceInfo`` tuples.
"""
for device in _enumerate_devices():
d_info = _match('add', device, filterfn)
d_info = _match("add", device, filterfn)
if 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):
assert device_handle
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)
if size < 0:
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)
ret = func(device_handle, buf, max_length)
if ret < 0:
raise HIDError('Error reading device property')
raise HIDError("Error reading device property")
return buf.value

View File

@ -40,10 +40,10 @@ except NameError:
read_packet = input
interactive = os.isatty(0)
prompt = '?? Input: ' if interactive else ''
prompt = "?? Input: " if interactive else ""
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):
t = time.time() - start_time
if isinstance(data, str):
s = marker + ' ' + data
s = marker + " " + data
else:
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:
# 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:
# scroll the entire screen above the current line up by 1 line
sys.stdout.write(
'\033[s' # save cursor position
'\033[S' # scroll up
'\033[A' # cursor up
'\033[L' # insert 1 line
'\033[G'
"\033[s" # save cursor position
"\033[S" # scroll up
"\033[A" # cursor up
"\033[L" # insert 1 line
"\033[G"
) # move cursor to column 1
sys.stdout.write(s)
if interactive and scroll:
# restore cursor position
sys.stdout.write('\033[u')
sys.stdout.write("\033[u")
else:
sys.stdout.write('\n')
sys.stdout.write("\n")
# flush stdout manually...
# because trying to open stdin/out unbuffered programmatically
@ -88,7 +88,7 @@ def _print(marker, data, scroll=False):
def _error(text, scroll=False):
_print('!!', text, scroll)
_print("!!", text, scroll)
def _continuous_read(handle, timeout=2000):
@ -96,79 +96,78 @@ def _continuous_read(handle, timeout=2000):
try:
reply = _hid.read(handle, 128, timeout)
except OSError as e:
_error('Read failed, aborting: ' + str(e), True)
_error("Read failed, aborting: " + str(e), True)
break
assert reply is not None
if reply:
_print('>>', reply, True)
_print(">>", reply, True)
def _validate_input(line, hidpp=False):
try:
data = unhexlify(line.encode('ascii'))
data = unhexlify(line.encode("ascii"))
except Exception as e:
_error('Invalid input: ' + str(e))
_error("Invalid input: " + str(e))
return None
if hidpp:
if len(data) < 4:
_error('Invalid HID++ request: need at least 4 bytes')
_error("Invalid HID++ request: need at least 4 bytes")
return None
if data[:1] not in b'\x10\x11':
_error('Invalid HID++ request: first byte must be 0x10 or 0x11')
if data[:1] not in b"\x10\x11":
_error("Invalid HID++ request: first byte must be 0x10 or 0x11")
return None
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')
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")
return None
if data[:1] == b'\x10':
if data[:1] == b"\x10":
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
while len(data) < 7:
data = (data + b'\x00' * 7)[:7]
elif data[:1] == b'\x11':
data = (data + b"\x00" * 7)[:7]
elif data[:1] == b"\x11":
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
while len(data) < 20:
data = (data + b'\x00' * 20)[:20]
data = (data + b"\x00" * 20)[:20]
return data
def _open(args):
def matchfn(bid, vid, pid, _a, _b):
if vid == 0x046d:
return {'vid': 0x046d}
if vid == 0x046D:
return {"vid": 0x046D}
device = args.device
if args.hidpp and not device:
for d in _hid.enumerate(matchfn):
if d.driver == 'logitech-djreceiver':
if d.driver == "logitech-djreceiver":
device = d.path
break
if not device:
sys.exit('!! No HID++ receiver found.')
sys.exit("!! No HID++ receiver found.")
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)
if not handle:
sys.exit('!! Failed to open %s, aborting.' % device)
sys.exit("!! Failed to open %s, aborting." % device)
print(
'.. Opened handle %r, vendor %r product %r serial %r.' %
(handle, _hid.get_manufacturer(handle), _hid.get_product(handle), _hid.get_serial(handle))
".. Opened handle %r, vendor %r product %r serial %r."
% (handle, _hid.get_manufacturer(handle), _hid.get_product(handle), _hid.get_serial(handle))
)
if args.hidpp:
if _hid.get_manufacturer(handle) is not None and _hid.get_manufacturer(handle) != b'Logitech':
sys.exit('!! Only Logitech devices support the HID++ protocol.')
print('.. HID++ validation enabled.')
if _hid.get_manufacturer(handle) is not None and _hid.get_manufacturer(handle) != b"Logitech":
sys.exit("!! Only Logitech devices support the HID++ protocol.")
print(".. HID++ validation enabled.")
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
print('.. Logitech receiver detected, HID++ validation enabled.')
print(".. Logitech receiver detected, HID++ validation enabled.")
return handle
@ -180,13 +179,13 @@ def _open(args):
def _parse_arguments():
arg_parser = argparse.ArgumentParser()
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("--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(
'device',
nargs='?',
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'
"device",
nargs="?",
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",
)
return arg_parser.parse_args()
@ -196,10 +195,10 @@ def main():
handle = _open(args)
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:
args.history = os.path.join(os.path.expanduser('~'), '.hidconsole-history')
args.history = os.path.join(os.path.expanduser("~"), ".hidconsole-history")
try:
readline.read_history_file(args.history)
except Exception:
@ -207,17 +206,17 @@ def main():
pass
try:
t = Thread(target=_continuous_read, args=(handle, ))
t = Thread(target=_continuous_read, args=(handle,))
t.daemon = True
t.start()
if interactive:
# 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():
line = read_packet(prompt)
line = line.strip().replace(' ', '')
line = line.strip().replace(" ", "")
# print ("line", line)
if not line:
continue
@ -226,12 +225,12 @@ def main():
if data is None:
continue
_print('<<', data)
_print("<<", data)
_hid.write(handle, data)
# wait for some kind of reply
if args.hidpp and not interactive:
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
time.sleep(0.010)
else:
@ -239,16 +238,16 @@ def main():
time.sleep(0.700)
except EOFError:
if interactive:
print('')
print("")
else:
time.sleep(1)
finally:
print('.. Closing handle %r' % handle)
print(".. Closing handle %r" % handle)
_hid.close(handle)
if interactive:
readline.write_history_file(args.history)
if __name__ == '__main__':
if __name__ == "__main__":
main()

View File

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

View File

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

View File

@ -45,14 +45,14 @@ logger = logging.getLogger(__name__)
#
_wired_device = lambda product_id, interface: {
'vendor_id': 0x046d,
'product_id': product_id,
'bus_id': 0x3,
'usb_interface': interface,
'isDevice': True
"vendor_id": 0x046D,
"product_id": product_id,
"bus_id": 0x3,
"usb_interface": interface,
"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 = []
@ -66,13 +66,13 @@ for _ignore, d in _DEVICES.items():
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
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
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)
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)
@ -80,7 +80,7 @@ def product_information(usb_id):
if isinstance(usb_id, str):
usb_id = int(usb_id, 16)
for r in _RECEIVER_USB_IDS:
if usb_id == r.get('product_id'):
if usb_id == r.get("product_id"):
return r
return {}
@ -103,7 +103,7 @@ report_lengths = {
HIDPP_SHORT_MESSAGE_ID: _SHORT_MESSAGE_SIZE,
HIDPP_LONG_MESSAGE_ID: _LONG_MESSAGE_SIZE,
DJ_MESSAGE_ID: _MEDIUM_MESSAGE_SIZE,
0x21: _MAX_READ_SIZE
0x21: _MAX_READ_SIZE,
}
"""Default timeout on read (in seconds)."""
DEFAULT_TIMEOUT = 4
@ -120,9 +120,11 @@ _PING_TIMEOUT = DEFAULT_TIMEOUT
def match(record, bus_id, vendor_id, product_id):
return ((record.get('bus_id') is None or record.get('bus_id') == bus_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))
return (
(record.get("bus_id") is None or record.get("bus_id") == bus_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):
@ -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):
return record
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():
@ -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):
return record
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
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 isinstance(data, bytes), (repr(data), type(data))
if long_message or len(data) > _SHORT_MESSAGE_SIZE - 2 or data[:1] == b'\x82':
wdata = _pack('!BB18s', HIDPP_LONG_MESSAGE_ID, devnumber, data)
if long_message or len(data) > _SHORT_MESSAGE_SIZE - 2 or data[:1] == b"\x82":
wdata = _pack("!BB18s", HIDPP_LONG_MESSAGE_ID, devnumber, data)
else:
wdata = _pack('!BB5s', HIDPP_SHORT_MESSAGE_ID, devnumber, data)
wdata = _pack("!BB5s", HIDPP_SHORT_MESSAGE_ID, devnumber, data)
if logger.isEnabledFor(logging.DEBUG):
logger.debug('(%s) <= w[%02X %02X %s %s]', handle, ord(wdata[:1]), devnumber, _strhex(wdata[2:4]), _strhex(wdata[4:]))
logger.debug("(%s) <= w[%02X %02X %s %s]", handle, ord(wdata[:1]), devnumber, _strhex(wdata[2:4]), _strhex(wdata[4:]))
try:
_hid.write(int(handle), wdata)
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)
raise exceptions.NoReceiver(reason=reason)
@ -270,7 +272,7 @@ def check_message(data):
if report_lengths.get(report_id) == len(data):
return True
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
@ -288,7 +290,7 @@ def _read(handle, timeout):
timeout = int(timeout * 1000)
data = _hid.read(int(handle), _MAX_READ_SIZE, timeout)
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)
raise exceptions.NoReceiver(reason=reason)
@ -296,9 +298,10 @@ def _read(handle, timeout):
report_id = ord(data[:1])
devnumber = ord(data[1:2])
if logger.isEnabledFor(logging.DEBUG
) and (report_id != DJ_MESSAGE_ID or ord(data[2:3]) > 0x10): # ignore DJ input messages
logger.debug('(%s) => r[%02X %02X %s %s]', handle, report_id, devnumber, _strhex(data[2:4]), _strhex(data[4:]))
if logger.isEnabledFor(logging.DEBUG) and (
report_id != DJ_MESSAGE_ID or ord(data[2:3]) > 0x10
): # ignore DJ input messages
logger.debug("(%s) => r[%02X %02X %s %s]", handle, report_id, devnumber, _strhex(data[2:4]), _strhex(data[4:]))
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
data = _hid.read(ihandle, _MAX_READ_SIZE, 0)
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)
raise exceptions.NoReceiver(reason=reason)
@ -355,20 +358,27 @@ def make_notification(report_id, devnumber, data):
if (
# 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
(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
(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
(address & 0x0F == 0x00)
): # noqa: E129
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.__str__ = lambda self: 'Notification(%02x,%d,%02X,%02X,%s)' % (
self.report_id, self.devnumber, self.sub_id, self.address, _strhex(self.data)
_HIDPP_Notification = namedtuple("_HIDPP_Notification", ("report_id", "devnumber", "sub_id", "address", "data"))
_HIDPP_Notification.__str__ = lambda self: "Notification(%02x,%d,%02X,%02X,%s)" % (
self.report_id,
self.devnumber,
self.sub_id,
self.address,
_strhex(self.data),
)
del namedtuple
@ -384,7 +394,7 @@ def handle_lock(handle):
with request_lock:
if handles_lock.get(handle) is None:
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
return handles_lock[handle]
@ -395,7 +405,7 @@ def acquire_timeout(lock, handle, timeout):
result = lock.acquire(timeout=timeout)
try:
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
finally:
if result:
@ -415,7 +425,7 @@ def request(handle, devnumber, request_id, *params, no_reply=False, return_error
# import inspect as _inspect
# 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)
if (devnumber != 0xFF or protocol >= 2.0) and request_id < 0x8000:
# 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
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:
params = b''
params = b""
# if logger.isEnabledFor(logging.DEBUG):
# logger.debug("(%s) device %d request_id {%04X} params [%s]", handle, devnumber, request_id, _strhex(params))
request_data = _pack('!H', request_id) + params
request_data = _pack("!H", request_id) + params
ihandle = int(handle)
notifications_hook = getattr(handle, 'notifications_hook', None)
notifications_hook = getattr(handle, "notifications_hook", None)
try:
_skip_incoming(handle, ihandle, notifications_hook)
except exceptions.NoReceiver:
logger.warning('device or receiver disconnected')
logger.warning("device or receiver disconnected")
return None
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:
report_id, reply_devnumber, reply_data = reply
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 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]
):
error = ord(reply_data[3:4])
if logger.isEnabledFor(logging.DEBUG):
logger.debug(
'(%s) device 0x%02X error on request {%04X}: %d = %s', handle, devnumber, request_id, error,
_hidpp10_constants.ERROR[error]
"(%s) device 0x%02X error on request {%04X}: %d = %s",
handle,
devnumber,
request_id,
error,
_hidpp10_constants.ERROR[error],
)
return _hidpp10_constants.ERROR[error] if return_error else None
if reply_data[:1] == b'\xFF' and reply_data[1:3] == request_data[:2]:
if reply_data[:1] == b"\xFF" and reply_data[1:3] == request_data[:2]:
# a HID++ 2.0 feature call returned with an error
error = ord(reply_data[3:4])
logger.error(
'(%s) device %d error on feature request {%04X}: %d = %s', handle, devnumber, request_id, error,
_hidpp20_constants.ERROR[error]
"(%s) device %d error on feature request {%04X}: %d = %s",
handle,
devnumber,
request_id,
error,
_hidpp20_constants.ERROR[error],
)
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.warning(
'timeout (%0.2f/%0.2f) on device %d request {%04X} params [%s]', delta, timeout, devnumber, request_id,
_strhex(params)
"timeout (%0.2f/%0.2f) on device %d request {%04X} params [%s]",
delta,
timeout,
devnumber,
request_id,
_strhex(params),
)
# 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.
"""
if logger.isEnabledFor(logging.DEBUG):
logger.debug('(%s) pinging device %d', handle, devnumber)
with acquire_timeout(handle_lock(handle), handle, 10.):
notifications_hook = getattr(handle, 'notifications_hook', None)
logger.debug("(%s) pinging device %d", handle, devnumber)
with acquire_timeout(handle_lock(handle), handle, 10.0):
notifications_hook = getattr(handle, "notifications_hook", None)
try:
_skip_incoming(handle, int(handle), notifications_hook)
except exceptions.NoReceiver:
logger.warning('device or receiver disconnected')
logger.warning("device or receiver disconnected")
return
# randomize the SoftwareId and mark byte to be able to identify the ping
# reply, and set most significant (0x8) bit in SoftwareId so that the reply
# is always distinguishable from notifications
request_id = 0x0018 | _random_bits(3)
request_data = _pack('!HBBB', request_id, 0, 0, _random_bits(8))
request_data = _pack("!HBBB", request_id, 0, 0, _random_bits(8))
write(int(handle), devnumber, request_data, long_message)
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)
if 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:]:
# HID++ 2.0+ device, currently connected
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 \
reply_data[1:3] == request_data[:2]: # error response
if (
report_id == HIDPP_SHORT_MESSAGE_ID
and reply_data[:1] == b"\x8F"
and reply_data[1:3] == request_data[:2]
): # error response
error = ord(reply_data[3:4])
if error == _hidpp10_constants.ERROR.invalid_SubID__command: # a valid reply from a HID++ 1.0 device
return 1.0
if error == _hidpp10_constants.ERROR.resource_error or \
error == _hidpp10_constants.ERROR.connection_request_failed:
if (
error == _hidpp10_constants.ERROR.resource_error
or error == _hidpp10_constants.ERROR.connection_request_failed
):
return # device unreachable
if error == _hidpp10_constants.ERROR.unknown_device: # no paired device with that number
logger.error('(%s) device %d error on ping request: unknown device', handle, devnumber)
logger.error("(%s) device %d error on ping request: unknown device", handle, devnumber)
raise exceptions.NoSuchDevice(number=devnumber, request=request_id)
if notifications_hook:
@ -570,4 +600,4 @@ def ping(handle, devnumber, long_message=False):
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
## 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: {
'vendor_id': 0x046d,
'product_id': product_id,
'usb_interface': 2,
'hid_driver': _DRIVER, # noqa: F821
'name': _('Bolt Receiver'),
'receiver_kind': 'bolt',
'max_devices': 6,
'may_unpair': True
"vendor_id": 0x046D,
"product_id": product_id,
"usb_interface": 2,
"hid_driver": _DRIVER, # noqa: F821
"name": _("Bolt Receiver"),
"receiver_kind": "bolt",
"max_devices": 6,
"may_unpair": True,
}
_unifying_receiver = lambda product_id: {
'vendor_id': 0x046d,
'product_id': product_id,
'usb_interface': 2,
'hid_driver': _DRIVER, # noqa: F821
'name': _('Unifying Receiver'),
'receiver_kind': 'unifying',
'may_unpair': True
"vendor_id": 0x046D,
"product_id": product_id,
"usb_interface": 2,
"hid_driver": _DRIVER, # noqa: F821
"name": _("Unifying Receiver"),
"receiver_kind": "unifying",
"may_unpair": True,
}
_nano_receiver = lambda product_id: {
'vendor_id': 0x046d,
'product_id': product_id,
'usb_interface': 1,
'hid_driver': _DRIVER, # noqa: F821
'name': _('Nano Receiver'),
'receiver_kind': 'nano',
'may_unpair': False,
're_pairs': True
"vendor_id": 0x046D,
"product_id": product_id,
"usb_interface": 1,
"hid_driver": _DRIVER, # noqa: F821
"name": _("Nano Receiver"),
"receiver_kind": "nano",
"may_unpair": False,
"re_pairs": True,
}
_nano_receiver_no_unpair = lambda product_id: {
'vendor_id': 0x046d,
'product_id': product_id,
'usb_interface': 1,
'hid_driver': _DRIVER, # noqa: F821
'name': _('Nano Receiver'),
'receiver_kind': 'nano',
'may_unpair': False,
'unpair': False,
're_pairs': True
"vendor_id": 0x046D,
"product_id": product_id,
"usb_interface": 1,
"hid_driver": _DRIVER, # noqa: F821
"name": _("Nano Receiver"),
"receiver_kind": "nano",
"may_unpair": False,
"unpair": False,
"re_pairs": True,
}
_nano_receiver_max2 = lambda product_id: {
'vendor_id': 0x046d,
'product_id': product_id,
'usb_interface': 1,
'hid_driver': _DRIVER, # noqa: F821
'name': _('Nano Receiver'),
'receiver_kind': 'nano',
'max_devices': 2,
'may_unpair': False,
're_pairs': True
"vendor_id": 0x046D,
"product_id": product_id,
"usb_interface": 1,
"hid_driver": _DRIVER, # noqa: F821
"name": _("Nano Receiver"),
"receiver_kind": "nano",
"max_devices": 2,
"may_unpair": False,
"re_pairs": True,
}
_nano_receiver_maxn = lambda product_id, max: {
'vendor_id': 0x046d,
'product_id': product_id,
'usb_interface': 1,
'hid_driver': _DRIVER, # noqa: F821
'name': _('Nano Receiver'),
'receiver_kind': 'nano',
'max_devices': max,
'may_unpair': False,
're_pairs': True
"vendor_id": 0x046D,
"product_id": product_id,
"usb_interface": 1,
"hid_driver": _DRIVER, # noqa: F821
"name": _("Nano Receiver"),
"receiver_kind": "nano",
"max_devices": max,
"may_unpair": False,
"re_pairs": True,
}
_lenovo_receiver = lambda product_id: {
'vendor_id': 0x17ef,
'product_id': product_id,
'usb_interface': 1,
'hid_driver': _DRIVER, # noqa: F821
'name': _('Nano Receiver'),
'receiver_kind': 'nano',
'may_unpair': False
"vendor_id": 0x17EF,
"product_id": product_id,
"usb_interface": 1,
"hid_driver": _DRIVER, # noqa: F821
"name": _("Nano Receiver"),
"receiver_kind": "nano",
"may_unpair": False,
}
_lightspeed_receiver = lambda product_id: {
'vendor_id': 0x046d,
'product_id': product_id,
'usb_interface': 2,
'hid_driver': _DRIVER, # noqa: F821
'name': _('Lightspeed Receiver'),
'may_unpair': False
"vendor_id": 0x046D,
"product_id": product_id,
"usb_interface": 2,
"hid_driver": _DRIVER, # noqa: F821
"name": _("Lightspeed Receiver"),
"may_unpair": False,
}
_ex100_receiver = lambda product_id: {
'vendor_id': 0x046d,
'product_id': product_id,
'usb_interface': 1,
'hid_driver': _DRIVER, # noqa: F821
'name': _('EX100 Receiver 27 Mhz'),
'receiver_kind': '27Mhz',
'max_devices': 4,
'may_unpair': False,
're_pairs': True
"vendor_id": 0x046D,
"product_id": product_id,
"usb_interface": 1,
"hid_driver": _DRIVER, # noqa: F821
"name": _("EX100 Receiver 27 Mhz"),
"receiver_kind": "27Mhz",
"max_devices": 4,
"may_unpair": False,
"re_pairs": True,
}
# 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
# 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)
UNIFYING_RECEIVER_C52B = _unifying_receiver(0xc52b)
UNIFYING_RECEIVER_C532 = _unifying_receiver(0xc532)
UNIFYING_RECEIVER_C52B = _unifying_receiver(0xC52B)
UNIFYING_RECEIVER_C532 = _unifying_receiver(0xC532)
# Nano receivers (usually sold with low-end devices)
NANO_RECEIVER_ADVANCED = _nano_receiver_no_unpair(0xc52f)
NANO_RECEIVER_C518 = _nano_receiver(0xc518)
NANO_RECEIVER_C51A = _nano_receiver(0xc51a)
NANO_RECEIVER_C51B = _nano_receiver(0xc51b)
NANO_RECEIVER_C521 = _nano_receiver(0xc521)
NANO_RECEIVER_C525 = _nano_receiver(0xc525)
NANO_RECEIVER_C526 = _nano_receiver(0xc526)
NANO_RECEIVER_C52E = _nano_receiver_no_unpair(0xc52e)
NANO_RECEIVER_C531 = _nano_receiver(0xc531)
NANO_RECEIVER_C534 = _nano_receiver_max2(0xc534)
NANO_RECEIVER_C535 = _nano_receiver(0xc535) # branded as Dell
NANO_RECEIVER_C537 = _nano_receiver(0xc537)
NANO_RECEIVER_ADVANCED = _nano_receiver_no_unpair(0xC52F)
NANO_RECEIVER_C518 = _nano_receiver(0xC518)
NANO_RECEIVER_C51A = _nano_receiver(0xC51A)
NANO_RECEIVER_C51B = _nano_receiver(0xC51B)
NANO_RECEIVER_C521 = _nano_receiver(0xC521)
NANO_RECEIVER_C525 = _nano_receiver(0xC525)
NANO_RECEIVER_C526 = _nano_receiver(0xC526)
NANO_RECEIVER_C52E = _nano_receiver_no_unpair(0xC52E)
NANO_RECEIVER_C531 = _nano_receiver(0xC531)
NANO_RECEIVER_C534 = _nano_receiver_max2(0xC534)
NANO_RECEIVER_C535 = _nano_receiver(0xC535) # branded as Dell
NANO_RECEIVER_C537 = _nano_receiver(0xC537)
# NANO_RECEIVER_C542 = _nano_receiver(0xc542) # does not use HID++
NANO_RECEIVER_6042 = _lenovo_receiver(0x6042)
# Lightspeed receivers (usually sold with gaming devices)
LIGHTSPEED_RECEIVER_C539 = _lightspeed_receiver(0xc539)
LIGHTSPEED_RECEIVER_C53A = _lightspeed_receiver(0xc53a)
LIGHTSPEED_RECEIVER_C53D = _lightspeed_receiver(0xc53d)
LIGHTSPEED_RECEIVER_C53F = _lightspeed_receiver(0xc53f)
LIGHTSPEED_RECEIVER_C541 = _lightspeed_receiver(0xc541)
LIGHTSPEED_RECEIVER_C545 = _lightspeed_receiver(0xc545)
LIGHTSPEED_RECEIVER_C547 = _lightspeed_receiver(0xc547)
LIGHTSPEED_RECEIVER_C539 = _lightspeed_receiver(0xC539)
LIGHTSPEED_RECEIVER_C53A = _lightspeed_receiver(0xC53A)
LIGHTSPEED_RECEIVER_C53D = _lightspeed_receiver(0xC53D)
LIGHTSPEED_RECEIVER_C53F = _lightspeed_receiver(0xC53F)
LIGHTSPEED_RECEIVER_C541 = _lightspeed_receiver(0xC541)
LIGHTSPEED_RECEIVER_C545 = _lightspeed_receiver(0xC545)
LIGHTSPEED_RECEIVER_C547 = _lightspeed_receiver(0xC547)
# EX100 old style receiver pre-unifying protocol
# 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
ALL = (

View File

@ -31,28 +31,266 @@ is_string = lambda d: isinstance(d, str)
def crc16(data: bytes):
'''
"""
CRC-16 (CCITT) implemented with a precomputed lookup table
'''
"""
table = [
0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50A5, 0x60C6, 0x70E7, 0x8108, 0x9129, 0xA14A, 0xB16B, 0xC18C, 0xD1AD, 0xE1CE,
0xF1EF, 0x1231, 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
0x0000,
0x1021,
0x2042,
0x3063,
0x4084,
0x50A5,
0x60C6,
0x70E7,
0x8108,
0x9129,
0xA14A,
0xB16B,
0xC18C,
0xD1AD,
0xE1CE,
0xF1EF,
0x1231,
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
@ -88,7 +326,7 @@ class NamedInt(int):
return self.name.lower() == other.lower()
# this should catch comparisons with bytes in Py3
if other is not None:
raise TypeError('Unsupported type ' + str(type(other)))
raise TypeError("Unsupported type " + str(type(other)))
def __ne__(self, other):
return not self.__eq__(other)
@ -100,19 +338,19 @@ class NamedInt(int):
return self.name
def __repr__(self):
return 'NamedInt(%d, %r)' % (int(self), self.name)
return "NamedInt(%d, %r)" % (int(self), self.name)
@classmethod
def from_yaml(cls, loader, node):
args = loader.construct_mapping(node)
return cls(value=args['value'], name=args['name'])
return cls(value=args["value"], name=args["name"])
@classmethod
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)
@ -129,14 +367,14 @@ class NamedInts:
if the value already exists in the set (int or string), ValueError will be
raised.
"""
__slots__ = ('__dict__', '_values', '_indexed', '_fallback', '_is_sorted')
__slots__ = ("__dict__", "_values", "_indexed", "_fallback", "_is_sorted")
def __init__(self, dict=None, **kwargs):
def _readable_name(n):
if not is_string(n):
raise TypeError('expected string, got ' + str(type(n)))
return n.replace('__', '/').replace('_', ' ')
raise TypeError("expected string, got " + str(type(n)))
return n.replace("__", "/").replace("_", " ")
# print (repr(kwargs))
elements = dict if dict else kwargs
@ -163,13 +401,13 @@ class NamedInts:
def flag_names(self, value):
unknown_bits = value
for k in self._indexed:
assert bin(k).count('1') == 1
assert bin(k).count("1") == 1
if k & value == k:
unknown_bits &= ~k
yield str(self._indexed[k])
if unknown_bits:
yield 'unknown:%06X' % unknown_bits
yield "unknown:%06X" % unknown_bits
def _sort_values(self):
self._values = sorted(self._values)
@ -190,7 +428,7 @@ class NamedInts:
elif is_string(index):
if index in self.__dict__:
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):
values = self._values if self._is_sorted else sorted(self._values)
@ -224,17 +462,17 @@ class NamedInts:
def __setitem__(self, index, name):
assert isinstance(index, int), type(index)
if isinstance(name, NamedInt):
assert int(index) == int(name), repr(index) + ' ' + repr(name)
assert int(index) == int(name), repr(index) + " " + repr(name)
value = name
elif is_string(name):
value = NamedInt(index, name)
else:
raise TypeError('name must be a string')
raise TypeError("name must be a string")
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:
raise ValueError('%d (%s) already known' % (int(value), value))
raise ValueError("%d (%s) already known" % (int(value), value))
self._values.append(value)
self._is_sorted = False
@ -257,14 +495,13 @@ class NamedInts:
return len(self._values)
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):
return NamedInts(**self.__dict__, **other.__dict__)
class UnsortedNamedInts(NamedInts):
def _sort_values(self):
pass
@ -276,18 +513,18 @@ class UnsortedNamedInts(NamedInts):
def strhex(x):
assert x is not None
"""Produce a hex-string representation of a sequence of bytes."""
return _hexlify(x).decode('ascii').upper()
return _hexlify(x).decode("ascii").upper()
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):
if count:
return x.to_bytes(length=count, byteorder='big', signed=signed)
return x.to_bytes(length=count, byteorder="big", signed=signed)
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):
@ -306,7 +543,7 @@ class KwException(Exception):
"""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)

View File

@ -33,7 +33,6 @@ from .hidpp10_constants import REGISTERS as _R
class _DeviceDescriptor:
def __init__(
self,
name=None,
@ -44,7 +43,7 @@ class _DeviceDescriptor:
registers=None,
usbid=None,
interface=None,
btid=None
btid=None,
):
self.name = name
self.kind = kind
@ -76,21 +75,30 @@ def _D(
):
if kind is None:
kind = (
_DK.mouse 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
_DK.mouse
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 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:
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:
if w[0:1] == '1':
assert kind == _DK.mouse, '%s has protocol %0.1f, wpid %s' % (name, protocol, w)
elif w[0:1] == '2':
assert kind in (_DK.keyboard, _DK.numpad), '%s has protocol %0.1f, wpid %s' % (name, protocol, w)
if w[0:1] == "1":
assert kind == _DK.mouse, "%s has protocol %0.1f, wpid %s" % (name, protocol, w)
elif w[0:1] == "2":
assert kind in (_DK.keyboard, _DK.numpad), "%s has protocol %0.1f, wpid %s" % (name, protocol, w)
device_descriptor = _DeviceDescriptor(
name=name,
@ -101,23 +109,23 @@ def _D(
registers=registers,
usbid=usbid,
interface=interface,
btid=btid
btid=btid,
)
if 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:
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:
DEVICES[codename] = device_descriptor
if 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], )
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],)
DEVICES_WPID[w] = device_descriptor

View File

@ -64,7 +64,7 @@ class Device:
long=None,
product_id=None,
bus_id=None,
setting_callback=None
setting_callback=None,
):
assert receiver or handle
Device.instances.append(self)
@ -125,15 +125,15 @@ class Device:
# assert link_notification.address == (0x04 if unifying else 0x03)
kind = ord(link_notification.data[0:1]) & 0x0F
# get 27Mhz wpid and set kind based on index
if receiver.receiver_kind == '27Mhz': # 27 Mhz receiver
self.wpid = '00' + _strhex(link_notification.data[2:3])
if receiver.receiver_kind == "27Mhz": # 27 Mhz receiver
self.wpid = "00" + _strhex(link_notification.data[2:3])
kind = receiver.get_kind_from_index(number)
self._kind = _hidpp10_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)
if not self.wpid:
logger.error('Unable to get wpid from udev for device %d of %s', number, receiver)
raise exceptions.NoSuchDevice(number=number, receiver=receiver, error='Not present 27Mhz device')
logger.error("Unable to get wpid from udev for device %d of %s", number, receiver)
raise exceptions.NoSuchDevice(number=number, receiver=receiver, error="Not present 27Mhz device")
kind = receiver.get_kind_from_index(number)
self._kind = _hidpp10_constants.DEVICE_KIND[kind]
else: # get information from pairing registers
@ -141,10 +141,10 @@ class Device:
self.update_pairing_information()
self.update_extended_pairing_information()
if not self.wpid and not self._serial: # if neither then the device almost certainly wasn't found
raise 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
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)
if self.descriptor is None:
@ -155,8 +155,9 @@ class Device:
self.descriptor = _descriptors.get_codename(self._codename)
else:
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 \
_descriptors.get_usbid(self.product_id)
self.descriptor = (
_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
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)
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
for device in Device.instances:
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:
self._codename = _hidpp20.get_friendly_name(self)
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:
codename = self.receiver.device_codename(self.number)
if codename:
self._codename = codename
elif self.protocol < 2.0:
self._codename = '? (%s)' % (self.wpid or self.product_id)
return self._codename or '?? (%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)
@property
def name(self):
@ -216,14 +217,14 @@ class Device:
pass
if self.online and self.protocol >= 2.0:
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):
ids = _hidpp20.get_ids(self)
if ids:
self._unitId, self._modelId, self._tid_map = ids
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
def unitId(self):
@ -251,7 +252,7 @@ class Device:
if not self._kind:
self._kind = kind
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):
if self.receiver:
@ -268,7 +269,7 @@ class Device:
if not self._kind and self.protocol >= 2.0:
kind = _hidpp20.get_kind(self)
self._kind = KIND_MAP[kind] if kind else None
return self._kind or '?'
return self._kind or "?"
@property
def firmware(self):
@ -283,13 +284,13 @@ class Device:
def serial(self):
if not self._serial:
self.update_extended_pairing_information()
return self._serial or ''
return self._serial or ""
@property
def id(self):
if not self.serial:
if self.persister and self.persister.get('_serial', None):
self._serial = self.persister.get('_serial', None)
if self.persister and self.persister.get("_serial", None):
self._serial = self.persister.get("_serial", None)
return self.unitId or self.serial
@property
@ -399,17 +400,17 @@ class Device:
if self.protocol < 2.0:
return _hidpp10.get_battery(self)
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:
result = _hidpp20.get_battery(self, battery_feature)
try:
feature, level, next, status, voltage = result
if self.persister and battery_feature is None:
self.persister['_battery'] = feature
self.persister["_battery"] = feature
return level, next, status, voltage
except Exception:
if self.persister and battery_feature is None:
self.persister['_battery'] = result
self.persister["_battery"] = result
def enable_connection_notifications(self, enable=True):
"""Enable or disable device (dis)connection notifications on this
@ -428,12 +429,12 @@ class Device:
set_flag_bits = 0
ok = _hidpp10.set_notification_flags(self, set_flag_bits)
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_names = None if flag_bits is None else tuple(_hidpp10_constants.NOTIFICATION_FLAG.flag_names(flag_bits))
if logger.isEnabledFor(logging.INFO):
logger.info('%s: device notifications %s %s', self, 'enabled' if enable else 'disabled', flag_names)
logger.info("%s: device notifications %s %s", self, "enabled" if enable else "disabled", flag_names)
return flag_bits if ok else None
def add_notification_handler(self, id: str, fn):
@ -454,7 +455,7 @@ class Device:
"""Unregisters the notification handler under name `id`."""
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:
del self._notification_handlers[id]
@ -477,7 +478,7 @@ class Device:
*params,
no_reply=no_reply,
long_message=long,
protocol=self.protocol
protocol=self.protocol,
)
def feature_request(self, feature, function=0x00, *params, no_reply=False):
@ -517,10 +518,10 @@ class Device:
def __str__(self):
try:
name = self.name or self.codename or '?'
name = self.name or self.codename or "?"
except exceptions.NoSuchDevice:
name = 'name not available'
return '<Device(%d,%s,%s,%s)>' % (self.number, self.wpid or self.product_id, name, self.serial)
name = "name not available"
return "<Device(%d,%s,%s,%s)>" % (self.number, self.wpid or self.product_id, name, self.serial)
__repr__ = __str__
@ -544,20 +545,20 @@ class Device:
long=device_info.hidpp_long,
product_id=device_info.product_id,
bus_id=device_info.bus_id,
setting_callback=setting_callback
setting_callback=setting_callback,
)
except OSError as e:
logger.exception('open %s', device_info)
logger.exception("open %s", device_info)
if e.errno == _errno.EACCES:
raise
except Exception:
logger.exception('open %s', device_info)
logger.exception("open %s", device_info)
def close(self):
handle, self.handle = self.handle, None
if self in Device.instances:
Device.instances.remove(self)
return (handle and _base.close(handle))
return handle and _base.close(handle)
def __del__(self):
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
physically disconnected from the machine, or its kernel driver module is
unloaded."""
pass
class NoSuchDevice(_KwException):
"""Raised when trying to reach a device number not paired to the receiver."""
pass
class DeviceUnreachable(_KwException):
"""Raised when a request is made to an unreachable (turned off) device."""
pass
class FeatureNotSupported(_KwException):
"""Raised when trying to request a feature not supported by the device."""
pass
class FeatureCallError(_KwException):
"""Raised if the device replied to a feature call with an error."""
pass

View File

@ -34,14 +34,14 @@ logger = logging.getLogger(__name__)
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
request_id = 0x8100 | (int(register_number) & 0x2FF)
return device.request(request_id, *params)
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
request_id = 0x8000 | (int(register_number) & 0x2FF)
return device.request(request_id, *value)
@ -83,18 +83,27 @@ def parse_battery_status(register, reply):
charge = ord(reply[:1])
status_byte = ord(reply[2:3]) & 0xF0
status_text = (
BATTERY_STATUS.discharging if status_byte == 0x30 else
BATTERY_STATUS.recharging if status_byte == 0x50 else BATTERY_STATUS.full if status_byte == 0x90 else None
BATTERY_STATUS.discharging
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
if register == REGISTERS.battery_status:
status_byte = ord(reply[:1])
charge = (
_BATTERY_APPROX.full if status_byte == 7 # full
else _BATTERY_APPROX.good if status_byte == 5 # good
else _BATTERY_APPROX.low if status_byte == 3 # low
else _BATTERY_APPROX.critical if status_byte == 1 # critical
_BATTERY_APPROX.full
if status_byte == 7 # full
else _BATTERY_APPROX.good
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
else _BATTERY_APPROX.empty
)
@ -107,7 +116,7 @@ def parse_battery_status(register, reply):
elif charging_byte & 0x22 == 0x22:
status_text = BATTERY_STATUS.full
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
if charging_byte & 0x03 and status_byte == 0:
@ -129,25 +138,25 @@ def get_firmware(device):
return
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)
if reply:
fw_version += '.B' + _strhex(reply[1:3])
fw = _FirmwareInfo(FIRMWARE_KIND.Firmware, '', fw_version, None)
fw_version += ".B" + _strhex(reply[1:3])
fw = _FirmwareInfo(FIRMWARE_KIND.Firmware, "", fw_version, None)
firmware[0] = fw
reply = read_register(device, REGISTERS.firmware, 0x04)
if reply:
bl_version = _strhex(reply[1:3])
bl_version = '%s.%s' % (bl_version[0:2], bl_version[2:4])
bl = _FirmwareInfo(FIRMWARE_KIND.Bootloader, '', bl_version, None)
bl_version = "%s.%s" % (bl_version[0:2], bl_version[2:4])
bl = _FirmwareInfo(FIRMWARE_KIND.Bootloader, "", bl_version, None)
firmware[1] = bl
reply = read_register(device, REGISTERS.firmware, 0x03)
if reply:
o_version = _strhex(reply[1:3])
o_version = '%s.%s' % (o_version[0:2], o_version[2:4])
o = _FirmwareInfo(FIRMWARE_KIND.Other, '', o_version, None)
o_version = "%s.%s" % (o_version[0:2], o_version[2:4])
o = _FirmwareInfo(FIRMWARE_KIND.Other, "", o_version, None)
firmware[2] = o
if any(firmware):
@ -182,8 +191,8 @@ def set_3leds(device, battery_level=None, charging=None, warning=None):
v1, v2 = 0x20, 0x22
if warning:
# set the blinking flag for the leds already set
v1 |= (v1 >> 1)
v2 |= (v2 >> 1)
v1 |= v1 >> 1
v2 |= v2 >> 1
elif charging:
# blink all green
v1, v2 = 0x30, 0x33

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -128,7 +128,7 @@ FEATURE = NamedInts(
# Fake features for Solaar internal use
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)
@ -150,7 +150,7 @@ BATTERY_STATUS = NamedInts(
full=0x03,
slow_recharge=0x04,
invalid_battery=0x05,
thermal_error=0x06
thermal_error=0x06,
)
ONBOARD_MODES = NamedInts(MODE_NO_CHANGE=0x00, MODE_ONBOARD=0x01, MODE_HOST=0x02)
@ -170,7 +170,7 @@ ERROR = NamedInts(
invalid_feature_index=0x06,
invalid_function=0x07,
busy=0x08,
unsupported=0x09
unsupported=0x09,
)
# Gesture Ids for feature GESTURE_2
@ -236,7 +236,7 @@ GESTURE = NamedInts(
Finger10=99,
DeviceSpecificRawData=100,
)
GESTURE._fallback = lambda x: 'unknown:%04X' % x
GESTURE._fallback = lambda x: "unknown:%04X" % x
# Param Ids for feature GESTURE_2
PARAM = NamedInts(
@ -245,4 +245,4 @@ PARAM = NamedInts(
RatioZone=3, # 4 bytes, left, bottom, width, height; unit 1/240 pad size
ScaleFactor=4, # 2-byte integer, with 256 as normal scale
)
PARAM._fallback = lambda x: 'unknown:%04X' % x
PARAM._fallback = lambda x: "unknown:%04X" % x

View File

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

View File

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

View File

@ -49,7 +49,7 @@ def process(device, notification):
assert device
assert notification
assert hasattr(device, 'status')
assert hasattr(device, "status")
status = device.status
assert status is not None
@ -70,9 +70,9 @@ def _process_receiver_notification(receiver, status, n):
if n.sub_id == 0x4A: # pairing lock notification
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):
logger.info('%s: %s', receiver, reason)
logger.info("%s: %s", receiver, reason)
status[_K.ERROR] = None
if status.lock_open:
status.new_device = None
@ -80,16 +80,16 @@ def _process_receiver_notification(receiver, status, n):
if pair_error:
status[_K.ERROR] = error_string = _hidpp10.PAIRING_ERRORS[pair_error]
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)
return True
elif n.sub_id == _R.discovery_status_notification: # Bolt pairing
with notification_lock:
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):
logger.info('%s: %s', receiver, reason)
logger.info("%s: %s", receiver, reason)
status[_K.ERROR] = None
if status.discovering:
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])
if 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)
return True
@ -114,16 +114,16 @@ def _process_receiver_notification(receiver, status, n):
status.device_address = n.data[6:12]
status.device_authentication = n.data[14]
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
elif n.sub_id == _R.pairing_status_notification: # Bolt pairing
with notification_lock:
status.device_passkey = None
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):
logger.info('%s: %s', receiver, reason)
logger.info("%s: %s", receiver, reason)
status[_K.ERROR] = None
if not status.lock_open:
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:
status[_K.ERROR] = error_string = _hidpp10.BOLT_PAIRING_ERRORS[pair_error]
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)
return True
elif n.sub_id == _R.passkey_request_notification: # Bolt pairing
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
elif n.sub_id == _R.passkey_pressed_notification: # Bolt pairing
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
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
try:
feature = device.features.get_feature(n.sub_id)
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 _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):
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:
# do all DJ paired notifications also show up as HID++ 1.0 notifications?
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
if n.sub_id == 0x41:
# do all DJ paired notifications also show up as HID++ 1.0 notifications?
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
if n.sub_id == 0x42:
connected = not n.address & 0x01
if logger.isEnabledFor(logging.INFO):
logger.info('%s: DJ connection: %s %s', device, connected, n)
status.changed(active=connected, alert=_ALERT.NONE, reason=_('connected') if connected else _('disconnected'))
logger.info("%s: DJ connection: %s %s", device, connected, n)
status.changed(active=connected, alert=_ALERT.NONE, reason=_("connected") if connected else _("disconnected"))
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):
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):
# 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
charge, next_charge, status_text, voltage = _hidpp10.parse_battery_status(n.sub_id, data)
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>
# TODO anything we can do with this?
if logger.isEnabledFor(logging.INFO):
logger.info('illumination event: %s', n)
logger.info("illumination event: %s", n)
return True
logger.warning('%s: unrecognized %s', device, n)
logger.warning("%s: unrecognized %s", device, n)
def _process_hidpp10_notification(device, status, n):
@ -257,16 +257,16 @@ def _process_hidpp10_notification(device, status, n):
device.status = None
if device.number in device.receiver:
del device.receiver[device.number]
status.changed(active=False, alert=_ALERT.ALL, reason=_('unpaired'))
status.changed(active=False, alert=_ALERT.ALL, reason=_("unpaired"))
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
# device connection (and disconnection)
if n.sub_id == 0x41:
flags = ord(n.data[:1]) & 0xF0
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_encrypted = bool(flags & 0x80)
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_encrypted = bool(flags & 0x20) or n.address == 0x10 # Bolt protocol always encrypted
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
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):
logger.debug(
'%s: protocol %s connection notification: software=%s, encrypted=%s, link=%s, payload=%s', device, n.address,
bool(flags & 0x10), link_encrypted, link_established, bool(flags & 0x80)
"%s: protocol %s connection notification: software=%s, encrypted=%s, link=%s, payload=%s",
device,
n.address,
bool(flags & 0x10),
link_encrypted,
link_established,
bool(flags & 0x80),
)
status[_K.LINK_ENCRYPTED] = link_encrypted
status.changed(active=link_established)
@ -298,19 +303,19 @@ def _process_hidpp10_notification(device, status, n):
if n.sub_id == 0x4B:
if n.address == 0x01:
if logger.isEnabledFor(logging.DEBUG):
logger.debug('%s: device powered on', device)
reason = status.to_string() or _('powered on')
logger.debug("%s: device powered on", device)
reason = status.to_string() or _("powered on")
status.changed(active=True, alert=_ALERT.NOTIFICATION, reason=reason)
else:
logger.warning('%s: unknown %s', device, n)
logger.warning("%s: unknown %s", device, n)
return True
logger.warning('%s: unrecognized %s', device, n)
logger.warning("%s: unrecognized %s", device, n)
def _process_feature_notification(device, status, n, feature):
if logger.isEnabledFor(logging.DEBUG):
logger.debug('%s: notification for feature %s, report %s, data %s', device, feature, n.address >> 4, _strhex(n.data))
logger.debug("%s: notification for feature %s, report %s, data %s", device, feature, n.address >> 4, _strhex(n.data))
if feature == _F.BATTERY_STATUS:
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)
elif n.address == 0x10:
if logger.isEnabledFor(logging.INFO):
logger.info('%s: spurious BATTERY status %s', device, n)
logger.info("%s: spurious BATTERY status %s", device, n)
else:
logger.warning('%s: unknown BATTERY %s', device, n)
logger.warning("%s: unknown BATTERY %s", device, n)
elif feature == _F.BATTERY_VOLTAGE:
if n.address == 0x00:
_ignore, level, nextl, battery_status, voltage = _hidpp20.decipher_battery_voltage(n.data)
status.set_battery_info(level, nextl, battery_status, voltage)
else:
logger.warning('%s: unknown VOLTAGE %s', device, n)
logger.warning("%s: unknown VOLTAGE %s", device, n)
elif feature == _F.UNIFIED_BATTERY:
if n.address == 0x00:
_ignore, level, nextl, battery_status, voltage = _hidpp20.decipher_battery_unified(n.data)
status.set_battery_info(level, nextl, battery_status, voltage)
else:
logger.warning('%s: unknown UNIFIED BATTERY %s', device, n)
logger.warning("%s: unknown UNIFIED BATTERY %s", device, n)
elif feature == _F.ADC_MEASUREMENT:
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
status.changed(active=False)
else:
logger.warning('%s: unknown ADC MEASUREMENT %s', device, n)
logger.warning("%s: unknown ADC MEASUREMENT %s", device, n)
elif feature == _F.SOLAR_DASHBOARD:
if n.data[5:9] == b'GOOD':
charge, lux, adc = _unpack('!BHH', n.data[:5])
if n.data[5:9] == b"GOOD":
charge, lux, adc = _unpack("!BHH", n.data[:5])
# guesstimate the battery voltage, emphasis on 'guess'
# status_text = '%1.2fV' % (adc * 2.67793237653 / 0x0672)
status_text = _hidpp20_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)
elif n.address == 0x20:
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)
# first cancel any reporting
# device.feature_request(_F.SOLAR_DASHBOARD)
@ -372,93 +377,93 @@ def _process_feature_notification(device, status, n, feature):
reports_period = 2 # seconds
device.feature_request(_F.SOLAR_DASHBOARD, 0x00, reports_count, reports_period)
else:
logger.warning('%s: unknown SOLAR CHARGE %s', device, n)
logger.warning("%s: unknown SOLAR CHARGE %s", device, n)
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:
if n.address == 0x00:
if logger.isEnabledFor(logging.DEBUG):
logger.debug('wireless status: %s', n)
reason = 'powered on' if n.data[2] == 1 else None
logger.debug("wireless status: %s", n)
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
alert = _ALERT.NONE
status.changed(active=True, alert=alert, reason=reason, push=True)
else:
logger.warning('%s: unknown WIRELESS %s', device, n)
logger.warning("%s: unknown WIRELESS %s", device, n)
elif feature == _F.TOUCHMOUSE_RAW_POINTS:
if n.address == 0x00:
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:
touch = ord(n.data[:1])
button_down = bool(touch & 0x02)
mouse_lifted = bool(touch & 0x01)
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:
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}?
elif feature == _F.REPROG_CONTROLS:
if n.address == 0x00:
if logger.isEnabledFor(logging.INFO):
logger.info('%s: reprogrammable key: %s', device, n)
logger.info("%s: reprogrammable key: %s", device, n)
else:
logger.warning('%s: unknown REPROG_CONTROLS %s', device, n)
logger.warning("%s: unknown REPROG_CONTROLS %s", device, n)
elif feature == _F.BACKLIGHT2:
if (n.address == 0x00):
level = _unpack('!B', n.data[1:2])[0]
if n.address == 0x00:
level = _unpack("!B", n.data[1:2])[0]
if device.setting_callback:
device.setting_callback(device, _st.Backlight2Level, [level])
elif feature == _F.REPROG_CONTROLS_V4:
if n.address == 0x00:
if logger.isEnabledFor(logging.DEBUG):
cid1, cid2, cid3, cid4 = _unpack('!HHHH', n.data[:8])
logger.debug('%s: diverted controls pressed: 0x%x, 0x%x, 0x%x, 0x%x', device, cid1, cid2, cid3, cid4)
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)
elif n.address == 0x10:
if logger.isEnabledFor(logging.DEBUG):
dx, dy = _unpack('!hh', n.data[:4])
logger.debug('%s: rawXY dx=%i dy=%i', device, dx, dy)
dx, dy = _unpack("!hh", n.data[:4])
logger.debug("%s: rawXY dx=%i dy=%i", device, dx, dy)
elif n.address == 0x20:
if logger.isEnabledFor(logging.DEBUG):
logger.debug('%s: received analyticsKeyEvents', device)
logger.debug("%s: received analyticsKeyEvents", device)
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:
if (n.address == 0x00):
if n.address == 0x00:
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
periods = flags & 0x0f
logger.info('%s: WHEEL: res: %d periods: %d delta V:%-3d', device, high_res, periods, delta_v)
elif (n.address == 0x10):
periods = flags & 0x0F
logger.info("%s: WHEEL: res: %d periods: %d delta V:%-3d", device, high_res, periods, delta_v)
elif n.address == 0x10:
ratchet = n.data[0]
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 device.setting_callback:
device.setting_callback(device, _st.ScrollRatchet, [2 if ratchet else 1])
else:
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:
if (n.address > 0x10):
if n.address > 0x10:
if logger.isEnabledFor(logging.INFO):
logger.info('%s: unknown ONBOARD PROFILES %s', device, n)
logger.info("%s: unknown ONBOARD PROFILES %s", device, n)
else:
if (n.address == 0x00):
profile_sector = _unpack('!H', n.data[:2])[0]
if n.address == 0x00:
profile_sector = _unpack("!H", n.data[:2])[0]
if profile_sector:
_st.profile_change(device, profile_sector)
elif (n.address == 0x10):
resolution_index = _unpack('!B', n.data[:1])[0]
profile_sector = _unpack('!H', device.feature_request(_F.ONBOARD_PROFILES, 0x40)[:2])[0]
elif n.address == 0x10:
resolution_index = _unpack("!B", n.data[:1])[0]
profile_sector = _unpack("!H", device.feature_request(_F.ONBOARD_PROFILES, 0x40)[:2])[0]
if device.setting_callback:
for profile in device.profiles.profiles.values() if device.profiles else []:
if profile.sector == profile_sector:

View File

@ -22,9 +22,11 @@ logger = logging.getLogger(__name__)
try:
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
available = True
except (ValueError, ImportError):
available = False
@ -39,18 +41,18 @@ if available:
if available:
if not Notify.is_initted():
if logger.isEnabledFor(logging.INFO):
logger.info('starting desktop notifications')
logger.info("starting desktop notifications")
try:
return Notify.init('solaar') # replace with better name later
return Notify.init("solaar") # replace with better name later
except Exception:
logger.exception('initializing desktop notifications')
logger.exception("initializing desktop notifications")
available = False
return available and Notify.is_initted()
def uninit():
if available and Notify.is_initted():
if logger.isEnabledFor(logging.INFO):
logger.info('stopping desktop notifications')
logger.info("stopping desktop notifications")
_notifications.clear()
Notify.uninit()
@ -64,32 +66,32 @@ if available:
icon_name = device_icon_name(dev.name, dev.kind) if icon is None else icon
n.update(summary, message, icon_name)
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:
# if logger.isEnabledFor(logging.DEBUG):
# logger.debug("showing %s", n)
n.show()
except Exception:
logger.exception('showing %s', n)
logger.exception("showing %s", n)
_ICON_LISTS = {}
def device_icon_list(name='_', kind=None):
def device_icon_list(name="_", kind=None):
icon_list = _ICON_LISTS.get(name)
if icon_list is None:
# names of possible icons, in reverse order of likelihood
# the theme will hopefully pick up the most appropriate
icon_list = ['preferences-desktop-peripherals']
icon_list = ["preferences-desktop-peripherals"]
if kind:
if str(kind) == 'numpad':
icon_list += ('input-keyboard', 'input-dialpad')
elif str(kind) == 'touchpad':
icon_list += ('input-mouse', 'input-tablet')
elif str(kind) == 'trackball':
icon_list += ('input-mouse', )
elif str(kind) == 'headset':
icon_list += ('audio-headphones', 'audio-headset')
icon_list += ('input-' + str(kind), )
if str(kind) == "numpad":
icon_list += ("input-keyboard", "input-dialpad")
elif str(kind) == "touchpad":
icon_list += ("input-mouse", "input-tablet")
elif str(kind) == "trackball":
icon_list += ("input-mouse",)
elif str(kind) == "headset":
icon_list += ("audio-headphones", "audio-headset")
icon_list += ("input-" + str(kind),)
_ICON_LISTS[name] = icon_list
return icon_list

View File

@ -44,6 +44,7 @@ class Receiver:
The paired devices are available through the sequence interface.
"""
number = 0xFF
kind = None
@ -56,33 +57,36 @@ class Receiver:
self.setting_callback = setting_callback
product_info = _product_information(self.product_id)
if not product_info:
logger.warning('Unknown receiver type: %s', self.product_id)
logger.warning("Unknown receiver type: %s", self.product_id)
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
if self.receiver_kind == 'bolt':
if self.receiver_kind == "bolt":
serial_reply = self.read_register(_R.bolt_uniqueId)
self.serial = _strhex(serial_reply)
self.max_devices = product_info.get('max_devices', 1)
self.may_unpair = product_info.get('may_unpair', False)
self.max_devices = product_info.get("max_devices", 1)
self.may_unpair = product_info.get("may_unpair", False)
else:
serial_reply = self.read_register(_R.receiver_info, _IR.receiver_information)
if serial_reply:
self.serial = _strhex(serial_reply[1:5])
self.max_devices = ord(serial_reply[6:7])
if self.max_devices <= 0 or self.max_devices > 6:
self.max_devices = product_info.get('max_devices', 1)
self.may_unpair = product_info.get('may_unpair', False)
self.max_devices = product_info.get("max_devices", 1)
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)
self.serial = None
self.max_devices = product_info.get('max_devices', 1)
self.may_unpair = product_info.get('may_unpair', False)
self.max_devices = product_info.get("max_devices", 1)
self.may_unpair = product_info.get("may_unpair", False)
self.name = product_info.get('name', 'Receiver')
self.re_pairs = product_info.get('re_pairs', False)
self._str = '<%s(%s,%s%s)>' % (
self.name.replace(' ', ''), self.path, '' if isinstance(self.handle, int) else 'T', self.handle
self.name = product_info.get("name", "Receiver")
self.re_pairs = product_info.get("re_pairs", False)
self._str = "<%s(%s,%s%s)>" % (
self.name.replace(" ", ""),
self.path,
"" if isinstance(self.handle, int) else "T",
self.handle,
)
self._firmware = None
@ -95,7 +99,7 @@ class Receiver:
if d:
d.close()
self._devices.clear()
return (handle and _base.close(handle))
return handle and _base.close(handle)
def __del__(self):
self.close()
@ -131,36 +135,36 @@ class Receiver:
set_flag_bits = 0
ok = _hidpp10.set_notification_flags(self, set_flag_bits)
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
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))
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
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)
if codename:
codename = codename[3:3 + min(14, ord(codename[2:3]))]
return codename.decode('ascii')
codename = codename[3 : 3 + min(14, ord(codename[2:3]))]
return codename.decode("ascii")
return
codename = self.read_register(_R.receiver_info, _IR.device_name + n - 1)
if codename:
codename = codename[2:2 + ord(codename[1:2])]
return codename.decode('ascii')
codename = codename[2 : 2 + ord(codename[1:2])]
return codename.decode("ascii")
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)
if pair_info:
wpid = _strhex(pair_info[3:4]) + _strhex(pair_info[2:3])
kind = _hidpp10_constants.DEVICE_KIND[ord(pair_info[1:2]) & 0x0F]
return wpid, kind, 0
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
kind = None
polling_rate = None
@ -168,35 +172,35 @@ class Receiver:
if pair_info: # may be either a Unifying receiver, or an Unifying-ready receiver
wpid = _strhex(pair_info[3:5])
kind = _hidpp10_constants.DEVICE_KIND[ord(pair_info[7:8]) & 0x0F]
polling_rate = str(ord(pair_info[2:3])) + 'ms'
elif self.receiver_kind == '27Mz': # 27Mhz receiver, fill extracting WPID from udev path
polling_rate = str(ord(pair_info[2:3])) + "ms"
elif self.receiver_kind == "27Mz": # 27Mhz receiver, fill extracting WPID from udev path
wpid = _hid.find_paired_node_wpid(self.path, n)
if not wpid:
logger.error('Unable to get wpid from udev for device %d of %s', n, self)
raise exceptions.NoSuchDevice(number=n, receiver=self, error='Not present 27Mhz device')
logger.error("Unable to get wpid from udev for device %d of %s", n, self)
raise exceptions.NoSuchDevice(number=n, receiver=self, error="Not present 27Mhz device")
kind = _hidpp10_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)
if device_info:
wpid = _strhex(device_info[3:5])
kind = _hidpp10_constants.DEVICE_KIND[0x00] # unknown kind
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:
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
def device_extended_pairing_information(self, n):
serial = None
power_switch = '(unknown)'
if self.receiver_kind == 'bolt':
power_switch = "(unknown)"
if self.receiver_kind == "bolt":
pair_info = self.read_register(_R.receiver_info, _IR.bolt_pairing_information + n)
if pair_info:
serial = _strhex(pair_info[4:8])
return serial, power_switch
else:
return '?', power_switch
return "?", power_switch
pair_info = self.read_register(_R.receiver_info, _IR.extended_pairing_information + n - 1)
if pair_info:
power_switch = _hidpp10_constants.POWER_SWITCH_LOCATION[ord(pair_info[9:10]) & 0x0F]
@ -220,19 +224,19 @@ class Receiver:
elif index == 4: # numpad
kind = 3
else: # unknown device number on 27Mhz receiver
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')
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")
return kind
def notify_devices(self):
"""Scan all devices."""
if self.handle:
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):
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.sub_id == 0x41
@ -240,13 +244,13 @@ class Receiver:
try:
dev = Device(self, number, notification, setting_callback=self.setting_callback)
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
return dev
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
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)
if reply:
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
assert self.receiver_kind == 'bolt'
assert self.receiver_kind == "bolt"
if self.handle:
action = 0x02 if cancel else 0x01
reply = self.write_register(_R.bolt_device_discovery, timeout, action)
if reply:
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
assert self.receiver_kind == 'bolt'
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"
if self.handle:
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)
if reply:
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):
count = self.read_register(_R.receiver_connection)
@ -312,7 +316,7 @@ class Receiver:
return dev
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
raise IndexError(key)
@ -339,9 +343,9 @@ class Receiver:
dev.wpid = None
if key in self._devices:
del self._devices[key]
logger.warning('%s removed device %s', self, dev)
logger.warning("%s removed device %s", self, dev)
else:
if self.receiver_kind == 'bolt':
if self.receiver_kind == "bolt":
reply = self.write_register(_R.bolt_pairing, 0x03, key)
else:
reply = self.write_register(_R.receiver_pairing, 0x03, key)
@ -352,10 +356,10 @@ class Receiver:
if key in self._devices:
del self._devices[key]
if logger.isEnabledFor(logging.INFO):
logger.info('%s unpaired device %s', self, dev)
logger.info("%s unpaired device %s", self, dev)
else:
logger.error('%s failed to unpair device %s', self, dev)
raise Exception('failed to unpair device %s: %s' % (dev.name, key))
logger.error("%s failed to unpair device %s", self, dev)
raise Exception("failed to unpair device %s: %s" % (dev.name, key))
def __len__(self):
return len([d for d in self._devices.values() if d is not None])
@ -393,8 +397,8 @@ class Receiver:
if handle:
return Receiver(handle, device_info.path, device_info.product_id, setting_callback)
except OSError as e:
logger.exception('open %s', device_info)
logger.exception("open %s", device_info)
if e.errno == _errno.EACCES:
raise
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 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:
device.status = ReceiverStatus(device, changed_callback)
else:
@ -97,10 +97,9 @@ class ReceiverStatus(dict):
def to_string(self):
count = len(self._receiver)
return (
_('No paired devices.')
if count == 0 else ngettext('%(count)s paired device.', '%(count)s paired devices.', count) % {
'count': count
}
_("No paired devices.")
if count == 0
else ngettext("%(count)s paired device.", "%(count)s paired devices.", count) % {"count": count}
)
def __str__(self):
@ -129,21 +128,20 @@ class DeviceStatus(dict):
self._active = None # is the device active?
def to_string(self):
status = ''
status = ""
battery_level = self.get(KEYS.BATTERY_LEVEL)
if battery_level is not None:
if isinstance(battery_level, _NamedInt):
status = _('Battery: %(level)s') % {'level': _(str(battery_level))}
status = _("Battery: %(level)s") % {"level": _(str(battery_level))}
else:
status = _('Battery: %(percent)d%%') % {'percent': battery_level}
status = _("Battery: %(percent)d%%") % {"percent": battery_level}
battery_status = self.get(KEYS.BATTERY_STATUS)
if battery_status is not None:
status += ' (%s)' % _(str(battery_status))
status += " (%s)" % _(str(battery_status))
return status
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):
return bool(self._active)
@ -152,7 +150,7 @@ class DeviceStatus(dict):
def set_battery_info(self, level, nextLevel, status, voltage):
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:
# 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
charging = status in (
_hidpp20_constants.BATTERY_STATUS.recharging, _hidpp20_constants.BATTERY_STATUS.almost_full,
_hidpp20_constants.BATTERY_STATUS.full, _hidpp20_constants.BATTERY_STATUS.slow_recharge
_hidpp20_constants.BATTERY_STATUS.recharging,
_hidpp20_constants.BATTERY_STATUS.almost_full,
_hidpp20_constants.BATTERY_STATUS.full,
_hidpp20_constants.BATTERY_STATUS.slow_recharge,
)
old_charging, self[KEYS.BATTERY_CHARGING] = self.get(KEYS.BATTERY_CHARGING), charging
@ -187,15 +187,15 @@ class DeviceStatus(dict):
if _hidpp20_constants.BATTERY_OK(status) and (level is None or level > _BATTERY_ATTENTION_LEVEL):
self[KEYS.ERROR] = None
else:
logger.warning('%s: battery %d%%, ALERT %s', self._device, level, status)
logger.warning("%s: battery %d%%, ALERT %s", self._device, level, status)
if self.get(KEYS.ERROR) != status:
self[KEYS.ERROR] = status
# only show the notification once
alert = ALERT.NOTIFICATION | ALERT.ATTENTION
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:
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
# update the leds on the device, if any
@ -241,11 +241,14 @@ class DeviceStatus(dict):
# Push settings for new devices (was_active is None),
# when devices request software reconfiguration
# and when devices become active if they don't have wireless device status feature,
if was_active is None or push or not was_active and (
not d.features or _hidpp20_constants.FEATURE.WIRELESS_DEVICE_STATUS not in d.features
if (
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):
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)
else:

View File

@ -20,13 +20,16 @@ import pkgutil as _pkgutil
import subprocess as _subprocess
import sys as _sys
NAME = 'solaar'
NAME = "solaar"
try:
__version__ = _subprocess.check_output(['git', 'describe', '--always'], cwd=_sys.path[0],
stderr=_subprocess.DEVNULL).strip().decode()
__version__ = (
_subprocess.check_output(["git", "describe", "--always"], cwd=_sys.path[0], stderr=_subprocess.DEVNULL)
.strip()
.decode()
)
except Exception:
try:
__version__ = _pkgutil.get_data('solaar', 'commit').strip().decode()
__version__ = _pkgutil.get_data("solaar", "commit").strip().decode()
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
from logitech_receiver.base import receivers, receivers_and_devices
from solaar import NAME
logger = logging.getLogger(__name__)
@ -38,70 +39,66 @@ logger = logging.getLogger(__name__)
def _create_parser():
parser = _argparse.ArgumentParser(
prog=NAME.lower(),
add_help=False,
epilog='For details on individual actions, run `%s <action> --help`.' % NAME.lower()
prog=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(
'device',
nargs='?',
default='all',
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)'
"device",
nargs="?",
default="all",
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)',
)
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(
'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(
'device',
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'
"device",
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",
)
sp.add_argument('profiles', nargs='?', help='file containing YAML dump of profiles')
sp.set_defaults(action='profiles')
sp.add_argument("profiles", nargs="?", help="file containing YAML dump of profiles")
sp.set_defaults(action="profiles")
sp = subparsers.add_parser(
'config',
help='read/write device-specific settings',
epilog='Please note that configuration only works on active devices.'
"config",
help="read/write device-specific settings",
epilog="Please note that configuration only works on active devices.",
)
sp.add_argument(
'device',
help='device to configure; may be a device number (1..6), a serial number, '
'or a substring of a device\'s name'
"device",
help="device to configure; may be a device number (1..6), a serial number, " "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('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('extra2', nargs='?', help='value for subkeyed settings')
sp.set_defaults(action='config')
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("extra_subkey", nargs="?", help="value for keyed or subkey for subkeyed settings")
sp.add_argument("extra2", nargs="?", help="value for subkeyed settings")
sp.set_defaults(action="config")
sp = subparsers.add_parser(
'pair',
help='pair a new device',
epilog='The Logitech Unifying Receiver supports up to 6 paired devices at the same time.'
"pair",
help="pair a new device",
epilog="The Logitech Unifying Receiver supports up to 6 paired devices at the same time.",
)
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(
'device',
help='device to unpair; may be a device number (1..6), a serial number, '
'or a substring of a device\'s name.'
"device",
help="device to unpair; may be a device number (1..6), a serial number, " "or a substring of a device's name.",
)
sp.set_defaults(action='unpair')
sp.set_defaults(action="unpair")
return parser, subparsers.choices
@ -117,12 +114,12 @@ def _receivers(dev_path=None):
try:
r = _receiver.Receiver.open(dev_info)
if logger.isEnabledFor(logging.DEBUG):
logger.debug('[%s] => %s', dev_info.path, r)
logger.debug("[%s] => %s", dev_info.path, r)
if r:
yield r
except Exception as e:
logger.exception('opening ' + str(dev_info))
_sys.exit('%s: error: %s' % (NAME, str(e)))
logger.exception("opening " + str(dev_info))
_sys.exit("%s: error: %s" % (NAME, str(e)))
def _receivers_and_devices(dev_path=None):
@ -132,12 +129,12 @@ def _receivers_and_devices(dev_path=None):
try:
d = _device.Device.open(dev_info) if dev_info.isDevice else _receiver.Receiver.open(dev_info)
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:
yield d
except Exception as e:
logger.exception('opening ' + str(dev_info))
_sys.exit('%s: error: %s' % (NAME, str(e)))
logger.exception("opening " + str(dev_info))
_sys.exit("%s: error: %s" % (NAME, str(e)))
def _find_receiver(receivers, name):
@ -178,7 +175,9 @@ def _find_device(receivers, name):
for dev in r:
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()
):
yield dev
@ -191,7 +190,6 @@ def _find_device(receivers, name):
def run(cli_args=None, hidraw_path=None):
if cli_args:
action = cli_args[0]
args = _cli_parser.parse_args(cli_args)
@ -199,15 +197,15 @@ def run(cli_args=None, hidraw_path=None):
args = _cli_parser.parse_args()
# Python 3 has an undocumented 'feature' that breaks parsing empty args
# http://bugs.python.org/issue16308
if 'cmd' not in args:
if "cmd" not in args:
_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)
action = args.action
assert action in actions
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))
else:
c = list(_receivers(hidraw_path))
@ -215,10 +213,10 @@ def run(cli_args=None, hidraw_path=None):
raise Exception(
'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)
except AssertionError:
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:
_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_templates as _settings_templates
from logitech_receiver.common import NamedInts as _NamedInts
from solaar import configuration as _configuration
def _print_setting(s, verbose=True):
print('#', s.label)
print("#", s.label)
if verbose:
if s.description:
print('#', s.description.replace('\n', ' '))
print("#", s.description.replace("\n", " "))
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:
print(
'# possible values: one of [', ', '.join(str(v) for v in s.choices),
'], or higher/lower/highest/max/lowest/min'
"# possible values: one of [",
", ".join(str(v) for v in s.choices),
"], or higher/lower/highest/max/lowest/min",
)
else:
# wtf?
pass
value = s.read(cached=False)
if value is None:
print(s.name, '= ? (failed to read from device)')
print(s.name, "= ? (failed to read from device)")
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):
print('#', s.label)
print("#", s.label)
if verbose:
if s.description:
print('#', s.description.replace('\n', ' '))
print("#", s.description.replace("\n", " "))
if s.kind == _settings.KIND.multiple_toggle:
k = next((k for k in s._labels if key == k), None)
if k is None:
print(s.name, '=? (key not found)')
print(s.name, "=? (key not found)")
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)
if value is None:
print(s.name, '= ? (failed to read from device)')
print(s.name, "= ? (failed to read from device)")
else:
print(s.name, s.val_to_string({k: value[str(int(k))]}))
elif s.kind == _settings.KIND.map_choice:
k = next((k for k in s.choices.keys() if key == k), None)
if k is None:
print(s.name, '=? (key not found)')
print(s.name, "=? (key not found)")
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)
if value is None:
print(s.name, '= ? (failed to read from device)')
print(s.name, "= ? (failed to read from device)")
else:
print(s.name, s.val_to_string({k: value[int(k)]}))
@ -94,35 +96,35 @@ def select_choice(value, choices, setting, key):
value = val
elif ivalue is not None and ivalue >= 1 and ivalue <= len(choices):
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)
if old_value is None:
raise Exception('%s: could not read current value' % setting.name)
if lvalue == 'lower':
raise Exception("%s: could not read current value" % setting.name)
if lvalue == "lower":
lower_values = choices[:old_value]
value = lower_values[-1] if lower_values else choices[:][0]
elif lvalue == 'higher':
higher_values = choices[old_value + 1:]
elif lvalue == "higher":
higher_values = choices[old_value + 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]
elif lvalue in ('lowest', 'min', 'last'):
elif lvalue in ("lowest", "min", "last"):
value = choices[:][0]
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
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))])
else:
try:
value = bool(int(value))
except Exception:
if value.lower() in ('true', 'yes', 'on', 't', 'y'):
if value.lower() in ("true", "yes", "on", "t", "y"):
value = True
elif value.lower() in ('false', 'no', 'off', 'f', 'n'):
elif value.lower() in ("false", "no", "off", "f", "n"):
value = False
else:
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 dev.settings:
raise Exception('no settings for %s' % dev.name)
raise Exception("no settings for %s" % dev.name)
_configuration.attach_to(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:
print('')
print("")
_print_setting(s)
return
@ -186,10 +188,12 @@ def run(receivers, args, find_receiver, find_device):
remote = False
try:
import gi
gi.require_version('Gtk', '3.0')
gi.require_version("Gtk", "3.0")
from gi.repository import Gio, Gtk
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.register()
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 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])
application.run(_yaml.safe_dump(argl))
else:
@ -217,19 +221,19 @@ def set(dev, setting, args, save):
if setting.kind == _settings.KIND.toggle:
value = select_toggle(args.value_key, setting)
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)
elif setting.kind == _settings.KIND.range:
value = select_range(args.value_key, setting)
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)
elif setting.kind == _settings.KIND.choice:
value = select_choice(args.value_key, setting.choices, setting, None)
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)
elif setting.kind == _settings.KIND.map_choice:
@ -247,7 +251,7 @@ def set(dev, setting, args, save):
args.value_key = str(int(k))
else:
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)
elif setting.kind == _settings.KIND.multiple_toggle:
@ -255,7 +259,7 @@ def set(dev, setting, args, save):
_print_setting_keyed(setting, args.value_key)
return (None, None, None)
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)
k = next((k for k in setting._labels if key == k), 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))
else:
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)
elif setting.kind == _settings.KIND.multiple_range:
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
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)
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
setting.read()
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))
else:
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)
value = item
else:
raise Exception('NotImplemented')
raise Exception("NotImplemented")
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]
class _HandleWithNotificationHook(int):
def notifications_hook(self, n):
nonlocal known_devices
assert n
@ -68,9 +67,9 @@ def run(receivers, args, find_receiver, _ignore):
timeout = 30 # seconds
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)
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()
patience = 5 # the discovering notification may come slightly later, so be patient
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
authentication = receiver.status.device_authentication
kind = receiver.status.device_kind
print(f'Bolt Pairing: discovered {name}')
print(f"Bolt Pairing: discovered {name}")
receiver.pair_device(
address=address,
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()
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:
receiver.handle.notifications_hook(n)
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:
passkey = f'{int(receiver.status.device_passkey):010b}'
passkey = ', '.join(['right' if bit == '1' else 'left' for bit in passkey])
print(f'Bolt Pairing: press {passkey}')
print('and then press left and right buttons simultaneously')
passkey = f"{int(receiver.status.device_passkey):010b}"
passkey = ", ".join(["right" if bit == "1" else "left" for bit in passkey])
print(f"Bolt Pairing: press {passkey}")
print("and then press left and right buttons simultaneously")
while receiver.status.lock_open:
n = _base.read(receiver.handle)
n = _base.make_notification(*n) if n else None
@ -114,7 +113,7 @@ def run(receivers, args, find_receiver, _ignore):
else:
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()
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:
@ -131,10 +130,10 @@ def run(receivers, args, find_receiver, _ignore):
if 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:
error = receiver.status.get(_status.KEYS.ERROR)
if error:
raise Exception('pairing failed: %s' % error)
raise Exception("pairing failed: %s" % error)
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 hidpp10_constants as _hidpp10_constants
from logitech_receiver.common import strhex as _strhex
from solaar.cli.show import _print_device, _print_receiver
_R = _hidpp10_constants.REGISTERS
@ -43,49 +44,53 @@ def run(receivers, args, find_receiver, _ignore):
_print_receiver(receiver)
print('')
print(' Register Dump')
print("")
print(" Register Dump")
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)
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)
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):
rgst = receiver.read_register(_R.receiver_info, sub_reg)
print(
' Pairing Register %#04x %#04x: %s' %
(_R.receiver_info % 0x100, sub_reg, '0x' + _strhex(rgst) if rgst else 'None')
" Pairing Register %#04x %#04x: %s"
% (_R.receiver_info % 0x100, sub_reg, "0x" + _strhex(rgst) if rgst else "None")
)
for device in range(0, 7):
for sub_reg in [0x10, 0x20, 0x30, 0x50]:
rgst = receiver.read_register(_R.receiver_info, sub_reg + device)
print(
' Pairing Register %#04x %#04x: %s' %
(_R.receiver_info % 0x100, sub_reg + device, '0x' + _strhex(rgst) if rgst else 'None')
" Pairing Register %#04x %#04x: %s"
% (_R.receiver_info % 0x100, sub_reg + device, "0x" + _strhex(rgst) if rgst else "None")
)
rgst = receiver.read_register(_R.receiver_info, 0x40 + device)
print(
' Pairing Name %#04x %#02x: %s' %
(_R.receiver_info % 0x100, 0x40 + device, rgst[2:2 + ord(rgst[1:2])] if rgst else 'None')
" Pairing Name %#04x %#02x: %s"
% (_R.receiver_info % 0x100, 0x40 + device, rgst[2 : 2 + ord(rgst[1:2])] if rgst else "None")
)
for part in range(1, 4):
rgst = receiver.read_register(_R.receiver_info, 0x60 + device, part)
print(
' 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'
" 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",
)
)
for sub_reg in range(0, 5):
rgst = receiver.read_register(_R.firmware, sub_reg)
print(
' Firmware %#04x %#04x: %s' %
(_R.firmware % 0x100, sub_reg, '0x' + _strhex(rgst) if rgst is not None else 'None')
" Firmware %#04x %#04x: %s"
% (_R.firmware % 0x100, sub_reg, "0x" + _strhex(rgst) if rgst is not None else "None")
)
print('')
print("")
for reg in range(0, 0xFF):
last = None
for sub in range(0, 0xFF):
@ -97,8 +102,8 @@ def run(receivers, args, find_receiver, _ignore):
else:
if not isinstance(last, bytes) or not isinstance(rgst, bytes) or last != rgst:
print(
' Register Short %#04x %#04x: %s' %
(reg, sub, '0x' + _strhex(rgst) if isinstance(rgst, bytes) else str(rgst))
" Register Short %#04x %#04x: %s"
% (reg, sub, "0x" + _strhex(rgst) if isinstance(rgst, bytes) else str(rgst))
)
last = rgst
last = None
@ -111,7 +116,7 @@ def run(receivers, args, find_receiver, _ignore):
else:
if not isinstance(last, bytes) or not isinstance(rgst, bytes) or last != rgst:
print(
' Register Long %#04x %#04x: %s' %
(reg, sub, '0x' + _strhex(rgst) if isinstance(rgst, bytes) else str(rgst))
" Register Long %#04x %#04x: %s"
% (reg, sub, "0x" + _strhex(rgst) if isinstance(rgst, bytes) else str(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)
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:
print(f'#Dumping profiles from {dev.name}')
print(f"#Dumping profiles from {dev.name}")
print(_yaml.dump(dev.profiles))
else:
try:
with open(profiles_file, 'r') as f:
print(f'Reading profiles from {profiles_file}')
with open(profiles_file, "r") as f:
print(f"Reading profiles from {profiles_file}")
profiles = _yaml.safe_load(f)
if not isinstance(profiles, _OnboardProfiles):
print('Profiles file does not contain current onboard profiles')
elif getattr(profiles, 'version', None) != _OnboardProfilesVersion:
version = getattr(profiles, 'version', None)
print(f'Missing or incorrect profile version {version} in loaded profile')
elif getattr(profiles, 'name', None) != dev.name:
name = getattr(profiles, 'name', None)
print(f'Different device name {name} in loaded profile')
print("Profiles file does not contain current onboard profiles")
elif getattr(profiles, "version", None) != _OnboardProfilesVersion:
version = getattr(profiles, "version", None)
print(f"Missing or incorrect profile version {version} in loaded profile")
elif getattr(profiles, "name", None) != dev.name:
name = getattr(profiles, "name", None)
print(f"Different device name {name} in loaded profile")
else:
print(f'Loading profiles into {dev.name}')
print(f"Loading profiles into {dev.name}")
written = profiles.write(dev)
print(f'Wrote {written} sectors to {dev.name}')
print(f"Wrote {written} sectors to {dev.name}")
except Exception as exc:
print('Profiles not written:', exc)
print("Profiles not written:", 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.common import NamedInt as _NamedInt
from logitech_receiver.common import strhex as _strhex
from solaar import NAME, __version__
_F = _hidpp20_constants.FEATURE
@ -34,39 +35,39 @@ def _print_receiver(receiver):
paired_count = receiver.count()
print(receiver.name)
print(' Device path :', receiver.path)
print(' USB id : 046d:%s' % receiver.product_id)
print(' Serial :', receiver.serial)
print(" Device path :", receiver.path)
print(" USB id : 046d:%s" % receiver.product_id)
print(" Serial :", receiver.serial)
if 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:
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)
if notification_flags is not None:
if 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:
print(' Notifications: (none)')
print(" Notifications: (none)")
activity = receiver.read_register(_hidpp10_constants.REGISTERS.devices_activity)
if activity:
activity = [(d, ord(activity[d - 1:d])) for d in range(1, receiver.max_devices)]
activity_text = ', '.join(('%d=%d' % (d, a)) for d, a in activity if a > 0)
print(' Device activity counters:', activity_text or '(empty)')
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)
print(" Device activity counters:", activity_text or "(empty)")
def _battery_text(level):
if level is None:
return 'N/A'
return "N/A"
elif isinstance(level, _NamedInt):
return str(level)
else:
return '%d%%' % level
return "%d%%" % level
def _battery_line(dev):
@ -75,11 +76,11 @@ def _battery_line(dev):
level, nextLevel, status, voltage = battery
text = _battery_text(level)
if voltage is not None:
text = text + (' %smV ' % voltage)
nextText = '' if nextLevel is None else ', next level ' + _battery_text(nextLevel)
print(' Battery: %s, %s%s.' % (text, status, nextText))
text = text + (" %smV " % voltage)
nextText = "" if nextLevel is None else ", next level " + _battery_text(nextLevel)
print(" Battery: %s, %s%s." % (text, status, nextText))
else:
print(' Battery status unavailable.')
print(" Battery status unavailable.")
def _print_device(dev, num=None):
@ -88,56 +89,56 @@ def _print_device(dev, num=None):
try:
dev.ping()
except exceptions.NoSuchDevice:
print(' %s: Device not found' % num or dev.number)
print(" %s: Device not found" % num or dev.number)
return
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:
print('%s' % dev.name)
print(' Device path :', dev.path)
print("%s" % dev.name)
print(" Device path :", dev.path)
if dev.wpid:
print(' WPID : %s' % dev.wpid)
print(" WPID : %s" % dev.wpid)
if dev.product_id:
print(' USB id : 046d:%s' % dev.product_id)
print(' Codename :', dev.codename)
print(' Kind :', dev.kind)
print(" USB id : 046d:%s" % dev.product_id)
print(" Codename :", dev.codename)
print(" Kind :", dev.kind)
if dev.protocol:
print(' Protocol : HID++ %1.1f' % dev.protocol)
print(" Protocol : HID++ %1.1f" % dev.protocol)
else:
print(' Protocol : unknown (device is offline)')
print(" Protocol : unknown (device is offline)")
if dev.polling_rate:
print(' Report Rate :', dev.polling_rate)
print(' Serial number:', dev.serial)
print(" Report Rate :", dev.polling_rate)
print(" Serial number:", dev.serial)
if dev.modelId:
print(' Model ID: ', dev.modelId)
print(" Model ID: ", dev.modelId)
if dev.unitId:
print(' Unit ID: ', dev.unitId)
print(" Unit ID: ", dev.unitId)
if 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:
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:
notification_flags = _hidpp10.get_notification_flags(dev)
if notification_flags is not None:
if 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:
print(' Notifications: (none).')
print(" Notifications: (none).")
device_features = _hidpp10.get_device_features(dev)
if device_features is not None:
if 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:
print(' Features: (none)')
print(" Features: (none)")
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 = []
_settings_templates.check_feature_settings(dev, dev_settings)
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)
version = dev.features.get_feature_version(int(feature))
version = version if version else 0
print(' %2d: %-22s {%04X} V%s %s ' % (index, feature, feature, version, ', '.join(flags)))
print(" %2d: %-22s {%04X} V%s %s " % (index, feature, feature, version, ", ".join(flags)))
if feature == _hidpp20_constants.FEATURE.HIRES_WHEEL:
wheel = _hidpp20.get_hires_wheel(dev)
if wheel:
multi, has_invert, has_switch, inv, res, target, ratchet = wheel
print(' Multiplier: %s' % multi)
print(" Multiplier: %s" % multi)
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:
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:
print(' High resolution mode')
print(" High resolution mode")
else:
print(' Low resolution mode')
print(" Low resolution mode")
if target:
print(' HID++ notification')
print(" HID++ notification")
else:
print(' HID notification')
print(" HID notification")
elif feature == _hidpp20_constants.FEATURE.MOUSE_POINTER:
mouse_pointer = _hidpp20.get_mouse_pointer_info(dev)
if mouse_pointer:
print(' DPI: %s' % mouse_pointer['dpi'])
print(' Acceleration: %s' % mouse_pointer['acceleration'])
if mouse_pointer['suggest_os_ballistics']:
print(' Use OS ballistics')
print(" DPI: %s" % mouse_pointer["dpi"])
print(" Acceleration: %s" % mouse_pointer["acceleration"])
if mouse_pointer["suggest_os_ballistics"]:
print(" Use OS ballistics")
else:
print(' Override OS ballistics')
if mouse_pointer['suggest_vertical_orientation']:
print(' Provide vertical tuning, trackball')
print(" Override OS ballistics")
if mouse_pointer["suggest_vertical_orientation"]:
print(" Provide vertical tuning, trackball")
else:
print(' No vertical tuning, standard mice')
print(" No vertical tuning, standard mice")
elif feature == _hidpp20_constants.FEATURE.VERTICAL_SCROLLING:
vertical_scrolling_info = _hidpp20.get_vertical_scrolling_info(dev)
if vertical_scrolling_info:
print(' Roller type: %s' % vertical_scrolling_info['roller'])
print(' Ratchet per turn: %s' % vertical_scrolling_info['ratchet'])
print(' Scroll lines: %s' % vertical_scrolling_info['lines'])
print(" Roller type: %s" % vertical_scrolling_info["roller"])
print(" Ratchet per turn: %s" % vertical_scrolling_info["ratchet"])
print(" Scroll lines: %s" % vertical_scrolling_info["lines"])
elif feature == _hidpp20_constants.FEATURE.HI_RES_SCROLLING:
scrolling_mode, scrolling_resolution = _hidpp20.get_hi_res_scrolling_info(dev)
if scrolling_mode:
print(' Hi-res scrolling enabled')
print(" Hi-res scrolling enabled")
else:
print(' Hi-res scrolling disabled')
print(" Hi-res scrolling disabled")
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:
pointer_speed = _hidpp20.get_pointer_speed_info(dev)
if pointer_speed:
print(' Pointer Speed: %s' % pointer_speed)
print(" Pointer Speed: %s" % pointer_speed)
elif feature == _hidpp20_constants.FEATURE.LOWRES_WHEEL:
wheel_status = _hidpp20.get_lowres_wheel_status(dev)
if wheel_status:
print(' Wheel Reports: %s' % wheel_status)
print(" Wheel Reports: %s" % wheel_status)
elif feature == _hidpp20_constants.FEATURE.NEW_FN_INVERSION:
inversion = _hidpp20.get_new_fn_inversion(dev)
if inversion:
inverted, default_inverted = inversion
print(' Fn-swap:', 'enabled' if inverted else 'disabled')
print(' Fn-swap default:', 'enabled' if default_inverted else 'disabled')
print(" Fn-swap:", "enabled" if inverted else "disabled")
print(" Fn-swap default:", "enabled" if default_inverted else "disabled")
elif feature == _hidpp20_constants.FEATURE.HOSTS_INFO:
host_names = _hidpp20.get_host_names(dev)
for host, (paired, name) in host_names.items():
print(' Host %s (%s): %s' % (host, 'paired' if paired else 'unpaired', name))
print(" Host %s (%s): %s" % (host, "paired" if paired else "unpaired", name))
elif feature == _hidpp20_constants.FEATURE.DEVICE_NAME:
print(' Name: %s' % _hidpp20.get_name(dev))
print(' Kind: %s' % _hidpp20.get_kind(dev))
print(" Name: %s" % _hidpp20.get_name(dev))
print(" Kind: %s" % _hidpp20.get_kind(dev))
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:
for fw in _hidpp20.get_firmware(dev):
extras = _strhex(fw.extras) if fw.extras else ''
print(' Firmware: %s %s %s %s' % (fw.kind, fw.name, fw.version, extras))
extras = _strhex(fw.extras) if fw.extras else ""
print(" Firmware: %s %s %s %s" % (fw.kind, fw.name, fw.version, extras))
ids = _hidpp20.get_ids(dev)
if ids:
unitId, modelId, tid_map = ids
print(' Unit ID: %s Model ID: %s Transport IDs: %s' % (unitId, modelId, tid_map))
elif feature == _hidpp20_constants.FEATURE.REPORT_RATE or \
feature == _hidpp20_constants.FEATURE.EXTENDED_ADJUSTABLE_REPORT_RATE:
print(' Report Rate: %s' % _hidpp20.get_polling_rate(dev))
print(" Unit ID: %s Model ID: %s Transport IDs: %s" % (unitId, modelId, tid_map))
elif (
feature == _hidpp20_constants.FEATURE.REPORT_RATE
or feature == _hidpp20_constants.FEATURE.EXTENDED_ADJUSTABLE_REPORT_RATE
):
print(" Report Rate: %s" % _hidpp20.get_polling_rate(dev))
elif feature == _hidpp20_constants.FEATURE.REMAINING_PAIRING:
print(' Remaining Pairings: %d' % _hidpp20.get_remaining_pairing(dev))
print(" Remaining Pairings: %d" % _hidpp20.get_remaining_pairing(dev))
elif feature == _hidpp20_constants.FEATURE.ONBOARD_PROFILES:
if _hidpp20.get_onboard_mode(dev) == _hidpp20_constants.ONBOARD_MODES.MODE_HOST:
mode = 'Host'
mode = "Host"
else:
mode = 'On-Board'
print(' Device Mode: %s' % mode)
mode = "On-Board"
print(" Device Mode: %s" % mode)
elif _hidpp20.battery_functions.get(feature, None):
print('', end=' ')
print("", end=" ")
_battery_line(dev)
for setting in dev_settings:
if setting.feature == feature:
if setting._device and getattr(setting._device, 'persister', None) and \
setting._device.persister.get(setting.name) is not None:
if (
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))
print(' %s (saved): %s' % (setting.label, v))
print(" %s (saved): %s" % (setting.label, v))
try:
v = setting.val_to_string(setting.read(False))
except _hidpp20.FeatureCallError as e:
v = 'HID++ error ' + str(e)
v = "HID++ error " + str(e)
except AssertionError as e:
v = 'AssertionError ' + str(e)
print(' %s : %s' % (setting.label, v))
v = "AssertionError " + str(e)
print(" %s : %s" % (setting.label, v))
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:
# TODO: add here additional variants for other REPROG_CONTROLS
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:
print(' %2d: %-26s, default: %-27s => %-26s' % (k.index, k.key, k.default_task, k.mapped_to))
gmask_fmt = ','.join(k.group_mask)
gmask_fmt = gmask_fmt if gmask_fmt else 'empty'
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 = report_fmt if report_fmt else 'default'
print(' reporting: %s' % (report_fmt))
print(" %2d: %-26s, default: %-27s => %-26s" % (k.index, k.key, k.default_task, k.mapped_to))
gmask_fmt = ",".join(k.group_mask)
gmask_fmt = gmask_fmt if gmask_fmt else "empty"
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 = report_fmt if report_fmt else "default"
print(" reporting: %s" % (report_fmt))
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:
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:
print(
' Has %d gesture(s), %d param(s) and %d spec(s):' %
(len(dev.gestures.gestures), len(dev.gestures.params), len(dev.gestures.specs))
" Has %d gesture(s), %d param(s) and %d spec(s):"
% (len(dev.gestures.gestures), len(dev.gestures.params), len(dev.gestures.specs))
)
for k in dev.gestures.gestures.values():
print(
' %-26s Enabled(%4s): %-5s Diverted:(%4s) %s' %
(k.gesture, k.index, k.enabled(), k.diversion_index, k.diverted())
" %-26s Enabled(%4s): %-5s Diverted:(%4s) %s"
% (k.gesture, k.index, k.enabled(), k.diversion_index, k.diverted())
)
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():
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:
_battery_line(dev)
else:
print(' Battery: unknown (device is offline).')
print(" Battery: unknown (device is offline).")
def run(devices, args, find_receiver, find_device):
assert devices
assert args.device
print('%s version %s' % (NAME, __version__))
print('')
print("%s version %s" % (NAME, __version__))
print("")
device_name = args.device.lower()
if device_name == 'all':
if device_name == "all":
for d in devices:
if isinstance(d, _receiver.Receiver):
_print_receiver(d)
count = d.count()
if count:
for dev in d:
print('')
print("")
_print_device(dev, dev.number)
count -= 1
if not count:
break
print('')
print("")
else:
print('')
print("")
_print_device(d)
return

View File

@ -28,13 +28,13 @@ def run(receivers, args, find_receiver, find_device):
if not dev.receiver.may_unpair:
print(
'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)
"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)
)
try:
# query these now, it's last chance to get them
number, codename, wpid, serial = dev.number, dev.codename, dev.wpid, dev.serial
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:
raise e

View File

@ -28,22 +28,23 @@ import yaml as _yaml
from gi.repository import GLib
from logitech_receiver.common import NamedInt as _NamedInt
from solaar import __version__
logger = logging.getLogger(__name__)
_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')
_json_file_path = _path.join(_XDG_CONFIG_HOME, 'solaar', 'config.json')
_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")
_json_file_path = _path.join(_XDG_CONFIG_HOME, "solaar", "config.json")
_KEY_VERSION = '_version'
_KEY_NAME = '_NAME'
_KEY_WPID = '_wpid'
_KEY_SERIAL = '_serial'
_KEY_MODEL_ID = '_modelId'
_KEY_UNIT_ID = '_unitId'
_KEY_ABSENT = '_absent'
_KEY_SENSITIVE = '_sensitive'
_KEY_VERSION = "_version"
_KEY_NAME = "_NAME"
_KEY_WPID = "_wpid"
_KEY_SERIAL = "_serial"
_KEY_MODEL_ID = "_modelId"
_KEY_UNIT_ID = "_unitId"
_KEY_ABSENT = "_absent"
_KEY_SENSITIVE = "_sensitive"
_config = []
@ -55,19 +56,19 @@ def _load():
with open(_yaml_file_path) as config_file:
loaded_config = _yaml.safe_load(config_file)
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):
path = _json_file_path
try:
with open(_json_file_path) as config_file:
loaded_config = _json.load(config_file)
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)
else:
path = None
if logger.isEnabledFor(logging.DEBUG):
logger.debug('load => %s', loaded_config)
logger.debug("load => %s", loaded_config)
global _config
_config = _parse_config(loaded_config, path)
@ -84,41 +85,43 @@ def _parse_config(loaded_config, config_path):
if discard_derived_properties:
if logger.isEnabledFor(logging.INFO):
logger.info(
'config file \'%s\' was generated by another version of solaar '
'(config: %s, current: %s). refreshing detected device capabilities', config_path, loaded_version,
current_version
"config file '%s' was generated by another version of solaar "
"(config: %s, current: %s). refreshing detected device capabilities",
config_path,
loaded_version,
current_version,
)
for device in loaded_config[1:]:
assert isinstance(device, dict)
parsed_config.append(_device_entry_from_config_dict(device, discard_derived_properties))
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
def _device_entry_from_config_dict(data, discard_derived_properties):
divert = data.get('divert-keys')
divert = data.get("divert-keys")
if divert:
sliding = data.get('dpi-sliding')
sliding = data.get("dpi-sliding")
if sliding: # convert old-style dpi-sliding setting to divert-keys entry
divert[int(sliding)] = 3
data.pop('dpi-sliding', None)
gestures = data.get('mouse-gestures')
data.pop("dpi-sliding", None)
gestures = data.get("mouse-gestures")
if gestures: # convert old-style mouse-gestures setting to divert-keys entry
divert[int(gestures)] = 2
data.pop('mouse-gestures', None)
data.pop("mouse-gestures", None)
# remove any string entries (from bad conversions)
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
data['_sensitive'] = {
'hires-smooth-resolution': 'ignore',
'hires-smooth-invert': 'ignore',
'hires-scroll-mode': 'ignore'
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
data["_sensitive"] = {
"hires-smooth-resolution": "ignore",
"hires-smooth-invert": "ignore",
"hires-scroll-mode": "ignore",
}
if discard_derived_properties:
data.pop('_absent', None)
data.pop('_battery', None)
data.pop("_absent", None)
data.pop("_battery", None)
return _DeviceEntry(**data)
@ -136,7 +139,7 @@ def save(defer=False):
try:
_os.makedirs(dirname)
except Exception:
logger.error('failed to create %s', dirname)
logger.error("failed to create %s", dirname)
return
if not defer or not defer_saves:
do_save()
@ -154,38 +157,37 @@ def do_save():
save_timer.cancel()
save_timer = None
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)
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:
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):
config = [json_dict.get(_KEY_VERSION)]
for key, dev in json_dict.items():
key = key.split(':')
key = key.split(":")
if len(key) == 2:
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]
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()}
dev[k] = v
for k in ['mouse-gestures', 'dpi-sliding']:
for k in ["mouse-gestures", "dpi-sliding"]:
v = dev.get(k, None)
if v is True or v is False:
dev.pop(k)
if '_name' in dev:
dev[_KEY_NAME] = dev['_name']
dev.pop('_name')
if "_name" in dev:
dev[_KEY_NAME] = dev["_name"]
dev.pop("_name")
config.append(dev)
return config
class _DeviceEntry(dict):
def __init__(self, **kwargs):
super().__init__(**kwargs)
@ -198,7 +200,7 @@ class _DeviceEntry(dict):
super().__setitem__(_KEY_NAME, device.name)
if device.wpid and device.wpid != self.get(_KEY_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)
if modelId and modelId != self.get(_KEY_MODEL_ID):
super().__setitem__(_KEY_MODEL_ID, modelId)
@ -216,14 +218,14 @@ class _DeviceEntry(dict):
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)
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)
@ -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.
# So new entries are not created for unseen off-line receiver-connected devices except for those with protocol 1.0
def persister(device):
def match(wpid, serial, modelId, unitId, c):
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
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
and unitId == c.get(_KEY_UNIT_ID)
))
)
if not _config:
_load()
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:
if isinstance(c, _DeviceEntry) and match(device.wpid, device.serial, modelId, device.unitId, c):
entry = c
@ -254,10 +258,10 @@ def persister(device):
if not entry:
if not device.online and not device.serial: # don't create entry for offline devices without serial number
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
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()
_config.append(entry)
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)
return importlib.import_module(module)
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'
temp = tempfile.NamedTemporaryFile(prefix='Solaar_', mode='w', delete=True)
battery_icons_style = "regular"
temp = tempfile.NamedTemporaryFile(prefix="Solaar_", mode="w", delete=True)
def _parse_arguments():
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(
'-d',
'--debug',
action='count',
"-d",
"--debug",
action="count",
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(
'-D',
'--hidraw',
action='store',
dest='hidraw_path',
metavar='PATH',
help='unifying receiver to use; the first detected receiver if unspecified. Example: /dev/hidraw2'
"-D",
"--hidraw",
action="store",
dest="hidraw_path",
metavar="PATH",
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(
'-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(
'-b',
'--battery-icons',
choices=('regular', 'symbolic', 'solaar'),
help='prefer regular battery / symbolic battery / solaar icons'
"-b",
"--battery-icons",
choices=("regular", "symbolic", "solaar"),
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('-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('action', nargs=argparse.REMAINDER, choices=_cli.actions, help='optional actions to perform')
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("--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")
args = arg_parser.parse_args()
@ -101,29 +101,29 @@ def _parse_arguments():
return
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
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
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
logging.getLogger('').setLevel(min(log_level, logging.WARNING))
logging.getLogger("").setLevel(min(log_level, logging.WARNING))
file_handler = logging.StreamHandler(temp)
file_handler.setLevel(max(min(log_level, logging.WARNING), logging.INFO))
file_handler.setFormatter(logging.Formatter(log_format))
logging.getLogger('').addHandler(file_handler)
logging.getLogger("").addHandler(file_handler)
if args.debug > 0:
stream_handler = logging.StreamHandler()
stream_handler.setFormatter(logging.Formatter(log_format))
stream_handler.setLevel(log_level)
logging.getLogger('').addHandler(stream_handler)
logging.getLogger("").addHandler(stream_handler)
if not args.action:
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
@ -136,14 +136,14 @@ def _handlesig(signl, stack):
if signl == int(signal.SIGINT):
if logger.isEnabledFor(logging.INFO):
faulthandler.dump_traceback()
sys.exit('%s: exit due to keyboard interrupt' % (NAME.lower()))
sys.exit("%s: exit due to keyboard interrupt" % (NAME.lower()))
else:
sys.exit(0)
def main():
if platform.system() not in ('Darwin', 'Windows'):
_require('pyudev', 'python3-pyudev')
if platform.system() not in ("Darwin", "Windows"):
_require("pyudev", "python3-pyudev")
args = _parse_arguments()
if not args:
@ -152,21 +152,23 @@ def main():
# if any argument, run comandline and exit
return _cli.run(args.action, args.hidraw_path)
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')
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")
# handle ^C in console
signal.signal(signal.SIGINT, signal.SIG_DFL)
signal.signal(signal.SIGINT, _handlesig)
signal.signal(signal.SIGTERM, _handlesig)
udev_file = '42-logitech-unify-permissions.rules'
if logger.isEnabledFor(logging.WARNING) \
and not os.path.isfile('/etc/udev/rules.d/' + udev_file) \
and not os.path.isfile('/usr/lib/udev/rules.d/' + udev_file) \
and not os.path.isfile('/usr/local/lib/udev/rules.d/' + udev_file):
logger.warning('Solaar udev file not found in expected location')
logger.warning('See https://pwr-solaar.github.io/Solaar/installation for more information')
udev_file = "42-logitech-unify-permissions.rules"
if (
logger.isEnabledFor(logging.WARNING)
and not os.path.isfile("/etc/udev/rules.d/" + udev_file)
and not os.path.isfile("/usr/lib/udev/rules.d/" + udev_file)
and not os.path.isfile("/usr/local/lib/udev/rules.d/" + udev_file)
):
logger.warning("Solaar udev file not found in expected location")
logger.warning("See https://pwr-solaar.github.io/Solaar/installation for more information")
try:
_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
# 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:
sys.exit('%s: error: %s' % (NAME.lower(), format_exc()))
sys.exit("%s: error: %s" % (NAME.lower(), format_exc()))
temp.close()
if __name__ == '__main__':
if __name__ == "__main__":
main()

View File

@ -31,19 +31,19 @@ from solaar import NAME as _NAME
def _find_locale_path(lc_domain):
prefix_share = _path.normpath(_path.join(_path.realpath(_sys.path[0]), '..'))
src_share = _path.normpath(_path.join(_path.realpath(_sys.path[0]), '..', 'share'))
prefix_share = _path.normpath(_path.join(_path.realpath(_sys.path[0]), ".."))
src_share = _path.normpath(_path.join(_path.realpath(_sys.path[0]), "..", "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:
return _path.join(location, 'locale')
return _path.join(location, "locale")
# del _path
try:
locale.setlocale(locale.LC_ALL, '')
locale.setlocale(locale.LC_ALL, "")
except Exception:
pass

View File

@ -36,7 +36,7 @@ from logitech_receiver import status as _status
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 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.__nonzero__ = _GHOST_DEVICE.__bool__
del namedtuple
@ -72,8 +72,7 @@ def _ghost(device):
class ReceiverListener(_listener.EventsListener):
"""Keeps the status of a Receiver.
"""
"""Keeps the status of a Receiver."""
def __init__(self, receiver, status_changed_callback):
super().__init__(receiver, self._notifications_handler)
@ -87,13 +86,13 @@ class ReceiverListener(_listener.EventsListener):
def has_started(self):
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()
if logger.isEnabledFor(logging.WARNING):
if not self.receiver.isDevice and not ((nfs if nfs else 0) & _hidpp10_constants.NOTIFICATION_FLAG.wireless):
logger.warning(
'Receiver on %s might not support connection notifications, GUI might not show its devices',
self.receiver.path
"Receiver on %s might not support connection notifications, GUI might not show its devices",
self.receiver.path,
)
self.receiver.status[_status.KEYS.NOTIFICATION_FLAGS] = nfs
self.receiver.notify_devices()
@ -103,7 +102,7 @@ class ReceiverListener(_listener.EventsListener):
r, self.receiver = self.receiver, None
assert r is not None
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,
# make sure to clean up in _all_listeners
@ -114,7 +113,7 @@ class ReceiverListener(_listener.EventsListener):
try:
r.close()
except Exception:
logger.exception('closing receiver %s' % r.path)
logger.exception("closing receiver %s" % r.path)
self.status_changed_callback(r) # , _status.ALERT.NOTIFICATION)
# def tick(self, timestamp):
@ -161,16 +160,25 @@ class ReceiverListener(_listener.EventsListener):
device.ping()
if device.kind is None:
logger.info(
'status_changed %r: %s, %s (%X) %s', device, 'present' if bool(device) else 'removed', device.status,
alert, reason or ''
"status_changed %r: %s, %s (%X) %s",
device,
"present" if bool(device) else "removed",
device.status,
alert,
reason or "",
)
else:
logger.info(
'status_changed %r: %s %s, %s (%X) %s', device, 'paired' if bool(device) else 'unpaired',
'online' if device.online else 'offline', device.status, alert, reason or ''
"status_changed %r: %s %s, %s (%X) %s",
device,
"paired" if bool(device) else "unpaired",
"online" if device.online else "offline",
device.status,
alert,
reason or "",
)
except Exception:
logger.info('status_changed for unknown device')
logger.info("status_changed for unknown device")
if device.kind is None:
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
# with while cleaning up.
if logger.isEnabledFor(logging.INFO):
logger.info('device %s was unpaired, ghosting', device)
logger.info("device %s was unpaired, ghosting", device)
device = _ghost(device)
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
if self.receiver.isDevice:
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
# 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 logger.isEnabledFor(logging.INFO):
logger.info('ignoring DJ pairing notification %s', n)
logger.info("ignoring DJ pairing notification %s", n)
return
# a device notification
if not (0 < n.devnumber <= 16): # some receivers have devices past their max # devices
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
already_known = n.devnumber in self.receiver
@ -237,7 +245,7 @@ class ReceiverListener(_listener.EventsListener):
if n.sub_id == 0x41:
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
if self.receiver.read_register(_R.receiver_info, _IR.pairing_information + n.devnumber - 1) is None:
return
@ -254,7 +262,7 @@ class ReceiverListener(_listener.EventsListener):
dev = self.receiver[n.devnumber]
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
# Apply settings every time the device connects
@ -262,9 +270,9 @@ class ReceiverListener(_listener.EventsListener):
if logger.isEnabledFor(logging.INFO):
try:
dev.ping()
logger.info('connection %s for %r', n, dev)
logger.info("connection %s for %r", n, dev)
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.
# They will be applied when the device is marked as online.
configuration.attach_to(dev)
@ -272,23 +280,23 @@ class ReceiverListener(_listener.EventsListener):
# the receiver changed status as well
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
logger.warning('%s before device %s has status', n, dev)
logger.warning("%s before device %s has status", n, dev)
else:
_notifications.process(dev, n)
if self.receiver.status.lock_open and not already_known:
# 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):
logger.info('%s: pairing detected new device', self.receiver)
logger.info("%s: pairing detected new device", self.receiver)
self.receiver.status.new_device = dev
elif dev.online is None:
dev.ping()
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
return rl
logger.warning('failed to open %s', device_info)
logger.warning("failed to open %s", device_info)
def start_all():
@ -323,9 +331,9 @@ def start_all():
stop_all()
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():
_process_receiver_event('add', device_info)
_process_receiver_event("add", device_info)
def stop_all():
@ -334,7 +342,7 @@ def stop_all():
if listeners:
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:
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
def ping_all(resuming=False):
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():
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
if l.receiver.ping():
l.receiver.status.changed(active=True, push=True)
@ -363,7 +371,7 @@ def ping_all(resuming=False):
count = l.receiver.count()
if count:
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
if dev.ping():
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):
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
_setting_callback = setting_changed_callback
@ -395,19 +403,19 @@ def _process_add(device_info, retry):
except OSError as e:
if e.errno == _errno.EACCES:
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):
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:
pass
if retry:
GLib.timeout_add(2000.0, _process_add, device_info, retry - 1)
else:
_error_callback('permissions', device_info.path)
_error_callback("permissions", device_info.path)
else:
_error_callback('nodevice', device_info.path)
_error_callback("nodevice", device_info.path)
except exceptions.NoReceiver:
_error_callback('nodevice', device_info.path)
_error_callback("nodevice", device_info.path)
# receiver add/remove events will start/stop listener threads
@ -417,7 +425,7 @@ def _process_receiver_event(action, device_info):
assert _error_callback
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
l = _all_listeners.pop(device_info.path, None)
@ -425,7 +433,7 @@ def _process_receiver_event(action, device_info):
assert isinstance(l, ReceiverListener)
l.stop()
if action == 'add': # a new device was detected
if action == "add": # a new device was detected
_process_add(device_info, 3)
return False

View File

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

View File

@ -22,13 +22,14 @@ import gi
import yaml as _yaml
from logitech_receiver.status import ALERT
from solaar.i18n import _
from solaar.ui.config_panel import change_setting, record_setting
from solaar.ui.window import find_device
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
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()
@ -48,7 +49,7 @@ GLib.threads_init()
def _startup(app, startup_hook, use_tray, show_window):
if logger.isEnabledFor(logging.DEBUG):
logger.debug('startup registered=%s, remote=%s', app.get_is_registered(), app.get_is_remote())
logger.debug("startup registered=%s, remote=%s", app.get_is_registered(), app.get_is_remote())
common.start_async()
notify.init()
if use_tray:
@ -59,7 +60,7 @@ def _startup(app, startup_hook, use_tray, show_window):
def _activate(app):
if logger.isEnabledFor(logging.DEBUG):
logger.debug('activate')
logger.debug("activate")
if app.get_windows():
window.popup()
else:
@ -68,12 +69,12 @@ def _activate(app):
def _command_line(app, command_line):
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:
_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):
logger.info('remote command line %s', args)
logger.info("remote command line %s", args)
dev = find_device(args[1])
if dev:
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):
if logger.isEnabledFor(logging.DEBUG):
logger.debug('shutdown')
logger.debug("shutdown")
shutdown_hook()
common.stop_async()
tray.destroy()
@ -92,18 +93,18 @@ def _shutdown(app, shutdown_hook):
def run_loop(startup_hook, shutdown_hook, use_tray, show_window):
assert use_tray or show_window, 'need either tray or visible window'
APP_ID = 'io.github.pwr_solaar.solaar'
assert use_tray or show_window, "need either tray or visible window"
APP_ID = "io.github.pwr_solaar.solaar"
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('command-line', _command_line)
application.connect('activate', _activate)
application.connect('shutdown', _shutdown, shutdown_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("activate", _activate)
application.connect("shutdown", _shutdown, shutdown_hook)
application.register()
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()
@ -115,7 +116,7 @@ def run_loop(startup_hook, shutdown_hook, use_tray, show_window):
def _status_changed(device, alert, reason, refresh=False):
assert device is not None
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)
if alert & ALERT.ATTENTION:

View File

@ -20,6 +20,7 @@
import logging
from gi.repository import Gtk
from solaar import NAME, __version__
from solaar.i18n import _
@ -35,60 +36,64 @@ def _create():
about.set_program_name(NAME)
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_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_authors(('Daniel Pavel http://github.com/pwr', ))
about.set_authors(("Daniel Pavel http://github.com/pwr",))
try:
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(_("Additional Programming"), ("Filipe Laíns", "Peter F. Patel-Schneider"))
about.add_credit_section(_("GUI design"), ("Julien Gascard", "Daniel Pavel"))
about.add_credit_section(
_('Testing'), (
'Douglas Wagner',
'Julien Gascard',
'Peter Wu http://www.lekensteyn.nl/logitech-unifying.html',
)
_("Testing"),
(
"Douglas Wagner",
"Julien Gascard",
"Peter Wu http://www.lekensteyn.nl/logitech-unifying.html",
),
)
about.add_credit_section(
_('Logitech documentation'), (
'Julien Danjou http://julien.danjou.info/blog/2012/logitech-unifying-upower',
'Nestor Lopez Casado http://drive.google.com/folderview?id=0BxbRzx7vEV7eWmgwazJ3NUFfQ28',
)
_("Logitech documentation"),
(
"Julien Danjou http://julien.danjou.info/blog/2012/logitech-unifying-upower",
"Nestor Lopez Casado http://drive.google.com/folderview?id=0BxbRzx7vEV7eWmgwazJ3NUFfQ28",
),
)
except TypeError:
# 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:
# 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(
'\n'.join((
'gogo (croatian)',
'Papoteur, David Geiger, Damien Lallement (français)',
'Michele Olivo (italiano)',
'Adrian Piotrowicz (polski)',
'Drovetto, JrBenito (Portuguese-BR)',
'Daniel Pavel (română)',
'Daniel Zippert, Emelie Snecker (svensk)',
'Dimitriy Ryazantcev (Russian)',
'El Jinete Sin Cabeza (Español)',
))
"\n".join(
(
"gogo (croatian)",
"Papoteur, David Geiger, Damien Lallement (français)",
"Michele Olivo (italiano)",
"Adrian Piotrowicz (polski)",
"Drovetto, JrBenito (Portuguese-BR)",
"Daniel Pavel (română)",
"Daniel Zippert, Emelie Snecker (svensk)",
"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.connect('response', lambda x, y: x.hide())
about.connect("response", lambda x, y: x.hide())
def _hide(dialog, event):
dialog.hide()
return True
about.connect('delete-event', _hide)
about.connect("delete-event", _hide)
return about

View File

@ -17,6 +17,7 @@
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
from gi.repository import Gdk, Gtk
from solaar.i18n import _
from . import pair_window
@ -36,7 +37,7 @@ def make(name, label, function, stock_id=None, *args):
if stock_id is not None:
action.set_stock_id(stock_id)
if function:
action.connect('activate', function, *args)
action.connect("activate", function, *args)
return action
@ -45,7 +46,7 @@ def make_toggle(name, label, function, stock_id=None, *args):
action.set_icon_name(name)
if stock_id is not None:
action.set_stock_id(stock_id)
action.connect('activate', function, *args)
action.connect("activate", function, *args)
return action
@ -80,12 +81,11 @@ def unpair(window, device):
assert device.kind is not None
qdialog = Gtk.MessageDialog(
window, 0, Gtk.MessageType.QUESTION, Gtk.ButtonsType.NONE,
_('Unpair') + ' ' + device.name + ' ?'
window, 0, Gtk.MessageType.QUESTION, Gtk.ButtonsType.NONE, _("Unpair") + " " + device.name + " ?"
)
qdialog.set_icon_name('remove')
qdialog.add_button(_('Cancel'), Gtk.ResponseType.CANCEL)
qdialog.add_button(_('Unpair'), Gtk.ResponseType.ACCEPT)
qdialog.set_icon_name("remove")
qdialog.add_button(_("Cancel"), Gtk.ResponseType.CANCEL)
qdialog.add_button(_("Unpair"), Gtk.ResponseType.ACCEPT)
choice = qdialog.run()
qdialog.destroy()
if choice == Gtk.ResponseType.ACCEPT:
@ -97,4 +97,4 @@ def unpair(window, device):
del receiver[device_number]
except Exception:
# 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.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
logger = logging.getLogger(__name__)
def _error_dialog(reason, object):
logger.error('error: %s %s', reason, object)
logger.error("error: %s %s", reason, object)
if reason == 'permissions':
title = _('Permissions error')
if reason == "permissions":
title = _("Permissions error")
text = (
_('Found a Logitech receiver or device (%s), but did not have permission to open it.') % object + '\n\n' +
_("If you've just installed Solaar, try disconnecting the receiver or device and then reconnecting it.")
_("Found a Logitech receiver or device (%s), but did not have permission to open it.") % object
+ "\n\n"
+ _("If you've just installed Solaar, try disconnecting the receiver or device and then reconnecting it.")
)
elif reason == 'nodevice':
title = _('Cannot connect to device error')
elif reason == "nodevice":
title = _("Cannot connect to device error")
text = (
_('Found a Logitech receiver or device at %s, but encountered an error connecting to it.') % object + '\n\n' +
_('Try disconnecting the device and then reconnecting it or turning it off and then on.')
_("Found a Logitech receiver or device at %s, but encountered an error connecting to it.") % object
+ "\n\n"
+ _("Try disconnecting the device and then reconnecting it or turning it off and then on.")
)
elif reason == 'unpair':
title = _('Unpairing failed')
elif reason == "unpair":
title = _("Unpairing failed")
text = (
_('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.')
_("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.")
)
else:
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():
global _task_runner
_task_runner = _TaskRunner('AsyncUI')
_task_runner = _TaskRunner("AsyncUI")
_task_runner.start()

View File

@ -26,11 +26,12 @@ import gi
from logitech_receiver.hidpp20 import LEDEffectSetting as _LEDEffectSetting
from logitech_receiver.settings import KIND as _SETTING_KIND
from logitech_receiver.settings import SENSITIVITY_IGNORE as _SENSITIVITY_IGNORE
from solaar.i18n import _, ngettext
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
logger = logging.getLogger(__name__)
@ -41,7 +42,6 @@ logger = logging.getLogger(__name__)
def _read_async(setting, force_read, sbox, device_is_online, sensitive):
def _do_read(s, force, sb, online, sensitive):
v = s.read(not force)
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 _do_write(s, v, sb, key):
try:
if key is None:
@ -78,7 +77,6 @@ def _write_async(setting, value, sbox, sensitive=True, key=None):
class ComboBoxText(Gtk.ComboBoxText):
def get_value(self):
return int(self.get_active_id())
@ -87,13 +85,11 @@ class ComboBoxText(Gtk.ComboBoxText):
class Scale(Gtk.Scale):
def get_value(self):
return int(super().get_value())
class Control():
class Control:
def __init__(**kwargs):
pass
@ -119,11 +115,10 @@ class Control():
class ToggleControl(Gtk.Switch, Control):
def __init__(self, sbox, delegate=None):
super().__init__(halign=Gtk.Align.CENTER, valign=Gtk.Align.CENTER)
self.init(sbox, delegate)
self.connect('notify::active', self.changed)
self.connect("notify::active", self.changed)
def set_value(self, value):
if value is not None:
@ -134,7 +129,6 @@ class ToggleControl(Gtk.Switch, Control):
class SliderControl(Gtk.Scale, Control):
def __init__(self, sbox, delegate=None):
super().__init__(halign=Gtk.Align.FILL)
self.init(sbox, delegate)
@ -143,7 +137,7 @@ class SliderControl(Gtk.Scale, Control):
self.set_round_digits(0)
self.set_digits(0)
self.set_increments(1, 5)
self.connect('value-changed', self.changed)
self.connect("value-changed", self.changed)
def get_value(self):
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
class ChoiceControlLittle(Gtk.ComboBoxText, Control):
def __init__(self, sbox, delegate=None, choices=None):
super().__init__(halign=Gtk.Align.FILL)
self.init(sbox, delegate)
self.choices = choices if choices is not None else sbox.setting.choices
for entry in self.choices:
self.append(str(int(entry)), str(entry))
self.connect('changed', self.changed)
self.connect("changed", self.changed)
def get_value(self):
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):
def __init__(self, sbox, delegate=None, choices=None):
super().__init__(halign=Gtk.Align.FILL)
self.init(sbox, delegate)
@ -208,13 +200,13 @@ class ChoiceControlBig(Gtk.Entry, Control):
liststore.append((int(v), str(v)))
completion = Gtk.EntryCompletion()
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_text_column(1)
self.set_completion(completion)
self.connect('changed', self.changed)
self.connect('activate', self.activate)
completion.connect('match_selected', self.select)
self.connect("changed", self.changed)
self.connect("activate", self.activate)
completion.connect("match_selected", self.select)
def get_value(self):
choice = self.get_choice()
@ -230,25 +222,24 @@ class ChoiceControlBig(Gtk.Entry, Control):
def changed(self, *args):
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)
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)
def activate(self, *args):
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()
def select(self, completion, model, iter):
self.set_value(model.get(iter, 0)[0])
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()
class MapChoiceControl(Gtk.HBox, Control):
def __init__(self, sbox, delegate=None):
super().__init__(homogeneous=False, spacing=6)
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.pack_start(self.keyBox, 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):
key_choice = int(self.keyBox.get_active_id())
@ -301,8 +292,7 @@ class MapChoiceControl(Gtk.HBox, 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__()
self.init(sbox, delegate)
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
btn = Gtk.Button(button_label)
btn.set_alignment(1.0, 0.5)
btn.connect('clicked', self.toggle_display)
btn.connect("clicked", self.toggle_display)
self._button = btn
hbox = Gtk.HBox(homogeneous=False, spacing=6)
hbox.pack_end(change, False, False, 0)
@ -345,22 +335,21 @@ class MultipleControl(Gtk.ListBox, Control):
class MultipleToggleControl(MultipleControl):
def setup(self, setting):
self._label_control_pairs = []
for k in setting._validator.get_options():
h = Gtk.HBox(homogeneous=False, spacing=0)
lbl_text = str(k)
lbl_tooltip = None
if hasattr(setting, '_labels'):
if hasattr(setting, "_labels"):
l1, l2 = setting._labels.get(k, (None, None))
lbl_text = l1 if l1 else lbl_text
lbl_tooltip = l2 if l2 else lbl_tooltip
lbl = Gtk.Label(lbl_text)
h.set_tooltip_text(lbl_tooltip or ' ')
h.set_tooltip_text(lbl_tooltip or " ")
control = Gtk.Switch()
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_end(control, False, False, 0)
lbl.set_alignment(0.0, 0.5)
@ -388,26 +377,25 @@ class MultipleToggleControl(MultipleControl):
elem.set_state(v)
if elem.get_state():
active += 1
to_join.append(lbl.get_text() + ': ' + str(elem.get_state()))
b = ', '.join(to_join)
self._button.set_label(f'{active} / {total}')
to_join.append(lbl.get_text() + ": " + str(elem.get_state()))
b = ", ".join(to_join)
self._button.set_label(f"{active} / {total}")
self._button.set_tooltip_text(b)
class MultipleRangeControl(MultipleControl):
def setup(self, setting):
self._items = []
for item in setting._validator.items:
lbl_text = str(item)
lbl_tooltip = None
if hasattr(setting, '_labels'):
if hasattr(setting, "_labels"):
l1, l2 = setting._labels.get(int(item), (None, None))
lbl_text = l1 if l1 else lbl_text
lbl_tooltip = l2 if l2 else lbl_tooltip
item_lbl = Gtk.Label(lbl_text)
self.add(item_lbl)
self.set_tooltip_text(lbl_tooltip or ' ')
self.set_tooltip_text(lbl_tooltip or " ")
item_lb = Gtk.ListBox()
item_lb.set_selection_mode(Gtk.SelectionMode.NONE)
item_lb._sub_items = []
@ -415,27 +403,27 @@ class MultipleRangeControl(MultipleControl):
h = Gtk.HBox(homogeneous=False, spacing=20)
lbl_text = str(sub_item)
lbl_tooltip = None
if hasattr(setting, '_labels_sub'):
if hasattr(setting, "_labels_sub"):
l1, l2 = setting._labels_sub.get(str(sub_item), (None, None))
lbl_text = l1 if l1 else lbl_text
lbl_tooltip = l2 if l2 else lbl_tooltip
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)
sub_item_lbl.set_margin_left(30)
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.set_round_digits(0)
control.set_digits(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.set_digits(0)
h.pack_end(control, False, False, 0)
else:
raise NotImplementedError
control.connect('value-changed', self.changed, item, sub_item)
control.connect("value-changed", self.changed, item, sub_item)
item_lb.add(h)
h._setting_sub_item = sub_item
h._label, h._control = sub_item_lbl, control
@ -447,14 +435,14 @@ class MultipleRangeControl(MultipleControl):
def changed(self, control, item, sub_item):
if control.get_sensitive():
if hasattr(control, '_timer'):
if hasattr(control, "_timer"):
control._timer.cancel()
control._timer = _Timer(0.5, lambda: GLib.idle_add(self._write, control, item, sub_item))
control._timer.start()
def _write(self, control, item, sub_item):
control._timer.cancel()
delattr(control, '_timer')
delattr(control, "_timer")
new_state = int(control.get_value())
if 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):
if value is None:
return
b = ''
b = ""
n = 0
for ch in self._items:
item = ch._setting_item
v = value.get(int(item), None)
if v is not None:
b += str(item) + ': ('
b += str(item) + ": ("
to_join = []
for c in ch._sub_items:
sub_item = c._setting_sub_item
@ -479,15 +467,14 @@ class MultipleRangeControl(MultipleControl):
sub_item_value = c._control.get_value()
c._control.set_value(sub_item_value)
n += 1
to_join.append(str(sub_item) + f'={sub_item_value}')
b += ', '.join(to_join) + ') '
lbl_text = ngettext('%d value', '%d values', n) % n
to_join.append(str(sub_item) + f"={sub_item_value}")
b += ", ".join(to_join) + ") "
lbl_text = ngettext("%d value", "%d values", n) % n
self._button.set_label(lbl_text)
self._button.set_tooltip_text(b)
class PackedRangeControl(MultipleRangeControl):
def setup(self, setting):
validator = setting._validator
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.set_round_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_end(control, True, True, 0)
h._setting_item = validator.keys[item]
@ -509,14 +496,14 @@ class PackedRangeControl(MultipleRangeControl):
def changed(self, control, item):
if control.get_sensitive():
if hasattr(control, '_timer'):
if hasattr(control, "_timer"):
control._timer.cancel()
control._timer = _Timer(0.5, lambda: GLib.idle_add(self._write, control, item))
control._timer.start()
def _write(self, control, item):
control._timer.cancel()
delattr(control, '_timer')
delattr(control, "_timer")
new_state = int(control.get_value())
if 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):
if value is None:
return
b = ''
b = ""
n = len(self._items)
for h in self._items:
item = h._setting_item
@ -534,43 +521,42 @@ class PackedRangeControl(MultipleRangeControl):
h.control.set_value(v)
else:
v = self.sbox.setting._value[int(item)]
b += str(item) + ': (' + str(v) + ') '
lbl_text = ngettext('%d value', '%d values', n) % n
b += str(item) + ": (" + str(v) + ") "
lbl_text = ngettext("%d value", "%d values", n) % n
self._button.set_label(lbl_text)
self._button.set_tooltip_text(b)
# control with an ID key that determines what else to show
class HeteroKeyControl(Gtk.HBox, Control):
def __init__(self, sbox, delegate=None):
super().__init__(homogeneous=False, spacing=6)
self.init(sbox, delegate)
self._items = {}
for item in sbox.setting.possible_fields:
if item['label']:
item_lblbox = Gtk.Label(item['label'])
if item["label"]:
item_lblbox = Gtk.Label(item["label"])
self.pack_start(item_lblbox, False, False, 0)
item_lblbox.set_visible(False)
else:
item_lblbox = None
if item['kind'] == _SETTING_KIND.choice:
if item["kind"] == _SETTING_KIND.choice:
item_box = ComboBoxText()
for entry in item['choices']:
for entry in item["choices"]:
item_box.append(str(int(entry)), str(entry))
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)
elif item['kind'] == _SETTING_KIND.range:
elif item["kind"] == _SETTING_KIND.range:
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_digits(0)
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)
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):
result = {}
@ -591,23 +577,23 @@ class HeteroKeyControl(Gtk.HBox, Control):
def setup_visibles(self, ID):
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():
visible = name in fields or name == 'ID'
visible = name in fields or name == "ID"
if lblbox:
lblbox.set_visible(visible)
box.set_visible(visible)
def changed(self, control):
if self.get_sensitive() and control.get_sensitive():
if 'ID' in self._items and control == self._items['ID'][1]:
self.setup_visibles(int(self._items['ID'][1].get_value()))
if hasattr(control, '_timer'):
if "ID" in self._items and control == self._items["ID"][1]:
self.setup_visibles(int(self._items["ID"][1].get_value()))
if hasattr(control, "_timer"):
control._timer.cancel()
control._timer = _Timer(0.3, lambda: GLib.idle_add(self._write, control))
control._timer.start()
def _write(self, control):
control._timer.cancel()
delattr(control, '_timer')
delattr(control, "_timer")
new_state = self.get_value()
if self.sbox.setting._value != new_state:
_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 = {
True: _('Changes allowed'),
False: _('No changes allowed'),
_SENSITIVITY_IGNORE: _('Ignore this setting')
True: _("Changes allowed"),
False: _("No changes allowed"),
_SENSITIVITY_IGNORE: _("Ignore this setting"),
}
_next_allowable = {True: False, False: _SENSITIVITY_IGNORE, _SENSITIVITY_IGNORE: True}
_icons_allowables = {v: k for k, v in _allowables_icons.items()}
@ -666,19 +652,19 @@ def _create_sbox(s, device):
label = Gtk.EventBox()
label.add(lbl)
spinner = Gtk.Spinner()
spinner.set_tooltip_text(_('Working') + '...')
spinner.set_tooltip_text(_("Working") + "...")
sbox._spinner = spinner
failed = Gtk.Image.new_from_icon_name('dialog-warning', Gtk.IconSize.SMALL_TOOLBAR)
failed.set_tooltip_text(_('Read/write operation failed.'))
failed = Gtk.Image.new_from_icon_name("dialog-warning", Gtk.IconSize.SMALL_TOOLBAR)
failed.set_tooltip_text(_("Read/write operation 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
_change_icon(False, change_icon)
change = Gtk.Button()
change.set_relief(Gtk.ReliefStyle.NONE)
change.add(change_icon)
change.set_sensitive(True)
change.connect('clicked', _change_click, sbox)
change.connect("clicked", _change_click, sbox)
if s.kind == _SETTING_KIND.toggle:
control = ToggleControl(sbox)
@ -698,7 +684,7 @@ def _create_sbox(s, device):
control = HeteroKeyControl(sbox, change)
else:
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
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):
colour = Gdk.RGBA()
colour.parse('rgba(0,0,0,0)')
colour.parse("rgba(0,0,0,0)")
for child in lb.get_children():
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):
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)
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:
assert device == setting._device
if len(values) > 1:

File diff suppressed because it is too large Load Diff

View File

@ -18,10 +18,10 @@
import logging
import solaar.gtk as gtk
from gi.repository import Gtk
import solaar.gtk as gtk
logger = logging.getLogger(__name__)
#
@ -29,12 +29,12 @@ logger = logging.getLogger(__name__)
#
_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)
TRAY_INIT = 'solaar-init'
TRAY_OKAY = 'solaar'
TRAY_ATTENTION = 'solaar-attention'
TRAY_INIT = "solaar-init"
TRAY_OKAY = "solaar"
TRAY_ATTENTION = "solaar-attention"
_default_theme = None
@ -46,18 +46,18 @@ def _init_icon_paths():
_default_theme = Gtk.IconTheme.get_default()
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
TRAY_OKAY = TRAY_INIT # use monochrome tray icon
if not _default_theme.has_icon('battery-good-symbolic'):
logger.warning('failed to detect symbolic icons')
gtk.battery_icons_style = 'regular'
if gtk.battery_icons_style == 'regular':
if not _default_theme.has_icon('battery-good'):
logger.warning('failed to detect icons')
gtk.battery_icons_style = 'solaar'
if not _default_theme.has_icon("battery-good-symbolic"):
logger.warning("failed to detect symbolic icons")
gtk.battery_icons_style = "regular"
if gtk.battery_icons_style == "regular":
if not _default_theme.has_icon("battery-good"):
logger.warning("failed to detect icons")
gtk.battery_icons_style = "solaar"
#
@ -68,10 +68,10 @@ def _init_icon_paths():
def battery(level=None, charging=False):
icon_name = _battery_icon_name(level, charging)
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
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
@ -85,11 +85,13 @@ def _battery_icon_name(level, charging):
_init_icon_paths()
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')))
return 'battery-%s%s%s' % (
level_name, '-charging' if charging else '', '-symbolic' if gtk.battery_icons_style == 'symbolic' else ''
level_name = _first_res(level, ((90, "full"), (30, "good"), (20, "low"), (5, "caution"), (0, "empty")))
return "battery-%s%s%s" % (
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):
if level is None or level < 0:
return 'light_unknown'
return 'solaar-light_%03d' % (20 * ((level + 50) // 100))
return "light_unknown"
return "solaar-light_%03d" % (20 * ((level + 50) // 100))
#
@ -111,7 +113,7 @@ def lux(level=None):
_ICON_SETS = {}
def device_icon_set(name='_', kind=None):
def device_icon_set(name="_", kind=None):
icon_set = _ICON_SETS.get(name)
if icon_set is None:
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
# the theme will hopefully pick up the most appropriate
names = ['preferences-desktop-peripherals']
names = ["preferences-desktop-peripherals"]
if kind:
if str(kind) == 'numpad':
names += ('input-keyboard', 'input-dialpad')
elif str(kind) == 'touchpad':
names += ('input-mouse', 'input-tablet')
elif str(kind) == 'trackball':
names += ('input-mouse', )
elif str(kind) == 'headset':
names += ('audio-headphones', 'audio-headset')
names += ('input-' + str(kind), )
if str(kind) == "numpad":
names += ("input-keyboard", "input-dialpad")
elif str(kind) == "touchpad":
names += ("input-mouse", "input-tablet")
elif str(kind) == "trackball":
names += ("input-mouse",)
elif str(kind) == "headset":
names += ("audio-headphones", "audio-headset")
names += ("input-" + str(kind),)
# names += (name.replace(' ', '-'),)
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)
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:
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
from gi.repository import GLib, Notify
@ -44,7 +45,6 @@ except (ValueError, ImportError):
available = False
if available:
# cache references to shown notifications here, so if another status comes
# while its notification is still visible we don't create another one
_notifications = {}
@ -55,18 +55,18 @@ if available:
if available:
if not Notify.is_initted():
if logger.isEnabledFor(logging.INFO):
logger.info('starting desktop notifications')
logger.info("starting desktop notifications")
try:
return Notify.init(NAME)
except Exception:
logger.exception('initializing desktop notifications')
logger.exception("initializing desktop notifications")
available = False
return available and Notify.is_initted()
def uninit():
if available and Notify.is_initted():
if logger.isEnabledFor(logging.INFO):
logger.info('stopping desktop notifications')
logger.info("stopping desktop notifications")
_notifications.clear()
Notify.uninit()
@ -92,14 +92,14 @@ if available:
n.update(NAME, reason, icon_file)
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:
# if logger.isEnabledFor(logging.DEBUG):
# logger.debug("showing %s", n)
n.show()
except Exception:
logger.exception('showing %s', n)
logger.exception("showing %s", n)
def show(dev, reason=None, icon=None, progress=None):
"""Show a notification with title and text.
@ -116,11 +116,11 @@ if available:
if reason:
message = reason
elif dev.status is None:
message = _('unpaired')
message = _("unpaired")
elif bool(dev.status):
message = dev.status.to_string() or _('connected')
message = dev.status.to_string() or _("connected")
else:
message = _('offline')
message = _("offline")
# we need to use the filename here because the notifications daemon
# is an external application that does not know about our icon sets
@ -129,16 +129,16 @@ if available:
n.update(summary, message, icon_file)
urgency = Notify.Urgency.LOW if dev.status else Notify.Urgency.NORMAL
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:
n.set_hint('value', GLib.Variant('i', progress))
n.set_hint("value", GLib.Variant("i", progress))
try:
# if logger.isEnabledFor(logging.DEBUG):
# logger.debug("showing %s", n)
n.show()
except Exception:
logger.exception('showing %s', n)
logger.exception("showing %s", n)
else:
init = lambda: False

View File

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

View File

@ -22,11 +22,13 @@ import os
from time import time as _timestamp
import gi
import solaar.gtk as gtk
from gi.repository import GLib, Gtk
from gi.repository.Gdk import ScrollDirection
from logitech_receiver.status import KEYS as _K
import solaar.gtk as gtk
from solaar import NAME
from solaar.i18n import _
@ -55,13 +57,13 @@ def _create_menu(quit_handler):
# 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)
menu.append(no_receiver)
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('application-exit', _('Quit %s') % NAME, quit_handler, stock_id='application-exit').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.show_all()
@ -140,26 +142,28 @@ def _scroll(tray_icon, event, direction=None):
_picked_device = candidate or _picked_device
if logger.isEnabledFor(logging.DEBUG):
logger.debug('scroll: picked %s', _picked_device)
logger.debug("scroll: picked %s", _picked_device)
_update_tray_icon()
try:
try:
gi.require_version('AyatanaAppIndicator3', '0.1')
gi.require_version("AyatanaAppIndicator3", "0.1")
from gi.repository import AyatanaAppIndicator3 as AppIndicator3
ayatana_appindicator_found = True
except ValueError:
try:
gi.require_version('AppIndicator3', '0.1')
gi.require_version("AppIndicator3", "0.1")
from gi.repository import AppIndicator3
ayatana_appindicator_found = False
except ValueError:
# treat unavailable versions the same as unavailable packages
raise ImportError
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
# https://bugs.launchpad.net/ubuntu/+source/libappindicator/+bug/1363277
@ -175,7 +179,7 @@ try:
def _create(menu):
_icons._init_icon_paths()
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_status(AppIndicator3.IndicatorStatus.ACTIVE)
@ -183,7 +187,7 @@ try:
# ind.set_label(NAME, NAME)
ind.set_menu(menu)
ind.connect('scroll-event', _scroll)
ind.connect("scroll-event", _scroll)
return ind
@ -194,19 +198,19 @@ try:
indicator.set_status(AppIndicator3.IndicatorStatus.ACTIVE)
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
battery_level = device_status.get(_K.BATTERY_LEVEL)
battery_charging = device_status.get(_K.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:
# there may be a receiver, but no peripherals
tray_icon_name = _icons.TRAY_OKAY if _devices_info else _icons.TRAY_INIT
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.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)
except ImportError:
if logger.isEnabledFor(logging.DEBUG):
logger.debug('using StatusIcon')
logger.debug("using StatusIcon")
def _create(menu):
icon = Gtk.StatusIcon.new_from_icon_name(_icons.TRAY_INIT)
icon.set_name(NAME)
icon.set_title(NAME)
icon.set_tooltip_text(NAME)
icon.connect('activate', _window_toggle)
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("activate", _window_toggle)
icon.connect("scroll-event", _scroll)
icon.connect("popup-menu", lambda icon, button, time: menu.popup(None, None, icon.position_menu, icon, button, time))
return icon
@ -247,10 +250,10 @@ except ImportError:
def _update_tray_icon():
tooltip_lines = _generate_tooltip_lines()
tooltip = '\n'.join(tooltip_lines).rstrip('\n')
tooltip = "\n".join(tooltip_lines).rstrip("\n")
_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
battery_level = device_status.get(_K.BATTERY_LEVEL)
battery_charging = device_status.get(_K.BATTERY_CHARGING)
@ -291,7 +294,7 @@ except ImportError:
def _generate_tooltip_lines():
if not _devices_info:
yield '<b>%s</b>: ' % NAME + _('no receiver')
yield "<b>%s</b>: " % NAME + _("no receiver")
return
yield from _generate_description_lines()
@ -299,7 +302,7 @@ def _generate_tooltip_lines():
def _generate_description_lines():
if not _devices_info:
yield _('no receiver')
yield _("no receiver")
return
for _ignore, number, name, status in _devices_info:
@ -308,16 +311,16 @@ def _generate_description_lines():
p = status.to_string()
if p: # does it have any properties to print?
yield '<b>%s</b>' % name
yield "<b>%s</b>" % name
if status:
yield '\t%s' % p
yield "\t%s" % p
else:
yield '\t%s <small>(' % p + _('offline') + ')</small>'
yield "\t%s <small>(" % p + _("offline") + ")</small>"
else:
if status:
yield '<b>%s</b> <small>(' % name + _('no status') + ')</small>'
yield "<b>%s</b> <small>(" % name + _("no status") + ")</small>"
else:
yield '<b>%s</b> <small>(' % name + _('offline') + ')</small>'
yield "<b>%s</b> <small>(" % name + _("offline") + ")</small>"
def _pick_device_with_lowest_battery():
@ -337,7 +340,7 @@ def _pick_device_with_lowest_battery():
picked_level = level or 0
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
@ -369,11 +372,11 @@ def _add_device(device):
new_device_info = (receiver_path, device.number, device.name, device.status)
_devices_info.insert(index, new_device_info)
label_prefix = ' '
new_menu_item = Gtk.ImageMenuItem.new_with_label((label_prefix if device.number else '') + device.name)
label_prefix = " "
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.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)
return index
@ -402,7 +405,7 @@ def _add_receiver(receiver):
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.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)
return 0
@ -421,7 +424,7 @@ def _remove_receiver(receiver):
def _update_menu_item(index, device):
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
menu_items = _menu.get_children()
@ -431,7 +434,7 @@ def _update_menu_item(index, device):
charging = device.status.get(_K.BATTERY_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.set_sensitive(bool(device.online))
_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 NamedInts as _NamedInts
from logitech_receiver.status import KEYS as _K
from solaar import NAME
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
gi.require_version('Gdk', '3.0')
gi.require_version("Gdk", "3.0")
from gi.repository import Gdk, GLib, Gtk # NOQA: E402
logger = logging.getLogger(__name__)
@ -52,10 +53,10 @@ _TREE_ICON_SIZE = Gtk.IconSize.BUTTON
_INFO_ICON_SIZE = Gtk.IconSize.LARGE_TOOLBAR
_DEVICE_ICON_SIZE = Gtk.IconSize.DND
try:
gi.check_version('3.7.4')
gi.check_version("3.7.4")
_CAN_SET_ROW_NONE = None
except (ValueError, AttributeError):
_CAN_SET_ROW_NONE = ''
_CAN_SET_ROW_NONE = ""
# tree model columns
_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)
b.add(c)
if clicked is not None:
b.connect('clicked', clicked)
b.connect("clicked", clicked)
if tooltip:
b.set_tooltip_text(tooltip)
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.pack_start(p._count, True, True, 0)
p._scanning = Gtk.Label(_('Scanning') + '...')
p._scanning = Gtk.Label(_("Scanning") + "...")
p._spinner = Gtk.Spinner()
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_end(p._spinner, False, False, 0)
p.pack_end(bp, False, False, 0)
@ -128,14 +129,14 @@ def _create_device_panel():
return b
p._battery = _status_line(_('Battery'))
p._battery = _status_line(_("Battery"))
p.pack_start(p._battery, False, False, 0)
p._secure = _status_line(_('Wireless Link'))
p._secure._icon.set_from_icon_name('dialog-warning', _INFO_ICON_SIZE)
p._secure = _status_line(_("Wireless Link"))
p._secure._icon.set_from_icon_name("dialog-warning", _INFO_ICON_SIZE)
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(Gtk.Separator.new(Gtk.Orientation.HORIZONTAL), False, False, 0) # spacer
@ -167,11 +168,11 @@ def _create_buttons_box():
bb._details = _new_button(
None,
'dialog-information',
"dialog-information",
_SMALL_BUTTON_ICON_SIZE,
tooltip=_('Show Technical Details'),
tooltip=_("Show Technical Details"),
toggle=True,
clicked=_update_details
clicked=_update_details,
)
bb.add(bb._details)
bb.set_child_secondary(bb._details, True)
@ -185,7 +186,7 @@ def _create_buttons_box():
assert receiver.kind is None
_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)
def _unpair_current_device(trigger):
@ -196,7 +197,7 @@ def _create_buttons_box():
assert device.kind is not None
_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)
return bb
@ -204,7 +205,7 @@ def _create_buttons_box():
def _create_empty_panel():
p = Gtk.Label()
p.set_markup('<small>' + _('Select a device') + '</small>')
p.set_markup("<small>" + _("Select a device") + "</small>")
p.set_sensitive(False)
return p
@ -213,7 +214,7 @@ def _create_empty_panel():
def _create_info_panel():
p = Gtk.Box.new(Gtk.Orientation.VERTICAL, 4)
p._title = Gtk.Label(' ')
p._title = Gtk.Label(" ")
p._title.set_alignment(0, 0.5)
p._icon = Gtk.Image()
@ -256,34 +257,34 @@ def _create_tree(model):
tree.set_row_separator_func(_is_separator, None)
icon_cell_renderer = Gtk.CellRendererPixbuf()
icon_cell_renderer.set_property('stock-size', _TREE_ICON_SIZE)
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, 'icon-name', _COLUMN.ICON)
icon_cell_renderer.set_property("stock-size", _TREE_ICON_SIZE)
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, "icon-name", _COLUMN.ICON)
tree.append_column(icon_column)
name_cell_renderer = Gtk.CellRendererText()
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, 'text', _COLUMN.NAME)
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, "text", _COLUMN.NAME)
name_column.set_expand(True)
tree.append_column(name_column)
tree.set_expander_column(name_column)
status_cell_renderer = Gtk.CellRendererText()
status_cell_renderer.set_property('scale', 0.85)
status_cell_renderer.set_property('xalign', 1)
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, 'text', _COLUMN.STATUS_TEXT)
status_cell_renderer.set_property("scale", 0.85)
status_cell_renderer.set_property("xalign", 1)
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, "text", _COLUMN.STATUS_TEXT)
status_column.set_expand(True)
tree.append_column(status_column)
battery_cell_renderer = Gtk.CellRendererPixbuf()
battery_cell_renderer.set_property('stock-size', _TREE_ICON_SIZE)
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, 'icon-name', _COLUMN.STATUS_ICON)
battery_cell_renderer.set_property("stock-size", _TREE_ICON_SIZE)
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, "icon-name", _COLUMN.STATUS_ICON)
tree.append_column(battery_column)
return tree
@ -296,7 +297,7 @@ def _create_window_layout():
assert _empty is not None
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.add(_tree)
@ -316,12 +317,12 @@ def _create_window_layout():
bottom_buttons_box = Gtk.ButtonBox(Gtk.Orientation.HORIZONTAL)
bottom_buttons_box.set_layout(Gtk.ButtonBoxStyle.START)
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)
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)
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.set_child_secondary(diversion_button, True)
@ -345,12 +346,12 @@ def _create_window_layout():
def _create(delete_action):
window = Gtk.Window()
window.set_title(NAME)
window.set_role('status-window')
window.set_role("status-window")
# window.set_type_hint(Gdk.WindowTypeHint.UTILITY)
# window.set_skip_taskbar_hint(True)
# window.set_skip_pager_hint(True)
window.connect('delete-event', delete_action)
window.connect("delete-event", delete_action)
vbox = _create_window_layout()
window.add(vbox)
@ -362,7 +363,7 @@ def _create(delete_action):
window.set_position(Gtk.WindowPosition.CENTER)
style = window.get_style_context()
style.add_class('solaar')
style.add_class("solaar")
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)
assert len(row_data) == len(_TREE_SEPATATOR)
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)
if _TREE_SEPATATOR:
_model.append(None, _TREE_SEPATATOR)
@ -446,8 +447,9 @@ def _device_row(receiver_path, device_number, device=None):
while item:
if _model.get_value(item, _COLUMN.PATH) != receiver_path:
logger.warning(
'path for device row %s different from path for receiver %s', _model.get_value(item, _COLUMN.PATH),
receiver_path
"path for device row %s different from path for receiver %s",
_model.get_value(item, _COLUMN.PATH),
receiver_path,
)
item_number = _model.get_value(item, _COLUMN.NUMBER)
if item_number == device_number:
@ -463,11 +465,18 @@ def _device_row(receiver_path, device_number, device=None):
status_text = None
status_icon = None
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)
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)
return item or None
@ -489,7 +498,7 @@ def select(receiver_path, device_number=None):
selection = _tree.get_selection()
selection.select_iter(item)
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):
@ -532,57 +541,57 @@ def _update_details(button):
# If read_all is False, only return stuff that is ~100% already
# cached, and involves no HID++ calls.
yield (_('Path'), device.path)
yield (_("Path"), device.path)
if device.kind is None:
# 046d is the Logitech vendor id
yield (_('USB ID'), '046d:' + device.product_id)
yield (_("USB ID"), "046d:" + device.product_id)
if read_all:
yield (_('Serial'), device.serial)
yield (_("Serial"), device.serial)
else:
yield (_('Serial'), '...')
yield (_("Serial"), "...")
else:
# yield ('Codename', device.codename)
yield (_('Index'), device.number)
yield (_("Index"), device.number)
if device.wpid:
yield (_('Wireless PID'), device.wpid)
yield (_("Wireless PID"), device.wpid)
if device.product_id:
yield (_('Product ID'), '046d:' + device.product_id)
yield (_("Product ID"), "046d:" + device.product_id)
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:
yield (_('Polling rate'), device.polling_rate)
yield (_("Polling rate"), device.polling_rate)
if read_all or not device.online:
yield (_('Serial'), device.serial)
yield (_("Serial"), device.serial)
else:
yield (_('Serial'), '...')
yield (_("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 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:
yield (' %s' % _('Firmware'), '...')
yield (" %s" % _("Firmware"), "...")
flag_bits = device.status.get(_K.NOTIFICATION_FLAGS)
if flag_bits is not None:
flag_names = ('(%s)' % _('none'), ) if flag_bits == 0 else _hidpp10.NOTIFICATION_FLAG.flag_names(flag_bits)
yield (_('Notifications'), ('\n%15s' % ' ').join(flag_names))
flag_names = ("(%s)" % _("none"),) if flag_bits == 0 else _hidpp10.NOTIFICATION_FLAG.flag_names(flag_bits)
yield (_("Notifications"), ("\n%15s" % " ").join(flag_names))
def _set_details(text):
_details._text.set_markup(text)
def _make_text(items):
text = '\n'.join('%-13s: %s' % (name, value) for name, value in items)
return '<small><tt>' + text + '</tt></small>'
text = "\n".join("%-13s: %s" % (name, value) for name, value in items)
return "<small><tt>" + text + "</tt></small>"
def _displayable_items(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:
yield name, value
@ -614,26 +623,29 @@ def _update_receiver_panel(receiver, panel, buttons, full=False):
devices_count = len(receiver)
paired_text = _(
_('No device paired.')
) if devices_count == 0 else ngettext('%(count)s paired device.', '%(count)s paired devices.', devices_count) % {
'count': devices_count
}
paired_text = (
_(_("No device paired."))
if devices_count == 0
else ngettext("%(count)s paired device.", "%(count)s paired devices.", devices_count) % {"count": devices_count}
)
if (receiver.max_devices > 0):
paired_text += '\n\n<small>%s</small>' % ngettext(
'Up to %(max_count)s device can be paired to this receiver.',
'Up to %(max_count)s devices can be paired to this receiver.', receiver.max_devices
) % {
'max_count': receiver.max_devices
}
if receiver.max_devices > 0:
paired_text += (
"\n\n<small>%s</small>"
% ngettext(
"Up to %(max_count)s device can be paired to this receiver.",
"Up to %(max_count)s devices can be paired to this receiver.",
receiver.max_devices,
)
% {"max_count": receiver.max_devices}
)
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()
if (pairings is not None and pairings >= 0):
paired_text += '\n<small>%s</small>' % (
ngettext('This receiver has %d pairing remaining.', 'This receiver has %d pairings remaining.', pairings) %
pairings
if pairings is not None and pairings >= 0:
paired_text += "\n<small>%s</small>" % (
ngettext("This receiver has %d pairing remaining.", "This receiver has %d pairings remaining.", pairings)
% pairings
)
panel._count.set_markup(paired_text)
@ -655,8 +667,11 @@ def _update_receiver_panel(receiver, panel, buttons, full=False):
# b._insecure.set_visible(False)
buttons._unpair.set_visible(False)
if not is_pairing and (receiver.remaining_pairings() is None or receiver.remaining_pairings() != 0) and \
(receiver.re_pairs or devices_count < receiver.max_devices):
if (
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)
else:
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)
if battery_voltage is not None:
panel._battery._label.set_text(_('Battery Voltage'))
text = '%dmV' % battery_voltage
tooltip_text = _('Voltage reported by battery')
panel._battery._label.set_text(_("Battery Voltage"))
text = "%dmV" % battery_voltage
tooltip_text = _("Voltage reported by battery")
else:
panel._battery._label.set_text(_('Battery Level'))
text = ''
tooltip_text = _('Approximate level reported by battery')
panel._battery._label.set_text(_("Battery Level"))
text = ""
tooltip_text = _("Approximate level reported by battery")
if battery_voltage is not None and battery_level is not None:
text += ', '
text += ", "
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 isinstance(battery_next_level, _NamedInt):
text += '<small> (' + _('next reported ') + _(str(battery_next_level)) + ')</small>'
text += "<small> (" + _("next reported ") + _(str(battery_next_level)) + ")</small>"
else:
text += '<small> (' + _('next reported ') + ('%d%%' % battery_next_level) + ')</small>'
tooltip_text = tooltip_text + _(' and next level to be reported.')
text += "<small> (" + _("next reported ") + ("%d%%" % battery_next_level) + ")</small>"
tooltip_text = tooltip_text + _(" and next level to be reported.")
if is_online:
if charging:
text += ' <small>(%s)</small>' % _('charging')
text += " <small>(%s)</small>" % _("charging")
else:
text += ' <small>(%s)</small>' % _('last known')
text += " <small>(%s)</small>" % _("last known")
panel._battery._text.set_markup(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._icon.set_visible(True)
if device.status.get(_K.LINK_ENCRYPTED) is True:
panel._secure._text.set_text(_('encrypted'))
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._text.set_text(_("encrypted"))
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."))
else:
panel._secure._text.set_text(_('not encrypted'))
panel._secure._icon.set_from_icon_name('security-low', _INFO_ICON_SIZE)
panel._secure._text.set_text(_("not encrypted"))
panel._secure._icon.set_from_icon_name("security-low", _INFO_ICON_SIZE)
panel._secure.set_tooltip_text(
_(
'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.'
"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."
)
)
else:
panel._secure.set_visible(True)
panel._secure._icon.set_visible(False)
panel._secure._text.set_markup('<small>%s</small>' % _('offline'))
panel._secure.set_tooltip_text('')
panel._secure._text.set_markup("<small>%s</small>" % _("offline"))
panel._secure.set_tooltip_text("")
if is_online:
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)
else:
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)
else:
panel._lux.set_visible(False)
@ -769,7 +784,7 @@ def _update_info_panel(device, full=False):
# a device must be paired
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)
_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:
was_pairing = bool(_model.get_value(item, _COLUMN.STATUS_ICON))
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):
full_update = need_popup or was_pairing != is_pairing
@ -875,7 +890,7 @@ def update(device, need_popup=False, refresh=False):
else:
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)
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)
else:
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):
status_text = _(str(battery_level))
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)
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):
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
def check(_store, _treepath, row):

View File

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

View File

@ -9,88 +9,89 @@ try:
except ImportError:
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()
try: # get commit from git describe
commit = subprocess.check_output(['git', 'describe', '--always'], stderr=subprocess.DEVNULL).strip().decode()
with open('lib/solaar/commit', 'w') as vfile:
vfile.write(f'{commit}\n')
commit = subprocess.check_output(["git", "describe", "--always"], stderr=subprocess.DEVNULL).strip().decode()
with open("lib/solaar/commit", "w") as vfile:
vfile.write(f"{commit}\n")
except Exception: # get commit from Ubuntu dpkg-parsechangelog
try:
commit = subprocess.check_output(['dpkg-parsechangelog', '--show-field', 'Version'],
stderr=subprocess.DEVNULL).strip().decode()
commit = commit.split('~')
with open('lib/solaar/commit', 'w') as vfile:
vfile.write(f'{commit[0]}\n')
commit = (
subprocess.check_output(["dpkg-parsechangelog", "--show-field", "Version"], stderr=subprocess.DEVNULL)
.strip()
.decode()
)
commit = commit.split("~")
with open("lib/solaar/commit", "w") as vfile:
vfile.write(f"{commit[0]}\n")
except Exception as e:
print('Exception using dpkg-parsechangelog', e)
print("Exception using dpkg-parsechangelog", e)
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')
yield 'share/icons/hicolor/32x32/apps', _glob('share/solaar/icons/solaar-light_*.png')
for mo in _glob('share/locale/*/LC_MESSAGES/solaar.mo'):
for mo in _glob("share/locale/*/LC_MESSAGES/solaar.mo"):
yield _dirname(mo), [mo]
yield 'share/applications', ['share/applications/solaar.desktop']
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/applications", ["share/applications/solaar.desktop"]
yield "lib/udev/rules.d", ["rules.d/42-logitech-unify-permissions.rules"]
yield "share/metainfo", ["share/solaar/io.github.pwr_solaar.solaar.metainfo.xml"]
setup(
name=NAME.lower(),
version=version,
description='Linux device manager for Logitech receivers, keyboards, mice, and tablets.',
long_description='''
description="Linux device manager for Logitech receivers, keyboards, mice, and tablets.",
long_description="""
Solaar is a Linux device manager for many Logitech peripherals that connect through
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
modifiable features of devices.
For instructions on installing Solaar see https://pwr-solaar.github.io/Solaar/installation'''.strip(),
author='Daniel Pavel',
license='GPLv2',
url='http://pwr-solaar.github.io/Solaar/',
For instructions on installing Solaar see https://pwr-solaar.github.io/Solaar/installation""".strip(),
author="Daniel Pavel",
license="GPLv2",
url="http://pwr-solaar.github.io/Solaar/",
classifiers=[
'Development Status :: 4 - Beta',
'Environment :: X11 Applications :: GTK',
'Environment :: Console',
'Intended Audience :: End Users/Desktop',
'License :: DFSG approved',
'License :: OSI Approved :: GNU General Public License v2 (GPLv2)',
'Natural Language :: English',
'Programming Language :: Python :: 3 :: Only',
'Operating System :: POSIX :: Linux',
'Topic :: Utilities',
"Development Status :: 4 - Beta",
"Environment :: X11 Applications :: GTK",
"Environment :: Console",
"Intended Audience :: End Users/Desktop",
"License :: DFSG approved",
"License :: OSI Approved :: GNU General Public License v2 (GPLv2)",
"Natural Language :: English",
"Programming Language :: Python :: 3 :: Only",
"Operating System :: POSIX :: Linux",
"Topic :: Utilities",
],
platforms=['linux'],
platforms=["linux"],
# sudo apt install python-gi python3-gi \
# 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)'],
python_requires='>=3.7',
python_requires=">=3.7",
install_requires=[
'evdev (>= 1.1.2) ; platform_system=="Linux"',
'pyudev (>= 0.13)',
'PyYAML (>= 3.12)',
'python-xlib (>= 0.27)',
'psutil (>= 5.4.3)',
"pyudev (>= 0.13)",
"PyYAML (>= 3.12)",
"python-xlib (>= 0.27)",
"psutil (>= 5.4.3)",
'dbus-python ; platform_system=="Linux"',
],
extras_require={
'report-descriptor': ['hid-parser'],
'desktop-notifications': ['Notify (>= 0.7)'],
'git-commit': ['python-git-info'],
'test': ['pytest', 'pytest-cov'],
'dev': ['ruff'],
"report-descriptor": ["hid-parser"],
"desktop-notifications": ["Notify (>= 0.7)"],
"git-commit": ["python-git-info"],
"test": ["pytest", "pytest-cov"],
"dev": ["ruff"],
},
package_dir={'': 'lib'},
packages=['keysyms', 'hidapi', 'logitech_receiver', 'solaar', 'solaar.ui', 'solaar.cli'],
package_dir={"": "lib"},
packages=["keysyms", "hidapi", "logitech_receiver", "solaar", "solaar.ui", "solaar.cli"],
data_files=list(_data_files()),
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():
value = b'123456789'
value = b"123456789"
expected = 0x29B1
result = common.crc16(value)
@ -13,21 +13,21 @@ def test_crc16():
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
def test_named_int_comparison():
default_value = 0
default_name = 'entry'
default_name = "entry"
named_int = 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 = common.NamedInt(2, 'unequal')
named_int_unequal = common.NamedInt(2, "unequal")
assert named_int == named_int_equal
assert named_int != named_int_unequal_name
@ -42,23 +42,26 @@ def named_ints():
def test_named_ints(named_ints):
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.name == 'critical'
assert named_ints.critical.name == "critical"
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.name == 'good'
assert named_ints.good.name == "good"
assert named_ints.full == 90
assert named_ints.full.name == 'full'
assert named_ints.full.name == "full"
assert len(named_ints) == 5
@pytest.mark.parametrize('bytes_input, expected_output', [
(b'\x01\x02\x03\x04', '01020304'),
(b'', ''),
])
@pytest.mark.parametrize(
"bytes_input, expected_output",
[
(b"\x01\x02\x03\x04", "01020304"),
(b"", ""),
],
)
def test_strhex(bytes_input, expected_output):
result = common.strhex(bytes_input)
@ -66,7 +69,7 @@ def test_strhex(bytes_input, expected_output):
def test_bytest2int():
value = b'\x12\x34\x56\x78'
value = b"\x12\x34\x56\x78"
expected = 0x12345678
result = common.bytes2int(value)
@ -76,7 +79,7 @@ def test_bytest2int():
def test_int2bytes():
value = 0x12345678
expected = b'\x12\x34\x56\x78'
expected = b"\x12\x34\x56\x78"
result = common.int2bytes(value)

View File

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