From f2dac70131ae666cadf96aeb551c85475c317bd9 Mon Sep 17 00:00:00 2001 From: Daniel Pavel Date: Thu, 18 Oct 2012 13:40:37 +0300 Subject: [PATCH] reworked the receiver and devices into classes --- README | 4 +- app/receiver.py | 288 ++++++++++++++++++ app/solaar.py | 130 ++++---- app/ui/icon.py | 54 +++- app/ui/notify.py | 107 +++---- app/ui/window.py | 103 ++++--- app/watcher.py | 266 +++++----------- bin/solaar | 6 +- lib/logitech/devices/__init__.py | 75 ++--- lib/logitech/devices/constants.py | 47 +-- lib/logitech/devices/k750.py | 27 +- lib/logitech/unifying_receiver/api.py | 110 +++---- lib/logitech/unifying_receiver/base.py | 15 +- lib/logitech/unifying_receiver/common.py | 8 +- lib/logitech/unifying_receiver/constants.py | 10 +- lib/logitech/unifying_receiver/exceptions.py | 12 +- lib/logitech/unifying_receiver/listener.py | 147 ++++----- .../unifying_receiver/tests/test_50_api.py | 8 +- lib/logitech/ur_scanner.py | 12 +- .../hicolor/128x128/apps/Solaar-init.png | Bin 0 -> 2670 bytes 20 files changed, 805 insertions(+), 624 deletions(-) create mode 100644 app/receiver.py create mode 100644 share/icons/hicolor/128x128/apps/Solaar-init.png diff --git a/README b/README index d5856f6b..b408e9dc 100644 --- a/README +++ b/README @@ -17,10 +17,10 @@ Requirements ------------ - Python (2.7 or 3.2). -- Gtk 3 (preferred), though Gtk 2 should work with minor problems. +- Gtk 3; Gtk 2 should partially work with some problems. - Python GI (GObject Introspection), for Gtk bindings. +- Optional libnotify GI bindings, for desktop notifications. - A hidapi native implementation (see the INSTALL file for details). -- Optional python-notify2 for desktop notifications. Thanks diff --git a/app/receiver.py b/app/receiver.py new file mode 100644 index 00000000..c4c2164f --- /dev/null +++ b/app/receiver.py @@ -0,0 +1,288 @@ +# +# +# + +from logging import getLogger as _Logger +_LOG_LEVEL = 6 + +from threading import Event as _Event + +from logitech.unifying_receiver import base as _base +from logitech.unifying_receiver import api as _api +from logitech.unifying_receiver import listener as _listener +from logitech import devices as _devices +from logitech.devices.constants import (STATUS, STATUS_NAME, PROPS, NAMES) + +# +# +# + +class DeviceInfo(object): + """A device attached to the receiver. + """ + def __init__(self, receiver, number, status=STATUS.UNKNOWN): + self.LOG = _Logger("Device-%d" % number) + self.receiver = receiver + self.number = number + self._name = None + self._kind = None + self._firmware = None + self._features = None + + self._status = status + self.props = {} + + @property + def handle(self): + return self.receiver.handle + + @property + def status(self): + return self._status + + @status.setter + def status(self, new_status): + if new_status != self._status and not (new_status == STATUS.CONNECTED and self._status > new_status): + self.LOG.debug("status %d => %d", self._status, new_status) + urgent = new_status < STATUS.CONNECTED or self._status < STATUS.CONNECTED + self._status = new_status + self.receiver._device_changed(self, urgent) + + @property + def status_text(self): + if self._status < STATUS.CONNECTED: + return STATUS_NAME[self._status] + + t = [] + if self.props.get(PROPS.BATTERY_LEVEL): + t.append('Battery: %d%%' % self.props[PROPS.BATTERY_LEVEL]) + if self.props.get(PROPS.BATTERY_STATUS): + t.append(self.props[PROPS.BATTERY_STATUS]) + if self.props.get(PROPS.LIGHT_LEVEL): + t.append('Light: %d lux' % self.props[PROPS.LIGHT_LEVEL]) + return ', '.join(t) if t else STATUS_NAME[STATUS.CONNECTED] + + @property + def name(self): + if self._name is None: + if self._status >= STATUS.CONNECTED: + self._name = self.receiver.request(_api.get_device_name, self.number, self.features) + return self._name or '?' + + @property + def device_name(self): + return self.name + + @property + def kind(self): + if self._kind is None: + if self._status >= STATUS.CONNECTED: + self._kind = self.receiver.request(_api.get_device_kind, self.number, self.features) + return self._kind or '?' + + @property + def firmware(self): + if self._firmware is None: + if self._status >= STATUS.CONNECTED: + self._firmware = self.receiver.request(_api.get_device_firmware, self.number, self.features) + return self._firmware or () + + @property + def features(self): + if self._features is None: + if self._status >= STATUS.CONNECTED: + self._features = self.receiver.request(_api.get_device_features, self.number) + return self._features or () + + def ping(self): + return self.receiver.request(_api.ping, self.number) + + def process_event(self, code, data): + if code == 0x10 and data[:1] == b'\x8F': + self.status = STATUS.UNAVAILABLE + return True + + if code == 0x11: + status = _devices.process_event(self, data, self.receiver) + if status: + if type(status) == int: + self.status = status + return True + + if type(status) == tuple: + p = dict(self.props) + self.props.update(status[1]) + if self.status == status[0]: + if p != self.props: + self.receiver._device_changed(self) + else: + self.status = status[0] + return True + + self.LOG.warn("don't know how to handle status %s", status) + + return False + + def __hash__(self): + return self.number + + def __str__(self): + return 'DeviceInfo(%d,%s,%d)' % (self.number, self.name, self._status) + + def __repr__(self): + return '' % (self.number, self.name, self._status) + +# +# +# + +class Receiver(_listener.EventsListener): + """Keeps the status of a Unifying Receiver. + """ + NAME = kind = 'Unifying Receiver' + max_devices = _api.MAX_ATTACHED_DEVICES + + def __init__(self, path, handle): + super(Receiver, self).__init__(handle, self._events_handler) + self.path = path + + self._status = STATUS.BOOTING + self.status_changed = _Event() + self.status_changed.urgent = False + self.status_changed.reason = None + + self.LOG = _Logger("Receiver-%s" % path) + self.LOG.info("initializing") + + self.devices = {} + self.events_handler = None + + init = (_base.request(handle, 0xFF, b'\x81\x00') and + _base.request(handle, 0xFF, b'\x80\x00', b'\x00\x01') and + _base.request(handle, 0xFF, b'\x81\x02')) + if init: + self.LOG.info("initialized") + else: + self.LOG.warn("initialization failed") + + if _base.request(handle, 0xFF, b'\x80\x02', b'\x02'): + self.LOG.info("triggered device events") + else: + self.LOG.warn("failed to trigger device events") + + def close(self): + """Closes the receiver's handle. + + The receiver can no longer be used in API calls after this. + """ + self.LOG.info("closing") + self.stop() + + @property + def status(self): + return self._status + + @status.setter + def status(self, new_status): + if new_status != self._status: + self.LOG.debug("status %d => %d", self._status, new_status) + self._status = new_status + self.status_changed.reason = self + self.status_changed.urgent = True + self.status_changed.set() + + @property + def status_text(self): + status = self._status + if status == STATUS.UNKNOWN: + return 'Initializing...' + if status == STATUS.UNAVAILABLE: + return 'Receiver not found.' + if status == STATUS.BOOTING: + return 'Scanning...' + if status == STATUS.CONNECTED: + return 'No devices found.' + if len(self.devices) > 1: + return '%d devices found' % len(self.devices) + return '1 device found' + + @property + def device_name(self): + return self.NAME + + def _device_changed(self, dev, urgent=False): + self.status_changed.reason = dev + self.status_changed.urgent = urgent + self.status_changed.set() + + def _events_handler(self, event): + if event.code == 0x10 and event.data[0:2] == b'\x41\x04': + state_code = ord(event.data[2:3]) & 0xF0 + state = STATUS.UNAVAILABLE if state_code == 0x60 else \ + STATUS.CONNECTED if state_code == 0xA0 else \ + STATUS.CONNECTED if state_code == 0x20 else \ + None + if state is None: + self.LOG.warn("don't know how to handle status 0x%02x: %s", state_code, event) + return + + if event.devnumber in self.devices: + self.devices[event.devnumber].status = state + return + + if event.devnumber < 1 or event.devnumber > self.max_devices: + self.LOG.warn("got event for invalid device number %d: %s", event.devnumber, event) + return + + dev = DeviceInfo(self, event.devnumber, state) + if state == STATUS.CONNECTED: + n, k = dev.name, dev.kind + else: + # we can query the receiver for the device short name + dev_id = self.request(_base.request, 0xFF, b'\x83\xB5', event.data[4:5]) + if dev_id: + shortname = str(dev_id[2:].rstrip(b'\x00')) + if shortname in NAMES: + dev._name, dev._kind = NAMES[shortname] + else: + self.LOG.warn("could not properly detect inactive device %d: %s", event.devnumber, shortname) + self.devices[event.devnumber] = dev + self.LOG.info("new device ready %s", dev) + self.status = STATUS.CONNECTED + len(self.devices) + return + + if event.devnumber == 0xFF: + if event.code == 0xFF and event.data is None: + # receiver disconnected + self.LOG.info("disconnected") + self.devices = {} + self.status = STATUS.UNAVAILABLE + return + self.LOG.warn("don't know how to handle event %s", event) + elif event.devnumber in self.devices: + dev = self.devices[event.devnumber] + if dev.process_event(event.code, event.data): + return + + if self.events_handler: + self.events_handler(event) + + def __str__(self): + return 'Receiver(%s,%x,%d:%d)' % (self.path, self._handle, self._active, self._status) + + @classmethod + def open(self): + """Opens the first Logitech Unifying Receiver found attached to the machine. + + :returns: An open file handle for the found receiver, or ``None``. + """ + for rawdevice in _base.list_receiver_devices(): + _Logger("receiver").log(_LOG_LEVEL, "checking %s", rawdevice) + + handle = _base.try_open(rawdevice.path) + if handle: + receiver = Receiver(rawdevice.path, handle) + receiver.start() + return receiver + + return None diff --git a/app/solaar.py b/app/solaar.py index a9c2cb4c..2c0628ca 100644 --- a/app/solaar.py +++ b/app/solaar.py @@ -1,105 +1,91 @@ #!/usr/bin/env python -__version__ = '0.4' +__version__ = '0.5' # # # -import logging - -# from gi import pygtkcompat -# pygtkcompat.enable_gtk() -from gi.repository import (Gtk, GObject) - -from logitech.devices import constants as C - -import ui - - APP_TITLE = 'Solaar' -def _status_check(watcher, tray_icon, window): - last_text = None - - while True: - watcher.status_changed.wait() - watcher.status_changed.clear() - - if watcher.devices: - lines = [] - if watcher.rstatus.code < C.STATUS.CONNECTED: - lines += (watcher.rstatus.text, '') - - devstatuses = [watcher.devices[d] for d in range(1, 1 + watcher.rstatus.max_devices) if d in watcher.devices] - for devstatus in devstatuses: - if devstatus.text: - if ' ' in devstatus.text: - lines += ('' + devstatus.name + '', ' ' + devstatus.text) - else: - lines.append('' + devstatus.name + ' ' + devstatus.text) - else: - lines.append('' + devstatus.name + '') - lines.append('') - - text = '\n'.join(lines).rstrip('\n') - else: - text = watcher.rstatus.text - - if text != last_text: - last_text = text - icon_name = APP_TITLE + '-fail' if watcher.rstatus.code < C.STATUS.CONNECTED else APP_TITLE - - if tray_icon: - GObject.idle_add(ui.icon.update, tray_icon, watcher.rstatus, text, icon_name) - - if window: - GObject.idle_add(ui.window.update, window, watcher.rstatus, watcher.devices, icon_name) - - if __name__ == '__main__': import argparse arg_parser = argparse.ArgumentParser(prog=APP_TITLE) - arg_parser.add_argument('-v', '--verbose', action='count', default=0, + arg_parser.add_argument('-v', '--verbose', + action='count', default=0, help='increase the logger verbosity (may be repeated)') - arg_parser.add_argument('-S', '--no-systray', action='store_false', dest='systray', + arg_parser.add_argument('-S', '--no-systray', + action='store_false', + dest='systray', help='don\'t embed the application window into the systray') - arg_parser.add_argument('-N', '--no-notifications', action='store_false', dest='notifications', + arg_parser.add_argument('-N', '--no-notifications', + action='store_false', + dest='notifications', help='disable desktop notifications (shown only when in systray)') - arg_parser.add_argument('-V', '--version', action='version', version='%(prog)s ' + __version__) + arg_parser.add_argument('-V', '--version', + action='version', + version='%(prog)s ' + __version__) args = arg_parser.parse_args() + import logging log_level = logging.root.level - 10 * args.verbose - log_format='%(asctime)s %(levelname)8s [%(threadName)s] %(name)s: %(message)s' - logging.basicConfig(level=log_level if log_level > 0 else 1, format=log_format) + log_format='%(asctime)s.%(msecs)03d %(levelname)8s [%(threadName)s] %(name)s: %(message)s' + logging.basicConfig(level=log_level if log_level > 0 else 1, format=log_format, datefmt='%H:%M:%S') + from gi.repository import GObject GObject.threads_init() + import ui + args.notifications &= args.systray if args.notifications: - ui.notify.init(APP_TITLE) + args.notifications &= ui.notify.init(APP_TITLE) - from watcher import Watcher - watcher = Watcher(APP_TITLE, ui.notify.show if args.notifications else None) - watcher.start() + import watcher + tray_icon = None + window = ui.window.create(APP_TITLE, + watcher.DUMMY.NAME, + watcher.DUMMY.max_devices, + args.systray) + window.set_icon_name(APP_TITLE + '-init') - window = ui.window.create(APP_TITLE, watcher.rstatus, args.systray) - window.set_icon_name(APP_TITLE + '-fail') + def _ui_update(receiver, tray_icon, window): + icon_name = APP_TITLE + '-fail' if receiver.status < 1 else APP_TITLE + if window: + GObject.idle_add(ui.window.update, window, receiver, icon_name) + if tray_icon: + GObject.idle_add(ui.icon.update, tray_icon, receiver, icon_name) + + def _notify(device): + GObject.idle_add(ui.notify.show, device) + + w = watcher.Watcher(APP_TITLE, + lambda r: _ui_update(r, tray_icon, window), + _notify if args.notifications else None) + w.start() if args.systray: - tray_icon = ui.icon.create(APP_TITLE, (ui.window.toggle, window)) - tray_icon.set_from_icon_name(APP_TITLE + '-fail') + def _toggle_notifications(item): + # logging.debug("toggle notifications %s", item) + if ui.notify.available: + if item.get_active(): + ui.notify.init(APP_TITLE) + else: + ui.notify.uninit() + item.set_sensitive(ui.notify.available) + + menu = ( + ('Notifications', _toggle_notifications if args.notifications else None, args.notifications), + ) + + tray_icon = ui.icon.create(APP_TITLE, (ui.window.toggle, window), menu) + tray_icon.set_from_icon_name(APP_TITLE + '-init') else: - tray_icon = None window.present() - from threading import Thread - status_check = Thread(group=APP_TITLE, name='StatusCheck', target=_status_check, args=(watcher, tray_icon, window)) - status_check.daemon = True - status_check.start() - + from gi.repository import Gtk Gtk.main() - watcher.stop() - ui.notify.set_active(False) + w.stop() + ui.notify.uninit() diff --git a/app/ui/icon.py b/app/ui/icon.py index 3590d783..b54ee904 100644 --- a/app/ui/icon.py +++ b/app/ui/icon.py @@ -5,7 +5,7 @@ from gi.repository import Gtk -def create(title, click_action=None): +def create(title, click_action=None, actions=None): icon = Gtk.StatusIcon() icon.set_title(title) icon.set_name(title) @@ -19,9 +19,30 @@ def create(title, click_action=None): icon.connect('activate', click_action) menu = Gtk.Menu() - item = Gtk.MenuItem('Quit') - item.connect('activate', Gtk.main_quit) - menu.append(item) + + if actions: + for name, activate, checked in actions: + if checked is None: + item = Gtk.MenuItem(name) + if activate is None: + item.set_sensitive(False) + else: + item.connect('activate', activate) + else: + item = Gtk.CheckMenuItem(name) + if activate is None: + item.set_sensitive(False) + else: + item.set_active(checked or False) + item.connect('toggled', activate) + + menu.append(item) + menu.append(Gtk.SeparatorMenuItem()) + + quit_item = Gtk.MenuItem('Quit') + quit_item.connect('activate', Gtk.main_quit) + menu.append(quit_item) + menu.show_all() icon.connect('popup_menu', @@ -32,8 +53,27 @@ def create(title, click_action=None): return icon -def update(icon, receiver, tooltip=None, icon_name=None): - if tooltip is not None: - icon.set_tooltip_markup(tooltip) +def update(icon, receiver, icon_name=None): if icon_name is not None: icon.set_from_icon_name(icon_name) + + if receiver.devices: + lines = [] + if receiver.status < 1: + lines += (receiver.status_text, '') + + devlist = [receiver.devices[d] for d in range(1, 1 + receiver.max_devices) if d in receiver.devices] + for dev in devlist: + name = '' + dev.name + '' + if dev.status < 1: + lines.append(name + ' (' + dev.status_text + ')') + else: + lines.append(name) + if dev.status > 1: + lines.append(' ' + dev.status_text) + lines.append('') + + text = '\n'.join(lines).rstrip('\n') + icon.set_tooltip_markup(text) + else: + icon.set_tooltip_text(receiver.status_text) diff --git a/app/ui/notify.py b/app/ui/notify.py index 90816271..f2c28e0e 100644 --- a/app/ui/notify.py +++ b/app/ui/notify.py @@ -6,83 +6,72 @@ import logging try: - import notify2 as _notify - from time import time as timestamp + from gi.repository import Notify + from gi.repository import Gtk - available = True # assumed to be working since the import succeeded - _active = False # not yet active - _app_title = None + from logitech.devices.constants import STATUS + + # necessary because the notifications daemon does not know about our XDG_DATA_DIRS + theme = Gtk.IconTheme.get_default() + _icons = {} + + def _icon(title): + if title not in _icons: + icon = theme.lookup_icon(title, 0, 0) + _icons[title] = icon.get_filename() if icon else None + + return _icons.get(title) + + # assumed to be working since the import succeeded + available = True - _TIMEOUT = 5 * 60 # after this many seconds assume the notification object is no longer valid _notifications = {} - def init(app_title, active=True): + def init(app_title=None): """Init the notifications system.""" - global _app_title - _app_title = app_title - return set_active(active) - - - def set_active(active=True): - global available, _active + global available if available: - if active: - if not _active: - try: - _notify.init(_app_title) - _active = True - except: - logging.exception("initializing desktop notifications") - available = False - else: - if _active: - for n in _notifications.values(): - try: - n.close() - except: - logging.exception("closing notification %s", n) - try: - _notify.uninit() - except: - logging.exception("stopping desktop notifications") - available = False - _active = False - return _active + logging.info("starting desktop notifications") + if not Notify.is_initted(): + try: + return Notify.init(app_title or Notify.get_app_name()) + except: + logging.exception("initializing desktop notifications") + available = False + return available and Notify.is_initted() - def active(): - return _active + def uninit(): + if available and Notify.is_initted(): + logging.info("stopping desktop notifications") + _notifications.clear() + Notify.uninit() - def show(status_code, title, text='', icon=None): + def show(dev): """Show a notification with title and text.""" - if available and _active: - n = None - if title in _notifications: - n = _notifications[title] - if timestamp() - n.timestamp > _TIMEOUT: - del _notifications[title] - n = None + if available and Notify.is_initted(): + summary = dev.device_name + # if a notification with same name is already visible, reuse it to avoid spamming + n = _notifications.get(summary) if n is None: - n = _notify.Notification(title) - _notifications[title] = n + n = _notifications[summary] = Notify.Notification() + + n.update(summary, dev.status_text, _icon(summary) or dev.kind) + urgency = Notify.Urgency.LOW if dev.status > STATUS.CONNECTED else Notify.Urgency.NORMAL + n.set_urgency(urgency) - n.update(title, text, icon or title) - n.timestamp = timestamp() try: - # logging.debug("showing notification %s", n) + # logging.debug("showing %s", n) n.show() except Exception: - logging.exception("showing notification %s", n) - + logging.exception("showing %s", n) except ImportError: - logging.warn("python-notify2 not found, desktop notifications are disabled") + logging.warn("Notify not found in gi.repository, desktop notifications are disabled") available = False - active = False - def init(app_title, active=True): return False - def active(): return False - def set_active(active=True): return False - def show(status_code, title, text, icon=None): pass + init = lambda app_title: False + uninit = lambda: None + show = lambda status_code, title, text: None diff --git a/app/ui/window.py b/app/ui/window.py index 65520f8f..a0e7d9cb 100644 --- a/app/ui/window.py +++ b/app/ui/window.py @@ -4,7 +4,7 @@ from gi.repository import (Gtk, Gdk) -from logitech.devices import constants as C +from logitech.devices.constants import (STATUS, PROPS) _SMALL_DEVICE_ICON_SIZE = Gtk.IconSize.BUTTON @@ -40,14 +40,14 @@ def _find_children(container, *child_names): return result if count > 1 else result[0] -def _update_receiver_box(box, rstatus): +def _update_receiver_box(box, receiver): label, buttons = _find_children(box, 'label', 'buttons') - label.set_text(rstatus.text or '') - buttons.set_visible(rstatus.code >= C.STATUS.CONNECTED) + label.set_text(receiver.status_text or '') + buttons.set_visible(receiver.status >= STATUS.CONNECTED) -def _update_device_box(frame, devstatus): - if devstatus is None: +def _update_device_box(frame, dev): + if dev is None: frame.set_visible(False) frame.set_name(_PLACEHOLDER) return @@ -55,19 +55,19 @@ def _update_device_box(frame, devstatus): icon, label = _find_children(frame, 'icon', 'label') frame.set_visible(True) - if frame.get_name() != devstatus.name: - frame.set_name(devstatus.name) - if theme.has_icon(devstatus.name): - icon.set_from_icon_name(devstatus.name, _DEVICE_ICON_SIZE) + if frame.get_name() != dev.name: + frame.set_name(dev.name) + if theme.has_icon(dev.name): + icon.set_from_icon_name(dev.name, _DEVICE_ICON_SIZE) else: - icon.set_from_icon_name(devstatus.type.lower(), _DEVICE_ICON_SIZE) - icon.set_tooltip_text(devstatus.name) - label.set_markup('' + devstatus.name + '') + icon.set_from_icon_name(dev.kind, _DEVICE_ICON_SIZE) + icon.set_tooltip_text(dev.name) + label.set_markup('' + dev.name + '') status = _find_children(frame, 'status') - if devstatus.code < C.STATUS.CONNECTED: + if dev.status < STATUS.CONNECTED: icon.set_sensitive(False) - icon.set_tooltip_text(devstatus.text) + icon.set_tooltip_text(dev.status_text) label.set_sensitive(False) status.set_visible(False) return @@ -79,7 +79,7 @@ def _update_device_box(frame, devstatus): status_icons = status.get_children() battery_icon, battery_label = status_icons[0:2] - battery_level = getattr(devstatus, C.PROPS.BATTERY_LEVEL, None) + battery_level = dev.props.get(PROPS.BATTERY_LEVEL) if battery_level is None: battery_icon.set_sensitive(False) battery_icon.set_from_icon_name('battery_unknown', _STATUS_ICON_SIZE) @@ -92,14 +92,14 @@ def _update_device_box(frame, devstatus): battery_label.set_sensitive(True) battery_label.set_text('%d%%' % battery_level) - battery_status = getattr(devstatus, C.PROPS.BATTERY_STATUS, None) + battery_status = dev.props.get(PROPS.BATTERY_STATUS) if battery_status is None: battery_icon.set_tooltip_text('') else: battery_icon.set_tooltip_text(battery_status) light_icon, light_label = status_icons[2:4] - light_level = getattr(devstatus, C.PROPS.LIGHT_LEVEL, None) + light_level = dev.props.get(PROPS.LIGHT_LEVEL) if light_level is None: light_icon.set_visible(False) light_label.set_visible(False) @@ -111,24 +111,30 @@ def _update_device_box(frame, devstatus): light_label.set_text('%d lux' % light_level) -def update(window, rstatus, devices, icon_name=None): +def update(window, receiver, icon_name=None): if window and window.get_child(): if icon_name is not None: window.set_icon_name(icon_name) vbox = window.get_child() controls = list(vbox.get_children()) - _update_receiver_box(controls[0], rstatus) + + _update_receiver_box(controls[0], receiver) + for index in range(1, len(controls)): - _update_device_box(controls[index], devices.get(index)) + dev = receiver.devices[index] if index in receiver.devices else None + _update_device_box(controls[index], dev) +# +# +# -def _receiver_box(rstatus): +def _receiver_box(name): box = _device_box(False, False) icon, status_box = _find_children(box, 'icon', 'status') - icon.set_from_icon_name(rstatus.name, _SMALL_DEVICE_ICON_SIZE) - icon.set_tooltip_text(rstatus.name) + icon.set_from_icon_name(name, _SMALL_DEVICE_ICON_SIZE) + icon.set_tooltip_text(name) toolbar = Gtk.Toolbar() toolbar.set_name('buttons') @@ -139,10 +145,7 @@ def _receiver_box(rstatus): pair_button = Gtk.ToolButton() pair_button.set_icon_name('add') pair_button.set_tooltip_text('Pair new device') - if rstatus.pair: - pair_button.connect('clicked', rstatus.pair) - else: - pair_button.set_sensitive(False) + pair_button.set_sensitive(False) toolbar.insert(pair_button, 0) toolbar.show_all() @@ -205,17 +208,18 @@ def _device_box(has_status_icons=True, has_frame=True): return box -def create(title, rstatus, systray=False): +def create(title, name, max_devices, systray=False): window = Gtk.Window() window.set_title(title) + # window.set_icon_name(title) window.set_role('status-window') vbox = Gtk.VBox(homogeneous=False, spacing=4) vbox.set_border_width(4) - rbox = _receiver_box(rstatus) + rbox = _receiver_box(name) vbox.add(rbox) - for i in range(1, 1 + rstatus.max_devices): + for i in range(1, 1 + max_devices): dbox = _device_box() vbox.add(dbox) vbox.set_visible(True) @@ -229,35 +233,40 @@ def create(title, rstatus, systray=False): window.set_resizable(False) if systray: - def _state_event(window, event): - if event.new_window_state & Gdk.WindowState.ICONIFIED: - # position = window.get_position() - window.hide() - window.deiconify() - # window.move(*position) - return True + # def _state_event(w, e): + # if e.new_window_state & Gdk.WindowState.ICONIFIED: + # w.hide() + # w.deiconify() + # return True + # window.connect('window-state-event', _state_event) window.set_keep_above(True) - # window.set_deletable(False) + window.set_deletable(False) # window.set_decorated(False) - window.set_position(Gtk.WindowPosition.MOUSE) - # window.set_type_hint(Gdk.WindowTypeHint.MENU) + # window.set_position(Gtk.WindowPosition.MOUSE) + # ulgy, but hides the minimize icon from the window + window.set_type_hint(Gdk.WindowTypeHint.MENU) window.set_skip_taskbar_hint(True) window.set_skip_pager_hint(True) - window.connect('window-state-event', _state_event) - window.connect('delete-event', lambda w, e: toggle(None, window) or True) + window.connect('delete-event', lambda w, e: toggle(None, w) or True) else: - window.set_position(Gtk.WindowPosition.CENTER) + # window.set_position(Gtk.WindowPosition.CENTER) window.connect('delete-event', Gtk.main_quit) return window -def toggle(_, window): +def toggle(icon, window): if window.get_visible(): - # position = window.get_position() + position = window.get_position() window.hide() - # window.move(*position) + window.move(*position) else: + if icon: + x, y = window.get_position() + if x == 0 and y == 0: + x, y, _ = Gtk.StatusIcon.position_menu(Gtk.Menu(), icon) + window.move(x, y) window.present() + return True diff --git a/app/watcher.py b/app/watcher.py index 8daa9bab..4f5766ea 100644 --- a/app/watcher.py +++ b/app/watcher.py @@ -2,231 +2,105 @@ # # -from threading import (Thread, Event) +from threading import Thread import time -from logging import getLogger as _Logger +import logging +from collections import namedtuple -from logitech.unifying_receiver import (api, base) -from logitech.unifying_receiver.listener import EventsListener -from logitech import devices -from logitech.devices import constants as C +from logitech.devices.constants import STATUS +from receiver import Receiver -_l = _Logger('watcher') - -_UNIFYING_RECEIVER = 'Unifying Receiver' -_NO_RECEIVER = 'Receiver not found.' -_INITIALIZING = 'Initializing...' -_SCANNING = 'Scanning...' -_NO_DEVICES = 'No devices found.' -_OKAY = 'Status ok.' +_DUMMY_RECEIVER = namedtuple('_DUMMY_RECEIVER', ['NAME', 'kind', 'status', 'status_text', 'max_devices', 'devices']) +_DUMMY_RECEIVER.__nonzero__ = lambda _: False +_DUMMY_RECEIVER.device_name = Receiver.NAME +DUMMY = _DUMMY_RECEIVER(Receiver.NAME, Receiver.NAME, STATUS.UNAVAILABLE, 'Receiver not found.', Receiver.max_devices, {}) -class _DevStatus(api.AttachedDeviceInfo): - code = C.STATUS.UNKNOWN - text = _INITIALIZING - - def __str__(self): - return 'DevStatus(%d,%s,%d)' % (self.number, self.name, self.code) +def _sleep(seconds, granularity, breakout=lambda: False): + for index in range(0, int(seconds / granularity)): + if breakout(): + return + time.sleep(granularity) class Watcher(Thread): - """Keeps a map of all attached devices and their statuses.""" - def __init__(self, apptitle, notify=None): + """Keeps an active receiver object if possible, and updates the UI when + necessary. + """ + def __init__(self, apptitle, update_ui, notify=None): super(Watcher, self).__init__(group=apptitle, name='Watcher') self.daemon = True self._active = False - self.listener = None - self.no_receiver = Event() + self.update_ui = update_ui + self.notify = notify or (lambda d: None) - self.rstatus = _DevStatus(0, 0xFF, 'UR', _UNIFYING_RECEIVER, ()) - self.rstatus.max_devices = api.C.MAX_ATTACHED_DEVICES - self.rstatus.pair = None - - self.devices = {} - - self.notify = notify - self.status_changed = Event() + self.receiver = DUMMY def run(self): self._active = True + notify_missing = True while self._active: - if self.listener is None: - receiver = api.open() - if receiver: - self._device_status_changed(self.rstatus, C.STATUS.BOOTING, _INITIALIZING) + if self.receiver == DUMMY: + r = Receiver.open() + if r: + logging.info("receiver %s ", r) + self.update_ui(r) + self.notify(r) + r.events_handler = self._events_callback - init = (base.request(receiver, 0xFF, b'\x81\x00') and - base.request(receiver, 0xFF, b'\x80\x00', b'\x00\x01') and - base.request(receiver, 0xFF, b'\x81\x02')) - if init: - _l.debug("receiver initialized ok") + # give it some time to read all devices + r.status_changed.clear() + _sleep(8, 0.4, r.status_changed.is_set) + if r.devices: + logging.info("%d device(s) found", len(r.devices)) + for d in r.devices.values(): + self.notify(d) else: - _l.debug("receiver initialization failed") + # if no devices found so far, assume none at all + logging.info("no devices found") + r.status = STATUS.CONNECTED - self._device_status_changed(self.rstatus, C.STATUS.BOOTING, _SCANNING) - - self.listener = EventsListener(receiver, self._events_callback) - self.listener.start() - - _l.debug("requesting devices status") - self.listener.request(base.request, 0xFF, b'\x80\x02', b'\x02') - - # give it some time to get the devices - time.sleep(3) - elif not self.listener: - self.listener = None - self.devices.clear() - - if self.listener: - if self.devices: - self._device_status_changed(self.rstatus, C.STATUS.CONNECTED, _OKAY) + self.receiver = r + notify_missing = True else: - self._device_status_changed(self.rstatus, C.STATUS.CONNECTED, _NO_DEVICES) + if notify_missing: + _sleep(0.8, 0.4, lambda: not self._active) + notify_missing = False + self.update_ui(DUMMY) + self.notify(DUMMY) + _sleep(4, 0.4, lambda: not self._active) + continue - self.no_receiver.wait() - self.no_receiver.clear() - else: - self._device_status_changed(self.rstatus, C.STATUS.UNAVAILABLE, _NO_RECEIVER) - time.sleep(3) + if self._active: + if self.receiver: + logging.debug("waiting for status_changed") + sc = self.receiver.status_changed + sc.wait() + sc.clear() + logging.debug("status_changed %s %d", sc.reason, sc.urgent) + self.update_ui(self.receiver) + if sc.reason and sc.urgent: + self.notify(sc.reason) + else: + self.receiver = DUMMY + self.update_ui(DUMMY) + self.notify(DUMMY) - if self.listener: - self.listener.stop() - self.listener = None + if self.receiver: + self.receiver.close() def stop(self): if self._active: - _l.debug("stopping %s", self) + logging.info("stopping %s", self) self._active = False - self.no_receiver.set() + if self.receiver: + # break out of an eventual wait() + self.receiver.status_changed.reason = None + self.receiver.status_changed.set() self.join() - def request_status(self, devstatus=None, **kwargs): - """Trigger a status update on a device.""" - if self.listener: - if devstatus is None or devstatus == self.rstatus: - for devstatus in self.devices.values(): - self.request_status(devstatus) - else: - status = devices.request_status(devstatus, self.listener) - self._handle_status(devstatus, status) - - def _handle_status(self, devstatus, status): - if status is not None: - if type(status) == int: - self._device_status_changed(devstatus, status) - else: - self._device_status_changed(devstatus, *status) - - def _new_device(self, dev): - if not self._active: - return None - - if type(dev) == int: - assert self.listener - dev = self.listener.request(api.get_device_info, dev) - - if dev: - devstatus = _DevStatus(*dev) - self.devices[dev.number] = devstatus - self._device_status_changed(devstatus, C.STATUS.CONNECTED) - _l.debug("new devstatus %s", devstatus) - self._device_status_changed(self.rstatus, C.STATUS.CONNECTED, _OKAY) - return devstatus - - def _device_status_changed(self, devstatus, status_code, status_data=None): - old_status_code = devstatus.code - status_text = devstatus.text - - if status_data is None: - if status_code in C.STATUS_NAME: - status_text = C.STATUS_NAME[status_code] - elif isinstance(status_data, str): - status_text = status_data - elif isinstance(status_data, dict): - status_text = '' - for key, value in status_data.items(): - if key == 'text': - status_text = value - else: - setattr(devstatus, key, value) - else: - _l.warn("don't know how to handle status %s", status_data) - return False - - if status_code >= C.STATUS.CONNECTED and devstatus.type is None: - # ghost device that became active - if devstatus.code != C.STATUS.CONNECTED: - # initial update, while we're getting the devinfo - devstatus.code = C.STATUS.CONNECTED - devstatus.text = C.STATUS_NAME[C.STATUS.CONNECTED] - self.status_changed.set() - if self._new_device(devstatus.number) is None: - _l.warn("could not materialize device from %s", devstatus) - return False - - if ((status_code == old_status_code and status_text == devstatus.text) or - (status_code == C.STATUS.CONNECTED and old_status_code > C.STATUS.CONNECTED)): - # this is just successful ping for a device with an already known status - return False - - devstatus.code = status_code - devstatus.text = status_text - _l.debug("%s update %s => %s: %s", devstatus, old_status_code, status_code, status_text) - - if self.notify and (status_code <= C.STATUS.CONNECTED or status_code != old_status_code): - self.notify(devstatus.code, devstatus.name, devstatus.text) - - self.status_changed.set() - return True - def _events_callback(self, event): - if event.code == 0xFF and event.devnumber == 0xFF and event.data is None: - self.no_receiver.set() - return - - if event.code == 0x10 and event.data[0:2] == b'\x41\x04': - # 2 = 0010 ping - # 6 = 0110 off - # a = 1010 on - change = ord(event.data[2:3]) & 0xF0 - status_code = C.STATUS.UNAVAILABLE if change == 0x60 else \ - C.STATUS.CONNECTED if change == 0xA0 else \ - C.STATUS.CONNECTED if change == 0x20 else \ - None - if status_code is None: - _l.warn("don't know how to handle status %x: %s", change, event) - return - - if event.devnumber in self.devices: - devstatus = self.devices[event.devnumber] - self._device_status_changed(devstatus, status_code) - return - - if status_code == C.STATUS.CONNECTED: - self._new_device(event.devnumber) - return - - # a device the UR knows about, but is not connected at this time - dev_id = self.listener.request(base.request, 0xFF, b'\x83\xB5', event.data[4:5]) - name = str(dev_id[2:].rstrip(b'\x00')) if dev_id else '?' - name = devices.C.FULL_NAME[name] - ghost = _DevStatus(handle=self.listener.receiver, number=event.devnumber, type=None, name=name, features=[]) - self.devices[event.devnumber] = ghost - self._device_status_changed(ghost, C.STATUS.UNAVAILABLE) - self._device_status_changed(self.rstatus, C.STATUS.CONNECTED, _OKAY) - return - - if event.devnumber in self.devices: - devstatus = self.devices[event.devnumber] - if event.code == 0x11: - status = devices.process_event(devstatus, event.data, self.listener) - self._handle_status(devstatus, status) - return - if event.code == 0x10 and event.data[:1] == b'\x8F': - self._device_status_changed(devstatus, C.STATUS.UNAVAILABLE) - return - - _l.warn("don't know how to handle event %s", event) + logging.warn("don't know how to handle event %s", event) diff --git a/bin/solaar b/bin/solaar index 6bf9f738..ab108e9b 100755 --- a/bin/solaar +++ b/bin/solaar @@ -1,9 +1,9 @@ #!/bin/sh Z=`readlink -f "$0"` -APP=`dirname "$Z"`/../app -LIB=`dirname "$Z"`/../lib -SHARE=`dirname "$Z"`/../share +APP=`readlink -f $(dirname "$Z")/../app` +LIB=`readlink -f $(dirname "$Z")/../lib` +SHARE=`readlink -f $(dirname "$Z")/../share` export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$LIB/native/`uname -m` export PYTHONPATH=$APP:$LIB diff --git a/lib/logitech/devices/__init__.py b/lib/logitech/devices/__init__.py index c0f1e1ef..2e463fd2 100644 --- a/lib/logitech/devices/__init__.py +++ b/lib/logitech/devices/__init__.py @@ -4,50 +4,49 @@ import logging -from . import k750 -from . import constants as C - +from .constants import (STATUS, PROPS) +from ..unifying_receiver.constants import (FEATURE, BATTERY_STATUS) from ..unifying_receiver import api as _api # # # -_REQUEST_STATUS_FUNCTIONS = { - C.NAME.K750: k750.request_status, - } +_DEVICE_MODULES = {} -_PROCESS_EVENT_FUNCTIONS = { - C.NAME.K750: k750.process_event, - } +def _module(device_name): + if device_name not in _DEVICE_MODULES: + shortname = device_name.split(' ')[-1].lower() + try: + m = __import__(shortname, globals(), level=1) + _DEVICE_MODULES[device_name] = m + except: + # logging.exception(shortname) + _DEVICE_MODULES[device_name] = None + + return _DEVICE_MODULES[device_name] # # # -def ping(devinfo, listener=None): - if listener is None: - reply = _api.ping(devinfo.number) - elif listener: - reply = listener.request(_api.ping, devinfo.number) - else: - return None - - return C.STATUS.CONNECTED if reply else C.STATUS.UNAVAILABLE - - def default_request_status(devinfo, listener=None): - if _api.C.FEATURE.BATTERY in devinfo.features: - if listener is None: - reply = _api.get_device_battery_level(devinfo.handle, devinfo.number, features=devinfo.features) - elif listener: + if FEATURE.BATTERY in devinfo.features: + if listener: reply = listener.request(_api.get_device_battery_level, devinfo.number, features=devinfo.features) else: - reply = None + reply = _api.get_device_battery_level(devinfo.handle, devinfo.number, features=devinfo.features) if reply: discharge, dischargeNext, status = reply - return C.STATUS.CONNECTED, {C.PROPS.BATTERY_LEVEL: discharge} + return STATUS.CONNECTED, {PROPS.BATTERY_LEVEL: discharge, PROPS.BATTERY_STATUS: status} + + if listener: + reply = listener.request(_api.ping, devinfo.number) + else: + reply = _api.ping(devinfo.handle, devinfo.number) + + return STATUS.CONNECTED if reply else STATUS.UNAVAILABLE def default_process_event(devinfo, data, listener=None): @@ -59,21 +58,23 @@ def default_process_event(devinfo, data, listener=None): feature = devinfo.features[feature_index] feature_function = ord(data[1:2]) & 0xF0 - if feature == _api.C.FEATURE.BATTERY: + if feature == FEATURE.BATTERY: if feature_function == 0: discharge = ord(data[2:3]) - status = _api.C.BATTERY_STATUS[ord(data[3:4])] - return C.STATUS.CONNECTED, {C.PROPS.BATTERY_LEVEL: discharge, C.PROPS.BATTERY_STATUS: status} + status = BATTERY_STATUS[ord(data[3:4])] + return STATUS.CONNECTED, {PROPS.BATTERY_LEVEL: discharge, PROPS.BATTERY_STATUS: status} # ? - elif feature == _api.C.FEATURE.REPROGRAMMABLE_KEYS: + elif feature == FEATURE.REPROGRAMMABLE_KEYS: if feature_function == 0: logging.debug('reprogrammable key: %s', repr(data)) # TODO pass # ? - elif feature == _api.C.FEATURE.WIRELESS: + elif feature == FEATURE.WIRELESS: if feature_function == 0: logging.debug("wireless status: %s", repr(data)) + if data[2:5] == b'\x01\x01\x01': + return STATUS.CONNECTED # TODO pass # ? @@ -86,9 +87,10 @@ def request_status(devinfo, listener=None): :param listener: the EventsListener that will be used to send the request, and which will receive the status events from the device. """ - if devinfo.name in _REQUEST_STATUS_FUNCTIONS: - return _REQUEST_STATUS_FUNCTIONS[devinfo.name](devinfo, listener) - return default_request_status(devinfo, listener) or ping(devinfo, listener) + m = _module(devinfo.name) + if m and 'request_status' in m.__dict__: + return m.request_status(devinfo, listener) + return default_request_status(devinfo, listener) def process_event(devinfo, data, listener=None): @@ -101,5 +103,6 @@ def process_event(devinfo, data, listener=None): if default_result is not None: return default_result - if devinfo.name in _PROCESS_EVENT_FUNCTIONS: - return _PROCESS_EVENT_FUNCTIONS[devinfo.name](devinfo, data, listener) + m = _module(devinfo.name) + if m and 'process_event' in m.__dict__: + return m.process_event(devinfo, data, listener) diff --git a/lib/logitech/devices/constants.py b/lib/logitech/devices/constants.py index 17e1f1e1..3fb3bcfd 100644 --- a/lib/logitech/devices/constants.py +++ b/lib/logitech/devices/constants.py @@ -11,47 +11,32 @@ STATUS = type('STATUS', (), )) STATUS_NAME = { + STATUS.UNKNOWN: '...', STATUS.UNAVAILABLE: 'inactive', STATUS.BOOTING: 'initializing', STATUS.CONNECTED: 'connected', } +# device properties that may be reported PROPS = type('PROPS', (), dict( - TEXT='text', BATTERY_LEVEL='battery_level', BATTERY_STATUS='battery_status', LIGHT_LEVEL='light_level', )) - -NAME = type('NAME', (), - dict( - M315='Wireless Mouse M315', - M325='Wireless Mouse M325', - M510='Wireless Mouse M510', - M515='Couch Mouse M515', - M570='Wireless Trackball M570', - K270='Wireless Keyboard K270', - K350='Wireless Keyboard K350', - K750='Wireless Solar Keyboard K750', - K800='Wireless Illuminated Keyboard K800', - )) - -from ..unifying_receiver.common import FallbackDict - -FULL_NAME = FallbackDict(lambda x: x, - dict( - M315=NAME.M315, - M325=NAME.M325, - M510=NAME.M510, - M515=NAME.M515, - M570=NAME.M570, - K270=NAME.K270, - K350=NAME.K350, - K750=NAME.K750, - K800=NAME.K800, - )) - -del FallbackDict +# when the receiver reports a device that is not connected +# (and thus cannot be queried), guess the name and type +# based on this table +NAMES = { + 'M315': ('Wireless Mouse M315', 'mouse'), + 'M325': ('Wireless Mouse M325', 'mouse'), + 'M510': ('Wireless Mouse M510', 'mouse'), + 'M515': ('Couch Mouse M515', 'mouse'), + 'M570': ('Wireless Trackball M570', 'trackball'), + 'K270': ('Wireless Keyboard K270', 'keyboard'), + 'K350': ('Wireless Keyboard K350', 'keyboard'), + 'K750': ('Wireless Solar Keyboard K750', 'keyboard'), + 'K800': ('Wireless Illuminated Keyboard K800', 'keyboard'), + } diff --git a/lib/logitech/devices/k750.py b/lib/logitech/devices/k750.py index 8c87d442..7d595ce4 100644 --- a/lib/logitech/devices/k750.py +++ b/lib/logitech/devices/k750.py @@ -5,40 +5,33 @@ import logging from struct import unpack as _unpack +from .constants import (STATUS, PROPS) +from ..unifying_receiver.constants import FEATURE from ..unifying_receiver import api as _api -from . import constants as C # # # +_CHARGE_LEVELS = (10, 25, 256) def _charge_status(data, hasLux=False): charge, lux = _unpack('!BH', data[2:5]) - d = {} - - _CHARGE_LEVELS = (10, 25, 256) for i in range(0, len(_CHARGE_LEVELS)): if charge < _CHARGE_LEVELS[i]: charge_index = i break - d[C.PROPS.BATTERY_LEVEL] = charge - text = 'Battery %d%%' % charge - if hasLux: - d[C.PROPS.LIGHT_LEVEL] = lux - text = 'Light: %d lux' % lux + ', ' + text - else: - d[C.PROPS.LIGHT_LEVEL] = None - - d[C.PROPS.TEXT] = text - return 0x10 << charge_index, d + return 0x10 << charge_index, { + PROPS.BATTERY_LEVEL: charge, + PROPS.LIGHT_LEVEL: lux if hasLux else None, + } def request_status(devinfo, listener=None): def _trigger_solar_charge_events(handle, devinfo): return _api.request(handle, devinfo.number, - feature=_api.C.FEATURE.SOLAR_CHARGE, function=b'\x03', params=b'\x78\x01', + feature=FEATURE.SOLAR_CHARGE, function=b'\x03', params=b'\x78\x01', features=devinfo.features) if listener is None: reply = _trigger_solar_charge_events(devinfo.handle, devinfo) @@ -48,7 +41,7 @@ def request_status(devinfo, listener=None): reply = 0 if reply is None: - return C.STATUS.UNAVAILABLE + return STATUS.UNAVAILABLE def process_event(devinfo, data, listener=None): @@ -68,4 +61,4 @@ def process_event(devinfo, data, listener=None): # wireless device status if data[2:5] == b'\x01\x01\x01': logging.debug("Keyboard just started") - return C.STATUS.CONNECTED + return STATUS.CONNECTED diff --git a/lib/logitech/unifying_receiver/api.py b/lib/logitech/unifying_receiver/api.py index eccdf34d..34916013 100644 --- a/lib/logitech/unifying_receiver/api.py +++ b/lib/logitech/unifying_receiver/api.py @@ -7,12 +7,15 @@ from struct import pack as _pack from struct import unpack as _unpack from binascii import hexlify as _hexlify -from .common import FirmwareInfo -from .common import AttachedDeviceInfo -from .common import ReprogrammableKeyInfo -from . import constants as C -from . import exceptions as E from . import base as _base +from .common import (FirmwareInfo as _FirmwareInfo, + AttachedDeviceInfo as _AttachedDeviceInfo, + ReprogrammableKeyInfo as _ReprogrammableKeyInfo) +from .constants import (FEATURE, FEATURE_NAME, FEATURE_FLAGS, + FIRMWARE_KIND, DEVICE_KIND, + BATTERY_STATUS, KEY_NAME, + MAX_ATTACHED_DEVICES) +from .exceptions import FeatureNotSupported as _FeatureNotSupported _LOG_LEVEL = 5 @@ -83,7 +86,7 @@ def request(handle, devnumber, feature, function=b'\x00', params=b'', features=N """ feature_index = None - if feature == C.FEATURE.ROOT: + if feature == FEATURE.ROOT: feature_index = b'\x00' else: if features is None: @@ -95,8 +98,8 @@ def request(handle, devnumber, feature, function=b'\x00', params=b'', features=N feature_index = _pack('!B', features.index(feature)) if feature_index is None: - _l.warn("(%d) feature <%s:%s> not supported", devnumber, _hexlify(feature), C.FEATURE_NAME[feature]) - raise E.FeatureNotSupported(devnumber, feature) + _l.warn("(%d) feature <%s:%s> not supported", devnumber, _hexlify(feature), FEATURE_NAME[feature]) + raise _FeatureNotSupported(devnumber, feature) if type(function) == int: function = _pack('!B', function) @@ -129,7 +132,7 @@ def find_device_by_name(handle, name): """ _l.log(_LOG_LEVEL, "searching for device '%s'", name) - for devnumber in range(1, 1 + C.MAX_ATTACHED_DEVICES): + for devnumber in range(1, 1 + MAX_ATTACHED_DEVICES): features = get_device_features(handle, devnumber) if features: d_name = get_device_name(handle, devnumber, features) @@ -146,7 +149,7 @@ def list_devices(handle): devices = [] - for device in range(1, 1 + C.MAX_ATTACHED_DEVICES): + for device in range(1, 1 + MAX_ATTACHED_DEVICES): features = get_device_features(handle, device) if features: devices.append(get_device_info(handle, device, features=features)) @@ -164,9 +167,9 @@ def get_device_info(handle, devnumber, name=None, features=None): if features is None: return None - d_type = get_device_type(handle, devnumber, features) + d_kind = get_device_kind(handle, devnumber, features) d_name = get_device_name(handle, devnumber, features) if name is None else name - devinfo = AttachedDeviceInfo(handle, devnumber, d_type, d_name, features) + devinfo = _AttachedDeviceInfo(handle, devnumber, d_kind, d_name, features) _l.log(_LOG_LEVEL, "(%d) found device %s", devnumber, devinfo) return devinfo @@ -176,12 +179,12 @@ def get_feature_index(handle, devnumber, feature): :returns: An int, or ``None`` if the feature is not available. """ - _l.log(_LOG_LEVEL, "(%d) get feature index <%s:%s>", devnumber, _hexlify(feature), C.FEATURE_NAME[feature]) + _l.log(_LOG_LEVEL, "(%d) get feature index <%s:%s>", devnumber, _hexlify(feature), FEATURE_NAME[feature]) if len(feature) != 2: raise ValueError("invalid feature <%s>: it must be a two-byte string" % feature) # FEATURE.ROOT should always be available for any attached devices - reply = _base.request(handle, devnumber, C.FEATURE.ROOT, feature) + reply = _base.request(handle, devnumber, FEATURE.ROOT, feature) if reply: # only consider active and supported features feature_index = ord(reply[0:1]) @@ -190,18 +193,18 @@ def get_feature_index(handle, devnumber, feature): if _l.isEnabledFor(_LOG_LEVEL): if feature_flags: _l.log(_LOG_LEVEL, "(%d) feature <%s:%s> has index %d: %s", - devnumber, _hexlify(feature), C.FEATURE_NAME[feature], feature_index, - ','.join([C.FEATURE_FLAGS[k] for k in C.FEATURE_FLAGS if feature_flags & k])) + devnumber, _hexlify(feature), FEATURE_NAME[feature], feature_index, + ','.join([FEATURE_FLAGS[k] for k in FEATURE_FLAGS if feature_flags & k])) else: - _l.log(_LOG_LEVEL, "(%d) feature <%s:%s> has index %d", devnumber, _hexlify(feature), C.FEATURE_NAME[feature], feature_index) + _l.log(_LOG_LEVEL, "(%d) feature <%s:%s> has index %d", devnumber, _hexlify(feature), FEATURE_NAME[feature], feature_index) # if feature_flags: # raise E.FeatureNotSupported(devnumber, feature) return feature_index - _l.warn("(%d) feature <%s:%s> not supported by the device", devnumber, _hexlify(feature), C.FEATURE_NAME[feature]) - raise E.FeatureNotSupported(devnumber, feature) + _l.warn("(%d) feature <%s:%s> not supported by the device", devnumber, _hexlify(feature), FEATURE_NAME[feature]) + raise _FeatureNotSupported(devnumber, feature) def get_device_features(handle, devnumber): @@ -214,7 +217,7 @@ def get_device_features(handle, devnumber): # get the index of the FEATURE_SET # FEATURE.ROOT should always be available for all devices - fs_index = _base.request(handle, devnumber, C.FEATURE.ROOT, C.FEATURE.FEATURE_SET) + fs_index = _base.request(handle, devnumber, FEATURE.ROOT, FEATURE.FEATURE_SET) if fs_index is None: # _l.warn("(%d) FEATURE_SET not available", device) return None @@ -246,15 +249,15 @@ def get_device_features(handle, devnumber): if _l.isEnabledFor(_LOG_LEVEL): if feature_flags: _l.log(_LOG_LEVEL, "(%d) feature <%s:%s> at index %d: %s", - devnumber, _hexlify(feature), C.FEATURE_NAME[feature], index, - ','.join([C.FEATURE_FLAGS[k] for k in C.FEATURE_FLAGS if feature_flags & k])) + devnumber, _hexlify(feature), FEATURE_NAME[feature], index, + ','.join([FEATURE_FLAGS[k] for k in FEATURE_FLAGS if feature_flags & k])) else: - _l.log(_LOG_LEVEL, "(%d) feature <%s:%s> at index %d", devnumber, _hexlify(feature), C.FEATURE_NAME[feature], index) + _l.log(_LOG_LEVEL, "(%d) feature <%s:%s> at index %d", devnumber, _hexlify(feature), FEATURE_NAME[feature], index) - features[0] = C.FEATURE.ROOT + features[0] = FEATURE.ROOT while features[-1] is None: del features[-1] - return features + return tuple(features) def get_device_firmware(handle, devnumber, features=None): @@ -262,20 +265,20 @@ def get_device_firmware(handle, devnumber, features=None): :returns: a list of FirmwareInfo tuples, ordered by firmware layer. """ - def _makeFirmwareInfo(level, type, name=None, version=None, build=None, extras=None): - return FirmwareInfo(level, type, name, version, build, extras) + def _makeFirmwareInfo(level, kind, name=None, version=None, build=None, extras=None): + return _FirmwareInfo(level, kind, name, version, build, extras) - fw_count = request(handle, devnumber, C.FEATURE.FIRMWARE, features=features) + fw_count = request(handle, devnumber, FEATURE.FIRMWARE, features=features) if fw_count: fw_count = ord(fw_count[:1]) fw = [] for index in range(0, fw_count): - fw_info = request(handle, devnumber, C.FEATURE.FIRMWARE, function=b'\x10', params=index, features=features) + fw_info = request(handle, devnumber, FEATURE.FIRMWARE, function=b'\x10', params=index, features=features) if fw_info: - fw_level = ord(fw_info[:1]) & 0x0F - if fw_level == 0 or fw_level == 1: - fw_type = C.FIRMWARE_TYPE[fw_level] + level = ord(fw_info[:1]) & 0x0F + if level == 0 or level == 1: + kind = FIRMWARE_KIND[level] name, = _unpack('!3s', fw_info[1:4]) name = name.decode('ascii') version = _hexlify(fw_info[4:6]) @@ -283,31 +286,31 @@ def get_device_firmware(handle, devnumber, features=None): build, = _unpack('!H', fw_info[6:8]) extras = fw_info[9:].rstrip(b'\x00') if extras: - fw_info = _makeFirmwareInfo(level=fw_level, type=fw_type, name=name, version=version, build=build, extras=extras) + fw_info = _makeFirmwareInfo(level, kind, name, version, build, extras) else: - fw_info = _makeFirmwareInfo(level=fw_level, type=fw_type, name=name, version=version, build=build) - elif fw_level == 2: - fw_info = _makeFirmwareInfo(level=2, type=C.FIRMWARE_TYPE[2], version=ord(fw_info[1:2])) + fw_info = _makeFirmwareInfo(level, kind, name, version, build) + elif level == 2: + fw_info = _makeFirmwareInfo(2, FIRMWARE_KIND[2], version=ord(fw_info[1:2])) else: - fw_info = _makeFirmwareInfo(level=fw_level, type=C.FIRMWARE_TYPE[-1]) + fw_info = _makeFirmwareInfo(level, FIRMWARE_KIND[-1]) fw.append(fw_info) _l.log(_LOG_LEVEL, "(%d) firmware %s", devnumber, fw_info) - return fw + return tuple(fw) -def get_device_type(handle, devnumber, features=None): +def get_device_kind(handle, devnumber, features=None): """Reads a device's type. - :see DEVICE_TYPE: + :see DEVICE_KIND: :returns: a string describing the device type, or ``None`` if the device is not available or does not support the ``NAME`` feature. """ - d_type = request(handle, devnumber, C.FEATURE.NAME, function=b'\x20', features=features) - if d_type: - d_type = ord(d_type[:1]) - _l.log(_LOG_LEVEL, "(%d) device type %d = %s", devnumber, d_type, C.DEVICE_TYPE[d_type]) - return C.DEVICE_TYPE[d_type] + d_kind = request(handle, devnumber, FEATURE.NAME, function=b'\x20', features=features) + if d_kind: + d_kind = ord(d_kind[:1]) + _l.log(_LOG_LEVEL, "(%d) device type %d = %s", devnumber, d_kind, DEVICE_KIND[d_kind]) + return DEVICE_KIND[d_kind] def get_device_name(handle, devnumber, features=None): @@ -316,13 +319,13 @@ def get_device_name(handle, devnumber, features=None): :returns: a string with the device name, or ``None`` if the device is not available or does not support the ``NAME`` feature. """ - name_length = request(handle, devnumber, C.FEATURE.NAME, features=features) + name_length = request(handle, devnumber, FEATURE.NAME, features=features) if name_length: name_length = ord(name_length[:1]) d_name = b'' while len(d_name) < name_length: - name_fragment = request(handle, devnumber, C.FEATURE.NAME, function=b'\x10', params=len(d_name), features=features) + name_fragment = request(handle, devnumber, FEATURE.NAME, function=b'\x10', params=len(d_name), features=features) if name_fragment: name_fragment = name_fragment[:name_length - len(d_name)] d_name += name_fragment @@ -339,24 +342,25 @@ def get_device_battery_level(handle, devnumber, features=None): :raises FeatureNotSupported: if the device does not support this feature. """ - battery = request(handle, devnumber, C.FEATURE.BATTERY, features=features) + battery = request(handle, devnumber, FEATURE.BATTERY, features=features) if battery: discharge, dischargeNext, status = _unpack('!BBB', battery[:3]) _l.log(_LOG_LEVEL, "(%d) battery %d%% charged, next level %d%% charge, status %d = %s", - devnumber, discharge, dischargeNext, status, C.BATTERY_STATUSE[status]) - return (discharge, dischargeNext, C.BATTERY_STATUS[status]) + devnumber, discharge, dischargeNext, status, BATTERY_STATUS[status]) + return (discharge, dischargeNext, BATTERY_STATUS[status]) def get_device_keys(handle, devnumber, features=None): - count = request(handle, devnumber, C.FEATURE.REPROGRAMMABLE_KEYS, features=features) + count = request(handle, devnumber, FEATURE.REPROGRAMMABLE_KEYS, features=features) if count: keys = [] count = ord(count[:1]) for index in range(0, count): - keydata = request(handle, devnumber, C.FEATURE.REPROGRAMMABLE_KEYS, function=b'\x10', params=index, features=features) + keydata = request(handle, devnumber, FEATURE.REPROGRAMMABLE_KEYS, function=b'\x10', params=index, features=features) if keydata: key, key_task, flags = _unpack('!HHB', keydata[:5]) - keys.append(ReprogrammableKeyInfo(index, key, C.KEY_NAME[key], key_task, C.KEY_NAME[key_task], flags)) + rki = _ReprogrammableKeyInfo(index, key, KEY_NAME[key], key_task, KEY_NAME[key_task], flags) + keys.append(rki) return keys diff --git a/lib/logitech/unifying_receiver/base.py b/lib/logitech/unifying_receiver/base.py index 1dc5204d..799ca06a 100644 --- a/lib/logitech/unifying_receiver/base.py +++ b/lib/logitech/unifying_receiver/base.py @@ -7,8 +7,9 @@ from logging import getLogger as _Logger from struct import pack as _pack from binascii import hexlify as _hexlify -from . import constants as C -from . import exceptions as E +from .constants import ERROR_NAME +from .exceptions import (NoReceiver as _NoReceiver, + FeatureCallError as _FeatureCallError) import hidapi as _hid @@ -96,7 +97,7 @@ def try_open(path): _l.log(_LOG_LEVEL, "[%s] open failed", path) return None - _l.log(_LOG_LEVEL, "[%s] receiver handle 0x%x", path, receiver_handle) + _l.log(_LOG_LEVEL, "[%s] receiver handle %x", path, receiver_handle) # ping on device id 0 (always an error) _hid.write(receiver_handle, b'\x10\x00\x00\x10\x00\x00\xAA') @@ -176,7 +177,7 @@ def write(handle, devnumber, data): if not _hid.write(handle, wdata): _l.warn("(%d) write failed, assuming receiver %x no longer available", devnumber, handle) close(handle) - raise E.NoReceiver + raise _NoReceiver def read(handle, timeout=DEFAULT_TIMEOUT): @@ -199,7 +200,7 @@ def read(handle, timeout=DEFAULT_TIMEOUT): if data is None: _l.warn("(-) read failed, assuming receiver %x no longer available", handle) close(handle) - raise E.NoReceiver + raise _NoReceiver if data: if len(data) < _MIN_REPLY_SIZE: @@ -274,11 +275,11 @@ def request(handle, devnumber, feature_index_function, params=b'', features=None if reply_code == 0x11 and reply_data[0] == b'\xFF' and reply_data[1:3] == feature_index_function: # the feature call returned with an error error_code = ord(reply_data[3]) - _l.warn("(%d) request feature call error %d = %s: %s", devnumber, error_code, C.ERROR_NAME[error_code], _hexlify(reply_data)) + _l.warn("(%d) request feature call error %d = %s: %s", devnumber, error_code, ERROR_NAME[error_code], _hexlify(reply_data)) feature_index = ord(feature_index_function[:1]) feature_function = feature_index_function[1:2] feature = None if features is None else features[feature_index] if feature_index < len(features) else None - raise E.FeatureCallError(devnumber, feature, feature_index, feature_function, error_code, reply_data) + raise _FeatureCallError(devnumber, feature, feature_index, feature_function, error_code, reply_data) if reply_code == 0x11 and reply_data[:2] == feature_index_function: # a matching reply diff --git a/lib/logitech/unifying_receiver/common.py b/lib/logitech/unifying_receiver/common.py index b9af9524..6a181e2a 100644 --- a/lib/logitech/unifying_receiver/common.py +++ b/lib/logitech/unifying_receiver/common.py @@ -3,6 +3,7 @@ # from binascii import hexlify as _hexlify +from collections import namedtuple class FallbackDict(dict): @@ -21,20 +22,18 @@ def list2dict(values_list): return dict(zip(range(0, len(values_list)), values_list)) -from collections import namedtuple - """Tuple returned by list_devices and find_device_by_name.""" AttachedDeviceInfo = namedtuple('AttachedDeviceInfo', [ 'handle', 'number', - 'type', + 'kind', 'name', 'features']) """Firmware information.""" FirmwareInfo = namedtuple('FirmwareInfo', [ 'level', - 'type', + 'kind', 'name', 'version', 'build', @@ -49,6 +48,7 @@ ReprogrammableKeyInfo = namedtuple('ReprogrammableKeyInfo', [ 'task_name', 'flags']) + class Packet(namedtuple('Packet', ['code', 'devnumber', 'data'])): def __str__(self): return 'Packet(0x%02x,%d,%s)' % (self.code, self.devnumber, '' if self.data is None else _hexlify(self.data)) diff --git a/lib/logitech/unifying_receiver/constants.py b/lib/logitech/unifying_receiver/constants.py index c452ad1e..98669b4b 100644 --- a/lib/logitech/unifying_receiver/constants.py +++ b/lib/logitech/unifying_receiver/constants.py @@ -48,17 +48,17 @@ FEATURE_NAME[FEATURE.SOLAR_CHARGE] = 'SOLAR_CHARGE' FEATURE_FLAGS = { 0x20: 'internal', 0x40: 'hidden', 0x80: 'obsolete' } -_DEVICE_TYPES = ('Keyboard', 'Remote Control', 'NUMPAD', 'Mouse', - 'Touchpad', 'Trackball', 'Presenter', 'Receiver') +_DEVICE_KINDS = ('keyboard', 'remote control', 'numpad', 'mouse', + 'touchpad', 'trackball', 'presenter', 'receiver') """Possible types of devices connected to an UR.""" -DEVICE_TYPE = FallbackDict(lambda x: 'unknown', list2dict(_DEVICE_TYPES)) +DEVICE_KIND = FallbackDict(lambda x: 'unknown', list2dict(_DEVICE_KINDS)) -_FIRMWARE_TYPES = ('Main (HID)', 'Bootloader', 'Hardware', 'Other') +_FIRMWARE_KINDS = ('Main (HID)', 'Bootloader', 'Hardware', 'Other') """Names of different firmware levels possible, indexed by level.""" -FIRMWARE_TYPE = FallbackDict(lambda x: 'Unknown', list2dict(_FIRMWARE_TYPES)) +FIRMWARE_KIND = FallbackDict(lambda x: 'Unknown', list2dict(_FIRMWARE_KINDS)) _BATTERY_STATUSES = ('Discharging (in use)', 'Recharging', 'Almost full', diff --git a/lib/logitech/unifying_receiver/exceptions.py b/lib/logitech/unifying_receiver/exceptions.py index 452cd5a2..69795cd4 100644 --- a/lib/logitech/unifying_receiver/exceptions.py +++ b/lib/logitech/unifying_receiver/exceptions.py @@ -2,7 +2,7 @@ # Exceptions that may be raised by this API. # -from . import constants as C +from .constants import (FEATURE_NAME, ERROR_NAME) class NoReceiver(Exception): @@ -16,21 +16,21 @@ class NoReceiver(Exception): class FeatureNotSupported(Exception): """Raised when trying to request a feature not supported by the device.""" def __init__(self, devnumber, feature): - super(FeatureNotSupported, self).__init__(devnumber, feature, C.FEATURE_NAME[feature]) + super(FeatureNotSupported, self).__init__(devnumber, feature, FEATURE_NAME[feature]) self.devnumber = devnumber self.feature = feature - self.feature_name = C.FEATURE_NAME[feature] + self.feature_name = FEATURE_NAME[feature] class FeatureCallError(Exception): """Raised if the device replied to a feature call with an error.""" def __init__(self, devnumber, feature, feature_index, feature_function, error_code, data=None): - super(FeatureCallError, self).__init__(devnumber, feature, feature_index, feature_function, error_code, C.ERROR_NAME[error_code]) + super(FeatureCallError, self).__init__(devnumber, feature, feature_index, feature_function, error_code, ERROR_NAME[error_code]) self.devnumber = devnumber self.feature = feature - self.feature_name = None if feature is None else C.FEATURE_NAME[feature] + self.feature_name = None if feature is None else FEATURE_NAME[feature] self.feature_index = feature_index self.feature_function = feature_function self.error_code = error_code - self.error_string = C.ERROR_NAME[error_code] + self.error_string = ERROR_NAME[error_code] self.data = data diff --git a/lib/logitech/unifying_receiver/listener.py b/lib/logitech/unifying_receiver/listener.py index 8023797f..5cf7fe10 100644 --- a/lib/logitech/unifying_receiver/listener.py +++ b/lib/logitech/unifying_receiver/listener.py @@ -7,8 +7,8 @@ from threading import (Thread, Event, Lock) # from time import sleep as _sleep from . import base as _base -from . import exceptions as E -from .common import Packet +from .exceptions import NoReceiver as _NoReceiver +from .common import Packet as _Packet # for both Python 2 and 3 try: @@ -17,95 +17,109 @@ except ImportError: from queue import Queue -_LOG_LEVEL = 4 +_LOG_LEVEL = 6 _l = _Logger('lur.listener') _READ_EVENT_TIMEOUT = int(_base.DEFAULT_TIMEOUT / 4) # ms -def _callback_caller(listener, callback): - # _l.log(_LOG_LEVEL, "%s starting callback caller", listener) - while listener._active or not listener.events.empty(): - event = listener.events.get() - _l.log(_LOG_LEVEL, "%s delivering event %s", listener, event) +def _event_dispatch(listener, callback): + # _l.log(_LOG_LEVEL, "starting dispatch") + while listener._active: # or not listener._events.empty(): + event = listener._events.get() + _l.log(_LOG_LEVEL, "delivering event %s", event) try: - callback.__call__(event) + callback(event) except: _l.exception("callback for %s", event) - # _l.log(_LOG_LEVEL, "%s stopped callback caller", listener) + # _l.log(_LOG_LEVEL, "stopped dispatch") class EventsListener(Thread): """Listener thread for events from the Unifying Receiver. - Incoming events (reply_code, devnumber, data) will be passed to the callback - function in sequence, by a separate thread. + Incoming packets will be passed to the callback function in sequence, by a + separate thread. - While this listener is running, you should use the request() method to make - regular UR API calls, otherwise the expected API replies are most likely to - be captured by the listener and delivered as events to the callback. + While this listener is running, you must use the request() method to make + regular UR API calls; otherwise the expected API replies are most likely to + be captured by the listener and delivered to the callback. """ - def __init__(self, receiver, events_callback): - super(EventsListener, self).__init__(group='Unifying Receiver', name='Events-%x' % receiver) + def __init__(self, receiver_handle, events_callback): + super(EventsListener, self).__init__(group='Unifying Receiver', name='%s-%x' % (self.__class__.__name__, receiver_handle)) + self.daemon = True self._active = False - self.receiver = receiver + self._handle = receiver_handle - self.task = None - self.task_processing = Lock() - self.task_reply = None - self.task_done = Event() + self._task = None + self._task_processing = Lock() + self._task_reply = None + self._task_done = Event() - self.events = Queue(32) + self._events = Queue(32) + _base.unhandled_hook = self._unhandled - self.event_caller = Thread(group='Unifying Receiver', name='Callback-%x' % receiver, target=_callback_caller, args=(self, events_callback)) - self.event_caller.daemon = True - - self.__str_cached = 'Events(%x)' % self.receiver + self._dispatcher = Thread(group='Unifying Receiver', + name='%s-%x-dispatch' % (self.__class__.__name__, receiver_handle), + target=_event_dispatch, args=(self, events_callback)) + self._dispatcher.daemon = True def run(self): self._active = True - _l.log(_LOG_LEVEL, "%s started", self) + _l.log(_LOG_LEVEL, "started") - self.__str_cached = 'Events(%x:active)' % self.receiver - self.event_caller.start() - - last_hook = _base.unhandled_hook - _base.unhandled_hook = self._unhandled + self._dispatcher.start() while self._active: event = None try: - event = _base.read(self.receiver, _READ_EVENT_TIMEOUT) - except E.NoReceiver: - self.receiver = 0 - _l.warn("%s receiver disconnected", self) - self.events.put(Packet(0xFF, 0xFF, None)) + event = _base.read(self._handle, _READ_EVENT_TIMEOUT) + except _NoReceiver: + self._handle = 0 + _l.warn("receiver disconnected") + self._events.put(_Packet(0xFF, 0xFF, None)) self._active = False + break if event: - _l.log(_LOG_LEVEL, "%s queueing event %s", self, event) - self.events.put(Packet(*event)) + _l.log(_LOG_LEVEL, "queueing event %s", event) + self._events.put(_Packet(*event)) - if self.task: - task, self.task = self.task, None - self.task_reply = self._make_request(*task) - self.task_done.set() + if self._task: + (api_function, args, kwargs), self._task = self._task, None + # _l.log(_LOG_LEVEL, "calling '%s.%s' with %s, %s", api_function.__module__, api_function.__name__, args, kwargs) + try: + self._task_reply = api_function.__call__(self._handle, *args, **kwargs) + except _NoReceiver as nr: + self._handle = 0 + _l.warn("receiver disconnected") + self._events.put(_Packet(0xFF, 0xFF, None)) + self._task_reply = nr + self._active = False + break + except Exception as e: + # _l.exception("task %s.%s", api_function.__module__, api_function.__name__) + self._task_reply = e + finally: + self._task_done.set() - _base.close(self.receiver) - self.__str_cached = 'Events(%x)' % self.receiver - - _base.unhandled_hook = last_hook + _base.close(self._handle) def stop(self): """Tells the listener to stop as soon as possible.""" if self._active: - _l.log(_LOG_LEVEL, "stopping %s", self) + _l.log(_LOG_LEVEL, "stopping") self._active = False + # wait for the receiver handle to be closed self.join() + @property + def handle(self): + return self._handle + def request(self, api_function, *args, **kwargs): """Make an UR API request through this listener's receiver. @@ -114,37 +128,24 @@ class EventsListener(Thread): """ # _l.log(_LOG_LEVEL, "%s request '%s.%s' with %s, %s", self, api_function.__module__, api_function.__name__, args, kwargs) - if not self._active: - return None + # if not self._active: + # return None - with self.task_processing: - self.task_done.clear() - self.task = (api_function, args, kwargs) - self.task_done.wait() - reply, self.task_reply = self.task_reply, None + with self._task_processing: + self._task_done.clear() + self._task = (api_function, args, kwargs) + self._task_done.wait() + reply, self._task_reply = self._task_reply, None # _l.log(_LOG_LEVEL, "%s request '%s.%s' => %s", self, api_function.__module__, api_function.__name__, repr(reply)) if isinstance(reply, Exception): raise reply return reply - def _make_request(self, api_function, args, kwargs): - _l.log(_LOG_LEVEL, "%s calling '%s.%s' with %s, %s", self, api_function.__module__, api_function.__name__, args, kwargs) - try: - return api_function.__call__(self.receiver, *args, **kwargs) - except E.NoReceiver as nr: - self.task_reply = nr - self._active = False - except Exception as e: - self.task_reply = e - def _unhandled(self, reply_code, devnumber, data): - event = Packet(reply_code, devnumber, data) - # _l.log(_LOG_LEVEL, "%s queueing unhandled event %s", self, event) - self.events.put(event) - - def __str__(self): - return self.__str_cached + event = _Packet(reply_code, devnumber, data) + # _l.log(_LOG_LEVEL, "queueing unhandled event %s", event) + self._events.put(event) def __nonzero__(self): - return self._active and self.receiver + return self._active and self._handle diff --git a/lib/logitech/unifying_receiver/tests/test_50_api.py b/lib/logitech/unifying_receiver/tests/test_50_api.py index 390606ab..b29ccfe1 100644 --- a/lib/logitech/unifying_receiver/tests/test_50_api.py +++ b/lib/logitech/unifying_receiver/tests/test_50_api.py @@ -91,12 +91,12 @@ class Test_UR_API(unittest.TestCase): for fw in d_firmware: self.assertIsInstance(fw, FirmwareInfo) - def test_52_get_device_type(self): + def test_52_get_device_kind(self): self._check(check_features=True) - d_type = api.get_device_type(self.handle, self.device, self.features) - self.assertIsNotNone(d_type, "failed to get device type") - self.assertGreater(len(d_type), 0, "empty device type") + d_kind = api.get_device_kind(self.handle, self.device, self.features) + self.assertIsNotNone(d_kind, "failed to get device kind") + self.assertGreater(len(d_kind), 0, "empty device kind") def test_55_get_device_name(self): self._check(check_features=True) diff --git a/lib/logitech/ur_scanner.py b/lib/logitech/ur_scanner.py index 3a5c040c..a2e1d6d8 100644 --- a/lib/logitech/ur_scanner.py +++ b/lib/logitech/ur_scanner.py @@ -31,12 +31,12 @@ def scan_devices(receiver): return for devinfo in devices: - print ("Device [%d] %s (%s)" % (devinfo.number, devinfo.name, devinfo.type)) + print ("Device [%d] %s (%s)" % (devinfo.number, devinfo.name, devinfo.kind)) # print " Protocol %s" % devinfo.protocol firmware = api.get_device_firmware(receiver, devinfo.number, features=devinfo.features) for fw in firmware: - print (" %s firmware: %s version %s build %d" % (fw.type, fw.name, fw.version, fw.build)) + print (" %s firmware: %s version %s build %d" % (fw.kind, fw.name, fw.version, fw.build)) for index in range(0, len(devinfo.features)): feature = devinfo.features[index] @@ -77,3 +77,11 @@ if __name__ == '__main__': break else: print ("!! Logitech Unifying Receiver not found.") + + + # import pyudev + # ctx = pyudev.Context() + # m = pyudev.Monitor.from_netlink(ctx) + # m.filter_by(subsystem='hid') + # for action, device in m: + # print '%s: %s' % (action, device) diff --git a/share/icons/hicolor/128x128/apps/Solaar-init.png b/share/icons/hicolor/128x128/apps/Solaar-init.png new file mode 100644 index 0000000000000000000000000000000000000000..6b7c00b29c4fc08ddc510e272d82136a863f6e31 GIT binary patch literal 2670 zcmZuzc{tQ-8-9N?W=0H3F}5sWa16=TVwd5B}@2{CQ^}P--&SI7$nDD z%^)Tx#%_kl9Mx$-_K?9ho$LGad#>xb@B6*q>wW)tu6KFj?N1Rzgk*#O03tTl77l!- z{z^e4A3IDhyyX+}B9UML5`E&t<{iE+toGUKdN_@ zi3aMDym7L|&E7ihkOs5#^RnzBmklMZH$3*PC9!#7_1$Ga;{ zE~*4kpf9`fI!W#5dfA_8lA#V=H*2JpN)c1&x>x>Q%86PXZ){8-7?ECo0z2`54kvJ6i^hb`z(u|OM0 zeqxsWX+o2$JGW7ANDrj|XxRL0FZdBS7e`$ND*Nfm8fH^a_M4f#fC4>}U>Jg{W8(Ph zk}jws41;hiwg#2-D80`E#l`rxMA=b9?3DyYKW6^k$aqSn--jipk}~n&ZbPG_4L_>xWTuSEXTKVB})-s z5_7i9jX_B})#J**YQScmc@pVjt7bm>HPhTf9RK{yqAPHfw*EcDc<`v&>z?1D)T&?$ zSF_^MBI(N6=p(1SLzB3uwCqIF4Atau9QF03l?ulhBwZueCF`Ox9B*z^d~NIf5Ld9%x748Y z+(@l7x%cbYTf4}i#Ds@vATfu~sg{YNo8VY#vWdZ0RQY5aeZ!^XhDU7%^vqfTcH|Ek z9yP=b{nS}%j`xcqU@Y>(?-{^6{q*N_HNk-2xR1a4HS0$Qdqf#*`;FkTAms)zY}Va}pZ7hs zhvVDWw5Fu420xYho>^d_T;bzE@XnKeoGneQ+Yn)qrUlef?p9qU!?;n$R~6C?2VvB^ zRiDWa2^|^ETUUc=+O*KURvp_TMpRLwZFu)pP2+*tAw&L)t6t$qSf<7-?iO$O!Qs-eWH?Le|wa? zBaFW{riVwpnu=b%7rIUkVtsgSA41@KcwQR(82NcAH_i=3FC;QDd3P!Jrjx=zrXirX z0?EmpN)|j4-v6sQ?qnFrJv!D{GI!zUY7u_)n z{5YKWWB%q|eM5{X38${TjiQk+8=A1@fqlM}~EnHYVrVEpP9k$FC$W^H?n#$Pd7 zx*l^30PZ7%#8TNp$^rLEskPyQ+XU}v zsJx3tekcG$D+_h)It_2YSkmyCO}#-UN{EKxkPt%;{3s8*-)8ZE8A$hO;%?40CQ!oa z2(>x`u=p`%j?|j1A!Q$3eFp# z{$vXDNA>;{;BH^vIX z8~W(eyGJ9s=E5dU4YHtinZETO8|UPV%-$_0jk`#QXd2#zio6a=ycGcbVPQKZIZRm9 zckJI*T3HBT5S|vKyzEj`Kl5#yP@m+eD0POS0X^?rRm_TXA)N^_9z?mlN^$25juYPOyF zx0hTb|085sOBdlZZhwPQ={vlt1ke4l3StFq9BcZ~8z6#s2JUT$)c3LL$); zdkFrF*`a|M0Ll8Bnp-_r0_!i^0_)*&oqjS_Fd~h$eF4z#blr>-S5<#(fac0jZ{_at ztK1b!x2I@%L`lPg_AuxW55uqn$3Zwy$?%o~nUElGL2#KMT8O0QB+r(SKz~0x(SYdB;gTOPcV?Kf-&d>#UTl$xw zObBfXl_RO8?KKm%i9+yTlR2(U(Ev^&s^a?actAa8skF4z;c`q$aRrmcP9PumzSI;i zsD_D6Ug2JS_|o%(r?I@(JZ)R1sIb%KCEKcw6U^^2GNSQS?6b=F+K9wb-!tclfSjZ@ z=lsW>tm4#6kg9t-hb3v)$2=++uo&(bEY_)YCI73l~u%HQ}9w+g* z=g=UQ!D;gn2T}rEBHO7Df6{j$C;Q*{GO`1@n}nCCWg8FqZ6L6*JY`XRf)w*#bU4g# literal 0 HcmV?d00001