From 692ea58937a74c73ce5cdf9ad368ed1e55ec5c2e Mon Sep 17 00:00:00 2001 From: Daniel Pavel Date: Thu, 27 Sep 2012 15:52:42 +0300 Subject: [PATCH] raise NoReceiver if the receiver is removed during read --- lib/logitech/unifying_receiver/api.py | 2 +- lib/logitech/unifying_receiver/base.py | 14 +++-- lib/logitech/unifying_receiver/listener.py | 63 +++++++++++++++------- 3 files changed, 56 insertions(+), 23 deletions(-) diff --git a/lib/logitech/unifying_receiver/api.py b/lib/logitech/unifying_receiver/api.py index fd37d053..35f3c60a 100644 --- a/lib/logitech/unifying_receiver/api.py +++ b/lib/logitech/unifying_receiver/api.py @@ -187,7 +187,7 @@ def list_devices(handle): def get_device_info(handle, device, device_name=None, features_array=None): - """Gets the complete info for a device. + """Gets the complete info for a device (type, name, firmwares, and features_array). :returns: an AttachedDeviceInfo tuple, or ``None``. """ diff --git a/lib/logitech/unifying_receiver/base.py b/lib/logitech/unifying_receiver/base.py index f2aa5c0a..83a2480d 100644 --- a/lib/logitech/unifying_receiver/base.py +++ b/lib/logitech/unifying_receiver/base.py @@ -141,7 +141,7 @@ def write(handle, device, data): :raises NoReceiver: if the receiver is no longer available, i.e. has been physically removed from the machine, or the kernel driver has been - unloaded. + unloaded. The handle will be closed automatically. """ wdata = b'\x10' + chr(device) + data + b'\x00' * (5 - len(data)) _l.log(_LOG_LEVEL, "(%d,%d) <= w[%s]", handle, device, wdata.encode('hex')) @@ -166,8 +166,17 @@ def read(handle, timeout=DEFAULT_TIMEOUT): (reply_code, device, message data). The reply code should be ``0x11`` for a successful feature call, or ``0x10`` to indicate some error, e.g. the device is no longer available. + + :raises NoReceiver: if the receiver is no longer available, i.e. has + been physically removed from the machine, or the kernel driver has been + unloaded. The handle will be closed automatically. """ data = _hid.read(handle, _MAX_REPLY_SIZE * 2, timeout) + if data is None: + _l.warn("(%d,*) read failed, assuming receiver no longer available", handle) + close(handle) + raise NoReceiver + if data: _l.log(_LOG_LEVEL, "(%d,*) => r[%s]", handle, data.encode('hex')) if len(data) < _MIN_REPLY_SIZE: @@ -175,8 +184,7 @@ def read(handle, timeout=DEFAULT_TIMEOUT): if len(data) > _MAX_REPLY_SIZE: _l.warn("(%d,*) => r[%s] read packet too long: %d bytes", handle, data.encode('hex'), len(data)) return ord(data[0]), ord(data[1]), data[2:] - else: - _l.log(_LOG_LEVEL, "(%d,*) => r[]", handle) + _l.log(_LOG_LEVEL, "(%d,*) => r[]", handle) def request(handle, device, feature_index_function, params=b'', features_array=None): diff --git a/lib/logitech/unifying_receiver/listener.py b/lib/logitech/unifying_receiver/listener.py index 8a99bea4..1b32f187 100644 --- a/lib/logitech/unifying_receiver/listener.py +++ b/lib/logitech/unifying_receiver/listener.py @@ -4,7 +4,7 @@ import logging import threading -import time +from time import sleep from . import base from .exceptions import * @@ -13,16 +13,29 @@ from .exceptions import * _LOG_LEVEL = 6 _l = logging.getLogger('logitech.unifying_receiver.listener') -_EVENT_TIMEOUT = 100 -_IDLE_SLEEP = 1000.0 / 1000.0 + +_READ_EVENT_TIMEOUT = 90 # ms +_IDLE_SLEEP = 900 # ms class EventsListener(threading.Thread): - def __init__(self, receiver, callback): + """Listener thread for events from the Unifying Receiver. + + Incoming events (code, device, data) will be delivered to the callback + function. The callback is called in the listener thread, so it should return + as fast as possible. + + While this listener is running, you should use the request() method to make + regular UR API calls, otherwise the replies will be captured by the listener + and delivered as events to the callback. As an exception, you can make UR + API calls in the events callback. + """ + def __init__(self, receiver, events_callback): super(EventsListener, self).__init__(name='Unifying_Receiver_Listener_' + str(receiver)) + self.daemon = True self.receiver = receiver - self.callback = callback + self.callback = events_callback self.task = None self.task_processing = threading.Lock() @@ -30,38 +43,50 @@ class EventsListener(threading.Thread): self.task_reply = None self.task_done = threading.Event() - self.active = False - def run(self): _l.log(_LOG_LEVEL, "(%d) starting", self.receiver) self.active = True while self.active: - # _l.log(_LOG_LEVEL, "(%d) reading next event", self.receiver) - event = base.read(self.receiver, _EVENT_TIMEOUT) - if event: - _l.log(_LOG_LEVEL, "(%d) got event %s", self.receiver, event) - self.callback.__call__(*event) - elif self.task is None: - # _l.log(_LOG_LEVEL, "(%d) idle sleep", self.receiver) - time.sleep(_IDLE_SLEEP) - else: - self.task_reply = self._make_request(*self.task) - self.task_done.set() + try: + # _l.log(_LOG_LEVEL, "(%d) reading next event", self.receiver) + event = base.read(self.receiver, _READ_EVENT_TIMEOUT) + except NoReceiver: + _l.warn("(%d) receiver disconnected", self.receiver) + self.active = False + break + + if self.active: + if event: + _l.log(_LOG_LEVEL, "(%d) got event %s", self.receiver, event) + self.callback.__call__(*event) + elif self.task is None: + # _l.log(_LOG_LEVEL, "(%d) idle sleep", self.receiver) + sleep(_IDLE_SLEEP / 1000.0) + else: + self.task_reply = self._make_request(*self.task) + self.task_done.set() def stop(self): + """Tells the listener to stop as soon as possible.""" _l.log(_LOG_LEVEL, "(%d) stopping", self.receiver) self.active = False - self.join() def request(self, api_function, *args, **kwargs): + """Make an UR API request. + + The api_function will get the receiver handle as a first agument, all + other args and kwargs will follow. + """ # _l.log(_LOG_LEVEL, "(%d) request '%s' with %s, %s", self.receiver, api_function.__name__, args, kwargs) self.task_processing.acquire() self.task_done.clear() self.task = (api_function, args, kwargs) + self.task_done.wait() reply = self.task_reply self.task = self.task_reply = None self.task_processing.release() + # _l.log(_LOG_LEVEL, "(%d) request '%s' => [%s]", self.receiver, api_function.__name__, reply.encode('hex')) if isinstance(reply, Exception): raise reply