yapf: change code style to yapf

Signed-off-by: Filipe Laíns <lains@archlinux.org>
This commit is contained in:
Filipe Laíns 2020-07-02 13:49:34 +01:00 committed by Filipe Laíns
parent cab523e122
commit 72a8d311bc
44 changed files with 8093 additions and 7034 deletions

View File

@ -22,36 +22,38 @@ from __future__ import absolute_import, unicode_literals
def init_paths(): def init_paths():
"""Make the app work in the source tree.""" """Make the app work in the source tree."""
import sys import sys
import os.path as _path import os.path as _path
# Python 2 need conversion from utf-8 filenames # Python 2 need conversion from utf-8 filenames
# Python 3 might have problems converting back to UTF-8 in case of Unicode surrogates # Python 3 might have problems converting back to UTF-8 in case of Unicode surrogates
try: try:
if sys.version_info < (3,): if sys.version_info < (3, ):
decoded_path = sys.path[0].decode(sys.getfilesystemencoding()) decoded_path = sys.path[0].decode(sys.getfilesystemencoding())
else: else:
decoded_path = sys.path[0] decoded_path = sys.path[0]
sys.path[0].encode(sys.getfilesystemencoding()) sys.path[0].encode(sys.getfilesystemencoding())
except UnicodeError: 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.stderr.write(
sys.exit(1) '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), '..')) prefix = _path.normpath(_path.join(_path.realpath(decoded_path), '..'))
src_lib = _path.join(prefix, 'lib') src_lib = _path.join(prefix, 'lib')
share_lib = _path.join(prefix, 'share', 'solaar', 'lib') share_lib = _path.join(prefix, 'share', 'solaar', 'lib')
for location in src_lib, share_lib: for location in src_lib, share_lib:
init_py = _path.join(location, 'solaar', '__init__.py') init_py = _path.join(location, 'solaar', '__init__.py')
# print ("sys.path[0]: checking", init_py) # print ("sys.path[0]: checking", init_py)
if _path.exists(init_py): if _path.exists(init_py):
# print ("sys.path[0]: found", location, "replacing", sys.path[0]) # print ("sys.path[0]: found", location, "replacing", sys.path[0])
sys.path[0] = location sys.path[0] = location
break break
if __name__ == '__main__': if __name__ == '__main__':
init_paths() init_paths()
import solaar.gtk import solaar.gtk
solaar.gtk.main() solaar.gtk.main()

View File

@ -22,22 +22,24 @@ from __future__ import absolute_import, unicode_literals
def init_paths(): def init_paths():
"""Make the app work in the source tree.""" """Make the app work in the source tree."""
import sys import sys
import os.path as _path import os.path as _path
prefix = _path.normpath(_path.join(_path.realpath(sys.path[0]), '..')) prefix = _path.normpath(_path.join(_path.realpath(sys.path[0]), '..'))
src_lib = _path.join(prefix, 'lib') src_lib = _path.join(prefix, 'lib')
share_lib = _path.join(prefix, 'share', 'solaar', 'lib') share_lib = _path.join(prefix, 'share', 'solaar', 'lib')
for location in src_lib, share_lib: for location in src_lib, share_lib:
init_py = _path.join(location, 'solaar', '__init__.py') init_py = _path.join(location, 'solaar', '__init__.py')
if _path.exists(init_py): if _path.exists(init_py):
sys.path[0] = location sys.path[0] = location
break break
if __name__ == '__main__': if __name__ == '__main__':
print ('WARNING: solaar-cli is deprecated; use solaar with the usual arguments') print(
init_paths() 'WARNING: solaar-cli is deprecated; use solaar with the usual arguments'
import solaar.cli )
solaar.cli.run() init_paths()
import solaar.cli
solaar.cli.run()

View File

@ -16,7 +16,6 @@
## You should have received a copy of the GNU General Public License along ## You should have received a copy of the GNU General Public License along
## with this program; if not, write to the Free Software Foundation, Inc., ## with this program; if not, write to the Free Software Foundation, Inc.,
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
"""Generic Human Interface Device API.""" """Generic Human Interface Device API."""
from __future__ import absolute_import, division, print_function, unicode_literals 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' __version__ = '0.9'
from hidapi.udev import ( from hidapi.udev import (
enumerate, enumerate,
open, open,
close, close,
open_path, open_path,
monitor_glib, monitor_glib,
read, read,
write, write,
get_manufacturer, get_manufacturer,
get_product, get_product,
get_serial, get_serial,
) )

View File

@ -31,10 +31,10 @@ import hidapi as _hid
# #
try: try:
read_packet = raw_input read_packet = raw_input
except NameError: except NameError:
# Python 3 equivalent of raw_input # Python 3 equivalent of raw_input
read_packet = input read_packet = input
interactive = os.isatty(0) interactive = os.isatty(0)
prompt = '?? Input: ' if interactive else '' prompt = '?? Input: ' if interactive else ''
@ -42,18 +42,18 @@ start_time = time.time()
strhex = lambda d: hexlify(d).decode('ascii').upper() strhex = lambda d: hexlify(d).decode('ascii').upper()
try: try:
unicode unicode
# this is certanly Python 2 # this is certanly Python 2
is_string = lambda d: isinstance(d, unicode) is_string = lambda d: isinstance(d, unicode)
# no easy way to distinguish between b'' and '' :( # no easy way to distinguish between b'' and '' :(
# or (isinstance(d, str) \ # 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(0x00, 0x1F))) \
# and not any((chr(k) in d for k in range(0x80, 0xFF))) \ # and not any((chr(k) in d for k in range(0x80, 0xFF))) \
# ) # )
except: except:
# this is certanly Python 3 # this is certanly Python 3
# In Py3, unicode and str are equal (the unicode object does not exist) # In Py3, unicode and str are equal (the unicode object does not exist)
is_string = lambda d: isinstance(d, str) is_string = lambda d: isinstance(d, str)
# #
# #
@ -63,196 +63,215 @@ from threading import Lock
print_lock = Lock() print_lock = Lock()
del Lock del Lock
def _print(marker, data, scroll=False): def _print(marker, data, scroll=False):
t = time.time() - start_time t = time.time() - start_time
if is_string(data): if is_string(data):
s = marker + ' ' + data s = marker + ' ' + data
else: else:
hexs = strhex(data) hexs = strhex(data)
s = '%s (% 8.3f) [%s %s %s %s] %s' % (marker, t, hexs[0:2], hexs[2:4], hexs[4:8], hexs[8:], repr(data)) s = '%s (% 8.3f) [%s %s %s %s] %s' % (marker, t, hexs[0:2], hexs[2:4],
hexs[4:8], hexs[8:], repr(data))
with print_lock: with print_lock:
# allow only one thread at a time to write to the console, otherwise # allow only one thread at a time to write to the console, otherwise
# the output gets garbled, especially with ANSI codes. # the output gets garbled, especially with ANSI codes.
if interactive and scroll: if interactive and scroll:
# scroll the entire screen above the current line up by 1 line # scroll the entire screen above the current line up by 1 line
sys.stdout.write('\033[s' # save cursor position sys.stdout.write('\033[s' # save cursor position
'\033[S' # scroll up '\033[S' # scroll up
'\033[A' # cursor up '\033[A' # cursor up
'\033[L' # insert 1 line '\033[L' # insert 1 line
'\033[G') # move cursor to column 1 '\033[G') # move cursor to column 1
sys.stdout.write(s) sys.stdout.write(s)
if interactive and scroll: if interactive and scroll:
# restore cursor position # restore cursor position
sys.stdout.write('\033[u') sys.stdout.write('\033[u')
else: else:
sys.stdout.write('\n') sys.stdout.write('\n')
# flush stdout manually... # flush stdout manually...
# because trying to open stdin/out unbuffered programmatically # because trying to open stdin/out unbuffered programmatically
# works much too differently in Python 2/3 # works much too differently in Python 2/3
sys.stdout.flush() sys.stdout.flush()
def _error(text, scroll=False): def _error(text, scroll=False):
_print('!!', text, scroll) _print('!!', text, scroll)
def _continuous_read(handle, timeout=2000): def _continuous_read(handle, timeout=2000):
while True: while True:
try: try:
reply = _hid.read(handle, 128, timeout) reply = _hid.read(handle, 128, timeout)
except OSError as e: except OSError as e:
_error("Read failed, aborting: " + str(e), True) _error("Read failed, aborting: " + str(e), True)
break break
assert reply is not None assert reply is not None
if reply: if reply:
_print('>>', reply, True) _print('>>', reply, True)
def _validate_input(line, hidpp=False): def _validate_input(line, hidpp=False):
try: try:
data = unhexlify(line.encode('ascii')) data = unhexlify(line.encode('ascii'))
except Exception as e: except Exception as e:
_error("Invalid input: " + str(e)) _error("Invalid input: " + str(e))
return None return None
if hidpp: if hidpp:
if len(data) < 4: if len(data) < 4:
_error("Invalid HID++ request: need at least 4 bytes") _error("Invalid HID++ request: need at least 4 bytes")
return None return None
if data[:1] not in b'\x10\x11': if data[:1] not in b'\x10\x11':
_error("Invalid HID++ request: first byte must be 0x10 or 0x11") _error("Invalid HID++ request: first byte must be 0x10 or 0x11")
return None return None
if data[1:2] not in b'\xFF\x01\x02\x03\x04\x05\x06': 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") _error(
return None "Invalid HID++ request: second byte must be 0xFF or one of 0x01..0x06"
if data[:1] == b'\x10': )
if len(data) > 7: return None
_error("Invalid HID++ request: maximum length of a 0x10 request is 7 bytes") if data[:1] == b'\x10':
return None if len(data) > 7:
while len(data) < 7: _error(
data = (data + b'\x00' * 7)[:7] "Invalid HID++ request: maximum length of a 0x10 request is 7 bytes"
elif data[:1] == b'\x11': )
if len(data) > 20: return None
_error("Invalid HID++ request: maximum length of a 0x11 request is 20 bytes") while len(data) < 7:
return None data = (data + b'\x00' * 7)[:7]
while len(data) < 20: elif data[:1] == b'\x11':
data = (data + b'\x00' * 20)[:20] 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): def _open(args):
device = args.device device = args.device
if args.hidpp and not device: if args.hidpp and not device:
for d in _hid.enumerate(vendor_id=0x046d): for d in _hid.enumerate(vendor_id=0x046d):
if d.driver == 'logitech-djreceiver': if d.driver == 'logitech-djreceiver':
device = d.path device = d.path
break break
if not device: if not device:
sys.exit("!! No HID++ receiver found.") sys.exit("!! No HID++ receiver found.")
if not device: if not device:
sys.exit("!! Device path required.") sys.exit("!! Device path required.")
print (".. Opening device", device) print(".. Opening device", device)
handle = _hid.open_path(device) handle = _hid.open_path(device)
if not handle: if not handle:
sys.exit("!! Failed to open %s, aborting." % device) sys.exit("!! Failed to open %s, aborting." % device)
print (".. Opened handle %r, vendor %r product %r serial %r." % ( print(".. Opened handle %r, vendor %r product %r serial %r." %
handle, (handle, _hid.get_manufacturer(handle), _hid.get_product(handle),
_hid.get_manufacturer(handle), _hid.get_serial(handle)))
_hid.get_product(handle), if args.hidpp:
_hid.get_serial(handle))) if _hid.get_manufacturer(handle) != b'Logitech':
if args.hidpp: sys.exit("!! Only Logitech devices support the HID++ protocol.")
if _hid.get_manufacturer(handle) != b'Logitech': print(".. HID++ validation enabled.")
sys.exit("!! Only Logitech devices support the HID++ protocol.") else:
print (".. HID++ validation enabled.") if (_hid.get_manufacturer(handle) == b'Logitech'
else: and b'Receiver' in _hid.get_product(handle)):
if (_hid.get_manufacturer(handle) == b'Logitech' and args.hidpp = True
b'Receiver' in _hid.get_product(handle)): print(".. Logitech receiver detected, HID++ validation enabled.")
args.hidpp = True
print (".. Logitech receiver detected, HID++ validation enabled.") return handle
return handle
# #
# #
# #
def _parse_arguments(): def _parse_arguments():
import argparse import argparse
arg_parser = argparse.ArgumentParser() arg_parser = argparse.ArgumentParser()
arg_parser.add_argument('--history', help="history file (default ~/.hidconsole-history)") arg_parser.add_argument(
arg_parser.add_argument('--hidpp', action='store_true', help="ensure input data is a valid HID++ request") '--history', help="history file (default ~/.hidconsole-history)")
arg_parser.add_argument('device', nargs='?', help="linux device to connect to (/dev/hidrawX); " arg_parser.add_argument('--hidpp',
"may be omitted if --hidpp is given, in which case it looks for the first Logitech receiver") action='store_true',
return arg_parser.parse_args() 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(): def main():
args = _parse_arguments() args = _parse_arguments()
handle = _open(args) handle = _open(args)
if interactive: if interactive:
print (".. Press ^C/^D to exit, or type hex bytes to write to the device.") print(
".. Press ^C/^D to exit, or type hex bytes to write to the device."
)
import readline import readline
if args.history is None: if args.history is None:
import os.path import os.path
args.history = os.path.join(os.path.expanduser('~'), '.hidconsole-history') args.history = os.path.join(os.path.expanduser('~'),
try: '.hidconsole-history')
readline.read_history_file(args.history) try:
except: readline.read_history_file(args.history)
# file may not exist yet except:
pass # file may not exist yet
pass
try: try:
from threading import Thread from threading import Thread
t = Thread(target=_continuous_read, args=(handle,)) t = Thread(target=_continuous_read, args=(handle, ))
t.daemon = True t.daemon = True
t.start() t.start()
if interactive: if interactive:
# move the cursor at the bottom of the screen # move the cursor at the bottom of the screen
sys.stdout.write('\033[300B') # move cusor at most 300 lines down, don't scroll sys.stdout.write(
'\033[300B') # move cusor at most 300 lines down, don't scroll
while t.is_alive(): while t.is_alive():
line = read_packet(prompt) line = read_packet(prompt)
line = line.strip().replace(' ', '') line = line.strip().replace(' ', '')
# print ("line", line) # print ("line", line)
if not line: if not line:
continue continue
data = _validate_input(line, args.hidpp) data = _validate_input(line, args.hidpp)
if data is None: if data is None:
continue continue
_print('<<', data) _print('<<', data)
_hid.write(handle, data) _hid.write(handle, data)
# wait for some kind of reply # wait for some kind of reply
if args.hidpp and not interactive: if args.hidpp and not interactive:
rlist, wlist, xlist = _select([handle], [], [], 1) rlist, wlist, xlist = _select([handle], [], [], 1)
if data[1:2] == b'\xFF': if data[1:2] == b'\xFF':
# the receiver will reply very fast, in a few milliseconds # the receiver will reply very fast, in a few milliseconds
time.sleep(0.010) time.sleep(0.010)
else: else:
# the devices might reply quite slow # the devices might reply quite slow
time.sleep(0.700) time.sleep(0.700)
except EOFError: except EOFError:
if interactive: if interactive:
print ("") print("")
else: else:
time.sleep(1) time.sleep(1)
finally: finally:
print (".. Closing handle %r" % handle) print(".. Closing handle %r" % handle)
_hid.close(handle) _hid.close(handle)
if interactive: if interactive:
readline.write_history_file(args.history) readline.write_history_file(args.history)
if __name__ == '__main__': if __name__ == '__main__':
main() main()

View File

@ -16,7 +16,6 @@
## You should have received a copy of the GNU General Public License along ## You should have received a copy of the GNU General Public License along
## with this program; if not, write to the Free Software Foundation, Inc., ## with this program; if not, write to the Free Software Foundation, Inc.,
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
"""Generic Human Interface Device API. """Generic Human Interface Device API.
It is currently a partial pure-Python implementation of the native HID 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 Context as _Context, Monitor as _Monitor, Device as _Device
from pyudev import DeviceNotFoundError from pyudev import DeviceNotFoundError
native_implementation = 'udev' native_implementation = 'udev'
# the tuple object we'll expose when enumerating devices # the tuple object we'll expose when enumerating devices
from collections import namedtuple from collections import namedtuple
DeviceInfo = namedtuple('DeviceInfo', [ DeviceInfo = namedtuple('DeviceInfo', [
'path', 'path',
'vendor_id', 'vendor_id',
'product_id', 'product_id',
'serial', 'serial',
'release', 'release',
'manufacturer', 'manufacturer',
'product', 'product',
'interface', 'interface',
'driver', 'driver',
]) ])
del namedtuple del namedtuple
# #
# exposed API # exposed API
# docstrings mostly copied from hidapi.h # docstrings mostly copied from hidapi.h
# #
def init(): 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. implementation.
:returns: ``True``. :returns: ``True``.
""" """
return True return True
def exit(): 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. implementation.
:returns: ``True``. :returns: ``True``.
""" """
return True return True
# The filter is used to determine whether this is a device of interest to Solaar # The filter is used to determine whether this is a device of interest to Solaar
def _match(action, device, filter): def _match(action, device, filter):
vendor_id=filter.get('vendor_id') vendor_id = filter.get('vendor_id')
product_id=filter.get('product_id') product_id = filter.get('product_id')
interface_number=filter.get('usb_interface') interface_number = filter.get('usb_interface')
hid_driver=filter.get('hid_driver') hid_driver = filter.get('hid_driver')
usb_device = device.find_parent('usb', 'usb_device') usb_device = device.find_parent('usb', 'usb_device')
# print ("* parent", action, device, "usb:", usb_device) # print ("* parent", action, device, "usb:", usb_device)
if not usb_device: if not usb_device:
return return
vid = usb_device.get('ID_VENDOR_ID') vid = usb_device.get('ID_VENDOR_ID')
pid = usb_device.get('ID_MODEL_ID') pid = usb_device.get('ID_MODEL_ID')
if vid is None or pid is None: 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 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 if not ((vendor_id is None or vendor_id == int(vid, 16)) and
(product_id is None or product_id == int(pid, 16))): (product_id is None or product_id == int(pid, 16))):
return return
if action == 'add': if action == 'add':
hid_device = device.find_parent('hid') hid_device = device.find_parent('hid')
if not hid_device: if not hid_device:
return return
hid_driver_name = hid_device.get('DRIVER') hid_driver_name = hid_device.get('DRIVER')
# print ("** found hid", action, device, "hid:", hid_device, hid_driver_name) # print ("** found hid", action, device, "hid:", hid_device, hid_driver_name)
if hid_driver: if hid_driver:
if isinstance(hid_driver, tuple): if isinstance(hid_driver, tuple):
if hid_driver_name not in hid_driver: if hid_driver_name not in hid_driver:
return return
elif hid_driver_name != hid_driver: elif hid_driver_name != hid_driver:
return return
intf_device = device.find_parent('usb', 'usb_interface') intf_device = device.find_parent('usb', 'usb_interface')
# print ("*** usb interface", action, device, "usb_interface:", intf_device) # print ("*** usb interface", action, device, "usb_interface:", intf_device)
if interface_number is None: if interface_number is None:
usb_interface = None if intf_device is None else intf_device.attributes.asint('bInterfaceNumber') usb_interface = None if intf_device is None else intf_device.attributes.asint(
else: 'bInterfaceNumber')
usb_interface = None if intf_device is None else intf_device.attributes.asint('bInterfaceNumber') else:
if usb_interface is None or interface_number != usb_interface: usb_interface = None if intf_device is None else intf_device.attributes.asint(
return 'bInterfaceNumber')
if usb_interface is None or interface_number != usb_interface:
return
attrs = usb_device.attributes attrs = usb_device.attributes
d_info = DeviceInfo(path=device.device_node, d_info = DeviceInfo(path=device.device_node,
vendor_id=vid[-4:], vendor_id=vid[-4:],
product_id=pid[-4:], product_id=pid[-4:],
serial=hid_device.get('HID_UNIQ'), serial=hid_device.get('HID_UNIQ'),
release=attrs.get('bcdDevice'), release=attrs.get('bcdDevice'),
manufacturer=attrs.get('manufacturer'), manufacturer=attrs.get('manufacturer'),
product=attrs.get('product'), product=attrs.get('product'),
interface=usb_interface, interface=usb_interface,
driver=hid_driver_name) driver=hid_driver_name)
return d_info return d_info
elif action == 'remove': elif action == 'remove':
# print (dict(device), dict(usb_device)) # print (dict(device), dict(usb_device))
d_info = DeviceInfo(path=device.device_node, d_info = DeviceInfo(path=device.device_node,
vendor_id=vid[-4:], vendor_id=vid[-4:],
product_id=pid[-4:], product_id=pid[-4:],
serial=None, serial=None,
release=None, release=None,
manufacturer=None, manufacturer=None,
product=None, product=None,
interface=None, interface=None,
driver=None) driver=None)
return d_info return d_info
def monitor_glib(callback, *device_filters): def monitor_glib(callback, *device_filters):
from gi.repository import GLib from gi.repository import GLib
c = _Context() c = _Context()
# already existing devices # already existing devices
# for device in c.list_devices(subsystem='hidraw'): # for device in c.list_devices(subsystem='hidraw'):
# # print (device, dict(device), dict(device.attributes)) # # print (device, dict(device), dict(device.attributes))
# for filter in device_filters: # for filter in device_filters:
# d_info = _match('add', device, *filter) # d_info = _match('add', device, *filter)
# if d_info: # if d_info:
# GLib.idle_add(callback, 'add', d_info) # GLib.idle_add(callback, 'add', d_info)
# break # break
m = _Monitor.from_netlink(c) m = _Monitor.from_netlink(c)
m.filter_by(subsystem='hidraw') m.filter_by(subsystem='hidraw')
def _process_udev_event(monitor, condition, cb, filters): def _process_udev_event(monitor, condition, cb, filters):
if condition == GLib.IO_IN: if condition == GLib.IO_IN:
event = monitor.receive_device() event = monitor.receive_device()
if event: if event:
action, device = event action, device = event
# print ("***", action, device) # print ("***", action, device)
if action == 'add': if action == 'add':
for filter in filters: for filter in filters:
d_info = _match(action, device, filter) d_info = _match(action, device, filter)
if d_info: if d_info:
GLib.idle_add(cb, action, d_info) GLib.idle_add(cb, action, d_info)
break break
elif action == 'remove': elif action == 'remove':
# the GLib notification does _not_ match! # the GLib notification does _not_ match!
pass pass
return True return True
try: try:
# io_add_watch_full may not be available... # 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) GLib.io_add_watch_full(m, GLib.PRIORITY_LOW, GLib.IO_IN,
# print ("did io_add_watch_full") _process_udev_event, callback, device_filters)
except AttributeError: # print ("did io_add_watch_full")
try: except AttributeError:
# and the priority parameter appeared later in the API try:
GLib.io_add_watch(m, GLib.PRIORITY_LOW, GLib.IO_IN, _process_udev_event, callback, device_filters) # and the priority parameter appeared later in the API
# print ("did io_add_watch with priority") GLib.io_add_watch(m, GLib.PRIORITY_LOW, GLib.IO_IN,
except: _process_udev_event, callback, device_filters)
GLib.io_add_watch(m, GLib.IO_IN, _process_udev_event, callback, device_filters) # print ("did io_add_watch with priority")
# print ("did io_add_watch") 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): def enumerate(usb_id):
"""Enumerate the HID Devices. """Enumerate the HID Devices.
List all the HID devices attached to the system, optionally filtering by List all the HID devices attached to the system, optionally filtering by
vendor_id, product_id, and/or interface_number. vendor_id, product_id, and/or interface_number.
@ -206,48 +208,48 @@ def enumerate(usb_id):
:returns: a list of matching ``DeviceInfo`` tuples. :returns: a list of matching ``DeviceInfo`` tuples.
""" """
for dev in _Context().list_devices(subsystem='hidraw'): for dev in _Context().list_devices(subsystem='hidraw'):
dev_info = _match('add', dev, usb_id) dev_info = _match('add', dev, usb_id)
if dev_info: if dev_info:
yield dev_info yield dev_info
def open(vendor_id, product_id, serial=None): 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. If no serial is provided, the first device with the specified IDs is opened.
:returns: an opaque device handle, or ``None``. :returns: an opaque device handle, or ``None``.
""" """
for device in enumerate(vendor_id, product_id): for device in enumerate(vendor_id, product_id):
if serial is None or serial == device.serial: if serial is None or serial == device.serial:
return open_path(device.path) return open_path(device.path)
def 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 :param device_path: the path of a ``DeviceInfo`` tuple returned by
enumerate(). enumerate().
:returns: an opaque device handle, or ``None``. :returns: an opaque device handle, or ``None``.
""" """
assert device_path assert device_path
assert device_path.startswith('/dev/hidraw') assert device_path.startswith('/dev/hidraw')
return _os.open(device_path, _os.O_RDWR | _os.O_SYNC) return _os.open(device_path, _os.O_RDWR | _os.O_SYNC)
def close(device_handle): def close(device_handle):
"""Close a HID device. """Close a HID device.
:param device_handle: a device handle returned by open() or open_path(). :param device_handle: a device handle returned by open() or open_path().
""" """
assert device_handle assert device_handle
_os.close(device_handle) _os.close(device_handle)
def write(device_handle, data): 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 device_handle: a device handle returned by open() or open_path().
:param data: the data bytes to send including the report number as the :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 one exists. If it does not, it will send the data through
the Control Endpoint (Endpoint 0). the Control Endpoint (Endpoint 0).
""" """
assert device_handle assert device_handle
assert data assert data
assert isinstance(data, bytes), (repr(data), type(data)) assert isinstance(data, bytes), (repr(data), type(data))
retrycount = 0 retrycount = 0
bytes_written = 0 bytes_written = 0
while(retrycount < 3): while (retrycount < 3):
try: try:
bytes_written = _os.write(device_handle, data) bytes_written = _os.write(device_handle, data)
retrycount += 1 retrycount += 1
except IOError as e: except IOError as e:
if e.errno == _errno.EPIPE: if e.errno == _errno.EPIPE:
sleep(0.1) sleep(0.1)
else: else:
break break
if bytes_written != len(data): if bytes_written != len(data):
raise IOError(_errno.EIO, 'written %d bytes out of expected %d' % (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): 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 device_handle: a device handle returned by open() or open_path().
:param bytes_count: maximum number of bytes to read. :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 :returns: the data packet read, an empty bytes string if a timeout was
reached, or None if there was an error while reading. reached, or None if there was an error while reading.
""" """
assert device_handle assert device_handle
timeout = None if timeout_ms < 0 else timeout_ms / 1000.0 timeout = None if timeout_ms < 0 else timeout_ms / 1000.0
rlist, wlist, xlist = _select([device_handle], [], [device_handle], timeout) rlist, wlist, xlist = _select([device_handle], [], [device_handle],
timeout)
if xlist: if xlist:
assert xlist == [device_handle] assert xlist == [device_handle]
raise IOError(_errno.EIO, 'exception on file descriptor %d' % device_handle) raise IOError(_errno.EIO,
'exception on file descriptor %d' % device_handle)
if rlist: if rlist:
assert rlist == [device_handle] assert rlist == [device_handle]
data = _os.read(device_handle, bytes_count) data = _os.read(device_handle, bytes_count)
assert data is not None assert data is not None
assert isinstance(data, bytes), (repr(data), type(data)) assert isinstance(data, bytes), (repr(data), type(data))
return data return data
else: else:
return b'' return b''
_DEVICE_STRINGS = { _DEVICE_STRINGS = {
0: 'manufacturer', 0: 'manufacturer',
1: 'product', 1: 'product',
2: 'serial', 2: 'serial',
} }
def get_manufacturer(device_handle): 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(). :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): 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(). :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): 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(). :param device_handle: a device handle returned by open() or open_path().
""" """
serial = get_indexed_string(device_handle, 2) serial = get_indexed_string(device_handle, 2)
if serial is not None: if serial is not None:
return ''.join(hex(ord(c)) for c in serial) return ''.join(hex(ord(c)) for c in serial)
def get_indexed_string(device_handle, index): 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. 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 :returns: the value corresponding to index, or None if no value found
:rtype: bytes or NoneType :rtype: bytes or NoneType
""" """
try: try:
key = _DEVICE_STRINGS[index] key = _DEVICE_STRINGS[index]
except KeyError: except KeyError:
return None return None
assert device_handle assert device_handle
stat = _os.fstat(device_handle) stat = _os.fstat(device_handle)
try: try:
dev = _Device.from_device_number(_Context(), 'char', stat.st_rdev) dev = _Device.from_device_number(_Context(), 'char', stat.st_rdev)
except (DeviceNotFoundError, ValueError): except (DeviceNotFoundError, ValueError):
return None return None
hid_dev = dev.find_parent('hid') hid_dev = dev.find_parent('hid')
if hid_dev: if hid_dev:
assert 'HID_ID' in hid_dev assert 'HID_ID' in hid_dev
bus, _ignore, _ignore = hid_dev['HID_ID'].split(':') bus, _ignore, _ignore = hid_dev['HID_ID'].split(':')
if bus == '0003': # USB if bus == '0003': # USB
usb_dev = dev.find_parent('usb', 'usb_device') usb_dev = dev.find_parent('usb', 'usb_device')
assert usb_dev assert usb_dev
return usb_dev.attributes.get(key) return usb_dev.attributes.get(key)
elif bus == '0005': # BLUETOOTH elif bus == '0005': # BLUETOOTH
# TODO # TODO
pass pass

View File

@ -16,7 +16,6 @@
## You should have received a copy of the GNU General Public License along ## You should have received a copy of the GNU General Public License along
## with this program; if not, write to the Free Software Foundation, Inc., ## with this program; if not, write to the Free Software Foundation, Inc.,
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
"""Low-level interface for devices connected through a Logitech Universal """Low-level interface for devices connected through a Logitech Universal
Receiver (UR). Receiver (UR).
@ -43,10 +42,8 @@ _log.setLevel(logging.root.level)
del logging del logging
__version__ = '0.9' __version__ = '0.9'
from .common import strhex from .common import strhex
from .base import NoReceiver, NoSuchDevice, DeviceUnreachable from .base import NoReceiver, NoSuchDevice, DeviceUnreachable
from .receiver import Receiver, PairedDevice from .receiver import Receiver, PairedDevice

View File

@ -29,7 +29,6 @@ from logging import getLogger, DEBUG as _DEBUG
_log = getLogger(__name__) _log = getLogger(__name__)
del getLogger del getLogger
from .common import strhex as _strhex, KwException as _KwException, pack as _pack from .common import strhex as _strhex, KwException as _KwException, pack as _pack
from . import hidpp10 as _hidpp10 from . import hidpp10 as _hidpp10
from . import hidpp20 as _hidpp20 from . import hidpp20 as _hidpp20
@ -46,13 +45,11 @@ _MAX_READ_SIZE = 32
# mapping from report_id to message length # mapping from report_id to message length
report_lengths = { report_lengths = {
0x10: _SHORT_MESSAGE_SIZE, 0x10: _SHORT_MESSAGE_SIZE,
0x11: _LONG_MESSAGE_SIZE, 0x11: _LONG_MESSAGE_SIZE,
0x20: _MEDIUM_MESSAGE_SIZE, 0x20: _MEDIUM_MESSAGE_SIZE,
0x21: _MAX_READ_SIZE 0x21: _MAX_READ_SIZE
} }
"""Default timeout on read (in seconds).""" """Default timeout on read (in seconds)."""
DEFAULT_TIMEOUT = 4 DEFAULT_TIMEOUT = 4
# the receiver itself should reply very fast, within 500ms # 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. # Exceptions that may be raised by this API.
# #
class NoReceiver(_KwException): class NoReceiver(_KwException):
"""Raised when trying to talk through a previously open handle, when the """Raised when trying to talk through a previously open handle, when the
receiver is no longer available. Should only happen if the receiver is receiver is no longer available. Should only happen if the receiver is
physically disconnected from the machine, or its kernel driver module is physically disconnected from the machine, or its kernel driver module is
unloaded.""" unloaded."""
pass pass
class NoSuchDevice(_KwException): class NoSuchDevice(_KwException):
"""Raised when trying to reach a device number not paired to the receiver.""" """Raised when trying to reach a device number not paired to the receiver."""
pass pass
class DeviceUnreachable(_KwException): class DeviceUnreachable(_KwException):
"""Raised when a request is made to an unreachable (turned off) device.""" """Raised when a request is made to an unreachable (turned off) device."""
pass pass
# #
# #
@ -89,23 +88,26 @@ class DeviceUnreachable(_KwException):
from .base_usb import ALL as _RECEIVER_USB_IDS from .base_usb import ALL as _RECEIVER_USB_IDS
def receivers(): def receivers():
"""List all the Linux devices exposed by the UR attached to the machine.""" """List all the Linux devices exposed by the UR attached to the machine."""
for receiver_usb_id in _RECEIVER_USB_IDS: for receiver_usb_id in _RECEIVER_USB_IDS:
for d in _hid.enumerate(receiver_usb_id): for d in _hid.enumerate(receiver_usb_id):
yield d yield d
def notify_on_receivers_glib(callback): def notify_on_receivers_glib(callback):
"""Watch for matching devices and notifies the callback on the GLib thread.""" """Watch for matching devices and notifies the callback on the GLib thread."""
_hid.monitor_glib(callback, *_RECEIVER_USB_IDS) _hid.monitor_glib(callback, *_RECEIVER_USB_IDS)
# #
# #
# #
def open_path(path): 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. :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 :returns: an open receiver handle if this is the right Linux device, or
``None``. ``None``.
""" """
return _hid.open_path(path) return _hid.open_path(path)
def open(): 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``. :returns: An open file handle for the found receiver, or ``None``.
""" """
for rawdevice in receivers(): for rawdevice in receivers():
handle = open_path(rawdevice.path) handle = open_path(rawdevice.path)
if handle: if handle:
return handle return handle
def close(handle): def close(handle):
"""Closes a HID device handle.""" """Closes a HID device handle."""
if handle: if handle:
try: try:
if isinstance(handle, int): if isinstance(handle, int):
_hid.close(handle) _hid.close(handle)
else: else:
handle.close() handle.close()
# _log.info("closed receiver handle %r", handle) # _log.info("closed receiver handle %r", handle)
return True return True
except: except:
# _log.exception("closing receiver handle %r", handle) # _log.exception("closing receiver handle %r", handle)
pass pass
return False return False
def write(handle, devnumber, data): 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 handle: an open UR handle.
:param devnumber: attached device number. :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 been physically removed from the machine, or the kernel driver has been
unloaded. The handle will be closed automatically. unloaded. The handle will be closed automatically.
""" """
# the data is padded to either 5 or 18 bytes # the data is padded to either 5 or 18 bytes
assert data is not None assert data is not None
assert isinstance(data, bytes), (repr(data), type(data)) assert isinstance(data, bytes), (repr(data), type(data))
if len(data) > _SHORT_MESSAGE_SIZE - 2 or data[:1] == b'\x82': if len(data) > _SHORT_MESSAGE_SIZE - 2 or data[:1] == b'\x82':
wdata = _pack('!BB18s', 0x11, devnumber, data) wdata = _pack('!BB18s', 0x11, devnumber, data)
else: else:
wdata = _pack('!BB5s', 0x10, devnumber, data) wdata = _pack('!BB5s', 0x10, devnumber, data)
if _log.isEnabledFor(_DEBUG): if _log.isEnabledFor(_DEBUG):
_log.debug("(%s) <= w[%02X %02X %s %s]", handle, ord(wdata[:1]), devnumber, _strhex(wdata[2:4]), _strhex(wdata[4:])) _log.debug("(%s) <= w[%02X %02X %s %s]", handle, ord(wdata[:1]),
devnumber, _strhex(wdata[2:4]), _strhex(wdata[4:]))
try: try:
_hid.write(int(handle), wdata) _hid.write(int(handle), wdata)
except Exception as reason: except Exception as reason:
_log.error("write failed, assuming handle %r no longer available", handle) _log.error("write failed, assuming handle %r no longer available",
close(handle) handle)
raise NoReceiver(reason=reason) close(handle)
raise NoReceiver(reason=reason)
def read(handle, timeout=DEFAULT_TIMEOUT): 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. call), to get the reply.
:param: handle open handle to the receiver :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 been physically removed from the machine, or the kernel driver has been
unloaded. The handle will be closed automatically. unloaded. The handle will be closed automatically.
""" """
reply = _read(handle, timeout) reply = _read(handle, timeout)
if reply: if reply:
return reply[1:] return reply[1:]
# sanity checks on message report id and size # sanity checks on message report id and size
def check_message(data) : def check_message(data):
assert isinstance(data, bytes), (repr(data), type(data)) assert isinstance(data, bytes), (repr(data), type(data))
report_id = ord(data[:1]) report_id = ord(data[:1])
if report_id in report_lengths: # is this an HID++ or DJ message? if report_id in report_lengths: # is this an HID++ or DJ message?
if report_lengths.get(report_id) == len(data): if report_lengths.get(report_id) == len(data):
return True return True
else: else:
_log.warn("unexpected message size: report_id %02X message %s" % (report_id, _strhex(data))) _log.warn("unexpected message size: report_id %02X message %s" %
return False (report_id, _strhex(data)))
return False
def _read(handle, timeout): 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`. :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 been physically removed from the machine, or the kernel driver has been
unloaded. The handle will be closed automatically. unloaded. The handle will be closed automatically.
""" """
try: try:
# convert timeout to milliseconds, the hidapi expects it # convert timeout to milliseconds, the hidapi expects it
timeout = int(timeout * 1000) timeout = int(timeout * 1000)
data = _hid.read(int(handle), _MAX_READ_SIZE, timeout) data = _hid.read(int(handle), _MAX_READ_SIZE, timeout)
except Exception as reason: except Exception as reason:
_log.error("read failed, assuming handle %r no longer available", handle) _log.error("read failed, assuming handle %r no longer available",
close(handle) handle)
raise NoReceiver(reason=reason) close(handle)
raise NoReceiver(reason=reason)
if data and check_message(data): # ignore messages that fail check if data and check_message(data): # ignore messages that fail check
report_id = ord(data[:1]) report_id = ord(data[:1])
devnumber = ord(data[1:2]) devnumber = ord(data[1:2])
if _log.isEnabledFor(_DEBUG): if _log.isEnabledFor(_DEBUG):
_log.debug("(%s) => r[%02X %02X %s %s]", handle, report_id, devnumber, _strhex(data[2:4]), _strhex(data[4:])) _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): 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. Used by request() and ping() before their write.
""" """
while True: while True:
try: try:
# read whatever is already in the buffer, if any # read whatever is already in the buffer, if any
data = _hid.read(ihandle, _MAX_READ_SIZE, 0) data = _hid.read(ihandle, _MAX_READ_SIZE, 0)
except Exception as reason: except Exception as reason:
_log.error("read failed, assuming receiver %s no longer available", handle) _log.error("read failed, assuming receiver %s no longer available",
close(handle) handle)
raise NoReceiver(reason=reason) close(handle)
raise NoReceiver(reason=reason)
if data: if data:
if check_message(data): # only process messages that pass check if check_message(data): # only process messages that pass check
report_id = ord(data[:1]) report_id = ord(data[:1])
if notifications_hook: if notifications_hook:
n = make_notification(ord(data[1:2]), data[2:]) n = make_notification(ord(data[1:2]), data[2:])
if n: if n:
notifications_hook(n) notifications_hook(n)
else: else:
# nothing in the input buffer, we're done # nothing in the input buffer, we're done
return return
def make_notification(devnumber, data): 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.""" return a Notification tuple if it is."""
sub_id = ord(data[:1]) sub_id = ord(data[:1])
if sub_id & 0x80 == 0x80: if sub_id & 0x80 == 0x80:
# this is either a HID++1.0 register r/w, or an error reply # this is either a HID++1.0 register r/w, or an error reply
return return
# DJ input records are not notifications # DJ input records are not notifications
# it would be better to check for report_id 0x20 but that information is not sent here # 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): if len(data) == _MEDIUM_MESSAGE_SIZE - 2 and (sub_id < 0x10):
return 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 from collections import namedtuple
_HIDPP_Notification = namedtuple('_HIDPP_Notification', ('devnumber', 'sub_id', 'address', 'data')) _HIDPP_Notification = namedtuple('_HIDPP_Notification',
_HIDPP_Notification.__str__ = lambda self: 'Notification(%d,%02X,%02X,%s)' % (self.devnumber, self.sub_id, self.address, _strhex(self.data)) ('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__ _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 del namedtuple
# #
# #
# #
def request(handle, devnumber, request_id, *params): 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. 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. :returns: the reply data, or ``None`` if some error occurred.
""" """
# import inspect as _inspect # import inspect as _inspect
# print ('\n '.join(str(s) for s in _inspect.stack())) # print ('\n '.join(str(s) for s in _inspect.stack()))
assert isinstance(request_id, int) assert isinstance(request_id, int)
if devnumber != 0xFF and request_id < 0x8000: if devnumber != 0xFF and request_id < 0x8000:
# For HID++ 2.0 feature requests, randomize the SoftwareId to make it # For HID++ 2.0 feature requests, randomize the SoftwareId to make it
# easier to recognize the reply for this request. also, always set the # easier to recognize the reply for this request. also, always set the
# most significant bit (8) in SoftwareId, to make notifications easier # most significant bit (8) in SoftwareId, to make notifications easier
# to distinguish from request replies. # to distinguish from request replies.
# This only applies to peripheral requests, ofc. # This only applies to peripheral requests, ofc.
request_id = (request_id & 0xFFF0) | 0x08 | _random_bits(3) request_id = (request_id & 0xFFF0) | 0x08 | _random_bits(3)
timeout = _RECEIVER_REQUEST_TIMEOUT if devnumber == 0xFF else _DEVICE_REQUEST_TIMEOUT timeout = _RECEIVER_REQUEST_TIMEOUT if devnumber == 0xFF else _DEVICE_REQUEST_TIMEOUT
# be extra patient on long register read # be extra patient on long register read
if request_id & 0xFF00 == 0x8300: if request_id & 0xFF00 == 0x8300:
timeout *= 2 timeout *= 2
if params: if params:
params = b''.join(_pack('B', p) if isinstance(p, int) else p for p in params) params = b''.join(
else: _pack('B', p) if isinstance(p, int) else p for p in params)
params = b'' else:
# if _log.isEnabledFor(_DEBUG): params = b''
# _log.debug("(%s) device %d request_id {%04X} params [%s]", handle, devnumber, request_id, _strhex(params)) # if _log.isEnabledFor(_DEBUG):
request_data = _pack('!H', request_id) + params # _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) ihandle = int(handle)
notifications_hook = getattr(handle, 'notifications_hook', None) notifications_hook = getattr(handle, 'notifications_hook', None)
_skip_incoming(handle, ihandle, notifications_hook) _skip_incoming(handle, ihandle, notifications_hook)
write(ihandle, devnumber, request_data) write(ihandle, devnumber, request_data)
# we consider timeout from this point # we consider timeout from this point
request_started = _timestamp() request_started = _timestamp()
delta = 0 delta = 0
while delta < timeout: while delta < timeout:
reply = _read(handle, timeout) reply = _read(handle, timeout)
if reply: if reply:
report_id, reply_devnumber, reply_data = reply report_id, reply_devnumber, reply_data = reply
if reply_devnumber == devnumber: if reply_devnumber == devnumber:
if report_id == 0x10 and reply_data[:1] == b'\x8F' and reply_data[1:3] == request_data[:2]: if report_id == 0x10 and reply_data[:1] == b'\x8F' and reply_data[
error = ord(reply_data[3:4]) 1:3] == request_data[:2]:
error = ord(reply_data[3:4])
# if error == _hidpp10.ERROR.resource_error: # device unreachable # if error == _hidpp10.ERROR.resource_error: # device unreachable
# _log.warn("(%s) device %d error on request {%04X}: unknown device", handle, devnumber, request_id) # _log.warn("(%s) device %d error on request {%04X}: unknown device", handle, devnumber, request_id)
# raise DeviceUnreachable(number=devnumber, request=request_id) # raise DeviceUnreachable(number=devnumber, request=request_id)
# if error == _hidpp10.ERROR.unknown_device: # unknown device # if error == _hidpp10.ERROR.unknown_device: # unknown device
# _log.error("(%s) device %d error on request {%04X}: unknown device", handle, devnumber, request_id) # _log.error("(%s) device %d error on request {%04X}: unknown device", handle, devnumber, request_id)
# raise NoSuchDevice(number=devnumber, request=request_id) # raise NoSuchDevice(number=devnumber, request=request_id)
if _log.isEnabledFor(_DEBUG): if _log.isEnabledFor(_DEBUG):
_log.debug("(%s) device 0x%02X error on request {%04X}: %d = %s", _log.debug(
handle, devnumber, request_id, error, _hidpp10.ERROR[error]) "(%s) device 0x%02X error on request {%04X}: %d = %s",
return handle, devnumber, request_id, error,
_hidpp10.ERROR[error])
return
if reply_data[:1] == b'\xFF' and reply_data[1:3] == request_data[:2]: if reply_data[:1] == b'\xFF' and reply_data[
# a HID++ 2.0 feature call returned with an error 1:3] == request_data[:2]:
error = ord(reply_data[3:4]) # a HID++ 2.0 feature call returned with an error
_log.error("(%s) device %d error on feature request {%04X}: %d = %s", error = ord(reply_data[3:4])
handle, devnumber, request_id, error, _hidpp20.ERROR[error]) _log.error(
raise _hidpp20.FeatureCallError(number=devnumber, request=request_id, error=error, params=params) "(%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 reply_data[:2] == request_data[:2]:
if request_id & 0xFE00 == 0x8200: if request_id & 0xFE00 == 0x8200:
# long registry r/w should return a long reply # long registry r/w should return a long reply
assert report_id == 0x11 assert report_id == 0x11
elif request_id & 0xFE00 == 0x8000: elif request_id & 0xFE00 == 0x8000:
# short registry r/w should return a short reply # short registry r/w should return a short reply
assert report_id == 0x10 assert report_id == 0x10
if devnumber == 0xFF: if devnumber == 0xFF:
if request_id == 0x83B5 or request_id == 0x81F1: if request_id == 0x83B5 or request_id == 0x81F1:
# these replies have to match the first parameter as well # these replies have to match the first parameter as well
if reply_data[2:3] == params[:1]: if reply_data[2:3] == params[:1]:
return reply_data[2:] return reply_data[2:]
else: else:
# hm, not matching my request, and certainly not a notification # hm, not matching my request, and certainly not a notification
continue continue
else: else:
return reply_data[2:] return reply_data[2:]
else: else:
return reply_data[2:] return reply_data[2:]
else: else:
# a reply was received, but did not match our request in any way # a reply was received, but did not match our request in any way
# reset the timeout starting point # reset the timeout starting point
request_started = _timestamp() request_started = _timestamp()
if notifications_hook: if notifications_hook:
n = make_notification(reply_devnumber, reply_data) n = make_notification(reply_devnumber, reply_data)
if n: if n:
notifications_hook(n) notifications_hook(n)
# elif _log.isEnabledFor(_DEBUG): # elif _log.isEnabledFor(_DEBUG):
# _log.debug("(%s) ignoring reply %02X [%s]", handle, reply_devnumber, _strhex(reply_data)) # _log.debug("(%s) ignoring reply %02X [%s]", handle, reply_devnumber, _strhex(reply_data))
# elif _log.isEnabledFor(_DEBUG): # elif _log.isEnabledFor(_DEBUG):
# _log.debug("(%s) ignoring reply %02X [%s]", handle, reply_devnumber, _strhex(reply_data)) # _log.debug("(%s) ignoring reply %02X [%s]", handle, reply_devnumber, _strhex(reply_data))
delta = _timestamp() - request_started delta = _timestamp() - request_started
# if _log.isEnabledFor(_DEBUG): # if _log.isEnabledFor(_DEBUG):
# _log.debug("(%s) still waiting for reply, delta %f", handle, delta) # _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]", _log.warn("timeout (%0.2f/%0.2f) on device %d request {%04X} params [%s]",
delta, timeout, devnumber, request_id, _strhex(params)) delta, timeout, devnumber, request_id, _strhex(params))
# raise DeviceUnreachable(number=devnumber, request=request_id) # raise DeviceUnreachable(number=devnumber, request=request_id)
def ping(handle, devnumber): 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. :returns: The HID protocol supported by the device, as a floating point number, if the device is active.
""" """
if _log.isEnabledFor(_DEBUG): if _log.isEnabledFor(_DEBUG):
_log.debug("(%s) pinging device %d", handle, devnumber) _log.debug("(%s) pinging device %d", handle, devnumber)
# import inspect as _inspect # import inspect as _inspect
# print ('\n '.join(str(s) for s in _inspect.stack())) # print ('\n '.join(str(s) for s in _inspect.stack()))
assert devnumber != 0xFF assert devnumber != 0xFF
assert devnumber > 0x00 assert devnumber > 0x00
assert devnumber < 0x0F assert devnumber < 0x0F
# randomize the SoftwareId and mark byte to be able to identify the ping # randomize the SoftwareId and mark byte to be able to identify the ping
# reply, and set most significant (0x8) bit in SoftwareId so that the reply # reply, and set most significant (0x8) bit in SoftwareId so that the reply
# is always distinguishable from notifications # is always distinguishable from notifications
request_id = 0x0018 | _random_bits(3) request_id = 0x0018 | _random_bits(3)
request_data = _pack('!HBBB', request_id, 0, 0, _random_bits(8)) request_data = _pack('!HBBB', request_id, 0, 0, _random_bits(8))
ihandle = int(handle) ihandle = int(handle)
notifications_hook = getattr(handle, 'notifications_hook', None) notifications_hook = getattr(handle, 'notifications_hook', None)
_skip_incoming(handle, ihandle, notifications_hook) _skip_incoming(handle, ihandle, notifications_hook)
write(ihandle, devnumber, request_data) write(ihandle, devnumber, request_data)
# we consider timeout from this point # we consider timeout from this point
request_started = _timestamp() request_started = _timestamp()
delta = 0 delta = 0
while delta < _PING_TIMEOUT: while delta < _PING_TIMEOUT:
reply = _read(handle, _PING_TIMEOUT) reply = _read(handle, _PING_TIMEOUT)
if reply: if reply:
report_id, reply_devnumber, reply_data = reply report_id, reply_devnumber, reply_data = reply
if reply_devnumber == devnumber: if reply_devnumber == devnumber:
if reply_data[:2] == request_data[:2] and reply_data[4:5] == request_data[-1:]: if reply_data[:2] == request_data[:2] and reply_data[
# HID++ 2.0+ device, currently connected 4:5] == request_data[-1:]:
return ord(reply_data[2:3]) + ord(reply_data[3:4]) / 10.0 # 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]: if report_id == 0x10 and reply_data[:1] == b'\x8F' and reply_data[
assert reply_data[-1:] == b'\x00' 1:3] == request_data[:2]:
error = ord(reply_data[3:4]) 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 if error == _hidpp10.ERROR.invalid_SubID__command: # a valid reply from a HID++ 1.0 device
return 1.0 return 1.0
if error == _hidpp10.ERROR.resource_error: # device unreachable if error == _hidpp10.ERROR.resource_error: # device unreachable
return return
if error == _hidpp10.ERROR.unknown_device: # no paired device with that number 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) _log.error(
raise NoSuchDevice(number=devnumber, request=request_id) "(%s) device %d error on ping request: unknown device",
handle, devnumber)
raise NoSuchDevice(number=devnumber,
request=request_id)
if notifications_hook: if notifications_hook:
n = make_notification(reply_devnumber, reply_data) n = make_notification(reply_devnumber, reply_data)
if n: if n:
notifications_hook(n) notifications_hook(n)
# elif _log.isEnabledFor(_DEBUG): # elif _log.isEnabledFor(_DEBUG):
# _log.debug("(%s) ignoring reply %02X [%s]", handle, reply_devnumber, _strhex(reply_data)) # _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) _log.warn("(%s) timeout (%0.2f/%0.2f) on device %d ping", handle, delta,
# raise DeviceUnreachable(number=devnumber, request=request_id) _PING_TIMEOUT, devnumber)
# raise DeviceUnreachable(number=devnumber, request=request_id)

View File

@ -22,7 +22,6 @@
from __future__ import absolute_import, division, print_function, unicode_literals from __future__ import absolute_import, division, print_function, unicode_literals
_DRIVER = ('hid-generic', 'generic-usb', 'logitech-djreceiver') _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 # 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? ## currently only one receiver is so marked - should there be more?
_unifying_receiver = lambda product_id: { _unifying_receiver = lambda product_id: {
'vendor_id':0x046d, 'vendor_id': 0x046d,
'product_id':product_id, 'product_id': product_id,
'usb_interface':2, 'usb_interface': 2,
'hid_driver':_DRIVER, 'hid_driver': _DRIVER,
'name':'Unifying Receiver' 'name': 'Unifying Receiver'
} }
_nano_receiver = lambda product_id: { _nano_receiver = lambda product_id: {
'vendor_id':0x046d, 'vendor_id': 0x046d,
'product_id':product_id, 'product_id': product_id,
'usb_interface':1, 'usb_interface': 1,
'hid_driver':_DRIVER, 'hid_driver': _DRIVER,
'name':'Nano Receiver', 'name': 'Nano Receiver',
'may_unpair': False, 'may_unpair': False,
're_pairs': True 're_pairs': True
} }
_nano_receiver_max2 = lambda product_id: { _nano_receiver_max2 = lambda product_id: {
'vendor_id':0x046d, 'vendor_id': 0x046d,
'product_id':product_id, 'product_id': product_id,
'usb_interface':1, 'usb_interface': 1,
'hid_driver':_DRIVER, 'hid_driver': _DRIVER,
'name':'Nano Receiver', 'name': 'Nano Receiver',
'max_devices': 2, 'max_devices': 2,
'may_unpair': False, 'may_unpair': False,
're_pairs': True 're_pairs': True
} }
_nano_receiver_maxn = lambda product_id, max: { _nano_receiver_maxn = lambda product_id, max: {
'vendor_id':0x046d, 'vendor_id': 0x046d,
'product_id':product_id, 'product_id': product_id,
'usb_interface':1, 'usb_interface': 1,
'hid_driver':_DRIVER, 'hid_driver': _DRIVER,
'name':'Nano Receiver', 'name': 'Nano Receiver',
'max_devices': max, 'max_devices': max,
'may_unpair': False, 'may_unpair': False,
're_pairs': True 're_pairs': True
} }
_lenovo_receiver = lambda product_id: { _lenovo_receiver = lambda product_id: {
'vendor_id':0x17ef, 'vendor_id': 0x17ef,
'product_id':product_id, 'product_id': product_id,
'usb_interface':1, 'usb_interface': 1,
'hid_driver':_DRIVER, 'hid_driver': _DRIVER,
'name':'Nano Receiver' 'name': 'Nano Receiver'
} }
_lightspeed_receiver = lambda product_id: { _lightspeed_receiver = lambda product_id: {
'vendor_id':0x046d, 'vendor_id': 0x046d,
'product_id':product_id, 'product_id': product_id,
'usb_interface':2, 'usb_interface': 2,
'hid_driver':_DRIVER, 'hid_driver': _DRIVER,
'name':'Lightspeed Receiver' 'name': 'Lightspeed Receiver'
} }
# standard Unifying receivers (marked with the orange Unifying logo) # standard Unifying receivers (marked with the orange Unifying logo)
UNIFYING_RECEIVER_C52B = _unifying_receiver(0xc52b) UNIFYING_RECEIVER_C52B = _unifying_receiver(0xc52b)
UNIFYING_RECEIVER_C532 = _unifying_receiver(0xc532) UNIFYING_RECEIVER_C532 = _unifying_receiver(0xc532)
# Nano receviers that support the Unifying protocol # 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 receivers that don't support the Unifying protocol
NANO_RECEIVER_C517 = _nano_receiver_maxn(0xc517,6) NANO_RECEIVER_C517 = _nano_receiver_maxn(0xc517, 6)
NANO_RECEIVER_C518 = _nano_receiver(0xc518) NANO_RECEIVER_C518 = _nano_receiver(0xc518)
NANO_RECEIVER_C51A = _nano_receiver(0xc51a) NANO_RECEIVER_C51A = _nano_receiver(0xc51a)
NANO_RECEIVER_C51B = _nano_receiver(0xc51b) NANO_RECEIVER_C51B = _nano_receiver(0xc51b)
NANO_RECEIVER_C521 = _nano_receiver(0xc521) NANO_RECEIVER_C521 = _nano_receiver(0xc521)
NANO_RECEIVER_C525 = _nano_receiver(0xc525) NANO_RECEIVER_C525 = _nano_receiver(0xc525)
NANO_RECEIVER_C526 = _nano_receiver(0xc526) NANO_RECEIVER_C526 = _nano_receiver(0xc526)
NANO_RECEIVER_C52e = _nano_receiver(0xc52e) NANO_RECEIVER_C52e = _nano_receiver(0xc52e)
NANO_RECEIVER_C531 = _nano_receiver(0xc531) NANO_RECEIVER_C531 = _nano_receiver(0xc531)
NANO_RECEIVER_C534 = _nano_receiver_max2(0xc534) NANO_RECEIVER_C534 = _nano_receiver_max2(0xc534)
NANO_RECEIVER_C537 = _nano_receiver(0xc537) NANO_RECEIVER_C537 = _nano_receiver(0xc537)
NANO_RECEIVER_6042 = _lenovo_receiver(0x6042) NANO_RECEIVER_6042 = _lenovo_receiver(0x6042)
# Lightspeed receivers # Lightspeed receivers
LIGHTSPEED_RECEIVER_C539 = _lightspeed_receiver(0xc539) LIGHTSPEED_RECEIVER_C539 = _lightspeed_receiver(0xc539)
LIGHTSPEED_RECEIVER_C53a = _lightspeed_receiver(0xc53a) LIGHTSPEED_RECEIVER_C53a = _lightspeed_receiver(0xc53a)
LIGHTSPEED_RECEIVER_C53f = _lightspeed_receiver(0xc53f) LIGHTSPEED_RECEIVER_C53f = _lightspeed_receiver(0xc53f)
LIGHTSPEED_RECEIVER_C53d = _lightspeed_receiver(0xc53d) LIGHTSPEED_RECEIVER_C53d = _lightspeed_receiver(0xc53d)
del _DRIVER, _unifying_receiver, _nano_receiver, _lenovo_receiver, _lightspeed_receiver del _DRIVER, _unifying_receiver, _nano_receiver, _lenovo_receiver, _lightspeed_receiver
ALL = ( ALL = (
UNIFYING_RECEIVER_C52B, UNIFYING_RECEIVER_C52B,
UNIFYING_RECEIVER_C532, UNIFYING_RECEIVER_C532,
NANO_RECEIVER_ADVANCED, NANO_RECEIVER_ADVANCED,
NANO_RECEIVER_C517, NANO_RECEIVER_C517,
NANO_RECEIVER_C518, NANO_RECEIVER_C518,
NANO_RECEIVER_C51A, NANO_RECEIVER_C51A,
NANO_RECEIVER_C51B, NANO_RECEIVER_C51B,
NANO_RECEIVER_C521, NANO_RECEIVER_C521,
NANO_RECEIVER_C525, NANO_RECEIVER_C525,
NANO_RECEIVER_C526, NANO_RECEIVER_C526,
NANO_RECEIVER_C52e, NANO_RECEIVER_C52e,
NANO_RECEIVER_C531, NANO_RECEIVER_C531,
NANO_RECEIVER_C534, NANO_RECEIVER_C534,
NANO_RECEIVER_C537, NANO_RECEIVER_C537,
NANO_RECEIVER_6042, NANO_RECEIVER_6042,
LIGHTSPEED_RECEIVER_C539, LIGHTSPEED_RECEIVER_C539,
LIGHTSPEED_RECEIVER_C53a, LIGHTSPEED_RECEIVER_C53a,
LIGHTSPEED_RECEIVER_C53f, LIGHTSPEED_RECEIVER_C53f,
LIGHTSPEED_RECEIVER_C53d, LIGHTSPEED_RECEIVER_C53d,
) )
def product_information(usb_id): def product_information(usb_id):
if isinstance(usb_id,str): if isinstance(usb_id, str):
usb_id = int(usb_id,16) usb_id = int(usb_id, 16)
for r in ALL: for r in ALL:
if usb_id == r.get('product_id'): if usb_id == r.get('product_id'):
return r return r
return { } return {}

View File

@ -24,70 +24,71 @@ from __future__ import absolute_import, division, print_function, unicode_litera
from binascii import hexlify as _hexlify from binascii import hexlify as _hexlify
from struct import pack, unpack from struct import pack, unpack
try: try:
unicode unicode
# if Python2, unicode_literals will mess our first (un)pack() argument # if Python2, unicode_literals will mess our first (un)pack() argument
_pack_str = pack _pack_str = pack
_unpack_str = unpack _unpack_str = unpack
pack = lambda x, *args: _pack_str(str(x), *args) pack = lambda x, *args: _pack_str(str(x), *args)
unpack = lambda x, *args: _unpack_str(str(x), *args) unpack = lambda x, *args: _unpack_str(str(x), *args)
is_string = lambda d: isinstance(d, unicode) or isinstance(d, str) is_string = lambda d: isinstance(d, unicode) or isinstance(d, str)
# no easy way to distinguish between b'' and '' :( # no easy way to distinguish between b'' and '' :(
# or (isinstance(d, str) \ # 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(0x00, 0x1F))) \
# and not any((chr(k) in d for k in range(0x80, 0xFF))) \ # and not any((chr(k) in d for k in range(0x80, 0xFF))) \
# ) # )
except: except:
# this is certanly Python 3 # this is certanly Python 3
# In Py3, unicode and str are equal (the unicode object does not exist) # In Py3, unicode and str are equal (the unicode object does not exist)
is_string = lambda d: isinstance(d, str) is_string = lambda d: isinstance(d, str)
# #
# #
# #
class NamedInt(int): 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 Caution: comparison with strings will also match this NamedInt's name
(case-insensitive).""" (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): def bytes(self, count=2):
assert is_string(name) return int2bytes(self, count)
obj = int.__new__(cls, value)
obj.name = str(name)
return obj
def bytes(self, count=2): def __eq__(self, other):
return int2bytes(self, count) 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): def __ne__(self, other):
if isinstance(other, NamedInt): return not self.__eq__(other)
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): def __hash__(self):
return not self.__eq__(other) return int(self)
def __hash__(self): def __str__(self):
return int(self) return self.name
def __str__(self): __unicode__ = __str__
return self.name
__unicode__ = __str__
def __repr__(self): def __repr__(self):
return 'NamedInt(%d, %r)' % (int(self), self.name) return 'NamedInt(%d, %r)' % (int(self), self.name)
class NamedInts(object): 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 Indexing can be made by int or string, and will return the corresponding
NamedInt if it exists in this set, or `None`. 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 if the value already exists in the set (int or string), ValueError will be
raised. raised.
""" """
__slots__ = ('__dict__', '_values', '_indexed', '_fallback') __slots__ = ('__dict__', '_values', '_indexed', '_fallback')
def __init__(self, **kwargs): def __init__(self, **kwargs):
def _readable_name(n): def _readable_name(n):
if not is_string(n): if not is_string(n):
raise TypeError("expected (unicode) string, got " + str(type(n))) raise TypeError("expected (unicode) string, got " +
return n.replace('__', '/').replace('_', ' ') str(type(n)))
return n.replace('__', '/').replace('_', ' ')
# print (repr(kwargs)) # print (repr(kwargs))
values = {k: NamedInt(v, _readable_name(k)) for (k, v) in kwargs.items()} values = {
self.__dict__ = values k: NamedInt(v, _readable_name(k))
self._values = sorted(list(values.values())) for (k, v) in kwargs.items()
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.__dict__ = values
self._fallback = None 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 @classmethod
def list(cls, items, name_generator=lambda x: str(x)): def list(cls, items, name_generator=lambda x: str(x)):
values = {name_generator(x): x for x in items} values = {name_generator(x): x for x in items}
return NamedInts(**values) return NamedInts(**values)
@classmethod @classmethod
def range(cls, from_value, to_value, name_generator=lambda x: str(x), step=1): def range(cls,
values = {name_generator(x): x for x in range(from_value, to_value + 1, step)} from_value,
return NamedInts(**values) 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): def flag_names(self, value):
unknown_bits = value unknown_bits = value
for k in self._indexed: for k in self._indexed:
assert bin(k).count('1') == 1 assert bin(k).count('1') == 1
if k & value == k: if k & value == k:
unknown_bits &= ~k unknown_bits &= ~k
yield str(self._indexed[k]) yield str(self._indexed[k])
if unknown_bits: if unknown_bits:
yield 'unknown:%06X' % unknown_bits yield 'unknown:%06X' % unknown_bits
def __getitem__(self, index): def __getitem__(self, index):
if isinstance(index, int): if isinstance(index, int):
if index in self._indexed: if index in self._indexed:
return self._indexed[int(index)] return self._indexed[int(index)]
if self._fallback and isinstance(index, int): if self._fallback and isinstance(index, int):
value = NamedInt(index, self._fallback(index)) value = NamedInt(index, self._fallback(index))
self._indexed[index] = value self._indexed[index] = value
self._values = sorted(self._values + [value]) self._values = sorted(self._values + [value])
return value return value
elif is_string(index): elif is_string(index):
if index in self.__dict__: if index in self.__dict__:
return self.__dict__[index] return self.__dict__[index]
elif isinstance(index, slice): elif isinstance(index, slice):
if index.start is None and index.stop is None: if index.start is None and index.stop is None:
return self._values[:] return self._values[:]
v_start = int(self._values[0]) if index.start is None else int(index.start) v_start = int(self._values[0]) if index.start is None else int(
v_stop = (self._values[-1] + 1) if index.stop is None else int(index.stop) 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]: if v_start > v_stop or v_start > self._values[
return [] -1] or v_stop <= self._values[0]:
return []
if v_start <= self._values[0] and v_stop > self._values[-1]: if v_start <= self._values[0] and v_stop > self._values[-1]:
return self._values[:] return self._values[:]
start_index = 0 start_index = 0
stop_index = len(self._values) stop_index = len(self._values)
for i, value in enumerate(self._values): for i, value in enumerate(self._values):
if value < v_start: if value < v_start:
start_index = i + 1 start_index = i + 1
elif index.stop is None: elif index.stop is None:
break break
if value >= v_stop: if value >= v_stop:
stop_index = i stop_index = i
break break
return self._values[start_index:stop_index] return self._values[start_index:stop_index]
def __setitem__(self, index, name): def __setitem__(self, index, name):
assert isinstance(index, int), type(index) assert isinstance(index, int), type(index)
if isinstance(name, NamedInt): if isinstance(name, NamedInt):
assert int(index) == int(name), repr(index) + ' ' + repr(name) assert int(index) == int(name), repr(index) + ' ' + repr(name)
value = name value = name
elif is_string(name): elif is_string(name):
value = NamedInt(index, name) value = NamedInt(index, name)
else: else:
raise TypeError('name must be a string') raise TypeError('name must be a string')
if str(value) in self.__dict__: if str(value) in self.__dict__:
raise ValueError('%s (%d) already known' % (value, int(value))) raise ValueError('%s (%d) already known' % (value, int(value)))
if int(value) in self._indexed: if int(value) in self._indexed:
raise ValueError('%d (%s) already known' % (int(value), value)) raise ValueError('%d (%s) already known' % (int(value), value))
self._values = sorted(self._values + [value]) self._values = sorted(self._values + [value])
self.__dict__[str(value)] = value self.__dict__[str(value)] = value
self._indexed[int(value)] = value self._indexed[int(value)] = value
def __contains__(self, value): def __contains__(self, value):
if isinstance(value, int): if isinstance(value, int):
return value in self._indexed return value in self._indexed
elif is_string(value): elif is_string(value):
return value in self.__dict__ return value in self.__dict__
def __iter__(self): def __iter__(self):
for v in self._values: for v in self._values:
yield v yield v
def __len__(self): def __len__(self):
return len(self._values) return len(self._values)
def __repr__(self): def __repr__(self):
return 'NamedInts(%s)' % ', '.join(repr(v) for v in self._values) return 'NamedInts(%s)' % ', '.join(repr(v) for v in self._values)
def strhex(x): def strhex(x):
assert x is not None assert x is not None
"""Produce a hex-string representation of a sequence of bytes.""" """Produce a hex-string representation of a sequence of bytes."""
return _hexlify(x).decode('ascii').upper() return _hexlify(x).decode('ascii').upper()
def bytes2int(x): 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. The bytes are assumed to be in most-significant-first order.
""" """
assert isinstance(x, bytes) assert isinstance(x, bytes)
assert len(x) < 9 assert len(x) < 9
qx = (b'\x00' * 8) + x qx = (b'\x00' * 8) + x
result, = unpack('!Q', qx[-8:]) result, = unpack('!Q', qx[-8:])
# assert x == int2bytes(result, len(x)) # assert x == int2bytes(result, len(x))
return result return result
def int2bytes(x, count=None): 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. The bytes are ordered in most-significant-first order.
If 'count' is not given, the necessary number of bytes is computed. If 'count' is not given, the necessary number of bytes is computed.
""" """
assert isinstance(x, int) assert isinstance(x, int)
result = pack('!Q', x) result = pack('!Q', x)
assert isinstance(result, bytes) assert isinstance(result, bytes)
# assert x == bytes2int(result) # assert x == bytes2int(result)
if count is None: if count is None:
return result.lstrip(b'\x00') return result.lstrip(b'\x00')
assert isinstance(count, int) assert isinstance(count, int)
assert count > 0 assert count > 0
assert x.bit_length() <= count * 8 assert x.bit_length() <= count * 8
return result[-count:] return result[-count:]
class KwException(Exception): 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. They can be later accessed by simple member access.
""" """
def __init__(self, **kwargs): def __init__(self, **kwargs):
super(KwException, self).__init__(kwargs) super(KwException, self).__init__(kwargs)
def __getattr__(self, k): def __getattr__(self, k):
try: try:
return super(KwException, self).__getattr__(k) return super(KwException, self).__getattr__(k)
except AttributeError: except AttributeError:
return self.args[0][k] return self.args[0][k]
from collections import namedtuple from collections import namedtuple
"""Firmware information.""" """Firmware information."""
FirmwareInfo = namedtuple('FirmwareInfo', [ FirmwareInfo = namedtuple('FirmwareInfo',
'kind', ['kind', 'name', 'version', 'extras'])
'name',
'version',
'extras'])
"""Reprogrammable keys information.""" """Reprogrammable keys information."""
ReprogrammableKeyInfo = namedtuple('ReprogrammableKeyInfo', [ ReprogrammableKeyInfo = namedtuple('ReprogrammableKeyInfo',
'index', ['index', 'key', 'task', 'flags'])
'key',
'task',
'flags'])
ReprogrammableKeyInfoV4 = namedtuple('ReprogrammableKeyInfoV4', [ ReprogrammableKeyInfoV4 = namedtuple('ReprogrammableKeyInfoV4', [
'index', 'index', 'key', 'task', 'flags', 'pos', 'group', 'group_mask', 'remapped'
'key', ])
'task',
'flags',
'pos',
'group',
'group_mask',
'remapped'])
del namedtuple del namedtuple

View File

@ -19,7 +19,6 @@
from __future__ import absolute_import, division, print_function, unicode_literals from __future__ import absolute_import, division, print_function, unicode_literals
from .common import NamedInts as _NamedInts from .common import NamedInts as _NamedInts
from .hidpp10 import REGISTERS as _R, DEVICE_KIND as _DK from .hidpp10 import REGISTERS as _R, DEVICE_KIND as _DK
from .settings_templates import RegisterSettings as _RS, FeatureSettings as _FS 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 from collections import namedtuple
_DeviceDescriptor = namedtuple('_DeviceDescriptor', _DeviceDescriptor = namedtuple('_DeviceDescriptor',
('name', 'kind', 'wpid', 'codename', 'protocol', 'registers', 'settings', 'persister')) ('name', 'kind', 'wpid', 'codename', 'protocol',
'registers', 'settings', 'persister'))
del namedtuple del namedtuple
DEVICES = {} DEVICES = {}
def _D(name, codename=None, kind=None, wpid=None, protocol=None, registers=None, settings=None, persister=None):
assert name
if kind is None: def _D(name,
kind = (_DK.mouse if 'Mouse' in name codename=None,
else _DK.keyboard if 'Keyboard' in name kind=None,
else _DK.numpad if 'Number Pad' in name wpid=None,
else _DK.touchpad if 'Touchpad' in name protocol=None,
else _DK.trackball if 'Trackball' in name registers=None,
else None) settings=None,
assert kind is not None, 'descriptor for %s does not have kind set' % name persister=None):
assert name
# heuristic: the codename is the last word in the device name if kind is None:
if codename is None and ' ' in name: kind = (_DK.mouse if 'Mouse' in name else
codename = name.split(' ')[-1] _DK.keyboard if 'Keyboard' in name else _DK.numpad
assert codename is not None, 'descriptor for %s does not have codename set' % name 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: # heuristic: the codename is the last word in the device name
# ? 2.0 devices should not have any registers if codename is None and ' ' in name:
_kind = lambda s : s._rw.kind if hasattr(s, '_rw') else s._rw_kind codename = name.split(' ')[-1]
if protocol < 2.0: assert codename is not None, 'descriptor for %s does not have codename set' % name
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)
if wpid: if protocol is not None:
for w in wpid if isinstance(wpid, tuple) else (wpid, ): # ? 2.0 devices should not have any registers
if protocol > 1.0: _kind = lambda s: s._rw.kind if hasattr(s, '_rw') else s._rw_kind
assert w[0:1] == '4', '%s has protocol %0.1f, wpid %s' % (name, protocol, w) if protocol < 2.0:
else: assert settings is None or all(_kind(s) == 1 for s in settings)
if w[0:1] == '1': else:
assert kind == _DK.mouse, '%s has protocol %0.1f, wpid %s' % (name, protocol, w) assert registers is None
elif w[0:1] == '2': assert settings is None or all(_kind(s) == 2 for s in settings)
assert kind in (_DK.keyboard, _DK.numpad), '%s has protocol %0.1f, wpid %s' % (name, protocol, w)
device_descriptor = _DeviceDescriptor(name=name, kind=kind, if wpid:
wpid=wpid, codename=codename, protocol=protocol, for w in wpid if isinstance(wpid, tuple) else (wpid, ):
registers=registers, settings=settings, persister=persister) 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], ) device_descriptor = _DeviceDescriptor(name=name,
DEVICES[codename] = device_descriptor kind=kind,
wpid=wpid,
codename=codename,
protocol=protocol,
registers=registers,
settings=settings,
persister=persister)
if wpid: assert codename not in DEVICES, 'duplicate codename in device descriptors: %s' % (
if not isinstance(wpid, tuple): DEVICES[codename], )
wpid = (wpid, ) 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 K230', protocol=2.0, wpid='400D')
_D('Wireless Keyboard K270(unifying)', protocol=2.0, wpid='4003') _D('Wireless Keyboard K270(unifying)', protocol=2.0, wpid='4003')
_D('Wireless Keyboard MK270', protocol=2.0, wpid='4023', _D(
settings=[ 'Wireless Keyboard MK270',
_FS.fn_swap() protocol=2.0,
], wpid='4023',
) settings=[_FS.fn_swap()],
_D('Wireless Keyboard K270', protocol=1.0, )
registers=(_R.battery_status, ), _D(
) 'Wireless Keyboard K270',
_D('Wireless Keyboard MK300', protocol=1.0, wpid='8521', protocol=1.0,
registers=(_R.battery_status, ), 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', _D(
registers=(_R.battery_status, ), 'Wireless Keyboard MK320',
) protocol=1.0,
wpid='200F',
registers=(_R.battery_status, ),
)
_D('Wireless Keyboard MK330') _D('Wireless Keyboard MK330')
_D('Wireless Compact Keyboard K340', protocol=1.0, wpid='2007', _D(
registers=(_R.battery_status, ), 'Wireless Compact Keyboard K340',
) protocol=1.0,
_D('Wireless Wave Keyboard K350', protocol=1.0, wpid='200A', wpid='2007',
registers=(_R.battery_status, ), registers=(_R.battery_status, ),
) )
_D('Wireless Keyboard K360', protocol=2.0, wpid='4004', _D(
settings=[ 'Wireless Wave Keyboard K350',
_FS.fn_swap() protocol=1.0,
], wpid='200A',
) registers=(_R.battery_status, ),
_D('Wireless Keyboard K375s', protocol=2.0, wpid='4061', )
settings=[ _D(
_FS.k375s_fn_swap() 'Wireless Keyboard K360',
], protocol=2.0,
) wpid='4004',
_D('Wireless Touch Keyboard K400', protocol=2.0, wpid=('400E', '4024'), settings=[_FS.fn_swap()],
settings=[ )
_FS.fn_swap() _D(
], 'Wireless Keyboard K375s',
) protocol=2.0,
_D('Wireless Touch Keyboard K400 Plus', codename='K400 Plus', protocol=2.0, wpid='404D', wpid='4061',
settings=[ settings=[_FS.k375s_fn_swap()],
_FS.new_fn_swap(), )
_FS.reprogrammable_keys(), _D(
_FS.disable_keyboard_keys(), 'Wireless Touch Keyboard K400',
], protocol=2.0,
) wpid=('400E', '4024'),
_D('Wireless Keyboard K520', protocol=1.0, wpid='2011', settings=[_FS.fn_swap()],
registers=(_R.battery_status, ), )
settings=[ _D(
_RS.fn_swap(), 'Wireless Touch Keyboard K400 Plus',
], codename='K400 Plus',
) protocol=2.0,
_D('Number Pad N545', protocol=1.0, wpid='2006', wpid='404D',
registers=(_R.battery_status, ), 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 MK550')
_D('Wireless Keyboard MK700', protocol=1.0, wpid='2008', _D(
registers=(_R.battery_status, ), 'Wireless Keyboard MK700',
settings=[ protocol=1.0,
_RS.fn_swap(), wpid='2008',
], registers=(_R.battery_status, ),
) settings=[
_D('Wireless Solar Keyboard K750', protocol=2.0, wpid='4002', _RS.fn_swap(),
settings=[ ],
_FS.fn_swap() )
], _D(
) 'Wireless Solar Keyboard K750',
_D('Wireless Multi-Device Keyboard K780', protocol=4.5, wpid='405B', protocol=2.0,
settings=[ wpid='4002',
_FS.new_fn_swap() settings=[_FS.fn_swap()],
], )
) _D(
_D('Wireless Illuminated Keyboard K800', protocol=1.0, wpid='2010', 'Wireless Multi-Device Keyboard K780',
registers=(_R.battery_status, _R.three_leds, ), protocol=4.5,
settings=[ wpid='405B',
_RS.fn_swap(), settings=[_FS.new_fn_swap()],
_RS.hand_detection(), )
], _D(
) 'Wireless Illuminated Keyboard K800',
_D('Wireless Illuminated Keyboard K800 new', codename='K800 new', protocol=4.5, wpid='406E', protocol=1.0,
settings=[ wpid='2010',
_FS.fn_swap() registers=(
], _R.battery_status,
) _R.three_leds,
_D('Illuminated Living-Room Keyboard K830', protocol=2.0, wpid='4032', ),
settings=[ settings=[
_FS.new_fn_swap() _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('Craft Advanced Keyboard', codename='Craft', protocol=4.5, wpid='4066')
_D('Wireless Keyboard S510', codename='S510', protocol=1.0, wpid='3622', _D(
registers=(_R.battery_status, ), 'Wireless Keyboard S510',
) codename='S510',
protocol=1.0,
wpid='3622',
registers=(_R.battery_status, ),
)
# Mice # Mice
_D('Wireless Mouse M150', protocol=2.0, wpid='4022') _D('Wireless Mouse M150', protocol=2.0, wpid='4022')
_D('Wireless Mouse M175', protocol=2.0, wpid='4008') _D('Wireless Mouse M175', protocol=2.0, wpid='4008')
_D('Wireless Mouse M185 new', codename='M185n', protocol=4.5, wpid='4054', _D('Wireless Mouse M185 new',
settings=[ codename='M185n',
_FS.lowres_smooth_scroll(), protocol=4.5,
_FS.pointer_speed(), wpid='4054',
]) settings=[
_FS.lowres_smooth_scroll(),
_FS.pointer_speed(),
])
# Apparently Logitech uses wpid 4055 for three different mice # 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 # 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', _D('Wireless Mouse M185/M235/M310',
settings=[ codename='M185/M235/M310',
_FS.lowres_smooth_scroll(), protocol=4.5,
_FS.pointer_speed(), wpid='4055',
]) settings=[
_FS.lowres_smooth_scroll(),
_FS.pointer_speed(),
])
_D('Wireless Mouse M185', protocol=2.0, wpid='4038') _D('Wireless Mouse M185', protocol=2.0, wpid='4038')
_D('Wireless Mouse M187', protocol=2.0, wpid='4019') _D('Wireless Mouse M187', protocol=2.0, wpid='4019')
_D('Wireless Mouse M215', protocol=1.0, wpid='1020') _D('Wireless Mouse M215', protocol=1.0, wpid='1020')
_D('Wireless Mouse M305', protocol=1.0, wpid='101F', _D(
registers=(_R.battery_status, ), 'Wireless Mouse M305',
settings=[ protocol=1.0,
_RS.side_scroll(), wpid='101F',
], registers=(_R.battery_status, ),
) settings=[
_D('Wireless Mouse M310', protocol=1.0, wpid='1024', _RS.side_scroll(),
registers=(_R.battery_status, ), ],
) )
_D(
'Wireless Mouse M310',
protocol=1.0,
wpid='1024',
registers=(_R.battery_status, ),
)
_D('Wireless Mouse M315') _D('Wireless Mouse M315')
_D('Wireless Mouse M317') _D('Wireless Mouse M317')
_D('Wireless Mouse M325', protocol=2.0, wpid='400A', _D('Wireless Mouse M325',
settings=[ protocol=2.0,
_FS.hi_res_scroll(), wpid='400A',
]) settings=[
_FS.hi_res_scroll(),
])
_D('Wireless Mouse M345', protocol=2.0, wpid='4017') _D('Wireless Mouse M345', protocol=2.0, wpid='4017')
_D('Wireless Mouse M350', protocol=1.0, wpid='101C', _D(
registers=(_R.battery_charge, ), '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 Pebble M350', codename='Pebble', protocol=2.0, wpid='4080')
_D('Wireless Mouse M505', codename='M505/B605', protocol=1.0, wpid='101D', _D(
registers=(_R.battery_charge, ), 'Wireless Mouse M505',
settings=[ codename='M505/B605',
_RS.smooth_scroll(), protocol=1.0,
_RS.side_scroll(), wpid='101D',
], registers=(_R.battery_charge, ),
) settings=[
_D('Wireless Mouse M510', protocol=1.0, wpid='1025', _RS.smooth_scroll(),
registers=(_R.battery_status, ), _RS.side_scroll(),
settings=[ ],
_RS.smooth_scroll(), )
_RS.side_scroll(), _D(
], 'Wireless Mouse M510',
) protocol=1.0,
_D('Wireless Mouse M510', codename='M510v2', protocol=2.0, wpid='4051', wpid='1025',
settings=[ registers=(_R.battery_status, ),
_FS.lowres_smooth_scroll(), 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('Couch Mouse M515', protocol=2.0, wpid='4007')
_D('Wireless Mouse M525', protocol=2.0, wpid='4013') _D('Wireless Mouse M525', protocol=2.0, wpid='4013')
_D('Multi Device Silent Mouse M585/M590', codename='M585/M590', protocol=4.5, wpid='406B', _D(
settings=[ 'Multi Device Silent Mouse M585/M590',
_FS.lowres_smooth_scroll(), codename='M585/M590',
_FS.pointer_speed(), protocol=4.5,
], wpid='406B',
) settings=[
_FS.lowres_smooth_scroll(),
_FS.pointer_speed(),
],
)
_D('Touch Mouse M600', protocol=2.0, wpid='401A') _D('Touch Mouse M600', protocol=2.0, wpid='401A')
_D('Marathon Mouse M705 (M-R0009)', codename='M705 (M-R0009)', protocol=1.0, wpid='101B', _D(
registers=(_R.battery_charge, ), 'Marathon Mouse M705 (M-R0009)',
settings=[ codename='M705 (M-R0009)',
_RS.smooth_scroll(), protocol=1.0,
_RS.side_scroll(), wpid='101B',
], registers=(_R.battery_charge, ),
) settings=[
_D('Marathon Mouse M705 (M-R0073)', codename='M705 (M-R0073)', protocol=4.5, wpid='406D', _RS.smooth_scroll(),
settings=[ _RS.side_scroll(),
_FS.hires_smooth_invert(), ],
_FS.hires_smooth_resolution(), )
_FS.pointer_speed(), _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('Zone Touch Mouse T400')
_D('Touch Mouse T620', protocol=2.0) _D('Touch Mouse T620', protocol=2.0)
_D('Logitech Cube', kind=_DK.mouse, protocol=2.0) _D('Logitech Cube', kind=_DK.mouse, protocol=2.0)
_D('Anywhere Mouse MX', codename='Anywhere MX', protocol=1.0, wpid='1017', _D(
registers=(_R.battery_charge, ), 'Anywhere Mouse MX',
settings=[ codename='Anywhere MX',
_RS.smooth_scroll(), protocol=1.0,
_RS.side_scroll(), wpid='1017',
], registers=(_R.battery_charge, ),
) settings=[
_D('Anywhere Mouse MX 2', codename='Anywhere MX 2', protocol=4.5, wpid='404A', _RS.smooth_scroll(),
settings=[ _RS.side_scroll(),
_FS.hires_smooth_invert(), ],
_FS.hires_smooth_resolution(), )
], _D(
) 'Anywhere Mouse MX 2',
_D('Performance Mouse MX', codename='Performance MX', protocol=1.0, wpid='101A', codename='Anywhere MX 2',
registers=(_R.battery_status, _R.three_leds, ), protocol=4.5,
settings=[ wpid='404A',
_RS.dpi(choices=_PERFORMANCE_MX_DPIS), settings=[
_RS.smooth_scroll(), _FS.hires_smooth_invert(),
_RS.side_scroll(), _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', _D(
settings=[ 'Wireless Mouse MX Master',
_FS.hires_smooth_invert(), codename='MX Master',
_FS.hires_smooth_resolution(), 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', _D(
settings=[ 'Wireless Mouse MX Master 2S',
_FS.hires_smooth_invert(), codename='MX Master 2S',
_FS.hires_smooth_resolution(), 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', _D(
registers=(_R.battery_status, ), 'G7 Cordless Laser Mouse',
) codename='G7',
_D('G700 Gaming Mouse', codename='G700', protocol=1.0, wpid='1023', protocol=1.0,
registers=(_R.battery_status, _R.three_leds, ), wpid='1002',
settings=[ registers=(_R.battery_status, ),
_RS.smooth_scroll(), )
_RS.side_scroll(), _D(
], 'G700 Gaming Mouse',
) codename='G700',
_D('G700s Gaming Mouse', codename='G700s', protocol=1.0, wpid='102A', protocol=1.0,
registers=(_R.battery_status, _R.three_leds, ), wpid='1023',
settings=[ registers=(
_RS.smooth_scroll(), _R.battery_status,
_RS.side_scroll(), _R.three_leds,
], ),
) settings=[
_D('LX5 Cordless Mouse', codename='LX5', protocol=1.0, wpid='5612', _RS.smooth_scroll(),
registers=(_R.battery_status, ), _RS.side_scroll(),
) ],
_D('Wireless Mouse M30', codename='M30', protocol=1.0, wpid='6822', )
registers=(_R.battery_status, ), _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 # Trackballs
@ -396,57 +552,109 @@ _D('Wireless Touchpad', codename='Wireless Touch', protocol=2.0, wpid='4011')
# A wpid is necessary to properly identify them. # A wpid is necessary to properly identify them.
# #
_D('VX Nano Cordless Laser Mouse', codename='VX Nano', protocol=1.0, wpid=('100B', '100F'), _D(
registers=(_R.battery_charge, ), 'VX Nano Cordless Laser Mouse',
settings=[ codename='VX Nano',
_RS.smooth_scroll(), protocol=1.0,
_RS.side_scroll(), wpid=('100B', '100F'),
], registers=(_R.battery_charge, ),
) settings=[
_D('V450 Nano Cordless Laser Mouse', codename='V450 Nano', protocol=1.0, wpid='1011', _RS.smooth_scroll(),
registers=(_R.battery_charge, ), _RS.side_scroll(),
) ],
_D('V550 Nano Cordless Laser Mouse', codename='V550 Nano', protocol=1.0, wpid='1013', )
registers=(_R.battery_charge, ), _D(
settings=[ 'V450 Nano Cordless Laser Mouse',
_RS.smooth_scroll(), codename='V450 Nano',
_RS.side_scroll(), 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 # Mini receiver mice
_D('MX610 Laser Cordless Mouse', codename='MX610', protocol=1.0, wpid='1001', _D(
registers=(_R.battery_status, ), 'MX610 Laser Cordless Mouse',
) codename='MX610',
_D('MX620 Laser Cordless Mouse', codename='MX620', protocol=1.0, wpid=('100A', '1016'), protocol=1.0,
registers=(_R.battery_charge, ), wpid='1001',
) registers=(_R.battery_status, ),
_D('MX610 Left-Handled Mouse', codename='MX610L', protocol=1.0, wpid='1004', )
registers=(_R.battery_status, ), _D(
) 'MX620 Laser Cordless Mouse',
_D('V400 Laser Cordless Mouse', codename='V400', protocol=1.0, wpid='1003', codename='MX620',
registers=(_R.battery_status, ), protocol=1.0,
) wpid=('100A', '1016'),
_D('V450 Laser Cordless Mouse', codename='V450', protocol=1.0, wpid='1005', registers=(_R.battery_charge, ),
registers=(_R.battery_status, ), )
) _D(
_D('VX Revolution', codename='VX Revolution', kind=_DK.mouse, protocol=1.0, wpid=('1006', '100D'), 'MX610 Left-Handled Mouse',
registers=(_R.battery_charge, ), codename='MX610L',
) protocol=1.0,
_D('MX Air', codename='MX Air', protocol=1.0, kind=_DK.mouse, wpid=('1007', '100E'), wpid='1004',
registers=(_R.battery_charge, ), registers=(_R.battery_status, ),
) )
_D('MX Revolution', codename='MX Revolution', protocol=1.0, kind=_DK.mouse, wpid=('1008', '100C'), _D(
registers=(_R.battery_charge, ), 'V400 Laser Cordless Mouse',
) codename='V400',
_D('MX 1100 Cordless Laser Mouse', codename='MX 1100', protocol=1.0, kind=_DK.mouse, wpid='1014', protocol=1.0,
registers=(_R.battery_charge, ), wpid='1003',
settings=[ registers=(_R.battery_status, ),
_RS.smooth_scroll(), )
_RS.side_scroll(), _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... # Some exotics...

View File

@ -23,12 +23,9 @@ from logging import getLogger # , DEBUG as _DEBUG
_log = getLogger(__name__) _log = getLogger(__name__)
del getLogger del getLogger
from .common import (strhex as _strhex, bytes2int as _bytes2int, int2bytes as
from .common import (strhex as _strhex, _int2bytes, NamedInts as _NamedInts, FirmwareInfo as
bytes2int as _bytes2int, _FirmwareInfo)
int2bytes as _int2bytes,
NamedInts as _NamedInts,
FirmwareInfo as _FirmwareInfo)
from .hidpp20 import FIRMWARE_KIND, BATTERY_STATUS from .hidpp20 import FIRMWARE_KIND, BATTERY_STATUS
# #
@ -36,26 +33,24 @@ from .hidpp20 import FIRMWARE_KIND, BATTERY_STATUS
# documentation, some of them guessed. # documentation, some of them guessed.
# #
DEVICE_KIND = _NamedInts( DEVICE_KIND = _NamedInts(keyboard=0x01,
keyboard=0x01, mouse=0x02,
mouse=0x02, numpad=0x03,
numpad=0x03, presenter=0x04,
presenter=0x04, trackball=0x08,
trackball=0x08, touchpad=0x09)
touchpad=0x09)
POWER_SWITCH_LOCATION = _NamedInts( POWER_SWITCH_LOCATION = _NamedInts(base=0x01,
base=0x01, top_case=0x02,
top_case=0x02, edge_of_top_right_corner=0x03,
edge_of_top_right_corner=0x03, top_left_corner=0x05,
top_left_corner=0x05, bottom_left_corner=0x06,
bottom_left_corner=0x06, top_right_corner=0x07,
top_right_corner=0x07, bottom_right_corner=0x08,
bottom_right_corner=0x08, top_edge=0x09,
top_edge=0x09, right_edge=0x0A,
right_edge=0x0A, left_edge=0x0B,
left_edge=0x0B, bottom_edge=0x0C)
bottom_edge=0x0C)
# Some flags are used both by devices and receivers. The Logitech documentation # Some flags are used both by devices and receivers. The Logitech documentation
# mentions that the first and last (third) byte are used for devices while the # 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, # In the future would be useful to have separate enums for receiver and device notification flags,
# but right now we don't know enough. # but right now we don't know enough.
NOTIFICATION_FLAG = _NamedInts( NOTIFICATION_FLAG = _NamedInts(
battery_status= 0x100000, # send battery charge notifications (0x07 or 0x0D) battery_status=0x100000, # send battery charge notifications (0x07 or 0x0D)
keyboard_sleep_raw= 0x020000, # system control keys such as Sleep keyboard_sleep_raw=0x020000, # system control keys such as Sleep
keyboard_multimedia_raw=0x010000, # consumer controls such as Mute and Calculator keyboard_multimedia_raw=
# reserved_r1b4= 0x001000, # unknown, seen on a unifying receiver 0x010000, # consumer controls such as Mute and Calculator
software_present= 0x000800, # .. no idea # reserved_r1b4= 0x001000, # unknown, seen on a unifying receiver
keyboard_illumination= 0x000200, # illumination brightness level changes (by pressing keys) software_present=0x000800, # .. no idea
wireless= 0x000100, # notify when the device wireless goes on/off-line keyboard_illumination=
) 0x000200, # illumination brightness level changes (by pressing keys)
wireless=0x000100, # notify when the device wireless goes on/off-line
)
ERROR = _NamedInts( ERROR = _NamedInts(invalid_SubID__command=0x01,
invalid_SubID__command=0x01, invalid_address=0x02,
invalid_address=0x02, invalid_value=0x03,
invalid_value=0x03, connection_request_failed=0x04,
connection_request_failed=0x04, too_many_devices=0x05,
too_many_devices=0x05, already_exists=0x06,
already_exists=0x06, busy=0x07,
busy=0x07, unknown_device=0x08,
unknown_device=0x08, resource_error=0x09,
resource_error=0x09, request_unavailable=0x0A,
request_unavailable=0x0A, unsupported_parameter_value=0x0B,
unsupported_parameter_value=0x0B, wrong_pin_code=0x0C)
wrong_pin_code=0x0C)
PAIRING_ERRORS = _NamedInts( PAIRING_ERRORS = _NamedInts(device_timeout=0x01,
device_timeout=0x01, device_not_supported=0x02,
device_not_supported=0x02, too_many_devices=0x03,
too_many_devices=0x03, sequence_timeout=0x06)
sequence_timeout=0x06)
BATTERY_APPOX = _NamedInts(
empty = 0,
critical = 5,
low = 20,
good = 50,
full = 90)
BATTERY_APPOX = _NamedInts(empty=0, critical=5, low=20, good=50, full=90)
"""Known registers. """Known registers.
Devices usually have a (small) sub-set of these. Some registers are only 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.""" applicable to certain device kinds (e.g. smooth_scroll only applies to mice."""
REGISTERS = _NamedInts( REGISTERS = _NamedInts(
# only apply to receivers # only apply to receivers
receiver_connection=0x02, receiver_connection=0x02,
receiver_pairing=0xB2, receiver_pairing=0xB2,
devices_activity=0x2B3, devices_activity=0x2B3,
receiver_info=0x2B5, receiver_info=0x2B5,
# only apply to devices # only apply to devices
mouse_button_flags=0x01, mouse_button_flags=0x01,
keyboard_hand_detection=0x01, keyboard_hand_detection=0x01,
battery_status=0x07, battery_status=0x07,
keyboard_fn_swap=0x09, keyboard_fn_swap=0x09,
battery_charge=0x0D, battery_charge=0x0D,
keyboard_illumination=0x17, keyboard_illumination=0x17,
three_leds=0x51, three_leds=0x51,
mouse_dpi=0x63, mouse_dpi=0x63,
# apply to both # apply to both
notifications=0x00, notifications=0x00,
firmware=0xF1, firmware=0xF1,
) )
# #
# functions # functions
# #
def read_register(device, register_number, *params): def read_register(device, register_number, *params):
assert device, 'tried to read register %02X from invalid device %s' % (register_number, device) assert device, 'tried to read register %02X from invalid device %s' % (
# support long registers by adding a 2 in front of the register number register_number, device)
request_id = 0x8100 | (int(register_number) & 0x2FF) # support long registers by adding a 2 in front of the register number
return device.request(request_id, *params) request_id = 0x8100 | (int(register_number) & 0x2FF)
return device.request(request_id, *params)
def write_register(device, register_number, *value): def write_register(device, register_number, *value):
assert device, 'tried to write register %02X to invalid device %s' % (register_number, device) assert device, 'tried to write register %02X to invalid device %s' % (
# support long registers by adding a 2 in front of the register number register_number, device)
request_id = 0x8000 | (int(register_number) & 0x2FF) # support long registers by adding a 2 in front of the register number
return device.request(request_id, *value) request_id = 0x8000 | (int(register_number) & 0x2FF)
return device.request(request_id, *value)
def get_battery(device): def get_battery(device):
assert device assert device
assert device.kind is not None assert device.kind is not None
if not device.online: if not device.online:
return 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.""" for r in (REGISTERS.battery_status, REGISTERS.battery_charge):
if device.protocol and device.protocol >= 2.0: if r in device.registers:
# let's just assume HID++ 2.0 devices do not provide the battery info in a register reply = read_register(device, r)
return if reply:
return parse_battery_status(r, reply)
return
for r in (REGISTERS.battery_status, REGISTERS.battery_charge): # the descriptor does not tell us which register this device has, try them both
if r in device.registers: reply = read_register(device, REGISTERS.battery_charge)
reply = read_register(device, r) if reply:
if reply: # remember this for the next time
return parse_battery_status(r, reply) device.registers.append(REGISTERS.battery_charge)
return 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_status)
reply = read_register(device, REGISTERS.battery_charge) if reply:
if reply: # remember this for the next time
# remember this for the next time device.registers.append(REGISTERS.battery_status)
device.registers.append(REGISTERS.battery_charge) return parse_battery_status(REGISTERS.battery_status, reply)
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)
def parse_battery_status(register, reply): def parse_battery_status(register, reply):
if register == REGISTERS.battery_charge: if register == REGISTERS.battery_charge:
charge = ord(reply[:1]) charge = ord(reply[:1])
status_byte = ord(reply[2:3]) & 0xF0 status_byte = ord(reply[2:3]) & 0xF0
status_text = (BATTERY_STATUS.discharging if status_byte == 0x30 status_text = (BATTERY_STATUS.discharging if status_byte == 0x30 else
else BATTERY_STATUS.recharging if status_byte == 0x50 BATTERY_STATUS.recharging if status_byte == 0x50 else
else BATTERY_STATUS.full if status_byte == 0x90 BATTERY_STATUS.full if status_byte == 0x90 else None)
else None) return charge, status_text, None
return charge, status_text, None
if register == REGISTERS.battery_status: if register == REGISTERS.battery_status:
status_byte = ord(reply[:1]) status_byte = ord(reply[:1])
charge = (BATTERY_APPOX.full if status_byte == 7 # full charge = (
else BATTERY_APPOX.good if status_byte == 5 # good BATTERY_APPOX.full if status_byte == 7 # full
else BATTERY_APPOX.low if status_byte == 3 # low else BATTERY_APPOX.good if status_byte == 5 # good
else BATTERY_APPOX.critical if status_byte == 1 # critical else BATTERY_APPOX.low if status_byte == 3 # low
# pure 'charging' notifications may come without a status else BATTERY_APPOX.critical if status_byte == 1 # critical
else BATTERY_APPOX.empty) # pure 'charging' notifications may come without a status
else BATTERY_APPOX.empty)
charging_byte = ord(reply[1:2]) charging_byte = ord(reply[1:2])
if charging_byte == 0x00: if charging_byte == 0x00:
status_text = BATTERY_STATUS.discharging status_text = BATTERY_STATUS.discharging
elif charging_byte & 0x21 == 0x21: elif charging_byte & 0x21 == 0x21:
status_text = BATTERY_STATUS.recharging status_text = BATTERY_STATUS.recharging
elif charging_byte & 0x22 == 0x22: elif charging_byte & 0x22 == 0x22:
status_text = BATTERY_STATUS.full status_text = BATTERY_STATUS.full
else: else:
_log.warn("could not parse 0x07 battery status: %02X (level %02X)", charging_byte, status_byte) _log.warn("could not parse 0x07 battery status: %02X (level %02X)",
status_text = None charging_byte, status_byte)
status_text = None
if charging_byte & 0x03 and status_byte == 0: if charging_byte & 0x03 and status_byte == 0:
# some 'charging' notifications may come with no battery level information # some 'charging' notifications may come with no battery level information
charge = None charge = None
# Return None for next charge level as this is not in HID++ 1.0 spec # Return None for next charge level as this is not in HID++ 1.0 spec
return charge, status_text, None return charge, status_text, None
def get_firmware(device): def get_firmware(device):
assert device assert device
firmware = [None, None, None] firmware = [None, None, None]
reply = read_register(device, REGISTERS.firmware, 0x01) reply = read_register(device, REGISTERS.firmware, 0x01)
if not reply: if not reply:
# won't be able to read any of it now... # won't be able to read any of it now...
return return
fw_version = _strhex(reply[1:3]) fw_version = _strhex(reply[1:3])
fw_version = '%s.%s' % (fw_version[0:2], fw_version[2:4]) fw_version = '%s.%s' % (fw_version[0:2], fw_version[2:4])
reply = read_register(device, REGISTERS.firmware, 0x02) reply = read_register(device, REGISTERS.firmware, 0x02)
if reply: if reply:
fw_version += '.B' + _strhex(reply[1:3]) fw_version += '.B' + _strhex(reply[1:3])
fw = _FirmwareInfo(FIRMWARE_KIND.Firmware, '', fw_version, None) fw = _FirmwareInfo(FIRMWARE_KIND.Firmware, '', fw_version, None)
firmware[0] = fw firmware[0] = fw
reply = read_register(device, REGISTERS.firmware, 0x04) reply = read_register(device, REGISTERS.firmware, 0x04)
if reply: if reply:
bl_version = _strhex(reply[1:3]) bl_version = _strhex(reply[1:3])
bl_version = '%s.%s' % (bl_version[0:2], bl_version[2:4]) bl_version = '%s.%s' % (bl_version[0:2], bl_version[2:4])
bl = _FirmwareInfo(FIRMWARE_KIND.Bootloader, '', bl_version, None) bl = _FirmwareInfo(FIRMWARE_KIND.Bootloader, '', bl_version, None)
firmware[1] = bl firmware[1] = bl
reply = read_register(device, REGISTERS.firmware, 0x03) reply = read_register(device, REGISTERS.firmware, 0x03)
if reply: if reply:
o_version = _strhex(reply[1:3]) o_version = _strhex(reply[1:3])
o_version = '%s.%s' % (o_version[0:2], o_version[2:4]) o_version = '%s.%s' % (o_version[0:2], o_version[2:4])
o = _FirmwareInfo(FIRMWARE_KIND.Other, '', o_version, None) o = _FirmwareInfo(FIRMWARE_KIND.Other, '', o_version, None)
firmware[2] = o firmware[2] = o
if any(firmware): if any(firmware):
return tuple(f for f in firmware if f) return tuple(f for f in firmware if f)
def set_3leds(device, battery_level=None, charging=None, warning=None): def set_3leds(device, battery_level=None, charging=None, warning=None):
assert device assert device
assert device.kind is not None assert device.kind is not None
if not device.online: if not device.online:
return return
if REGISTERS.three_leds not in device.registers: if REGISTERS.three_leds not in device.registers:
return return
if battery_level is not None: if battery_level is not None:
if battery_level < BATTERY_APPOX.critical: if battery_level < BATTERY_APPOX.critical:
# 1 orange, and force blink # 1 orange, and force blink
v1, v2 = 0x22, 0x00 v1, v2 = 0x22, 0x00
warning = True warning = True
elif battery_level < BATTERY_APPOX.low: elif battery_level < BATTERY_APPOX.low:
# 1 orange # 1 orange
v1, v2 = 0x22, 0x00 v1, v2 = 0x22, 0x00
elif battery_level < BATTERY_APPOX.good: elif battery_level < BATTERY_APPOX.good:
# 1 green # 1 green
v1, v2 = 0x20, 0x00 v1, v2 = 0x20, 0x00
elif battery_level < BATTERY_APPOX.full: elif battery_level < BATTERY_APPOX.full:
# 2 greens # 2 greens
v1, v2 = 0x20, 0x02 v1, v2 = 0x20, 0x02
else: else:
# all 3 green # all 3 green
v1, v2 = 0x20, 0x22 v1, v2 = 0x20, 0x22
if warning: if warning:
# set the blinking flag for the leds already set # set the blinking flag for the leds already set
v1 |= (v1 >> 1) v1 |= (v1 >> 1)
v2 |= (v2 >> 1) v2 |= (v2 >> 1)
elif charging: elif charging:
# blink all green # blink all green
v1, v2 = 0x30,0x33 v1, v2 = 0x30, 0x33
elif warning: elif warning:
# 1 red # 1 red
v1, v2 = 0x02, 0x00 v1, v2 = 0x02, 0x00
else: else:
# turn off all leds # turn off all leds
v1, v2 = 0x11, 0x11 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): def get_notification_flags(device):
assert device assert device
# Avoid a call if the device is not online, # Avoid a call if the device is not online,
# or the device does not support registers. # or the device does not support registers.
if device.kind is not None: if device.kind is not None:
# peripherals with protocol >= 2.0 don't support registers # peripherals with protocol >= 2.0 don't support registers
if device.protocol and device.protocol >= 2.0: if device.protocol and device.protocol >= 2.0:
return return
flags = read_register(device, REGISTERS.notifications) flags = read_register(device, REGISTERS.notifications)
if flags is not None: if flags is not None:
assert len(flags) == 3 assert len(flags) == 3
return _bytes2int(flags) return _bytes2int(flags)
def set_notification_flags(device, *flag_bits): def set_notification_flags(device, *flag_bits):
assert device assert device
# Avoid a call if the device is not online, # Avoid a call if the device is not online,
# or the device does not support registers. # or the device does not support registers.
if device.kind is not None: if device.kind is not None:
# peripherals with protocol >= 2.0 don't support registers # peripherals with protocol >= 2.0 don't support registers
if device.protocol and device.protocol >= 2.0: if device.protocol and device.protocol >= 2.0:
return return
flag_bits = sum(int(b) for b in flag_bits) flag_bits = sum(int(b) for b in flag_bits)
assert flag_bits & 0x00FFFFFF == flag_bits assert flag_bits & 0x00FFFFFF == flag_bits
result = write_register(device, REGISTERS.notifications, _int2bytes(flag_bits, 3)) result = write_register(device, REGISTERS.notifications,
return result is not None _int2bytes(flag_bits, 3))
return result is not None

File diff suppressed because it is too large Load Diff

View File

@ -23,30 +23,42 @@ from __future__ import absolute_import, division, print_function, unicode_litera
import gettext as _gettext import gettext as _gettext
try: try:
unicode unicode
_ = lambda x: _gettext.gettext(x).decode('UTF-8') _ = lambda x: _gettext.gettext(x).decode('UTF-8')
ngettext = lambda *x: _gettext.ngettext(*x).decode('UTF-8') ngettext = lambda *x: _gettext.ngettext(*x).decode('UTF-8')
except: except:
_ = _gettext.gettext _ = _gettext.gettext
ngettext = _gettext.ngettext ngettext = _gettext.ngettext
# A few common strings, not always accessible as such in the code. # A few common strings, not always accessible as such in the code.
_DUMMY = ( _DUMMY = (
# approximative battery levels # approximative battery levels
_("empty"), _("critical"), _("low"), _("good"), _("full"), _("empty"),
_("critical"),
_("low"),
_("good"),
_("full"),
# battery charging statuses # battery charging statuses
_("discharging"), _("recharging"), _("almost full"), _("charged"), _("discharging"),
_("slow recharge"), _("invalid battery"), _("thermal error"), _("recharging"),
_("almost full"),
_("charged"),
_("slow recharge"),
_("invalid battery"),
_("thermal error"),
# pairing errors # pairing errors
_("device timeout"), _("device not supported"), _("too many devices"), _("sequence timeout"), _("device timeout"),
_("device not supported"),
_("too many devices"),
_("sequence timeout"),
# firmware kinds # firmware kinds
_("Firmware"), _("Bootloader"), _("Hardware"), _("Other"), _("Firmware"),
_("Bootloader"),
) _("Hardware"),
_("Other"),
)

View File

@ -24,92 +24,96 @@ import threading as _threading
# for both Python 2 and 3 # for both Python 2 and 3
try: try:
from Queue import Queue as _Queue from Queue import Queue as _Queue
except ImportError: except ImportError:
from queue import Queue as _Queue from queue import Queue as _Queue
from logging import getLogger, DEBUG as _DEBUG, INFO as _INFO from logging import getLogger, DEBUG as _DEBUG, INFO as _INFO
_log = getLogger(__name__) _log = getLogger(__name__)
del getLogger del getLogger
from . import base as _base from . import base as _base
# #
# #
# #
class _ThreadedHandle(object): 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. Closing a ThreadedHandle will close all handles.
""" """
__slots__ = ('path', '_local', '_handles', '_listener') __slots__ = ('path', '_local', '_handles', '_listener')
def __init__(self, listener, path, handle): def __init__(self, listener, path, handle):
assert listener is not None assert listener is not None
assert path is not None assert path is not None
assert handle is not None assert handle is not None
assert isinstance(handle, int) assert isinstance(handle, int)
self._listener = listener self._listener = listener
self.path = path self.path = path
self._local = _threading.local() self._local = _threading.local()
# take over the current handle for the thread doing the replacement # take over the current handle for the thread doing the replacement
self._local.handle = handle self._local.handle = handle
self._handles = [handle] self._handles = [handle]
def _open(self): def _open(self):
handle = _base.open_path(self.path) handle = _base.open_path(self.path)
if handle is None: if handle is None:
_log.error("%r failed to open new handle", self) _log.error("%r failed to open new handle", self)
else: else:
# if _log.isEnabledFor(_DEBUG): # if _log.isEnabledFor(_DEBUG):
# _log.debug("%r opened new handle %d", self, handle) # _log.debug("%r opened new handle %d", self, handle)
self._local.handle = handle self._local.handle = handle
self._handles.append(handle) self._handles.append(handle)
return handle return handle
def close(self): def close(self):
if self._local: if self._local:
self._local = None self._local = None
handles, self._handles = self._handles, [] handles, self._handles = self._handles, []
if _log.isEnabledFor(_DEBUG): if _log.isEnabledFor(_DEBUG):
_log.debug("%r closing %s", self, handles) _log.debug("%r closing %s", self, handles)
for h in handles: for h in handles:
_base.close(h) _base.close(h)
@property @property
def notifications_hook(self): def notifications_hook(self):
if self._listener: if self._listener:
assert isinstance(self._listener, _threading.Thread) assert isinstance(self._listener, _threading.Thread)
if _threading.current_thread() == self._listener: if _threading.current_thread() == self._listener:
return self._listener._notifications_hook return self._listener._notifications_hook
def __del__(self): def __del__(self):
self._listener = None self._listener = None
self.close() self.close()
def __index__(self): def __index__(self):
if self._local: if self._local:
try: try:
return self._local.handle return self._local.handle
except: except:
return self._open() return self._open()
__int__ = __index__
def __str__(self): __int__ = __index__
if self._local:
return str(int(self))
__unicode__ = __str__
def __repr__(self): def __str__(self):
return '<_ThreadedHandle(%s)>' % self.path 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): 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. Incoming packets will be passed to the callback function in sequence.
""" """
def __init__(self, receiver, notifications_callback): def __init__(self, receiver, notifications_callback):
super(EventsListener, self).__init__(name=self.__class__.__name__ + ':' + receiver.path.split('/')[2]) super(EventsListener, self).__init__(name=self.__class__.__name__ +
':' + receiver.path.split('/')[2])
self.daemon = True self.daemon = True
self._active = False self._active = False
self.receiver = receiver self.receiver = receiver
self._queued_notifications = _Queue(16) self._queued_notifications = _Queue(16)
self._notifications_callback = notifications_callback self._notifications_callback = notifications_callback
# self.tick_period = 0 # self.tick_period = 0
def run(self): def run(self):
self._active = True self._active = True
# replace the handle with a threaded one # replace the handle with a threaded one
self.receiver.handle = _ThreadedHandle(self, self.receiver.path, self.receiver.handle) self.receiver.handle = _ThreadedHandle(self, self.receiver.path,
# get the right low-level handle for this thread self.receiver.handle)
ihandle = int(self.receiver.handle) # get the right low-level handle for this thread
if _log.isEnabledFor(_INFO): ihandle = int(self.receiver.handle)
_log.info("started with %s (%d)", self.receiver, ihandle) if _log.isEnabledFor(_INFO):
_log.info("started with %s (%d)", self.receiver, ihandle)
self.has_started() self.has_started()
# last_tick = 0 # last_tick = 0
# the first idle read -- delay it a bit, and make sure to stagger # the first idle read -- delay it a bit, and make sure to stagger
# idle reads for multiple receivers # idle reads for multiple receivers
# idle_reads = _IDLE_READS + (ihandle % 5) * 2 # idle_reads = _IDLE_READS + (ihandle % 5) * 2
while self._active: while self._active:
if self._queued_notifications.empty(): if self._queued_notifications.empty():
try: try:
# _log.debug("read next notification") # _log.debug("read next notification")
n = _base.read(ihandle, _EVENT_READ_TIMEOUT) n = _base.read(ihandle, _EVENT_READ_TIMEOUT)
except _base.NoReceiver: except _base.NoReceiver:
_log.warning("receiver disconnected") _log.warning("receiver disconnected")
self.receiver.close() self.receiver.close()
break break
if n: if n:
n = _base.make_notification(*n) n = _base.make_notification(*n)
else: else:
# deliver any queued notifications # deliver any queued notifications
n = self._queued_notifications.get() n = self._queued_notifications.get()
if n: if n:
# if _log.isEnabledFor(_DEBUG): # if _log.isEnabledFor(_DEBUG):
# _log.debug("%s: processing %s", self.receiver, n) # _log.debug("%s: processing %s", self.receiver, n)
try: try:
self._notifications_callback(n) self._notifications_callback(n)
except: except:
_log.exception("processing %s", n) _log.exception("processing %s", n)
# elif self.tick_period: # elif self.tick_period:
# idle_reads -= 1 # idle_reads -= 1
# if idle_reads <= 0: # if idle_reads <= 0:
# idle_reads = _IDLE_READS # idle_reads = _IDLE_READS
# now = _timestamp() # now = _timestamp()
# if now - last_tick >= self.tick_period: # if now - last_tick >= self.tick_period:
# last_tick = now # last_tick = now
# self.tick(now) # self.tick(now)
del self._queued_notifications del self._queued_notifications
self.has_stopped() self.has_stopped()
def stop(self): def stop(self):
"""Tells the listener to stop as soon as possible.""" """Tells the listener to stop as soon as possible."""
self._active = False self._active = False
def has_started(self): def has_started(self):
"""Called right after the thread has started, and before it starts """Called right after the thread has started, and before it starts
reading notification packets.""" reading notification packets."""
pass pass
def has_stopped(self): def has_stopped(self):
"""Called right before the thread stops.""" """Called right before the thread stops."""
pass pass
# def tick(self, timestamp): # def tick(self, timestamp):
# """Called about every tick_period seconds.""" # """Called about every tick_period seconds."""
# pass # pass
def _notifications_hook(self, n): def _notifications_hook(self, n):
# Only consider unhandled notifications that were sent from this thread, # Only consider unhandled notifications that were sent from this thread,
# i.e. triggered by a callback handling a previous notification. # i.e. triggered by a callback handling a previous notification.
assert _threading.current_thread() == self assert _threading.current_thread() == self
if self._active: # and _threading.current_thread() == self: if self._active: # and _threading.current_thread() == self:
# if _log.isEnabledFor(_DEBUG): # if _log.isEnabledFor(_DEBUG):
# _log.debug("queueing unhandled %s", n) # _log.debug("queueing unhandled %s", n)
if not self._queued_notifications.full(): if not self._queued_notifications.full():
self._queued_notifications.put(n) self._queued_notifications.put(n)
def __bool__(self): def __bool__(self):
return bool(self._active and self.receiver) return bool(self._active and self.receiver)
__nonzero__ = __bool__
__nonzero__ = __bool__

View File

@ -26,7 +26,6 @@ from logging import getLogger, DEBUG as _DEBUG, INFO as _INFO
_log = getLogger(__name__) _log = getLogger(__name__)
del getLogger del getLogger
from .i18n import _ from .i18n import _
from .common import strhex as _strhex, unpack as _unpack from .common import strhex as _strhex, unpack as _unpack
from . import hidpp10 as _hidpp10 from . import hidpp10 as _hidpp10
@ -41,302 +40,329 @@ _F = _hidpp20.FEATURE
# #
# #
def process(device, notification): def process(device, notification):
assert device assert device
assert notification assert notification
assert hasattr(device, 'status') assert hasattr(device, 'status')
status = device.status status = device.status
assert status is not None assert status is not None
if device.kind is None: if device.kind is None:
return _process_receiver_notification(device, status, notification) 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): def _process_receiver_notification(receiver, status, n):
# supposedly only 0x4x notifications arrive for the receiver # supposedly only 0x4x notifications arrive for the receiver
assert n.sub_id & 0x40 == 0x40 assert n.sub_id & 0x40 == 0x40
# pairing lock notification # pairing lock notification
if n.sub_id == 0x4A: if n.sub_id == 0x4A:
status.lock_open = bool(n.address & 0x01) status.lock_open = bool(n.address & 0x01)
reason = (_("pairing lock is open") if status.lock_open else _("pairing lock is closed")) reason = (_("pairing lock is open")
if _log.isEnabledFor(_INFO): if status.lock_open else _("pairing lock is closed"))
_log.info("%s: %s", receiver, reason) if _log.isEnabledFor(_INFO):
_log.info("%s: %s", receiver, reason)
status[_K.ERROR] = None status[_K.ERROR] = None
if status.lock_open: if status.lock_open:
status.new_device = None status.new_device = None
pair_error = ord(n.data[:1]) pair_error = ord(n.data[:1])
if pair_error: if pair_error:
status[_K.ERROR] = error_string = _hidpp10.PAIRING_ERRORS[pair_error] status[
status.new_device = None _K.ERROR] = error_string = _hidpp10.PAIRING_ERRORS[pair_error]
_log.warn("pairing error %d: %s", pair_error, error_string) status.new_device = None
_log.warn("pairing error %d: %s", pair_error, error_string)
status.changed(reason=reason) status.changed(reason=reason)
return True 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): def _process_device_notification(device, status, n):
# incoming packets with SubId >= 0x80 are supposedly replies from # incoming packets with SubId >= 0x80 are supposedly replies from
# HID++ 1.0 requests, should never get here # HID++ 1.0 requests, should never get here
assert n.sub_id & 0x80 == 0 assert n.sub_id & 0x80 == 0
# 0x40 to 0x7F appear to be HID++ 1.0 or DJ notifications # 0x40 to 0x7F appear to be HID++ 1.0 or DJ notifications
if n.sub_id >= 0x40: if n.sub_id >= 0x40:
if len(n.data) == _DJ_NOTIFICATION_LENGTH : if len(n.data) == _DJ_NOTIFICATION_LENGTH:
return _process_dj_notification(device, status, n) return _process_dj_notification(device, status, n)
else: else:
return _process_hidpp10_notification(device, status, n) return _process_hidpp10_notification(device, status, n)
# At this point, we need to know the device's protocol, otherwise it's # At this point, we need to know the device's protocol, otherwise it's
# possible to not know how to handle it. # possible to not know how to handle it.
assert device.protocol is not None assert device.protocol is not None
# some custom battery events for HID++ 1.0 devices # some custom battery events for HID++ 1.0 devices
if device.protocol < 2.0: if device.protocol < 2.0:
return _process_hidpp10_custom_notification(device, status, n) return _process_hidpp10_custom_notification(device, status, n)
# assuming 0x00 to 0x3F are feature (HID++ 2.0) notifications # assuming 0x00 to 0x3F are feature (HID++ 2.0) notifications
assert device.features assert device.features
try: try:
feature = device.features[n.sub_id] feature = device.features[n.sub_id]
except IndexError: except IndexError:
_log.warn("%s: notification from invalid feature index %02X: %s", device, n.sub_id, n) _log.warn("%s: notification from invalid feature index %02X: %s",
return False 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) : def _process_dj_notification(device, status, n):
if _log.isEnabledFor(_DEBUG): if _log.isEnabledFor(_DEBUG):
_log.debug("%s (%s) DJ notification %s", device, device.protocol, n) _log.debug("%s (%s) DJ notification %s", device, device.protocol, n)
if n.sub_id == 0x40: if n.sub_id == 0x40:
# do all DJ paired notifications also show up as HID++ 1.0 notifications? # do all DJ paired notifications also show up as HID++ 1.0 notifications?
if _log.isEnabledFor(_INFO): if _log.isEnabledFor(_INFO):
_log.info("%s: ignoring DJ unpaired: %s", device, n) _log.info("%s: ignoring DJ unpaired: %s", device, n)
return True return True
if n.sub_id == 0x41: if n.sub_id == 0x41:
# do all DJ paired notifications also show up as HID++ 1.0 notifications? # do all DJ paired notifications also show up as HID++ 1.0 notifications?
if _log.isEnabledFor(_INFO): if _log.isEnabledFor(_INFO):
_log.info("%s: ignoring DJ paired: %s", device, n) _log.info("%s: ignoring DJ paired: %s", device, n)
return True return True
if n.sub_id == 0x42: if n.sub_id == 0x42:
if _log.isEnabledFor(_INFO): if _log.isEnabledFor(_INFO):
_log.info("%s: ignoring DJ connection: %s", device, n) _log.info("%s: ignoring DJ connection: %s", device, n)
return True 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): def _process_hidpp10_custom_notification(device, status, n):
if _log.isEnabledFor(_DEBUG): if _log.isEnabledFor(_DEBUG):
_log.debug("%s (%s) custom notification %s", device, device.protocol, n) _log.debug("%s (%s) custom notification %s", device, device.protocol,
n)
if n.sub_id in (_R.battery_status, _R.battery_charge): if n.sub_id in (_R.battery_status, _R.battery_charge):
# message layout: 10 ix <register> <xx> <yy> <zz> <00> # message layout: 10 ix <register> <xx> <yy> <zz> <00>
assert n.data[-1:] == b'\x00' assert n.data[-1:] == b'\x00'
data = chr(n.address).encode() + n.data data = chr(n.address).encode() + n.data
charge, status_text = _hidpp10.parse_battery_status(n.sub_id, data) charge, status_text = _hidpp10.parse_battery_status(n.sub_id, data)
status.set_battery_info(charge, status_text, None) status.set_battery_info(charge, status_text, None)
return True return True
if n.sub_id == _R.keyboard_illumination: if n.sub_id == _R.keyboard_illumination:
# message layout: 10 ix 17("address") <??> <?> <??> <light level 1=off..5=max> # message layout: 10 ix 17("address") <??> <?> <??> <light level 1=off..5=max>
# TODO anything we can do with this? # TODO anything we can do with this?
if _log.isEnabledFor(_INFO): if _log.isEnabledFor(_INFO):
_log.info("illumination event: %s", n) _log.info("illumination event: %s", n)
return True return True
_log.warn("%s: unrecognized %s", device, n) _log.warn("%s: unrecognized %s", device, n)
def _process_hidpp10_notification(device, status, n): def _process_hidpp10_notification(device, status, n):
# unpair notification # unpair notification
if n.sub_id == 0x40: if n.sub_id == 0x40:
if n.address == 0x02: if n.address == 0x02:
# device un-paired # device un-paired
status.clear() status.clear()
device.wpid = None device.wpid = None
device.status = None device.status = None
if device.number in device.receiver: if device.number in device.receiver:
del device.receiver[device.number] del device.receiver[device.number]
status.changed(active=False, alert=_ALERT.ALL, reason=_("unpaired")) status.changed(active=False,
else: alert=_ALERT.ALL,
_log.warn("%s: disconnection with unknown type %02X: %s", device, n.address, n) reason=_("unpaired"))
return True else:
_log.warn("%s: disconnection with unknown type %02X: %s", device,
n.address, n)
return True
# wireless link notification # wireless link notification
if n.sub_id == 0x41: if n.sub_id == 0x41:
protocol_name = ('Bluetooth' if n.address == 0x01 protocol_name = (
else '27 MHz' if n.address == 0x02 'Bluetooth' if n.address == 0x01 else '27 MHz'
else 'QUAD or eQUAD' if n.address == 0x03 if n.address == 0x02 else 'QUAD or eQUAD' if n.address == 0x03 else
else 'eQUAD step 4 DJ' if n.address == 0x04 'eQUAD step 4 DJ' if n.address == 0x04 else 'DFU Lite' if n.
else 'DFU Lite' if n.address == 0x05 address == 0x05 else 'eQUAD step 4 Lite' if n.address ==
else 'eQUAD step 4 Lite' if n.address == 0x06 0x06 else 'eQUAD step 4 Gaming' if n.address ==
else 'eQUAD step 4 Gaming' if n.address == 0x07 0x07 else 'eQUAD step 4 for gamepads' if n.address ==
else 'eQUAD step 4 for gamepads' if n.address == 0x08 0x08 else 'eQUAD nano Lite' if n.address ==
else 'eQUAD nano Lite' if n.address == 0x0A 0x0A else 'Lightspeed 1' if n.address ==
else 'Lightspeed 1' if n.address == 0x0C 0x0C else 'Lightspeed 1_1' if n.address == 0x0D else None)
else 'Lightspeed 1_1' if n.address == 0x0D if protocol_name:
else None) if _log.isEnabledFor(_DEBUG):
if protocol_name: wpid = _strhex(n.data[2:3] + n.data[1:2])
if _log.isEnabledFor(_DEBUG): assert wpid == device.wpid, "%s wpid mismatch, got %s" % (
wpid = _strhex(n.data[2:3] + n.data[1:2]) device, wpid)
assert wpid == device.wpid, "%s wpid mismatch, got %s" % (device, wpid)
flags = ord(n.data[:1]) & 0xF0 flags = ord(n.data[:1]) & 0xF0
link_encrypted = bool(flags & 0x20) link_encrypted = bool(flags & 0x20)
link_established = not (flags & 0x40) link_established = not (flags & 0x40)
if _log.isEnabledFor(_DEBUG): if _log.isEnabledFor(_DEBUG):
sw_present = bool(flags & 0x10) sw_present = bool(flags & 0x10)
has_payload = bool(flags & 0x80) has_payload = bool(flags & 0x80)
_log.debug("%s: %s connection notification: software=%s, encrypted=%s, link=%s, payload=%s", _log.debug(
device, protocol_name, sw_present, link_encrypted, link_established, has_payload) "%s: %s connection notification: software=%s, encrypted=%s, link=%s, payload=%s",
status[_K.LINK_ENCRYPTED] = link_encrypted device, protocol_name, sw_present, link_encrypted,
status.changed(active=link_established) link_established, has_payload)
else: status[_K.LINK_ENCRYPTED] = link_encrypted
_log.warn("%s: connection notification with unknown protocol %02X: %s", device.number, n.address, n) 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: if n.sub_id == 0x49:
# raw input event? just ignore it # raw input event? just ignore it
# if n.address == 0x01, no idea what it is, but they keep on coming # 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, # if n.address == 0x03, appears to be an actual input event,
# because they only come when input happents # because they only come when input happents
return True return True
# power notification # power notification
if n.sub_id == 0x4B: if n.sub_id == 0x4B:
if n.address == 0x01: if n.address == 0x01:
if _log.isEnabledFor(_DEBUG): if _log.isEnabledFor(_DEBUG):
_log.debug("%s: device powered on", device) _log.debug("%s: device powered on", device)
reason = status.to_string() or _("powered on") reason = status.to_string() or _("powered on")
status.changed(active=True, alert=_ALERT.NOTIFICATION, reason=reason) status.changed(active=True,
else: alert=_ALERT.NOTIFICATION,
_log.warn("%s: unknown %s", device, n) reason=reason)
return True 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): def _process_feature_notification(device, status, n, feature):
if feature == _F.BATTERY_STATUS: if feature == _F.BATTERY_STATUS:
if n.address == 0x00: if n.address == 0x00:
discharge_level = ord(n.data[:1]) discharge_level = ord(n.data[:1])
discharge_level = None if discharge_level == 0 else discharge_level discharge_level = None if discharge_level == 0 else discharge_level
discharge_next_level = ord(n.data[1:2]) discharge_next_level = ord(n.data[1:2])
battery_status = ord(n.data[2:3]) battery_status = ord(n.data[2:3])
status.set_battery_info(discharge_level, _hidpp20.BATTERY_STATUS[battery_status], discharge_next_level) status.set_battery_info(discharge_level,
else: _hidpp20.BATTERY_STATUS[battery_status],
_log.warn("%s: unknown BATTERY %s", device, n) discharge_next_level)
return True else:
_log.warn("%s: unknown BATTERY %s", device, n)
return True
if feature == _F.BATTERY_VOLTAGE: if feature == _F.BATTERY_VOLTAGE:
if n.address == 0x00: if n.address == 0x00:
level, status, voltage, _ignore, _ignore =_hidpp20.decipher_voltage(n.data) level, status, voltage, _ignore, _ignore = _hidpp20.decipher_voltage(
status.set_battery_info(level, status, None, voltage) n.data)
else: status.set_battery_info(level, status, None, voltage)
_log.warn("%s: unknown VOLTAGE %s", device, n) else:
return True _log.warn("%s: unknown VOLTAGE %s", device, n)
return True
# TODO: what are REPROG_CONTROLS_V{2,3}? # TODO: what are REPROG_CONTROLS_V{2,3}?
if feature == _F.REPROG_CONTROLS: if feature == _F.REPROG_CONTROLS:
if n.address == 0x00: if n.address == 0x00:
if _log.isEnabledFor(_INFO): if _log.isEnabledFor(_INFO):
_log.info("%s: reprogrammable key: %s", device, n) _log.info("%s: reprogrammable key: %s", device, n)
else: else:
_log.warn("%s: unknown REPROGRAMMABLE KEYS %s", device, n) _log.warn("%s: unknown REPROGRAMMABLE KEYS %s", device, n)
return True return True
if feature == _F.WIRELESS_DEVICE_STATUS: if feature == _F.WIRELESS_DEVICE_STATUS:
if n.address == 0x00: if n.address == 0x00:
if _log.isEnabledFor(_DEBUG): if _log.isEnabledFor(_DEBUG):
_log.debug("wireless status: %s", n) _log.debug("wireless status: %s", n)
if n.data[0:3] == b'\x01\x01\x01': if n.data[0:3] == b'\x01\x01\x01':
status.changed(active=True, alert=_ALERT.NOTIFICATION, reason='powered on') status.changed(active=True,
else: alert=_ALERT.NOTIFICATION,
_log.warn("%s: unknown WIRELESS %s", device, n) reason='powered on')
else: else:
_log.warn("%s: unknown WIRELESS %s", device, n) _log.warn("%s: unknown WIRELESS %s", device, n)
return True else:
_log.warn("%s: unknown WIRELESS %s", device, n)
return True
if feature == _F.SOLAR_DASHBOARD: if feature == _F.SOLAR_DASHBOARD:
if n.data[5:9] == b'GOOD': if n.data[5:9] == b'GOOD':
charge, lux, adc = _unpack('!BHH', n.data[:5]) charge, lux, adc = _unpack('!BHH', n.data[:5])
# guesstimate the battery voltage, emphasis on 'guess' # guesstimate the battery voltage, emphasis on 'guess'
# status_text = '%1.2fV' % (adc * 2.67793237653 / 0x0672) # status_text = '%1.2fV' % (adc * 2.67793237653 / 0x0672)
status_text = _hidpp20.BATTERY_STATUS.discharging status_text = _hidpp20.BATTERY_STATUS.discharging
if n.address == 0x00: if n.address == 0x00:
status[_K.LIGHT_LEVEL] = None status[_K.LIGHT_LEVEL] = None
status.set_battery_info(charge, status_text, None) status.set_battery_info(charge, status_text, None)
elif n.address == 0x10: elif n.address == 0x10:
status[_K.LIGHT_LEVEL] = lux status[_K.LIGHT_LEVEL] = lux
if lux > 200: if lux > 200:
status_text = _hidpp20.BATTERY_STATUS.recharging status_text = _hidpp20.BATTERY_STATUS.recharging
status.set_battery_info(charge, status_text, None) status.set_battery_info(charge, status_text, None)
elif n.address == 0x20: elif n.address == 0x20:
if _log.isEnabledFor(_DEBUG): if _log.isEnabledFor(_DEBUG):
_log.debug("%s: Light Check button pressed", device) _log.debug("%s: Light Check button pressed", device)
status.changed(alert=_ALERT.SHOW_WINDOW) status.changed(alert=_ALERT.SHOW_WINDOW)
# first cancel any reporting # first cancel any reporting
# device.feature_request(_F.SOLAR_DASHBOARD) # device.feature_request(_F.SOLAR_DASHBOARD)
# trigger a new report chain # trigger a new report chain
reports_count = 15 reports_count = 15
reports_period = 2 # seconds reports_period = 2 # seconds
device.feature_request(_F.SOLAR_DASHBOARD, 0x00, reports_count, reports_period) device.feature_request(_F.SOLAR_DASHBOARD, 0x00, reports_count,
else: reports_period)
_log.warn("%s: unknown SOLAR CHARGE %s", device, n) else:
else: _log.warn("%s: unknown SOLAR CHARGE %s", device, n)
_log.warn("%s: SOLAR CHARGE not GOOD? %s", device, n) else:
return True _log.warn("%s: SOLAR CHARGE not GOOD? %s", device, n)
return True
if feature == _F.TOUCHMOUSE_RAW_POINTS: if feature == _F.TOUCHMOUSE_RAW_POINTS:
if n.address == 0x00: if n.address == 0x00:
if _log.isEnabledFor(_INFO): if _log.isEnabledFor(_INFO):
_log.info("%s: TOUCH MOUSE points %s", device, n) _log.info("%s: TOUCH MOUSE points %s", device, n)
elif n.address == 0x10: elif n.address == 0x10:
touch = ord(n.data[:1]) touch = ord(n.data[:1])
button_down = bool(touch & 0x02) button_down = bool(touch & 0x02)
mouse_lifted = bool(touch & 0x01) mouse_lifted = bool(touch & 0x01)
if _log.isEnabledFor(_INFO): if _log.isEnabledFor(_INFO):
_log.info("%s: TOUCH MOUSE status: button_down=%s mouse_lifted=%s", device, button_down, mouse_lifted) _log.info(
else: "%s: TOUCH MOUSE status: button_down=%s mouse_lifted=%s",
_log.warn("%s: unknown TOUCH MOUSE %s", device, n) device, button_down, mouse_lifted)
return True else:
_log.warn("%s: unknown TOUCH MOUSE %s", device, n)
return True
if feature == _F.HIRES_WHEEL: if feature == _F.HIRES_WHEEL:
if (n.address == 0x00): if (n.address == 0x00):
if _log.isEnabledFor(_INFO): if _log.isEnabledFor(_INFO):
flags, delta_v = _unpack('>bh', n.data[:3]) flags, delta_v = _unpack('>bh', n.data[:3])
high_res = (flags & 0x10) != 0 high_res = (flags & 0x10) != 0
periods = flags & 0x0f periods = flags & 0x0f
_log.info("%s: WHEEL: res: %d periods: %d delta V:%-3d", device, high_res, periods, delta_v) _log.info("%s: WHEEL: res: %d periods: %d delta V:%-3d",
return True device, high_res, periods, delta_v)
elif (n.address == 0x10): return True
if _log.isEnabledFor(_INFO): elif (n.address == 0x10):
flags = ord(n.data[:1]) if _log.isEnabledFor(_INFO):
ratchet = flags & 0x01 flags = ord(n.data[:1])
_log.info("%s: WHEEL: ratchet: %d", device, ratchet) ratchet = flags & 0x01
return True _log.info("%s: WHEEL: ratchet: %d", device, ratchet)
else: return True
_log.warn("%s: unknown WHEEL %s", device, n) else:
return True _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)

View File

@ -25,7 +25,6 @@ from logging import getLogger, INFO as _INFO
_log = getLogger(__name__) _log = getLogger(__name__)
del getLogger del getLogger
from .i18n import _ from .i18n import _
from . import base as _base from . import base as _base
from . import hidpp10 as _hidpp10 from . import hidpp10 as _hidpp10
@ -41,516 +40,552 @@ _R = _hidpp10.REGISTERS
# #
# #
class PairedDevice(object): class PairedDevice(object):
def __init__(self, receiver, number, link_notification=None): def __init__(self, receiver, number, link_notification=None):
assert receiver assert receiver
self.receiver = receiver self.receiver = receiver
assert number > 0 and number <= receiver.max_devices assert number > 0 and number <= receiver.max_devices
# Device number, 1..6 for unifying devices, 1 otherwise. # Device number, 1..6 for unifying devices, 1 otherwise.
self.number = number self.number = number
# 'device active' flag; requires manual management. # 'device active' flag; requires manual management.
self.online = None self.online = None
# the Wireless PID is unique per device model # the Wireless PID is unique per device model
self.wpid = None self.wpid = None
self.descriptor = None self.descriptor = None
# mouse, keyboard, etc (see _hidpp10.DEVICE_KIND) # mouse, keyboard, etc (see _hidpp10.DEVICE_KIND)
self._kind = None self._kind = None
# Unifying peripherals report a codename. # Unifying peripherals report a codename.
self._codename = None self._codename = None
# the full name of the model # the full name of the model
self._name = None self._name = None
# HID++ protocol version, 1.0 or 2.0 # HID++ protocol version, 1.0 or 2.0
self._protocol = None self._protocol = None
# serial number (an 8-char hex string) # serial number (an 8-char hex string)
self._serial = None self._serial = None
self._firmware = None self._firmware = None
self._keys = None self._keys = None
self._registers = None self._registers = None
self._settings = None self._settings = None
self._feature_settings_checked = False self._feature_settings_checked = False
# Misc stuff that's irrelevant to any functionality, but may be # Misc stuff that's irrelevant to any functionality, but may be
# displayed in the UI and caching it here helps. # displayed in the UI and caching it here helps.
self._polling_rate = None self._polling_rate = None
self._power_switch = None self._power_switch = None
# if _log.isEnabledFor(_DEBUG): # if _log.isEnabledFor(_DEBUG):
# _log.debug("new PairedDevice(%s, %s, %s)", receiver, number, link_notification) # _log.debug("new PairedDevice(%s, %s, %s)", receiver, number, link_notification)
if link_notification is not None: if link_notification is not None:
self.online = not bool(ord(link_notification.data[0:1]) & 0x40) self.online = not bool(ord(link_notification.data[0:1]) & 0x40)
self.wpid = _strhex(link_notification.data[2:3] + link_notification.data[1:2]) self.wpid = _strhex(link_notification.data[2:3] +
# assert link_notification.address == (0x04 if unifying else 0x03) link_notification.data[1:2])
kind = ord(link_notification.data[0:1]) & 0x0F # assert link_notification.address == (0x04 if unifying else 0x03)
self._kind = _hidpp10.DEVICE_KIND[kind] kind = ord(link_notification.data[0:1]) & 0x0F
else: self._kind = _hidpp10.DEVICE_KIND[kind]
# force a reading of the wpid else:
pair_info = receiver.read_register(_R.receiver_info, 0x20 + number - 1) # force a reading of the wpid
if pair_info: pair_info = receiver.read_register(_R.receiver_info,
# may be either a Unifying receiver, or an Unifying-ready receiver 0x20 + number - 1)
self.wpid = _strhex(pair_info[3:5]) if pair_info:
kind = ord(pair_info[7:8]) & 0x0F # may be either a Unifying receiver, or an Unifying-ready receiver
self._kind = _hidpp10.DEVICE_KIND[kind] self.wpid = _strhex(pair_info[3:5])
self._polling_rate = ord(pair_info[2:3]) kind = ord(pair_info[7:8]) & 0x0F
self._kind = _hidpp10.DEVICE_KIND[kind]
self._polling_rate = ord(pair_info[2:3])
else: else:
# unifying protocol not supported, must be a Nano receiver # unifying protocol not supported, must be a Nano receiver
device_info = self.receiver.read_register(_R.receiver_info, 0x04) device_info = self.receiver.read_register(
if device_info is None: _R.receiver_info, 0x04)
_log.error("failed to read Nano wpid for device %d of %s", number, receiver) if device_info is None:
raise _base.NoSuchDevice(number=number, receiver=receiver, error="read Nano wpid") _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.wpid = _strhex(device_info[3:5])
self._polling_rate = 0 self._polling_rate = 0
self._power_switch = '(' + _("unknown") + ')' self._power_switch = '(' + _("unknown") + ')'
# the wpid is necessary to properly identify wireless link on/off notifications # 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 # 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) assert self.wpid is not None, "failed to read wpid: device %d of %s" % (
number, receiver)
self.descriptor = _DESCRIPTORS.get(self.wpid) self.descriptor = _DESCRIPTORS.get(self.wpid)
if self.descriptor is None: if self.descriptor is None:
# Last chance to correctly identify the device; many Nano receivers # Last chance to correctly identify the device; many Nano receivers
# do not support this call. # do not support this call.
codename = self.receiver.read_register(_R.receiver_info, 0x40 + self.number - 1) codename = self.receiver.read_register(_R.receiver_info,
if codename: 0x40 + self.number - 1)
codename_length = ord(codename[1:2]) if codename:
codename = codename[2:2 + codename_length] codename_length = ord(codename[1:2])
self._codename = codename.decode('ascii') codename = codename[2:2 + codename_length]
self.descriptor = _DESCRIPTORS.get(self._codename) self._codename = codename.decode('ascii')
self.descriptor = _DESCRIPTORS.get(self._codename)
if self.descriptor: if self.descriptor:
self._name = self.descriptor.name self._name = self.descriptor.name
self._protocol = self.descriptor.protocol self._protocol = self.descriptor.protocol
if self._codename is None: if self._codename is None:
self._codename = self.descriptor.codename self._codename = self.descriptor.codename
if self._kind is None: if self._kind is None:
self._kind = self.descriptor.kind self._kind = self.descriptor.kind
if self._protocol is not None: if self._protocol is not None:
self.features = None if self._protocol < 2.0 else _hidpp20.FeaturesArray(self) self.features = None if self._protocol < 2.0 else _hidpp20.FeaturesArray(
else: self)
# may be a 2.0 device; if not, it will fix itself later else:
self.features = _hidpp20.FeaturesArray(self) # may be a 2.0 device; if not, it will fix itself later
self.features = _hidpp20.FeaturesArray(self)
@property @property
def protocol(self): def protocol(self):
if self._protocol is None and self.online is not False: if self._protocol is None and self.online is not False:
self._protocol = _base.ping(self.receiver.handle, self.number) self._protocol = _base.ping(self.receiver.handle, self.number)
# if the ping failed, the peripheral is (almost) certainly offline # if the ping failed, the peripheral is (almost) certainly offline
self.online = self._protocol is not None self.online = self._protocol is not None
# if _log.isEnabledFor(_DEBUG): # if _log.isEnabledFor(_DEBUG):
# _log.debug("device %d protocol %s", self.number, self._protocol) # _log.debug("device %d protocol %s", self.number, self._protocol)
return self._protocol or 0 return self._protocol or 0
@property @property
def codename(self): def codename(self):
if self._codename is None: if self._codename is None:
codename = self.receiver.read_register(_R.receiver_info, 0x40 + self.number - 1) codename = self.receiver.read_register(_R.receiver_info,
if codename: 0x40 + self.number - 1)
codename_length = ord(codename[1:2]) if codename:
codename = codename[2:2 + codename_length] codename_length = ord(codename[1:2])
self._codename = codename.decode('ascii') codename = codename[2:2 + codename_length]
# if _log.isEnabledFor(_DEBUG): self._codename = codename.decode('ascii')
# _log.debug("device %d codename %s", self.number, self._codename) # if _log.isEnabledFor(_DEBUG):
else: # _log.debug("device %d codename %s", self.number, self._codename)
self._codename = '? (%s)' % self.wpid else:
return self._codename self._codename = '? (%s)' % self.wpid
return self._codename
@property @property
def name(self): def name(self):
if self._name is None: if self._name is None:
if self.online and self.protocol >= 2.0: if self.online and self.protocol >= 2.0:
self._name = _hidpp20.get_name(self) self._name = _hidpp20.get_name(self)
return self._name or self.codename or ('Unknown device %s' % self.wpid) return self._name or self.codename or ('Unknown device %s' % self.wpid)
@property @property
def kind(self): def kind(self):
if self._kind is None: if self._kind is None:
pair_info = self.receiver.read_register(_R.receiver_info, 0x20 + self.number - 1) pair_info = self.receiver.read_register(_R.receiver_info,
if pair_info: 0x20 + self.number - 1)
kind = ord(pair_info[7:8]) & 0x0F if pair_info:
self._kind = _hidpp10.DEVICE_KIND[kind] kind = ord(pair_info[7:8]) & 0x0F
self._polling_rate = ord(pair_info[2:3]) self._kind = _hidpp10.DEVICE_KIND[kind]
elif self.online and self.protocol >= 2.0: self._polling_rate = ord(pair_info[2:3])
self._kind = _hidpp20.get_kind(self) elif self.online and self.protocol >= 2.0:
return self._kind or '?' self._kind = _hidpp20.get_kind(self)
return self._kind or '?'
@property @property
def firmware(self): def firmware(self):
if self._firmware is None and self.online: if self._firmware is None and self.online:
if self.protocol >= 2.0: if self.protocol >= 2.0:
self._firmware = _hidpp20.get_firmware(self) self._firmware = _hidpp20.get_firmware(self)
else: else:
self._firmware = _hidpp10.get_firmware(self) self._firmware = _hidpp10.get_firmware(self)
return self._firmware or () return self._firmware or ()
@property @property
def serial(self): def serial(self):
if self._serial is None: if self._serial is None:
serial = self.receiver.read_register(_R.receiver_info, 0x30 + self.number - 1) serial = self.receiver.read_register(_R.receiver_info,
if serial: 0x30 + self.number - 1)
ps = ord(serial[9:10]) & 0x0F if serial:
self._power_switch = _hidpp10.POWER_SWITCH_LOCATION[ps] ps = ord(serial[9:10]) & 0x0F
else: self._power_switch = _hidpp10.POWER_SWITCH_LOCATION[ps]
# some Nano receivers? else:
serial = self.receiver.read_register(0x2D5) # some Nano receivers?
serial = self.receiver.read_register(0x2D5)
if serial: if serial:
self._serial = _strhex(serial[1:5]) self._serial = _strhex(serial[1:5])
else: else:
# fallback... # fallback...
self._serial = self.receiver.serial self._serial = self.receiver.serial
return self._serial or '?' return self._serial or '?'
@property @property
def power_switch_location(self): def power_switch_location(self):
if self._power_switch is None: if self._power_switch is None:
ps = self.receiver.read_register(_R.receiver_info, 0x30 + self.number - 1) ps = self.receiver.read_register(_R.receiver_info,
if ps is not None: 0x30 + self.number - 1)
ps = ord(ps[9:10]) & 0x0F if ps is not None:
self._power_switch = _hidpp10.POWER_SWITCH_LOCATION[ps] ps = ord(ps[9:10]) & 0x0F
else: self._power_switch = _hidpp10.POWER_SWITCH_LOCATION[ps]
self._power_switch = '(unknown)' else:
return self._power_switch self._power_switch = '(unknown)'
return self._power_switch
@property @property
def polling_rate(self): def polling_rate(self):
if self._polling_rate is None: if self._polling_rate is None:
pair_info = self.receiver.read_register(_R.receiver_info, 0x20 + self.number - 1) pair_info = self.receiver.read_register(_R.receiver_info,
if pair_info: 0x20 + self.number - 1)
self._polling_rate = ord(pair_info[2:3]) if pair_info:
else: self._polling_rate = ord(pair_info[2:3])
self._polling_rate = 0 else:
return self._polling_rate self._polling_rate = 0
return self._polling_rate
@property @property
def keys(self): def keys(self):
if self._keys is None: if self._keys is None:
if self.online and self.protocol >= 2.0: if self.online and self.protocol >= 2.0:
self._keys = _hidpp20.get_keys(self) or () self._keys = _hidpp20.get_keys(self) or ()
return self._keys return self._keys
@property @property
def registers(self): def registers(self):
if self._registers is None: if self._registers is None:
if self.descriptor and self.descriptor.registers: if self.descriptor and self.descriptor.registers:
self._registers = list(self.descriptor.registers) self._registers = list(self.descriptor.registers)
else: else:
self._registers = [] self._registers = []
return self._registers return self._registers
@property @property
def settings(self): def settings(self):
if self._settings is None: if self._settings is None:
if self.descriptor and self.descriptor.settings: if self.descriptor and self.descriptor.settings:
self._settings = [s(self) for s in 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] self._settings = [s for s in self._settings if s is not None]
else: else:
self._settings = [] self._settings = []
if not self._feature_settings_checked: if not self._feature_settings_checked:
self._feature_settings_checked =_check_feature_settings(self, self._settings) self._feature_settings_checked = _check_feature_settings(
return self._settings self, self._settings)
return self._settings
def enable_notifications(self, enable=True): def enable_notifications(self, enable=True):
"""Enable or disable device (dis)connection notifications on this """Enable or disable device (dis)connection notifications on this
receiver.""" receiver."""
if not bool(self.receiver) or self.protocol >= 2.0: if not bool(self.receiver) or self.protocol >= 2.0:
return False return False
if enable: if enable:
set_flag_bits = ( _hidpp10.NOTIFICATION_FLAG.battery_status set_flag_bits = (_hidpp10.NOTIFICATION_FLAG.battery_status
| _hidpp10.NOTIFICATION_FLAG.keyboard_illumination | _hidpp10.NOTIFICATION_FLAG.keyboard_illumination
| _hidpp10.NOTIFICATION_FLAG.wireless | _hidpp10.NOTIFICATION_FLAG.wireless
| _hidpp10.NOTIFICATION_FLAG.software_present ) | _hidpp10.NOTIFICATION_FLAG.software_present)
else: else:
set_flag_bits = 0 set_flag_bits = 0
ok = _hidpp10.set_notification_flags(self, set_flag_bits) ok = _hidpp10.set_notification_flags(self, set_flag_bits)
if ok is None: if ok is None:
_log.warn("%s: failed to %s device notifications", self, 'enable' if enable else 'disable') _log.warn("%s: failed to %s device notifications", self,
'enable' if enable else 'disable')
flag_bits = _hidpp10.get_notification_flags(self) flag_bits = _hidpp10.get_notification_flags(self)
flag_names = None if flag_bits is None else tuple(_hidpp10.NOTIFICATION_FLAG.flag_names(flag_bits)) flag_names = None if flag_bits is None else tuple(
if _log.isEnabledFor(_INFO): _hidpp10.NOTIFICATION_FLAG.flag_names(flag_bits))
_log.info("%s: device notifications %s %s", self, 'enabled' if enable else 'disabled', flag_names) if _log.isEnabledFor(_INFO):
return flag_bits if ok else None _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): def request(self, request_id, *params):
return _base.request(self.receiver.handle, self.number, request_id, *params) return _base.request(self.receiver.handle, self.number, request_id,
*params)
read_register = _hidpp10.read_register read_register = _hidpp10.read_register
write_register = _hidpp10.write_register write_register = _hidpp10.write_register
def feature_request(self, feature, function=0x00, *params): def feature_request(self, feature, function=0x00, *params):
if self.protocol >= 2.0: if self.protocol >= 2.0:
return _hidpp20.feature_request(self, feature, function, *params) return _hidpp20.feature_request(self, feature, function, *params)
def ping(self): def ping(self):
"""Checks if the device is online, returns True of False""" """Checks if the device is online, returns True of False"""
protocol = _base.ping(self.receiver.handle, self.number) protocol = _base.ping(self.receiver.handle, self.number)
self.online = protocol is not None self.online = protocol is not None
if protocol is not None: if protocol is not None:
self._protocol = protocol self._protocol = protocol
return self.online return self.online
def __index__(self): def __index__(self):
return self.number return self.number
__int__ = __index__
def __eq__(self, other): __int__ = __index__
return other is not None and self.kind == other.kind and self.wpid == other.wpid
def __ne__(self, other): def __eq__(self, other):
return other is None or self.kind != other.kind or self.wpid != other.wpid return other is not None and self.kind == other.kind and self.wpid == other.wpid
def __hash__(self): def __ne__(self, other):
return self.wpid.__hash__() 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 '<PairedDevice(%d,%s,%s,%s)>' % (
self.number, self.wpid, self.codename or '?', self.serial)
__unicode__ = __repr__ = __str__
def __str__(self):
return '<PairedDevice(%d,%s,%s,%s)>' % (self.number, self.wpid, self.codename or '?', self.serial)
__unicode__ = __repr__ = __str__
# #
# #
# #
class Receiver(object): class Receiver(object):
"""A Unifying Receiver instance. """A Unifying Receiver instance.
The paired devices are available through the sequence interface. The paired devices are available through the sequence interface.
""" """
number = 0xFF number = 0xFF
kind = None kind = None
def __init__(self, handle, device_info): def __init__(self, handle, device_info):
assert handle assert handle
self.handle = handle self.handle = handle
assert device_info assert device_info
self.path = device_info.path self.path = device_info.path
# USB product id, used for some Nano receivers # USB product id, used for some Nano receivers
self.product_id = device_info.product_id self.product_id = device_info.product_id
product_info = _product_information(self.product_id) product_info = _product_information(self.product_id)
if not product_info: if not product_info:
raise Exception("Unknown receiver type", self.product_id) raise Exception("Unknown receiver type", self.product_id)
# read the serial immediately, so we can find out max_devices # read the serial immediately, so we can find out max_devices
serial_reply = self.read_register(_R.receiver_info, 0x03) serial_reply = self.read_register(_R.receiver_info, 0x03)
if serial_reply : if serial_reply:
self.serial = _strhex(serial_reply[1:5]) self.serial = _strhex(serial_reply[1:5])
self.max_devices = ord(serial_reply[6:7]) self.max_devices = ord(serial_reply[6:7])
# TODO _properly_ figure out which receivers do and which don't support unpairing # 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 # 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 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) else: # handle receivers that don't have a serial number specially (i.e., c534)
self.serial = None self.serial = None
self.max_devices = product_info.get('max_devices',1) self.max_devices = product_info.get('max_devices', 1)
self.may_unpair = product_info.get('may_unpair',False) self.may_unpair = product_info.get('may_unpair', False)
self.name = product_info.get('name','') self.name = product_info.get('name', '')
self.re_pairs = product_info.get('re_pairs',False) self.re_pairs = product_info.get('re_pairs', False)
self._str = '<%s(%s,%s%s)>' % (self.name.replace(' ', ''), self.path, '' if isinstance(self.handle, int) else 'T', self.handle) self._str = '<%s(%s,%s%s)>' % (self.name.replace(
' ', ''), self.path, '' if isinstance(self.handle, int) else 'T',
self.handle)
self._firmware = None self._firmware = None
self._devices = {} self._devices = {}
self._remaining_pairings = None self._remaining_pairings = None
def close(self): def close(self):
handle, self.handle = self.handle, None handle, self.handle = self.handle, None
self._devices.clear() self._devices.clear()
return (handle and _base.close(handle)) return (handle and _base.close(handle))
def __del__(self): def __del__(self):
self.close() self.close()
@property @property
def firmware(self): def firmware(self):
if self._firmware is None and self.handle: if self._firmware is None and self.handle:
self._firmware = _hidpp10.get_firmware(self) self._firmware = _hidpp10.get_firmware(self)
return self._firmware return self._firmware
# how many pairings remain (None for unknown, -1 for unlimited) # how many pairings remain (None for unknown, -1 for unlimited)
def remaining_pairings(self,cache=True): def remaining_pairings(self, cache=True):
if self._remaining_pairings is None or not cache: if self._remaining_pairings is None or not cache:
ps = self.read_register(_R.receiver_connection) ps = self.read_register(_R.receiver_connection)
if ps is not None: if ps is not None:
ps = ord(ps[2:3]) ps = ord(ps[2:3])
self._remaining_pairings = ps-5 if ps >= 5 else -1 self._remaining_pairings = ps - 5 if ps >= 5 else -1
return self._remaining_pairings return self._remaining_pairings
def enable_notifications(self, enable=True): def enable_notifications(self, enable=True):
"""Enable or disable device (dis)connection notifications on this """Enable or disable device (dis)connection notifications on this
receiver.""" receiver."""
if not self.handle: if not self.handle:
return False return False
if enable: if enable:
set_flag_bits = ( _hidpp10.NOTIFICATION_FLAG.battery_status set_flag_bits = (_hidpp10.NOTIFICATION_FLAG.battery_status
| _hidpp10.NOTIFICATION_FLAG.wireless | _hidpp10.NOTIFICATION_FLAG.wireless
| _hidpp10.NOTIFICATION_FLAG.software_present ) | _hidpp10.NOTIFICATION_FLAG.software_present)
else: else:
set_flag_bits = 0 set_flag_bits = 0
ok = _hidpp10.set_notification_flags(self, set_flag_bits) ok = _hidpp10.set_notification_flags(self, set_flag_bits)
if ok is None: if ok is None:
_log.warn("%s: failed to %s receiver notifications", self, 'enable' if enable else 'disable') _log.warn("%s: failed to %s receiver notifications", self,
return None 'enable' if enable else 'disable')
return None
flag_bits = _hidpp10.get_notification_flags(self) flag_bits = _hidpp10.get_notification_flags(self)
flag_names = None if flag_bits is None else tuple(_hidpp10.NOTIFICATION_FLAG.flag_names(flag_bits)) flag_names = None if flag_bits is None else tuple(
if _log.isEnabledFor(_INFO): _hidpp10.NOTIFICATION_FLAG.flag_names(flag_bits))
_log.info("%s: receiver notifications %s => %s", self, 'enabled' if enable else 'disabled', flag_names) if _log.isEnabledFor(_INFO):
return flag_bits _log.info("%s: receiver notifications %s => %s", self,
'enabled' if enable else 'disabled', flag_names)
return flag_bits
def notify_devices(self): def notify_devices(self):
"""Scan all devices.""" """Scan all devices."""
if self.handle: if self.handle:
if not self.write_register(_R.receiver_connection, 0x02): if not self.write_register(_R.receiver_connection, 0x02):
_log.warn("%s: failed to trigger device link notifications", self) _log.warn("%s: failed to trigger device link notifications",
self)
def register_new_device(self, number, notification=None): def register_new_device(self, number, notification=None):
if self._devices.get(number) is not None: if self._devices.get(number) is not None:
raise IndexError("%s: device number %d already registered" % (self, number)) raise IndexError("%s: device number %d already registered" %
(self, number))
assert notification is None or notification.devnumber == number assert notification is None or notification.devnumber == number
assert notification is None or notification.sub_id == 0x41 assert notification is None or notification.sub_id == 0x41
try: try:
dev = PairedDevice(self, number, notification) dev = PairedDevice(self, number, notification)
assert dev.wpid assert dev.wpid
if _log.isEnabledFor(_INFO): if _log.isEnabledFor(_INFO):
_log.info("%s: found new device %d (%s)", self, number, dev.wpid) _log.info("%s: found new device %d (%s)", self, number,
self._devices[number] = dev dev.wpid)
return dev self._devices[number] = dev
except _base.NoSuchDevice: return dev
_log.exception("register_new_device") except _base.NoSuchDevice:
_log.exception("register_new_device")
_log.warning("%s: looked for device %d, not found", self, number) _log.warning("%s: looked for device %d, not found", self, number)
self._devices[number] = None self._devices[number] = None
def set_lock(self, lock_closed=True, device=0, timeout=0): def set_lock(self, lock_closed=True, device=0, timeout=0):
if self.handle: if self.handle:
action = 0x02 if lock_closed else 0x01 action = 0x02 if lock_closed else 0x01
reply = self.write_register(_R.receiver_pairing, action, device, timeout) reply = self.write_register(_R.receiver_pairing, action, device,
if reply: timeout)
return True if reply:
_log.warn("%s: failed to %s the receiver lock", self, 'close' if lock_closed else 'open') return True
_log.warn("%s: failed to %s the receiver lock", self,
'close' if lock_closed else 'open')
def count(self): def count(self):
count = self.read_register(_R.receiver_connection) count = self.read_register(_R.receiver_connection)
return 0 if count is None else ord(count[1:2]) return 0 if count is None else ord(count[1:2])
# def has_devices(self): # def has_devices(self):
# return len(self) > 0 or self.count() > 0 # return len(self) > 0 or self.count() > 0
def request(self, request_id, *params): def request(self, request_id, *params):
if bool(self): if bool(self):
return _base.request(self.handle, 0xFF, request_id, *params) return _base.request(self.handle, 0xFF, request_id, *params)
read_register = _hidpp10.read_register read_register = _hidpp10.read_register
write_register = _hidpp10.write_register write_register = _hidpp10.write_register
def __iter__(self): def __iter__(self):
for number in range(1, 1 + self.max_devices): for number in range(1, 1 + self.max_devices):
if number in self._devices: if number in self._devices:
dev = self._devices[number] dev = self._devices[number]
else: else:
dev = self.__getitem__(number) dev = self.__getitem__(number)
if dev is not None: if dev is not None:
yield dev yield dev
def __getitem__(self, key): def __getitem__(self, key):
if not bool(self): if not bool(self):
return None return None
dev = self._devices.get(key) dev = self._devices.get(key)
if dev is not None: if dev is not None:
return dev return dev
if not isinstance(key, int): if not isinstance(key, int):
raise TypeError('key must be an integer') raise TypeError('key must be an integer')
if key < 1 or key > self.max_devices: if key < 1 or key > self.max_devices:
raise IndexError(key) raise IndexError(key)
return self.register_new_device(key) return self.register_new_device(key)
def __delitem__(self, key): def __delitem__(self, key):
self._unpair_device(key, False) self._unpair_device(key, False)
def _unpair_device(self, key, force=False): def _unpair_device(self, key, force=False):
key = int(key) key = int(key)
if self._devices.get(key) is None: if self._devices.get(key) is None:
raise IndexError(key) raise IndexError(key)
dev = self._devices[key] dev = self._devices[key]
if not dev: if not dev:
if key in self._devices: if key in self._devices:
del self._devices[key] del self._devices[key]
return return
if self.re_pairs and not force: if self.re_pairs and not force:
# invalidate the device, but these receivers don't unpair per se # invalidate the device, but these receivers don't unpair per se
dev.online = False dev.online = False
dev.wpid = None dev.wpid = None
if key in self._devices: if key in self._devices:
del self._devices[key] del self._devices[key]
_log.warn("%s removed device %s", self, dev) _log.warn("%s removed device %s", self, dev)
else: else:
reply = self.write_register(_R.receiver_pairing, 0x03, key) reply = self.write_register(_R.receiver_pairing, 0x03, key)
if reply: if reply:
# invalidate the device # invalidate the device
dev.online = False dev.online = False
dev.wpid = None dev.wpid = None
if key in self._devices: if key in self._devices:
del self._devices[key] del self._devices[key]
_log.warn("%s unpaired device %s", self, dev) _log.warn("%s unpaired device %s", self, dev)
else: else:
_log.error("%s failed to unpair device %s", self, dev) _log.error("%s failed to unpair device %s", self, dev)
raise IndexError(key) raise IndexError(key)
def __len__(self): def __len__(self):
return len([d for d in self._devices.values() if d is not None]) return len([d for d in self._devices.values() if d is not None])
def __contains__(self, dev): def __contains__(self, dev):
if isinstance(dev, int): if isinstance(dev, int):
return self._devices.get(dev) is not None return self._devices.get(dev) is not None
return self.__contains__(dev.number) return self.__contains__(dev.number)
def __eq__(self, other): def __eq__(self, other):
return other is not None and self.kind == other.kind and self.path == other.path return other is not None and self.kind == other.kind and self.path == other.path
def __ne__(self, other): def __ne__(self, other):
return other is None or self.kind != other.kind or self.path != other.path return other is None or self.kind != other.kind or self.path != other.path
def __hash__(self): def __hash__(self):
return self.path.__hash__() return self.path.__hash__()
def __str__(self): def __str__(self):
return self._str return self._str
__unicode__ = __repr__ = __str__
__bool__ = __nonzero__ = lambda self: self.handle is not None __unicode__ = __repr__ = __str__
@classmethod __bool__ = __nonzero__ = lambda self: self.handle is not None
def open(self, device_info):
"""Opens a Logitech Receiver found attached to the machine, by Linux device path. @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``. :returns: An open file handle for the found receiver, or ``None``.
""" """
try: try:
handle = _base.open_path(device_info.path) handle = _base.open_path(device_info.path)
if handle: if handle:
return Receiver(handle, device_info) return Receiver(handle, device_info)
except OSError as e: except OSError as e:
_log.exception("open %s", device_info) _log.exception("open %s", device_info)
if e.errno == _errno.EACCES: if e.errno == _errno.EACCES:
raise raise
except: except:
_log.exception("open %s", device_info) _log.exception("open %s", device_info)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -22,490 +22,488 @@
from __future__ import absolute_import, division, print_function, unicode_literals from __future__ import absolute_import, division, print_function, unicode_literals
from .common import NamedInts as _NamedInts from .common import NamedInts as _NamedInts
# <controls.xml awk -F\" '/<Control /{sub(/^LD_FINFO_(CTRLID_)?/, "", $2);printf("\t%s=0x%04X,\n", $2, $4)}' | sort -t= -k2 # <controls.xml awk -F\" '/<Control /{sub(/^LD_FINFO_(CTRLID_)?/, "", $2);printf("\t%s=0x%04X,\n", $2, $4)}' | sort -t= -k2
CONTROL = _NamedInts( CONTROL = _NamedInts(
Volume_Up=0x0001, Volume_Up=0x0001,
Volume_Down=0x0002, Volume_Down=0x0002,
Mute=0x0003, Mute=0x0003,
Play__Pause=0x0004, Play__Pause=0x0004,
Next=0x0005, Next=0x0005,
Previous=0x0006, Previous=0x0006,
Stop=0x0007, Stop=0x0007,
Application_Switcher=0x0008, Application_Switcher=0x0008,
BURN=0x0009, BURN=0x0009,
Calculator=0x000A, Calculator=0x000A,
CALENDAR=0x000B, CALENDAR=0x000B,
CLOSE=0x000C, CLOSE=0x000C,
EJECT=0x000D, EJECT=0x000D,
Mail=0x000E, Mail=0x000E,
HELP_AS_HID=0x000F, HELP_AS_HID=0x000F,
HELP_AS_F1=0x0010, HELP_AS_F1=0x0010,
LAUNCH_WORD_PROC=0x0011, LAUNCH_WORD_PROC=0x0011,
LAUNCH_SPREADSHEET=0x0012, LAUNCH_SPREADSHEET=0x0012,
LAUNCH_PRESENTATION=0x0013, LAUNCH_PRESENTATION=0x0013,
UNDO_AS_CTRL_Z=0x0014, UNDO_AS_CTRL_Z=0x0014,
UNDO_AS_HID=0x0015, UNDO_AS_HID=0x0015,
REDO_AS_CTRL_Y=0x0016, REDO_AS_CTRL_Y=0x0016,
REDO_AS_HID=0x0017, REDO_AS_HID=0x0017,
PRINT_AS_CTRL_P=0x0018, PRINT_AS_CTRL_P=0x0018,
PRINT_AS_HID=0x0019, PRINT_AS_HID=0x0019,
SAVE_AS_CTRL_S=0x001A, SAVE_AS_CTRL_S=0x001A,
SAVE_AS_HID=0x001B, SAVE_AS_HID=0x001B,
PRESET_A=0x001C, PRESET_A=0x001C,
PRESET_B=0x001D, PRESET_B=0x001D,
PRESET_C=0x001E, PRESET_C=0x001E,
PRESET_D=0x001F, PRESET_D=0x001F,
FAVORITES=0x0020, FAVORITES=0x0020,
GADGETS=0x0021, GADGETS=0x0021,
MY_HOME=0x0022, MY_HOME=0x0022,
GADGETS_AS_WIN_G=0x0023, GADGETS_AS_WIN_G=0x0023,
MAXIMIZE_AS_HID=0x0024, MAXIMIZE_AS_HID=0x0024,
MAXIMIZE_AS_WIN_SHIFT_M=0x0025, MAXIMIZE_AS_WIN_SHIFT_M=0x0025,
MINIMIZE_AS_HID=0x0026, MINIMIZE_AS_HID=0x0026,
MINIMIZE_AS_WIN_M=0x0027, MINIMIZE_AS_WIN_M=0x0027,
MEDIA_PLAYER=0x0028, MEDIA_PLAYER=0x0028,
MEDIA_CENTER_LOGI=0x0029, MEDIA_CENTER_LOGI=0x0029,
MEDIA_CENTER_MSFT=0x002A, # Should not be used as it is not reprogrammable under Windows MEDIA_CENTER_MSFT=
CUSTOM_MENU=0x002B, 0x002A, # Should not be used as it is not reprogrammable under Windows
MESSENGER=0x002C, CUSTOM_MENU=0x002B,
MY_DOCUMENTS=0x002D, MESSENGER=0x002C,
MY_MUSIC=0x002E, MY_DOCUMENTS=0x002D,
WEBCAM=0x002F, MY_MUSIC=0x002E,
MY_PICTURES=0x0030, WEBCAM=0x002F,
MY_VIDEOS=0x0031, MY_PICTURES=0x0030,
MY_COMPUTER_AS_HID=0x0032, MY_VIDEOS=0x0031,
MY_COMPUTER_AS_WIN_E=0x0033, MY_COMPUTER_AS_HID=0x0032,
LAUNC_PICTURE_VIEWER=0x0035, MY_COMPUTER_AS_WIN_E=0x0033,
ONE_TOUCH_SEARCH=0x0036, LAUNC_PICTURE_VIEWER=0x0035,
PRESET_1=0x0037, ONE_TOUCH_SEARCH=0x0036,
PRESET_2=0x0038, PRESET_1=0x0037,
PRESET_3=0x0039, PRESET_2=0x0038,
PRESET_4=0x003A, PRESET_3=0x0039,
RECORD=0x003B, PRESET_4=0x003A,
INTERNET_REFRESH=0x003C, RECORD=0x003B,
ROTATE_RIGHT=0x003D, INTERNET_REFRESH=0x003C,
Search=0x003E, # SEARCH ROTATE_RIGHT=0x003D,
SHUFFLE=0x003F, Search=0x003E, # SEARCH
SLEEP=0x0040, SHUFFLE=0x003F,
INTERNET_STOP=0x0041, SLEEP=0x0040,
SYNCHRONIZE=0x0042, INTERNET_STOP=0x0041,
ZOOM=0x0043, SYNCHRONIZE=0x0042,
ZOOM_IN_AS_HID=0x0044, ZOOM=0x0043,
ZOOM_IN_AS_CTRL_WHEEL=0x0045, ZOOM_IN_AS_HID=0x0044,
ZOOM_IN_AS_CLTR_PLUS=0x0046, ZOOM_IN_AS_CTRL_WHEEL=0x0045,
ZOOM_OUT_AS_HID=0x0047, ZOOM_IN_AS_CLTR_PLUS=0x0046,
ZOOM_OUT_AS_CTRL_WHEEL=0x0048, ZOOM_OUT_AS_HID=0x0047,
ZOOM_OUT_AS_CLTR_MINUS=0x0049, ZOOM_OUT_AS_CTRL_WHEEL=0x0048,
ZOOM_RESET=0x004A, ZOOM_OUT_AS_CLTR_MINUS=0x0049,
ZOOM_FULL_SCREEN=0x004B, ZOOM_RESET=0x004A,
PRINT_SCREEN=0x004C, ZOOM_FULL_SCREEN=0x004B,
PAUSE_BREAK=0x004D, PRINT_SCREEN=0x004C,
SCROLL_LOCK=0x004E, PAUSE_BREAK=0x004D,
CONTEXTUAL_MENU=0x004F, SCROLL_LOCK=0x004E,
Left_Button=0x0050, # LEFT_CLICK CONTEXTUAL_MENU=0x004F,
Right_Button=0x0051, # RIGHT_CLICK Left_Button=0x0050, # LEFT_CLICK
Middle_Button=0x0052, # MIDDLE_BUTTON Right_Button=0x0051, # RIGHT_CLICK
Back_Button=0x0053, # from M510v2 was BACK_AS_BUTTON_4 Middle_Button=0x0052, # MIDDLE_BUTTON
Back=0x0054, # BACK_AS_HID Back_Button=0x0053, # from M510v2 was BACK_AS_BUTTON_4
BACK_AS_ALT_WIN_ARROW=0x0055, Back=0x0054, # BACK_AS_HID
Forward_Button=0x0056, # from M510v2 was FORWARD_AS_BUTTON_5 BACK_AS_ALT_WIN_ARROW=0x0055,
FORWARD_AS_HID=0x0057, Forward_Button=0x0056, # from M510v2 was FORWARD_AS_BUTTON_5
FORWARD_AS_ALT_WIN_ARROW=0x0058, FORWARD_AS_HID=0x0057,
BUTTON_6=0x0059, FORWARD_AS_ALT_WIN_ARROW=0x0058,
LEFT_SCROLL_AS_BUTTON_7=0x005A, BUTTON_6=0x0059,
Left_Tilt=0x005B, # from M510v2 was LEFT_SCROLL_AS_AC_PAN LEFT_SCROLL_AS_BUTTON_7=0x005A,
RIGHT_SCROLL_AS_BUTTON_8=0x005C, Left_Tilt=0x005B, # from M510v2 was LEFT_SCROLL_AS_AC_PAN
Right_Tilt=0x005D, # from M510v2 was RIGHT_SCROLL_AS_AC_PAN RIGHT_SCROLL_AS_BUTTON_8=0x005C,
BUTTON_9=0x005E, Right_Tilt=0x005D, # from M510v2 was RIGHT_SCROLL_AS_AC_PAN
BUTTON_10=0x005F, BUTTON_9=0x005E,
BUTTON_11=0x0060, BUTTON_10=0x005F,
BUTTON_12=0x0061, BUTTON_11=0x0060,
BUTTON_13=0x0062, BUTTON_12=0x0061,
BUTTON_14=0x0063, BUTTON_13=0x0062,
BUTTON_15=0x0064, BUTTON_14=0x0063,
BUTTON_16=0x0065, BUTTON_15=0x0064,
BUTTON_17=0x0066, BUTTON_16=0x0065,
BUTTON_18=0x0067, BUTTON_17=0x0066,
BUTTON_19=0x0068, BUTTON_18=0x0067,
BUTTON_20=0x0069, BUTTON_19=0x0068,
BUTTON_21=0x006A, BUTTON_20=0x0069,
BUTTON_22=0x006B, BUTTON_21=0x006A,
BUTTON_23=0x006C, BUTTON_22=0x006B,
BUTTON_24=0x006D, BUTTON_23=0x006C,
Show_Desktop=0x006E, # Show_Desktop BUTTON_24=0x006D,
Lock_PC=0x006F, Show_Desktop=0x006E, # Show_Desktop
FN_F1=0x0070, Lock_PC=0x006F,
FN_F2=0x0071, FN_F1=0x0070,
FN_F3=0x0072, FN_F2=0x0071,
FN_F4=0x0073, FN_F3=0x0072,
FN_F5=0x0074, FN_F4=0x0073,
FN_F6=0x0075, FN_F5=0x0074,
FN_F7=0x0076, FN_F6=0x0075,
FN_F8=0x0077, FN_F7=0x0076,
FN_F9=0x0078, FN_F8=0x0077,
FN_F10=0x0079, FN_F9=0x0078,
FN_F11=0x007A, FN_F10=0x0079,
FN_F12=0x007B, FN_F11=0x007A,
FN_F13=0x007C, FN_F12=0x007B,
FN_F14=0x007D, FN_F13=0x007C,
FN_F15=0x007E, FN_F14=0x007D,
FN_F16=0x007F, FN_F15=0x007E,
FN_F17=0x0080, FN_F16=0x007F,
FN_F18=0x0081, FN_F17=0x0080,
FN_F19=0x0082, FN_F18=0x0081,
IOS_HOME=0x0083, FN_F19=0x0082,
ANDROID_HOME=0x0084, IOS_HOME=0x0083,
ANDROID_MENU=0x0085, ANDROID_HOME=0x0084,
ANDROID_SEARCH=0x0086, ANDROID_MENU=0x0085,
ANDROID_BACK=0x0087, ANDROID_SEARCH=0x0086,
HOME_COMBO=0x0088, ANDROID_BACK=0x0087,
LOCK_COMBO=0x0089, HOME_COMBO=0x0088,
IOS_VIRTUAL_KEYBOARD=0x008A, LOCK_COMBO=0x0089,
IOS_LANGUAGE_SWICH=0x008B, IOS_VIRTUAL_KEYBOARD=0x008A,
MAC_EXPOSE=0x008C, IOS_LANGUAGE_SWICH=0x008B,
MAC_DASHBOARD=0x008D, MAC_EXPOSE=0x008C,
WIN7_SNAP_LEFT=0x008E, MAC_DASHBOARD=0x008D,
WIN7_SNAP_RIGHT=0x008F, WIN7_SNAP_LEFT=0x008E,
Minimize_Window=0x0090, # WIN7_MINIMIZE_AS_WIN_ARROW WIN7_SNAP_RIGHT=0x008F,
Maximize_Window=0x0091, # WIN7_MAXIMIZE_AS_WIN_ARROW Minimize_Window=0x0090, # WIN7_MINIMIZE_AS_WIN_ARROW
WIN7_STRETCH_UP=0x0092, Maximize_Window=0x0091, # WIN7_MAXIMIZE_AS_WIN_ARROW
WIN7_MONITOR_SWITCH_AS_WIN_SHIFT_LEFTARROW=0x0093, WIN7_STRETCH_UP=0x0092,
WIN7_MONITOR_SWITCH_AS_WIN_SHIFT_RIGHTARROW=0x0094, WIN7_MONITOR_SWITCH_AS_WIN_SHIFT_LEFTARROW=0x0093,
Switch_Screen=0x0095, # WIN7_SHOW_PRESENTATION_MODE WIN7_MONITOR_SWITCH_AS_WIN_SHIFT_RIGHTARROW=0x0094,
WIN7_SHOW_MOBILITY_CENTER=0x0096, Switch_Screen=0x0095, # WIN7_SHOW_PRESENTATION_MODE
ANALOG_HSCROLL=0x0097, WIN7_SHOW_MOBILITY_CENTER=0x0096,
METRO_APPSWITCH=0x009F, ANALOG_HSCROLL=0x0097,
METRO_APPBAR=0x00A0, METRO_APPSWITCH=0x009F,
METRO_CHARMS=0x00A1, METRO_APPBAR=0x00A0,
CALC_VKEYBOARD=0x00A2, METRO_CHARMS=0x00A1,
METRO_SEARCH=0x00A3, CALC_VKEYBOARD=0x00A2,
COMBO_SLEEP=0x00A4, METRO_SEARCH=0x00A3,
METRO_SHARE=0x00A5, COMBO_SLEEP=0x00A4,
METRO_SETTINGS=0x00A6, METRO_SHARE=0x00A5,
METRO_DEVICES=0x00A7, METRO_SETTINGS=0x00A6,
METRO_START_SCREEN=0x00A9, METRO_DEVICES=0x00A7,
ZOOMIN=0x00AA, METRO_START_SCREEN=0x00A9,
ZOOMOUT=0x00AB, ZOOMIN=0x00AA,
BACK_HSCROLL=0x00AC, ZOOMOUT=0x00AB,
SHOW_DESKTOP_HPP=0x00AE, BACK_HSCROLL=0x00AC,
Fn_Left_Click=0x00B7, # from K400 Plus SHOW_DESKTOP_HPP=0x00AE,
# https://docs.google.com/document/u/0/d/1YvXICgSe8BcBAuMr4Xu_TutvAxaa-RnGfyPFWBWzhkc/export?format=docx Fn_Left_Click=0x00B7, # from K400 Plus
# Extract to csv. Eliminate extra linefeeds and spaces. # https://docs.google.com/document/u/0/d/1YvXICgSe8BcBAuMr4Xu_TutvAxaa-RnGfyPFWBWzhkc/export?format=docx
# awk -F, '/0x/{gsub(" \\+ ","_",$2); gsub("/","__",$2); gsub(" -","_Down",$2); gsub(" \\+","_Up",$2); gsub("[()\"-]","",$2); gsub(" ","_",$2); printf("\t%s=0x%04X,\n", $2, $1)}' < controls.cvs # Extract to csv. Eliminate extra linefeeds and spaces.
Second_Left_Click=0x00B8, # Second_LClick / on K400 Plus # awk -F, '/0x/{gsub(" \\+ ","_",$2); gsub("/","__",$2); gsub(" -","_Down",$2); gsub(" \\+","_Up",$2); gsub("[()\"-]","",$2); gsub(" ","_",$2); printf("\t%s=0x%04X,\n", $2, $1)}' < controls.cvs
Fn_Second_Left_Click=0x00B9, # Fn_Second_LClick Second_Left_Click=0x00B8, # Second_LClick / on K400 Plus
MultiPlatform_App_Switch=0x00BA, Fn_Second_Left_Click=0x00B9, # Fn_Second_LClick
MultiPlatform_Home=0x00BB, MultiPlatform_App_Switch=0x00BA,
MultiPlatform_Menu=0x00BC, MultiPlatform_Home=0x00BB,
MultiPlatform_Back=0x00BD, MultiPlatform_Menu=0x00BC,
MultiPlatform_Insert=0x00BE, MultiPlatform_Back=0x00BD,
Screen_Capture__Print_Screen=0x00BF, # on Craft Keyboard MultiPlatform_Insert=0x00BE,
Fn_Down=0x00C0, Screen_Capture__Print_Screen=0x00BF, # on Craft Keyboard
Fn_Up=0x00C1, Fn_Down=0x00C0,
Multiplatform_Lock=0x00C2, Fn_Up=0x00C1,
App_Switch_Gesture=0x00C3, # Thumb_Button on MX Master Multiplatform_Lock=0x00C2,
Smart_Shift=0x00C4, # Top_Button on MX Master App_Switch_Gesture=0x00C3, # Thumb_Button on MX Master
Microphone=0x00C5, Smart_Shift=0x00C4, # Top_Button on MX Master
Wifi=0x00C6, Microphone=0x00C5,
Brightness_Down=0x00C7, Wifi=0x00C6,
Brightness_Up=0x00C8, Brightness_Down=0x00C7,
Display_out__project_screen_=0x00C9, Brightness_Up=0x00C8,
View_Open_Apps=0x00CA, Display_out__project_screen_=0x00C9,
View_all_apps=0x00CB, View_Open_Apps=0x00CA,
Switch_App=0x00CC, View_all_apps=0x00CB,
Fn_inversion_change=0x00CD, Switch_App=0x00CC,
MultiPlatform_back=0x00CE, Fn_inversion_change=0x00CD,
Multiplatform_forward=0x00CF, MultiPlatform_back=0x00CE,
Multiplatform_gesture_button=0x00D0, Multiplatform_forward=0x00CF,
Host_Switch_channel_1=0x00D1, Multiplatform_gesture_button=0x00D0,
Host_Switch_channel_2=0x00D2, Host_Switch_channel_1=0x00D1,
Host_Switch_channel_3=0x00D3, Host_Switch_channel_2=0x00D2,
Multiplatform_search=0x00D4, Host_Switch_channel_3=0x00D3,
Multiplatform_Home__Mission_Control=0x00D5, Multiplatform_search=0x00D4,
Multiplatform_Menu__Show__Hide_Virtual_Keyboard__Launchpad=0x00D6, Multiplatform_Home__Mission_Control=0x00D5,
Virtual_Gesture_Button=0x00D7, Multiplatform_Menu__Show__Hide_Virtual_Keyboard__Launchpad=0x00D6,
Cursor_Button_Long_press=0x00D8, Virtual_Gesture_Button=0x00D7,
Next_Button_Shortpress=0x00D9, # Next_Button Cursor_Button_Long_press=0x00D8,
Next_Button_Longpress=0x00DA, Next_Button_Shortpress=0x00D9, # Next_Button
Back_Button_Shortpress=0x00DB, # Back Next_Button_Longpress=0x00DA,
Back_Button_Longpress=0x00DC, Back_Button_Shortpress=0x00DB, # Back
Multi_Platform_Language_Switch=0x00DD, Back_Button_Longpress=0x00DC,
F_Lock=0x00DE, Multi_Platform_Language_Switch=0x00DD,
Switch_Highlight=0x00DF, F_Lock=0x00DE,
Mission_Control__Task_View=0x00E0, # Switch_Workspaces on Craft Keyboard Switch_Highlight=0x00DF,
Dashboard_Launchpad__Action_Center=0x00E1, # Application_Launcher on Craft Keyboard Mission_Control__Task_View=0x00E0, # Switch_Workspaces on Craft Keyboard
Backlight_Down=0x00E2, Dashboard_Launchpad__Action_Center=
Backlight_Up=0x00E3, 0x00E1, # Application_Launcher on Craft Keyboard
Previous_Fn=0x00E4, # Reprogrammable_Previous_Track / on Craft Keyboard Backlight_Down=0x00E2,
Play__Pause_Fn=0x00E5, # Reprogrammable_Play__Pause / on Craft Keyboard Backlight_Up=0x00E3,
Next_Fn=0x00E6, # Reprogrammable_Next_Track / on Craft Keyboard Previous_Fn=0x00E4, # Reprogrammable_Previous_Track / on Craft Keyboard
Mute_Fn=0x00E7, # Reprogrammable_Mute / on Craft Keyboard Play__Pause_Fn=0x00E5, # Reprogrammable_Play__Pause / on Craft Keyboard
Volume_Down_Fn=0x00E8, # Reprogrammable_Volume_Down / on Craft Keyboard Next_Fn=0x00E6, # Reprogrammable_Next_Track / on Craft Keyboard
Volume_Up_Fn=0x00E9, # Reprogrammable_Volume_Up / on Craft Keyboard Mute_Fn=0x00E7, # Reprogrammable_Mute / on Craft Keyboard
App_Contextual_Menu__Right_Click=0x00EA, # Context_Menu on Craft Keyboard Volume_Down_Fn=0x00E8, # Reprogrammable_Volume_Down / on Craft Keyboard
Right_Arrow=0x00EB, Volume_Up_Fn=0x00E9, # Reprogrammable_Volume_Up / on Craft Keyboard
Left_Arrow=0x00EC, App_Contextual_Menu__Right_Click=0x00EA, # Context_Menu on Craft Keyboard
DPI_Change=0x00ED, Right_Arrow=0x00EB,
New_Tab=0x00EE, Left_Arrow=0x00EC,
F2=0x00EF, DPI_Change=0x00ED,
F3=0x00F0, New_Tab=0x00EE,
F4=0x00F1, F2=0x00EF,
F5=0x00F2, F3=0x00F0,
F6=0x00F3, F4=0x00F1,
F7=0x00F4, F5=0x00F2,
F8=0x00F5, F6=0x00F3,
F1=0x00F6, F7=0x00F4,
Next_Color_Effect=0x00F7, F8=0x00F5,
Increase_Color_Effect_Speed=0x00F8, F1=0x00F6,
Decrease_Color_Effect_Speed=0x00F9, Next_Color_Effect=0x00F7,
Load_Lighting_Custom_Profile=0x00FA, Increase_Color_Effect_Speed=0x00F8,
Laser_button_short_press=0x00FB, Decrease_Color_Effect_Speed=0x00F9,
Laser_button_long_press=0x00FC, Load_Lighting_Custom_Profile=0x00FA,
DPI_switch=0x00FD, Laser_button_short_press=0x00FB,
MultiPlatform_Home__Show_Desktop=0x00FE, Laser_button_long_press=0x00FC,
MultiPlatform_App_Switch__Show_Dashboard=0x00FF, DPI_switch=0x00FD,
MultiPlatform_App_Switch_2=0x0100, # MultiPlatform_App_Switch MultiPlatform_Home__Show_Desktop=0x00FE,
Fn_Inversion__Hot_Key=0x0101, MultiPlatform_App_Switch__Show_Dashboard=0x00FF,
LeftAndRightClick=0x0102, MultiPlatform_App_Switch_2=0x0100, # MultiPlatform_App_Switch
LED_TOGGLE=0x013B, # Fn_Inversion__Hot_Key=0x0101,
LeftAndRightClick=0x0102,
LED_TOGGLE=0x013B, #
) )
CONTROL._fallback = lambda x: 'unknown:%04X' % x CONTROL._fallback = lambda x: 'unknown:%04X' % x
# <tasks.xml awk -F\" '/<Task /{gsub(/ /, "_", $6); printf("\t%s=0x%04X,\n", $6, $4)}' # <tasks.xml awk -F\" '/<Task /{gsub(/ /, "_", $6); printf("\t%s=0x%04X,\n", $6, $4)}'
TASK = _NamedInts( TASK = _NamedInts(
Volume_Up=0x0001, Volume_Up=0x0001,
Volume_Down=0x0002, Volume_Down=0x0002,
Mute=0x0003, Mute=0x0003,
# Multimedia tasks: # Multimedia tasks:
Play__Pause=0x0004, Play__Pause=0x0004,
Next=0x0005, Next=0x0005,
Previous=0x0006, Previous=0x0006,
Stop=0x0007, Stop=0x0007,
Application_Switcher=0x0008,
BurnMediaPlayer=0x0009,
Calculator=0x000A,
Calendar=0x000B,
Close_Application=0x000C,
Eject=0x000D,
Email=0x000E,
Help=0x000F,
OffDocument=0x0010,
OffSpreadsheet=0x0011,
OffPowerpnt=0x0012,
Undo=0x0013,
Redo=0x0014,
Print=0x0015,
Save=0x0016,
SmartKeySet=0x0017,
Favorites=0x0018,
GadgetsSet=0x0019,
HomePage=0x001A,
WindowsRestore=0x001B,
WindowsMinimize=0x001C,
Music=0x001D, # also known as MediaPlayer
Application_Switcher=0x0008, # Both 0x001E and 0x001F are known as MediaCenterSet
BurnMediaPlayer=0x0009, Media_Center_Logitech=0x001E,
Calculator=0x000A, Media_Center_Microsoft=0x001F,
Calendar=0x000B, UserMenu=0x0020,
Close_Application=0x000C, Messenger=0x0021,
Eject=0x000D, PersonalFolders=0x0022,
Email=0x000E, MyMusic=0x0023,
Help=0x000F, Webcam=0x0024,
OffDocument=0x0010, PicturesFolder=0x0025,
OffSpreadsheet=0x0011, MyVideos=0x0026,
OffPowerpnt=0x0012, My_Computer=0x0027,
Undo=0x0013, PictureAppSet=0x0028,
Redo=0x0014, Search=0x0029, # also known as AdvSmartSearch
Print=0x0015, RecordMediaPlayer=0x002A,
Save=0x0016, BrowserRefresh=0x002B,
SmartKeySet=0x0017, RotateRight=0x002C,
Favorites=0x0018, Search_Files=0x002D, # SearchForFiles
GadgetsSet=0x0019, MM_SHUFFLE=0x002E,
HomePage=0x001A, Sleep=0x002F, # also known as StandBySet
WindowsRestore=0x001B, BrowserStop=0x0030,
WindowsMinimize=0x001C, OneTouchSync=0x0031,
Music=0x001D, # also known as MediaPlayer ZoomSet=0x0032,
ZoomBtnInSet2=0x0033,
# Both 0x001E and 0x001F are known as MediaCenterSet ZoomBtnInSet=0x0034,
Media_Center_Logitech=0x001E, ZoomBtnOutSet2=0x0035,
Media_Center_Microsoft=0x001F, ZoomBtnOutSet=0x0036,
ZoomBtnResetSet=0x0037,
UserMenu=0x0020, Left_Click=0x0038, # LeftClick
Messenger=0x0021, Right_Click=0x0039, # RightClick
PersonalFolders=0x0022, Mouse_Middle_Button=0x003A, # from M510v2 was MiddleMouseButton
MyMusic=0x0023, Back=0x003B,
Webcam=0x0024, Mouse_Back_Button=0x003C, # from M510v2 was BackEx
PicturesFolder=0x0025, BrowserForward=0x003D,
MyVideos=0x0026, Mouse_Forward_Button=0x003E, # from M510v2 was BrowserForwardEx
My_Computer=0x0027, Mouse_Scroll_Left_Button_=0x003F, # from M510v2 was HorzScrollLeftSet
PictureAppSet=0x0028, Mouse_Scroll_Right_Button=0x0040, # from M510v2 was HorzScrollRightSet
Search=0x0029, # also known as AdvSmartSearch QuickSwitch=0x0041,
RecordMediaPlayer=0x002A, BatteryStatus=0x0042,
BrowserRefresh=0x002B, Show_Desktop=0x0043, # ShowDesktop
RotateRight=0x002C, WindowsLock=0x0044,
Search_Files=0x002D, # SearchForFiles FileLauncher=0x0045,
MM_SHUFFLE=0x002E, FolderLauncher=0x0046,
Sleep=0x002F, # also known as StandBySet GotoWebAddress=0x0047,
BrowserStop=0x0030, GenericMouseButton=0x0048,
OneTouchSync=0x0031, KeystrokeAssignment=0x0049,
ZoomSet=0x0032, LaunchProgram=0x004A,
ZoomBtnInSet2=0x0033, MinMaxWindow=0x004B,
ZoomBtnInSet=0x0034, VOLUMEMUTE_NoOSD=0x004C,
ZoomBtnOutSet2=0x0035, New=0x004D,
ZoomBtnOutSet=0x0036, Copy=0x004E,
ZoomBtnResetSet=0x0037, CruiseDown=0x004F,
Left_Click=0x0038, # LeftClick CruiseUp=0x0050,
Right_Click=0x0039, # RightClick Cut=0x0051,
Mouse_Middle_Button=0x003A, # from M510v2 was MiddleMouseButton Do_Nothing=0x0052,
Back=0x003B, PageDown=0x0053,
Mouse_Back_Button=0x003C, # from M510v2 was BackEx PageUp=0x0054,
BrowserForward=0x003D, Paste=0x0055,
Mouse_Forward_Button=0x003E, # from M510v2 was BrowserForwardEx SearchPicture=0x0056,
Mouse_Scroll_Left_Button_=0x003F, # from M510v2 was HorzScrollLeftSet Reply=0x0057,
Mouse_Scroll_Right_Button=0x0040, # from M510v2 was HorzScrollRightSet PhotoGallerySet=0x0058,
QuickSwitch=0x0041, MM_REWIND=0x0059,
BatteryStatus=0x0042, MM_FASTFORWARD=0x005A,
Show_Desktop=0x0043, # ShowDesktop Send=0x005B,
WindowsLock=0x0044, ControlPanel=0x005C,
FileLauncher=0x0045, UniversalScroll=0x005D,
FolderLauncher=0x0046, AutoScroll=0x005E,
GotoWebAddress=0x0047, GenericButton=0x005F,
GenericMouseButton=0x0048, MM_NEXT=0x0060,
KeystrokeAssignment=0x0049, MM_PREVIOUS=0x0061,
LaunchProgram=0x004A, Do_Nothing_One=0x0062, # also known as Do_Nothing
MinMaxWindow=0x004B, SnapLeft=0x0063,
VOLUMEMUTE_NoOSD=0x004C, SnapRight=0x0064,
New=0x004D, WinMinRestore=0x0065,
Copy=0x004E, WinMaxRestore=0x0066,
CruiseDown=0x004F, WinStretch=0x0067,
CruiseUp=0x0050, SwitchMonitorLeft=0x0068,
Cut=0x0051, SwitchMonitorRight=0x0069,
Do_Nothing=0x0052, ShowPresentation=0x006A,
PageDown=0x0053, ShowMobilityCenter=0x006B,
PageUp=0x0054, HorzScrollNoRepeatSet=0x006C,
Paste=0x0055, TouchBackForwardHorzScroll=0x0077,
SearchPicture=0x0056, MetroAppSwitch=0x0078,
Reply=0x0057, MetroAppBar=0x0079,
PhotoGallerySet=0x0058, MetroCharms=0x007A,
MM_REWIND=0x0059, Calculator_VKEY=0x007B, # also known as Calculator
MM_FASTFORWARD=0x005A, MetroSearch=0x007C,
Send=0x005B, MetroStartScreen=0x0080,
ControlPanel=0x005C, MetroShare=0x007D,
UniversalScroll=0x005D, MetroSettings=0x007E,
AutoScroll=0x005E, MetroDevices=0x007F,
GenericButton=0x005F, MetroBackLeftHorz=0x0082,
MM_NEXT=0x0060, MetroForwRightHorz=0x0083,
MM_PREVIOUS=0x0061, Win8_Back=0x0084, # also known as MetroCharms
Do_Nothing_One=0x0062, # also known as Do_Nothing Win8_Forward=0x0085, # also known as AppSwitchBar
SnapLeft=0x0063, Win8Charm_Appswitch_GifAnimation=0x0086,
SnapRight=0x0064, Win8BackHorzLeft=0x008B, # also known as Back
WinMinRestore=0x0065, Win8ForwardHorzRight=0x008C, # also known as BrowserForward
WinMaxRestore=0x0066, MetroSearch2=0x0087,
WinStretch=0x0067, MetroShare2=0x0088,
SwitchMonitorLeft=0x0068, MetroSettings2=0x008A,
SwitchMonitorRight=0x0069, MetroDevices2=0x0089,
ShowPresentation=0x006A, Win8MetroWin7Forward=0x008D, # also known as MetroStartScreen
ShowMobilityCenter=0x006B, Win8ShowDesktopWin7Back=0x008E, # also known as ShowDesktop
HorzScrollNoRepeatSet=0x006C, MetroApplicationSwitch=0x0090, # also known as MetroStartScreen
TouchBackForwardHorzScroll=0x0077, ShowUI=0x0092,
MetroAppSwitch=0x0078, # https://docs.google.com/document/d/1Dpx_nWRQAZox_zpZ8SNc9nOkSDE9svjkghOCbzopabc/edit
MetroAppBar=0x0079, # Extract to csv. Eliminate extra linefeeds and spaces. Turn / into __ and space into _
MetroCharms=0x007A, # 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
Calculator_VKEY=0x007B, # also known as Calculator Switch_Presentation__Switch_Screen=0x0093, # on K400 Plus
MetroSearch=0x007C, Minimize_Window=0x0094,
MetroStartScreen=0x0080, Maximize_Window=0x0095, # on K400 Plus
MetroShare=0x007D, MultiPlatform_App_Switch=0x0096,
MetroSettings=0x007E, MultiPlatform_Home=0x0097,
MetroDevices=0x007F, MultiPlatform_Menu=0x0098,
MetroBackLeftHorz=0x0082, MultiPlatform_Back=0x0099,
MetroForwRightHorz=0x0083, Switch_Language=0x009A, # Mac_switch_language
Win8_Back=0x0084, # also known as MetroCharms Screen_Capture=0x009B, # Mac_screen_Capture, on Craft Keyboard
Win8_Forward=0x0085, # also known as AppSwitchBar Gesture_Button=0x009C,
Win8Charm_Appswitch_GifAnimation=0x0086, Smart_Shift=0x009D,
Win8BackHorzLeft=0x008B, # also known as Back AppExpose=0x009E,
Win8ForwardHorzRight=0x008C, # also known as BrowserForward Smart_Zoom=0x009F,
MetroSearch2=0x0087, Lookup=0x00A0,
MetroShare2=0x0088, Microphone_on__off=0x00A1,
MetroSettings2=0x008A, Wifi_on__off=0x00A2,
MetroDevices2=0x0089, Brightness_Down=0x00A3,
Win8MetroWin7Forward=0x008D, # also known as MetroStartScreen Brightness_Up=0x00A4,
Win8ShowDesktopWin7Back=0x008E, # also known as ShowDesktop Display_Out=0x00A5,
MetroApplicationSwitch=0x0090, # also known as MetroStartScreen View_Open_Apps=0x00A6,
ShowUI=0x0092, View_All_Open_Apps=0x00A7,
# https://docs.google.com/document/d/1Dpx_nWRQAZox_zpZ8SNc9nOkSDE9svjkghOCbzopabc/edit AppSwitch=0x00A8,
# Extract to csv. Eliminate extra linefeeds and spaces. Turn / into __ and space into _ Gesture_Button_Navigation=0x00A9, # Mouse_Thumb_Button on MX Master
# 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 Fn_inversion=0x00AA,
Switch_Presentation__Switch_Screen=0x0093, # on K400 Plus Multiplatform_Back=0x00AB,
Minimize_Window=0x0094, Multiplatform_Forward=0x00AC,
Maximize_Window=0x0095, # on K400 Plus Multiplatform_Gesture_Button=0x00AD,
MultiPlatform_App_Switch=0x0096, HostSwitch_Channel_1=0x00AE,
MultiPlatform_Home=0x0097, HostSwitch_Channel_2=0x00AF,
MultiPlatform_Menu=0x0098, HostSwitch_Channel_3=0x00B0,
MultiPlatform_Back=0x0099, Multiplatform_Search=0x00B1,
Switch_Language=0x009A, # Mac_switch_language Multiplatform_Home__Mission_Control=0x00B2,
Screen_Capture=0x009B, # Mac_screen_Capture, on Craft Keyboard Multiplatform_Menu__Launchpad=0x00B3,
Gesture_Button=0x009C, Virtual_Gesture_Button=0x00B4,
Smart_Shift=0x009D, Cursor=0x00B5,
AppExpose=0x009E, Keyboard_Right_Arrow=0x00B6,
Smart_Zoom=0x009F, SW_Custom_Highlight=0x00B7,
Lookup=0x00A0, Keyboard_Left_Arrow=0x00B8,
Microphone_on__off=0x00A1, TBD=0x00B9,
Wifi_on__off=0x00A2, Multiplatform_Language_Switch=0x00BA,
Brightness_Down=0x00A3, SW_Custom_Highlight_2=0x00BB,
Brightness_Up=0x00A4, Fast_Forward=0x00BC,
Display_Out=0x00A5, Fast_Backward=0x00BD,
View_Open_Apps=0x00A6, Switch_Highlighting=0x00BE,
View_All_Open_Apps=0x00A7, Mission_Control__Task_View=0x00BF, # Switch_Workspace on Craft Keyboard
AppSwitch=0x00A8, Dashboard_Launchpad__Action_Center=
Gesture_Button_Navigation=0x00A9, # Mouse_Thumb_Button on MX Master 0x00C0, # Application_Launcher on Craft Keyboard
Fn_inversion=0x00AA, Backlight_Down=0x00C1, # Backlight_Down_FW_internal_function
Multiplatform_Back=0x00AB, Backlight_Up=0x00C2, # Backlight_Up_FW_internal_function
Multiplatform_Forward=0x00AC, Right_Click__App_Contextual_Menu=0x00C3, # Context_Menu on Craft Keyboard
Multiplatform_Gesture_Button=0x00AD, DPI_Change=0x00C4,
HostSwitch_Channel_1=0x00AE, New_Tab=0x00C5,
HostSwitch_Channel_2=0x00AF, F2=0x00C6,
HostSwitch_Channel_3=0x00B0, F3=0x00C7,
Multiplatform_Search=0x00B1, F4=0x00C8,
Multiplatform_Home__Mission_Control=0x00B2, F5=0x00C9,
Multiplatform_Menu__Launchpad=0x00B3, F6=0x00CA,
Virtual_Gesture_Button=0x00B4, F7=0x00CB,
Cursor=0x00B5, F8=0x00CC,
Keyboard_Right_Arrow=0x00B6, F1=0x00CD,
SW_Custom_Highlight=0x00B7, Laser_Button=0x00CE,
Keyboard_Left_Arrow=0x00B8, Laser_Button_Long_Press=0x00CF,
TBD=0x00B9, Start_Presentation=0x00D0,
Multiplatform_Language_Switch=0x00BA, Blank_Screen=0x00D1,
SW_Custom_Highlight_2=0x00BB, DPI_Switch=0x00D2, # AdjustDPI on MX Vertical
Fast_Forward=0x00BC, Home__Show_Desktop=0x00D3,
Fast_Backward=0x00BD, App_Switch__Dashboard=0x00D4,
Switch_Highlighting=0x00BE, App_Switch=0x00D5,
Mission_Control__Task_View=0x00BF, # Switch_Workspace on Craft Keyboard Fn_Inversion=0x00D6,
Dashboard_Launchpad__Action_Center=0x00C0, # Application_Launcher on Craft Keyboard LeftAndRightClick=0x00D7,
Backlight_Down=0x00C1, # Backlight_Down_FW_internal_function LedToggle=0x00DD, #
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 TASK._fallback = lambda x: 'unknown:%04X' % x
# hidpp 4.5 info from https://lekensteyn.nl/files/logitech/x1b04_specialkeysmsebuttons.html # hidpp 4.5 info from https://lekensteyn.nl/files/logitech/x1b04_specialkeysmsebuttons.html
KEY_FLAG = _NamedInts( KEY_FLAG = _NamedInts(virtual=0x80,
virtual=0x80, persistently_divertable=0x40,
persistently_divertable=0x40, divertable=0x20,
divertable=0x20, reprogrammable=0x10,
reprogrammable=0x10, FN_sensitive=0x08,
FN_sensitive=0x08, nonstandard=0x04,
nonstandard=0x04, is_FN=0x02,
is_FN=0x02, mse=0x01)
mse=0x01
)
DISABLE = _NamedInts( DISABLE = _NamedInts(
Caps_Lock=0x01, Caps_Lock=0x01,
Num_Lock=0x02, Num_Lock=0x02,
Scroll_Lock=0x04, Scroll_Lock=0x04,
Insert=0x08, Insert=0x08,
Win=0x10, # aka Super Win=0x10, # aka Super
) )
DISABLE._fallback = lambda x: 'unknown:%02X' % x DISABLE._fallback = lambda x: 'unknown:%02X' % x

View File

@ -25,7 +25,6 @@ from logging import getLogger, DEBUG as _DEBUG
_log = getLogger(__name__) _log = getLogger(__name__)
del getLogger del getLogger
from .i18n import _, ngettext from .i18n import _, ngettext
from .common import NamedInts as _NamedInts, NamedInt as _NamedInt from .common import NamedInts as _NamedInts, NamedInt as _NamedInt
from . import hidpp10 as _hidpp10 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( KEYS = _NamedInts(
BATTERY_LEVEL=1, BATTERY_LEVEL=1,
BATTERY_CHARGING=2, BATTERY_CHARGING=2,
BATTERY_STATUS=3, BATTERY_STATUS=3,
LIGHT_LEVEL=4, LIGHT_LEVEL=4,
LINK_ENCRYPTED=5, LINK_ENCRYPTED=5,
NOTIFICATION_FLAGS=6, NOTIFICATION_FLAGS=6,
ERROR=7, ERROR=7,
BATTERY_NEXT_LEVEL=8, BATTERY_NEXT_LEVEL=8,
BATTERY_VOLTAGE=9, BATTERY_VOLTAGE=9,
) )
# If the battery charge is under this percentage, trigger an attention event # If the battery charge is under this percentage, trigger an attention event
# (blink systray icon/notification/whatever). # (blink systray icon/notification/whatever).
@ -64,286 +67,331 @@ _LONG_SLEEP = 15 * 60 # seconds
# #
# #
def attach_to(device, changed_callback): def attach_to(device, changed_callback):
assert device assert device
assert changed_callback assert changed_callback
if not hasattr(device, 'status') or device.status is None:
if 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): 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. is the pairing lock open or closed, any pairing errors, etc.
""" """
def __init__(self, receiver, changed_callback): def __init__(self, receiver, changed_callback):
assert receiver assert receiver
self._receiver = receiver self._receiver = receiver
assert changed_callback assert changed_callback
self._changed_callback = changed_callback self._changed_callback = changed_callback
# self.updated = 0 # self.updated = 0
self.lock_open = False self.lock_open = False
self.new_device = None self.new_device = None
self[KEYS.ERROR] = None self[KEYS.ERROR] = None
def __str__(self): def __str__(self):
count = len(self._receiver) count = len(self._receiver)
return (_("No paired devices.") if count == 0 else return (_("No paired devices.") if count == 0 else ngettext(
ngettext("%(count)s paired device.", "%(count)s paired devices.", count) % { 'count': count }) "%(count)s paired device.", "%(count)s paired devices.", count) % {
__unicode__ = __str__ 'count': count
})
def changed(self, alert=ALERT.NOTIFICATION, reason=None): __unicode__ = __str__
# self.updated = _timestamp()
self._changed_callback(self._receiver, alert=alert, reason=reason) 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): 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 active/inactive, battery charge, lux, etc. It updates them mostly by
processing incoming notification events from the device itself. processing incoming notification events from the device itself.
""" """
def __init__(self, device, changed_callback): def __init__(self, device, changed_callback):
assert device assert device
self._device = device self._device = device
assert changed_callback assert changed_callback
self._changed_callback = changed_callback self._changed_callback = changed_callback
# is the device active? # is the device active?
self._active = None self._active = None
# timestamp of when this status object was last updated # timestamp of when this status object was last updated
self.updated = 0 self.updated = 0
def to_string(self): def to_string(self):
def _items(): def _items():
comma = False comma = False
battery_level = self.get(KEYS.BATTERY_LEVEL) battery_level = self.get(KEYS.BATTERY_LEVEL)
if battery_level is not None: if battery_level is not None:
if isinstance(battery_level, _NamedInt): if isinstance(battery_level, _NamedInt):
yield _("Battery: %(level)s") % { 'level': _(str(battery_level)) } yield _("Battery: %(level)s") % {
else: 'level': _(str(battery_level))
yield _("Battery: %(percent)d%%") % { 'percent': battery_level } }
else:
yield _("Battery: %(percent)d%%") % {
'percent': battery_level
}
battery_status = self.get(KEYS.BATTERY_STATUS) battery_status = self.get(KEYS.BATTERY_STATUS)
if battery_status is not None: if battery_status is not None:
yield ' (%s)' % _(str(battery_status)) yield ' (%s)' % _(str(battery_status))
comma = True comma = True
light_level = self.get(KEYS.LIGHT_LEVEL) light_level = self.get(KEYS.LIGHT_LEVEL)
if light_level is not None: if light_level is not None:
if comma: yield ', ' if comma: yield ', '
yield _("Lighting: %(level)s lux") % { 'level': light_level } 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): def __repr__(self):
return '{' + ', '.join('\'%s\': %r' % (k, v) for k, v in self.items()) + '}' return '{' + ', '.join('\'%s\': %r' % (k, v)
for k, v in self.items()) + '}'
def __bool__(self): def __bool__(self):
return bool(self._active) return bool(self._active)
__nonzero__ = __bool__
def set_battery_info(self, level, status, nextLevel=None, voltage=None, timestamp=None): __nonzero__ = __bool__
if _log.isEnabledFor(_DEBUG):
_log.debug("%s: battery %s, %s", self._device, level, status)
if level is None: def set_battery_info(self,
# Some notifications may come with no battery level info, just level,
# charging state info, so do our best to infer a level (even if it is just the last level) status,
# It is not always possible to do this well nextLevel=None,
if status == _hidpp20.BATTERY_STATUS.full: voltage=None,
level = _hidpp10.BATTERY_APPOX.full timestamp=None):
elif status in (_hidpp20.BATTERY_STATUS.almost_full, _hidpp20.BATTERY_STATUS.recharging): if _log.isEnabledFor(_DEBUG):
level = _hidpp10.BATTERY_APPOX.good _log.debug("%s: battery %s, %s", self._device, level, status)
elif status == _hidpp20.BATTERY_STATUS.slow_recharge:
level = _hidpp10.BATTERY_APPOX.low
else:
level = self.get(KEYS.BATTERY_LEVEL)
else:
assert isinstance(level, int)
# TODO: this is also executed when pressing Fn+F7 on K800. if level is None:
old_level, self[KEYS.BATTERY_LEVEL] = self.get(KEYS.BATTERY_LEVEL), level # Some notifications may come with no battery level info, just
old_status, self[KEYS.BATTERY_STATUS] = self.get(KEYS.BATTERY_STATUS), status # charging state info, so do our best to infer a level (even if it is just the last level)
self[KEYS.BATTERY_NEXT_LEVEL] = nextLevel # It is not always possible to do this well
if voltage is not None: if status == _hidpp20.BATTERY_STATUS.full:
self[KEYS.BATTERY_VOLTAGE] = voltage 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, # TODO: this is also executed when pressing Fn+F7 on K800.
_hidpp20.BATTERY_STATUS.full, _hidpp20.BATTERY_STATUS.slow_recharge) old_level, self[KEYS.BATTERY_LEVEL] = self.get(
old_charging, self[KEYS.BATTERY_CHARGING] = self.get(KEYS.BATTERY_CHARGING), charging 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 charging = status in (_hidpp20.BATTERY_STATUS.recharging,
alert, reason = ALERT.NONE, None _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 ): changed = old_level != level or old_status != status or old_charging != charging
self[KEYS.ERROR] = None alert, reason = ALERT.NONE, 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 }
if changed or reason: if _hidpp20.BATTERY_OK(status) and (level is None or
# update the leds on the device, if any level > _BATTERY_ATTENTION_LEVEL):
_hidpp10.set_3leds(self._device, level, charging=charging, warning=bool(alert)) self[KEYS.ERROR] = None
self.changed(active=True, alert=alert, reason=reason, timestamp=timestamp) 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 if changed or reason:
def read_battery(self, timestamp=None): # update the leds on the device, if any
if self._active: _hidpp10.set_3leds(self._device,
d = self._device level,
assert d charging=charging,
warning=bool(alert))
self.changed(active=True,
alert=alert,
reason=reason,
timestamp=timestamp)
if d.protocol < 2.0: # Retrieve and regularize battery status
battery = _hidpp10.get_battery(d) def read_battery(self, timestamp=None):
self.set_battery_keys(battery) if self._active:
return d = self._device
assert d
battery = _hidpp20.get_battery(d) if d.protocol < 2.0:
if battery is None: battery = _hidpp10.get_battery(d)
v = _hidpp20.get_voltage(d) self.set_battery_keys(battery)
if v is not None: return
level, status, voltage, _ignore, _ignore = v
self.set_battery_keys( (level, status, None), voltage)
return
# Really unnecessary, if the device has SOLAR_DASHBOARD it should be battery = _hidpp20.get_battery(d)
# broadcasting it's battery status anyway, it will just take a little while. if battery is None:
# However, when the device has just been detected, it will not show v = _hidpp20.get_voltage(d)
# any battery status for a while (broadcasts happen every 90 seconds). if v is not None:
if battery is None and d.features and _hidpp20.FEATURE.SOLAR_DASHBOARD in d.features: level, status, voltage, _ignore, _ignore = v
d.feature_request(_hidpp20.FEATURE.SOLAR_DASHBOARD, 0x00, 1, 1) self.set_battery_keys((level, status, None), voltage)
return return
self.set_battery_keys(battery)
def set_battery_keys(self, battery, voltage=None) : # Really unnecessary, if the device has SOLAR_DASHBOARD it should be
if battery is not None: # broadcasting it's battery status anyway, it will just take a little while.
level, status, nextLevel = battery # However, when the device has just been detected, it will not show
self.set_battery_info(level, status, nextLevel, voltage) # any battery status for a while (broadcasts happen every 90 seconds).
elif KEYS.BATTERY_STATUS in self: if battery is None and d.features and _hidpp20.FEATURE.SOLAR_DASHBOARD in d.features:
self[KEYS.BATTERY_STATUS] = None d.feature_request(_hidpp20.FEATURE.SOLAR_DASHBOARD, 0x00, 1, 1)
self[KEYS.BATTERY_CHARGING] = None return
self.changed() self.set_battery_keys(battery)
def changed(self, active=None, alert=ALERT.NONE, reason=None, timestamp=None): def set_battery_keys(self, battery, voltage=None):
assert self._changed_callback if battery is not None:
d = self._device level, status, nextLevel = battery
# assert d # may be invalid when processing the 'unpaired' notification self.set_battery_info(level, status, nextLevel, voltage)
timestamp = timestamp or _timestamp() elif KEYS.BATTERY_STATUS in self:
self[KEYS.BATTERY_STATUS] = None
self[KEYS.BATTERY_CHARGING] = None
self.changed()
if active is not None: def changed(self,
d.online = active active=None,
was_active, self._active = self._active, active alert=ALERT.NONE,
if active: reason=None,
if not was_active: timestamp=None):
# Make sure to set notification flags on the device, they assert self._changed_callback
# get cleared when the device is turned off (but not when the device d = self._device
# goes idle, and we can't tell the difference right now). # assert d # may be invalid when processing the 'unpaired' notification
if d.protocol < 2.0: timestamp = timestamp or _timestamp()
self[KEYS.NOTIFICATION_FLAGS] = d.enable_notifications()
# If we've been inactive for a long time, forget anything if active is not None:
# about the battery. d.online = active
if self.updated > 0 and timestamp - self.updated > _LONG_SLEEP: was_active, self._active = self._active, active
self[KEYS.BATTERY_LEVEL] = None if active:
self[KEYS.BATTERY_STATUS] = None if not was_active:
self[KEYS.BATTERY_CHARGING] = None # 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, # If we've been inactive for a long time, forget anything
# make sure they're up-to-date. # about the battery.
if _log.isEnabledFor(_DEBUG): if self.updated > 0 and timestamp - self.updated > _LONG_SLEEP:
_log.debug("%s pushing device settings %s", d, d.settings) self[KEYS.BATTERY_LEVEL] = None
for s in d.settings: self[KEYS.BATTERY_STATUS] = None
s.apply() self[KEYS.BATTERY_CHARGING] = None
if self.get(KEYS.BATTERY_LEVEL) is None: # Devices lose configuration when they are turned off,
self.read_battery(timestamp) # make sure they're up-to-date.
else: if _log.isEnabledFor(_DEBUG):
if was_active: _log.debug("%s pushing device settings %s", d,
battery = self.get(KEYS.BATTERY_LEVEL) d.settings)
self.clear() for s in d.settings:
# If we had a known battery level before, assume it's not going s.apply()
# to change much while the device is offline.
if battery is not None:
self[KEYS.BATTERY_LEVEL] = battery
if self.updated == 0 and active == True: if self.get(KEYS.BATTERY_LEVEL) is None:
# if the device is active on the very first status notification, self.read_battery(timestamp)
# (meaning just when the program started or a new receiver was just else:
# detected), pop-up a notification about it if was_active:
alert |= ALERT.NOTIFICATION battery = self.get(KEYS.BATTERY_LEVEL)
self.updated = timestamp 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): if self.updated == 0 and active == True:
# _log.debug("device %d changed: active=%s %s", d.number, self._active, dict(self)) # if the device is active on the very first status notification,
self._changed_callback(d, alert, reason) # (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): # if _log.isEnabledFor(_DEBUG):
# d = self._device # _log.debug("device %d changed: active=%s %s", d.number, self._active, dict(self))
# if not d: self._changed_callback(d, alert, reason)
# _log.error("polling status of invalid device")
# return # def poll(self, timestamp):
# # d = self._device
# if self._active: # if not d:
# if _log.isEnabledFor(_DEBUG): # _log.error("polling status of invalid device")
# _log.debug("polling status of %s", d) # return
# #
# # read these from the device, the UI may need them later # if self._active:
# d.protocol, d.serial, d.firmware, d.kind, d.name, d.settings, None # if _log.isEnabledFor(_DEBUG):
# # _log.debug("polling status of %s", d)
# # make sure we know all the features of the device #
# # if d.features: # # read these from the device, the UI may need them later
# # d.features[:] # d.protocol, d.serial, d.firmware, d.kind, d.name, d.settings, None
# #
# # devices may go out-of-range while still active, or the computer # # make sure we know all the features of the device
# # may go to sleep and wake up without the devices available # # if d.features:
# if timestamp - self.updated > _STATUS_TIMEOUT: # # d.features[:]
# if d.ping(): #
# timestamp = self.updated = _timestamp() # # devices may go out-of-range while still active, or the computer
# else: # # may go to sleep and wake up without the devices available
# self.changed(active=False, reason='out of range') # if timestamp - self.updated > _STATUS_TIMEOUT:
# # if d.ping():
# # if still active, make sure we know the battery level # timestamp = self.updated = _timestamp()
# if KEYS.BATTERY_LEVEL not in self: # else:
# self.read_battery(timestamp) # self.changed(active=False, reason='out of range')
# #
# elif timestamp - self.updated > _STATUS_TIMEOUT: # # if still active, make sure we know the battery level
# if d.ping(): # if KEYS.BATTERY_LEVEL not in self:
# self.changed(active=True) # self.read_battery(timestamp)
# else: #
# self.updated = _timestamp() # elif timestamp - self.updated > _STATUS_TIMEOUT:
# if d.ping():
# self.changed(active=True)
# else:
# self.updated = _timestamp()

View File

@ -19,7 +19,6 @@
from __future__ import absolute_import, division, print_function, unicode_literals from __future__ import absolute_import, division, print_function, unicode_literals
import argparse as _argparse import argparse as _argparse
import sys as _sys import sys as _sys
@ -27,54 +26,76 @@ from logging import getLogger, DEBUG as _DEBUG
_log = getLogger(__name__) _log = getLogger(__name__)
del getLogger del getLogger
from solaar import NAME from solaar import NAME
# #
# #
# #
def _create_parser(): def _create_parser():
parser = _argparse.ArgumentParser(prog=NAME.lower(), add_help=False, parser = _argparse.ArgumentParser(
epilog='For details on individual actions, run `%s <action> --help`.' % NAME.lower()) prog=NAME.lower(),
subparsers = parser.add_subparsers(title='actions', add_help=False,
help='optional action to perform') epilog='For details on individual actions, run `%s <action> --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 = subparsers.add_parser('show', help='show information about devices')
sp.add_argument('device', nargs='?', default='all', sp.add_argument(
help='device to show information about; may be a device number (1..6), a serial, ' 'device',
'a substring of a device\'s name, or "all" (the default)') nargs='?',
sp.set_defaults(action='show') 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 = subparsers.add_parser('probe',
sp.add_argument('receiver', nargs='?', help='probe a receiver (debugging use only)')
help='select a certain receiver when more than one is present') sp.add_argument(
sp.set_defaults(action='probe') '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', sp = subparsers.add_parser(
epilog='Please note that configuration only works on active devices.') 'config',
sp.add_argument('device', help='read/write device-specific settings',
help='device to configure; may be a device number (1..6), a device serial, ' epilog='Please note that configuration only works on active devices.')
'or at least 3 characters of a device\'s name') sp.add_argument(
sp.add_argument('setting', nargs='?', 'device',
help='device-specific setting; leave empty to list available settings') help=
sp.add_argument('value', nargs='?', 'device to configure; may be a device number (1..6), a device serial, '
help='new value for the setting') 'or at least 3 characters of a device\'s name')
sp.set_defaults(action='config') 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', sp = subparsers.add_parser(
epilog='The Logitech Unifying Receiver supports up to 6 paired devices at the same time.') 'pair',
sp.add_argument('receiver', nargs='?', help='pair a new device',
help='select a certain receiver when more than one is present') epilog=
sp.set_defaults(action='pair') '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 = subparsers.add_parser('unpair', help='unpair a device')
sp.add_argument('device', sp.add_argument(
help='device to unpair; may be a device number (1..6), a serial, ' 'device',
'or a substring of a device\'s name.') help='device to unpair; may be a device number (1..6), a serial, '
sp.set_defaults(action='unpair') '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() _cli_parser, actions = _create_parser()
@ -82,88 +103,89 @@ print_help = _cli_parser.print_help
def _receivers(dev_path=None): def _receivers(dev_path=None):
from logitech_receiver import Receiver from logitech_receiver import Receiver
from logitech_receiver.base import receivers from logitech_receiver.base import receivers
for dev_info in receivers(): for dev_info in receivers():
if dev_path is not None and dev_path != dev_info.path: if dev_path is not None and dev_path != dev_info.path:
continue continue
try: try:
r = Receiver.open(dev_info) r = Receiver.open(dev_info)
if _log.isEnabledFor(_DEBUG): if _log.isEnabledFor(_DEBUG):
_log.debug("[%s] => %s", dev_info.path, r) _log.debug("[%s] => %s", dev_info.path, r)
if r: if r:
yield r yield r
except Exception as e: except Exception as e:
_log.exception('opening ' + str(dev_info)) _log.exception('opening ' + str(dev_info))
_sys.exit("%s: error: %s" % (NAME, str(e))) _sys.exit("%s: error: %s" % (NAME, str(e)))
def _find_receiver(receivers, name): def _find_receiver(receivers, name):
assert receivers assert receivers
assert name assert name
for r in receivers: for r in receivers:
if name in r.name.lower() or (r.serial is not None and name == r.serial.lower()): if name in r.name.lower() or (r.serial is not None
return r and name == r.serial.lower()):
return r
def _find_device(receivers, name): def _find_device(receivers, name):
assert receivers assert receivers
assert name assert name
number = None number = None
if len(name) == 1: if len(name) == 1:
try: try:
number = int(name) number = int(name)
except: except:
pass pass
else: else:
assert not (number < 0) assert not (number < 0)
if number > 6: number = None if number > 6: number = None
for r in receivers: for r in receivers:
if number and number <= r.max_devices: if number and number <= r.max_devices:
dev = r[number] dev = r[number]
if dev: if dev:
return dev return dev
for dev in r: for dev in r:
if (name == dev.serial.lower() or if (name == dev.serial.lower() or name == dev.codename.lower()
name == dev.codename.lower() or or name == str(dev.kind).lower()
name == str(dev.kind).lower() or or name in dev.name.lower()):
name in dev.name.lower()): return dev
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): def run(cli_args=None, hidraw_path=None):
if cli_args: if cli_args:
action = cli_args[0] action = cli_args[0]
args = _cli_parser.parse_args(cli_args) args = _cli_parser.parse_args(cli_args)
else: else:
args = _cli_parser.parse_args() args = _cli_parser.parse_args()
# Python 3 has an undocumented 'feature' that breaks parsing empty args # Python 3 has an undocumented 'feature' that breaks parsing empty args
# http://bugs.python.org/issue16308 # http://bugs.python.org/issue16308
if not 'cmd' in args: if not 'cmd' in args:
_cli_parser.print_usage(_sys.stderr) _cli_parser.print_usage(_sys.stderr)
_sys.stderr.write('%s: error: too few arguments\n' % NAME.lower()) _sys.stderr.write('%s: error: too few arguments\n' % NAME.lower())
_sys.exit(2) _sys.exit(2)
action = args.action action = args.action
assert action in actions assert action in actions
try: try:
c = list(_receivers(hidraw_path)) c = list(_receivers(hidraw_path))
if not c: if not c:
raise Exception('Logitech receiver not found') raise Exception('Logitech receiver not found')
from importlib import import_module from importlib import import_module
m = import_module('.' + action, package=__name__) m = import_module('.' + action, package=__name__)
m.run(c, args, _find_receiver, _find_device) m.run(c, args, _find_receiver, _find_device)
except AssertionError as e: except AssertionError as e:
from traceback import extract_tb from traceback import extract_tb
tb_last = extract_tb(_sys.exc_info()[2])[-1] tb_last = extract_tb(_sys.exc_info()[2])[-1]
_sys.exit('%s: assertion failed: %s line %d' % (NAME.lower(), tb_last[0], tb_last[1])) _sys.exit('%s: assertion failed: %s line %d' %
except Exception as e: (NAME.lower(), tb_last[0], tb_last[1]))
from traceback import format_exc except Exception as e:
_sys.exit('%s: error: %s' % (NAME.lower(), format_exc())) from traceback import format_exc
_sys.exit('%s: error: %s' % (NAME.lower(), format_exc()))

View File

@ -19,109 +19,118 @@
from __future__ import absolute_import, division, print_function, unicode_literals from __future__ import absolute_import, division, print_function, unicode_literals
from solaar import configuration as _configuration from solaar import configuration as _configuration
from logitech_receiver import settings as _settings from logitech_receiver import settings as _settings
def _print_setting(s, verbose=True): def _print_setting(s, verbose=True):
print ('#', s.label) print('#', s.label)
if verbose: if verbose:
if s.description: if s.description:
print ('#', s.description.replace('\n', ' ')) print('#', s.description.replace('\n', ' '))
if s.kind == _settings.KIND.toggle: if s.kind == _settings.KIND.toggle:
print ('# possible values: on/true/t/yes/y/1 or off/false/f/no/n/0') print(
elif s.choices: '# possible values: on/true/t/yes/y/1 or off/false/f/no/n/0')
print ('# possible values: one of [', ', '.join(str(v) for v in s.choices), '], or higher/lower/highest/max/lowest/min') elif s.choices:
else: print('# possible values: one of [',
# wtf? ', '.join(str(v) for v in s.choices),
pass '], or higher/lower/highest/max/lowest/min')
value = s.read(cached=False) else:
if value is None: # wtf?
print (s.name, '= ? (failed to read from device)') pass
else: value = s.read(cached=False)
print (s.name, '= %r' % value) 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): def run(receivers, args, find_receiver, find_device):
assert receivers assert receivers
assert args.device assert args.device
device_name = args.device.lower() device_name = args.device.lower()
dev = find_device(receivers, device_name) dev = find_device(receivers, device_name)
if not dev.ping(): if not dev.ping():
raise Exception('%s is offline' % dev.name) raise Exception('%s is offline' % dev.name)
if not dev.settings: if not dev.settings:
raise Exception('no settings for %s' % dev.name) raise Exception('no settings for %s' % dev.name)
_configuration.attach_to(dev) _configuration.attach_to(dev)
if not args.setting: if not args.setting:
print (dev.name, '(%s) [%s:%s]' % (dev.codename, dev.wpid, dev.serial)) print(dev.name, '(%s) [%s:%s]' % (dev.codename, dev.wpid, dev.serial))
for s in dev.settings: for s in dev.settings:
print ('') print('')
_print_setting(s) _print_setting(s)
return return
setting_name = args.setting.lower() setting_name = args.setting.lower()
setting = None setting = None
for s in dev.settings: for s in dev.settings:
if setting_name == s.name.lower(): if setting_name == s.name.lower():
setting = s setting = s
break break
if setting is None: if setting is None:
raise Exception("no setting '%s' for %s" % (args.setting, dev.name)) raise Exception("no setting '%s' for %s" % (args.setting, dev.name))
if args.value is None: if args.value is None:
_print_setting(setting) _print_setting(setting)
return return
if setting.kind == _settings.KIND.toggle: if setting.kind == _settings.KIND.toggle:
value = args.value value = args.value
try: try:
value = bool(int(value)) value = bool(int(value))
except: except:
if value.lower() in ('true', 'yes', 'on', 't', 'y'): if value.lower() in ('true', 'yes', 'on', 't', 'y'):
value = True value = True
elif value.lower() in ('false', 'no', 'off', 'f', 'n'): elif value.lower() in ('false', 'no', 'off', 'f', 'n'):
value = False value = False
else: else:
raise Exception("don't know how to interpret '%s' as boolean" % value) raise Exception("don't know how to interpret '%s' as boolean" %
value)
elif setting.choices: elif setting.choices:
value = args.value.lower() value = args.value.lower()
if value in ('higher', 'lower'): if value in ('higher', 'lower'):
old_value = setting.read() old_value = setting.read()
if old_value is None: if old_value is None:
raise Exception("could not read current value of '%s'" % setting.name) raise Exception("could not read current value of '%s'" %
setting.name)
if value == 'lower': if value == 'lower':
lower_values = setting.choices[:old_value] lower_values = setting.choices[:old_value]
value = lower_values[-1] if lower_values else setting.choices[:][0] value = lower_values[
elif value == 'higher': -1] if lower_values else setting.choices[:][0]
higher_values = setting.choices[old_value + 1:] elif value == 'higher':
value = higher_values[0] if higher_values else setting.choices[:][-1] higher_values = setting.choices[old_value + 1:]
elif value in ('highest', 'max'): value = higher_values[
value = setting.choices[:][-1] 0] if higher_values else setting.choices[:][-1]
elif value in ('lowest', 'min'): elif value in ('highest', 'max'):
value = setting.choices[:][0] value = setting.choices[:][-1]
elif value not in setting.choices: elif value in ('lowest', 'min'):
raise Exception("possible values for '%s' are: [%s]" % (setting.name, ', '.join(str(v) for v in setting.choices))) value = setting.choices[:][0]
value = setting.choices[value] 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: elif setting.kind == _settings.KIND.range:
try: try:
value = int(args.value) value = int(args.value)
except ValueError: except ValueError:
raise Exception("can't interpret '%s' as integer" % args.value) raise Exception("can't interpret '%s' as integer" % args.value)
else: else:
raise Exception("NotImplemented") raise Exception("NotImplemented")
result = setting.write(value) result = setting.write(value)
if result is None: if result is None:
raise Exception("failed to set '%s' = '%s' [%r]" % (setting.name, str(value), value)) raise Exception("failed to set '%s' = '%s' [%r]" %
_print_setting(setting, False) (setting.name, str(value), value))
_print_setting(setting, False)

View File

@ -19,80 +19,85 @@
from __future__ import absolute_import, division, print_function, unicode_literals from __future__ import absolute_import, division, print_function, unicode_literals
from time import time as _timestamp from time import time as _timestamp
from logitech_receiver import ( from logitech_receiver import (
base as _base, base as _base,
hidpp10 as _hidpp10, hidpp10 as _hidpp10,
status as _status, status as _status,
notifications as _notifications, notifications as _notifications,
) )
def run(receivers, args, find_receiver, _ignore): def run(receivers, args, find_receiver, _ignore):
assert receivers assert receivers
if args.receiver: if args.receiver:
receiver_name = args.receiver.lower() receiver_name = args.receiver.lower()
receiver = find_receiver(receiver_name) receiver = find_receiver(receiver_name)
if not receiver: if not receiver:
raise Exception("no receiver found matching '%s'" % receiver_name) raise Exception("no receiver found matching '%s'" % receiver_name)
else: else:
receiver = receivers[0] receiver = receivers[0]
assert receiver assert receiver
receiver.status = _status.ReceiverStatus(receiver, lambda *args, **kwargs: None) receiver.status = _status.ReceiverStatus(receiver,
lambda *args, **kwargs: None)
# check if it's necessary to set the notification flags # check if it's necessary to set the notification flags
old_notification_flags = _hidpp10.get_notification_flags(receiver) or 0 old_notification_flags = _hidpp10.get_notification_flags(receiver) or 0
if not (old_notification_flags & _hidpp10.NOTIFICATION_FLAG.wireless): if not (old_notification_flags & _hidpp10.NOTIFICATION_FLAG.wireless):
_hidpp10.set_notification_flags(receiver, old_notification_flags | _hidpp10.NOTIFICATION_FLAG.wireless) _hidpp10.set_notification_flags(
receiver,
old_notification_flags | _hidpp10.NOTIFICATION_FLAG.wireless)
# get all current devices # get all current devices
known_devices = [dev.number for dev in receiver] known_devices = [dev.number for dev in receiver]
class _HandleWithNotificationHook(int): class _HandleWithNotificationHook(int):
def notifications_hook(self, n): def notifications_hook(self, n):
assert n assert n
if n.devnumber == 0xFF: if n.devnumber == 0xFF:
_notifications.process(receiver, n) _notifications.process(receiver, n)
elif n.sub_id == 0x41: # allow for other protocols! (was and n.address == 0x04) elif n.sub_id == 0x41: # allow for other protocols! (was and n.address == 0x04)
if n.devnumber not in known_devices: if n.devnumber not in known_devices:
receiver.status.new_device = receiver[n.devnumber] receiver.status.new_device = receiver[n.devnumber]
elif receiver.re_pairs: elif receiver.re_pairs:
del receiver[n.devnumber] # get rid of information on device re-paired away del receiver[
receiver.status.new_device = receiver[n.devnumber] n.
devnumber] # get rid of information on device re-paired away
receiver.status.new_device = receiver[n.devnumber]
timeout = 20 # seconds timeout = 20 # seconds
receiver.handle = _HandleWithNotificationHook(receiver.handle) receiver.handle = _HandleWithNotificationHook(receiver.handle)
receiver.set_lock(False, timeout=timeout) receiver.set_lock(False, timeout=timeout)
print ('Pairing: turn your new device on (timing out in', timeout, 'seconds).') print('Pairing: turn your new device on (timing out in', timeout,
'seconds).')
# the lock-open notification may come slightly later, wait for it a bit # the lock-open notification may come slightly later, wait for it a bit
pairing_start = _timestamp() pairing_start = _timestamp()
patience = 5 # seconds patience = 5 # seconds
while receiver.status.lock_open or _timestamp() - pairing_start < patience: while receiver.status.lock_open or _timestamp() - pairing_start < patience:
n = _base.read(receiver.handle) n = _base.read(receiver.handle)
if n: if n:
n = _base.make_notification(*n) n = _base.make_notification(*n)
if n: if n:
receiver.handle.notifications_hook(n) receiver.handle.notifications_hook(n)
if not (old_notification_flags & _hidpp10.NOTIFICATION_FLAG.wireless): if not (old_notification_flags & _hidpp10.NOTIFICATION_FLAG.wireless):
# only clear the flags if they weren't set before, otherwise a # only clear the flags if they weren't set before, otherwise a
# concurrently running Solaar app might stop working properly # concurrently running Solaar app might stop working properly
_hidpp10.set_notification_flags(receiver, old_notification_flags) _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 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

View File

@ -19,52 +19,65 @@
from __future__ import absolute_import, division, print_function, unicode_literals from __future__ import absolute_import, division, print_function, unicode_literals
from time import time as _timestamp from time import time as _timestamp
from logitech_receiver.common import strhex as _strhex from logitech_receiver.common import strhex as _strhex
from logitech_receiver import ( from logitech_receiver import (
base as _base, base as _base,
hidpp10 as _hidpp10, hidpp10 as _hidpp10,
status as _status, status as _status,
notifications as _notifications, notifications as _notifications,
) )
_R = _hidpp10.REGISTERS _R = _hidpp10.REGISTERS
from solaar.cli.show import _print_receiver from solaar.cli.show import _print_receiver
def run(receivers, args, find_receiver, _ignore): def run(receivers, args, find_receiver, _ignore):
assert receivers assert receivers
if args.receiver: if args.receiver:
receiver_name = args.receiver.lower() receiver_name = args.receiver.lower()
receiver = find_receiver(receiver_name) receiver = find_receiver(receiver_name)
if not receiver: if not receiver:
raise Exception("no receiver found matching '%s'" % receiver_name) raise Exception("no receiver found matching '%s'" % receiver_name)
else: else:
receiver = receivers[0] receiver = receivers[0]
assert receiver assert receiver
_print_receiver(receiver) _print_receiver(receiver)
print (' Register Dump') print(' Register Dump')
register = receiver.read_register(_R.notifications) register = receiver.read_register(_R.notifications)
print(" Notification Register %#04x: %s" % (_R.notifications%0x100,'0x'+_strhex(register) if register else "None")) print(" Notification Register %#04x: %s" %
register = receiver.read_register(_R.receiver_connection) (_R.notifications % 0x100,
print(" Connection State %#04x: %s" % (_R.receiver_connection%0x100,'0x'+_strhex(register) if register else "None")) '0x' + _strhex(register) if register else "None"))
register = receiver.read_register(_R.devices_activity) register = receiver.read_register(_R.receiver_connection)
print(" Device Activity %#04x: %s" % (_R.devices_activity%0x100,'0x'+_strhex(register) if register else "None")) 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 device in range(0, 6):
for sub_reg in [ 0x0, 0x10, 0x20, 0x30 ] : for sub_reg in [0x0, 0x10, 0x20, 0x30]:
register = receiver.read_register(_R.receiver_info, sub_reg + device) register = receiver.read_register(_R.receiver_info,
print(" Pairing Register %#04x %#04x: %s" % (_R.receiver_info%0x100,sub_reg + device,'0x'+_strhex(register) if register else "None")) sub_reg + device)
register = receiver.read_register(_R.receiver_info, 0x40 + device) print(" Pairing Register %#04x %#04x: %s" %
print(" Pairing Name %#04x %#02x: %s" % (_R.receiver_info%0x100,0x40 + device,register[2:2+ord(register[1:2])] if register else "None")) (_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): for sub_reg in range(0, 5):
register = receiver.read_register(_R.firmware, sub_reg) 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")) print(" Firmware %#04x %#04x: %s" %
(_R.firmware % 0x100, sub_reg,
'0x' + _strhex(register) if register else "None"))

View File

@ -19,220 +19,244 @@
from __future__ import absolute_import, division, print_function, unicode_literals from __future__ import absolute_import, division, print_function, unicode_literals
from logitech_receiver import (hidpp10 as _hidpp10, hidpp20 as _hidpp20,
from logitech_receiver import ( special_keys as _special_keys,
hidpp10 as _hidpp10, settings_templates as _settings_templates)
hidpp20 as _hidpp20,
special_keys as _special_keys,
settings_templates as _settings_templates
)
from logitech_receiver.common import NamedInt as _NamedInt from logitech_receiver.common import NamedInt as _NamedInt
def _print_receiver(receiver): def _print_receiver(receiver):
paired_count = receiver.count() paired_count = receiver.count()
print (receiver.name) print(receiver.name)
print (' Device path :', receiver.path) print(' Device path :', receiver.path)
print (' USB id : 046d:%s' % receiver.product_id) print(' USB id : 046d:%s' % receiver.product_id)
print (' Serial :', receiver.serial) print(' Serial :', receiver.serial)
if receiver.firmware: if receiver.firmware:
for f in receiver.firmware: for f in receiver.firmware:
print (' %-11s: %s' % (f.kind, f.version)) print(' %-11s: %s' % (f.kind, f.version))
print (' Has', paired_count, 'paired device(s) out of a maximum of %d.' % receiver.max_devices) print(' Has', paired_count,
if receiver.remaining_pairings() and receiver.remaining_pairings() >= 0 : 'paired device(s) out of a maximum of %d.' % receiver.max_devices)
print (' Has %d successful pairing(s) remaining.' % receiver.remaining_pairings() ) 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) notification_flags = _hidpp10.get_notification_flags(receiver)
if notification_flags is not None: if notification_flags is not None:
if notification_flags: if notification_flags:
notification_names = _hidpp10.NOTIFICATION_FLAG.flag_names(notification_flags) notification_names = _hidpp10.NOTIFICATION_FLAG.flag_names(
print (' Notifications: %s (0x%06X)' % (', '.join(notification_names), notification_flags)) notification_flags)
else: print(' Notifications: %s (0x%06X)' %
print (' Notifications: (none)') (', '.join(notification_names), notification_flags))
else:
print(' Notifications: (none)')
activity = receiver.read_register(_hidpp10.REGISTERS.devices_activity) activity = receiver.read_register(_hidpp10.REGISTERS.devices_activity)
if activity: if activity:
activity = [(d, ord(activity[d - 1:d])) for d in range(1, receiver.max_devices)] activity = [(d, ord(activity[d - 1:d]))
activity_text = ', '.join(('%d=%d' % (d, a)) for d, a in activity if a > 0) for d in range(1, receiver.max_devices)]
print (' Device activity counters:', activity_text or '(empty)') 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): def _print_device(dev):
assert dev assert dev
# check if the device is online # check if the device is online
dev.ping() dev.ping()
print (' %d: %s' % (dev.number, dev.name)) print(' %d: %s' % (dev.number, dev.name))
print (' Codename :', dev.codename) print(' Codename :', dev.codename)
print (' Kind :', dev.kind) print(' Kind :', dev.kind)
print (' Wireless PID :', dev.wpid) print(' Wireless PID :', dev.wpid)
if dev.protocol: if dev.protocol:
print (' Protocol : HID++ %1.1f' % dev.protocol) print(' Protocol : HID++ %1.1f' % dev.protocol)
else: else:
print (' Protocol : unknown (device is offline)') print(' Protocol : unknown (device is offline)')
if dev.polling_rate: if dev.polling_rate:
print (' Polling rate :', dev.polling_rate, 'ms (%dHz)' % (1000 // dev.polling_rate)) print(' Polling rate :', dev.polling_rate,
print (' Serial number:', dev.serial) 'ms (%dHz)' % (1000 // dev.polling_rate))
if dev.firmware: print(' Serial number:', dev.serial)
for fw in dev.firmware: if dev.firmware:
print (' %11s:' % fw.kind, (fw.name + ' ' + fw.version).strip()) for fw in dev.firmware:
print(' %11s:' % fw.kind,
(fw.name + ' ' + fw.version).strip())
if dev.power_switch_location: if dev.power_switch_location:
print (' The power switch is located on the %s.' % dev.power_switch_location) print(' The power switch is located on the %s.' %
dev.power_switch_location)
if dev.online: if dev.online:
notification_flags = _hidpp10.get_notification_flags(dev) notification_flags = _hidpp10.get_notification_flags(dev)
if notification_flags is not None: if notification_flags is not None:
if notification_flags: if notification_flags:
notification_names = _hidpp10.NOTIFICATION_FLAG.flag_names(notification_flags) notification_names = _hidpp10.NOTIFICATION_FLAG.flag_names(
print (' Notifications: %s (0x%06X).' % (', '.join(notification_names), notification_flags)) notification_flags)
else: print(' Notifications: %s (0x%06X).' %
print (' Notifications: (none).') (', '.join(notification_names), notification_flags))
else:
print(' Notifications: (none).')
if dev.online and dev.features: if dev.online and dev.features:
print (' Supports %d HID++ 2.0 features:' % len(dev.features)) print(' Supports %d HID++ 2.0 features:' % len(dev.features))
dev.persister = None # Give the device a fake persister dev.persister = None # Give the device a fake persister
dev_settings = [] dev_settings = []
_settings_templates.check_feature_settings(dev, dev_settings) _settings_templates.check_feature_settings(dev, dev_settings)
for index, feature in enumerate(dev.features): for index, feature in enumerate(dev.features):
feature = dev.features[index] feature = dev.features[index]
flags = dev.request(0x0000, feature.bytes(2)) flags = dev.request(0x0000, feature.bytes(2))
flags = 0 if flags is None else ord(flags[1:2]) flags = 0 if flags is None else ord(flags[1:2])
flags = _hidpp20.FEATURE_FLAG.flag_names(flags) flags = _hidpp20.FEATURE_FLAG.flag_names(flags)
print (' %2d: %-22s {%04X} %s' % (index, feature, feature, ', '.join(flags))) print(' %2d: %-22s {%04X} %s' %
if feature == _hidpp20.FEATURE.HIRES_WHEEL: (index, feature, feature, ', '.join(flags)))
wheel = _hidpp20.get_hires_wheel(dev) if feature == _hidpp20.FEATURE.HIRES_WHEEL:
if wheel: wheel = _hidpp20.get_hires_wheel(dev)
multi, has_invert, has_switch, inv, res, target, ratchet = wheel if wheel:
print(" Multiplier: %s" % multi) multi, has_invert, has_switch, inv, res, target, ratchet = wheel
if has_invert: print(" Multiplier: %s" % multi)
print(" Has invert") if has_invert:
if inv: print(" Has invert")
print(" Inverse wheel motion") if inv:
else: print(" Inverse wheel motion")
print(" Normal wheel motion") else:
if has_switch: print(" Normal wheel motion")
print(" Has ratchet switch") if has_switch:
if ratchet: print(" Has ratchet switch")
print(" Normal wheel mode") if ratchet:
else: print(" Normal wheel mode")
print(" Free wheel mode") else:
if res: print(" Free wheel mode")
print(" High resolution mode") if res:
else: print(" High resolution mode")
print(" Low resolution mode") else:
if target: print(" Low resolution mode")
print(" HID++ notification") if target:
else: print(" HID++ notification")
print(" HID notification") else:
elif feature == _hidpp20.FEATURE.MOUSE_POINTER: print(" HID notification")
mouse_pointer = _hidpp20.get_mouse_pointer_info(dev) elif feature == _hidpp20.FEATURE.MOUSE_POINTER:
if mouse_pointer: mouse_pointer = _hidpp20.get_mouse_pointer_info(dev)
print(" DPI: %s" % mouse_pointer['dpi']) if mouse_pointer:
print(" Acceleration: %s" % mouse_pointer['acceleration']) print(" DPI: %s" % mouse_pointer['dpi'])
if mouse_pointer['suggest_os_ballistics']: print(" Acceleration: %s" %
print(" Use OS ballistics") mouse_pointer['acceleration'])
else: if mouse_pointer['suggest_os_ballistics']:
print(" Override OS ballistics") print(" Use OS ballistics")
if mouse_pointer['suggest_vertical_orientation']: else:
print(" Provide vertical tuning, trackball") print(" Override OS ballistics")
else: if mouse_pointer['suggest_vertical_orientation']:
print(" No vertical tuning, standard mice") print(" Provide vertical tuning, trackball")
if feature == _hidpp20.FEATURE.VERTICAL_SCROLLING: else:
vertical_scrolling_info = _hidpp20.get_vertical_scrolling_info(dev) print(" No vertical tuning, standard mice")
if vertical_scrolling_info: if feature == _hidpp20.FEATURE.VERTICAL_SCROLLING:
print(" Roller type: %s" % vertical_scrolling_info['roller']) vertical_scrolling_info = _hidpp20.get_vertical_scrolling_info(
print(" Ratchet per turn: %s" % vertical_scrolling_info['ratchet']) dev)
print(" Scroll lines: %s" % vertical_scrolling_info['lines']) if vertical_scrolling_info:
elif feature == _hidpp20.FEATURE.HI_RES_SCROLLING: print(" Roller type: %s" %
scrolling_mode, scrolling_resolution = _hidpp20.get_hi_res_scrolling_info(dev) vertical_scrolling_info['roller'])
if scrolling_mode: print(" Ratchet per turn: %s" %
print(" Hi-res scrolling enabled") vertical_scrolling_info['ratchet'])
else: print(" Scroll lines: %s" %
print(" Hi-res scrolling disabled") vertical_scrolling_info['lines'])
if scrolling_resolution: elif feature == _hidpp20.FEATURE.HI_RES_SCROLLING:
print(" Hi-res scrolling multiplier: %s" % scrolling_resolution) scrolling_mode, scrolling_resolution = _hidpp20.get_hi_res_scrolling_info(
elif feature == _hidpp20.FEATURE.POINTER_SPEED: dev)
pointer_speed = _hidpp20.get_pointer_speed_info(dev) if scrolling_mode:
if pointer_speed: print(" Hi-res scrolling enabled")
print(" Pointer Speed: %s" % pointer_speed) else:
elif feature == _hidpp20.FEATURE.LOWRES_WHEEL: print(" Hi-res scrolling disabled")
wheel_status = _hidpp20.get_lowres_wheel_status(dev) if scrolling_resolution:
if wheel_status: print(" Hi-res scrolling multiplier: %s" %
print(" Wheel Reports: %s" % wheel_status) scrolling_resolution)
elif feature == _hidpp20.FEATURE.NEW_FN_INVERSION: elif feature == _hidpp20.FEATURE.POINTER_SPEED:
inverted, default_inverted = _hidpp20.get_new_fn_inversion(dev) pointer_speed = _hidpp20.get_pointer_speed_info(dev)
print(" Fn-swap:", "enabled" if inverted else "disabled") if pointer_speed:
print(" Fn-swap default:", "enabled" if default_inverted else "disabled") print(" Pointer Speed: %s" % pointer_speed)
for setting in dev_settings: elif feature == _hidpp20.FEATURE.LOWRES_WHEEL:
if setting.feature == feature: wheel_status = _hidpp20.get_lowres_wheel_status(dev)
v = setting.read(False) if wheel_status:
print(" %s: %s" % (setting.label, v) ) 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: if dev.online and dev.keys:
print (' Has %d reprogrammable keys:' % len(dev.keys)) print(' Has %d reprogrammable keys:' % len(dev.keys))
for k in dev.keys: for k in dev.keys:
flags = _special_keys.KEY_FLAG.flag_names(k.flags) flags = _special_keys.KEY_FLAG.flag_names(k.flags)
# TODO: add here additional variants for other REPROG_CONTROLS # TODO: add here additional variants for other REPROG_CONTROLS
if dev.keys.keyversion == 1: if dev.keys.keyversion == 1:
print (' %2d: %-26s => %-27s %s' % (k.index, k.key, k.task, ', '.join(flags))) print(' %2d: %-26s => %-27s %s' %
if dev.keys.keyversion == 4: (k.index, k.key, k.task, ', '.join(flags)))
print (' %2d: %-26s, default: %-27s => %-26s' % (k.index, k.key, k.task, k.remapped)) if dev.keys.keyversion == 4:
print (' %s, pos:%d, group:%1d, gmask:%d' % ( ', '.join(flags), k.pos, k.group, k.group_mask)) print(' %2d: %-26s, default: %-27s => %-26s' %
if dev.online: (k.index, k.key, k.task, k.remapped))
battery = _hidpp20.get_battery(dev) print(' %s, pos:%d, group:%1d, gmask:%d' %
if battery is None: (', '.join(flags), k.pos, k.group, k.group_mask))
battery = _hidpp10.get_battery(dev) if dev.online:
if battery is not None: battery = _hidpp20.get_battery(dev)
level, status, nextLevel = battery if battery is None:
text = _battery_text(level) battery = _hidpp10.get_battery(dev)
nextText = '' if nextLevel is None else ', next level ' +_battery_text(nextLevel) if battery is not None:
print (' Battery: %s, %s%s.' % (text, status, nextText)) level, status, nextLevel = battery
else: text = _battery_text(level)
battery_voltage = _hidpp20.get_voltage(dev) nextText = '' if nextLevel is None else ', next level ' + _battery_text(
if battery_voltage : nextLevel)
(level, status, voltage, charge_sts, charge_type) = battery_voltage print(' Battery: %s, %s%s.' % (text, status, nextText))
print (' Battery: %smV, %s, %s.' % (voltage, status, level)) else:
else: battery_voltage = _hidpp20.get_voltage(dev)
print (' Battery status unavailable.') if battery_voltage:
else: (level, status, voltage, charge_sts,
print (' Battery: unknown (device is offline).') 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): def run(receivers, args, find_receiver, find_device):
assert receivers assert receivers
assert args.device assert args.device
device_name = args.device.lower() device_name = args.device.lower()
if device_name == 'all': if device_name == 'all':
for r in receivers: for r in receivers:
_print_receiver(r) _print_receiver(r)
count = r.count() count = r.count()
if count: if count:
for dev in r: for dev in r:
print ('') print('')
_print_device(dev) _print_device(dev)
count -= 1 count -= 1
if not count: if not count:
break break
print ('') print('')
return return
dev = find_receiver(receivers, device_name) dev = find_receiver(receivers, device_name)
if dev: if dev:
_print_receiver(dev) _print_receiver(dev)
return return
dev = find_device(receivers, device_name) dev = find_device(receivers, device_name)
assert dev assert dev
_print_device(dev) _print_device(dev)

View File

@ -21,19 +21,22 @@ from __future__ import absolute_import, division, print_function, unicode_litera
def run(receivers, args, find_receiver, find_device): def run(receivers, args, find_receiver, find_device):
assert receivers assert receivers
assert args.device assert args.device
device_name = args.device.lower() device_name = args.device.lower()
dev = find_device(receivers, device_name) dev = find_device(receivers, device_name)
if not dev.receiver.may_unpair: if not dev.receiver.may_unpair:
print('Receiver for %s [%s:%s] does not unpair, but attempting anyway' % (dev.name,dev.wpid,dev.serial)) print(
'Receiver for %s [%s:%s] does not unpair, but attempting anyway' %
(dev.name, dev.wpid, dev.serial))
try: try:
# query these now, it's last chance to get them # query these now, it's last chance to get them
number, codename, wpid, serial = dev.number, dev.codename, dev.wpid, dev.serial number, codename, wpid, serial = dev.number, dev.codename, dev.wpid, dev.serial
dev.receiver._unpair_device(number, True) # force an unpair dev.receiver._unpair_device(number, True) # force an unpair
print ('Unpaired %d: %s (%s) [%s:%s]' % (number, dev.name, codename, wpid, serial)) print('Unpaired %d: %s (%s) [%s:%s]' %
except Exception as e: (number, dev.name, codename, wpid, serial))
raise Exception('failed to unpair device %s: %s' % (dev.name, e)) except Exception as e:
raise Exception('failed to unpair device %s: %s' % (dev.name, e))

View File

@ -25,106 +25,109 @@ from logging import getLogger, DEBUG as _DEBUG, INFO as _INFO
_log = getLogger(__name__) _log = getLogger(__name__)
del getLogger 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') _file_path = _path.join(_XDG_CONFIG_HOME, 'solaar', 'config.json')
from solaar import __version__ from solaar import __version__
_KEY_VERSION = '_version' _KEY_VERSION = '_version'
_KEY_NAME = '_name' _KEY_NAME = '_name'
_configuration = {} _configuration = {}
def _load(): def _load():
if _path.isfile(_file_path): if _path.isfile(_file_path):
loaded_configuration = {} loaded_configuration = {}
try: try:
with open(_file_path, 'r') as config_file: with open(_file_path, 'r') as config_file:
loaded_configuration = _json_load(config_file) loaded_configuration = _json_load(config_file)
except: except:
_log.error("failed to load from %s", _file_path) _log.error("failed to load from %s", _file_path)
# loaded_configuration.update(_configuration) # loaded_configuration.update(_configuration)
_configuration.clear() _configuration.clear()
_configuration.update(loaded_configuration) _configuration.update(loaded_configuration)
if _log.isEnabledFor(_DEBUG): if _log.isEnabledFor(_DEBUG):
_log.debug("load => %s", _configuration) _log.debug("load => %s", _configuration)
_cleanup(_configuration) _cleanup(_configuration)
_configuration[_KEY_VERSION] = __version__ _configuration[_KEY_VERSION] = __version__
return _configuration return _configuration
def save(): def save():
# don't save if the configuration hasn't been loaded # don't save if the configuration hasn't been loaded
if _KEY_VERSION not in _configuration: if _KEY_VERSION not in _configuration:
return return
dirname = _os.path.dirname(_file_path) dirname = _os.path.dirname(_file_path)
if not _path.isdir(dirname): if not _path.isdir(dirname):
try: try:
_os.makedirs(dirname) _os.makedirs(dirname)
except: except:
_log.error("failed to create %s", dirname) _log.error("failed to create %s", dirname)
return False return False
_cleanup(_configuration) _cleanup(_configuration)
try: try:
with open(_file_path, 'w') as config_file: with open(_file_path, 'w') as config_file:
_json_save(_configuration, config_file, skipkeys=True, indent=2, sort_keys=True) _json_save(_configuration,
config_file,
skipkeys=True,
indent=2,
sort_keys=True)
if _log.isEnabledFor(_INFO): if _log.isEnabledFor(_INFO):
_log.info("saved %s to %s", _configuration, _file_path) _log.info("saved %s to %s", _configuration, _file_path)
return True return True
except: except:
_log.error("failed to save to %s", _file_path) _log.error("failed to save to %s", _file_path)
def _cleanup(d): def _cleanup(d):
# remove None values from the dict # remove None values from the dict
for key in list(d.keys()): for key in list(d.keys()):
value = d.get(key) value = d.get(key)
if value is None: if value is None:
del d[key] del d[key]
elif isinstance(value, dict): elif isinstance(value, dict):
_cleanup(value) _cleanup(value)
def _device_key(device): def _device_key(device):
return '%s:%s' % (device.wpid, device.serial) return '%s:%s' % (device.wpid, device.serial)
class _DeviceEntry(dict): class _DeviceEntry(dict):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(_DeviceEntry, self).__init__(*args, **kwargs) super(_DeviceEntry, self).__init__(*args, **kwargs)
def __setitem__(self, key, value): def __setitem__(self, key, value):
super(_DeviceEntry, self).__setitem__(key, value) super(_DeviceEntry, self).__setitem__(key, value)
save() save()
def _device_entry(device): def _device_entry(device):
if not _configuration: if not _configuration:
_load() _load()
device_key = _device_key(device) device_key = _device_key(device)
c = _configuration.get(device_key) or {} c = _configuration.get(device_key) or {}
if not isinstance(c, _DeviceEntry): if not isinstance(c, _DeviceEntry):
c[_KEY_NAME] = device.name c[_KEY_NAME] = device.name
c = _DeviceEntry(c) c = _DeviceEntry(c)
_configuration[device_key] = c _configuration[device_key] = c
return c return c
def attach_to(device): def attach_to(device):
"""Apply the last saved configuration to a device.""" """Apply the last saved configuration to a device."""
if not _configuration: if not _configuration:
_load() _load()
persister = _device_entry(device) persister = _device_entry(device)
device.persister = persister device.persister = persister

View File

@ -22,7 +22,6 @@ from __future__ import absolute_import, division, print_function, unicode_litera
import importlib import importlib
from solaar import __version__, NAME from solaar import __version__, NAME
import solaar.i18n as _i18n import solaar.i18n as _i18n
import solaar.cli as _cli 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): def _require(module, os_package, gi=None, gi_package=None, gi_version=None):
try: try:
if gi is not None: if gi is not None:
gi.require_version(gi_package,gi_version) gi.require_version(gi_package, gi_version)
return importlib.import_module(module) return importlib.import_module(module)
except (ImportError, ValueError): except (ImportError, ValueError):
import sys import sys
sys.exit("%s: missing required system package %s" % (NAME, os_package)) sys.exit("%s: missing required system package %s" % (NAME, os_package))
prefer_symbolic_battery_icons = False prefer_symbolic_battery_icons = False
def _parse_arguments(): def _parse_arguments():
import argparse import argparse
arg_parser = argparse.ArgumentParser(prog=NAME.lower()) arg_parser = argparse.ArgumentParser(prog=NAME.lower())
arg_parser.add_argument('-d', '--debug', action='count', default=0, arg_parser.add_argument(
help='print logging messages, for debugging purposes (may be repeated for extra verbosity)') '-d',
arg_parser.add_argument('-D', '--hidraw', action='store', dest='hidraw_path', metavar='PATH', '--debug',
help='unifying receiver to use; the first detected receiver if unspecified. Example: /dev/hidraw2') action='count',
arg_parser.add_argument('--restart-on-wake-up', action='store_true', default=0,
help='restart Solaar on sleep wake-up (experimental)') help=
arg_parser.add_argument('-w', '--window', choices=('show','hide','only'), help='start with window showing / hidden / only (no tray icon)') 'print logging messages, for debugging purposes (may be repeated for extra verbosity)'
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(
arg_parser.add_argument('--help-actions', action='store_true', '-D',
help='print help for the optional actions') '--hidraw',
arg_parser.add_argument('action', nargs=argparse.REMAINDER, choices=_cli.actions, action='store',
help='optional actions to perform') 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: if args.help_actions:
_cli.print_help() _cli.print_help()
return return
if args.window is None: if args.window is None:
args.window = 'show' # default behaviour is to show main window args.window = 'show' # default behaviour is to show main window
global prefer_symbolic_battery_icons global prefer_symbolic_battery_icons
prefer_symbolic_battery_icons = True if args.battery_icons == 'symbolic' else False prefer_symbolic_battery_icons = True if args.battery_icons == 'symbolic' else False
import logging import logging
if args.debug > 0: if args.debug > 0:
log_level = logging.WARNING - 10 * args.debug log_level = logging.WARNING - 10 * args.debug
log_format='%(asctime)s,%(msecs)03d %(levelname)8s [%(threadName)s] %(name)s: %(message)s' log_format = '%(asctime)s,%(msecs)03d %(levelname)8s [%(threadName)s] %(name)s: %(message)s'
logging.basicConfig(level=max(log_level, logging.DEBUG), format=log_format, datefmt='%H:%M:%S') logging.basicConfig(level=max(log_level, logging.DEBUG),
else: format=log_format,
logging.root.addHandler(logging.NullHandler()) datefmt='%H:%M:%S')
logging.root.setLevel(logging.ERROR) else:
logging.root.addHandler(logging.NullHandler())
logging.root.setLevel(logging.ERROR)
if not args.action: if not args.action:
if logging.root.isEnabledFor(logging.INFO): if logging.root.isEnabledFor(logging.INFO):
logging.info("language %s (%s), translations path %s", _i18n.language, _i18n.encoding, _i18n.path) logging.info("language %s (%s), translations path %s",
_i18n.language, _i18n.encoding, _i18n.path)
return args return args
def main(): def main():
_require('pyudev', 'python3-pyudev') _require('pyudev', 'python3-pyudev')
# handle ^C in console # handle ^C in console
import signal import signal
signal.signal(signal.SIGINT, signal.SIG_DFL) signal.signal(signal.SIGINT, signal.SIG_DFL)
args = _parse_arguments() args = _parse_arguments()
if not args: return if not args: return
if args.action: if args.action:
# if any argument, run comandline and exit # if any argument, run comandline and exit
return _cli.run(args.action, args.hidraw_path) return _cli.run(args.action, args.hidraw_path)
gi = _require('gi', 'python3-gi or python3-gobject') gi = _require('gi', 'python3-gi or python3-gobject')
_require('gi.repository.Gtk', 'gir1.2-gtk-3.0', gi, 'Gtk', '3.0') _require('gi.repository.Gtk', 'gir1.2-gtk-3.0', gi, 'Gtk', '3.0')
try: try:
import solaar.ui as ui import solaar.ui as ui
import solaar.listener as listener import solaar.listener as listener
listener.setup_scanner(ui.status_changed, ui.error_dialog) listener.setup_scanner(ui.status_changed, ui.error_dialog)
import solaar.upower as _upower import solaar.upower as _upower
if args.restart_on_wake_up: if args.restart_on_wake_up:
_upower.watch(listener.start_all, listener.stop_all) _upower.watch(listener.start_all, listener.stop_all)
else: else:
_upower.watch(lambda: listener.ping_all(True)) _upower.watch(lambda: listener.ping_all(True))
# main UI event loop # main UI event loop
ui.run_loop(listener.start_all, listener.stop_all, args.window!='only', args.window!='hide') ui.run_loop(listener.start_all, listener.stop_all,
except Exception as e: args.window != 'only', args.window != 'hide')
import sys except Exception as e:
from traceback import format_exc import sys
sys.exit('%s: error: %s' % (NAME.lower(), format_exc())) from traceback import format_exc
sys.exit('%s: error: %s' % (NAME.lower(), format_exc()))
if __name__ == '__main__': if __name__ == '__main__':
main() main()

View File

@ -25,22 +25,27 @@ from solaar import NAME as _NAME
# #
# #
def _find_locale_path(lc_domain): def _find_locale_path(lc_domain):
import os.path as _path import os.path as _path
import sys as _sys import sys as _sys
prefix_share = _path.normpath(_path.join(_path.realpath(_sys.path[0]), '..')) prefix_share = _path.normpath(
src_share = _path.normpath(_path.join(_path.realpath(_sys.path[0]), '..', 'share')) _path.join(_path.realpath(_sys.path[0]), '..'))
del _sys 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: for location in prefix_share, src_share:
mo_files = _glob(_path.join(location, 'locale', '*', 'LC_MESSAGES', lc_domain + '.mo')) mo_files = _glob(
if mo_files: _path.join(location, 'locale', '*', 'LC_MESSAGES',
return _path.join(location, 'locale') lc_domain + '.mo'))
if mo_files:
return _path.join(location, 'locale')
# del _path # del _path
import locale import locale
@ -58,9 +63,9 @@ _gettext.textdomain(_LOCALE_DOMAIN)
_gettext.install(_LOCALE_DOMAIN) _gettext.install(_LOCALE_DOMAIN)
try: try:
unicode unicode
_ = lambda x: _gettext.gettext(x).decode('UTF-8') _ = lambda x: _gettext.gettext(x).decode('UTF-8')
ngettext = lambda *x: _gettext.ngettext(*x).decode('UTF-8') ngettext = lambda *x: _gettext.ngettext(*x).decode('UTF-8')
except: except:
_ = _gettext.gettext _ = _gettext.gettext
ngettext = _gettext.ngettext ngettext = _gettext.ngettext

View File

@ -24,34 +24,32 @@ from logging import getLogger, INFO as _INFO, WARNING as _WARNING
_log = getLogger(__name__) _log = getLogger(__name__)
del getLogger del getLogger
from solaar.i18n import _ from solaar.i18n import _
from . import configuration from . import configuration
from logitech_receiver import ( from logitech_receiver import (Receiver, listener as _listener, status as
Receiver, _status, notifications as _notifications)
listener as _listener,
status as _status,
notifications as _notifications
)
# #
# #
# #
from collections import namedtuple 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.__bool__ = lambda self: False
_GHOST_DEVICE.__nonzero__ = _GHOST_DEVICE.__bool__ _GHOST_DEVICE.__nonzero__ = _GHOST_DEVICE.__bool__
del namedtuple del namedtuple
def _ghost(device): def _ghost(device):
return _GHOST_DEVICE( return _GHOST_DEVICE(receiver=device.receiver,
receiver=device.receiver, number=device.number,
number=device.number, name=device.name,
name=device.name, kind=device.kind,
kind=device.kind, status=None,
status=None, online=False)
online=False)
# #
# #
@ -63,188 +61,201 @@ def _ghost(device):
class ReceiverListener(_listener.EventsListener): class ReceiverListener(_listener.EventsListener):
"""Keeps the status of a Receiver. """Keeps the status of a Receiver.
""" """
def __init__(self, receiver, status_changed_callback): def __init__(self, receiver, status_changed_callback):
super(ReceiverListener, self).__init__(receiver, self._notifications_handler) super(ReceiverListener, self).__init__(receiver,
# no reason to enable polling yet self._notifications_handler)
# self.tick_period = _POLL_TICK # no reason to enable polling yet
# self._last_tick = 0 # self.tick_period = _POLL_TICK
# self._last_tick = 0
assert status_changed_callback assert status_changed_callback
self.status_changed_callback = status_changed_callback self.status_changed_callback = status_changed_callback
_status.attach_to(receiver, self._status_changed) _status.attach_to(receiver, self._status_changed)
def has_started(self): def has_started(self):
if _log.isEnabledFor(_INFO): if _log.isEnabledFor(_INFO):
_log.info("%s: notifications listener has started (%s)", self.receiver, self.receiver.handle) _log.info("%s: notifications listener has started (%s)",
notification_flags = self.receiver.enable_notifications() self.receiver, self.receiver.handle)
self.receiver.status[_status.KEYS.NOTIFICATION_FLAGS] = notification_flags notification_flags = self.receiver.enable_notifications()
self.receiver.notify_devices() self.receiver.status[
self._status_changed(self.receiver) #, _status.ALERT.NOTIFICATION) _status.KEYS.NOTIFICATION_FLAGS] = notification_flags
self.receiver.notify_devices()
self._status_changed(self.receiver) #, _status.ALERT.NOTIFICATION)
def has_stopped(self): def has_stopped(self):
r, self.receiver = self.receiver, None r, self.receiver = self.receiver, None
assert r is not None assert r is not None
if _log.isEnabledFor(_INFO): if _log.isEnabledFor(_INFO):
_log.info("%s: notifications listener has stopped", r) _log.info("%s: notifications listener has stopped", r)
# because udev is not notifying us about device removal, # because udev is not notifying us about device removal,
# make sure to clean up in _all_listeners # make sure to clean up in _all_listeners
_all_listeners.pop(r.path, None) _all_listeners.pop(r.path, None)
r.status = _("The receiver was unplugged.") r.status = _("The receiver was unplugged.")
if r: if r:
try: try:
r.close() r.close()
except: except:
_log.exception("closing receiver %s" % r.path) _log.exception("closing receiver %s" % r.path)
self.status_changed_callback(r) #, _status.ALERT.NOTIFICATION) self.status_changed_callback(r) #, _status.ALERT.NOTIFICATION)
# def tick(self, timestamp): # def tick(self, timestamp):
# if not self.tick_period: # if not self.tick_period:
# raise Exception("tick() should not be called without a tick_period: %s", self) # 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 # # 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 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 # # # if we missed a couple of polls, most likely the computer went into
# # # sleep, and we have to reinitialize the receiver again # # # sleep, and we have to reinitialize the receiver again
# # _log.warn("%s: possible sleep detected, closing this listener", self.receiver) # # _log.warn("%s: possible sleep detected, closing this listener", self.receiver)
# # self.stop() # # self.stop()
# # return # # return
# #
# self._last_tick = timestamp # self._last_tick = timestamp
# #
# try: # try:
# # read these in case they haven't been read already # # read these in case they haven't been read already
# # self.receiver.serial, self.receiver.firmware # # self.receiver.serial, self.receiver.firmware
# if self.receiver.status.lock_open: # if self.receiver.status.lock_open:
# # don't mess with stuff while pairing # # don't mess with stuff while pairing
# return # return
# #
# self.receiver.status.poll(timestamp) # self.receiver.status.poll(timestamp)
# #
# # Iterating directly through the reciver would unnecessarily probe # # Iterating directly through the reciver would unnecessarily probe
# # all possible devices, even unpaired ones. # # all possible devices, even unpaired ones.
# # Checking for each device number in turn makes sure only already # # Checking for each device number in turn makes sure only already
# # known devices are polled. # # known devices are polled.
# # This is okay because we should have already known about them all # # This is okay because we should have already known about them all
# # long before the first poll() happents, through notifications. # # long before the first poll() happents, through notifications.
# for number in range(1, 6): # for number in range(1, 6):
# if number in self.receiver: # if number in self.receiver:
# dev = self.receiver[number] # dev = self.receiver[number]
# if dev and dev.status is not None: # if dev and dev.status is not None:
# dev.status.poll(timestamp) # dev.status.poll(timestamp)
# except Exception as e: # except Exception as e:
# _log.exception("polling", e) # _log.exception("polling", e)
def _status_changed(self, device, alert=_status.ALERT.NONE, reason=None): def _status_changed(self, device, alert=_status.ALERT.NONE, reason=None):
assert device is not None assert device is not None
if _log.isEnabledFor(_INFO): if _log.isEnabledFor(_INFO):
if device.kind is None: if device.kind is None:
_log.info("status_changed %s: %s, %s (%X) %s", device, _log.info("status_changed %s: %s, %s (%X) %s", device,
'present' if bool(device) else 'removed', 'present' if bool(device) else 'removed',
device.status, alert, reason or '') device.status, alert, reason or '')
else: else:
_log.info("status_changed %s: %s %s, %s (%X) %s", device, _log.info("status_changed %s: %s %s, %s (%X) %s", device,
'paired' if bool(device) else 'unpaired', 'paired' if bool(device) else 'unpaired',
'online' if device.online else 'offline', 'online' if device.online else 'offline',
device.status, alert, reason or '') device.status, alert, reason or '')
if device.kind is None: if device.kind is None:
assert device == self.receiver assert device == self.receiver
# the status of the receiver changed # the status of the receiver changed
self.status_changed_callback(device, alert, reason) self.status_changed_callback(device, alert, reason)
return return
assert device.receiver == self.receiver assert device.receiver == self.receiver
if not device: if not device:
# Device was unpaired, and isn't valid anymore. # Device was unpaired, and isn't valid anymore.
# We replace it with a ghost so that the UI has something to work # We replace it with a ghost so that the UI has something to work
# with while cleaning up. # with while cleaning up.
_log.warn("device %s was unpaired, ghosting", device) _log.warn("device %s was unpaired, ghosting", device)
device = _ghost(device) device = _ghost(device)
self.status_changed_callback(device, alert, reason) self.status_changed_callback(device, alert, reason)
if not device: if not device:
# the device was just unpaired, need to update the # the device was just unpaired, need to update the
# status of the receiver as well # status of the receiver as well
self.status_changed_callback(self.receiver) self.status_changed_callback(self.receiver)
def _notifications_handler(self, n): def _notifications_handler(self, n):
assert self.receiver assert self.receiver
# if _log.isEnabledFor(_DEBUG): # if _log.isEnabledFor(_DEBUG):
# _log.debug("%s: handling %s", self.receiver, n) # _log.debug("%s: handling %s", self.receiver, n)
if n.devnumber == 0xFF: if n.devnumber == 0xFF:
# a receiver notification # a receiver notification
_notifications.process(self.receiver, n) _notifications.process(self.receiver, n)
return return
# a device notification # a device notification
if not(0 < n.devnumber <= self.receiver.max_devices): if not (0 < n.devnumber <= self.receiver.max_devices):
if _log.isEnabledFor(_WARNING): if _log.isEnabledFor(_WARNING):
_log.warning(_("Unexpected device number (%s) in notification %s." % (n.devnumber, n))) _log.warning(
return _("Unexpected device number (%s) in notification %s." %
already_known = n.devnumber in self.receiver (n.devnumber, n)))
return
already_known = n.devnumber in self.receiver
# FIXME: hacky fix for kernel/hardware race condition # FIXME: hacky fix for kernel/hardware race condition
# If the device was just turned on or woken up from sleep, it may not # 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 # be ready to receive commands. The "payload" bit of the wireless
# status notification seems to tell us this. If this is the case, we # 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 # must wait a short amount of time to avoid causing a broken pipe
# error. # error.
device_ready = not bool(ord(n.data[0:1]) & 0x80) or n.sub_id != 0x41 device_ready = not bool(ord(n.data[0:1]) & 0x80) or n.sub_id != 0x41
if not device_ready: if not device_ready:
time.sleep(0.01) time.sleep(0.01)
if n.sub_id == 0x40 and not already_known: if n.sub_id == 0x40 and not already_known:
return # disconnecting something that is not known - nothing to do return # disconnecting something that is not known - nothing to do
if n.sub_id == 0x41: if n.sub_id == 0x41:
if not already_known: if not already_known:
dev = self.receiver.register_new_device(n.devnumber, n) 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: elif self.receiver.status.lock_open and self.receiver.re_pairs and not ord(
dev = self.receiver[n.devnumber] n.data[0:1]) & 0x40:
del self.receiver[n.devnumber] # get rid of information on device re-paired away dev = self.receiver[n.devnumber]
self._status_changed(dev) # signal that this device has changed del self.receiver[
dev = self.receiver.register_new_device(n.devnumber, n) n.
self.receiver.status.new_device = self.receiver[n.devnumber] devnumber] # get rid of information on device re-paired away
else: self._status_changed(
dev = self.receiver[n.devnumber] dev) # signal that this device has changed
else: dev = self.receiver.register_new_device(n.devnumber, n)
dev = self.receiver[n.devnumber] self.receiver.status.new_device = self.receiver[n.devnumber]
else:
dev = self.receiver[n.devnumber]
else:
dev = self.receiver[n.devnumber]
if not dev: if not dev:
_log.warn("%s: received %s for invalid device %d: %r", self.receiver, n, n.devnumber, dev) _log.warn("%s: received %s for invalid device %d: %r",
return self.receiver, n, n.devnumber, dev)
return
# Apply settings every time the device connects # Apply settings every time the device connects
if n.sub_id == 0x41: if n.sub_id == 0x41:
if _log.isEnabledFor(_INFO): if _log.isEnabledFor(_INFO):
_log.info("%s triggered new device %s (%s)", n, dev, dev.kind) _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. # If there are saved configs, bring the device's settings up-to-date.
# They will be applied when the device is marked as online. # They will be applied when the device is marked as online.
configuration.attach_to(dev) configuration.attach_to(dev)
_status.attach_to(dev, self._status_changed) _status.attach_to(dev, self._status_changed)
# the receiver changed status as well # the receiver changed status as well
self._status_changed(self.receiver) self._status_changed(self.receiver)
assert dev assert dev
assert dev.status is not None assert dev.status is not None
_notifications.process(dev, n) _notifications.process(dev, n)
if self.receiver.status.lock_open and not already_known: if self.receiver.status.lock_open and not already_known:
# this should be the first notification after a device was paired # this should be the first notification after a device was paired
assert n.sub_id == 0x41 and n.address == 0x04 assert n.sub_id == 0x41 and n.address == 0x04
if _log.isEnabledFor(_INFO): if _log.isEnabledFor(_INFO):
_log.info("%s: pairing detected new device", self.receiver) _log.info("%s: pairing detected new device", self.receiver)
self.receiver.status.new_device = dev self.receiver.status.new_device = dev
elif dev.online is None: elif dev.online is None:
dev.ping() dev.ping()
def __str__(self):
return '<ReceiverListener(%s,%s)>' % (self.receiver.path,
self.receiver.handle)
__unicode__ = __str__
def __str__(self):
return '<ReceiverListener(%s,%s)>' % (self.receiver.path, self.receiver.handle)
__unicode__ = __str__
# #
# #
@ -256,103 +267,106 @@ _all_listeners = {}
def _start(device_info): def _start(device_info):
assert _status_callback assert _status_callback
receiver = Receiver.open(device_info) receiver = Receiver.open(device_info)
if receiver: if receiver:
rl = ReceiverListener(receiver, _status_callback) rl = ReceiverListener(receiver, _status_callback)
rl.start() rl.start()
_all_listeners[device_info.path] = rl _all_listeners[device_info.path] = rl
return rl return rl
_log.warn("failed to open %s", device_info) _log.warn("failed to open %s", device_info)
def start_all(): def start_all():
# just in case this it called twice in a row... # just in case this it called twice in a row...
stop_all() stop_all()
if _log.isEnabledFor(_INFO): if _log.isEnabledFor(_INFO):
_log.info("starting receiver listening threads") _log.info("starting receiver listening threads")
for device_info in _base.receivers(): for device_info in _base.receivers():
_process_receiver_event('add', device_info) _process_receiver_event('add', device_info)
def stop_all(): def stop_all():
listeners = list(_all_listeners.values()) listeners = list(_all_listeners.values())
_all_listeners.clear() _all_listeners.clear()
if listeners: if listeners:
if _log.isEnabledFor(_INFO): if _log.isEnabledFor(_INFO):
_log.info("stopping receiver listening threads %s", listeners) _log.info("stopping receiver listening threads %s", listeners)
for l in listeners: for l in listeners:
l.stop() 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 # ping all devices to find out whether they are connected
# after a resume, the device may have been off # 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 # so mark its saved status to ensure that the status is pushed to the device when it comes back
def ping_all(resuming = False): def ping_all(resuming=False):
for l in _all_listeners.values(): for l in _all_listeners.values():
count = l.receiver.count() count = l.receiver.count()
if count: if count:
for dev in l.receiver: for dev in l.receiver:
if resuming: if resuming:
dev.status._active = False dev.status._active = False
dev.ping() dev.ping()
l._status_changed(dev) l._status_changed(dev)
count -= 1 count -= 1
if not count: if not count:
break break
from logitech_receiver import base as _base from logitech_receiver import base as _base
_status_callback = None _status_callback = None
_error_callback = None _error_callback = None
def setup_scanner(status_changed_callback, error_callback): def setup_scanner(status_changed_callback, error_callback):
global _status_callback, _error_callback global _status_callback, _error_callback
assert _status_callback is None, 'scanner was already set-up' assert _status_callback is None, 'scanner was already set-up'
_status_callback = status_changed_callback _status_callback = status_changed_callback
_error_callback = error_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 # receiver add/remove events will start/stop listener threads
def _process_receiver_event(action, device_info): def _process_receiver_event(action, device_info):
assert action is not None assert action is not None
assert device_info is not None assert device_info is not None
assert _error_callback assert _error_callback
if _log.isEnabledFor(_INFO): if _log.isEnabledFor(_INFO):
_log.info("receiver event %s %s", action, device_info) _log.info("receiver event %s %s", action, device_info)
# whatever the action, stop any previous receivers at this path # whatever the action, stop any previous receivers at this path
l = _all_listeners.pop(device_info.path, None) l = _all_listeners.pop(device_info.path, None)
if l is not None: if l is not None:
assert isinstance(l, ReceiverListener) assert isinstance(l, ReceiverListener)
l.stop() l.stop()
if action == 'add': if action == 'add':
# a new receiver device was detected # a new receiver device was detected
try: try:
_start(device_info) _start(device_info)
except OSError: except OSError:
# permission error, ignore this path for now # permission error, ignore this path for now
# If receiver has extended ACL but not writable then it is for another seat. # 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 # (It would be easier to use pylibacl but adding the pylibacl dependencies
# for this special case is not good.) # for this special case is not good.)
try: try:
import subprocess, re import subprocess, re
output = subprocess.check_output(['/usr/bin/getfacl', '-p', device_info.path]) output = subprocess.check_output(
if not re.search(b'user:.+:',output) : ['/usr/bin/getfacl', '-p', device_info.path])
_error_callback('permissions', device_info.path) if not re.search(b'user:.+:', output):
except: _error_callback('permissions', device_info.path)
_error_callback('permissions', device_info.path) except:
_error_callback('permissions', device_info.path)

View File

@ -27,44 +27,45 @@ del getLogger
from threading import Thread as _Thread from threading import Thread as _Thread
try: try:
from Queue import Queue as _Queue from Queue import Queue as _Queue
except ImportError: except ImportError:
from queue import Queue as _Queue from queue import Queue as _Queue
# #
# #
# #
class TaskRunner(_Thread): class TaskRunner(_Thread):
def __init__(self, name): def __init__(self, name):
super(TaskRunner, self).__init__(name=name) super(TaskRunner, self).__init__(name=name)
self.daemon = True self.daemon = True
self.queue = _Queue(16) self.queue = _Queue(16)
self.alive = False self.alive = False
def __call__(self, function, *args, **kwargs): def __call__(self, function, *args, **kwargs):
task = (function, args, kwargs) task = (function, args, kwargs)
self.queue.put(task) self.queue.put(task)
def stop(self): def stop(self):
self.alive = False self.alive = False
self.queue.put(None) self.queue.put(None)
def run(self): def run(self):
self.alive = True self.alive = True
if _log.isEnabledFor(_DEBUG): if _log.isEnabledFor(_DEBUG):
_log.debug("started") _log.debug("started")
while self.alive: while self.alive:
task = self.queue.get() task = self.queue.get()
if task: if task:
function, args, kwargs = task function, args, kwargs = task
assert function assert function
try: try:
function(*args, **kwargs) function(*args, **kwargs)
except: except:
_log.exception("calling %s", function) _log.exception("calling %s", function)
if _log.isEnabledFor(_DEBUG): if _log.isEnabledFor(_DEBUG):
_log.debug("stopped") _log.debug("stopped")

View File

@ -19,14 +19,12 @@
from __future__ import absolute_import, division, print_function, unicode_literals from __future__ import absolute_import, division, print_function, unicode_literals
from logging import getLogger, DEBUG as _DEBUG from logging import getLogger, DEBUG as _DEBUG
_log = getLogger(__name__) _log = getLogger(__name__)
del getLogger del getLogger
from gi.repository import GLib, Gtk from gi.repository import GLib, Gtk
from solaar.i18n import _ from solaar.i18n import _
# #
@ -41,43 +39,50 @@ GLib.threads_init()
# #
# #
def _error_dialog(reason, object): def _error_dialog(reason, object):
_log.error("error: %s %s", reason, object) _log.error("error: %s %s", reason, object)
if reason == 'permissions': if reason == 'permissions':
title = _("Permissions error") title = _("Permissions error")
text = _("Found a Logitech Receiver (%s), but did not have permission to open it.") % object + \ text = _("Found a Logitech Receiver (%s), but did not have permission to open it.") % object + \
'\n\n' + \ '\n\n' + \
_("If you've just installed Solaar, try removing the receiver and plugging it back in.") _("If you've just installed Solaar, try removing the receiver and plugging it back in.")
elif reason == 'unpair': elif reason == 'unpair':
title = _("Unpairing failed") title = _("Unpairing failed")
text = _("Failed to unpair %{device} from %{receiver}.").format(device=object.name, receiver=object.receiver.name) + \ text = _("Failed to unpair %{device} from %{receiver}.").format(device=object.name, receiver=object.receiver.name) + \
'\n\n' + \ '\n\n' + \
_("The receiver returned an error, with no further details.") _("The receiver returned an error, with no further details.")
else: else:
raise Exception("ui.error_dialog: don't know how to handle (%s, %s)", reason, object) raise Exception("ui.error_dialog: don't know how to handle (%s, %s)",
reason, object)
assert title assert title
assert text assert text
m = Gtk.MessageDialog(None, Gtk.DialogFlags.MODAL, Gtk.MessageType.ERROR, Gtk.ButtonsType.CLOSE, text) m = Gtk.MessageDialog(None, Gtk.DialogFlags.MODAL, Gtk.MessageType.ERROR,
m.set_title(title) Gtk.ButtonsType.CLOSE, text)
m.run() m.set_title(title)
m.destroy() m.run()
m.destroy()
def error_dialog(reason, object): def error_dialog(reason, object):
assert reason is not None assert reason is not None
GLib.idle_add(_error_dialog, reason, object) GLib.idle_add(_error_dialog, reason, object)
# #
# #
# #
_task_runner = None _task_runner = None
def ui_async(function, *args, **kwargs): def ui_async(function, *args, **kwargs):
if _task_runner: if _task_runner:
_task_runner(function, *args, **kwargs) _task_runner(function, *args, **kwargs)
# #
# #
@ -87,65 +92,70 @@ from . import notify, tray, window
def _startup(app, startup_hook, use_tray, show_window): def _startup(app, startup_hook, use_tray, show_window):
if _log.isEnabledFor(_DEBUG): if _log.isEnabledFor(_DEBUG):
_log.debug("startup registered=%s, remote=%s", app.get_is_registered(), app.get_is_remote()) _log.debug("startup registered=%s, remote=%s", app.get_is_registered(),
app.get_is_remote())
from solaar.tasks import TaskRunner as _TaskRunner from solaar.tasks import TaskRunner as _TaskRunner
global _task_runner global _task_runner
_task_runner = _TaskRunner('AsyncUI') _task_runner = _TaskRunner('AsyncUI')
_task_runner.start() _task_runner.start()
notify.init() notify.init()
if use_tray: if use_tray:
tray.init(lambda _ignore: window.destroy()) tray.init(lambda _ignore: window.destroy())
window.init(show_window, use_tray) window.init(show_window, use_tray)
startup_hook() startup_hook()
def _activate(app): def _activate(app):
if _log.isEnabledFor(_DEBUG): if _log.isEnabledFor(_DEBUG):
_log.debug("activate") _log.debug("activate")
if app.get_windows(): if app.get_windows():
window.popup() window.popup()
else: else:
app.add_window(window._window) app.add_window(window._window)
def _command_line(app, command_line): def _command_line(app, command_line):
if _log.isEnabledFor(_DEBUG): if _log.isEnabledFor(_DEBUG):
_log.debug("command_line %s", command_line.get_arguments()) _log.debug("command_line %s", command_line.get_arguments())
return 0 return 0
def _shutdown(app, shutdown_hook): def _shutdown(app, shutdown_hook):
if _log.isEnabledFor(_DEBUG): if _log.isEnabledFor(_DEBUG):
_log.debug("shutdown") _log.debug("shutdown")
shutdown_hook() shutdown_hook()
# stop the async UI processor # stop the async UI processor
global _task_runner global _task_runner
_task_runner.stop() _task_runner.stop()
_task_runner = None _task_runner = None
tray.destroy() tray.destroy()
notify.uninit() notify.uninit()
def run_loop(startup_hook, shutdown_hook, use_tray, show_window, args=None): 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' assert use_tray or show_window, 'need either tray or visible window'
# from gi.repository.Gio import ApplicationFlags as _ApplicationFlags # from gi.repository.Gio import ApplicationFlags as _ApplicationFlags
APP_ID = 'io.github.pwr.solaar' APP_ID = 'io.github.pwr.solaar'
application = Gtk.Application.new(APP_ID, 0) # _ApplicationFlags.HANDLES_COMMAND_LINE) 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(
application.connect('command-line', _command_line) 'startup', lambda app, startup_hook: _startup(
application.connect('activate', _activate) app, startup_hook, use_tray, show_window), startup_hook)
application.connect('shutdown', _shutdown, shutdown_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): def _status_changed(device, alert, reason):
assert device is not None assert device is not None
if _log.isEnabledFor(_DEBUG): if _log.isEnabledFor(_DEBUG):
_log.debug("status changed: %s (%s) %s", device, alert, reason) _log.debug("status changed: %s (%s) %s", device, alert, reason)
tray.update(device) tray.update(device)
if alert & ALERT.ATTENTION: if alert & ALERT.ATTENTION:
tray.attention(reason) tray.attention(reason)
need_popup = alert & ALERT.SHOW_WINDOW need_popup = alert & ALERT.SHOW_WINDOW
window.update(device, need_popup) window.update(device, need_popup)
if alert & (ALERT.NOTIFICATION | ALERT.ATTENTION): if alert & (ALERT.NOTIFICATION | ALERT.ATTENTION):
notify.show(device, reason) notify.show(device, reason)
def status_changed(device, alert=ALERT.NONE, reason=None): 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)

View File

@ -32,64 +32,68 @@ _dialog = None
def _create(): def _create():
about = Gtk.AboutDialog() about = Gtk.AboutDialog()
about.set_program_name(NAME) about.set_program_name(NAME)
about.set_version(__version__) about.set_version(__version__)
about.set_comments(_("Shows status of devices connected\nthrough wireless Logitech receivers.")) 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_copyright('© 2012-2013 Daniel Pavel')
about.set_license_type(Gtk.License.GPL_2_0) about.set_license_type(Gtk.License.GPL_2_0)
about.set_authors(('Daniel Pavel http://github.com/pwr',)) about.set_authors(('Daniel Pavel http://github.com/pwr', ))
try: try:
about.add_credit_section(_("GUI design"), ('Julien Gascard', 'Daniel Pavel')) about.add_credit_section(_("GUI design"),
about.add_credit_section(_("Testing"), ( ('Julien Gascard', 'Daniel Pavel'))
'Douglas Wagner', about.add_credit_section(_("Testing"), (
'Julien Gascard', 'Douglas Wagner',
'Peter Wu http://www.lekensteyn.nl/logitech-unifying.html', 'Julien Gascard',
)) 'Peter Wu http://www.lekensteyn.nl/logitech-unifying.html',
about.add_credit_section(_("Logitech documentation"), ( ))
'Julien Danjou http://julien.danjou.info/blog/2012/logitech-unifying-upower', about.add_credit_section(_("Logitech documentation"), (
'Nestor Lopez Casado http://drive.google.com/folderview?id=0BxbRzx7vEV7eWmgwazJ3NUFfQ28', 'Julien Danjou http://julien.danjou.info/blog/2012/logitech-unifying-upower',
)) 'Nestor Lopez Casado http://drive.google.com/folderview?id=0BxbRzx7vEV7eWmgwazJ3NUFfQ28',
except TypeError: ))
# gtk3 < ~3.6.4 has incorrect gi bindings except TypeError:
import logging # gtk3 < ~3.6.4 has incorrect gi bindings
logging.exception("failed to fully create the about dialog") import logging
except: logging.exception("failed to fully create the about dialog")
# the Gtk3 version may be too old, and the function does not exist except:
import logging # the Gtk3 version may be too old, and the function does not exist
logging.exception("failed to fully create the about dialog") import logging
logging.exception("failed to fully create the about dialog")
about.set_translator_credits('\n'.join(( about.set_translator_credits('\n'.join((
'gogo (croatian)', 'gogo (croatian)',
'Papoteur, David Geiger, Damien Lallement (français)', 'Papoteur, David Geiger, Damien Lallement (français)',
'Michele Olivo (italiano)', 'Michele Olivo (italiano)',
'Adrian Piotrowicz (polski)', 'Adrian Piotrowicz (polski)',
'Drovetto, JrBenito (Portuguese-BR)', 'Drovetto, JrBenito (Portuguese-BR)',
'Daniel Pavel (română)', 'Daniel Pavel (română)',
'Daniel Zippert, Emelie Snecker (svensk)', 'Daniel Zippert, Emelie Snecker (svensk)',
'Dimitriy Ryazantcev (Russian)', 'Dimitriy Ryazantcev (Russian)',
))) )))
about.set_website('http://pwr-solaar.github.io/Solaar/') about.set_website('http://pwr-solaar.github.io/Solaar/')
about.set_website_label(NAME) about.set_website_label(NAME)
about.connect('response', lambda x, y: x.hide()) about.connect('response', lambda x, y: x.hide())
def _hide(dialog, event): def _hide(dialog, event):
dialog.hide() dialog.hide()
return True return True
about.connect('delete-event', _hide)
return about about.connect('delete-event', _hide)
return about
def show_window(trigger=None): def show_window(trigger=None):
global _dialog global _dialog
if _dialog is None: if _dialog is None:
_dialog = _create() _dialog = _create()
_dialog.present() _dialog.present()

View File

@ -25,30 +25,31 @@ from gi.repository import Gtk, Gdk
# _log = getLogger(__name__) # _log = getLogger(__name__)
# del getLogger # del getLogger
from solaar.i18n import _ from solaar.i18n import _
# #
# #
# #
def make(name, label, function, stock_id=None, *args): def make(name, label, function, stock_id=None, *args):
action = Gtk.Action(name, label, label, None) action = Gtk.Action(name, label, label, None)
action.set_icon_name(name) action.set_icon_name(name)
if stock_id is not None: if stock_id is not None:
action.set_stock_id(stock_id) action.set_stock_id(stock_id)
if function: if function:
action.connect('activate', function, *args) action.connect('activate', function, *args)
return action return action
def make_toggle(name, label, function, stock_id=None, *args): def make_toggle(name, label, function, stock_id=None, *args):
action = Gtk.ToggleAction(name, label, label, None) action = Gtk.ToggleAction(name, label, label, None)
action.set_icon_name(name) action.set_icon_name(name)
if stock_id is not None: if stock_id is not None:
action.set_stock_id(stock_id) action.set_stock_id(stock_id)
action.connect('activate', function, *args) action.connect('activate', function, *args)
return action return action
# #
# #
@ -62,49 +63,55 @@ def make_toggle(name, label, function, stock_id=None, *args):
# action.set_sensitive(notify.available) # action.set_sensitive(notify.available)
# toggle_notifications = make_toggle('notifications', 'Notifications', _toggle_notifications) # toggle_notifications = make_toggle('notifications', 'Notifications', _toggle_notifications)
from .about import show_window as _show_about_window from .about import show_window as _show_about_window
from solaar import NAME 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 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) def pair(window, receiver):
pair_dialog.set_destroy_with_parent(True) assert receiver
pair_dialog.set_modal(True) assert receiver.kind is None
pair_dialog.set_type_hint(Gdk.WindowTypeHint.DIALOG)
pair_dialog.set_position(Gtk.WindowPosition.CENTER) pair_dialog = pair_window.create(receiver)
pair_dialog.present() 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 from ..ui import error_dialog
def unpair(window, device): def unpair(window, device):
assert device assert device
assert device.kind is not None assert device.kind is not None
qdialog = Gtk.MessageDialog(window, 0, qdialog = Gtk.MessageDialog(window, 0, Gtk.MessageType.QUESTION,
Gtk.MessageType.QUESTION, Gtk.ButtonsType.NONE, Gtk.ButtonsType.NONE,
_("Unpair") + ' ' + device.name + ' ?') _("Unpair") + ' ' + device.name + ' ?')
qdialog.set_icon_name('remove') qdialog.set_icon_name('remove')
qdialog.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL) qdialog.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)
qdialog.add_button(_("Unpair"), Gtk.ResponseType.ACCEPT) qdialog.add_button(_("Unpair"), Gtk.ResponseType.ACCEPT)
choice = qdialog.run() choice = qdialog.run()
qdialog.destroy() qdialog.destroy()
if choice == Gtk.ResponseType.ACCEPT: if choice == Gtk.ResponseType.ACCEPT:
receiver = device.receiver receiver = device.receiver
assert receiver assert receiver
device_number = device.number device_number = device.number
try: try:
del receiver[device_number] del receiver[device_number]
except: except:
# _log.exception("unpairing %s", device) # _log.exception("unpairing %s", device)
error_dialog('unpair', device) error_dialog('unpair', device)

View File

@ -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): def _write_async(setting, value, sbox):
_ignore, failed, spinner, control = sbox.get_children() _ignore, failed, spinner, control = sbox.get_children()
control.set_sensitive(False) control.set_sensitive(False)
failed.set_visible(False) failed.set_visible(False)
spinner.set_visible(True) spinner.set_visible(True)
spinner.start() spinner.start()
def _do_write(s, v, sb): def _do_write(s, v, sb):
v = setting.write(v) v = setting.write(v)
GLib.idle_add(_update_setting_item, sb, v, True, priority=99) 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): def _write_async_key_value(setting, key, value, sbox):
_ignore, failed, spinner, control = sbox.get_children() _ignore, failed, spinner, control = sbox.get_children()
control.set_sensitive(False) control.set_sensitive(False)
failed.set_visible(False) failed.set_visible(False)
spinner.set_visible(True) spinner.set_visible(True)
spinner.start() spinner.start()
def _do_write_key_value(s, k, v, sb): def _do_write_key_value(s, k, v, sb):
v = setting.write_key_value(k, v) v = setting.write_key_value(k, v)
GLib.idle_add(_update_setting_item, sb, {k: v}, True, priority=99) 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 _create_toggle_control(setting):
def _switch_notify(switch, _ignore, s): def _switch_notify(switch, _ignore, s):
if switch.get_sensitive(): if switch.get_sensitive():
_write_async(s, switch.get_active() == True, switch.get_parent()) _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 _create_choice_control(setting):
def _combo_notify(cbbox, s): def _combo_notify(cbbox, s):
if cbbox.get_sensitive(): if cbbox.get_sensitive():
_write_async(s, cbbox.get_active_id(), cbbox.get_parent()) _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 _create_map_choice_control(setting):
def _map_value_notify_key(cbbox, s): def _map_value_notify_key(cbbox, s):
setting, valueBox = s setting, valueBox = s
key_choice = int(cbbox.get_active_id()) key_choice = int(cbbox.get_active_id())
if cbbox.get_sensitive(): if cbbox.get_sensitive():
valueBox.remove_all() valueBox.remove_all()
_map_populate_value_box(valueBox, setting, key_choice) _map_populate_value_box(valueBox, setting, key_choice)
def _map_value_notify_value(cbbox, s): def _map_value_notify_value(cbbox, s):
setting, keyBox = s setting, keyBox = s
key_choice = keyBox.get_active_id() key_choice = keyBox.get_active_id()
if key_choice is not None and cbbox.get_sensitive() and cbbox.get_active_id(): if key_choice is not None and cbbox.get_sensitive(
if setting._value.get(key_choice) != int(cbbox.get_active_id()): ) and cbbox.get_active_id():
setting._value[key_choice] = int(cbbox.get_active_id()) if setting._value.get(key_choice) != int(cbbox.get_active_id()):
_write_async_key_value(setting, key_choice, setting._value[key_choice], cbbox.get_parent().get_parent()) 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): def _map_populate_value_box(valueBox, setting, key_choice):
choices = None choices = None
choices = setting.choices[key_choice] choices = setting.choices[key_choice]
current = setting._value.get(str(key_choice)) # just in case the persisted value is missing some keys current = setting._value.get(
if choices: str(key_choice
# TODO i18n text entries )) # just in case the persisted value is missing some keys
for choice in choices: if choices:
valueBox.append(str(int(choice)), str(choice)) # TODO i18n text entries
if current is not None: for choice in choices:
valueBox.set_active_id(str(int(current))) 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): def _create_slider_control(setting):
class SliderControl: class SliderControl:
__slots__ = ('gtk_range', 'timer', 'setting') __slots__ = ('gtk_range', 'timer', 'setting')
def __init__(self, setting):
self.setting = setting
self.timer = None
self.gtk_range = Gtk.Scale() def __init__(self, setting):
self.gtk_range.set_range(*self.setting.range) self.setting = setting
self.gtk_range.set_round_digits(0) self.timer = None
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 _write(self): self.gtk_range = Gtk.Scale()
_write_async(self.setting, self.gtk_range.set_range(*self.setting.range)
int(self.gtk_range.get_value()), self.gtk_range.set_round_digits(0)
self.gtk_range.get_parent()) self.gtk_range.set_digits(0)
self.timer.cancel() self.gtk_range.set_increments(1, 5)
self.gtk_range.connect('value-changed', lambda _, c: c._changed(),
self)
def _changed(self): def _write(self):
if self.gtk_range.get_sensitive(): _write_async(self.setting, int(self.gtk_range.get_value()),
if self.timer: self.gtk_range.get_parent())
self.timer.cancel() self.timer.cancel()
self.timer = _Timer(0.5, lambda: GLib.idle_add(self._write))
self.timer.start() 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): def _create_sbox(s):
sbox = Gtk.HBox(homogeneous=False, spacing=6) sbox = Gtk.HBox(homogeneous=False, spacing=6)
sbox.pack_start(Gtk.Label(s.label), False, False, 0) sbox.pack_start(Gtk.Label(s.label), False, False, 0)
spinner = Gtk.Spinner() spinner = Gtk.Spinner()
spinner.set_tooltip_text(_("Working") + '...') spinner.set_tooltip_text(_("Working") + '...')
failed = Gtk.Image.new_from_icon_name('dialog-warning', Gtk.IconSize.SMALL_TOOLBAR) failed = Gtk.Image.new_from_icon_name('dialog-warning',
failed.set_tooltip_text(_("Read/write operation failed.")) Gtk.IconSize.SMALL_TOOLBAR)
failed.set_tooltip_text(_("Read/write operation failed."))
if s.kind == _SETTING_KIND.toggle: if s.kind == _SETTING_KIND.toggle:
control = _create_toggle_control(s) control = _create_toggle_control(s)
sbox.pack_end(control, False, False, 0) sbox.pack_end(control, False, False, 0)
elif s.kind == _SETTING_KIND.choice: elif s.kind == _SETTING_KIND.choice:
control = _create_choice_control(s) control = _create_choice_control(s)
sbox.pack_end(control, False, False, 0) sbox.pack_end(control, False, False, 0)
elif s.kind == _SETTING_KIND.range: elif s.kind == _SETTING_KIND.range:
control = _create_slider_control(s) control = _create_slider_control(s)
sbox.pack_end(control, True, True, 0) sbox.pack_end(control, True, True, 0)
elif s.kind == _SETTING_KIND.map_choice: elif s.kind == _SETTING_KIND.map_choice:
control = _create_map_choice_control(s) control = _create_map_choice_control(s)
sbox.pack_end(control, True, True, 0) sbox.pack_end(control, True, True, 0)
elif s.kind == _SETTING_KIND.multiple_toggle: elif s.kind == _SETTING_KIND.multiple_toggle:
# ugly temporary hack! # ugly temporary hack!
choices = {k : [False, True] for k in s._validator.options} 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")
control.set_sensitive(False) # the first read will enable it class X:
sbox.pack_end(spinner, False, False, 0) def __init__(self, obj, ext):
sbox.pack_end(failed, False, False, 0) self.obj = obj
self.ext = ext
if s.description: def __getattr__(self, attr):
sbox.set_tooltip_text(s.description) try:
return self.ext[attr]
except KeyError:
return getattr(self.obj, attr)
sbox.show_all() control = _create_map_choice_control(X(s, {'choices': choices}))
spinner.start() # the first read will stop it sbox.pack_end(control, True, True, 0)
failed.set_visible(False) 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): def _update_setting_item(sbox, value, is_online=True):
_ignore, failed, spinner, control = sbox.get_children() # depends on box layout _ignore, failed, spinner, control = sbox.get_children(
spinner.set_visible(False) ) # depends on box layout
spinner.stop() spinner.set_visible(False)
spinner.stop()
if value is None: if value is None:
control.set_sensitive(False) control.set_sensitive(False)
failed.set_visible(is_online) failed.set_visible(is_online)
return 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 _box = None
_items = {} _items = {}
def create(): def create():
global _box global _box
assert _box is None assert _box is None
_box = Gtk.VBox(homogeneous=False, spacing=8) _box = Gtk.VBox(homogeneous=False, spacing=8)
_box._last_device = None _box._last_device = None
return _box return _box
def update(device, is_online=None): def update(device, is_online=None):
assert _box is not None assert _box is not None
assert device assert device
device_id = (device.receiver.path, device.number) device_id = (device.receiver.path, device.number)
if is_online is None: if is_online is None:
is_online = bool(device.online) is_online = bool(device.online)
# if the device changed since last update, clear the box first # if the device changed since last update, clear the box first
if device_id != _box._last_device: if device_id != _box._last_device:
_box.set_visible(False) _box.set_visible(False)
_box._last_device = device_id _box._last_device = device_id
# hide controls belonging to other devices # hide controls belonging to other devices
for k, sbox in _items.items(): for k, sbox in _items.items():
sbox = _items[k] sbox = _items[k]
sbox.set_visible(k[0:2] == device_id) sbox.set_visible(k[0:2] == device_id)
for s in device.settings: for s in device.settings:
k = (device_id[0], device_id[1], s.name) k = (device_id[0], device_id[1], s.name)
if k in _items: if k in _items:
sbox = _items[k] sbox = _items[k]
else: else:
sbox = _items[k] = _create_sbox(s) sbox = _items[k] = _create_sbox(s)
_box.pack_start(sbox, False, False, 0) _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): 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. Needed after the device has been unpaired.
""" """
assert _box is not None assert _box is not None
device_id = (device.receiver.path, device.number) device_id = (device.receiver.path, device.number)
for k in list(_items.keys()): for k in list(_items.keys()):
if k[0:2] == device_id: if k[0:2] == device_id:
_box.remove(_items[k]) _box.remove(_items[k])
del _items[k] del _items[k]
def destroy(): def destroy():
global _box global _box
_box = None _box = None
_items.clear() _items.clear()

View File

@ -46,92 +46,110 @@ TRAY_ATTENTION = 'solaar-attention'
def _look_for_application_icons(): def _look_for_application_icons():
import os.path as _path import os.path as _path
from os import environ as _environ from os import environ as _environ
import sys as _sys import sys as _sys
if _log.isEnabledFor(_DEBUG): if _log.isEnabledFor(_DEBUG):
_log.debug("sys.path[0] = %s", _sys.path[0]) _log.debug("sys.path[0] = %s", _sys.path[0])
prefix_share = _path.normpath(_path.join(_path.realpath(_sys.path[0]), '..')) prefix_share = _path.normpath(
src_share = _path.normpath(_path.join(_path.realpath(_sys.path[0]), '..', 'share')) _path.join(_path.realpath(_sys.path[0]), '..'))
local_share = _environ.get('XDG_DATA_HOME', _path.expanduser(_path.join('~', '.local', 'share'))) src_share = _path.normpath(
data_dirs = _environ.get('XDG_DATA_DIRS', '/usr/local/share:/usr/share') _path.join(_path.realpath(_sys.path[0]), '..', 'share'))
repo_share = _path.normpath(_path.join(_path.dirname(__file__), '..', '..', '..', 'share')) local_share = _environ.get(
setuptools_share = _path.normpath(_path.join(_path.dirname(__file__), '..', '..', 'share')) 'XDG_DATA_HOME', _path.expanduser(_path.join('~', '.local', 'share')))
del _sys 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(':')) share_solaar = [prefix_share] + list(
for location in share_solaar: _path.join(x, 'solaar')
location = _path.join(location, 'icons') for x in [src_share, local_share, setuptools_share, repo_share] +
if _log.isEnabledFor(_DEBUG): data_dirs.split(':'))
_log.debug("looking for icons in %s", location) 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')): if _path.exists(_path.join(location, TRAY_ATTENTION + '.svg')):
yield location yield location
del _environ del _environ
# del _path # del _path
_default_theme = None _default_theme = None
_use_symbolic_icons = False _use_symbolic_icons = False
def _init_icon_paths(): def _init_icon_paths():
global _default_theme global _default_theme
if _default_theme: if _default_theme:
return return
_default_theme = Gtk.IconTheme.get_default() _default_theme = Gtk.IconTheme.get_default()
for p in _look_for_application_icons(): for p in _look_for_application_icons():
_default_theme.prepend_search_path(p) _default_theme.prepend_search_path(p)
if _log.isEnabledFor(_DEBUG): if _log.isEnabledFor(_DEBUG):
_log.debug("icon theme paths: %s", _default_theme.get_search_path()) _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): def battery(level=None, charging=False):
icon_name = _battery_icon_name(level, charging) icon_name = _battery_icon_name(level, charging)
if not _default_theme.has_icon(icon_name): if not _default_theme.has_icon(icon_name):
_log.warning("icon %s not found in current theme", icon_name) _log.warning("icon %s not found in current theme", icon_name)
return TRAY_OKAY # use Solaar icon if battery icon not available return TRAY_OKAY # use Solaar icon if battery icon not available
elif _log.isEnabledFor(_DEBUG): elif _log.isEnabledFor(_DEBUG):
_log.debug("battery icon for %s:%s = %s", level, charging, icon_name) _log.debug("battery icon for %s:%s = %s", level, charging, icon_name)
return icon_name return icon_name
# return first res where val >= guard # return first res where val >= guard
# _first_res(val,((guard,res),...)) # _first_res(val,((guard,res),...))
def _first_res(val,pairs): def _first_res(val, pairs):
return next((res for guard,res in pairs if val >= guard),None) return next((res for guard, res in pairs if val >= guard), None)
def _battery_icon_name(level, charging): def _battery_icon_name(level, charging):
_init_icon_paths() _init_icon_paths()
if level is None or level < 0: if level is None or level < 0:
return 'battery-missing' + ( '-symbolic' if _use_symbolic_icons else '' ) 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): def lux(level=None):
if level is None or level < 0: if level is None or level < 0:
return 'light_unknown' return 'light_unknown'
return 'light_%03d' % (20 * ((level + 50) // 100)) return 'light_%03d' % (20 * ((level + 50) // 100))
# #
# #
@ -139,65 +157,66 @@ def lux(level=None):
_ICON_SETS = {} _ICON_SETS = {}
def device_icon_set(name='_', kind=None): def device_icon_set(name='_', kind=None):
icon_set = _ICON_SETS.get(name) icon_set = _ICON_SETS.get(name)
if icon_set is None: if icon_set is None:
icon_set = Gtk.IconSet.new() icon_set = Gtk.IconSet.new()
_ICON_SETS[name] = icon_set _ICON_SETS[name] = icon_set
# names of possible icons, in reverse order of likelihood # names of possible icons, in reverse order of likelihood
# the theme will hopefully pick up the most appropriate # the theme will hopefully pick up the most appropriate
names = ['preferences-desktop-peripherals'] names = ['preferences-desktop-peripherals']
if kind: if kind:
if str(kind) == 'numpad': if str(kind) == 'numpad':
names += ('input-keyboard', 'input-dialpad') names += ('input-keyboard', 'input-dialpad')
elif str(kind) == 'touchpad': elif str(kind) == 'touchpad':
names += ('input-mouse', 'input-tablet') names += ('input-mouse', 'input-tablet')
elif str(kind) == 'trackball': elif str(kind) == 'trackball':
names += ('input-mouse',) names += ('input-mouse', )
names += ('input-' + str(kind),) names += ('input-' + str(kind), )
# names += (name.replace(' ', '-'),) # names += (name.replace(' ', '-'),)
source = Gtk.IconSource.new() source = Gtk.IconSource.new()
for n in names: for n in names:
source.set_icon_name(n) source.set_icon_name(n)
icon_set.add_source(source) icon_set.add_source(source)
icon_set.names = names icon_set.names = names
return icon_set return icon_set
def device_icon_file(name, kind=None, size=_LARGE_SIZE): def device_icon_file(name, kind=None, size=_LARGE_SIZE):
_init_icon_paths() _init_icon_paths()
icon_set = device_icon_set(name, kind) icon_set = device_icon_set(name, kind)
assert icon_set assert icon_set
for n in reversed(icon_set.names): for n in reversed(icon_set.names):
if _default_theme.has_icon(n): if _default_theme.has_icon(n):
return _default_theme.lookup_icon(n, size, 0).get_filename() return _default_theme.lookup_icon(n, size, 0).get_filename()
def device_icon_name(name, kind=None): def device_icon_name(name, kind=None):
_init_icon_paths() _init_icon_paths()
icon_set = device_icon_set(name, kind) icon_set = device_icon_set(name, kind)
assert icon_set assert icon_set
for n in reversed(icon_set.names): for n in reversed(icon_set.names):
if _default_theme.has_icon(n): if _default_theme.has_icon(n):
return n return n
def icon_file(name, size=_LARGE_SIZE): def icon_file(name, size=_LARGE_SIZE):
_init_icon_paths() _init_icon_paths()
# has_icon() somehow returned False while lookup_icon returns non-None. # has_icon() somehow returned False while lookup_icon returns non-None.
# I guess it happens because share/solaar/icons/ has no hicolor and # I guess it happens because share/solaar/icons/ has no hicolor and
# resolution subdirs # resolution subdirs
theme_icon = _default_theme.lookup_icon(name, size, 0) theme_icon = _default_theme.lookup_icon(name, size, 0)
if theme_icon: if theme_icon:
file_name = theme_icon.get_filename() file_name = theme_icon.get_filename()
# if _log.isEnabledFor(_DEBUG): # if _log.isEnabledFor(_DEBUG):
# _log.debug("icon %s(%d) => %s", name, size, file_name) # _log.debug("icon %s(%d) => %s", name, size, file_name)
return 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)

View File

@ -21,7 +21,6 @@
from __future__ import absolute_import, division, print_function, unicode_literals from __future__ import absolute_import, division, print_function, unicode_literals
from solaar.i18n import _ from solaar.i18n import _
# #
@ -29,125 +28,121 @@ from solaar.i18n import _
# #
try: try:
import gi import gi
gi.require_version('Notify', '0.7') gi.require_version('Notify', '0.7')
# this import is allowed to fail, in which case the entire feature is unavailable # this import is allowed to fail, in which case the entire feature is unavailable
from gi.repository import Notify, GLib from gi.repository import Notify, GLib
# assumed to be working since the import succeeded # assumed to be working since the import succeeded
available = True available = True
except (ValueError, ImportError): except (ValueError, ImportError):
available = False available = False
if available: if available:
from logging import getLogger, INFO as _INFO from logging import getLogger, INFO as _INFO
_log = getLogger(__name__) _log = getLogger(__name__)
del getLogger del getLogger
from solaar import NAME from solaar import NAME
from . import icons as _icons from . import icons as _icons
# cache references to shown notifications here, so if another status comes # cache references to shown notifications here, so if another status comes
# while its notification is still visible we don't create another one # while its notification is still visible we don't create another one
_notifications = {} _notifications = {}
def init(): def init():
"""Init the notifications system.""" """Init the notifications system."""
global available global available
if available: if available:
if not Notify.is_initted(): if not Notify.is_initted():
if _log.isEnabledFor(_INFO): if _log.isEnabledFor(_INFO):
_log.info("starting desktop notifications") _log.info("starting desktop notifications")
try: try:
return Notify.init(NAME) return Notify.init(NAME)
except: except:
_log.exception("initializing desktop notifications") _log.exception("initializing desktop notifications")
available = False available = False
return available and Notify.is_initted() 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(): # def toggle(action):
if available and Notify.is_initted(): # if action.get_active():
if _log.isEnabledFor(_INFO): # init()
_log.info("stopping desktop notifications") # else:
_notifications.clear() # uninit()
Notify.uninit() # action.set_sensitive(available)
# return action.get_active()
def alert(reason, icon=None):
assert reason
# def toggle(action): if available and Notify.is_initted():
# if action.get_active(): n = _notifications.get(NAME)
# init() if n is None:
# else: n = _notifications[NAME] = Notify.Notification()
# uninit()
# action.set_sensitive(available)
# return action.get_active()
# 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): n.update(NAME, reason, icon_file)
assert reason n.set_urgency(Notify.Urgency.NORMAL)
n.set_hint("desktop-entry", GLib.Variant('s', NAME.lower()))
if available and Notify.is_initted(): try:
n = _notifications.get(NAME) # if _log.isEnabledFor(_DEBUG):
if n is None: # _log.debug("showing %s", n)
n = _notifications[NAME] = Notify.Notification() n.show()
except Exception:
_log.exception("showing %s", n)
# we need to use the filename here because the notifications daemon def show(dev, reason=None, icon=None):
# is an external application that does not know about our icon sets """Show a notification with title and text."""
icon_file = _icons.icon_file(NAME.lower()) if icon is None \ if available and Notify.is_initted():
else _icons.icon_file(icon) summary = dev.name
n.update(NAME, reason, icon_file) # if a notification with same name is already visible, reuse it to avoid spamming
n.set_urgency(Notify.Urgency.NORMAL) n = _notifications.get(summary)
n.set_hint("desktop-entry", GLib.Variant('s', NAME.lower())) if n is None:
n = _notifications[summary] = Notify.Notification()
try: if reason:
# if _log.isEnabledFor(_DEBUG): message = reason
# _log.debug("showing %s", n) elif dev.status is None:
n.show() message = _("unpaired")
except Exception: elif bool(dev.status):
_log.exception("showing %s", n) 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): n.update(summary, message, icon_file)
"""Show a notification with title and text.""" urgency = Notify.Urgency.LOW if dev.status else Notify.Urgency.NORMAL
if available and Notify.is_initted(): n.set_urgency(urgency)
summary = dev.name n.set_hint("desktop-entry", GLib.Variant('s', NAME.lower()))
# if a notification with same name is already visible, reuse it to avoid spamming try:
n = _notifications.get(summary) # if _log.isEnabledFor(_DEBUG):
if n is None: # _log.debug("showing %s", n)
n = _notifications[summary] = Notify.Notification() n.show()
except Exception:
if reason: _log.exception("showing %s", n)
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)
else: else:
init = lambda: False init = lambda: False
uninit = lambda: None uninit = lambda: None
# toggle = lambda action: False # toggle = lambda action: False
alert = lambda reason: None alert = lambda reason: None
show = lambda dev, reason=None: None show = lambda dev, reason=None: None

View File

@ -25,7 +25,6 @@ from logging import getLogger, DEBUG as _DEBUG
_log = getLogger(__name__) _log = getLogger(__name__)
del getLogger del getLogger
from solaar.i18n import _ from solaar.i18n import _
from . import icons as _icons from . import icons as _icons
from logitech_receiver.status import KEYS as _K 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): def _create_page(assistant, kind, header=None, icon_name=None, text=None):
p = Gtk.VBox(False, 8) p = Gtk.VBox(False, 8)
assistant.append_page(p) assistant.append_page(p)
assistant.set_page_type(p, kind) assistant.set_page_type(p, kind)
if header: if header:
item = Gtk.HBox(False, 16) item = Gtk.HBox(False, 16)
p.pack_start(item, False, True, 0) p.pack_start(item, False, True, 0)
label = Gtk.Label(header) label = Gtk.Label(header)
label.set_alignment(0, 0) label.set_alignment(0, 0)
label.set_line_wrap(True) label.set_line_wrap(True)
item.pack_start(label, True, True, 0) item.pack_start(label, True, True, 0)
if icon_name: if icon_name:
icon = Gtk.Image.new_from_icon_name(icon_name, Gtk.IconSize.DIALOG) icon = Gtk.Image.new_from_icon_name(icon_name, Gtk.IconSize.DIALOG)
icon.set_alignment(1, 0) icon.set_alignment(1, 0)
item.pack_start(icon, False, False, 0) item.pack_start(icon, False, False, 0)
if text: if text:
label = Gtk.Label(text) label = Gtk.Label(text)
label.set_alignment(0, 0) label.set_alignment(0, 0)
label.set_line_wrap(True) label.set_line_wrap(True)
p.pack_start(label, False, False, 0) p.pack_start(label, False, False, 0)
p.show_all() p.show_all()
return p return p
def _check_lock_state(assistant, receiver, count=2): def _check_lock_state(assistant, receiver, count=2):
if not assistant.is_drawable(): if not assistant.is_drawable():
if _log.isEnabledFor(_DEBUG): if _log.isEnabledFor(_DEBUG):
_log.debug("assistant %s destroyed, bailing out", assistant) _log.debug("assistant %s destroyed, bailing out", assistant)
return False return False
if receiver.status.get(_K.ERROR): if receiver.status.get(_K.ERROR):
# receiver.status.new_device = _fake_device(receiver) # receiver.status.new_device = _fake_device(receiver)
_pairing_failed(assistant, receiver, receiver.status.pop(_K.ERROR)) _pairing_failed(assistant, receiver, receiver.status.pop(_K.ERROR))
return False return False
if receiver.status.new_device: if receiver.status.new_device:
device, receiver.status.new_device = receiver.status.new_device, None device, receiver.status.new_device = receiver.status.new_device, None
_pairing_succeeded(assistant, receiver, device) _pairing_succeeded(assistant, receiver, device)
return False return False
if not receiver.status.lock_open: if not receiver.status.lock_open:
if count > 0: if count > 0:
# the actual device notification may arrive after the lock was paired, # the actual device notification may arrive after the lock was paired,
# so have a little patience # so have a little patience
GLib.timeout_add(_STATUS_CHECK, _check_lock_state, assistant, receiver, count - 1) GLib.timeout_add(_STATUS_CHECK, _check_lock_state, assistant,
else: receiver, count - 1)
_pairing_failed(assistant, receiver, 'failed to open pairing lock') else:
return False _pairing_failed(assistant, receiver, 'failed to open pairing lock')
return False
return True return True
def _prepare(assistant, page, receiver): def _prepare(assistant, page, receiver):
index = assistant.get_current_page() index = assistant.get_current_page()
if _log.isEnabledFor(_DEBUG): if _log.isEnabledFor(_DEBUG):
_log.debug("prepare %s %d %s", assistant, index, page) _log.debug("prepare %s %d %s", assistant, index, page)
if index == 0: if index == 0:
if receiver.set_lock(False, timeout=_PAIRING_TIMEOUT): if receiver.set_lock(False, timeout=_PAIRING_TIMEOUT):
assert receiver.status.new_device is None assert receiver.status.new_device is None
assert receiver.status.get(_K.ERROR) is None assert receiver.status.get(_K.ERROR) is None
spinner = page.get_children()[-1] spinner = page.get_children()[-1]
spinner.start() spinner.start()
GLib.timeout_add(_STATUS_CHECK, _check_lock_state, assistant, receiver) GLib.timeout_add(_STATUS_CHECK, _check_lock_state, assistant,
assistant.set_page_complete(page, True) receiver)
else: assistant.set_page_complete(page, True)
GLib.idle_add(_pairing_failed, assistant, receiver, 'the pairing lock did not open') else:
else: GLib.idle_add(_pairing_failed, assistant, receiver,
assistant.remove_page(0) 'the pairing lock did not open')
else:
assistant.remove_page(0)
def _finish(assistant, receiver): def _finish(assistant, receiver):
if _log.isEnabledFor(_DEBUG): if _log.isEnabledFor(_DEBUG):
_log.debug("finish %s", assistant) _log.debug("finish %s", assistant)
assistant.destroy() assistant.destroy()
receiver.status.new_device = None receiver.status.new_device = None
if receiver.status.lock_open: if receiver.status.lock_open:
receiver.set_lock() receiver.set_lock()
else: else:
receiver.status[_K.ERROR] = None receiver.status[_K.ERROR] = None
def _pairing_failed(assistant, receiver, error): def _pairing_failed(assistant, receiver, error):
if _log.isEnabledFor(_DEBUG): if _log.isEnabledFor(_DEBUG):
_log.debug("%s fail: %s", receiver, error) _log.debug("%s fail: %s", receiver, error)
assistant.commit() assistant.commit()
header = _("Pairing failed") + ': ' + _(str(error)) + '.' header = _("Pairing failed") + ': ' + _(str(error)) + '.'
if 'timeout' in str(error): if 'timeout' in str(error):
text = _("Make sure your device is within range, and has a decent battery charge.") text = _(
elif str(error) == 'device not supported': "Make sure your device is within range, and has a decent battery charge."
text = _("A new device was detected, but it is not compatible with this receiver.") )
elif 'many' in str(error): elif str(error) == 'device not supported':
text = _("The receiver only supports %d paired device(s).") text = _(
else: "A new device was detected, but it is not compatible with this receiver."
text = _("No further details are available about the error.") )
_create_page(assistant, Gtk.AssistantPageType.SUMMARY, header, 'dialog-error', text) 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.next_page()
assistant.commit() assistant.commit()
def _pairing_succeeded(assistant, receiver, device): def _pairing_succeeded(assistant, receiver, device):
assert device assert device
if _log.isEnabledFor(_DEBUG): if _log.isEnabledFor(_DEBUG):
_log.debug("%s success: %s", receiver, device) _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 = Gtk.Label(_("Found a new device:"))
header.set_alignment(0.5, 0) header.set_alignment(0.5, 0)
page.pack_start(header, False, False, 0) page.pack_start(header, False, False, 0)
device_icon = Gtk.Image() device_icon = Gtk.Image()
icon_set = _icons.device_icon_set(device.name, device.kind) icon_set = _icons.device_icon_set(device.name, device.kind)
device_icon.set_from_icon_set(icon_set, Gtk.IconSize.LARGE) device_icon.set_from_icon_set(icon_set, Gtk.IconSize.LARGE)
device_icon.set_alignment(0.5, 1) device_icon.set_alignment(0.5, 1)
page.pack_start(device_icon, True, True, 0) page.pack_start(device_icon, True, True, 0)
device_label = Gtk.Label() device_label = Gtk.Label()
device_label.set_markup('<b>%s</b>' % device.name) device_label.set_markup('<b>%s</b>' % device.name)
device_label.set_alignment(0.5, 0) device_label.set_alignment(0.5, 0)
page.pack_start(device_label, True, True, 0) page.pack_start(device_label, True, True, 0)
hbox = Gtk.HBox(False, 8) hbox = Gtk.HBox(False, 8)
hbox.pack_start(Gtk.Label(' '), False, False, 0) hbox.pack_start(Gtk.Label(' '), False, False, 0)
hbox.set_property('expand', False) hbox.set_property('expand', False)
hbox.set_property('halign', Gtk.Align.CENTER) hbox.set_property('halign', Gtk.Align.CENTER)
page.pack_start(hbox, False, False, 0) page.pack_start(hbox, False, False, 0)
def _check_encrypted(dev): def _check_encrypted(dev):
if assistant.is_drawable(): if assistant.is_drawable():
if device.status.get(_K.LINK_ENCRYPTED) == False: 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(
hbox.pack_start(Gtk.Label(_("The wireless link is not encrypted") + '!'), False, False, 0) Gtk.Image.new_from_icon_name('security-low',
hbox.show_all() Gtk.IconSize.MENU), False,
else: False, 0)
return True hbox.pack_start(
GLib.timeout_add(_STATUS_CHECK, _check_encrypted, device) 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() page.show_all()
assistant.commit()
assistant.next_page()
assistant.commit()
def create(receiver): def create(receiver):
assert receiver is not None assert receiver is not None
assert receiver.kind is None assert receiver.kind is None
assistant = Gtk.Assistant() assistant = Gtk.Assistant()
assistant.set_title(_('%(receiver_name)s: pair new device') % { 'receiver_name': receiver.name }) assistant.set_title(
assistant.set_icon_name('list-add') _('%(receiver_name)s: pair new device') %
{'receiver_name': receiver.name})
assistant.set_icon_name('list-add')
assistant.set_size_request(400, 240) assistant.set_size_request(400, 240)
assistant.set_resizable(False) assistant.set_resizable(False)
assistant.set_role('pair-device') assistant.set_role('pair-device')
page_text = _("If the device is already turned on, turn if off and on again.") page_text = _(
if receiver.remaining_pairings() and receiver.remaining_pairings() >= 0: "If the device is already turned on, turn if off and on again.")
page_text += _("\n\nThis receiver has %d pairing(s) remaining.")%receiver.remaining_pairings() if receiver.remaining_pairings() and receiver.remaining_pairings() >= 0:
page_text += _("\nCancelling at this point will not use up a pairing.") 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, page_intro = _create_page(assistant, Gtk.AssistantPageType.PROGRESS,
_("Turn on the device you want to pair."), 'preferences-desktop-peripherals', _("Turn on the device you want to pair."),
page_text) 'preferences-desktop-peripherals', page_text)
spinner = Gtk.Spinner() spinner = Gtk.Spinner()
spinner.set_visible(True) spinner.set_visible(True)
page_intro.pack_end(spinner, True, True, 24) page_intro.pack_end(spinner, True, True, 24)
assistant.connect('prepare', _prepare, receiver) assistant.connect('prepare', _prepare, receiver)
assistant.connect('cancel', _finish, receiver) assistant.connect('cancel', _finish, receiver)
assistant.connect('close', _finish, receiver) assistant.connect('close', _finish, receiver)
return assistant return assistant

View File

@ -29,7 +29,6 @@ from time import time as _timestamp
from gi.repository import Gtk, GLib from gi.repository import Gtk, GLib
from gi.repository.Gdk import ScrollDirection from gi.repository.Gdk import ScrollDirection
from solaar import NAME from solaar import NAME
from solaar.i18n import _ from solaar.i18n import _
from logitech_receiver.status import KEYS as _K from logitech_receiver.status import KEYS as _K
@ -40,7 +39,7 @@ from .window import popup as _window_popup, toggle as _window_toggle
# constants # constants
# #
_TRAY_ICON_SIZE = 32 # pixels _TRAY_ICON_SIZE = 32 # pixels
_MENU_ICON_SIZE = Gtk.IconSize.LARGE_TOOLBAR _MENU_ICON_SIZE = Gtk.IconSize.LARGE_TOOLBAR
_RECEIVER_SEPARATOR = ('~', None, None, None) _RECEIVER_SEPARATOR = ('~', None, None, None)
@ -48,418 +47,429 @@ _RECEIVER_SEPARATOR = ('~', None, None, None)
# #
# #
def _create_menu(quit_handler): 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 = Gtk.MenuItem.new_with_label(_("No Logitech receiver found"))
no_receiver.set_sensitive(False) no_receiver.set_sensitive(False)
menu.append(no_receiver) menu.append(no_receiver)
menu.append(Gtk.SeparatorMenuItem.new()) menu.append(Gtk.SeparatorMenuItem.new())
from .action import about, make from .action import about, make
menu.append(about.create_menu_item()) menu.append(about.create_menu_item())
menu.append(make('application-exit', _("Quit"), quit_handler, stock_id=Gtk.STOCK_QUIT).create_menu_item()) menu.append(
del about, make 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 _last_scroll = 0
def _scroll(tray_icon, event, direction=None): def _scroll(tray_icon, event, direction=None):
if direction is None: if direction is None:
direction = event.direction direction = event.direction
now = event.time / 1000.0 now = event.time / 1000.0
else: else:
now = None now = None
if direction != ScrollDirection.UP and direction != ScrollDirection.DOWN: if direction != ScrollDirection.UP and direction != ScrollDirection.DOWN:
# ignore all other directions # ignore all other directions
return return
if len(_devices_info) < 4: if len(_devices_info) < 4:
# don't bother with scrolling when there's only one receiver # don't bother with scrolling when there's only one receiver
# with only one device (3 = [receiver, device, separator]) # with only one device (3 = [receiver, device, separator])
return return
# scroll events come way too fast (at least 5-6 at once) # scroll events come way too fast (at least 5-6 at once)
# so take a little break between them # so take a little break between them
global _last_scroll global _last_scroll
now = now or _timestamp() now = now or _timestamp()
if now - _last_scroll < 0.33: # seconds if now - _last_scroll < 0.33: # seconds
return return
_last_scroll = now _last_scroll = now
# if _log.isEnabledFor(_DEBUG): # if _log.isEnabledFor(_DEBUG):
# _log.debug("scroll direction %s", direction) # _log.debug("scroll direction %s", direction)
global _picked_device global _picked_device
candidate = None candidate = None
if _picked_device is None: if _picked_device is None:
for info in _devices_info: for info in _devices_info:
# pick first peripheral found # pick first peripheral found
if info[1] is not None: if info[1] is not None:
candidate = info candidate = info
break break
else: else:
found = False found = False
for info in _devices_info: for info in _devices_info:
if not info[1]: if not info[1]:
# only conside peripherals # only conside peripherals
continue continue
# compare peripherals # compare peripherals
if info[0:2] == _picked_device[0:2]: if info[0:2] == _picked_device[0:2]:
if direction == ScrollDirection.UP and candidate: if direction == ScrollDirection.UP and candidate:
# select previous device # select previous device
break break
found = True found = True
else: else:
if found: if found:
candidate = info candidate = info
if direction == ScrollDirection.DOWN: if direction == ScrollDirection.DOWN:
break break
# if direction is up, but no candidate found before _picked, # if direction is up, but no candidate found before _picked,
# let it run through all candidates, will get stuck with the last one # let it run through all candidates, will get stuck with the last one
else: else:
if direction == ScrollDirection.DOWN: if direction == ScrollDirection.DOWN:
# only use the first one, in case no candidates are after _picked # only use the first one, in case no candidates are after _picked
if candidate is None: if candidate is None:
candidate = info candidate = info
else: else:
candidate = info candidate = info
# if the last _picked_device is gone, clear it # if the last _picked_device is gone, clear it
# the candidate will be either the first or last one remaining, # the candidate will be either the first or last one remaining,
# depending on the scroll direction # depending on the scroll direction
if not found: if not found:
_picked_device = None _picked_device = None
_picked_device = candidate or _picked_device _picked_device = candidate or _picked_device
if _log.isEnabledFor(_DEBUG): if _log.isEnabledFor(_DEBUG):
_log.debug("scroll: picked %s", _picked_device) _log.debug("scroll: picked %s", _picked_device)
_update_tray_icon() _update_tray_icon()
try: try:
import gi import gi
try: try:
gi.require_version('AyatanaAppIndicator3', '0.1') gi.require_version('AyatanaAppIndicator3', '0.1')
ayatana_appindicator_found = True ayatana_appindicator_found = True
except ValueError: except ValueError:
try: try:
gi.require_version('AppIndicator3', '0.1') gi.require_version('AppIndicator3', '0.1')
ayatana_appindicator_found = False ayatana_appindicator_found = False
except ValueError: except ValueError:
# treat unavailable versions the same as unavailable packages # treat unavailable versions the same as unavailable packages
raise ImportError raise ImportError
if ayatana_appindicator_found: if ayatana_appindicator_found:
from gi.repository import AyatanaAppIndicator3 as AppIndicator3 from gi.repository import AyatanaAppIndicator3 as AppIndicator3
else: else:
from gi.repository import AppIndicator3 from gi.repository import AppIndicator3
if _log.isEnabledFor(_DEBUG): if _log.isEnabledFor(_DEBUG):
_log.debug("using %sAppIndicator3" % ('Ayatana ' if ayatana_appindicator_found else '')) _log.debug("using %sAppIndicator3" %
('Ayatana ' if ayatana_appindicator_found else ''))
# Defense against AppIndicator3 bug that treats files in current directory as icon files # Defense against AppIndicator3 bug that treats files in current directory as icon files
# https://bugs.launchpad.net/ubuntu/+source/libappindicator/+bug/1363277 # https://bugs.launchpad.net/ubuntu/+source/libappindicator/+bug/1363277
def _icon_file(icon_name): def _icon_file(icon_name):
if not os.path.isfile(icon_name): if not os.path.isfile(icon_name):
return icon_name return icon_name
icon_info = Gtk.IconTheme.get_default().lookup_icon(icon_name,_TRAY_ICON_SIZE,0) icon_info = Gtk.IconTheme.get_default().lookup_icon(
return icon_info.get_filename() if icon_info else icon_name icon_name, _TRAY_ICON_SIZE, 0)
return icon_info.get_filename() if icon_info else icon_name
def _create(menu): def _create(menu):
theme_paths = Gtk.IconTheme.get_default().get_search_path() theme_paths = Gtk.IconTheme.get_default().get_search_path()
ind = AppIndicator3.Indicator.new_with_path( ind = AppIndicator3.Indicator.new_with_path(
'indicator-solaar', 'indicator-solaar', _icon_file(_icons.TRAY_INIT),
_icon_file(_icons.TRAY_INIT), AppIndicator3.IndicatorCategory.HARDWARE, ':'.join(theme_paths))
AppIndicator3.IndicatorCategory.HARDWARE, ind.set_title(NAME)
':'.join(theme_paths)) ind.set_status(AppIndicator3.IndicatorStatus.ACTIVE)
ind.set_title(NAME) ind.set_attention_icon_full(_icon_file(_icons.TRAY_ATTENTION), '')
ind.set_status(AppIndicator3.IndicatorStatus.ACTIVE) # ind.set_label(NAME, NAME)
ind.set_attention_icon_full(_icon_file(_icons.TRAY_ATTENTION), '')
# ind.set_label(NAME, NAME)
ind.set_menu(menu) ind.set_menu(menu)
ind.connect('scroll-event', _scroll) ind.connect('scroll-event', _scroll)
return ind return ind
def _destroy(indicator):
indicator.set_status(AppIndicator3.IndicatorStatus.PASSIVE)
def _destroy(indicator): def _update_tray_icon():
indicator.set_status(AppIndicator3.IndicatorStatus.PASSIVE) 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(): description_lines = _generate_description_lines()
if _picked_device: description = '\n'.join(description_lines).rstrip('\n')
_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()) # icon_file = _icons.icon_file(icon_name, _TRAY_ICON_SIZE)
else: _icon.set_icon_full(_icon_file(tray_icon_name), description)
# there may be a receiver, but no peripherals
tray_icon_name = _icons.TRAY_OKAY if _devices_info else _icons.TRAY_INIT
description_lines = _generate_description_lines() def _update_menu_icon(image_widget, icon_name):
description = '\n'.join(description_lines).rstrip('\n') 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) def attention(reason=None):
_icon.set_icon_full(_icon_file(tray_icon_name), description) if _icon.get_status() != AppIndicator3.IndicatorStatus.ATTENTION:
_icon.set_attention_icon_full(_icon_file(_icons.TRAY_ATTENTION),
reason or '')
def _update_menu_icon(image_widget, icon_name): _icon.set_status(AppIndicator3.IndicatorStatus.ATTENTION)
image_widget.set_from_icon_name(icon_name, _MENU_ICON_SIZE) GLib.timeout_add(10 * 1000, _icon.set_status,
# icon_file = _icons.icon_file(icon_name, _MENU_ICON_SIZE) AppIndicator3.IndicatorStatus.ACTIVE)
# 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)
except ImportError: except ImportError:
if _log.isEnabledFor(_DEBUG): if _log.isEnabledFor(_DEBUG):
_log.debug("using StatusIcon") _log.debug("using StatusIcon")
def _create(menu): def _create(menu):
icon = Gtk.StatusIcon.new_from_icon_name(_icons.TRAY_INIT) icon = Gtk.StatusIcon.new_from_icon_name(_icons.TRAY_INIT)
icon.set_name(NAME) icon.set_name(NAME)
icon.set_title(NAME) icon.set_title(NAME)
icon.set_tooltip_text(NAME) icon.set_tooltip_text(NAME)
icon.connect('activate', _window_toggle) icon.connect('activate', _window_toggle)
icon.connect('scroll-event', _scroll) icon.connect('scroll-event', _scroll)
icon.connect('popup-menu', icon.connect(
lambda icon, button, time: menu.popup(None, None, icon.position_menu, icon, button, time)) '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): def _update_tray_icon():
icon.set_visible(False) 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(): def _update_menu_icon(image_widget, icon_name):
tooltip_lines = _generate_tooltip_lines() image_widget.set_from_icon_name(icon_name, _MENU_ICON_SIZE)
tooltip = '\n'.join(tooltip_lines).rstrip('\n')
_icon.set_tooltip_markup(tooltip)
if _picked_device: _icon_before_attention = None
_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 _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): if count > 0:
image_widget.set_from_icon_name(icon_name, _MENU_ICON_SIZE) 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(): def _generate_tooltip_lines():
if not _devices_info: if not _devices_info:
yield '<b>%s</b>: ' % NAME + _("no receiver") yield '<b>%s</b>: ' % NAME + _("no receiver")
return return
yield from _generate_description_lines() yield from _generate_description_lines()
def _generate_description_lines(): def _generate_description_lines():
if not _devices_info: if not _devices_info:
yield _("no receiver") yield _("no receiver")
return return
for _ignore, number, name, status in _devices_info: for _ignore, number, name, status in _devices_info:
if number is None: # receiver if number is None: # receiver
continue continue
p = status.to_string() p = status.to_string()
if p: # does it have any properties to print? if p: # does it have any properties to print?
yield '<b>%s</b>' % name yield '<b>%s</b>' % name
if status: if status:
yield '\t%s' % p yield '\t%s' % p
else: else:
yield '\t%s <small>(' % p + _("offline") + ')</small>' yield '\t%s <small>(' % p + _("offline") + ')</small>'
else: else:
if status: if status:
yield '<b>%s</b> <small>(' % name + _("no status") + ')</small>' yield '<b>%s</b> <small>(' % name + _(
else: "no status") + ')</small>'
yield '<b>%s</b> <small>(' % name + _("offline") + ')</small>' else:
yield '' yield '<b>%s</b> <small>(' % name + _("offline") + ')</small>'
yield ''
def _pick_device_with_lowest_battery(): def _pick_device_with_lowest_battery():
if not _devices_info: if not _devices_info:
return None return None
picked = None picked = None
picked_level = 1000 picked_level = 1000
for info in _devices_info: for info in _devices_info:
if info[1] is None: # is receiver/separator if info[1] is None: # is receiver/separator
continue continue
level = info[-1].get(_K.BATTERY_LEVEL) level = info[-1].get(_K.BATTERY_LEVEL)
# print ("checking %s -> %s", info, level) # print ("checking %s -> %s", info, level)
if level is not None and picked_level > level: if level is not None and picked_level > level:
picked = info picked = info
picked_level = level or 0 picked_level = level or 0
if _log.isEnabledFor(_DEBUG): if _log.isEnabledFor(_DEBUG):
_log.debug("picked device with lowest battery: %s", picked) _log.debug("picked device with lowest battery: %s", picked)
return picked return picked
# #
# #
# #
def _add_device(device): def _add_device(device):
assert device assert device
assert device.receiver assert device.receiver
receiver_path = device.receiver.path receiver_path = device.receiver.path
assert receiver_path assert receiver_path
index = None index = None
for idx, (path, _ignore, _ignore, _ignore) in enumerate(_devices_info): for idx, (path, _ignore, _ignore, _ignore) in enumerate(_devices_info):
if path == receiver_path: if path == receiver_path:
# the first entry matching the receiver serial should be for the receiver itself # the first entry matching the receiver serial should be for the receiver itself
index = idx + 1 index = idx + 1
break break
assert index is not None assert index is not None
# proper ordering (according to device.number) for a receiver's devices # proper ordering (according to device.number) for a receiver's devices
while True: while True:
path, number, _ignore, _ignore = _devices_info[index] path, number, _ignore, _ignore = _devices_info[index]
if path == _RECEIVER_SEPARATOR[0]: if path == _RECEIVER_SEPARATOR[0]:
break break
assert path == receiver_path assert path == receiver_path
assert number != device.number assert number != device.number
if number > device.number: if number > device.number:
break break
index = index + 1 index = index + 1
new_device_info = (receiver_path, device.number, device.name, device.status) new_device_info = (receiver_path, device.number, device.name,
assert len(new_device_info) == len(_RECEIVER_SEPARATOR) device.status)
_devices_info.insert(index, new_device_info) 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 = b'\xE2\x94\x84 '.decode('utf-8')
label_prefix = ' ' label_prefix = ' '
new_menu_item = Gtk.ImageMenuItem.new_with_label(label_prefix + device.name) new_menu_item = Gtk.ImageMenuItem.new_with_label(label_prefix +
new_menu_item.set_image(Gtk.Image()) device.name)
new_menu_item.show_all() new_menu_item.set_image(Gtk.Image())
new_menu_item.connect('activate', _window_popup, receiver_path, device.number) new_menu_item.show_all()
_menu.insert(new_menu_item, index) 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): def _remove_device(index):
assert index is not None assert index is not None
menu_items = _menu.get_children() menu_items = _menu.get_children()
_menu.remove(menu_items[index]) _menu.remove(menu_items[index])
removed_device = _devices_info.pop(index) removed_device = _devices_info.pop(index)
global _picked_device global _picked_device
if _picked_device and _picked_device[0:2] == removed_device[0:2]: if _picked_device and _picked_device[0:2] == removed_device[0:2]:
# the current pick was unpaired # the current pick was unpaired
_picked_device = None _picked_device = None
def _add_receiver(receiver): def _add_receiver(receiver):
index = len(_devices_info) index = len(_devices_info)
new_receiver_info = (receiver.path, None, receiver.name, None) new_receiver_info = (receiver.path, None, receiver.name, None)
assert len(new_receiver_info) == len(_RECEIVER_SEPARATOR) assert len(new_receiver_info) == len(_RECEIVER_SEPARATOR)
_devices_info.append(new_receiver_info) _devices_info.append(new_receiver_info)
new_menu_item = Gtk.ImageMenuItem.new_with_label(receiver.name) new_menu_item = Gtk.ImageMenuItem.new_with_label(receiver.name)
_menu.insert(new_menu_item, index) _menu.insert(new_menu_item, index)
icon_set = _icons.device_icon_set(receiver.name) icon_set = _icons.device_icon_set(receiver.name)
new_menu_item.set_image(Gtk.Image().new_from_icon_set(icon_set, _MENU_ICON_SIZE)) new_menu_item.set_image(Gtk.Image().new_from_icon_set(
new_menu_item.show_all() icon_set, _MENU_ICON_SIZE))
new_menu_item.connect('activate', _window_popup, receiver.path) new_menu_item.show_all()
new_menu_item.connect('activate', _window_popup, receiver.path)
_devices_info.append(_RECEIVER_SEPARATOR) _devices_info.append(_RECEIVER_SEPARATOR)
separator = Gtk.SeparatorMenuItem.new() separator = Gtk.SeparatorMenuItem.new()
separator.set_visible(True) separator.set_visible(True)
_menu.insert(separator, index + 1) _menu.insert(separator, index + 1)
return 0 return 0
def _remove_receiver(receiver): def _remove_receiver(receiver):
index = 0 index = 0
found = False found = False
# remove all entries in devices_info that match this receiver # remove all entries in devices_info that match this receiver
while index < len(_devices_info): while index < len(_devices_info):
path, _ignore, _ignore, _ignore = _devices_info[index] path, _ignore, _ignore, _ignore = _devices_info[index]
if path == receiver.path: if path == receiver.path:
found = True found = True
_remove_device(index) _remove_device(index)
elif found and path == _RECEIVER_SEPARATOR[0]: elif found and path == _RECEIVER_SEPARATOR[0]:
# the separator after this receiver # the separator after this receiver
_remove_device(index) _remove_device(index)
break break
else: else:
index += 1 index += 1
def _update_menu_item(index, device): def _update_menu_item(index, device):
assert device assert device
assert device.status is not None assert device.status is not None
menu_items = _menu.get_children() menu_items = _menu.get_children()
menu_item = menu_items[index] menu_item = menu_items[index]
level = device.status.get(_K.BATTERY_LEVEL) level = device.status.get(_K.BATTERY_LEVEL)
charging = device.status.get(_K.BATTERY_CHARGING) charging = device.status.get(_K.BATTERY_CHARGING)
icon_name = _icons.battery(level, charging) icon_name = _icons.battery(level, charging)
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 _menu = None
_icon = None _icon = None
def init(_quit_handler): def init(_quit_handler):
global _menu, _icon global _menu, _icon
assert _menu is None assert _menu is None
_menu = _create_menu(_quit_handler) _menu = _create_menu(_quit_handler)
assert _icon is None assert _icon is None
_icon = _create(_menu) _icon = _create(_menu)
def destroy(): def destroy():
global _icon, _menu, _devices_info global _icon, _menu, _devices_info
if _icon is not None: if _icon is not None:
i, _icon = _icon, None i, _icon = _icon, None
_destroy(i) _destroy(i)
i = None i = None
_icon = None _icon = None
_menu = None _menu = None
_devices_info = None _devices_info = None
def update(device=None): def update(device=None):
if _icon is None: if _icon is None:
return return
if device is not None: if device is not None:
if device.kind is None: if device.kind is None:
# receiver # receiver
is_alive = bool(device) is_alive = bool(device)
receiver_path = device.path receiver_path = device.path
if is_alive: if is_alive:
index = None index = None
for idx, (path, _ignore, _ignore, _ignore) in enumerate(_devices_info): for idx, (path, _ignore, _ignore,
if path == receiver_path: _ignore) in enumerate(_devices_info):
index = idx if path == receiver_path:
break index = idx
break
if index is None: if index is None:
_add_receiver(device) _add_receiver(device)
else: else:
_remove_receiver(device) _remove_receiver(device)
else: else:
# peripheral # peripheral
is_paired = bool(device) is_paired = bool(device)
receiver_path = device.receiver.path receiver_path = device.receiver.path
index = None index = None
for idx, (path, number, _ignore, _ignore) in enumerate(_devices_info): for idx, (path, number, _ignore,
if path == receiver_path and number == device.number: _ignore) in enumerate(_devices_info):
index = idx if path == receiver_path and number == device.number:
index = idx
if is_paired: if is_paired:
if index is None: if index is None:
index = _add_device(device) index = _add_device(device)
_update_menu_item(index, device) _update_menu_item(index, device)
else: else:
# was just unpaired # was just unpaired
if index: if index:
_remove_device(index) _remove_device(index)
menu_items = _menu.get_children() menu_items = _menu.get_children()
no_receivers_index = len(_devices_info) no_receivers_index = len(_devices_info)
menu_items[no_receivers_index].set_visible(not _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[no_receivers_index + 1].set_visible(not _devices_info)
global _picked_device global _picked_device
if (not _picked_device or _last_scroll == 0) and device is not None and device.kind is not None: if (not _picked_device or _last_scroll
# if it's just a receiver update, it's unlikely the picked device would change == 0) and device is not None and device.kind is not None:
_picked_device = _pick_device_with_lowest_battery() # 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()

File diff suppressed because it is too large Load Diff

View File

@ -28,61 +28,74 @@ del getLogger
# #
_suspend_callback = None _suspend_callback = None
def _suspend(): def _suspend():
if _suspend_callback: if _suspend_callback:
if _log.isEnabledFor(_INFO): if _log.isEnabledFor(_INFO):
_log.info("received suspend event") _log.info("received suspend event")
_suspend_callback() _suspend_callback()
_resume_callback = None _resume_callback = None
def _resume(): def _resume():
if _resume_callback: if _resume_callback:
if _log.isEnabledFor(_INFO): if _log.isEnabledFor(_INFO):
_log.info("received resume event") _log.info("received resume event")
_resume_callback() _resume_callback()
def _suspend_or_resume(suspend): 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): 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.""" They are called only if the system DBus is running, and the UPower daemon is available."""
global _resume_callback, _suspend_callback global _resume_callback, _suspend_callback
_suspend_callback = on_suspend_callback _suspend_callback = on_suspend_callback
_resume_callback = on_resume_callback _resume_callback = on_resume_callback
try: try:
import dbus import dbus
_UPOWER_BUS = 'org.freedesktop.UPower' _UPOWER_BUS = 'org.freedesktop.UPower'
_UPOWER_INTERFACE = 'org.freedesktop.UPower' _UPOWER_INTERFACE = 'org.freedesktop.UPower'
_LOGIND_BUS = 'org.freedesktop.login1' _LOGIND_BUS = 'org.freedesktop.login1'
_LOGIND_INTERFACE = 'org.freedesktop.login1.Manager' _LOGIND_INTERFACE = 'org.freedesktop.login1.Manager'
# integration into the main GLib loop # integration into the main GLib loop
from dbus.mainloop.glib import DBusGMainLoop from dbus.mainloop.glib import DBusGMainLoop
DBusGMainLoop(set_as_default=True) DBusGMainLoop(set_as_default=True)
bus = dbus.SystemBus() bus = dbus.SystemBus()
assert bus assert bus
bus.add_signal_receiver(_suspend, signal_name='Sleeping', bus.add_signal_receiver(_suspend,
dbus_interface=_UPOWER_INTERFACE, bus_name=_UPOWER_BUS) signal_name='Sleeping',
dbus_interface=_UPOWER_INTERFACE,
bus_name=_UPOWER_BUS)
bus.add_signal_receiver(_resume, signal_name='Resuming', bus.add_signal_receiver(_resume,
dbus_interface=_UPOWER_INTERFACE, bus_name=_UPOWER_BUS) signal_name='Resuming',
dbus_interface=_UPOWER_INTERFACE,
bus_name=_UPOWER_BUS)
bus.add_signal_receiver(_suspend_or_resume,'PrepareForSleep', bus.add_signal_receiver(_suspend_or_resume,
dbus_interface=_LOGIND_INTERFACE, bus_name=_LOGIND_BUS) 'PrepareForSleep',
dbus_interface=_LOGIND_INTERFACE,
bus_name=_LOGIND_BUS)
if _log.isEnabledFor(_INFO): if _log.isEnabledFor(_INFO):
_log.info("connected to system dbus, watching for suspend/resume events") _log.info(
"connected to system dbus, watching for suspend/resume events")
except: except:
# Either: # Either:
# - the dbus library is not available # - the dbus library is not available
# - the system dbus is not running # - the system dbus is not running
_log.warn("failed to register suspend/resume callbacks") _log.warn("failed to register suspend/resume callbacks")
pass pass

View File

@ -14,56 +14,61 @@ NAME = 'Solaar'
def _data_files(): 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/solaar*.svg')
yield 'share/solaar/icons', _glob('share/solaar/icons/light_*.png') yield 'share/solaar/icons', _glob('share/solaar/icons/light_*.png')
yield 'share/icons/hicolor/scalable/apps', ['share/solaar/icons/solaar.svg'] yield 'share/icons/hicolor/scalable/apps', [
'share/solaar/icons/solaar.svg'
]
for mo in _glob('share/locale/*/LC_MESSAGES/solaar.mo'): for mo in _glob('share/locale/*/LC_MESSAGES/solaar.mo'):
yield _dirname(mo), [mo] yield _dirname(mo), [mo]
yield 'share/applications', ['share/applications/solaar.desktop'] yield 'share/applications', ['share/applications/solaar.desktop']
yield autostart_path, ['share/autostart/solaar.desktop'] yield autostart_path, ['share/autostart/solaar.desktop']
yield '/etc/udev/rules.d', ['rules.d/42-logitech-unify-permissions.rules'] yield '/etc/udev/rules.d', ['rules.d/42-logitech-unify-permissions.rules']
del _dirname del _dirname
setup(name=NAME.lower(), setup(
version=__version__, name=NAME.lower(),
description='Linux devices manager for the Logitech Unifying Receiver.', version=__version__,
long_description=''' description='Linux devices manager for the Logitech Unifying Receiver.',
long_description='''
Solaar is a Linux device manager for Logitech's Unifying Receiver peripherals. 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 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. battery status, and show and modify some of the modifiable features of devices.
'''.strip(), '''.strip(),
author='Daniel Pavel', author='Daniel Pavel',
license='GPLv2', license='GPLv2',
url='http://pwr-solaar.github.io/Solaar/', url='http://pwr-solaar.github.io/Solaar/',
classifiers=[ classifiers=[
'Development Status :: 4 - Beta', 'Development Status :: 4 - Beta',
'Environment :: X11 Applications :: GTK', 'Environment :: X11 Applications :: GTK',
'Environment :: Console', 'Environment :: Console',
'Intended Audience :: End Users/Desktop', 'Intended Audience :: End Users/Desktop',
'License :: DFSG approved', 'License :: DFSG approved',
'License :: OSI Approved :: GNU General Public License v2 (GPLv2)', 'License :: OSI Approved :: GNU General Public License v2 (GPLv2)',
'Natural Language :: English', 'Natural Language :: English',
'Programming Language :: Python :: 3 :: Only', 'Programming Language :: Python :: 3 :: Only',
'Operating System :: POSIX :: Linux', 'Operating System :: POSIX :: Linux',
'Topic :: Utilities', 'Topic :: Utilities',
], ],
platforms=['linux'],
platforms=['linux'], # sudo apt install python-gi python3-gi \
# gir1.2-gtk-3.0 gir1.2-notify-0.7 gir1.2-ayatanaappindicator3-0.1
# sudo apt install python-gi python3-gi \ # os_requires=['gi.repository.GObject (>= 2.0)', 'gi.repository.Gtk (>= 3.0)'],
# gir1.2-gtk-3.0 gir1.2-notify-0.7 gir1.2-ayatanaappindicator3-0.1 python_requires='>=3.2',
# os_requires=['gi.repository.GObject (>= 2.0)', 'gi.repository.Gtk (>= 3.0)'], install_requires=[
'pyudev (>= 0.13)',
python_requires='>=3.2', ],
install_requires=['pyudev (>= 0.13)', ], package_dir={'': 'lib'},
package_dir={'': 'lib'}, packages=[
packages=['hidapi', 'logitech_receiver', 'solaar', 'solaar.ui', 'solaar.cli'], 'hidapi', 'logitech_receiver', 'solaar', 'solaar.ui', 'solaar.cli'
data_files=list(_data_files()), ],
scripts=_glob('bin/*'), data_files=list(_data_files()),
) scripts=_glob('bin/*'),
)

View File

@ -6,17 +6,18 @@ from __future__ import absolute_import
def init_paths(): def init_paths():
"""Make the app work in the source tree.""" """Make the app work in the source tree."""
import sys import sys
import os.path as _path import os.path as _path
src_lib = _path.normpath(_path.join(_path.realpath(sys.path[0]), '..', 'lib')) src_lib = _path.normpath(
init_py = _path.join(src_lib, 'hidapi', '__init__.py') _path.join(_path.realpath(sys.path[0]), '..', 'lib'))
if _path.exists(init_py): init_py = _path.join(src_lib, 'hidapi', '__init__.py')
sys.path[0] = src_lib if _path.exists(init_py):
sys.path[0] = src_lib
if __name__ == '__main__': if __name__ == '__main__':
init_paths() init_paths()
from hidapi import hidconsole from hidapi import hidconsole
hidconsole.main() hidconsole.main()

View File

@ -4,7 +4,7 @@
from __future__ import absolute_import, division, print_function, unicode_literals from __future__ import absolute_import, division, print_function, unicode_literals
import sys import sys
sys.path += (sys.path[0] + '/../lib',) sys.path += (sys.path[0] + '/../lib', )
import hidapi import hidapi
from logitech.unifying_receiver.base import DEVICE_UNIFYING_RECEIVER 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): def print_event(action, device):
print ("~~~~ device [%s] %s" % (action, device)) print("~~~~ device [%s] %s" % (action, device))
hidapi.monitor(print_event, hidapi.monitor(print_event, DEVICE_UNIFYING_RECEIVER,
DEVICE_UNIFYING_RECEIVER, DEVICE_UNIFYING_RECEIVER_2, DEVICE_NANO_RECEIVER)
DEVICE_UNIFYING_RECEIVER_2,
DEVICE_NANO_RECEIVER
)