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)