re-worked the UI a bit to give better info on devices status

This commit is contained in:
Daniel Pavel 2012-11-12 15:28:38 +02:00
parent 6db4deafee
commit 4c5cf85091
12 changed files with 94 additions and 87 deletions

View File

@ -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

View File

@ -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()

View File

@ -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)

View File

@ -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('<small>%s</small>' % 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('<small>(%s)</small>' % dev.status_text)
else:
battery_label.set_markup('%d%% <small>(%s)</small>' % (battery_level, dev.status_text))
for c in status_icons[2:-1]:
c.set_visible(False)

View File

@ -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 = '<b>' + dev.name + '</b>'
if dev.status < STATUS.CONNECTED:
lines.append(name + ' (' + dev.status_text + ')')
lines.append('<b>' + dev.name + '</b>')
p = dev.properties_text
if p:
p = '\t' + p
if dev.status < STATUS.CONNECTED:
p += ' (<small>' + dev.status_text + '</small>)'
lines.append(p)
elif dev.status < STATUS.CONNECTED:
lines.append('\t(<small>' + dev.status_text + '</small>)')
elif dev.protocol < 2.0:
lines.append('\t' + '<small>no status</small>')
else:
lines.append(name)
if dev.status > STATUS.CONNECTED:
lines.append(' ' + dev.status_text)
lines.append('\t' + '<small>waiting for status...</small>')
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))

View File

@ -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 "$@"

View File

@ -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 "$@"

View File

@ -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 "$@"

View File

@ -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:

View File

@ -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):

View File

@ -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

View File

@ -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)