From e7bb599689a63006b467a1f36c2b927914d80d83 Mon Sep 17 00:00:00 2001 From: Daniel Pavel Date: Sat, 27 Oct 2012 18:40:54 +0300 Subject: [PATCH] reworked the way tasks are processed by the listener --- app/receiver.py | 167 +++++++++++++++------ app/solaar.py | 9 +- app/ui/action.py | 25 +-- app/ui/main_window.py | 40 ++--- app/watcher.py | 24 +-- bin/hidconsole | 2 +- bin/scan | 2 +- bin/solaar | 4 +- lib/logitech/devices/__init__.py | 12 +- lib/logitech/devices/k750.py | 20 +-- lib/logitech/scanner.py | 10 +- lib/logitech/unifying_receiver/__init__.py | 24 ++- lib/logitech/unifying_receiver/api.py | 140 +++++++++-------- lib/logitech/unifying_receiver/base.py | 113 +++++++------- lib/logitech/unifying_receiver/listener.py | 145 +++++++++--------- 15 files changed, 412 insertions(+), 325 deletions(-) diff --git a/app/receiver.py b/app/receiver.py index 4289ed8c..93e55a80 100644 --- a/app/receiver.py +++ b/app/receiver.py @@ -3,7 +3,6 @@ # from logging import getLogger as _Logger -_LOG_LEVEL = 6 from threading import Event as _Event from struct import pack as _pack @@ -18,22 +17,113 @@ from logitech.devices.constants import (STATUS, STATUS_NAME, PROPS, NAMES) # # + +class _FeaturesArray(object): + __slots__ = ('device', 'features', 'supported') + + def __init__(self, device): + self.device = device + self.features = None + self.supported = True + + def _check(self): + if not self.supported: + return False + + if self.features is not None: + return True + + if self.device.status >= STATUS.CONNECTED: + handle = self.device.receiver.handle + try: + index = _api.get_feature_index(handle, self.device.number, _api.FEATURE.FEATURE_SET) + except _api._FeatureNotSupported: + index = None + + if index is None: + self.supported = False + else: + count = _base.request(handle, self.device.number, _pack('!B', index) + b'\x00') + if count is None: + self.supported = False + else: + count = ord(count[:1]) + self.features = [None] * (1 + count) + self.features[0] = _api.FEATURE.ROOT + self.features[index] = _api.FEATURE.FEATURE_SET + return True + + return False + + __bool__ = __nonzero__ = _check + + def __getitem__(self, index): + if not self._check(): + return None + + if index < 0 or index >= len(self.features): + raise IndexError + if self.features[index] is None: + fs_index = self.features.index(_api.FEATURE.FEATURE_SET) + feature = _base.request(self.device.receiver.handle, self.device.number, _pack('!BB', fs_index, 0x10), _pack('!B', index)) + if feature is not None: + self.features[index] = feature[:2] + + return self.features[index] + + def __contains__(self, value): + if self._check(): + if value in self.features: + return True + + for index in range(0, len(self.features)): + f = self.features[index] or self.__getitem__(index) + assert f is not None + if f == value: + return True + if f > value: + break + + return False + + def index(self, value): + if self._check(): + if self.features is not None and value in self.features: + return self.features.index(value) + raise ValueError("%s not in list" % repr(value)) + + def __iter__(self): + if self._check(): + yield _api.FEATURE.ROOT + index = 1 + last_index = len(self.features) + while index < last_index: + yield self.__getitem__(index) + index += 1 + + def __len__(self): + return len(self.features) if self._check() else 0 + + class DeviceInfo(object): """A device attached to the receiver. """ - def __init__(self, receiver, number, status=STATUS.UNKNOWN): + def __init__(self, receiver, number, pair_code, status=STATUS.UNKNOWN): self.LOG = _Logger("Device[%d]" % number) self.receiver = receiver self.number = number + self._pair_code = pair_code + self._serial = None + self._codename = None self._name = None self._kind = None - self._serial = None self._firmware = None - self._features = None self._status = status self.props = {} + self.features = _FeaturesArray(self) + @property def handle(self): return self.receiver.handle @@ -71,8 +161,8 @@ class DeviceInfo(object): def name(self): if self._name is None: if self._status >= STATUS.CONNECTED: - self._name = self.receiver.call_api(_api.get_device_name, self.number, self.features) - return self._name or '?' + self._name = _api.get_device_name(self.receiver.handle, self.number, self.features) + return self._name or self.codename @property def device_name(self): @@ -81,33 +171,42 @@ class DeviceInfo(object): @property def kind(self): if self._kind is None: - if self._status >= STATUS.CONNECTED: - self._kind = self.receiver.call_api(_api.get_device_kind, self.number, self.features) + if self._status < STATUS.CONNECTED: + codename = self.codename + if codename in NAMES: + self._kind = NAMES[codename][-1] + else: + self._kind = _api.get_device_kind(self.receiver.handle, self.number, self.features) return self._kind or '?' @property def serial(self): if self._serial is None: - if self._status >= STATUS.CONNECTED: - pass + # dodgy + b = bytearray(self._pair_code) + b[0] -= 0x10 + serial = _base.request(self.receiver.handle, 0xFF, b'\x83\xB5', bytes(b)) + if serial: + self._serial = _base._hex(serial[1:5]) return self._serial or '?' + @property + def codename(self): + if self._codename is None: + codename = _base.request(self.receiver.handle, 0xFF, b'\x83\xB5', self._pair_code) + if codename: + self._codename = codename[2:].rstrip(b'\x00').decode('ascii') + return self._codename or '?' + @property def firmware(self): if self._firmware is None: if self._status >= STATUS.CONNECTED: - self._firmware = self.receiver.call_api(_api.get_device_firmware, self.number, self.features) + self._firmware = _api.get_device_firmware(self.receiver.handle, 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.call_api(_api.get_device_features, self.number) - return self._features or () - def ping(self): - return self.receiver.call_api(_api.ping, self.number) + return _api.ping(self.receiver.handle, self.number) def process_event(self, code, data): if code == 0x10 and data[:1] == b'\x8F': @@ -224,20 +323,20 @@ class Receiver(_listener.EventsListener): return self.NAME def count_devices(self): - return self.call_api(_api.count_devices) + return _api.count_devices(self._handle) @property def serial(self): if self._serial is None: if self: - self._serial, self._firmware = self.call_api(_api.get_receiver_info) + self._serial, self._firmware = _api.get_receiver_info(self._handle) return self._serial or '?' @property def firmware(self): if self._firmware is None: if self: - self._serial, self._firmware = self.call_api(_api.get_receiver_info) + self._serial, self._firmware = _api.get_receiver_info(self._handle) return self._firmware or ('?', '?') @@ -303,30 +402,12 @@ class Receiver(_listener.EventsListener): self.LOG.warn("don't know how to handle device status 0x%02X: %s", state_code, event) return None - 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(0xFF, b'\x83\xB5', event.data[4:5]) - if dev_id: - shortname = dev_id[2:].rstrip(b'\x00').decode('ascii') - if shortname in NAMES: - dev._name, dev._kind = NAMES[shortname] - else: - self.LOG.warn("could not identify inactive device %d: %s", event.devnumber, shortname) - - b = bytearray(event.data[4:5]) - b[0] -= 0x10 - serial = self.request(0xFF, b'\x83\xB5', bytes(b)) - if serial: - dev._serial = _base._hex(serial[1:5]) - return dev + return DeviceInfo(self, event.devnumber, event.data[4:5], state) def unpair_device(self, number): if number in self.devices: dev = self.devices[number] - reply = self.request(0xFF, b'\x80\xB2', _pack('!BB', 0x03, number)) + reply = _base.request(self._handle, 0xFF, b'\x80\xB2', _pack('!BB', 0x03, number)) if reply: self.LOG.debug("remove device %s => %s", dev, _base._hex(reply)) del self.devices[number] @@ -346,7 +427,7 @@ class Receiver(_listener.EventsListener): :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) + _Logger("receiver").debug("checking %s", rawdevice) handle = _base.try_open(rawdevice.path) if handle: receiver = Receiver(rawdevice.path, handle) diff --git a/app/solaar.py b/app/solaar.py index 8477dfd2..bb1c0c58 100644 --- a/app/solaar.py +++ b/app/solaar.py @@ -32,10 +32,8 @@ def _parse_arguments(): import logging log_level = logging.root.level - 10 * args.verbose - 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') + log_format='%(asctime)s %(levelname)8s [%(threadName)s] %(name)s: %(message)s' + logging.basicConfig(level=max(log_level, 1), format=log_format) return args @@ -83,3 +81,6 @@ if __name__ == '__main__': w.stop() ui.notify.uninit() + + import logging + logging.shutdown() diff --git a/app/ui/action.py b/app/ui/action.py index 5d9bfd9a..0a995200 100644 --- a/app/ui/action.py +++ b/app/ui/action.py @@ -19,10 +19,18 @@ def _toggle_action(name, label, function, *args): return action -# -# -# +def wrap_action(action, prefire): + def _wrap(aw, aa): + prefire(aa) + aa.activate() + wrapper = _action(action.get_name(), action.get_label(), None) + wrapper.set_icon_name(action.get_icon_name()) + wrapper.connect('activate', _wrap, action) + return wrapper +# +# +# def _toggle_notifications(action): if action.get_active(): @@ -57,9 +65,6 @@ import pairing def _pair_device(action): action.set_sensitive(False) pair_dialog = ui.pair_window.create(action, pairing.state) - action.window.present() - pair_dialog.set_transient_for(action.window) - pair_dialog.set_destroy_with_parent(action.window) pair_dialog.set_modal(True) pair_dialog.present() pair = _action('add', 'Pair new device', _pair_device) @@ -69,9 +74,11 @@ def _unpair_device(action): dev = pairing.state.device(action.devnumber) action.devnumber = 0 if dev: - q = Gtk.MessageDialog.new(action.window, + qdialog = Gtk.MessageDialog(action.window, 0, Gtk.MessageType.QUESTION, Gtk.ButtonsType.YES_NO, - 'Unpair device %s?', dev.name) - if q.run() == Gtk.ResponseType.YES: + "Unpair device '%s' ?" % dev.name) + choice = qdialog.run() + qdialog.destroy() + if choice == Gtk.ResponseType.YES: pairing.state.unpair(dev.number) unpair = _action('remove', 'Unpair', _unpair_device) diff --git a/app/ui/main_window.py b/app/ui/main_window.py index ec4aaf98..62fc99e0 100644 --- a/app/ui/main_window.py +++ b/app/ui/main_window.py @@ -2,7 +2,7 @@ # # -from gi.repository import (Gtk, Gdk) +from gi.repository import (Gtk, Gdk, GObject) import ui from logitech.devices.constants import (STATUS, PROPS) @@ -22,6 +22,7 @@ def _toggle_info_button(label, widget): action = ui.action._toggle_action('info', label, toggle, widget) return action.create_tool_item() + def _receiver_box(name): icon = Gtk.Image.new_from_icon_name(name, _SMALL_DEVICE_ICON_SIZE) @@ -65,7 +66,7 @@ def _receiver_box(name): return frame -def _device_box(): +def _device_box(index): icon = Gtk.Image.new_from_icon_name('image-missing', _DEVICE_ICON_SIZE) icon.set_name('icon') icon.set_alignment(0.5, 0) @@ -111,7 +112,10 @@ def _device_box(): info_box.add(info_label) toolbar.insert(_toggle_info_button('Device info', info_box), 0) - toolbar.insert(ui.action.unpair.create_tool_item(), -1) + def _set_number(action): + action.devnumber = index + unpair_action = ui.action.wrap_action(ui.action.unpair, _set_number) + toolbar.insert(unpair_action.create_tool_item(), -1) vbox = Gtk.VBox(homogeneous=False, spacing=4) vbox.pack_start(label, True, True, 0) @@ -131,7 +135,6 @@ def _device_box(): def toggle(window, trigger): - # print 'window toggle', window, trigger if window.get_visible(): position = window.get_position() window.hide() @@ -158,7 +161,7 @@ def create(title, name, max_devices, systray=False): rbox = _receiver_box(name) vbox.add(rbox) for i in range(1, 1 + max_devices): - dbox = _device_box() + dbox = _device_box(i) vbox.add(dbox) vbox.set_visible(True) @@ -209,18 +212,12 @@ def _update_receiver_box(frame, receiver): def _update_device_box(frame, dev): - if dev is None: - frame.set_visible(False) - frame.set_name(_PLACEHOLDER) - return - icon, label, info_label = ui.find_children(frame, 'icon', 'label', 'info-label') if frame.get_name() != dev.name: frame.set_name(dev.name) icon.set_from_icon_name(ui.get_icon(dev.name, dev.kind), _DEVICE_ICON_SIZE) label.set_markup('' + dev.name + '') - frame.set_visible(True) status = ui.find_children(frame, 'status') status_icons = status.get_children() @@ -271,16 +268,21 @@ def _update_device_box(frame, dev): for b in toolbar.get_children()[:-1]: b.set_sensitive(True) + frame.set_visible(True) def update(window, receiver): - if window and window.get_child(): - window.set_icon_name(ui.appicon(receiver.status)) + window.set_icon_name(ui.appicon(receiver.status)) - vbox = window.get_child() - controls = list(vbox.get_children()) + vbox = window.get_child() + controls = list(vbox.get_children()) - _update_receiver_box(controls[0], receiver) + GObject.idle_add(_update_receiver_box, controls[0], receiver) - for index in range(1, len(controls)): - dev = receiver.devices[index] if index in receiver.devices else None - _update_device_box(controls[index], dev) + for index in range(1, len(controls)): + dev = receiver.devices[index] if index in receiver.devices else None + frame = controls[index] + if dev is None: + frame.set_visible(False) + frame.set_name(_PLACEHOLDER) + else: + GObject.idle_add(_update_device_box, frame, dev) diff --git a/app/watcher.py b/app/watcher.py index b46578f3..122451ac 100644 --- a/app/watcher.py +++ b/app/watcher.py @@ -65,31 +65,19 @@ class Watcher(Thread): continue _l.info("receiver %s ", r) - self.update_ui(r) - self.notify(r) - - if r.count_devices() > 0: - # give it some time to read all devices - r.status_changed.clear() - _sleep(8, 0.4, r.status_changed.is_set) - - if r.devices: - _l.info("%d device(s) found", len(r.devices)) - for d in r.devices.values(): - self.notify(d) - else: - # if no devices found so far, assume none at all - _l.info("no devices found") - r.status = STATUS.CONNECTED - self._receiver = r notify_missing = True + self.update_ui(r) + self.notify(r) + if self._active: if self._receiver: _l.debug("waiting for status_changed") sc = self._receiver.status_changed sc.wait() + if not self._active: + break sc.clear() if sc.urgent: _l.info("status_changed %s", sc.reason) @@ -103,6 +91,7 @@ class Watcher(Thread): if self._receiver: self._receiver.close() + self._receiver = _DUMMY_RECEIVER def stop(self): if self._active: @@ -112,3 +101,4 @@ class Watcher(Thread): # break out of an eventual wait() self._receiver.status_changed.reason = None self._receiver.status_changed.set() + self.join() diff --git a/bin/hidconsole b/bin/hidconsole index 2cdda26a..dfbbf461 100755 --- a/bin/hidconsole +++ b/bin/hidconsole @@ -4,4 +4,4 @@ LIB=`dirname "$0"`/../lib export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$LIB/native/`uname -m` export PYTHONPATH=$LIB -exec python -OO -u -m hidapi.hidconsole "$@" +exec python -OOu -m hidapi.hidconsole "$@" diff --git a/bin/scan b/bin/scan index bbb8b839..b9299a92 100755 --- a/bin/scan +++ b/bin/scan @@ -4,4 +4,4 @@ LIB=`dirname "$0"`/../lib export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$LIB/native/`uname -m` export PYTHONPATH=$LIB -exec python -OO -m logitech.scanner "$@" +exec python -OOu -m logitech.scanner "$@" diff --git a/bin/solaar b/bin/solaar index 9c5cc574..cd4b3e33 100755 --- a/bin/solaar +++ b/bin/solaar @@ -9,5 +9,5 @@ export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$LIB/native/`uname -m` export PYTHONPATH=$APP:$LIB export XDG_DATA_DIRS=$SHARE:$XDG_DATA_DIRS -exec python3 -OO -m solaar "$@" -#exec python -OO -m profile -o $TMPDIR/profile.log app/solaar.py "$@" +exec python -OOu -m solaar "$@" +#exec python -OOu -m profile -o $TMPDIR/profile.log app/solaar.py "$@" diff --git a/lib/logitech/devices/__init__.py b/lib/logitech/devices/__init__.py index 17150285..90fe56d1 100644 --- a/lib/logitech/devices/__init__.py +++ b/lib/logitech/devices/__init__.py @@ -32,20 +32,12 @@ def _module(device_name): def default_request_status(devinfo, listener=None): if FEATURE.BATTERY in devinfo.features: - if listener: - reply = listener.call_api(_api.get_device_battery_level, devinfo.number, features=devinfo.features) - else: - reply = _api.get_device_battery_level(devinfo.handle, devinfo.number, features=devinfo.features) - + reply = _api.get_device_battery_level(devinfo.handle, devinfo.number, features=devinfo.features) if reply: discharge, dischargeNext, status = reply return STATUS.CONNECTED, {PROPS.BATTERY_LEVEL: discharge, PROPS.BATTERY_STATUS: status} - if listener: - reply = listener.call_api(_api.ping, devinfo.number) - else: - reply = _api.ping(devinfo.handle, devinfo.number) - + reply = _api.ping(devinfo.handle, devinfo.number) return STATUS.CONNECTED if reply else STATUS.UNAVAILABLE diff --git a/lib/logitech/devices/k750.py b/lib/logitech/devices/k750.py index c55b8471..c935ecc0 100644 --- a/lib/logitech/devices/k750.py +++ b/lib/logitech/devices/k750.py @@ -29,17 +29,9 @@ def _charge_status(data, hasLux=False): def request_status(devinfo, listener=None): - def _trigger_solar_charge_events(handle, devinfo): - return _api.request(handle, devinfo.number, - 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) - elif listener: - reply = listener.call_api(_trigger_solar_charge_events, devinfo) - else: - reply = 0 - + reply = _api.request(devinfo.handle, devinfo.number, + feature=FEATURE.SOLAR_CHARGE, function=b'\x03', params=b'\x78\x01', + features=devinfo.features) if reply is None: return STATUS.UNAVAILABLE @@ -56,9 +48,3 @@ def process_event(devinfo, data, listener=None): if data[:2] == b'\x09\x20' and data[7:11] == b'GOOD': logging.debug("Solar key pressed") return request_status(devinfo, listener) or _charge_status(data) - - if data[:2] == b'\x05\x00': - # wireless device status - if data[2:5] == b'\x01\x01\x01': - logging.debug("Keyboard just started") - return STATUS.CONNECTED diff --git a/lib/logitech/scanner.py b/lib/logitech/scanner.py index c24a13c5..2a5a8a93 100644 --- a/lib/logitech/scanner.py +++ b/lib/logitech/scanner.py @@ -39,7 +39,7 @@ def scan_devices(receiver): for index in range(0, len(devinfo.features)): feature = devinfo.features[index] if feature: - print (" ~ Feature %-20s (%s) at index %d" % (FEATURE_NAME[feature], api._hex(feature), index)) + print (" ~ Feature %-20s (%s) at index %02X" % (FEATURE_NAME[feature], api._hex(feature), index)) if FEATURE.BATTERY in devinfo.features: discharge, dischargeNext, status = api.get_device_battery_level(receiver, devinfo.number, features=devinfo.features) @@ -75,11 +75,3 @@ 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/lib/logitech/unifying_receiver/__init__.py b/lib/logitech/unifying_receiver/__init__.py index aa24719c..f6485ea7 100644 --- a/lib/logitech/unifying_receiver/__init__.py +++ b/lib/logitech/unifying_receiver/__init__.py @@ -20,12 +20,24 @@ http://julien.danjou.info/blog/2012/logitech-k750-linux-support http://6xq.net/git/lars/lshidpp.git/plain/doc/ """ +import logging + +log = logging.getLogger('LUR') +log.propagate = 0 +log.setLevel(logging.DEBUG) + +if logging.root.level < logging.DEBUG: + handler = logging.FileHandler('lur.log', mode='w') + handler.setFormatter(logging.root.handlers[0].formatter) +else: + handler = logging.NullHandler() +log.addHandler(handler) +del handler + +del log +del logging + + from .constants import * from .exceptions import * from .api import * - - -import logging -logging.addLevelName(4, 'UR_TRACE') -logging.addLevelName(5, 'UR_DEBUG') -logging.addLevelName(6, 'UR_INFO') diff --git a/lib/logitech/unifying_receiver/api.py b/lib/logitech/unifying_receiver/api.py index 50baf621..382b82ce 100644 --- a/lib/logitech/unifying_receiver/api.py +++ b/lib/logitech/unifying_receiver/api.py @@ -2,7 +2,6 @@ # Logitech Unifying Receiver API. # -from logging import getLogger as _Logger from struct import pack as _pack from struct import unpack as _unpack @@ -19,8 +18,10 @@ from .exceptions import FeatureNotSupported as _FeatureNotSupported _hex = _base._hex -_LOG_LEVEL = 5 -_l = _Logger('lur.api') + +from logging import getLogger +_log = getLogger('LUR').getChild('api') +del getLogger # # @@ -61,7 +62,7 @@ def get_receiver_info(handle): def count_devices(handle): - count = _base.request(handle, 0xFF, b'\x80\x02', b'\x02') + count = _base.request(handle, 0xFF, b'\x81\x00') return 0 if count is None else ord(count[1:2]) @@ -88,22 +89,16 @@ def request(handle, devnumber, feature, function=b'\x00', params=b'', features=N :raises FeatureNotSupported: if the device does not support the feature. """ - feature_index = None if feature == FEATURE.ROOT: feature_index = b'\x00' else: - if features is None: - features = get_device_features(handle, devnumber) - if features is None: - _l.log(_LOG_LEVEL, "(%d) no features array available", devnumber) - return None - if feature in features: - feature_index = _pack('!B', features.index(feature)) + feature_index = _get_feature_index(handle, devnumber, feature, features) + if feature_index is None: + # i/o read error + return None - if feature_index is None: - _l.warn("(%d) feature <%s:%s> not supported", devnumber, _hex(feature), FEATURE_NAME[feature]) - raise _FeatureNotSupported(devnumber, feature) + feature_index = _pack('!B', feature_index) if type(function) == int: function = _pack('!B', function) @@ -132,9 +127,11 @@ def get_device_protocol(handle, devnumber): def find_device_by_name(handle, name): """Searches for an attached device by name. + This function does it the hard way, querying all possible device numbers. + :returns: an AttachedDeviceInfo tuple, or ``None``. """ - _l.log(_LOG_LEVEL, "searching for device '%s'", name) + _log.debug("searching for device '%s'", name) for devnumber in range(1, 1 + MAX_ATTACHED_DEVICES): features = get_device_features(handle, devnumber) @@ -147,9 +144,11 @@ def find_device_by_name(handle, name): def list_devices(handle): """List all devices attached to the UR. + This function does it the hard way, querying all possible device numbers. + :returns: a list of AttachedDeviceInfo tuples. """ - _l.log(_LOG_LEVEL, "listing all devices") + _log.debug("listing all devices") devices = [] @@ -162,7 +161,7 @@ def list_devices(handle): def get_device_info(handle, devnumber, name=None, features=None): - """Gets the complete info for a device (type, name, firmware versions, features). + """Gets the complete info for a device (type, name, features). :returns: an AttachedDeviceInfo tuple, or ``None``. """ @@ -174,7 +173,7 @@ def get_device_info(handle, devnumber, name=None, features=None): 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_kind, d_name, features) - _l.log(_LOG_LEVEL, "(%d) found device %s", devnumber, devinfo) + _log.debug("(%d) found device %s", devnumber, devinfo) return devinfo @@ -183,41 +182,53 @@ 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, _hex(feature), FEATURE_NAME[feature]) + _log.debug("(%d) get feature index <%s:%s>", devnumber, _hex(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, FEATURE.ROOT, feature) if reply: - # only consider active and supported features feature_index = ord(reply[0:1]) if feature_index: feature_flags = ord(reply[1:2]) & 0xE0 - if _l.isEnabledFor(_LOG_LEVEL): - if feature_flags: - _l.log(_LOG_LEVEL, "(%d) feature <%s:%s> has index %d: %s", + if feature_flags: + _log.debug("(%d) feature <%s:%s> has index %d: %s", devnumber, _hex(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, _hex(feature), FEATURE_NAME[feature], feature_index) + else: + _log.debug("(%d) feature <%s:%s> has index %d", devnumber, _hex(feature), FEATURE_NAME[feature], feature_index) + # only consider active and supported features? # if feature_flags: # raise E.FeatureNotSupported(devnumber, feature) return feature_index - _l.warn("(%d) feature <%s:%s> not supported by the device", devnumber, _hex(feature), FEATURE_NAME[feature]) + _log.warn("(%d) feature <%s:%s> not supported by the device", devnumber, _hex(feature), FEATURE_NAME[feature]) raise _FeatureNotSupported(devnumber, feature) +def _get_feature_index(handle, devnumber, feature, features=None): + if features is None: + return get_feature_index(handle, devnumber, feature) + + if feature in features: + return features.index(feature) + + index = get_feature_index(handle, devnumber, feature) + if index is not None: + features[index] = feature + return index + + def get_device_features(handle, devnumber): """Returns an array of feature ids. Their position in the array is the index to be used when requesting that feature on the device. """ - _l.log(_LOG_LEVEL, "(%d) get device features", devnumber) + _log.debug("(%d) get device features", devnumber) # get the index of the FEATURE_SET # FEATURE.ROOT should always be available for all devices @@ -235,11 +246,11 @@ def get_device_features(handle, devnumber): if not features_count: # this can happen if the device disappeard since the fs_index request # otherwise we should get at least a count of 1 (the FEATURE_SET we've just used above) - _l.log(_LOG_LEVEL, "(%d) no features available?!", devnumber) + _log.debug("(%d) no features available?!", devnumber) return None features_count = ord(features_count[:1]) - _l.log(_LOG_LEVEL, "(%d) found %d features", devnumber, features_count) + _log.debug("(%d) found %d features", devnumber, features_count) features = [None] * 0x20 for index in range(1, 1 + features_count): @@ -250,13 +261,12 @@ def get_device_features(handle, devnumber): feature = feature[0:2].upper() features[index] = feature - if _l.isEnabledFor(_LOG_LEVEL): - if feature_flags: - _l.log(_LOG_LEVEL, "(%d) feature <%s:%s> at index %d: %s", + if feature_flags: + _log.debug("(%d) feature <%s:%s> at index %d: %s", devnumber, _hex(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, _hex(feature), FEATURE_NAME[feature], index) + else: + _log.debug("(%d) feature <%s:%s> at index %d", devnumber, _hex(feature), FEATURE_NAME[feature], index) features[0] = FEATURE.ROOT while features[-1] is None: @@ -269,16 +279,17 @@ def get_device_firmware(handle, devnumber, features=None): :returns: a list of FirmwareInfo tuples, ordered by firmware layer. """ - def _makeFirmwareInfo(level, kind, name='', version='', extras=None): - return _FirmwareInfo(level, kind, name, version, extras) + fw_fi = _get_feature_index(handle, devnumber, FEATURE.FIRMWARE, features) + if fw_fi is None: + return None - fw_count = request(handle, devnumber, FEATURE.FIRMWARE, features=features) + fw_count = _base.request(handle, devnumber, _pack('!BB', fw_fi, 0x00)) if fw_count: fw_count = ord(fw_count[:1]) fw = [] for index in range(0, fw_count): - fw_info = request(handle, devnumber, FEATURE.FIRMWARE, function=b'\x10', params=index, features=features) + fw_info = _base.request(handle, devnumber, _pack('!BB', fw_fi, 0x10), params=index) if fw_info: level = ord(fw_info[:1]) & 0x0F if level == 0 or level == 1: @@ -290,18 +301,15 @@ def get_device_firmware(handle, devnumber, features=None): build, = _unpack('!H', fw_info[6:8]) if build: version += ' b%d' % build - extras = fw_info[9:].rstrip(b'\x00') - if extras: - fw_info = _makeFirmwareInfo(level, kind, name, version, extras) - else: - fw_info = _makeFirmwareInfo(level, kind, name, version) + extras = fw_info[9:].rstrip(b'\x00') or None + fw_info = _FirmwareInfo(level, kind, name, version, extras) elif level == 2: - fw_info = _makeFirmwareInfo(2, FIRMWARE_KIND[2], version=ord(fw_info[1:2])) + fw_info = _FirmwareInfo(2, FIRMWARE_KIND[2], '', ord(fw_info[1:2]), None) else: - fw_info = _makeFirmwareInfo(level, FIRMWARE_KIND[-1]) + fw_info = _FirmwareInfo(level, FIRMWARE_KIND[-1], '', '', None) fw.append(fw_info) - _l.log(_LOG_LEVEL, "(%d) firmware %s", devnumber, fw_info) + _log.debug("(%d) firmware %s", devnumber, fw_info) return tuple(fw) @@ -312,10 +320,14 @@ def get_device_kind(handle, devnumber, features=None): :returns: a string describing the device type, or ``None`` if the device is not available or does not support the ``NAME`` feature. """ - d_kind = request(handle, devnumber, FEATURE.NAME, function=b'\x20', features=features) + name_fi = _get_feature_index(handle, devnumber, FEATURE.NAME, features) + if name_fi is None: + return None + + d_kind = _base.request(handle, devnumber, _pack('!BB', name_fi, 0x20)) 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]) + _log.debug("(%d) device type %d = %s", devnumber, d_kind, DEVICE_KIND[d_kind]) return DEVICE_KIND[d_kind] @@ -325,13 +337,17 @@ 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, FEATURE.NAME, features=features) + name_fi = _get_feature_index(handle, devnumber, FEATURE.NAME, features) + if name_fi is None: + return None + + name_length = _base.request(handle, devnumber, _pack('!BB', name_fi, 0x00)) if name_length: name_length = ord(name_length[:1]) d_name = b'' while len(d_name) < name_length: - name_fragment = request(handle, devnumber, FEATURE.NAME, function=b'\x10', params=len(d_name), features=features) + name_fragment = _base.request(handle, devnumber, _pack('!BB', name_fi, 0x10), len(d_name)) if name_fragment: name_fragment = name_fragment[:name_length - len(d_name)] d_name += name_fragment @@ -339,7 +355,7 @@ def get_device_name(handle, devnumber, features=None): break d_name = d_name.decode('ascii') - _l.log(_LOG_LEVEL, "(%d) device name %s", devnumber, d_name) + _log.debug("(%d) device name %s", devnumber, d_name) return d_name @@ -348,22 +364,28 @@ def get_device_battery_level(handle, devnumber, features=None): :raises FeatureNotSupported: if the device does not support this feature. """ - 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", + bat_fi = _get_feature_index(handle, devnumber, FEATURE.BATTERY, features) + if bat_fi is not None: + battery = _base.request(handle, devnumber, _pack('!BB', bat_fi, 0)) + if battery: + discharge, dischargeNext, status = _unpack('!BBB', battery[:3]) + _log.debug("(%d) battery %d%% charged, next level %d%% charge, status %d = %s", devnumber, discharge, dischargeNext, status, BATTERY_STATUS[status]) - return (discharge, dischargeNext, BATTERY_STATUS[status]) + return (discharge, dischargeNext, BATTERY_STATUS[status]) def get_device_keys(handle, devnumber, features=None): - count = request(handle, devnumber, FEATURE.REPROGRAMMABLE_KEYS, features=features) + rk_fi = _get_feature_index(handle, devnumber, FEATURE.REPROGRAMMABLE_KEYS, features) + if rk_fi is None: + return None + + count = _base.request(handle, devnumber, _pack('!BB', rk_fi, 0)) if count: keys = [] count = ord(count[:1]) for index in range(0, count): - keydata = request(handle, devnumber, FEATURE.REPROGRAMMABLE_KEYS, function=b'\x10', params=index, features=features) + keydata = _base.request(handle, devnumber, _pack('!BB', rk_fi, 0x10), index) if keydata: key, key_task, flags = _unpack('!HHB', keydata[:5]) rki = _ReprogrammableKeyInfo(index, key, KEY_NAME[key], key_task, KEY_NAME[key_task], flags) diff --git a/lib/logitech/unifying_receiver/base.py b/lib/logitech/unifying_receiver/base.py index e15ba486..9974d0bf 100644 --- a/lib/logitech/unifying_receiver/base.py +++ b/lib/logitech/unifying_receiver/base.py @@ -3,7 +3,6 @@ # Unlikely to be used directly unless you're expanding the API. # -from logging import getLogger as _Logger from struct import pack as _pack from binascii import hexlify as _hexlify _hex = lambda d: _hexlify(d).decode('ascii').upper() @@ -12,12 +11,13 @@ from .constants import ERROR_NAME from .exceptions import (NoReceiver as _NoReceiver, FeatureCallError as _FeatureCallError) +from logging import getLogger +_log = getLogger('LUR').getChild('base') +del getLogger + import hidapi as _hid -_LOG_LEVEL = 4 -_l = _Logger('lur.base') - # # These values are defined by the Logitech documentation. # Overstepping these boundaries will only produce log warnings. @@ -48,7 +48,7 @@ DEFAULT_TIMEOUT = 1000 def _logdebug_hook(reply_code, devnumber, data): """Default unhandled hook, logs the reply as DEBUG.""" - _l.warn("UNHANDLED [%02X %02X %s %s] (%s)", reply_code, devnumber, _hex(data[:2]), _hex(data[2:]), repr(data)) + _log.warn("UNHANDLED [%02X %02X %s %s] (%s)", reply_code, devnumber, _hex(data[:2]), _hex(data[2:]), repr(data)) """The function that will be called on unhandled incoming events. @@ -78,7 +78,7 @@ def list_receiver_devices(): return _hid.enumerate(0x046d, 0xc52b, 2) -_PING_RECEIVER = b'\x10\xFF\x81\x00\x00\x00\x00' +_COUNT_DEVICES_REQUEST = b'\x10\xFF\x81\x00\x00\x00\x00' def try_open(path): """Checks if the given Linux device path points to the right UR device. @@ -97,31 +97,28 @@ def try_open(path): if receiver_handle is None: # could be a file permissions issue (did you add the udev rules?) # in any case, unreachable - _l.log(_LOG_LEVEL, "[%s] open failed", path) + _log.debug("[%s] open failed", path) return None - _l.log(_LOG_LEVEL, "[%s] receiver handle %X", path, receiver_handle) - # ping on device id 0 (always an error) - _hid.write(receiver_handle, _PING_RECEIVER) + _hid.write(receiver_handle, _COUNT_DEVICES_REQUEST) # if this is the right hidraw device, we'll receive a 'bad device' from the UR # otherwise, the read should produce nothing reply = _hid.read(receiver_handle, _MAX_REPLY_SIZE, DEFAULT_TIMEOUT) if reply: - if reply[:5] == _PING_RECEIVER[:5]: + if reply[:5] == _COUNT_DEVICES_REQUEST[:5]: # 'device 0 unreachable' is the expected reply from a valid receiver handle - _l.log(_LOG_LEVEL, "[%s] success: handle %X", path, receiver_handle) + _log.info("[%s] success: handle %X", path, receiver_handle) return receiver_handle # any other replies are ignored, and will assume this is the wrong Linux device - if _l.isEnabledFor(_LOG_LEVEL): - if reply == b'\x01\x00\x00\x00\x00\x00\x00\x00': - # no idea what this is, but it comes up occasionally - _l.log(_LOG_LEVEL, "[%s] %X mistery reply [%s]", path, receiver_handle, _hex(reply)) - else: - _l.log(_LOG_LEVEL, "[%s] %X unknown reply [%s]", path, receiver_handle, _hex(reply)) + if reply == b'\x01\x00\x00\x00\x00\x00\x00\x00': + # no idea what this is, but it comes up occasionally + _log.debug("[%s] %X mistery reply [%s]", path, receiver_handle, _hex(reply)) + else: + _log.debug("[%s] %X unknown reply [%s]", path, receiver_handle, _hex(reply)) else: - _l.log(_LOG_LEVEL, "[%s] %X no reply", path, receiver_handle) + _log.debug("[%s] %X no reply", path, receiver_handle) close(receiver_handle) @@ -132,7 +129,7 @@ def open(): :returns: An open file handle for the found receiver, or ``None``. """ for rawdevice in list_receiver_devices(): - _l.log(_LOG_LEVEL, "checking %s", rawdevice) + _log.info("checking %s", rawdevice) receiver = try_open(rawdevice.path) if receiver: @@ -146,10 +143,10 @@ def close(handle): if handle: try: _hid.close(handle) - _l.log(_LOG_LEVEL, "closed receiver handle %X", handle) + _log.info("closed receiver handle %X", handle) return True except: - _l.exception("closing receiver handle %X", handle) + _log.exception("closing receiver handle %X", handle) return False @@ -168,15 +165,13 @@ def write(handle, devnumber, data): been physically removed from the machine, or the kernel driver has been unloaded. The handle will be closed automatically. """ - if _l.isEnabledFor(_LOG_LEVEL): - _l.log(_LOG_LEVEL, "(%d) <= w[10 %02X %s %s]", devnumber, devnumber, _hex(data[:2]), _hex(data[2:])) - assert _MIN_CALL_SIZE == 7 assert _MAX_CALL_SIZE == 20 # the data is padded to either 5 or 18 bytes wdata = _pack('!BB18s' if len(data) > 5 else '!BB5s', 0x10, devnumber, data) + _log.debug("(%d) <= w[10 %02X %s %s]", devnumber, devnumber, _hex(wdata[2:4]), _hex(wdata[4:])) if not _hid.write(handle, wdata): - _l.warn("(%d) write failed, assuming receiver %X no longer available", devnumber, handle) + _log.warn("(%d) write failed, assuming receiver %X no longer available", devnumber, handle) close(handle) raise _NoReceiver @@ -197,26 +192,33 @@ def read(handle, timeout=DEFAULT_TIMEOUT): 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) + data = _hid.read(handle, _MAX_REPLY_SIZE, timeout) if data is None: - _l.warn("(-) read failed, assuming receiver %X no longer available", handle) + _log.warn("(-) read failed, assuming receiver %X no longer available", handle) close(handle) raise _NoReceiver if data: if len(data) < _MIN_REPLY_SIZE: - _l.warn("(%d) => r[%s] read packet too short: %d bytes", ord(data[1:2]), _hex(data), len(data)) + _log.warn("(%d) => r[%s] read packet too short: %d bytes", ord(data[1:2]), _hex(data), len(data)) + data += b'\x00' * (_MIN_REPLY_SIZE - len(data)) if len(data) > _MAX_REPLY_SIZE: - _l.warn("(%d) => r[%s] read packet too long: %d bytes", ord(data[1:2]), _hex(data), len(data)) + _log.warn("(%d) => r[%s] read packet too long: %d bytes", ord(data[1:2]), _hex(data), len(data)) code = ord(data[:1]) devnumber = ord(data[1:2]) - if _l.isEnabledFor(_LOG_LEVEL): - _l.log(_LOG_LEVEL, "(%d) => r[%02X %02X %s %s]", devnumber, code, devnumber, _hex(data[2:4]), _hex(data[4:])) + _log.debug("(%d) => r[%02X %02X %s %s]", devnumber, code, devnumber, _hex(data[2:4]), _hex(data[4:])) return code, devnumber, data[2:] - # _l.log(_LOG_LEVEL, "(-) => r[]", handle) + # _l.log(_LOG_LEVEL, "(-) => r[]") +_MAX_READ_TIMES = 3 +request_context = None +from collections import namedtuple +_DEFAULT_REQUEST_CONTEXT_CLASS = namedtuple('_DEFAULT_REQUEST_CONTEXT_CLASS', ['write', 'read', 'unhandled_hook']) +_DEFAULT_REQUEST_CONTEXT = _DEFAULT_REQUEST_CONTEXT_CLASS(write=write, read=read, unhandled_hook=unhandled_hook) +del namedtuple + def request(handle, devnumber, feature_index_function, params=b'', features=None): """Makes a feature call to a device and waits for a matching reply. @@ -234,18 +236,27 @@ def request(handle, devnumber, feature_index_function, params=b'', features=None available. :raisees FeatureCallError: if the feature call replied with an error. """ - if _l.isEnabledFor(_LOG_LEVEL): - _l.log(_LOG_LEVEL, "(%d) request {%s} params [%s]", devnumber, _hex(feature_index_function), _hex(params)) + if type(params) == int: + params = _pack('!B', params) + + _log.debug("(%d) request {%s} params [%s]", devnumber, _hex(feature_index_function), _hex(params)) if len(feature_index_function) != 2: raise ValueError('invalid feature_index_function {%s}: it must be a two-byte string' % _hex(feature_index_function)) - retries = 5 + if request_context is None or handle != request_context.handle: + context = _DEFAULT_REQUEST_CONTEXT + _unhandled = unhandled_hook + else: + context = request_context + _unhandled = getattr(context, 'unhandled_hook') - write(handle, devnumber, feature_index_function + params) - while retries > 0: - divisor = (6 - retries) - reply = read(handle, int(DEFAULT_TIMEOUT * (divisor + 1) / 2 / divisor)) - retries -= 1 + context.write(handle, devnumber, feature_index_function + params) + + read_times = _MAX_READ_TIMES + while read_times > 0: + divisor = (1 + _MAX_READ_TIMES - read_times) + reply = context.read(handle, int(DEFAULT_TIMEOUT * (divisor + 1) / 2 / divisor)) + read_times -= 1 if not reply: # keep waiting... @@ -258,24 +269,24 @@ def request(handle, devnumber, feature_index_function, params=b'', features=None # _l.log(_LOG_LEVEL, "(%d) request got reply for unexpected device %d: [%s]", devnumber, reply_devnumber, _hex(reply_data)) # worst case scenario, this is a reply for a concurrent request # on this receiver - if unhandled_hook: - unhandled_hook(reply_code, reply_devnumber, reply_data) + if _unhandled: + _unhandled(reply_code, reply_devnumber, reply_data) continue if reply_code == 0x10 and reply_data[:1] == b'\x8F' and reply_data[1:3] == feature_index_function: # device not present - _l.log(_LOG_LEVEL, "(%d) request ping failed on {%s} call: [%s]", devnumber, _hex(feature_index_function), _hex(reply_data)) + _log.debug("(%d) request ping failed on {%s} call: [%s]", devnumber, _hex(feature_index_function), _hex(reply_data)) return None if reply_code == 0x10 and reply_data[:1] == b'\x8F': # device not present - _l.log(_LOG_LEVEL, "(%d) request ping failed: [%s]", devnumber, _hex(reply_data)) + _log.debug("(%d) request ping failed: [%s]", devnumber, _hex(reply_data)) return 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, ERROR_NAME[error_code], _hex(reply_data)) + _log.warn("(%d) request feature call error %d = %s: %s", devnumber, error_code, ERROR_NAME[error_code], _hex(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 @@ -283,14 +294,14 @@ def request(handle, devnumber, feature_index_function, params=b'', features=None if reply_code == 0x11 and reply_data[:2] == feature_index_function: # a matching reply - # _l.log(_LOG_LEVEL, "(%d) matched reply with feature-index-function [%s]", devnumber, _hex(reply_data[2:])) + # _log.debug("(%d) matched reply with feature-index-function [%s]", devnumber, _hex(reply_data[2:])) return reply_data[2:] if reply_code == 0x10 and devnumber == 0xFF and reply_data[:2] == feature_index_function: # direct calls to the receiver (device 0xFF) may also return successfully with reply code 0x10 - # _l.log(_LOG_LEVEL, "(%d) matched reply with feature-index-function [%s]", devnumber, _hex(reply_data[2:])) + # _log.debug("(%d) matched reply with feature-index-function [%s]", devnumber, _hex(reply_data[2:])) return reply_data[2:] - # _l.log(_LOG_LEVEL, "(%d) unmatched reply {%s} (expected {%s})", devnumber, _hex(reply_data[:2]), _hex(feature_index_function)) - if unhandled_hook: - unhandled_hook(reply_code, reply_devnumber, reply_data) + # _log.debug("(%d) unmatched reply {%s} (expected {%s})", devnumber, _hex(reply_data[:2]), _hex(feature_index_function)) + if _unhandled: + _unhandled(reply_code, reply_devnumber, reply_data) diff --git a/lib/logitech/unifying_receiver/listener.py b/lib/logitech/unifying_receiver/listener.py index f9e7daa1..220a7fcb 100644 --- a/lib/logitech/unifying_receiver/listener.py +++ b/lib/logitech/unifying_receiver/listener.py @@ -2,8 +2,7 @@ # # -from logging import getLogger as _Logger -from threading import (Thread, Event, Lock) +from threading import Thread as _Thread # from time import sleep as _sleep from . import base as _base @@ -12,108 +11,107 @@ from .common import Packet as _Packet # for both Python 2 and 3 try: - from Queue import Queue + from Queue import Queue as _Queue except ImportError: - from queue import Queue + from queue import Queue as _Queue -_LOG_LEVEL = 6 -_l = _Logger('lur.listener') +from logging import getLogger +_log = getLogger('LUR').getChild('listener') +del getLogger -_READ_EVENT_TIMEOUT = int(_base.DEFAULT_TIMEOUT / 4) # ms - +_READ_EVENT_TIMEOUT = int(_base.DEFAULT_TIMEOUT) # ms 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: + event = listener._events.get(True, _READ_EVENT_TIMEOUT * 10) + except: + continue + _log.debug("delivering event %s", event) try: callback(event) except: - _l.exception("callback for %s", event) - # _l.log(_LOG_LEVEL, "stopped dispatch") + _log.exception("callback for %s", event) -class EventsListener(Thread): +class EventsListener(_Thread): """Listener thread for events from the Unifying Receiver. Incoming packets will be passed to the callback function in sequence, by a separate thread. - - While this listener is running, you must use the call_api() 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_handle, events_callback): - super(EventsListener, self).__init__(group='Unifying Receiver', name='%s-%X' % (self.__class__.__name__, receiver_handle)) + super(EventsListener, self).__init__(group='Unifying Receiver', name=self.__class__.__name__) self.daemon = True self._active = False self._handle = receiver_handle - self._task = None - self._task_processing = Lock() - self._task_reply = None - self._task_done = Event() + self._tasks = _Queue(1) + self._backup_unhandled_hook = _base.unhandled_hook + _base.unhandled_hook = self.unhandled_hook - self._events = Queue(32) - _base.unhandled_hook = self._unhandled - - self._dispatcher = Thread(group='Unifying Receiver', - name='%s-%X-dispatch' % (self.__class__.__name__, receiver_handle), + self._events = _Queue(32) + self._dispatcher = _Thread(group='Unifying Receiver', + name=self.__class__.__name__ + '-dispatch', target=_event_dispatch, args=(self, events_callback)) self._dispatcher.daemon = True def run(self): self._active = True - _l.log(_LOG_LEVEL, "started") + _log.debug("started") + _base.request_context = self + _base.unhandled_hook = self._backup_unhandled_hook + del self._backup_unhandled_hook self._dispatcher.start() while self._active: event = None try: + # _log.debug("read next event") event = _base.read(self._handle, _READ_EVENT_TIMEOUT) except _NoReceiver: self._handle = 0 - _l.warn("receiver disconnected") + _log.warn("receiver disconnected") self._events.put(_Packet(0xFF, 0xFF, None)) self._active = False break - if event: - event = _Packet(*event) - _l.log(_LOG_LEVEL, "queueing event %s", event) - self._events.put(event) + if event is not None: + matched = False + task = None if self._tasks.empty() else self._tasks.queue[0] + if task and task[0] and task[-1] is None: + devnumber, data = task[1:3] + if event[1] == devnumber: + # _log.debug("matching %s to %d, %s", event, devnumber, repr(data)) + if event[0] == 0x11 or (event[0] == 0x10 and devnumber == 0xFF): + matched = (event[2][:2] == data[:2]) or (event[2][:1] == b'\xFF' and event[2][1:3] == data[:2]) + elif event[0] == 0x10: + if event[2][:1] == b'\x8F' and event[2][1:3] == data[:2]: + matched = True - 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() + if matched: + # _log.debug("request reply %s", event) + task[-1] = event + self._tasks.task_done() + else: + event = _Packet(*event) + _log.info("queueing event %s", event) + self._events.put(event) + _base.request_context = None _base.close(self._handle) + _log.debug("stopped") self._handle = 0 def stop(self): """Tells the listener to stop as soon as possible.""" if self._active: - _l.log(_LOG_LEVEL, "stopping") + _log.debug("stopping") self._active = False # wait for the receiver handle to be closed self.join() @@ -122,34 +120,27 @@ class EventsListener(Thread): def handle(self): return self._handle - def request(self, device, feature_function_index, params=b''): - return self.call_api(_base.request, device, feature_function_index, params) + def write(self, handle, devnumber, data): + assert handle == self._handle + # _log.debug("write %02X %s", devnumber, _base._hex(data)) + task = [False, devnumber, data, None] + self._tasks.put(task) + _base.write(self._handle, devnumber, data) + task[0] = True + _log.debug("task queued %s", task) - def call_api(self, api_function, *args, **kwargs): - """Make an UR API request through this listener's receiver. + def read(self, handle, timeout=_base.DEFAULT_TIMEOUT): + assert handle == self._handle + # _log.debug("read %d", timeout) + assert not self._tasks.empty() + self._tasks.join() + task = self._tasks.get(False) + _log.debug("task ready %s", task) + return task[-1] - The api_function must have a receiver handle as a first agument, all - other passed args and kwargs will follow. - """ - # _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 - - 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 _unhandled(self, reply_code, devnumber, data): + def unhandled_hook(self, reply_code, devnumber, data): event = _Packet(reply_code, devnumber, data) - # _l.log(_LOG_LEVEL, "queueing unhandled event %s", event) + _log.info("queueing unhandled event %s", event) self._events.put(event) def __nonzero__(self):