From 72a8d311bce64b1536c08e754d22bb91efb66460 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filipe=20La=C3=ADns?= Date: Thu, 2 Jul 2020 13:49:34 +0100 Subject: [PATCH] yapf: change code style to yapf MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Filipe Laíns --- bin/solaar | 56 +- bin/solaar-cli | 32 +- lib/hidapi/__init__.py | 23 +- lib/hidapi/hidconsole.py | 351 +++--- lib/hidapi/udev.py | 402 +++--- lib/logitech_receiver/__init__.py | 3 - lib/logitech_receiver/base.py | 556 +++++---- lib/logitech_receiver/base_usb.py | 169 ++- lib/logitech_receiver/common.py | 365 +++--- lib/logitech_receiver/descriptors.py | 808 +++++++----- lib/logitech_receiver/hidpp10.py | 447 ++++--- lib/logitech_receiver/hidpp20.py | 990 +++++++-------- lib/logitech_receiver/i18n.py | 48 +- lib/logitech_receiver/listener.py | 279 +++-- lib/logitech_receiver/notifications.py | 510 ++++---- lib/logitech_receiver/receiver.py | 875 ++++++------- lib/logitech_receiver/settings.py | 1188 +++++++++--------- lib/logitech_receiver/settings_templates.py | 951 ++++++++------ lib/logitech_receiver/special_keys.py | 938 +++++++------- lib/logitech_receiver/status.py | 524 ++++---- lib/solaar/cli/__init__.py | 232 ++-- lib/solaar/cli/config.py | 177 +-- lib/solaar/cli/pair.py | 125 +- lib/solaar/cli/probe.py | 77 +- lib/solaar/cli/show.py | 412 ++++--- lib/solaar/cli/unpair.py | 29 +- lib/solaar/configuration.py | 131 +- lib/solaar/gtk.py | 174 +-- lib/solaar/i18n.py | 37 +- lib/solaar/listener.py | 502 ++++---- lib/solaar/tasks.py | 57 +- lib/solaar/ui/__init__.py | 158 +-- lib/solaar/ui/about.py | 102 +- lib/solaar/ui/action.py | 97 +- lib/solaar/ui/config_panel.py | 422 ++++--- lib/solaar/ui/icons.py | 219 ++-- lib/solaar/ui/notify.py | 195 ++- lib/solaar/ui/pair_window.py | 289 +++-- lib/solaar/ui/tray.py | 746 +++++------ lib/solaar/ui/window.py | 1225 ++++++++++--------- lib/solaar/upower.py | 85 +- setup.py | 89 +- tools/hidconsole | 21 +- tools/monitor.py | 11 +- 44 files changed, 8093 insertions(+), 7034 deletions(-) diff --git a/bin/solaar b/bin/solaar index 75374e43..e5e048a5 100755 --- a/bin/solaar +++ b/bin/solaar @@ -22,36 +22,38 @@ from __future__ import absolute_import, unicode_literals def init_paths(): - """Make the app work in the source tree.""" - import sys - import os.path as _path + """Make the app work in the source tree.""" + import sys + import os.path as _path - # Python 2 need conversion from utf-8 filenames - # Python 3 might have problems converting back to UTF-8 in case of Unicode surrogates - try: - if sys.version_info < (3,): - decoded_path = sys.path[0].decode(sys.getfilesystemencoding()) - else: - decoded_path = sys.path[0] - sys.path[0].encode(sys.getfilesystemencoding()) + # Python 2 need conversion from utf-8 filenames + # Python 3 might have problems converting back to UTF-8 in case of Unicode surrogates + try: + if sys.version_info < (3, ): + decoded_path = sys.path[0].decode(sys.getfilesystemencoding()) + else: + decoded_path = sys.path[0] + sys.path[0].encode(sys.getfilesystemencoding()) - 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') - sys.exit(1) + 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' + ) + 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') - for location in src_lib, share_lib: - 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]) - sys.path[0] = location - break + 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') + # print ("sys.path[0]: checking", init_py) + if _path.exists(init_py): + # print ("sys.path[0]: found", location, "replacing", sys.path[0]) + sys.path[0] = location + break if __name__ == '__main__': - init_paths() - import solaar.gtk - solaar.gtk.main() + init_paths() + import solaar.gtk + solaar.gtk.main() diff --git a/bin/solaar-cli b/bin/solaar-cli index 862086c8..cb0784a8 100755 --- a/bin/solaar-cli +++ b/bin/solaar-cli @@ -22,22 +22,24 @@ from __future__ import absolute_import, unicode_literals def init_paths(): - """Make the app work in the source tree.""" - import sys - import os.path as _path + """Make the app work in the source tree.""" + import sys + import os.path as _path - prefix = _path.normpath(_path.join(_path.realpath(sys.path[0]), '..')) - 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') - if _path.exists(init_py): - sys.path[0] = location - break + prefix = _path.normpath(_path.join(_path.realpath(sys.path[0]), '..')) + 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') + if _path.exists(init_py): + sys.path[0] = location + break if __name__ == '__main__': - print ('WARNING: solaar-cli is deprecated; use solaar with the usual arguments') - init_paths() - import solaar.cli - solaar.cli.run() + print( + 'WARNING: solaar-cli is deprecated; use solaar with the usual arguments' + ) + init_paths() + import solaar.cli + solaar.cli.run() diff --git a/lib/hidapi/__init__.py b/lib/hidapi/__init__.py index ac2b3a51..bdcb4a46 100644 --- a/lib/hidapi/__init__.py +++ b/lib/hidapi/__init__.py @@ -16,7 +16,6 @@ ## You should have received a copy of the GNU General Public License along ## with this program; if not, write to the Free Software Foundation, Inc., ## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - """Generic Human Interface Device API.""" from __future__ import absolute_import, division, print_function, unicode_literals @@ -24,14 +23,14 @@ from __future__ import absolute_import, division, print_function, unicode_litera __version__ = '0.9' from hidapi.udev import ( - enumerate, - open, - close, - open_path, - monitor_glib, - read, - write, - get_manufacturer, - get_product, - get_serial, - ) + enumerate, + open, + close, + open_path, + monitor_glib, + read, + write, + get_manufacturer, + get_product, + get_serial, +) diff --git a/lib/hidapi/hidconsole.py b/lib/hidapi/hidconsole.py index f7426ff9..f4fc3c75 100644 --- a/lib/hidapi/hidconsole.py +++ b/lib/hidapi/hidconsole.py @@ -31,10 +31,10 @@ import hidapi as _hid # try: - read_packet = raw_input + read_packet = raw_input except NameError: - # Python 3 equivalent of raw_input - read_packet = input + # Python 3 equivalent of raw_input + read_packet = input interactive = os.isatty(0) prompt = '?? Input: ' if interactive else '' @@ -42,18 +42,18 @@ start_time = time.time() strhex = lambda d: hexlify(d).decode('ascii').upper() try: - unicode - # this is certanly Python 2 - is_string = lambda d: isinstance(d, unicode) - # no easy way to distinguish between b'' and '' :( - # or (isinstance(d, str) \ - # and not any((chr(k) in d for k in range(0x00, 0x1F))) \ - # and not any((chr(k) in d for k in range(0x80, 0xFF))) \ - # ) + unicode + # this is certanly Python 2 + is_string = lambda d: isinstance(d, unicode) + # no easy way to distinguish between b'' and '' :( + # or (isinstance(d, str) \ + # and not any((chr(k) in d for k in range(0x00, 0x1F))) \ + # and not any((chr(k) in d for k in range(0x80, 0xFF))) \ + # ) except: - # this is certanly Python 3 - # In Py3, unicode and str are equal (the unicode object does not exist) - is_string = lambda d: isinstance(d, str) + # this is certanly Python 3 + # In Py3, unicode and str are equal (the unicode object does not exist) + is_string = lambda d: isinstance(d, str) # # @@ -63,196 +63,215 @@ from threading import Lock print_lock = Lock() del Lock + def _print(marker, data, scroll=False): - t = time.time() - start_time - if is_string(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)) + t = time.time() - start_time + if is_string(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)) - with print_lock: - # allow only one thread at a time to write to the console, otherwise - # the output gets garbled, especially with ANSI codes. + with print_lock: + # allow only one thread at a time to write to the console, otherwise + # the output gets garbled, especially with ANSI codes. - 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') # move cursor to column 1 - sys.stdout.write(s) - if interactive and scroll: - # restore cursor position - sys.stdout.write('\033[u') - else: - sys.stdout.write('\n') + 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') # move cursor to column 1 + sys.stdout.write(s) + if interactive and scroll: + # restore cursor position + sys.stdout.write('\033[u') + else: + sys.stdout.write('\n') - # flush stdout manually... - # because trying to open stdin/out unbuffered programmatically - # works much too differently in Python 2/3 - sys.stdout.flush() + # flush stdout manually... + # because trying to open stdin/out unbuffered programmatically + # works much too differently in Python 2/3 + sys.stdout.flush() def _error(text, scroll=False): - _print('!!', text, scroll) + _print('!!', text, scroll) def _continuous_read(handle, timeout=2000): - while True: - try: - reply = _hid.read(handle, 128, timeout) - except OSError as e: - _error("Read failed, aborting: " + str(e), True) - break - assert reply is not None - if reply: - _print('>>', reply, True) + while True: + try: + reply = _hid.read(handle, 128, timeout) + except OSError as e: + _error("Read failed, aborting: " + str(e), True) + break + assert reply is not None + if reply: + _print('>>', reply, True) def _validate_input(line, hidpp=False): - try: - data = unhexlify(line.encode('ascii')) - except Exception as e: - _error("Invalid input: " + str(e)) - return None + try: + data = unhexlify(line.encode('ascii')) + except Exception as e: + _error("Invalid input: " + str(e)) + return None - if hidpp: - if len(data) < 4: - _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") - return None - if data[1:2] not in b'\xFF\x01\x02\x03\x04\x05\x06': - _error("Invalid HID++ request: second byte must be 0xFF or one of 0x01..0x06") - return None - if data[:1] == b'\x10': - if len(data) > 7: - _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': - if len(data) > 20: - _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] + if hidpp: + if len(data) < 4: + _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") + return None + if data[1:2] not in b'\xFF\x01\x02\x03\x04\x05\x06': + _error( + "Invalid HID++ request: second byte must be 0xFF or one of 0x01..0x06" + ) + return None + if data[:1] == b'\x10': + if len(data) > 7: + _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': + if len(data) > 20: + _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] - return data + return data def _open(args): - device = args.device - if args.hidpp and not device: - for d in _hid.enumerate(vendor_id=0x046d): - if d.driver == 'logitech-djreceiver': - device = d.path - break - if not device: - sys.exit("!! No HID++ receiver found.") - if not device: - sys.exit("!! Device path required.") + device = args.device + if args.hidpp and not device: + for d in _hid.enumerate(vendor_id=0x046d): + if d.driver == 'logitech-djreceiver': + device = d.path + break + if not device: + sys.exit("!! No HID++ receiver found.") + if not device: + sys.exit("!! Device path required.") - print (".. Opening device", device) - handle = _hid.open_path(device) - if not handle: - sys.exit("!! Failed to open %s, aborting." % device) + print(".. Opening device", device) + handle = _hid.open_path(device) + if not handle: + 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))) - if args.hidpp: - if _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)): - args.hidpp = True - print (".. Logitech receiver detected, HID++ validation enabled.") + print(".. 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) != 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)): + args.hidpp = True + print(".. Logitech receiver detected, HID++ validation enabled.") + + return handle - return handle # # # + def _parse_arguments(): - import argparse - 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('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() + import argparse + 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( + '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() def main(): - args = _parse_arguments() - handle = _open(args) + args = _parse_arguments() + handle = _open(args) - if interactive: - print (".. Press ^C/^D to exit, or type hex bytes to write to the device.") + if interactive: + print( + ".. Press ^C/^D to exit, or type hex bytes to write to the device." + ) - import readline - if args.history is None: - import os.path - args.history = os.path.join(os.path.expanduser('~'), '.hidconsole-history') - try: - readline.read_history_file(args.history) - except: - # file may not exist yet - pass + import readline + if args.history is None: + import os.path + args.history = os.path.join(os.path.expanduser('~'), + '.hidconsole-history') + try: + readline.read_history_file(args.history) + except: + # file may not exist yet + pass - try: - from threading import Thread - t = Thread(target=_continuous_read, args=(handle,)) - t.daemon = True - t.start() + try: + from threading import Thread + 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 + 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 - while t.is_alive(): - line = read_packet(prompt) - line = line.strip().replace(' ', '') - # print ("line", line) - if not line: - continue + while t.is_alive(): + line = read_packet(prompt) + line = line.strip().replace(' ', '') + # print ("line", line) + if not line: + continue - data = _validate_input(line, args.hidpp) - if data is None: - continue + data = _validate_input(line, args.hidpp) + if data is None: + continue - _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': - # the receiver will reply very fast, in a few milliseconds - time.sleep(0.010) - else: - # the devices might reply quite slow - time.sleep(0.700) - except EOFError: - if interactive: - print ("") - else: - time.sleep(1) + _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': + # the receiver will reply very fast, in a few milliseconds + time.sleep(0.010) + else: + # the devices might reply quite slow + time.sleep(0.700) + except EOFError: + if interactive: + print("") + else: + time.sleep(1) - finally: - print (".. Closing handle %r" % handle) - _hid.close(handle) - if interactive: - readline.write_history_file(args.history) + finally: + print(".. Closing handle %r" % handle) + _hid.close(handle) + if interactive: + readline.write_history_file(args.history) if __name__ == '__main__': - main() + main() diff --git a/lib/hidapi/udev.py b/lib/hidapi/udev.py index ff8854c0..36352e5f 100644 --- a/lib/hidapi/udev.py +++ b/lib/hidapi/udev.py @@ -16,7 +16,6 @@ ## You should have received a copy of the GNU General Public License along ## with this program; if not, write to the Free Software Foundation, Inc., ## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - """Generic Human Interface Device API. It is currently a partial pure-Python implementation of the native HID API @@ -35,170 +34,173 @@ from select import select as _select from pyudev import Context as _Context, Monitor as _Monitor, Device as _Device from pyudev import DeviceNotFoundError - native_implementation = 'udev' - # the tuple object we'll expose when enumerating devices from collections import namedtuple DeviceInfo = namedtuple('DeviceInfo', [ - 'path', - 'vendor_id', - 'product_id', - 'serial', - 'release', - 'manufacturer', - 'product', - 'interface', - 'driver', - ]) + 'path', + 'vendor_id', + 'product_id', + 'serial', + 'release', + 'manufacturer', + 'product', + 'interface', + 'driver', +]) del namedtuple - # # exposed API # docstrings mostly copied from hidapi.h # + def init(): - """This function is a no-op, and exists only to match the native hidapi + """This function is a no-op, and exists only to match the native hidapi implementation. :returns: ``True``. """ - return True + return True def exit(): - """This function is a no-op, and exists only to match the native hidapi + """This function is a no-op, and exists only to match the native hidapi implementation. :returns: ``True``. """ - return True + return True # The filter is used to determine whether this is a device of interest to Solaar def _match(action, device, filter): - vendor_id=filter.get('vendor_id') - product_id=filter.get('product_id') - interface_number=filter.get('usb_interface') - hid_driver=filter.get('hid_driver') + vendor_id = filter.get('vendor_id') + product_id = filter.get('product_id') + interface_number = filter.get('usb_interface') + hid_driver = filter.get('hid_driver') - usb_device = device.find_parent('usb', 'usb_device') - # print ("* parent", action, device, "usb:", usb_device) - if not usb_device: - return + usb_device = device.find_parent('usb', 'usb_device') + # print ("* parent", action, device, "usb:", usb_device) + if not usb_device: + return - vid = usb_device.get('ID_VENDOR_ID') - pid = usb_device.get('ID_MODEL_ID') - if vid is None or pid is None: - return # there are reports that sometimes the usb_device isn't set up right so be defensive - if not ((vendor_id is None or vendor_id == int(vid, 16)) and - (product_id is None or product_id == int(pid, 16))): - return + vid = usb_device.get('ID_VENDOR_ID') + pid = usb_device.get('ID_MODEL_ID') + if vid is None or pid is None: + return # there are reports that sometimes the usb_device isn't set up right so be defensive + if not ((vendor_id is None or vendor_id == int(vid, 16)) and + (product_id is None or product_id == int(pid, 16))): + return - if action == 'add': - hid_device = device.find_parent('hid') - if not hid_device: - return - 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): - if hid_driver_name not in hid_driver: - return - elif hid_driver_name != hid_driver: - return + if action == 'add': + hid_device = device.find_parent('hid') + if not hid_device: + return + 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): + if hid_driver_name not in hid_driver: + return + elif hid_driver_name != hid_driver: + return - intf_device = device.find_parent('usb', 'usb_interface') - # print ("*** usb interface", action, device, "usb_interface:", intf_device) - if interface_number is None: - usb_interface = None if intf_device is None else intf_device.attributes.asint('bInterfaceNumber') - else: - usb_interface = None if intf_device is None else intf_device.attributes.asint('bInterfaceNumber') - if usb_interface is None or interface_number != usb_interface: - return + intf_device = device.find_parent('usb', 'usb_interface') + # print ("*** usb interface", action, device, "usb_interface:", intf_device) + if interface_number is None: + usb_interface = None if intf_device is None else intf_device.attributes.asint( + 'bInterfaceNumber') + else: + usb_interface = None if intf_device is None else intf_device.attributes.asint( + 'bInterfaceNumber') + if usb_interface is None or interface_number != usb_interface: + return - attrs = usb_device.attributes - d_info = DeviceInfo(path=device.device_node, - vendor_id=vid[-4:], - product_id=pid[-4:], - serial=hid_device.get('HID_UNIQ'), - release=attrs.get('bcdDevice'), - manufacturer=attrs.get('manufacturer'), - product=attrs.get('product'), - interface=usb_interface, - driver=hid_driver_name) - return d_info + attrs = usb_device.attributes + d_info = DeviceInfo(path=device.device_node, + vendor_id=vid[-4:], + product_id=pid[-4:], + serial=hid_device.get('HID_UNIQ'), + release=attrs.get('bcdDevice'), + manufacturer=attrs.get('manufacturer'), + product=attrs.get('product'), + interface=usb_interface, + driver=hid_driver_name) + return d_info - elif action == 'remove': - # print (dict(device), dict(usb_device)) + elif action == 'remove': + # print (dict(device), dict(usb_device)) - d_info = DeviceInfo(path=device.device_node, - vendor_id=vid[-4:], - product_id=pid[-4:], - serial=None, - release=None, - manufacturer=None, - product=None, - interface=None, - driver=None) - return d_info + d_info = DeviceInfo(path=device.device_node, + vendor_id=vid[-4:], + product_id=pid[-4:], + serial=None, + release=None, + manufacturer=None, + product=None, + interface=None, + driver=None) + return d_info def monitor_glib(callback, *device_filters): - from gi.repository import GLib + from gi.repository import GLib - c = _Context() + c = _Context() - # already existing devices - # for device in c.list_devices(subsystem='hidraw'): - # # print (device, dict(device), dict(device.attributes)) - # for filter in device_filters: - # d_info = _match('add', device, *filter) - # if d_info: - # GLib.idle_add(callback, 'add', d_info) - # break + # already existing devices + # for device in c.list_devices(subsystem='hidraw'): + # # print (device, dict(device), dict(device.attributes)) + # for filter in device_filters: + # d_info = _match('add', device, *filter) + # if d_info: + # GLib.idle_add(callback, 'add', d_info) + # break - m = _Monitor.from_netlink(c) - m.filter_by(subsystem='hidraw') + m = _Monitor.from_netlink(c) + m.filter_by(subsystem='hidraw') - def _process_udev_event(monitor, condition, cb, filters): - if condition == GLib.IO_IN: - event = monitor.receive_device() - if event: - action, device = event - # print ("***", action, device) - if action == 'add': - for filter in filters: - d_info = _match(action, device, filter) - if d_info: - GLib.idle_add(cb, action, d_info) - break - elif action == 'remove': - # the GLib notification does _not_ match! - pass - return True + def _process_udev_event(monitor, condition, cb, filters): + if condition == GLib.IO_IN: + event = monitor.receive_device() + if event: + action, device = event + # print ("***", action, device) + if action == 'add': + for filter in filters: + d_info = _match(action, device, filter) + if d_info: + GLib.idle_add(cb, action, d_info) + break + elif action == 'remove': + # the GLib notification does _not_ match! + pass + return True - try: - # io_add_watch_full may not be available... - GLib.io_add_watch_full(m, GLib.PRIORITY_LOW, GLib.IO_IN, _process_udev_event, callback, device_filters) - # print ("did io_add_watch_full") - except AttributeError: - try: - # and the priority parameter appeared later in the API - GLib.io_add_watch(m, GLib.PRIORITY_LOW, GLib.IO_IN, _process_udev_event, callback, device_filters) - # print ("did io_add_watch with priority") - except: - GLib.io_add_watch(m, GLib.IO_IN, _process_udev_event, callback, device_filters) - # print ("did io_add_watch") + try: + # io_add_watch_full may not be available... + GLib.io_add_watch_full(m, GLib.PRIORITY_LOW, GLib.IO_IN, + _process_udev_event, callback, device_filters) + # print ("did io_add_watch_full") + except AttributeError: + try: + # and the priority parameter appeared later in the API + GLib.io_add_watch(m, GLib.PRIORITY_LOW, GLib.IO_IN, + _process_udev_event, callback, device_filters) + # print ("did io_add_watch with priority") + except: + GLib.io_add_watch(m, GLib.IO_IN, _process_udev_event, callback, + device_filters) + # print ("did io_add_watch") - m.start() + m.start() def enumerate(usb_id): - """Enumerate the HID Devices. + """Enumerate the HID Devices. List all the HID devices attached to the system, optionally filtering by vendor_id, product_id, and/or interface_number. @@ -206,48 +208,48 @@ def enumerate(usb_id): :returns: a list of matching ``DeviceInfo`` tuples. """ - for dev in _Context().list_devices(subsystem='hidraw'): - dev_info = _match('add', dev, usb_id) - if dev_info: - yield dev_info + for dev in _Context().list_devices(subsystem='hidraw'): + dev_info = _match('add', dev, usb_id) + if dev_info: + yield dev_info def open(vendor_id, product_id, serial=None): - """Open a HID device by its Vendor ID, Product ID and optional serial number. + """Open a HID device by its Vendor ID, Product ID and optional serial number. If no serial is provided, the first device with the specified IDs is opened. :returns: an opaque device handle, or ``None``. """ - for device in enumerate(vendor_id, product_id): - if serial is None or serial == device.serial: - return open_path(device.path) + for device in enumerate(vendor_id, product_id): + if serial is None or serial == device.serial: + return open_path(device.path) def open_path(device_path): - """Open a HID device by its path name. + """Open a HID device by its path name. :param device_path: the path of a ``DeviceInfo`` tuple returned by enumerate(). :returns: an opaque device handle, or ``None``. """ - assert device_path - assert device_path.startswith('/dev/hidraw') - return _os.open(device_path, _os.O_RDWR | _os.O_SYNC) + assert device_path + assert device_path.startswith('/dev/hidraw') + return _os.open(device_path, _os.O_RDWR | _os.O_SYNC) def close(device_handle): - """Close a HID device. + """Close a HID device. :param device_handle: a device handle returned by open() or open_path(). """ - assert device_handle - _os.close(device_handle) + assert device_handle + _os.close(device_handle) def write(device_handle, data): - """Write an Output report to a HID device. + """Write an Output report to a HID device. :param device_handle: a device handle returned by open() or open_path(). :param data: the data bytes to send including the report number as the @@ -267,26 +269,28 @@ def write(device_handle, data): one exists. If it does not, it will send the data through the Control Endpoint (Endpoint 0). """ - assert device_handle - assert data - assert isinstance(data, bytes), (repr(data), type(data)) - retrycount = 0 - bytes_written = 0 - while(retrycount < 3): - try: - bytes_written = _os.write(device_handle, data) - retrycount += 1 - except IOError as e: - if e.errno == _errno.EPIPE: - sleep(0.1) - else: - break - if bytes_written != len(data): - raise IOError(_errno.EIO, 'written %d bytes out of expected %d' % (bytes_written, len(data))) + assert device_handle + assert data + assert isinstance(data, bytes), (repr(data), type(data)) + retrycount = 0 + bytes_written = 0 + while (retrycount < 3): + try: + bytes_written = _os.write(device_handle, data) + retrycount += 1 + except IOError as e: + if e.errno == _errno.EPIPE: + sleep(0.1) + else: + break + if bytes_written != len(data): + raise IOError( + _errno.EIO, + 'written %d bytes out of expected %d' % (bytes_written, len(data))) def read(device_handle, bytes_count, timeout_ms=-1): - """Read an Input report from a HID device. + """Read an Input report from a HID device. :param device_handle: a device handle returned by open() or open_path(). :param bytes_count: maximum number of bytes to read. @@ -301,59 +305,61 @@ def read(device_handle, bytes_count, timeout_ms=-1): :returns: the data packet read, an empty bytes string if a timeout was reached, or None if there was an error while reading. """ - assert device_handle - timeout = None if timeout_ms < 0 else timeout_ms / 1000.0 - rlist, wlist, xlist = _select([device_handle], [], [device_handle], timeout) + assert device_handle + timeout = None if timeout_ms < 0 else timeout_ms / 1000.0 + rlist, wlist, xlist = _select([device_handle], [], [device_handle], + timeout) - if xlist: - assert xlist == [device_handle] - raise IOError(_errno.EIO, 'exception on file descriptor %d' % device_handle) + if xlist: + assert xlist == [device_handle] + raise IOError(_errno.EIO, + 'exception on file descriptor %d' % device_handle) - if rlist: - assert rlist == [device_handle] - data = _os.read(device_handle, bytes_count) - assert data is not None - assert isinstance(data, bytes), (repr(data), type(data)) - return data - else: - return b'' + if rlist: + assert rlist == [device_handle] + data = _os.read(device_handle, bytes_count) + assert data is not None + assert isinstance(data, bytes), (repr(data), type(data)) + return data + else: + return b'' _DEVICE_STRINGS = { - 0: 'manufacturer', - 1: 'product', - 2: 'serial', + 0: 'manufacturer', + 1: 'product', + 2: 'serial', } def get_manufacturer(device_handle): - """Get the Manufacturer String from a HID device. + """Get the Manufacturer String from a HID device. :param device_handle: a device handle returned by open() or open_path(). """ - return get_indexed_string(device_handle, 0) + return get_indexed_string(device_handle, 0) def get_product(device_handle): - """Get the Product String from a HID device. + """Get the Product String from a HID device. :param device_handle: a device handle returned by open() or open_path(). """ - return get_indexed_string(device_handle, 1) + return get_indexed_string(device_handle, 1) def get_serial(device_handle): - """Get the serial number from a HID device. + """Get the serial number from a HID device. :param device_handle: a device handle returned by open() or open_path(). """ - serial = get_indexed_string(device_handle, 2) - if serial is not None: - return ''.join(hex(ord(c)) for c in serial) + serial = get_indexed_string(device_handle, 2) + if serial is not None: + return ''.join(hex(ord(c)) for c in serial) def get_indexed_string(device_handle, index): - """Get a string from a HID device, based on its string index. + """Get a string from a HID device, based on its string index. Note: currently not working in the ``hidraw`` native implementation. @@ -362,28 +368,28 @@ def get_indexed_string(device_handle, index): :returns: the value corresponding to index, or None if no value found :rtype: bytes or NoneType """ - try: - key = _DEVICE_STRINGS[index] - except KeyError: - return None + try: + key = _DEVICE_STRINGS[index] + except KeyError: + return None - assert device_handle - stat = _os.fstat(device_handle) - try: - dev = _Device.from_device_number(_Context(), 'char', stat.st_rdev) - except (DeviceNotFoundError, ValueError): - return None + assert device_handle + stat = _os.fstat(device_handle) + try: + dev = _Device.from_device_number(_Context(), 'char', stat.st_rdev) + except (DeviceNotFoundError, ValueError): + return None - hid_dev = dev.find_parent('hid') - if hid_dev: - assert 'HID_ID' in hid_dev - bus, _ignore, _ignore = hid_dev['HID_ID'].split(':') + hid_dev = dev.find_parent('hid') + if hid_dev: + 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') - assert usb_dev - return usb_dev.attributes.get(key) + 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 - # TODO - pass + elif bus == '0005': # BLUETOOTH + # TODO + pass diff --git a/lib/logitech_receiver/__init__.py b/lib/logitech_receiver/__init__.py index 38d40fad..2605d2eb 100644 --- a/lib/logitech_receiver/__init__.py +++ b/lib/logitech_receiver/__init__.py @@ -16,7 +16,6 @@ ## You should have received a copy of the GNU General Public License along ## with this program; if not, write to the Free Software Foundation, Inc., ## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - """Low-level interface for devices connected through a Logitech Universal Receiver (UR). @@ -43,10 +42,8 @@ _log.setLevel(logging.root.level) del logging - __version__ = '0.9' - from .common import strhex from .base import NoReceiver, NoSuchDevice, DeviceUnreachable from .receiver import Receiver, PairedDevice diff --git a/lib/logitech_receiver/base.py b/lib/logitech_receiver/base.py index 08054938..f9acaa31 100644 --- a/lib/logitech_receiver/base.py +++ b/lib/logitech_receiver/base.py @@ -29,7 +29,6 @@ from logging import getLogger, DEBUG as _DEBUG _log = getLogger(__name__) del getLogger - from .common import strhex as _strhex, KwException as _KwException, pack as _pack from . import hidpp10 as _hidpp10 from . import hidpp20 as _hidpp20 @@ -46,13 +45,11 @@ _MAX_READ_SIZE = 32 # mapping from report_id to message length report_lengths = { - 0x10: _SHORT_MESSAGE_SIZE, - 0x11: _LONG_MESSAGE_SIZE, - 0x20: _MEDIUM_MESSAGE_SIZE, - 0x21: _MAX_READ_SIZE + 0x10: _SHORT_MESSAGE_SIZE, + 0x11: _LONG_MESSAGE_SIZE, + 0x20: _MEDIUM_MESSAGE_SIZE, + 0x21: _MAX_READ_SIZE } - - """Default timeout on read (in seconds).""" DEFAULT_TIMEOUT = 4 # the receiver itself should reply very fast, within 500ms @@ -66,22 +63,24 @@ _PING_TIMEOUT = DEFAULT_TIMEOUT * 2 # Exceptions that may be raised by this API. # + class NoReceiver(_KwException): - """Raised when trying to talk through a previously open handle, when the + """Raised when trying to talk through a previously open handle, when the receiver is no longer available. Should only happen if the receiver is physically disconnected from the machine, or its kernel driver module is unloaded.""" - pass + pass class NoSuchDevice(_KwException): - """Raised when trying to reach a device number not paired to the receiver.""" - pass + """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 + """Raised when a request is made to an unreachable (turned off) device.""" + pass + # # @@ -89,23 +88,26 @@ class DeviceUnreachable(_KwException): from .base_usb import ALL as _RECEIVER_USB_IDS + def receivers(): - """List all the Linux devices exposed by the UR attached to the machine.""" - for receiver_usb_id in _RECEIVER_USB_IDS: - for d in _hid.enumerate(receiver_usb_id): - yield d + """List all the Linux devices exposed by the UR attached to the machine.""" + for receiver_usb_id in _RECEIVER_USB_IDS: + for d in _hid.enumerate(receiver_usb_id): + yield d def notify_on_receivers_glib(callback): - """Watch for matching devices and notifies the callback on the GLib thread.""" - _hid.monitor_glib(callback, *_RECEIVER_USB_IDS) + """Watch for matching devices and notifies the callback on the GLib thread.""" + _hid.monitor_glib(callback, *_RECEIVER_USB_IDS) + # # # + def open_path(path): - """Checks if the given Linux device path points to the right UR device. + """Checks if the given Linux device path points to the right UR device. :param path: the Linux device path. @@ -117,39 +119,39 @@ def open_path(path): :returns: an open receiver handle if this is the right Linux device, or ``None``. """ - return _hid.open_path(path) + return _hid.open_path(path) def open(): - """Opens the first Logitech Unifying Receiver found attached to the machine. + """Opens the first Logitech Unifying Receiver found attached to the machine. :returns: An open file handle for the found receiver, or ``None``. """ - for rawdevice in receivers(): - handle = open_path(rawdevice.path) - if handle: - return handle + for rawdevice in receivers(): + handle = open_path(rawdevice.path) + if handle: + return handle def close(handle): - """Closes a HID device handle.""" - if handle: - try: - if isinstance(handle, int): - _hid.close(handle) - else: - handle.close() - # _log.info("closed receiver handle %r", handle) - return True - except: - # _log.exception("closing receiver handle %r", handle) - pass + """Closes a HID device handle.""" + if handle: + try: + if isinstance(handle, int): + _hid.close(handle) + else: + handle.close() + # _log.info("closed receiver handle %r", handle) + return True + except: + # _log.exception("closing receiver handle %r", handle) + pass - return False + return False def write(handle, devnumber, data): - """Writes some data to the receiver, addressed to a certain device. + """Writes some data to the receiver, addressed to a certain device. :param handle: an open UR handle. :param devnumber: attached device number. @@ -161,27 +163,29 @@ def write(handle, devnumber, data): been physically removed from the machine, or the kernel driver has been unloaded. The handle will be closed automatically. """ - # the data is padded to either 5 or 18 bytes - assert data is not None - assert isinstance(data, bytes), (repr(data), type(data)) + # the data is padded to either 5 or 18 bytes + assert data is not None + assert isinstance(data, bytes), (repr(data), type(data)) - if len(data) > _SHORT_MESSAGE_SIZE - 2 or data[:1] == b'\x82': - wdata = _pack('!BB18s', 0x11, devnumber, data) - else: - wdata = _pack('!BB5s', 0x10, devnumber, data) - if _log.isEnabledFor(_DEBUG): - _log.debug("(%s) <= w[%02X %02X %s %s]", handle, ord(wdata[:1]), devnumber, _strhex(wdata[2:4]), _strhex(wdata[4:])) + if len(data) > _SHORT_MESSAGE_SIZE - 2 or data[:1] == b'\x82': + wdata = _pack('!BB18s', 0x11, devnumber, data) + else: + wdata = _pack('!BB5s', 0x10, devnumber, data) + if _log.isEnabledFor(_DEBUG): + _log.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: - _log.error("write failed, assuming handle %r no longer available", handle) - close(handle) - raise NoReceiver(reason=reason) + try: + _hid.write(int(handle), wdata) + except Exception as reason: + _log.error("write failed, assuming handle %r no longer available", + handle) + close(handle) + raise NoReceiver(reason=reason) def read(handle, timeout=DEFAULT_TIMEOUT): - """Read some data from the receiver. Usually called after a write (feature + """Read some data from the receiver. Usually called after a write (feature call), to get the reply. :param: handle open handle to the receiver @@ -193,25 +197,26 @@ def read(handle, timeout=DEFAULT_TIMEOUT): been physically removed from the machine, or the kernel driver has been unloaded. The handle will be closed automatically. """ - reply = _read(handle, timeout) - if reply: - return reply[1:] + reply = _read(handle, timeout) + if reply: + return reply[1:] # sanity checks on message report id and size -def check_message(data) : - assert isinstance(data, bytes), (repr(data), type(data)) - report_id = ord(data[:1]) - if report_id in report_lengths: # is this an HID++ or DJ message? - if report_lengths.get(report_id) == len(data): - return True - else: - _log.warn("unexpected message size: report_id %02X message %s" % (report_id, _strhex(data))) - return False +def check_message(data): + assert isinstance(data, bytes), (repr(data), type(data)) + report_id = ord(data[:1]) + if report_id in report_lengths: # is this an HID++ or DJ message? + if report_lengths.get(report_id) == len(data): + return True + else: + _log.warn("unexpected message size: report_id %02X message %s" % + (report_id, _strhex(data))) + return False def _read(handle, timeout): - """Read an incoming packet from the receiver. + """Read an incoming packet from the receiver. :returns: a tuple of (report_id, devnumber, data), or `None`. @@ -219,98 +224,103 @@ def _read(handle, timeout): been physically removed from the machine, or the kernel driver has been unloaded. The handle will be closed automatically. """ - try: - # convert timeout to milliseconds, the hidapi expects it - timeout = int(timeout * 1000) - data = _hid.read(int(handle), _MAX_READ_SIZE, timeout) - except Exception as reason: - _log.error("read failed, assuming handle %r no longer available", handle) - close(handle) - raise NoReceiver(reason=reason) + try: + # convert timeout to milliseconds, the hidapi expects it + timeout = int(timeout * 1000) + data = _hid.read(int(handle), _MAX_READ_SIZE, timeout) + except Exception as reason: + _log.error("read failed, assuming handle %r no longer available", + handle) + close(handle) + raise NoReceiver(reason=reason) - if data and check_message(data): # ignore messages that fail check - report_id = ord(data[:1]) - devnumber = ord(data[1:2]) + if data and check_message(data): # ignore messages that fail check + report_id = ord(data[:1]) + devnumber = ord(data[1:2]) - if _log.isEnabledFor(_DEBUG): - _log.debug("(%s) => r[%02X %02X %s %s]", handle, report_id, devnumber, _strhex(data[2:4]), _strhex(data[4:])) + if _log.isEnabledFor(_DEBUG): + _log.debug("(%s) => r[%02X %02X %s %s]", handle, report_id, + devnumber, _strhex(data[2:4]), _strhex(data[4:])) + + return report_id, devnumber, data[2:] - return report_id, devnumber, data[2:] # # # + def _skip_incoming(handle, ihandle, notifications_hook): - """Read anything already in the input buffer. + """Read anything already in the input buffer. Used by request() and ping() before their write. """ - while True: - try: - # read whatever is already in the buffer, if any - data = _hid.read(ihandle, _MAX_READ_SIZE, 0) - except Exception as reason: - _log.error("read failed, assuming receiver %s no longer available", handle) - close(handle) - raise NoReceiver(reason=reason) + while True: + try: + # read whatever is already in the buffer, if any + data = _hid.read(ihandle, _MAX_READ_SIZE, 0) + except Exception as reason: + _log.error("read failed, assuming receiver %s no longer available", + handle) + close(handle) + raise NoReceiver(reason=reason) - if data: - if check_message(data): # only process messages that pass check - report_id = ord(data[:1]) - if notifications_hook: - n = make_notification(ord(data[1:2]), data[2:]) - if n: - notifications_hook(n) - else: - # nothing in the input buffer, we're done - return + if data: + if check_message(data): # only process messages that pass check + report_id = ord(data[:1]) + if notifications_hook: + n = make_notification(ord(data[1:2]), data[2:]) + if n: + notifications_hook(n) + else: + # nothing in the input buffer, we're done + return def make_notification(devnumber, data): - """Guess if this is a notification (and not just a request reply), and + """Guess if this is a notification (and not just a request reply), and return a Notification tuple if it is.""" - sub_id = ord(data[:1]) - if sub_id & 0x80 == 0x80: - # this is either a HID++1.0 register r/w, or an error reply - return + sub_id = ord(data[:1]) + if sub_id & 0x80 == 0x80: + # this is either a HID++1.0 register r/w, or an error reply + return - # DJ input records are not notifications - # it would be better to check for report_id 0x20 but that information is not sent here - if len(data) == _MEDIUM_MESSAGE_SIZE-2 and (sub_id < 0x10): - return + # DJ input records are not notifications + # it would be better to check for report_id 0x20 but that information is not sent here + if len(data) == _MEDIUM_MESSAGE_SIZE - 2 and (sub_id < 0x10): + return + + address = ord(data[1:2]) + if ( + # standard HID++ 1.0 notification, SubId may be 0x40 - 0x7F + (sub_id >= 0x40) 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 + # custom HID++1.0 illumination event, where SubId is 0x17 + (sub_id == 0x17 and len(data) == 5) or + # HID++ 2.0 feature notifications have the SoftwareID 0 + (address & 0x0F == 0x00)): + return _HIDPP_Notification(devnumber, sub_id, address, data[2:]) - address = ord(data[1:2]) - if ( - # standard HID++ 1.0 notification, SubId may be 0x40 - 0x7F - (sub_id >= 0x40) - 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 - # custom HID++1.0 illumination event, where SubId is 0x17 - (sub_id == 0x17 and len(data) == 5) - or - # HID++ 2.0 feature notifications have the SoftwareID 0 - (address & 0x0F == 0x00) - ): - return _HIDPP_Notification(devnumber, sub_id, address, data[2:]) from collections import namedtuple -_HIDPP_Notification = namedtuple('_HIDPP_Notification', ('devnumber', 'sub_id', 'address', 'data')) -_HIDPP_Notification.__str__ = lambda self: 'Notification(%d,%02X,%02X,%s)' % (self.devnumber, self.sub_id, self.address, _strhex(self.data)) +_HIDPP_Notification = namedtuple('_HIDPP_Notification', + ('devnumber', 'sub_id', 'address', 'data')) +_HIDPP_Notification.__str__ = lambda self: 'Notification(%d,%02X,%02X,%s)' % ( + self.devnumber, self.sub_id, self.address, _strhex(self.data)) _HIDPP_Notification.__unicode__ = _HIDPP_Notification.__str__ -DJ_NOTIFICATION_LENGTH = _MEDIUM_MESSAGE_SIZE - 4 # to allow easy distinguishing of DJ notifications +DJ_NOTIFICATION_LENGTH = _MEDIUM_MESSAGE_SIZE - 4 # to allow easy distinguishing of DJ notifications del namedtuple # # # + def request(handle, devnumber, request_id, *params): - """Makes a feature call to a device and waits for a matching reply. + """Makes a feature call to a device and waits for a matching reply. This function will wait for a matching reply indefinitely. @@ -321,174 +331,190 @@ def request(handle, devnumber, request_id, *params): :returns: the reply data, or ``None`` if some error occurred. """ - # import inspect as _inspect - # print ('\n '.join(str(s) for s in _inspect.stack())) + # import inspect as _inspect + # print ('\n '.join(str(s) for s in _inspect.stack())) - assert isinstance(request_id, int) - if devnumber != 0xFF and request_id < 0x8000: - # For HID++ 2.0 feature requests, randomize the SoftwareId to make it - # easier to recognize the reply for this request. also, always set the - # most significant bit (8) in SoftwareId, to make notifications easier - # to distinguish from request replies. - # This only applies to peripheral requests, ofc. - request_id = (request_id & 0xFFF0) | 0x08 | _random_bits(3) + assert isinstance(request_id, int) + if devnumber != 0xFF and request_id < 0x8000: + # For HID++ 2.0 feature requests, randomize the SoftwareId to make it + # easier to recognize the reply for this request. also, always set the + # most significant bit (8) in SoftwareId, to make notifications easier + # to distinguish from request replies. + # This only applies to peripheral requests, ofc. + request_id = (request_id & 0xFFF0) | 0x08 | _random_bits(3) - timeout = _RECEIVER_REQUEST_TIMEOUT if devnumber == 0xFF else _DEVICE_REQUEST_TIMEOUT - # be extra patient on long register read - if request_id & 0xFF00 == 0x8300: - timeout *= 2 + timeout = _RECEIVER_REQUEST_TIMEOUT if devnumber == 0xFF else _DEVICE_REQUEST_TIMEOUT + # be extra patient on long register read + if request_id & 0xFF00 == 0x8300: + timeout *= 2 - if params: - params = b''.join(_pack('B', p) if isinstance(p, int) else p for p in params) - else: - params = b'' - # if _log.isEnabledFor(_DEBUG): - # _log.debug("(%s) device %d request_id {%04X} params [%s]", handle, devnumber, request_id, _strhex(params)) - request_data = _pack('!H', request_id) + params + if params: + params = b''.join( + _pack('B', p) if isinstance(p, int) else p for p in params) + else: + params = b'' + # if _log.isEnabledFor(_DEBUG): + # _log.debug("(%s) device %d request_id {%04X} params [%s]", handle, devnumber, request_id, _strhex(params)) + request_data = _pack('!H', request_id) + params - ihandle = int(handle) - notifications_hook = getattr(handle, 'notifications_hook', None) - _skip_incoming(handle, ihandle, notifications_hook) - write(ihandle, devnumber, request_data) + ihandle = int(handle) + notifications_hook = getattr(handle, 'notifications_hook', None) + _skip_incoming(handle, ihandle, notifications_hook) + write(ihandle, devnumber, request_data) - # we consider timeout from this point - request_started = _timestamp() - delta = 0 + # we consider timeout from this point + request_started = _timestamp() + delta = 0 - while delta < timeout: - reply = _read(handle, timeout) + while delta < timeout: + reply = _read(handle, timeout) - if reply: - report_id, reply_devnumber, reply_data = reply - if reply_devnumber == devnumber: - if report_id == 0x10 and reply_data[:1] == b'\x8F' and reply_data[1:3] == request_data[:2]: - error = ord(reply_data[3:4]) + if reply: + report_id, reply_devnumber, reply_data = reply + if reply_devnumber == devnumber: + if report_id == 0x10 and reply_data[:1] == b'\x8F' and reply_data[ + 1:3] == request_data[:2]: + error = ord(reply_data[3:4]) - # if error == _hidpp10.ERROR.resource_error: # device unreachable - # _log.warn("(%s) device %d error on request {%04X}: unknown device", handle, devnumber, request_id) - # raise DeviceUnreachable(number=devnumber, request=request_id) + # if error == _hidpp10.ERROR.resource_error: # device unreachable + # _log.warn("(%s) device %d error on request {%04X}: unknown device", handle, devnumber, request_id) + # raise DeviceUnreachable(number=devnumber, request=request_id) - # if error == _hidpp10.ERROR.unknown_device: # unknown device - # _log.error("(%s) device %d error on request {%04X}: unknown device", handle, devnumber, request_id) - # raise NoSuchDevice(number=devnumber, request=request_id) + # if error == _hidpp10.ERROR.unknown_device: # unknown device + # _log.error("(%s) device %d error on request {%04X}: unknown device", handle, devnumber, request_id) + # raise NoSuchDevice(number=devnumber, request=request_id) - if _log.isEnabledFor(_DEBUG): - _log.debug("(%s) device 0x%02X error on request {%04X}: %d = %s", - handle, devnumber, request_id, error, _hidpp10.ERROR[error]) - return + if _log.isEnabledFor(_DEBUG): + _log.debug( + "(%s) device 0x%02X error on request {%04X}: %d = %s", + handle, devnumber, request_id, error, + _hidpp10.ERROR[error]) + return - 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]) - _log.error("(%s) device %d error on feature request {%04X}: %d = %s", - handle, devnumber, request_id, error, _hidpp20.ERROR[error]) - raise _hidpp20.FeatureCallError(number=devnumber, request=request_id, error=error, params=params) + 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]) + _log.error( + "(%s) device %d error on feature request {%04X}: %d = %s", + handle, devnumber, request_id, error, + _hidpp20.ERROR[error]) + raise _hidpp20.FeatureCallError(number=devnumber, + request=request_id, + error=error, + params=params) - if reply_data[:2] == request_data[:2]: - if request_id & 0xFE00 == 0x8200: - # long registry r/w should return a long reply - assert report_id == 0x11 - elif request_id & 0xFE00 == 0x8000: - # short registry r/w should return a short reply - assert report_id == 0x10 + if reply_data[:2] == request_data[:2]: + if request_id & 0xFE00 == 0x8200: + # long registry r/w should return a long reply + assert report_id == 0x11 + elif request_id & 0xFE00 == 0x8000: + # short registry r/w should return a short reply + assert report_id == 0x10 - if devnumber == 0xFF: - if request_id == 0x83B5 or request_id == 0x81F1: - # these replies have to match the first parameter as well - if reply_data[2:3] == params[:1]: - return reply_data[2:] - else: - # hm, not matching my request, and certainly not a notification - continue - else: - return reply_data[2:] - else: - return reply_data[2:] - else: - # a reply was received, but did not match our request in any way - # reset the timeout starting point - request_started = _timestamp() + if devnumber == 0xFF: + if request_id == 0x83B5 or request_id == 0x81F1: + # these replies have to match the first parameter as well + if reply_data[2:3] == params[:1]: + return reply_data[2:] + else: + # hm, not matching my request, and certainly not a notification + continue + else: + return reply_data[2:] + else: + return reply_data[2:] + else: + # a reply was received, but did not match our request in any way + # reset the timeout starting point + request_started = _timestamp() - if notifications_hook: - n = make_notification(reply_devnumber, reply_data) - if n: - notifications_hook(n) - # elif _log.isEnabledFor(_DEBUG): - # _log.debug("(%s) ignoring reply %02X [%s]", handle, reply_devnumber, _strhex(reply_data)) - # elif _log.isEnabledFor(_DEBUG): - # _log.debug("(%s) ignoring reply %02X [%s]", handle, reply_devnumber, _strhex(reply_data)) + if notifications_hook: + n = make_notification(reply_devnumber, reply_data) + if n: + notifications_hook(n) + # elif _log.isEnabledFor(_DEBUG): + # _log.debug("(%s) ignoring reply %02X [%s]", handle, reply_devnumber, _strhex(reply_data)) + # elif _log.isEnabledFor(_DEBUG): + # _log.debug("(%s) ignoring reply %02X [%s]", handle, reply_devnumber, _strhex(reply_data)) - delta = _timestamp() - request_started - # if _log.isEnabledFor(_DEBUG): - # _log.debug("(%s) still waiting for reply, delta %f", handle, delta) + delta = _timestamp() - request_started + # if _log.isEnabledFor(_DEBUG): + # _log.debug("(%s) still waiting for reply, delta %f", handle, delta) - _log.warn("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) + _log.warn("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) def ping(handle, devnumber): - """Check if a device is connected to the receiver. + """Check if a device is connected to the receiver. :returns: The HID protocol supported by the device, as a floating point number, if the device is active. """ - if _log.isEnabledFor(_DEBUG): - _log.debug("(%s) pinging device %d", handle, devnumber) + if _log.isEnabledFor(_DEBUG): + _log.debug("(%s) pinging device %d", handle, devnumber) - # import inspect as _inspect - # print ('\n '.join(str(s) for s in _inspect.stack())) + # import inspect as _inspect + # print ('\n '.join(str(s) for s in _inspect.stack())) - assert devnumber != 0xFF - assert devnumber > 0x00 - assert devnumber < 0x0F + assert devnumber != 0xFF + assert devnumber > 0x00 + assert devnumber < 0x0F - # 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)) + # 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)) - ihandle = int(handle) - notifications_hook = getattr(handle, 'notifications_hook', None) - _skip_incoming(handle, ihandle, notifications_hook) - write(ihandle, devnumber, request_data) + ihandle = int(handle) + notifications_hook = getattr(handle, 'notifications_hook', None) + _skip_incoming(handle, ihandle, notifications_hook) + write(ihandle, devnumber, request_data) - # we consider timeout from this point - request_started = _timestamp() - delta = 0 + # we consider timeout from this point + request_started = _timestamp() + delta = 0 - while delta < _PING_TIMEOUT: - reply = _read(handle, _PING_TIMEOUT) + while delta < _PING_TIMEOUT: + reply = _read(handle, _PING_TIMEOUT) - if reply: - report_id, reply_devnumber, reply_data = reply - if reply_devnumber == devnumber: - 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 reply: + report_id, reply_devnumber, reply_data = reply + if reply_devnumber == devnumber: + 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 == 0x10 and reply_data[:1] == b'\x8F' and reply_data[1:3] == request_data[:2]: - assert reply_data[-1:] == b'\x00' - error = ord(reply_data[3:4]) + if report_id == 0x10 and reply_data[:1] == b'\x8F' and reply_data[ + 1:3] == request_data[:2]: + assert reply_data[-1:] == b'\x00' + error = ord(reply_data[3:4]) - if error == _hidpp10.ERROR.invalid_SubID__command: # a valid reply from a HID++ 1.0 device - return 1.0 + if error == _hidpp10.ERROR.invalid_SubID__command: # a valid reply from a HID++ 1.0 device + return 1.0 - if error == _hidpp10.ERROR.resource_error: # device unreachable - return + if error == _hidpp10.ERROR.resource_error: # device unreachable + return - if error == _hidpp10.ERROR.unknown_device: # no paired device with that number - _log.error("(%s) device %d error on ping request: unknown device", handle, devnumber) - raise NoSuchDevice(number=devnumber, request=request_id) + if error == _hidpp10.ERROR.unknown_device: # no paired device with that number + _log.error( + "(%s) device %d error on ping request: unknown device", + handle, devnumber) + raise NoSuchDevice(number=devnumber, + request=request_id) - if notifications_hook: - n = make_notification(reply_devnumber, reply_data) - if n: - notifications_hook(n) - # elif _log.isEnabledFor(_DEBUG): - # _log.debug("(%s) ignoring reply %02X [%s]", handle, reply_devnumber, _strhex(reply_data)) + if notifications_hook: + n = make_notification(reply_devnumber, reply_data) + if n: + notifications_hook(n) + # elif _log.isEnabledFor(_DEBUG): + # _log.debug("(%s) ignoring reply %02X [%s]", handle, reply_devnumber, _strhex(reply_data)) - delta = _timestamp() - request_started + delta = _timestamp() - request_started - _log.warn("(%s) timeout (%0.2f/%0.2f) on device %d ping", handle, delta, _PING_TIMEOUT, devnumber) - # raise DeviceUnreachable(number=devnumber, request=request_id) + _log.warn("(%s) timeout (%0.2f/%0.2f) on device %d ping", handle, delta, + _PING_TIMEOUT, devnumber) + # raise DeviceUnreachable(number=devnumber, request=request_id) diff --git a/lib/logitech_receiver/base_usb.py b/lib/logitech_receiver/base_usb.py index 16857e22..4b074d71 100644 --- a/lib/logitech_receiver/base_usb.py +++ b/lib/logitech_receiver/base_usb.py @@ -22,7 +22,6 @@ from __future__ import absolute_import, division, print_function, unicode_literals - _DRIVER = ('hid-generic', 'generic-usb', 'logitech-djreceiver') # max_devices is only used for receivers that do not support reading from _R.receiver_info offset 0x03, default to 1 @@ -32,117 +31,117 @@ _DRIVER = ('hid-generic', 'generic-usb', 'logitech-djreceiver') ## currently only one receiver is so marked - should there be more? _unifying_receiver = lambda product_id: { - 'vendor_id':0x046d, - 'product_id':product_id, - 'usb_interface':2, - 'hid_driver':_DRIVER, - 'name':'Unifying Receiver' + 'vendor_id': 0x046d, + 'product_id': product_id, + 'usb_interface': 2, + 'hid_driver': _DRIVER, + 'name': 'Unifying Receiver' } _nano_receiver = lambda product_id: { - 'vendor_id':0x046d, - 'product_id':product_id, - 'usb_interface':1, - 'hid_driver':_DRIVER, - 'name':'Nano Receiver', - 'may_unpair': False, - 're_pairs': True + 'vendor_id': 0x046d, + 'product_id': product_id, + 'usb_interface': 1, + 'hid_driver': _DRIVER, + 'name': 'Nano Receiver', + 'may_unpair': False, + 're_pairs': True } _nano_receiver_max2 = lambda product_id: { - 'vendor_id':0x046d, - 'product_id':product_id, - 'usb_interface':1, - 'hid_driver':_DRIVER, - 'name':'Nano Receiver', - 'max_devices': 2, - 'may_unpair': False, - 're_pairs': True + 'vendor_id': 0x046d, + 'product_id': product_id, + 'usb_interface': 1, + 'hid_driver': _DRIVER, + 'name': 'Nano Receiver', + '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, - 'name':'Nano Receiver', - 'max_devices': max, - 'may_unpair': False, - 're_pairs': True + 'vendor_id': 0x046d, + 'product_id': product_id, + 'usb_interface': 1, + 'hid_driver': _DRIVER, + 'name': 'Nano Receiver', + '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, - 'name':'Nano Receiver' + 'vendor_id': 0x17ef, + 'product_id': product_id, + 'usb_interface': 1, + 'hid_driver': _DRIVER, + 'name': 'Nano Receiver' } _lightspeed_receiver = lambda product_id: { - 'vendor_id':0x046d, - 'product_id':product_id, - 'usb_interface':2, - 'hid_driver':_DRIVER, - 'name':'Lightspeed Receiver' + 'vendor_id': 0x046d, + 'product_id': product_id, + 'usb_interface': 2, + 'hid_driver': _DRIVER, + 'name': 'Lightspeed Receiver' } # 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 receviers that support the Unifying protocol -NANO_RECEIVER_ADVANCED = _nano_receiver(0xc52f) +NANO_RECEIVER_ADVANCED = _nano_receiver(0xc52f) # Nano receivers that don't support the Unifying protocol -NANO_RECEIVER_C517 = _nano_receiver_maxn(0xc517,6) -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(0xc52e) -NANO_RECEIVER_C531 = _nano_receiver(0xc531) -NANO_RECEIVER_C534 = _nano_receiver_max2(0xc534) -NANO_RECEIVER_C537 = _nano_receiver(0xc537) -NANO_RECEIVER_6042 = _lenovo_receiver(0x6042) +NANO_RECEIVER_C517 = _nano_receiver_maxn(0xc517, 6) +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(0xc52e) +NANO_RECEIVER_C531 = _nano_receiver(0xc531) +NANO_RECEIVER_C534 = _nano_receiver_max2(0xc534) +NANO_RECEIVER_C537 = _nano_receiver(0xc537) +NANO_RECEIVER_6042 = _lenovo_receiver(0x6042) # Lightspeed receivers -LIGHTSPEED_RECEIVER_C539 = _lightspeed_receiver(0xc539) -LIGHTSPEED_RECEIVER_C53a = _lightspeed_receiver(0xc53a) -LIGHTSPEED_RECEIVER_C53f = _lightspeed_receiver(0xc53f) -LIGHTSPEED_RECEIVER_C53d = _lightspeed_receiver(0xc53d) +LIGHTSPEED_RECEIVER_C539 = _lightspeed_receiver(0xc539) +LIGHTSPEED_RECEIVER_C53a = _lightspeed_receiver(0xc53a) +LIGHTSPEED_RECEIVER_C53f = _lightspeed_receiver(0xc53f) +LIGHTSPEED_RECEIVER_C53d = _lightspeed_receiver(0xc53d) del _DRIVER, _unifying_receiver, _nano_receiver, _lenovo_receiver, _lightspeed_receiver - ALL = ( - UNIFYING_RECEIVER_C52B, - UNIFYING_RECEIVER_C532, - NANO_RECEIVER_ADVANCED, - NANO_RECEIVER_C517, - NANO_RECEIVER_C518, - NANO_RECEIVER_C51A, - NANO_RECEIVER_C51B, - NANO_RECEIVER_C521, - NANO_RECEIVER_C525, - NANO_RECEIVER_C526, - NANO_RECEIVER_C52e, - NANO_RECEIVER_C531, - NANO_RECEIVER_C534, - NANO_RECEIVER_C537, - NANO_RECEIVER_6042, - LIGHTSPEED_RECEIVER_C539, - LIGHTSPEED_RECEIVER_C53a, - LIGHTSPEED_RECEIVER_C53f, - LIGHTSPEED_RECEIVER_C53d, - ) + UNIFYING_RECEIVER_C52B, + UNIFYING_RECEIVER_C532, + NANO_RECEIVER_ADVANCED, + NANO_RECEIVER_C517, + NANO_RECEIVER_C518, + NANO_RECEIVER_C51A, + NANO_RECEIVER_C51B, + NANO_RECEIVER_C521, + NANO_RECEIVER_C525, + NANO_RECEIVER_C526, + NANO_RECEIVER_C52e, + NANO_RECEIVER_C531, + NANO_RECEIVER_C534, + NANO_RECEIVER_C537, + NANO_RECEIVER_6042, + LIGHTSPEED_RECEIVER_C539, + LIGHTSPEED_RECEIVER_C53a, + LIGHTSPEED_RECEIVER_C53f, + LIGHTSPEED_RECEIVER_C53d, +) + def product_information(usb_id): - if isinstance(usb_id,str): - usb_id = int(usb_id,16) - for r in ALL: - if usb_id == r.get('product_id'): - return r - return { } + if isinstance(usb_id, str): + usb_id = int(usb_id, 16) + for r in ALL: + if usb_id == r.get('product_id'): + return r + return {} diff --git a/lib/logitech_receiver/common.py b/lib/logitech_receiver/common.py index 6fee6ec7..57a0fe35 100644 --- a/lib/logitech_receiver/common.py +++ b/lib/logitech_receiver/common.py @@ -24,70 +24,71 @@ from __future__ import absolute_import, division, print_function, unicode_litera from binascii import hexlify as _hexlify from struct import pack, unpack try: - unicode - # if Python2, unicode_literals will mess our first (un)pack() argument - _pack_str = pack - _unpack_str = unpack - pack = lambda x, *args: _pack_str(str(x), *args) - unpack = lambda x, *args: _unpack_str(str(x), *args) + unicode + # if Python2, unicode_literals will mess our first (un)pack() argument + _pack_str = pack + _unpack_str = unpack + pack = lambda x, *args: _pack_str(str(x), *args) + unpack = lambda x, *args: _unpack_str(str(x), *args) - is_string = lambda d: isinstance(d, unicode) or isinstance(d, str) - # no easy way to distinguish between b'' and '' :( - # or (isinstance(d, str) \ - # and not any((chr(k) in d for k in range(0x00, 0x1F))) \ - # and not any((chr(k) in d for k in range(0x80, 0xFF))) \ - # ) + is_string = lambda d: isinstance(d, unicode) or isinstance(d, str) + # no easy way to distinguish between b'' and '' :( + # or (isinstance(d, str) \ + # and not any((chr(k) in d for k in range(0x00, 0x1F))) \ + # and not any((chr(k) in d for k in range(0x80, 0xFF))) \ + # ) except: - # this is certanly Python 3 - # In Py3, unicode and str are equal (the unicode object does not exist) - is_string = lambda d: isinstance(d, str) + # this is certanly Python 3 + # In Py3, unicode and str are equal (the unicode object does not exist) + is_string = lambda d: isinstance(d, str) # # # + class NamedInt(int): - """An reqular Python integer with an attached name. + """An reqular Python integer with an attached name. Caution: comparison with strings will also match this NamedInt's name (case-insensitive).""" + def __new__(cls, value, name): + assert is_string(name) + obj = int.__new__(cls, value) + obj.name = str(name) + return obj - def __new__(cls, value, name): - assert is_string(name) - obj = int.__new__(cls, value) - obj.name = str(name) - return obj + def bytes(self, count=2): + return int2bytes(self, count) - def bytes(self, count=2): - return int2bytes(self, count) + def __eq__(self, other): + if isinstance(other, NamedInt): + return int(self) == int(other) and self.name == other.name + if isinstance(other, int): + return int(self) == int(other) + if is_string(other): + 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))) - def __eq__(self, other): - if isinstance(other, NamedInt): - return int(self) == int(other) and self.name == other.name - if isinstance(other, int): - return int(self) == int(other) - if is_string(other): - 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))) + def __ne__(self, other): + return not self.__eq__(other) - def __ne__(self, other): - return not self.__eq__(other) + def __hash__(self): + return int(self) - def __hash__(self): - return int(self) + def __str__(self): + return self.name - def __str__(self): - return self.name - __unicode__ = __str__ + __unicode__ = __str__ - def __repr__(self): - return 'NamedInt(%d, %r)' % (int(self), self.name) + def __repr__(self): + return 'NamedInt(%d, %r)' % (int(self), self.name) class NamedInts(object): - """An ordered set of NamedInt values. + """An ordered set of NamedInt values. Indexing can be made by int or string, and will return the corresponding NamedInt if it exists in this set, or `None`. @@ -99,194 +100,194 @@ class NamedInts(object): if the value already exists in the set (int or string), ValueError will be raised. """ - __slots__ = ('__dict__', '_values', '_indexed', '_fallback') + __slots__ = ('__dict__', '_values', '_indexed', '_fallback') - def __init__(self, **kwargs): - def _readable_name(n): - if not is_string(n): - raise TypeError("expected (unicode) string, got " + str(type(n))) - return n.replace('__', '/').replace('_', ' ') + def __init__(self, **kwargs): + def _readable_name(n): + if not is_string(n): + raise TypeError("expected (unicode) string, got " + + str(type(n))) + return n.replace('__', '/').replace('_', ' ') - # print (repr(kwargs)) - values = {k: NamedInt(v, _readable_name(k)) for (k, v) in kwargs.items()} - self.__dict__ = values - self._values = sorted(list(values.values())) - self._indexed = {int(v): v for v in self._values} - # assert len(values) == len(self._indexed), "(%d) %r\n=> (%d) %r" % (len(values), values, len(self._indexed), self._indexed) - self._fallback = None + # print (repr(kwargs)) + values = { + k: NamedInt(v, _readable_name(k)) + for (k, v) in kwargs.items() + } + self.__dict__ = values + self._values = sorted(list(values.values())) + self._indexed = {int(v): v for v in self._values} + # assert len(values) == len(self._indexed), "(%d) %r\n=> (%d) %r" % (len(values), values, len(self._indexed), self._indexed) + self._fallback = None - @classmethod - def list(cls, items, name_generator=lambda x: str(x)): - values = {name_generator(x): x for x in items} - return NamedInts(**values) + @classmethod + def list(cls, items, name_generator=lambda x: str(x)): + values = {name_generator(x): x for x in items} + return NamedInts(**values) - @classmethod - def range(cls, from_value, to_value, name_generator=lambda x: str(x), step=1): - values = {name_generator(x): x for x in range(from_value, to_value + 1, step)} - return NamedInts(**values) + @classmethod + def range(cls, + from_value, + to_value, + name_generator=lambda x: str(x), + step=1): + values = { + name_generator(x): x + for x in range(from_value, to_value + 1, step) + } + return NamedInts(**values) - def flag_names(self, value): - unknown_bits = value - for k in self._indexed: - assert bin(k).count('1') == 1 - if k & value == k: - unknown_bits &= ~k - yield str(self._indexed[k]) + def flag_names(self, value): + unknown_bits = value + for k in self._indexed: + 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 + if unknown_bits: + yield 'unknown:%06X' % unknown_bits - def __getitem__(self, index): - if isinstance(index, int): - if index in self._indexed: - return self._indexed[int(index)] - if self._fallback and isinstance(index, int): - value = NamedInt(index, self._fallback(index)) - self._indexed[index] = value - self._values = sorted(self._values + [value]) - return value + def __getitem__(self, index): + if isinstance(index, int): + if index in self._indexed: + return self._indexed[int(index)] + if self._fallback and isinstance(index, int): + value = NamedInt(index, self._fallback(index)) + self._indexed[index] = value + self._values = sorted(self._values + [value]) + return value - elif is_string(index): - if index in self.__dict__: - return self.__dict__[index] + elif is_string(index): + if index in self.__dict__: + return self.__dict__[index] - elif isinstance(index, slice): - if index.start is None and index.stop is None: - return self._values[:] + elif isinstance(index, slice): + if index.start is None and index.stop is None: + return self._values[:] - v_start = int(self._values[0]) if index.start is None else int(index.start) - v_stop = (self._values[-1] + 1) if index.stop is None else int(index.stop) + v_start = int(self._values[0]) if index.start is None else int( + index.start) + v_stop = (self._values[-1] + + 1) if index.stop is None else int(index.stop) - if v_start > v_stop or v_start > self._values[-1] or v_stop <= self._values[0]: - return [] + if v_start > v_stop or v_start > self._values[ + -1] or v_stop <= self._values[0]: + return [] - if v_start <= self._values[0] and v_stop > self._values[-1]: - return self._values[:] + if v_start <= self._values[0] and v_stop > self._values[-1]: + return self._values[:] - start_index = 0 - stop_index = len(self._values) - for i, value in enumerate(self._values): - if value < v_start: - start_index = i + 1 - elif index.stop is None: - break - if value >= v_stop: - stop_index = i - break + start_index = 0 + stop_index = len(self._values) + for i, value in enumerate(self._values): + if value < v_start: + start_index = i + 1 + elif index.stop is None: + break + if value >= v_stop: + stop_index = i + break - return self._values[start_index:stop_index] + return self._values[start_index:stop_index] - def __setitem__(self, index, name): - assert isinstance(index, int), type(index) - if isinstance(name, NamedInt): - 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') + def __setitem__(self, index, name): + assert isinstance(index, int), type(index) + if isinstance(name, NamedInt): + 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') - if str(value) in self.__dict__: - raise ValueError('%s (%d) already known' % (value, int(value))) - if int(value) in self._indexed: - raise ValueError('%d (%s) already known' % (int(value), value)) + if str(value) in self.__dict__: + raise ValueError('%s (%d) already known' % (value, int(value))) + if int(value) in self._indexed: + raise ValueError('%d (%s) already known' % (int(value), value)) - self._values = sorted(self._values + [value]) - self.__dict__[str(value)] = value - self._indexed[int(value)] = value + self._values = sorted(self._values + [value]) + self.__dict__[str(value)] = value + self._indexed[int(value)] = value - def __contains__(self, value): - if isinstance(value, int): - return value in self._indexed - elif is_string(value): - return value in self.__dict__ + def __contains__(self, value): + if isinstance(value, int): + return value in self._indexed + elif is_string(value): + return value in self.__dict__ - def __iter__(self): - for v in self._values: - yield v + def __iter__(self): + for v in self._values: + yield v - def __len__(self): - return len(self._values) + def __len__(self): + return len(self._values) - def __repr__(self): - return 'NamedInts(%s)' % ', '.join(repr(v) for v in self._values) + def __repr__(self): + return 'NamedInts(%s)' % ', '.join(repr(v) for v in self._values) def strhex(x): - assert x is not None - """Produce a hex-string representation of a sequence of bytes.""" - return _hexlify(x).decode('ascii').upper() + assert x is not None + """Produce a hex-string representation of a sequence of bytes.""" + return _hexlify(x).decode('ascii').upper() def bytes2int(x): - """Convert a bytes string to an int. + """Convert a bytes string to an int. The bytes are assumed to be in most-significant-first order. """ - assert isinstance(x, bytes) - assert len(x) < 9 - qx = (b'\x00' * 8) + x - result, = unpack('!Q', qx[-8:]) - # assert x == int2bytes(result, len(x)) - return result + assert isinstance(x, bytes) + assert len(x) < 9 + qx = (b'\x00' * 8) + x + result, = unpack('!Q', qx[-8:]) + # assert x == int2bytes(result, len(x)) + return result def int2bytes(x, count=None): - """Convert an int to a bytes representation. + """Convert an int to a bytes representation. The bytes are ordered in most-significant-first order. If 'count' is not given, the necessary number of bytes is computed. """ - assert isinstance(x, int) - result = pack('!Q', x) - assert isinstance(result, bytes) - # assert x == bytes2int(result) + assert isinstance(x, int) + result = pack('!Q', x) + assert isinstance(result, bytes) + # assert x == bytes2int(result) - if count is None: - return result.lstrip(b'\x00') + if count is None: + return result.lstrip(b'\x00') - assert isinstance(count, int) - assert count > 0 - assert x.bit_length() <= count * 8 - return result[-count:] + assert isinstance(count, int) + assert count > 0 + assert x.bit_length() <= count * 8 + return result[-count:] class KwException(Exception): - """An exception that remembers all arguments passed to the constructor. + """An exception that remembers all arguments passed to the constructor. They can be later accessed by simple member access. """ - def __init__(self, **kwargs): - super(KwException, self).__init__(kwargs) + def __init__(self, **kwargs): + super(KwException, self).__init__(kwargs) - def __getattr__(self, k): - try: - return super(KwException, self).__getattr__(k) - except AttributeError: - return self.args[0][k] + def __getattr__(self, k): + try: + return super(KwException, self).__getattr__(k) + except AttributeError: + return self.args[0][k] from collections import namedtuple - """Firmware information.""" -FirmwareInfo = namedtuple('FirmwareInfo', [ - 'kind', - 'name', - 'version', - 'extras']) - +FirmwareInfo = namedtuple('FirmwareInfo', + ['kind', 'name', 'version', 'extras']) """Reprogrammable keys information.""" -ReprogrammableKeyInfo = namedtuple('ReprogrammableKeyInfo', [ - 'index', - 'key', - 'task', - 'flags']) +ReprogrammableKeyInfo = namedtuple('ReprogrammableKeyInfo', + ['index', 'key', 'task', 'flags']) ReprogrammableKeyInfoV4 = namedtuple('ReprogrammableKeyInfoV4', [ - 'index', - 'key', - 'task', - 'flags', - 'pos', - 'group', - 'group_mask', - 'remapped']) + 'index', 'key', 'task', 'flags', 'pos', 'group', 'group_mask', 'remapped' +]) del namedtuple diff --git a/lib/logitech_receiver/descriptors.py b/lib/logitech_receiver/descriptors.py index af479ab4..41db0051 100644 --- a/lib/logitech_receiver/descriptors.py +++ b/lib/logitech_receiver/descriptors.py @@ -19,7 +19,6 @@ from __future__ import absolute_import, division, print_function, unicode_literals - from .common import NamedInts as _NamedInts from .hidpp10 import REGISTERS as _R, DEVICE_KIND as _DK from .settings_templates import RegisterSettings as _RS, FeatureSettings as _FS @@ -30,67 +29,88 @@ from .settings_templates import RegisterSettings as _RS, FeatureSettings as _FS from collections import namedtuple _DeviceDescriptor = namedtuple('_DeviceDescriptor', - ('name', 'kind', 'wpid', 'codename', 'protocol', 'registers', 'settings', 'persister')) + ('name', 'kind', 'wpid', 'codename', 'protocol', + 'registers', 'settings', 'persister')) del namedtuple DEVICES = {} -def _D(name, codename=None, kind=None, wpid=None, protocol=None, registers=None, settings=None, persister=None): - assert name - 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) - assert kind is not None, 'descriptor for %s does not have kind set' % name +def _D(name, + codename=None, + kind=None, + wpid=None, + protocol=None, + registers=None, + settings=None, + persister=None): + assert name - # heuristic: the codename is the last word in the device name - if codename is None and ' ' in name: - codename = name.split(' ')[-1] - assert codename is not None, 'descriptor for %s does not have codename set' % name + 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) + assert kind is not None, 'descriptor for %s does not have kind set' % name - if protocol is not None: - # ? 2.0 devices should not have any registers - _kind = lambda s : s._rw.kind if hasattr(s, '_rw') else s._rw_kind - if protocol < 2.0: - assert settings is None or all(_kind(s) == 1 for s in settings) - else: - assert registers is None - assert settings is None or all(_kind(s) == 2 for s in settings) + # heuristic: the codename is the last word in the device name + if codename is None and ' ' in name: + codename = name.split(' ')[-1] + assert codename is not None, 'descriptor for %s does not have codename set' % name - if 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) - 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 protocol is not None: + # ? 2.0 devices should not have any registers + _kind = lambda s: s._rw.kind if hasattr(s, '_rw') else s._rw_kind + if protocol < 2.0: + assert settings is None or all(_kind(s) == 1 for s in settings) + else: + assert registers is None + assert settings is None or all(_kind(s) == 2 for s in settings) - device_descriptor = _DeviceDescriptor(name=name, kind=kind, - wpid=wpid, codename=codename, protocol=protocol, - registers=registers, settings=settings, persister=persister) + if 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) + 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) - assert codename not in DEVICES, 'duplicate codename in device descriptors: %s' % (DEVICES[codename], ) - DEVICES[codename] = device_descriptor + device_descriptor = _DeviceDescriptor(name=name, + kind=kind, + wpid=wpid, + codename=codename, + protocol=protocol, + registers=registers, + settings=settings, + persister=persister) - if wpid: - if not isinstance(wpid, tuple): - wpid = (wpid, ) + assert codename not in DEVICES, 'duplicate codename in device descriptors: %s' % ( + DEVICES[codename], ) + DEVICES[codename] = device_descriptor + + if wpid: + if not isinstance(wpid, tuple): + wpid = (wpid, ) + + for w in wpid: + assert w not in DEVICES, 'duplicate wpid in device descriptors: %s' % ( + DEVICES[w], ) + DEVICES[w] = device_descriptor - for w in wpid: - assert w not in DEVICES, 'duplicate wpid in device descriptors: %s' % (DEVICES[w], ) - DEVICES[w] = device_descriptor # # # -_PERFORMANCE_MX_DPIS = _NamedInts.range(0x81, 0x8F, lambda x: str((x - 0x80) * 100)) +_PERFORMANCE_MX_DPIS = _NamedInts.range(0x81, 0x8F, lambda x: str( + (x - 0x80) * 100)) # # @@ -145,242 +165,378 @@ _PERFORMANCE_MX_DPIS = _NamedInts.range(0x81, 0x8F, lambda x: str((x - 0x80) * 1 _D('Wireless Keyboard K230', protocol=2.0, wpid='400D') _D('Wireless Keyboard K270(unifying)', protocol=2.0, wpid='4003') -_D('Wireless Keyboard MK270', protocol=2.0, wpid='4023', - settings=[ - _FS.fn_swap() - ], - ) -_D('Wireless Keyboard K270', protocol=1.0, - registers=(_R.battery_status, ), - ) -_D('Wireless Keyboard MK300', protocol=1.0, wpid='8521', - registers=(_R.battery_status, ), - ) +_D( + 'Wireless Keyboard MK270', + protocol=2.0, + wpid='4023', + settings=[_FS.fn_swap()], +) +_D( + 'Wireless Keyboard K270', + protocol=1.0, + registers=(_R.battery_status, ), +) +_D( + 'Wireless Keyboard MK300', + protocol=1.0, + wpid='8521', + registers=(_R.battery_status, ), +) -_D('Wireless Keyboard MK320', protocol=1.0, wpid='200F', - registers=(_R.battery_status, ), - ) +_D( + 'Wireless Keyboard MK320', + protocol=1.0, + wpid='200F', + registers=(_R.battery_status, ), +) _D('Wireless Keyboard MK330') -_D('Wireless Compact Keyboard K340', protocol=1.0, wpid='2007', - registers=(_R.battery_status, ), - ) -_D('Wireless Wave Keyboard K350', protocol=1.0, wpid='200A', - registers=(_R.battery_status, ), - ) -_D('Wireless Keyboard K360', protocol=2.0, wpid='4004', - settings=[ - _FS.fn_swap() - ], - ) -_D('Wireless Keyboard K375s', protocol=2.0, wpid='4061', - settings=[ - _FS.k375s_fn_swap() - ], - ) -_D('Wireless Touch Keyboard K400', protocol=2.0, wpid=('400E', '4024'), - settings=[ - _FS.fn_swap() - ], - ) -_D('Wireless Touch Keyboard K400 Plus', codename='K400 Plus', protocol=2.0, wpid='404D', - settings=[ - _FS.new_fn_swap(), - _FS.reprogrammable_keys(), - _FS.disable_keyboard_keys(), - ], - ) -_D('Wireless Keyboard K520', protocol=1.0, wpid='2011', - registers=(_R.battery_status, ), - settings=[ - _RS.fn_swap(), - ], - ) -_D('Number Pad N545', protocol=1.0, wpid='2006', - registers=(_R.battery_status, ), - ) +_D( + 'Wireless Compact Keyboard K340', + protocol=1.0, + wpid='2007', + registers=(_R.battery_status, ), +) +_D( + 'Wireless Wave Keyboard K350', + protocol=1.0, + wpid='200A', + registers=(_R.battery_status, ), +) +_D( + 'Wireless Keyboard K360', + protocol=2.0, + wpid='4004', + settings=[_FS.fn_swap()], +) +_D( + 'Wireless Keyboard K375s', + protocol=2.0, + wpid='4061', + settings=[_FS.k375s_fn_swap()], +) +_D( + 'Wireless Touch Keyboard K400', + protocol=2.0, + wpid=('400E', '4024'), + settings=[_FS.fn_swap()], +) +_D( + 'Wireless Touch Keyboard K400 Plus', + codename='K400 Plus', + protocol=2.0, + wpid='404D', + settings=[ + _FS.new_fn_swap(), + _FS.reprogrammable_keys(), + _FS.disable_keyboard_keys(), + ], +) +_D( + 'Wireless Keyboard K520', + protocol=1.0, + wpid='2011', + registers=(_R.battery_status, ), + settings=[ + _RS.fn_swap(), + ], +) +_D( + 'Number Pad N545', + protocol=1.0, + wpid='2006', + registers=(_R.battery_status, ), +) _D('Wireless Keyboard MK550') -_D('Wireless Keyboard MK700', protocol=1.0, wpid='2008', - registers=(_R.battery_status, ), - settings=[ - _RS.fn_swap(), - ], - ) -_D('Wireless Solar Keyboard K750', protocol=2.0, wpid='4002', - settings=[ - _FS.fn_swap() - ], - ) -_D('Wireless Multi-Device Keyboard K780', protocol=4.5, wpid='405B', - settings=[ - _FS.new_fn_swap() - ], - ) -_D('Wireless Illuminated Keyboard K800', protocol=1.0, wpid='2010', - registers=(_R.battery_status, _R.three_leds, ), - settings=[ - _RS.fn_swap(), - _RS.hand_detection(), - ], - ) -_D('Wireless Illuminated Keyboard K800 new', codename='K800 new', protocol=4.5, wpid='406E', - settings=[ - _FS.fn_swap() - ], - ) -_D('Illuminated Living-Room Keyboard K830', protocol=2.0, wpid='4032', - settings=[ - _FS.new_fn_swap() - ], - ) +_D( + 'Wireless Keyboard MK700', + protocol=1.0, + wpid='2008', + registers=(_R.battery_status, ), + settings=[ + _RS.fn_swap(), + ], +) +_D( + 'Wireless Solar Keyboard K750', + protocol=2.0, + wpid='4002', + settings=[_FS.fn_swap()], +) +_D( + 'Wireless Multi-Device Keyboard K780', + protocol=4.5, + wpid='405B', + settings=[_FS.new_fn_swap()], +) +_D( + 'Wireless Illuminated Keyboard K800', + protocol=1.0, + wpid='2010', + registers=( + _R.battery_status, + _R.three_leds, + ), + settings=[ + _RS.fn_swap(), + _RS.hand_detection(), + ], +) +_D( + 'Wireless Illuminated Keyboard K800 new', + codename='K800 new', + protocol=4.5, + wpid='406E', + settings=[_FS.fn_swap()], +) +_D( + 'Illuminated Living-Room Keyboard K830', + protocol=2.0, + wpid='4032', + settings=[_FS.new_fn_swap()], +) _D('Craft Advanced Keyboard', codename='Craft', protocol=4.5, wpid='4066') -_D('Wireless Keyboard S510', codename='S510', protocol=1.0, wpid='3622', - registers=(_R.battery_status, ), - ) +_D( + 'Wireless Keyboard S510', + codename='S510', + protocol=1.0, + wpid='3622', + registers=(_R.battery_status, ), +) # Mice _D('Wireless Mouse M150', protocol=2.0, wpid='4022') _D('Wireless Mouse M175', protocol=2.0, wpid='4008') -_D('Wireless Mouse M185 new', codename='M185n', protocol=4.5, wpid='4054', - settings=[ - _FS.lowres_smooth_scroll(), - _FS.pointer_speed(), - ]) +_D('Wireless Mouse M185 new', + codename='M185n', + protocol=4.5, + wpid='4054', + settings=[ + _FS.lowres_smooth_scroll(), + _FS.pointer_speed(), + ]) # Apparently Logitech uses wpid 4055 for three different mice # That's not so strange, as M185 is used on both Unifying-ready and non-Unifying-ready mice -_D('Wireless Mouse M185/M235/M310', codename='M185/M235/M310', protocol=4.5, wpid='4055', - settings=[ - _FS.lowres_smooth_scroll(), - _FS.pointer_speed(), - ]) +_D('Wireless Mouse M185/M235/M310', + codename='M185/M235/M310', + protocol=4.5, + wpid='4055', + settings=[ + _FS.lowres_smooth_scroll(), + _FS.pointer_speed(), + ]) _D('Wireless Mouse M185', protocol=2.0, wpid='4038') _D('Wireless Mouse M187', protocol=2.0, wpid='4019') _D('Wireless Mouse M215', protocol=1.0, wpid='1020') -_D('Wireless Mouse M305', protocol=1.0, wpid='101F', - registers=(_R.battery_status, ), - settings=[ - _RS.side_scroll(), - ], - ) -_D('Wireless Mouse M310', protocol=1.0, wpid='1024', - registers=(_R.battery_status, ), - ) +_D( + 'Wireless Mouse M305', + protocol=1.0, + wpid='101F', + registers=(_R.battery_status, ), + settings=[ + _RS.side_scroll(), + ], +) +_D( + 'Wireless Mouse M310', + protocol=1.0, + wpid='1024', + registers=(_R.battery_status, ), +) _D('Wireless Mouse M315') _D('Wireless Mouse M317') -_D('Wireless Mouse M325', protocol=2.0, wpid='400A', - settings=[ - _FS.hi_res_scroll(), - ]) +_D('Wireless Mouse M325', + protocol=2.0, + wpid='400A', + settings=[ + _FS.hi_res_scroll(), + ]) _D('Wireless Mouse M345', protocol=2.0, wpid='4017') -_D('Wireless Mouse M350', protocol=1.0, wpid='101C', - registers=(_R.battery_charge, ), - ) +_D( + 'Wireless Mouse M350', + protocol=1.0, + wpid='101C', + registers=(_R.battery_charge, ), +) _D('Wireless Mouse Pebble M350', codename='Pebble', protocol=2.0, wpid='4080') -_D('Wireless Mouse M505', codename='M505/B605', protocol=1.0, wpid='101D', - registers=(_R.battery_charge, ), - settings=[ - _RS.smooth_scroll(), - _RS.side_scroll(), - ], - ) -_D('Wireless Mouse M510', protocol=1.0, wpid='1025', - registers=(_R.battery_status, ), - settings=[ - _RS.smooth_scroll(), - _RS.side_scroll(), - ], - ) -_D('Wireless Mouse M510', codename='M510v2', protocol=2.0, wpid='4051', - settings=[ - _FS.lowres_smooth_scroll(), - ]) +_D( + 'Wireless Mouse M505', + codename='M505/B605', + protocol=1.0, + wpid='101D', + registers=(_R.battery_charge, ), + settings=[ + _RS.smooth_scroll(), + _RS.side_scroll(), + ], +) +_D( + 'Wireless Mouse M510', + protocol=1.0, + wpid='1025', + registers=(_R.battery_status, ), + settings=[ + _RS.smooth_scroll(), + _RS.side_scroll(), + ], +) +_D('Wireless Mouse M510', + codename='M510v2', + protocol=2.0, + wpid='4051', + settings=[ + _FS.lowres_smooth_scroll(), + ]) _D('Couch Mouse M515', protocol=2.0, wpid='4007') _D('Wireless Mouse M525', protocol=2.0, wpid='4013') -_D('Multi Device Silent Mouse M585/M590', codename='M585/M590', protocol=4.5, wpid='406B', - settings=[ - _FS.lowres_smooth_scroll(), - _FS.pointer_speed(), - ], - ) +_D( + 'Multi Device Silent Mouse M585/M590', + codename='M585/M590', + protocol=4.5, + wpid='406B', + settings=[ + _FS.lowres_smooth_scroll(), + _FS.pointer_speed(), + ], +) _D('Touch Mouse M600', protocol=2.0, wpid='401A') -_D('Marathon Mouse M705 (M-R0009)', codename='M705 (M-R0009)', protocol=1.0, wpid='101B', - registers=(_R.battery_charge, ), - settings=[ - _RS.smooth_scroll(), - _RS.side_scroll(), - ], - ) -_D('Marathon Mouse M705 (M-R0073)', codename='M705 (M-R0073)', protocol=4.5, wpid='406D', - settings=[ - _FS.hires_smooth_invert(), - _FS.hires_smooth_resolution(), - _FS.pointer_speed(), - ]) +_D( + 'Marathon Mouse M705 (M-R0009)', + codename='M705 (M-R0009)', + protocol=1.0, + wpid='101B', + registers=(_R.battery_charge, ), + settings=[ + _RS.smooth_scroll(), + _RS.side_scroll(), + ], +) +_D('Marathon Mouse M705 (M-R0073)', + codename='M705 (M-R0073)', + protocol=4.5, + wpid='406D', + settings=[ + _FS.hires_smooth_invert(), + _FS.hires_smooth_resolution(), + _FS.pointer_speed(), + ]) _D('Zone Touch Mouse T400') _D('Touch Mouse T620', protocol=2.0) _D('Logitech Cube', kind=_DK.mouse, protocol=2.0) -_D('Anywhere Mouse MX', codename='Anywhere MX', protocol=1.0, wpid='1017', - registers=(_R.battery_charge, ), - settings=[ - _RS.smooth_scroll(), - _RS.side_scroll(), - ], - ) -_D('Anywhere Mouse MX 2', codename='Anywhere MX 2', protocol=4.5, wpid='404A', - settings=[ - _FS.hires_smooth_invert(), - _FS.hires_smooth_resolution(), - ], - ) -_D('Performance Mouse MX', codename='Performance MX', protocol=1.0, wpid='101A', - registers=(_R.battery_status, _R.three_leds, ), - settings=[ - _RS.dpi(choices=_PERFORMANCE_MX_DPIS), - _RS.smooth_scroll(), - _RS.side_scroll(), - ], - ) +_D( + 'Anywhere Mouse MX', + codename='Anywhere MX', + protocol=1.0, + wpid='1017', + registers=(_R.battery_charge, ), + settings=[ + _RS.smooth_scroll(), + _RS.side_scroll(), + ], +) +_D( + 'Anywhere Mouse MX 2', + codename='Anywhere MX 2', + protocol=4.5, + wpid='404A', + settings=[ + _FS.hires_smooth_invert(), + _FS.hires_smooth_resolution(), + ], +) +_D( + 'Performance Mouse MX', + codename='Performance MX', + protocol=1.0, + wpid='101A', + registers=( + _R.battery_status, + _R.three_leds, + ), + settings=[ + _RS.dpi(choices=_PERFORMANCE_MX_DPIS), + _RS.smooth_scroll(), + _RS.side_scroll(), + ], +) -_D('Wireless Mouse MX Master', codename='MX Master', protocol=4.5, wpid='4041', - settings=[ - _FS.hires_smooth_invert(), - _FS.hires_smooth_resolution(), - ], - ) +_D( + 'Wireless Mouse MX Master', + codename='MX Master', + protocol=4.5, + wpid='4041', + settings=[ + _FS.hires_smooth_invert(), + _FS.hires_smooth_resolution(), + ], +) -_D('Wireless Mouse MX Master 2S', codename='MX Master 2S', protocol=4.5,wpid='4069', - settings=[ - _FS.hires_smooth_invert(), - _FS.hires_smooth_resolution(), - ], - ) +_D( + 'Wireless Mouse MX Master 2S', + codename='MX Master 2S', + protocol=4.5, + wpid='4069', + settings=[ + _FS.hires_smooth_invert(), + _FS.hires_smooth_resolution(), + ], +) -_D('Wireless Mouse MX Vertical', codename='MX Vertical', protocol=4.5, wpid='407B') +_D('Wireless Mouse MX Vertical', + codename='MX Vertical', + protocol=4.5, + wpid='407B') -_D('G7 Cordless Laser Mouse', codename='G7', protocol=1.0, wpid='1002', - registers=(_R.battery_status, ), - ) -_D('G700 Gaming Mouse', codename='G700', protocol=1.0, wpid='1023', - registers=(_R.battery_status, _R.three_leds, ), - settings=[ - _RS.smooth_scroll(), - _RS.side_scroll(), - ], - ) -_D('G700s Gaming Mouse', codename='G700s', protocol=1.0, wpid='102A', - registers=(_R.battery_status, _R.three_leds, ), - settings=[ - _RS.smooth_scroll(), - _RS.side_scroll(), - ], - ) -_D('LX5 Cordless Mouse', codename='LX5', protocol=1.0, wpid='5612', - registers=(_R.battery_status, ), - ) -_D('Wireless Mouse M30', codename='M30', protocol=1.0, wpid='6822', - registers=(_R.battery_status, ), - ) +_D( + 'G7 Cordless Laser Mouse', + codename='G7', + protocol=1.0, + wpid='1002', + registers=(_R.battery_status, ), +) +_D( + 'G700 Gaming Mouse', + codename='G700', + protocol=1.0, + wpid='1023', + registers=( + _R.battery_status, + _R.three_leds, + ), + settings=[ + _RS.smooth_scroll(), + _RS.side_scroll(), + ], +) +_D( + 'G700s Gaming Mouse', + codename='G700s', + protocol=1.0, + wpid='102A', + registers=( + _R.battery_status, + _R.three_leds, + ), + settings=[ + _RS.smooth_scroll(), + _RS.side_scroll(), + ], +) +_D( + 'LX5 Cordless Mouse', + codename='LX5', + protocol=1.0, + wpid='5612', + registers=(_R.battery_status, ), +) +_D( + 'Wireless Mouse M30', + codename='M30', + protocol=1.0, + wpid='6822', + registers=(_R.battery_status, ), +) # Trackballs @@ -396,57 +552,109 @@ _D('Wireless Touchpad', codename='Wireless Touch', protocol=2.0, wpid='4011') # A wpid is necessary to properly identify them. # -_D('VX Nano Cordless Laser Mouse', codename='VX Nano', protocol=1.0, wpid=('100B', '100F'), - registers=(_R.battery_charge, ), - settings=[ - _RS.smooth_scroll(), - _RS.side_scroll(), - ], - ) -_D('V450 Nano Cordless Laser Mouse', codename='V450 Nano', protocol=1.0, wpid='1011', - registers=(_R.battery_charge, ), - ) -_D('V550 Nano Cordless Laser Mouse', codename='V550 Nano', protocol=1.0, wpid='1013', - registers=(_R.battery_charge, ), - settings=[ - _RS.smooth_scroll(), - _RS.side_scroll(), - ], - ) +_D( + 'VX Nano Cordless Laser Mouse', + codename='VX Nano', + protocol=1.0, + wpid=('100B', '100F'), + registers=(_R.battery_charge, ), + settings=[ + _RS.smooth_scroll(), + _RS.side_scroll(), + ], +) +_D( + 'V450 Nano Cordless Laser Mouse', + codename='V450 Nano', + protocol=1.0, + wpid='1011', + registers=(_R.battery_charge, ), +) +_D( + 'V550 Nano Cordless Laser Mouse', + codename='V550 Nano', + protocol=1.0, + wpid='1013', + registers=(_R.battery_charge, ), + settings=[ + _RS.smooth_scroll(), + _RS.side_scroll(), + ], +) # Mini receiver mice -_D('MX610 Laser Cordless Mouse', codename='MX610', protocol=1.0, wpid='1001', - registers=(_R.battery_status, ), - ) -_D('MX620 Laser Cordless Mouse', codename='MX620', protocol=1.0, wpid=('100A', '1016'), - registers=(_R.battery_charge, ), - ) -_D('MX610 Left-Handled Mouse', codename='MX610L', protocol=1.0, wpid='1004', - registers=(_R.battery_status, ), - ) -_D('V400 Laser Cordless Mouse', codename='V400', protocol=1.0, wpid='1003', - registers=(_R.battery_status, ), - ) -_D('V450 Laser Cordless Mouse', codename='V450', protocol=1.0, wpid='1005', - registers=(_R.battery_status, ), - ) -_D('VX Revolution', codename='VX Revolution', kind=_DK.mouse, protocol=1.0, wpid=('1006', '100D'), - registers=(_R.battery_charge, ), - ) -_D('MX Air', codename='MX Air', protocol=1.0, kind=_DK.mouse, wpid=('1007', '100E'), - registers=(_R.battery_charge, ), - ) -_D('MX Revolution', codename='MX Revolution', protocol=1.0, kind=_DK.mouse, wpid=('1008', '100C'), - registers=(_R.battery_charge, ), - ) -_D('MX 1100 Cordless Laser Mouse', codename='MX 1100', protocol=1.0, kind=_DK.mouse, wpid='1014', - registers=(_R.battery_charge, ), - settings=[ - _RS.smooth_scroll(), - _RS.side_scroll(), - ], - ) +_D( + 'MX610 Laser Cordless Mouse', + codename='MX610', + protocol=1.0, + wpid='1001', + registers=(_R.battery_status, ), +) +_D( + 'MX620 Laser Cordless Mouse', + codename='MX620', + protocol=1.0, + wpid=('100A', '1016'), + registers=(_R.battery_charge, ), +) +_D( + 'MX610 Left-Handled Mouse', + codename='MX610L', + protocol=1.0, + wpid='1004', + registers=(_R.battery_status, ), +) +_D( + 'V400 Laser Cordless Mouse', + codename='V400', + protocol=1.0, + wpid='1003', + registers=(_R.battery_status, ), +) +_D( + 'V450 Laser Cordless Mouse', + codename='V450', + protocol=1.0, + wpid='1005', + registers=(_R.battery_status, ), +) +_D( + 'VX Revolution', + codename='VX Revolution', + kind=_DK.mouse, + protocol=1.0, + wpid=('1006', '100D'), + registers=(_R.battery_charge, ), +) +_D( + 'MX Air', + codename='MX Air', + protocol=1.0, + kind=_DK.mouse, + wpid=('1007', '100E'), + registers=(_R.battery_charge, ), +) +_D( + 'MX Revolution', + codename='MX Revolution', + protocol=1.0, + kind=_DK.mouse, + wpid=('1008', '100C'), + registers=(_R.battery_charge, ), +) +_D( + 'MX 1100 Cordless Laser Mouse', + codename='MX 1100', + protocol=1.0, + kind=_DK.mouse, + wpid='1014', + registers=(_R.battery_charge, ), + settings=[ + _RS.smooth_scroll(), + _RS.side_scroll(), + ], +) # Some exotics... diff --git a/lib/logitech_receiver/hidpp10.py b/lib/logitech_receiver/hidpp10.py index 99c2af75..4e8b62dd 100644 --- a/lib/logitech_receiver/hidpp10.py +++ b/lib/logitech_receiver/hidpp10.py @@ -23,12 +23,9 @@ from logging import getLogger # , DEBUG as _DEBUG _log = getLogger(__name__) del getLogger - -from .common import (strhex as _strhex, - bytes2int as _bytes2int, - int2bytes as _int2bytes, - NamedInts as _NamedInts, - FirmwareInfo as _FirmwareInfo) +from .common import (strhex as _strhex, bytes2int as _bytes2int, int2bytes as + _int2bytes, NamedInts as _NamedInts, FirmwareInfo as + _FirmwareInfo) from .hidpp20 import FIRMWARE_KIND, BATTERY_STATUS # @@ -36,26 +33,24 @@ from .hidpp20 import FIRMWARE_KIND, BATTERY_STATUS # documentation, some of them guessed. # -DEVICE_KIND = _NamedInts( - keyboard=0x01, - mouse=0x02, - numpad=0x03, - presenter=0x04, - trackball=0x08, - touchpad=0x09) +DEVICE_KIND = _NamedInts(keyboard=0x01, + mouse=0x02, + numpad=0x03, + presenter=0x04, + trackball=0x08, + touchpad=0x09) -POWER_SWITCH_LOCATION = _NamedInts( - base=0x01, - top_case=0x02, - edge_of_top_right_corner=0x03, - top_left_corner=0x05, - bottom_left_corner=0x06, - top_right_corner=0x07, - bottom_right_corner=0x08, - top_edge=0x09, - right_edge=0x0A, - left_edge=0x0B, - bottom_edge=0x0C) +POWER_SWITCH_LOCATION = _NamedInts(base=0x01, + top_case=0x02, + edge_of_top_right_corner=0x03, + top_left_corner=0x05, + bottom_left_corner=0x06, + top_right_corner=0x07, + bottom_right_corner=0x08, + top_edge=0x09, + right_edge=0x0A, + left_edge=0x0B, + bottom_edge=0x0C) # Some flags are used both by devices and receivers. The Logitech documentation # mentions that the first and last (third) byte are used for devices while the @@ -70,261 +65,259 @@ POWER_SWITCH_LOCATION = _NamedInts( # In the future would be useful to have separate enums for receiver and device notification flags, # but right now we don't know enough. NOTIFICATION_FLAG = _NamedInts( - battery_status= 0x100000, # send battery charge notifications (0x07 or 0x0D) - keyboard_sleep_raw= 0x020000, # system control keys such as Sleep - keyboard_multimedia_raw=0x010000, # consumer controls such as Mute and Calculator - # reserved_r1b4= 0x001000, # unknown, seen on a unifying receiver - software_present= 0x000800, # .. no idea - keyboard_illumination= 0x000200, # illumination brightness level changes (by pressing keys) - wireless= 0x000100, # notify when the device wireless goes on/off-line - ) + battery_status=0x100000, # send battery charge notifications (0x07 or 0x0D) + keyboard_sleep_raw=0x020000, # system control keys such as Sleep + keyboard_multimedia_raw= + 0x010000, # consumer controls such as Mute and Calculator + # reserved_r1b4= 0x001000, # unknown, seen on a unifying receiver + software_present=0x000800, # .. no idea + keyboard_illumination= + 0x000200, # illumination brightness level changes (by pressing keys) + wireless=0x000100, # notify when the device wireless goes on/off-line +) -ERROR = _NamedInts( - invalid_SubID__command=0x01, - invalid_address=0x02, - invalid_value=0x03, - connection_request_failed=0x04, - too_many_devices=0x05, - already_exists=0x06, - busy=0x07, - unknown_device=0x08, - resource_error=0x09, - request_unavailable=0x0A, - unsupported_parameter_value=0x0B, - wrong_pin_code=0x0C) +ERROR = _NamedInts(invalid_SubID__command=0x01, + invalid_address=0x02, + invalid_value=0x03, + connection_request_failed=0x04, + too_many_devices=0x05, + already_exists=0x06, + busy=0x07, + unknown_device=0x08, + resource_error=0x09, + request_unavailable=0x0A, + unsupported_parameter_value=0x0B, + wrong_pin_code=0x0C) -PAIRING_ERRORS = _NamedInts( - device_timeout=0x01, - device_not_supported=0x02, - too_many_devices=0x03, - sequence_timeout=0x06) - -BATTERY_APPOX = _NamedInts( - empty = 0, - critical = 5, - low = 20, - good = 50, - full = 90) +PAIRING_ERRORS = _NamedInts(device_timeout=0x01, + device_not_supported=0x02, + too_many_devices=0x03, + sequence_timeout=0x06) +BATTERY_APPOX = _NamedInts(empty=0, critical=5, low=20, good=50, full=90) """Known registers. Devices usually have a (small) sub-set of these. Some registers are only applicable to certain device kinds (e.g. smooth_scroll only applies to mice.""" REGISTERS = _NamedInts( - # only apply to receivers - receiver_connection=0x02, - receiver_pairing=0xB2, - devices_activity=0x2B3, - receiver_info=0x2B5, + # only apply to receivers + receiver_connection=0x02, + receiver_pairing=0xB2, + devices_activity=0x2B3, + receiver_info=0x2B5, - # only apply to devices - mouse_button_flags=0x01, - keyboard_hand_detection=0x01, - battery_status=0x07, - keyboard_fn_swap=0x09, - battery_charge=0x0D, - keyboard_illumination=0x17, - three_leds=0x51, - mouse_dpi=0x63, + # only apply to devices + mouse_button_flags=0x01, + keyboard_hand_detection=0x01, + battery_status=0x07, + keyboard_fn_swap=0x09, + battery_charge=0x0D, + keyboard_illumination=0x17, + three_leds=0x51, + mouse_dpi=0x63, - # apply to both - notifications=0x00, - firmware=0xF1, - ) + # apply to both + notifications=0x00, + firmware=0xF1, +) # # functions # + def read_register(device, register_number, *params): - assert device, '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) + assert device, '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, '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) + assert device, '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) def get_battery(device): - assert device - assert device.kind is not None - if not device.online: - return + assert device + assert device.kind is not None + if not device.online: + return + """Reads a device's battery level, if provided by the HID++ 1.0 protocol.""" + if device.protocol and device.protocol >= 2.0: + # let's just assume HID++ 2.0 devices do not provide the battery info in a register + return - """Reads a device's battery level, if provided by the HID++ 1.0 protocol.""" - if device.protocol and device.protocol >= 2.0: - # let's just assume HID++ 2.0 devices do not provide the battery info in a register - return + for r in (REGISTERS.battery_status, REGISTERS.battery_charge): + if r in device.registers: + reply = read_register(device, r) + if reply: + return parse_battery_status(r, reply) + return - for r in (REGISTERS.battery_status, REGISTERS.battery_charge): - if r in device.registers: - reply = read_register(device, r) - if reply: - return parse_battery_status(r, reply) - return + # the descriptor does not tell us which register this device has, try them both + reply = read_register(device, REGISTERS.battery_charge) + if reply: + # remember this for the next time + device.registers.append(REGISTERS.battery_charge) + return parse_battery_status(REGISTERS.battery_charge, reply) - # the descriptor does not tell us which register this device has, try them both - reply = read_register(device, REGISTERS.battery_charge) - if reply: - # remember this for the next time - device.registers.append(REGISTERS.battery_charge) - return parse_battery_status(REGISTERS.battery_charge, reply) - - reply = read_register(device, REGISTERS.battery_status) - if reply: - # remember this for the next time - device.registers.append(REGISTERS.battery_status) - return parse_battery_status(REGISTERS.battery_status, reply) + reply = read_register(device, REGISTERS.battery_status) + if reply: + # remember this for the next time + device.registers.append(REGISTERS.battery_status) + return parse_battery_status(REGISTERS.battery_status, reply) def parse_battery_status(register, reply): - if register == REGISTERS.battery_charge: - 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) - return charge, status_text, None + if register == REGISTERS.battery_charge: + 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) + return charge, status_text, None - if register == REGISTERS.battery_status: - status_byte = ord(reply[:1]) - charge = (BATTERY_APPOX.full if status_byte == 7 # full - else BATTERY_APPOX.good if status_byte == 5 # good - else BATTERY_APPOX.low if status_byte == 3 # low - else BATTERY_APPOX.critical if status_byte == 1 # critical - # pure 'charging' notifications may come without a status - else BATTERY_APPOX.empty) + if register == REGISTERS.battery_status: + status_byte = ord(reply[:1]) + charge = ( + BATTERY_APPOX.full if status_byte == 7 # full + else BATTERY_APPOX.good if status_byte == 5 # good + else BATTERY_APPOX.low if status_byte == 3 # low + else BATTERY_APPOX.critical if status_byte == 1 # critical + # pure 'charging' notifications may come without a status + else BATTERY_APPOX.empty) - charging_byte = ord(reply[1:2]) - if charging_byte == 0x00: - status_text = BATTERY_STATUS.discharging - elif charging_byte & 0x21 == 0x21: - status_text = BATTERY_STATUS.recharging - elif charging_byte & 0x22 == 0x22: - status_text = BATTERY_STATUS.full - else: - _log.warn("could not parse 0x07 battery status: %02X (level %02X)", charging_byte, status_byte) - status_text = None + charging_byte = ord(reply[1:2]) + if charging_byte == 0x00: + status_text = BATTERY_STATUS.discharging + elif charging_byte & 0x21 == 0x21: + status_text = BATTERY_STATUS.recharging + elif charging_byte & 0x22 == 0x22: + status_text = BATTERY_STATUS.full + else: + _log.warn("could not parse 0x07 battery status: %02X (level %02X)", + charging_byte, status_byte) + status_text = None - if charging_byte & 0x03 and status_byte == 0: - # some 'charging' notifications may come with no battery level information - charge = None + if charging_byte & 0x03 and status_byte == 0: + # some 'charging' notifications may come with no battery level information + charge = None - # Return None for next charge level as this is not in HID++ 1.0 spec - return charge, status_text, None + # Return None for next charge level as this is not in HID++ 1.0 spec + return charge, status_text, None def get_firmware(device): - assert device + assert device - firmware = [None, None, None] + firmware = [None, None, None] - reply = read_register(device, REGISTERS.firmware, 0x01) - if not reply: - # won't be able to read any of it now... - return + reply = read_register(device, REGISTERS.firmware, 0x01) + if not reply: + # won't be able to read any of it now... + return - fw_version = _strhex(reply[1:3]) - 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) - firmware[0] = fw + fw_version = _strhex(reply[1:3]) + 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) + 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) - firmware[1] = bl + 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) + 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) - firmware[2] = o + 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) + firmware[2] = o - if any(firmware): - return tuple(f for f in firmware if f) + if any(firmware): + return tuple(f for f in firmware if f) def set_3leds(device, battery_level=None, charging=None, warning=None): - assert device - assert device.kind is not None - if not device.online: - return + assert device + assert device.kind is not None + if not device.online: + return - if REGISTERS.three_leds not in device.registers: - return + if REGISTERS.three_leds not in device.registers: + return - if battery_level is not None: - if battery_level < BATTERY_APPOX.critical: - # 1 orange, and force blink - v1, v2 = 0x22, 0x00 - warning = True - elif battery_level < BATTERY_APPOX.low: - # 1 orange - v1, v2 = 0x22, 0x00 - elif battery_level < BATTERY_APPOX.good: - # 1 green - v1, v2 = 0x20, 0x00 - elif battery_level < BATTERY_APPOX.full: - # 2 greens - v1, v2 = 0x20, 0x02 - else: - # all 3 green - v1, v2 = 0x20, 0x22 - if warning: - # set the blinking flag for the leds already set - v1 |= (v1 >> 1) - v2 |= (v2 >> 1) - elif charging: - # blink all green - v1, v2 = 0x30,0x33 - elif warning: - # 1 red - v1, v2 = 0x02, 0x00 - else: - # turn off all leds - v1, v2 = 0x11, 0x11 + if battery_level is not None: + if battery_level < BATTERY_APPOX.critical: + # 1 orange, and force blink + v1, v2 = 0x22, 0x00 + warning = True + elif battery_level < BATTERY_APPOX.low: + # 1 orange + v1, v2 = 0x22, 0x00 + elif battery_level < BATTERY_APPOX.good: + # 1 green + v1, v2 = 0x20, 0x00 + elif battery_level < BATTERY_APPOX.full: + # 2 greens + v1, v2 = 0x20, 0x02 + else: + # all 3 green + v1, v2 = 0x20, 0x22 + if warning: + # set the blinking flag for the leds already set + v1 |= (v1 >> 1) + v2 |= (v2 >> 1) + elif charging: + # blink all green + v1, v2 = 0x30, 0x33 + elif warning: + # 1 red + v1, v2 = 0x02, 0x00 + else: + # turn off all leds + v1, v2 = 0x11, 0x11 - write_register(device, REGISTERS.three_leds, v1, v2) + write_register(device, REGISTERS.three_leds, v1, v2) def get_notification_flags(device): - assert device + assert device - # Avoid a call if the device is not online, - # or the device does not support registers. - if device.kind is not None: - # peripherals with protocol >= 2.0 don't support registers - if device.protocol and device.protocol >= 2.0: - return + # Avoid a call if the device is not online, + # or the device does not support registers. + if device.kind is not None: + # peripherals with protocol >= 2.0 don't support registers + if device.protocol and device.protocol >= 2.0: + return - flags = read_register(device, REGISTERS.notifications) - if flags is not None: - assert len(flags) == 3 - return _bytes2int(flags) + flags = read_register(device, REGISTERS.notifications) + if flags is not None: + assert len(flags) == 3 + return _bytes2int(flags) def set_notification_flags(device, *flag_bits): - assert device + assert device - # Avoid a call if the device is not online, - # or the device does not support registers. - if device.kind is not None: - # peripherals with protocol >= 2.0 don't support registers - if device.protocol and device.protocol >= 2.0: - return + # Avoid a call if the device is not online, + # or the device does not support registers. + if device.kind is not None: + # peripherals with protocol >= 2.0 don't support registers + if device.protocol and device.protocol >= 2.0: + return - flag_bits = sum(int(b) for b in flag_bits) - assert flag_bits & 0x00FFFFFF == flag_bits - result = write_register(device, REGISTERS.notifications, _int2bytes(flag_bits, 3)) - return result is not None + flag_bits = sum(int(b) for b in flag_bits) + assert flag_bits & 0x00FFFFFF == flag_bits + result = write_register(device, REGISTERS.notifications, + _int2bytes(flag_bits, 3)) + return result is not None diff --git a/lib/logitech_receiver/hidpp20.py b/lib/logitech_receiver/hidpp20.py index 09505e33..d41f4ed1 100644 --- a/lib/logitech_receiver/hidpp20.py +++ b/lib/logitech_receiver/hidpp20.py @@ -25,14 +25,10 @@ from logging import getLogger, DEBUG as _DEBUG _log = getLogger(__name__) del getLogger - -from .common import (FirmwareInfo as _FirmwareInfo, - ReprogrammableKeyInfo as _ReprogrammableKeyInfo, - ReprogrammableKeyInfoV4 as _ReprogrammableKeyInfoV4, - KwException as _KwException, - NamedInts as _NamedInts, - pack as _pack, - unpack as _unpack) +from .common import (FirmwareInfo as _FirmwareInfo, ReprogrammableKeyInfo as + _ReprogrammableKeyInfo, ReprogrammableKeyInfoV4 as + _ReprogrammableKeyInfoV4, KwException as _KwException, + NamedInts as _NamedInts, pack as _pack, unpack as _unpack) from . import special_keys # @@ -47,618 +43,640 @@ A particular device might not support all these features, and may support other unknown features as well. """ FEATURE = _NamedInts( - ROOT=0x0000, - FEATURE_SET=0x0001, - FEATURE_INFO=0x0002, - # Common - DEVICE_FW_VERSION=0x0003, - DEVICE_UNIT_ID=0x0004, - DEVICE_NAME=0x0005, - DEVICE_GROUPS=0x0006, - DEVICE_FRIENDLY_NAME=0x0007, - KEEP_ALIVE=0x0008, - RESET=0x0020, # "Config Change" - CRYPTO_ID=0x0021, - TARGET_SOFTWARE=0x0030, - WIRELESS_SIGNAL_STRENGTH=0x0080, - DFUCONTROL_LEGACY=0x00C0, - DFUCONTROL_UNSIGNED=0x00C1, - DFUCONTROL_SIGNED=0x00C2, - DFU=0x00D0, - BATTERY_STATUS=0x1000, - BATTERY_VOLTAGE=0x1001, - CHARGING_CONTROL=0x1010, - LED_CONTROL=0x1300, - GENERIC_TEST=0x1800, - DEVICE_RESET=0x1802, - OOBSTATE=0x1805, - CONFIG_DEVICE_PROPS=0x1806, - CHANGE_HOST=0x1814, - HOSTS_INFO=0x1815, - BACKLIGHT=0x1981, - BACKLIGHT2=0x1982, - BACKLIGHT3=0x1983, - PRESENTER_CONTROL=0x1A00, - SENSOR_3D=0x1A01, - REPROG_CONTROLS=0x1B00, - REPROG_CONTROLS_V2=0x1B01, - REPROG_CONTROLS_V2_2=0x1B02, # LogiOptions 2.10.73 features.xml - REPROG_CONTROLS_V3=0x1B03, - REPROG_CONTROLS_V4=0x1B04, - REPORT_HID_USAGE=0x1BC0, - PERSISTENT_REMAPPABLE_ACTION=0x1C00, - WIRELESS_DEVICE_STATUS=0x1D4B, - REMAINING_PAIRING=0x1DF0, - FIRMWARE_PROPERTIES=0x1F1F, - ADC_MEASUREMENT=0x1F20, - # Mouse - LEFT_RIGHT_SWAP=0x2001, - SWAP_BUTTON_CANCEL=0x2005, - POINTER_AXIS_ORIENTATION=0x2006, - VERTICAL_SCROLLING=0x2100, - SMART_SHIFT=0x2110, - HI_RES_SCROLLING=0x2120, - HIRES_WHEEL=0x2121, - LOWRES_WHEEL=0x2130, - THUMB_WHEEL=0x2150, - MOUSE_POINTER=0x2200, - ADJUSTABLE_DPI=0x2201, - POINTER_SPEED=0x2205, - ANGLE_SNAPPING=0x2230, - SURFACE_TUNING=0x2240, - HYBRID_TRACKING=0x2400, - # Keyboard - FN_INVERSION=0x40A0, - NEW_FN_INVERSION=0x40A2, - K375S_FN_INVERSION=0x40A3, - ENCRYPTION=0x4100, - LOCK_KEY_STATE=0x4220, - SOLAR_DASHBOARD=0x4301, - KEYBOARD_LAYOUT=0x4520, - KEYBOARD_DISABLE_KEYS=0x4521, - KEYBOARD_DISABLE_BY_USAGE=0x4522, - DUALPLATFORM=0x4530, - MULTIPLATFORM=0x4531, - KEYBOARD_LAYOUT_2=0x4540, - CROWN=0x4600, - # Touchpad - TOUCHPAD_FW_ITEMS=0x6010, - TOUCHPAD_SW_ITEMS=0x6011, - TOUCHPAD_WIN8_FW_ITEMS=0x6012, - TAP_ENABLE=0x6020, - TAP_ENABLE_EXTENDED=0x6021, - CURSOR_BALLISTIC=0x6030, - TOUCHPAD_RESOLUTION=0x6040, - TOUCHPAD_RAW_XY=0x6100, - TOUCHMOUSE_RAW_POINTS=0x6110, - TOUCHMOUSE_6120=0x6120, - GESTURE=0x6500, - GESTURE_2=0x6501, - # Gaming Devices - GKEY=0x8010, - MKEYS=0x8020, - MR=0x8030, - BRIGHTNESS_CONTROL=0x8040, - REPORT_RATE=0x8060, - COLOR_LED_EFFECTS=0x8070, - RGB_EFFECTS=0X8071, - PER_KEY_LIGHTING=0x8080, - PER_KEY_LIGHTING_V2=0x8081, - MODE_STATUS=0x8090, - ONBOARD_PROFILES=0x8100, - MOUSE_BUTTON_SPY=0x8110, - LATENCY_MONITORING=0x8111, - GAMING_ATTACHMENTS=0x8120, - FORCE_FEEDBACK=0x8123, - SIDETONE=0x8300, - EQUALIZER=0x8310, - HEADSET_OUT=0x8320, + ROOT=0x0000, + FEATURE_SET=0x0001, + FEATURE_INFO=0x0002, + # Common + DEVICE_FW_VERSION=0x0003, + DEVICE_UNIT_ID=0x0004, + DEVICE_NAME=0x0005, + DEVICE_GROUPS=0x0006, + DEVICE_FRIENDLY_NAME=0x0007, + KEEP_ALIVE=0x0008, + RESET=0x0020, # "Config Change" + CRYPTO_ID=0x0021, + TARGET_SOFTWARE=0x0030, + WIRELESS_SIGNAL_STRENGTH=0x0080, + DFUCONTROL_LEGACY=0x00C0, + DFUCONTROL_UNSIGNED=0x00C1, + DFUCONTROL_SIGNED=0x00C2, + DFU=0x00D0, + BATTERY_STATUS=0x1000, + BATTERY_VOLTAGE=0x1001, + CHARGING_CONTROL=0x1010, + LED_CONTROL=0x1300, + GENERIC_TEST=0x1800, + DEVICE_RESET=0x1802, + OOBSTATE=0x1805, + CONFIG_DEVICE_PROPS=0x1806, + CHANGE_HOST=0x1814, + HOSTS_INFO=0x1815, + BACKLIGHT=0x1981, + BACKLIGHT2=0x1982, + BACKLIGHT3=0x1983, + PRESENTER_CONTROL=0x1A00, + SENSOR_3D=0x1A01, + REPROG_CONTROLS=0x1B00, + REPROG_CONTROLS_V2=0x1B01, + REPROG_CONTROLS_V2_2=0x1B02, # LogiOptions 2.10.73 features.xml + REPROG_CONTROLS_V3=0x1B03, + REPROG_CONTROLS_V4=0x1B04, + REPORT_HID_USAGE=0x1BC0, + PERSISTENT_REMAPPABLE_ACTION=0x1C00, + WIRELESS_DEVICE_STATUS=0x1D4B, + REMAINING_PAIRING=0x1DF0, + FIRMWARE_PROPERTIES=0x1F1F, + ADC_MEASUREMENT=0x1F20, + # Mouse + LEFT_RIGHT_SWAP=0x2001, + SWAP_BUTTON_CANCEL=0x2005, + POINTER_AXIS_ORIENTATION=0x2006, + VERTICAL_SCROLLING=0x2100, + SMART_SHIFT=0x2110, + HI_RES_SCROLLING=0x2120, + HIRES_WHEEL=0x2121, + LOWRES_WHEEL=0x2130, + THUMB_WHEEL=0x2150, + MOUSE_POINTER=0x2200, + ADJUSTABLE_DPI=0x2201, + POINTER_SPEED=0x2205, + ANGLE_SNAPPING=0x2230, + SURFACE_TUNING=0x2240, + HYBRID_TRACKING=0x2400, + # Keyboard + FN_INVERSION=0x40A0, + NEW_FN_INVERSION=0x40A2, + K375S_FN_INVERSION=0x40A3, + ENCRYPTION=0x4100, + LOCK_KEY_STATE=0x4220, + SOLAR_DASHBOARD=0x4301, + KEYBOARD_LAYOUT=0x4520, + KEYBOARD_DISABLE_KEYS=0x4521, + KEYBOARD_DISABLE_BY_USAGE=0x4522, + DUALPLATFORM=0x4530, + MULTIPLATFORM=0x4531, + KEYBOARD_LAYOUT_2=0x4540, + CROWN=0x4600, + # Touchpad + TOUCHPAD_FW_ITEMS=0x6010, + TOUCHPAD_SW_ITEMS=0x6011, + TOUCHPAD_WIN8_FW_ITEMS=0x6012, + TAP_ENABLE=0x6020, + TAP_ENABLE_EXTENDED=0x6021, + CURSOR_BALLISTIC=0x6030, + TOUCHPAD_RESOLUTION=0x6040, + TOUCHPAD_RAW_XY=0x6100, + TOUCHMOUSE_RAW_POINTS=0x6110, + TOUCHMOUSE_6120=0x6120, + GESTURE=0x6500, + GESTURE_2=0x6501, + # Gaming Devices + GKEY=0x8010, + MKEYS=0x8020, + MR=0x8030, + BRIGHTNESS_CONTROL=0x8040, + REPORT_RATE=0x8060, + COLOR_LED_EFFECTS=0x8070, + RGB_EFFECTS=0X8071, + PER_KEY_LIGHTING=0x8080, + PER_KEY_LIGHTING_V2=0x8081, + MODE_STATUS=0x8090, + ONBOARD_PROFILES=0x8100, + MOUSE_BUTTON_SPY=0x8110, + LATENCY_MONITORING=0x8111, + GAMING_ATTACHMENTS=0x8120, + FORCE_FEEDBACK=0x8123, + SIDETONE=0x8300, + EQUALIZER=0x8310, + HEADSET_OUT=0x8320, ) FEATURE._fallback = lambda x: 'unknown:%04X' % x -FEATURE_FLAG = _NamedInts( - internal=0x20, - hidden=0x40, - obsolete=0x80) +FEATURE_FLAG = _NamedInts(internal=0x20, hidden=0x40, obsolete=0x80) -DEVICE_KIND = _NamedInts( - keyboard=0x00, - remote_control=0x01, - numpad=0x02, - mouse=0x03, - touchpad=0x04, - trackball=0x05, - presenter=0x06, - receiver=0x07) +DEVICE_KIND = _NamedInts(keyboard=0x00, + remote_control=0x01, + numpad=0x02, + mouse=0x03, + touchpad=0x04, + trackball=0x05, + presenter=0x06, + receiver=0x07) -FIRMWARE_KIND = _NamedInts( - Firmware=0x00, - Bootloader=0x01, - Hardware=0x02, - Other=0x03) +FIRMWARE_KIND = _NamedInts(Firmware=0x00, + Bootloader=0x01, + Hardware=0x02, + Other=0x03) -BATTERY_OK = lambda status: status not in (BATTERY_STATUS.invalid_battery, BATTERY_STATUS.thermal_error) +BATTERY_OK = lambda status: status not in (BATTERY_STATUS.invalid_battery, + BATTERY_STATUS.thermal_error) -BATTERY_STATUS = _NamedInts( - discharging=0x00, - recharging=0x01, - almost_full=0x02, - full=0x03, - slow_recharge=0x04, - invalid_battery=0x05, - thermal_error=0x06) +BATTERY_STATUS = _NamedInts(discharging=0x00, + recharging=0x01, + almost_full=0x02, + full=0x03, + slow_recharge=0x04, + invalid_battery=0x05, + thermal_error=0x06) -CHARGE_STATUS = _NamedInts( - charging=0x00, - full=0x01, - not_charging=0x02, - error=0x07) +CHARGE_STATUS = _NamedInts(charging=0x00, + full=0x01, + not_charging=0x02, + error=0x07) -CHARGE_LEVEL = _NamedInts( - average=50, - full=90, - critical=5) +CHARGE_LEVEL = _NamedInts(average=50, full=90, critical=5) -CHARGE_TYPE = _NamedInts( - standard=0x00, - fast=0x01, - slow=0x02) +CHARGE_TYPE = _NamedInts(standard=0x00, fast=0x01, slow=0x02) -ERROR = _NamedInts( - unknown=0x01, - invalid_argument=0x02, - out_of_range=0x03, - hardware_error=0x04, - logitech_internal=0x05, - invalid_feature_index=0x06, - invalid_function=0x07, - busy=0x08, - unsupported=0x09) +ERROR = _NamedInts(unknown=0x01, + invalid_argument=0x02, + out_of_range=0x03, + hardware_error=0x04, + logitech_internal=0x05, + invalid_feature_index=0x06, + invalid_function=0x07, + busy=0x08, + unsupported=0x09) # # # + class FeatureNotSupported(_KwException): - """Raised when trying to request a feature not supported by the device.""" - pass + """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 + """Raised if the device replied to a feature call with an error.""" + pass + # # # + class FeaturesArray(object): - """A sequence of features supported by a HID++ 2.0 device.""" - __slots__ = ('supported', 'device', 'features', 'non_features') - assert FEATURE.ROOT == 0x0000 + """A sequence of features supported by a HID++ 2.0 device.""" + __slots__ = ('supported', 'device', 'features', 'non_features') + assert FEATURE.ROOT == 0x0000 - def __init__(self, device): - assert device is not None - self.device = device - self.supported = True - self.features = None - self.non_features = set() + def __init__(self, device): + assert device is not None + self.device = device + self.supported = True + self.features = None + self.non_features = set() - def __del__(self): - self.supported = False - self.device = None - self.features = None + def __del__(self): + self.supported = False + self.device = None + self.features = None - def _check(self): - # print (self.device, "check", self.supported, self.features, self.device.protocol) - if self.supported: - assert self.device - if self.features is not None: - return True + def _check(self): + # print (self.device, "check", self.supported, self.features, self.device.protocol) + if self.supported: + assert self.device + if self.features is not None: + return True - if not self.device.online: - # device is not connected right now, will have to try later - return False + if not self.device.online: + # device is not connected right now, will have to try later + return False - # I _think_ this is universally true - if self.device.protocol and self.device.protocol < 2.0: - self.supported = False - self.device.features = None - self.device = None - return False + # I _think_ this is universally true + if self.device.protocol and self.device.protocol < 2.0: + self.supported = False + self.device.features = None + self.device = None + return False - reply = self.device.request(0x0000, _pack('!H', FEATURE.FEATURE_SET)) - if reply is None: - self.supported = False - else: - fs_index = ord(reply[0:1]) - if fs_index: - count = self.device.request(fs_index << 8) - if count is None: - _log.warn("FEATURE_SET found, but failed to read features count") - # most likely the device is unavailable - return False - else: - count = ord(count[:1]) - assert count >= fs_index - self.features = [None] * (1 + count) - self.features[0] = FEATURE.ROOT - self.features[fs_index] = FEATURE.FEATURE_SET - return True - else: - self.supported = False + reply = self.device.request(0x0000, _pack('!H', + FEATURE.FEATURE_SET)) + if reply is None: + self.supported = False + else: + fs_index = ord(reply[0:1]) + if fs_index: + count = self.device.request(fs_index << 8) + if count is None: + _log.warn( + "FEATURE_SET found, but failed to read features count" + ) + # most likely the device is unavailable + return False + else: + count = ord(count[:1]) + assert count >= fs_index + self.features = [None] * (1 + count) + self.features[0] = FEATURE.ROOT + self.features[fs_index] = FEATURE.FEATURE_SET + return True + else: + self.supported = False - return False + return False - __bool__ = __nonzero__ = _check + __bool__ = __nonzero__ = _check - def __getitem__(self, index): - if self._check(): - if isinstance(index, int): - if index < 0 or index >= len(self.features): - raise IndexError(index) + def __getitem__(self, index): + if self._check(): + if isinstance(index, int): + if index < 0 or index >= len(self.features): + raise IndexError(index) - if self.features[index] is None: - feature = self.device.feature_request(FEATURE.FEATURE_SET, 0x10, index) - if feature: - feature, = _unpack('!H', feature[:2]) - self.features[index] = FEATURE[feature] + if self.features[index] is None: + feature = self.device.feature_request( + FEATURE.FEATURE_SET, 0x10, index) + if feature: + feature, = _unpack('!H', feature[:2]) + self.features[index] = FEATURE[feature] - return self.features[index] + return self.features[index] - elif isinstance(index, slice): - indices = index.indices(len(self.features)) - return [self.__getitem__(i) for i in range(*indices)] + elif isinstance(index, slice): + indices = index.indices(len(self.features)) + return [self.__getitem__(i) for i in range(*indices)] - def __contains__(self, featureId): - """Tests whether the list contains given Feature ID""" - if self._check(): - ivalue = int(featureId) - if ivalue in self.non_features: - return False; + def __contains__(self, featureId): + """Tests whether the list contains given Feature ID""" + if self._check(): + ivalue = int(featureId) + if ivalue in self.non_features: + return False - may_have = False - for f in self.features: - if f is None: - may_have = True - elif ivalue == int(f): - return True + may_have = False + for f in self.features: + if f is None: + may_have = True + elif ivalue == int(f): + return True - if may_have: - reply = self.device.request(0x0000, _pack('!H', ivalue)) - if reply: - index = ord(reply[0:1]) - if index: - self.features[index] = FEATURE[ivalue] - return True - else: - self.non_features.add(ivalue) - return False + if may_have: + reply = self.device.request(0x0000, _pack('!H', ivalue)) + if reply: + index = ord(reply[0:1]) + if index: + self.features[index] = FEATURE[ivalue] + return True + else: + self.non_features.add(ivalue) + return False - def index(self, featureId): - """Gets the Feature Index for a given Feature ID""" - if self._check(): - may_have = False - ivalue = int(featureId) - for index, f in enumerate(self.features): - if f is None: - may_have = True - elif ivalue == int(f): - return index + def index(self, featureId): + """Gets the Feature Index for a given Feature ID""" + if self._check(): + may_have = False + ivalue = int(featureId) + for index, f in enumerate(self.features): + if f is None: + may_have = True + elif ivalue == int(f): + return index - if may_have: - reply = self.device.request(0x0000, _pack('!H', ivalue)) - if reply: - index = ord(reply[0:1]) - self.features[index] = FEATURE[ivalue] - return index + if may_have: + reply = self.device.request(0x0000, _pack('!H', ivalue)) + if reply: + index = ord(reply[0:1]) + self.features[index] = FEATURE[ivalue] + return index - raise ValueError("%r not in list" % featureId) + raise ValueError("%r not in list" % featureId) - def __iter__(self): - if self._check(): - yield FEATURE.ROOT - index = 1 - last_index = len(self.features) - while index < last_index: - yield self.__getitem__(index) - index += 1 + def __iter__(self): + if self._check(): + yield FEATURE.ROOT + index = 1 + last_index = len(self.features) + while index < last_index: + yield self.__getitem__(index) + index += 1 + + def __len__(self): + return len(self.features) if self._check() else 0 - def __len__(self): - return len(self.features) if self._check() else 0 # # # + class KeysArray(object): - """A sequence of key mappings supported by a HID++ 2.0 device.""" - __slots__ = ('device', 'keys', 'keyversion') + """A sequence of key mappings supported by a HID++ 2.0 device.""" + __slots__ = ('device', 'keys', 'keyversion') - def __init__(self, device, count): - assert device is not None - self.device = device - self.keyversion = 0 - self.keys = [None] * count + def __init__(self, device, count): + assert device is not None + self.device = device + self.keyversion = 0 + self.keys = [None] * count - def __getitem__(self, index): - if isinstance(index, int): - if index < 0 or index >= len(self.keys): - raise IndexError(index) + def __getitem__(self, index): + if isinstance(index, int): + if index < 0 or index >= len(self.keys): + raise IndexError(index) - # TODO: add here additional variants for other REPROG_CONTROLS - if self.keys[index] is None: - keydata = feature_request(self.device, FEATURE.REPROG_CONTROLS, 0x10, index) - self.keyversion=1 - if keydata is None: - keydata = feature_request(self.device, FEATURE.REPROG_CONTROLS_V4, 0x10, index) - self.keyversion=4 - if keydata: - key, key_task, flags, pos, group, gmask = _unpack('!HHBBBB', keydata[:8]) - ctrl_id_text = special_keys.CONTROL[key] - ctrl_task_text = special_keys.TASK[key_task] - if self.keyversion == 1: - self.keys[index] = _ReprogrammableKeyInfo(index, ctrl_id_text, ctrl_task_text, flags) - if self.keyversion == 4: - try: - mapped_data = feature_request(self.device, FEATURE.REPROG_CONTROLS_V4, 0x20, key&0xff00, key&0xff) - if mapped_data: - remap_key, remap_flag, remapped = _unpack('!HBH', mapped_data[:5]) - # if key not mapped map it to itself for display - if remapped == 0: - remapped = key - except Exception: - remapped = key - remap_key = key - remap_flag = 0 + # TODO: add here additional variants for other REPROG_CONTROLS + if self.keys[index] is None: + keydata = feature_request(self.device, FEATURE.REPROG_CONTROLS, + 0x10, index) + self.keyversion = 1 + if keydata is None: + keydata = feature_request(self.device, + FEATURE.REPROG_CONTROLS_V4, 0x10, + index) + self.keyversion = 4 + if keydata: + key, key_task, flags, pos, group, gmask = _unpack( + '!HHBBBB', keydata[:8]) + ctrl_id_text = special_keys.CONTROL[key] + ctrl_task_text = special_keys.TASK[key_task] + if self.keyversion == 1: + self.keys[index] = _ReprogrammableKeyInfo( + index, ctrl_id_text, ctrl_task_text, flags) + if self.keyversion == 4: + try: + mapped_data = feature_request( + self.device, FEATURE.REPROG_CONTROLS_V4, 0x20, + key & 0xff00, key & 0xff) + if mapped_data: + remap_key, remap_flag, remapped = _unpack( + '!HBH', mapped_data[:5]) + # if key not mapped map it to itself for display + if remapped == 0: + remapped = key + except Exception: + remapped = key + remap_key = key + remap_flag = 0 - remapped_text = special_keys.CONTROL[remapped] - self.keys[index] = _ReprogrammableKeyInfoV4(index, ctrl_id_text, ctrl_task_text, flags, pos, group, gmask, remapped_text) + remapped_text = special_keys.CONTROL[remapped] + self.keys[index] = _ReprogrammableKeyInfoV4( + index, ctrl_id_text, ctrl_task_text, flags, pos, + group, gmask, remapped_text) - return self.keys[index] + return self.keys[index] - elif isinstance(index, slice): - indices = index.indices(len(self.keys)) - return [self.__getitem__(i) for i in range(*indices)] + elif isinstance(index, slice): + indices = index.indices(len(self.keys)) + return [self.__getitem__(i) for i in range(*indices)] - def index(self, value): - for index, k in enumerate(self.keys): - if k is not None and int(value) == int(k.key): - return index + def index(self, value): + for index, k in enumerate(self.keys): + if k is not None and int(value) == int(k.key): + return index - for index, k in enumerate(self.keys): - if k is None: - k = self.__getitem__(index) - if k is not None: - return index + for index, k in enumerate(self.keys): + if k is None: + k = self.__getitem__(index) + if k is not None: + return index - def __iter__(self): - for k in range(0, len(self.keys)): - yield self.__getitem__(k) + def __iter__(self): + for k in range(0, len(self.keys)): + yield self.__getitem__(k) + + def __len__(self): + return len(self.keys) - def __len__(self): - return len(self.keys) # # # + def feature_request(device, feature, function=0x00, *params): - if device.online and device.features: - if feature in device.features: - feature_index = device.features.index(int(feature)) - return device.request((feature_index << 8) + (function & 0xFF), *params) + if device.online and device.features: + if feature in device.features: + feature_index = device.features.index(int(feature)) + return device.request((feature_index << 8) + (function & 0xFF), + *params) def get_firmware(device): - """Reads a device's firmware info. + """Reads a device's firmware info. :returns: a list of FirmwareInfo tuples, ordered by firmware layer. """ - count = feature_request(device, FEATURE.DEVICE_FW_VERSION) - if count: - count = ord(count[:1]) + count = feature_request(device, FEATURE.DEVICE_FW_VERSION) + if count: + count = ord(count[:1]) - fw = [] - for index in range(0, count): - fw_info = feature_request(device, FEATURE.DEVICE_FW_VERSION, 0x10, index) - if fw_info: - level = ord(fw_info[:1]) & 0x0F - if level == 0 or level == 1: - name, version_major, version_minor, build = _unpack('!3sBBH', fw_info[1:8]) - version = '%02X.%02X' % (version_major, version_minor) - if build: - version += '.B%04X' % build - extras = fw_info[9:].rstrip(b'\x00') or None - fw_info = _FirmwareInfo(FIRMWARE_KIND[level], name.decode('ascii'), version, extras) - elif level == FIRMWARE_KIND.Hardware: - fw_info = _FirmwareInfo(FIRMWARE_KIND.Hardware, '', str(ord(fw_info[1:2])), None) - else: - fw_info = _FirmwareInfo(FIRMWARE_KIND.Other, '', '', None) + fw = [] + for index in range(0, count): + fw_info = feature_request(device, FEATURE.DEVICE_FW_VERSION, 0x10, + index) + if fw_info: + level = ord(fw_info[:1]) & 0x0F + if level == 0 or level == 1: + name, version_major, version_minor, build = _unpack( + '!3sBBH', fw_info[1:8]) + version = '%02X.%02X' % (version_major, version_minor) + if build: + version += '.B%04X' % build + extras = fw_info[9:].rstrip(b'\x00') or None + fw_info = _FirmwareInfo(FIRMWARE_KIND[level], + name.decode('ascii'), version, + extras) + elif level == FIRMWARE_KIND.Hardware: + fw_info = _FirmwareInfo(FIRMWARE_KIND.Hardware, '', + str(ord(fw_info[1:2])), None) + else: + fw_info = _FirmwareInfo(FIRMWARE_KIND.Other, '', '', None) - fw.append(fw_info) - # if _log.isEnabledFor(_DEBUG): - # _log.debug("device %d firmware %s", devnumber, fw_info) - return tuple(fw) + fw.append(fw_info) + # if _log.isEnabledFor(_DEBUG): + # _log.debug("device %d firmware %s", devnumber, fw_info) + return tuple(fw) def get_kind(device): - """Reads a device's type. + """Reads a device's type. :see DEVICE_KIND: :returns: a string describing the device type, or ``None`` if the device is not available or does not support the ``DEVICE_NAME`` feature. """ - kind = feature_request(device, FEATURE.DEVICE_NAME, 0x20) - if kind: - kind = ord(kind[:1]) - # if _log.isEnabledFor(_DEBUG): - # _log.debug("device %d type %d = %s", devnumber, kind, DEVICE_KIND[kind]) - return DEVICE_KIND[kind] + kind = feature_request(device, FEATURE.DEVICE_NAME, 0x20) + if kind: + kind = ord(kind[:1]) + # if _log.isEnabledFor(_DEBUG): + # _log.debug("device %d type %d = %s", devnumber, kind, DEVICE_KIND[kind]) + return DEVICE_KIND[kind] def get_name(device): - """Reads a device's name. + """Reads a device's name. :returns: a string with the device name, or ``None`` if the device is not available or does not support the ``DEVICE_NAME`` feature. """ - name_length = feature_request(device, FEATURE.DEVICE_NAME) - if name_length: - name_length = ord(name_length[:1]) + name_length = feature_request(device, FEATURE.DEVICE_NAME) + if name_length: + name_length = ord(name_length[:1]) - name = b'' - while len(name) < name_length: - fragment = feature_request(device, FEATURE.DEVICE_NAME, 0x10, len(name)) - if fragment: - name += fragment[:name_length - len(name)] - else: - _log.error("failed to read whole name of %s (expected %d chars)", device, name_length) - return None + name = b'' + while len(name) < name_length: + fragment = feature_request(device, FEATURE.DEVICE_NAME, 0x10, + len(name)) + if fragment: + name += fragment[:name_length - len(name)] + else: + _log.error( + "failed to read whole name of %s (expected %d chars)", + device, name_length) + return None - return name.decode('ascii') + return name.decode('ascii') def get_battery(device): - """Reads a device's battery level.""" - battery = feature_request(device, FEATURE.BATTERY_STATUS) - if battery: - discharge, dischargeNext, status = _unpack('!BBB', battery[:3]) - discharge = None if discharge == 0 else discharge - if _log.isEnabledFor(_DEBUG): - _log.debug("device %d battery %d%% charged, next level %d%% charge, status %d = %s", - device.number, discharge, dischargeNext, status, BATTERY_STATUS[status]) - return discharge, BATTERY_STATUS[status], dischargeNext + """Reads a device's battery level.""" + battery = feature_request(device, FEATURE.BATTERY_STATUS) + if battery: + discharge, dischargeNext, status = _unpack('!BBB', battery[:3]) + discharge = None if discharge == 0 else discharge + if _log.isEnabledFor(_DEBUG): + _log.debug( + "device %d battery %d%% charged, next level %d%% charge, status %d = %s", + device.number, discharge, dischargeNext, status, + BATTERY_STATUS[status]) + return discharge, BATTERY_STATUS[status], dischargeNext def get_voltage(device): - battery_voltage = feature_request(device, FEATURE.BATTERY_VOLTAGE) - if battery_voltage: - return decipher_voltage(battery_voltage) + battery_voltage = feature_request(device, FEATURE.BATTERY_VOLTAGE) + if battery_voltage: + return decipher_voltage(battery_voltage) # modified to be much closer to battery reports def decipher_voltage(voltage_report): - voltage, flags = _unpack('>HB', voltage_report[:3]) - status = BATTERY_STATUS.discharging - charge_sts = ERROR.unknown - charge_lvl = CHARGE_LEVEL.average - charge_type = CHARGE_TYPE.standard + voltage, flags = _unpack('>HB', voltage_report[:3]) + status = BATTERY_STATUS.discharging + charge_sts = ERROR.unknown + charge_lvl = CHARGE_LEVEL.average + charge_type = CHARGE_TYPE.standard - if flags & (1 << 7): - status = BATTERY_STATUS.recharging - charge_sts = CHARGE_STATUS[flags & 0x03] - if charge_sts is None: - charge_sts = ERROR.unknown - elif charge_sts == CHARGE_STATUS.full: - charge_lvl = CHARGE_LEVEL.full - status = BATTERY_STATUS.full - if (flags & (1 << 3)): - charge_type = CHARGE_TYPE.fast - elif (flags & (1 << 4)): - charge_type = CHARGE_TYPE.slow - status = BATTERY_STATUS.slow_recharge - elif (flags & (1 << 5)): - charge_lvl = CHARGE_LEVEL.critical + if flags & (1 << 7): + status = BATTERY_STATUS.recharging + charge_sts = CHARGE_STATUS[flags & 0x03] + if charge_sts is None: + charge_sts = ERROR.unknown + elif charge_sts == CHARGE_STATUS.full: + charge_lvl = CHARGE_LEVEL.full + status = BATTERY_STATUS.full + if (flags & (1 << 3)): + charge_type = CHARGE_TYPE.fast + elif (flags & (1 << 4)): + charge_type = CHARGE_TYPE.slow + status = BATTERY_STATUS.slow_recharge + elif (flags & (1 << 5)): + charge_lvl = CHARGE_LEVEL.critical - if _log.isEnabledFor(_DEBUG): - _log.debug("device %d, battery voltage %d mV, charging = %s, charge status %d = %s, charge level %s, charge type %s", - device.number, voltage, status, (flags & 0x03), charge_sts, charge_lvl, charge_type) + if _log.isEnabledFor(_DEBUG): + _log.debug( + "device %d, battery voltage %d mV, charging = %s, charge status %d = %s, charge level %s, charge type %s", + device.number, voltage, status, (flags & 0x03), charge_sts, + charge_lvl, charge_type) - return charge_lvl, status, voltage, charge_sts, charge_type + return charge_lvl, status, voltage, charge_sts, charge_type def get_keys(device): - # TODO: add here additional variants for other REPROG_CONTROLS - count = feature_request(device, FEATURE.REPROG_CONTROLS) - if count is None: - count = feature_request(device, FEATURE.REPROG_CONTROLS_V4) - if count: - return KeysArray(device, ord(count[:1])) + # TODO: add here additional variants for other REPROG_CONTROLS + count = feature_request(device, FEATURE.REPROG_CONTROLS) + if count is None: + count = feature_request(device, FEATURE.REPROG_CONTROLS_V4) + if count: + return KeysArray(device, ord(count[:1])) def get_mouse_pointer_info(device): - pointer_info = feature_request(device, FEATURE.MOUSE_POINTER) - if pointer_info: - dpi, flags = _unpack('!HB', pointer_info[:3]) - acceleration = ('none', 'low', 'med', 'high')[flags & 0x3] - suggest_os_ballistics = (flags & 0x04) != 0 - suggest_vertical_orientation = (flags & 0x08) != 0 - return { - 'dpi': dpi, - 'acceleration': acceleration, - 'suggest_os_ballistics': suggest_os_ballistics, - 'suggest_vertical_orientation': suggest_vertical_orientation - } + pointer_info = feature_request(device, FEATURE.MOUSE_POINTER) + if pointer_info: + dpi, flags = _unpack('!HB', pointer_info[:3]) + acceleration = ('none', 'low', 'med', 'high')[flags & 0x3] + suggest_os_ballistics = (flags & 0x04) != 0 + suggest_vertical_orientation = (flags & 0x08) != 0 + return { + 'dpi': dpi, + 'acceleration': acceleration, + 'suggest_os_ballistics': suggest_os_ballistics, + 'suggest_vertical_orientation': suggest_vertical_orientation + } def get_vertical_scrolling_info(device): - vertical_scrolling_info = feature_request(device, FEATURE.VERTICAL_SCROLLING) - if vertical_scrolling_info: - roller, ratchet, lines = _unpack('!BBB', vertical_scrolling_info[:3]) - roller_type = ('reserved', 'standard', 'reserved', '3G', 'micro', 'normal touch pad', 'inverted touch pad', 'reserved')[roller] - return { - 'roller': roller_type, - 'ratchet': ratchet, - 'lines': lines - } + vertical_scrolling_info = feature_request(device, + FEATURE.VERTICAL_SCROLLING) + if vertical_scrolling_info: + roller, ratchet, lines = _unpack('!BBB', vertical_scrolling_info[:3]) + roller_type = ('reserved', 'standard', 'reserved', '3G', 'micro', + 'normal touch pad', 'inverted touch pad', + 'reserved')[roller] + return {'roller': roller_type, 'ratchet': ratchet, 'lines': lines} def get_hi_res_scrolling_info(device): - hi_res_scrolling_info = feature_request(device, FEATURE.HI_RES_SCROLLING) - if hi_res_scrolling_info: - mode, resolution = _unpack('!BB', hi_res_scrolling_info[:2]) - return mode, resolution + hi_res_scrolling_info = feature_request(device, FEATURE.HI_RES_SCROLLING) + if hi_res_scrolling_info: + mode, resolution = _unpack('!BB', hi_res_scrolling_info[:2]) + return mode, resolution def get_pointer_speed_info(device): - pointer_speed_info = feature_request(device, FEATURE.POINTER_SPEED) - if pointer_speed_info: - pointer_speed_hi, pointer_speed_lo = _unpack('!BB', pointer_speed_info[:2]) - #if pointer_speed_lo > 0: - # pointer_speed_lo = pointer_speed_lo - return pointer_speed_hi+pointer_speed_lo/256 + pointer_speed_info = feature_request(device, FEATURE.POINTER_SPEED) + if pointer_speed_info: + pointer_speed_hi, pointer_speed_lo = _unpack('!BB', + pointer_speed_info[:2]) + #if pointer_speed_lo > 0: + # pointer_speed_lo = pointer_speed_lo + return pointer_speed_hi + pointer_speed_lo / 256 def get_lowres_wheel_status(device): - lowres_wheel_status = feature_request(device, FEATURE.LOWRES_WHEEL) - if lowres_wheel_status: - wheel_flag = _unpack('!B', lowres_wheel_status[:1])[0] - wheel_reporting = ('HID', 'HID++')[wheel_flag & 0x01] - return wheel_reporting + lowres_wheel_status = feature_request(device, FEATURE.LOWRES_WHEEL) + if lowres_wheel_status: + wheel_flag = _unpack('!B', lowres_wheel_status[:1])[0] + wheel_reporting = ('HID', 'HID++')[wheel_flag & 0x01] + return wheel_reporting def get_hires_wheel(device): - caps = feature_request(device, FEATURE.HIRES_WHEEL, 0x00) - mode = feature_request(device, FEATURE.HIRES_WHEEL, 0x10) - ratchet = feature_request(device, FEATURE.HIRES_WHEEL, 0x030) + caps = feature_request(device, FEATURE.HIRES_WHEEL, 0x00) + mode = feature_request(device, FEATURE.HIRES_WHEEL, 0x10) + ratchet = feature_request(device, FEATURE.HIRES_WHEEL, 0x030) + if caps and mode and ratchet: + # Parse caps + multi, flags = _unpack('!BB', caps[:2]) - if caps and mode and ratchet: - # Parse caps - multi, flags = _unpack('!BB', caps[:2]) + has_invert = (flags & 0x08) != 0 + has_ratchet = (flags & 0x04) != 0 - has_invert = (flags & 0x08) != 0 - has_ratchet = (flags & 0x04) != 0 + # Parse mode + wheel_mode, reserved = _unpack('!BB', mode[:2]) - # Parse mode - wheel_mode, reserved = _unpack('!BB', mode[:2]) + target = (wheel_mode & 0x01) != 0 + res = (wheel_mode & 0x02) != 0 + inv = (wheel_mode & 0x04) != 0 - target = (wheel_mode & 0x01) != 0 - res = (wheel_mode & 0x02) != 0 - inv = (wheel_mode & 0x04) != 0 + # Parse Ratchet switch + ratchet_mode, reserved = _unpack('!BB', ratchet[:2]) - # Parse Ratchet switch - ratchet_mode, reserved = _unpack('!BB', ratchet[:2]) + ratchet = (ratchet_mode & 0x01) != 0 - ratchet = (ratchet_mode & 0x01) != 0 + return multi, has_invert, has_ratchet, inv, res, target, ratchet - return multi, has_invert, has_ratchet, inv, res, target, ratchet def get_new_fn_inversion(device): - state = feature_request(device, FEATURE.NEW_FN_INVERSION, 0x00) + state = feature_request(device, FEATURE.NEW_FN_INVERSION, 0x00) - if state: - inverted, default_inverted = _unpack('!BB', state[:2]) - inverted = (inverted & 0x01) != 0 - default_inverted = (default_inverted & 0x01) != 0 - return inverted, default_inverted + if state: + inverted, default_inverted = _unpack('!BB', state[:2]) + inverted = (inverted & 0x01) != 0 + default_inverted = (default_inverted & 0x01) != 0 + return inverted, default_inverted diff --git a/lib/logitech_receiver/i18n.py b/lib/logitech_receiver/i18n.py index 7b105408..721d1908 100644 --- a/lib/logitech_receiver/i18n.py +++ b/lib/logitech_receiver/i18n.py @@ -23,30 +23,42 @@ from __future__ import absolute_import, division, print_function, unicode_litera import gettext as _gettext - try: - unicode - _ = lambda x: _gettext.gettext(x).decode('UTF-8') - ngettext = lambda *x: _gettext.ngettext(*x).decode('UTF-8') + unicode + _ = lambda x: _gettext.gettext(x).decode('UTF-8') + ngettext = lambda *x: _gettext.ngettext(*x).decode('UTF-8') except: - _ = _gettext.gettext - ngettext = _gettext.ngettext - + _ = _gettext.gettext + ngettext = _gettext.ngettext # A few common strings, not always accessible as such in the code. _DUMMY = ( - # approximative battery levels - _("empty"), _("critical"), _("low"), _("good"), _("full"), + # approximative battery levels + _("empty"), + _("critical"), + _("low"), + _("good"), + _("full"), - # battery charging statuses - _("discharging"), _("recharging"), _("almost full"), _("charged"), - _("slow recharge"), _("invalid battery"), _("thermal error"), + # battery charging statuses + _("discharging"), + _("recharging"), + _("almost full"), + _("charged"), + _("slow recharge"), + _("invalid battery"), + _("thermal error"), - # pairing errors - _("device timeout"), _("device not supported"), _("too many devices"), _("sequence timeout"), + # pairing errors + _("device timeout"), + _("device not supported"), + _("too many devices"), + _("sequence timeout"), - # firmware kinds - _("Firmware"), _("Bootloader"), _("Hardware"), _("Other"), - - ) + # firmware kinds + _("Firmware"), + _("Bootloader"), + _("Hardware"), + _("Other"), +) diff --git a/lib/logitech_receiver/listener.py b/lib/logitech_receiver/listener.py index c00a81b8..ab06b36c 100644 --- a/lib/logitech_receiver/listener.py +++ b/lib/logitech_receiver/listener.py @@ -24,92 +24,96 @@ import threading as _threading # for both Python 2 and 3 try: - from Queue import Queue as _Queue + from Queue import Queue as _Queue except ImportError: - from queue import Queue as _Queue + from queue import Queue as _Queue from logging import getLogger, DEBUG as _DEBUG, INFO as _INFO _log = getLogger(__name__) del getLogger - from . import base as _base # # # + class _ThreadedHandle(object): - """A thread-local wrapper with different open handles for each thread. + """A thread-local wrapper with different open handles for each thread. 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 - assert path is not None - assert handle is not None - assert isinstance(handle, int) + def __init__(self, listener, path, handle): + assert listener is not None + assert path is not None + assert handle is not None + assert isinstance(handle, int) - self._listener = listener - self.path = path - self._local = _threading.local() - # take over the current handle for the thread doing the replacement - self._local.handle = handle - self._handles = [handle] + self._listener = listener + self.path = path + self._local = _threading.local() + # take over the current handle for the thread doing the replacement + self._local.handle = handle + self._handles = [handle] - def _open(self): - handle = _base.open_path(self.path) - if handle is None: - _log.error("%r failed to open new handle", self) - else: - # if _log.isEnabledFor(_DEBUG): - # _log.debug("%r opened new handle %d", self, handle) - self._local.handle = handle - self._handles.append(handle) - return handle + def _open(self): + handle = _base.open_path(self.path) + if handle is None: + _log.error("%r failed to open new handle", self) + else: + # if _log.isEnabledFor(_DEBUG): + # _log.debug("%r opened new handle %d", self, handle) + self._local.handle = handle + self._handles.append(handle) + return handle - def close(self): - if self._local: - self._local = None - handles, self._handles = self._handles, [] - if _log.isEnabledFor(_DEBUG): - _log.debug("%r closing %s", self, handles) - for h in handles: - _base.close(h) + def close(self): + if self._local: + self._local = None + handles, self._handles = self._handles, [] + if _log.isEnabledFor(_DEBUG): + _log.debug("%r closing %s", self, handles) + for h in handles: + _base.close(h) - @property - def notifications_hook(self): - if self._listener: - assert isinstance(self._listener, _threading.Thread) - if _threading.current_thread() == self._listener: - return self._listener._notifications_hook + @property + def notifications_hook(self): + if self._listener: + assert isinstance(self._listener, _threading.Thread) + if _threading.current_thread() == self._listener: + return self._listener._notifications_hook - def __del__(self): - self._listener = None - self.close() + def __del__(self): + self._listener = None + self.close() - def __index__(self): - if self._local: - try: - return self._local.handle - except: - return self._open() - __int__ = __index__ + def __index__(self): + if self._local: + try: + return self._local.handle + except: + return self._open() - def __str__(self): - if self._local: - return str(int(self)) - __unicode__ = __str__ + __int__ = __index__ - def __repr__(self): - return '<_ThreadedHandle(%s)>' % self.path + def __str__(self): + if self._local: + return str(int(self)) + + __unicode__ = __str__ + + def __repr__(self): + return '<_ThreadedHandle(%s)>' % self.path + + def __bool__(self): + return bool(self._local) + + __nonzero__ = __bool__ - def __bool__(self): - return bool(self._local) - __nonzero__ = __bool__ # # @@ -129,102 +133,105 @@ _EVENT_READ_TIMEOUT = 0.4 # in seconds class EventsListener(_threading.Thread): - """Listener thread for notifications from the Unifying Receiver. + """Listener thread for notifications from the Unifying Receiver. Incoming packets will be passed to the callback function in sequence. """ - def __init__(self, receiver, notifications_callback): - super(EventsListener, self).__init__(name=self.__class__.__name__ + ':' + receiver.path.split('/')[2]) + def __init__(self, receiver, notifications_callback): + super(EventsListener, self).__init__(name=self.__class__.__name__ + + ':' + receiver.path.split('/')[2]) - self.daemon = True - self._active = False + self.daemon = True + self._active = False - self.receiver = receiver - self._queued_notifications = _Queue(16) - self._notifications_callback = notifications_callback + self.receiver = receiver + self._queued_notifications = _Queue(16) + self._notifications_callback = notifications_callback - # self.tick_period = 0 + # self.tick_period = 0 - def run(self): - self._active = True + def run(self): + self._active = True - # replace the handle with a threaded one - self.receiver.handle = _ThreadedHandle(self, self.receiver.path, self.receiver.handle) - # get the right low-level handle for this thread - ihandle = int(self.receiver.handle) - if _log.isEnabledFor(_INFO): - _log.info("started with %s (%d)", self.receiver, ihandle) + # replace the handle with a threaded one + self.receiver.handle = _ThreadedHandle(self, self.receiver.path, + self.receiver.handle) + # get the right low-level handle for this thread + ihandle = int(self.receiver.handle) + if _log.isEnabledFor(_INFO): + _log.info("started with %s (%d)", self.receiver, ihandle) - self.has_started() + self.has_started() - # last_tick = 0 - # the first idle read -- delay it a bit, and make sure to stagger - # idle reads for multiple receivers - # idle_reads = _IDLE_READS + (ihandle % 5) * 2 + # last_tick = 0 + # the first idle read -- delay it a bit, and make sure to stagger + # idle reads for multiple receivers + # idle_reads = _IDLE_READS + (ihandle % 5) * 2 - while self._active: - if self._queued_notifications.empty(): - try: - # _log.debug("read next notification") - n = _base.read(ihandle, _EVENT_READ_TIMEOUT) - except _base.NoReceiver: - _log.warning("receiver disconnected") - self.receiver.close() - break + while self._active: + if self._queued_notifications.empty(): + try: + # _log.debug("read next notification") + n = _base.read(ihandle, _EVENT_READ_TIMEOUT) + except _base.NoReceiver: + _log.warning("receiver disconnected") + self.receiver.close() + break - if n: - n = _base.make_notification(*n) - else: - # deliver any queued notifications - n = self._queued_notifications.get() + if n: + n = _base.make_notification(*n) + else: + # deliver any queued notifications + n = self._queued_notifications.get() - if n: - # if _log.isEnabledFor(_DEBUG): - # _log.debug("%s: processing %s", self.receiver, n) - try: - self._notifications_callback(n) - except: - _log.exception("processing %s", n) + if n: + # if _log.isEnabledFor(_DEBUG): + # _log.debug("%s: processing %s", self.receiver, n) + try: + self._notifications_callback(n) + except: + _log.exception("processing %s", n) - # elif self.tick_period: - # idle_reads -= 1 - # if idle_reads <= 0: - # idle_reads = _IDLE_READS - # now = _timestamp() - # if now - last_tick >= self.tick_period: - # last_tick = now - # self.tick(now) + # elif self.tick_period: + # idle_reads -= 1 + # if idle_reads <= 0: + # idle_reads = _IDLE_READS + # now = _timestamp() + # if now - last_tick >= self.tick_period: + # last_tick = now + # self.tick(now) - del self._queued_notifications - self.has_stopped() + del self._queued_notifications + self.has_stopped() - def stop(self): - """Tells the listener to stop as soon as possible.""" - self._active = False + def stop(self): + """Tells the listener to stop as soon as possible.""" + self._active = False - def has_started(self): - """Called right after the thread has started, and before it starts + def has_started(self): + """Called right after the thread has started, and before it starts reading notification packets.""" - pass + pass - def has_stopped(self): - """Called right before the thread stops.""" - pass + def has_stopped(self): + """Called right before the thread stops.""" + pass - # def tick(self, timestamp): - # """Called about every tick_period seconds.""" - # pass + # def tick(self, timestamp): + # """Called about every tick_period seconds.""" + # pass - def _notifications_hook(self, n): - # Only consider unhandled notifications that were sent from this thread, - # i.e. triggered by a callback handling a previous notification. - assert _threading.current_thread() == self - if self._active: # and _threading.current_thread() == self: - # if _log.isEnabledFor(_DEBUG): - # _log.debug("queueing unhandled %s", n) - if not self._queued_notifications.full(): - self._queued_notifications.put(n) + def _notifications_hook(self, n): + # Only consider unhandled notifications that were sent from this thread, + # i.e. triggered by a callback handling a previous notification. + assert _threading.current_thread() == self + if self._active: # and _threading.current_thread() == self: + # if _log.isEnabledFor(_DEBUG): + # _log.debug("queueing unhandled %s", n) + if not self._queued_notifications.full(): + self._queued_notifications.put(n) - def __bool__(self): - return bool(self._active and self.receiver) - __nonzero__ = __bool__ + def __bool__(self): + return bool(self._active and self.receiver) + + __nonzero__ = __bool__ diff --git a/lib/logitech_receiver/notifications.py b/lib/logitech_receiver/notifications.py index a1ed718e..fe1c0066 100644 --- a/lib/logitech_receiver/notifications.py +++ b/lib/logitech_receiver/notifications.py @@ -26,7 +26,6 @@ from logging import getLogger, DEBUG as _DEBUG, INFO as _INFO _log = getLogger(__name__) del getLogger - from .i18n import _ from .common import strhex as _strhex, unpack as _unpack from . import hidpp10 as _hidpp10 @@ -41,302 +40,329 @@ _F = _hidpp20.FEATURE # # + def process(device, notification): - assert device - assert notification + assert device + assert notification - assert hasattr(device, 'status') - status = device.status - assert status is not None + assert hasattr(device, 'status') + status = device.status + assert status is not None - if device.kind is None: - return _process_receiver_notification(device, status, notification) + if device.kind is None: + return _process_receiver_notification(device, status, notification) + + return _process_device_notification(device, status, notification) - return _process_device_notification(device, status, notification) # # # + def _process_receiver_notification(receiver, status, n): - # supposedly only 0x4x notifications arrive for the receiver - assert n.sub_id & 0x40 == 0x40 + # supposedly only 0x4x notifications arrive for the receiver + assert n.sub_id & 0x40 == 0x40 - # pairing lock notification - if n.sub_id == 0x4A: - status.lock_open = bool(n.address & 0x01) - reason = (_("pairing lock is open") if status.lock_open else _("pairing lock is closed")) - if _log.isEnabledFor(_INFO): - _log.info("%s: %s", receiver, reason) + # pairing lock notification + if n.sub_id == 0x4A: + status.lock_open = bool(n.address & 0x01) + reason = (_("pairing lock is open") + if status.lock_open else _("pairing lock is closed")) + if _log.isEnabledFor(_INFO): + _log.info("%s: %s", receiver, reason) - status[_K.ERROR] = None - if status.lock_open: - status.new_device = None + status[_K.ERROR] = None + if status.lock_open: + status.new_device = None - pair_error = ord(n.data[:1]) - if pair_error: - status[_K.ERROR] = error_string = _hidpp10.PAIRING_ERRORS[pair_error] - status.new_device = None - _log.warn("pairing error %d: %s", pair_error, error_string) + pair_error = ord(n.data[:1]) + if pair_error: + status[ + _K.ERROR] = error_string = _hidpp10.PAIRING_ERRORS[pair_error] + status.new_device = None + _log.warn("pairing error %d: %s", pair_error, error_string) - status.changed(reason=reason) - return True + status.changed(reason=reason) + return True + + _log.warn("%s: unhandled notification %s", receiver, n) - _log.warn("%s: unhandled notification %s", receiver, n) # # # + def _process_device_notification(device, status, n): - # incoming packets with SubId >= 0x80 are supposedly replies from - # HID++ 1.0 requests, should never get here - assert n.sub_id & 0x80 == 0 + # incoming packets with SubId >= 0x80 are supposedly replies from + # HID++ 1.0 requests, should never get here + assert n.sub_id & 0x80 == 0 - # 0x40 to 0x7F appear to be HID++ 1.0 or DJ notifications - if n.sub_id >= 0x40: - if len(n.data) == _DJ_NOTIFICATION_LENGTH : - return _process_dj_notification(device, status, n) - else: - return _process_hidpp10_notification(device, status, n) + # 0x40 to 0x7F appear to be HID++ 1.0 or DJ notifications + if n.sub_id >= 0x40: + if len(n.data) == _DJ_NOTIFICATION_LENGTH: + return _process_dj_notification(device, status, n) + else: + return _process_hidpp10_notification(device, status, n) - # At this point, we need to know the device's protocol, otherwise it's - # possible to not know how to handle it. - assert device.protocol is not None + # At this point, we need to know the device's protocol, otherwise it's + # possible to not know how to handle it. + assert device.protocol is not None - # some custom battery events for HID++ 1.0 devices - if device.protocol < 2.0: - return _process_hidpp10_custom_notification(device, status, n) + # some custom battery events for HID++ 1.0 devices + if device.protocol < 2.0: + return _process_hidpp10_custom_notification(device, status, n) - # assuming 0x00 to 0x3F are feature (HID++ 2.0) notifications - assert device.features - try: - feature = device.features[n.sub_id] - except IndexError: - _log.warn("%s: notification from invalid feature index %02X: %s", device, n.sub_id, n) - return False + # assuming 0x00 to 0x3F are feature (HID++ 2.0) notifications + assert device.features + try: + feature = device.features[n.sub_id] + except IndexError: + _log.warn("%s: notification from invalid feature index %02X: %s", + device, n.sub_id, n) + return False - return _process_feature_notification(device, status, n, feature) + return _process_feature_notification(device, status, n, feature) -def _process_dj_notification(device, status, n) : - if _log.isEnabledFor(_DEBUG): - _log.debug("%s (%s) DJ notification %s", device, device.protocol, n) +def _process_dj_notification(device, status, n): + if _log.isEnabledFor(_DEBUG): + _log.debug("%s (%s) DJ notification %s", device, device.protocol, n) - if n.sub_id == 0x40: - # do all DJ paired notifications also show up as HID++ 1.0 notifications? - if _log.isEnabledFor(_INFO): - _log.info("%s: ignoring DJ unpaired: %s", device, n) - return True + if n.sub_id == 0x40: + # do all DJ paired notifications also show up as HID++ 1.0 notifications? + if _log.isEnabledFor(_INFO): + _log.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 _log.isEnabledFor(_INFO): - _log.info("%s: ignoring DJ paired: %s", device, n) - return True + if n.sub_id == 0x41: + # do all DJ paired notifications also show up as HID++ 1.0 notifications? + if _log.isEnabledFor(_INFO): + _log.info("%s: ignoring DJ paired: %s", device, n) + return True - if n.sub_id == 0x42: - if _log.isEnabledFor(_INFO): - _log.info("%s: ignoring DJ connection: %s", device, n) - return True + if n.sub_id == 0x42: + if _log.isEnabledFor(_INFO): + _log.info("%s: ignoring DJ connection: %s", device, n) + return True - _log.warn("%s: unrecognized DJ %s", device, n) + _log.warn("%s: unrecognized DJ %s", device, n) def _process_hidpp10_custom_notification(device, status, n): - if _log.isEnabledFor(_DEBUG): - _log.debug("%s (%s) custom notification %s", device, device.protocol, n) + if _log.isEnabledFor(_DEBUG): + _log.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 <00> - assert n.data[-1:] == b'\x00' - data = chr(n.address).encode() + n.data - charge, status_text = _hidpp10.parse_battery_status(n.sub_id, data) - status.set_battery_info(charge, status_text, None) - return True + if n.sub_id in (_R.battery_status, _R.battery_charge): + # message layout: 10 ix <00> + assert n.data[-1:] == b'\x00' + data = chr(n.address).encode() + n.data + charge, status_text = _hidpp10.parse_battery_status(n.sub_id, data) + status.set_battery_info(charge, status_text, None) + return True - if n.sub_id == _R.keyboard_illumination: - # message layout: 10 ix 17("address") - # TODO anything we can do with this? - if _log.isEnabledFor(_INFO): - _log.info("illumination event: %s", n) - return True + if n.sub_id == _R.keyboard_illumination: + # message layout: 10 ix 17("address") + # TODO anything we can do with this? + if _log.isEnabledFor(_INFO): + _log.info("illumination event: %s", n) + return True - _log.warn("%s: unrecognized %s", device, n) + _log.warn("%s: unrecognized %s", device, n) def _process_hidpp10_notification(device, status, n): - # unpair notification - if n.sub_id == 0x40: - if n.address == 0x02: - # device un-paired - status.clear() - device.wpid = None - device.status = None - if device.number in device.receiver: - del device.receiver[device.number] - status.changed(active=False, alert=_ALERT.ALL, reason=_("unpaired")) - else: - _log.warn("%s: disconnection with unknown type %02X: %s", device, n.address, n) - return True + # unpair notification + if n.sub_id == 0x40: + if n.address == 0x02: + # device un-paired + status.clear() + device.wpid = None + device.status = None + if device.number in device.receiver: + del device.receiver[device.number] + status.changed(active=False, + alert=_ALERT.ALL, + reason=_("unpaired")) + else: + _log.warn("%s: disconnection with unknown type %02X: %s", device, + n.address, n) + return True - # wireless link notification - if n.sub_id == 0x41: - protocol_name = ('Bluetooth' if n.address == 0x01 - else '27 MHz' if n.address == 0x02 - else 'QUAD or eQUAD' if n.address == 0x03 - else 'eQUAD step 4 DJ' if n.address == 0x04 - else 'DFU Lite' if n.address == 0x05 - else 'eQUAD step 4 Lite' if n.address == 0x06 - else 'eQUAD step 4 Gaming' if n.address == 0x07 - else 'eQUAD step 4 for gamepads' if n.address == 0x08 - else 'eQUAD nano Lite' if n.address == 0x0A - else 'Lightspeed 1' if n.address == 0x0C - else 'Lightspeed 1_1' if n.address == 0x0D - else None) - if protocol_name: - if _log.isEnabledFor(_DEBUG): - wpid = _strhex(n.data[2:3] + n.data[1:2]) - assert wpid == device.wpid, "%s wpid mismatch, got %s" % (device, wpid) + # wireless link notification + if n.sub_id == 0x41: + protocol_name = ( + 'Bluetooth' if n.address == 0x01 else '27 MHz' + if n.address == 0x02 else 'QUAD or eQUAD' if n.address == 0x03 else + 'eQUAD step 4 DJ' if n.address == 0x04 else 'DFU Lite' if n. + address == 0x05 else 'eQUAD step 4 Lite' if n.address == + 0x06 else 'eQUAD step 4 Gaming' if n.address == + 0x07 else 'eQUAD step 4 for gamepads' if n.address == + 0x08 else 'eQUAD nano Lite' if n.address == + 0x0A else 'Lightspeed 1' if n.address == + 0x0C else 'Lightspeed 1_1' if n.address == 0x0D else None) + if protocol_name: + if _log.isEnabledFor(_DEBUG): + wpid = _strhex(n.data[2:3] + n.data[1:2]) + assert wpid == device.wpid, "%s wpid mismatch, got %s" % ( + device, wpid) - flags = ord(n.data[:1]) & 0xF0 - link_encrypted = bool(flags & 0x20) - link_established = not (flags & 0x40) - if _log.isEnabledFor(_DEBUG): - sw_present = bool(flags & 0x10) - has_payload = bool(flags & 0x80) - _log.debug("%s: %s connection notification: software=%s, encrypted=%s, link=%s, payload=%s", - device, protocol_name, sw_present, link_encrypted, link_established, has_payload) - status[_K.LINK_ENCRYPTED] = link_encrypted - status.changed(active=link_established) - else: - _log.warn("%s: connection notification with unknown protocol %02X: %s", device.number, n.address, n) + flags = ord(n.data[:1]) & 0xF0 + link_encrypted = bool(flags & 0x20) + link_established = not (flags & 0x40) + if _log.isEnabledFor(_DEBUG): + sw_present = bool(flags & 0x10) + has_payload = bool(flags & 0x80) + _log.debug( + "%s: %s connection notification: software=%s, encrypted=%s, link=%s, payload=%s", + device, protocol_name, sw_present, link_encrypted, + link_established, has_payload) + status[_K.LINK_ENCRYPTED] = link_encrypted + status.changed(active=link_established) + else: + _log.warn( + "%s: connection notification with unknown protocol %02X: %s", + device.number, n.address, n) - return True + return True - if n.sub_id == 0x49: - # raw input event? just ignore it - # if n.address == 0x01, no idea what it is, but they keep on coming - # if n.address == 0x03, appears to be an actual input event, - # because they only come when input happents - return True + if n.sub_id == 0x49: + # raw input event? just ignore it + # if n.address == 0x01, no idea what it is, but they keep on coming + # if n.address == 0x03, appears to be an actual input event, + # because they only come when input happents + return True - # power notification - if n.sub_id == 0x4B: - if n.address == 0x01: - if _log.isEnabledFor(_DEBUG): - _log.debug("%s: device powered on", device) - reason = status.to_string() or _("powered on") - status.changed(active=True, alert=_ALERT.NOTIFICATION, reason=reason) - else: - _log.warn("%s: unknown %s", device, n) - return True + # power notification + if n.sub_id == 0x4B: + if n.address == 0x01: + if _log.isEnabledFor(_DEBUG): + _log.debug("%s: device powered on", device) + reason = status.to_string() or _("powered on") + status.changed(active=True, + alert=_ALERT.NOTIFICATION, + reason=reason) + else: + _log.warn("%s: unknown %s", device, n) + return True - _log.warn("%s: unrecognized %s", device, n) + _log.warn("%s: unrecognized %s", device, n) def _process_feature_notification(device, status, n, feature): - if feature == _F.BATTERY_STATUS: - if n.address == 0x00: - discharge_level = ord(n.data[:1]) - discharge_level = None if discharge_level == 0 else discharge_level - discharge_next_level = ord(n.data[1:2]) - battery_status = ord(n.data[2:3]) - status.set_battery_info(discharge_level, _hidpp20.BATTERY_STATUS[battery_status], discharge_next_level) - else: - _log.warn("%s: unknown BATTERY %s", device, n) - return True + if feature == _F.BATTERY_STATUS: + if n.address == 0x00: + discharge_level = ord(n.data[:1]) + discharge_level = None if discharge_level == 0 else discharge_level + discharge_next_level = ord(n.data[1:2]) + battery_status = ord(n.data[2:3]) + status.set_battery_info(discharge_level, + _hidpp20.BATTERY_STATUS[battery_status], + discharge_next_level) + else: + _log.warn("%s: unknown BATTERY %s", device, n) + return True - if feature == _F.BATTERY_VOLTAGE: - if n.address == 0x00: - level, status, voltage, _ignore, _ignore =_hidpp20.decipher_voltage(n.data) - status.set_battery_info(level, status, None, voltage) - else: - _log.warn("%s: unknown VOLTAGE %s", device, n) - return True + if feature == _F.BATTERY_VOLTAGE: + if n.address == 0x00: + level, status, voltage, _ignore, _ignore = _hidpp20.decipher_voltage( + n.data) + status.set_battery_info(level, status, None, voltage) + else: + _log.warn("%s: unknown VOLTAGE %s", device, n) + return True - # TODO: what are REPROG_CONTROLS_V{2,3}? - if feature == _F.REPROG_CONTROLS: - if n.address == 0x00: - if _log.isEnabledFor(_INFO): - _log.info("%s: reprogrammable key: %s", device, n) - else: - _log.warn("%s: unknown REPROGRAMMABLE KEYS %s", device, n) - return True + # TODO: what are REPROG_CONTROLS_V{2,3}? + if feature == _F.REPROG_CONTROLS: + if n.address == 0x00: + if _log.isEnabledFor(_INFO): + _log.info("%s: reprogrammable key: %s", device, n) + else: + _log.warn("%s: unknown REPROGRAMMABLE KEYS %s", device, n) + return True - if feature == _F.WIRELESS_DEVICE_STATUS: - if n.address == 0x00: - if _log.isEnabledFor(_DEBUG): - _log.debug("wireless status: %s", n) - if n.data[0:3] == b'\x01\x01\x01': - status.changed(active=True, alert=_ALERT.NOTIFICATION, reason='powered on') - else: - _log.warn("%s: unknown WIRELESS %s", device, n) - else: - _log.warn("%s: unknown WIRELESS %s", device, n) - return True + if feature == _F.WIRELESS_DEVICE_STATUS: + if n.address == 0x00: + if _log.isEnabledFor(_DEBUG): + _log.debug("wireless status: %s", n) + if n.data[0:3] == b'\x01\x01\x01': + status.changed(active=True, + alert=_ALERT.NOTIFICATION, + reason='powered on') + else: + _log.warn("%s: unknown WIRELESS %s", device, n) + else: + _log.warn("%s: unknown WIRELESS %s", device, n) + return True - if feature == _F.SOLAR_DASHBOARD: - 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.BATTERY_STATUS.discharging + if feature == _F.SOLAR_DASHBOARD: + 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.BATTERY_STATUS.discharging - if n.address == 0x00: - status[_K.LIGHT_LEVEL] = None - status.set_battery_info(charge, status_text, None) - elif n.address == 0x10: - status[_K.LIGHT_LEVEL] = lux - if lux > 200: - status_text = _hidpp20.BATTERY_STATUS.recharging - status.set_battery_info(charge, status_text, None) - elif n.address == 0x20: - if _log.isEnabledFor(_DEBUG): - _log.debug("%s: Light Check button pressed", device) - status.changed(alert=_ALERT.SHOW_WINDOW) - # first cancel any reporting - # device.feature_request(_F.SOLAR_DASHBOARD) - # trigger a new report chain - reports_count = 15 - reports_period = 2 # seconds - device.feature_request(_F.SOLAR_DASHBOARD, 0x00, reports_count, reports_period) - else: - _log.warn("%s: unknown SOLAR CHARGE %s", device, n) - else: - _log.warn("%s: SOLAR CHARGE not GOOD? %s", device, n) - return True + if n.address == 0x00: + status[_K.LIGHT_LEVEL] = None + status.set_battery_info(charge, status_text, None) + elif n.address == 0x10: + status[_K.LIGHT_LEVEL] = lux + if lux > 200: + status_text = _hidpp20.BATTERY_STATUS.recharging + status.set_battery_info(charge, status_text, None) + elif n.address == 0x20: + if _log.isEnabledFor(_DEBUG): + _log.debug("%s: Light Check button pressed", device) + status.changed(alert=_ALERT.SHOW_WINDOW) + # first cancel any reporting + # device.feature_request(_F.SOLAR_DASHBOARD) + # trigger a new report chain + reports_count = 15 + reports_period = 2 # seconds + device.feature_request(_F.SOLAR_DASHBOARD, 0x00, reports_count, + reports_period) + else: + _log.warn("%s: unknown SOLAR CHARGE %s", device, n) + else: + _log.warn("%s: SOLAR CHARGE not GOOD? %s", device, n) + return True - if feature == _F.TOUCHMOUSE_RAW_POINTS: - if n.address == 0x00: - if _log.isEnabledFor(_INFO): - _log.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 _log.isEnabledFor(_INFO): - _log.info("%s: TOUCH MOUSE status: button_down=%s mouse_lifted=%s", device, button_down, mouse_lifted) - else: - _log.warn("%s: unknown TOUCH MOUSE %s", device, n) - return True + if feature == _F.TOUCHMOUSE_RAW_POINTS: + if n.address == 0x00: + if _log.isEnabledFor(_INFO): + _log.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 _log.isEnabledFor(_INFO): + _log.info( + "%s: TOUCH MOUSE status: button_down=%s mouse_lifted=%s", + device, button_down, mouse_lifted) + else: + _log.warn("%s: unknown TOUCH MOUSE %s", device, n) + return True - if feature == _F.HIRES_WHEEL: - if (n.address == 0x00): - if _log.isEnabledFor(_INFO): - flags, delta_v = _unpack('>bh', n.data[:3]) - high_res = (flags & 0x10) != 0 - periods = flags & 0x0f - _log.info("%s: WHEEL: res: %d periods: %d delta V:%-3d", device, high_res, periods, delta_v) - return True - elif (n.address == 0x10): - if _log.isEnabledFor(_INFO): - flags = ord(n.data[:1]) - ratchet = flags & 0x01 - _log.info("%s: WHEEL: ratchet: %d", device, ratchet) - return True - else: - _log.warn("%s: unknown WHEEL %s", device, n) - return True + if feature == _F.HIRES_WHEEL: + if (n.address == 0x00): + if _log.isEnabledFor(_INFO): + flags, delta_v = _unpack('>bh', n.data[:3]) + high_res = (flags & 0x10) != 0 + periods = flags & 0x0f + _log.info("%s: WHEEL: res: %d periods: %d delta V:%-3d", + device, high_res, periods, delta_v) + return True + elif (n.address == 0x10): + if _log.isEnabledFor(_INFO): + flags = ord(n.data[:1]) + ratchet = flags & 0x01 + _log.info("%s: WHEEL: ratchet: %d", device, ratchet) + return True + else: + _log.warn("%s: unknown WHEEL %s", device, n) + return True - _log.warn("%s: unrecognized %s for feature %s (index %02X)", device, n, feature, n.sub_id) + _log.warn("%s: unrecognized %s for feature %s (index %02X)", device, n, + feature, n.sub_id) diff --git a/lib/logitech_receiver/receiver.py b/lib/logitech_receiver/receiver.py index 98efca1c..222c51b4 100644 --- a/lib/logitech_receiver/receiver.py +++ b/lib/logitech_receiver/receiver.py @@ -25,7 +25,6 @@ from logging import getLogger, INFO as _INFO _log = getLogger(__name__) del getLogger - from .i18n import _ from . import base as _base from . import hidpp10 as _hidpp10 @@ -41,516 +40,552 @@ _R = _hidpp10.REGISTERS # # + class PairedDevice(object): - def __init__(self, receiver, number, link_notification=None): - assert receiver - self.receiver = receiver + def __init__(self, receiver, number, link_notification=None): + assert receiver + self.receiver = receiver - assert number > 0 and number <= receiver.max_devices - # Device number, 1..6 for unifying devices, 1 otherwise. - self.number = number - # 'device active' flag; requires manual management. - self.online = None + assert number > 0 and number <= receiver.max_devices + # Device number, 1..6 for unifying devices, 1 otherwise. + self.number = number + # 'device active' flag; requires manual management. + self.online = None - # the Wireless PID is unique per device model - self.wpid = None - self.descriptor = None + # the Wireless PID is unique per device model + self.wpid = None + self.descriptor = None - # mouse, keyboard, etc (see _hidpp10.DEVICE_KIND) - self._kind = None - # Unifying peripherals report a codename. - self._codename = None - # the full name of the model - self._name = None - # HID++ protocol version, 1.0 or 2.0 - self._protocol = None - # serial number (an 8-char hex string) - self._serial = None + # mouse, keyboard, etc (see _hidpp10.DEVICE_KIND) + self._kind = None + # Unifying peripherals report a codename. + self._codename = None + # the full name of the model + self._name = None + # HID++ protocol version, 1.0 or 2.0 + self._protocol = None + # serial number (an 8-char hex string) + self._serial = None - self._firmware = None - self._keys = None - self._registers = None - self._settings = None - self._feature_settings_checked = False + self._firmware = None + self._keys = None + self._registers = None + self._settings = None + self._feature_settings_checked = False - # Misc stuff that's irrelevant to any functionality, but may be - # displayed in the UI and caching it here helps. - self._polling_rate = None - self._power_switch = None + # Misc stuff that's irrelevant to any functionality, but may be + # displayed in the UI and caching it here helps. + self._polling_rate = None + self._power_switch = None - # if _log.isEnabledFor(_DEBUG): - # _log.debug("new PairedDevice(%s, %s, %s)", receiver, number, link_notification) + # if _log.isEnabledFor(_DEBUG): + # _log.debug("new PairedDevice(%s, %s, %s)", receiver, number, link_notification) - if link_notification is not None: - self.online = not bool(ord(link_notification.data[0:1]) & 0x40) - self.wpid = _strhex(link_notification.data[2:3] + link_notification.data[1:2]) - # assert link_notification.address == (0x04 if unifying else 0x03) - kind = ord(link_notification.data[0:1]) & 0x0F - self._kind = _hidpp10.DEVICE_KIND[kind] - else: - # force a reading of the wpid - pair_info = receiver.read_register(_R.receiver_info, 0x20 + number - 1) - if pair_info: - # may be either a Unifying receiver, or an Unifying-ready receiver - self.wpid = _strhex(pair_info[3:5]) - kind = ord(pair_info[7:8]) & 0x0F - self._kind = _hidpp10.DEVICE_KIND[kind] - self._polling_rate = ord(pair_info[2:3]) + if link_notification is not None: + self.online = not bool(ord(link_notification.data[0:1]) & 0x40) + self.wpid = _strhex(link_notification.data[2:3] + + link_notification.data[1:2]) + # assert link_notification.address == (0x04 if unifying else 0x03) + kind = ord(link_notification.data[0:1]) & 0x0F + self._kind = _hidpp10.DEVICE_KIND[kind] + else: + # force a reading of the wpid + pair_info = receiver.read_register(_R.receiver_info, + 0x20 + number - 1) + if pair_info: + # may be either a Unifying receiver, or an Unifying-ready receiver + self.wpid = _strhex(pair_info[3:5]) + kind = ord(pair_info[7:8]) & 0x0F + self._kind = _hidpp10.DEVICE_KIND[kind] + self._polling_rate = ord(pair_info[2:3]) - else: - # unifying protocol not supported, must be a Nano receiver - device_info = self.receiver.read_register(_R.receiver_info, 0x04) - if device_info is None: - _log.error("failed to read Nano wpid for device %d of %s", number, receiver) - raise _base.NoSuchDevice(number=number, receiver=receiver, error="read Nano wpid") + else: + # unifying protocol not supported, must be a Nano receiver + device_info = self.receiver.read_register( + _R.receiver_info, 0x04) + if device_info is None: + _log.error("failed to read Nano wpid for device %d of %s", + number, receiver) + raise _base.NoSuchDevice(number=number, + receiver=receiver, + error="read Nano wpid") - self.wpid = _strhex(device_info[3:5]) - self._polling_rate = 0 - self._power_switch = '(' + _("unknown") + ')' + self.wpid = _strhex(device_info[3:5]) + self._polling_rate = 0 + self._power_switch = '(' + _("unknown") + ')' - # the wpid is necessary to properly identify wireless link on/off notifications - # also it gets 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) + # the wpid is necessary to properly identify wireless link on/off notifications + # also it gets 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) - self.descriptor = _DESCRIPTORS.get(self.wpid) - if self.descriptor is None: - # Last chance to correctly identify the device; many Nano receivers - # do not support this call. - codename = self.receiver.read_register(_R.receiver_info, 0x40 + self.number - 1) - if codename: - codename_length = ord(codename[1:2]) - codename = codename[2:2 + codename_length] - self._codename = codename.decode('ascii') - self.descriptor = _DESCRIPTORS.get(self._codename) + self.descriptor = _DESCRIPTORS.get(self.wpid) + if self.descriptor is None: + # Last chance to correctly identify the device; many Nano receivers + # do not support this call. + codename = self.receiver.read_register(_R.receiver_info, + 0x40 + self.number - 1) + if codename: + codename_length = ord(codename[1:2]) + codename = codename[2:2 + codename_length] + self._codename = codename.decode('ascii') + self.descriptor = _DESCRIPTORS.get(self._codename) - if self.descriptor: - self._name = self.descriptor.name - self._protocol = self.descriptor.protocol - if self._codename is None: - self._codename = self.descriptor.codename - if self._kind is None: - self._kind = self.descriptor.kind + if self.descriptor: + self._name = self.descriptor.name + self._protocol = self.descriptor.protocol + if self._codename is None: + self._codename = self.descriptor.codename + if self._kind is None: + self._kind = self.descriptor.kind - if self._protocol is not None: - self.features = None if self._protocol < 2.0 else _hidpp20.FeaturesArray(self) - else: - # may be a 2.0 device; if not, it will fix itself later - self.features = _hidpp20.FeaturesArray(self) + if self._protocol is not None: + self.features = None if self._protocol < 2.0 else _hidpp20.FeaturesArray( + self) + else: + # may be a 2.0 device; if not, it will fix itself later + self.features = _hidpp20.FeaturesArray(self) - @property - def protocol(self): - if self._protocol is None and self.online is not False: - self._protocol = _base.ping(self.receiver.handle, self.number) - # if the ping failed, the peripheral is (almost) certainly offline - self.online = self._protocol is not None + @property + def protocol(self): + if self._protocol is None and self.online is not False: + self._protocol = _base.ping(self.receiver.handle, self.number) + # if the ping failed, the peripheral is (almost) certainly offline + self.online = self._protocol is not None - # if _log.isEnabledFor(_DEBUG): - # _log.debug("device %d protocol %s", self.number, self._protocol) - return self._protocol or 0 + # if _log.isEnabledFor(_DEBUG): + # _log.debug("device %d protocol %s", self.number, self._protocol) + return self._protocol or 0 - @property - def codename(self): - if self._codename is None: - codename = self.receiver.read_register(_R.receiver_info, 0x40 + self.number - 1) - if codename: - codename_length = ord(codename[1:2]) - codename = codename[2:2 + codename_length] - self._codename = codename.decode('ascii') - # if _log.isEnabledFor(_DEBUG): - # _log.debug("device %d codename %s", self.number, self._codename) - else: - self._codename = '? (%s)' % self.wpid - return self._codename + @property + def codename(self): + if self._codename is None: + codename = self.receiver.read_register(_R.receiver_info, + 0x40 + self.number - 1) + if codename: + codename_length = ord(codename[1:2]) + codename = codename[2:2 + codename_length] + self._codename = codename.decode('ascii') + # if _log.isEnabledFor(_DEBUG): + # _log.debug("device %d codename %s", self.number, self._codename) + else: + self._codename = '? (%s)' % self.wpid + return self._codename - @property - def name(self): - if self._name is None: - 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) + @property + def name(self): + if self._name is None: + 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) - @property - def kind(self): - if self._kind is None: - pair_info = self.receiver.read_register(_R.receiver_info, 0x20 + self.number - 1) - if pair_info: - kind = ord(pair_info[7:8]) & 0x0F - self._kind = _hidpp10.DEVICE_KIND[kind] - self._polling_rate = ord(pair_info[2:3]) - elif self.online and self.protocol >= 2.0: - self._kind = _hidpp20.get_kind(self) - return self._kind or '?' + @property + def kind(self): + if self._kind is None: + pair_info = self.receiver.read_register(_R.receiver_info, + 0x20 + self.number - 1) + if pair_info: + kind = ord(pair_info[7:8]) & 0x0F + self._kind = _hidpp10.DEVICE_KIND[kind] + self._polling_rate = ord(pair_info[2:3]) + elif self.online and self.protocol >= 2.0: + self._kind = _hidpp20.get_kind(self) + return self._kind or '?' - @property - def firmware(self): - if self._firmware is None and self.online: - if self.protocol >= 2.0: - self._firmware = _hidpp20.get_firmware(self) - else: - self._firmware = _hidpp10.get_firmware(self) - return self._firmware or () + @property + def firmware(self): + if self._firmware is None and self.online: + if self.protocol >= 2.0: + self._firmware = _hidpp20.get_firmware(self) + else: + self._firmware = _hidpp10.get_firmware(self) + return self._firmware or () - @property - def serial(self): - if self._serial is None: - serial = self.receiver.read_register(_R.receiver_info, 0x30 + self.number - 1) - if serial: - ps = ord(serial[9:10]) & 0x0F - self._power_switch = _hidpp10.POWER_SWITCH_LOCATION[ps] - else: - # some Nano receivers? - serial = self.receiver.read_register(0x2D5) + @property + def serial(self): + if self._serial is None: + serial = self.receiver.read_register(_R.receiver_info, + 0x30 + self.number - 1) + if serial: + ps = ord(serial[9:10]) & 0x0F + self._power_switch = _hidpp10.POWER_SWITCH_LOCATION[ps] + else: + # some Nano receivers? + serial = self.receiver.read_register(0x2D5) - if serial: - self._serial = _strhex(serial[1:5]) - else: - # fallback... - self._serial = self.receiver.serial - return self._serial or '?' + if serial: + self._serial = _strhex(serial[1:5]) + else: + # fallback... + self._serial = self.receiver.serial + return self._serial or '?' - @property - def power_switch_location(self): - if self._power_switch is None: - ps = self.receiver.read_register(_R.receiver_info, 0x30 + self.number - 1) - if ps is not None: - ps = ord(ps[9:10]) & 0x0F - self._power_switch = _hidpp10.POWER_SWITCH_LOCATION[ps] - else: - self._power_switch = '(unknown)' - return self._power_switch + @property + def power_switch_location(self): + if self._power_switch is None: + ps = self.receiver.read_register(_R.receiver_info, + 0x30 + self.number - 1) + if ps is not None: + ps = ord(ps[9:10]) & 0x0F + self._power_switch = _hidpp10.POWER_SWITCH_LOCATION[ps] + else: + self._power_switch = '(unknown)' + return self._power_switch - @property - def polling_rate(self): - if self._polling_rate is None: - pair_info = self.receiver.read_register(_R.receiver_info, 0x20 + self.number - 1) - if pair_info: - self._polling_rate = ord(pair_info[2:3]) - else: - self._polling_rate = 0 - return self._polling_rate + @property + def polling_rate(self): + if self._polling_rate is None: + pair_info = self.receiver.read_register(_R.receiver_info, + 0x20 + self.number - 1) + if pair_info: + self._polling_rate = ord(pair_info[2:3]) + else: + self._polling_rate = 0 + return self._polling_rate - @property - def keys(self): - if self._keys is None: - if self.online and self.protocol >= 2.0: - self._keys = _hidpp20.get_keys(self) or () - return self._keys + @property + def keys(self): + if self._keys is None: + if self.online and self.protocol >= 2.0: + self._keys = _hidpp20.get_keys(self) or () + return self._keys - @property - def registers(self): - if self._registers is None: - if self.descriptor and self.descriptor.registers: - self._registers = list(self.descriptor.registers) - else: - self._registers = [] - return self._registers + @property + def registers(self): + if self._registers is None: + if self.descriptor and self.descriptor.registers: + self._registers = list(self.descriptor.registers) + else: + self._registers = [] + return self._registers - @property - def settings(self): - if self._settings is None: - if self.descriptor and self.descriptor.settings: - self._settings = [s(self) for s in self.descriptor.settings] - self._settings = [s for s in self._settings if s is not None] - else: - self._settings = [] - if not self._feature_settings_checked: - self._feature_settings_checked =_check_feature_settings(self, self._settings) - return self._settings + @property + def settings(self): + if self._settings is None: + if self.descriptor and self.descriptor.settings: + self._settings = [s(self) for s in self.descriptor.settings] + self._settings = [s for s in self._settings if s is not None] + else: + self._settings = [] + if not self._feature_settings_checked: + self._feature_settings_checked = _check_feature_settings( + self, self._settings) + return self._settings - def enable_notifications(self, enable=True): - """Enable or disable device (dis)connection notifications on this + def enable_notifications(self, enable=True): + """Enable or disable device (dis)connection notifications on this receiver.""" - if not bool(self.receiver) or self.protocol >= 2.0: - return False + if not bool(self.receiver) or self.protocol >= 2.0: + return False - if enable: - set_flag_bits = ( _hidpp10.NOTIFICATION_FLAG.battery_status - | _hidpp10.NOTIFICATION_FLAG.keyboard_illumination - | _hidpp10.NOTIFICATION_FLAG.wireless - | _hidpp10.NOTIFICATION_FLAG.software_present ) - else: - set_flag_bits = 0 - ok = _hidpp10.set_notification_flags(self, set_flag_bits) - if ok is None: - _log.warn("%s: failed to %s device notifications", self, 'enable' if enable else 'disable') + if enable: + set_flag_bits = (_hidpp10.NOTIFICATION_FLAG.battery_status + | _hidpp10.NOTIFICATION_FLAG.keyboard_illumination + | _hidpp10.NOTIFICATION_FLAG.wireless + | _hidpp10.NOTIFICATION_FLAG.software_present) + else: + set_flag_bits = 0 + ok = _hidpp10.set_notification_flags(self, set_flag_bits) + if ok is None: + _log.warn("%s: failed to %s device notifications", self, + 'enable' if enable else 'disable') - flag_bits = _hidpp10.get_notification_flags(self) - flag_names = None if flag_bits is None else tuple(_hidpp10.NOTIFICATION_FLAG.flag_names(flag_bits)) - if _log.isEnabledFor(_INFO): - _log.info("%s: device notifications %s %s", self, 'enabled' if enable else 'disabled', flag_names) - return flag_bits if ok else None + flag_bits = _hidpp10.get_notification_flags(self) + flag_names = None if flag_bits is None else tuple( + _hidpp10.NOTIFICATION_FLAG.flag_names(flag_bits)) + if _log.isEnabledFor(_INFO): + _log.info("%s: device notifications %s %s", self, + 'enabled' if enable else 'disabled', flag_names) + return flag_bits if ok else None - def request(self, request_id, *params): - return _base.request(self.receiver.handle, self.number, request_id, *params) + def request(self, request_id, *params): + return _base.request(self.receiver.handle, self.number, request_id, + *params) - read_register = _hidpp10.read_register - write_register = _hidpp10.write_register + read_register = _hidpp10.read_register + write_register = _hidpp10.write_register - def feature_request(self, feature, function=0x00, *params): - if self.protocol >= 2.0: - return _hidpp20.feature_request(self, feature, function, *params) + def feature_request(self, feature, function=0x00, *params): + if self.protocol >= 2.0: + return _hidpp20.feature_request(self, feature, function, *params) - def ping(self): - """Checks if the device is online, returns True of False""" - protocol = _base.ping(self.receiver.handle, self.number) - self.online = protocol is not None - if protocol is not None: - self._protocol = protocol - return self.online + def ping(self): + """Checks if the device is online, returns True of False""" + protocol = _base.ping(self.receiver.handle, self.number) + self.online = protocol is not None + if protocol is not None: + self._protocol = protocol + return self.online - def __index__(self): - return self.number - __int__ = __index__ + def __index__(self): + return self.number - def __eq__(self, other): - return other is not None and self.kind == other.kind and self.wpid == other.wpid + __int__ = __index__ - def __ne__(self, other): - return other is None or self.kind != other.kind or self.wpid != other.wpid + def __eq__(self, other): + return other is not None and self.kind == other.kind and self.wpid == other.wpid - def __hash__(self): - return self.wpid.__hash__() + def __ne__(self, other): + return other is None or self.kind != other.kind or self.wpid != other.wpid - __bool__ = __nonzero__ = lambda self: self.wpid is not None and self.number in self.receiver + def __hash__(self): + return self.wpid.__hash__() + + __bool__ = __nonzero__ = lambda self: self.wpid is not None and self.number in self.receiver + + def __str__(self): + return '' % ( + self.number, self.wpid, self.codename or '?', self.serial) + + __unicode__ = __repr__ = __str__ - def __str__(self): - return '' % (self.number, self.wpid, self.codename or '?', self.serial) - __unicode__ = __repr__ = __str__ # # # + class Receiver(object): - """A Unifying Receiver instance. + """A Unifying Receiver instance. The paired devices are available through the sequence interface. """ - number = 0xFF - kind = None + number = 0xFF + kind = None - def __init__(self, handle, device_info): - assert handle - self.handle = handle - assert device_info - self.path = device_info.path - # USB product id, used for some Nano receivers - self.product_id = device_info.product_id - product_info = _product_information(self.product_id) - if not product_info: - raise Exception("Unknown receiver type", self.product_id) + def __init__(self, handle, device_info): + assert handle + self.handle = handle + assert device_info + self.path = device_info.path + # USB product id, used for some Nano receivers + self.product_id = device_info.product_id + product_info = _product_information(self.product_id) + if not product_info: + raise Exception("Unknown receiver type", self.product_id) - # read the serial immediately, so we can find out max_devices - serial_reply = self.read_register(_R.receiver_info, 0x03) - if serial_reply : - self.serial = _strhex(serial_reply[1:5]) - self.max_devices = ord(serial_reply[6:7]) - # TODO _properly_ figure out which receivers do and which don't support unpairing - # This code supposes that receivers that don't unpair support a pairing request for device index 0 - self.may_unpair = self.write_register(_R.receiver_pairing) is None - else: # handle receivers that don't have a serial number specially (i.e., c534) - self.serial = None - self.max_devices = product_info.get('max_devices',1) - self.may_unpair = product_info.get('may_unpair',False) + # read the serial immediately, so we can find out max_devices + serial_reply = self.read_register(_R.receiver_info, 0x03) + if serial_reply: + self.serial = _strhex(serial_reply[1:5]) + self.max_devices = ord(serial_reply[6:7]) + # TODO _properly_ figure out which receivers do and which don't support unpairing + # This code supposes that receivers that don't unpair support a pairing request for device index 0 + self.may_unpair = self.write_register(_R.receiver_pairing) is None + else: # handle receivers that don't have a serial number specially (i.e., c534) + self.serial = None + self.max_devices = product_info.get('max_devices', 1) + self.may_unpair = product_info.get('may_unpair', False) - self.name = product_info.get('name','') - 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', '') + 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 - self._devices = {} - self._remaining_pairings = None + self._firmware = None + self._devices = {} + self._remaining_pairings = None - def close(self): - handle, self.handle = self.handle, None - self._devices.clear() - return (handle and _base.close(handle)) + def close(self): + handle, self.handle = self.handle, None + self._devices.clear() + return (handle and _base.close(handle)) - def __del__(self): - self.close() + def __del__(self): + self.close() - @property - def firmware(self): - if self._firmware is None and self.handle: - self._firmware = _hidpp10.get_firmware(self) - return self._firmware + @property + def firmware(self): + if self._firmware is None and self.handle: + self._firmware = _hidpp10.get_firmware(self) + return self._firmware - # how many pairings remain (None for unknown, -1 for unlimited) - def remaining_pairings(self,cache=True): - if self._remaining_pairings is None or not cache: - ps = self.read_register(_R.receiver_connection) - if ps is not None: - ps = ord(ps[2:3]) - self._remaining_pairings = ps-5 if ps >= 5 else -1 - return self._remaining_pairings + # how many pairings remain (None for unknown, -1 for unlimited) + def remaining_pairings(self, cache=True): + if self._remaining_pairings is None or not cache: + ps = self.read_register(_R.receiver_connection) + if ps is not None: + ps = ord(ps[2:3]) + self._remaining_pairings = ps - 5 if ps >= 5 else -1 + return self._remaining_pairings - def enable_notifications(self, enable=True): - """Enable or disable device (dis)connection notifications on this + def enable_notifications(self, enable=True): + """Enable or disable device (dis)connection notifications on this receiver.""" - if not self.handle: - return False + if not self.handle: + return False - if enable: - set_flag_bits = ( _hidpp10.NOTIFICATION_FLAG.battery_status - | _hidpp10.NOTIFICATION_FLAG.wireless - | _hidpp10.NOTIFICATION_FLAG.software_present ) - else: - set_flag_bits = 0 - ok = _hidpp10.set_notification_flags(self, set_flag_bits) - if ok is None: - _log.warn("%s: failed to %s receiver notifications", self, 'enable' if enable else 'disable') - return None + if enable: + set_flag_bits = (_hidpp10.NOTIFICATION_FLAG.battery_status + | _hidpp10.NOTIFICATION_FLAG.wireless + | _hidpp10.NOTIFICATION_FLAG.software_present) + else: + set_flag_bits = 0 + ok = _hidpp10.set_notification_flags(self, set_flag_bits) + if ok is None: + _log.warn("%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.NOTIFICATION_FLAG.flag_names(flag_bits)) - if _log.isEnabledFor(_INFO): - _log.info("%s: receiver notifications %s => %s", self, 'enabled' if enable else 'disabled', flag_names) - return flag_bits + flag_bits = _hidpp10.get_notification_flags(self) + flag_names = None if flag_bits is None else tuple( + _hidpp10.NOTIFICATION_FLAG.flag_names(flag_bits)) + if _log.isEnabledFor(_INFO): + _log.info("%s: receiver notifications %s => %s", self, + 'enabled' if enable else 'disabled', flag_names) + return flag_bits - def notify_devices(self): - """Scan all devices.""" - if self.handle: - if not self.write_register(_R.receiver_connection, 0x02): - _log.warn("%s: failed to trigger device link notifications", self) + def notify_devices(self): + """Scan all devices.""" + if self.handle: + if not self.write_register(_R.receiver_connection, 0x02): + _log.warn("%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)) + 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)) - assert notification is None or notification.devnumber == number - assert notification is None or notification.sub_id == 0x41 + assert notification is None or notification.devnumber == number + assert notification is None or notification.sub_id == 0x41 - try: - dev = PairedDevice(self, number, notification) - assert dev.wpid - if _log.isEnabledFor(_INFO): - _log.info("%s: found new device %d (%s)", self, number, dev.wpid) - self._devices[number] = dev - return dev - except _base.NoSuchDevice: - _log.exception("register_new_device") + try: + dev = PairedDevice(self, number, notification) + assert dev.wpid + if _log.isEnabledFor(_INFO): + _log.info("%s: found new device %d (%s)", self, number, + dev.wpid) + self._devices[number] = dev + return dev + except _base.NoSuchDevice: + _log.exception("register_new_device") - _log.warning("%s: looked for device %d, not found", self, number) - self._devices[number] = None + _log.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): - if self.handle: - action = 0x02 if lock_closed else 0x01 - reply = self.write_register(_R.receiver_pairing, action, device, timeout) - if reply: - return True - _log.warn("%s: failed to %s the receiver lock", self, 'close' if lock_closed else 'open') + def set_lock(self, lock_closed=True, device=0, timeout=0): + if self.handle: + action = 0x02 if lock_closed else 0x01 + reply = self.write_register(_R.receiver_pairing, action, device, + timeout) + if reply: + return True + _log.warn("%s: failed to %s the receiver lock", self, + 'close' if lock_closed else 'open') - def count(self): - count = self.read_register(_R.receiver_connection) - return 0 if count is None else ord(count[1:2]) + def count(self): + count = self.read_register(_R.receiver_connection) + return 0 if count is None else ord(count[1:2]) - # def has_devices(self): - # return len(self) > 0 or self.count() > 0 + # def has_devices(self): + # return len(self) > 0 or self.count() > 0 - def request(self, request_id, *params): - if bool(self): - return _base.request(self.handle, 0xFF, request_id, *params) + def request(self, request_id, *params): + if bool(self): + return _base.request(self.handle, 0xFF, request_id, *params) - read_register = _hidpp10.read_register - write_register = _hidpp10.write_register + read_register = _hidpp10.read_register + write_register = _hidpp10.write_register - def __iter__(self): - for number in range(1, 1 + self.max_devices): - if number in self._devices: - dev = self._devices[number] - else: - dev = self.__getitem__(number) - if dev is not None: - yield dev + def __iter__(self): + for number in range(1, 1 + self.max_devices): + if number in self._devices: + dev = self._devices[number] + else: + dev = self.__getitem__(number) + if dev is not None: + yield dev - def __getitem__(self, key): - if not bool(self): - return None + def __getitem__(self, key): + if not bool(self): + return None - dev = self._devices.get(key) - if dev is not None: - return dev + dev = self._devices.get(key) + if dev is not None: + return dev - if not isinstance(key, int): - raise TypeError('key must be an integer') - if key < 1 or key > self.max_devices: - raise IndexError(key) + if not isinstance(key, int): + raise TypeError('key must be an integer') + if key < 1 or key > self.max_devices: + raise IndexError(key) - return self.register_new_device(key) + return self.register_new_device(key) - def __delitem__(self, key): - self._unpair_device(key, False) + def __delitem__(self, key): + self._unpair_device(key, False) - def _unpair_device(self, key, force=False): - key = int(key) + def _unpair_device(self, key, force=False): + key = int(key) - if self._devices.get(key) is None: - raise IndexError(key) + if self._devices.get(key) is None: + raise IndexError(key) - dev = self._devices[key] - if not dev: - if key in self._devices: - del self._devices[key] - return + dev = self._devices[key] + if not dev: + if key in self._devices: + del self._devices[key] + return - if self.re_pairs and not force: - # invalidate the device, but these receivers don't unpair per se - dev.online = False - dev.wpid = None - if key in self._devices: - del self._devices[key] - _log.warn("%s removed device %s", self, dev) - else: - reply = self.write_register(_R.receiver_pairing, 0x03, key) - if reply: - # invalidate the device - dev.online = False - dev.wpid = None - if key in self._devices: - del self._devices[key] - _log.warn("%s unpaired device %s", self, dev) - else: - _log.error("%s failed to unpair device %s", self, dev) - raise IndexError(key) + if self.re_pairs and not force: + # invalidate the device, but these receivers don't unpair per se + dev.online = False + dev.wpid = None + if key in self._devices: + del self._devices[key] + _log.warn("%s removed device %s", self, dev) + else: + reply = self.write_register(_R.receiver_pairing, 0x03, key) + if reply: + # invalidate the device + dev.online = False + dev.wpid = None + if key in self._devices: + del self._devices[key] + _log.warn("%s unpaired device %s", self, dev) + else: + _log.error("%s failed to unpair device %s", self, dev) + raise IndexError(key) - def __len__(self): - return len([d for d in self._devices.values() if d is not None]) + def __len__(self): + return len([d for d in self._devices.values() if d is not None]) - def __contains__(self, dev): - if isinstance(dev, int): - return self._devices.get(dev) is not None + def __contains__(self, dev): + if isinstance(dev, int): + return self._devices.get(dev) is not None - return self.__contains__(dev.number) + return self.__contains__(dev.number) - def __eq__(self, other): - return other is not None and self.kind == other.kind and self.path == other.path + def __eq__(self, other): + return other is not None and self.kind == other.kind and self.path == other.path - def __ne__(self, other): - return other is None or self.kind != other.kind or self.path != other.path + def __ne__(self, other): + return other is None or self.kind != other.kind or self.path != other.path - def __hash__(self): - return self.path.__hash__() + def __hash__(self): + return self.path.__hash__() - def __str__(self): - return self._str - __unicode__ = __repr__ = __str__ + def __str__(self): + return self._str - __bool__ = __nonzero__ = lambda self: self.handle is not None + __unicode__ = __repr__ = __str__ - @classmethod - def open(self, device_info): - """Opens a Logitech Receiver found attached to the machine, by Linux device path. + __bool__ = __nonzero__ = lambda self: self.handle is not None + + @classmethod + def open(self, device_info): + """Opens a Logitech Receiver found attached to the machine, by Linux device path. :returns: An open file handle for the found receiver, or ``None``. """ - try: - handle = _base.open_path(device_info.path) - if handle: - return Receiver(handle, device_info) - except OSError as e: - _log.exception("open %s", device_info) - if e.errno == _errno.EACCES: - raise - except: - _log.exception("open %s", device_info) + try: + handle = _base.open_path(device_info.path) + if handle: + return Receiver(handle, device_info) + except OSError as e: + _log.exception("open %s", device_info) + if e.errno == _errno.EACCES: + raise + except: + _log.exception("open %s", device_info) diff --git a/lib/logitech_receiver/settings.py b/lib/logitech_receiver/settings.py index 928b4fe0..84d79d46 100644 --- a/lib/logitech_receiver/settings.py +++ b/lib/logitech_receiver/settings.py @@ -27,447 +27,502 @@ from copy import copy as _copy import math from .common import ( - NamedInt as _NamedInt, - NamedInts as _NamedInts, - bytes2int as _bytes2int, - int2bytes as _int2bytes, - ) + NamedInt as _NamedInt, + NamedInts as _NamedInts, + bytes2int as _bytes2int, + int2bytes as _int2bytes, +) # # # -KIND = _NamedInts(toggle=0x01, choice=0x02, range=0x04, map_choice=0x0A, multiple_toggle=0x10) +KIND = _NamedInts(toggle=0x01, + choice=0x02, + range=0x04, + map_choice=0x0A, + multiple_toggle=0x10) + class Setting(object): - """A setting descriptor. + """A setting descriptor. Needs to be instantiated for each specific device.""" - __slots__ = ('name', 'label', 'description', 'kind', 'device_kind', 'feature', - '_rw', '_validator', '_device', '_value') + __slots__ = ('name', 'label', 'description', 'kind', 'device_kind', + 'feature', '_rw', '_validator', '_device', '_value') - def __init__(self, name, rw, validator, kind=None, label=None, description=None, device_kind=None, feature=None): - assert name - self.name = name - self.label = label or name - self.description = description - self.device_kind = device_kind - self.feature = feature + def __init__(self, + name, + rw, + validator, + kind=None, + label=None, + description=None, + device_kind=None, + feature=None): + assert name + self.name = name + self.label = label or name + self.description = description + self.device_kind = device_kind + self.feature = feature - self._rw = rw - self._validator = validator + self._rw = rw + self._validator = validator - assert kind is None or kind & validator.kind != 0 - self.kind = kind or validator.kind + assert kind is None or kind & validator.kind != 0 + self.kind = kind or validator.kind - def __call__(self, device): - assert not hasattr(self, '_value') - # combined keyboards and touchpads (e.g., K400) break this assertion so don't use it - # assert self.device_kind is None or device.kind in self.device_kind - p = device.protocol - if p == 1.0: - # HID++ 1.0 devices do not support features - assert self._rw.kind == RegisterRW.kind - elif p >= 2.0: - # HID++ 2.0 devices do not support registers - assert self._rw.kind == FeatureRW.kind + def __call__(self, device): + assert not hasattr(self, '_value') + # combined keyboards and touchpads (e.g., K400) break this assertion so don't use it + # assert self.device_kind is None or device.kind in self.device_kind + p = device.protocol + if p == 1.0: + # HID++ 1.0 devices do not support features + assert self._rw.kind == RegisterRW.kind + elif p >= 2.0: + # HID++ 2.0 devices do not support registers + assert self._rw.kind == FeatureRW.kind - o = _copy(self) - o._value = None - o._device = device - return o + o = _copy(self) + o._value = None + o._device = device + return o - @property - def choices(self): - assert hasattr(self, '_value') - assert hasattr(self, '_device') + @property + def choices(self): + assert hasattr(self, '_value') + assert hasattr(self, '_device') - return self._validator.choices if self._validator.kind & KIND.choice else None + return self._validator.choices if self._validator.kind & KIND.choice else None - @property - def range(self): - assert hasattr(self, '_value') - assert hasattr(self, '_device') + @property + def range(self): + assert hasattr(self, '_value') + assert hasattr(self, '_device') - if self._validator.kind == KIND.range: - return (self._validator.min_value, self._validator.max_value) + if self._validator.kind == KIND.range: + return (self._validator.min_value, self._validator.max_value) - def read(self, cached=True): - assert hasattr(self, '_value') - assert hasattr(self, '_device') + def read(self, cached=True): + assert hasattr(self, '_value') + assert hasattr(self, '_device') - if _log.isEnabledFor(_DEBUG): - _log.debug("%s: settings read %r from %s", self.name, self._value, self._device) + if _log.isEnabledFor(_DEBUG): + _log.debug("%s: settings read %r from %s", self.name, self._value, + self._device) - if self._value is None and self._device.persister: - # We haven't read a value from the device yet, - # maybe we have something in the configuration. - self._value = self._device.persister.get(self.name) + if self._value is None and self._device.persister: + # We haven't read a value from the device yet, + # maybe we have something in the configuration. + self._value = self._device.persister.get(self.name) - if cached and self._value is not None: - if self._device.persister and self.name not in self._device.persister: - # If this is a new device (or a new setting for an old device), - # make sure to save its current value for the next time. - self._device.persister[self.name] = self._value - return self._value + if cached and self._value is not None: + if self._device.persister and self.name not in self._device.persister: + # If this is a new device (or a new setting for an old device), + # make sure to save its current value for the next time. + self._device.persister[self.name] = self._value + return self._value - if self._device.online: - reply = self._rw.read(self._device) - if reply: - self._value = self._validator.validate_read(reply) - if self._device.persister and self.name not in self._device.persister: - # Don't update the persister if it already has a value, - # otherwise the first read might overwrite the value we wanted. - self._device.persister[self.name] = self._value - return self._value + if self._device.online: + reply = self._rw.read(self._device) + if reply: + self._value = self._validator.validate_read(reply) + if self._device.persister and self.name not in self._device.persister: + # Don't update the persister if it already has a value, + # otherwise the first read might overwrite the value we wanted. + self._device.persister[self.name] = self._value + return self._value - def write(self, value): - assert hasattr(self, '_value') - assert hasattr(self, '_device') - assert value is not None + def write(self, value): + assert hasattr(self, '_value') + assert hasattr(self, '_device') + assert value is not None - if _log.isEnabledFor(_DEBUG): - _log.debug("%s: settings write %r to %s", self.name, value, self._device) + if _log.isEnabledFor(_DEBUG): + _log.debug("%s: settings write %r to %s", self.name, value, + self._device) - if self._device.online: - # Remember the value we're trying to set, even if the write fails. - # This way even if the device is offline or some other error occurs, - # the last value we've tried to write is remembered in the configuration. - self._value = value - if self._device.persister: - self._device.persister[self.name] = value + if self._device.online: + # Remember the value we're trying to set, even if the write fails. + # This way even if the device is offline or some other error occurs, + # the last value we've tried to write is remembered in the configuration. + self._value = value + if self._device.persister: + self._device.persister[self.name] = value - current_value = None - if self._validator.needs_current_value: - # the validator needs the current value, possibly to merge flag values - current_value = self._rw.read(self._device) + current_value = None + if self._validator.needs_current_value: + # the validator needs the current value, possibly to merge flag values + current_value = self._rw.read(self._device) - data_bytes = self._validator.prepare_write(value, current_value) - if data_bytes is not None: - if _log.isEnabledFor(_DEBUG): - _log.debug("%s: settings prepare write(%s) => %r", self.name, value, data_bytes) + data_bytes = self._validator.prepare_write(value, current_value) + if data_bytes is not None: + if _log.isEnabledFor(_DEBUG): + _log.debug("%s: settings prepare write(%s) => %r", + self.name, value, data_bytes) - reply = self._rw.write(self._device, data_bytes) - if not reply: - # tell whomever is calling that the write failed - return None + reply = self._rw.write(self._device, data_bytes) + if not reply: + # tell whomever is calling that the write failed + return None - return value + return value - def apply(self): - assert hasattr(self, '_value') - assert hasattr(self, '_device') + def apply(self): + assert hasattr(self, '_value') + assert hasattr(self, '_device') - if _log.isEnabledFor(_DEBUG): - _log.debug("%s: apply %s (%s)", self.name, self._value, self._device) + if _log.isEnabledFor(_DEBUG): + _log.debug("%s: apply %s (%s)", self.name, self._value, + self._device) - value = self.read() - if value is not None: - self.write(value) + value = self.read() + if value is not None: + self.write(value) - def __str__(self): - if hasattr(self, '_value'): - assert hasattr(self, '_device') - return '' % (self._rw.kind, self._validator.kind, self._device.codename, self.name, self._value) - return '' % (self._rw.kind, self._validator.kind, self.name) - __unicode__ = __repr__ = __str__ + def __str__(self): + if hasattr(self, '_value'): + assert hasattr(self, '_device') + return '' % ( + self._rw.kind, self._validator.kind, self._device.codename, + self.name, self._value) + return '' % (self._rw.kind, self._validator.kind, + self.name) + + __unicode__ = __repr__ = __str__ class Settings(Setting): - """A setting descriptor for multiple choices, being a map from keys to values. + """A setting descriptor for multiple choices, being a map from keys to values. Needs to be instantiated for each specific device.""" + def read(self, cached=True): + assert hasattr(self, '_value') + assert hasattr(self, '_device') - def read(self, cached=True): - assert hasattr(self, '_value') - assert hasattr(self, '_device') + if _log.isEnabledFor(_DEBUG): + _log.debug("%s: settings read %r from %s", self.name, self._value, + self._device) - if _log.isEnabledFor(_DEBUG): - _log.debug("%s: settings read %r from %s", self.name, self._value, self._device) + if self._value is None and getattr(self._device, 'persister', None): + # We haven't read a value from the device yet, + # maybe we have something in the configuration. + self._value = self._device.persister.get(self.name) - if self._value is None and getattr(self._device,'persister',None): - # We haven't read a value from the device yet, - # maybe we have something in the configuration. - self._value = self._device.persister.get(self.name) + if cached and self._value is not None: + if getattr(self._device, 'persister', + None) and self.name not in self._device.persister: + # If this is a new device (or a new setting for an old device), + # make sure to save its current value for the next time. + self._device.persister[self.name] = self._value + return self._value - if cached and self._value is not None: - if getattr(self._device,'persister',None) and self.name not in self._device.persister: - # If this is a new device (or a new setting for an old device), - # make sure to save its current value for the next time. - self._device.persister[self.name] = self._value - return self._value + if self._device.online: + reply_map = dict() + for key, value in self._validator.choices.items(): + reply = self._rw.read(self._device, key) + if reply: + # keys are ints, because that is what the device uses, + # encoded into strings because JSON requires strings as keys + reply_map[str(int(key))] = self._validator.validate_read( + reply, key) + self._value = reply_map + if getattr(self._device, 'persister', + None) and self.name not in self._device.persister: + # Don't update the persister if it already has a value, + # otherwise the first read might overwrite the value we wanted. + self._device.persister[self.name] = self._value + return self._value - if self._device.online: - reply_map = dict() - for key, value in self._validator.choices.items(): - reply = self._rw.read(self._device, key) - if reply: - # keys are ints, because that is what the device uses, - # encoded into strings because JSON requires strings as keys - reply_map[str(int(key))] = self._validator.validate_read(reply,key) - self._value = reply_map - if getattr(self._device,'persister',None) and self.name not in self._device.persister: - # Don't update the persister if it already has a value, - # otherwise the first read might overwrite the value we wanted. - self._device.persister[self.name] = self._value - return self._value + def read_key(self, key, cached=True): + assert hasattr(self, '_value') + assert hasattr(self, '_device') + assert key is not None - def read_key(self, key, cached=True): - assert hasattr(self, '_value') - assert hasattr(self, '_device') - assert key is not None + if _log.isEnabledFor(_DEBUG): + _log.debug("%s: settings read %r key %r from %s", self.name, + self._value, key, self._device) - if _log.isEnabledFor(_DEBUG): - _log.debug("%s: settings read %r key %r from %s", self.name, self._value, key, self._device) + if self._value is None and getattr(self._device, 'persister', None): + self._value = self._device.persister.get(self.name) - if self._value is None and getattr(self._device,'persister',None): - self._value = self._device.persister.get(self.name) + if cached and self._value is not None: + if getattr(self._device, 'persister', + None) and self.name not in self._device.persister: + self._device.persister[self.name] = self._value + return self._value[str(int(key))] - if cached and self._value is not None: - if getattr(self._device,'persister',None) and self.name not in self._device.persister: - self._device.persister[self.name] = self._value - return self._value[str(int(key))] + if self._device.online: + reply = self._rw.read(self._device, key) + if reply: + self._value[str(int(key))] = self._validator.validate_read( + reply, key) + if getattr(self._device, 'persister', + None) and self.name not in self._device.persister: + self._device.persister[self.name] = self._value + return self._value[str(int(key))] - if self._device.online: - reply = self._rw.read(self._device, key) - if reply: - self._value[str(int(key))] = self._validator.validate_read(reply,key) - if getattr(self._device,'persister',None) and self.name not in self._device.persister: - self._device.persister[self.name] = self._value - return self._value[str(int(key))] + def write(self, map): + assert hasattr(self, '_value') + assert hasattr(self, '_device') + assert map is not None - def write(self, map): - assert hasattr(self, '_value') - assert hasattr(self, '_device') - assert map is not None + if _log.isEnabledFor(_DEBUG): + _log.debug("%s: settings write %r to %s", self.name, map, + self._device) - if _log.isEnabledFor(_DEBUG): - _log.debug("%s: settings write %r to %s", self.name, map, self._device) + if self._device.online: + # Remember the value we're trying to set, even if the write fails. + # This way even if the device is offline or some other error occurs, + # the last value we've tried to write is remembered in the configuration. + self._value = map + if self._device.persister: + self._device.persister[self.name] = map - if self._device.online: - # Remember the value we're trying to set, even if the write fails. - # This way even if the device is offline or some other error occurs, - # the last value we've tried to write is remembered in the configuration. - self._value = map - if self._device.persister: - self._device.persister[self.name] = map + for key, value in map.items(): + data_bytes = self._validator.prepare_write(int(key), value) + if data_bytes is not None: + if _log.isEnabledFor(_DEBUG): + _log.debug( + "%s: settings prepare map write(%s,%s) => %r", + self.name, key, value, data_bytes) + reply = self._rw.write(self._device, int(key), data_bytes) + if not reply: + return None - for key, value in map.items(): - data_bytes = self._validator.prepare_write(int(key), value) - if data_bytes is not None: - if _log.isEnabledFor(_DEBUG): - _log.debug("%s: settings prepare map write(%s,%s) => %r", self.name, key, value, data_bytes) - reply = self._rw.write(self._device, int(key), data_bytes) - if not reply: - return None + return map - return map + def write_key_value(self, key, value): + assert hasattr(self, '_value') + assert hasattr(self, '_device') + assert key is not None + assert value is not None - def write_key_value(self, key, value): - assert hasattr(self, '_value') - assert hasattr(self, '_device') - assert key is not None - assert value is not None + if _log.isEnabledFor(_DEBUG): + _log.debug("%s: settings write key %r value %r to %s", self.name, + key, value, self._device) - if _log.isEnabledFor(_DEBUG): - _log.debug("%s: settings write key %r value %r to %s", self.name, key, value, self._device) + if self._device.online: + # Remember the value we're trying to set, even if the write fails. + # This way even if the device is offline or some other error occurs, + # the last value we've tried to write is remembered in the configuration. + self._value[str(key)] = value + if self._device.persister: + self._device.persister[self.name] = self._value - if self._device.online: - # Remember the value we're trying to set, even if the write fails. - # This way even if the device is offline or some other error occurs, - # the last value we've tried to write is remembered in the configuration. - self._value[str(key)] = value - if self._device.persister: - self._device.persister[self.name] = self._value + data_bytes = self._validator.prepare_write(int(key), value) + if data_bytes is not None: + if _log.isEnabledFor(_DEBUG): + _log.debug( + "%s: settings prepare key value write(%s,%s) => %r", + self.name, key, value, data_bytes) + reply = self._rw.write(self._device, int(key), data_bytes) + if not reply: + # tell whomever is calling that the write failed + return None - data_bytes = self._validator.prepare_write(int(key), value) - if data_bytes is not None: - if _log.isEnabledFor(_DEBUG): - _log.debug("%s: settings prepare key value write(%s,%s) => %r", self.name, key, value, data_bytes) - reply = self._rw.write(self._device, int(key), data_bytes) - if not reply: - # tell whomever is calling that the write failed - return None - - return value + return value class BitFieldSetting(Setting): - """A setting descriptor for a set of choices represented by one bit each, being a map from options to booleans. + """A setting descriptor for a set of choices represented by one bit each, being a map from options to booleans. Needs to be instantiated for each specific device.""" + def read(self, cached=True): + assert hasattr(self, '_value') + assert hasattr(self, '_device') - def read(self, cached=True): - assert hasattr(self, '_value') - assert hasattr(self, '_device') + if _log.isEnabledFor(_DEBUG): + _log.debug("%s: settings read %r from %s", self.name, self._value, + self._device) - if _log.isEnabledFor(_DEBUG): - _log.debug("%s: settings read %r from %s", self.name, self._value, self._device) + if self._value is None and getattr(self._device, 'persister', None): + # We haven't read a value from the device yet, + # maybe we have something in the configuration. + self._value = self._device.persister.get(self.name) - if self._value is None and getattr(self._device,'persister',None): - # We haven't read a value from the device yet, - # maybe we have something in the configuration. - self._value = self._device.persister.get(self.name) + if cached and self._value is not None: + if getattr(self._device, 'persister', + None) and self.name not in self._device.persister: + # If this is a new device (or a new setting for an old device), + # make sure to save its current value for the next time. + self._device.persister[self.name] = self._value + return self._value - if cached and self._value is not None: - if getattr(self._device,'persister',None) and self.name not in self._device.persister: - # If this is a new device (or a new setting for an old device), - # make sure to save its current value for the next time. - self._device.persister[self.name] = self._value - return self._value + if self._device.online: + reply_map = dict() + reply = self._rw.read(self._device) + if reply: + # keys are ints, because that is what the device uses, + # encoded into strings because JSON requires strings as keys + reply_map = self._validator.validate_read(reply) + self._value = reply_map + if getattr(self._device, 'persister', + None) and self.name not in self._device.persister: + # Don't update the persister if it already has a value, + # otherwise the first read might overwrite the value we wanted. + self._device.persister[self.name] = self._value + return self._value - if self._device.online: - reply_map = dict() - reply = self._rw.read(self._device) - if reply: - # keys are ints, because that is what the device uses, - # encoded into strings because JSON requires strings as keys - reply_map = self._validator.validate_read(reply) - self._value = reply_map - if getattr(self._device,'persister',None) and self.name not in self._device.persister: - # Don't update the persister if it already has a value, - # otherwise the first read might overwrite the value we wanted. - self._device.persister[self.name] = self._value - return self._value + def read_key(self, key, cached=True): + assert hasattr(self, '_value') + assert hasattr(self, '_device') + assert key is not None - def read_key(self, key, cached=True): - assert hasattr(self, '_value') - assert hasattr(self, '_device') - assert key is not None + if _log.isEnabledFor(_DEBUG): + _log.debug("%s: settings read %r key %r from %s", self.name, + self._value, key, self._device) - if _log.isEnabledFor(_DEBUG): - _log.debug("%s: settings read %r key %r from %s", self.name, self._value, key, self._device) + if self._value is None and getattr(self._device, 'persister', None): + self._value = self._device.persister.get(self.name) - if self._value is None and getattr(self._device,'persister',None): - self._value = self._device.persister.get(self.name) + if cached and self._value is not None: + if getattr(self._device, 'persister', + None) and self.name not in self._device.persister: + self._device.persister[self.name] = self._value + return self._value[str(int(key))] - if cached and self._value is not None: - if getattr(self._device,'persister',None) and self.name not in self._device.persister: - self._device.persister[self.name] = self._value - return self._value[str(int(key))] + if self._device.online: + reply = self._rw.read(self._device, key) + if reply: + self._value = self._validator.validate_read(reply) + if getattr(self._device, 'persister', + None) and self.name not in self._device.persister: + self._device.persister[self.name] = self._value + return self._value[str(int(key))] - if self._device.online: - reply = self._rw.read(self._device, key) - if reply: - self._value = self._validator.validate_read(reply) - if getattr(self._device,'persister',None) and self.name not in self._device.persister: - self._device.persister[self.name] = self._value - return self._value[str(int(key))] + def write(self, map): + assert hasattr(self, '_value') + assert hasattr(self, '_device') + assert map is not None - def write(self, map): - assert hasattr(self, '_value') - assert hasattr(self, '_device') - assert map is not None + if _log.isEnabledFor(_DEBUG): + _log.debug("%s: settings write %r to %s", self.name, map, + self._device) - if _log.isEnabledFor(_DEBUG): - _log.debug("%s: settings write %r to %s", self.name, map, self._device) + if self._device.online: + # Remember the value we're trying to set, even if the write fails. + # This way even if the device is offline or some other error occurs, + # the last value we've tried to write is remembered in the configuration. + self._value = map + if self._device.persister: + self._device.persister[self.name] = map + data_bytes = self._validator.prepare_write(self._value) + if data_bytes is not None: + if _log.isEnabledFor(_DEBUG): + _log.debug("%s: settings prepare map write(%s) => %r", + self.name, self._value, data_bytes) + reply = self._rw.write(self._device, data_bytes) + if not reply: + return None + return map - if self._device.online: - # Remember the value we're trying to set, even if the write fails. - # This way even if the device is offline or some other error occurs, - # the last value we've tried to write is remembered in the configuration. - self._value = map - if self._device.persister: - self._device.persister[self.name] = map - data_bytes = self._validator.prepare_write(self._value) - if data_bytes is not None: - if _log.isEnabledFor(_DEBUG): - _log.debug("%s: settings prepare map write(%s) => %r", self.name, self._value, data_bytes) - reply = self._rw.write(self._device, data_bytes) - if not reply: - return None - return map + def write_key_value(self, key, value): + assert hasattr(self, '_value') + assert hasattr(self, '_device') + assert key is not None + assert value is not None - def write_key_value(self, key, value): - assert hasattr(self, '_value') - assert hasattr(self, '_device') - assert key is not None - assert value is not None + if _log.isEnabledFor(_DEBUG): + _log.debug("%s: settings write key %r value %r to %s", self.name, + key, value, self._device) - if _log.isEnabledFor(_DEBUG): - _log.debug("%s: settings write key %r value %r to %s", self.name, key, value, self._device) + if self._device.online: + # Remember the value we're trying to set, even if the write fails. + # This way even if the device is offline or some other error occurs, + # the last value we've tried to write is remembered in the configuration. + value = bool(value) + self._value[str(key)] = value + if self._device.persister: + self._device.persister[self.name] = self._value - if self._device.online: - # Remember the value we're trying to set, even if the write fails. - # This way even if the device is offline or some other error occurs, - # the last value we've tried to write is remembered in the configuration. - value = bool(value) - self._value[str(key)] = value - if self._device.persister: - self._device.persister[self.name] = self._value + data_bytes = self._validator.prepare_write(self._value) + if data_bytes is not None: + if _log.isEnabledFor(_DEBUG): + _log.debug( + "%s: settings prepare key value write(%s,%s) => %r", + self.name, key, str(value), data_bytes) + reply = self._rw.write(self._device, data_bytes) + if not reply: + # tell whomever is calling that the write failed + return None - data_bytes = self._validator.prepare_write(self._value) - if data_bytes is not None: - if _log.isEnabledFor(_DEBUG): - _log.debug("%s: settings prepare key value write(%s,%s) => %r", self.name, key, str(value), data_bytes) - reply = self._rw.write(self._device, data_bytes) - if not reply: - # tell whomever is calling that the write failed - return None + return value - return value # # read/write low-level operators # + class RegisterRW(object): - __slots__ = ('register', ) + __slots__ = ('register', ) - kind = _NamedInt(0x01, 'register') + kind = _NamedInt(0x01, 'register') - def __init__(self, register): - assert isinstance(register, int) - self.register = register + def __init__(self, register): + assert isinstance(register, int) + self.register = register - def read(self, device): - return device.read_register(self.register) + def read(self, device): + return device.read_register(self.register) - def write(self, device, data_bytes): - return device.write_register(self.register, data_bytes) + def write(self, device, data_bytes): + return device.write_register(self.register, data_bytes) class FeatureRW(object): - __slots__ = ('feature', 'read_fnid', 'write_fnid') + __slots__ = ('feature', 'read_fnid', 'write_fnid') - kind = _NamedInt(0x02, 'feature') - default_read_fnid = 0x00 - default_write_fnid = 0x10 + kind = _NamedInt(0x02, 'feature') + default_read_fnid = 0x00 + default_write_fnid = 0x10 - def __init__(self, feature, read_fnid=default_read_fnid, write_fnid=default_write_fnid): - assert isinstance(feature, _NamedInt) - self.feature = feature - self.read_fnid = read_fnid - self.write_fnid = write_fnid + def __init__(self, + feature, + read_fnid=default_read_fnid, + write_fnid=default_write_fnid): + assert isinstance(feature, _NamedInt) + self.feature = feature + self.read_fnid = read_fnid + self.write_fnid = write_fnid - def read(self, device): - assert self.feature is not None - return device.feature_request(self.feature, self.read_fnid) + def read(self, device): + assert self.feature is not None + return device.feature_request(self.feature, self.read_fnid) - def write(self, device, data_bytes): - assert self.feature is not None - return device.feature_request(self.feature, self.write_fnid, data_bytes) + def write(self, device, data_bytes): + assert self.feature is not None + return device.feature_request(self.feature, self.write_fnid, + data_bytes) class FeatureRWMap(FeatureRW): - kind = _NamedInt(0x02, 'feature') - default_read_fnid = 0x00 - default_write_fnid = 0x10 - default_key_bytes = 1 + kind = _NamedInt(0x02, 'feature') + default_read_fnid = 0x00 + default_write_fnid = 0x10 + default_key_bytes = 1 - def __init__(self, feature, read_fnid=default_read_fnid, write_fnid=default_write_fnid, key_bytes=default_key_bytes): - assert isinstance(feature, _NamedInt) - self.feature = feature - self.read_fnid = read_fnid - self.write_fnid = write_fnid - self.key_bytes = key_bytes + def __init__(self, + feature, + read_fnid=default_read_fnid, + write_fnid=default_write_fnid, + key_bytes=default_key_bytes): + assert isinstance(feature, _NamedInt) + self.feature = feature + self.read_fnid = read_fnid + self.write_fnid = write_fnid + self.key_bytes = key_bytes - def read(self, device, key): - assert self.feature is not None - key_bytes = _int2bytes(key, self.key_bytes) - return device.feature_request(self.feature, self.read_fnid, key_bytes) + def read(self, device, key): + assert self.feature is not None + key_bytes = _int2bytes(key, self.key_bytes) + return device.feature_request(self.feature, self.read_fnid, key_bytes) - def write(self, device, key, data_bytes): - assert self.feature is not None - key_bytes = _int2bytes(key, self.key_bytes) - return device.feature_request(self.feature, self.write_fnid, key_bytes, data_bytes) + def write(self, device, key, data_bytes): + assert self.feature is not None + key_bytes = _int2bytes(key, self.key_bytes) + return device.feature_request(self.feature, self.write_fnid, key_bytes, + data_bytes) # @@ -475,269 +530,290 @@ class FeatureRWMap(FeatureRW): # handle the conversion from read bytes, to setting value, and back # + class BooleanValidator(object): - __slots__ = ('true_value', 'false_value', 'mask', 'needs_current_value') + __slots__ = ('true_value', 'false_value', 'mask', 'needs_current_value') - kind = KIND.toggle - default_true = 0x01 - default_false = 0x00 - # mask specifies all the affected bits in the value - default_mask = 0xFF + kind = KIND.toggle + default_true = 0x01 + default_false = 0x00 + # mask specifies all the affected bits in the value + default_mask = 0xFF - def __init__(self, true_value=default_true, false_value=default_false, mask=default_mask): - if isinstance(true_value, int): - assert isinstance(false_value, int) - if mask is None: - mask = self.default_mask - else: - assert isinstance(mask, int) - assert true_value & false_value == 0 - assert true_value & mask == true_value - assert false_value & mask == false_value - self.needs_current_value = (mask != self.default_mask) - elif isinstance(true_value, bytes): - if false_value is None or false_value == self.default_false: - false_value = b'\x00' * len(true_value) - else: - assert isinstance(false_value, bytes) - if mask is None or mask == self.default_mask: - mask = b'\xFF' * len(true_value) - else: - assert isinstance(mask, bytes) - assert len(mask) == len(true_value) == len(false_value) - tv = _bytes2int(true_value) - fv = _bytes2int(false_value) - mv = _bytes2int(mask) - assert tv != fv # true and false might be something other than bit values - assert tv & mv == tv - assert fv & mv == fv - self.needs_current_value = any(m != b'\xFF' for m in mask) - else: - raise Exception("invalid mask '%r', type %s" % (mask, type(mask))) + def __init__(self, + true_value=default_true, + false_value=default_false, + mask=default_mask): + if isinstance(true_value, int): + assert isinstance(false_value, int) + if mask is None: + mask = self.default_mask + else: + assert isinstance(mask, int) + assert true_value & false_value == 0 + assert true_value & mask == true_value + assert false_value & mask == false_value + self.needs_current_value = (mask != self.default_mask) + elif isinstance(true_value, bytes): + if false_value is None or false_value == self.default_false: + false_value = b'\x00' * len(true_value) + else: + assert isinstance(false_value, bytes) + if mask is None or mask == self.default_mask: + mask = b'\xFF' * len(true_value) + else: + assert isinstance(mask, bytes) + assert len(mask) == len(true_value) == len(false_value) + tv = _bytes2int(true_value) + fv = _bytes2int(false_value) + mv = _bytes2int(mask) + assert tv != fv # true and false might be something other than bit values + assert tv & mv == tv + assert fv & mv == fv + self.needs_current_value = any(m != b'\xFF' for m in mask) + else: + raise Exception("invalid mask '%r', type %s" % (mask, type(mask))) - self.true_value = true_value - self.false_value = false_value - self.mask = mask + self.true_value = true_value + self.false_value = false_value + self.mask = mask - def validate_read(self, reply_bytes): - if isinstance(self.mask, int): - reply_value = ord(reply_bytes[:1]) & self.mask - if _log.isEnabledFor(_DEBUG): - _log.debug("BooleanValidator: validate read %r => %02X", reply_bytes, reply_value) - if reply_value == self.true_value: - return True - if reply_value == self.false_value: - return False - _log.warn("BooleanValidator: reply %02X mismatched %02X/%02X/%02X", - reply_value, self.true_value, self.false_value, self.mask) - return False + def validate_read(self, reply_bytes): + if isinstance(self.mask, int): + reply_value = ord(reply_bytes[:1]) & self.mask + if _log.isEnabledFor(_DEBUG): + _log.debug("BooleanValidator: validate read %r => %02X", + reply_bytes, reply_value) + if reply_value == self.true_value: + return True + if reply_value == self.false_value: + return False + _log.warn("BooleanValidator: reply %02X mismatched %02X/%02X/%02X", + reply_value, self.true_value, self.false_value, + self.mask) + return False - count = len(self.mask) - mask = _bytes2int(self.mask) - reply_value = _bytes2int(reply_bytes[:count]) & mask + count = len(self.mask) + mask = _bytes2int(self.mask) + reply_value = _bytes2int(reply_bytes[:count]) & mask - true_value = _bytes2int(self.true_value) - if reply_value == true_value: - return True + true_value = _bytes2int(self.true_value) + if reply_value == true_value: + return True - false_value = _bytes2int(self.false_value) - if reply_value == false_value: - return False + false_value = _bytes2int(self.false_value) + if reply_value == false_value: + return False - _log.warn("BooleanValidator: reply %r mismatched %r/%r/%r", - reply_bytes, self.true_value, self.false_value, self.mask) - return False + _log.warn("BooleanValidator: reply %r mismatched %r/%r/%r", + reply_bytes, self.true_value, self.false_value, self.mask) + return False - def prepare_write(self, new_value, current_value=None): - if new_value is None: - new_value = False - else: - assert isinstance(new_value, bool) + def prepare_write(self, new_value, current_value=None): + if new_value is None: + new_value = False + else: + assert isinstance(new_value, bool) - to_write = self.true_value if new_value else self.false_value + to_write = self.true_value if new_value else self.false_value - if isinstance(self.mask, int): - if current_value is not None and self.needs_current_value: - to_write |= ord(current_value[:1]) & (0xFF ^ self.mask) - if current_value is not None and to_write == ord(current_value[:1]): - return None - else: - to_write = bytearray(to_write) - count = len(self.mask) - for i in range(0, count): - b = ord(to_write[i:i+1]) - m = ord(self.mask[i : i + 1]) - assert b & m == b - # b &= m - if current_value is not None and self.needs_current_value: - b |= ord(current_value[i : i + 1]) & (0xFF ^ m) - to_write[i] = b - to_write = bytes(to_write) + if isinstance(self.mask, int): + if current_value is not None and self.needs_current_value: + to_write |= ord(current_value[:1]) & (0xFF ^ self.mask) + if current_value is not None and to_write == ord( + current_value[:1]): + return None + else: + to_write = bytearray(to_write) + count = len(self.mask) + for i in range(0, count): + b = ord(to_write[i:i + 1]) + m = ord(self.mask[i:i + 1]) + assert b & m == b + # b &= m + if current_value is not None and self.needs_current_value: + b |= ord(current_value[i:i + 1]) & (0xFF ^ m) + to_write[i] = b + to_write = bytes(to_write) - if current_value is not None and to_write == current_value[:len(to_write)]: - return None + if current_value is not None and to_write == current_value[:len( + to_write)]: + return None - if _log.isEnabledFor(_DEBUG): - _log.debug("BooleanValidator: prepare_write(%s, %s) => %r", new_value, current_value, to_write) + if _log.isEnabledFor(_DEBUG): + _log.debug("BooleanValidator: prepare_write(%s, %s) => %r", + new_value, current_value, to_write) - return to_write + return to_write class BitFieldValidator(object): - __slots__ = ('byte_count', 'options') + __slots__ = ('byte_count', 'options') - kind = KIND.multiple_toggle + kind = KIND.multiple_toggle - def __init__(self, options, byte_count=None): - assert(isinstance(options, list)) - self.options = options - self.byte_count = (max(x.bit_length() for x in options) + 7) // 8 - if byte_count: - assert(isinstance(byte_count, int) and byte_count >= self.byte_count) - self.byte_count = byte_count + def __init__(self, options, byte_count=None): + assert (isinstance(options, list)) + self.options = options + self.byte_count = (max(x.bit_length() for x in options) + 7) // 8 + if byte_count: + assert (isinstance(byte_count, int) + and byte_count >= self.byte_count) + self.byte_count = byte_count - def validate_read(self, reply_bytes): - r = _bytes2int(reply_bytes[:self.byte_count]) - value = {str(int(k)) : False for k in self.options} - m = 1 - for i in range(8 * self.byte_count): - if m in self.options: - value[str(int(m))] = bool(r & m) - m <<= 1 - return value + def validate_read(self, reply_bytes): + r = _bytes2int(reply_bytes[:self.byte_count]) + value = {str(int(k)): False for k in self.options} + m = 1 + for i in range(8 * self.byte_count): + if m in self.options: + value[str(int(m))] = bool(r & m) + m <<= 1 + return value - def prepare_write(self, new_value): - assert(isinstance(new_value, dict)) - w = 0 - for k, v in new_value.items(): - if v: - w |= int(k) - return _int2bytes(w, self.byte_count) + def prepare_write(self, new_value): + assert (isinstance(new_value, dict)) + w = 0 + for k, v in new_value.items(): + if v: + w |= int(k) + return _int2bytes(w, self.byte_count) class ChoicesValidator(object): - __slots__ = ('choices', 'flag', '_bytes_count', 'needs_current_value') + __slots__ = ('choices', 'flag', '_bytes_count', 'needs_current_value') - kind = KIND.choice - - """Translates between NamedInts and a byte sequence. + kind = KIND.choice + """Translates between NamedInts and a byte sequence. :param choices: a list of NamedInts :param bytes_count: the size of the derived byte sequence. If None, it will be calculated from the choices.""" - def __init__(self, choices, bytes_count=None): - assert choices is not None - assert isinstance(choices, _NamedInts) - assert len(choices) > 2 - self.choices = choices - self.needs_current_value = False + def __init__(self, choices, bytes_count=None): + assert choices is not None + assert isinstance(choices, _NamedInts) + assert len(choices) > 2 + self.choices = choices + self.needs_current_value = False - max_bits = max(x.bit_length() for x in choices) - self._bytes_count = (max_bits // 8) + (1 if max_bits % 8 else 0) - if bytes_count: - assert self._bytes_count <= bytes_count - self._bytes_count = bytes_count - assert self._bytes_count < 8 + max_bits = max(x.bit_length() for x in choices) + self._bytes_count = (max_bits // 8) + (1 if max_bits % 8 else 0) + if bytes_count: + assert self._bytes_count <= bytes_count + self._bytes_count = bytes_count + assert self._bytes_count < 8 - def validate_read(self, reply_bytes): - reply_value = _bytes2int(reply_bytes[:self._bytes_count]) - valid_value = self.choices[reply_value] - assert valid_value is not None, "%s: failed to validate read value %02X" % (self.__class__.__name__, reply_value) - return valid_value + def validate_read(self, reply_bytes): + reply_value = _bytes2int(reply_bytes[:self._bytes_count]) + valid_value = self.choices[reply_value] + assert valid_value is not None, "%s: failed to validate read value %02X" % ( + self.__class__.__name__, reply_value) + return valid_value - def prepare_write(self, new_value, current_value=None): - if new_value is None: - choice = self.choices[:][0] - else: - if isinstance(new_value, int): - choice = self.choices[new_value] - elif int(new_value) in self.choices: - choice = self.choices[int(new_value)] - elif new_value in self.choices: - choice = self.choices[new_value] - else: - raise ValueError(new_value) + def prepare_write(self, new_value, current_value=None): + if new_value is None: + choice = self.choices[:][0] + else: + if isinstance(new_value, int): + choice = self.choices[new_value] + elif int(new_value) in self.choices: + choice = self.choices[int(new_value)] + elif new_value in self.choices: + choice = self.choices[new_value] + else: + raise ValueError(new_value) + + if choice is None: + raise ValueError("invalid choice %r" % new_value) + assert isinstance(choice, _NamedInt) + return choice.bytes(self._bytes_count) - if choice is None: - raise ValueError("invalid choice %r" % new_value) - assert isinstance(choice, _NamedInt) - return choice.bytes(self._bytes_count) class ChoicesMapValidator(ChoicesValidator): - kind = KIND.map_choice + kind = KIND.map_choice - def __init__(self, choices_map, key_bytes_count=None, skip_bytes_count=None, value_bytes_count=None, extra_default=None): - assert choices_map is not None - assert isinstance(choices_map, dict) - max_key_bits = 0 - max_value_bits = 0 - for key, choices in choices_map.items(): - assert isinstance(key, _NamedInt) - assert isinstance(choices, list) - max_key_bits = max(max_key_bits, key.bit_length()) - for key_value in choices: - assert isinstance(key_value, _NamedInt) - max_value_bits = max(max_value_bits, key_value.bit_length()) - self.choices = choices_map - self.needs_current_value = False - self.extra_default=extra_default + def __init__(self, + choices_map, + key_bytes_count=None, + skip_bytes_count=None, + value_bytes_count=None, + extra_default=None): + assert choices_map is not None + assert isinstance(choices_map, dict) + max_key_bits = 0 + max_value_bits = 0 + for key, choices in choices_map.items(): + assert isinstance(key, _NamedInt) + assert isinstance(choices, list) + max_key_bits = max(max_key_bits, key.bit_length()) + for key_value in choices: + assert isinstance(key_value, _NamedInt) + max_value_bits = max(max_value_bits, key_value.bit_length()) + self.choices = choices_map + self.needs_current_value = False + self.extra_default = extra_default - self._key_bytes_count = (max_key_bits+7) // 8 - if key_bytes_count: - assert self._key_bytes_count <= key_bytes_count - self._key_bytes_count = key_bytes_count - self._value_bytes_count = (max_value_bits+7) // 8 - if value_bytes_count: - assert self._value_bytes_count <= value_bytes_count - self._value_bytes_count = value_bytes_count - self._skip_bytes_count = skip_bytes_count if skip_bytes_count is not None else 0 - self._bytes_count = self._key_bytes_count + self._skip_bytes_count + self._value_bytes_count + self._key_bytes_count = (max_key_bits + 7) // 8 + if key_bytes_count: + assert self._key_bytes_count <= key_bytes_count + self._key_bytes_count = key_bytes_count + self._value_bytes_count = (max_value_bits + 7) // 8 + if value_bytes_count: + assert self._value_bytes_count <= value_bytes_count + self._value_bytes_count = value_bytes_count + self._skip_bytes_count = skip_bytes_count if skip_bytes_count is not None else 0 + self._bytes_count = self._key_bytes_count + self._skip_bytes_count + self._value_bytes_count - def validate_read(self, reply_bytes, key): - start = self._key_bytes_count + self._skip_bytes_count - end = start + self._value_bytes_count - reply_value = _bytes2int(reply_bytes[start:end]) - # reprogrammable keys starts out as 0, which is not a choice, so don't use assert here - if self.extra_default is not None and self.extra_default==reply_value: - return int(self.choices[key][0]) - assert reply_value in self.choices[key], "%s: failed to validate read value %02X" % (self.__class__.__name__, reply_value) - return reply_value + def validate_read(self, reply_bytes, key): + start = self._key_bytes_count + self._skip_bytes_count + end = start + self._value_bytes_count + reply_value = _bytes2int(reply_bytes[start:end]) + # reprogrammable keys starts out as 0, which is not a choice, so don't use assert here + if self.extra_default is not None and self.extra_default == reply_value: + return int(self.choices[key][0]) + assert reply_value in self.choices[ + key], "%s: failed to validate read value %02X" % ( + self.__class__.__name__, reply_value) + return reply_value - def prepare_write(self, key, new_value): - choices = self.choices[key] - if new_value not in choices and new_value != self.extra_default: - raise ValueError("invalid choice %r" % new_value) - return _int2bytes(new_value, self._skip_bytes_count + self._value_bytes_count) + def prepare_write(self, key, new_value): + choices = self.choices[key] + if new_value not in choices and new_value != self.extra_default: + raise ValueError("invalid choice %r" % new_value) + return _int2bytes(new_value, + self._skip_bytes_count + self._value_bytes_count) class RangeValidator(object): - __slots__ = ('min_value', 'max_value', 'flag', '_bytes_count', 'needs_current_value') + __slots__ = ('min_value', 'max_value', 'flag', '_bytes_count', + 'needs_current_value') - kind = KIND.range - - """Translates between integers and a byte sequence. + kind = KIND.range + """Translates between integers and a byte sequence. :param min_value: minimum accepted value (inclusive) :param max_value: maximum accepted value (inclusive) :param bytes_count: the size of the derived byte sequence. If None, it will be calculated from the range.""" - def __init__(self, min_value, max_value, bytes_count=None): - assert max_value > min_value - self.min_value = min_value - self.max_value = max_value - self.needs_current_value = False + def __init__(self, min_value, max_value, bytes_count=None): + assert max_value > min_value + self.min_value = min_value + self.max_value = max_value + self.needs_current_value = False - self._bytes_count = math.ceil(math.log(max_value + 1, 256)) - if bytes_count: - assert self._bytes_count <= bytes_count - self._bytes_count = bytes_count - assert self._bytes_count < 8 + self._bytes_count = math.ceil(math.log(max_value + 1, 256)) + if bytes_count: + assert self._bytes_count <= bytes_count + self._bytes_count = bytes_count + assert self._bytes_count < 8 - def validate_read(self, reply_bytes): - reply_value = _bytes2int(reply_bytes[:self._bytes_count]) - assert reply_value >= self.min_value, "%s: failed to validate read value %02X" % (self.__class__.__name__, reply_value) - assert reply_value <= self.max_value, "%s: failed to validate read value %02X" % (self.__class__.__name__, reply_value) - return reply_value + def validate_read(self, reply_bytes): + reply_value = _bytes2int(reply_bytes[:self._bytes_count]) + assert reply_value >= self.min_value, "%s: failed to validate read value %02X" % ( + self.__class__.__name__, reply_value) + assert reply_value <= self.max_value, "%s: failed to validate read value %02X" % ( + self.__class__.__name__, reply_value) + return reply_value - def prepare_write(self, new_value, current_value=None): - if new_value < self.min_value or new_value > self.max_value: - raise ValueError("invalid choice %r" % new_value) - return _int2bytes(new_value, self._bytes_count) + def prepare_write(self, new_value, current_value=None): + if new_value < self.min_value or new_value > self.max_value: + raise ValueError("invalid choice %r" % new_value) + return _int2bytes(new_value, self._bytes_count) diff --git a/lib/logitech_receiver/settings_templates.py b/lib/logitech_receiver/settings_templates.py index 590967d3..021bdc97 100644 --- a/lib/logitech_receiver/settings_templates.py +++ b/lib/logitech_receiver/settings_templates.py @@ -28,27 +28,27 @@ from . import hidpp10 as _hidpp10 from . import hidpp20 as _hidpp20 from . import special_keys as _special_keys from .common import ( - bytes2int as _bytes2int, - int2bytes as _int2bytes, - NamedInt as _NamedInt, - NamedInts as _NamedInts, - unpack as _unpack, - ReprogrammableKeyInfoV4 as _ReprogrammableKeyInfoV4, - ) + bytes2int as _bytes2int, + int2bytes as _int2bytes, + NamedInt as _NamedInt, + NamedInts as _NamedInts, + unpack as _unpack, + ReprogrammableKeyInfoV4 as _ReprogrammableKeyInfoV4, +) from .settings import ( - KIND as _KIND, - Setting as _Setting, - BitFieldSetting as _BitFieldSetting, - Settings as _Settings, - RegisterRW as _RegisterRW, - FeatureRW as _FeatureRW, - FeatureRWMap as _FeatureRWMap, - BooleanValidator as _BooleanV, - BitFieldValidator as _BitFieldV, - ChoicesValidator as _ChoicesV, - ChoicesMapValidator as _ChoicesMapV, - RangeValidator as _RangeV, - ) + KIND as _KIND, + Setting as _Setting, + BitFieldSetting as _BitFieldSetting, + Settings as _Settings, + RegisterRW as _RegisterRW, + FeatureRW as _FeatureRW, + FeatureRWMap as _FeatureRWMap, + BooleanValidator as _BooleanV, + BitFieldValidator as _BitFieldV, + ChoicesValidator as _ChoicesV, + ChoicesMapValidator as _ChoicesMapV, + RangeValidator as _RangeV, +) _DK = _hidpp10.DEVICE_KIND _R = _hidpp10.REGISTERS @@ -58,368 +58,613 @@ _F = _hidpp20.FEATURE # pre-defined basic setting descriptors # -def register_toggle(name, register, - true_value=_BooleanV.default_true, - false_value=_BooleanV.default_false, - mask=_BooleanV.default_mask, - label=None, description=None, device_kind=None): - validator = _BooleanV(true_value=true_value, false_value=false_value, mask=mask) - rw = _RegisterRW(register) - return _Setting(name, rw, validator, label=label, description=description, device_kind=device_kind) + +def register_toggle(name, + register, + true_value=_BooleanV.default_true, + false_value=_BooleanV.default_false, + mask=_BooleanV.default_mask, + label=None, + description=None, + device_kind=None): + validator = _BooleanV(true_value=true_value, + false_value=false_value, + mask=mask) + rw = _RegisterRW(register) + return _Setting(name, + rw, + validator, + label=label, + description=description, + device_kind=device_kind) -def register_choices(name, register, choices, - kind=_KIND.choice, - label=None, description=None, device_kind=None): - assert choices - validator = _ChoicesV(choices) - rw = _RegisterRW(register) - return _Setting(name, rw, validator, kind=kind, label=label, description=description, device_kind=device_kind) +def register_choices(name, + register, + choices, + kind=_KIND.choice, + label=None, + description=None, + device_kind=None): + assert choices + validator = _ChoicesV(choices) + rw = _RegisterRW(register) + return _Setting(name, + rw, + validator, + kind=kind, + label=label, + description=description, + device_kind=device_kind) -def feature_toggle(name, feature, - read_function_id=_FeatureRW.default_read_fnid, - write_function_id=_FeatureRW.default_write_fnid, - true_value=_BooleanV.default_true, - false_value=_BooleanV.default_false, - mask=_BooleanV.default_mask, - label=None, description=None, device_kind=None): - validator = _BooleanV(true_value=true_value, false_value=false_value, mask=mask) - rw = _FeatureRW(feature, read_function_id, write_function_id) - return _Setting(name, rw, validator, feature=feature, label=label, description=description, device_kind=device_kind) +def feature_toggle(name, + feature, + read_function_id=_FeatureRW.default_read_fnid, + write_function_id=_FeatureRW.default_write_fnid, + true_value=_BooleanV.default_true, + false_value=_BooleanV.default_false, + mask=_BooleanV.default_mask, + label=None, + description=None, + device_kind=None): + validator = _BooleanV(true_value=true_value, + false_value=false_value, + mask=mask) + rw = _FeatureRW(feature, read_function_id, write_function_id) + return _Setting(name, + rw, + validator, + feature=feature, + label=label, + description=description, + device_kind=device_kind) -def feature_bitfield_toggle(name, feature, options, - read_function_id=_FeatureRW.default_read_fnid, - write_function_id=_FeatureRW.default_write_fnid, - label=None, description=None, device_kind=None): - assert options - validator = _BitFieldV(options) - rw = _FeatureRW(feature, read_function_id, write_function_id) - return _BitFieldSetting(name, rw, validator, feature=feature, label=label, description=description, device_kind=device_kind) -def feature_bitfield_toggle_dynamic(name, feature, options_callback, - read_function_id=_FeatureRW.default_read_fnid, - write_function_id=_FeatureRW.default_write_fnid, - label=None, description=None, device_kind=None): - def instantiate(device): - options = options_callback(device) - setting = feature_bitfield_toggle(name, feature, options, - read_function_id=read_function_id, - write_function_id=write_function_id, - label=label, - description=description, device_kind=device_kind) - return setting(device) - instantiate._rw_kind = _FeatureRW.kind - return instantiate +def feature_bitfield_toggle(name, + feature, + options, + read_function_id=_FeatureRW.default_read_fnid, + write_function_id=_FeatureRW.default_write_fnid, + label=None, + description=None, + device_kind=None): + assert options + validator = _BitFieldV(options) + rw = _FeatureRW(feature, read_function_id, write_function_id) + return _BitFieldSetting(name, + rw, + validator, + feature=feature, + label=label, + description=description, + device_kind=device_kind) -def feature_choices(name, feature, choices, - read_function_id, write_function_id, - bytes_count=None, - label=None, description=None, device_kind=None): - assert choices - validator = _ChoicesV(choices, bytes_count=bytes_count) - rw = _FeatureRW(feature, read_function_id, write_function_id) - return _Setting(name, rw, validator, feature=feature, kind=_KIND.choice, label=label, description=description, device_kind=device_kind) -def feature_choices_dynamic(name, feature, choices_callback, - read_function_id, write_function_id, - bytes_count=None, - label=None, description=None, device_kind=None): - # Proxy that obtains choices dynamically from a device - def instantiate(device): - # Obtain choices for this feature - choices = choices_callback(device) - setting = feature_choices(name, feature, choices, - read_function_id, write_function_id, - bytes_count=bytes_count, - label=label, description=description, device_kind=device_kind) - return setting(device) - instantiate._rw_kind = _FeatureRW.kind - return instantiate +def feature_bitfield_toggle_dynamic( + name, + feature, + options_callback, + read_function_id=_FeatureRW.default_read_fnid, + write_function_id=_FeatureRW.default_write_fnid, + label=None, + description=None, + device_kind=None): + def instantiate(device): + options = options_callback(device) + setting = feature_bitfield_toggle(name, + feature, + options, + read_function_id=read_function_id, + write_function_id=write_function_id, + label=label, + description=description, + device_kind=device_kind) + return setting(device) + + instantiate._rw_kind = _FeatureRW.kind + return instantiate + + +def feature_choices(name, + feature, + choices, + read_function_id, + write_function_id, + bytes_count=None, + label=None, + description=None, + device_kind=None): + assert choices + validator = _ChoicesV(choices, bytes_count=bytes_count) + rw = _FeatureRW(feature, read_function_id, write_function_id) + return _Setting(name, + rw, + validator, + feature=feature, + kind=_KIND.choice, + label=label, + description=description, + device_kind=device_kind) + + +def feature_choices_dynamic(name, + feature, + choices_callback, + read_function_id, + write_function_id, + bytes_count=None, + label=None, + description=None, + device_kind=None): + # Proxy that obtains choices dynamically from a device + def instantiate(device): + # Obtain choices for this feature + choices = choices_callback(device) + setting = feature_choices(name, + feature, + choices, + read_function_id, + write_function_id, + bytes_count=bytes_count, + label=label, + description=description, + device_kind=device_kind) + return setting(device) + + instantiate._rw_kind = _FeatureRW.kind + return instantiate + # maintain a mapping from keys (NamedInts) to one of a list of choices (NamedInts), default is first one # the setting is stored as a JSON-compatible object mapping the key int (as a string) to the choice int # extra_default is an extra value that comes from the device that also means the default -def feature_map_choices(name, feature, choicesmap, - read_function_id, write_function_id, - key_bytes_count=None, skip_bytes_count=None, value_bytes_count=None, - label=None, description=None, device_kind=None, extra_default=None): - assert choicesmap - validator = _ChoicesMapV(choicesmap, key_bytes_count=key_bytes_count, skip_bytes_count=skip_bytes_count, value_bytes_count=value_bytes_count, extra_default=extra_default ) - rw = _FeatureRWMap(feature, read_function_id, write_function_id, key_bytes=key_bytes_count) - return _Settings(name, rw, validator, feature=feature, kind=_KIND.map_choice, label=label, description=description, device_kind=device_kind) +def feature_map_choices(name, + feature, + choicesmap, + read_function_id, + write_function_id, + key_bytes_count=None, + skip_bytes_count=None, + value_bytes_count=None, + label=None, + description=None, + device_kind=None, + extra_default=None): + assert choicesmap + validator = _ChoicesMapV(choicesmap, + key_bytes_count=key_bytes_count, + skip_bytes_count=skip_bytes_count, + value_bytes_count=value_bytes_count, + extra_default=extra_default) + rw = _FeatureRWMap(feature, + read_function_id, + write_function_id, + key_bytes=key_bytes_count) + return _Settings(name, + rw, + validator, + feature=feature, + kind=_KIND.map_choice, + label=label, + description=description, + device_kind=device_kind) -def feature_map_choices_dynamic(name, feature, choices_callback, - read_function_id, write_function_id, - key_bytes_count=None, skip_bytes_count=None, value_bytes_count=None, - label=None, description=None, device_kind=None, extra_default=None): - # Proxy that obtains choices dynamically from a device - def instantiate(device): - choices = choices_callback(device) - if not choices: # no choices, so don't create a Setting - return None - setting = feature_map_choices(name, feature, choices, - read_function_id, write_function_id, - key_bytes_count=key_bytes_count, skip_bytes_count=skip_bytes_count, value_bytes_count=value_bytes_count, - label=label, description=description, device_kind=device_kind, extra_default=extra_default) - return setting(device) - instantiate._rw_kind = _FeatureRWMap.kind - return instantiate -def feature_range(name, feature, min_value, max_value, - read_function_id=_FeatureRW.default_read_fnid, - write_function_id=_FeatureRW.default_write_fnid, - rw=None, - bytes_count=None, - label=None, description=None, device_kind=None): - validator = _RangeV(min_value, max_value, bytes_count=bytes_count) - if rw is None: - rw = _FeatureRW(feature, read_function_id, write_function_id) - return _Setting(name, rw, validator, feature=feature, kind=_KIND.range, label=label, description=description, device_kind=device_kind) +def feature_map_choices_dynamic(name, + feature, + choices_callback, + read_function_id, + write_function_id, + key_bytes_count=None, + skip_bytes_count=None, + value_bytes_count=None, + label=None, + description=None, + device_kind=None, + extra_default=None): + # Proxy that obtains choices dynamically from a device + def instantiate(device): + choices = choices_callback(device) + if not choices: # no choices, so don't create a Setting + return None + setting = feature_map_choices(name, + feature, + choices, + read_function_id, + write_function_id, + key_bytes_count=key_bytes_count, + skip_bytes_count=skip_bytes_count, + value_bytes_count=value_bytes_count, + label=label, + description=description, + device_kind=device_kind, + extra_default=extra_default) + return setting(device) + + instantiate._rw_kind = _FeatureRWMap.kind + return instantiate + + +def feature_range(name, + feature, + min_value, + max_value, + read_function_id=_FeatureRW.default_read_fnid, + write_function_id=_FeatureRW.default_write_fnid, + rw=None, + bytes_count=None, + label=None, + description=None, + device_kind=None): + validator = _RangeV(min_value, max_value, bytes_count=bytes_count) + if rw is None: + rw = _FeatureRW(feature, read_function_id, write_function_id) + return _Setting(name, + rw, + validator, + feature=feature, + kind=_KIND.range, + label=label, + description=description, + device_kind=device_kind) + # # common strings for settings - name, string to display in main window, tool tip for main window -# +# -_HAND_DETECTION = ('hand-detection', _("Hand Detection"), _("Turn on illumination when the hands hover over the keyboard.")) -_SMOOTH_SCROLL = ('smooth-scroll', _("Smooth Scrolling"), _("High-sensitivity mode for vertical scroll with the wheel.")) -_SIDE_SCROLL = ('side-scroll', _("Side Scrolling"), _("When disabled, pushing the wheel sideways sends custom button events\n" - "instead of the standard side-scrolling events.")) -_HI_RES_SCROLL = ('hi-res-scroll', _("High Resolution Scrolling"), - _("High-sensitivity mode for vertical scroll with the wheel.")) -_LOW_RES_SCROLL = ('lowres-smooth-scroll', _("HID++ Scrolling"), _("HID++ mode for vertical scroll with the wheel.") + '\n' + - _("Effectively turns off wheel scrolling in Linux.")) +_HAND_DETECTION = ( + 'hand-detection', _("Hand Detection"), + _("Turn on illumination when the hands hover over the keyboard.")) +_SMOOTH_SCROLL = ( + 'smooth-scroll', _("Smooth Scrolling"), + _("High-sensitivity mode for vertical scroll with the wheel.")) +_SIDE_SCROLL = ( + 'side-scroll', _("Side Scrolling"), + _("When disabled, pushing the wheel sideways sends custom button events\n" + "instead of the standard side-scrolling events.")) +_HI_RES_SCROLL = ( + 'hi-res-scroll', _("High Resolution Scrolling"), + _("High-sensitivity mode for vertical scroll with the wheel.")) +_LOW_RES_SCROLL = ('lowres-smooth-scroll', _("HID++ Scrolling"), + _("HID++ mode for vertical scroll with the wheel.") + '\n' + + _("Effectively turns off wheel scrolling in Linux.")) _HIRES_INV = ('hires-smooth-invert', _("High Resolution Wheel Invert"), - _("High-sensitivity wheel invert mode for vertical scroll.")) + _("High-sensitivity wheel invert mode for vertical scroll.")) _HIRES_RES = ('hires-smooth-resolution', _("Wheel Resolution"), - _("High-sensitivity mode for vertical scroll with the wheel.")) -_FN_SWAP = ('fn-swap', _("Swap Fx function"), _("When set, the F1..F12 keys will activate their special function,\n" - "and you must hold the FN key to activate their standard function.") - + '\n\n' + - _("When unset, the F1..F12 keys will activate their standard function,\n" - "and you must hold the FN key to activate their special function.")) + _("High-sensitivity mode for vertical scroll with the wheel.")) +_FN_SWAP = ( + 'fn-swap', _("Swap Fx function"), + _("When set, the F1..F12 keys will activate their special function,\n" + "and you must hold the FN key to activate their standard function.") + + '\n\n' + + _("When unset, the F1..F12 keys will activate their standard function,\n" + "and you must hold the FN key to activate their special function.")) _DPI = ('dpi', _("Sensitivity (DPI)"), None) -_POINTER_SPEED = ('pointer_speed', _("Sensitivity (Pointer Speed)"), _("Speed multiplier for mouse (256 is normal multiplier).")) -_SMART_SHIFT = ('smart-shift', _("Smart Shift"), _("Automatically switch the mouse wheel between ratchet and freespin mode.\n" - "The mouse wheel is always free at 0, and always locked at 50")) -_BACKLIGHT = ('backlight', _("Backlight"), _("Turn illumination on or off on keyboard.")) -_REPROGRAMMABLE_KEYS = ('reprogrammable-keys', _("Actions"), _("Change the action for the key or button.") + "\n" + - _("Changing important actions (such as for the left mouse button) can result in an unusable system.")) -_DISABLE_KEYS = ('disable-keyboard-keys', _("Disable keys"), _("Disable specific keyboard keys.")) +_POINTER_SPEED = ('pointer_speed', _("Sensitivity (Pointer Speed)"), + _("Speed multiplier for mouse (256 is normal multiplier).")) +_SMART_SHIFT = ( + 'smart-shift', _("Smart Shift"), + _("Automatically switch the mouse wheel between ratchet and freespin mode.\n" + "The mouse wheel is always free at 0, and always locked at 50")) +_BACKLIGHT = ('backlight', _("Backlight"), + _("Turn illumination on or off on keyboard.")) +_REPROGRAMMABLE_KEYS = ('reprogrammable-keys', _( + "Actions" +), _("Change the action for the key or button.") + "\n" + _( + "Changing important actions (such as for the left mouse button) can result in an unusable system." +)) +_DISABLE_KEYS = ('disable-keyboard-keys', _("Disable keys"), + _("Disable specific keyboard keys.")) # # # + def _register_hand_detection(register=_R.keyboard_hand_detection, - true_value=b'\x00\x00\x00', false_value=b'\x00\x00\x30', mask=b'\x00\x00\xFF'): - return register_toggle(_HAND_DETECTION[0], register, true_value=true_value, false_value=false_value, - label=_HAND_DETECTION[1], description=_HAND_DETECTION[2], - device_kind=(_DK.keyboard,)) + true_value=b'\x00\x00\x00', + false_value=b'\x00\x00\x30', + mask=b'\x00\x00\xFF'): + return register_toggle(_HAND_DETECTION[0], + register, + true_value=true_value, + false_value=false_value, + label=_HAND_DETECTION[1], + description=_HAND_DETECTION[2], + device_kind=(_DK.keyboard, )) -def _register_fn_swap(register=_R.keyboard_fn_swap, true_value=b'\x00\x01', mask=b'\x00\x01'): - return register_toggle(_FN_SWAP[0], register, true_value=true_value, mask=mask, - label=_FN_SWAP[1], description=_FN_SWAP[2], - device_kind=(_DK.keyboard,)) -def _register_smooth_scroll(register=_R.mouse_button_flags, true_value=0x40, mask=0x40): - return register_toggle(_SMOOTH_SCROLL[0], register, true_value=true_value, mask=mask, - label=_SMOOTH_SCROLL[1], description=_SMOOTH_SCROLL[2], - device_kind=(_DK.mouse, _DK.trackball)) +def _register_fn_swap(register=_R.keyboard_fn_swap, + true_value=b'\x00\x01', + mask=b'\x00\x01'): + return register_toggle(_FN_SWAP[0], + register, + true_value=true_value, + mask=mask, + label=_FN_SWAP[1], + description=_FN_SWAP[2], + device_kind=(_DK.keyboard, )) + + +def _register_smooth_scroll(register=_R.mouse_button_flags, + true_value=0x40, + mask=0x40): + return register_toggle(_SMOOTH_SCROLL[0], + register, + true_value=true_value, + mask=mask, + label=_SMOOTH_SCROLL[1], + description=_SMOOTH_SCROLL[2], + device_kind=(_DK.mouse, _DK.trackball)) + + +def _register_side_scroll(register=_R.mouse_button_flags, + true_value=0x02, + mask=0x02): + return register_toggle(_SIDE_SCROLL[0], + register, + true_value=true_value, + mask=mask, + label=_SIDE_SCROLL[1], + description=_SIDE_SCROLL[2], + device_kind=(_DK.mouse, _DK.trackball)) -def _register_side_scroll(register=_R.mouse_button_flags, true_value=0x02, mask=0x02): - return register_toggle(_SIDE_SCROLL[0], register, true_value=true_value, mask=mask, - label=_SIDE_SCROLL[1], description=_SIDE_SCROLL[2], - device_kind=(_DK.mouse, _DK.trackball)) def _register_dpi(register=_R.mouse_dpi, choices=None): - return register_choices(_DPI[0], register, choices, - label=_DPI[1], description=_DPI[2], - device_kind=(_DK.mouse, _DK.trackball)) + return register_choices(_DPI[0], + register, + choices, + label=_DPI[1], + description=_DPI[2], + device_kind=(_DK.mouse, _DK.trackball)) def _feature_fn_swap(): - return feature_toggle(_FN_SWAP[0], _F.FN_INVERSION, - label=_FN_SWAP[1], description=_FN_SWAP[2], - device_kind=(_DK.keyboard,)) + return feature_toggle(_FN_SWAP[0], + _F.FN_INVERSION, + label=_FN_SWAP[1], + description=_FN_SWAP[2], + device_kind=(_DK.keyboard, )) + # this might not be correct for this feature def _feature_new_fn_swap(): - return feature_toggle(_FN_SWAP[0], _F.NEW_FN_INVERSION, - label=_FN_SWAP[1], description=_FN_SWAP[2], - device_kind=(_DK.keyboard,)) + return feature_toggle(_FN_SWAP[0], + _F.NEW_FN_INVERSION, + label=_FN_SWAP[1], + description=_FN_SWAP[2], + device_kind=(_DK.keyboard, )) # ignore the capabilities part of the feature - all devices should be able to swap Fn state # just use the current host (first byte = 0xFF) part of the feature to read and set the Fn state def _feature_k375s_fn_swap(): - return feature_toggle(_FN_SWAP[0], _F.K375S_FN_INVERSION, - label=_FN_SWAP[1], description=_FN_SWAP[2], - true_value=b'\xFF\x01', false_value=b'\xFF\x00', - device_kind=(_DK.keyboard,)) + return feature_toggle(_FN_SWAP[0], + _F.K375S_FN_INVERSION, + label=_FN_SWAP[1], + description=_FN_SWAP[2], + true_value=b'\xFF\x01', + false_value=b'\xFF\x00', + device_kind=(_DK.keyboard, )) + # FIXME: This will enable all supported backlight settings, we should allow the users to select which settings they want to enable. def _feature_backlight2(): - return feature_toggle(_BACKLIGHT[0], _F.BACKLIGHT2, - label=_BACKLIGHT[1], description=_BACKLIGHT[2], - device_kind=(_DK.keyboard,)) + return feature_toggle(_BACKLIGHT[0], + _F.BACKLIGHT2, + label=_BACKLIGHT[1], + description=_BACKLIGHT[2], + device_kind=(_DK.keyboard, )) + def _feature_hi_res_scroll(): - return feature_toggle(_HI_RES_SCROLL[0], _F.HI_RES_SCROLLING, - label=_HI_RES_SCROLL[1], description=_HI_RES_SCROLL[2], - device_kind=(_DK.mouse, _DK.trackball)) + return feature_toggle(_HI_RES_SCROLL[0], + _F.HI_RES_SCROLLING, + label=_HI_RES_SCROLL[1], + description=_HI_RES_SCROLL[2], + device_kind=(_DK.mouse, _DK.trackball)) + def _feature_lowres_smooth_scroll(): - return feature_toggle(_LOW_RES_SCROLL[0], _F.LOWRES_WHEEL, - label=_LOW_RES_SCROLL[1], description=_LOW_RES_SCROLL[2], - device_kind=(_DK.mouse, _DK.trackball)) + return feature_toggle(_LOW_RES_SCROLL[0], + _F.LOWRES_WHEEL, + label=_LOW_RES_SCROLL[1], + description=_LOW_RES_SCROLL[2], + device_kind=(_DK.mouse, _DK.trackball)) + def _feature_hires_smooth_invert(): - return feature_toggle(_HIRES_INV[0], _F.HIRES_WHEEL, - read_function_id=0x10, - write_function_id=0x20, - true_value=0x04, mask=0x04, - label=_HIRES_INV[1], description=_HIRES_INV[2], - device_kind=(_DK.mouse, _DK.trackball)) + return feature_toggle(_HIRES_INV[0], + _F.HIRES_WHEEL, + read_function_id=0x10, + write_function_id=0x20, + true_value=0x04, + mask=0x04, + label=_HIRES_INV[1], + description=_HIRES_INV[2], + device_kind=(_DK.mouse, _DK.trackball)) + def _feature_hires_smooth_resolution(): - return feature_toggle(_HIRES_RES[0], _F.HIRES_WHEEL, - read_function_id=0x10, - write_function_id=0x20, - true_value=0x02, mask=0x02, - label=_HIRES_RES[1], description=_HIRES_RES[2], - device_kind=(_DK.mouse, _DK.trackball)) + return feature_toggle(_HIRES_RES[0], + _F.HIRES_WHEEL, + read_function_id=0x10, + write_function_id=0x20, + true_value=0x02, + mask=0x02, + label=_HIRES_RES[1], + description=_HIRES_RES[2], + device_kind=(_DK.mouse, _DK.trackball)) + def _feature_smart_shift(): - _MIN_SMART_SHIFT_VALUE = 0 - _MAX_SMART_SHIFT_VALUE = 50 - class _SmartShiftRW(_FeatureRW): - def __init__(self, feature): - super(_SmartShiftRW, self).__init__(feature) + _MIN_SMART_SHIFT_VALUE = 0 + _MAX_SMART_SHIFT_VALUE = 50 - def read(self, device): - value = super(_SmartShiftRW, self).read(device) - if _bytes2int(value[0:1]) == 1: - # Mode = Freespin, map to minimum - return _int2bytes(_MIN_SMART_SHIFT_VALUE, count=1) - else: - # Mode = smart shift, map to the value, capped at maximum - threshold = min(_bytes2int(value[1:2]), _MAX_SMART_SHIFT_VALUE) - return _int2bytes(threshold, count=1) + class _SmartShiftRW(_FeatureRW): + def __init__(self, feature): + super(_SmartShiftRW, self).__init__(feature) - def write(self, device, data_bytes): - threshold = _bytes2int(data_bytes) - # Freespin at minimum - mode = 1 if threshold == _MIN_SMART_SHIFT_VALUE else 2 + def read(self, device): + value = super(_SmartShiftRW, self).read(device) + if _bytes2int(value[0:1]) == 1: + # Mode = Freespin, map to minimum + return _int2bytes(_MIN_SMART_SHIFT_VALUE, count=1) + else: + # Mode = smart shift, map to the value, capped at maximum + threshold = min(_bytes2int(value[1:2]), _MAX_SMART_SHIFT_VALUE) + return _int2bytes(threshold, count=1) - # Ratchet at maximum - if threshold == _MAX_SMART_SHIFT_VALUE: - threshold = 255 + def write(self, device, data_bytes): + threshold = _bytes2int(data_bytes) + # Freespin at minimum + mode = 1 if threshold == _MIN_SMART_SHIFT_VALUE else 2 - data = _int2bytes(mode, count=1) + _int2bytes(threshold, count=1) * 2 - return super(_SmartShiftRW, self).write(device, data) + # Ratchet at maximum + if threshold == _MAX_SMART_SHIFT_VALUE: + threshold = 255 + + data = _int2bytes(mode, + count=1) + _int2bytes(threshold, count=1) * 2 + return super(_SmartShiftRW, self).write(device, data) + + return feature_range(_SMART_SHIFT[0], + _F.SMART_SHIFT, + _MIN_SMART_SHIFT_VALUE, + _MAX_SMART_SHIFT_VALUE, + bytes_count=1, + rw=_SmartShiftRW(_F.SMART_SHIFT), + label=_SMART_SHIFT[1], + description=_SMART_SHIFT[2], + device_kind=(_DK.mouse, _DK.trackball)) - return feature_range(_SMART_SHIFT[0], _F.SMART_SHIFT, - _MIN_SMART_SHIFT_VALUE, _MAX_SMART_SHIFT_VALUE, - bytes_count=1, - rw=_SmartShiftRW(_F.SMART_SHIFT), - label=_SMART_SHIFT[1], description=_SMART_SHIFT[2], - device_kind=(_DK.mouse, _DK.trackball)) def _feature_adjustable_dpi_choices(device): - # [1] getSensorDpiList(sensorIdx) - reply = device.feature_request(_F.ADJUSTABLE_DPI, 0x10) - # Should not happen, but might happen when the user unplugs device while the - # query is being executed. TODO retry logic? - assert reply, 'Oops, DPI list cannot be retrieved!' - dpi_list = [] - step = None - for val in _unpack('!7H', reply[1:1+14]): - if val == 0: - break - if val >> 13 == 0b111: - assert step is None and len(dpi_list) == 1, \ - 'Invalid DPI list item: %r' % val - step = val & 0x1fff - else: - dpi_list.append(val) - if step: - assert len(dpi_list) == 2, 'Invalid DPI list range: %r' % dpi_list - dpi_list = range(dpi_list[0], dpi_list[1] + 1, step) - return _NamedInts.list(dpi_list) + # [1] getSensorDpiList(sensorIdx) + reply = device.feature_request(_F.ADJUSTABLE_DPI, 0x10) + # Should not happen, but might happen when the user unplugs device while the + # query is being executed. TODO retry logic? + assert reply, 'Oops, DPI list cannot be retrieved!' + dpi_list = [] + step = None + for val in _unpack('!7H', reply[1:1 + 14]): + if val == 0: + break + if val >> 13 == 0b111: + assert step is None and len(dpi_list) == 1, \ + 'Invalid DPI list item: %r' % val + step = val & 0x1fff + else: + dpi_list.append(val) + if step: + assert len(dpi_list) == 2, 'Invalid DPI list range: %r' % dpi_list + dpi_list = range(dpi_list[0], dpi_list[1] + 1, step) + return _NamedInts.list(dpi_list) + def _feature_adjustable_dpi(): - """Pointer Speed feature""" - # Assume sensorIdx 0 (there is only one sensor) - # [2] getSensorDpi(sensorIdx) -> sensorIdx, dpiMSB, dpiLSB - # [3] setSensorDpi(sensorIdx, dpi) - return feature_choices_dynamic(_DPI[0], _F.ADJUSTABLE_DPI, - _feature_adjustable_dpi_choices, - read_function_id=0x20, - write_function_id=0x30, - bytes_count=3, - label=_DPI[1], description=_DPI[2], - device_kind=(_DK.mouse, _DK.trackball)) + """Pointer Speed feature""" + # Assume sensorIdx 0 (there is only one sensor) + # [2] getSensorDpi(sensorIdx) -> sensorIdx, dpiMSB, dpiLSB + # [3] setSensorDpi(sensorIdx, dpi) + return feature_choices_dynamic(_DPI[0], + _F.ADJUSTABLE_DPI, + _feature_adjustable_dpi_choices, + read_function_id=0x20, + write_function_id=0x30, + bytes_count=3, + label=_DPI[1], + description=_DPI[2], + device_kind=(_DK.mouse, _DK.trackball)) + def _feature_pointer_speed(): - """Pointer Speed feature""" - # min and max values taken from usb traces of Win software - return feature_range(_POINTER_SPEED[0], _F.POINTER_SPEED, 0x002e, 0x01ff, - read_function_id=0x0, - write_function_id=0x10, - bytes_count=2, - label=_POINTER_SPEED[1], description=_POINTER_SPEED[2], - device_kind=(_DK.mouse, _DK.trackball)) + """Pointer Speed feature""" + # min and max values taken from usb traces of Win software + return feature_range(_POINTER_SPEED[0], + _F.POINTER_SPEED, + 0x002e, + 0x01ff, + read_function_id=0x0, + write_function_id=0x10, + bytes_count=2, + label=_POINTER_SPEED[1], + description=_POINTER_SPEED[2], + device_kind=(_DK.mouse, _DK.trackball)) + # the keys for the choice map are Logitech controls (from special_keys) # each choice value is a NamedInt with the string from a task (to be shown to the user) # and the integer being the control number for that task (to be written to the device) # Solaar only remaps keys (controlled by key gmask and group), not other key reprogramming def _feature_reprogrammable_keys_choices(device): - count = device.feature_request(_F.REPROG_CONTROLS_V4) - assert count, 'Oops, reprogrammable key count cannot be retrieved!' - count = ord(count[:1]) # the number of key records - keys = [None] * count - groups = [ [] for i in range(0,9) ] - choices = {} - for i in range(0,count): # get the data for each key record on device - keydata = device.feature_request(_F.REPROG_CONTROLS_V4, 0x10, i) - key, key_task, flags, pos, group, gmask = _unpack('!HHBBBB', keydata[:8]) - action =_NamedInt(key, str(_special_keys.TASK[key_task])) - keys[i] = ( _special_keys.CONTROL[key], action, flags, gmask ) - groups[group].append(action) - for k in keys: - # if k[2] & _special_keys.KEY_FLAG.reprogrammable: # this flag is only to show in UI, ignore in Solaar - if k[3]: # only keys with a non-zero gmask are remappable - key_choices = [ k[1] ] # it should always be possible to map the key to itself - for g in range(1,9): # group 0 and gmask 0 (k[3]) does not indicate remappability so don't consider group 0 - if ( k[3]==0 if g==0 else k[3] & 2**(g-1) ): - for gm in groups[g]: - if int(gm) != int(k[0]): # don't put itself in twice - key_choices.append(gm) - if len(key_choices) > 1: - choices[k[0]] = key_choices - return choices + count = device.feature_request(_F.REPROG_CONTROLS_V4) + assert count, 'Oops, reprogrammable key count cannot be retrieved!' + count = ord(count[:1]) # the number of key records + keys = [None] * count + groups = [[] for i in range(0, 9)] + choices = {} + for i in range(0, count): # get the data for each key record on device + keydata = device.feature_request(_F.REPROG_CONTROLS_V4, 0x10, i) + key, key_task, flags, pos, group, gmask = _unpack( + '!HHBBBB', keydata[:8]) + action = _NamedInt(key, str(_special_keys.TASK[key_task])) + keys[i] = (_special_keys.CONTROL[key], action, flags, gmask) + groups[group].append(action) + for k in keys: + # if k[2] & _special_keys.KEY_FLAG.reprogrammable: # this flag is only to show in UI, ignore in Solaar + if k[3]: # only keys with a non-zero gmask are remappable + key_choices = [ + k[1] + ] # it should always be possible to map the key to itself + for g in range( + 1, 9 + ): # group 0 and gmask 0 (k[3]) does not indicate remappability so don't consider group 0 + if (k[3] == 0 if g == 0 else k[3] & 2**(g - 1)): + for gm in groups[g]: + if int(gm) != int(k[0]): # don't put itself in twice + key_choices.append(gm) + if len(key_choices) > 1: + choices[k[0]] = key_choices + return choices + def _feature_reprogrammable_keys(): - return feature_map_choices_dynamic(_REPROGRAMMABLE_KEYS[0], _F.REPROG_CONTROLS_V4, - _feature_reprogrammable_keys_choices, - read_function_id=0x20, write_function_id=0x30, - key_bytes_count=2, skip_bytes_count=1, value_bytes_count=2, - label=_REPROGRAMMABLE_KEYS[1], description=_REPROGRAMMABLE_KEYS[2], - device_kind=(_DK.keyboard,), extra_default=0) + return feature_map_choices_dynamic(_REPROGRAMMABLE_KEYS[0], + _F.REPROG_CONTROLS_V4, + _feature_reprogrammable_keys_choices, + read_function_id=0x20, + write_function_id=0x30, + key_bytes_count=2, + skip_bytes_count=1, + value_bytes_count=2, + label=_REPROGRAMMABLE_KEYS[1], + description=_REPROGRAMMABLE_KEYS[2], + device_kind=(_DK.keyboard, ), + extra_default=0) def _feature_disable_keyboard_keys_key_list(device): - mask = device.feature_request(_F.KEYBOARD_DISABLE_KEYS)[0] - options = [_special_keys.DISABLE[1 << i] for i in range(8) if mask & (1 << i)] - return options + mask = device.feature_request(_F.KEYBOARD_DISABLE_KEYS)[0] + options = [ + _special_keys.DISABLE[1 << i] for i in range(8) if mask & (1 << i) + ] + return options + def _feature_disable_keyboard_keys(): - return feature_bitfield_toggle_dynamic(_DISABLE_KEYS[0], _F.KEYBOARD_DISABLE_KEYS, - _feature_disable_keyboard_keys_key_list, - read_function_id=0x10, write_function_id=0x20, - label=_DISABLE_KEYS[1], description=_DISABLE_KEYS[2], device_kind=(_DK.keyboard,)) + return feature_bitfield_toggle_dynamic( + _DISABLE_KEYS[0], + _F.KEYBOARD_DISABLE_KEYS, + _feature_disable_keyboard_keys_key_list, + read_function_id=0x10, + write_function_id=0x20, + label=_DISABLE_KEYS[1], + description=_DISABLE_KEYS[2], + device_kind=(_DK.keyboard, )) + # # @@ -427,29 +672,46 @@ def _feature_disable_keyboard_keys(): from collections import namedtuple + def _S(name, featureID=None, featureFn=None, registerFn=None, identifier=None): - return ( name, featureID, featureFn, registerFn, identifier if identifier else name.replace('-','_') ) + return (name, featureID, featureFn, registerFn, + identifier if identifier else name.replace('-', '_')) + _SETTINGS_TABLE = [ - _S( _HAND_DETECTION[0], registerFn=_register_hand_detection ), - _S( _SMOOTH_SCROLL[0], registerFn=_register_smooth_scroll ), - _S( _SIDE_SCROLL[0], registerFn=_register_side_scroll ), - _S( _HI_RES_SCROLL[0], _F.HI_RES_SCROLLING, _feature_hi_res_scroll ), - _S( _LOW_RES_SCROLL[0], _F.LOWRES_WHEEL, _feature_lowres_smooth_scroll ), - _S( _HIRES_INV[0], _F.HIRES_WHEEL, _feature_hires_smooth_invert ), - _S( _HIRES_RES[0], _F.HIRES_WHEEL, _feature_hires_smooth_resolution ), - _S( _FN_SWAP[0], _F.FN_INVERSION, _feature_fn_swap, registerFn=_register_fn_swap ), - _S( _FN_SWAP[0], _F.NEW_FN_INVERSION, _feature_new_fn_swap, identifier='new_fn_swap' ), - _S( _FN_SWAP[0], _F.K375S_FN_INVERSION, _feature_k375s_fn_swap, identifier='k375s_fn_swap' ), - _S( _DPI[0], _F.ADJUSTABLE_DPI, _feature_adjustable_dpi, registerFn=_register_dpi ), - _S( _POINTER_SPEED[0], _F.POINTER_SPEED, _feature_pointer_speed ), - _S( _SMART_SHIFT[0], _F.SMART_SHIFT, _feature_smart_shift ), - _S( _BACKLIGHT[0], _F.BACKLIGHT2, _feature_backlight2 ), - _S( _REPROGRAMMABLE_KEYS[0], _F.REPROG_CONTROLS_V4, _feature_reprogrammable_keys ), - _S( _DISABLE_KEYS[0], _F.KEYBOARD_DISABLE_KEYS, _feature_disable_keyboard_keys ), + _S(_HAND_DETECTION[0], registerFn=_register_hand_detection), + _S(_SMOOTH_SCROLL[0], registerFn=_register_smooth_scroll), + _S(_SIDE_SCROLL[0], registerFn=_register_side_scroll), + _S(_HI_RES_SCROLL[0], _F.HI_RES_SCROLLING, _feature_hi_res_scroll), + _S(_LOW_RES_SCROLL[0], _F.LOWRES_WHEEL, _feature_lowres_smooth_scroll), + _S(_HIRES_INV[0], _F.HIRES_WHEEL, _feature_hires_smooth_invert), + _S(_HIRES_RES[0], _F.HIRES_WHEEL, _feature_hires_smooth_resolution), + _S(_FN_SWAP[0], + _F.FN_INVERSION, + _feature_fn_swap, + registerFn=_register_fn_swap), + _S(_FN_SWAP[0], + _F.NEW_FN_INVERSION, + _feature_new_fn_swap, + identifier='new_fn_swap'), + _S(_FN_SWAP[0], + _F.K375S_FN_INVERSION, + _feature_k375s_fn_swap, + identifier='k375s_fn_swap'), + _S(_DPI[0], + _F.ADJUSTABLE_DPI, + _feature_adjustable_dpi, + registerFn=_register_dpi), + _S(_POINTER_SPEED[0], _F.POINTER_SPEED, _feature_pointer_speed), + _S(_SMART_SHIFT[0], _F.SMART_SHIFT, _feature_smart_shift), + _S(_BACKLIGHT[0], _F.BACKLIGHT2, _feature_backlight2), + _S(_REPROGRAMMABLE_KEYS[0], _F.REPROG_CONTROLS_V4, + _feature_reprogrammable_keys), + _S(_DISABLE_KEYS[0], _F.KEYBOARD_DISABLE_KEYS, + _feature_disable_keyboard_keys), ] -_SETTINGS_LIST = namedtuple('_SETTINGS_LIST',[s[4] for s in _SETTINGS_TABLE]) +_SETTINGS_LIST = namedtuple('_SETTINGS_LIST', [s[4] for s in _SETTINGS_TABLE]) RegisterSettings = _SETTINGS_LIST._make([s[3] for s in _SETTINGS_TABLE]) FeatureSettings = _SETTINGS_LIST._make([s[2] for s in _SETTINGS_TABLE]) @@ -459,35 +721,38 @@ del _SETTINGS_LIST # # + # Returns True if device was queried to find features, False otherwise def check_feature_settings(device, already_known): - """Try to auto-detect device settings by the HID++ 2.0 features they have.""" - if device.features is None or not device.online: - return False - if device.protocol and device.protocol < 2.0: - return False + """Try to auto-detect device settings by the HID++ 2.0 features they have.""" + if device.features is None or not device.online: + return False + if device.protocol and device.protocol < 2.0: + return False - def check_feature(name, featureId, featureFn): - """ + def check_feature(name, featureId, featureFn): + """ :param name: name for the setting :param featureId: the numeric Feature ID for this setting implementation :param featureFn: the function for this setting implementation """ - if not featureId in device.features: - return - if any(s.name == name for s in already_known): - return + if not featureId in device.features: + return + if any(s.name == name for s in already_known): + return - try: - detected = featureFn()(device) - if _log.isEnabledFor(_DEBUG): - _log.debug("check_feature[%s] detected %s", featureId, detected) - if detected: - already_known.append(detected) - except Exception as reason: - _log.error("check_feature[%s] inconsistent feature %s", featureId, reason) + try: + detected = featureFn()(device) + if _log.isEnabledFor(_DEBUG): + _log.debug("check_feature[%s] detected %s", featureId, + detected) + if detected: + already_known.append(detected) + except Exception as reason: + _log.error("check_feature[%s] inconsistent feature %s", featureId, + reason) - for name, featureId, featureFn, _, _ in _SETTINGS_TABLE : - if featureId and featureFn: - check_feature(name, featureId, featureFn) - return True + for name, featureId, featureFn, _, _ in _SETTINGS_TABLE: + if featureId and featureFn: + check_feature(name, featureId, featureFn) + return True diff --git a/lib/logitech_receiver/special_keys.py b/lib/logitech_receiver/special_keys.py index a9e80aae..95140342 100644 --- a/lib/logitech_receiver/special_keys.py +++ b/lib/logitech_receiver/special_keys.py @@ -22,490 +22,488 @@ from __future__ import absolute_import, division, print_function, unicode_literals - from .common import NamedInts as _NamedInts # tasks.py - Switch_Presentation__Switch_Screen=0x0093, # on K400 Plus - Minimize_Window=0x0094, - Maximize_Window=0x0095, # on K400 Plus - MultiPlatform_App_Switch=0x0096, - MultiPlatform_Home=0x0097, - MultiPlatform_Menu=0x0098, - MultiPlatform_Back=0x0099, - Switch_Language=0x009A, # Mac_switch_language - Screen_Capture=0x009B, # Mac_screen_Capture, on Craft Keyboard - Gesture_Button=0x009C, - Smart_Shift=0x009D, - AppExpose=0x009E, - Smart_Zoom=0x009F, - Lookup=0x00A0, - Microphone_on__off=0x00A1, - Wifi_on__off=0x00A2, - Brightness_Down=0x00A3, - Brightness_Up=0x00A4, - Display_Out=0x00A5, - View_Open_Apps=0x00A6, - View_All_Open_Apps=0x00A7, - AppSwitch=0x00A8, - Gesture_Button_Navigation=0x00A9, # Mouse_Thumb_Button on MX Master - Fn_inversion=0x00AA, - Multiplatform_Back=0x00AB, - Multiplatform_Forward=0x00AC, - Multiplatform_Gesture_Button=0x00AD, - HostSwitch_Channel_1=0x00AE, - HostSwitch_Channel_2=0x00AF, - HostSwitch_Channel_3=0x00B0, - Multiplatform_Search=0x00B1, - Multiplatform_Home__Mission_Control=0x00B2, - Multiplatform_Menu__Launchpad=0x00B3, - Virtual_Gesture_Button=0x00B4, - Cursor=0x00B5, - Keyboard_Right_Arrow=0x00B6, - SW_Custom_Highlight=0x00B7, - Keyboard_Left_Arrow=0x00B8, - TBD=0x00B9, - Multiplatform_Language_Switch=0x00BA, - SW_Custom_Highlight_2=0x00BB, - Fast_Forward=0x00BC, - Fast_Backward=0x00BD, - Switch_Highlighting=0x00BE, - Mission_Control__Task_View=0x00BF, # Switch_Workspace on Craft Keyboard - Dashboard_Launchpad__Action_Center=0x00C0, # Application_Launcher on Craft Keyboard - Backlight_Down=0x00C1, # Backlight_Down_FW_internal_function - Backlight_Up=0x00C2, # Backlight_Up_FW_internal_function - Right_Click__App_Contextual_Menu=0x00C3, # Context_Menu on Craft Keyboard - DPI_Change=0x00C4, - New_Tab=0x00C5, - F2=0x00C6, - F3=0x00C7, - F4=0x00C8, - F5=0x00C9, - F6=0x00CA, - F7=0x00CB, - F8=0x00CC, - F1=0x00CD, - Laser_Button=0x00CE, - Laser_Button_Long_Press=0x00CF, - Start_Presentation=0x00D0, - Blank_Screen=0x00D1, - DPI_Switch=0x00D2, # AdjustDPI on MX Vertical - Home__Show_Desktop=0x00D3, - App_Switch__Dashboard=0x00D4, - App_Switch=0x00D5, - Fn_Inversion=0x00D6, - LeftAndRightClick=0x00D7, - LedToggle=0x00DD, # + # Both 0x001E and 0x001F are known as MediaCenterSet + Media_Center_Logitech=0x001E, + Media_Center_Microsoft=0x001F, + UserMenu=0x0020, + Messenger=0x0021, + PersonalFolders=0x0022, + MyMusic=0x0023, + Webcam=0x0024, + PicturesFolder=0x0025, + MyVideos=0x0026, + My_Computer=0x0027, + PictureAppSet=0x0028, + Search=0x0029, # also known as AdvSmartSearch + RecordMediaPlayer=0x002A, + BrowserRefresh=0x002B, + RotateRight=0x002C, + Search_Files=0x002D, # SearchForFiles + MM_SHUFFLE=0x002E, + Sleep=0x002F, # also known as StandBySet + BrowserStop=0x0030, + OneTouchSync=0x0031, + ZoomSet=0x0032, + ZoomBtnInSet2=0x0033, + ZoomBtnInSet=0x0034, + ZoomBtnOutSet2=0x0035, + ZoomBtnOutSet=0x0036, + ZoomBtnResetSet=0x0037, + Left_Click=0x0038, # LeftClick + Right_Click=0x0039, # RightClick + Mouse_Middle_Button=0x003A, # from M510v2 was MiddleMouseButton + Back=0x003B, + Mouse_Back_Button=0x003C, # from M510v2 was BackEx + BrowserForward=0x003D, + Mouse_Forward_Button=0x003E, # from M510v2 was BrowserForwardEx + Mouse_Scroll_Left_Button_=0x003F, # from M510v2 was HorzScrollLeftSet + Mouse_Scroll_Right_Button=0x0040, # from M510v2 was HorzScrollRightSet + QuickSwitch=0x0041, + BatteryStatus=0x0042, + Show_Desktop=0x0043, # ShowDesktop + WindowsLock=0x0044, + FileLauncher=0x0045, + FolderLauncher=0x0046, + GotoWebAddress=0x0047, + GenericMouseButton=0x0048, + KeystrokeAssignment=0x0049, + LaunchProgram=0x004A, + MinMaxWindow=0x004B, + VOLUMEMUTE_NoOSD=0x004C, + New=0x004D, + Copy=0x004E, + CruiseDown=0x004F, + CruiseUp=0x0050, + Cut=0x0051, + Do_Nothing=0x0052, + PageDown=0x0053, + PageUp=0x0054, + Paste=0x0055, + SearchPicture=0x0056, + Reply=0x0057, + PhotoGallerySet=0x0058, + MM_REWIND=0x0059, + MM_FASTFORWARD=0x005A, + Send=0x005B, + ControlPanel=0x005C, + UniversalScroll=0x005D, + AutoScroll=0x005E, + GenericButton=0x005F, + MM_NEXT=0x0060, + MM_PREVIOUS=0x0061, + Do_Nothing_One=0x0062, # also known as Do_Nothing + SnapLeft=0x0063, + SnapRight=0x0064, + WinMinRestore=0x0065, + WinMaxRestore=0x0066, + WinStretch=0x0067, + SwitchMonitorLeft=0x0068, + SwitchMonitorRight=0x0069, + ShowPresentation=0x006A, + ShowMobilityCenter=0x006B, + HorzScrollNoRepeatSet=0x006C, + TouchBackForwardHorzScroll=0x0077, + MetroAppSwitch=0x0078, + MetroAppBar=0x0079, + MetroCharms=0x007A, + Calculator_VKEY=0x007B, # also known as Calculator + MetroSearch=0x007C, + MetroStartScreen=0x0080, + MetroShare=0x007D, + MetroSettings=0x007E, + MetroDevices=0x007F, + MetroBackLeftHorz=0x0082, + MetroForwRightHorz=0x0083, + Win8_Back=0x0084, # also known as MetroCharms + Win8_Forward=0x0085, # also known as AppSwitchBar + Win8Charm_Appswitch_GifAnimation=0x0086, + Win8BackHorzLeft=0x008B, # also known as Back + Win8ForwardHorzRight=0x008C, # also known as BrowserForward + MetroSearch2=0x0087, + MetroShare2=0x0088, + MetroSettings2=0x008A, + MetroDevices2=0x0089, + Win8MetroWin7Forward=0x008D, # also known as MetroStartScreen + Win8ShowDesktopWin7Back=0x008E, # also known as ShowDesktop + MetroApplicationSwitch=0x0090, # also known as MetroStartScreen + ShowUI=0x0092, + # https://docs.google.com/document/d/1Dpx_nWRQAZox_zpZ8SNc9nOkSDE9svjkghOCbzopabc/edit + # Extract to csv. Eliminate extra linefeeds and spaces. Turn / into __ and space into _ + # awk -F, '/0x/{gsub(" \\+ ","_",$2); gsub("_-","_Down",$2); gsub("_\\+","_Up",$2); gsub("[()\"-]","",$2); gsub(" ","_",$2); printf("\t%s=0x%04X,\n", $2, $1)}' < tasks.csv > tasks.py + Switch_Presentation__Switch_Screen=0x0093, # on K400 Plus + Minimize_Window=0x0094, + Maximize_Window=0x0095, # on K400 Plus + MultiPlatform_App_Switch=0x0096, + MultiPlatform_Home=0x0097, + MultiPlatform_Menu=0x0098, + MultiPlatform_Back=0x0099, + Switch_Language=0x009A, # Mac_switch_language + Screen_Capture=0x009B, # Mac_screen_Capture, on Craft Keyboard + Gesture_Button=0x009C, + Smart_Shift=0x009D, + AppExpose=0x009E, + Smart_Zoom=0x009F, + Lookup=0x00A0, + Microphone_on__off=0x00A1, + Wifi_on__off=0x00A2, + Brightness_Down=0x00A3, + Brightness_Up=0x00A4, + Display_Out=0x00A5, + View_Open_Apps=0x00A6, + View_All_Open_Apps=0x00A7, + AppSwitch=0x00A8, + Gesture_Button_Navigation=0x00A9, # Mouse_Thumb_Button on MX Master + Fn_inversion=0x00AA, + Multiplatform_Back=0x00AB, + Multiplatform_Forward=0x00AC, + Multiplatform_Gesture_Button=0x00AD, + HostSwitch_Channel_1=0x00AE, + HostSwitch_Channel_2=0x00AF, + HostSwitch_Channel_3=0x00B0, + Multiplatform_Search=0x00B1, + Multiplatform_Home__Mission_Control=0x00B2, + Multiplatform_Menu__Launchpad=0x00B3, + Virtual_Gesture_Button=0x00B4, + Cursor=0x00B5, + Keyboard_Right_Arrow=0x00B6, + SW_Custom_Highlight=0x00B7, + Keyboard_Left_Arrow=0x00B8, + TBD=0x00B9, + Multiplatform_Language_Switch=0x00BA, + SW_Custom_Highlight_2=0x00BB, + Fast_Forward=0x00BC, + Fast_Backward=0x00BD, + Switch_Highlighting=0x00BE, + Mission_Control__Task_View=0x00BF, # Switch_Workspace on Craft Keyboard + Dashboard_Launchpad__Action_Center= + 0x00C0, # Application_Launcher on Craft Keyboard + Backlight_Down=0x00C1, # Backlight_Down_FW_internal_function + Backlight_Up=0x00C2, # Backlight_Up_FW_internal_function + Right_Click__App_Contextual_Menu=0x00C3, # Context_Menu on Craft Keyboard + DPI_Change=0x00C4, + New_Tab=0x00C5, + F2=0x00C6, + F3=0x00C7, + F4=0x00C8, + F5=0x00C9, + F6=0x00CA, + F7=0x00CB, + F8=0x00CC, + F1=0x00CD, + Laser_Button=0x00CE, + Laser_Button_Long_Press=0x00CF, + Start_Presentation=0x00D0, + Blank_Screen=0x00D1, + DPI_Switch=0x00D2, # AdjustDPI on MX Vertical + Home__Show_Desktop=0x00D3, + App_Switch__Dashboard=0x00D4, + App_Switch=0x00D5, + Fn_Inversion=0x00D6, + LeftAndRightClick=0x00D7, + LedToggle=0x00DD, # ) TASK._fallback = lambda x: 'unknown:%04X' % x # hidpp 4.5 info from https://lekensteyn.nl/files/logitech/x1b04_specialkeysmsebuttons.html -KEY_FLAG = _NamedInts( - virtual=0x80, - persistently_divertable=0x40, - divertable=0x20, - reprogrammable=0x10, - FN_sensitive=0x08, - nonstandard=0x04, - is_FN=0x02, - mse=0x01 -) +KEY_FLAG = _NamedInts(virtual=0x80, + persistently_divertable=0x40, + divertable=0x20, + reprogrammable=0x10, + FN_sensitive=0x08, + nonstandard=0x04, + is_FN=0x02, + mse=0x01) DISABLE = _NamedInts( - Caps_Lock=0x01, - Num_Lock=0x02, - Scroll_Lock=0x04, - Insert=0x08, - Win=0x10, # aka Super + Caps_Lock=0x01, + Num_Lock=0x02, + Scroll_Lock=0x04, + Insert=0x08, + Win=0x10, # aka Super ) DISABLE._fallback = lambda x: 'unknown:%02X' % x diff --git a/lib/logitech_receiver/status.py b/lib/logitech_receiver/status.py index 726b84a5..9c77d9bc 100644 --- a/lib/logitech_receiver/status.py +++ b/lib/logitech_receiver/status.py @@ -25,7 +25,6 @@ from logging import getLogger, DEBUG as _DEBUG _log = getLogger(__name__) del getLogger - from .i18n import _, ngettext from .common import NamedInts as _NamedInts, NamedInt as _NamedInt from . import hidpp10 as _hidpp10 @@ -37,19 +36,23 @@ _R = _hidpp10.REGISTERS # # -ALERT = _NamedInts(NONE=0x00, NOTIFICATION=0x01, SHOW_WINDOW=0x02, ATTENTION=0x04, ALL=0xFF) +ALERT = _NamedInts(NONE=0x00, + NOTIFICATION=0x01, + SHOW_WINDOW=0x02, + ATTENTION=0x04, + ALL=0xFF) KEYS = _NamedInts( - BATTERY_LEVEL=1, - BATTERY_CHARGING=2, - BATTERY_STATUS=3, - LIGHT_LEVEL=4, - LINK_ENCRYPTED=5, - NOTIFICATION_FLAGS=6, - ERROR=7, - BATTERY_NEXT_LEVEL=8, - BATTERY_VOLTAGE=9, - ) + BATTERY_LEVEL=1, + BATTERY_CHARGING=2, + BATTERY_STATUS=3, + LIGHT_LEVEL=4, + LINK_ENCRYPTED=5, + NOTIFICATION_FLAGS=6, + ERROR=7, + BATTERY_NEXT_LEVEL=8, + BATTERY_VOLTAGE=9, +) # If the battery charge is under this percentage, trigger an attention event # (blink systray icon/notification/whatever). @@ -64,286 +67,331 @@ _LONG_SLEEP = 15 * 60 # seconds # # + def attach_to(device, changed_callback): - assert device - assert changed_callback + assert device + assert changed_callback + + if not hasattr(device, 'status') or device.status is None: + if device.kind is None: + device.status = ReceiverStatus(device, changed_callback) + else: + device.status = DeviceStatus(device, changed_callback) - if not hasattr(device, 'status') or device.status is None: - if device.kind is None: - device.status = ReceiverStatus(device, changed_callback) - else: - device.status = DeviceStatus(device, changed_callback) # # # + class ReceiverStatus(dict): - """The 'runtime' status of a receiver, mostly about the pairing process -- + """The 'runtime' status of a receiver, mostly about the pairing process -- is the pairing lock open or closed, any pairing errors, etc. """ - def __init__(self, receiver, changed_callback): - assert receiver - self._receiver = receiver + def __init__(self, receiver, changed_callback): + assert receiver + self._receiver = receiver - assert changed_callback - self._changed_callback = changed_callback + assert changed_callback + self._changed_callback = changed_callback - # self.updated = 0 + # self.updated = 0 - self.lock_open = False - self.new_device = None + self.lock_open = False + self.new_device = None - self[KEYS.ERROR] = None + self[KEYS.ERROR] = None - def __str__(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 }) - __unicode__ = __str__ + def __str__(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 + }) - def changed(self, alert=ALERT.NOTIFICATION, reason=None): - # self.updated = _timestamp() - self._changed_callback(self._receiver, alert=alert, reason=reason) + __unicode__ = __str__ + + def changed(self, alert=ALERT.NOTIFICATION, reason=None): + # self.updated = _timestamp() + self._changed_callback(self._receiver, alert=alert, reason=reason) + + # def poll(self, timestamp): + # r = self._receiver + # assert r + # + # if _log.isEnabledFor(_DEBUG): + # _log.debug("polling status of %s", r) + # + # # make sure to read some stuff that may be read later by the UI + # r.serial, r.firmware, None + # + # # get an update of the notification flags + # # self[KEYS.NOTIFICATION_FLAGS] = _hidpp10.get_notification_flags(r) - # def poll(self, timestamp): - # r = self._receiver - # assert r - # - # if _log.isEnabledFor(_DEBUG): - # _log.debug("polling status of %s", r) - # - # # make sure to read some stuff that may be read later by the UI - # r.serial, r.firmware, None - # - # # get an update of the notification flags - # # self[KEYS.NOTIFICATION_FLAGS] = _hidpp10.get_notification_flags(r) # # # + class DeviceStatus(dict): - """Holds the 'runtime' status of a peripheral -- things like + """Holds the 'runtime' status of a peripheral -- things like active/inactive, battery charge, lux, etc. It updates them mostly by processing incoming notification events from the device itself. """ - def __init__(self, device, changed_callback): - assert device - self._device = device + def __init__(self, device, changed_callback): + assert device + self._device = device - assert changed_callback - self._changed_callback = changed_callback + assert changed_callback + self._changed_callback = changed_callback - # is the device active? - self._active = None + # is the device active? + self._active = None - # timestamp of when this status object was last updated - self.updated = 0 + # timestamp of when this status object was last updated + self.updated = 0 - def to_string(self): - def _items(): - comma = False + def to_string(self): + def _items(): + comma = False - battery_level = self.get(KEYS.BATTERY_LEVEL) - if battery_level is not None: - if isinstance(battery_level, _NamedInt): - yield _("Battery: %(level)s") % { 'level': _(str(battery_level)) } - else: - yield _("Battery: %(percent)d%%") % { 'percent': battery_level } + battery_level = self.get(KEYS.BATTERY_LEVEL) + if battery_level is not None: + if isinstance(battery_level, _NamedInt): + yield _("Battery: %(level)s") % { + 'level': _(str(battery_level)) + } + else: + yield _("Battery: %(percent)d%%") % { + 'percent': battery_level + } - battery_status = self.get(KEYS.BATTERY_STATUS) - if battery_status is not None: - yield ' (%s)' % _(str(battery_status)) + battery_status = self.get(KEYS.BATTERY_STATUS) + if battery_status is not None: + yield ' (%s)' % _(str(battery_status)) - comma = True + comma = True - light_level = self.get(KEYS.LIGHT_LEVEL) - if light_level is not None: - if comma: yield ', ' - yield _("Lighting: %(level)s lux") % { 'level': light_level } + light_level = self.get(KEYS.LIGHT_LEVEL) + if light_level is not None: + if comma: yield ', ' + yield _("Lighting: %(level)s lux") % {'level': light_level} - return ''.join(i for i in _items()) + return ''.join(i for i in _items()) - def __repr__(self): - return '{' + ', '.join('\'%s\': %r' % (k, v) for k, v in self.items()) + '}' + def __repr__(self): + return '{' + ', '.join('\'%s\': %r' % (k, v) + for k, v in self.items()) + '}' - def __bool__(self): - return bool(self._active) - __nonzero__ = __bool__ + def __bool__(self): + return bool(self._active) - def set_battery_info(self, level, status, nextLevel=None, voltage=None, timestamp=None): - if _log.isEnabledFor(_DEBUG): - _log.debug("%s: battery %s, %s", self._device, level, status) + __nonzero__ = __bool__ - if level is None: - # Some notifications may come with no battery level info, just - # charging state info, so do our best to infer a level (even if it is just the last level) - # It is not always possible to do this well - if status == _hidpp20.BATTERY_STATUS.full: - level = _hidpp10.BATTERY_APPOX.full - elif status in (_hidpp20.BATTERY_STATUS.almost_full, _hidpp20.BATTERY_STATUS.recharging): - level = _hidpp10.BATTERY_APPOX.good - elif status == _hidpp20.BATTERY_STATUS.slow_recharge: - level = _hidpp10.BATTERY_APPOX.low - else: - level = self.get(KEYS.BATTERY_LEVEL) - else: - assert isinstance(level, int) + def set_battery_info(self, + level, + status, + nextLevel=None, + voltage=None, + timestamp=None): + if _log.isEnabledFor(_DEBUG): + _log.debug("%s: battery %s, %s", self._device, level, status) - # TODO: this is also executed when pressing Fn+F7 on K800. - old_level, self[KEYS.BATTERY_LEVEL] = self.get(KEYS.BATTERY_LEVEL), level - old_status, self[KEYS.BATTERY_STATUS] = self.get(KEYS.BATTERY_STATUS), status - self[KEYS.BATTERY_NEXT_LEVEL] = nextLevel - if voltage is not None: - self[KEYS.BATTERY_VOLTAGE] = voltage + if level is None: + # Some notifications may come with no battery level info, just + # charging state info, so do our best to infer a level (even if it is just the last level) + # It is not always possible to do this well + if status == _hidpp20.BATTERY_STATUS.full: + level = _hidpp10.BATTERY_APPOX.full + elif status in (_hidpp20.BATTERY_STATUS.almost_full, + _hidpp20.BATTERY_STATUS.recharging): + level = _hidpp10.BATTERY_APPOX.good + elif status == _hidpp20.BATTERY_STATUS.slow_recharge: + level = _hidpp10.BATTERY_APPOX.low + else: + level = self.get(KEYS.BATTERY_LEVEL) + else: + assert isinstance(level, int) - charging = status in (_hidpp20.BATTERY_STATUS.recharging, _hidpp20.BATTERY_STATUS.almost_full, - _hidpp20.BATTERY_STATUS.full, _hidpp20.BATTERY_STATUS.slow_recharge) - old_charging, self[KEYS.BATTERY_CHARGING] = self.get(KEYS.BATTERY_CHARGING), charging + # TODO: this is also executed when pressing Fn+F7 on K800. + old_level, self[KEYS.BATTERY_LEVEL] = self.get( + KEYS.BATTERY_LEVEL), level + old_status, self[KEYS.BATTERY_STATUS] = self.get( + KEYS.BATTERY_STATUS), status + self[KEYS.BATTERY_NEXT_LEVEL] = nextLevel + if voltage is not None: + self[KEYS.BATTERY_VOLTAGE] = voltage - changed = old_level != level or old_status != status or old_charging != charging - alert, reason = ALERT.NONE, None + charging = status in (_hidpp20.BATTERY_STATUS.recharging, + _hidpp20.BATTERY_STATUS.almost_full, + _hidpp20.BATTERY_STATUS.full, + _hidpp20.BATTERY_STATUS.slow_recharge) + old_charging, self[KEYS.BATTERY_CHARGING] = self.get( + KEYS.BATTERY_CHARGING), charging - if _hidpp20.BATTERY_OK(status) and ( level is None or level > _BATTERY_ATTENTION_LEVEL ): - self[KEYS.ERROR] = None - else: - _log.warn("%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) } - else: - reason = _("Battery: %(percent)d%% (%(status)s)") % { 'percent': level, 'status': status.name } + changed = old_level != level or old_status != status or old_charging != charging + alert, reason = ALERT.NONE, None - if changed or reason: - # update the leds on the device, if any - _hidpp10.set_3leds(self._device, level, charging=charging, warning=bool(alert)) - self.changed(active=True, alert=alert, reason=reason, timestamp=timestamp) + if _hidpp20.BATTERY_OK(status) and (level is None or + level > _BATTERY_ATTENTION_LEVEL): + self[KEYS.ERROR] = None + else: + _log.warn("%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) + } + else: + reason = _("Battery: %(percent)d%% (%(status)s)") % { + 'percent': level, + 'status': status.name + } - # Retrieve and regularize battery status - def read_battery(self, timestamp=None): - if self._active: - d = self._device - assert d + if changed or reason: + # update the leds on the device, if any + _hidpp10.set_3leds(self._device, + level, + charging=charging, + warning=bool(alert)) + self.changed(active=True, + alert=alert, + reason=reason, + timestamp=timestamp) - if d.protocol < 2.0: - battery = _hidpp10.get_battery(d) - self.set_battery_keys(battery) - return + # Retrieve and regularize battery status + def read_battery(self, timestamp=None): + if self._active: + d = self._device + assert d - battery = _hidpp20.get_battery(d) - if battery is None: - v = _hidpp20.get_voltage(d) - if v is not None: - level, status, voltage, _ignore, _ignore = v - self.set_battery_keys( (level, status, None), voltage) - return + if d.protocol < 2.0: + battery = _hidpp10.get_battery(d) + self.set_battery_keys(battery) + return - # Really unnecessary, if the device has SOLAR_DASHBOARD it should be - # broadcasting it's battery status anyway, it will just take a little while. - # However, when the device has just been detected, it will not show - # any battery status for a while (broadcasts happen every 90 seconds). - if battery is None and d.features and _hidpp20.FEATURE.SOLAR_DASHBOARD in d.features: - d.feature_request(_hidpp20.FEATURE.SOLAR_DASHBOARD, 0x00, 1, 1) - return - self.set_battery_keys(battery) + battery = _hidpp20.get_battery(d) + if battery is None: + v = _hidpp20.get_voltage(d) + if v is not None: + level, status, voltage, _ignore, _ignore = v + self.set_battery_keys((level, status, None), voltage) + return - def set_battery_keys(self, battery, voltage=None) : - if battery is not None: - level, status, nextLevel = battery - self.set_battery_info(level, status, nextLevel, voltage) - elif KEYS.BATTERY_STATUS in self: - self[KEYS.BATTERY_STATUS] = None - self[KEYS.BATTERY_CHARGING] = None - self.changed() + # Really unnecessary, if the device has SOLAR_DASHBOARD it should be + # broadcasting it's battery status anyway, it will just take a little while. + # However, when the device has just been detected, it will not show + # any battery status for a while (broadcasts happen every 90 seconds). + if battery is None and d.features and _hidpp20.FEATURE.SOLAR_DASHBOARD in d.features: + d.feature_request(_hidpp20.FEATURE.SOLAR_DASHBOARD, 0x00, 1, 1) + return + self.set_battery_keys(battery) - def changed(self, active=None, alert=ALERT.NONE, reason=None, timestamp=None): - assert self._changed_callback - d = self._device - # assert d # may be invalid when processing the 'unpaired' notification - timestamp = timestamp or _timestamp() + def set_battery_keys(self, battery, voltage=None): + if battery is not None: + level, status, nextLevel = battery + self.set_battery_info(level, status, nextLevel, voltage) + elif KEYS.BATTERY_STATUS in self: + self[KEYS.BATTERY_STATUS] = None + self[KEYS.BATTERY_CHARGING] = None + self.changed() - if active is not None: - d.online = active - was_active, self._active = self._active, active - if active: - if not was_active: - # Make sure to set notification flags on the device, they - # get cleared when the device is turned off (but not when the device - # goes idle, and we can't tell the difference right now). - if d.protocol < 2.0: - self[KEYS.NOTIFICATION_FLAGS] = d.enable_notifications() + def changed(self, + active=None, + alert=ALERT.NONE, + reason=None, + timestamp=None): + assert self._changed_callback + d = self._device + # assert d # may be invalid when processing the 'unpaired' notification + timestamp = timestamp or _timestamp() - # If we've been inactive for a long time, forget anything - # about the battery. - if self.updated > 0 and timestamp - self.updated > _LONG_SLEEP: - self[KEYS.BATTERY_LEVEL] = None - self[KEYS.BATTERY_STATUS] = None - self[KEYS.BATTERY_CHARGING] = None + if active is not None: + d.online = active + was_active, self._active = self._active, active + if active: + if not was_active: + # Make sure to set notification flags on the device, they + # get cleared when the device is turned off (but not when the device + # goes idle, and we can't tell the difference right now). + if d.protocol < 2.0: + self[KEYS.NOTIFICATION_FLAGS] = d.enable_notifications( + ) - # Devices lose configuration when they are turned off, - # make sure they're up-to-date. - if _log.isEnabledFor(_DEBUG): - _log.debug("%s pushing device settings %s", d, d.settings) - for s in d.settings: - s.apply() + # If we've been inactive for a long time, forget anything + # about the battery. + if self.updated > 0 and timestamp - self.updated > _LONG_SLEEP: + self[KEYS.BATTERY_LEVEL] = None + self[KEYS.BATTERY_STATUS] = None + self[KEYS.BATTERY_CHARGING] = None - if self.get(KEYS.BATTERY_LEVEL) is None: - self.read_battery(timestamp) - else: - if was_active: - battery = self.get(KEYS.BATTERY_LEVEL) - self.clear() - # If we had a known battery level before, assume it's not going - # to change much while the device is offline. - if battery is not None: - self[KEYS.BATTERY_LEVEL] = battery + # Devices lose configuration when they are turned off, + # make sure they're up-to-date. + if _log.isEnabledFor(_DEBUG): + _log.debug("%s pushing device settings %s", d, + d.settings) + for s in d.settings: + s.apply() - if self.updated == 0 and active == True: - # if the device is active on the very first status notification, - # (meaning just when the program started or a new receiver was just - # detected), pop-up a notification about it - alert |= ALERT.NOTIFICATION - self.updated = timestamp + if self.get(KEYS.BATTERY_LEVEL) is None: + self.read_battery(timestamp) + else: + if was_active: + battery = self.get(KEYS.BATTERY_LEVEL) + self.clear() + # If we had a known battery level before, assume it's not going + # to change much while the device is offline. + if battery is not None: + self[KEYS.BATTERY_LEVEL] = battery - # if _log.isEnabledFor(_DEBUG): - # _log.debug("device %d changed: active=%s %s", d.number, self._active, dict(self)) - self._changed_callback(d, alert, reason) + if self.updated == 0 and active == True: + # if the device is active on the very first status notification, + # (meaning just when the program started or a new receiver was just + # detected), pop-up a notification about it + alert |= ALERT.NOTIFICATION + self.updated = timestamp - # def poll(self, timestamp): - # d = self._device - # if not d: - # _log.error("polling status of invalid device") - # return - # - # if self._active: - # if _log.isEnabledFor(_DEBUG): - # _log.debug("polling status of %s", d) - # - # # read these from the device, the UI may need them later - # d.protocol, d.serial, d.firmware, d.kind, d.name, d.settings, None - # - # # make sure we know all the features of the device - # # if d.features: - # # d.features[:] - # - # # devices may go out-of-range while still active, or the computer - # # may go to sleep and wake up without the devices available - # if timestamp - self.updated > _STATUS_TIMEOUT: - # if d.ping(): - # timestamp = self.updated = _timestamp() - # else: - # self.changed(active=False, reason='out of range') - # - # # if still active, make sure we know the battery level - # if KEYS.BATTERY_LEVEL not in self: - # self.read_battery(timestamp) - # - # elif timestamp - self.updated > _STATUS_TIMEOUT: - # if d.ping(): - # self.changed(active=True) - # else: - # self.updated = _timestamp() + # if _log.isEnabledFor(_DEBUG): + # _log.debug("device %d changed: active=%s %s", d.number, self._active, dict(self)) + self._changed_callback(d, alert, reason) + + # def poll(self, timestamp): + # d = self._device + # if not d: + # _log.error("polling status of invalid device") + # return + # + # if self._active: + # if _log.isEnabledFor(_DEBUG): + # _log.debug("polling status of %s", d) + # + # # read these from the device, the UI may need them later + # d.protocol, d.serial, d.firmware, d.kind, d.name, d.settings, None + # + # # make sure we know all the features of the device + # # if d.features: + # # d.features[:] + # + # # devices may go out-of-range while still active, or the computer + # # may go to sleep and wake up without the devices available + # if timestamp - self.updated > _STATUS_TIMEOUT: + # if d.ping(): + # timestamp = self.updated = _timestamp() + # else: + # self.changed(active=False, reason='out of range') + # + # # if still active, make sure we know the battery level + # if KEYS.BATTERY_LEVEL not in self: + # self.read_battery(timestamp) + # + # elif timestamp - self.updated > _STATUS_TIMEOUT: + # if d.ping(): + # self.changed(active=True) + # else: + # self.updated = _timestamp() diff --git a/lib/solaar/cli/__init__.py b/lib/solaar/cli/__init__.py index 2b669d4c..05754be6 100644 --- a/lib/solaar/cli/__init__.py +++ b/lib/solaar/cli/__init__.py @@ -19,7 +19,6 @@ from __future__ import absolute_import, division, print_function, unicode_literals - import argparse as _argparse import sys as _sys @@ -27,54 +26,76 @@ from logging import getLogger, DEBUG as _DEBUG _log = getLogger(__name__) del getLogger - from solaar import NAME # # # + def _create_parser(): - parser = _argparse.ArgumentParser(prog=NAME.lower(), add_help=False, - epilog='For details on individual actions, run `%s --help`.' % NAME.lower()) - subparsers = parser.add_subparsers(title='actions', - help='optional action to perform') + parser = _argparse.ArgumentParser( + prog=NAME.lower(), + add_help=False, + epilog='For details on individual actions, run `%s --help`.' % + NAME.lower()) + subparsers = parser.add_subparsers(title='actions', + help='optional action to perform') - 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, ' - 'a substring of a device\'s name, or "all" (the default)') - sp.set_defaults(action='show') + 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, ' + 'a substring of a device\'s name, or "all" (the default)') + sp.set_defaults(action='show') - sp = subparsers.add_parser('probe', help='probe a receiver (debugging use only)') - sp.add_argument('receiver', nargs='?', - help='select a certain receiver when more than one is present') - sp.set_defaults(action='probe') + sp = subparsers.add_parser('probe', + help='probe a receiver (debugging use only)') + sp.add_argument( + 'receiver', + nargs='?', + help='select a certain receiver when more than one is present') + sp.set_defaults(action='probe') - sp = subparsers.add_parser('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 device serial, ' - 'or at least 3 characters of a device\'s name') - sp.add_argument('setting', nargs='?', - help='device-specific setting; leave empty to list available settings') - sp.add_argument('value', nargs='?', - help='new value for the setting') - sp.set_defaults(action='config') + sp = subparsers.add_parser( + '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 device serial, ' + 'or at least 3 characters of a device\'s name') + sp.add_argument( + 'setting', + nargs='?', + help='device-specific setting; leave empty to list available settings') + sp.add_argument('value', nargs='?', help='new value for the setting') + 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.') - sp.add_argument('receiver', nargs='?', - help='select a certain receiver when more than one is present') - sp.set_defaults(action='pair') + 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.' + ) + sp.add_argument( + 'receiver', + nargs='?', + help='select a certain receiver when more than one is present') + sp.set_defaults(action='pair') - 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, ' - 'or a substring of a device\'s name.') - sp.set_defaults(action='unpair') + 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, ' + 'or a substring of a device\'s name.') + sp.set_defaults(action='unpair') - return parser, subparsers.choices + return parser, subparsers.choices _cli_parser, actions = _create_parser() @@ -82,88 +103,89 @@ print_help = _cli_parser.print_help def _receivers(dev_path=None): - from logitech_receiver import Receiver - from logitech_receiver.base import receivers - for dev_info in receivers(): - if dev_path is not None and dev_path != dev_info.path: - continue - try: - r = Receiver.open(dev_info) - if _log.isEnabledFor(_DEBUG): - _log.debug("[%s] => %s", dev_info.path, r) - if r: - yield r - except Exception as e: - _log.exception('opening ' + str(dev_info)) - _sys.exit("%s: error: %s" % (NAME, str(e))) + from logitech_receiver import Receiver + from logitech_receiver.base import receivers + for dev_info in receivers(): + if dev_path is not None and dev_path != dev_info.path: + continue + try: + r = Receiver.open(dev_info) + if _log.isEnabledFor(_DEBUG): + _log.debug("[%s] => %s", dev_info.path, r) + if r: + yield r + except Exception as e: + _log.exception('opening ' + str(dev_info)) + _sys.exit("%s: error: %s" % (NAME, str(e))) def _find_receiver(receivers, name): - assert receivers - assert name + assert receivers + assert name - for r in receivers: - if name in r.name.lower() or (r.serial is not None and name == r.serial.lower()): - return r + for r in receivers: + if name in r.name.lower() or (r.serial is not None + and name == r.serial.lower()): + return r def _find_device(receivers, name): - assert receivers - assert name + assert receivers + assert name - number = None - if len(name) == 1: - try: - number = int(name) - except: - pass - else: - assert not (number < 0) - if number > 6: number = None + number = None + if len(name) == 1: + try: + number = int(name) + except: + pass + else: + assert not (number < 0) + if number > 6: number = None - for r in receivers: - if number and number <= r.max_devices: - dev = r[number] - if dev: - return dev + for r in receivers: + if number and number <= r.max_devices: + dev = r[number] + if dev: + return dev - for dev in r: - if (name == dev.serial.lower() or - name == dev.codename.lower() or - name == str(dev.kind).lower() or - name in dev.name.lower()): - return dev + for dev in r: + if (name == dev.serial.lower() or name == dev.codename.lower() + or name == str(dev.kind).lower() + or name in dev.name.lower()): + return dev - raise Exception("no device found matching '%s'" % name) + raise Exception("no device found matching '%s'" % name) def run(cli_args=None, hidraw_path=None): - if cli_args: - action = cli_args[0] - args = _cli_parser.parse_args(cli_args) - else: - args = _cli_parser.parse_args() - # Python 3 has an undocumented 'feature' that breaks parsing empty args - # http://bugs.python.org/issue16308 - if not 'cmd' in args: - _cli_parser.print_usage(_sys.stderr) - _sys.stderr.write('%s: error: too few arguments\n' % NAME.lower()) - _sys.exit(2) - action = args.action - assert action in actions + if cli_args: + action = cli_args[0] + args = _cli_parser.parse_args(cli_args) + else: + args = _cli_parser.parse_args() + # Python 3 has an undocumented 'feature' that breaks parsing empty args + # http://bugs.python.org/issue16308 + if not 'cmd' in args: + _cli_parser.print_usage(_sys.stderr) + _sys.stderr.write('%s: error: too few arguments\n' % NAME.lower()) + _sys.exit(2) + action = args.action + assert action in actions - try: - c = list(_receivers(hidraw_path)) - if not c: - raise Exception('Logitech receiver not found') + try: + c = list(_receivers(hidraw_path)) + if not c: + raise Exception('Logitech receiver not found') - from importlib import import_module - m = import_module('.' + action, package=__name__) - m.run(c, args, _find_receiver, _find_device) - except AssertionError as e: - from traceback import extract_tb - 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])) - except Exception as e: - from traceback import format_exc - _sys.exit('%s: error: %s' % (NAME.lower(), format_exc())) + from importlib import import_module + m = import_module('.' + action, package=__name__) + m.run(c, args, _find_receiver, _find_device) + except AssertionError as e: + from traceback import extract_tb + 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])) + except Exception as e: + from traceback import format_exc + _sys.exit('%s: error: %s' % (NAME.lower(), format_exc())) diff --git a/lib/solaar/cli/config.py b/lib/solaar/cli/config.py index 301c6829..a24c4e48 100644 --- a/lib/solaar/cli/config.py +++ b/lib/solaar/cli/config.py @@ -19,109 +19,118 @@ from __future__ import absolute_import, division, print_function, unicode_literals - from solaar import configuration as _configuration from logitech_receiver import settings as _settings def _print_setting(s, verbose=True): - print ('#', s.label) - if verbose: - if s.description: - 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') - elif s.choices: - print ('# 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)') - else: - print (s.name, '= %r' % value) + print('#', s.label) + if verbose: + if s.description: + 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') + elif s.choices: + print('# 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)') + else: + print(s.name, '= %r' % value) def run(receivers, args, find_receiver, find_device): - assert receivers - assert args.device + assert receivers + assert args.device - device_name = args.device.lower() - dev = find_device(receivers, device_name) + device_name = args.device.lower() + dev = find_device(receivers, device_name) - if not dev.ping(): - raise Exception('%s is offline' % dev.name) + if not dev.ping(): + raise Exception('%s is offline' % dev.name) - if not dev.settings: - raise Exception('no settings for %s' % dev.name) + if not dev.settings: + raise Exception('no settings for %s' % dev.name) - _configuration.attach_to(dev) + _configuration.attach_to(dev) - if not args.setting: - print (dev.name, '(%s) [%s:%s]' % (dev.codename, dev.wpid, dev.serial)) - for s in dev.settings: - print ('') - _print_setting(s) - return + if not args.setting: + print(dev.name, '(%s) [%s:%s]' % (dev.codename, dev.wpid, dev.serial)) + for s in dev.settings: + print('') + _print_setting(s) + return - setting_name = args.setting.lower() - setting = None - for s in dev.settings: - if setting_name == s.name.lower(): - setting = s - break - if setting is None: - raise Exception("no setting '%s' for %s" % (args.setting, dev.name)) + setting_name = args.setting.lower() + setting = None + for s in dev.settings: + if setting_name == s.name.lower(): + setting = s + break + if setting is None: + raise Exception("no setting '%s' for %s" % (args.setting, dev.name)) - if args.value is None: - _print_setting(setting) - return + if args.value is None: + _print_setting(setting) + return - if setting.kind == _settings.KIND.toggle: - value = args.value - try: - value = bool(int(value)) - except: - if value.lower() in ('true', 'yes', 'on', 't', 'y'): - value = True - elif value.lower() in ('false', 'no', 'off', 'f', 'n'): - value = False - else: - raise Exception("don't know how to interpret '%s' as boolean" % value) + if setting.kind == _settings.KIND.toggle: + value = args.value + try: + value = bool(int(value)) + except: + if value.lower() in ('true', 'yes', 'on', 't', 'y'): + value = True + elif value.lower() in ('false', 'no', 'off', 'f', 'n'): + value = False + else: + raise Exception("don't know how to interpret '%s' as boolean" % + value) - elif setting.choices: - value = args.value.lower() + elif setting.choices: + value = args.value.lower() - if value in ('higher', 'lower'): - old_value = setting.read() - if old_value is None: - raise Exception("could not read current value of '%s'" % setting.name) + if value in ('higher', 'lower'): + old_value = setting.read() + if old_value is None: + raise Exception("could not read current value of '%s'" % + setting.name) - if value == 'lower': - lower_values = setting.choices[:old_value] - value = lower_values[-1] if lower_values else setting.choices[:][0] - elif value == 'higher': - higher_values = setting.choices[old_value + 1:] - value = higher_values[0] if higher_values else setting.choices[:][-1] - elif value in ('highest', 'max'): - value = setting.choices[:][-1] - elif value in ('lowest', 'min'): - value = setting.choices[:][0] - elif value not in setting.choices: - raise Exception("possible values for '%s' are: [%s]" % (setting.name, ', '.join(str(v) for v in setting.choices))) - value = setting.choices[value] + if value == 'lower': + lower_values = setting.choices[:old_value] + value = lower_values[ + -1] if lower_values else setting.choices[:][0] + elif value == 'higher': + higher_values = setting.choices[old_value + 1:] + value = higher_values[ + 0] if higher_values else setting.choices[:][-1] + elif value in ('highest', 'max'): + value = setting.choices[:][-1] + elif value in ('lowest', 'min'): + value = setting.choices[:][0] + elif value not in setting.choices: + raise Exception( + "possible values for '%s' are: [%s]" % + (setting.name, ', '.join(str(v) for v in setting.choices))) + value = setting.choices[value] - elif setting.kind == _settings.KIND.range: - try: - value = int(args.value) - except ValueError: - raise Exception("can't interpret '%s' as integer" % args.value) + elif setting.kind == _settings.KIND.range: + try: + value = int(args.value) + except ValueError: + raise Exception("can't interpret '%s' as integer" % args.value) - else: - raise Exception("NotImplemented") + else: + raise Exception("NotImplemented") - result = setting.write(value) - if result is None: - raise Exception("failed to set '%s' = '%s' [%r]" % (setting.name, str(value), value)) - _print_setting(setting, False) + result = setting.write(value) + if result is None: + raise Exception("failed to set '%s' = '%s' [%r]" % + (setting.name, str(value), value)) + _print_setting(setting, False) diff --git a/lib/solaar/cli/pair.py b/lib/solaar/cli/pair.py index 81705600..9f1c0b00 100644 --- a/lib/solaar/cli/pair.py +++ b/lib/solaar/cli/pair.py @@ -19,80 +19,85 @@ from __future__ import absolute_import, division, print_function, unicode_literals - from time import time as _timestamp from logitech_receiver import ( - base as _base, - hidpp10 as _hidpp10, - status as _status, - notifications as _notifications, - ) + base as _base, + hidpp10 as _hidpp10, + status as _status, + notifications as _notifications, +) def run(receivers, args, find_receiver, _ignore): - assert receivers + assert receivers - if args.receiver: - receiver_name = args.receiver.lower() - receiver = find_receiver(receiver_name) - if not receiver: - raise Exception("no receiver found matching '%s'" % receiver_name) - else: - receiver = receivers[0] + if args.receiver: + receiver_name = args.receiver.lower() + receiver = find_receiver(receiver_name) + if not receiver: + raise Exception("no receiver found matching '%s'" % receiver_name) + else: + receiver = receivers[0] - assert receiver - receiver.status = _status.ReceiverStatus(receiver, lambda *args, **kwargs: None) + assert receiver + receiver.status = _status.ReceiverStatus(receiver, + lambda *args, **kwargs: None) - # check if it's necessary to set the notification flags - old_notification_flags = _hidpp10.get_notification_flags(receiver) or 0 - if not (old_notification_flags & _hidpp10.NOTIFICATION_FLAG.wireless): - _hidpp10.set_notification_flags(receiver, old_notification_flags | _hidpp10.NOTIFICATION_FLAG.wireless) + # check if it's necessary to set the notification flags + old_notification_flags = _hidpp10.get_notification_flags(receiver) or 0 + if not (old_notification_flags & _hidpp10.NOTIFICATION_FLAG.wireless): + _hidpp10.set_notification_flags( + receiver, + old_notification_flags | _hidpp10.NOTIFICATION_FLAG.wireless) - # get all current devices - known_devices = [dev.number for dev in receiver] + # get all current devices + known_devices = [dev.number for dev in receiver] - class _HandleWithNotificationHook(int): - def notifications_hook(self, n): - assert n - if n.devnumber == 0xFF: - _notifications.process(receiver, n) - elif n.sub_id == 0x41: # allow for other protocols! (was and n.address == 0x04) - if n.devnumber not in known_devices: - receiver.status.new_device = receiver[n.devnumber] - elif receiver.re_pairs: - del receiver[n.devnumber] # get rid of information on device re-paired away - receiver.status.new_device = receiver[n.devnumber] + class _HandleWithNotificationHook(int): + def notifications_hook(self, n): + assert n + if n.devnumber == 0xFF: + _notifications.process(receiver, n) + elif n.sub_id == 0x41: # allow for other protocols! (was and n.address == 0x04) + if n.devnumber not in known_devices: + receiver.status.new_device = receiver[n.devnumber] + elif receiver.re_pairs: + del receiver[ + n. + devnumber] # get rid of information on device re-paired away + receiver.status.new_device = receiver[n.devnumber] - timeout = 20 # seconds - receiver.handle = _HandleWithNotificationHook(receiver.handle) + timeout = 20 # seconds + receiver.handle = _HandleWithNotificationHook(receiver.handle) - receiver.set_lock(False, timeout=timeout) - print ('Pairing: turn your new device on (timing out in', timeout, 'seconds).') + receiver.set_lock(False, timeout=timeout) + print('Pairing: turn your new device on (timing out in', timeout, + 'seconds).') - # the lock-open notification may come slightly later, wait for it a bit - pairing_start = _timestamp() - patience = 5 # seconds + # the lock-open notification may come slightly later, wait for it a bit + pairing_start = _timestamp() + patience = 5 # seconds - while receiver.status.lock_open or _timestamp() - pairing_start < patience: - n = _base.read(receiver.handle) - if n: - n = _base.make_notification(*n) - if n: - receiver.handle.notifications_hook(n) + while receiver.status.lock_open or _timestamp() - pairing_start < patience: + n = _base.read(receiver.handle) + if n: + n = _base.make_notification(*n) + if n: + receiver.handle.notifications_hook(n) - if not (old_notification_flags & _hidpp10.NOTIFICATION_FLAG.wireless): - # only clear the flags if they weren't set before, otherwise a - # concurrently running Solaar app might stop working properly - _hidpp10.set_notification_flags(receiver, old_notification_flags) - - 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)) - else: - error = receiver.status.get(_status.KEYS.ERROR) - if error : - raise Exception("pairing failed: %s" % error) - else : - print ('Paired a device') # this is better than an error + if not (old_notification_flags & _hidpp10.NOTIFICATION_FLAG.wireless): + # only clear the flags if they weren't set before, otherwise a + # concurrently running Solaar app might stop working properly + _hidpp10.set_notification_flags(receiver, old_notification_flags) + 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)) + else: + error = receiver.status.get(_status.KEYS.ERROR) + if error: + raise Exception("pairing failed: %s" % error) + else: + print('Paired a device') # this is better than an error diff --git a/lib/solaar/cli/probe.py b/lib/solaar/cli/probe.py index def94a74..924c5a91 100644 --- a/lib/solaar/cli/probe.py +++ b/lib/solaar/cli/probe.py @@ -19,52 +19,65 @@ from __future__ import absolute_import, division, print_function, unicode_literals - from time import time as _timestamp from logitech_receiver.common import strhex as _strhex from logitech_receiver import ( - base as _base, - hidpp10 as _hidpp10, - status as _status, - notifications as _notifications, - ) + base as _base, + hidpp10 as _hidpp10, + status as _status, + notifications as _notifications, +) _R = _hidpp10.REGISTERS from solaar.cli.show import _print_receiver + def run(receivers, args, find_receiver, _ignore): - assert receivers + assert receivers - if args.receiver: - receiver_name = args.receiver.lower() - receiver = find_receiver(receiver_name) - if not receiver: - raise Exception("no receiver found matching '%s'" % receiver_name) - else: - receiver = receivers[0] + if args.receiver: + receiver_name = args.receiver.lower() + receiver = find_receiver(receiver_name) + if not receiver: + raise Exception("no receiver found matching '%s'" % receiver_name) + else: + receiver = receivers[0] - assert receiver + assert receiver - _print_receiver(receiver) + _print_receiver(receiver) - print (' Register Dump') - register = receiver.read_register(_R.notifications) - print(" Notification Register %#04x: %s" % (_R.notifications%0x100,'0x'+_strhex(register) if register else "None")) - register = receiver.read_register(_R.receiver_connection) - print(" Connection State %#04x: %s" % (_R.receiver_connection%0x100,'0x'+_strhex(register) if register else "None")) - register = receiver.read_register(_R.devices_activity) - print(" Device Activity %#04x: %s" % (_R.devices_activity%0x100,'0x'+_strhex(register) if register else "None")) + print(' Register Dump') + register = receiver.read_register(_R.notifications) + print(" Notification Register %#04x: %s" % + (_R.notifications % 0x100, + '0x' + _strhex(register) if register else "None")) + register = receiver.read_register(_R.receiver_connection) + print(" Connection State %#04x: %s" % + (_R.receiver_connection % 0x100, + '0x' + _strhex(register) if register else "None")) + register = receiver.read_register(_R.devices_activity) + print(" Device Activity %#04x: %s" % + (_R.devices_activity % 0x100, + '0x' + _strhex(register) if register else "None")) - for device in range(0,6): - for sub_reg in [ 0x0, 0x10, 0x20, 0x30 ] : - register = receiver.read_register(_R.receiver_info, sub_reg + device) - print(" Pairing Register %#04x %#04x: %s" % (_R.receiver_info%0x100,sub_reg + device,'0x'+_strhex(register) if register else "None")) - register = receiver.read_register(_R.receiver_info, 0x40 + device) - print(" Pairing Name %#04x %#02x: %s" % (_R.receiver_info%0x100,0x40 + device,register[2:2+ord(register[1:2])] if register else "None")) + for device in range(0, 6): + for sub_reg in [0x0, 0x10, 0x20, 0x30]: + register = receiver.read_register(_R.receiver_info, + sub_reg + device) + print(" Pairing Register %#04x %#04x: %s" % + (_R.receiver_info % 0x100, sub_reg + device, + '0x' + _strhex(register) if register else "None")) + register = receiver.read_register(_R.receiver_info, 0x40 + device) + print(" Pairing Name %#04x %#02x: %s" % + (_R.receiver_info % 0x100, 0x40 + device, + register[2:2 + ord(register[1:2])] if register else "None")) - for sub_reg in range(0,5): - register = receiver.read_register(_R.firmware, sub_reg) - print(" Firmware %#04x %#04x: %s" % (_R.firmware%0x100,sub_reg,'0x'+_strhex(register) if register else "None")) + for sub_reg in range(0, 5): + register = receiver.read_register(_R.firmware, sub_reg) + print(" Firmware %#04x %#04x: %s" % + (_R.firmware % 0x100, sub_reg, + '0x' + _strhex(register) if register else "None")) diff --git a/lib/solaar/cli/show.py b/lib/solaar/cli/show.py index a25f1dd7..6679f2d7 100644 --- a/lib/solaar/cli/show.py +++ b/lib/solaar/cli/show.py @@ -19,220 +19,244 @@ from __future__ import absolute_import, division, print_function, unicode_literals - -from logitech_receiver import ( - hidpp10 as _hidpp10, - hidpp20 as _hidpp20, - special_keys as _special_keys, - settings_templates as _settings_templates - ) +from logitech_receiver import (hidpp10 as _hidpp10, hidpp20 as _hidpp20, + special_keys as _special_keys, + settings_templates as _settings_templates) from logitech_receiver.common import NamedInt as _NamedInt def _print_receiver(receiver): - paired_count = receiver.count() + paired_count = receiver.count() - print (receiver.name) - 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(receiver.name) + 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 (' 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', 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()) - notification_flags = _hidpp10.get_notification_flags(receiver) - if notification_flags is not None: - if notification_flags: - notification_names = _hidpp10.NOTIFICATION_FLAG.flag_names(notification_flags) - print (' Notifications: %s (0x%06X)' % (', '.join(notification_names), notification_flags)) - else: - print (' Notifications: (none)') + notification_flags = _hidpp10.get_notification_flags(receiver) + if notification_flags is not None: + if notification_flags: + notification_names = _hidpp10.NOTIFICATION_FLAG.flag_names( + notification_flags) + print(' Notifications: %s (0x%06X)' % + (', '.join(notification_names), notification_flags)) + else: + print(' Notifications: (none)') - activity = receiver.read_register(_hidpp10.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 = receiver.read_register(_hidpp10.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)') + + +def _battery_text(level): + if level is None: + return 'N/A' + elif isinstance(level, _NamedInt): + return str(level) + else: + return '%d%%' % level -def _battery_text(level) : - if level is None: - return 'N/A' - elif isinstance(level, _NamedInt): - return str(level) - else: - return '%d%%' % level def _print_device(dev): - assert dev - # check if the device is online - dev.ping() + assert dev + # check if the device is online + dev.ping() - print (' %d: %s' % (dev.number, dev.name)) - print (' Codename :', dev.codename) - print (' Kind :', dev.kind) - print (' Wireless PID :', dev.wpid) - if dev.protocol: - print (' Protocol : HID++ %1.1f' % dev.protocol) - else: - print (' Protocol : unknown (device is offline)') - if dev.polling_rate: - print (' Polling rate :', dev.polling_rate, 'ms (%dHz)' % (1000 // dev.polling_rate)) - print (' Serial number:', dev.serial) - if dev.firmware: - for fw in dev.firmware: - print (' %11s:' % fw.kind, (fw.name + ' ' + fw.version).strip()) + print(' %d: %s' % (dev.number, dev.name)) + print(' Codename :', dev.codename) + print(' Kind :', dev.kind) + print(' Wireless PID :', dev.wpid) + if dev.protocol: + print(' Protocol : HID++ %1.1f' % dev.protocol) + else: + print(' Protocol : unknown (device is offline)') + if dev.polling_rate: + print(' Polling rate :', dev.polling_rate, + 'ms (%dHz)' % (1000 // dev.polling_rate)) + print(' Serial number:', dev.serial) + if dev.firmware: + for fw in dev.firmware: + 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) + if 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.NOTIFICATION_FLAG.flag_names(notification_flags) - print (' Notifications: %s (0x%06X).' % (', '.join(notification_names), notification_flags)) - else: - print (' Notifications: (none).') + if dev.online: + notification_flags = _hidpp10.get_notification_flags(dev) + if notification_flags is not None: + if notification_flags: + notification_names = _hidpp10.NOTIFICATION_FLAG.flag_names( + notification_flags) + print(' Notifications: %s (0x%06X).' % + (', '.join(notification_names), notification_flags)) + else: + print(' Notifications: (none).') - if dev.online and dev.features: - print (' Supports %d HID++ 2.0 features:' % len(dev.features)) - dev.persister = None # Give the device a fake persister - dev_settings = [] - _settings_templates.check_feature_settings(dev, dev_settings) - for index, feature in enumerate(dev.features): - feature = dev.features[index] - flags = dev.request(0x0000, feature.bytes(2)) - flags = 0 if flags is None else ord(flags[1:2]) - flags = _hidpp20.FEATURE_FLAG.flag_names(flags) - print (' %2d: %-22s {%04X} %s' % (index, feature, feature, ', '.join(flags))) - if feature == _hidpp20.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) - if has_invert: - print(" Has invert") - if inv: - print(" Inverse wheel motion") - else: - print(" Normal wheel motion") - if has_switch: - print(" Has ratchet switch") - if ratchet: - print(" Normal wheel mode") - else: - print(" Free wheel mode") - if res: - print(" High resolution mode") - else: - print(" Low resolution mode") - if target: - print(" HID++ notification") - else: - print(" HID notification") - elif feature == _hidpp20.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") - else: - print(" Override OS ballistics") - if mouse_pointer['suggest_vertical_orientation']: - print(" Provide vertical tuning, trackball") - else: - print(" No vertical tuning, standard mice") - if feature == _hidpp20.FEATURE.VERTICAL_SCROLLING: - vertical_scrolling_info = _hidpp20.get_vertical_scrolling_info(dev) - if vertical_scrolling_info: - print(" Roller type: %s" % vertical_scrolling_info['roller']) - print(" Ratchet per turn: %s" % vertical_scrolling_info['ratchet']) - print(" Scroll lines: %s" % vertical_scrolling_info['lines']) - elif feature == _hidpp20.FEATURE.HI_RES_SCROLLING: - scrolling_mode, scrolling_resolution = _hidpp20.get_hi_res_scrolling_info(dev) - if scrolling_mode: - print(" Hi-res scrolling enabled") - else: - print(" Hi-res scrolling disabled") - if scrolling_resolution: - print(" Hi-res scrolling multiplier: %s" % scrolling_resolution) - elif feature == _hidpp20.FEATURE.POINTER_SPEED: - pointer_speed = _hidpp20.get_pointer_speed_info(dev) - if pointer_speed: - print(" Pointer Speed: %s" % pointer_speed) - elif feature == _hidpp20.FEATURE.LOWRES_WHEEL: - wheel_status = _hidpp20.get_lowres_wheel_status(dev) - if wheel_status: - print(" Wheel Reports: %s" % wheel_status) - elif feature == _hidpp20.FEATURE.NEW_FN_INVERSION: - inverted, default_inverted = _hidpp20.get_new_fn_inversion(dev) - print(" Fn-swap:", "enabled" if inverted else "disabled") - print(" Fn-swap default:", "enabled" if default_inverted else "disabled") - for setting in dev_settings: - if setting.feature == feature: - v = setting.read(False) - print(" %s: %s" % (setting.label, v) ) + if dev.online and dev.features: + print(' Supports %d HID++ 2.0 features:' % len(dev.features)) + dev.persister = None # Give the device a fake persister + dev_settings = [] + _settings_templates.check_feature_settings(dev, dev_settings) + for index, feature in enumerate(dev.features): + feature = dev.features[index] + flags = dev.request(0x0000, feature.bytes(2)) + flags = 0 if flags is None else ord(flags[1:2]) + flags = _hidpp20.FEATURE_FLAG.flag_names(flags) + print(' %2d: %-22s {%04X} %s' % + (index, feature, feature, ', '.join(flags))) + if feature == _hidpp20.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) + if has_invert: + print(" Has invert") + if inv: + print(" Inverse wheel motion") + else: + print(" Normal wheel motion") + if has_switch: + print(" Has ratchet switch") + if ratchet: + print(" Normal wheel mode") + else: + print(" Free wheel mode") + if res: + print(" High resolution mode") + else: + print(" Low resolution mode") + if target: + print(" HID++ notification") + else: + print(" HID notification") + elif feature == _hidpp20.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") + else: + print(" Override OS ballistics") + if mouse_pointer['suggest_vertical_orientation']: + print(" Provide vertical tuning, trackball") + else: + print(" No vertical tuning, standard mice") + if feature == _hidpp20.FEATURE.VERTICAL_SCROLLING: + vertical_scrolling_info = _hidpp20.get_vertical_scrolling_info( + dev) + if vertical_scrolling_info: + print(" Roller type: %s" % + vertical_scrolling_info['roller']) + print(" Ratchet per turn: %s" % + vertical_scrolling_info['ratchet']) + print(" Scroll lines: %s" % + vertical_scrolling_info['lines']) + elif feature == _hidpp20.FEATURE.HI_RES_SCROLLING: + scrolling_mode, scrolling_resolution = _hidpp20.get_hi_res_scrolling_info( + dev) + if scrolling_mode: + print(" Hi-res scrolling enabled") + else: + print(" Hi-res scrolling disabled") + if scrolling_resolution: + print(" Hi-res scrolling multiplier: %s" % + scrolling_resolution) + elif feature == _hidpp20.FEATURE.POINTER_SPEED: + pointer_speed = _hidpp20.get_pointer_speed_info(dev) + if pointer_speed: + print(" Pointer Speed: %s" % pointer_speed) + elif feature == _hidpp20.FEATURE.LOWRES_WHEEL: + wheel_status = _hidpp20.get_lowres_wheel_status(dev) + if wheel_status: + print(" Wheel Reports: %s" % wheel_status) + elif feature == _hidpp20.FEATURE.NEW_FN_INVERSION: + inverted, default_inverted = _hidpp20.get_new_fn_inversion(dev) + print(" Fn-swap:", + "enabled" if inverted else "disabled") + print(" Fn-swap default:", + "enabled" if default_inverted else "disabled") + for setting in dev_settings: + if setting.feature == feature: + v = setting.read(False) + print(" %s: %s" % (setting.label, v)) - if dev.online and dev.keys: - print (' Has %d reprogrammable keys:' % len(dev.keys)) - for k in dev.keys: - flags = _special_keys.KEY_FLAG.flag_names(k.flags) - # TODO: add here additional variants for other REPROG_CONTROLS - if dev.keys.keyversion == 1: - print (' %2d: %-26s => %-27s %s' % (k.index, k.key, k.task, ', '.join(flags))) - if dev.keys.keyversion == 4: - print (' %2d: %-26s, default: %-27s => %-26s' % (k.index, k.key, k.task, k.remapped)) - print (' %s, pos:%d, group:%1d, gmask:%d' % ( ', '.join(flags), k.pos, k.group, k.group_mask)) - if dev.online: - battery = _hidpp20.get_battery(dev) - if battery is None: - battery = _hidpp10.get_battery(dev) - if battery is not None: - level, status, nextLevel = battery - text = _battery_text(level) - nextText = '' if nextLevel is None else ', next level ' +_battery_text(nextLevel) - print (' Battery: %s, %s%s.' % (text, status, nextText)) - else: - battery_voltage = _hidpp20.get_voltage(dev) - if battery_voltage : - (level, status, voltage, charge_sts, charge_type) = battery_voltage - print (' Battery: %smV, %s, %s.' % (voltage, status, level)) - else: - print (' Battery status unavailable.') - else: - print (' Battery: unknown (device is offline).') + if dev.online and dev.keys: + print(' Has %d reprogrammable keys:' % len(dev.keys)) + for k in dev.keys: + flags = _special_keys.KEY_FLAG.flag_names(k.flags) + # TODO: add here additional variants for other REPROG_CONTROLS + if dev.keys.keyversion == 1: + print(' %2d: %-26s => %-27s %s' % + (k.index, k.key, k.task, ', '.join(flags))) + if dev.keys.keyversion == 4: + print(' %2d: %-26s, default: %-27s => %-26s' % + (k.index, k.key, k.task, k.remapped)) + print(' %s, pos:%d, group:%1d, gmask:%d' % + (', '.join(flags), k.pos, k.group, k.group_mask)) + if dev.online: + battery = _hidpp20.get_battery(dev) + if battery is None: + battery = _hidpp10.get_battery(dev) + if battery is not None: + level, status, nextLevel = battery + text = _battery_text(level) + nextText = '' if nextLevel is None else ', next level ' + _battery_text( + nextLevel) + print(' Battery: %s, %s%s.' % (text, status, nextText)) + else: + battery_voltage = _hidpp20.get_voltage(dev) + if battery_voltage: + (level, status, voltage, charge_sts, + charge_type) = battery_voltage + print(' Battery: %smV, %s, %s.' % (voltage, status, level)) + else: + print(' Battery status unavailable.') + else: + print(' Battery: unknown (device is offline).') def run(receivers, args, find_receiver, find_device): - assert receivers - assert args.device + assert receivers + assert args.device - device_name = args.device.lower() + device_name = args.device.lower() - if device_name == 'all': - for r in receivers: - _print_receiver(r) - count = r.count() - if count: - for dev in r: - print ('') - _print_device(dev) - count -= 1 - if not count: - break - print ('') - return + if device_name == 'all': + for r in receivers: + _print_receiver(r) + count = r.count() + if count: + for dev in r: + print('') + _print_device(dev) + count -= 1 + if not count: + break + print('') + return - dev = find_receiver(receivers, device_name) - if dev: - _print_receiver(dev) - return + dev = find_receiver(receivers, device_name) + if dev: + _print_receiver(dev) + return - dev = find_device(receivers, device_name) - assert dev - _print_device(dev) + dev = find_device(receivers, device_name) + assert dev + _print_device(dev) diff --git a/lib/solaar/cli/unpair.py b/lib/solaar/cli/unpair.py index a4029cb6..fcb6ef2d 100644 --- a/lib/solaar/cli/unpair.py +++ b/lib/solaar/cli/unpair.py @@ -21,19 +21,22 @@ from __future__ import absolute_import, division, print_function, unicode_litera def run(receivers, args, find_receiver, find_device): - assert receivers - assert args.device + assert receivers + assert args.device - device_name = args.device.lower() - dev = find_device(receivers, device_name) + device_name = args.device.lower() + dev = find_device(receivers, device_name) - if not dev.receiver.may_unpair: - print('Receiver for %s [%s:%s] does not unpair, but attempting anyway' % (dev.name,dev.wpid,dev.serial)) + if not dev.receiver.may_unpair: + print( + 'Receiver for %s [%s:%s] does not unpair, but attempting anyway' % + (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)) - except Exception as e: - raise Exception('failed to unpair device %s: %s' % (dev.name, e)) + 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)) + except Exception as e: + raise Exception('failed to unpair device %s: %s' % (dev.name, e)) diff --git a/lib/solaar/configuration.py b/lib/solaar/configuration.py index d970eac8..1af77050 100644 --- a/lib/solaar/configuration.py +++ b/lib/solaar/configuration.py @@ -25,106 +25,109 @@ from logging import getLogger, DEBUG as _DEBUG, INFO as _INFO _log = getLogger(__name__) del getLogger -_XDG_CONFIG_HOME = _os.environ.get('XDG_CONFIG_HOME') or _path.expanduser(_path.join('~', '.config')) +_XDG_CONFIG_HOME = _os.environ.get('XDG_CONFIG_HOME') or _path.expanduser( + _path.join('~', '.config')) _file_path = _path.join(_XDG_CONFIG_HOME, 'solaar', 'config.json') - from solaar import __version__ _KEY_VERSION = '_version' _KEY_NAME = '_name' _configuration = {} - def _load(): - if _path.isfile(_file_path): - loaded_configuration = {} - try: - with open(_file_path, 'r') as config_file: - loaded_configuration = _json_load(config_file) - except: - _log.error("failed to load from %s", _file_path) + if _path.isfile(_file_path): + loaded_configuration = {} + try: + with open(_file_path, 'r') as config_file: + loaded_configuration = _json_load(config_file) + except: + _log.error("failed to load from %s", _file_path) - # loaded_configuration.update(_configuration) - _configuration.clear() - _configuration.update(loaded_configuration) + # loaded_configuration.update(_configuration) + _configuration.clear() + _configuration.update(loaded_configuration) - if _log.isEnabledFor(_DEBUG): - _log.debug("load => %s", _configuration) + if _log.isEnabledFor(_DEBUG): + _log.debug("load => %s", _configuration) - _cleanup(_configuration) - _configuration[_KEY_VERSION] = __version__ - return _configuration + _cleanup(_configuration) + _configuration[_KEY_VERSION] = __version__ + return _configuration def save(): - # don't save if the configuration hasn't been loaded - if _KEY_VERSION not in _configuration: - return + # don't save if the configuration hasn't been loaded + if _KEY_VERSION not in _configuration: + return - dirname = _os.path.dirname(_file_path) - if not _path.isdir(dirname): - try: - _os.makedirs(dirname) - except: - _log.error("failed to create %s", dirname) - return False + dirname = _os.path.dirname(_file_path) + if not _path.isdir(dirname): + try: + _os.makedirs(dirname) + except: + _log.error("failed to create %s", dirname) + return False - _cleanup(_configuration) + _cleanup(_configuration) - try: - with open(_file_path, 'w') as config_file: - _json_save(_configuration, config_file, skipkeys=True, indent=2, sort_keys=True) + try: + with open(_file_path, 'w') as config_file: + _json_save(_configuration, + config_file, + skipkeys=True, + indent=2, + sort_keys=True) - if _log.isEnabledFor(_INFO): - _log.info("saved %s to %s", _configuration, _file_path) - return True - except: - _log.error("failed to save to %s", _file_path) + if _log.isEnabledFor(_INFO): + _log.info("saved %s to %s", _configuration, _file_path) + return True + except: + _log.error("failed to save to %s", _file_path) def _cleanup(d): - # remove None values from the dict - for key in list(d.keys()): - value = d.get(key) - if value is None: - del d[key] - elif isinstance(value, dict): - _cleanup(value) + # remove None values from the dict + for key in list(d.keys()): + value = d.get(key) + if value is None: + del d[key] + elif isinstance(value, dict): + _cleanup(value) def _device_key(device): - return '%s:%s' % (device.wpid, device.serial) + return '%s:%s' % (device.wpid, device.serial) class _DeviceEntry(dict): - def __init__(self, *args, **kwargs): - super(_DeviceEntry, self).__init__(*args, **kwargs) + def __init__(self, *args, **kwargs): + super(_DeviceEntry, self).__init__(*args, **kwargs) - def __setitem__(self, key, value): - super(_DeviceEntry, self).__setitem__(key, value) - save() + def __setitem__(self, key, value): + super(_DeviceEntry, self).__setitem__(key, value) + save() def _device_entry(device): - if not _configuration: - _load() + if not _configuration: + _load() - device_key = _device_key(device) - c = _configuration.get(device_key) or {} + device_key = _device_key(device) + c = _configuration.get(device_key) or {} - if not isinstance(c, _DeviceEntry): - c[_KEY_NAME] = device.name - c = _DeviceEntry(c) - _configuration[device_key] = c + if not isinstance(c, _DeviceEntry): + c[_KEY_NAME] = device.name + c = _DeviceEntry(c) + _configuration[device_key] = c - return c + return c def attach_to(device): - """Apply the last saved configuration to a device.""" - if not _configuration: - _load() + """Apply the last saved configuration to a device.""" + if not _configuration: + _load() - persister = _device_entry(device) - device.persister = persister + persister = _device_entry(device) + device.persister = persister diff --git a/lib/solaar/gtk.py b/lib/solaar/gtk.py index b2f3c2c2..4fe55aba 100755 --- a/lib/solaar/gtk.py +++ b/lib/solaar/gtk.py @@ -22,7 +22,6 @@ from __future__ import absolute_import, division, print_function, unicode_litera import importlib - from solaar import __version__, NAME import solaar.i18n as _i18n import solaar.cli as _cli @@ -31,96 +30,131 @@ import solaar.cli as _cli # # + def _require(module, os_package, gi=None, gi_package=None, gi_version=None): - try: - if gi is not None: - gi.require_version(gi_package,gi_version) - return importlib.import_module(module) - except (ImportError, ValueError): - import sys - sys.exit("%s: missing required system package %s" % (NAME, os_package)) + try: + if gi is not None: + gi.require_version(gi_package, gi_version) + return importlib.import_module(module) + except (ImportError, ValueError): + import sys + sys.exit("%s: missing required system package %s" % (NAME, os_package)) + prefer_symbolic_battery_icons = False + def _parse_arguments(): - import argparse - arg_parser = argparse.ArgumentParser(prog=NAME.lower()) - arg_parser.add_argument('-d', '--debug', action='count', default=0, - 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') - 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)') - arg_parser.add_argument('-b', '--battery-icons', choices=('regular','symbolic'), help='prefer regular / symbolic 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') + import argparse + arg_parser = argparse.ArgumentParser(prog=NAME.lower()) + arg_parser.add_argument( + '-d', + '--debug', + action='count', + default=0, + 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' + ) + 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)') + arg_parser.add_argument('-b', + '--battery-icons', + choices=('regular', 'symbolic'), + help='prefer regular / symbolic 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() + args = arg_parser.parse_args() - if args.help_actions: - _cli.print_help() - return + if args.help_actions: + _cli.print_help() + return - if args.window is None: - args.window = 'show' # default behaviour is to show main window + if args.window is None: + args.window = 'show' # default behaviour is to show main window - global prefer_symbolic_battery_icons - prefer_symbolic_battery_icons = True if args.battery_icons == 'symbolic' else False + global prefer_symbolic_battery_icons + prefer_symbolic_battery_icons = True if args.battery_icons == 'symbolic' else False - import logging - if args.debug > 0: - log_level = logging.WARNING - 10 * args.debug - log_format='%(asctime)s,%(msecs)03d %(levelname)8s [%(threadName)s] %(name)s: %(message)s' - logging.basicConfig(level=max(log_level, logging.DEBUG), format=log_format, datefmt='%H:%M:%S') - else: - logging.root.addHandler(logging.NullHandler()) - logging.root.setLevel(logging.ERROR) + import logging + if args.debug > 0: + log_level = logging.WARNING - 10 * args.debug + log_format = '%(asctime)s,%(msecs)03d %(levelname)8s [%(threadName)s] %(name)s: %(message)s' + logging.basicConfig(level=max(log_level, logging.DEBUG), + format=log_format, + datefmt='%H:%M:%S') + else: + logging.root.addHandler(logging.NullHandler()) + logging.root.setLevel(logging.ERROR) - if not args.action: - if logging.root.isEnabledFor(logging.INFO): - logging.info("language %s (%s), translations path %s", _i18n.language, _i18n.encoding, _i18n.path) + if not args.action: + if logging.root.isEnabledFor(logging.INFO): + logging.info("language %s (%s), translations path %s", + _i18n.language, _i18n.encoding, _i18n.path) - return args + return args def main(): - _require('pyudev', 'python3-pyudev') + _require('pyudev', 'python3-pyudev') - # handle ^C in console - import signal - signal.signal(signal.SIGINT, signal.SIG_DFL) + # handle ^C in console + import signal + signal.signal(signal.SIGINT, signal.SIG_DFL) - args = _parse_arguments() - if not args: return - if args.action: - # if any argument, run comandline and exit - return _cli.run(args.action, args.hidraw_path) + args = _parse_arguments() + if not args: return + if args.action: + # if any argument, run comandline and exit + return _cli.run(args.action, args.hidraw_path) - gi = _require('gi', 'python3-gi or python3-gobject') - _require('gi.repository.Gtk', 'gir1.2-gtk-3.0', gi, 'Gtk', '3.0') + gi = _require('gi', 'python3-gi or python3-gobject') + _require('gi.repository.Gtk', 'gir1.2-gtk-3.0', gi, 'Gtk', '3.0') - try: - import solaar.ui as ui - import solaar.listener as listener - listener.setup_scanner(ui.status_changed, ui.error_dialog) + try: + import solaar.ui as ui + import solaar.listener as listener + listener.setup_scanner(ui.status_changed, ui.error_dialog) - import solaar.upower as _upower - if args.restart_on_wake_up: - _upower.watch(listener.start_all, listener.stop_all) - else: - _upower.watch(lambda: listener.ping_all(True)) + import solaar.upower as _upower + if args.restart_on_wake_up: + _upower.watch(listener.start_all, listener.stop_all) + else: + _upower.watch(lambda: listener.ping_all(True)) - # main UI event loop - ui.run_loop(listener.start_all, listener.stop_all, args.window!='only', args.window!='hide') - except Exception as e: - import sys - from traceback import format_exc - sys.exit('%s: error: %s' % (NAME.lower(), format_exc())) + # main UI event loop + ui.run_loop(listener.start_all, listener.stop_all, + args.window != 'only', args.window != 'hide') + except Exception as e: + import sys + from traceback import format_exc + sys.exit('%s: error: %s' % (NAME.lower(), format_exc())) if __name__ == '__main__': - main() + main() diff --git a/lib/solaar/i18n.py b/lib/solaar/i18n.py index b2094ce8..e7ac026d 100644 --- a/lib/solaar/i18n.py +++ b/lib/solaar/i18n.py @@ -25,22 +25,27 @@ from solaar import NAME as _NAME # # + def _find_locale_path(lc_domain): - import os.path as _path + import os.path as _path - import sys as _sys - prefix_share = _path.normpath(_path.join(_path.realpath(_sys.path[0]), '..')) - src_share = _path.normpath(_path.join(_path.realpath(_sys.path[0]), '..', 'share')) - del _sys + import sys as _sys + prefix_share = _path.normpath( + _path.join(_path.realpath(_sys.path[0]), '..')) + src_share = _path.normpath( + _path.join(_path.realpath(_sys.path[0]), '..', 'share')) + del _sys - from glob import glob as _glob + from glob import glob as _glob - for location in prefix_share, src_share: - mo_files = _glob(_path.join(location, 'locale', '*', 'LC_MESSAGES', lc_domain + '.mo')) - if mo_files: - return _path.join(location, 'locale') + for location in prefix_share, src_share: + mo_files = _glob( + _path.join(location, 'locale', '*', 'LC_MESSAGES', + lc_domain + '.mo')) + if mo_files: + return _path.join(location, 'locale') - # del _path + # del _path import locale @@ -58,9 +63,9 @@ _gettext.textdomain(_LOCALE_DOMAIN) _gettext.install(_LOCALE_DOMAIN) try: - unicode - _ = lambda x: _gettext.gettext(x).decode('UTF-8') - ngettext = lambda *x: _gettext.ngettext(*x).decode('UTF-8') + unicode + _ = lambda x: _gettext.gettext(x).decode('UTF-8') + ngettext = lambda *x: _gettext.ngettext(*x).decode('UTF-8') except: - _ = _gettext.gettext - ngettext = _gettext.ngettext + _ = _gettext.gettext + ngettext = _gettext.ngettext diff --git a/lib/solaar/listener.py b/lib/solaar/listener.py index 7d38f70a..8d46cf91 100644 --- a/lib/solaar/listener.py +++ b/lib/solaar/listener.py @@ -24,34 +24,32 @@ from logging import getLogger, INFO as _INFO, WARNING as _WARNING _log = getLogger(__name__) del getLogger - from solaar.i18n import _ from . import configuration -from logitech_receiver import ( - Receiver, - listener as _listener, - status as _status, - notifications as _notifications - ) +from logitech_receiver import (Receiver, listener as _listener, status as + _status, notifications as _notifications) # # # from collections import namedtuple -_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 + def _ghost(device): - return _GHOST_DEVICE( - receiver=device.receiver, - number=device.number, - name=device.name, - kind=device.kind, - status=None, - online=False) + return _GHOST_DEVICE(receiver=device.receiver, + number=device.number, + name=device.name, + kind=device.kind, + status=None, + online=False) + # # @@ -63,188 +61,201 @@ 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(ReceiverListener, self).__init__(receiver, self._notifications_handler) - # no reason to enable polling yet - # self.tick_period = _POLL_TICK - # self._last_tick = 0 + def __init__(self, receiver, status_changed_callback): + super(ReceiverListener, self).__init__(receiver, + self._notifications_handler) + # no reason to enable polling yet + # self.tick_period = _POLL_TICK + # self._last_tick = 0 - assert status_changed_callback - self.status_changed_callback = status_changed_callback - _status.attach_to(receiver, self._status_changed) + assert status_changed_callback + self.status_changed_callback = status_changed_callback + _status.attach_to(receiver, self._status_changed) - def has_started(self): - if _log.isEnabledFor(_INFO): - _log.info("%s: notifications listener has started (%s)", self.receiver, self.receiver.handle) - notification_flags = self.receiver.enable_notifications() - self.receiver.status[_status.KEYS.NOTIFICATION_FLAGS] = notification_flags - self.receiver.notify_devices() - self._status_changed(self.receiver) #, _status.ALERT.NOTIFICATION) + def has_started(self): + if _log.isEnabledFor(_INFO): + _log.info("%s: notifications listener has started (%s)", + self.receiver, self.receiver.handle) + notification_flags = self.receiver.enable_notifications() + self.receiver.status[ + _status.KEYS.NOTIFICATION_FLAGS] = notification_flags + self.receiver.notify_devices() + self._status_changed(self.receiver) #, _status.ALERT.NOTIFICATION) - def has_stopped(self): - r, self.receiver = self.receiver, None - assert r is not None - if _log.isEnabledFor(_INFO): - _log.info("%s: notifications listener has stopped", r) + def has_stopped(self): + r, self.receiver = self.receiver, None + assert r is not None + if _log.isEnabledFor(_INFO): + _log.info("%s: notifications listener has stopped", r) - # because udev is not notifying us about device removal, - # make sure to clean up in _all_listeners - _all_listeners.pop(r.path, None) + # because udev is not notifying us about device removal, + # make sure to clean up in _all_listeners + _all_listeners.pop(r.path, None) - r.status = _("The receiver was unplugged.") - if r: - try: - r.close() - except: - _log.exception("closing receiver %s" % r.path) - self.status_changed_callback(r) #, _status.ALERT.NOTIFICATION) + r.status = _("The receiver was unplugged.") + if r: + try: + r.close() + except: + _log.exception("closing receiver %s" % r.path) + self.status_changed_callback(r) #, _status.ALERT.NOTIFICATION) - # def tick(self, timestamp): - # if not self.tick_period: - # raise Exception("tick() should not be called without a tick_period: %s", self) - # - # # not necessary anymore, we're now using udev monitor to watch for receiver status - # # if self._last_tick > 0 and timestamp - self._last_tick > _POLL_TICK * 2: - # # # if we missed a couple of polls, most likely the computer went into - # # # sleep, and we have to reinitialize the receiver again - # # _log.warn("%s: possible sleep detected, closing this listener", self.receiver) - # # self.stop() - # # return - # - # self._last_tick = timestamp - # - # try: - # # read these in case they haven't been read already - # # self.receiver.serial, self.receiver.firmware - # if self.receiver.status.lock_open: - # # don't mess with stuff while pairing - # return - # - # self.receiver.status.poll(timestamp) - # - # # Iterating directly through the reciver would unnecessarily probe - # # all possible devices, even unpaired ones. - # # Checking for each device number in turn makes sure only already - # # known devices are polled. - # # This is okay because we should have already known about them all - # # long before the first poll() happents, through notifications. - # for number in range(1, 6): - # if number in self.receiver: - # dev = self.receiver[number] - # if dev and dev.status is not None: - # dev.status.poll(timestamp) - # except Exception as e: - # _log.exception("polling", e) + # def tick(self, timestamp): + # if not self.tick_period: + # raise Exception("tick() should not be called without a tick_period: %s", self) + # + # # not necessary anymore, we're now using udev monitor to watch for receiver status + # # if self._last_tick > 0 and timestamp - self._last_tick > _POLL_TICK * 2: + # # # if we missed a couple of polls, most likely the computer went into + # # # sleep, and we have to reinitialize the receiver again + # # _log.warn("%s: possible sleep detected, closing this listener", self.receiver) + # # self.stop() + # # return + # + # self._last_tick = timestamp + # + # try: + # # read these in case they haven't been read already + # # self.receiver.serial, self.receiver.firmware + # if self.receiver.status.lock_open: + # # don't mess with stuff while pairing + # return + # + # self.receiver.status.poll(timestamp) + # + # # Iterating directly through the reciver would unnecessarily probe + # # all possible devices, even unpaired ones. + # # Checking for each device number in turn makes sure only already + # # known devices are polled. + # # This is okay because we should have already known about them all + # # long before the first poll() happents, through notifications. + # for number in range(1, 6): + # if number in self.receiver: + # dev = self.receiver[number] + # if dev and dev.status is not None: + # dev.status.poll(timestamp) + # except Exception as e: + # _log.exception("polling", e) - def _status_changed(self, device, alert=_status.ALERT.NONE, reason=None): - assert device is not None - if _log.isEnabledFor(_INFO): - if device.kind is None: - _log.info("status_changed %s: %s, %s (%X) %s", device, - 'present' if bool(device) else 'removed', - device.status, alert, reason or '') - else: - _log.info("status_changed %s: %s %s, %s (%X) %s", device, - 'paired' if bool(device) else 'unpaired', - 'online' if device.online else 'offline', - device.status, alert, reason or '') + def _status_changed(self, device, alert=_status.ALERT.NONE, reason=None): + assert device is not None + if _log.isEnabledFor(_INFO): + if device.kind is None: + _log.info("status_changed %s: %s, %s (%X) %s", device, + 'present' if bool(device) else 'removed', + device.status, alert, reason or '') + else: + _log.info("status_changed %s: %s %s, %s (%X) %s", device, + 'paired' if bool(device) else 'unpaired', + 'online' if device.online else 'offline', + device.status, alert, reason or '') - if device.kind is None: - assert device == self.receiver - # the status of the receiver changed - self.status_changed_callback(device, alert, reason) - return + if device.kind is None: + assert device == self.receiver + # the status of the receiver changed + self.status_changed_callback(device, alert, reason) + return - assert device.receiver == self.receiver - if not device: - # Device was unpaired, and isn't valid anymore. - # We replace it with a ghost so that the UI has something to work - # with while cleaning up. - _log.warn("device %s was unpaired, ghosting", device) - device = _ghost(device) + assert device.receiver == self.receiver + if not device: + # Device was unpaired, and isn't valid anymore. + # We replace it with a ghost so that the UI has something to work + # with while cleaning up. + _log.warn("device %s was unpaired, ghosting", device) + device = _ghost(device) - self.status_changed_callback(device, alert, reason) + self.status_changed_callback(device, alert, reason) - if not device: - # the device was just unpaired, need to update the - # status of the receiver as well - self.status_changed_callback(self.receiver) + if not device: + # the device was just unpaired, need to update the + # status of the receiver as well + self.status_changed_callback(self.receiver) - def _notifications_handler(self, n): - assert self.receiver - # if _log.isEnabledFor(_DEBUG): - # _log.debug("%s: handling %s", self.receiver, n) - if n.devnumber == 0xFF: - # a receiver notification - _notifications.process(self.receiver, n) - return + def _notifications_handler(self, n): + assert self.receiver + # if _log.isEnabledFor(_DEBUG): + # _log.debug("%s: handling %s", self.receiver, n) + if n.devnumber == 0xFF: + # a receiver notification + _notifications.process(self.receiver, n) + return - # a device notification - if not(0 < n.devnumber <= self.receiver.max_devices): - if _log.isEnabledFor(_WARNING): - _log.warning(_("Unexpected device number (%s) in notification %s." % (n.devnumber, n))) - return - already_known = n.devnumber in self.receiver + # a device notification + if not (0 < n.devnumber <= self.receiver.max_devices): + if _log.isEnabledFor(_WARNING): + _log.warning( + _("Unexpected device number (%s) in notification %s." % + (n.devnumber, n))) + return + already_known = n.devnumber in self.receiver - # FIXME: hacky fix for kernel/hardware race condition - # If the device was just turned on or woken up from sleep, it may not - # be ready to receive commands. The "payload" bit of the wireless - # status notification seems to tell us this. If this is the case, we - # must wait a short amount of time to avoid causing a broken pipe - # error. - device_ready = not bool(ord(n.data[0:1]) & 0x80) or n.sub_id != 0x41 - if not device_ready: - time.sleep(0.01) + # FIXME: hacky fix for kernel/hardware race condition + # If the device was just turned on or woken up from sleep, it may not + # be ready to receive commands. The "payload" bit of the wireless + # status notification seems to tell us this. If this is the case, we + # must wait a short amount of time to avoid causing a broken pipe + # error. + device_ready = not bool(ord(n.data[0:1]) & 0x80) or n.sub_id != 0x41 + if not device_ready: + time.sleep(0.01) - if n.sub_id == 0x40 and not already_known: - return # disconnecting something that is not known - nothing to do + if n.sub_id == 0x40 and not already_known: + return # disconnecting something that is not known - nothing to do - if n.sub_id == 0x41: - if not already_known: - dev = self.receiver.register_new_device(n.devnumber, n) - elif self.receiver.status.lock_open and self.receiver.re_pairs and not ord(n.data[0:1]) & 0x40: - dev = self.receiver[n.devnumber] - del self.receiver[n.devnumber] # get rid of information on device re-paired away - self._status_changed(dev) # signal that this device has changed - dev = self.receiver.register_new_device(n.devnumber, n) - self.receiver.status.new_device = self.receiver[n.devnumber] - else: - dev = self.receiver[n.devnumber] - else: - dev = self.receiver[n.devnumber] + if n.sub_id == 0x41: + if not already_known: + dev = self.receiver.register_new_device(n.devnumber, n) + elif self.receiver.status.lock_open and self.receiver.re_pairs and not ord( + n.data[0:1]) & 0x40: + dev = self.receiver[n.devnumber] + del self.receiver[ + n. + devnumber] # get rid of information on device re-paired away + self._status_changed( + dev) # signal that this device has changed + dev = self.receiver.register_new_device(n.devnumber, n) + self.receiver.status.new_device = self.receiver[n.devnumber] + else: + dev = self.receiver[n.devnumber] + else: + dev = self.receiver[n.devnumber] - if not dev: - _log.warn("%s: received %s for invalid device %d: %r", self.receiver, n, n.devnumber, dev) - return + if not dev: + _log.warn("%s: received %s for invalid device %d: %r", + self.receiver, n, n.devnumber, dev) + return - # Apply settings every time the device connects - if n.sub_id == 0x41: - if _log.isEnabledFor(_INFO): - _log.info("%s triggered new device %s (%s)", n, dev, dev.kind) - # 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) - _status.attach_to(dev, self._status_changed) - # the receiver changed status as well - self._status_changed(self.receiver) + # Apply settings every time the device connects + if n.sub_id == 0x41: + if _log.isEnabledFor(_INFO): + _log.info("%s triggered new device %s (%s)", n, dev, dev.kind) + # 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) + _status.attach_to(dev, self._status_changed) + # the receiver changed status as well + self._status_changed(self.receiver) - assert dev - assert dev.status is not None - _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 and n.address == 0x04 - if _log.isEnabledFor(_INFO): - _log.info("%s: pairing detected new device", self.receiver) - self.receiver.status.new_device = dev - elif dev.online is None: - dev.ping() + assert dev + assert dev.status is not None + _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 and n.address == 0x04 + if _log.isEnabledFor(_INFO): + _log.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 '' % (self.receiver.path, + self.receiver.handle) + + __unicode__ = __str__ - def __str__(self): - return '' % (self.receiver.path, self.receiver.handle) - __unicode__ = __str__ # # @@ -256,103 +267,106 @@ _all_listeners = {} def _start(device_info): - assert _status_callback - receiver = Receiver.open(device_info) - if receiver: - rl = ReceiverListener(receiver, _status_callback) - rl.start() - _all_listeners[device_info.path] = rl - return rl + assert _status_callback + receiver = Receiver.open(device_info) + if receiver: + rl = ReceiverListener(receiver, _status_callback) + rl.start() + _all_listeners[device_info.path] = rl + return rl - _log.warn("failed to open %s", device_info) + _log.warn("failed to open %s", device_info) def start_all(): - # just in case this it called twice in a row... - stop_all() + # just in case this it called twice in a row... + stop_all() - if _log.isEnabledFor(_INFO): - _log.info("starting receiver listening threads") - for device_info in _base.receivers(): - _process_receiver_event('add', device_info) + if _log.isEnabledFor(_INFO): + _log.info("starting receiver listening threads") + for device_info in _base.receivers(): + _process_receiver_event('add', device_info) def stop_all(): - listeners = list(_all_listeners.values()) - _all_listeners.clear() + listeners = list(_all_listeners.values()) + _all_listeners.clear() - if listeners: - if _log.isEnabledFor(_INFO): - _log.info("stopping receiver listening threads %s", listeners) + if listeners: + if _log.isEnabledFor(_INFO): + _log.info("stopping receiver listening threads %s", listeners) - for l in listeners: - l.stop() + for l in listeners: + l.stop() - configuration.save() + configuration.save() + + if listeners: + for l in listeners: + l.join() - if listeners: - for l in listeners: - l.join() # ping all devices to find out whether they are connected # after a resume, the device may have been off # so mark its saved status to ensure that the status is pushed to the device when it comes back -def ping_all(resuming = False): - for l in _all_listeners.values(): - count = l.receiver.count() - if count: - for dev in l.receiver: - if resuming: - dev.status._active = False - dev.ping() - l._status_changed(dev) - count -= 1 - if not count: - break +def ping_all(resuming=False): + for l in _all_listeners.values(): + count = l.receiver.count() + if count: + for dev in l.receiver: + if resuming: + dev.status._active = False + dev.ping() + l._status_changed(dev) + count -= 1 + if not count: + break from logitech_receiver import base as _base _status_callback = None _error_callback = None + def setup_scanner(status_changed_callback, error_callback): - global _status_callback, _error_callback - assert _status_callback is None, 'scanner was already set-up' + global _status_callback, _error_callback + assert _status_callback is None, 'scanner was already set-up' - _status_callback = status_changed_callback - _error_callback = error_callback + _status_callback = status_changed_callback + _error_callback = error_callback - _base.notify_on_receivers_glib(_process_receiver_event) + _base.notify_on_receivers_glib(_process_receiver_event) # receiver add/remove events will start/stop listener threads def _process_receiver_event(action, device_info): - assert action is not None - assert device_info is not None - assert _error_callback + assert action is not None + assert device_info is not None + assert _error_callback - if _log.isEnabledFor(_INFO): - _log.info("receiver event %s %s", action, device_info) + if _log.isEnabledFor(_INFO): + _log.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) - if l is not None: - assert isinstance(l, ReceiverListener) - l.stop() + # whatever the action, stop any previous receivers at this path + l = _all_listeners.pop(device_info.path, None) + if l is not None: + assert isinstance(l, ReceiverListener) + l.stop() - if action == 'add': - # a new receiver device was detected - try: - _start(device_info) - except OSError: - # permission error, ignore this path for now - # If receiver has extended ACL but not writable then it is for another seat. - # (It would be easier to use pylibacl but adding the pylibacl dependencies - # for this special case is not good.) - try: - import subprocess, re - output = subprocess.check_output(['/usr/bin/getfacl', '-p', device_info.path]) - if not re.search(b'user:.+:',output) : - _error_callback('permissions', device_info.path) - except: - _error_callback('permissions', device_info.path) + if action == 'add': + # a new receiver device was detected + try: + _start(device_info) + except OSError: + # permission error, ignore this path for now + # If receiver has extended ACL but not writable then it is for another seat. + # (It would be easier to use pylibacl but adding the pylibacl dependencies + # for this special case is not good.) + try: + import subprocess, re + output = subprocess.check_output( + ['/usr/bin/getfacl', '-p', device_info.path]) + if not re.search(b'user:.+:', output): + _error_callback('permissions', device_info.path) + except: + _error_callback('permissions', device_info.path) diff --git a/lib/solaar/tasks.py b/lib/solaar/tasks.py index baed1584..5679c6df 100644 --- a/lib/solaar/tasks.py +++ b/lib/solaar/tasks.py @@ -27,44 +27,45 @@ del getLogger from threading import Thread as _Thread try: - from Queue import Queue as _Queue + from Queue import Queue as _Queue except ImportError: - from queue import Queue as _Queue + from queue import Queue as _Queue # # # + class TaskRunner(_Thread): - def __init__(self, name): - super(TaskRunner, self).__init__(name=name) - self.daemon = True - self.queue = _Queue(16) - self.alive = False + def __init__(self, name): + super(TaskRunner, self).__init__(name=name) + self.daemon = True + self.queue = _Queue(16) + self.alive = False - def __call__(self, function, *args, **kwargs): - task = (function, args, kwargs) - self.queue.put(task) + def __call__(self, function, *args, **kwargs): + task = (function, args, kwargs) + self.queue.put(task) - def stop(self): - self.alive = False - self.queue.put(None) + def stop(self): + self.alive = False + self.queue.put(None) - def run(self): - self.alive = True + def run(self): + self.alive = True - if _log.isEnabledFor(_DEBUG): - _log.debug("started") + if _log.isEnabledFor(_DEBUG): + _log.debug("started") - while self.alive: - task = self.queue.get() - if task: - function, args, kwargs = task - assert function - try: - function(*args, **kwargs) - except: - _log.exception("calling %s", function) + while self.alive: + task = self.queue.get() + if task: + function, args, kwargs = task + assert function + try: + function(*args, **kwargs) + except: + _log.exception("calling %s", function) - if _log.isEnabledFor(_DEBUG): - _log.debug("stopped") + if _log.isEnabledFor(_DEBUG): + _log.debug("stopped") diff --git a/lib/solaar/ui/__init__.py b/lib/solaar/ui/__init__.py index be437e44..ea3f1fe8 100644 --- a/lib/solaar/ui/__init__.py +++ b/lib/solaar/ui/__init__.py @@ -19,14 +19,12 @@ from __future__ import absolute_import, division, print_function, unicode_literals - from logging import getLogger, DEBUG as _DEBUG _log = getLogger(__name__) del getLogger from gi.repository import GLib, Gtk - from solaar.i18n import _ # @@ -41,43 +39,50 @@ GLib.threads_init() # # + def _error_dialog(reason, object): - _log.error("error: %s %s", reason, object) + _log.error("error: %s %s", reason, object) - if reason == 'permissions': - title = _("Permissions error") - text = _("Found a Logitech Receiver (%s), but did not have permission to open it.") % object + \ - '\n\n' + \ - _("If you've just installed Solaar, try removing the receiver and plugging it back in.") - 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.") - else: - raise Exception("ui.error_dialog: don't know how to handle (%s, %s)", reason, object) + if reason == 'permissions': + title = _("Permissions error") + text = _("Found a Logitech Receiver (%s), but did not have permission to open it.") % object + \ + '\n\n' + \ + _("If you've just installed Solaar, try removing the receiver and plugging it back in.") + 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.") + else: + raise Exception("ui.error_dialog: don't know how to handle (%s, %s)", + reason, object) - assert title - assert text + assert title + assert text - m = Gtk.MessageDialog(None, Gtk.DialogFlags.MODAL, Gtk.MessageType.ERROR, Gtk.ButtonsType.CLOSE, text) - m.set_title(title) - m.run() - m.destroy() + m = Gtk.MessageDialog(None, Gtk.DialogFlags.MODAL, Gtk.MessageType.ERROR, + Gtk.ButtonsType.CLOSE, text) + m.set_title(title) + m.run() + m.destroy() def error_dialog(reason, object): - assert reason is not None - GLib.idle_add(_error_dialog, reason, object) + assert reason is not None + GLib.idle_add(_error_dialog, reason, object) + # # # _task_runner = None + + def ui_async(function, *args, **kwargs): - if _task_runner: - _task_runner(function, *args, **kwargs) + if _task_runner: + _task_runner(function, *args, **kwargs) + # # @@ -87,65 +92,70 @@ from . import notify, tray, window def _startup(app, startup_hook, use_tray, show_window): - if _log.isEnabledFor(_DEBUG): - _log.debug("startup registered=%s, remote=%s", app.get_is_registered(), app.get_is_remote()) + if _log.isEnabledFor(_DEBUG): + _log.debug("startup registered=%s, remote=%s", app.get_is_registered(), + app.get_is_remote()) - from solaar.tasks import TaskRunner as _TaskRunner - global _task_runner - _task_runner = _TaskRunner('AsyncUI') - _task_runner.start() + from solaar.tasks import TaskRunner as _TaskRunner + global _task_runner + _task_runner = _TaskRunner('AsyncUI') + _task_runner.start() - notify.init() - if use_tray: - tray.init(lambda _ignore: window.destroy()) - window.init(show_window, use_tray) + notify.init() + if use_tray: + tray.init(lambda _ignore: window.destroy()) + window.init(show_window, use_tray) - startup_hook() + startup_hook() def _activate(app): - if _log.isEnabledFor(_DEBUG): - _log.debug("activate") - if app.get_windows(): - window.popup() - else: - app.add_window(window._window) + if _log.isEnabledFor(_DEBUG): + _log.debug("activate") + if app.get_windows(): + window.popup() + else: + app.add_window(window._window) def _command_line(app, command_line): - if _log.isEnabledFor(_DEBUG): - _log.debug("command_line %s", command_line.get_arguments()) + if _log.isEnabledFor(_DEBUG): + _log.debug("command_line %s", command_line.get_arguments()) - return 0 + return 0 def _shutdown(app, shutdown_hook): - if _log.isEnabledFor(_DEBUG): - _log.debug("shutdown") + if _log.isEnabledFor(_DEBUG): + _log.debug("shutdown") - shutdown_hook() + shutdown_hook() - # stop the async UI processor - global _task_runner - _task_runner.stop() - _task_runner = None + # stop the async UI processor + global _task_runner + _task_runner.stop() + _task_runner = None - tray.destroy() - notify.uninit() + tray.destroy() + notify.uninit() def run_loop(startup_hook, shutdown_hook, use_tray, show_window, args=None): - assert use_tray or show_window, 'need either tray or visible window' - # from gi.repository.Gio import ApplicationFlags as _ApplicationFlags - APP_ID = 'io.github.pwr.solaar' - application = Gtk.Application.new(APP_ID, 0) # _ApplicationFlags.HANDLES_COMMAND_LINE) + assert use_tray or show_window, 'need either tray or visible window' + # from gi.repository.Gio import ApplicationFlags as _ApplicationFlags + APP_ID = 'io.github.pwr.solaar' + application = Gtk.Application.new( + APP_ID, 0) # _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.run(args) - application.run(args) # # @@ -155,20 +165,20 @@ from logitech_receiver.status import ALERT def _status_changed(device, alert, reason): - assert device is not None - if _log.isEnabledFor(_DEBUG): - _log.debug("status changed: %s (%s) %s", device, alert, reason) + assert device is not None + if _log.isEnabledFor(_DEBUG): + _log.debug("status changed: %s (%s) %s", device, alert, reason) - tray.update(device) - if alert & ALERT.ATTENTION: - tray.attention(reason) + tray.update(device) + if alert & ALERT.ATTENTION: + tray.attention(reason) - need_popup = alert & ALERT.SHOW_WINDOW - window.update(device, need_popup) + need_popup = alert & ALERT.SHOW_WINDOW + window.update(device, need_popup) - if alert & (ALERT.NOTIFICATION | ALERT.ATTENTION): - notify.show(device, reason) + if alert & (ALERT.NOTIFICATION | ALERT.ATTENTION): + notify.show(device, reason) def status_changed(device, alert=ALERT.NONE, reason=None): - GLib.idle_add(_status_changed, device, alert, reason) + GLib.idle_add(_status_changed, device, alert, reason) diff --git a/lib/solaar/ui/about.py b/lib/solaar/ui/about.py index 2c860c24..183a4c91 100644 --- a/lib/solaar/ui/about.py +++ b/lib/solaar/ui/about.py @@ -32,64 +32,68 @@ _dialog = None def _create(): - about = Gtk.AboutDialog() + about = Gtk.AboutDialog() - about.set_program_name(NAME) - about.set_version(__version__) - about.set_comments(_("Shows status of devices connected\nthrough wireless Logitech receivers.")) + about.set_program_name(NAME) + about.set_version(__version__) + about.set_comments( + _("Shows status of devices connected\nthrough wireless Logitech receivers." + )) - about.set_logo_icon_name(NAME.lower()) + about.set_logo_icon_name(NAME.lower()) - about.set_copyright('© 2012-2013 Daniel Pavel') - about.set_license_type(Gtk.License.GPL_2_0) + about.set_copyright('© 2012-2013 Daniel Pavel') + about.set_license_type(Gtk.License.GPL_2_0) - about.set_authors(('Daniel Pavel http://github.com/pwr',)) - try: - 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', - )) - 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', - )) - except TypeError: - # gtk3 < ~3.6.4 has incorrect gi bindings - import logging - logging.exception("failed to fully create the about dialog") - except: - # the Gtk3 version may be too old, and the function does not exist - import logging - logging.exception("failed to fully create the about dialog") + about.set_authors(('Daniel Pavel http://github.com/pwr', )) + try: + 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', + )) + 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', + )) + except TypeError: + # gtk3 < ~3.6.4 has incorrect gi bindings + import logging + logging.exception("failed to fully create the about dialog") + except: + # the Gtk3 version may be too old, and the function does not exist + import logging + 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)', - ))) + 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)', + ))) - about.set_website('http://pwr-solaar.github.io/Solaar/') - about.set_website_label(NAME) + about.set_website('http://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) + def _hide(dialog, event): + dialog.hide() + return True - return about + about.connect('delete-event', _hide) + + return about def show_window(trigger=None): - global _dialog - if _dialog is None: - _dialog = _create() - _dialog.present() + global _dialog + if _dialog is None: + _dialog = _create() + _dialog.present() diff --git a/lib/solaar/ui/action.py b/lib/solaar/ui/action.py index b712232c..09bd4201 100644 --- a/lib/solaar/ui/action.py +++ b/lib/solaar/ui/action.py @@ -25,30 +25,31 @@ from gi.repository import Gtk, Gdk # _log = getLogger(__name__) # del getLogger - from solaar.i18n import _ # # # + def make(name, label, function, stock_id=None, *args): - action = Gtk.Action(name, label, label, None) - action.set_icon_name(name) - if stock_id is not None: - action.set_stock_id(stock_id) - if function: - action.connect('activate', function, *args) - return action + action = Gtk.Action(name, label, label, None) + action.set_icon_name(name) + if stock_id is not None: + action.set_stock_id(stock_id) + if function: + action.connect('activate', function, *args) + return action def make_toggle(name, label, function, stock_id=None, *args): - action = Gtk.ToggleAction(name, label, label, None) - action.set_icon_name(name) - if stock_id is not None: - action.set_stock_id(stock_id) - action.connect('activate', function, *args) - return action + action = Gtk.ToggleAction(name, label, label, None) + action.set_icon_name(name) + if stock_id is not None: + action.set_stock_id(stock_id) + action.connect('activate', function, *args) + return action + # # @@ -62,49 +63,55 @@ def make_toggle(name, label, function, stock_id=None, *args): # action.set_sensitive(notify.available) # toggle_notifications = make_toggle('notifications', 'Notifications', _toggle_notifications) - from .about import show_window as _show_about_window from solaar import NAME -about = make('help-about', _("About") + ' ' + NAME, _show_about_window, stock_id=Gtk.STOCK_ABOUT) +about = make('help-about', + _("About") + ' ' + NAME, + _show_about_window, + stock_id=Gtk.STOCK_ABOUT) # # # from . import pair_window -def pair(window, receiver): - assert receiver - assert receiver.kind is None - pair_dialog = pair_window.create(receiver) - pair_dialog.set_transient_for(window) - pair_dialog.set_destroy_with_parent(True) - pair_dialog.set_modal(True) - pair_dialog.set_type_hint(Gdk.WindowTypeHint.DIALOG) - pair_dialog.set_position(Gtk.WindowPosition.CENTER) - pair_dialog.present() + +def pair(window, receiver): + assert receiver + assert receiver.kind is None + + pair_dialog = pair_window.create(receiver) + pair_dialog.set_transient_for(window) + pair_dialog.set_destroy_with_parent(True) + pair_dialog.set_modal(True) + pair_dialog.set_type_hint(Gdk.WindowTypeHint.DIALOG) + pair_dialog.set_position(Gtk.WindowPosition.CENTER) + pair_dialog.present() from ..ui import error_dialog + + def unpair(window, device): - assert device - assert device.kind is not None + assert device + assert device.kind is not None - qdialog = Gtk.MessageDialog(window, 0, - Gtk.MessageType.QUESTION, Gtk.ButtonsType.NONE, - _("Unpair") + ' ' + device.name + ' ?') - qdialog.set_icon_name('remove') - qdialog.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL) - qdialog.add_button(_("Unpair"), Gtk.ResponseType.ACCEPT) - choice = qdialog.run() - qdialog.destroy() - if choice == Gtk.ResponseType.ACCEPT: - receiver = device.receiver - assert receiver - device_number = device.number + qdialog = Gtk.MessageDialog(window, 0, Gtk.MessageType.QUESTION, + Gtk.ButtonsType.NONE, + _("Unpair") + ' ' + device.name + ' ?') + qdialog.set_icon_name('remove') + qdialog.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL) + qdialog.add_button(_("Unpair"), Gtk.ResponseType.ACCEPT) + choice = qdialog.run() + qdialog.destroy() + if choice == Gtk.ResponseType.ACCEPT: + receiver = device.receiver + assert receiver + device_number = device.number - try: - del receiver[device_number] - except: - # _log.exception("unpairing %s", device) - error_dialog('unpair', device) + try: + del receiver[device_number] + except: + # _log.exception("unpairing %s", device) + error_dialog('unpair', device) diff --git a/lib/solaar/ui/config_panel.py b/lib/solaar/ui/config_panel.py index 48146fed..4435daed 100644 --- a/lib/solaar/ui/config_panel.py +++ b/lib/solaar/ui/config_panel.py @@ -30,221 +30,238 @@ from logitech_receiver.settings import KIND as _SETTING_KIND # # -def _read_async(setting, force_read, sbox, device_is_online): - def _do_read(s, force, sb, online): - v = s.read(not force) - GLib.idle_add(_update_setting_item, sb, v, online, priority=99) - _ui_async(_do_read, setting, force_read, sbox, device_is_online) +def _read_async(setting, force_read, sbox, device_is_online): + def _do_read(s, force, sb, online): + v = s.read(not force) + GLib.idle_add(_update_setting_item, sb, v, online, priority=99) + + _ui_async(_do_read, setting, force_read, sbox, device_is_online) def _write_async(setting, value, sbox): - _ignore, failed, spinner, control = sbox.get_children() - control.set_sensitive(False) - failed.set_visible(False) - spinner.set_visible(True) - spinner.start() + _ignore, failed, spinner, control = sbox.get_children() + control.set_sensitive(False) + failed.set_visible(False) + spinner.set_visible(True) + spinner.start() - def _do_write(s, v, sb): - v = setting.write(v) - GLib.idle_add(_update_setting_item, sb, v, True, priority=99) + def _do_write(s, v, sb): + v = setting.write(v) + GLib.idle_add(_update_setting_item, sb, v, True, priority=99) - _ui_async(_do_write, setting, value, sbox) + _ui_async(_do_write, setting, value, sbox) -def _write_async_key_value(setting, key, value, sbox): - _ignore, failed, spinner, control = sbox.get_children() - control.set_sensitive(False) - failed.set_visible(False) - spinner.set_visible(True) - spinner.start() +def _write_async_key_value(setting, key, value, sbox): + _ignore, failed, spinner, control = sbox.get_children() + control.set_sensitive(False) + failed.set_visible(False) + spinner.set_visible(True) + spinner.start() - def _do_write_key_value(s, k, v, sb): - v = setting.write_key_value(k, v) - GLib.idle_add(_update_setting_item, sb, {k: v}, True, priority=99) + def _do_write_key_value(s, k, v, sb): + v = setting.write_key_value(k, v) + GLib.idle_add(_update_setting_item, sb, {k: v}, True, priority=99) - _ui_async(_do_write_key_value, setting, key, value, sbox) + _ui_async(_do_write_key_value, setting, key, value, sbox) # # # + def _create_toggle_control(setting): - def _switch_notify(switch, _ignore, s): - if switch.get_sensitive(): - _write_async(s, switch.get_active() == True, switch.get_parent()) + def _switch_notify(switch, _ignore, s): + if switch.get_sensitive(): + _write_async(s, switch.get_active() == True, switch.get_parent()) + + c = Gtk.Switch() + c.connect('notify::active', _switch_notify, setting) + return c - c = Gtk.Switch() - c.connect('notify::active', _switch_notify, setting) - return c def _create_choice_control(setting): - def _combo_notify(cbbox, s): - if cbbox.get_sensitive(): - _write_async(s, cbbox.get_active_id(), cbbox.get_parent()) + def _combo_notify(cbbox, s): + if cbbox.get_sensitive(): + _write_async(s, cbbox.get_active_id(), cbbox.get_parent()) + + c = Gtk.ComboBoxText() + # TODO i18n text entries + for entry in setting.choices: + c.append(str(entry), str(entry)) + c.connect('changed', _combo_notify, setting) + return c - c = Gtk.ComboBoxText() - # TODO i18n text entries - for entry in setting.choices: - c.append(str(entry), str(entry)) - c.connect('changed', _combo_notify, setting) - return c def _create_map_choice_control(setting): - def _map_value_notify_key(cbbox, s): - setting, valueBox = s - key_choice = int(cbbox.get_active_id()) - if cbbox.get_sensitive(): - valueBox.remove_all() - _map_populate_value_box(valueBox, setting, key_choice) + def _map_value_notify_key(cbbox, s): + setting, valueBox = s + key_choice = int(cbbox.get_active_id()) + if cbbox.get_sensitive(): + valueBox.remove_all() + _map_populate_value_box(valueBox, setting, key_choice) - def _map_value_notify_value(cbbox, s): - setting, keyBox = s - key_choice = keyBox.get_active_id() - if key_choice is not None and cbbox.get_sensitive() and cbbox.get_active_id(): - if setting._value.get(key_choice) != int(cbbox.get_active_id()): - setting._value[key_choice] = int(cbbox.get_active_id()) - _write_async_key_value(setting, key_choice, setting._value[key_choice], cbbox.get_parent().get_parent()) + def _map_value_notify_value(cbbox, s): + setting, keyBox = s + key_choice = keyBox.get_active_id() + if key_choice is not None and cbbox.get_sensitive( + ) and cbbox.get_active_id(): + if setting._value.get(key_choice) != int(cbbox.get_active_id()): + setting._value[key_choice] = int(cbbox.get_active_id()) + _write_async_key_value(setting, key_choice, + setting._value[key_choice], + cbbox.get_parent().get_parent()) - def _map_populate_value_box(valueBox, setting, key_choice): - choices = None - choices = setting.choices[key_choice] - current = setting._value.get(str(key_choice)) # just in case the persisted value is missing some keys - if choices: - # TODO i18n text entries - for choice in choices: - valueBox.append(str(int(choice)), str(choice)) - if current is not None: - valueBox.set_active_id(str(int(current))) + def _map_populate_value_box(valueBox, setting, key_choice): + choices = None + choices = setting.choices[key_choice] + current = setting._value.get( + str(key_choice + )) # just in case the persisted value is missing some keys + if choices: + # TODO i18n text entries + for choice in choices: + valueBox.append(str(int(choice)), str(choice)) + if current is not None: + valueBox.set_active_id(str(int(current))) + + c = Gtk.HBox(homogeneous=False, spacing=6) + keyBox = Gtk.ComboBoxText() + valueBox = Gtk.ComboBoxText() + c.pack_start(keyBox, False, False, 0) + c.pack_end(valueBox, False, False, 0) + # TODO i18n text entries + for entry in setting.choices: + keyBox.append(str(int(entry)), str(entry)) + keyBox.set_active(0) + keyBox.connect('changed', _map_value_notify_key, (setting, valueBox)) + _map_populate_value_box(valueBox, setting, int(keyBox.get_active_id())) + valueBox.connect('changed', _map_value_notify_value, (setting, keyBox)) + return c - c = Gtk.HBox(homogeneous=False, spacing=6) - keyBox = Gtk.ComboBoxText() - valueBox = Gtk.ComboBoxText() - c.pack_start(keyBox, False, False, 0) - c.pack_end(valueBox, False, False, 0) - # TODO i18n text entries - for entry in setting.choices: - keyBox.append(str(int(entry)), str(entry)) - keyBox.set_active(0) - keyBox.connect('changed', _map_value_notify_key, (setting,valueBox)) - _map_populate_value_box(valueBox, setting, int(keyBox.get_active_id())) - valueBox.connect('changed', _map_value_notify_value, (setting,keyBox)) - return c def _create_slider_control(setting): - class SliderControl: - __slots__ = ('gtk_range', 'timer', 'setting') - def __init__(self, setting): - self.setting = setting - self.timer = None + class SliderControl: + __slots__ = ('gtk_range', 'timer', 'setting') - self.gtk_range = Gtk.Scale() - self.gtk_range.set_range(*self.setting.range) - self.gtk_range.set_round_digits(0) - self.gtk_range.set_digits(0) - self.gtk_range.set_increments(1, 5) - self.gtk_range.connect('value-changed', - lambda _, c: c._changed(), - self) + def __init__(self, setting): + self.setting = setting + self.timer = None - def _write(self): - _write_async(self.setting, - int(self.gtk_range.get_value()), - self.gtk_range.get_parent()) - self.timer.cancel() + self.gtk_range = Gtk.Scale() + self.gtk_range.set_range(*self.setting.range) + self.gtk_range.set_round_digits(0) + self.gtk_range.set_digits(0) + self.gtk_range.set_increments(1, 5) + self.gtk_range.connect('value-changed', lambda _, c: c._changed(), + self) - def _changed(self): - if self.gtk_range.get_sensitive(): - if self.timer: - self.timer.cancel() - self.timer = _Timer(0.5, lambda: GLib.idle_add(self._write)) - self.timer.start() + def _write(self): + _write_async(self.setting, int(self.gtk_range.get_value()), + self.gtk_range.get_parent()) + self.timer.cancel() + + def _changed(self): + if self.gtk_range.get_sensitive(): + if self.timer: + self.timer.cancel() + self.timer = _Timer(0.5, lambda: GLib.idle_add(self._write)) + self.timer.start() + + control = SliderControl(setting) + return control.gtk_range - control = SliderControl(setting) - return control.gtk_range # # # + def _create_sbox(s): - sbox = Gtk.HBox(homogeneous=False, spacing=6) - sbox.pack_start(Gtk.Label(s.label), False, False, 0) + sbox = Gtk.HBox(homogeneous=False, spacing=6) + sbox.pack_start(Gtk.Label(s.label), False, False, 0) - spinner = Gtk.Spinner() - spinner.set_tooltip_text(_("Working") + '...') + spinner = Gtk.Spinner() + spinner.set_tooltip_text(_("Working") + '...') - 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.")) - if s.kind == _SETTING_KIND.toggle: - control = _create_toggle_control(s) - sbox.pack_end(control, False, False, 0) - elif s.kind == _SETTING_KIND.choice: - control = _create_choice_control(s) - sbox.pack_end(control, False, False, 0) - elif s.kind == _SETTING_KIND.range: - control = _create_slider_control(s) - sbox.pack_end(control, True, True, 0) - elif s.kind == _SETTING_KIND.map_choice: - control = _create_map_choice_control(s) - sbox.pack_end(control, True, True, 0) - elif s.kind == _SETTING_KIND.multiple_toggle: - # ugly temporary hack! - choices = {k : [False, True] for k in s._validator.options} - class X: - def __init__(self, obj, ext): - self.obj = obj - self.ext = ext - def __getattr__(self, attr): - try: - return self.ext[attr] - except KeyError: - return getattr(self.obj, attr) - control = _create_map_choice_control(X(s, {'choices': choices})) - sbox.pack_end(control, True, True, 0) - else: - raise Exception("NotImplemented") + if s.kind == _SETTING_KIND.toggle: + control = _create_toggle_control(s) + sbox.pack_end(control, False, False, 0) + elif s.kind == _SETTING_KIND.choice: + control = _create_choice_control(s) + sbox.pack_end(control, False, False, 0) + elif s.kind == _SETTING_KIND.range: + control = _create_slider_control(s) + sbox.pack_end(control, True, True, 0) + elif s.kind == _SETTING_KIND.map_choice: + control = _create_map_choice_control(s) + sbox.pack_end(control, True, True, 0) + elif s.kind == _SETTING_KIND.multiple_toggle: + # ugly temporary hack! + choices = {k: [False, True] for k in s._validator.options} - control.set_sensitive(False) # the first read will enable it - sbox.pack_end(spinner, False, False, 0) - sbox.pack_end(failed, False, False, 0) + class X: + def __init__(self, obj, ext): + self.obj = obj + self.ext = ext - if s.description: - sbox.set_tooltip_text(s.description) + def __getattr__(self, attr): + try: + return self.ext[attr] + except KeyError: + return getattr(self.obj, attr) - sbox.show_all() - spinner.start() # the first read will stop it - failed.set_visible(False) + control = _create_map_choice_control(X(s, {'choices': choices})) + sbox.pack_end(control, True, True, 0) + else: + raise Exception("NotImplemented") - return sbox + control.set_sensitive(False) # the first read will enable it + sbox.pack_end(spinner, False, False, 0) + sbox.pack_end(failed, False, False, 0) + + if s.description: + sbox.set_tooltip_text(s.description) + + sbox.show_all() + spinner.start() # the first read will stop it + failed.set_visible(False) + + return sbox def _update_setting_item(sbox, value, is_online=True): - _ignore, failed, spinner, control = sbox.get_children() # depends on box layout - spinner.set_visible(False) - spinner.stop() + _ignore, failed, spinner, control = sbox.get_children( + ) # depends on box layout + spinner.set_visible(False) + spinner.stop() - if value is None: - control.set_sensitive(False) - failed.set_visible(is_online) - return + if value is None: + control.set_sensitive(False) + failed.set_visible(is_online) + return + + failed.set_visible(False) + if isinstance(control, Gtk.Switch): + control.set_active(value) + elif isinstance(control, Gtk.ComboBoxText): + control.set_active_id(str(value)) + elif isinstance(control, Gtk.Scale): + control.set_value(int(value)) + elif isinstance(control, Gtk.HBox): + kbox, vbox = control.get_children() # depends on box layout + if value.get(kbox.get_active_id()): + vbox.set_active_id(str(value.get(kbox.get_active_id()))) + else: + raise Exception("NotImplemented") + control.set_sensitive(True) - failed.set_visible(False) - if isinstance(control, Gtk.Switch): - control.set_active(value) - elif isinstance(control, Gtk.ComboBoxText): - control.set_active_id(str(value)) - elif isinstance(control, Gtk.Scale): - control.set_value(int(value)) - elif isinstance(control, Gtk.HBox): - kbox, vbox = control.get_children() # depends on box layout - if value.get(kbox.get_active_id()): - vbox.set_active_id(str(value.get(kbox.get_active_id()))) - else: - raise Exception("NotImplemented") - control.set_sensitive(True) # # @@ -254,57 +271,58 @@ def _update_setting_item(sbox, value, is_online=True): _box = None _items = {} + def create(): - global _box - assert _box is None - _box = Gtk.VBox(homogeneous=False, spacing=8) - _box._last_device = None - return _box + global _box + assert _box is None + _box = Gtk.VBox(homogeneous=False, spacing=8) + _box._last_device = None + return _box def update(device, is_online=None): - assert _box is not None - assert device - device_id = (device.receiver.path, device.number) - if is_online is None: - is_online = bool(device.online) + assert _box is not None + assert device + device_id = (device.receiver.path, device.number) + if is_online is None: + is_online = bool(device.online) - # if the device changed since last update, clear the box first - if device_id != _box._last_device: - _box.set_visible(False) - _box._last_device = device_id + # if the device changed since last update, clear the box first + if device_id != _box._last_device: + _box.set_visible(False) + _box._last_device = device_id - # hide controls belonging to other devices - for k, sbox in _items.items(): - sbox = _items[k] - sbox.set_visible(k[0:2] == device_id) + # hide controls belonging to other devices + for k, sbox in _items.items(): + sbox = _items[k] + sbox.set_visible(k[0:2] == device_id) - for s in device.settings: - k = (device_id[0], device_id[1], s.name) - if k in _items: - sbox = _items[k] - else: - sbox = _items[k] = _create_sbox(s) - _box.pack_start(sbox, False, False, 0) + for s in device.settings: + k = (device_id[0], device_id[1], s.name) + if k in _items: + sbox = _items[k] + else: + sbox = _items[k] = _create_sbox(s) + _box.pack_start(sbox, False, False, 0) - _read_async(s, False, sbox, is_online) + _read_async(s, False, sbox, is_online) - _box.set_visible(True) + _box.set_visible(True) def clean(device): - """Remove the controls for a given device serial. + """Remove the controls for a given device serial. Needed after the device has been unpaired. """ - assert _box is not None - device_id = (device.receiver.path, device.number) - for k in list(_items.keys()): - if k[0:2] == device_id: - _box.remove(_items[k]) - del _items[k] + assert _box is not None + device_id = (device.receiver.path, device.number) + for k in list(_items.keys()): + if k[0:2] == device_id: + _box.remove(_items[k]) + del _items[k] def destroy(): - global _box - _box = None - _items.clear() + global _box + _box = None + _items.clear() diff --git a/lib/solaar/ui/icons.py b/lib/solaar/ui/icons.py index f3a1f94a..666bb8d9 100644 --- a/lib/solaar/ui/icons.py +++ b/lib/solaar/ui/icons.py @@ -46,92 +46,110 @@ TRAY_ATTENTION = 'solaar-attention' def _look_for_application_icons(): - import os.path as _path - from os import environ as _environ + import os.path as _path + from os import environ as _environ - import sys as _sys - if _log.isEnabledFor(_DEBUG): - _log.debug("sys.path[0] = %s", _sys.path[0]) - prefix_share = _path.normpath(_path.join(_path.realpath(_sys.path[0]), '..')) - src_share = _path.normpath(_path.join(_path.realpath(_sys.path[0]), '..', 'share')) - local_share = _environ.get('XDG_DATA_HOME', _path.expanduser(_path.join('~', '.local', 'share'))) - data_dirs = _environ.get('XDG_DATA_DIRS', '/usr/local/share:/usr/share') - repo_share = _path.normpath(_path.join(_path.dirname(__file__), '..', '..', '..', 'share')) - setuptools_share = _path.normpath(_path.join(_path.dirname(__file__), '..', '..', 'share')) - del _sys + import sys as _sys + if _log.isEnabledFor(_DEBUG): + _log.debug("sys.path[0] = %s", _sys.path[0]) + prefix_share = _path.normpath( + _path.join(_path.realpath(_sys.path[0]), '..')) + src_share = _path.normpath( + _path.join(_path.realpath(_sys.path[0]), '..', 'share')) + local_share = _environ.get( + 'XDG_DATA_HOME', _path.expanduser(_path.join('~', '.local', 'share'))) + data_dirs = _environ.get('XDG_DATA_DIRS', '/usr/local/share:/usr/share') + repo_share = _path.normpath( + _path.join(_path.dirname(__file__), '..', '..', '..', 'share')) + setuptools_share = _path.normpath( + _path.join(_path.dirname(__file__), '..', '..', 'share')) + del _sys - share_solaar = [prefix_share] + list(_path.join(x, 'solaar') for x in [src_share, local_share, setuptools_share, repo_share] + data_dirs.split(':')) - for location in share_solaar: - location = _path.join(location, 'icons') - if _log.isEnabledFor(_DEBUG): - _log.debug("looking for icons in %s", location) + share_solaar = [prefix_share] + list( + _path.join(x, 'solaar') + for x in [src_share, local_share, setuptools_share, repo_share] + + data_dirs.split(':')) + for location in share_solaar: + location = _path.join(location, 'icons') + if _log.isEnabledFor(_DEBUG): + _log.debug("looking for icons in %s", location) - if _path.exists(_path.join(location, TRAY_ATTENTION + '.svg')): - yield location + if _path.exists(_path.join(location, TRAY_ATTENTION + '.svg')): + yield location - del _environ - # del _path + del _environ + # del _path _default_theme = None _use_symbolic_icons = False + def _init_icon_paths(): - global _default_theme - if _default_theme: - return + global _default_theme + if _default_theme: + return - _default_theme = Gtk.IconTheme.get_default() - for p in _look_for_application_icons(): - _default_theme.prepend_search_path(p) - if _log.isEnabledFor(_DEBUG): - _log.debug("icon theme paths: %s", _default_theme.get_search_path()) + _default_theme = Gtk.IconTheme.get_default() + for p in _look_for_application_icons(): + _default_theme.prepend_search_path(p) + if _log.isEnabledFor(_DEBUG): + _log.debug("icon theme paths: %s", _default_theme.get_search_path()) + + if gtk.prefer_symbolic_battery_icons: + if _default_theme.has_icon('battery-good-symbolic'): + global _use_symbolic_icons + _use_symbolic_icons = True + return + else: + _log.warning("failed to detect symbolic icons") + if not _default_theme.has_icon('battery-good'): + _log.warning("failed to detect icons") - if gtk.prefer_symbolic_battery_icons: - if _default_theme.has_icon('battery-good-symbolic'): - global _use_symbolic_icons - _use_symbolic_icons = True - return - else: - _log.warning("failed to detect symbolic icons") - if not _default_theme.has_icon('battery-good'): - _log.warning("failed to detect icons") # # # + def battery(level=None, charging=False): - icon_name = _battery_icon_name(level, charging) - if not _default_theme.has_icon(icon_name): - _log.warning("icon %s not found in current theme", icon_name) - return TRAY_OKAY # use Solaar icon if battery icon not available - elif _log.isEnabledFor(_DEBUG): - _log.debug("battery icon for %s:%s = %s", level, charging, icon_name) - return icon_name + icon_name = _battery_icon_name(level, charging) + if not _default_theme.has_icon(icon_name): + _log.warning("icon %s not found in current theme", icon_name) + return TRAY_OKAY # use Solaar icon if battery icon not available + elif _log.isEnabledFor(_DEBUG): + _log.debug("battery icon for %s:%s = %s", level, charging, icon_name) + return icon_name + # return first res where val >= guard # _first_res(val,((guard,res),...)) -def _first_res(val,pairs): - return next((res for guard,res in pairs if val >= guard),None) +def _first_res(val, pairs): + return next((res for guard, res in pairs if val >= guard), None) + def _battery_icon_name(level, charging): - _init_icon_paths() + _init_icon_paths() - if level is None or level < 0: - return 'battery-missing' + ( '-symbolic' if _use_symbolic_icons else '' ) + if level is None or level < 0: + return 'battery-missing' + ('-symbolic' if _use_symbolic_icons else '') + + level_name = _first_res(level, ((90, 'full'), (50, 'good'), (20, 'low'), + (5, 'caution'), (0, 'empty'))) + return 'battery-%s%s%s' % (level_name, '-charging' if charging else '', + '-symbolic' if _use_symbolic_icons else '') - level_name = _first_res(level,((90,'full'), (50,'good'), (20,'low'), (5,'caution'), (0,'empty'))) - return 'battery-%s%s%s' % (level_name, '-charging' if charging else '', '-symbolic' if _use_symbolic_icons else '') # # # + def lux(level=None): - if level is None or level < 0: - return 'light_unknown' - return 'light_%03d' % (20 * ((level + 50) // 100)) + if level is None or level < 0: + return 'light_unknown' + return 'light_%03d' % (20 * ((level + 50) // 100)) + # # @@ -139,65 +157,66 @@ def lux(level=None): _ICON_SETS = {} + def device_icon_set(name='_', kind=None): - icon_set = _ICON_SETS.get(name) - if icon_set is None: - icon_set = Gtk.IconSet.new() - _ICON_SETS[name] = icon_set + icon_set = _ICON_SETS.get(name) + if icon_set is None: + icon_set = Gtk.IconSet.new() + _ICON_SETS[name] = icon_set - # names of possible icons, in reverse order of likelihood - # the theme will hopefully pick up the most appropriate - 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',) - names += ('input-' + str(kind),) - # names += (name.replace(' ', '-'),) + # names of possible icons, in reverse order of likelihood + # the theme will hopefully pick up the most appropriate + 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', ) + names += ('input-' + str(kind), ) + # names += (name.replace(' ', '-'),) - source = Gtk.IconSource.new() - for n in names: - source.set_icon_name(n) - icon_set.add_source(source) - icon_set.names = names + source = Gtk.IconSource.new() + for n in names: + source.set_icon_name(n) + icon_set.add_source(source) + icon_set.names = names - return icon_set + return icon_set def device_icon_file(name, kind=None, size=_LARGE_SIZE): - _init_icon_paths() + _init_icon_paths() - icon_set = device_icon_set(name, kind) - assert icon_set - for n in reversed(icon_set.names): - if _default_theme.has_icon(n): - return _default_theme.lookup_icon(n, size, 0).get_filename() + icon_set = device_icon_set(name, kind) + assert icon_set + for n in reversed(icon_set.names): + if _default_theme.has_icon(n): + return _default_theme.lookup_icon(n, size, 0).get_filename() def device_icon_name(name, kind=None): - _init_icon_paths() + _init_icon_paths() - icon_set = device_icon_set(name, kind) - assert icon_set - for n in reversed(icon_set.names): - if _default_theme.has_icon(n): - return n + icon_set = device_icon_set(name, kind) + assert icon_set + for n in reversed(icon_set.names): + if _default_theme.has_icon(n): + return n def icon_file(name, size=_LARGE_SIZE): - _init_icon_paths() + _init_icon_paths() - # has_icon() somehow returned False while lookup_icon returns non-None. - # I guess it happens because share/solaar/icons/ has no hicolor and - # resolution subdirs - theme_icon = _default_theme.lookup_icon(name, size, 0) - if theme_icon: - file_name = theme_icon.get_filename() - # if _log.isEnabledFor(_DEBUG): - # _log.debug("icon %s(%d) => %s", name, size, file_name) - return file_name + # has_icon() somehow returned False while lookup_icon returns non-None. + # I guess it happens because share/solaar/icons/ has no hicolor and + # resolution subdirs + theme_icon = _default_theme.lookup_icon(name, size, 0) + if theme_icon: + file_name = theme_icon.get_filename() + # if _log.isEnabledFor(_DEBUG): + # _log.debug("icon %s(%d) => %s", name, size, file_name) + return file_name - _log.warn("icon %s(%d) not found in current theme", name, size) + _log.warn("icon %s(%d) not found in current theme", name, size) diff --git a/lib/solaar/ui/notify.py b/lib/solaar/ui/notify.py index ec6ee54d..14c411cd 100644 --- a/lib/solaar/ui/notify.py +++ b/lib/solaar/ui/notify.py @@ -21,7 +21,6 @@ from __future__ import absolute_import, division, print_function, unicode_literals - from solaar.i18n import _ # @@ -29,125 +28,121 @@ from solaar.i18n import _ # try: - import gi - gi.require_version('Notify', '0.7') - # this import is allowed to fail, in which case the entire feature is unavailable - from gi.repository import Notify, GLib + import gi + gi.require_version('Notify', '0.7') + # this import is allowed to fail, in which case the entire feature is unavailable + from gi.repository import Notify, GLib - # assumed to be working since the import succeeded - available = True + # assumed to be working since the import succeeded + available = True except (ValueError, ImportError): - available = False + available = False if available: - from logging import getLogger, INFO as _INFO - _log = getLogger(__name__) - del getLogger + from logging import getLogger, INFO as _INFO + _log = getLogger(__name__) + del getLogger - from solaar import NAME - from . import icons as _icons + from solaar import NAME + from . import icons as _icons - # cache references to shown notifications here, so if another status comes - # while its notification is still visible we don't create another one - _notifications = {} + # cache references to shown notifications here, so if another status comes + # while its notification is still visible we don't create another one + _notifications = {} - def init(): - """Init the notifications system.""" - global available - if available: - if not Notify.is_initted(): - if _log.isEnabledFor(_INFO): - _log.info("starting desktop notifications") - try: - return Notify.init(NAME) - except: - _log.exception("initializing desktop notifications") - available = False - return available and Notify.is_initted() + def init(): + """Init the notifications system.""" + global available + if available: + if not Notify.is_initted(): + if _log.isEnabledFor(_INFO): + _log.info("starting desktop notifications") + try: + return Notify.init(NAME) + except: + _log.exception("initializing desktop notifications") + available = False + return available and Notify.is_initted() + def uninit(): + if available and Notify.is_initted(): + if _log.isEnabledFor(_INFO): + _log.info("stopping desktop notifications") + _notifications.clear() + Notify.uninit() - def uninit(): - if available and Notify.is_initted(): - if _log.isEnabledFor(_INFO): - _log.info("stopping desktop notifications") - _notifications.clear() - Notify.uninit() + # def toggle(action): + # if action.get_active(): + # init() + # else: + # uninit() + # action.set_sensitive(available) + # return action.get_active() + def alert(reason, icon=None): + assert reason - # def toggle(action): - # if action.get_active(): - # init() - # else: - # uninit() - # action.set_sensitive(available) - # return action.get_active() + if available and Notify.is_initted(): + n = _notifications.get(NAME) + if n is None: + n = _notifications[NAME] = Notify.Notification() + # we need to use the filename here because the notifications daemon + # is an external application that does not know about our icon sets + icon_file = _icons.icon_file(NAME.lower()) if icon is None \ + else _icons.icon_file(icon) - def alert(reason, icon=None): - assert reason + n.update(NAME, reason, icon_file) + n.set_urgency(Notify.Urgency.NORMAL) + n.set_hint("desktop-entry", GLib.Variant('s', NAME.lower())) - if available and Notify.is_initted(): - n = _notifications.get(NAME) - if n is None: - n = _notifications[NAME] = Notify.Notification() + try: + # if _log.isEnabledFor(_DEBUG): + # _log.debug("showing %s", n) + n.show() + except Exception: + _log.exception("showing %s", n) - # we need to use the filename here because the notifications daemon - # is an external application that does not know about our icon sets - icon_file = _icons.icon_file(NAME.lower()) if icon is None \ - else _icons.icon_file(icon) + def show(dev, reason=None, icon=None): + """Show a notification with title and text.""" + if available and Notify.is_initted(): + summary = dev.name - n.update(NAME, reason, icon_file) - n.set_urgency(Notify.Urgency.NORMAL) - n.set_hint("desktop-entry", GLib.Variant('s', NAME.lower())) + # if a notification with same name is already visible, reuse it to avoid spamming + n = _notifications.get(summary) + if n is None: + n = _notifications[summary] = Notify.Notification() - try: - # if _log.isEnabledFor(_DEBUG): - # _log.debug("showing %s", n) - n.show() - except Exception: - _log.exception("showing %s", n) + if reason: + message = reason + elif dev.status is None: + message = _("unpaired") + elif bool(dev.status): + message = dev.status.to_string() or _("connected") + else: + 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 + icon_file = _icons.device_icon_file(dev.name, dev.kind) if icon is None \ + else _icons.icon_file(icon) - def show(dev, reason=None, icon=None): - """Show a notification with title and text.""" - if available and Notify.is_initted(): - summary = dev.name + 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())) - # if a notification with same name is already visible, reuse it to avoid spamming - n = _notifications.get(summary) - if n is None: - n = _notifications[summary] = Notify.Notification() - - if reason: - message = reason - elif dev.status is None: - message = _("unpaired") - elif bool(dev.status): - message = dev.status.to_string() or _("connected") - else: - 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 - icon_file = _icons.device_icon_file(dev.name, dev.kind) if icon is None \ - else _icons.icon_file(icon) - - 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())) - - try: - # if _log.isEnabledFor(_DEBUG): - # _log.debug("showing %s", n) - n.show() - except Exception: - _log.exception("showing %s", n) + try: + # if _log.isEnabledFor(_DEBUG): + # _log.debug("showing %s", n) + n.show() + except Exception: + _log.exception("showing %s", n) else: - init = lambda: False - uninit = lambda: None - # toggle = lambda action: False - alert = lambda reason: None - show = lambda dev, reason=None: None + init = lambda: False + uninit = lambda: None + # toggle = lambda action: False + alert = lambda reason: None + show = lambda dev, reason=None: None diff --git a/lib/solaar/ui/pair_window.py b/lib/solaar/ui/pair_window.py index b871228a..8deef87a 100644 --- a/lib/solaar/ui/pair_window.py +++ b/lib/solaar/ui/pair_window.py @@ -25,7 +25,6 @@ from logging import getLogger, DEBUG as _DEBUG _log = getLogger(__name__) del getLogger - from solaar.i18n import _ from . import icons as _icons from logitech_receiver.status import KEYS as _K @@ -38,183 +37,201 @@ _STATUS_CHECK = 500 # milliseconds def _create_page(assistant, kind, header=None, icon_name=None, text=None): - p = Gtk.VBox(False, 8) - assistant.append_page(p) - assistant.set_page_type(p, kind) + p = Gtk.VBox(False, 8) + assistant.append_page(p) + assistant.set_page_type(p, kind) - if header: - item = Gtk.HBox(False, 16) - p.pack_start(item, False, True, 0) + if header: + item = Gtk.HBox(False, 16) + p.pack_start(item, False, True, 0) - label = Gtk.Label(header) - label.set_alignment(0, 0) - label.set_line_wrap(True) - item.pack_start(label, True, True, 0) + label = Gtk.Label(header) + label.set_alignment(0, 0) + label.set_line_wrap(True) + item.pack_start(label, True, True, 0) - if icon_name: - icon = Gtk.Image.new_from_icon_name(icon_name, Gtk.IconSize.DIALOG) - icon.set_alignment(1, 0) - item.pack_start(icon, False, False, 0) + if icon_name: + icon = Gtk.Image.new_from_icon_name(icon_name, Gtk.IconSize.DIALOG) + icon.set_alignment(1, 0) + item.pack_start(icon, False, False, 0) - if text: - label = Gtk.Label(text) - label.set_alignment(0, 0) - label.set_line_wrap(True) - p.pack_start(label, False, False, 0) + if text: + label = Gtk.Label(text) + label.set_alignment(0, 0) + label.set_line_wrap(True) + p.pack_start(label, False, False, 0) - p.show_all() - return p + p.show_all() + return p def _check_lock_state(assistant, receiver, count=2): - if not assistant.is_drawable(): - if _log.isEnabledFor(_DEBUG): - _log.debug("assistant %s destroyed, bailing out", assistant) - return False + if not assistant.is_drawable(): + if _log.isEnabledFor(_DEBUG): + _log.debug("assistant %s destroyed, bailing out", assistant) + return False - if receiver.status.get(_K.ERROR): - # receiver.status.new_device = _fake_device(receiver) - _pairing_failed(assistant, receiver, receiver.status.pop(_K.ERROR)) - return False + if receiver.status.get(_K.ERROR): + # receiver.status.new_device = _fake_device(receiver) + _pairing_failed(assistant, receiver, receiver.status.pop(_K.ERROR)) + return False - if receiver.status.new_device: - device, receiver.status.new_device = receiver.status.new_device, None - _pairing_succeeded(assistant, receiver, device) - return False + if receiver.status.new_device: + device, receiver.status.new_device = receiver.status.new_device, None + _pairing_succeeded(assistant, receiver, device) + return False - if not receiver.status.lock_open: - if count > 0: - # the actual device notification may arrive after the lock was paired, - # 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') - return False + if not receiver.status.lock_open: + if count > 0: + # the actual device notification may arrive after the lock was paired, + # 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') + return False - return True + return True def _prepare(assistant, page, receiver): - index = assistant.get_current_page() - if _log.isEnabledFor(_DEBUG): - _log.debug("prepare %s %d %s", assistant, index, page) + index = assistant.get_current_page() + if _log.isEnabledFor(_DEBUG): + _log.debug("prepare %s %d %s", assistant, index, page) - if index == 0: - if receiver.set_lock(False, timeout=_PAIRING_TIMEOUT): - assert receiver.status.new_device is None - assert receiver.status.get(_K.ERROR) is None - spinner = page.get_children()[-1] - spinner.start() - 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') - else: - assistant.remove_page(0) + if index == 0: + if receiver.set_lock(False, timeout=_PAIRING_TIMEOUT): + assert receiver.status.new_device is None + assert receiver.status.get(_K.ERROR) is None + spinner = page.get_children()[-1] + spinner.start() + 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') + else: + assistant.remove_page(0) def _finish(assistant, receiver): - if _log.isEnabledFor(_DEBUG): - _log.debug("finish %s", assistant) - assistant.destroy() - receiver.status.new_device = None - if receiver.status.lock_open: - receiver.set_lock() - else: - receiver.status[_K.ERROR] = None + if _log.isEnabledFor(_DEBUG): + _log.debug("finish %s", assistant) + assistant.destroy() + receiver.status.new_device = None + if receiver.status.lock_open: + receiver.set_lock() + else: + receiver.status[_K.ERROR] = None def _pairing_failed(assistant, receiver, error): - if _log.isEnabledFor(_DEBUG): - _log.debug("%s fail: %s", receiver, error) + if _log.isEnabledFor(_DEBUG): + _log.debug("%s fail: %s", receiver, error) - assistant.commit() + 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 = _("The receiver only supports %d paired device(s).") - else: - text = _("No further details are available about the error.") - _create_page(assistant, Gtk.AssistantPageType.SUMMARY, header, 'dialog-error', text) + 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 = _("The receiver only supports %d paired device(s).") + else: + text = _("No further details are available about the error.") + _create_page(assistant, Gtk.AssistantPageType.SUMMARY, header, + 'dialog-error', text) - assistant.next_page() - assistant.commit() + assistant.next_page() + assistant.commit() def _pairing_succeeded(assistant, receiver, device): - assert device - if _log.isEnabledFor(_DEBUG): - _log.debug("%s success: %s", receiver, device) + assert device + if _log.isEnabledFor(_DEBUG): + _log.debug("%s success: %s", receiver, device) - page = _create_page(assistant, Gtk.AssistantPageType.SUMMARY) + page = _create_page(assistant, Gtk.AssistantPageType.SUMMARY) - header = Gtk.Label(_("Found a new device:")) - header.set_alignment(0.5, 0) - page.pack_start(header, False, False, 0) + header = Gtk.Label(_("Found a new device:")) + header.set_alignment(0.5, 0) + page.pack_start(header, False, False, 0) - device_icon = Gtk.Image() - icon_set = _icons.device_icon_set(device.name, device.kind) - device_icon.set_from_icon_set(icon_set, Gtk.IconSize.LARGE) - device_icon.set_alignment(0.5, 1) - page.pack_start(device_icon, True, True, 0) + device_icon = Gtk.Image() + icon_set = _icons.device_icon_set(device.name, device.kind) + device_icon.set_from_icon_set(icon_set, Gtk.IconSize.LARGE) + device_icon.set_alignment(0.5, 1) + page.pack_start(device_icon, True, True, 0) - device_label = Gtk.Label() - device_label.set_markup('%s' % device.name) - device_label.set_alignment(0.5, 0) - page.pack_start(device_label, True, True, 0) + device_label = Gtk.Label() + device_label.set_markup('%s' % 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) - page.pack_start(hbox, False, False, 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) + page.pack_start(hbox, False, False, 0) - def _check_encrypted(dev): - if assistant.is_drawable(): - if device.status.get(_K.LINK_ENCRYPTED) == 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.show_all() - else: - return True - GLib.timeout_add(_STATUS_CHECK, _check_encrypted, device) + def _check_encrypted(dev): + if assistant.is_drawable(): + if device.status.get(_K.LINK_ENCRYPTED) == 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.show_all() + else: + return True - page.show_all() + GLib.timeout_add(_STATUS_CHECK, _check_encrypted, device) - assistant.next_page() - assistant.commit() + page.show_all() + + assistant.next_page() + assistant.commit() def create(receiver): - assert receiver is not None - assert receiver.kind is None + assert receiver is not None + assert receiver.kind is None - assistant = Gtk.Assistant() - assistant.set_title(_('%(receiver_name)s: pair new device') % { 'receiver_name': receiver.name }) - assistant.set_icon_name('list-add') + assistant = Gtk.Assistant() + 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_size_request(400, 240) + assistant.set_resizable(False) + assistant.set_role('pair-device') - page_text = _("If the device is already turned on, turn if off and on again.") - if receiver.remaining_pairings() and receiver.remaining_pairings() >= 0: - page_text += _("\n\nThis receiver has %d pairing(s) remaining.")%receiver.remaining_pairings() - page_text += _("\nCancelling at this point will not use up a pairing.") + page_text = _( + "If the device is already turned on, turn if off and on again.") + if receiver.remaining_pairings() and receiver.remaining_pairings() >= 0: + page_text += _("\n\nThis receiver has %d pairing(s) remaining." + ) % receiver.remaining_pairings() + page_text += _("\nCancelling at this point will not use up a pairing.") - page_intro = _create_page(assistant, Gtk.AssistantPageType.PROGRESS, - _("Turn on the device you want to pair."), 'preferences-desktop-peripherals', - page_text) - spinner = Gtk.Spinner() - spinner.set_visible(True) - page_intro.pack_end(spinner, True, True, 24) + page_intro = _create_page(assistant, Gtk.AssistantPageType.PROGRESS, + _("Turn on the device you want to pair."), + '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 + return assistant diff --git a/lib/solaar/ui/tray.py b/lib/solaar/ui/tray.py index 02d791aa..3556fafb 100644 --- a/lib/solaar/ui/tray.py +++ b/lib/solaar/ui/tray.py @@ -29,7 +29,6 @@ from time import time as _timestamp from gi.repository import Gtk, GLib from gi.repository.Gdk import ScrollDirection - from solaar import NAME from solaar.i18n import _ from logitech_receiver.status import KEYS as _K @@ -40,7 +39,7 @@ from .window import popup as _window_popup, toggle as _window_toggle # constants # -_TRAY_ICON_SIZE = 32 # pixels +_TRAY_ICON_SIZE = 32 # pixels _MENU_ICON_SIZE = Gtk.IconSize.LARGE_TOOLBAR _RECEIVER_SEPARATOR = ('~', None, None, None) @@ -48,418 +47,429 @@ _RECEIVER_SEPARATOR = ('~', None, None, None) # # + def _create_menu(quit_handler): - menu = Gtk.Menu() + menu = Gtk.Menu() - # per-device menu entries will be generated as-needed + # per-device menu entries will be generated as-needed - no_receiver = Gtk.MenuItem.new_with_label(_("No Logitech receiver found")) - no_receiver.set_sensitive(False) - menu.append(no_receiver) - menu.append(Gtk.SeparatorMenuItem.new()) + no_receiver = Gtk.MenuItem.new_with_label(_("No Logitech receiver found")) + no_receiver.set_sensitive(False) + menu.append(no_receiver) + menu.append(Gtk.SeparatorMenuItem.new()) - from .action import about, make - menu.append(about.create_menu_item()) - menu.append(make('application-exit', _("Quit"), quit_handler, stock_id=Gtk.STOCK_QUIT).create_menu_item()) - del about, make + from .action import about, make + menu.append(about.create_menu_item()) + menu.append( + make('application-exit', + _("Quit"), + quit_handler, + stock_id=Gtk.STOCK_QUIT).create_menu_item()) + del about, make - menu.show_all() + menu.show_all() - return menu + return menu _last_scroll = 0 + + def _scroll(tray_icon, event, direction=None): - if direction is None: - direction = event.direction - now = event.time / 1000.0 - else: - now = None + if direction is None: + direction = event.direction + now = event.time / 1000.0 + else: + now = None - if direction != ScrollDirection.UP and direction != ScrollDirection.DOWN: - # ignore all other directions - return + if direction != ScrollDirection.UP and direction != ScrollDirection.DOWN: + # ignore all other directions + return - if len(_devices_info) < 4: - # don't bother with scrolling when there's only one receiver - # with only one device (3 = [receiver, device, separator]) - return + if len(_devices_info) < 4: + # don't bother with scrolling when there's only one receiver + # with only one device (3 = [receiver, device, separator]) + return - # scroll events come way too fast (at least 5-6 at once) - # so take a little break between them - global _last_scroll - now = now or _timestamp() - if now - _last_scroll < 0.33: # seconds - return - _last_scroll = now + # scroll events come way too fast (at least 5-6 at once) + # so take a little break between them + global _last_scroll + now = now or _timestamp() + if now - _last_scroll < 0.33: # seconds + return + _last_scroll = now - # if _log.isEnabledFor(_DEBUG): - # _log.debug("scroll direction %s", direction) + # if _log.isEnabledFor(_DEBUG): + # _log.debug("scroll direction %s", direction) - global _picked_device - candidate = None + global _picked_device + candidate = None - if _picked_device is None: - for info in _devices_info: - # pick first peripheral found - if info[1] is not None: - candidate = info - break - else: - found = False - for info in _devices_info: - if not info[1]: - # only conside peripherals - continue - # compare peripherals - if info[0:2] == _picked_device[0:2]: - if direction == ScrollDirection.UP and candidate: - # select previous device - break - found = True - else: - if found: - candidate = info - if direction == ScrollDirection.DOWN: - break - # if direction is up, but no candidate found before _picked, - # let it run through all candidates, will get stuck with the last one - else: - if direction == ScrollDirection.DOWN: - # only use the first one, in case no candidates are after _picked - if candidate is None: - candidate = info - else: - candidate = info + if _picked_device is None: + for info in _devices_info: + # pick first peripheral found + if info[1] is not None: + candidate = info + break + else: + found = False + for info in _devices_info: + if not info[1]: + # only conside peripherals + continue + # compare peripherals + if info[0:2] == _picked_device[0:2]: + if direction == ScrollDirection.UP and candidate: + # select previous device + break + found = True + else: + if found: + candidate = info + if direction == ScrollDirection.DOWN: + break + # if direction is up, but no candidate found before _picked, + # let it run through all candidates, will get stuck with the last one + else: + if direction == ScrollDirection.DOWN: + # only use the first one, in case no candidates are after _picked + if candidate is None: + candidate = info + else: + candidate = info - # if the last _picked_device is gone, clear it - # the candidate will be either the first or last one remaining, - # depending on the scroll direction - if not found: - _picked_device = None + # if the last _picked_device is gone, clear it + # the candidate will be either the first or last one remaining, + # depending on the scroll direction + if not found: + _picked_device = None - _picked_device = candidate or _picked_device - if _log.isEnabledFor(_DEBUG): - _log.debug("scroll: picked %s", _picked_device) - _update_tray_icon() + _picked_device = candidate or _picked_device + if _log.isEnabledFor(_DEBUG): + _log.debug("scroll: picked %s", _picked_device) + _update_tray_icon() try: - import gi - try: - gi.require_version('AyatanaAppIndicator3', '0.1') - ayatana_appindicator_found = True - except ValueError: - try: - gi.require_version('AppIndicator3', '0.1') - ayatana_appindicator_found = False - except ValueError: - # treat unavailable versions the same as unavailable packages - raise ImportError + import gi + try: + gi.require_version('AyatanaAppIndicator3', '0.1') + ayatana_appindicator_found = True + except ValueError: + try: + gi.require_version('AppIndicator3', '0.1') + ayatana_appindicator_found = False + except ValueError: + # treat unavailable versions the same as unavailable packages + raise ImportError - if ayatana_appindicator_found: - from gi.repository import AyatanaAppIndicator3 as AppIndicator3 - else: - from gi.repository import AppIndicator3 + if ayatana_appindicator_found: + from gi.repository import AyatanaAppIndicator3 as AppIndicator3 + else: + from gi.repository import AppIndicator3 - if _log.isEnabledFor(_DEBUG): - _log.debug("using %sAppIndicator3" % ('Ayatana ' if ayatana_appindicator_found else '')) + if _log.isEnabledFor(_DEBUG): + _log.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 - def _icon_file(icon_name): - if not os.path.isfile(icon_name): - return icon_name - icon_info = Gtk.IconTheme.get_default().lookup_icon(icon_name,_TRAY_ICON_SIZE,0) - return icon_info.get_filename() if icon_info else icon_name + # Defense against AppIndicator3 bug that treats files in current directory as icon files + # https://bugs.launchpad.net/ubuntu/+source/libappindicator/+bug/1363277 + def _icon_file(icon_name): + if not os.path.isfile(icon_name): + return icon_name + icon_info = Gtk.IconTheme.get_default().lookup_icon( + icon_name, _TRAY_ICON_SIZE, 0) + return icon_info.get_filename() if icon_info else icon_name - def _create(menu): - theme_paths = Gtk.IconTheme.get_default().get_search_path() + def _create(menu): + theme_paths = Gtk.IconTheme.get_default().get_search_path() - ind = AppIndicator3.Indicator.new_with_path( - 'indicator-solaar', - _icon_file(_icons.TRAY_INIT), - AppIndicator3.IndicatorCategory.HARDWARE, - ':'.join(theme_paths)) - ind.set_title(NAME) - ind.set_status(AppIndicator3.IndicatorStatus.ACTIVE) - ind.set_attention_icon_full(_icon_file(_icons.TRAY_ATTENTION), '') - # ind.set_label(NAME, NAME) + ind = AppIndicator3.Indicator.new_with_path( + 'indicator-solaar', _icon_file(_icons.TRAY_INIT), + AppIndicator3.IndicatorCategory.HARDWARE, ':'.join(theme_paths)) + ind.set_title(NAME) + ind.set_status(AppIndicator3.IndicatorStatus.ACTIVE) + ind.set_attention_icon_full(_icon_file(_icons.TRAY_ATTENTION), '') + # ind.set_label(NAME, NAME) - ind.set_menu(menu) - ind.connect('scroll-event', _scroll) + ind.set_menu(menu) + ind.connect('scroll-event', _scroll) - return ind + return ind + def _destroy(indicator): + indicator.set_status(AppIndicator3.IndicatorStatus.PASSIVE) - def _destroy(indicator): - indicator.set_status(AppIndicator3.IndicatorStatus.PASSIVE) + def _update_tray_icon(): + if _picked_device: + _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()) + else: + # there may be a receiver, but no peripherals + tray_icon_name = _icons.TRAY_OKAY if _devices_info else _icons.TRAY_INIT - def _update_tray_icon(): - if _picked_device: - _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_lines = _generate_description_lines() + description = '\n'.join(description_lines).rstrip('\n') - 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 + # icon_file = _icons.icon_file(icon_name, _TRAY_ICON_SIZE) + _icon.set_icon_full(_icon_file(tray_icon_name), description) - description_lines = _generate_description_lines() - description = '\n'.join(description_lines).rstrip('\n') + def _update_menu_icon(image_widget, icon_name): + image_widget.set_from_icon_name(icon_name, _MENU_ICON_SIZE) + # icon_file = _icons.icon_file(icon_name, _MENU_ICON_SIZE) + # image_widget.set_from_file(icon_file) + # image_widget.set_pixel_size(_TRAY_ICON_SIZE) - # icon_file = _icons.icon_file(icon_name, _TRAY_ICON_SIZE) - _icon.set_icon_full(_icon_file(tray_icon_name), description) - - - def _update_menu_icon(image_widget, icon_name): - image_widget.set_from_icon_name(icon_name, _MENU_ICON_SIZE) - # icon_file = _icons.icon_file(icon_name, _MENU_ICON_SIZE) - # image_widget.set_from_file(icon_file) - # image_widget.set_pixel_size(_TRAY_ICON_SIZE) - - - def attention(reason=None): - if _icon.get_status() != AppIndicator3.IndicatorStatus.ATTENTION: - _icon.set_attention_icon_full(_icon_file(_icons.TRAY_ATTENTION), reason or '') - _icon.set_status(AppIndicator3.IndicatorStatus.ATTENTION) - GLib.timeout_add(10 * 1000, _icon.set_status, AppIndicator3.IndicatorStatus.ACTIVE) + def attention(reason=None): + if _icon.get_status() != AppIndicator3.IndicatorStatus.ATTENTION: + _icon.set_attention_icon_full(_icon_file(_icons.TRAY_ATTENTION), + reason or '') + _icon.set_status(AppIndicator3.IndicatorStatus.ATTENTION) + GLib.timeout_add(10 * 1000, _icon.set_status, + AppIndicator3.IndicatorStatus.ACTIVE) except ImportError: - if _log.isEnabledFor(_DEBUG): - _log.debug("using StatusIcon") + if _log.isEnabledFor(_DEBUG): + _log.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)) + 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)) - return icon + return icon + def _destroy(icon): + icon.set_visible(False) - def _destroy(icon): - icon.set_visible(False) + def _update_tray_icon(): + tooltip_lines = _generate_tooltip_lines() + tooltip = '\n'.join(tooltip_lines).rstrip('\n') + _icon.set_tooltip_markup(tooltip) + if _picked_device: + _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) + else: + # there may be a receiver, but no peripherals + tray_icon_name = _icons.TRAY_OKAY if _devices_info else _icons.TRAY_ATTENTION + _icon.set_from_icon_name(tray_icon_name) - def _update_tray_icon(): - tooltip_lines = _generate_tooltip_lines() - tooltip = '\n'.join(tooltip_lines).rstrip('\n') - _icon.set_tooltip_markup(tooltip) + def _update_menu_icon(image_widget, icon_name): + image_widget.set_from_icon_name(icon_name, _MENU_ICON_SIZE) - if _picked_device: - _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) - else: - # there may be a receiver, but no peripherals - tray_icon_name = _icons.TRAY_OKAY if _devices_info else _icons.TRAY_ATTENTION - _icon.set_from_icon_name(tray_icon_name) + _icon_before_attention = None + def _blink(count): + global _icon_before_attention + if count % 2: + _icon.set_from_icon_name(_icons.TRAY_ATTENTION) + else: + _icon.set_from_icon_name(_icon_before_attention) - def _update_menu_icon(image_widget, icon_name): - image_widget.set_from_icon_name(icon_name, _MENU_ICON_SIZE) + if count > 0: + GLib.timeout_add(1000, _blink, count - 1) + else: + _icon_before_attention = None + def attention(reason=None): + global _icon_before_attention + if _icon_before_attention is None: + _icon_before_attention = _icon.get_icon_name() + GLib.idle_add(_blink, 9) - _icon_before_attention = None - - def _blink(count): - global _icon_before_attention - if count % 2: - _icon.set_from_icon_name(_icons.TRAY_ATTENTION) - else: - _icon.set_from_icon_name(_icon_before_attention) - - if count > 0: - GLib.timeout_add(1000, _blink, count - 1) - else: - _icon_before_attention = None - - def attention(reason=None): - global _icon_before_attention - if _icon_before_attention is None: - _icon_before_attention = _icon.get_icon_name() - GLib.idle_add(_blink, 9) # # # + def _generate_tooltip_lines(): - if not _devices_info: - yield '%s: ' % NAME + _("no receiver") - return + if not _devices_info: + yield '%s: ' % NAME + _("no receiver") + return - yield from _generate_description_lines() + yield from _generate_description_lines() def _generate_description_lines(): - if not _devices_info: - yield _("no receiver") - return + if not _devices_info: + yield _("no receiver") + return - for _ignore, number, name, status in _devices_info: - if number is None: # receiver - continue + for _ignore, number, name, status in _devices_info: + if number is None: # receiver + continue - p = status.to_string() - if p: # does it have any properties to print? - yield '%s' % name - if status: - yield '\t%s' % p - else: - yield '\t%s (' % p + _("offline") + ')' - else: - if status: - yield '%s (' % name + _("no status") + ')' - else: - yield '%s (' % name + _("offline") + ')' - yield '' + p = status.to_string() + if p: # does it have any properties to print? + yield '%s' % name + if status: + yield '\t%s' % p + else: + yield '\t%s (' % p + _("offline") + ')' + else: + if status: + yield '%s (' % name + _( + "no status") + ')' + else: + yield '%s (' % name + _("offline") + ')' + yield '' def _pick_device_with_lowest_battery(): - if not _devices_info: - return None + if not _devices_info: + return None - picked = None - picked_level = 1000 + picked = None + picked_level = 1000 - for info in _devices_info: - if info[1] is None: # is receiver/separator - continue - level = info[-1].get(_K.BATTERY_LEVEL) - # print ("checking %s -> %s", info, level) - if level is not None and picked_level > level: - picked = info - picked_level = level or 0 + for info in _devices_info: + if info[1] is None: # is receiver/separator + continue + level = info[-1].get(_K.BATTERY_LEVEL) + # print ("checking %s -> %s", info, level) + if level is not None and picked_level > level: + picked = info + picked_level = level or 0 - if _log.isEnabledFor(_DEBUG): - _log.debug("picked device with lowest battery: %s", picked) + if _log.isEnabledFor(_DEBUG): + _log.debug("picked device with lowest battery: %s", picked) - return picked + return picked # # # + def _add_device(device): - assert device - assert device.receiver - receiver_path = device.receiver.path - assert receiver_path + assert device + assert device.receiver + receiver_path = device.receiver.path + assert receiver_path - index = None - for idx, (path, _ignore, _ignore, _ignore) in enumerate(_devices_info): - if path == receiver_path: - # the first entry matching the receiver serial should be for the receiver itself - index = idx + 1 - break - assert index is not None + index = None + for idx, (path, _ignore, _ignore, _ignore) in enumerate(_devices_info): + if path == receiver_path: + # the first entry matching the receiver serial should be for the receiver itself + index = idx + 1 + break + assert index is not None - # proper ordering (according to device.number) for a receiver's devices - while True: - path, number, _ignore, _ignore = _devices_info[index] - if path == _RECEIVER_SEPARATOR[0]: - break - assert path == receiver_path - assert number != device.number - if number > device.number: - break - index = index + 1 + # proper ordering (according to device.number) for a receiver's devices + while True: + path, number, _ignore, _ignore = _devices_info[index] + if path == _RECEIVER_SEPARATOR[0]: + break + assert path == receiver_path + assert number != device.number + if number > device.number: + break + index = index + 1 - new_device_info = (receiver_path, device.number, device.name, device.status) - assert len(new_device_info) == len(_RECEIVER_SEPARATOR) - _devices_info.insert(index, new_device_info) + new_device_info = (receiver_path, device.number, device.name, + device.status) + assert len(new_device_info) == len(_RECEIVER_SEPARATOR) + _devices_info.insert(index, new_device_info) - # label_prefix = b'\xE2\x94\x84 '.decode('utf-8') - label_prefix = ' ' + # label_prefix = b'\xE2\x94\x84 '.decode('utf-8') + label_prefix = ' ' - new_menu_item = Gtk.ImageMenuItem.new_with_label(label_prefix + 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) - _menu.insert(new_menu_item, index) + new_menu_item = Gtk.ImageMenuItem.new_with_label(label_prefix + + 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) + _menu.insert(new_menu_item, index) - return index + return index def _remove_device(index): - assert index is not None + assert index is not None - menu_items = _menu.get_children() - _menu.remove(menu_items[index]) + menu_items = _menu.get_children() + _menu.remove(menu_items[index]) - removed_device = _devices_info.pop(index) - global _picked_device - if _picked_device and _picked_device[0:2] == removed_device[0:2]: - # the current pick was unpaired - _picked_device = None + removed_device = _devices_info.pop(index) + global _picked_device + if _picked_device and _picked_device[0:2] == removed_device[0:2]: + # the current pick was unpaired + _picked_device = None def _add_receiver(receiver): - index = len(_devices_info) + index = len(_devices_info) - new_receiver_info = (receiver.path, None, receiver.name, None) - assert len(new_receiver_info) == len(_RECEIVER_SEPARATOR) - _devices_info.append(new_receiver_info) + new_receiver_info = (receiver.path, None, receiver.name, None) + assert len(new_receiver_info) == len(_RECEIVER_SEPARATOR) + _devices_info.append(new_receiver_info) - new_menu_item = Gtk.ImageMenuItem.new_with_label(receiver.name) - _menu.insert(new_menu_item, index) - icon_set = _icons.device_icon_set(receiver.name) - new_menu_item.set_image(Gtk.Image().new_from_icon_set(icon_set, _MENU_ICON_SIZE)) - new_menu_item.show_all() - new_menu_item.connect('activate', _window_popup, receiver.path) + new_menu_item = Gtk.ImageMenuItem.new_with_label(receiver.name) + _menu.insert(new_menu_item, index) + icon_set = _icons.device_icon_set(receiver.name) + new_menu_item.set_image(Gtk.Image().new_from_icon_set( + icon_set, _MENU_ICON_SIZE)) + new_menu_item.show_all() + new_menu_item.connect('activate', _window_popup, receiver.path) - _devices_info.append(_RECEIVER_SEPARATOR) - separator = Gtk.SeparatorMenuItem.new() - separator.set_visible(True) - _menu.insert(separator, index + 1) + _devices_info.append(_RECEIVER_SEPARATOR) + separator = Gtk.SeparatorMenuItem.new() + separator.set_visible(True) + _menu.insert(separator, index + 1) - return 0 + return 0 def _remove_receiver(receiver): - index = 0 - found = False + index = 0 + found = False - # remove all entries in devices_info that match this receiver - while index < len(_devices_info): - path, _ignore, _ignore, _ignore = _devices_info[index] - if path == receiver.path: - found = True - _remove_device(index) - elif found and path == _RECEIVER_SEPARATOR[0]: - # the separator after this receiver - _remove_device(index) - break - else: - index += 1 + # remove all entries in devices_info that match this receiver + while index < len(_devices_info): + path, _ignore, _ignore, _ignore = _devices_info[index] + if path == receiver.path: + found = True + _remove_device(index) + elif found and path == _RECEIVER_SEPARATOR[0]: + # the separator after this receiver + _remove_device(index) + break + else: + index += 1 def _update_menu_item(index, device): - assert device - assert device.status is not None + assert device + assert device.status is not None - menu_items = _menu.get_children() - menu_item = menu_items[index] + menu_items = _menu.get_children() + menu_item = menu_items[index] - level = device.status.get(_K.BATTERY_LEVEL) - charging = device.status.get(_K.BATTERY_CHARGING) - icon_name = _icons.battery(level, charging) + level = device.status.get(_K.BATTERY_LEVEL) + charging = device.status.get(_K.BATTERY_CHARGING) + icon_name = _icons.battery(level, charging) + + image_widget = menu_item.get_image() + image_widget.set_sensitive(bool(device.online)) + _update_menu_icon(image_widget, icon_name) - image_widget = menu_item.get_image() - image_widget.set_sensitive(bool(device.online)) - _update_menu_icon(image_widget, icon_name) # # @@ -476,73 +486,77 @@ _devices_info = [] _menu = None _icon = None + def init(_quit_handler): - global _menu, _icon - assert _menu is None - _menu = _create_menu(_quit_handler) - assert _icon is None - _icon = _create(_menu) + global _menu, _icon + assert _menu is None + _menu = _create_menu(_quit_handler) + assert _icon is None + _icon = _create(_menu) def destroy(): - global _icon, _menu, _devices_info - if _icon is not None: - i, _icon = _icon, None - _destroy(i) - i = None + global _icon, _menu, _devices_info + if _icon is not None: + i, _icon = _icon, None + _destroy(i) + i = None - _icon = None - _menu = None - _devices_info = None + _icon = None + _menu = None + _devices_info = None def update(device=None): - if _icon is None: - return + if _icon is None: + return - if device is not None: - if device.kind is None: - # receiver - is_alive = bool(device) - receiver_path = device.path - if is_alive: - index = None - for idx, (path, _ignore, _ignore, _ignore) in enumerate(_devices_info): - if path == receiver_path: - index = idx - break + if device is not None: + if device.kind is None: + # receiver + is_alive = bool(device) + receiver_path = device.path + if is_alive: + index = None + for idx, (path, _ignore, _ignore, + _ignore) in enumerate(_devices_info): + if path == receiver_path: + index = idx + break - if index is None: - _add_receiver(device) - else: - _remove_receiver(device) + if index is None: + _add_receiver(device) + else: + _remove_receiver(device) - else: - # peripheral - is_paired = bool(device) - receiver_path = device.receiver.path - index = None - for idx, (path, number, _ignore, _ignore) in enumerate(_devices_info): - if path == receiver_path and number == device.number: - index = idx + else: + # peripheral + is_paired = bool(device) + receiver_path = device.receiver.path + index = None + for idx, (path, number, _ignore, + _ignore) in enumerate(_devices_info): + if path == receiver_path and number == device.number: + index = idx - if is_paired: - if index is None: - index = _add_device(device) - _update_menu_item(index, device) - else: - # was just unpaired - if index: - _remove_device(index) + if is_paired: + if index is None: + index = _add_device(device) + _update_menu_item(index, device) + else: + # was just unpaired + if index: + _remove_device(index) - menu_items = _menu.get_children() - no_receivers_index = len(_devices_info) - menu_items[no_receivers_index].set_visible(not _devices_info) - menu_items[no_receivers_index + 1].set_visible(not _devices_info) + menu_items = _menu.get_children() + no_receivers_index = len(_devices_info) + menu_items[no_receivers_index].set_visible(not _devices_info) + menu_items[no_receivers_index + 1].set_visible(not _devices_info) - global _picked_device - if (not _picked_device or _last_scroll == 0) and device is not None and device.kind is not None: - # if it's just a receiver update, it's unlikely the picked device would change - _picked_device = _pick_device_with_lowest_battery() + global _picked_device + if (not _picked_device or _last_scroll + == 0) and device is not None and device.kind is not None: + # if it's just a receiver update, it's unlikely the picked device would change + _picked_device = _pick_device_with_lowest_battery() - _update_tray_icon() + _update_tray_icon() diff --git a/lib/solaar/ui/window.py b/lib/solaar/ui/window.py index 643bc0db..d94b1ee9 100644 --- a/lib/solaar/ui/window.py +++ b/lib/solaar/ui/window.py @@ -54,7 +54,14 @@ except (ValueError, AttributeError): _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) +_COLUMN = _NamedInts(PATH=0, + NUMBER=1, + ACTIVE=2, + NAME=3, + ICON=4, + STATUS_TEXT=5, + STATUS_ICON=6, + DEVICE=7) _COLUMN_TYPES = (str, int, bool, str, str, str, str, TYPE_PYOBJECT) _TREE_SEPATATOR = (None, 0, False, None, None, None, None, None) assert len(_TREE_SEPATATOR) == len(_COLUMN_TYPES) @@ -64,667 +71,741 @@ assert len(_COLUMN_TYPES) == len(_COLUMN) # create UI layout # -def _new_button(label, icon_name=None, icon_size=_NORMAL_BUTTON_ICON_SIZE, tooltip=None, toggle=False, clicked=None): - if toggle: - b = Gtk.ToggleButton() - else: - b = Gtk.Button(label) if label else Gtk.Button() - if icon_name: - image = Gtk.Image.new_from_icon_name(icon_name, icon_size) - b.set_image(image) +def _new_button(label, + icon_name=None, + icon_size=_NORMAL_BUTTON_ICON_SIZE, + tooltip=None, + toggle=False, + clicked=None): + if toggle: + b = Gtk.ToggleButton() + else: + b = Gtk.Button(label) if label else Gtk.Button() - if tooltip: - b.set_tooltip_text(tooltip) + if icon_name: + image = Gtk.Image.new_from_icon_name(icon_name, icon_size) + b.set_image(image) - if not label and icon_size < _NORMAL_BUTTON_ICON_SIZE: - b.set_relief(Gtk.ReliefStyle.NONE) - b.set_focus_on_click(False) + if tooltip: + b.set_tooltip_text(tooltip) - if clicked is not None: - b.connect('clicked', clicked) + if not label and icon_size < _NORMAL_BUTTON_ICON_SIZE: + b.set_relief(Gtk.ReliefStyle.NONE) + b.set_focus_on_click(False) - return b + if clicked is not None: + b.connect('clicked', clicked) + + return b def _create_receiver_panel(): - p = Gtk.Box.new(Gtk.Orientation.VERTICAL, 4) + p = Gtk.Box.new(Gtk.Orientation.VERTICAL, 4) - p._count = Gtk.Label() - p._count.set_padding(24, 0) - p._count.set_alignment(0, 0.5) - p.pack_start(p._count, True, True, 0) + p._count = Gtk.Label() + p._count.set_padding(24, 0) + p._count.set_alignment(0, 0.5) + p.pack_start(p._count, True, True, 0) - p._scanning = Gtk.Label(_("Scanning") + '...') - p._spinner = Gtk.Spinner() + 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(p._scanning, False, False, 0) - bp.pack_end(p._spinner, False, False, 0) - p.pack_end(bp, False, False, 0) + bp = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 8) + 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) - return p + return p def _create_device_panel(): - p = Gtk.Box.new(Gtk.Orientation.VERTICAL, 4) + p = Gtk.Box.new(Gtk.Orientation.VERTICAL, 4) - def _status_line(label_text): - b = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 8) - b.set_size_request(10, 28) + def _status_line(label_text): + b = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 8) + b.set_size_request(10, 28) - b._label = Gtk.Label(label_text) - b._label.set_alignment(0, 0.5) - b._label.set_size_request(170, 10) - b.pack_start(b._label, False, False, 0) + b._label = Gtk.Label(label_text) + b._label.set_alignment(0, 0.5) + b._label.set_size_request(170, 10) + b.pack_start(b._label, False, False, 0) - b._icon = Gtk.Image() - b.pack_start(b._icon, False, False, 0) + b._icon = Gtk.Image() + b.pack_start(b._icon, False, False, 0) - b._text = Gtk.Label() - b._text.set_alignment(0, 0.5) - b.pack_start(b._text, True, True, 0) + b._text = Gtk.Label() + b._text.set_alignment(0, 0.5) + b.pack_start(b._text, True, True, 0) - return b + return b - p._battery = _status_line(_("Battery")) - p.pack_start(p._battery, False, False, 0) + 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.pack_start(p._secure, False, False, 0) + 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.pack_start(p._lux, False, False, 0) + p._lux = _status_line(_("Lighting")) + p.pack_start(p._lux, False, False, 0) - p._config = _config_panel.create() - p.pack_end(p._config, False, False, 4) + p._config = _config_panel.create() + p.pack_end(p._config, False, False, 4) - return p + return p def _create_details_panel(): - p = Gtk.Frame() - p.set_shadow_type(Gtk.ShadowType.NONE) - p.set_size_request(240, 0) - p.set_state_flags(Gtk.StateFlags.ACTIVE, True) + p = Gtk.Frame() + p.set_shadow_type(Gtk.ShadowType.NONE) + p.set_size_request(240, 0) + p.set_state_flags(Gtk.StateFlags.ACTIVE, True) - p._text = Gtk.Label() - p._text.set_padding(6, 4) - p._text.set_alignment(0, 0) - p._text.set_selectable(True) - p.add(p._text) + p._text = Gtk.Label() + p._text.set_padding(6, 4) + p._text.set_alignment(0, 0) + p._text.set_selectable(True) + p.add(p._text) - return p + return p def _create_buttons_box(): - bb = Gtk.ButtonBox(Gtk.Orientation.HORIZONTAL) - bb.set_layout(Gtk.ButtonBoxStyle.END) + bb = Gtk.ButtonBox(Gtk.Orientation.HORIZONTAL) + bb.set_layout(Gtk.ButtonBoxStyle.END) - bb._details = _new_button(None, 'dialog-information', _SMALL_BUTTON_ICON_SIZE, - tooltip=_("Show Technical Details"), toggle=True, clicked=_update_details) - bb.add(bb._details) - bb.set_child_secondary(bb._details, True) - bb.set_child_non_homogeneous(bb._details, True) + bb._details = _new_button(None, + 'dialog-information', + _SMALL_BUTTON_ICON_SIZE, + tooltip=_("Show Technical Details"), + toggle=True, + clicked=_update_details) + bb.add(bb._details) + bb.set_child_secondary(bb._details, True) + bb.set_child_non_homogeneous(bb._details, True) - def _pair_new_device(trigger): - assert _find_selected_device_id() is not None - receiver = _find_selected_device() - assert receiver is not None - assert bool(receiver) - assert receiver.kind is None - _action.pair(_window, receiver) + def _pair_new_device(trigger): + assert _find_selected_device_id() is not None + receiver = _find_selected_device() + assert receiver is not None + assert bool(receiver) + assert receiver.kind is None + _action.pair(_window, receiver) - bb._pair = _new_button(_("Pair new device"), 'list-add', clicked=_pair_new_device) - bb.add(bb._pair) + bb._pair = _new_button(_("Pair new device"), + 'list-add', + clicked=_pair_new_device) + bb.add(bb._pair) - def _unpair_current_device(trigger): - assert _find_selected_device_id() is not None - device = _find_selected_device() - assert device is not None - assert bool(device) - assert device.kind is not None - _action.unpair(_window, device) + def _unpair_current_device(trigger): + assert _find_selected_device_id() is not None + device = _find_selected_device() + assert device is not None + assert bool(device) + assert device.kind is not None + _action.unpair(_window, device) - bb._unpair = _new_button(_("Unpair"), 'edit-delete', clicked=_unpair_current_device) - bb.add(bb._unpair) + bb._unpair = _new_button(_("Unpair"), + 'edit-delete', + clicked=_unpair_current_device) + bb.add(bb._unpair) - return bb + return bb def _create_empty_panel(): - p = Gtk.Label() - p.set_markup('' + _("Select a device") + '') - p.set_sensitive(False) + p = Gtk.Label() + p.set_markup('' + _("Select a device") + '') + p.set_sensitive(False) - return p + return p def _create_info_panel(): - p = Gtk.Box.new(Gtk.Orientation.VERTICAL, 4) + p = Gtk.Box.new(Gtk.Orientation.VERTICAL, 4) - p._title = Gtk.Label(' ') - p._title.set_alignment(0, 0.5) - p._icon = Gtk.Image() + p._title = Gtk.Label(' ') + p._title.set_alignment(0, 0.5) + p._icon = Gtk.Image() - b1 = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 4) - b1.pack_start(p._title, True, True, 0) - b1.pack_start(p._icon, False, False, 0) - p.pack_start(b1, False, False, 0) + b1 = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 4) + b1.pack_start(p._title, True, True, 0) + b1.pack_start(p._icon, False, False, 0) + p.pack_start(b1, False, False, 0) - p.pack_start(Gtk.Separator.new(Gtk.Orientation.HORIZONTAL), False, False, 0) # spacer + p.pack_start(Gtk.Separator.new(Gtk.Orientation.HORIZONTAL), False, False, + 0) # spacer - p._receiver = _create_receiver_panel() - p.pack_start(p._receiver, True, True, 0) + p._receiver = _create_receiver_panel() + p.pack_start(p._receiver, True, True, 0) - p._device = _create_device_panel() - p.pack_start(p._device, True, True, 0) + p._device = _create_device_panel() + p.pack_start(p._device, True, True, 0) - p.pack_start(Gtk.Separator.new(Gtk.Orientation.HORIZONTAL), False, False, 0) # spacer + p.pack_start(Gtk.Separator.new(Gtk.Orientation.HORIZONTAL), False, False, + 0) # spacer - p._buttons = _create_buttons_box() - p.pack_end(p._buttons, False, False, 0) + p._buttons = _create_buttons_box() + p.pack_end(p._buttons, False, False, 0) - return p + return p def _create_tree(model): - tree = Gtk.TreeView() - tree.set_size_request(330, 0) # enough width for simple setups - tree.set_headers_visible(False) - tree.set_show_expanders(False) - tree.set_level_indentation(20) - # tree.set_fixed_height_mode(True) - tree.set_enable_tree_lines(True) - tree.set_reorderable(False) - tree.set_enable_search(False) - tree.set_model(model) + tree = Gtk.TreeView() + tree.set_size_request(330, 0) # enough width for simple setups + tree.set_headers_visible(False) + tree.set_show_expanders(False) + tree.set_level_indentation(20) + # tree.set_fixed_height_mode(True) + tree.set_enable_tree_lines(True) + tree.set_reorderable(False) + tree.set_enable_search(False) + tree.set_model(model) - def _is_separator(model, item, _ignore=None): - return model.get_value(item, _COLUMN.PATH) is None - tree.set_row_separator_func(_is_separator, None) + def _is_separator(model, item, _ignore=None): + return model.get_value(item, _COLUMN.PATH) is 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) - tree.append_column(icon_column) + tree.set_row_separator_func(_is_separator, None) - 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.set_expand(True) - tree.append_column(name_column) - tree.set_expander_column(name_column) + 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) + tree.append_column(icon_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_column.set_expand(True) - tree.append_column(status_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.set_expand(True) + tree.append_column(name_column) + tree.set_expander_column(name_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) - tree.append_column(battery_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_column.set_expand(True) + tree.append_column(status_column) - return tree + 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) + tree.append_column(battery_column) + + return tree def _create_window_layout(): - assert _tree is not None - assert _details is not None - assert _info is not None - assert _empty is not None + assert _tree is not None + assert _details is not None + assert _info is not None + assert _empty is not None - assert _tree.get_selection().get_mode() == Gtk.SelectionMode.SINGLE - _tree.get_selection().connect('changed', _device_selected) + assert _tree.get_selection().get_mode() == Gtk.SelectionMode.SINGLE + _tree.get_selection().connect('changed', _device_selected) - tree_scroll = Gtk.ScrolledWindow() - tree_scroll.add(_tree) - tree_scroll.set_min_content_width(_tree.get_size_request()[0]) - tree_scroll.set_shadow_type(Gtk.ShadowType.IN) + tree_scroll = Gtk.ScrolledWindow() + tree_scroll.add(_tree) + tree_scroll.set_min_content_width(_tree.get_size_request()[0]) + tree_scroll.set_shadow_type(Gtk.ShadowType.IN) - tree_panel = Gtk.Box.new(Gtk.Orientation.VERTICAL, 0) - tree_panel.set_homogeneous(False) - tree_panel.pack_start(tree_scroll, True, True, 0) - tree_panel.pack_start(_details, False, False, 0) + tree_panel = Gtk.Box.new(Gtk.Orientation.VERTICAL, 0) + tree_panel.set_homogeneous(False) + tree_panel.pack_start(tree_scroll, True, True, 0) + tree_panel.pack_start(_details, False, False, 0) - panel = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 16) - panel.pack_start(tree_panel, True, True, 0) - panel.pack_start(_info, True, True, 0) - panel.pack_start(_empty, True, True, 0) + panel = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 16) + panel.pack_start(tree_panel, True, True, 0) + panel.pack_start(_info, True, True, 0) + panel.pack_start(_empty, True, True, 0) - 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") + ' ' + NAME, 'application-exit', - icon_size=_SMALL_BUTTON_ICON_SIZE, clicked=destroy) - bottom_buttons_box.add(quit_button) - about_button = _new_button(_("About") + ' ' + NAME, 'help-about', - icon_size=_SMALL_BUTTON_ICON_SIZE, clicked=_show_about_window) - bottom_buttons_box.add(about_button) + 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") + ' ' + NAME, + 'application-exit', + icon_size=_SMALL_BUTTON_ICON_SIZE, + clicked=destroy) + bottom_buttons_box.add(quit_button) + about_button = _new_button(_("About") + ' ' + NAME, + 'help-about', + icon_size=_SMALL_BUTTON_ICON_SIZE, + clicked=_show_about_window) + bottom_buttons_box.add(about_button) - # solaar_version = Gtk.Label() - # solaar_version.set_markup('' + NAME + ' v' + VERSION + '') - # bottom_buttons_box.add(solaar_version) - # bottom_buttons_box.set_child_secondary(solaar_version, True) + # solaar_version = Gtk.Label() + # solaar_version.set_markup('' + NAME + ' v' + VERSION + '') + # bottom_buttons_box.add(solaar_version) + # bottom_buttons_box.set_child_secondary(solaar_version, True) - vbox = Gtk.Box.new(Gtk.Orientation.VERTICAL, 8) - vbox.set_border_width(8) - vbox.pack_start(panel, True, True, 0) - vbox.pack_end(bottom_buttons_box, False, False, 0) - vbox.show_all() + vbox = Gtk.Box.new(Gtk.Orientation.VERTICAL, 8) + vbox.set_border_width(8) + vbox.pack_start(panel, True, True, 0) + vbox.pack_end(bottom_buttons_box, False, False, 0) + vbox.show_all() - _details.set_visible(False) - _info.set_visible(False) - return vbox + _details.set_visible(False) + _info.set_visible(False) + return vbox def _create(delete_action): - window = Gtk.Window() - window.set_title(NAME) - window.set_role('status-window') + window = Gtk.Window() + window.set_title(NAME) + 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.set_type_hint(Gdk.WindowTypeHint.UTILITY) + # window.set_skip_taskbar_hint(True) + # window.set_skip_pager_hint(True) + window.connect('delete-event', delete_action) - vbox = _create_window_layout() - window.add(vbox) + vbox = _create_window_layout() + window.add(vbox) - geometry = Gdk.Geometry() - geometry.min_width = 600 - geometry.min_height = 320 - window.set_geometry_hints(vbox, geometry, Gdk.WindowHints.MIN_SIZE) - window.set_position(Gtk.WindowPosition.CENTER) + geometry = Gdk.Geometry() + geometry.min_width = 600 + geometry.min_height = 320 + window.set_geometry_hints(vbox, geometry, Gdk.WindowHints.MIN_SIZE) + window.set_position(Gtk.WindowPosition.CENTER) - style = window.get_style_context() - style.add_class('solaar') + style = window.get_style_context() + style.add_class('solaar') + + return window - return window # # window updates # + def _find_selected_device(): - selection = _tree.get_selection() - model, item = selection.get_selected() - return model.get_value(item, _COLUMN.DEVICE) if item else None + selection = _tree.get_selection() + model, item = selection.get_selected() + return model.get_value(item, _COLUMN.DEVICE) if item else None def _find_selected_device_id(): - selection = _tree.get_selection() - model, item = selection.get_selected() - if item: - return _model.get_value(item, _COLUMN.PATH), _model.get_value(item, _COLUMN.NUMBER) + selection = _tree.get_selection() + model, item = selection.get_selected() + if item: + return _model.get_value(item, _COLUMN.PATH), _model.get_value( + item, _COLUMN.NUMBER) # triggered by changing selection in the tree def _device_selected(selection): - model, item = selection.get_selected() - device = model.get_value(item, _COLUMN.DEVICE) if item else None - # if _log.isEnabledFor(_DEBUG): - # _log.debug("window tree selected device %s", device) - if device: - _update_info_panel(device, full=True) - else: - # When removing a receiver, one of its children may get automatically selected - # before the tree had time to remove them as well. - # Rather than chase around for another device to select, just clear the selection. - _tree.get_selection().unselect_all() - _update_info_panel(None, full=True) + model, item = selection.get_selected() + device = model.get_value(item, _COLUMN.DEVICE) if item else None + # if _log.isEnabledFor(_DEBUG): + # _log.debug("window tree selected device %s", device) + if device: + _update_info_panel(device, full=True) + else: + # When removing a receiver, one of its children may get automatically selected + # before the tree had time to remove them as well. + # Rather than chase around for another device to select, just clear the selection. + _tree.get_selection().unselect_all() + _update_info_panel(None, full=True) def _receiver_row(receiver_path, receiver=None): - assert receiver_path + assert receiver_path - item = _model.get_iter_first() - while item: - # first row matching the path must be the receiver one - if _model.get_value(item, _COLUMN.PATH) == receiver_path: - return item - item = _model.iter_next(item) + item = _model.get_iter_first() + while item: + # first row matching the path must be the receiver one + if _model.get_value(item, _COLUMN.PATH) == receiver_path: + return item + item = _model.iter_next(item) - if not item and receiver: - icon_name = _icons.device_icon_name(receiver.name) - status_text = None - status_icon = None - row_data = (receiver_path, 0, True, receiver.name, icon_name, status_text, status_icon, receiver) - assert len(row_data) == len(_TREE_SEPATATOR) - if _log.isEnabledFor(_DEBUG): - _log.debug("new receiver row %s", row_data) - item = _model.append(None, row_data) - if _TREE_SEPATATOR: - _model.append(None, _TREE_SEPATATOR) + if not item and receiver: + icon_name = _icons.device_icon_name(receiver.name) + status_text = None + status_icon = None + row_data = (receiver_path, 0, True, receiver.name, icon_name, + status_text, status_icon, receiver) + assert len(row_data) == len(_TREE_SEPATATOR) + if _log.isEnabledFor(_DEBUG): + _log.debug("new receiver row %s", row_data) + item = _model.append(None, row_data) + if _TREE_SEPATATOR: + _model.append(None, _TREE_SEPATATOR) - return item or None + return item or None def _device_row(receiver_path, device_number, device=None): - assert receiver_path - assert device_number is not None + assert receiver_path + assert device_number is not None - receiver_row = _receiver_row(receiver_path, None if device is None else device.receiver) - item = _model.iter_children(receiver_row) - new_child_index = 0 - while item: - assert _model.get_value(item, _COLUMN.PATH) == receiver_path - item_number = _model.get_value(item, _COLUMN.NUMBER) - if item_number == device_number: - return item - if item_number > device_number: - item = None - break - new_child_index += 1 - item = _model.iter_next(item) + receiver_row = _receiver_row(receiver_path, + None if device is None else device.receiver) + item = _model.iter_children(receiver_row) + new_child_index = 0 + while item: + assert _model.get_value(item, _COLUMN.PATH) == receiver_path + item_number = _model.get_value(item, _COLUMN.NUMBER) + if item_number == device_number: + return item + if item_number > device_number: + item = None + break + new_child_index += 1 + item = _model.iter_next(item) - if not item and device: - icon_name = _icons.device_icon_name(device.name, device.kind) - status_text = None - status_icon = None - row_data = (receiver_path, device_number, bool(device.online), device.codename, icon_name, status_text, status_icon, device) - assert len(row_data) == len(_TREE_SEPATATOR) - if _log.isEnabledFor(_DEBUG): - _log.debug("new device row %s at index %d", row_data, new_child_index) - item = _model.insert(receiver_row, new_child_index, row_data) + if not item and device: + icon_name = _icons.device_icon_name(device.name, device.kind) + status_text = None + status_icon = None + row_data = (receiver_path, device_number, bool(device.online), + device.codename, icon_name, status_text, status_icon, + device) + assert len(row_data) == len(_TREE_SEPATATOR) + if _log.isEnabledFor(_DEBUG): + _log.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 - return item or None # # # + def select(receiver_path, device_number=None): - assert _window - assert receiver_path is not None - if device_number is None: - item = _receiver_row(receiver_path) - else: - item = _device_row(receiver_path, device_number) - if item: - selection = _tree.get_selection() - selection.select_iter(item) - else: - _log.warn("select(%s, %s) failed to find an item", receiver_path, device_number) + assert _window + assert receiver_path is not None + if device_number is None: + item = _receiver_row(receiver_path) + else: + item = _device_row(receiver_path, device_number) + if item: + selection = _tree.get_selection() + selection.select_iter(item) + else: + _log.warn("select(%s, %s) failed to find an item", receiver_path, + device_number) def _hide(w, _ignore=None): - assert w == _window - # some window managers move the window to 0,0 after hide() - # so try to remember the last position - position = _window.get_position() - _window.hide() - _window.move(*position) - return True + assert w == _window + # some window managers move the window to 0,0 after hide() + # so try to remember the last position + position = _window.get_position() + _window.hide() + _window.move(*position) + return True def popup(trigger=None, receiver_path=None, device_id=None): - if receiver_path: - select(receiver_path, device_id) - _window.present() - return True + if receiver_path: + select(receiver_path, device_id) + _window.present() + return True def toggle(trigger=None): - if _window.get_visible(): - _hide(_window) - else: - _window.present() + if _window.get_visible(): + _hide(_window) + else: + _window.present() + # # # + def _update_details(button): - assert button - visible = button.get_active() + assert button + visible = button.get_active() - if visible: - # _details._text.set_markup('reading...') + if visible: + # _details._text.set_markup('reading...') - def _details_items(device, read_all=False): - # If read_all is False, only return stuff that is ~100% already - # cached, and involves no HID++ calls. + def _details_items(device, read_all=False): + # If read_all is False, only return stuff that is ~100% already + # cached, and involves no HID++ calls. - if device.kind is None: - yield (_("Path"), device.path) - # 046d is the Logitech vendor id - yield (_("USB id"), '046d:' + device.product_id) + if device.kind is None: + yield (_("Path"), device.path) + # 046d is the Logitech vendor id + yield (_("USB id"), '046d:' + device.product_id) - if read_all: - yield (_("Serial"), device.serial) - else: - yield (_("Serial"), '...') + if read_all: + yield (_("Serial"), device.serial) + else: + yield (_("Serial"), '...') - else: - # yield ('Codename', device.codename) - yield (_("Index"), device.number) - yield (_("Wireless PID"), device.wpid) - hid_version = device.protocol - yield (_("Protocol"), 'HID++ %1.1f' % hid_version if hid_version else _('Unknown')) - if read_all and device.polling_rate: - yield (_("Polling rate"), _('%(rate)d ms (%(rate_hz)dHz)') % { 'rate': device.polling_rate, 'rate_hz': 1000 // device.polling_rate }) + else: + # yield ('Codename', device.codename) + yield (_("Index"), device.number) + yield (_("Wireless PID"), device.wpid) + hid_version = device.protocol + yield (_("Protocol"), 'HID++ %1.1f' % + hid_version if hid_version else _('Unknown')) + if read_all and device.polling_rate: + yield (_("Polling rate"), + _('%(rate)d ms (%(rate_hz)dHz)') % { + 'rate': device.polling_rate, + 'rate_hz': 1000 // device.polling_rate + }) - if read_all or not device.online: - yield (_("Serial"), device.serial) - else: - yield (_("Serial"), '...') + if read_all or not device.online: + yield (_("Serial"), device.serial) + else: + yield (_("Serial"), '...') - if read_all: - if device.firmware: - for fw in list(device.firmware): - yield (' ' + _(str(fw.kind)), (fw.name + ' ' + fw.version).strip()) - elif device.kind is None or device.online: - yield (' %s' % _("Firmware"), '...') + if read_all: + if device.firmware: + for fw in list(device.firmware): + yield (' ' + _(str(fw.kind)), + (fw.name + ' ' + fw.version).strip()) + elif device.kind is None or device.online: + 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_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)) - def _set_details(text): - _details._text.set_markup(text) + def _set_details(text): + _details._text.set_markup(text) - def _make_text(items): - text = '\n'.join('%-13s: %s' % i for i in items) - return '' + text + '' + def _make_text(items): + text = '\n'.join('%-13s: %s' % i for i in items) + return '' + text + '' - def _read_slow(device): - items = _details_items(selected_device, True) - text = _make_text(items) - if device == _details._current_device: - GLib.idle_add(_set_details, text) + def _read_slow(device): + items = _details_items(selected_device, True) + text = _make_text(items) + if device == _details._current_device: + GLib.idle_add(_set_details, text) - selected_device = _find_selected_device() - assert selected_device - _details._current_device = selected_device + selected_device = _find_selected_device() + assert selected_device + _details._current_device = selected_device - read_all = not (selected_device.kind is None or selected_device.online) - items = _details_items(selected_device, read_all) - _set_details(_make_text(items)) + read_all = not (selected_device.kind is None or selected_device.online) + items = _details_items(selected_device, read_all) + _set_details(_make_text(items)) - if read_all: - _details._current_device = None - else: - _ui_async(_read_slow, selected_device) + if read_all: + _details._current_device = None + else: + _ui_async(_read_slow, selected_device) - _details.set_visible(visible) + _details.set_visible(visible) def _update_receiver_panel(receiver, panel, buttons, full=False): - assert receiver + assert receiver - devices_count = len(receiver) + 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%s' % 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%s' % _('Only one device can be paired to this receiver.') - pairings = receiver.remaining_pairings(False) - if ( pairings is not None and pairings >= 0 ) : - paired_text += '\n%s' % _('This receiver has %d pairing(s) remaining.') % pairings + if (receiver.max_devices > 0): + paired_text += '\n\n%s' % 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%s' % _( + 'Only one device can be paired to this receiver.') + pairings = receiver.remaining_pairings(False) + if (pairings is not None and pairings >= 0): + paired_text += '\n%s' % _( + 'This receiver has %d pairing(s) remaining.') % pairings - panel._count.set_markup(paired_text) + panel._count.set_markup(paired_text) - is_pairing = receiver.status.lock_open - if is_pairing: - panel._scanning.set_visible(True) - if not panel._spinner.get_visible(): - panel._spinner.start() - panel._spinner.set_visible(True) - else: - panel._scanning.set_visible(False) - if panel._spinner.get_visible(): - panel._spinner.stop() - panel._spinner.set_visible(False) + is_pairing = receiver.status.lock_open + if is_pairing: + panel._scanning.set_visible(True) + if not panel._spinner.get_visible(): + panel._spinner.start() + panel._spinner.set_visible(True) + else: + panel._scanning.set_visible(False) + if panel._spinner.get_visible(): + panel._spinner.stop() + panel._spinner.set_visible(False) - panel.set_visible(True) + panel.set_visible(True) - # b._insecure.set_visible(False) - buttons._unpair.set_visible(False) + # b._insecure.set_visible(False) + buttons._unpair.set_visible(False) - if ( receiver.may_unpair or receiver.re_pairs ) and not is_pairing and \ - ( receiver.remaining_pairings() is None or receiver.remaining_pairings() != 0 ): - if not receiver.re_pairs and devices_count >= receiver.max_devices: - paired_devices = tuple(n for n in range(1, receiver.max_devices+1) if n in receiver) - buttons._pair.set_sensitive(len(paired_devices) < receiver.max_devices) - else: - buttons._pair.set_sensitive(True) - else: - buttons._pair.set_sensitive(False) + if ( receiver.may_unpair or receiver.re_pairs ) and not is_pairing and \ + ( receiver.remaining_pairings() is None or receiver.remaining_pairings() != 0 ): + if not receiver.re_pairs and devices_count >= receiver.max_devices: + paired_devices = tuple(n + for n in range(1, receiver.max_devices + 1) + if n in receiver) + buttons._pair.set_sensitive( + len(paired_devices) < receiver.max_devices) + else: + buttons._pair.set_sensitive(True) + else: + buttons._pair.set_sensitive(False) - buttons._pair.set_visible(True) + buttons._pair.set_visible(True) def _update_device_panel(device, panel, buttons, full=False): - assert device - is_online = bool(device.online) - panel.set_sensitive(is_online) + assert device + is_online = bool(device.online) + panel.set_sensitive(is_online) - battery_level = device.status.get(_K.BATTERY_LEVEL) - battery_next_level = device.status.get(_K.BATTERY_NEXT_LEVEL) - battery_voltage = device.status.get(_K.BATTERY_VOLTAGE) + battery_level = device.status.get(_K.BATTERY_LEVEL) + battery_next_level = device.status.get(_K.BATTERY_NEXT_LEVEL) + battery_voltage = device.status.get(_K.BATTERY_VOLTAGE) - if battery_level is None: - icon_name = _icons.battery() - panel._battery._icon.set_sensitive(False) - panel._battery._icon.set_from_icon_name(icon_name, _INFO_ICON_SIZE) - panel._battery._text.set_sensitive(True) - panel._battery._text.set_markup('%s' % _("unknown")) - else: - charging = device.status.get(_K.BATTERY_CHARGING) - icon_name = _icons.battery(battery_level, charging) - panel._battery._icon.set_from_icon_name(icon_name, _INFO_ICON_SIZE) - panel._battery._icon.set_sensitive(True) + if battery_level is None: + icon_name = _icons.battery() + panel._battery._icon.set_sensitive(False) + panel._battery._icon.set_from_icon_name(icon_name, _INFO_ICON_SIZE) + panel._battery._text.set_sensitive(True) + panel._battery._text.set_markup('%s' % _("unknown")) + else: + charging = device.status.get(_K.BATTERY_CHARGING) + icon_name = _icons.battery(battery_level, charging) + panel._battery._icon.set_from_icon_name(icon_name, _INFO_ICON_SIZE) + panel._battery._icon.set_sensitive(True) - if battery_voltage is not None: - text = "%(battery_voltage)dmV" % { 'battery_voltage' : battery_voltage } - elif isinstance(battery_level, _NamedInt): - text = _(str(battery_level)) - else: - text = "%(battery_percent)d%%" % { 'battery_percent': battery_level } - if battery_next_level is not None: - if isinstance(battery_next_level, _NamedInt): - text += " (" +_("next ") + _(str(battery_next_level)) + ")" - else: - text += " (" + _("next ") + ( "%d%%" % battery_next_level ) + ")" - if is_online: - if charging: - text += ' (%s)' % _("charging") - else: - text += ' (%s)' % _("last known") - panel._battery._text.set_sensitive(is_online) - panel._battery._text.set_markup(text) + if battery_voltage is not None: + text = "%(battery_voltage)dmV" % { + 'battery_voltage': battery_voltage + } + elif isinstance(battery_level, _NamedInt): + text = _(str(battery_level)) + else: + text = "%(battery_percent)d%%" % {'battery_percent': battery_level} + if battery_next_level is not None: + if isinstance(battery_next_level, _NamedInt): + text += " (" + _("next ") + _( + str(battery_next_level)) + ")" + else: + text += " (" + _("next ") + ( + "%d%%" % battery_next_level) + ")" + if is_online: + if charging: + text += ' (%s)' % _("charging") + else: + text += ' (%s)' % _("last known") + panel._battery._text.set_sensitive(is_online) + panel._battery._text.set_markup(text) - if is_online: - not_secure = device.status.get(_K.LINK_ENCRYPTED) == False - if not_secure: - 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" - "\n" - "For pointing devices (mice, trackballs, trackpads), this is a minor security issue.\n" - "\n" - "It is, however, a major security issue for text-input devices (keyboards, numpads),\n" - "because typed text can be sniffed inconspicuously by 3rd parties within range.")) - else: - 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._icon.set_visible(True) - else: - panel._secure._text.set_markup('%s' % _("offline")) - panel._secure._icon.set_visible(False) - panel._secure.set_tooltip_text('') + if is_online: + not_secure = device.status.get(_K.LINK_ENCRYPTED) == False + if not_secure: + 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" + "\n" + "For pointing devices (mice, trackballs, trackpads), this is a minor security issue.\n" + "\n" + "It is, however, a major security issue for text-input devices (keyboards, numpads),\n" + "because typed text can be sniffed inconspicuously by 3rd parties within range." + )) + else: + 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._icon.set_visible(True) + else: + panel._secure._text.set_markup('%s' % _("offline")) + panel._secure._icon.set_visible(False) + panel._secure.set_tooltip_text('') - if is_online: - light_level = device.status.get(_K.LIGHT_LEVEL) - if light_level is None: - 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.set_visible(True) - else: - panel._lux.set_visible(False) + if is_online: + light_level = device.status.get(_K.LIGHT_LEVEL) + if light_level is None: + 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.set_visible(True) + else: + panel._lux.set_visible(False) - buttons._pair.set_visible(False) - buttons._unpair.set_sensitive(device.receiver.may_unpair) - buttons._unpair.set_visible(True) + buttons._pair.set_visible(False) + buttons._unpair.set_sensitive(device.receiver.may_unpair) + buttons._unpair.set_visible(True) - panel.set_visible(True) + panel.set_visible(True) - if full: - _config_panel.update(device, is_online) + if full: + _config_panel.update(device, is_online) def _update_info_panel(device, full=False): - if device is None: - # no selected device, show the 'empty' panel - _details.set_visible(False) - _info.set_visible(False) - _empty.set_visible(True) - return + if device is None: + # no selected device, show the 'empty' panel + _details.set_visible(False) + _info.set_visible(False) + _empty.set_visible(True) + return - # a receiver must be valid - # a device must be paired - assert device + # a receiver must be valid + # a device must be paired + assert device - _info._title.set_markup('%s' % device.name) - icon_name = _icons.device_icon_name(device.name, device.kind) - _info._icon.set_from_icon_name(icon_name, _DEVICE_ICON_SIZE) + _info._title.set_markup('%s' % device.name) + icon_name = _icons.device_icon_name(device.name, device.kind) + _info._icon.set_from_icon_name(icon_name, _DEVICE_ICON_SIZE) - if device.kind is None: - _info._device.set_visible(False) - _info._icon.set_sensitive(True) - _info._title.set_sensitive(True) - _update_receiver_panel(device, _info._receiver, _info._buttons, full) - else: - _info._receiver.set_visible(False) - is_online = bool(device.online) - _info._icon.set_sensitive(is_online) - _info._title.set_sensitive(is_online) - _update_device_panel(device, _info._device, _info._buttons, full) + if device.kind is None: + _info._device.set_visible(False) + _info._icon.set_sensitive(True) + _info._title.set_sensitive(True) + _update_receiver_panel(device, _info._receiver, _info._buttons, full) + else: + _info._receiver.set_visible(False) + is_online = bool(device.online) + _info._icon.set_sensitive(is_online) + _info._title.set_sensitive(is_online) + _update_device_panel(device, _info._device, _info._buttons, full) - _empty.set_visible(False) - _info.set_visible(True) + _empty.set_visible(False) + _info.set_visible(True) + + if full: + _update_details(_info._buttons._details) - if full: - _update_details(_info._buttons._details) # # window layout: @@ -747,105 +828,113 @@ _empty = None _window = None -def init(show_window,hide_on_close): - Gtk.Window.set_default_icon_name(NAME.lower()) - Gtk.Window.set_default_icon_from_file(_icons.icon_file(NAME.lower())) +def init(show_window, hide_on_close): + Gtk.Window.set_default_icon_name(NAME.lower()) + Gtk.Window.set_default_icon_from_file(_icons.icon_file(NAME.lower())) - global _model, _tree, _details, _info, _empty, _window - _model = Gtk.TreeStore(*_COLUMN_TYPES) - _tree = _create_tree(_model) - _details = _create_details_panel() - _info = _create_info_panel() - _empty = _create_empty_panel() - _window = _create(_hide if hide_on_close else destroy) - if show_window: - _window.present() + global _model, _tree, _details, _info, _empty, _window + _model = Gtk.TreeStore(*_COLUMN_TYPES) + _tree = _create_tree(_model) + _details = _create_details_panel() + _info = _create_info_panel() + _empty = _create_empty_panel() + _window = _create(_hide if hide_on_close else destroy) + if show_window: + _window.present() def destroy(_ignore1=None, _ignore2=None): - global _model, _tree, _details, _info, _empty, _window - w, _window = _window, None - w.destroy() - w = None - _config_panel.destroy() + global _model, _tree, _details, _info, _empty, _window + w, _window = _window, None + w.destroy() + w = None + _config_panel.destroy() - _empty = None - _info = None - _details = None - _tree = None - _model = None + _empty = None + _info = None + _details = None + _tree = None + _model = None def update(device, need_popup=False): - if _window is None: - return + if _window is None: + return - assert device is not None + assert device is not None - if need_popup: - popup() + if need_popup: + popup() - selected_device_id = _find_selected_device_id() + selected_device_id = _find_selected_device_id() - if device.kind is None: - # receiver - is_alive = bool(device) - item = _receiver_row(device.path, device if is_alive else None) + if device.kind is None: + # receiver + is_alive = bool(device) + item = _receiver_row(device.path, device if is_alive else None) - if is_alive and item: - was_pairing = bool(_model.get_value(item, _COLUMN.STATUS_ICON)) - is_pairing = bool(device.status.lock_open) - _model.set_value(item, _COLUMN.STATUS_ICON, 'network-wireless' if is_pairing else _CAN_SET_ROW_NONE) + if is_alive and item: + was_pairing = bool(_model.get_value(item, _COLUMN.STATUS_ICON)) + is_pairing = bool(device.status.lock_open) + _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 - _update_info_panel(device, full=full_update) + if selected_device_id == (device.path, 0): + full_update = need_popup or was_pairing != is_pairing + _update_info_panel(device, full=full_update) - elif item: - if _TREE_SEPATATOR: - separator = _model.iter_next(item) - _model.remove(separator) - _model.remove(item) + elif item: + if _TREE_SEPATATOR: + separator = _model.iter_next(item) + _model.remove(separator) + _model.remove(item) - else: - # peripheral - is_paired = bool(device) - assert device.receiver - assert device.number is not None and device.number > 0, "invalid device number" + str(device.number) - item = _device_row(device.receiver.path, device.number, device if is_paired else None) + else: + # peripheral + is_paired = bool(device) + assert device.receiver + assert device.number is not None and device.number > 0, "invalid device number" + str( + device.number) + item = _device_row(device.receiver.path, device.number, + device if is_paired else None) - if is_paired and item: - was_online = _model.get_value(item, _COLUMN.ACTIVE) - is_online = bool(device.online) - _model.set_value(item, _COLUMN.ACTIVE, is_online) + if is_paired and item: + was_online = _model.get_value(item, _COLUMN.ACTIVE) + is_online = bool(device.online) + _model.set_value(item, _COLUMN.ACTIVE, is_online) - battery_level = device.status.get(_K.BATTERY_LEVEL) - battery_voltage = device.status.get(_K.BATTERY_VOLTAGE) - if battery_level is None: - _model.set_value(item, _COLUMN.STATUS_TEXT, _CAN_SET_ROW_NONE) - _model.set_value(item, _COLUMN.STATUS_ICON, _CAN_SET_ROW_NONE) - else: - if battery_voltage is not None: - 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 } - _model.set_value(item, _COLUMN.STATUS_TEXT, status_text) + battery_level = device.status.get(_K.BATTERY_LEVEL) + battery_voltage = device.status.get(_K.BATTERY_VOLTAGE) + if battery_level is None: + _model.set_value(item, _COLUMN.STATUS_TEXT, _CAN_SET_ROW_NONE) + _model.set_value(item, _COLUMN.STATUS_ICON, _CAN_SET_ROW_NONE) + else: + if battery_voltage is not None: + 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 + } + _model.set_value(item, _COLUMN.STATUS_TEXT, status_text) - charging = device.status.get(_K.BATTERY_CHARGING) - icon_name = _icons.battery(battery_level, charging) - _model.set_value(item, _COLUMN.STATUS_ICON, icon_name) + charging = device.status.get(_K.BATTERY_CHARGING) + icon_name = _icons.battery(battery_level, charging) + _model.set_value(item, _COLUMN.STATUS_ICON, icon_name) - if selected_device_id is None or need_popup: - select(device.receiver.path, device.number) - elif selected_device_id == (device.receiver.path, device.number): - full_update = need_popup or was_online != is_online - _update_info_panel(device, full=full_update) + if selected_device_id is None or need_popup: + select(device.receiver.path, device.number) + elif selected_device_id == (device.receiver.path, device.number): + full_update = need_popup or was_online != is_online + _update_info_panel(device, full=full_update) - elif item: - _model.remove(item) - _config_panel.clean(device) + elif item: + _model.remove(item) + _config_panel.clean(device) - # make sure all rows are visible - _tree.expand_all() + # make sure all rows are visible + _tree.expand_all() diff --git a/lib/solaar/upower.py b/lib/solaar/upower.py index 498dbab7..63f3e0eb 100644 --- a/lib/solaar/upower.py +++ b/lib/solaar/upower.py @@ -28,61 +28,74 @@ del getLogger # _suspend_callback = None + + def _suspend(): - if _suspend_callback: - if _log.isEnabledFor(_INFO): - _log.info("received suspend event") - _suspend_callback() + if _suspend_callback: + if _log.isEnabledFor(_INFO): + _log.info("received suspend event") + _suspend_callback() _resume_callback = None + + def _resume(): - if _resume_callback: - if _log.isEnabledFor(_INFO): - _log.info("received resume event") - _resume_callback() + if _resume_callback: + if _log.isEnabledFor(_INFO): + _log.info("received resume event") + _resume_callback() + def _suspend_or_resume(suspend): - _suspend() if suspend else _resume() + _suspend() if suspend else _resume() + def watch(on_resume_callback=None, on_suspend_callback=None): - """Register callback for suspend/resume events. + """Register callback for suspend/resume events. They are called only if the system DBus is running, and the UPower daemon is available.""" - global _resume_callback, _suspend_callback - _suspend_callback = on_suspend_callback - _resume_callback = on_resume_callback + global _resume_callback, _suspend_callback + _suspend_callback = on_suspend_callback + _resume_callback = on_resume_callback try: - import dbus + import dbus - _UPOWER_BUS = 'org.freedesktop.UPower' - _UPOWER_INTERFACE = 'org.freedesktop.UPower' - _LOGIND_BUS = 'org.freedesktop.login1' - _LOGIND_INTERFACE = 'org.freedesktop.login1.Manager' + _UPOWER_BUS = 'org.freedesktop.UPower' + _UPOWER_INTERFACE = 'org.freedesktop.UPower' + _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) + # integration into the main GLib loop + from dbus.mainloop.glib import DBusGMainLoop + DBusGMainLoop(set_as_default=True) - bus = dbus.SystemBus() - assert bus + bus = dbus.SystemBus() + assert bus - bus.add_signal_receiver(_suspend, signal_name='Sleeping', - dbus_interface=_UPOWER_INTERFACE, bus_name=_UPOWER_BUS) + bus.add_signal_receiver(_suspend, + signal_name='Sleeping', + dbus_interface=_UPOWER_INTERFACE, + bus_name=_UPOWER_BUS) - bus.add_signal_receiver(_resume, signal_name='Resuming', - dbus_interface=_UPOWER_INTERFACE, bus_name=_UPOWER_BUS) + bus.add_signal_receiver(_resume, + signal_name='Resuming', + dbus_interface=_UPOWER_INTERFACE, + bus_name=_UPOWER_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 _log.isEnabledFor(_INFO): - _log.info("connected to system dbus, watching for suspend/resume events") + if _log.isEnabledFor(_INFO): + _log.info( + "connected to system dbus, watching for suspend/resume events") except: - # Either: - # - the dbus library is not available - # - the system dbus is not running - _log.warn("failed to register suspend/resume callbacks") - pass + # Either: + # - the dbus library is not available + # - the system dbus is not running + _log.warn("failed to register suspend/resume callbacks") + pass diff --git a/setup.py b/setup.py index d8a6a57b..52c8c689 100755 --- a/setup.py +++ b/setup.py @@ -14,56 +14,61 @@ NAME = 'Solaar' def _data_files(): - from os.path import dirname as _dirname + from os.path import dirname as _dirname - yield 'share/solaar/icons', _glob('share/solaar/icons/solaar*.svg') - yield 'share/solaar/icons', _glob('share/solaar/icons/light_*.png') - yield 'share/icons/hicolor/scalable/apps', ['share/solaar/icons/solaar.svg'] + yield 'share/solaar/icons', _glob('share/solaar/icons/solaar*.svg') + yield 'share/solaar/icons', _glob('share/solaar/icons/light_*.png') + yield 'share/icons/hicolor/scalable/apps', [ + 'share/solaar/icons/solaar.svg' + ] - for mo in _glob('share/locale/*/LC_MESSAGES/solaar.mo'): - yield _dirname(mo), [mo] + for mo in _glob('share/locale/*/LC_MESSAGES/solaar.mo'): + yield _dirname(mo), [mo] - yield 'share/applications', ['share/applications/solaar.desktop'] - yield autostart_path, ['share/autostart/solaar.desktop'] - yield '/etc/udev/rules.d', ['rules.d/42-logitech-unify-permissions.rules'] + yield 'share/applications', ['share/applications/solaar.desktop'] + yield autostart_path, ['share/autostart/solaar.desktop'] + yield '/etc/udev/rules.d', ['rules.d/42-logitech-unify-permissions.rules'] - del _dirname + del _dirname -setup(name=NAME.lower(), - version=__version__, - description='Linux devices manager for the Logitech Unifying Receiver.', - long_description=''' +setup( + name=NAME.lower(), + version=__version__, + description='Linux devices manager for the Logitech Unifying Receiver.', + long_description=''' Solaar is a Linux device manager for Logitech's Unifying Receiver peripherals. It is able to pair/unpair devices with the receiver, for many devices show battery status, and show and modify some of the modifiable features of devices. '''.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', - ], + 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', + ], + 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.2', - install_requires=['pyudev (>= 0.13)', ], - package_dir={'': 'lib'}, - packages=['hidapi', 'logitech_receiver', 'solaar', 'solaar.ui', 'solaar.cli'], - data_files=list(_data_files()), - scripts=_glob('bin/*'), - ) + # 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.2', + install_requires=[ + 'pyudev (>= 0.13)', + ], + package_dir={'': 'lib'}, + packages=[ + 'hidapi', 'logitech_receiver', 'solaar', 'solaar.ui', 'solaar.cli' + ], + data_files=list(_data_files()), + scripts=_glob('bin/*'), +) diff --git a/tools/hidconsole b/tools/hidconsole index ebc59f30..a497aa1b 100755 --- a/tools/hidconsole +++ b/tools/hidconsole @@ -6,17 +6,18 @@ from __future__ import absolute_import def init_paths(): - """Make the app work in the source tree.""" - import sys - import os.path as _path + """Make the app work in the source tree.""" + import sys + import os.path as _path - 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 + 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__': - init_paths() - from hidapi import hidconsole - hidconsole.main() + init_paths() + from hidapi import hidconsole + hidconsole.main() diff --git a/tools/monitor.py b/tools/monitor.py index a838020b..a3e9f079 100644 --- a/tools/monitor.py +++ b/tools/monitor.py @@ -4,7 +4,7 @@ from __future__ import absolute_import, division, print_function, unicode_literals import sys -sys.path += (sys.path[0] + '/../lib',) +sys.path += (sys.path[0] + '/../lib', ) import hidapi from logitech.unifying_receiver.base import DEVICE_UNIFYING_RECEIVER @@ -13,11 +13,8 @@ from logitech.unifying_receiver.base import DEVICE_NANO_RECEIVER def print_event(action, device): - print ("~~~~ device [%s] %s" % (action, device)) + print("~~~~ device [%s] %s" % (action, device)) -hidapi.monitor(print_event, - DEVICE_UNIFYING_RECEIVER, - DEVICE_UNIFYING_RECEIVER_2, - DEVICE_NANO_RECEIVER - ) +hidapi.monitor(print_event, DEVICE_UNIFYING_RECEIVER, + DEVICE_UNIFYING_RECEIVER_2, DEVICE_NANO_RECEIVER)