diff --git a/bin/solaar-cli b/bin/solaar-cli index b3231d43..35192c3b 100755 --- a/bin/solaar-cli +++ b/bin/solaar-cli @@ -37,6 +37,7 @@ def init_paths(): if __name__ == '__main__': + print ('WARNING: solaar-cli is deprecated; use solaar with the usual arguments') init_paths() import solaar.cli - solaar.cli.main() + solaar.cli.run() diff --git a/lib/solaar/cli.py b/lib/solaar/cli.py deleted file mode 100644 index b9e7eb77..00000000 --- a/lib/solaar/cli.py +++ /dev/null @@ -1,434 +0,0 @@ -#!/usr/bin/env python -# -*- python-mode -*- -# -*- coding: UTF-8 -*- - -## Copyright (C) 2012-2013 Daniel Pavel -## -## This program is free software; you can redistribute it and/or modify -## it under the terms of the GNU General Public License as published by -## the Free Software Foundation; either version 2 of the License, or -## (at your option) any later version. -## -## This program is distributed in the hope that it will be useful, -## but WITHOUT ANY WARRANTY; without even the implied warranty of -## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -## GNU General Public License for more details. -## -## 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. - -from __future__ import absolute_import, division, print_function, unicode_literals - -import sys -import logging - - -NAME = 'solaar-cli' -from solaar import __version__ - -# -# -# - -def _fail(text): - if sys.exc_info()[0]: - logging.exception(text) - sys.exit("%s: error: %s" % (NAME, text)) - - -def _require(module, os_package): - try: - __import__(module) - except ImportError: - _fail("missing required package '%s'" % os_package) - -# -# -# - -def _receiver(dev_path=None): - from logitech_receiver import Receiver - from logitech_receiver.base import receivers - for dev_info in receivers(): - if dev_path is not None and dev_path != dev_info.path: - continue - try: - r = Receiver.open(dev_info) - if r: - return r - except Exception as e: - _fail(str(e)) - return r - _fail("Logitech receiver not found") - - -def _find_device(receiver, name, may_be_receiver=False): - if len(name) == 1: - try: - number = int(name) - except: - pass - else: - if number < 1 or number > receiver.max_devices: - _fail("%s (%s) supports device numbers 1 to %d" % (receiver.name, receiver.path, receiver.max_devices)) - dev = receiver[number] - if dev is None: - _fail("no paired device with number %s" % number) - return dev - - if len(name) < 3: - _fail("need at least 3 characters to match a device") - - name = name.lower() - if may_be_receiver and ('receiver'.startswith(name) or name == receiver.serial.lower()): - return receiver - - for dev in receiver: - if (name == dev.serial.lower() or - name == dev.codename.lower() or - name == str(dev.kind).lower() or - name in dev.name.lower()): - return dev - - _fail("no device found matching '%s'" % name) - - -def _print_receiver(receiver, verbose=False): - paired_count = receiver.count() - if not verbose: - print ("Unifying Receiver [%s:%s] with %d devices" % (receiver.path, receiver.serial, paired_count)) - return - - print ("Unifying Receiver") - print (" Device path :", receiver.path) - print (" USB id : 046d:%s" % receiver.product_id) - print (" Serial :", receiver.serial) - for f in receiver.firmware: - print (" %-11s: %s" % (f.kind, f.version)) - - print (" Has", paired_count, "paired device(s) out of a maximum of", receiver.max_devices, ".") - - from logitech_receiver import hidpp10 - 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: 0x%06X = %s" % (notification_flags, ', '.join(notification_names))) - else: - print (" Notifications: (none)") - - activity = receiver.read_register(hidpp10.REGISTERS.devices_activity) - if activity: - activity = [(d, ord(activity[d - 1:d])) for d in range(1, receiver.max_devices)] - activity_text = ', '.join(('%d=%d' % (d, a)) for d, a in activity if a > 0) - print (" Device activity counters:", activity_text or '(empty)') - - -def _print_device(dev, verbose=False): - assert dev - state = '' if dev.ping() else 'offline' - - if not verbose: - print ("%d: %s [%s:%s]" % (dev.number, dev.name, dev.codename, dev.serial), state) - return - - print ("%d: %s" % (dev.number, dev.name)) - print (" Codename :", dev.codename) - print (" Kind :", dev.kind) - print (" Wireless PID :", dev.wpid) - if dev.protocol: - print (" Protocol : HID++ %1.1f" % dev.protocol) - else: - print (" Protocol : unknown (device is offline)") - print (" Polling rate :", dev.polling_rate, "ms") - print (" Serial number:", dev.serial) - for fw in dev.firmware: - print (" %11s:" % fw.kind, (fw.name + ' ' + fw.version).strip()) - - if dev.power_switch_location: - print (" The power switch is located on the %s." % dev.power_switch_location) - - from logitech_receiver import hidpp10, hidpp20, special_keys - - 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: 0x%06X = %s." % (notification_flags, ', '.join(notification_names))) - else: - print (" Notifications: (none).") - - if dev.online: - if dev.features: - print (" Supports %d HID++ 2.0 features:" % len(dev.features)) - for index, feature in enumerate(dev.features): - feature = dev.features[index] - flags = dev.request(0x0000, feature.bytes(2)) - flags = 0 if flags is None else ord(flags[1:2]) - flags = hidpp20.FEATURE_FLAG.flag_names(flags) - print (" %2d: %-22s {%04X} %s" % (index, feature, feature, ', '.join(flags))) - - if dev.online: - if dev.keys: - print (" Has %d reprogrammable keys:" % len(dev.keys)) - for k in dev.keys: - flags = special_keys.KEY_FLAG.flag_names(k.flags) - print (" %2d: %-26s => %-27s %s" % (k.index, k.key, k.task, ', '.join(flags))) - - if dev.online: - battery = hidpp20.get_battery(dev) - if battery is None: - battery = hidpp10.get_battery(dev) - if battery is not None: - from logitech_receiver.common import NamedInt as _NamedInt - level, status = battery - if isinstance(level, _NamedInt): - text = str(level) - else: - text = '%d%%' % level - print (" Battery: %s, %s," % (text, status)) - else: - print (" Battery status unavailable.") - else: - print (" Battery status is unknown (device is offline).") - -# -# -# - -def show_devices(receiver, args): - if args.device == 'all': - _print_receiver(receiver, args.verbose) - for dev in receiver: - if args.verbose: - print ("") - _print_device(dev, args.verbose) - else: - dev = _find_device(receiver, args.device, True) - if dev is receiver: - _print_receiver(receiver, args.verbose) - else: - _print_device(dev, args.verbose) - - -def pair_device(receiver, args): - # get all current devices - known_devices = [dev.number for dev in receiver] - - from logitech_receiver import base, hidpp10, status, notifications - 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) - - class HandleWithNotificationHook(int): - def notifications_hook(self, n): - assert n - if n.devnumber == 0xFF: - notifications.process(receiver, n) - elif n.sub_id == 0x41 and n.address == 0x04: - if n.devnumber not in known_devices: - 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).") - - # the lock-open notification may come slightly later, wait for it a bit - from time import time as timestamp - pairing_start = timestamp() - patience = 5 # seconds - - while receiver.status.lock_open or timestamp() - pairing_start < patience: - n = base.read(receiver.handle) - if n: - n = base.make_notification(*n) - if n: - receiver.handle.notifications_hook(n) - - if not (old_notification_flags & hidpp10.NOTIFICATION_FLAG.wireless): - # only clear the flags if they weren't set before, otherwise a - # concurrently running Solaar app might stop working properly - hidpp10.set_notification_flags(receiver, old_notification_flags) - - if receiver.status.new_device: - dev = receiver.status.new_device - print ("Paired device %d: %s [%s:%s:%s]" % (dev.number, dev.name, dev.wpid, dev.codename, dev.serial)) - else: - error = receiver.status[status.KEYS.ERROR] or 'no device detected?' - _fail(error) - - -def unpair_device(receiver, args): - dev = _find_device(receiver, args.device) - - # query these now, it's last chance to get them - number, name, codename, serial = dev.number, dev.name, dev.codename, dev.serial - try: - del receiver[number] - print ("Unpaired %d: %s [%s:%s]" % (number, name, codename, serial)) - except Exception as e: - _fail("failed to unpair device %s: %s" % (dev.name, e)) - - -def config_device(receiver, args): - dev = _find_device(receiver, args.device) - # if dev is receiver: - # _fail("no settings for the receiver") - - if not dev.settings: - _fail("no settings for %s" % dev.name) - - if not args.setting: - print ("[%s:%s]" % (dev.serial, dev.kind)) - print ("#", dev.name) - for s in dev.settings: - print ("") - print ("# %s" % s.label) - if s.choices: - print ("# possible values: one of [", ', '.join(str(v) for v in s.choices), "], or higher/lower/highest/max/lowest/min") - else: - print ("# possible values: on/true/t/yes/y/1 or off/false/f/no/n/0") - value = s.read() - if value is None: - print ("# %s = ? (failed to read from device)" % s.name) - else: - print (s.name, "=", value) - return - - setting = None - for s in dev.settings: - if args.setting.lower() == s.name.lower(): - setting = s - break - if setting is None: - _fail("no setting '%s' for %s" % (args.setting, dev.name)) - - if args.value is None: - result = setting.read() - if result is None: - _fail("failed to read '%s'" % setting.name) - print ("%s = %s" % (setting.name, setting.read())) - return - - from logitech_receiver import settings as _settings - - if setting.kind == _settings.KIND.toggle: - value = args.value - try: - value = bool(int(value)) - except: - if value.lower() in ('1', 'true', 'yes', 'on', 't', 'y'): - value = True - elif value.lower() in ('0', 'false', 'no', 'off', 'f', 'n'): - value = False - else: - _fail("don't know how to interpret '%s' as boolean" % value) - - elif setting.choices: - value = args.value.lower() - - if value in ('higher', 'lower'): - old_value = setting.read() - if old_value is None: - _fail("could not read current value of '%s'" % setting.name) - - if value == 'lower': - lower_values = setting.choices[:old_value] - value = lower_values[-1] if lower_values else setting.choices[:][0] - elif value == 'higher': - higher_values = setting.choices[old_value + 1:] - value = higher_values[0] if higher_values else setting.choices[:][-1] - elif value in ('highest', 'max'): - value = setting.choices[:][-1] - elif value in ('lowest', 'min'): - value = setting.choices[:][0] - elif value not in setting.choices: - _fail("possible values for '%s' are: [%s]" % (setting.name, ', '.join(str(v) for v in setting.choices))) - value = setting.choices[value] - - else: - raise NotImplemented - - result = setting.write(value) - if result is None: - _fail("failed to set '%s' = '%s' [%r]" % (setting.name, value, value)) - print ("%s = %s" % (setting.name, result)) - -# -# -# - -def _parse_arguments(): - from argparse import ArgumentParser - arg_parser = 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('-V', '--version', action='version', version='%(prog)s ' + __version__) - 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') - - subparsers = arg_parser.add_subparsers(title='commands') - - sp = subparsers.add_parser('show', help='show information about paired devices') - sp.add_argument('device', nargs='?', default='all', - help='device to show information about; may be a device number (1..6), a device serial, ' - 'at least 3 characters of a device\'s name, "receiver", or "all" (the default)') - sp.add_argument('-v', '--verbose', action='store_true', - help='print all available information about the inspected device(s)') - sp.set_defaults(cmd=show_devices) - - sp = subparsers.add_parser('config', help='read/write device-specific settings', - epilog='Please note that configuration only works on active devices.') - sp.add_argument('device', - help='device to configure; may be a device number (1..6), a device serial, ' - 'or at least 3 characters of a device\'s name') - sp.add_argument('setting', nargs='?', - help='device-specific setting; leave empty to list available settings') - sp.add_argument('value', nargs='?', - help='new value for the setting') - sp.set_defaults(cmd=config_device) - - 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.set_defaults(cmd=pair_device) - - sp = subparsers.add_parser('unpair', help='unpair a device') - sp.add_argument('device', - help='device to unpair; may be a device number (1..6), a device serial, ' - 'or at least 3 characters of a device\'s name.') - sp.set_defaults(cmd=unpair_device) - - args = arg_parser.parse_args() - - # Python 3 has an undocumented 'feature' that breaks parsing empty args - # http://bugs.python.org/issue16308 - if not 'cmd' in args: - arg_parser.print_usage(sys.stderr) - sys.stderr.write('%s: error: too few arguments\n' % NAME.lower()) - sys.exit(2) - - if args.debug > 0: - log_level = logging.WARNING - 10 * args.debug - log_format='%(asctime)s,%(msecs)03d %(levelname)8s %(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) - - return args - - -def main(): - _require('pyudev', 'python-pyudev') - args = _parse_arguments() - receiver = _receiver(args.hidraw_path) - args.cmd(receiver, args) - -if __name__ == '__main__': - main() diff --git a/lib/solaar/cli/__init__.py b/lib/solaar/cli/__init__.py new file mode 100644 index 00000000..a339acf3 --- /dev/null +++ b/lib/solaar/cli/__init__.py @@ -0,0 +1,154 @@ +# -*- python-mode -*- +# -*- coding: UTF-8 -*- + +## Copyright (C) 2012-2013 Daniel Pavel +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation; either version 2 of the License, or +## (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## 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. + +from __future__ import absolute_import, division, print_function, unicode_literals + + +import argparse as _argparse +import sys as _sys + +from logging import getLogger, DEBUG as _DEBUG +_log = getLogger(__name__) +del getLogger + + +from solaar import NAME + +# +# +# + +def _create_parser(): + parser = _argparse.ArgumentParser(prog=NAME.lower(), add_help=False, + epilog='For details on individual actions, run `%s --help`.' % NAME.lower()) + subparsers = parser.add_subparsers(title='actions', + help='optional action to perform') + + sp = subparsers.add_parser('show', help='show information about devices') + sp.add_argument('device', nargs='?', default='all', + help='device to show information about; may be a device number (1..6), a serial, ' + 'a substring of a device\'s name, or "all" (the default)') + sp.set_defaults(action='show') + + sp = subparsers.add_parser('config', help='read/write device-specific settings', + epilog='Please note that configuration only works on active devices.') + sp.add_argument('device', + help='device to configure; may be a device number (1..6), a device serial, ' + 'or at least 3 characters of a device\'s name') + sp.add_argument('setting', nargs='?', + help='device-specific setting; leave empty to list available settings') + sp.add_argument('value', nargs='?', + help='new value for the setting') + sp.set_defaults(action='config') + + sp = subparsers.add_parser('pair', help='pair a new device', + epilog='The Logitech Unifying Receiver supports up to 6 paired devices at the same time.') + sp.add_argument('receiver', nargs='?', + help='select a certain receiver when more than one is present') + sp.set_defaults(action='pair') + + sp = subparsers.add_parser('unpair', help='unpair a device') + sp.add_argument('device', + help='device to unpair; may be a device number (1..6), a serial, ' + 'or a substring of a device\'s name.') + sp.set_defaults(action='unpair') + + return parser, subparsers.choices + + +_cli_parser, actions = _create_parser() + + +def _receivers(): + from logitech_receiver import Receiver + from logitech_receiver.base import receivers + for dev_info in receivers(): + try: + r = Receiver.open(dev_info) + if _log.isEnabledFor(_DEBUG): + _log.debug("[%s] => %s", dev_info.path, r) + if r: + yield r + except Exception as e: + _log.exception('opening ' + str(dev_info)) + _sys.exit("%s: error: %s" % (NAME, str(e))) + + +def _find_receiver(receivers, name): + assert receivers + assert name + + for r in receivers: + if name in r.name.lower() or name == r.serial.lower(): + return r + + +def _find_device(receivers, name): + assert receivers + assert name + + number = None + if len(name) == 1: + try: + number = int(name) + except: + pass + else: + assert not (number < 0) + if number > 6: number = None + + for r in receivers: + if number and number <= r.max_devices: + dev = r[number] + if dev: + return dev + + for dev in r: + if (name == dev.serial.lower() or + name == dev.codename.lower() or + name == str(dev.kind).lower() or + name in dev.name.lower()): + return dev + + raise Exception("no device found matching '%s'" % name) + + +def run(cli_args=None): + if cli_args == 'help': + _cli_parser.print_help() + return + + if cli_args: + action = cli_args[0] + args = _cli_parser.parse_args(cli_args) + else: + args = _cli_parser.parse_args() + action = args.action + assert action in actions + + try: + c = list(_receivers()) + if not c: + raise Exception('Logitech receiver not found') + + from importlib import import_module + m = import_module('.' + action, package=__name__) + m.run(c, args, _find_receiver, _find_device) + except Exception as e: + _sys.exit('%s: error: %s' % (NAME.lower(), e)) diff --git a/lib/solaar/cli/config.py b/lib/solaar/cli/config.py new file mode 100644 index 00000000..d26522ec --- /dev/null +++ b/lib/solaar/cli/config.py @@ -0,0 +1,120 @@ +# -*- python-mode -*- +# -*- coding: UTF-8 -*- + +## Copyright (C) 2012-2013 Daniel Pavel +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation; either version 2 of the License, or +## (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## 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. + +from __future__ import absolute_import, division, print_function, unicode_literals + + +from solaar import configuration as _configuration +from logitech_receiver import settings as _settings + + +def _print_setting(s, verbose=True): + print ('#', s.label) + if verbose: + print ('#', s.description.replace('\n', ' ')) + if s.kind == _settings.KIND.toggle: + print ('# possible values: on/true/t/yes/y/1 or off/false/f/no/n/0') + elif s.choices: + print ('# possible values: one of [', ', '.join(str(v) for v in s.choices), '], or higher/lower/highest/max/lowest/min') + else: + # wtf? + pass + value = s.read(cached=False) + if value is None: + print (s.name, '= ? (failed to read from device)') + else: + print (s.name, '= %r' % value) + + +def run(receivers, args, find_receiver, find_device): + assert receivers + assert args.device + + device_name = args.device.lower() + dev = find_device(receivers, device_name) + + if not dev.ping(): + raise Exception('%s is offline' % dev.name) + + if not dev.settings: + raise Exception('no settings for %s' % dev.name) + + _configuration.attach_to(dev) + + if not args.setting: + print (dev.name, '(%s) [%s:%s]' % (dev.codename, dev.wpid, dev.serial)) + for s in dev.settings: + print ('') + _print_setting(s) + return + + setting_name = args.setting.lower() + setting = None + for s in dev.settings: + if setting_name == s.name.lower(): + setting = s + break + if setting is None: + raise Exception("no setting '%s' for %s" % (args.setting, dev.name)) + + if args.value is None: + _print_setting(setting) + return + + if setting.kind == _settings.KIND.toggle: + value = args.value + try: + value = bool(int(value)) + except: + if value.lower() in ('true', 'yes', 'on', 't', 'y'): + value = True + elif value.lower() in ('false', 'no', 'off', 'f', 'n'): + value = False + else: + raise Exception("don't know how to interpret '%s' as boolean" % value) + + elif setting.choices: + value = args.value.lower() + + if value in ('higher', 'lower'): + old_value = setting.read() + if old_value is None: + raise Exception("could not read current value of '%s'" % setting.name) + + if value == 'lower': + lower_values = setting.choices[:old_value] + value = lower_values[-1] if lower_values else setting.choices[:][0] + elif value == 'higher': + higher_values = setting.choices[old_value + 1:] + value = higher_values[0] if higher_values else setting.choices[:][-1] + elif value in ('highest', 'max'): + value = setting.choices[:][-1] + elif value in ('lowest', 'min'): + value = setting.choices[:][0] + elif value not in setting.choices: + raise Exception("possible values for '%s' are: [%s]" % (setting.name, ', '.join(str(v) for v in setting.choices))) + value = setting.choices[value] + + else: + raise NotImplemented + + result = setting.write(value) + if result is None: + raise Exception("failed to set '%s' = '%s' [%r]" % (setting.name, value, value)) + _print_setting(setting, False) diff --git a/lib/solaar/cli/pair.py b/lib/solaar/cli/pair.py new file mode 100644 index 00000000..f9e582f2 --- /dev/null +++ b/lib/solaar/cli/pair.py @@ -0,0 +1,91 @@ +# -*- python-mode -*- +# -*- coding: UTF-8 -*- + +## Copyright (C) 2012-2013 Daniel Pavel +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation; either version 2 of the License, or +## (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## 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. + +from __future__ import absolute_import, division, print_function, unicode_literals + + +from time import time as _timestamp + +from logitech_receiver import ( + base as _base, + hidpp10 as _hidpp10, + status as _status, + notifications as _notifications, + ) + + +def run(receivers, args, find_receiver, _ignore): + assert receivers + + if args.receiver: + receiver_name = args.receiver.lower() + receiver = find_receiver(receiver_name) + if not receiver: + raise Exception("no receiver found matching '%s'" % receiver_name) + else: + receiver = receivers[0] + + assert receiver + receiver.status = _status.ReceiverStatus(receiver, lambda *args, **kwargs: None) + + # check if it's necessary to set the notification flags + old_notification_flags = _hidpp10.get_notification_flags(receiver) or 0 + if not (old_notification_flags & _hidpp10.NOTIFICATION_FLAG.wireless): + _hidpp10.set_notification_flags(receiver, old_notification_flags | _hidpp10.NOTIFICATION_FLAG.wireless) + + # get all current devices + known_devices = [dev.number for dev in receiver] + + class _HandleWithNotificationHook(int): + def notifications_hook(self, n): + assert n + if n.devnumber == 0xFF: + _notifications.process(receiver, n) + elif n.sub_id == 0x41 and n.address == 0x04: + if n.devnumber not in known_devices: + 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).') + + # the lock-open notification may come slightly later, wait for it a bit + pairing_start = _timestamp() + patience = 5 # seconds + + while receiver.status.lock_open or _timestamp() - pairing_start < patience: + n = _base.read(receiver.handle) + if n: + n = _base.make_notification(*n) + if n: + receiver.handle.notifications_hook(n) + + if not (old_notification_flags & _hidpp10.NOTIFICATION_FLAG.wireless): + # only clear the flags if they weren't set before, otherwise a + # concurrently running Solaar app might stop working properly + _hidpp10.set_notification_flags(receiver, old_notification_flags) + + if receiver.status.new_device: + dev = receiver.status.new_device + print ('Paired device %d: %s (%s) [%s:%s]' % (dev.number, dev.name, dev.codename, dev.wpid, dev.serial)) + else: + error = receiver.status.get(_status.KEYS.ERROR) or 'no device detected?' + raise Exception("pairing failed: %s" % error) diff --git a/lib/solaar/cli/show.py b/lib/solaar/cli/show.py new file mode 100644 index 00000000..906cb40c --- /dev/null +++ b/lib/solaar/cli/show.py @@ -0,0 +1,146 @@ +# -*- python-mode -*- +# -*- coding: UTF-8 -*- + +## Copyright (C) 2012-2013 Daniel Pavel +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation; either version 2 of the License, or +## (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## 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. + +from __future__ import absolute_import, division, print_function, unicode_literals + + +from logitech_receiver import ( + hidpp10 as _hidpp10, + hidpp20 as _hidpp20, + special_keys as _special_keys, + ) + + +def _print_receiver(receiver): + paired_count = receiver.count() + + print ('Unifying Receiver') + print (' Device path :', receiver.path) + print (' USB id : 046d:%s' % receiver.product_id) + print (' Serial :', receiver.serial) + for f in receiver.firmware: + print (' %-11s: %s' % (f.kind, f.version)) + + print (' Has', paired_count, 'paired device(s) out of a maximum of %d.' % receiver.max_devices) + + notification_flags = _hidpp10.get_notification_flags(receiver) + if notification_flags is not None: + if notification_flags: + notification_names = _hidpp10.NOTIFICATION_FLAG.flag_names(notification_flags) + print (' Notifications: %s (0x%06X)' % (', '.join(notification_names), notification_flags)) + else: + print (' Notifications: (none)') + + activity = receiver.read_register(_hidpp10.REGISTERS.devices_activity) + if activity: + activity = [(d, ord(activity[d - 1:d])) for d in range(1, receiver.max_devices)] + activity_text = ', '.join(('%d=%d' % (d, a)) for d, a in activity if a > 0) + print (' Device activity counters:', activity_text or '(empty)') + + +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) + if dev.protocol: + print (' Protocol : HID++ %1.1f' % dev.protocol) + else: + print (' Protocol : unknown (device is offline)') + if dev.polling_rate: + print (' Polling rate :', dev.polling_rate, 'ms (%dHz)' % (1000 // dev.polling_rate)) + print (' Serial number:', dev.serial) + for fw in dev.firmware: + print (' %11s:' % fw.kind, (fw.name + ' ' + fw.version).strip()) + + if dev.power_switch_location: + print (' The power switch is located on the %s.' % dev.power_switch_location) + + if dev.online: + notification_flags = _hidpp10.get_notification_flags(dev) + if notification_flags is not None: + if notification_flags: + notification_names = _hidpp10.NOTIFICATION_FLAG.flag_names(notification_flags) + print (' Notifications: %s (0x%06X).' % (', '.join(notification_names), notification_flags)) + else: + print (' Notifications: (none).') + + if dev.online and dev.features: + print (' Supports %d HID++ 2.0 features:' % len(dev.features)) + for index, feature in enumerate(dev.features): + feature = dev.features[index] + flags = dev.request(0x0000, feature.bytes(2)) + flags = 0 if flags is None else ord(flags[1:2]) + flags = _hidpp20.FEATURE_FLAG.flag_names(flags) + print (' %2d: %-22s {%04X} %s' % (index, feature, feature, ', '.join(flags))) + + if dev.online and dev.keys: + print (' Has %d reprogrammable keys:' % len(dev.keys)) + for k in dev.keys: + flags = _special_keys.KEY_FLAG.flag_names(k.flags) + print (' %2d: %-26s => %-27s %s' % (k.index, k.key, k.task, ', '.join(flags))) + + if dev.online: + battery = _hidpp20.get_battery(dev) + if battery is None: + battery = _hidpp10.get_battery(dev) + if battery is not None: + from logitech_receiver.common import NamedInt as _NamedInt + level, status = battery + if isinstance(level, _NamedInt): + text = str(level) + else: + text = '%d%%' % level + print (' Battery: %s, %s.' % (text, status)) + else: + print (' Battery status unavailable.') + else: + print (' Battery: unknown (device is offline).') + + +def run(receivers, args, find_receiver, find_device): + assert receivers + assert args.device + + device_name = args.device.lower() + + if device_name == 'all': + for r in receivers: + _print_receiver(r) + count = r.count() + for dev in r: + print ('') + _print_device(dev) + count -= 1 + if count == 0: + break + return + + dev = find_receiver(receivers, device_name) + if dev: + _print_receiver(dev) + return + + dev = find_device(receivers, device_name) + assert dev + _print_device(dev) diff --git a/lib/solaar/cli/unpair.py b/lib/solaar/cli/unpair.py new file mode 100644 index 00000000..b035b413 --- /dev/null +++ b/lib/solaar/cli/unpair.py @@ -0,0 +1,36 @@ +# -*- python-mode -*- +# -*- coding: UTF-8 -*- + +## Copyright (C) 2012-2013 Daniel Pavel +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation; either version 2 of the License, or +## (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## 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. + +from __future__ import absolute_import, division, print_function, unicode_literals + + +def run(receivers, args, find_receiver, find_device): + assert receivers + assert args.device + + device_name = args.device.lower() + dev = find_device(receivers, device_name) + + # query these now, it's last chance to get them + try: + number, codename, wpid, serial = dev.number, dev.codename, dev.wpid, dev.serial + del dev.receiver[number] + print ('Unpaired %d: %s (%s) [%s:%s]' % (number, dev.name, codename, wpid, serial)) + except Exception as e: + raise Exception('failed to unpair device %s: %s' % (dev.name, e)) diff --git a/lib/solaar/gtk.py b/lib/solaar/gtk.py index bdb5c1ab..aabd35f5 100644 --- a/lib/solaar/gtk.py +++ b/lib/solaar/gtk.py @@ -23,6 +23,7 @@ from __future__ import absolute_import, division, print_function, unicode_litera from solaar import __version__, NAME import solaar.i18n as _i18n +import solaar.cli as _cli # # @@ -40,10 +41,18 @@ 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)") + help='print logging messages, for debugging purposes (may be repeated for extra verbosity)') arg_parser.add_argument('-V', '--version', action='version', version='%(prog)s ' + __version__) + arg_parser.add_argument('--help-actions', action='store_true', + help='print help for the optional actions') + arg_parser.add_argument('action', nargs=argparse.REMAINDER, choices=_cli.actions, + help='optional actions to perform') + args = arg_parser.parse_args() + if args.help_actions: + return 'help' + import logging if args.debug > 0: log_level = logging.WARNING - 10 * args.debug @@ -53,22 +62,27 @@ def _parse_arguments(): logging.root.addHandler(logging.NullHandler()) logging.root.setLevel(logging.ERROR) + if args.action: + return args.action + if logging.root.isEnabledFor(logging.INFO): logging.info("language %s (%s), translations path %s", _i18n.language, _i18n.encoding, _i18n.path) - return args - def main(): _require('pyudev', 'python-pyudev') - _require('gi.repository', 'python-gi') - _require('gi.repository.Gtk', 'gir1.2-gtk-3.0') - _parse_arguments() # handle ^C in console import signal signal.signal(signal.SIGINT, signal.SIG_DFL) + cli_action = _parse_arguments() + if cli_action: + return _cli.run(cli_action) + + _require('gi.repository', 'python-gi') + _require('gi.repository.Gtk', 'gir1.2-gtk-3.0') + try: import solaar.ui as ui import solaar.listener as listener