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():
"""Make the app work in the source tree."""
import sys
import os.path as _path
"""Make the app work in the source tree."""
import sys
import os.path as _path
# Python 2 need conversion from utf-8 filenames
# Python 3 might have problems converting back to UTF-8 in case of Unicode surrogates
try:
if sys.version_info < (3,):
decoded_path = sys.path[0].decode(sys.getfilesystemencoding())
else:
decoded_path = sys.path[0]
sys.path[0].encode(sys.getfilesystemencoding())
# Python 2 need conversion from utf-8 filenames
# Python 3 might have problems converting back to UTF-8 in case of Unicode surrogates
try:
if sys.version_info < (3, ):
decoded_path = sys.path[0].decode(sys.getfilesystemencoding())
else:
decoded_path = sys.path[0]
sys.path[0].encode(sys.getfilesystemencoding())
except UnicodeError:
sys.stderr.write('ERROR: Solaar cannot recognize encoding of filesystem path, this may happen because non UTF-8 characters in the pathname.\n')
sys.exit(1)
except UnicodeError:
sys.stderr.write(
'ERROR: Solaar cannot recognize encoding of filesystem path, this may happen because non UTF-8 characters in the pathname.\n'
)
sys.exit(1)
prefix = _path.normpath(_path.join(_path.realpath(decoded_path), '..'))
src_lib = _path.join(prefix, 'lib')
share_lib = _path.join(prefix, 'share', 'solaar', 'lib')
for location in src_lib, share_lib:
init_py = _path.join(location, 'solaar', '__init__.py')
# print ("sys.path[0]: checking", init_py)
if _path.exists(init_py):
# print ("sys.path[0]: found", location, "replacing", sys.path[0])
sys.path[0] = location
break
prefix = _path.normpath(_path.join(_path.realpath(decoded_path), '..'))
src_lib = _path.join(prefix, 'lib')
share_lib = _path.join(prefix, 'share', 'solaar', 'lib')
for location in src_lib, share_lib:
init_py = _path.join(location, 'solaar', '__init__.py')
# print ("sys.path[0]: checking", init_py)
if _path.exists(init_py):
# print ("sys.path[0]: found", location, "replacing", sys.path[0])
sys.path[0] = location
break
if __name__ == '__main__':
init_paths()
import solaar.gtk
solaar.gtk.main()
init_paths()
import solaar.gtk
solaar.gtk.main()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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
try:
unicode
_ = lambda x: _gettext.gettext(x).decode('UTF-8')
ngettext = lambda *x: _gettext.ngettext(*x).decode('UTF-8')
unicode
_ = lambda x: _gettext.gettext(x).decode('UTF-8')
ngettext = lambda *x: _gettext.ngettext(*x).decode('UTF-8')
except:
_ = _gettext.gettext
ngettext = _gettext.ngettext
_ = _gettext.gettext
ngettext = _gettext.ngettext
# A few common strings, not always accessible as such in the code.
_DUMMY = (
# approximative battery levels
_("empty"), _("critical"), _("low"), _("good"), _("full"),
# approximative battery levels
_("empty"),
_("critical"),
_("low"),
_("good"),
_("full"),
# battery charging statuses
_("discharging"), _("recharging"), _("almost full"), _("charged"),
_("slow recharge"), _("invalid battery"), _("thermal error"),
# battery charging statuses
_("discharging"),
_("recharging"),
_("almost full"),
_("charged"),
_("slow recharge"),
_("invalid battery"),
_("thermal error"),
# pairing errors
_("device timeout"), _("device not supported"), _("too many devices"), _("sequence timeout"),
# pairing errors
_("device timeout"),
_("device not supported"),
_("too many devices"),
_("sequence timeout"),
# firmware kinds
_("Firmware"), _("Bootloader"), _("Hardware"), _("Other"),
)
# firmware kinds
_("Firmware"),
_("Bootloader"),
_("Hardware"),
_("Other"),
)

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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