diff --git a/app/receiver.py b/app/receiver.py index 58dc76fd..5647ff05 100644 --- a/app/receiver.py +++ b/app/receiver.py @@ -20,16 +20,17 @@ class _FeaturesArray(object): __slots__ = ('device', 'features', 'supported') def __init__(self, device): + assert device is not None self.device = device self.features = None self.supported = True - self._check() def __del__(self): self.supported = False self.device = None def _check(self): + # print ("%s check" % self.device) if self.supported: if self.features is not None: return True @@ -118,10 +119,11 @@ class _FeaturesArray(object): class DeviceInfo(_api.PairedDevice): """A device attached to the receiver. """ - def __init__(self, handle, number, status=STATUS.UNKNOWN, status_changed_callback=None): + def __init__(self, handle, number, status_changed_callback, status=STATUS.BOOTING): super(DeviceInfo, self).__init__(handle, number) self.LOG = _Logger("Device[%d]" % (number)) + assert status_changed_callback self.status_changed_callback = status_changed_callback self._status = status self.props = {} @@ -131,6 +133,7 @@ class DeviceInfo(_api.PairedDevice): def __del__(self): super(ReceiverListener, self).__del__() self._features.supported = False + self._features.device = None @property def status(self): @@ -139,31 +142,36 @@ class DeviceInfo(_api.PairedDevice): @status.setter def status(self, new_status): if new_status < STATUS.CONNECTED: - self.props.clear() + for p in list(self.props): + if p != PROPS.BATTERY_LEVEL: + del self.props[p] else: self._features._check() - self.serial, self.codename, self.name, self.kind + self.protocol, self.codename, self.name, self.kind - if new_status != self._status and not (new_status == STATUS.CONNECTED and self._status > new_status): - self.LOG.debug("status %d => %d", self._status, new_status) + old_status = self._status + if new_status != old_status and not (new_status == STATUS.CONNECTED and old_status > new_status): + self.LOG.debug("status %d => %d", old_status, new_status) self._status = new_status - if self.status_changed_callback: - ui_flags = STATUS.UI_NOTIFY if new_status == STATUS.UNPAIRED else 0 - self.status_changed_callback(self, ui_flags) + ui_flags = STATUS.UI_NOTIFY if new_status == STATUS.UNPAIRED else 0 + self.status_changed_callback(self, ui_flags) @property def status_text(self): if self._status < STATUS.CONNECTED: return STATUS_NAME[self._status] + return STATUS_NAME[STATUS.CONNECTED] + @property + def properties_text(self): t = [] - if self.props.get(PROPS.BATTERY_LEVEL): + if self.props.get(PROPS.BATTERY_LEVEL) is not None: t.append('Battery: %d%%' % self.props[PROPS.BATTERY_LEVEL]) - if self.props.get(PROPS.BATTERY_STATUS): + if self.props.get(PROPS.BATTERY_STATUS) is not None: t.append(self.props[PROPS.BATTERY_STATUS]) - if self.props.get(PROPS.LIGHT_LEVEL): + if self.props.get(PROPS.LIGHT_LEVEL) is not None: t.append('Light: %d lux' % self.props[PROPS.LIGHT_LEVEL]) - return ', '.join(t) if t else STATUS_NAME[STATUS.CONNECTED] + return ', '.join(t) def process_event(self, code, data): if code == 0x10 and data[:1] == b'\x8F': @@ -179,13 +187,10 @@ class DeviceInfo(_api.PairedDevice): if type(status) == tuple: ui_flags = status[1].pop(PROPS.UI_FLAGS, 0) - p = dict(self.props) self.props.update(status[1]) - if self.status == status[0]: - if self.status_changed_callback and (ui_flags or p != self.props): - self.status_changed_callback(self, ui_flags) - else: - self.status = status[0] + self.status = status[0] + if ui_flags: + self.status_changed_callback(self, ui_flags) return True self.LOG.warn("don't know how to handle processed event status %s", status) @@ -304,15 +309,12 @@ class ReceiverListener(_EventsListener): status = self._device_status_from(event) if status is not None: - dev = DeviceInfo(self.handle, event.devnumber, status, self.status_changed) + dev = DeviceInfo(self.handle, event.devnumber, self.status_changed, status) self.LOG.info("new device %s", dev) - - self.change_status(STATUS.CONNECTED + 1 + len(self.receiver.devices)) - - if status == STATUS.CONNECTED: - dev.protocol, dev.name, dev.kind + dev.status = status self.status_changed(dev, STATUS.UI_NOTIFY) self.receiver.devices[event.devnumber] = dev + self.change_status(STATUS.CONNECTED + len(self.receiver.devices)) if status == STATUS.CONNECTED: dev.serial, dev.firmware return dev diff --git a/app/solaar.py b/app/solaar.py index f9167337..f0f0df24 100644 --- a/app/solaar.py +++ b/app/solaar.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python -u NAME = 'Solaar' VERSION = '0.7.3' @@ -44,27 +44,18 @@ def _parse_arguments(): return args -def _check_requirements(): +def _require(module, package): try: - import pyudev + __import__(module) except ImportError: - return 'python-pyudev' - - try: - import gi.repository - except ImportError: - return 'python-gi' - - try: - from gi.repository import Gtk - except ImportError: - return 'gir1.2-gtk-3.0' + import sys + sys.exit("%s: missing required package '%s'" % (NAME, package)) if __name__ == '__main__': - req_fail = _check_requirements() - if req_fail: - raise ImportError('missing required package: %s' % req_fail) + _require('pyudev', 'python-pyudev') + _require('gi.repository', 'python-gi') + _require('gi.repository.Gtk', 'gir1.2-gtk-3.0') args = _parse_arguments() @@ -95,10 +86,17 @@ if __name__ == '__main__': notify_missing = True def status_changed(receiver, device=None, ui_flags=0): - ui.update(receiver, icon, window, device) + assert receiver is not None + if window: + GObject.idle_add(ui.main_window.update, window, receiver, device) + if icon: + GObject.idle_add(ui.status_icon.update, icon, receiver) if ui_flags & STATUS.UI_POPUP: - window.present() + GObject.idle_add(window.popup, icon) + if device is None: + # always notify on receiver updates + ui_flags |= STATUS.UI_NOTIFY if ui_flags & STATUS.UI_NOTIFY and ui.notify.available: GObject.idle_add(ui.notify.show, device or receiver) @@ -131,10 +129,10 @@ if __name__ == '__main__': # print ("opened receiver", listener, listener.receiver) notify_missing = True - pairing.state = pairing.State(listener) status_changed(listener.receiver, None, STATUS.UI_NOTIFY) - listener.trigger_device_events() GObject.timeout_add(5 * 1000, _check_still_scanning, listener) + pairing.state = pairing.State(listener) + listener.trigger_device_events() GObject.timeout_add(50, check_for_listener, False) Gtk.main() diff --git a/app/ui/__init__.py b/app/ui/__init__.py index 55233775..7f8d530e 100644 --- a/app/ui/__init__.py +++ b/app/ui/__init__.py @@ -20,6 +20,8 @@ def get_icon(name, fallback): return name if name and _ICON_THEME.has_icon(name) else fallback def get_battery_icon(level): + if level < 0: + return 'battery_unknown' return 'battery_%03d' % (10 * ((level + 5) // 10)) def icon_file(name): @@ -59,12 +61,3 @@ def find_children(container, *child_names): result = [None] * count _iterate_children(container, names, result, count) return tuple(result) if count > 1 else result[0] - - -def update(receiver, icon, window, reason): - assert receiver is not None - assert reason is not None - if window: - GObject.idle_add(main_window.update, window, receiver, reason) - if icon: - GObject.idle_add(status_icon.update, icon, receiver) diff --git a/app/ui/main_window.py b/app/ui/main_window.py index 532be721..b2ac964e 100644 --- a/app/ui/main_window.py +++ b/app/ui/main_window.py @@ -151,6 +151,9 @@ def toggle(window, trigger): window.present() return True +def _popup(window, trigger): + if not window.get_visible(): + toggle(window, trigger) def create(title, name, max_devices, systray=False): window = Gtk.Window() @@ -177,6 +180,7 @@ def create(title, name, max_devices, systray=False): window.set_resizable(False) window.toggle_visible = lambda i: toggle(window, i) + window.popup = lambda i: _popup(window, i) if systray: window.set_keep_above(True) @@ -272,12 +276,16 @@ def _update_device_box(frame, dev): status_icons = status.get_children() if dev.status < STATUS.CONNECTED: - label.set_sensitive(True) + label.set_sensitive(False) battery_icon, battery_label = status_icons[0:2] battery_icon.set_sensitive(False) - battery_label.set_markup('%s' % dev.status_text) - battery_label.set_sensitive(True) + battery_label.set_sensitive(False) + battery_level = dev.props.get(PROPS.BATTERY_LEVEL) + if battery_level is None: + battery_label.set_markup('(%s)' % dev.status_text) + else: + battery_label.set_markup('%d%% (%s)' % (battery_level, dev.status_text)) for c in status_icons[2:-1]: c.set_visible(False) diff --git a/app/ui/status_icon.py b/app/ui/status_icon.py index b091d3d2..2e24d48d 100644 --- a/app/ui/status_icon.py +++ b/app/ui/status_icon.py @@ -34,29 +34,33 @@ def create(window, menu_actions=None): def update(icon, receiver): battery_level = None - if receiver.status > STATUS.CONNECTED and receiver.devices: - lines = [] - if receiver.status < STATUS.CONNECTED: - lines += (receiver.status_text, '') + lines = [ui.NAME + ': ' + receiver.status_text, ''] + if receiver.status > STATUS.CONNECTED: devlist = sorted(receiver.devices.values(), key=lambda x: x.number) for dev in devlist: - name = '' + dev.name + '' - if dev.status < STATUS.CONNECTED: - lines.append(name + ' (' + dev.status_text + ')') + lines.append('' + dev.name + '') + + p = dev.properties_text + if p: + p = '\t' + p + if dev.status < STATUS.CONNECTED: + p += ' (' + dev.status_text + ')' + lines.append(p) + elif dev.status < STATUS.CONNECTED: + lines.append('\t(' + dev.status_text + ')') + elif dev.protocol < 2.0: + lines.append('\t' + 'no status') else: - lines.append(name) - if dev.status > STATUS.CONNECTED: - lines.append(' ' + dev.status_text) + lines.append('\t' + 'waiting for status...') + lines.append('') - if battery_level is None and PROPS.BATTERY_LEVEL in dev.props: - battery_level = dev.props[PROPS.BATTERY_LEVEL] + if battery_level is None: + if PROPS.BATTERY_LEVEL in dev.props: + battery_level = dev.props[PROPS.BATTERY_LEVEL] - text = '\n'.join(lines).rstrip('\n') - icon.set_tooltip_markup(ui.NAME + ':\n' + text) - else: - icon.set_tooltip_text(ui.NAME + ': ' + receiver.status_text) + icon.set_tooltip_markup('\n'.join(lines).rstrip('\n')) if battery_level is None: icon.set_from_icon_name(ui.appicon(receiver.status)) diff --git a/bin/hidconsole b/bin/hidconsole index b876a386..d18cc12e 100755 --- a/bin/hidconsole +++ b/bin/hidconsole @@ -6,4 +6,4 @@ LIB=`readlink -f $(dirname "$Z")/../lib` export PYTHONPATH=$LIB PYTHON=`which python python2 python3 | head -n 1` -exec $PYTHON -OOu -m hidapi.hidconsole "$@" +exec $PYTHON -u -m hidapi.hidconsole "$@" diff --git a/bin/scan b/bin/scan index 8b5cb8c7..2a5b1061 100755 --- a/bin/scan +++ b/bin/scan @@ -6,4 +6,4 @@ LIB=`readlink -f $(dirname "$Z")/../lib` export PYTHONPATH=$LIB PYTHON=`which python python2 python3 | head -n 1` -exec $PYTHON -OOu -m logitech.scanner "$@" +exec $PYTHON -u -m logitech.scanner "$@" diff --git a/bin/solaar b/bin/solaar index 595ca7cb..e8b2c1c7 100755 --- a/bin/solaar +++ b/bin/solaar @@ -10,4 +10,4 @@ export PYTHONPATH=$APP:$LIB export XDG_DATA_DIRS=$SHARE:$XDG_DATA_DIRS PYTHON=`which python python2 python3 | head -n 1` -exec $PYTHON -OOu -m solaar "$@" +exec $PYTHON -u -m solaar "$@" diff --git a/lib/hidapi/udev.py b/lib/hidapi/udev.py index dc419db4..dea436b7 100644 --- a/lib/hidapi/udev.py +++ b/lib/hidapi/udev.py @@ -125,6 +125,7 @@ def open_path(device_path): :returns: an opaque device handle, or ``None``. """ + assert device_path assert '/dev/hidraw' in device_path return _os.open(device_path, _os.O_RDWR | _os.O_SYNC) @@ -134,6 +135,7 @@ def close(device_handle): :param device_handle: a device handle returned by open() or open_path(). """ + assert device_handle _os.close(device_handle) @@ -158,8 +160,8 @@ def write(device_handle, data): one exists. If it does not, it will send the data through the Control Endpoint (Endpoint 0). """ + assert device_handle bytes_written = _os.write(device_handle, data) - if bytes_written != len(data): raise OSError(errno=_errno.EIO, strerror='written %d bytes out of expected %d' % (bytes_written, len(data))) @@ -180,6 +182,7 @@ def read(device_handle, bytes_count, timeout_ms=-1): :returns: the data packet read, an empty bytes string if a timeout was reached, or None if there was an error while reading. """ + assert device_handle timeout = None if timeout_ms < 0 else timeout_ms / 1000.0 rlist, wlist, xlist = _select([device_handle], [], [device_handle], timeout) @@ -239,6 +242,7 @@ def get_indexed_string(device_handle, index): if index not in _DEVICE_STRINGS: return None + assert device_handle stat = _os.fstat(device_handle) dev = _Device.from_device_number(_Context(), 'char', stat.st_rdev) if dev: diff --git a/lib/logitech/scanner.py b/lib/logitech/scanner.py index 6c9a8cd7..dd11e707 100644 --- a/lib/logitech/scanner.py +++ b/lib/logitech/scanner.py @@ -7,7 +7,7 @@ def print_receiver(receiver): print (" Serial : %s" % receiver.serial) for f in receiver.firmware: print (" %-10s: %s" % (f.kind, f.version)) - print (" Receiver reported %d paired device(s)" % len(receiver)) + print (" Reported %d paired device(s)" % len(receiver)) def scan_devices(receiver): diff --git a/lib/logitech/unifying_receiver/api.py b/lib/logitech/unifying_receiver/api.py index 2bc0db2c..2366a652 100644 --- a/lib/logitech/unifying_receiver/api.py +++ b/lib/logitech/unifying_receiver/api.py @@ -32,9 +32,11 @@ class ThreadedHandle(object): __slots__ = ['path', '_local', '_handles'] def __init__(self, initial_handle, path): + assert initial_handle if type(initial_handle) != int: raise TypeError('expected int as initial handle, got %s' % repr(initial_handle)) + assert path self.path = path self._local = _local() self._local.handle = initial_handle @@ -80,7 +82,9 @@ class ThreadedHandle(object): class PairedDevice(object): def __init__(self, handle, number): + assert handle self.handle = handle + assert number > 0 and number <= MAX_ATTACHED_DEVICES self.number = number self._protocol = None @@ -168,7 +172,9 @@ class Receiver(object): max_devices = MAX_ATTACHED_DEVICES def __init__(self, handle, path=None): + assert handle self.handle = handle + assert path self.path = path self._serial = None diff --git a/lib/logitech/unifying_receiver/tests/test_50_api.py b/lib/logitech/unifying_receiver/tests/test_50_api.py index 9d73d13b..8ef6700a 100644 --- a/lib/logitech/unifying_receiver/tests/test_50_api.py +++ b/lib/logitech/unifying_receiver/tests/test_50_api.py @@ -38,14 +38,6 @@ class Test_UR_API(unittest.TestCase): Test_UR_API.receiver = api.Receiver.open() self._check(check_device=False) - def test_05_ping_device_zero(self): - self._check(check_device=False) - - d = api.PairedDevice(self.receiver.handle, 0) - ok = d.ping() - self.assertIsNotNone(ok, "invalid ping reply") - self.assertFalse(ok, "device zero replied") - def test_10_ping_all_devices(self): self._check(check_device=False)