From e34ad5104f6c3260e0754d9c522fed6dbaea0e81 Mon Sep 17 00:00:00 2001 From: Daniel Pavel Date: Wed, 10 Oct 2012 06:37:03 +0300 Subject: [PATCH] reworked the event listener --- app/__init__.py | 19 ++-- app/actions.py | 41 +++++++ app/ui/icon.py | 18 ++-- app/ui/pair.py | 17 +++ app/ui/window.py | 19 ++-- app/watcher.py | 99 ++++++++--------- lib/cli/hidconsole.py | 4 +- lib/logitech/devices/k750.py | 4 +- lib/logitech/unifying_receiver/__init__.py | 6 ++ lib/logitech/unifying_receiver/api.py | 45 ++++---- lib/logitech/unifying_receiver/base.py | 76 ++++++++----- lib/logitech/unifying_receiver/common.py | 1 + lib/logitech/unifying_receiver/listener.py | 102 ++++++++++++------ .../unifying_receiver/tests/test_30_base.py | 5 +- lib/logitech/unifying_receiver/unhandled.py | 36 ------- lib/unittest.sh | 5 +- solaar | 6 +- solaar.py | 3 +- tools/hidconsole | 4 +- tools/ur_scanner | 8 +- 20 files changed, 304 insertions(+), 214 deletions(-) create mode 100644 app/ui/pair.py delete mode 100644 lib/logitech/unifying_receiver/unhandled.py diff --git a/app/__init__.py b/app/__init__.py index 99baa9f0..ba48849c 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -2,27 +2,28 @@ # # -import threading -from gi.repository import Gtk from gi.repository import GObject -from .watcher import WatcherThread from . import ui +from logitech.devices import constants as C APP_TITLE = 'Solaar' + def _status_updated(watcher, icon, window): while True: watcher.status_changed.wait() text = watcher.status_text watcher.status_changed.clear() + icon_name = APP_TITLE + '-fail' if watcher.rstatus.code < C.STATUS.CONNECTED else APP_TITLE + if icon: - GObject.idle_add(ui.icon.update, icon, watcher.rstatus, text) + GObject.idle_add(ui.icon.update, icon, watcher.rstatus, text, icon_name) if window: - GObject.idle_add(ui.window.update, window, watcher.rstatus, dict(watcher.devices)) + GObject.idle_add(ui.window.update, window, watcher.rstatus, dict(watcher.devices), icon_name) def run(config): @@ -30,16 +31,22 @@ def run(config): ui.notify.init(APP_TITLE, config.notifications) + from .watcher import WatcherThread watcher = WatcherThread(ui.notify.show) watcher.start() window = ui.window.create(APP_TITLE, watcher.rstatus, not config.start_hidden, config.close_to_tray) - tray_icon = ui.icon.create(APP_TITLE, (ui.window.toggle, window)) + window.set_icon_name(APP_TITLE + '-fail') + tray_icon = ui.icon.create(APP_TITLE, (ui.window.toggle, window)) + tray_icon.set_from_icon_name(APP_TITLE + '-fail') + + import threading ui_update_thread = threading.Thread(target=_status_updated, name='ui_update', args=(watcher, tray_icon, window)) ui_update_thread.daemon = True ui_update_thread.start() + from gi.repository import Gtk Gtk.main() watcher.stop() diff --git a/app/actions.py b/app/actions.py index e69de29b..2504a143 100644 --- a/app/actions.py +++ b/app/actions.py @@ -0,0 +1,41 @@ +# +# +# + +import logging + +from logitech.unifying_receiver import api +from logitech import devices + +from . import ui +import ui.pair + + +def full_scan(button, watcher): + if watcher.active and watcher.listener: + updated = False + + for devnumber in range(1, 1 + api.C.MAX_ATTACHED_DEVICES): + devstatus = watcher.devices.get(devnumber) + if devstatus: + status = devices.request_status(devstatus, watcher.listener) + updated |= watcher._device_status_changed(devstatus, status) + else: + devstatus = watcher._new_device(devnumber) + updated |= devstatus is not None + + if updated: + watcher._update_status_text() + + return updated + + +def pair(button, watcher): + if watcher.active and watcher.listener: + logging.debug("pair") + + parent = button.get_toplevel() + title = parent.get_title() + ': ' + button.get_tooltip_text() + w = ui.pair.create(parent, title) + w.run() + w.destroy() diff --git a/app/ui/icon.py b/app/ui/icon.py index 83a97c67..3590d783 100644 --- a/app/ui/icon.py +++ b/app/ui/icon.py @@ -4,14 +4,9 @@ from gi.repository import Gtk -from logitech.devices import constants as C - -_ICON_OK = 'Solaar' -_ICON_FAIL = _ICON_OK + '-fail' - def create(title, click_action=None): - icon = Gtk.StatusIcon.new_from_icon_name(_ICON_OK) + icon = Gtk.StatusIcon() icon.set_title(title) icon.set_name(title) @@ -37,9 +32,8 @@ def create(title, click_action=None): return icon -def update(icon, receiver, tooltip): - icon.set_tooltip_markup(tooltip) - if receiver.code < C.STATUS.CONNECTED: - icon.set_from_icon_name(_ICON_FAIL) - else: - icon.set_from_icon_name(_ICON_OK) +def update(icon, receiver, tooltip=None, icon_name=None): + if tooltip is not None: + icon.set_tooltip_markup(tooltip) + if icon_name is not None: + icon.set_from_icon_name(icon_name) diff --git a/app/ui/pair.py b/app/ui/pair.py new file mode 100644 index 00000000..31d416b8 --- /dev/null +++ b/app/ui/pair.py @@ -0,0 +1,17 @@ +# +# +# + +from gi.repository import Gtk + + +def create(parent_window, title): + window = Gtk.Dialog(title, parent_window, Gtk.DialogFlags.MODAL, buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)) + + Gtk.Window.set_default_icon_name('add') + window.set_resizable(False) + + # window.set_wmclass(title, 'status-window') + # window.set_role('pair') + + return window diff --git a/app/ui/window.py b/app/ui/window.py index 8a9d18f4..3c0a8708 100644 --- a/app/ui/window.py +++ b/app/ui/window.py @@ -77,8 +77,11 @@ def _update_device_box(frame, devstatus): expander.set_label(_PLACEHOLDER) -def update(window, receiver, devices): +def update(window, receiver, devices, icon_name=None): if window and window.get_child(): + if icon_name is not None: + window.set_icon_name(icon_name) + controls = list(window.get_child().get_children()) _update_receiver_box(controls[0], receiver) for index in range(1, 1 + _MAX_DEVICES): @@ -107,9 +110,9 @@ def _receiver_box(rstatus): buttons_box.set_layout(Gtk.ButtonBoxStyle.START) vbox.pack_start(buttons_box, True, True, 0) - def _action(button, action): + def _action(button, function, params): button.set_sensitive(False) - action() + function(button, *params) button.set_sensitive(True) def _add_button(name, icon, action): @@ -119,7 +122,9 @@ def _receiver_box(rstatus): button.set_tooltip_text(name) button.set_focus_on_click(False) if action: - button.connect('clicked', _action, action) + function = action[0] + params = action[1:] + button.connect('clicked', _action, function, params) else: button.set_sensitive(False) buttons_box.pack_start(button, False, False, 0) @@ -167,14 +172,12 @@ def _device_box(): def create(title, rstatus, show=True, close_to_tray=False): window = Gtk.Window() - Gtk.Window.set_default_icon_name('mouse') - window.set_icon_name(title) - window.set_title(title) + window.set_keep_above(True) window.set_deletable(False) window.set_resizable(False) - window.set_size_request(200, 50) + # window.set_size_request(200, 50) window.set_default_size(200, 50) window.set_position(Gtk.WindowPosition.MOUSE) diff --git a/app/watcher.py b/app/watcher.py index 65dbc968..a09e44e2 100644 --- a/app/watcher.py +++ b/app/watcher.py @@ -5,13 +5,17 @@ import logging import threading import time -from binascii import hexlify as _hexlify from logitech.unifying_receiver import api from logitech.unifying_receiver.listener import EventsListener from logitech import devices from logitech.devices import constants as C +from . import actions + + +_l = logging.getLogger('watcher') + _STATUS_TIMEOUT = 31 # seconds _THREAD_SLEEP = 2 # seconds @@ -30,11 +34,14 @@ class _DevStatus(api.AttachedDeviceInfo): text = _INITIALIZING refresh = None + def __str__(self): + return 'DevStatus(%d,%s,%d)' % (self.number, self.name, self.code) + class WatcherThread(threading.Thread): """Keeps a map of all attached devices and their statuses.""" def __init__(self, notify_callback=None): - super(WatcherThread, self).__init__(name='WatcherThread') + super(WatcherThread, self).__init__(group='Solaar', name='Watcher') self.daemon = True self.active = False @@ -44,9 +51,9 @@ class WatcherThread(threading.Thread): self.listener = None - self.rstatus = _DevStatus(0, _UNIFYING_RECEIVER, _UNIFYING_RECEIVER, None, None) - self.rstatus.refresh = self.full_scan - self.rstatus.pair = None + self.rstatus = _DevStatus(0, 0xFF, None, _UNIFYING_RECEIVER, None, None) + self.rstatus.refresh = (actions.full_scan, self) + self.rstatus.pair = None # (actions.pair, self) self.devices = {} @@ -55,7 +62,7 @@ class WatcherThread(threading.Thread): while self.active: if self.listener is None: - self._device_status_changed(self.rstatus, (C.STATUS.UNKNOWN, _INITIALIZING)) + self._device_status_changed(self.rstatus, (C.STATUS.UNKNOWN, _SCANNING)) self._update_status_text() receiver = api.open() @@ -65,57 +72,38 @@ class WatcherThread(threading.Thread): for devinfo in api.list_devices(receiver): self._new_device(devinfo) - logging.debug("initial scan finished: %s", self.devices) if self.devices: - self._device_status_changed(self.rstatus, (C.STATUS.CONNECTED, _OKAY)) - else: - self._device_status_changed(self.rstatus, (C.STATUS.CONNECTED, _NO_DEVICES)) - self._update_status_text() + self._update_status_text() self.listener = EventsListener(receiver, self._events_callback) self.listener.start() - else: - self._device_status_changed(self.rstatus, (C.STATUS.UNAVAILABLE, _NO_RECEIVER)) - elif not self.listener.active: + elif not self.listener: self.listener = None - self._device_status_changed(self.rstatus, (C.STATUS.UNAVAILABLE, _NO_RECEIVER)) self.devices.clear() - if self.active: - update_icon = True - if self.listener and self.devices: - update_icon &= self._check_old_statuses() + if self.listener: + if self.devices: + update_icon = self._check_old_statuses() + else: + update_icon = self._device_status_changed(self.rstatus, (C.STATUS.CONNECTED, _NO_DEVICES)) + else: + update_icon = self._device_status_changed(self.rstatus, (C.STATUS.UNAVAILABLE, _NO_RECEIVER)) + + if update_icon: + self._update_status_text() if self.active: - if update_icon: - self._update_status_text() time.sleep(_THREAD_SLEEP) - self.listener.stop() if self.listener: + self.listener.stop() api.close(self.listener.receiver) - self.listener = None + self.listener = None def stop(self): self.active = False self.join() - def full_scan(self, *args): - if self.active and self.listener: - updated = False - - for devnumber in range(1, 1 + api.C.MAX_ATTACHED_DEVICES): - devstatus = self.devices.get(devnumber) - if devstatus: - status = devices.request_status(devstatus, self.listener) - updated |= self._device_status_changed(devstatus, status) - else: - devstatus = self._new_device(devnumber) - updated |= devstatus is not None - - if updated: - self._update_status_text() - def _request_status(self, devstatus): if self.listener and devstatus: status = devices.request_status(devstatus, self.listener) @@ -124,7 +112,7 @@ class WatcherThread(threading.Thread): def _check_old_statuses(self): updated = False - for devstatus in list(self.devices.values()): + for devstatus in self.devices.values(): if devstatus != self.rstatus: if time.time() - devstatus.timestamp > _STATUS_TIMEOUT: status = devices.ping(devstatus, self.listener) @@ -137,17 +125,20 @@ class WatcherThread(threading.Thread): return None if type(dev) == int: + # assert self.listener dev = self.listener.request(api.get_device_info, dev) + if dev: devstatus = _DevStatus(*dev) devstatus.refresh = self._request_status self.devices[dev.number] = devstatus + _l.debug("new devstatus %s", devstatus) self._device_status_changed(devstatus, C.STATUS.CONNECTED) - logging.debug("new devstatus %s", devstatus) + self._device_status_changed(self.rstatus, (C.STATUS.CONNECTED, _OKAY)) return devstatus def _events_callback(self, code, devnumber, data): - logging.debug("%s: event (%02x %02x [%s])", time.asctime(), code, devnumber, _hexlify(data)) + # _l.debug("event %s", (code, devnumber, data)) updated = False @@ -160,12 +151,12 @@ class WatcherThread(threading.Thread): status = devices.process_event(devstatus, self.listener, data) updated |= self._device_status_changed(devstatus, status) else: - logging.warn("unknown event code %02x", code) + _l.warn("unknown event code %02x", code) elif devnumber: self._new_device(devnumber) updated = True else: - logging.warn("don't know how to handle event (%02x, %02x, [%s])", code, devnumber, _hexlify(data)) + _l.warn("don't know how to handle event %s", (code, devnumber, data)) if updated: self._update_status_text() @@ -196,21 +187,21 @@ class WatcherThread(threading.Thread): status_code = C.STATUS.UNKNOWN status_text = '' - if not (status_code == C.STATUS.CONNECTED and old_status_code > C.STATUS.CONNECTED): - # if this is not just a ping for a device with an already known status - devstatus.code = status_code - devstatus.text = status_text - logging.debug("%s: device '%s' status update %s => %s: %s", time.asctime(), devstatus.name, old_status_code, status_code, status_text) + 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 status update %s => %s: %s", devstatus, old_status_code, status_code, status_text) + + if self.notify: if status_code < C.STATUS.CONNECTED or old_status_code < C.STATUS.CONNECTED or status_code < old_status_code: - self._notify(devstatus.code, devstatus.name, devstatus.text) + self.notify(devstatus.code, devstatus.name, devstatus.text) return True - def _notify(self, *args): - if self.notify: - self.notify(*args) - def _update_status_text(self): last_status_text = self.status_text diff --git a/lib/cli/hidconsole.py b/lib/cli/hidconsole.py index d2c8e65e..86ea255e 100644 --- a/lib/cli/hidconsole.py +++ b/lib/cli/hidconsole.py @@ -44,10 +44,10 @@ def _continuous_read(handle, timeout=1000): if __name__ == '__main__': import argparse arg_parser = argparse.ArgumentParser() - arg_parser.add_argument('device', default=None, - help='linux device to connect to') arg_parser.add_argument('--history', default='.hidconsole-history', help='history file') + arg_parser.add_argument('device', default=None, + help='linux device to connect to') args = arg_parser.parse_args() import hidapi diff --git a/lib/logitech/devices/k750.py b/lib/logitech/devices/k750.py index 0cb4ece2..a6c35b55 100644 --- a/lib/logitech/devices/k750.py +++ b/lib/logitech/devices/k750.py @@ -18,8 +18,8 @@ NAME = 'Wireless Solar Keyboard K750' # # -def _trigger_solar_charge_events(receiver, devinfo): - return _api.request(receiver, devinfo.number, +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', features_array=devinfo.features) diff --git a/lib/logitech/unifying_receiver/__init__.py b/lib/logitech/unifying_receiver/__init__.py index df465052..aa24719c 100644 --- a/lib/logitech/unifying_receiver/__init__.py +++ b/lib/logitech/unifying_receiver/__init__.py @@ -23,3 +23,9 @@ http://6xq.net/git/lars/lshidpp.git/plain/doc/ 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 fead7a85..b84e51b2 100644 --- a/lib/logitech/unifying_receiver/api.py +++ b/lib/logitech/unifying_receiver/api.py @@ -58,13 +58,13 @@ def request(handle, devnumber, feature, function=b'\x00', params=b'', features_a if features_array is None: features_array = get_device_features(handle, devnumber) if features_array is None: - _l.log(_LOG_LEVEL, "(%d,%d) no features array available", handle, devnumber) + _l.log(_LOG_LEVEL, "(%d) no features array available", devnumber) return None if feature in features_array: feature_index = _pack('!B', features_array.index(feature)) if feature_index is None: - _l.warn("(%d,%d) feature <%s:%s> not supported", handle, devnumber, _hexlify(feature), C.FEATURE_NAME[feature]) + _l.warn("(%d) feature <%s:%s> not supported", devnumber, _hexlify(feature), C.FEATURE_NAME[feature]) raise E.FeatureNotSupported(devnumber, feature) return _base.request(handle, devnumber, feature_index + function, params) @@ -91,7 +91,7 @@ def find_device_by_name(handle, device_name): :returns: an AttachedDeviceInfo tuple, or ``None``. """ - _l.log(_LOG_LEVEL, "(%d,) searching for device '%s'", handle, device_name) + _l.log(_LOG_LEVEL, "searching for device '%s'", device_name) for devnumber in range(1, 1 + C.MAX_ATTACHED_DEVICES): features_array = get_device_features(handle, devnumber) @@ -106,7 +106,7 @@ def list_devices(handle): :returns: a list of AttachedDeviceInfo tuples. """ - _l.log(_LOG_LEVEL, "(%d,) listing all devices", handle) + _l.log(_LOG_LEVEL, "listing all devices") devices = [] @@ -131,8 +131,8 @@ def get_device_info(handle, devnumber, device_name=None, features_array=None): d_type = get_device_type(handle, devnumber, features_array) d_name = get_device_name(handle, devnumber, features_array) if device_name is None else device_name d_firmware = get_device_firmware(handle, devnumber, features_array) - devinfo = AttachedDeviceInfo(devnumber, d_type, d_name, d_firmware, features_array) - _l.log(_LOG_LEVEL, "(%d,%d) found device %s", handle, devnumber, devinfo) + devinfo = AttachedDeviceInfo(handle, devnumber, d_type, d_name, d_firmware, features_array) + _l.log(_LOG_LEVEL, "(%d) found device %s", devnumber, devinfo) return devinfo @@ -141,7 +141,7 @@ def get_feature_index(handle, devnumber, feature): :returns: An int, or ``None`` if the feature is not available. """ - _l.log(_LOG_LEVEL, "(%d,%d) get feature index <%s:%s>", handle, devnumber, _hexlify(feature), C.FEATURE_NAME[feature]) + _l.log(_LOG_LEVEL, "(%d) get feature index <%s:%s>", devnumber, _hexlify(feature), C.FEATURE_NAME[feature]) if len(feature) != 2: raise ValueError("invalid feature <%s>: it must be a two-byte string" % feature) @@ -154,18 +154,18 @@ def get_feature_index(handle, devnumber, feature): feature_flags = ord(reply[1:2]) & 0xE0 if _l.isEnabledFor(_LOG_LEVEL): if feature_flags: - _l.log(_LOG_LEVEL, "(%d,%d) feature <%s:%s> has index %d: %s", - handle, devnumber, _hexlify(feature), C.FEATURE_NAME[feature], feature_index, + _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])) else: - _l.log(_LOG_LEVEL, "(%d,%d) feature <%s:%s> has index %d", handle, devnumber, _hexlify(feature), C.FEATURE_NAME[feature], feature_index) + _l.log(_LOG_LEVEL, "(%d) feature <%s:%s> has index %d", devnumber, _hexlify(feature), C.FEATURE_NAME[feature], feature_index) # if feature_flags: # raise E.FeatureNotSupported(devnumber, feature) return feature_index - _l.warn("(%d,%d) feature <%s:%s> not supported by the device", handle, devnumber, _hexlify(feature), C.FEATURE_NAME[feature]) + _l.warn("(%d) feature <%s:%s> not supported by the device", devnumber, _hexlify(feature), C.FEATURE_NAME[feature]) raise E.FeatureNotSupported(devnumber, feature) @@ -175,13 +175,13 @@ def get_device_features(handle, devnumber): Their position in the array is the index to be used when requesting that feature on the device. """ - _l.log(_LOG_LEVEL, "(%d,%d) get device features", handle, devnumber) + _l.log(_LOG_LEVEL, "(%d) get device features", 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) if fs_index is None: - # _l.warn("(%d,%d) FEATURE_SET not available", handle, device) + # _l.warn("(%d) FEATURE_SET not available", device) return None fs_index = fs_index[:1] @@ -193,11 +193,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,%d) no features available?!", handle, devnumber) + _l.log(_LOG_LEVEL, "(%d) no features available?!", devnumber) return None features_count = ord(features_count[:1]) - _l.log(_LOG_LEVEL, "(%d,%d) found %d features", handle, devnumber, features_count) + _l.log(_LOG_LEVEL, "(%d) found %d features", devnumber, features_count) features = [None] * 0x20 for index in range(1, 1 + features_count): @@ -210,11 +210,11 @@ def get_device_features(handle, devnumber): if _l.isEnabledFor(_LOG_LEVEL): if feature_flags: - _l.log(_LOG_LEVEL, "(%d,%d) feature <%s:%s> at index %d: %s", - handle, devnumber, _hexlify(feature), C.FEATURE_NAME[feature], index, + _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])) else: - _l.log(_LOG_LEVEL, "(%d,%d) feature <%s:%s> at index %d", handle, devnumber, _hexlify(feature), C.FEATURE_NAME[feature], index) + _l.log(_LOG_LEVEL, "(%d) feature <%s:%s> at index %d", devnumber, _hexlify(feature), C.FEATURE_NAME[feature], index) features[0] = C.FEATURE.ROOT while features[-1] is None: @@ -258,7 +258,7 @@ def get_device_firmware(handle, devnumber, features_array=None): fw_info = _makeFirmwareInfo(level=fw_level, type=C.FIRMWARE_TYPE[-1]) fw.append(fw_info) - _l.log(_LOG_LEVEL, "(%d:%d) firmware %s", handle, devnumber, fw_info) + _l.log(_LOG_LEVEL, "(%d) firmware %s", devnumber, fw_info) return fw @@ -272,7 +272,7 @@ def get_device_type(handle, devnumber, features_array=None): d_type = request(handle, devnumber, C.FEATURE.NAME, function=b'\x20', features_array=features_array) if d_type: d_type = ord(d_type[:1]) - _l.log(_LOG_LEVEL, "(%d,%d) device type %d = %s", handle, devnumber, d_type, C.DEVICE_TYPE[d_type]) + _l.log(_LOG_LEVEL, "(%d) device type %d = %s", devnumber, d_type, C.DEVICE_TYPE[d_type]) return C.DEVICE_TYPE[d_type] @@ -297,7 +297,7 @@ def get_device_name(handle, devnumber, features_array=None): break d_name = d_name.decode('ascii') - _l.log(_LOG_LEVEL, "(%d,%d) device name %s", handle, devnumber, d_name) + _l.log(_LOG_LEVEL, "(%d) device name %s", devnumber, d_name) return d_name @@ -309,7 +309,8 @@ def get_device_battery_level(handle, devnumber, features_array=None): battery = request(handle, devnumber, C.FEATURE.BATTERY, features_array=features_array) if battery: discharge, dischargeNext, status = _unpack('!BBB', battery[:3]) - _l.log(_LOG_LEVEL, "(%d:%d) battery %d%% charged, next level %d%% charge, status %d = %s", discharge, dischargeNext, status, C.BATTERY_STATUSE[status]) + _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]) diff --git a/lib/logitech/unifying_receiver/base.py b/lib/logitech/unifying_receiver/base.py index 9cb64ae1..a0c198c1 100644 --- a/lib/logitech/unifying_receiver/base.py +++ b/lib/logitech/unifying_receiver/base.py @@ -10,11 +10,10 @@ from binascii import hexlify as _hexlify from . import constants as C from . import exceptions as E -from . import unhandled as _unhandled import hidapi as _hid -_LOG_LEVEL = 5 +_LOG_LEVEL = 4 _l = logging.getLogger('lur.base') # @@ -45,6 +44,31 @@ DEFAULT_TIMEOUT = 1000 # # +def _logdebug_hook(reply_code, devnumber, data): + """Default unhandled hook, logs the reply as DEBUG.""" + _l.debug("UNHANDLED %s", (reply_code, devnumber, reply_code, data)) + + +"""The function that will be called on unhandled incoming events. + +The hook must be a function with the signature: ``_(int, int, str)``, where +the parameters are: (reply_code, devnumber, data). + +This hook will only be called by the request() function, when it receives +replies that do not match the requested feature call. As such, it is not +suitable for intercepting broadcast events from the device (e.g. special +keys being pressed, battery charge events, etc), at least not in a timely +manner. However, these events *may* be delivered here if they happen while +doing a feature call to the device. + +The default implementation logs the unhandled reply as DEBUG. +""" +unhandled_hook = _logdebug_hook + +# +# +# + def list_receiver_devices(): """List all the Linux devices exposed by the UR attached to the machine.""" # (Vendor ID, Product ID) = ('Logitech', 'Unifying Receiver') @@ -72,7 +96,7 @@ def try_open(path): _l.log(_LOG_LEVEL, "[%s] open failed", path) return None - _l.log(_LOG_LEVEL, "[%s] receiver handle (%d,)", path, receiver_handle) + _l.log(_LOG_LEVEL, "[%s] receiver handle 0x%x", path, receiver_handle) # ping on device id 0 (always an error) _hid.write(receiver_handle, b'\x10\x00\x00\x10\x00\x00\xAA') @@ -82,18 +106,18 @@ def try_open(path): if reply: if reply[:4] == b'\x10\x00\x8F\x00': # 'device 0 unreachable' is the expected reply from a valid receiver handle - _l.log(_LOG_LEVEL, "[%s] success: handle (%d,)", path, receiver_handle) + _l.log(_LOG_LEVEL, "[%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] (%d,) mistery reply [%s]", path, receiver_handle, _hexlify(reply)) + _l.log(_LOG_LEVEL, "[%s] %x mistery reply [%s]", path, receiver_handle, _hexlify(reply)) else: - _l.log(_LOG_LEVEL, "[%s] (%d,) unknown reply [%s]", path, receiver_handle, _hexlify(reply)) + _l.log(_LOG_LEVEL, "[%s] %x unknown reply [%s]", path, receiver_handle, _hexlify(reply)) else: - _l.log(_LOG_LEVEL, "[%s] (%d,) no reply", path, receiver_handle) + _l.log(_LOG_LEVEL, "[%s] %x no reply", path, receiver_handle) close(receiver_handle) @@ -118,10 +142,10 @@ def close(handle): if handle: try: _hid.close(handle) - _l.log(_LOG_LEVEL, "(%d,) closed", handle) + _l.log(_LOG_LEVEL, "%x closed", handle) return True except: - _l.exception("(%d,) closing", handle) + _l.exception("%x closing", handle) return False @@ -147,10 +171,10 @@ def write(handle, devnumber, data): if _l.isEnabledFor(_LOG_LEVEL): hexs = _hexlify(wdata) - _l.log(_LOG_LEVEL, "(%d,%d) <= w[%s %s %s %s]", handle, devnumber, hexs[0:2], hexs[2:4], hexs[4:8], hexs[8:]) + _l.log(_LOG_LEVEL, "(%d) <= w[%s %s %s %s]", devnumber, hexs[0:2], hexs[2:4], hexs[4:8], hexs[8:]) if not _hid.write(handle, wdata): - _l.warn("(%d,%d) write failed, assuming receiver no longer available", handle, devnumber) + _l.warn("(%d) write failed, assuming receiver %x no longer available", devnumber, handle) close(handle) raise E.NoReceiver @@ -173,23 +197,23 @@ def read(handle, timeout=DEFAULT_TIMEOUT): """ data = _hid.read(handle, _MAX_REPLY_SIZE * 2, timeout) if data is None: - _l.warn("(%d,*) read failed, assuming receiver no longer available", handle) + _l.warn("(-) read failed, assuming receiver %x no longer available", handle) close(handle) raise E.NoReceiver if data: if len(data) < _MIN_REPLY_SIZE: - _l.warn("(%d,*) => r[%s] read packet too short: %d bytes", handle, _hexlify(data), len(data)) + _l.warn("(%d) => r[%s] read packet too short: %d bytes", ord(data[1:2]), _hexlify(data), len(data)) if len(data) > _MAX_REPLY_SIZE: - _l.warn("(%d,*) => r[%s] read packet too long: %d bytes", handle, _hexlify(data), len(data)) + _l.warn("(%d) => r[%s] read packet too long: %d bytes", ord(data[1:2]), _hexlify(data), len(data)) if _l.isEnabledFor(_LOG_LEVEL): hexs = _hexlify(data) - _l.log(_LOG_LEVEL, "(%d,*) => r[%s %s %s %s]", handle, hexs[0:2], hexs[2:4], hexs[4:8], hexs[8:]) + _l.log(_LOG_LEVEL, "(%d) => r[%s %s %s %s]", ord(data[1:2]), hexs[0:2], hexs[2:4], hexs[4:8], hexs[8:]) code = ord(data[:1]) devnumber = ord(data[1:2]) return code, devnumber, data[2:] - # _l.log(_LOG_LEVEL, "(%d,*) => r[]", handle) + # _l.log(_LOG_LEVEL, "(-) => r[]", handle) def request(handle, devnumber, feature_index_function, params=b'', features_array=None): @@ -210,7 +234,7 @@ def request(handle, devnumber, feature_index_function, params=b'', features_arra :raisees FeatureCallError: if the feature call replied with an error. """ if _l.isEnabledFor(_LOG_LEVEL): - _l.log(_LOG_LEVEL, "(%d,%d) request {%s} params [%s]", handle, devnumber, _hexlify(feature_index_function), _hexlify(params)) + _l.log(_LOG_LEVEL, "(%d) request {%s} params [%s]", devnumber, _hexlify(feature_index_function), _hexlify(params)) if len(feature_index_function) != 2: raise ValueError('invalid feature_index_function {%s}: it must be a two-byte string' % _hexlify(feature_index_function)) @@ -230,26 +254,27 @@ def request(handle, devnumber, feature_index_function, params=b'', features_arra if reply_devnumber != devnumber: # this message not for the device we're interested in - _l.log(_LOG_LEVEL, "(%d,%d) request got reply for unexpected device %d: [%s]", handle, devnumber, reply_devnumber, _hexlify(reply_data)) + _l.log(_LOG_LEVEL, "(%d) request got reply for unexpected device %d: [%s]", devnumber, reply_devnumber, _hexlify(reply_data)) # worst case scenario, this is a reply for a concurrent request # on this receiver - _unhandled._publish(reply_code, reply_devnumber, reply_data) + if unhandled_hook: + unhandled_hook(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,%d) request ping failed on {%s} call: [%s]", handle, devnumber, _hexlify(feature_index_function), _hexlify(reply_data)) + _l.log(_LOG_LEVEL, "(%d) request ping failed on {%s} call: [%s]", devnumber, _hexlify(feature_index_function), _hexlify(reply_data)) return None if reply_code == 0x10 and reply_data[:1] == b'\x8F': # device not present - _l.log(_LOG_LEVEL, "(%d,%d) request ping failed: [%s]", handle, devnumber, _hexlify(reply_data)) + _l.log(_LOG_LEVEL, "(%d) request ping failed: [%s]", devnumber, _hexlify(reply_data)) return None if reply_code == 0x11 and reply_data[0] == b'\xFF' and reply_data[1:3] == feature_index_function: # an error returned from the device error_code = ord(reply_data[3]) - _l.warn("(%d,%d) request feature call error %d = %s: %s", handle, devnumber, error_code, C.ERROR_NAME[error_code], _hexlify(reply_data)) + _l.warn("(%d) request feature call error %d = %s: %s", devnumber, error_code, C.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_array is None else features_array[feature_index] @@ -257,8 +282,9 @@ def request(handle, devnumber, feature_index_function, params=b'', features_arra if reply_code == 0x11 and reply_data[:2] == feature_index_function: # a matching reply - # _l.log(_LOG_LEVEL, "(%d,%d) matched reply with feature-index-function [%s]", handle, devnumber, _hexlify(reply_data[2:])) + # _l.log(_LOG_LEVEL, "(%d) matched reply with feature-index-function [%s]", devnumber, _hexlify(reply_data[2:])) return reply_data[2:] - _l.log(_LOG_LEVEL, "(%d,%d) unmatched reply {%s} (expected {%s})", handle, devnumber, _hexlify(reply_data[:2]), _hexlify(feature_index_function)) - _unhandled._publish(reply_code, reply_devnumber, reply_data) + _l.log(_LOG_LEVEL, "(%d) unmatched reply {%s} (expected {%s})", devnumber, _hexlify(reply_data[:2]), _hexlify(feature_index_function)) + if unhandled_hook: + unhandled_hook(reply_code, reply_devnumber, reply_data) diff --git a/lib/logitech/unifying_receiver/common.py b/lib/logitech/unifying_receiver/common.py index fc6096af..c1f5c27e 100644 --- a/lib/logitech/unifying_receiver/common.py +++ b/lib/logitech/unifying_receiver/common.py @@ -22,6 +22,7 @@ from collections import namedtuple """Tuple returned by list_devices and find_device_by_name.""" AttachedDeviceInfo = namedtuple('AttachedDeviceInfo', [ + 'handle', 'number', 'type', 'name', diff --git a/lib/logitech/unifying_receiver/listener.py b/lib/logitech/unifying_receiver/listener.py index a76ca34f..d0ee65a9 100644 --- a/lib/logitech/unifying_receiver/listener.py +++ b/lib/logitech/unifying_receiver/listener.py @@ -5,40 +5,52 @@ import logging import threading from time import sleep as _sleep -from binascii import hexlify as _hexlify from . import base as _base from . import exceptions as E -# from . import unhandled as _unhandled + +# for both Python 2 and 3 +try: + import Queue as queue +except ImportError: + import queue _LOG_LEVEL = 5 _l = logging.getLogger('lur.listener') -_READ_EVENT_TIMEOUT = int(_base.DEFAULT_TIMEOUT / 5) # ms -_IDLE_SLEEP = _base.DEFAULT_TIMEOUT / 2 # ms +_READ_EVENT_TIMEOUT = int(_base.DEFAULT_TIMEOUT / 4) # ms +_IDLE_SLEEP = _base.DEFAULT_TIMEOUT / 4 # ms + + +def _callback_caller(listener, callback): + # _l.log(_LOG_LEVEL, "%s starting callback caller", listener) + while listener._active: + event = listener.events.get() + if _l.isEnabledFor(_LOG_LEVEL): + _l.log(_LOG_LEVEL, "%s delivering event %s", listener, event) + callback.__call__(*event) + # _l.log(_LOG_LEVEL, "%s stopped callback caller", listener) class EventsListener(threading.Thread): """Listener thread for events from the Unifying Receiver. Incoming events (reply_code, devnumber, data) will be passed to the callback - function. The callback is called in the listener thread, so for best results - it should return as fast as possible. + function. The callback is called in a separate thread. While this listener is running, you should use the request() method to make - regular UR API calls, otherwise the replies may be captured by the listener - and delivered as events to the callback. As an exception, you can make API - calls in the events callback. + regular UR API calls, otherwise the replies are very likely to be captured + by the listener and delivered as events to the callback. As an exception, + you can make API calls in the events callback. """ def __init__(self, receiver, events_callback): - super(EventsListener, self).__init__(name='Unifying_Receiver_Listener_' + hex(receiver)) + super(EventsListener, self).__init__(group='Unifying Receiver', name='Events-%x' % receiver) self.daemon = True - self.active = False + self._active = False self.receiver = receiver - self.callback = events_callback self.task = None self.task_processing = threading.Lock() @@ -46,40 +58,50 @@ class EventsListener(threading.Thread): self.task_reply = None self.task_done = threading.Event() + self.events = queue.Queue(32) + + self.event_caller = threading.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 + def run(self): - self.active = True - _l.log(_LOG_LEVEL, "(%d) starting", self.receiver) + self._active = True + _l.log(_LOG_LEVEL, "%s started", self) - # last_hook = _unhandled.hook - # _unhandled.hook = self.callback + self.__str_cached = 'Events(%x:active)' % self.receiver + self.event_caller.start() - while self.active: + last_hook = _base.unhandled_hook + _base.unhandled_hook = self._unhandled + + while self._active: try: - # _l.log(_LOG_LEVEL, "(%d) reading next event", self.receiver) event = _base.read(self.receiver, _READ_EVENT_TIMEOUT) except E.NoReceiver: - _l.warn("(%d) receiver disconnected", self.receiver) - self.active = False - break + _l.warn("%s receiver disconnected", self) + self._active = False - if self.active: + if self._active: if event: - if _l.isEnabledFor(_LOG_LEVEL): - _l.log(_LOG_LEVEL, "(%d) got event (%02x %02x [%s])", self.receiver, event[0], event[1], _hexlify(event[2])) - self.callback.__call__(*event) - elif self.task is None: - # _l.log(_LOG_LEVEL, "(%d) idle sleep", self.receiver) + # _l.log(_LOG_LEVEL, "%s queueing event %s", self, event) + self.events.put(event) + + if self.task is None: + # _l.log(_LOG_LEVEL, "%s idle sleep", self) _sleep(_IDLE_SLEEP / 1000.0) else: self.task_reply = self._make_request(*self.task) self.task_done.set() - # _unhandled.hook = last_hook + self.__str_cached = 'Events(%x)' % self.receiver + + _base.unhandled_hook = last_hook def stop(self): """Tells the listener to stop as soon as possible.""" - _l.log(_LOG_LEVEL, "(%d) stopping", self.receiver) - self.active = False + _l.log(_LOG_LEVEL, "%s stopping", self) + self._active = False def request(self, api_function, *args, **kwargs): """Make an UR API request. @@ -88,7 +110,8 @@ class EventsListener(threading.Thread): other args and kwargs will follow. """ # if _l.isEnabledFor(_LOG_LEVEL): - # _l.log(_LOG_LEVEL, "(%d) request '%s.%s' with %s, %s", self.receiver, api_function.__module__, api_function.__name__, args, kwargs) + # _l.log(_LOG_LEVEL, "%s request '%s.%s' with %s, %s", self, api_function.__module__, api_function.__name__, args, kwargs) + self.task_processing.acquire() self.task_done.clear() self.task = (api_function, args, kwargs) @@ -99,18 +122,29 @@ class EventsListener(threading.Thread): self.task_processing.release() # if _l.isEnabledFor(_LOG_LEVEL): - # _l.log(_LOG_LEVEL, "(%d) request '%s.%s' => [%s]", self.receiver, api_function.__module__, api_function.__name__, _hexlify(reply)) + # _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): if _l.isEnabledFor(_LOG_LEVEL): - _l.log(_LOG_LEVEL, "(%d) calling '%s.%s' with %s, %s", self.receiver, api_function.__module__, api_function.__name__, 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 + self._active = False except Exception as e: self.task_reply = e + + def _unhandled(self, reply_code, devnumber, data): + event = (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 + + def __nonzero__(self): + return self._active diff --git a/lib/logitech/unifying_receiver/tests/test_30_base.py b/lib/logitech/unifying_receiver/tests/test_30_base.py index 3185098d..be1dc7be 100644 --- a/lib/logitech/unifying_receiver/tests/test_30_base.py +++ b/lib/logitech/unifying_receiver/tests/test_30_base.py @@ -8,7 +8,6 @@ from binascii import hexlify from .. import base from ..exceptions import * from ..constants import * -from .. import unhandled class Test_UR_Base(unittest.TestCase): @@ -157,7 +156,7 @@ class Test_UR_Base(unittest.TestCase): global received_unhandled received_unhandled = (code, device, data) - unhandled.hook = _unhandled + base.unhandled_hook = _unhandled base.write(self.handle, self.device, FEATURE.ROOT + FEATURE.FEATURE_SET) reply = base.request(self.handle, self.device, fs_index + b'\x00') self.assertIsNotNone(reply, "request returned None reply") @@ -165,7 +164,7 @@ class Test_UR_Base(unittest.TestCase): self.assertIsNotNone(received_unhandled, "extra message not received by unhandled hook") received_unhandled = None - unhandled.hook = None + base.unhandled_hook = None base.write(self.handle, self.device, FEATURE.ROOT + FEATURE.FEATURE_SET) reply = base.request(self.handle, self.device, fs_index + b'\x00') self.assertIsNotNone(reply, "request returned None reply") diff --git a/lib/logitech/unifying_receiver/unhandled.py b/lib/logitech/unifying_receiver/unhandled.py deleted file mode 100644 index 7393387d..00000000 --- a/lib/logitech/unifying_receiver/unhandled.py +++ /dev/null @@ -1,36 +0,0 @@ -# -# Optional hook for unhandled data packets received while talking to the UR. -# These are usually broadcast events received from the attached devices. -# - -import logging -from binascii import hexlify as _hexlify - - -def _logdebug_hook(reply_code, devnumber, data): - """Default unhandled hook, logs the reply as DEBUG.""" - _l = logging.getLogger('lur.unhandled') - _l.debug("UNHANDLED (,%d) code 0x%02x data [%s]", devnumber, reply_code, _hexlify(data)) - - -"""The function that will be called on unhandled incoming events. - -The hook must be a function with the signature: ``_(int, int, str)``, where -the parameters are: (reply_code, devnumber, data). - -This hook will only be called by the request() function, when it receives -replies that do not match the requested feature call. As such, it is not -suitable for intercepting broadcast events from the device (e.g. special -keys being pressed, battery charge events, etc), at least not in a timely -manner. However, these events *may* be delivered here if they happen while -doing a feature call to the device. - -The default implementation logs the unhandled reply as DEBUG. -""" -hook = _logdebug_hook - - -def _publish(reply_code, devnumber, data): - """Delivers a reply to the unhandled hook, if any.""" - if hook is not None: - hook.__call__(reply_code, devnumber, data) diff --git a/lib/unittest.sh b/lib/unittest.sh index 1f633540..84cf75f8 100755 --- a/lib/unittest.sh +++ b/lib/unittest.sh @@ -1,6 +1,7 @@ #!/bin/sh -cd `dirname "$0"` -export LD_LIBRARY_PATH=$PWD +cd -P `dirname "$0"` + +export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$PWD/native/`uname -m` exec python -m unittest discover -v "$@" diff --git a/solaar b/solaar index 6c32b727..620fe707 100755 --- a/solaar +++ b/solaar @@ -1,10 +1,12 @@ #!/bin/sh -cd `dirname "$0"` +cd -P `dirname "$0"` -export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$PWD/lib +export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$PWD/lib/native/`uname -m` export PYTHONPATH=$PWD:$PWD/lib export XDG_DATA_DIRS=$PWD/resources:$XDG_DATA_DIRS +cd - + exec python -OO solaar.py "$@" # exec python -OO -m profile -o $TMPDIR/profile.log solaar.py "$@" diff --git a/solaar.py b/solaar.py index 49736058..646b1505 100644 --- a/solaar.py +++ b/solaar.py @@ -22,7 +22,8 @@ if __name__ == '__main__': import logging log_level = logging.root.level - 10 * args.verbose - logging.basicConfig(level=log_level if log_level > 0 else 1) + 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) import app app.run(args) diff --git a/tools/hidconsole b/tools/hidconsole index 03928fb3..c1d9f4fa 100755 --- a/tools/hidconsole +++ b/tools/hidconsole @@ -1,8 +1,8 @@ #!/bin/sh -cd `dirname "$0"`/.. +cd -P `dirname "$0"`/.. -export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$PWD/lib +export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$PWD/lib/native/`uname -m` export PYTHONPATH=$PWD/lib cd - diff --git a/tools/ur_scanner b/tools/ur_scanner index 6305ac64..9f32102e 100755 --- a/tools/ur_scanner +++ b/tools/ur_scanner @@ -1,8 +1,10 @@ #!/bin/sh -cd `dirname "$0"`/../lib +cd -P `dirname "$0"`/.. -export LD_LIBRARY_PATH=$PWD -export PYTHONPATH=$PWD +export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$PWD/lib/native/`uname -m` +export PYTHONPATH=$PWD/lib + +cd - exec python -OO -m cli.ur_scanner "$@"