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

@ -29,14 +29,16 @@ def init_paths():
# 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,):
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.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), '..'))

View File

@ -37,7 +37,9 @@ def init_paths():
if __name__ == '__main__':
print ('WARNING: solaar-cli is deprecated; use solaar with the usual arguments')
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
@ -34,4 +33,4 @@ from hidapi.udev import (
get_manufacturer,
get_product,
get_serial,
)
)

View File

@ -63,13 +63,15 @@ 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))
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
@ -126,17 +128,23 @@ def _validate_input(line, hidpp=False):
_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")
_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")
_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")
_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]
@ -156,39 +164,46 @@ def _open(args):
if not device:
sys.exit("!! Device path required.")
print (".. Opening device", device)
print(".. Opening device", device)
handle = _hid.open_path(device)
if not handle:
sys.exit("!! Failed to open %s, aborting." % device)
print (".. Opened handle %r, vendor %r product %r serial %r." % (
handle,
_hid.get_manufacturer(handle),
_hid.get_product(handle),
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.")
print(".. HID++ validation enabled.")
else:
if (_hid.get_manufacturer(handle) == b'Logitech' and
b'Receiver' in _hid.get_product(handle)):
if (_hid.get_manufacturer(handle) == b'Logitech'
and b'Receiver' in _hid.get_product(handle)):
args.hidpp = True
print (".. Logitech receiver detected, HID++ validation enabled.")
print(".. Logitech receiver detected, HID++ validation enabled.")
return handle
#
#
#
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")
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()
@ -197,12 +212,15 @@ def main():
handle = _open(args)
if interactive:
print (".. Press ^C/^D to exit, or type hex bytes to write to the device.")
print(
".. Press ^C/^D to exit, or type hex bytes to write to the device."
)
import readline
if args.history is None:
import os.path
args.history = os.path.join(os.path.expanduser('~'), '.hidconsole-history')
args.history = os.path.join(os.path.expanduser('~'),
'.hidconsole-history')
try:
readline.read_history_file(args.history)
except:
@ -211,13 +229,14 @@ def main():
try:
from threading import Thread
t = Thread(target=_continuous_read, args=(handle,))
t = Thread(target=_continuous_read, args=(handle, ))
t.daemon = True
t.start()
if interactive:
# move the cursor at the bottom of the screen
sys.stdout.write('\033[300B') # move cusor at most 300 lines down, don't scroll
sys.stdout.write(
'\033[300B') # move cusor at most 300 lines down, don't scroll
while t.is_alive():
line = read_packet(prompt)
@ -243,12 +262,12 @@ def main():
time.sleep(0.700)
except EOFError:
if interactive:
print ("")
print("")
else:
time.sleep(1)
finally:
print (".. Closing handle %r" % handle)
print(".. Closing handle %r" % handle)
_hid.close(handle)
if interactive:
readline.write_history_file(args.history)

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,10 +34,8 @@ 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', [
@ -51,15 +48,15 @@ DeviceInfo = namedtuple('DeviceInfo', [
'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
implementation.
@ -80,10 +77,10 @@ def exit():
# 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)
@ -114,9 +111,11 @@ def _match(action, device, filter):
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')
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')
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
@ -183,15 +182,18 @@ def monitor_glib(callback, *device_filters):
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)
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)
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)
GLib.io_add_watch(m, GLib.IO_IN, _process_udev_event, callback,
device_filters)
# print ("did io_add_watch")
m.start()
@ -272,7 +274,7 @@ def write(device_handle, data):
assert isinstance(data, bytes), (repr(data), type(data))
retrycount = 0
bytes_written = 0
while(retrycount < 3):
while (retrycount < 3):
try:
bytes_written = _os.write(device_handle, data)
retrycount += 1
@ -282,7 +284,9 @@ def write(device_handle, data):
else:
break
if bytes_written != len(data):
raise IOError(_errno.EIO, 'written %d bytes out of expected %d' % (bytes_written, len(data)))
raise IOError(
_errno.EIO,
'written %d bytes out of expected %d' % (bytes_written, len(data)))
def read(device_handle, bytes_count, timeout_ms=-1):
@ -303,11 +307,13 @@ def read(device_handle, bytes_count, timeout_ms=-1):
"""
assert device_handle
timeout = None if timeout_ms < 0 else timeout_ms / 1000.0
rlist, wlist, xlist = _select([device_handle], [], [device_handle], timeout)
rlist, wlist, xlist = _select([device_handle], [], [device_handle],
timeout)
if xlist:
assert xlist == [device_handle]
raise IOError(_errno.EIO, 'exception on file descriptor %d' % device_handle)
raise IOError(_errno.EIO,
'exception on file descriptor %d' % device_handle)
if rlist:
assert rlist == [device_handle]

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
@ -51,8 +50,6 @@ report_lengths = {
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,6 +63,7 @@ _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
receiver is no longer available. Should only happen if the receiver is
@ -83,12 +81,14 @@ class DeviceUnreachable(_KwException):
"""Raised when a request is made to an unreachable (turned off) device."""
pass
#
#
#
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:
@ -100,10 +100,12 @@ 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)
#
#
#
def open_path(path):
"""Checks if the given Linux device path points to the right UR device.
@ -170,12 +172,14 @@ def write(handle, 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:]))
_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)
_log.error("write failed, assuming handle %r no longer available",
handle)
close(handle)
raise NoReceiver(reason=reason)
@ -199,14 +203,15 @@ def read(handle, timeout=DEFAULT_TIMEOUT):
# sanity checks on message report id and size
def check_message(data) :
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)))
_log.warn("unexpected message size: report_id %02X message %s" %
(report_id, _strhex(data)))
return False
@ -224,7 +229,8 @@ def _read(handle, timeout):
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)
_log.error("read failed, assuming handle %r no longer available",
handle)
close(handle)
raise NoReceiver(reason=reason)
@ -233,14 +239,17 @@ def _read(handle, timeout):
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:]))
_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:]
#
#
#
def _skip_incoming(handle, ihandle, notifications_hook):
"""Read anything already in the input buffer.
@ -252,7 +261,8 @@ def _skip_incoming(handle, ihandle, notifications_hook):
# read whatever is already in the buffer, if any
data = _hid.read(ihandle, _MAX_READ_SIZE, 0)
except Exception as reason:
_log.error("read failed, assuming receiver %s no longer available", handle)
_log.error("read failed, assuming receiver %s no longer available",
handle)
close(handle)
raise NoReceiver(reason=reason)
@ -279,28 +289,27 @@ def make_notification(devnumber, data):
# 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):
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
(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
(sub_id in (0x07, 0x0D) and len(data) == 5 and data[4:5] == b'\x00') or
# custom HID++1.0 illumination event, where SubId is 0x17
(sub_id == 0x17 and len(data) == 5)
or
(sub_id == 0x17 and len(data) == 5) or
# HID++ 2.0 feature notifications have the SoftwareID 0
(address & 0x0F == 0x00)
):
(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
del namedtuple
@ -309,6 +318,7 @@ del namedtuple
#
#
def request(handle, devnumber, request_id, *params):
"""Makes a feature call to a device and waits for a matching reply.
@ -339,7 +349,8 @@ def request(handle, devnumber, request_id, *params):
timeout *= 2
if params:
params = b''.join(_pack('B', p) if isinstance(p, int) else p for p in params)
params = b''.join(
_pack('B', p) if isinstance(p, int) else p for p in params)
else:
params = b''
# if _log.isEnabledFor(_DEBUG):
@ -361,7 +372,8 @@ def request(handle, devnumber, request_id, *params):
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]:
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
@ -373,16 +385,24 @@ def request(handle, devnumber, request_id, *params):
# 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])
_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]:
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)
_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:
@ -463,11 +483,13 @@ def ping(handle, devnumber):
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:]:
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]:
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])
@ -478,8 +500,11 @@ def ping(handle, devnumber):
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)
_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)
@ -490,5 +515,6 @@ def ping(handle, devnumber):
delta = _timestamp() - request_started
_log.warn("(%s) timeout (%0.2f/%0.2f) on device %d ping", handle, delta, _PING_TIMEOUT, devnumber)
_log.warn("(%s) timeout (%0.2f/%0.2f) on device %d ping", handle, delta,
_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,59 +31,59 @@ _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',
'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',
'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',
'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)
@ -95,7 +94,7 @@ UNIFYING_RECEIVER_C532 = _unifying_receiver(0xc532)
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_C517 = _nano_receiver_maxn(0xc517, 6)
NANO_RECEIVER_C518 = _nano_receiver(0xc518)
NANO_RECEIVER_C51A = _nano_receiver(0xc51a)
NANO_RECEIVER_C51B = _nano_receiver(0xc51b)
@ -116,7 +115,6 @@ LIGHTSPEED_RECEIVER_C53d = _lightspeed_receiver(0xc53d)
del _DRIVER, _unifying_receiver, _nano_receiver, _lenovo_receiver, _lightspeed_receiver
ALL = (
UNIFYING_RECEIVER_C52B,
UNIFYING_RECEIVER_C532,
@ -137,12 +135,13 @@ ALL = (
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)
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 { }
return {}

View File

@ -46,12 +46,12 @@ except:
#
#
class NamedInt(int):
"""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)
@ -80,6 +80,7 @@ class NamedInt(int):
def __str__(self):
return self.name
__unicode__ = __str__
def __repr__(self):
@ -104,11 +105,15 @@ class NamedInts(object):
def __init__(self, **kwargs):
def _readable_name(n):
if not is_string(n):
raise TypeError("expected (unicode) string, got " + str(type(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()}
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}
@ -121,8 +126,15 @@ class NamedInts(object):
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)}
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):
@ -154,10 +166,13 @@ class NamedInts(object):
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]:
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]:
@ -264,29 +279,15 @@ class KwException(Exception):
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,21 +29,28 @@ 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):
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)
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
# heuristic: the codename is the last word in the device name
@ -54,7 +60,7 @@ def _D(name, codename=None, kind=None, wpid=None, protocol=None, registers=None,
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
_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:
@ -64,18 +70,29 @@ def _D(name, codename=None, kind=None, wpid=None, protocol=None, registers=None,
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)
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)
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 kind in (
_DK.keyboard,
_DK.numpad), '%s has protocol %0.1f, wpid %s' % (
name, protocol, w)
device_descriptor = _DeviceDescriptor(name=name, kind=kind,
wpid=wpid, codename=codename, protocol=protocol,
registers=registers, settings=settings, persister=persister)
device_descriptor = _DeviceDescriptor(name=name,
kind=kind,
wpid=wpid,
codename=codename,
protocol=protocol,
registers=registers,
settings=settings,
persister=persister)
assert codename not in DEVICES, 'duplicate codename in device descriptors: %s' % (DEVICES[codename], )
assert codename not in DEVICES, 'duplicate codename in device descriptors: %s' % (
DEVICES[codename], )
DEVICES[codename] = device_descriptor
if wpid:
@ -83,14 +100,17 @@ def _D(name, codename=None, kind=None, wpid=None, protocol=None, registers=None,
wpid = (wpid, )
for w in wpid:
assert w not in DEVICES, 'duplicate wpid in device descriptors: %s' % (DEVICES[w], )
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,111 +165,163 @@ _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,
_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',
)
_D(
'Wireless Keyboard MK300',
protocol=1.0,
wpid='8521',
registers=(_R.battery_status, ),
)
)
_D('Wireless Keyboard MK320', protocol=1.0, wpid='200F',
_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',
_D(
'Wireless Compact Keyboard K340',
protocol=1.0,
wpid='2007',
registers=(_R.battery_status, ),
)
_D('Wireless Wave Keyboard K350', protocol=1.0, wpid='200A',
)
_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',
)
_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',
)
_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',
)
_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',
_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, ),
)
_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 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',
_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',
_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',
_D('Wireless Mouse M185/M235/M310',
codename='M185/M235/M310',
protocol=4.5,
wpid='4055',
settings=[
_FS.lowres_smooth_scroll(),
_FS.pointer_speed(),
@ -257,61 +329,93 @@ _D('Wireless Mouse M185/M235/M310', codename='M185/M235/M310', protocol=4.5, wpi
_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',
_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',
)
_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',
_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',
_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',
_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',
)
_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',
)
_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',
_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',
_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',
)
_D('Marathon Mouse M705 (M-R0073)',
codename='M705 (M-R0073)',
protocol=4.5,
wpid='406D',
settings=[
_FS.hires_smooth_invert(),
_FS.hires_smooth_resolution(),
@ -320,67 +424,119 @@ _D('Marathon Mouse M705 (M-R0073)', codename='M705 (M-R0073)', protocol=4.5, wpi
_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',
_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',
)
_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, ),
)
_D(
'Performance Mouse MX',
codename='Performance MX',
protocol=1.0,
wpid='101A',
registers=(
_R.battery_status,
_R.three_leds,
),
settings=[
_RS.dpi(choices=_PERFORMANCE_MX_DPIS),
_RS.smooth_scroll(),
_RS.side_scroll(),
],
)
)
_D('Wireless Mouse MX Master', codename='MX Master', protocol=4.5, wpid='4041',
_D(
'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',
_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',
_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, ),
)
_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, ),
)
_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',
)
_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',
)
_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'),
_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',
)
_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',
)
_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',
_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'),
)
_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',
)
_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',
)
_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',
)
_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'),
)
_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'),
)
_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'),
)
_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',
)
_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,16 +33,14 @@ from .hidpp20 import FIRMWARE_KIND, BATTERY_STATUS
# documentation, some of them guessed.
#
DEVICE_KIND = _NamedInts(
keyboard=0x01,
DEVICE_KIND = _NamedInts(keyboard=0x01,
mouse=0x02,
numpad=0x03,
presenter=0x04,
trackball=0x08,
touchpad=0x09)
POWER_SWITCH_LOCATION = _NamedInts(
base=0x01,
POWER_SWITCH_LOCATION = _NamedInts(base=0x01,
top_case=0x02,
edge_of_top_right_corner=0x03,
top_left_corner=0x05,
@ -70,17 +65,18 @@ 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
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
)
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,
ERROR = _NamedInts(invalid_SubID__command=0x01,
invalid_address=0x02,
invalid_value=0x03,
connection_request_failed=0x04,
@ -93,19 +89,12 @@ ERROR = _NamedInts(
unsupported_parameter_value=0x0B,
wrong_pin_code=0x0C)
PAIRING_ERRORS = _NamedInts(
device_timeout=0x01,
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)
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."""
@ -129,21 +118,24 @@ REGISTERS = _NamedInts(
# 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)
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)
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)
@ -154,7 +146,6 @@ def get_battery(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
@ -185,15 +176,15 @@ 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)
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
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
@ -208,7 +199,8 @@ def parse_battery_status(register, reply):
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)
_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:
@ -287,7 +279,7 @@ def set_3leds(device, battery_level=None, charging=None, warning=None):
v2 |= (v2 >> 1)
elif charging:
# blink all green
v1, v2 = 0x30,0x33
v1, v2 = 0x30, 0x33
elif warning:
# 1 red
v1, v2 = 0x02, 0x00
@ -326,5 +318,6 @@ def set_notification_flags(device, *flag_bits):
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))
result = write_register(device, REGISTERS.notifications,
_int2bytes(flag_bits, 3))
return result is not None

View File

@ -25,14 +25,10 @@ from logging import getLogger, DEBUG as _DEBUG
_log = getLogger(__name__)
del getLogger
from .common import (FirmwareInfo as _FirmwareInfo,
ReprogrammableKeyInfo as _ReprogrammableKeyInfo,
ReprogrammableKeyInfoV4 as _ReprogrammableKeyInfoV4,
KwException as _KwException,
NamedInts as _NamedInts,
pack as _pack,
unpack as _unpack)
from .common import (FirmwareInfo as _FirmwareInfo, ReprogrammableKeyInfo as
_ReprogrammableKeyInfo, ReprogrammableKeyInfoV4 as
_ReprogrammableKeyInfoV4, KwException as _KwException,
NamedInts as _NamedInts, pack as _pack, unpack as _unpack)
from . import special_keys
#
@ -156,13 +152,9 @@ FEATURE = _NamedInts(
)
FEATURE._fallback = lambda x: 'unknown:%04X' % x
FEATURE_FLAG = _NamedInts(
internal=0x20,
hidden=0x40,
obsolete=0x80)
FEATURE_FLAG = _NamedInts(internal=0x20, hidden=0x40, obsolete=0x80)
DEVICE_KIND = _NamedInts(
keyboard=0x00,
DEVICE_KIND = _NamedInts(keyboard=0x00,
remote_control=0x01,
numpad=0x02,
mouse=0x03,
@ -171,16 +163,15 @@ DEVICE_KIND = _NamedInts(
presenter=0x06,
receiver=0x07)
FIRMWARE_KIND = _NamedInts(
Firmware=0x00,
FIRMWARE_KIND = _NamedInts(Firmware=0x00,
Bootloader=0x01,
Hardware=0x02,
Other=0x03)
BATTERY_OK = lambda status: status not in (BATTERY_STATUS.invalid_battery, BATTERY_STATUS.thermal_error)
BATTERY_OK = lambda status: status not in (BATTERY_STATUS.invalid_battery,
BATTERY_STATUS.thermal_error)
BATTERY_STATUS = _NamedInts(
discharging=0x00,
BATTERY_STATUS = _NamedInts(discharging=0x00,
recharging=0x01,
almost_full=0x02,
full=0x03,
@ -188,24 +179,16 @@ BATTERY_STATUS = _NamedInts(
invalid_battery=0x05,
thermal_error=0x06)
CHARGE_STATUS = _NamedInts(
charging=0x00,
CHARGE_STATUS = _NamedInts(charging=0x00,
full=0x01,
not_charging=0x02,
error=0x07)
CHARGE_LEVEL = _NamedInts(
average=50,
full=90,
critical=5)
CHARGE_LEVEL = _NamedInts(average=50, full=90, critical=5)
CHARGE_TYPE = _NamedInts(
standard=0x00,
fast=0x01,
slow=0x02)
CHARGE_TYPE = _NamedInts(standard=0x00, fast=0x01, slow=0x02)
ERROR = _NamedInts(
unknown=0x01,
ERROR = _NamedInts(unknown=0x01,
invalid_argument=0x02,
out_of_range=0x03,
hardware_error=0x04,
@ -219,18 +202,22 @@ ERROR = _NamedInts(
#
#
class FeatureNotSupported(_KwException):
"""Raised when trying to request a feature not supported by the device."""
pass
class FeatureCallError(_KwException):
"""Raised if the device replied to a feature call with an error."""
pass
#
#
#
class FeaturesArray(object):
"""A sequence of features supported by a HID++ 2.0 device."""
__slots__ = ('supported', 'device', 'features', 'non_features')
@ -266,7 +253,8 @@ class FeaturesArray(object):
self.device = None
return False
reply = self.device.request(0x0000, _pack('!H', FEATURE.FEATURE_SET))
reply = self.device.request(0x0000, _pack('!H',
FEATURE.FEATURE_SET))
if reply is None:
self.supported = False
else:
@ -274,7 +262,9 @@ class FeaturesArray(object):
if fs_index:
count = self.device.request(fs_index << 8)
if count is None:
_log.warn("FEATURE_SET found, but failed to read features count")
_log.warn(
"FEATURE_SET found, but failed to read features count"
)
# most likely the device is unavailable
return False
else:
@ -298,7 +288,8 @@ class FeaturesArray(object):
raise IndexError(index)
if self.features[index] is None:
feature = self.device.feature_request(FEATURE.FEATURE_SET, 0x10, index)
feature = self.device.feature_request(
FEATURE.FEATURE_SET, 0x10, index)
if feature:
feature, = _unpack('!H', feature[:2])
self.features[index] = FEATURE[feature]
@ -314,7 +305,7 @@ class FeaturesArray(object):
if self._check():
ivalue = int(featureId)
if ivalue in self.non_features:
return False;
return False
may_have = False
for f in self.features:
@ -366,10 +357,12 @@ class FeaturesArray(object):
def __len__(self):
return len(self.features) if self._check() else 0
#
#
#
class KeysArray(object):
"""A sequence of key mappings supported by a HID++ 2.0 device."""
__slots__ = ('device', 'keys', 'keyversion')
@ -387,22 +380,30 @@ class KeysArray(object):
# TODO: add here additional variants for other REPROG_CONTROLS
if self.keys[index] is None:
keydata = feature_request(self.device, FEATURE.REPROG_CONTROLS, 0x10, index)
self.keyversion=1
keydata = feature_request(self.device, FEATURE.REPROG_CONTROLS,
0x10, index)
self.keyversion = 1
if keydata is None:
keydata = feature_request(self.device, FEATURE.REPROG_CONTROLS_V4, 0x10, index)
self.keyversion=4
keydata = feature_request(self.device,
FEATURE.REPROG_CONTROLS_V4, 0x10,
index)
self.keyversion = 4
if keydata:
key, key_task, flags, pos, group, gmask = _unpack('!HHBBBB', keydata[:8])
key, key_task, flags, pos, group, gmask = _unpack(
'!HHBBBB', keydata[:8])
ctrl_id_text = special_keys.CONTROL[key]
ctrl_task_text = special_keys.TASK[key_task]
if self.keyversion == 1:
self.keys[index] = _ReprogrammableKeyInfo(index, ctrl_id_text, ctrl_task_text, flags)
self.keys[index] = _ReprogrammableKeyInfo(
index, ctrl_id_text, ctrl_task_text, flags)
if self.keyversion == 4:
try:
mapped_data = feature_request(self.device, FEATURE.REPROG_CONTROLS_V4, 0x20, key&0xff00, key&0xff)
mapped_data = feature_request(
self.device, FEATURE.REPROG_CONTROLS_V4, 0x20,
key & 0xff00, key & 0xff)
if mapped_data:
remap_key, remap_flag, remapped = _unpack('!HBH', mapped_data[:5])
remap_key, remap_flag, remapped = _unpack(
'!HBH', mapped_data[:5])
# if key not mapped map it to itself for display
if remapped == 0:
remapped = key
@ -412,7 +413,9 @@ class KeysArray(object):
remap_flag = 0
remapped_text = special_keys.CONTROL[remapped]
self.keys[index] = _ReprogrammableKeyInfoV4(index, ctrl_id_text, ctrl_task_text, flags, pos, group, gmask, remapped_text)
self.keys[index] = _ReprogrammableKeyInfoV4(
index, ctrl_id_text, ctrl_task_text, flags, pos,
group, gmask, remapped_text)
return self.keys[index]
@ -438,15 +441,18 @@ class KeysArray(object):
def __len__(self):
return len(self.keys)
#
#
#
def feature_request(device, feature, function=0x00, *params):
if device.online and device.features:
if feature in device.features:
feature_index = device.features.index(int(feature))
return device.request((feature_index << 8) + (function & 0xFF), *params)
return device.request((feature_index << 8) + (function & 0xFF),
*params)
def get_firmware(device):
@ -460,18 +466,23 @@ def get_firmware(device):
fw = []
for index in range(0, count):
fw_info = feature_request(device, FEATURE.DEVICE_FW_VERSION, 0x10, index)
fw_info = feature_request(device, FEATURE.DEVICE_FW_VERSION, 0x10,
index)
if fw_info:
level = ord(fw_info[:1]) & 0x0F
if level == 0 or level == 1:
name, version_major, version_minor, build = _unpack('!3sBBH', fw_info[1:8])
name, version_major, version_minor, build = _unpack(
'!3sBBH', fw_info[1:8])
version = '%02X.%02X' % (version_major, version_minor)
if build:
version += '.B%04X' % build
extras = fw_info[9:].rstrip(b'\x00') or None
fw_info = _FirmwareInfo(FIRMWARE_KIND[level], name.decode('ascii'), version, extras)
fw_info = _FirmwareInfo(FIRMWARE_KIND[level],
name.decode('ascii'), version,
extras)
elif level == FIRMWARE_KIND.Hardware:
fw_info = _FirmwareInfo(FIRMWARE_KIND.Hardware, '', str(ord(fw_info[1:2])), None)
fw_info = _FirmwareInfo(FIRMWARE_KIND.Hardware, '',
str(ord(fw_info[1:2])), None)
else:
fw_info = _FirmwareInfo(FIRMWARE_KIND.Other, '', '', None)
@ -508,11 +519,14 @@ def get_name(device):
name = b''
while len(name) < name_length:
fragment = feature_request(device, FEATURE.DEVICE_NAME, 0x10, len(name))
fragment = feature_request(device, FEATURE.DEVICE_NAME, 0x10,
len(name))
if fragment:
name += fragment[:name_length - len(name)]
else:
_log.error("failed to read whole name of %s (expected %d chars)", device, name_length)
_log.error(
"failed to read whole name of %s (expected %d chars)",
device, name_length)
return None
return name.decode('ascii')
@ -525,8 +539,10 @@ def get_battery(device):
discharge, dischargeNext, status = _unpack('!BBB', battery[:3])
discharge = None if discharge == 0 else discharge
if _log.isEnabledFor(_DEBUG):
_log.debug("device %d battery %d%% charged, next level %d%% charge, status %d = %s",
device.number, discharge, dischargeNext, status, BATTERY_STATUS[status])
_log.debug(
"device %d battery %d%% charged, next level %d%% charge, status %d = %s",
device.number, discharge, dischargeNext, status,
BATTERY_STATUS[status])
return discharge, BATTERY_STATUS[status], dischargeNext
@ -561,8 +577,10 @@ def decipher_voltage(voltage_report):
charge_lvl = CHARGE_LEVEL.critical
if _log.isEnabledFor(_DEBUG):
_log.debug("device %d, battery voltage %d mV, charging = %s, charge status %d = %s, charge level %s, charge type %s",
device.number, voltage, status, (flags & 0x03), charge_sts, charge_lvl, charge_type)
_log.debug(
"device %d, battery voltage %d mV, charging = %s, charge status %d = %s, charge level %s, charge type %s",
device.number, voltage, status, (flags & 0x03), charge_sts,
charge_lvl, charge_type)
return charge_lvl, status, voltage, charge_sts, charge_type
@ -592,15 +610,14 @@ def get_mouse_pointer_info(device):
def get_vertical_scrolling_info(device):
vertical_scrolling_info = feature_request(device, FEATURE.VERTICAL_SCROLLING)
vertical_scrolling_info = feature_request(device,
FEATURE.VERTICAL_SCROLLING)
if vertical_scrolling_info:
roller, ratchet, lines = _unpack('!BBB', vertical_scrolling_info[:3])
roller_type = ('reserved', 'standard', 'reserved', '3G', 'micro', 'normal touch pad', 'inverted touch pad', 'reserved')[roller]
return {
'roller': roller_type,
'ratchet': ratchet,
'lines': lines
}
roller_type = ('reserved', 'standard', 'reserved', '3G', 'micro',
'normal touch pad', 'inverted touch pad',
'reserved')[roller]
return {'roller': roller_type, 'ratchet': ratchet, 'lines': lines}
def get_hi_res_scrolling_info(device):
@ -613,10 +630,11 @@ def get_hi_res_scrolling_info(device):
def get_pointer_speed_info(device):
pointer_speed_info = feature_request(device, FEATURE.POINTER_SPEED)
if pointer_speed_info:
pointer_speed_hi, pointer_speed_lo = _unpack('!BB', pointer_speed_info[:2])
pointer_speed_hi, pointer_speed_lo = _unpack('!BB',
pointer_speed_info[:2])
#if pointer_speed_lo > 0:
# pointer_speed_lo = pointer_speed_lo
return pointer_speed_hi+pointer_speed_lo/256
return pointer_speed_hi + pointer_speed_lo / 256
def get_lowres_wheel_status(device):
@ -632,7 +650,6 @@ def get_hires_wheel(device):
mode = feature_request(device, FEATURE.HIRES_WHEEL, 0x10)
ratchet = feature_request(device, FEATURE.HIRES_WHEEL, 0x030)
if caps and mode and ratchet:
# Parse caps
multi, flags = _unpack('!BB', caps[:2])
@ -654,6 +671,7 @@ def get_hires_wheel(device):
return multi, has_invert, has_ratchet, inv, res, target, ratchet
def get_new_fn_inversion(device):
state = feature_request(device, FEATURE.NEW_FN_INVERSION, 0x00)

View File

@ -23,7 +23,6 @@ from __future__ import absolute_import, division, print_function, unicode_litera
import gettext as _gettext
try:
unicode
_ = lambda x: _gettext.gettext(x).decode('UTF-8')
@ -32,21 +31,34 @@ except:
_ = _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"),
_("empty"),
_("critical"),
_("low"),
_("good"),
_("full"),
# battery charging statuses
_("discharging"), _("recharging"), _("almost full"), _("charged"),
_("slow recharge"), _("invalid battery"), _("thermal error"),
_("discharging"),
_("recharging"),
_("almost full"),
_("charged"),
_("slow recharge"),
_("invalid battery"),
_("thermal error"),
# pairing errors
_("device timeout"), _("device not supported"), _("too many devices"), _("sequence timeout"),
_("device timeout"),
_("device not supported"),
_("too many devices"),
_("sequence timeout"),
# firmware kinds
_("Firmware"), _("Bootloader"), _("Hardware"), _("Other"),
)
_("Firmware"),
_("Bootloader"),
_("Hardware"),
_("Other"),
)

View File

@ -32,13 +32,13 @@ 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.
@ -97,11 +97,13 @@ class _ThreadedHandle(object):
return self._local.handle
except:
return self._open()
__int__ = __index__
def __str__(self):
if self._local:
return str(int(self))
__unicode__ = __str__
def __repr__(self):
@ -109,8 +111,10 @@ class _ThreadedHandle(object):
def __bool__(self):
return bool(self._local)
__nonzero__ = __bool__
#
#
#
@ -134,7 +138,8 @@ class EventsListener(_threading.Thread):
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])
super(EventsListener, self).__init__(name=self.__class__.__name__ +
':' + receiver.path.split('/')[2])
self.daemon = True
self._active = False
@ -149,7 +154,8 @@ class EventsListener(_threading.Thread):
self._active = True
# replace the handle with a threaded one
self.receiver.handle = _ThreadedHandle(self, self.receiver.path, self.receiver.handle)
self.receiver.handle = _ThreadedHandle(self, self.receiver.path,
self.receiver.handle)
# get the right low-level handle for this thread
ihandle = int(self.receiver.handle)
if _log.isEnabledFor(_INFO):
@ -227,4 +233,5 @@ class EventsListener(_threading.Thread):
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,6 +40,7 @@ _F = _hidpp20.FEATURE
#
#
def process(device, notification):
assert device
assert notification
@ -54,10 +54,12 @@ def process(device, 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
@ -65,7 +67,8 @@ def _process_receiver_notification(receiver, status, n):
# 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"))
reason = (_("pairing lock is open")
if status.lock_open else _("pairing lock is closed"))
if _log.isEnabledFor(_INFO):
_log.info("%s: %s", receiver, reason)
@ -75,7 +78,8 @@ def _process_receiver_notification(receiver, status, n):
pair_error = ord(n.data[:1])
if pair_error:
status[_K.ERROR] = error_string = _hidpp10.PAIRING_ERRORS[pair_error]
status[
_K.ERROR] = error_string = _hidpp10.PAIRING_ERRORS[pair_error]
status.new_device = None
_log.warn("pairing error %d: %s", pair_error, error_string)
@ -84,10 +88,12 @@ def _process_receiver_notification(receiver, status, 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
@ -95,7 +101,7 @@ def _process_device_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 :
if len(n.data) == _DJ_NOTIFICATION_LENGTH:
return _process_dj_notification(device, status, n)
else:
return _process_hidpp10_notification(device, status, n)
@ -113,13 +119,14 @@ def _process_device_notification(device, status, n):
try:
feature = device.features[n.sub_id]
except IndexError:
_log.warn("%s: notification from invalid feature index %02X: %s", device, n.sub_id, n)
_log.warn("%s: notification from invalid feature index %02X: %s",
device, n.sub_id, n)
return False
return _process_feature_notification(device, status, n, feature)
def _process_dj_notification(device, status, n) :
def _process_dj_notification(device, status, n):
if _log.isEnabledFor(_DEBUG):
_log.debug("%s (%s) DJ notification %s", device, device.protocol, n)
@ -145,7 +152,8 @@ def _process_dj_notification(device, status, n) :
def _process_hidpp10_custom_notification(device, status, n):
if _log.isEnabledFor(_DEBUG):
_log.debug("%s (%s) custom notification %s", device, device.protocol, n)
_log.debug("%s (%s) custom notification %s", device, device.protocol,
n)
if n.sub_id in (_R.battery_status, _R.battery_charge):
# message layout: 10 ix <register> <xx> <yy> <zz> <00>
@ -175,29 +183,31 @@ def _process_hidpp10_notification(device, status, n):
device.status = None
if device.number in device.receiver:
del device.receiver[device.number]
status.changed(active=False, alert=_ALERT.ALL, reason=_("unpaired"))
status.changed(active=False,
alert=_ALERT.ALL,
reason=_("unpaired"))
else:
_log.warn("%s: disconnection with unknown type %02X: %s", device, n.address, n)
_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)
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)
assert wpid == device.wpid, "%s wpid mismatch, got %s" % (
device, wpid)
flags = ord(n.data[:1]) & 0xF0
link_encrypted = bool(flags & 0x20)
@ -205,12 +215,16 @@ def _process_hidpp10_notification(device, status, n):
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)
_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)
_log.warn(
"%s: connection notification with unknown protocol %02X: %s",
device.number, n.address, n)
return True
@ -227,7 +241,9 @@ def _process_hidpp10_notification(device, status, n):
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)
status.changed(active=True,
alert=_ALERT.NOTIFICATION,
reason=reason)
else:
_log.warn("%s: unknown %s", device, n)
return True
@ -242,14 +258,17 @@ def _process_feature_notification(device, status, n, feature):
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)
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)
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)
@ -269,7 +288,9 @@ def _process_feature_notification(device, status, n, feature):
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')
status.changed(active=True,
alert=_ALERT.NOTIFICATION,
reason='powered on')
else:
_log.warn("%s: unknown WIRELESS %s", device, n)
else:
@ -300,7 +321,8 @@ def _process_feature_notification(device, status, n, feature):
# trigger a new report chain
reports_count = 15
reports_period = 2 # seconds
device.feature_request(_F.SOLAR_DASHBOARD, 0x00, reports_count, reports_period)
device.feature_request(_F.SOLAR_DASHBOARD, 0x00, reports_count,
reports_period)
else:
_log.warn("%s: unknown SOLAR CHARGE %s", device, n)
else:
@ -316,7 +338,9 @@ def _process_feature_notification(device, status, n, feature):
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)
_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
@ -327,7 +351,8 @@ def _process_feature_notification(device, status, n, feature):
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)
_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):
@ -339,4 +364,5 @@ def _process_feature_notification(device, status, n, feature):
_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,6 +40,7 @@ _R = _hidpp10.REGISTERS
#
#
class PairedDevice(object):
def __init__(self, receiver, number, link_notification=None):
assert receiver
@ -83,13 +83,15 @@ class PairedDevice(object):
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])
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)
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])
@ -99,10 +101,14 @@ class PairedDevice(object):
else:
# unifying protocol not supported, must be a Nano receiver
device_info = self.receiver.read_register(_R.receiver_info, 0x04)
device_info = self.receiver.read_register(
_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")
_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
@ -110,13 +116,15 @@ class PairedDevice(object):
# 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)
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)
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]
@ -132,7 +140,8 @@ class PairedDevice(object):
self._kind = self.descriptor.kind
if self._protocol is not None:
self.features = None if self._protocol < 2.0 else _hidpp20.FeaturesArray(self)
self.features = None if self._protocol < 2.0 else _hidpp20.FeaturesArray(
self)
else:
# may be a 2.0 device; if not, it will fix itself later
self.features = _hidpp20.FeaturesArray(self)
@ -151,7 +160,8 @@ class PairedDevice(object):
@property
def codename(self):
if self._codename is None:
codename = self.receiver.read_register(_R.receiver_info, 0x40 + self.number - 1)
codename = self.receiver.read_register(_R.receiver_info,
0x40 + self.number - 1)
if codename:
codename_length = ord(codename[1:2])
codename = codename[2:2 + codename_length]
@ -172,7 +182,8 @@ class PairedDevice(object):
@property
def kind(self):
if self._kind is None:
pair_info = self.receiver.read_register(_R.receiver_info, 0x20 + self.number - 1)
pair_info = self.receiver.read_register(_R.receiver_info,
0x20 + self.number - 1)
if pair_info:
kind = ord(pair_info[7:8]) & 0x0F
self._kind = _hidpp10.DEVICE_KIND[kind]
@ -193,7 +204,8 @@ class PairedDevice(object):
@property
def serial(self):
if self._serial is None:
serial = self.receiver.read_register(_R.receiver_info, 0x30 + self.number - 1)
serial = self.receiver.read_register(_R.receiver_info,
0x30 + self.number - 1)
if serial:
ps = ord(serial[9:10]) & 0x0F
self._power_switch = _hidpp10.POWER_SWITCH_LOCATION[ps]
@ -211,7 +223,8 @@ class PairedDevice(object):
@property
def power_switch_location(self):
if self._power_switch is None:
ps = self.receiver.read_register(_R.receiver_info, 0x30 + self.number - 1)
ps = self.receiver.read_register(_R.receiver_info,
0x30 + self.number - 1)
if ps is not None:
ps = ord(ps[9:10]) & 0x0F
self._power_switch = _hidpp10.POWER_SWITCH_LOCATION[ps]
@ -222,7 +235,8 @@ class PairedDevice(object):
@property
def polling_rate(self):
if self._polling_rate is None:
pair_info = self.receiver.read_register(_R.receiver_info, 0x20 + self.number - 1)
pair_info = self.receiver.read_register(_R.receiver_info,
0x20 + self.number - 1)
if pair_info:
self._polling_rate = ord(pair_info[2:3])
else:
@ -254,7 +268,8 @@ class PairedDevice(object):
else:
self._settings = []
if not self._feature_settings_checked:
self._feature_settings_checked =_check_feature_settings(self, self._settings)
self._feature_settings_checked = _check_feature_settings(
self, self._settings)
return self._settings
def enable_notifications(self, enable=True):
@ -264,24 +279,28 @@ class PairedDevice(object):
return False
if enable:
set_flag_bits = ( _hidpp10.NOTIFICATION_FLAG.battery_status
set_flag_bits = (_hidpp10.NOTIFICATION_FLAG.battery_status
| _hidpp10.NOTIFICATION_FLAG.keyboard_illumination
| _hidpp10.NOTIFICATION_FLAG.wireless
| _hidpp10.NOTIFICATION_FLAG.software_present )
| _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')
_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))
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)
_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)
return _base.request(self.receiver.handle, self.number, request_id,
*params)
read_register = _hidpp10.read_register
write_register = _hidpp10.write_register
@ -300,6 +319,7 @@ class PairedDevice(object):
def __index__(self):
return self.number
__int__ = __index__
def __eq__(self, other):
@ -314,13 +334,17 @@ class PairedDevice(object):
__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)
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.
@ -342,7 +366,7 @@ class Receiver(object):
# read the serial immediately, so we can find out max_devices
serial_reply = self.read_register(_R.receiver_info, 0x03)
if serial_reply :
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
@ -350,12 +374,14 @@ class Receiver(object):
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.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 = {}
@ -376,12 +402,12 @@ class Receiver(object):
return self._firmware
# how many pairings remain (None for unknown, -1 for unlimited)
def remaining_pairings(self,cache=True):
def remaining_pairings(self, cache=True):
if self._remaining_pairings is None or not cache:
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
self._remaining_pairings = ps - 5 if ps >= 5 else -1
return self._remaining_pairings
def enable_notifications(self, enable=True):
@ -391,31 +417,36 @@ class Receiver(object):
return False
if enable:
set_flag_bits = ( _hidpp10.NOTIFICATION_FLAG.battery_status
set_flag_bits = (_hidpp10.NOTIFICATION_FLAG.battery_status
| _hidpp10.NOTIFICATION_FLAG.wireless
| _hidpp10.NOTIFICATION_FLAG.software_present )
| _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')
_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))
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)
_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)
_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))
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
@ -424,7 +455,8 @@ class Receiver(object):
dev = PairedDevice(self, number, notification)
assert dev.wpid
if _log.isEnabledFor(_INFO):
_log.info("%s: found new device %d (%s)", self, number, dev.wpid)
_log.info("%s: found new device %d (%s)", self, number,
dev.wpid)
self._devices[number] = dev
return dev
except _base.NoSuchDevice:
@ -436,10 +468,12 @@ class Receiver(object):
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)
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')
_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)
@ -534,6 +568,7 @@ class Receiver(object):
def __str__(self):
return self._str
__unicode__ = __repr__ = __str__
__bool__ = __nonzero__ = lambda self: self.handle is not None

View File

@ -31,21 +31,34 @@ from .common import (
NamedInts as _NamedInts,
bytes2int as _bytes2int,
int2bytes as _int2bytes,
)
)
#
#
#
KIND = _NamedInts(toggle=0x01, choice=0x02, range=0x04, map_choice=0x0A, multiple_toggle=0x10)
KIND = _NamedInts(toggle=0x01,
choice=0x02,
range=0x04,
map_choice=0x0A,
multiple_toggle=0x10)
class Setting(object):
"""A setting descriptor.
Needs to be instantiated for each specific device."""
__slots__ = ('name', 'label', 'description', 'kind', 'device_kind', 'feature',
'_rw', '_validator', '_device', '_value')
__slots__ = ('name', 'label', 'description', 'kind', 'device_kind',
'feature', '_rw', '_validator', '_device', '_value')
def __init__(self, name, rw, validator, kind=None, label=None, description=None, device_kind=None, feature=None):
def __init__(self,
name,
rw,
validator,
kind=None,
label=None,
description=None,
device_kind=None,
feature=None):
assert name
self.name = name
self.label = label or name
@ -96,7 +109,8 @@ class Setting(object):
assert hasattr(self, '_device')
if _log.isEnabledFor(_DEBUG):
_log.debug("%s: settings read %r from %s", self.name, self._value, self._device)
_log.debug("%s: settings read %r from %s", self.name, self._value,
self._device)
if self._value is None and self._device.persister:
# We haven't read a value from the device yet,
@ -126,7 +140,8 @@ class Setting(object):
assert value is not None
if _log.isEnabledFor(_DEBUG):
_log.debug("%s: settings write %r to %s", self.name, value, self._device)
_log.debug("%s: settings write %r to %s", self.name, value,
self._device)
if self._device.online:
# Remember the value we're trying to set, even if the write fails.
@ -144,7 +159,8 @@ class Setting(object):
data_bytes = self._validator.prepare_write(value, current_value)
if data_bytes is not None:
if _log.isEnabledFor(_DEBUG):
_log.debug("%s: settings prepare write(%s) => %r", self.name, value, data_bytes)
_log.debug("%s: settings prepare write(%s) => %r",
self.name, value, data_bytes)
reply = self._rw.write(self._device, data_bytes)
if not reply:
@ -158,7 +174,8 @@ class Setting(object):
assert hasattr(self, '_device')
if _log.isEnabledFor(_DEBUG):
_log.debug("%s: apply %s (%s)", self.name, self._value, self._device)
_log.debug("%s: apply %s (%s)", self.name, self._value,
self._device)
value = self.read()
if value is not None:
@ -167,29 +184,34 @@ class Setting(object):
def __str__(self):
if hasattr(self, '_value'):
assert hasattr(self, '_device')
return '<Setting([%s:%s] %s:%s=%s)>' % (self._rw.kind, self._validator.kind, self._device.codename, self.name, self._value)
return '<Setting([%s:%s] %s)>' % (self._rw.kind, self._validator.kind, self.name)
return '<Setting([%s:%s] %s:%s=%s)>' % (
self._rw.kind, self._validator.kind, self._device.codename,
self.name, self._value)
return '<Setting([%s:%s] %s)>' % (self._rw.kind, self._validator.kind,
self.name)
__unicode__ = __repr__ = __str__
class Settings(Setting):
"""A setting descriptor for multiple choices, being a map from keys to values.
Needs to be instantiated for each specific device."""
def read(self, cached=True):
assert hasattr(self, '_value')
assert hasattr(self, '_device')
if _log.isEnabledFor(_DEBUG):
_log.debug("%s: settings read %r from %s", self.name, self._value, self._device)
_log.debug("%s: settings read %r from %s", self.name, self._value,
self._device)
if self._value is None and getattr(self._device,'persister',None):
if self._value is None and getattr(self._device, 'persister', None):
# We haven't read a value from the device yet,
# maybe we have something in the configuration.
self._value = self._device.persister.get(self.name)
if cached and self._value is not None:
if getattr(self._device,'persister',None) and self.name not in self._device.persister:
if getattr(self._device, 'persister',
None) and self.name not in self._device.persister:
# If this is a new device (or a new setting for an old device),
# make sure to save its current value for the next time.
self._device.persister[self.name] = self._value
@ -202,9 +224,11 @@ class Settings(Setting):
if reply:
# keys are ints, because that is what the device uses,
# encoded into strings because JSON requires strings as keys
reply_map[str(int(key))] = self._validator.validate_read(reply,key)
reply_map[str(int(key))] = self._validator.validate_read(
reply, key)
self._value = reply_map
if getattr(self._device,'persister',None) and self.name not in self._device.persister:
if getattr(self._device, 'persister',
None) and self.name not in self._device.persister:
# Don't update the persister if it already has a value,
# otherwise the first read might overwrite the value we wanted.
self._device.persister[self.name] = self._value
@ -216,21 +240,25 @@ class Settings(Setting):
assert key is not None
if _log.isEnabledFor(_DEBUG):
_log.debug("%s: settings read %r key %r from %s", self.name, self._value, key, self._device)
_log.debug("%s: settings read %r key %r from %s", self.name,
self._value, key, self._device)
if self._value is None and getattr(self._device,'persister',None):
if self._value is None and getattr(self._device, 'persister', None):
self._value = self._device.persister.get(self.name)
if cached and self._value is not None:
if getattr(self._device,'persister',None) and self.name not in self._device.persister:
if getattr(self._device, 'persister',
None) and self.name not in self._device.persister:
self._device.persister[self.name] = self._value
return self._value[str(int(key))]
if self._device.online:
reply = self._rw.read(self._device, key)
if reply:
self._value[str(int(key))] = self._validator.validate_read(reply,key)
if getattr(self._device,'persister',None) and self.name not in self._device.persister:
self._value[str(int(key))] = self._validator.validate_read(
reply, key)
if getattr(self._device, 'persister',
None) and self.name not in self._device.persister:
self._device.persister[self.name] = self._value
return self._value[str(int(key))]
@ -240,7 +268,8 @@ class Settings(Setting):
assert map is not None
if _log.isEnabledFor(_DEBUG):
_log.debug("%s: settings write %r to %s", self.name, map, self._device)
_log.debug("%s: settings write %r to %s", self.name, map,
self._device)
if self._device.online:
# Remember the value we're trying to set, even if the write fails.
@ -254,7 +283,9 @@ class Settings(Setting):
data_bytes = self._validator.prepare_write(int(key), value)
if data_bytes is not None:
if _log.isEnabledFor(_DEBUG):
_log.debug("%s: settings prepare map write(%s,%s) => %r", self.name, key, value, data_bytes)
_log.debug(
"%s: settings prepare map write(%s,%s) => %r",
self.name, key, value, data_bytes)
reply = self._rw.write(self._device, int(key), data_bytes)
if not reply:
return None
@ -268,7 +299,8 @@ class Settings(Setting):
assert value is not None
if _log.isEnabledFor(_DEBUG):
_log.debug("%s: settings write key %r value %r to %s", self.name, key, value, self._device)
_log.debug("%s: settings write key %r value %r to %s", self.name,
key, value, self._device)
if self._device.online:
# Remember the value we're trying to set, even if the write fails.
@ -281,7 +313,9 @@ class Settings(Setting):
data_bytes = self._validator.prepare_write(int(key), value)
if data_bytes is not None:
if _log.isEnabledFor(_DEBUG):
_log.debug("%s: settings prepare key value write(%s,%s) => %r", self.name, key, value, data_bytes)
_log.debug(
"%s: settings prepare key value write(%s,%s) => %r",
self.name, key, value, data_bytes)
reply = self._rw.write(self._device, int(key), data_bytes)
if not reply:
# tell whomever is calling that the write failed
@ -293,21 +327,22 @@ class Settings(Setting):
class BitFieldSetting(Setting):
"""A setting descriptor for a set of choices represented by one bit each, being a map from options to booleans.
Needs to be instantiated for each specific device."""
def read(self, cached=True):
assert hasattr(self, '_value')
assert hasattr(self, '_device')
if _log.isEnabledFor(_DEBUG):
_log.debug("%s: settings read %r from %s", self.name, self._value, self._device)
_log.debug("%s: settings read %r from %s", self.name, self._value,
self._device)
if self._value is None and getattr(self._device,'persister',None):
if self._value is None and getattr(self._device, 'persister', None):
# We haven't read a value from the device yet,
# maybe we have something in the configuration.
self._value = self._device.persister.get(self.name)
if cached and self._value is not None:
if getattr(self._device,'persister',None) and self.name not in self._device.persister:
if getattr(self._device, 'persister',
None) and self.name not in self._device.persister:
# If this is a new device (or a new setting for an old device),
# make sure to save its current value for the next time.
self._device.persister[self.name] = self._value
@ -321,7 +356,8 @@ class BitFieldSetting(Setting):
# encoded into strings because JSON requires strings as keys
reply_map = self._validator.validate_read(reply)
self._value = reply_map
if getattr(self._device,'persister',None) and self.name not in self._device.persister:
if getattr(self._device, 'persister',
None) and self.name not in self._device.persister:
# Don't update the persister if it already has a value,
# otherwise the first read might overwrite the value we wanted.
self._device.persister[self.name] = self._value
@ -333,13 +369,15 @@ class BitFieldSetting(Setting):
assert key is not None
if _log.isEnabledFor(_DEBUG):
_log.debug("%s: settings read %r key %r from %s", self.name, self._value, key, self._device)
_log.debug("%s: settings read %r key %r from %s", self.name,
self._value, key, self._device)
if self._value is None and getattr(self._device,'persister',None):
if self._value is None and getattr(self._device, 'persister', None):
self._value = self._device.persister.get(self.name)
if cached and self._value is not None:
if getattr(self._device,'persister',None) and self.name not in self._device.persister:
if getattr(self._device, 'persister',
None) and self.name not in self._device.persister:
self._device.persister[self.name] = self._value
return self._value[str(int(key))]
@ -347,7 +385,8 @@ class BitFieldSetting(Setting):
reply = self._rw.read(self._device, key)
if reply:
self._value = self._validator.validate_read(reply)
if getattr(self._device,'persister',None) and self.name not in self._device.persister:
if getattr(self._device, 'persister',
None) and self.name not in self._device.persister:
self._device.persister[self.name] = self._value
return self._value[str(int(key))]
@ -357,7 +396,8 @@ class BitFieldSetting(Setting):
assert map is not None
if _log.isEnabledFor(_DEBUG):
_log.debug("%s: settings write %r to %s", self.name, map, self._device)
_log.debug("%s: settings write %r to %s", self.name, map,
self._device)
if self._device.online:
# Remember the value we're trying to set, even if the write fails.
@ -369,7 +409,8 @@ class BitFieldSetting(Setting):
data_bytes = self._validator.prepare_write(self._value)
if data_bytes is not None:
if _log.isEnabledFor(_DEBUG):
_log.debug("%s: settings prepare map write(%s) => %r", self.name, self._value, data_bytes)
_log.debug("%s: settings prepare map write(%s) => %r",
self.name, self._value, data_bytes)
reply = self._rw.write(self._device, data_bytes)
if not reply:
return None
@ -382,7 +423,8 @@ class BitFieldSetting(Setting):
assert value is not None
if _log.isEnabledFor(_DEBUG):
_log.debug("%s: settings write key %r value %r to %s", self.name, key, value, self._device)
_log.debug("%s: settings write key %r value %r to %s", self.name,
key, value, self._device)
if self._device.online:
# Remember the value we're trying to set, even if the write fails.
@ -396,7 +438,9 @@ class BitFieldSetting(Setting):
data_bytes = self._validator.prepare_write(self._value)
if data_bytes is not None:
if _log.isEnabledFor(_DEBUG):
_log.debug("%s: settings prepare key value write(%s,%s) => %r", self.name, key, str(value), data_bytes)
_log.debug(
"%s: settings prepare key value write(%s,%s) => %r",
self.name, key, str(value), data_bytes)
reply = self._rw.write(self._device, data_bytes)
if not reply:
# tell whomever is calling that the write failed
@ -404,10 +448,12 @@ class BitFieldSetting(Setting):
return value
#
# read/write low-level operators
#
class RegisterRW(object):
__slots__ = ('register', )
@ -431,7 +477,10 @@ class FeatureRW(object):
default_read_fnid = 0x00
default_write_fnid = 0x10
def __init__(self, feature, read_fnid=default_read_fnid, write_fnid=default_write_fnid):
def __init__(self,
feature,
read_fnid=default_read_fnid,
write_fnid=default_write_fnid):
assert isinstance(feature, _NamedInt)
self.feature = feature
self.read_fnid = read_fnid
@ -443,7 +492,8 @@ class FeatureRW(object):
def write(self, device, data_bytes):
assert self.feature is not None
return device.feature_request(self.feature, self.write_fnid, data_bytes)
return device.feature_request(self.feature, self.write_fnid,
data_bytes)
class FeatureRWMap(FeatureRW):
@ -452,7 +502,11 @@ class FeatureRWMap(FeatureRW):
default_write_fnid = 0x10
default_key_bytes = 1
def __init__(self, feature, read_fnid=default_read_fnid, write_fnid=default_write_fnid, key_bytes=default_key_bytes):
def __init__(self,
feature,
read_fnid=default_read_fnid,
write_fnid=default_write_fnid,
key_bytes=default_key_bytes):
assert isinstance(feature, _NamedInt)
self.feature = feature
self.read_fnid = read_fnid
@ -467,7 +521,8 @@ class FeatureRWMap(FeatureRW):
def write(self, device, key, data_bytes):
assert self.feature is not None
key_bytes = _int2bytes(key, self.key_bytes)
return device.feature_request(self.feature, self.write_fnid, key_bytes, data_bytes)
return device.feature_request(self.feature, self.write_fnid, key_bytes,
data_bytes)
#
@ -475,6 +530,7 @@ class FeatureRWMap(FeatureRW):
# handle the conversion from read bytes, to setting value, and back
#
class BooleanValidator(object):
__slots__ = ('true_value', 'false_value', 'mask', 'needs_current_value')
@ -484,7 +540,10 @@ class BooleanValidator(object):
# mask specifies all the affected bits in the value
default_mask = 0xFF
def __init__(self, true_value=default_true, false_value=default_false, mask=default_mask):
def __init__(self,
true_value=default_true,
false_value=default_false,
mask=default_mask):
if isinstance(true_value, int):
assert isinstance(false_value, int)
if mask is None:
@ -523,13 +582,15 @@ class BooleanValidator(object):
if isinstance(self.mask, int):
reply_value = ord(reply_bytes[:1]) & self.mask
if _log.isEnabledFor(_DEBUG):
_log.debug("BooleanValidator: validate read %r => %02X", reply_bytes, reply_value)
_log.debug("BooleanValidator: validate read %r => %02X",
reply_bytes, reply_value)
if reply_value == self.true_value:
return True
if reply_value == self.false_value:
return False
_log.warn("BooleanValidator: reply %02X mismatched %02X/%02X/%02X",
reply_value, self.true_value, self.false_value, self.mask)
reply_value, self.true_value, self.false_value,
self.mask)
return False
count = len(self.mask)
@ -559,26 +620,29 @@ class BooleanValidator(object):
if isinstance(self.mask, int):
if current_value is not None and self.needs_current_value:
to_write |= ord(current_value[:1]) & (0xFF ^ self.mask)
if current_value is not None and to_write == ord(current_value[:1]):
if current_value is not None and to_write == ord(
current_value[:1]):
return None
else:
to_write = bytearray(to_write)
count = len(self.mask)
for i in range(0, count):
b = ord(to_write[i:i+1])
m = ord(self.mask[i : i + 1])
b = ord(to_write[i:i + 1])
m = ord(self.mask[i:i + 1])
assert b & m == b
# b &= m
if current_value is not None and self.needs_current_value:
b |= ord(current_value[i : i + 1]) & (0xFF ^ m)
b |= ord(current_value[i:i + 1]) & (0xFF ^ m)
to_write[i] = b
to_write = bytes(to_write)
if current_value is not None and to_write == current_value[:len(to_write)]:
if current_value is not None and to_write == current_value[:len(
to_write)]:
return None
if _log.isEnabledFor(_DEBUG):
_log.debug("BooleanValidator: prepare_write(%s, %s) => %r", new_value, current_value, to_write)
_log.debug("BooleanValidator: prepare_write(%s, %s) => %r",
new_value, current_value, to_write)
return to_write
@ -589,16 +653,17 @@ class BitFieldValidator(object):
kind = KIND.multiple_toggle
def __init__(self, options, byte_count=None):
assert(isinstance(options, list))
assert (isinstance(options, list))
self.options = options
self.byte_count = (max(x.bit_length() for x in options) + 7) // 8
if byte_count:
assert(isinstance(byte_count, int) and byte_count >= self.byte_count)
assert (isinstance(byte_count, int)
and byte_count >= self.byte_count)
self.byte_count = byte_count
def validate_read(self, reply_bytes):
r = _bytes2int(reply_bytes[:self.byte_count])
value = {str(int(k)) : False for k in self.options}
value = {str(int(k)): False for k in self.options}
m = 1
for i in range(8 * self.byte_count):
if m in self.options:
@ -607,7 +672,7 @@ class BitFieldValidator(object):
return value
def prepare_write(self, new_value):
assert(isinstance(new_value, dict))
assert (isinstance(new_value, dict))
w = 0
for k, v in new_value.items():
if v:
@ -619,7 +684,6 @@ class ChoicesValidator(object):
__slots__ = ('choices', 'flag', '_bytes_count', 'needs_current_value')
kind = KIND.choice
"""Translates between NamedInts and a byte sequence.
:param choices: a list of NamedInts
:param bytes_count: the size of the derived byte sequence. If None, it
@ -641,7 +705,8 @@ class ChoicesValidator(object):
def validate_read(self, reply_bytes):
reply_value = _bytes2int(reply_bytes[:self._bytes_count])
valid_value = self.choices[reply_value]
assert valid_value is not None, "%s: failed to validate read value %02X" % (self.__class__.__name__, reply_value)
assert valid_value is not None, "%s: failed to validate read value %02X" % (
self.__class__.__name__, reply_value)
return valid_value
def prepare_write(self, new_value, current_value=None):
@ -662,10 +727,16 @@ class ChoicesValidator(object):
assert isinstance(choice, _NamedInt)
return choice.bytes(self._bytes_count)
class ChoicesMapValidator(ChoicesValidator):
kind = KIND.map_choice
def __init__(self, choices_map, key_bytes_count=None, skip_bytes_count=None, value_bytes_count=None, extra_default=None):
def __init__(self,
choices_map,
key_bytes_count=None,
skip_bytes_count=None,
value_bytes_count=None,
extra_default=None):
assert choices_map is not None
assert isinstance(choices_map, dict)
max_key_bits = 0
@ -679,13 +750,13 @@ class ChoicesMapValidator(ChoicesValidator):
max_value_bits = max(max_value_bits, key_value.bit_length())
self.choices = choices_map
self.needs_current_value = False
self.extra_default=extra_default
self.extra_default = extra_default
self._key_bytes_count = (max_key_bits+7) // 8
self._key_bytes_count = (max_key_bits + 7) // 8
if key_bytes_count:
assert self._key_bytes_count <= key_bytes_count
self._key_bytes_count = key_bytes_count
self._value_bytes_count = (max_value_bits+7) // 8
self._value_bytes_count = (max_value_bits + 7) // 8
if value_bytes_count:
assert self._value_bytes_count <= value_bytes_count
self._value_bytes_count = value_bytes_count
@ -697,23 +768,26 @@ class ChoicesMapValidator(ChoicesValidator):
end = start + self._value_bytes_count
reply_value = _bytes2int(reply_bytes[start:end])
# reprogrammable keys starts out as 0, which is not a choice, so don't use assert here
if self.extra_default is not None and self.extra_default==reply_value:
if self.extra_default is not None and self.extra_default == reply_value:
return int(self.choices[key][0])
assert reply_value in self.choices[key], "%s: failed to validate read value %02X" % (self.__class__.__name__, reply_value)
assert reply_value in self.choices[
key], "%s: failed to validate read value %02X" % (
self.__class__.__name__, reply_value)
return reply_value
def prepare_write(self, key, new_value):
choices = self.choices[key]
if new_value not in choices and new_value != self.extra_default:
raise ValueError("invalid choice %r" % new_value)
return _int2bytes(new_value, self._skip_bytes_count + self._value_bytes_count)
return _int2bytes(new_value,
self._skip_bytes_count + self._value_bytes_count)
class RangeValidator(object):
__slots__ = ('min_value', 'max_value', 'flag', '_bytes_count', 'needs_current_value')
__slots__ = ('min_value', 'max_value', 'flag', '_bytes_count',
'needs_current_value')
kind = KIND.range
"""Translates between integers and a byte sequence.
:param min_value: minimum accepted value (inclusive)
:param max_value: maximum accepted value (inclusive)
@ -733,8 +807,10 @@ class RangeValidator(object):
def validate_read(self, reply_bytes):
reply_value = _bytes2int(reply_bytes[:self._bytes_count])
assert reply_value >= self.min_value, "%s: failed to validate read value %02X" % (self.__class__.__name__, reply_value)
assert reply_value <= self.max_value, "%s: failed to validate read value %02X" % (self.__class__.__name__, reply_value)
assert reply_value >= self.min_value, "%s: failed to validate read value %02X" % (
self.__class__.__name__, reply_value)
assert reply_value <= self.max_value, "%s: failed to validate read value %02X" % (
self.__class__.__name__, reply_value)
return reply_value
def prepare_write(self, new_value, current_value=None):

View File

@ -34,7 +34,7 @@ from .common import (
NamedInts as _NamedInts,
unpack as _unpack,
ReprogrammableKeyInfoV4 as _ReprogrammableKeyInfoV4,
)
)
from .settings import (
KIND as _KIND,
Setting as _Setting,
@ -48,7 +48,7 @@ from .settings import (
ChoicesValidator as _ChoicesV,
ChoicesMapValidator as _ChoicesMapV,
RangeValidator as _RangeV,
)
)
_DK = _hidpp10.DEVICE_KIND
_R = _hidpp10.REGISTERS
@ -58,241 +58,451 @@ _F = _hidpp20.FEATURE
# pre-defined basic setting descriptors
#
def register_toggle(name, register,
def register_toggle(name,
register,
true_value=_BooleanV.default_true,
false_value=_BooleanV.default_false,
mask=_BooleanV.default_mask,
label=None, description=None, device_kind=None):
validator = _BooleanV(true_value=true_value, false_value=false_value, mask=mask)
label=None,
description=None,
device_kind=None):
validator = _BooleanV(true_value=true_value,
false_value=false_value,
mask=mask)
rw = _RegisterRW(register)
return _Setting(name, rw, validator, label=label, description=description, device_kind=device_kind)
return _Setting(name,
rw,
validator,
label=label,
description=description,
device_kind=device_kind)
def register_choices(name, register, choices,
def register_choices(name,
register,
choices,
kind=_KIND.choice,
label=None, description=None, device_kind=None):
label=None,
description=None,
device_kind=None):
assert choices
validator = _ChoicesV(choices)
rw = _RegisterRW(register)
return _Setting(name, rw, validator, kind=kind, label=label, description=description, device_kind=device_kind)
return _Setting(name,
rw,
validator,
kind=kind,
label=label,
description=description,
device_kind=device_kind)
def feature_toggle(name, feature,
def feature_toggle(name,
feature,
read_function_id=_FeatureRW.default_read_fnid,
write_function_id=_FeatureRW.default_write_fnid,
true_value=_BooleanV.default_true,
false_value=_BooleanV.default_false,
mask=_BooleanV.default_mask,
label=None, description=None, device_kind=None):
validator = _BooleanV(true_value=true_value, false_value=false_value, mask=mask)
label=None,
description=None,
device_kind=None):
validator = _BooleanV(true_value=true_value,
false_value=false_value,
mask=mask)
rw = _FeatureRW(feature, read_function_id, write_function_id)
return _Setting(name, rw, validator, feature=feature, label=label, description=description, device_kind=device_kind)
return _Setting(name,
rw,
validator,
feature=feature,
label=label,
description=description,
device_kind=device_kind)
def feature_bitfield_toggle(name, feature, options,
def feature_bitfield_toggle(name,
feature,
options,
read_function_id=_FeatureRW.default_read_fnid,
write_function_id=_FeatureRW.default_write_fnid,
label=None, description=None, device_kind=None):
label=None,
description=None,
device_kind=None):
assert options
validator = _BitFieldV(options)
rw = _FeatureRW(feature, read_function_id, write_function_id)
return _BitFieldSetting(name, rw, validator, feature=feature, label=label, description=description, device_kind=device_kind)
return _BitFieldSetting(name,
rw,
validator,
feature=feature,
label=label,
description=description,
device_kind=device_kind)
def feature_bitfield_toggle_dynamic(name, feature, options_callback,
def feature_bitfield_toggle_dynamic(
name,
feature,
options_callback,
read_function_id=_FeatureRW.default_read_fnid,
write_function_id=_FeatureRW.default_write_fnid,
label=None, description=None, device_kind=None):
label=None,
description=None,
device_kind=None):
def instantiate(device):
options = options_callback(device)
setting = feature_bitfield_toggle(name, feature, options,
setting = feature_bitfield_toggle(name,
feature,
options,
read_function_id=read_function_id,
write_function_id=write_function_id,
label=label,
description=description, device_kind=device_kind)
description=description,
device_kind=device_kind)
return setting(device)
instantiate._rw_kind = _FeatureRW.kind
return instantiate
def feature_choices(name, feature, choices,
read_function_id, write_function_id,
def feature_choices(name,
feature,
choices,
read_function_id,
write_function_id,
bytes_count=None,
label=None, description=None, device_kind=None):
label=None,
description=None,
device_kind=None):
assert choices
validator = _ChoicesV(choices, bytes_count=bytes_count)
rw = _FeatureRW(feature, read_function_id, write_function_id)
return _Setting(name, rw, validator, feature=feature, kind=_KIND.choice, label=label, description=description, device_kind=device_kind)
return _Setting(name,
rw,
validator,
feature=feature,
kind=_KIND.choice,
label=label,
description=description,
device_kind=device_kind)
def feature_choices_dynamic(name, feature, choices_callback,
read_function_id, write_function_id,
def feature_choices_dynamic(name,
feature,
choices_callback,
read_function_id,
write_function_id,
bytes_count=None,
label=None, description=None, device_kind=None):
label=None,
description=None,
device_kind=None):
# Proxy that obtains choices dynamically from a device
def instantiate(device):
# Obtain choices for this feature
choices = choices_callback(device)
setting = feature_choices(name, feature, choices,
read_function_id, write_function_id,
setting = feature_choices(name,
feature,
choices,
read_function_id,
write_function_id,
bytes_count=bytes_count,
label=label, description=description, device_kind=device_kind)
label=label,
description=description,
device_kind=device_kind)
return setting(device)
instantiate._rw_kind = _FeatureRW.kind
return instantiate
# maintain a mapping from keys (NamedInts) to one of a list of choices (NamedInts), default is first one
# the setting is stored as a JSON-compatible object mapping the key int (as a string) to the choice int
# extra_default is an extra value that comes from the device that also means the default
def feature_map_choices(name, feature, choicesmap,
read_function_id, write_function_id,
key_bytes_count=None, skip_bytes_count=None, value_bytes_count=None,
label=None, description=None, device_kind=None, extra_default=None):
def feature_map_choices(name,
feature,
choicesmap,
read_function_id,
write_function_id,
key_bytes_count=None,
skip_bytes_count=None,
value_bytes_count=None,
label=None,
description=None,
device_kind=None,
extra_default=None):
assert choicesmap
validator = _ChoicesMapV(choicesmap, key_bytes_count=key_bytes_count, skip_bytes_count=skip_bytes_count, value_bytes_count=value_bytes_count, extra_default=extra_default )
rw = _FeatureRWMap(feature, read_function_id, write_function_id, key_bytes=key_bytes_count)
return _Settings(name, rw, validator, feature=feature, kind=_KIND.map_choice, label=label, description=description, device_kind=device_kind)
validator = _ChoicesMapV(choicesmap,
key_bytes_count=key_bytes_count,
skip_bytes_count=skip_bytes_count,
value_bytes_count=value_bytes_count,
extra_default=extra_default)
rw = _FeatureRWMap(feature,
read_function_id,
write_function_id,
key_bytes=key_bytes_count)
return _Settings(name,
rw,
validator,
feature=feature,
kind=_KIND.map_choice,
label=label,
description=description,
device_kind=device_kind)
def feature_map_choices_dynamic(name, feature, choices_callback,
read_function_id, write_function_id,
key_bytes_count=None, skip_bytes_count=None, value_bytes_count=None,
label=None, description=None, device_kind=None, extra_default=None):
def feature_map_choices_dynamic(name,
feature,
choices_callback,
read_function_id,
write_function_id,
key_bytes_count=None,
skip_bytes_count=None,
value_bytes_count=None,
label=None,
description=None,
device_kind=None,
extra_default=None):
# Proxy that obtains choices dynamically from a device
def instantiate(device):
choices = choices_callback(device)
if not choices: # no choices, so don't create a Setting
return None
setting = feature_map_choices(name, feature, choices,
read_function_id, write_function_id,
key_bytes_count=key_bytes_count, skip_bytes_count=skip_bytes_count, value_bytes_count=value_bytes_count,
label=label, description=description, device_kind=device_kind, extra_default=extra_default)
setting = feature_map_choices(name,
feature,
choices,
read_function_id,
write_function_id,
key_bytes_count=key_bytes_count,
skip_bytes_count=skip_bytes_count,
value_bytes_count=value_bytes_count,
label=label,
description=description,
device_kind=device_kind,
extra_default=extra_default)
return setting(device)
instantiate._rw_kind = _FeatureRWMap.kind
return instantiate
def feature_range(name, feature, min_value, max_value,
def feature_range(name,
feature,
min_value,
max_value,
read_function_id=_FeatureRW.default_read_fnid,
write_function_id=_FeatureRW.default_write_fnid,
rw=None,
bytes_count=None,
label=None, description=None, device_kind=None):
label=None,
description=None,
device_kind=None):
validator = _RangeV(min_value, max_value, bytes_count=bytes_count)
if rw is None:
rw = _FeatureRW(feature, read_function_id, write_function_id)
return _Setting(name, rw, validator, feature=feature, kind=_KIND.range, label=label, description=description, device_kind=device_kind)
return _Setting(name,
rw,
validator,
feature=feature,
kind=_KIND.range,
label=label,
description=description,
device_kind=device_kind)
#
# common strings for settings - name, string to display in main window, tool tip for main window
#
_HAND_DETECTION = ('hand-detection', _("Hand Detection"), _("Turn on illumination when the hands hover over the keyboard."))
_SMOOTH_SCROLL = ('smooth-scroll', _("Smooth Scrolling"), _("High-sensitivity mode for vertical scroll with the wheel."))
_SIDE_SCROLL = ('side-scroll', _("Side Scrolling"), _("When disabled, pushing the wheel sideways sends custom button events\n"
"instead of the standard side-scrolling events."))
_HI_RES_SCROLL = ('hi-res-scroll', _("High Resolution Scrolling"),
_HAND_DETECTION = (
'hand-detection', _("Hand Detection"),
_("Turn on illumination when the hands hover over the keyboard."))
_SMOOTH_SCROLL = (
'smooth-scroll', _("Smooth Scrolling"),
_("High-sensitivity mode for vertical scroll with the wheel."))
_LOW_RES_SCROLL = ('lowres-smooth-scroll', _("HID++ Scrolling"), _("HID++ mode for vertical scroll with the wheel.") + '\n' +
_SIDE_SCROLL = (
'side-scroll', _("Side Scrolling"),
_("When disabled, pushing the wheel sideways sends custom button events\n"
"instead of the standard side-scrolling events."))
_HI_RES_SCROLL = (
'hi-res-scroll', _("High Resolution Scrolling"),
_("High-sensitivity mode for vertical scroll with the wheel."))
_LOW_RES_SCROLL = ('lowres-smooth-scroll', _("HID++ Scrolling"),
_("HID++ mode for vertical scroll with the wheel.") + '\n' +
_("Effectively turns off wheel scrolling in Linux."))
_HIRES_INV = ('hires-smooth-invert', _("High Resolution Wheel Invert"),
_("High-sensitivity wheel invert mode for vertical scroll."))
_HIRES_RES = ('hires-smooth-resolution', _("Wheel Resolution"),
_("High-sensitivity mode for vertical scroll with the wheel."))
_FN_SWAP = ('fn-swap', _("Swap Fx function"), _("When set, the F1..F12 keys will activate their special function,\n"
"and you must hold the FN key to activate their standard function.")
+ '\n\n' +
_FN_SWAP = (
'fn-swap', _("Swap Fx function"),
_("When set, the F1..F12 keys will activate their special function,\n"
"and you must hold the FN key to activate their standard function.") +
'\n\n' +
_("When unset, the F1..F12 keys will activate their standard function,\n"
"and you must hold the FN key to activate their special function."))
_DPI = ('dpi', _("Sensitivity (DPI)"), None)
_POINTER_SPEED = ('pointer_speed', _("Sensitivity (Pointer Speed)"), _("Speed multiplier for mouse (256 is normal multiplier)."))
_SMART_SHIFT = ('smart-shift', _("Smart Shift"), _("Automatically switch the mouse wheel between ratchet and freespin mode.\n"
_POINTER_SPEED = ('pointer_speed', _("Sensitivity (Pointer Speed)"),
_("Speed multiplier for mouse (256 is normal multiplier)."))
_SMART_SHIFT = (
'smart-shift', _("Smart Shift"),
_("Automatically switch the mouse wheel between ratchet and freespin mode.\n"
"The mouse wheel is always free at 0, and always locked at 50"))
_BACKLIGHT = ('backlight', _("Backlight"), _("Turn illumination on or off on keyboard."))
_REPROGRAMMABLE_KEYS = ('reprogrammable-keys', _("Actions"), _("Change the action for the key or button.") + "\n" +
_("Changing important actions (such as for the left mouse button) can result in an unusable system."))
_DISABLE_KEYS = ('disable-keyboard-keys', _("Disable keys"), _("Disable specific keyboard keys."))
_BACKLIGHT = ('backlight', _("Backlight"),
_("Turn illumination on or off on keyboard."))
_REPROGRAMMABLE_KEYS = ('reprogrammable-keys', _(
"Actions"
), _("Change the action for the key or button.") + "\n" + _(
"Changing important actions (such as for the left mouse button) can result in an unusable system."
))
_DISABLE_KEYS = ('disable-keyboard-keys', _("Disable keys"),
_("Disable specific keyboard keys."))
#
#
#
def _register_hand_detection(register=_R.keyboard_hand_detection,
true_value=b'\x00\x00\x00', false_value=b'\x00\x00\x30', mask=b'\x00\x00\xFF'):
return register_toggle(_HAND_DETECTION[0], register, true_value=true_value, false_value=false_value,
label=_HAND_DETECTION[1], description=_HAND_DETECTION[2],
device_kind=(_DK.keyboard,))
true_value=b'\x00\x00\x00',
false_value=b'\x00\x00\x30',
mask=b'\x00\x00\xFF'):
return register_toggle(_HAND_DETECTION[0],
register,
true_value=true_value,
false_value=false_value,
label=_HAND_DETECTION[1],
description=_HAND_DETECTION[2],
device_kind=(_DK.keyboard, ))
def _register_fn_swap(register=_R.keyboard_fn_swap, true_value=b'\x00\x01', mask=b'\x00\x01'):
return register_toggle(_FN_SWAP[0], register, true_value=true_value, mask=mask,
label=_FN_SWAP[1], description=_FN_SWAP[2],
device_kind=(_DK.keyboard,))
def _register_smooth_scroll(register=_R.mouse_button_flags, true_value=0x40, mask=0x40):
return register_toggle(_SMOOTH_SCROLL[0], register, true_value=true_value, mask=mask,
label=_SMOOTH_SCROLL[1], description=_SMOOTH_SCROLL[2],
def _register_fn_swap(register=_R.keyboard_fn_swap,
true_value=b'\x00\x01',
mask=b'\x00\x01'):
return register_toggle(_FN_SWAP[0],
register,
true_value=true_value,
mask=mask,
label=_FN_SWAP[1],
description=_FN_SWAP[2],
device_kind=(_DK.keyboard, ))
def _register_smooth_scroll(register=_R.mouse_button_flags,
true_value=0x40,
mask=0x40):
return register_toggle(_SMOOTH_SCROLL[0],
register,
true_value=true_value,
mask=mask,
label=_SMOOTH_SCROLL[1],
description=_SMOOTH_SCROLL[2],
device_kind=(_DK.mouse, _DK.trackball))
def _register_side_scroll(register=_R.mouse_button_flags, true_value=0x02, mask=0x02):
return register_toggle(_SIDE_SCROLL[0], register, true_value=true_value, mask=mask,
label=_SIDE_SCROLL[1], description=_SIDE_SCROLL[2],
def _register_side_scroll(register=_R.mouse_button_flags,
true_value=0x02,
mask=0x02):
return register_toggle(_SIDE_SCROLL[0],
register,
true_value=true_value,
mask=mask,
label=_SIDE_SCROLL[1],
description=_SIDE_SCROLL[2],
device_kind=(_DK.mouse, _DK.trackball))
def _register_dpi(register=_R.mouse_dpi, choices=None):
return register_choices(_DPI[0], register, choices,
label=_DPI[1], description=_DPI[2],
return register_choices(_DPI[0],
register,
choices,
label=_DPI[1],
description=_DPI[2],
device_kind=(_DK.mouse, _DK.trackball))
def _feature_fn_swap():
return feature_toggle(_FN_SWAP[0], _F.FN_INVERSION,
label=_FN_SWAP[1], description=_FN_SWAP[2],
device_kind=(_DK.keyboard,))
return feature_toggle(_FN_SWAP[0],
_F.FN_INVERSION,
label=_FN_SWAP[1],
description=_FN_SWAP[2],
device_kind=(_DK.keyboard, ))
# this might not be correct for this feature
def _feature_new_fn_swap():
return feature_toggle(_FN_SWAP[0], _F.NEW_FN_INVERSION,
label=_FN_SWAP[1], description=_FN_SWAP[2],
device_kind=(_DK.keyboard,))
return feature_toggle(_FN_SWAP[0],
_F.NEW_FN_INVERSION,
label=_FN_SWAP[1],
description=_FN_SWAP[2],
device_kind=(_DK.keyboard, ))
# ignore the capabilities part of the feature - all devices should be able to swap Fn state
# just use the current host (first byte = 0xFF) part of the feature to read and set the Fn state
def _feature_k375s_fn_swap():
return feature_toggle(_FN_SWAP[0], _F.K375S_FN_INVERSION,
label=_FN_SWAP[1], description=_FN_SWAP[2],
true_value=b'\xFF\x01', false_value=b'\xFF\x00',
device_kind=(_DK.keyboard,))
return feature_toggle(_FN_SWAP[0],
_F.K375S_FN_INVERSION,
label=_FN_SWAP[1],
description=_FN_SWAP[2],
true_value=b'\xFF\x01',
false_value=b'\xFF\x00',
device_kind=(_DK.keyboard, ))
# FIXME: This will enable all supported backlight settings, we should allow the users to select which settings they want to enable.
def _feature_backlight2():
return feature_toggle(_BACKLIGHT[0], _F.BACKLIGHT2,
label=_BACKLIGHT[1], description=_BACKLIGHT[2],
device_kind=(_DK.keyboard,))
return feature_toggle(_BACKLIGHT[0],
_F.BACKLIGHT2,
label=_BACKLIGHT[1],
description=_BACKLIGHT[2],
device_kind=(_DK.keyboard, ))
def _feature_hi_res_scroll():
return feature_toggle(_HI_RES_SCROLL[0], _F.HI_RES_SCROLLING,
label=_HI_RES_SCROLL[1], description=_HI_RES_SCROLL[2],
return feature_toggle(_HI_RES_SCROLL[0],
_F.HI_RES_SCROLLING,
label=_HI_RES_SCROLL[1],
description=_HI_RES_SCROLL[2],
device_kind=(_DK.mouse, _DK.trackball))
def _feature_lowres_smooth_scroll():
return feature_toggle(_LOW_RES_SCROLL[0], _F.LOWRES_WHEEL,
label=_LOW_RES_SCROLL[1], description=_LOW_RES_SCROLL[2],
return feature_toggle(_LOW_RES_SCROLL[0],
_F.LOWRES_WHEEL,
label=_LOW_RES_SCROLL[1],
description=_LOW_RES_SCROLL[2],
device_kind=(_DK.mouse, _DK.trackball))
def _feature_hires_smooth_invert():
return feature_toggle(_HIRES_INV[0], _F.HIRES_WHEEL,
return feature_toggle(_HIRES_INV[0],
_F.HIRES_WHEEL,
read_function_id=0x10,
write_function_id=0x20,
true_value=0x04, mask=0x04,
label=_HIRES_INV[1], description=_HIRES_INV[2],
true_value=0x04,
mask=0x04,
label=_HIRES_INV[1],
description=_HIRES_INV[2],
device_kind=(_DK.mouse, _DK.trackball))
def _feature_hires_smooth_resolution():
return feature_toggle(_HIRES_RES[0], _F.HIRES_WHEEL,
return feature_toggle(_HIRES_RES[0],
_F.HIRES_WHEEL,
read_function_id=0x10,
write_function_id=0x20,
true_value=0x02, mask=0x02,
label=_HIRES_RES[1], description=_HIRES_RES[2],
true_value=0x02,
mask=0x02,
label=_HIRES_RES[1],
description=_HIRES_RES[2],
device_kind=(_DK.mouse, _DK.trackball))
def _feature_smart_shift():
_MIN_SMART_SHIFT_VALUE = 0
_MAX_SMART_SHIFT_VALUE = 50
class _SmartShiftRW(_FeatureRW):
def __init__(self, feature):
super(_SmartShiftRW, self).__init__(feature)
@ -316,16 +526,21 @@ def _feature_smart_shift():
if threshold == _MAX_SMART_SHIFT_VALUE:
threshold = 255
data = _int2bytes(mode, count=1) + _int2bytes(threshold, count=1) * 2
data = _int2bytes(mode,
count=1) + _int2bytes(threshold, count=1) * 2
return super(_SmartShiftRW, self).write(device, data)
return feature_range(_SMART_SHIFT[0], _F.SMART_SHIFT,
_MIN_SMART_SHIFT_VALUE, _MAX_SMART_SHIFT_VALUE,
return feature_range(_SMART_SHIFT[0],
_F.SMART_SHIFT,
_MIN_SMART_SHIFT_VALUE,
_MAX_SMART_SHIFT_VALUE,
bytes_count=1,
rw=_SmartShiftRW(_F.SMART_SHIFT),
label=_SMART_SHIFT[1], description=_SMART_SHIFT[2],
label=_SMART_SHIFT[1],
description=_SMART_SHIFT[2],
device_kind=(_DK.mouse, _DK.trackball))
def _feature_adjustable_dpi_choices(device):
# [1] getSensorDpiList(sensorIdx)
reply = device.feature_request(_F.ADJUSTABLE_DPI, 0x10)
@ -334,7 +549,7 @@ def _feature_adjustable_dpi_choices(device):
assert reply, 'Oops, DPI list cannot be retrieved!'
dpi_list = []
step = None
for val in _unpack('!7H', reply[1:1+14]):
for val in _unpack('!7H', reply[1:1 + 14]):
if val == 0:
break
if val >> 13 == 0b111:
@ -348,29 +563,38 @@ def _feature_adjustable_dpi_choices(device):
dpi_list = range(dpi_list[0], dpi_list[1] + 1, step)
return _NamedInts.list(dpi_list)
def _feature_adjustable_dpi():
"""Pointer Speed feature"""
# Assume sensorIdx 0 (there is only one sensor)
# [2] getSensorDpi(sensorIdx) -> sensorIdx, dpiMSB, dpiLSB
# [3] setSensorDpi(sensorIdx, dpi)
return feature_choices_dynamic(_DPI[0], _F.ADJUSTABLE_DPI,
return feature_choices_dynamic(_DPI[0],
_F.ADJUSTABLE_DPI,
_feature_adjustable_dpi_choices,
read_function_id=0x20,
write_function_id=0x30,
bytes_count=3,
label=_DPI[1], description=_DPI[2],
label=_DPI[1],
description=_DPI[2],
device_kind=(_DK.mouse, _DK.trackball))
def _feature_pointer_speed():
"""Pointer Speed feature"""
# min and max values taken from usb traces of Win software
return feature_range(_POINTER_SPEED[0], _F.POINTER_SPEED, 0x002e, 0x01ff,
return feature_range(_POINTER_SPEED[0],
_F.POINTER_SPEED,
0x002e,
0x01ff,
read_function_id=0x0,
write_function_id=0x10,
bytes_count=2,
label=_POINTER_SPEED[1], description=_POINTER_SPEED[2],
label=_POINTER_SPEED[1],
description=_POINTER_SPEED[2],
device_kind=(_DK.mouse, _DK.trackball))
# the keys for the choice map are Logitech controls (from special_keys)
# each choice value is a NamedInt with the string from a task (to be shown to the user)
# and the integer being the control number for that task (to be written to the device)
@ -380,20 +604,25 @@ def _feature_reprogrammable_keys_choices(device):
assert count, 'Oops, reprogrammable key count cannot be retrieved!'
count = ord(count[:1]) # the number of key records
keys = [None] * count
groups = [ [] for i in range(0,9) ]
groups = [[] for i in range(0, 9)]
choices = {}
for i in range(0,count): # get the data for each key record on device
for i in range(0, count): # get the data for each key record on device
keydata = device.feature_request(_F.REPROG_CONTROLS_V4, 0x10, i)
key, key_task, flags, pos, group, gmask = _unpack('!HHBBBB', keydata[:8])
action =_NamedInt(key, str(_special_keys.TASK[key_task]))
keys[i] = ( _special_keys.CONTROL[key], action, flags, gmask )
key, key_task, flags, pos, group, gmask = _unpack(
'!HHBBBB', keydata[:8])
action = _NamedInt(key, str(_special_keys.TASK[key_task]))
keys[i] = (_special_keys.CONTROL[key], action, flags, gmask)
groups[group].append(action)
for k in keys:
# if k[2] & _special_keys.KEY_FLAG.reprogrammable: # this flag is only to show in UI, ignore in Solaar
if k[3]: # only keys with a non-zero gmask are remappable
key_choices = [ k[1] ] # it should always be possible to map the key to itself
for g in range(1,9): # group 0 and gmask 0 (k[3]) does not indicate remappability so don't consider group 0
if ( k[3]==0 if g==0 else k[3] & 2**(g-1) ):
key_choices = [
k[1]
] # it should always be possible to map the key to itself
for g in range(
1, 9
): # group 0 and gmask 0 (k[3]) does not indicate remappability so don't consider group 0
if (k[3] == 0 if g == 0 else k[3] & 2**(g - 1)):
for gm in groups[g]:
if int(gm) != int(k[0]): # don't put itself in twice
key_choices.append(gm)
@ -401,25 +630,41 @@ def _feature_reprogrammable_keys_choices(device):
choices[k[0]] = key_choices
return choices
def _feature_reprogrammable_keys():
return feature_map_choices_dynamic(_REPROGRAMMABLE_KEYS[0], _F.REPROG_CONTROLS_V4,
return feature_map_choices_dynamic(_REPROGRAMMABLE_KEYS[0],
_F.REPROG_CONTROLS_V4,
_feature_reprogrammable_keys_choices,
read_function_id=0x20, write_function_id=0x30,
key_bytes_count=2, skip_bytes_count=1, value_bytes_count=2,
label=_REPROGRAMMABLE_KEYS[1], description=_REPROGRAMMABLE_KEYS[2],
device_kind=(_DK.keyboard,), extra_default=0)
read_function_id=0x20,
write_function_id=0x30,
key_bytes_count=2,
skip_bytes_count=1,
value_bytes_count=2,
label=_REPROGRAMMABLE_KEYS[1],
description=_REPROGRAMMABLE_KEYS[2],
device_kind=(_DK.keyboard, ),
extra_default=0)
def _feature_disable_keyboard_keys_key_list(device):
mask = device.feature_request(_F.KEYBOARD_DISABLE_KEYS)[0]
options = [_special_keys.DISABLE[1 << i] for i in range(8) if mask & (1 << i)]
options = [
_special_keys.DISABLE[1 << i] for i in range(8) if mask & (1 << i)
]
return options
def _feature_disable_keyboard_keys():
return feature_bitfield_toggle_dynamic(_DISABLE_KEYS[0], _F.KEYBOARD_DISABLE_KEYS,
return feature_bitfield_toggle_dynamic(
_DISABLE_KEYS[0],
_F.KEYBOARD_DISABLE_KEYS,
_feature_disable_keyboard_keys_key_list,
read_function_id=0x10, write_function_id=0x20,
label=_DISABLE_KEYS[1], description=_DISABLE_KEYS[2], device_kind=(_DK.keyboard,))
read_function_id=0x10,
write_function_id=0x20,
label=_DISABLE_KEYS[1],
description=_DISABLE_KEYS[2],
device_kind=(_DK.keyboard, ))
#
#
@ -427,29 +672,46 @@ def _feature_disable_keyboard_keys():
from collections import namedtuple
def _S(name, featureID=None, featureFn=None, registerFn=None, identifier=None):
return ( name, featureID, featureFn, registerFn, identifier if identifier else name.replace('-','_') )
return (name, featureID, featureFn, registerFn,
identifier if identifier else name.replace('-', '_'))
_SETTINGS_TABLE = [
_S( _HAND_DETECTION[0], registerFn=_register_hand_detection ),
_S( _SMOOTH_SCROLL[0], registerFn=_register_smooth_scroll ),
_S( _SIDE_SCROLL[0], registerFn=_register_side_scroll ),
_S( _HI_RES_SCROLL[0], _F.HI_RES_SCROLLING, _feature_hi_res_scroll ),
_S( _LOW_RES_SCROLL[0], _F.LOWRES_WHEEL, _feature_lowres_smooth_scroll ),
_S( _HIRES_INV[0], _F.HIRES_WHEEL, _feature_hires_smooth_invert ),
_S( _HIRES_RES[0], _F.HIRES_WHEEL, _feature_hires_smooth_resolution ),
_S( _FN_SWAP[0], _F.FN_INVERSION, _feature_fn_swap, registerFn=_register_fn_swap ),
_S( _FN_SWAP[0], _F.NEW_FN_INVERSION, _feature_new_fn_swap, identifier='new_fn_swap' ),
_S( _FN_SWAP[0], _F.K375S_FN_INVERSION, _feature_k375s_fn_swap, identifier='k375s_fn_swap' ),
_S( _DPI[0], _F.ADJUSTABLE_DPI, _feature_adjustable_dpi, registerFn=_register_dpi ),
_S( _POINTER_SPEED[0], _F.POINTER_SPEED, _feature_pointer_speed ),
_S( _SMART_SHIFT[0], _F.SMART_SHIFT, _feature_smart_shift ),
_S( _BACKLIGHT[0], _F.BACKLIGHT2, _feature_backlight2 ),
_S( _REPROGRAMMABLE_KEYS[0], _F.REPROG_CONTROLS_V4, _feature_reprogrammable_keys ),
_S( _DISABLE_KEYS[0], _F.KEYBOARD_DISABLE_KEYS, _feature_disable_keyboard_keys ),
_S(_HAND_DETECTION[0], registerFn=_register_hand_detection),
_S(_SMOOTH_SCROLL[0], registerFn=_register_smooth_scroll),
_S(_SIDE_SCROLL[0], registerFn=_register_side_scroll),
_S(_HI_RES_SCROLL[0], _F.HI_RES_SCROLLING, _feature_hi_res_scroll),
_S(_LOW_RES_SCROLL[0], _F.LOWRES_WHEEL, _feature_lowres_smooth_scroll),
_S(_HIRES_INV[0], _F.HIRES_WHEEL, _feature_hires_smooth_invert),
_S(_HIRES_RES[0], _F.HIRES_WHEEL, _feature_hires_smooth_resolution),
_S(_FN_SWAP[0],
_F.FN_INVERSION,
_feature_fn_swap,
registerFn=_register_fn_swap),
_S(_FN_SWAP[0],
_F.NEW_FN_INVERSION,
_feature_new_fn_swap,
identifier='new_fn_swap'),
_S(_FN_SWAP[0],
_F.K375S_FN_INVERSION,
_feature_k375s_fn_swap,
identifier='k375s_fn_swap'),
_S(_DPI[0],
_F.ADJUSTABLE_DPI,
_feature_adjustable_dpi,
registerFn=_register_dpi),
_S(_POINTER_SPEED[0], _F.POINTER_SPEED, _feature_pointer_speed),
_S(_SMART_SHIFT[0], _F.SMART_SHIFT, _feature_smart_shift),
_S(_BACKLIGHT[0], _F.BACKLIGHT2, _feature_backlight2),
_S(_REPROGRAMMABLE_KEYS[0], _F.REPROG_CONTROLS_V4,
_feature_reprogrammable_keys),
_S(_DISABLE_KEYS[0], _F.KEYBOARD_DISABLE_KEYS,
_feature_disable_keyboard_keys),
]
_SETTINGS_LIST = namedtuple('_SETTINGS_LIST',[s[4] for s in _SETTINGS_TABLE])
_SETTINGS_LIST = namedtuple('_SETTINGS_LIST', [s[4] for s in _SETTINGS_TABLE])
RegisterSettings = _SETTINGS_LIST._make([s[3] for s in _SETTINGS_TABLE])
FeatureSettings = _SETTINGS_LIST._make([s[2] for s in _SETTINGS_TABLE])
@ -459,6 +721,7 @@ del _SETTINGS_LIST
#
#
# Returns True if device was queried to find features, False otherwise
def check_feature_settings(device, already_known):
"""Try to auto-detect device settings by the HID++ 2.0 features they have."""
@ -481,13 +744,15 @@ def check_feature_settings(device, already_known):
try:
detected = featureFn()(device)
if _log.isEnabledFor(_DEBUG):
_log.debug("check_feature[%s] detected %s", featureId, detected)
_log.debug("check_feature[%s] detected %s", featureId,
detected)
if detected:
already_known.append(detected)
except Exception as reason:
_log.error("check_feature[%s] inconsistent feature %s", featureId, reason)
_log.error("check_feature[%s] inconsistent feature %s", featureId,
reason)
for name, featureId, featureFn, _, _ in _SETTINGS_TABLE :
for name, featureId, featureFn, _, _ in _SETTINGS_TABLE:
if featureId and featureFn:
check_feature(name, featureId, featureFn)
return True

View File

@ -22,7 +22,6 @@
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
@ -68,7 +67,8 @@ CONTROL = _NamedInts(
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
MEDIA_CENTER_MSFT=
0x002A, # Should not be used as it is not reprogrammable under Windows
CUSTOM_MENU=0x002B,
MESSENGER=0x002C,
MY_DOCUMENTS=0x002D,
@ -192,9 +192,9 @@ CONTROL = _NamedInts(
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
# 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,
@ -236,7 +236,8 @@ CONTROL = _NamedInts(
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
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
@ -284,7 +285,6 @@ TASK = _NamedInts(
Next=0x0005,
Previous=0x0006,
Stop=0x0007,
Application_Switcher=0x0008,
BurnMediaPlayer=0x0009,
Calculator=0x000A,
@ -311,7 +311,6 @@ TASK = _NamedInts(
# Both 0x001E and 0x001F are known as MediaCenterSet
Media_Center_Logitech=0x001E,
Media_Center_Microsoft=0x001F,
UserMenu=0x0020,
Messenger=0x0021,
PersonalFolders=0x0022,
@ -414,9 +413,9 @@ TASK = _NamedInts(
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
# 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
@ -462,7 +461,8 @@ TASK = _NamedInts(
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
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
@ -490,16 +490,14 @@ TASK = _NamedInts(
)
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,
KEY_FLAG = _NamedInts(virtual=0x80,
persistently_divertable=0x40,
divertable=0x20,
reprogrammable=0x10,
FN_sensitive=0x08,
nonstandard=0x04,
is_FN=0x02,
mse=0x01
)
mse=0x01)
DISABLE = _NamedInts(
Caps_Lock=0x01,

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,7 +36,11 @@ _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,
@ -49,7 +52,7 @@ KEYS = _NamedInts(
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,6 +67,7 @@ _LONG_SLEEP = 15 * 60 # seconds
#
#
def attach_to(device, changed_callback):
assert device
assert changed_callback
@ -74,10 +78,12 @@ def attach_to(device, changed_callback):
else:
device.status = DeviceStatus(device, changed_callback)
#
#
#
class ReceiverStatus(dict):
"""The 'runtime' status of a receiver, mostly about the pairing process --
is the pairing lock open or closed, any pairing errors, etc.
@ -98,8 +104,11 @@ class ReceiverStatus(dict):
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 })
return (_("No paired devices.") if count == 0 else ngettext(
"%(count)s paired device.", "%(count)s paired devices.", count) % {
'count': count
})
__unicode__ = __str__
def changed(self, alert=ALERT.NOTIFICATION, reason=None):
@ -119,10 +128,12 @@ class ReceiverStatus(dict):
# # 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
active/inactive, battery charge, lux, etc. It updates them mostly by
@ -148,9 +159,13 @@ class DeviceStatus(dict):
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)) }
yield _("Battery: %(level)s") % {
'level': _(str(battery_level))
}
else:
yield _("Battery: %(percent)d%%") % { 'percent': battery_level }
yield _("Battery: %(percent)d%%") % {
'percent': battery_level
}
battery_status = self.get(KEYS.BATTERY_STATUS)
if battery_status is not None:
@ -161,18 +176,25 @@ class DeviceStatus(dict):
light_level = self.get(KEYS.LIGHT_LEVEL)
if light_level is not None:
if comma: yield ', '
yield _("Lighting: %(level)s lux") % { 'level': light_level }
yield _("Lighting: %(level)s lux") % {'level': light_level}
return ''.join(i for i in _items())
def __repr__(self):
return '{' + ', '.join('\'%s\': %r' % (k, v) for k, v in self.items()) + '}'
return '{' + ', '.join('\'%s\': %r' % (k, v)
for k, v in self.items()) + '}'
def __bool__(self):
return bool(self._active)
__nonzero__ = __bool__
def set_battery_info(self, level, status, nextLevel=None, voltage=None, timestamp=None):
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)
@ -182,7 +204,8 @@ class DeviceStatus(dict):
# 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):
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
@ -192,36 +215,55 @@ class DeviceStatus(dict):
assert isinstance(level, int)
# 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
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
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
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
changed = old_level != level or old_status != status or old_charging != charging
alert, reason = ALERT.NONE, None
if _hidpp20.BATTERY_OK(status) and ( level is None or level > _BATTERY_ATTENTION_LEVEL ):
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)
_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) }
reason = _("Battery: %(level)s (%(status)s)") % {
'level': _(level),
'status': _(status)
}
else:
reason = _("Battery: %(percent)d%% (%(status)s)") % { 'percent': level, 'status': status.name }
reason = _("Battery: %(percent)d%% (%(status)s)") % {
'percent': level,
'status': status.name
}
if changed or reason:
# 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)
_hidpp10.set_3leds(self._device,
level,
charging=charging,
warning=bool(alert))
self.changed(active=True,
alert=alert,
reason=reason,
timestamp=timestamp)
# Retrieve and regularize battery status
def read_battery(self, timestamp=None):
@ -239,7 +281,7 @@ class DeviceStatus(dict):
v = _hidpp20.get_voltage(d)
if v is not None:
level, status, voltage, _ignore, _ignore = v
self.set_battery_keys( (level, status, None), voltage)
self.set_battery_keys((level, status, None), voltage)
return
# Really unnecessary, if the device has SOLAR_DASHBOARD it should be
@ -251,7 +293,7 @@ class DeviceStatus(dict):
return
self.set_battery_keys(battery)
def set_battery_keys(self, battery, voltage=None) :
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)
@ -260,7 +302,11 @@ class DeviceStatus(dict):
self[KEYS.BATTERY_CHARGING] = None
self.changed()
def changed(self, active=None, alert=ALERT.NONE, reason=None, timestamp=None):
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
@ -275,7 +321,8 @@ class DeviceStatus(dict):
# 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()
self[KEYS.NOTIFICATION_FLAGS] = d.enable_notifications(
)
# If we've been inactive for a long time, forget anything
# about the battery.
@ -287,7 +334,8 @@ class DeviceStatus(dict):
# 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)
_log.debug("%s pushing device settings %s", d,
d.settings)
for s in d.settings:
s.apply()

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,49 +26,71 @@ 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())
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, '
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='?',
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',
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, '
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='?',
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.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='?',
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',
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')
@ -103,7 +124,8 @@ def _find_receiver(receivers, name):
assert name
for r in receivers:
if name in r.name.lower() or (r.serial is not None and name == r.serial.lower()):
if name in r.name.lower() or (r.serial is not None
and name == r.serial.lower()):
return r
@ -128,10 +150,9 @@ def _find_device(receivers, name):
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()):
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)
@ -163,7 +184,8 @@ def run(cli_args=None, hidraw_path=None):
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]))
_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,28 +19,30 @@
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)
print('#', s.label)
if verbose:
if s.description:
print ('#', s.description.replace('\n', ' '))
print('#', s.description.replace('\n', ' '))
if s.kind == _settings.KIND.toggle:
print ('# possible values: on/true/t/yes/y/1 or off/false/f/no/n/0')
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')
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)')
print(s.name, '= ? (failed to read from device)')
else:
print (s.name, '= %r' % value)
print(s.name, '= %r' % value)
def run(receivers, args, find_receiver, find_device):
@ -59,9 +61,9 @@ def run(receivers, args, find_receiver, find_device):
_configuration.attach_to(dev)
if not args.setting:
print (dev.name, '(%s) [%s:%s]' % (dev.codename, dev.wpid, dev.serial))
print(dev.name, '(%s) [%s:%s]' % (dev.codename, dev.wpid, dev.serial))
for s in dev.settings:
print ('')
print('')
_print_setting(s)
return
@ -88,7 +90,8 @@ def run(receivers, args, find_receiver, find_device):
elif value.lower() in ('false', 'no', 'off', 'f', 'n'):
value = False
else:
raise Exception("don't know how to interpret '%s' as boolean" % value)
raise Exception("don't know how to interpret '%s' as boolean" %
value)
elif setting.choices:
value = args.value.lower()
@ -96,20 +99,25 @@ def run(receivers, args, find_receiver, find_device):
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)
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]
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]
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)))
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:
@ -123,5 +131,6 @@ def run(receivers, args, find_receiver, find_device):
result = setting.write(value)
if result is None:
raise Exception("failed to set '%s' = '%s' [%r]" % (setting.name, str(value), value))
raise Exception("failed to set '%s' = '%s' [%r]" %
(setting.name, str(value), value))
_print_setting(setting, False)

View File

@ -19,7 +19,6 @@
from __future__ import absolute_import, division, print_function, unicode_literals
from time import time as _timestamp
from logitech_receiver import (
@ -27,7 +26,7 @@ from logitech_receiver import (
hidpp10 as _hidpp10,
status as _status,
notifications as _notifications,
)
)
def run(receivers, args, find_receiver, _ignore):
@ -42,12 +41,15 @@ def run(receivers, args, find_receiver, _ignore):
receiver = receivers[0]
assert receiver
receiver.status = _status.ReceiverStatus(receiver, lambda *args, **kwargs: None)
receiver.status = _status.ReceiverStatus(receiver,
lambda *args, **kwargs: None)
# check if it's necessary to set the notification flags
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)
_hidpp10.set_notification_flags(
receiver,
old_notification_flags | _hidpp10.NOTIFICATION_FLAG.wireless)
# get all current devices
known_devices = [dev.number for dev in receiver]
@ -61,14 +63,17 @@ def run(receivers, args, find_receiver, _ignore):
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
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)
receiver.set_lock(False, timeout=timeout)
print ('Pairing: turn your new device on (timing out in', timeout, 'seconds).')
print('Pairing: turn your new device on (timing out in', timeout,
'seconds).')
# the lock-open notification may come slightly later, wait for it a bit
pairing_start = _timestamp()
@ -88,11 +93,11 @@ def run(receivers, args, find_receiver, _ignore):
if receiver.status.new_device:
dev = receiver.status.new_device
print ('Paired device %d: %s (%s) [%s:%s]' % (dev.number, dev.name, dev.codename, dev.wpid, dev.serial))
print('Paired device %d: %s (%s) [%s:%s]' %
(dev.number, dev.name, dev.codename, dev.wpid, dev.serial))
else:
error = receiver.status.get(_status.KEYS.ERROR)
if error :
if error:
raise Exception("pairing failed: %s" % error)
else :
print ('Paired a device') # this is better than an error
else:
print('Paired a device') # this is better than an error

View File

@ -19,7 +19,6 @@
from __future__ import absolute_import, division, print_function, unicode_literals
from time import time as _timestamp
from logitech_receiver.common import strhex as _strhex
@ -29,12 +28,13 @@ from logitech_receiver import (
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
@ -50,21 +50,34 @@ def run(receivers, args, find_receiver, _ignore):
_print_receiver(receiver)
print (' Register Dump')
print(' Register Dump')
register = receiver.read_register(_R.notifications)
print(" Notification Register %#04x: %s" % (_R.notifications%0x100,'0x'+_strhex(register) if register else "None"))
print(" Notification Register %#04x: %s" %
(_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"))
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(" 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"))
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"))
print(" Pairing Name %#04x %#02x: %s" %
(_R.receiver_info % 0x100, 0x40 + device,
register[2:2 + ord(register[1:2])] if register else "None"))
for sub_reg in range(0,5):
for sub_reg in range(0, 5):
register = receiver.read_register(_R.firmware, sub_reg)
print(" Firmware %#04x %#04x: %s" % (_R.firmware%0x100,sub_reg,'0x'+_strhex(register) if register else "None"))
print(" Firmware %#04x %#04x: %s" %
(_R.firmware % 0x100, sub_reg,
'0x' + _strhex(register) if register else "None"))

View File

@ -19,46 +19,49 @@
from __future__ import absolute_import, division, print_function, unicode_literals
from logitech_receiver import (
hidpp10 as _hidpp10,
hidpp20 as _hidpp20,
from logitech_receiver import (hidpp10 as _hidpp10, hidpp20 as _hidpp20,
special_keys as _special_keys,
settings_templates as _settings_templates
)
settings_templates as _settings_templates)
from logitech_receiver.common import NamedInt as _NamedInt
def _print_receiver(receiver):
paired_count = receiver.count()
print (receiver.name)
print (' Device path :', receiver.path)
print (' USB id : 046d:%s' % receiver.product_id)
print (' Serial :', receiver.serial)
print(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(' %-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))
notification_names = _hidpp10.NOTIFICATION_FLAG.flag_names(
notification_flags)
print(' Notifications: %s (0x%06X)' %
(', '.join(notification_names), notification_flags))
else:
print (' Notifications: (none)')
print(' Notifications: (none)')
activity = receiver.read_register(_hidpp10.REGISTERS.devices_activity)
if activity:
activity = [(d, ord(activity[d - 1:d])) for d in range(1, receiver.max_devices)]
activity_text = ', '.join(('%d=%d' % (d, a)) for d, a in activity if a > 0)
print (' Device activity counters:', activity_text or '(empty)')
activity = [(d, ord(activity[d - 1:d]))
for d in range(1, receiver.max_devices)]
activity_text = ', '.join(
('%d=%d' % (d, a)) for d, a in activity if a > 0)
print(' Device activity counters:', activity_text or '(empty)')
def _battery_text(level) :
def _battery_text(level):
if level is None:
return 'N/A'
elif isinstance(level, _NamedInt):
@ -66,40 +69,46 @@ def _battery_text(level) :
else:
return '%d%%' % level
def _print_device(dev):
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)
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)
print(' Protocol : HID++ %1.1f' % dev.protocol)
else:
print (' Protocol : unknown (device is offline)')
print(' Protocol : unknown (device is offline)')
if dev.polling_rate:
print (' Polling rate :', dev.polling_rate, 'ms (%dHz)' % (1000 // dev.polling_rate))
print (' Serial number:', dev.serial)
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(' %11s:' % fw.kind,
(fw.name + ' ' + fw.version).strip())
if dev.power_switch_location:
print (' The power switch is located on the %s.' % dev.power_switch_location)
print(' The power switch is located on the %s.' %
dev.power_switch_location)
if dev.online:
notification_flags = _hidpp10.get_notification_flags(dev)
if notification_flags is not None:
if notification_flags:
notification_names = _hidpp10.NOTIFICATION_FLAG.flag_names(notification_flags)
print (' Notifications: %s (0x%06X).' % (', '.join(notification_names), 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).')
print(' Notifications: (none).')
if dev.online and dev.features:
print (' Supports %d HID++ 2.0 features:' % len(dev.features))
print(' Supports %d HID++ 2.0 features:' % len(dev.features))
dev.persister = None # Give the device a fake persister
dev_settings = []
_settings_templates.check_feature_settings(dev, dev_settings)
@ -108,7 +117,8 @@ def _print_device(dev):
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)))
print(' %2d: %-22s {%04X} %s' %
(index, feature, feature, ', '.join(flags)))
if feature == _hidpp20.FEATURE.HIRES_WHEEL:
wheel = _hidpp20.get_hires_wheel(dev)
if wheel:
@ -138,7 +148,8 @@ def _print_device(dev):
mouse_pointer = _hidpp20.get_mouse_pointer_info(dev)
if mouse_pointer:
print(" DPI: %s" % mouse_pointer['dpi'])
print(" Acceleration: %s" % mouse_pointer['acceleration'])
print(" Acceleration: %s" %
mouse_pointer['acceleration'])
if mouse_pointer['suggest_os_ballistics']:
print(" Use OS ballistics")
else:
@ -148,19 +159,25 @@ def _print_device(dev):
else:
print(" No vertical tuning, standard mice")
if feature == _hidpp20.FEATURE.VERTICAL_SCROLLING:
vertical_scrolling_info = _hidpp20.get_vertical_scrolling_info(dev)
vertical_scrolling_info = _hidpp20.get_vertical_scrolling_info(
dev)
if vertical_scrolling_info:
print(" Roller type: %s" % vertical_scrolling_info['roller'])
print(" Ratchet per turn: %s" % vertical_scrolling_info['ratchet'])
print(" Scroll lines: %s" % vertical_scrolling_info['lines'])
print(" Roller type: %s" %
vertical_scrolling_info['roller'])
print(" Ratchet per turn: %s" %
vertical_scrolling_info['ratchet'])
print(" Scroll lines: %s" %
vertical_scrolling_info['lines'])
elif feature == _hidpp20.FEATURE.HI_RES_SCROLLING:
scrolling_mode, scrolling_resolution = _hidpp20.get_hi_res_scrolling_info(dev)
scrolling_mode, scrolling_resolution = _hidpp20.get_hi_res_scrolling_info(
dev)
if scrolling_mode:
print(" Hi-res scrolling enabled")
else:
print(" Hi-res scrolling disabled")
if scrolling_resolution:
print(" Hi-res scrolling multiplier: %s" % scrolling_resolution)
print(" Hi-res scrolling multiplier: %s" %
scrolling_resolution)
elif feature == _hidpp20.FEATURE.POINTER_SPEED:
pointer_speed = _hidpp20.get_pointer_speed_info(dev)
if pointer_speed:
@ -171,23 +188,28 @@ def _print_device(dev):
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")
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) )
print(" %s: %s" % (setting.label, v))
if dev.online and dev.keys:
print (' Has %d reprogrammable keys:' % len(dev.keys))
print(' Has %d reprogrammable keys:' % len(dev.keys))
for k in dev.keys:
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)))
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))
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:
@ -195,17 +217,19 @@ def _print_device(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))
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))
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.')
print(' Battery status unavailable.')
else:
print (' Battery: unknown (device is offline).')
print(' Battery: unknown (device is offline).')
def run(receivers, args, find_receiver, find_device):
@ -220,12 +244,12 @@ def run(receivers, args, find_receiver, find_device):
count = r.count()
if count:
for dev in r:
print ('')
print('')
_print_device(dev)
count -= 1
if not count:
break
print ('')
print('')
return
dev = find_receiver(receivers, device_name)

View File

@ -28,12 +28,15 @@ def run(receivers, args, find_receiver, find_device):
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))
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))
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,17 +25,16 @@ 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 = {}
@ -74,7 +73,11 @@ def save():
try:
with open(_file_path, 'w') as config_file:
_json_save(_configuration, config_file, skipkeys=True, indent=2, sort_keys=True)
_json_save(_configuration,
config_file,
skipkeys=True,
indent=2,
sort_keys=True)
if _log.isEnabledFor(_INFO):
_log.info("saved %s to %s", _configuration, _file_path)

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,32 +30,63 @@ 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)
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',
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',
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,
arg_parser.add_argument('action',
nargs=argparse.REMAINDER,
choices=_cli.actions,
help='optional actions to perform')
args = arg_parser.parse_args()
@ -74,15 +104,18 @@ def _parse_arguments():
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')
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)
logging.info("language %s (%s), translations path %s",
_i18n.language, _i18n.encoding, _i18n.path)
return args
@ -115,7 +148,8 @@ def main():
_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')
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

View File

@ -25,18 +25,23 @@ from solaar import NAME as _NAME
#
#
def _find_locale_path(lc_domain):
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'))
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
for location in prefix_share, src_share:
mo_files = _glob(_path.join(location, 'locale', '*', 'LC_MESSAGES', lc_domain + '.mo'))
mo_files = _glob(
_path.join(location, 'locale', '*', 'LC_MESSAGES',
lc_domain + '.mo'))
if mo_files:
return _path.join(location, 'locale')

View File

@ -24,35 +24,33 @@ 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,
return _GHOST_DEVICE(receiver=device.receiver,
number=device.number,
name=device.name,
kind=device.kind,
status=None,
online=False)
#
#
#
@ -66,7 +64,8 @@ class ReceiverListener(_listener.EventsListener):
"""Keeps the status of a Receiver.
"""
def __init__(self, receiver, status_changed_callback):
super(ReceiverListener, self).__init__(receiver, self._notifications_handler)
super(ReceiverListener, self).__init__(receiver,
self._notifications_handler)
# no reason to enable polling yet
# self.tick_period = _POLL_TICK
# self._last_tick = 0
@ -77,9 +76,11 @@ class ReceiverListener(_listener.EventsListener):
def has_started(self):
if _log.isEnabledFor(_INFO):
_log.info("%s: notifications listener has started (%s)", self.receiver, self.receiver.handle)
_log.info("%s: notifications listener has started (%s)",
self.receiver, self.receiver.handle)
notification_flags = self.receiver.enable_notifications()
self.receiver.status[_status.KEYS.NOTIFICATION_FLAGS] = notification_flags
self.receiver.status[
_status.KEYS.NOTIFICATION_FLAGS] = notification_flags
self.receiver.notify_devices()
self._status_changed(self.receiver) #, _status.ALERT.NOTIFICATION)
@ -182,9 +183,11 @@ class ReceiverListener(_listener.EventsListener):
return
# a device notification
if not(0 < n.devnumber <= self.receiver.max_devices):
if not (0 < n.devnumber <= self.receiver.max_devices):
if _log.isEnabledFor(_WARNING):
_log.warning(_("Unexpected device number (%s) in notification %s." % (n.devnumber, n)))
_log.warning(
_("Unexpected device number (%s) in notification %s." %
(n.devnumber, n)))
return
already_known = n.devnumber in self.receiver
@ -204,10 +207,14 @@ class ReceiverListener(_listener.EventsListener):
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:
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
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:
@ -216,7 +223,8 @@ class ReceiverListener(_listener.EventsListener):
dev = self.receiver[n.devnumber]
if not dev:
_log.warn("%s: received %s for invalid device %d: %r", self.receiver, n, n.devnumber, dev)
_log.warn("%s: received %s for invalid device %d: %r",
self.receiver, n, n.devnumber, dev)
return
# Apply settings every time the device connects
@ -243,9 +251,12 @@ class ReceiverListener(_listener.EventsListener):
dev.ping()
def __str__(self):
return '<ReceiverListener(%s,%s)>' % (self.receiver.path, self.receiver.handle)
return '<ReceiverListener(%s,%s)>' % (self.receiver.path,
self.receiver.handle)
__unicode__ = __str__
#
#
#
@ -294,10 +305,11 @@ def stop_all():
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):
def ping_all(resuming=False):
for l in _all_listeners.values():
count = l.receiver.count()
if count:
@ -315,6 +327,7 @@ 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'
@ -351,8 +364,9 @@ def _process_receiver_event(action, device_info):
# 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) :
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

@ -35,6 +35,7 @@ except ImportError:
#
#
class TaskRunner(_Thread):
def __init__(self, name):
super(TaskRunner, self).__init__(name=name)

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,6 +39,7 @@ GLib.threads_init()
#
#
def _error_dialog(reason, object):
_log.error("error: %s %s", reason, object)
@ -55,12 +54,14 @@ def _error_dialog(reason, object):
'\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)
raise Exception("ui.error_dialog: don't know how to handle (%s, %s)",
reason, object)
assert title
assert text
m = Gtk.MessageDialog(None, Gtk.DialogFlags.MODAL, Gtk.MessageType.ERROR, Gtk.ButtonsType.CLOSE, text)
m = Gtk.MessageDialog(None, Gtk.DialogFlags.MODAL, Gtk.MessageType.ERROR,
Gtk.ButtonsType.CLOSE, text)
m.set_title(title)
m.run()
m.destroy()
@ -70,15 +71,19 @@ def 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)
#
#
#
@ -88,7 +93,8 @@ 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())
_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
@ -138,15 +144,19 @@ 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)
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(
'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)
#
#
#

View File

@ -36,16 +36,19 @@ def _create():
about.set_program_name(NAME)
about.set_version(__version__)
about.set_comments(_("Shows status of devices connected\nthrough wireless Logitech receivers."))
about.set_comments(
_("Shows status of devices connected\nthrough wireless Logitech receivers."
))
about.set_logo_icon_name(NAME.lower())
about.set_copyright('© 2012-2013 Daniel Pavel')
about.set_license_type(Gtk.License.GPL_2_0)
about.set_authors(('Daniel Pavel http://github.com/pwr',))
about.set_authors(('Daniel Pavel http://github.com/pwr', ))
try:
about.add_credit_section(_("GUI design"), ('Julien Gascard', 'Daniel Pavel'))
about.add_credit_section(_("GUI design"),
('Julien Gascard', 'Daniel Pavel'))
about.add_credit_section(_("Testing"), (
'Douglas Wagner',
'Julien Gascard',
@ -83,6 +86,7 @@ def _create():
def _hide(dialog, event):
dialog.hide()
return True
about.connect('delete-event', _hide)
return about

View File

@ -25,13 +25,13 @@ 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)
@ -50,6 +50,7 @@ def make_toggle(name, label, function, stock_id=None, *args):
action.connect('activate', function, *args)
return action
#
#
#
@ -62,16 +63,20 @@ 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
@ -86,12 +91,14 @@ def pair(window, receiver):
from ..ui import error_dialog
def unpair(window, device):
assert device
assert device.kind is not None
qdialog = Gtk.MessageDialog(window, 0,
Gtk.MessageType.QUESTION, Gtk.ButtonsType.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)

View File

@ -30,6 +30,7 @@ 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)
@ -70,6 +71,7 @@ def _write_async_key_value(setting, key, value, sbox):
#
#
def _create_toggle_control(setting):
def _switch_notify(switch, _ignore, s):
if switch.get_sensitive():
@ -79,6 +81,7 @@ def _create_toggle_control(setting):
c.connect('notify::active', _switch_notify, setting)
return c
def _create_choice_control(setting):
def _combo_notify(cbbox, s):
if cbbox.get_sensitive():
@ -91,6 +94,7 @@ def _create_choice_control(setting):
c.connect('changed', _combo_notify, setting)
return c
def _create_map_choice_control(setting):
def _map_value_notify_key(cbbox, s):
setting, valueBox = s
@ -102,15 +106,20 @@ def _create_map_choice_control(setting):
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 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())
_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
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:
@ -127,14 +136,16 @@ def _create_map_choice_control(setting):
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))
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))
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
@ -144,13 +155,11 @@ def _create_slider_control(setting):
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.gtk_range.connect('value-changed', lambda _, c: c._changed(),
self)
def _write(self):
_write_async(self.setting,
int(self.gtk_range.get_value()),
_write_async(self.setting, int(self.gtk_range.get_value()),
self.gtk_range.get_parent())
self.timer.cancel()
@ -164,10 +173,12 @@ def _create_slider_control(setting):
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)
@ -175,7 +186,8 @@ def _create_sbox(s):
spinner = Gtk.Spinner()
spinner.set_tooltip_text(_("Working") + '...')
failed = Gtk.Image.new_from_icon_name('dialog-warning', Gtk.IconSize.SMALL_TOOLBAR)
failed = Gtk.Image.new_from_icon_name('dialog-warning',
Gtk.IconSize.SMALL_TOOLBAR)
failed.set_tooltip_text(_("Read/write operation failed."))
if s.kind == _SETTING_KIND.toggle:
@ -192,16 +204,19 @@ def _create_sbox(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}
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:
@ -222,7 +237,8 @@ def _create_sbox(s):
def _update_setting_item(sbox, value, is_online=True):
_ignore, failed, spinner, control = sbox.get_children() # depends on box layout
_ignore, failed, spinner, control = sbox.get_children(
) # depends on box layout
spinner.set_visible(False)
spinner.stop()
@ -246,6 +262,7 @@ def _update_setting_item(sbox, value, is_online=True):
raise Exception("NotImplemented")
control.set_sensitive(True)
#
#
#
@ -254,6 +271,7 @@ def _update_setting_item(sbox, value, is_online=True):
_box = None
_items = {}
def create():
global _box
assert _box is None

View File

@ -52,15 +52,23 @@ def _look_for_application_icons():
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')))
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'))
repo_share = _path.normpath(
_path.join(_path.dirname(__file__), '..', '..', '..', 'share'))
setuptools_share = _path.normpath(
_path.join(_path.dirname(__file__), '..', '..', 'share'))
del _sys
share_solaar = [prefix_share] + list(_path.join(x, 'solaar') for x in [src_share, local_share, setuptools_share, repo_share] + data_dirs.split(':'))
share_solaar = [prefix_share] + list(
_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):
@ -76,6 +84,7 @@ def _look_for_application_icons():
_default_theme = None
_use_symbolic_icons = False
def _init_icon_paths():
global _default_theme
if _default_theme:
@ -97,10 +106,12 @@ def _init_icon_paths():
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):
@ -110,35 +121,43 @@ def battery(level=None, charging=False):
_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()
if level is None or level < 0:
return 'battery-missing' + ( '-symbolic' if _use_symbolic_icons else '' )
return 'battery-missing' + ('-symbolic' if _use_symbolic_icons else '')
level_name = _first_res(level, ((90, 'full'), (50, 'good'), (20, 'low'),
(5, 'caution'), (0, 'empty')))
return 'battery-%s%s%s' % (level_name, '-charging' if charging else '',
'-symbolic' if _use_symbolic_icons else '')
level_name = _first_res(level,((90,'full'), (50,'good'), (20,'low'), (5,'caution'), (0,'empty')))
return 'battery-%s%s%s' % (level_name, '-charging' if charging else '', '-symbolic' if _use_symbolic_icons else '')
#
#
#
def lux(level=None):
if level is None or level < 0:
return 'light_unknown'
return 'light_%03d' % (20 * ((level + 50) // 100))
#
#
#
_ICON_SETS = {}
def device_icon_set(name='_', kind=None):
icon_set = _ICON_SETS.get(name)
if icon_set is None:
@ -154,8 +173,8 @@ def device_icon_set(name='_', kind=None):
elif str(kind) == 'touchpad':
names += ('input-mouse', 'input-tablet')
elif str(kind) == 'trackball':
names += ('input-mouse',)
names += ('input-' + str(kind),)
names += ('input-mouse', )
names += ('input-' + str(kind), )
# names += (name.replace(' ', '-'),)
source = Gtk.IconSource.new()

View File

@ -21,7 +21,6 @@
from __future__ import absolute_import, division, print_function, unicode_literals
from solaar.i18n import _
#
@ -66,7 +65,6 @@ if available:
available = False
return available and Notify.is_initted()
def uninit():
if available and Notify.is_initted():
if _log.isEnabledFor(_INFO):
@ -74,7 +72,6 @@ if available:
_notifications.clear()
Notify.uninit()
# def toggle(action):
# if action.get_active():
# init()
@ -83,7 +80,6 @@ if available:
# action.set_sensitive(available)
# return action.get_active()
def alert(reason, icon=None):
assert reason
@ -108,7 +104,6 @@ if available:
except Exception:
_log.exception("showing %s", n)
def show(dev, reason=None, icon=None):
"""Show a notification with title and text."""
if available and Notify.is_initted():

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
@ -86,7 +85,8 @@ def _check_lock_state(assistant, receiver, count=2):
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)
GLib.timeout_add(_STATUS_CHECK, _check_lock_state, assistant,
receiver, count - 1)
else:
_pairing_failed(assistant, receiver, 'failed to open pairing lock')
return False
@ -105,10 +105,12 @@ def _prepare(assistant, page, receiver):
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)
GLib.timeout_add(_STATUS_CHECK, _check_lock_state, assistant,
receiver)
assistant.set_page_complete(page, True)
else:
GLib.idle_add(_pairing_failed, assistant, receiver, 'the pairing lock did not open')
GLib.idle_add(_pairing_failed, assistant, receiver,
'the pairing lock did not open')
else:
assistant.remove_page(0)
@ -132,14 +134,19 @@ def _pairing_failed(assistant, receiver, error):
header = _("Pairing failed") + ': ' + _(str(error)) + '.'
if 'timeout' in str(error):
text = _("Make sure your device is within range, and has a decent battery charge.")
text = _(
"Make sure your device is within range, and has a decent battery charge."
)
elif str(error) == 'device not supported':
text = _("A new device was detected, but it is not compatible with this receiver.")
text = _(
"A new device was detected, but it is not compatible with this receiver."
)
elif 'many' in str(error):
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)
_create_page(assistant, Gtk.AssistantPageType.SUMMARY, header,
'dialog-error', text)
assistant.next_page()
assistant.commit()
@ -176,11 +183,17 @@ def _pairing_succeeded(assistant, receiver, 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.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)
page.show_all()
@ -194,21 +207,25 @@ def create(receiver):
assert receiver.kind is None
assistant = Gtk.Assistant()
assistant.set_title(_('%(receiver_name)s: pair new device') % { 'receiver_name': receiver.name })
assistant.set_title(
_('%(receiver_name)s: pair new device') %
{'receiver_name': receiver.name})
assistant.set_icon_name('list-add')
assistant.set_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.")
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 += _("\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)
_("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)

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
@ -48,6 +47,7 @@ _RECEIVER_SEPARATOR = ('~', None, None, None)
#
#
def _create_menu(quit_handler):
menu = Gtk.Menu()
@ -60,7 +60,11 @@ def _create_menu(quit_handler):
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())
menu.append(
make('application-exit',
_("Quit"),
quit_handler,
stock_id=Gtk.STOCK_QUIT).create_menu_item())
del about, make
menu.show_all()
@ -69,6 +73,8 @@ def _create_menu(quit_handler):
_last_scroll = 0
def _scroll(tray_icon, event, direction=None):
if direction is None:
direction = event.direction
@ -163,24 +169,24 @@ try:
from gi.repository import AppIndicator3
if _log.isEnabledFor(_DEBUG):
_log.debug("using %sAppIndicator3" % ('Ayatana ' if ayatana_appindicator_found else ''))
_log.debug("using %sAppIndicator3" %
('Ayatana ' if ayatana_appindicator_found else ''))
# Defense against AppIndicator3 bug that treats files in current directory as icon files
# 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)
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()
ind = AppIndicator3.Indicator.new_with_path(
'indicator-solaar',
_icon_file(_icons.TRAY_INIT),
AppIndicator3.IndicatorCategory.HARDWARE,
':'.join(theme_paths))
'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), '')
@ -191,11 +197,9 @@ try:
return ind
def _destroy(indicator):
indicator.set_status(AppIndicator3.IndicatorStatus.PASSIVE)
def _update_tray_icon():
if _picked_device:
_ignore, _ignore, name, device_status = _picked_device
@ -214,19 +218,19 @@ try:
# 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_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)
GLib.timeout_add(10 * 1000, _icon.set_status,
AppIndicator3.IndicatorStatus.ACTIVE)
except ImportError:
@ -240,16 +244,15 @@ except ImportError:
icon.set_tooltip_text(NAME)
icon.connect('activate', _window_toggle)
icon.connect('scroll-event', _scroll)
icon.connect('popup-menu',
lambda icon, button, time: menu.popup(None, None, icon.position_menu, icon, button, time))
icon.connect(
'popup-menu', lambda icon, button, time: menu.popup(
None, None, icon.position_menu, icon, button, time))
return icon
def _destroy(icon):
icon.set_visible(False)
def _update_tray_icon():
tooltip_lines = _generate_tooltip_lines()
tooltip = '\n'.join(tooltip_lines).rstrip('\n')
@ -265,11 +268,9 @@ except ImportError:
tray_icon_name = _icons.TRAY_OKAY if _devices_info else _icons.TRAY_ATTENTION
_icon.set_from_icon_name(tray_icon_name)
def _update_menu_icon(image_widget, icon_name):
image_widget.set_from_icon_name(icon_name, _MENU_ICON_SIZE)
_icon_before_attention = None
def _blink(count):
@ -290,10 +291,12 @@ except ImportError:
_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")
@ -320,7 +323,8 @@ def _generate_description_lines():
yield '\t%s <small>(' % p + _("offline") + ')</small>'
else:
if status:
yield '<b>%s</b> <small>(' % name + _("no status") + ')</small>'
yield '<b>%s</b> <small>(' % name + _(
"no status") + ')</small>'
else:
yield '<b>%s</b> <small>(' % name + _("offline") + ')</small>'
yield ''
@ -352,6 +356,7 @@ def _pick_device_with_lowest_battery():
#
#
def _add_device(device):
assert device
assert device.receiver
@ -377,17 +382,20 @@ def _add_device(device):
break
index = index + 1
new_device_info = (receiver_path, device.number, device.name, device.status)
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 = ' '
new_menu_item = Gtk.ImageMenuItem.new_with_label(label_prefix + device.name)
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)
new_menu_item.connect('activate', _window_popup, receiver_path,
device.number)
_menu.insert(new_menu_item, index)
return index
@ -416,7 +424,8 @@ def _add_receiver(receiver):
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.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)
@ -461,6 +470,7 @@ def _update_menu_item(index, device):
image_widget.set_sensitive(bool(device.online))
_update_menu_icon(image_widget, icon_name)
#
#
#
@ -476,6 +486,7 @@ _devices_info = []
_menu = None
_icon = None
def init(_quit_handler):
global _menu, _icon
assert _menu is None
@ -507,7 +518,8 @@ def update(device=None):
receiver_path = device.path
if is_alive:
index = None
for idx, (path, _ignore, _ignore, _ignore) in enumerate(_devices_info):
for idx, (path, _ignore, _ignore,
_ignore) in enumerate(_devices_info):
if path == receiver_path:
index = idx
break
@ -522,7 +534,8 @@ def update(device=None):
is_paired = bool(device)
receiver_path = device.receiver.path
index = None
for idx, (path, number, _ignore, _ignore) in enumerate(_devices_info):
for idx, (path, number, _ignore,
_ignore) in enumerate(_devices_info):
if path == receiver_path and number == device.number:
index = idx
@ -541,7 +554,8 @@ def update(device=None):
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 (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()

View File

@ -54,7 +54,14 @@ except (ValueError, AttributeError):
_CAN_SET_ROW_NONE = ''
# tree model columns
_COLUMN = _NamedInts(PATH=0, NUMBER=1, ACTIVE=2, NAME=3, ICON=4, STATUS_TEXT=5, STATUS_ICON=6, DEVICE=7)
_COLUMN = _NamedInts(PATH=0,
NUMBER=1,
ACTIVE=2,
NAME=3,
ICON=4,
STATUS_TEXT=5,
STATUS_ICON=6,
DEVICE=7)
_COLUMN_TYPES = (str, int, bool, str, str, str, str, TYPE_PYOBJECT)
_TREE_SEPATATOR = (None, 0, False, None, None, None, None, None)
assert len(_TREE_SEPATATOR) == len(_COLUMN_TYPES)
@ -64,7 +71,13 @@ assert len(_COLUMN_TYPES) == len(_COLUMN)
# create UI layout
#
def _new_button(label, icon_name=None, icon_size=_NORMAL_BUTTON_ICON_SIZE, tooltip=None, toggle=False, clicked=None):
def _new_button(label,
icon_name=None,
icon_size=_NORMAL_BUTTON_ICON_SIZE,
tooltip=None,
toggle=False,
clicked=None):
if toggle:
b = Gtk.ToggleButton()
else:
@ -163,8 +176,12 @@ def _create_buttons_box():
bb = Gtk.ButtonBox(Gtk.Orientation.HORIZONTAL)
bb.set_layout(Gtk.ButtonBoxStyle.END)
bb._details = _new_button(None, 'dialog-information', _SMALL_BUTTON_ICON_SIZE,
tooltip=_("Show Technical Details"), toggle=True, clicked=_update_details)
bb._details = _new_button(None,
'dialog-information',
_SMALL_BUTTON_ICON_SIZE,
tooltip=_("Show Technical Details"),
toggle=True,
clicked=_update_details)
bb.add(bb._details)
bb.set_child_secondary(bb._details, True)
bb.set_child_non_homogeneous(bb._details, True)
@ -177,7 +194,9 @@ def _create_buttons_box():
assert receiver.kind is None
_action.pair(_window, receiver)
bb._pair = _new_button(_("Pair new device"), 'list-add', clicked=_pair_new_device)
bb._pair = _new_button(_("Pair new device"),
'list-add',
clicked=_pair_new_device)
bb.add(bb._pair)
def _unpair_current_device(trigger):
@ -188,7 +207,9 @@ def _create_buttons_box():
assert device.kind is not None
_action.unpair(_window, device)
bb._unpair = _new_button(_("Unpair"), 'edit-delete', clicked=_unpair_current_device)
bb._unpair = _new_button(_("Unpair"),
'edit-delete',
clicked=_unpair_current_device)
bb.add(bb._unpair)
return bb
@ -214,7 +235,8 @@ def _create_info_panel():
b1.pack_start(p._icon, False, False, 0)
p.pack_start(b1, False, False, 0)
p.pack_start(Gtk.Separator.new(Gtk.Orientation.HORIZONTAL), False, False, 0) # spacer
p.pack_start(Gtk.Separator.new(Gtk.Orientation.HORIZONTAL), False, False,
0) # spacer
p._receiver = _create_receiver_panel()
p.pack_start(p._receiver, True, True, 0)
@ -222,7 +244,8 @@ def _create_info_panel():
p._device = _create_device_panel()
p.pack_start(p._device, True, True, 0)
p.pack_start(Gtk.Separator.new(Gtk.Orientation.HORIZONTAL), False, False, 0) # spacer
p.pack_start(Gtk.Separator.new(Gtk.Orientation.HORIZONTAL), False, False,
0) # spacer
p._buttons = _create_buttons_box()
p.pack_end(p._buttons, False, False, 0)
@ -244,6 +267,7 @@ def _create_tree(model):
def _is_separator(model, item, _ignore=None):
return model.get_value(item, _COLUMN.PATH) is None
tree.set_row_separator_func(_is_separator, None)
icon_cell_renderer = Gtk.CellRendererPixbuf()
@ -265,16 +289,20 @@ def _create_tree(model):
status_cell_renderer.set_property('scale', 0.85)
status_cell_renderer.set_property('xalign', 1)
status_column = Gtk.TreeViewColumn('status text', status_cell_renderer)
status_column.add_attribute(status_cell_renderer, 'sensitive', _COLUMN.ACTIVE)
status_column.add_attribute(status_cell_renderer, 'text', _COLUMN.STATUS_TEXT)
status_column.add_attribute(status_cell_renderer, 'sensitive',
_COLUMN.ACTIVE)
status_column.add_attribute(status_cell_renderer, 'text',
_COLUMN.STATUS_TEXT)
status_column.set_expand(True)
tree.append_column(status_column)
battery_cell_renderer = Gtk.CellRendererPixbuf()
battery_cell_renderer.set_property('stock-size', _TREE_ICON_SIZE)
battery_column = Gtk.TreeViewColumn('status icon', battery_cell_renderer)
battery_column.add_attribute(battery_cell_renderer, 'sensitive', _COLUMN.ACTIVE)
battery_column.add_attribute(battery_cell_renderer, 'icon-name', _COLUMN.STATUS_ICON)
battery_column.add_attribute(battery_cell_renderer, 'sensitive',
_COLUMN.ACTIVE)
battery_column.add_attribute(battery_cell_renderer, 'icon-name',
_COLUMN.STATUS_ICON)
tree.append_column(battery_column)
return tree
@ -307,11 +335,15 @@ def _create_window_layout():
bottom_buttons_box = Gtk.ButtonBox(Gtk.Orientation.HORIZONTAL)
bottom_buttons_box.set_layout(Gtk.ButtonBoxStyle.START)
bottom_buttons_box.set_spacing(20)
quit_button = _new_button(_("Quit") + ' ' + NAME, 'application-exit',
icon_size=_SMALL_BUTTON_ICON_SIZE, clicked=destroy)
quit_button = _new_button(_("Quit") + ' ' + NAME,
'application-exit',
icon_size=_SMALL_BUTTON_ICON_SIZE,
clicked=destroy)
bottom_buttons_box.add(quit_button)
about_button = _new_button(_("About") + ' ' + NAME, 'help-about',
icon_size=_SMALL_BUTTON_ICON_SIZE, clicked=_show_about_window)
about_button = _new_button(_("About") + ' ' + NAME,
'help-about',
icon_size=_SMALL_BUTTON_ICON_SIZE,
clicked=_show_about_window)
bottom_buttons_box.add(about_button)
# solaar_version = Gtk.Label()
@ -354,10 +386,12 @@ def _create(delete_action):
return window
#
# window updates
#
def _find_selected_device():
selection = _tree.get_selection()
model, item = selection.get_selected()
@ -368,7 +402,8 @@ def _find_selected_device_id():
selection = _tree.get_selection()
model, item = selection.get_selected()
if item:
return _model.get_value(item, _COLUMN.PATH), _model.get_value(item, _COLUMN.NUMBER)
return _model.get_value(item, _COLUMN.PATH), _model.get_value(
item, _COLUMN.NUMBER)
# triggered by changing selection in the tree
@ -401,7 +436,8 @@ def _receiver_row(receiver_path, receiver=None):
icon_name = _icons.device_icon_name(receiver.name)
status_text = None
status_icon = None
row_data = (receiver_path, 0, True, receiver.name, icon_name, status_text, status_icon, receiver)
row_data = (receiver_path, 0, True, receiver.name, icon_name,
status_text, status_icon, receiver)
assert len(row_data) == len(_TREE_SEPATATOR)
if _log.isEnabledFor(_DEBUG):
_log.debug("new receiver row %s", row_data)
@ -416,7 +452,8 @@ def _device_row(receiver_path, device_number, device=None):
assert receiver_path
assert device_number is not None
receiver_row = _receiver_row(receiver_path, None if device is None else device.receiver)
receiver_row = _receiver_row(receiver_path,
None if device is None else device.receiver)
item = _model.iter_children(receiver_row)
new_child_index = 0
while item:
@ -434,18 +471,23 @@ def _device_row(receiver_path, device_number, device=None):
icon_name = _icons.device_icon_name(device.name, device.kind)
status_text = None
status_icon = None
row_data = (receiver_path, device_number, bool(device.online), device.codename, icon_name, status_text, status_icon, device)
row_data = (receiver_path, device_number, bool(device.online),
device.codename, icon_name, status_text, status_icon,
device)
assert len(row_data) == len(_TREE_SEPATATOR)
if _log.isEnabledFor(_DEBUG):
_log.debug("new device row %s at index %d", row_data, new_child_index)
_log.debug("new device row %s at index %d", row_data,
new_child_index)
item = _model.insert(receiver_row, new_child_index, row_data)
return item or None
#
#
#
def select(receiver_path, device_number=None):
assert _window
assert receiver_path is not None
@ -457,7 +499,8 @@ def select(receiver_path, device_number=None):
selection = _tree.get_selection()
selection.select_iter(item)
else:
_log.warn("select(%s, %s) failed to find an item", receiver_path, device_number)
_log.warn("select(%s, %s) failed to find an item", receiver_path,
device_number)
def _hide(w, _ignore=None):
@ -483,10 +526,12 @@ def toggle(trigger=None):
else:
_window.present()
#
#
#
def _update_details(button):
assert button
visible = button.get_active()
@ -513,9 +558,14 @@ def _update_details(button):
yield (_("Index"), device.number)
yield (_("Wireless PID"), device.wpid)
hid_version = device.protocol
yield (_("Protocol"), 'HID++ %1.1f' % hid_version if hid_version else _('Unknown'))
yield (_("Protocol"), 'HID++ %1.1f' %
hid_version if hid_version else _('Unknown'))
if read_all and device.polling_rate:
yield (_("Polling rate"), _('%(rate)d ms (%(rate_hz)dHz)') % { 'rate': device.polling_rate, 'rate_hz': 1000 // device.polling_rate })
yield (_("Polling rate"),
_('%(rate)d ms (%(rate_hz)dHz)') % {
'rate': device.polling_rate,
'rate_hz': 1000 // device.polling_rate
})
if read_all or not device.online:
yield (_("Serial"), device.serial)
@ -525,13 +575,17 @@ def _update_details(button):
if read_all:
if device.firmware:
for fw in list(device.firmware):
yield (' ' + _(str(fw.kind)), (fw.name + ' ' + fw.version).strip())
yield (' ' + _(str(fw.kind)),
(fw.name + ' ' + fw.version).strip())
elif device.kind is None or device.online:
yield (' %s' % _("Firmware"), '...')
flag_bits = device.status.get(_K.NOTIFICATION_FLAGS)
if flag_bits is not None:
flag_names = ('(%s)' % _("none"),) if flag_bits == 0 else _hidpp10.NOTIFICATION_FLAG.flag_names(flag_bits)
flag_names = (
'(%s)' % _("none"),
) if flag_bits == 0 else _hidpp10.NOTIFICATION_FLAG.flag_names(
flag_bits)
yield (_("Notifications"), ('\n%15s' % ' ').join(flag_names))
def _set_details(text):
@ -568,15 +622,26 @@ def _update_receiver_panel(receiver, panel, buttons, full=False):
devices_count = len(receiver)
paired_text = _('No device paired.') if devices_count == 0 else ngettext('%(count)s paired device.', '%(count)s paired devices.', devices_count) % { 'count': devices_count }
paired_text = _('No device paired.') if devices_count == 0 else ngettext(
'%(count)s paired device.', '%(count)s paired devices.',
devices_count) % {
'count': devices_count
}
if(receiver.max_devices > 0):
paired_text += '\n\n<small>%s</small>' % ngettext('Up to %(max_count)s device can be paired to this receiver.', 'Up to %(max_count)s devices can be paired to this receiver.', receiver.max_devices) % { 'max_count': receiver.max_devices }
if (receiver.max_devices > 0):
paired_text += '\n\n<small>%s</small>' % ngettext(
'Up to %(max_count)s device can be paired to this receiver.',
'Up to %(max_count)s devices can be paired to this receiver.',
receiver.max_devices) % {
'max_count': receiver.max_devices
}
elif devices_count > 0:
paired_text += '\n\n<small>%s</small>' % _('Only one device can be paired to this receiver.')
paired_text += '\n\n<small>%s</small>' % _(
'Only one device can be paired to this receiver.')
pairings = receiver.remaining_pairings(False)
if ( pairings is not None and pairings >= 0 ) :
paired_text += '\n<small>%s</small>' % _('This receiver has %d pairing(s) remaining.') % pairings
if (pairings is not None and pairings >= 0):
paired_text += '\n<small>%s</small>' % _(
'This receiver has %d pairing(s) remaining.') % pairings
panel._count.set_markup(paired_text)
@ -600,8 +665,11 @@ def _update_receiver_panel(receiver, panel, buttons, full=False):
if ( receiver.may_unpair or receiver.re_pairs ) and not is_pairing and \
( receiver.remaining_pairings() is None or receiver.remaining_pairings() != 0 ):
if not receiver.re_pairs and devices_count >= receiver.max_devices:
paired_devices = tuple(n for n in range(1, receiver.max_devices+1) if n in receiver)
buttons._pair.set_sensitive(len(paired_devices) < receiver.max_devices)
paired_devices = tuple(n
for n in range(1, receiver.max_devices + 1)
if n in receiver)
buttons._pair.set_sensitive(
len(paired_devices) < receiver.max_devices)
else:
buttons._pair.set_sensitive(True)
else:
@ -632,16 +700,20 @@ def _update_device_panel(device, panel, buttons, full=False):
panel._battery._icon.set_sensitive(True)
if battery_voltage is not None:
text = "%(battery_voltage)dmV" % { 'battery_voltage' : battery_voltage }
text = "%(battery_voltage)dmV" % {
'battery_voltage': battery_voltage
}
elif isinstance(battery_level, _NamedInt):
text = _(str(battery_level))
else:
text = "%(battery_percent)d%%" % { 'battery_percent': battery_level }
text = "%(battery_percent)d%%" % {'battery_percent': battery_level}
if battery_next_level is not None:
if isinstance(battery_next_level, _NamedInt):
text += "<small> (" +_("next ") + _(str(battery_next_level)) + ")</small>"
text += "<small> (" + _("next ") + _(
str(battery_next_level)) + ")</small>"
else:
text += "<small> (" + _("next ") + ( "%d%%" % battery_next_level ) + ")</small>"
text += "<small> (" + _("next ") + (
"%d%%" % battery_next_level) + ")</small>"
if is_online:
if charging:
text += ' <small>(%s)</small>' % _("charging")
@ -654,17 +726,23 @@ def _update_device_panel(device, panel, buttons, full=False):
not_secure = device.status.get(_K.LINK_ENCRYPTED) == False
if not_secure:
panel._secure._text.set_text(_("not encrypted"))
panel._secure._icon.set_from_icon_name('security-low', _INFO_ICON_SIZE)
panel._secure.set_tooltip_text(_("The wireless link between this device and its receiver is not encrypted.\n"
panel._secure._icon.set_from_icon_name('security-low',
_INFO_ICON_SIZE)
panel._secure.set_tooltip_text(
_("The wireless link between this device and its receiver is not encrypted.\n"
"\n"
"For pointing devices (mice, trackballs, trackpads), this is a minor security issue.\n"
"\n"
"It is, however, a major security issue for text-input devices (keyboards, numpads),\n"
"because typed text can be sniffed inconspicuously by 3rd parties within range."))
"because typed text can be sniffed inconspicuously by 3rd parties within range."
))
else:
panel._secure._text.set_text(_("encrypted"))
panel._secure._icon.set_from_icon_name('security-high', _INFO_ICON_SIZE)
panel._secure.set_tooltip_text(_("The wireless link between this device and its receiver is encrypted."))
panel._secure._icon.set_from_icon_name('security-high',
_INFO_ICON_SIZE)
panel._secure.set_tooltip_text(
_("The wireless link between this device and its receiver is encrypted."
))
panel._secure._icon.set_visible(True)
else:
panel._secure._text.set_markup('<small>%s</small>' % _("offline"))
@ -676,8 +754,10 @@ def _update_device_panel(device, panel, buttons, full=False):
if light_level is None:
panel._lux.set_visible(False)
else:
panel._lux._icon.set_from_icon_name(_icons.lux(light_level), _INFO_ICON_SIZE)
panel._lux._text.set_text(_("%(light_level)d lux") % { 'light_level': light_level })
panel._lux._icon.set_from_icon_name(_icons.lux(light_level),
_INFO_ICON_SIZE)
panel._lux._text.set_text(
_("%(light_level)d lux") % {'light_level': light_level})
panel._lux.set_visible(True)
else:
panel._lux.set_visible(False)
@ -726,6 +806,7 @@ def _update_info_panel(device, full=False):
if full:
_update_details(_info._buttons._details)
#
# window layout:
# +--------------------------------+
@ -747,7 +828,7 @@ _empty = None
_window = None
def init(show_window,hide_on_close):
def init(show_window, hide_on_close):
Gtk.Window.set_default_icon_name(NAME.lower())
Gtk.Window.set_default_icon_from_file(_icons.icon_file(NAME.lower()))
@ -795,7 +876,9 @@ def update(device, need_popup=False):
if is_alive and item:
was_pairing = bool(_model.get_value(item, _COLUMN.STATUS_ICON))
is_pairing = bool(device.status.lock_open)
_model.set_value(item, _COLUMN.STATUS_ICON, 'network-wireless' if is_pairing else _CAN_SET_ROW_NONE)
_model.set_value(
item, _COLUMN.STATUS_ICON,
'network-wireless' if is_pairing else _CAN_SET_ROW_NONE)
if selected_device_id == (device.path, 0):
full_update = need_popup or was_pairing != is_pairing
@ -811,8 +894,10 @@ def update(device, need_popup=False):
# peripheral
is_paired = bool(device)
assert device.receiver
assert device.number is not None and device.number > 0, "invalid device number" + str(device.number)
item = _device_row(device.receiver.path, device.number, device if is_paired else None)
assert device.number is not None and device.number > 0, "invalid device number" + str(
device.number)
item = _device_row(device.receiver.path, device.number,
device if is_paired else None)
if is_paired and item:
was_online = _model.get_value(item, _COLUMN.ACTIVE)
@ -826,11 +911,15 @@ def update(device, need_popup=False):
_model.set_value(item, _COLUMN.STATUS_ICON, _CAN_SET_ROW_NONE)
else:
if battery_voltage is not None:
status_text = "%(battery_voltage)dmV" % { 'battery_voltage' : battery_voltage }
status_text = "%(battery_voltage)dmV" % {
'battery_voltage': battery_voltage
}
elif isinstance(battery_level, _NamedInt):
status_text = _(str(battery_level))
else:
status_text = "%(battery_percent)d%%" % { 'battery_percent': battery_level }
status_text = "%(battery_percent)d%%" % {
'battery_percent': battery_level
}
_model.set_value(item, _COLUMN.STATUS_TEXT, status_text)
charging = device.status.get(_K.BATTERY_CHARGING)

View File

@ -28,6 +28,8 @@ del getLogger
#
_suspend_callback = None
def _suspend():
if _suspend_callback:
if _log.isEnabledFor(_INFO):
@ -36,15 +38,19 @@ def _suspend():
_resume_callback = None
def _resume():
if _resume_callback:
if _log.isEnabledFor(_INFO):
_log.info("received resume event")
_resume_callback()
def _suspend_or_resume(suspend):
_suspend() if suspend else _resume()
def watch(on_resume_callback=None, on_suspend_callback=None):
"""Register callback for suspend/resume events.
They are called only if the system DBus is running, and the UPower daemon is available."""
@ -68,17 +74,24 @@ try:
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")
_log.info(
"connected to system dbus, watching for suspend/resume events")
except:
# Either:

View File

@ -18,7 +18,9 @@ def _data_files():
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/icons/hicolor/scalable/apps', [
'share/solaar/icons/solaar.svg'
]
for mo in _glob('share/locale/*/LC_MESSAGES/solaar.mo'):
yield _dirname(mo), [mo]
@ -30,7 +32,8 @@ def _data_files():
del _dirname
setup(name=NAME.lower(),
setup(
name=NAME.lower(),
version=__version__,
description='Linux devices manager for the Logitech Unifying Receiver.',
long_description='''
@ -53,17 +56,19 @@ battery status, and show and modify some of the modifiable features of devices.
'Operating System :: POSIX :: Linux',
'Topic :: Utilities',
],
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)', ],
install_requires=[
'pyudev (>= 0.13)',
],
package_dir={'': 'lib'},
packages=['hidapi', 'logitech_receiver', 'solaar', 'solaar.ui', 'solaar.cli'],
packages=[
'hidapi', 'logitech_receiver', 'solaar', 'solaar.ui', 'solaar.cli'
],
data_files=list(_data_files()),
scripts=_glob('bin/*'),
)
)

View File

@ -10,7 +10,8 @@ def init_paths():
import sys
import os.path as _path
src_lib = _path.normpath(_path.join(_path.realpath(sys.path[0]), '..', 'lib'))
src_lib = _path.normpath(
_path.join(_path.realpath(sys.path[0]), '..', 'lib'))
init_py = _path.join(src_lib, 'hidapi', '__init__.py')
if _path.exists(init_py):
sys.path[0] = src_lib

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)