From c829304e31f802095940e6047f219aef8963adae Mon Sep 17 00:00:00 2001 From: Daniel Pavel Date: Wed, 1 May 2013 15:47:23 +0200 Subject: [PATCH] use only udev events to detect receiver devices --- lib/hidapi/udev.py | 30 +++++++-- lib/logitech/unifying_receiver/base.py | 16 +++++ lib/logitech/unifying_receiver/listener.py | 5 +- lib/logitech/unifying_receiver/receiver.py | 28 +++----- lib/solaar/gtk.py | 76 ++++++++++++++-------- lib/solaar/listener.py | 6 +- 6 files changed, 105 insertions(+), 56 deletions(-) diff --git a/lib/hidapi/udev.py b/lib/hidapi/udev.py index 0c5c730b..014f2153 100644 --- a/lib/hidapi/udev.py +++ b/lib/hidapi/udev.py @@ -94,13 +94,29 @@ def monitor(callback, *device_filters): m = _Monitor.from_netlink(_Context()) m.filter_by(subsystem='hidraw') for action, device in m: - hid_dev = device.find_parent('hid') - if hid_dev: - for filter in device_filters: - dev_info = _match(device, hid_dev, *filter) - if dev_info: - callback(action, dev_info) - break + if action == 'add': + hid_dev = device.find_parent(subsystem='hid') + # print ("****", action, device, hid_dev) + if hid_dev: + for filter in device_filters: + d_info = _match(device, hid_dev, *filter) + if d_info: + callback(action, d_info) + break + elif action == 'remove': + # this is ugly, but... well. + # signal the callback for each removed device; it will have figure + # out for itself if it's a device it should handle + d_info = DeviceInfo(path=device.device_node, + vendor_id=None, + product_id=None, + serial=None, + release=None, + manufacturer=None, + product=None, + interface=None, + driver=None) + callback(action, d_info) def enumerate(vendor_id=None, product_id=None, interface_number=None, driver=None): diff --git a/lib/logitech/unifying_receiver/base.py b/lib/logitech/unifying_receiver/base.py index 8c3426d9..103d817f 100644 --- a/lib/logitech/unifying_receiver/base.py +++ b/lib/logitech/unifying_receiver/base.py @@ -74,6 +74,22 @@ def receivers(): # yield d +def notify_on_receivers(callback): + """Starts a thread that monitors receiver events from udev.""" + from threading import Thread as _Thread + t = _Thread(name='receivers_monitor', target=_hid.monitor, + args=(callback, + DEVICE_UNIFYING_RECEIVER, + DEVICE_UNIFYING_RECEIVER_2, + # DEVICE_NANO_RECEIVER, + )) + t.daemon = True + t.start() + + # the HID monitor will only send events when devices are added/removed, + # so we need to trigger the callback for all currently detected receivers + for r in receivers(): + callback('add', r) def open_path(path): diff --git a/lib/logitech/unifying_receiver/listener.py b/lib/logitech/unifying_receiver/listener.py index d8365826..ddf4515d 100644 --- a/lib/logitech/unifying_receiver/listener.py +++ b/lib/logitech/unifying_receiver/listener.py @@ -114,9 +114,6 @@ class EventsListener(_threading.Thread): def __init__(self, receiver, notifications_callback): super(EventsListener, self).__init__(name=self.__class__.__name__) - # replace the handle with a threaded one - receiver.handle = _ThreadedHandle(self, receiver.path, receiver.handle) - self.daemon = True self._active = False @@ -129,7 +126,9 @@ class EventsListener(_threading.Thread): def run(self): self._active = True + # replace the handle with a threaded one ihandle = int(self.receiver.handle) + self.receiver.handle = _ThreadedHandle(self, self.receiver.path, self.receiver.handle) _log.info("started with %s (%d)", self.receiver, ihandle) self.has_started() diff --git a/lib/logitech/unifying_receiver/receiver.py b/lib/logitech/unifying_receiver/receiver.py index 12ee6475..1d0a3a1f 100644 --- a/lib/logitech/unifying_receiver/receiver.py +++ b/lib/logitech/unifying_receiver/receiver.py @@ -375,24 +375,16 @@ class Receiver(object): __bool__ = __nonzero__ = lambda self: self.handle is not None @classmethod - def open(self): - """Opens the first Logitech Unifying Receiver found attached to the machine. + def open(self, path): + """Opens a Logitech Receiver found attached to the machine, by device path. :returns: An open file handle for the found receiver, or ``None``. """ - exception = None - - for rawdevice in _base.receivers(): - exception = None - try: - handle = _base.open_path(rawdevice.path) - if handle: - return Receiver(handle, rawdevice.path) - except OSError as e: - _log.exception("open %s", rawdevice.path) - if e.errno == _errno.EACCES: - exception = e - - if exception: - # only keep the last exception - raise exception + try: + handle = _base.open_path(path) + if handle: + return Receiver(handle, path) + except OSError as e: + _log.exception("open %s", path) + if e.errno == _errno.EACCES: + raise diff --git a/lib/solaar/gtk.py b/lib/solaar/gtk.py index b2d4982e..b994e4bb 100644 --- a/lib/solaar/gtk.py +++ b/lib/solaar/gtk.py @@ -45,41 +45,54 @@ def _run(args): ui.notify.init() - from solaar.listener import DUMMY_RECEIVER, ReceiverListener window = ui.main_window.create(NAME) assert window icon = ui.status_icon.create(window) assert icon listeners = {} + from logitech.unifying_receiver import base as _base - # initializes the receiver listener - def check_for_listener(notify=False): - # print ("check_for_listener", notify) + from solaar.listener import ReceiverListener - try: - l = ReceiverListener.open(status_changed) - except OSError: - l = None - ui.error_dialog(window, 'Permissions error', - 'Found a possible Unifying Receiver device,\n' - 'but did not have permission to open it.') + def handle_receivers_events(action, device): + assert action is not None + assert device is not None - listeners.clear() - if l: - listeners[l.receiver.serial] = l - else: - if notify: - status_changed(DUMMY_RECEIVER) - else: - return True + if action == 'add': + # a new receiver device was detected + if not listeners: + # handle only one receiver for now, the rest are ignored + try: + l = ReceiverListener.open(device.path, status_changed) + if l is not None: + listeners[device.path] = l + except OSError: + # permission error, blacklist this path for now + listeners.pop(device.path, None) + import logging + logging.exception("failed to open %s", device.path) + # ui.error_dialog(window, 'Permissions error', + # 'Found a possible Unifying Receiver device,\n' + # 'but did not have permission to open it.') - from gi.repository import Gtk, GLib - from logitech.unifying_receiver.status import ALERT + elif action == 'remove': + # we'll be receiving remove events for any hidraw devices, + # not just Logitech receivers, so it's okay if the device is not + # already in our listeners map + l = listeners.pop(device.path, None) + if l is not None: + assert isinstance(l, ReceiverListener) + l.stop() + + # print ("****", action, device, listeners) # callback delivering status notifications from the receiver/devices to the UI + from gi.repository import GLib + from logitech.unifying_receiver.status import ALERT def status_changed(device, alert=ALERT.NONE, reason=None): assert device is not None + # print ("status changed", device, reason) if alert & ALERT.SHOW_WINDOW: GLib.idle_add(window.present) @@ -87,14 +100,25 @@ def _run(args): GLib.idle_add(ui.status_icon.update, icon, device) if ui.notify.available: - # always notify on receiver updates - if device is DUMMY_RECEIVER or alert & ALERT.NOTIFICATION: + if alert & ALERT.NOTIFICATION: + GLib.idle_add(ui.notify.show, device, reason) + elif device.kind is None and not device: + # notify when a receiver was removed GLib.idle_add(ui.notify.show, device, reason) - if device is DUMMY_RECEIVER: - GLib.timeout_add(3000, check_for_listener) + # if device.kind is None and not device: + # # a receiver was removed + # listeners.clear() - GLib.timeout_add(10, check_for_listener, True) + # ugly... + def _startup_check_receiver(): + from solaar.listener import DUMMY_RECEIVER + if not listeners: + status_changed(DUMMY_RECEIVER, ALERT.NOTIFICATION) + GLib.timeout_add(1000, _startup_check_receiver) + + GLib.timeout_add(10, _base.notify_on_receivers, handle_receivers_events) + from gi.repository import Gtk Gtk.main() map(ReceiverListener.stop, listeners.values()) diff --git a/lib/solaar/listener.py b/lib/solaar/listener.py index dd8ae8dc..740c052b 100644 --- a/lib/solaar/listener.py +++ b/lib/solaar/listener.py @@ -153,10 +153,12 @@ class ReceiverListener(_listener.EventsListener): __unicode__ = __str__ @classmethod - def open(self, status_changed_callback): + def open(self, path, status_changed_callback): assert status_changed_callback - receiver = Receiver.open() + receiver = Receiver.open(path) if receiver: rl = ReceiverListener(receiver, status_changed_callback) rl.start() return rl + else: + _log.warn("failed to open %s", path)