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') __slots__ = ('device', 'features', 'supported')
def __init__(self, device): def __init__(self, device):
assert device is not None
self.device = device self.device = device
self.features = None self.features = None
self.supported = True self.supported = True
self._check()
def __del__(self): def __del__(self):
self.supported = False self.supported = False
self.device = None self.device = None
def _check(self): def _check(self):
# print ("%s check" % self.device)
if self.supported: if self.supported:
if self.features is not None: if self.features is not None:
return True return True
@ -118,10 +119,11 @@ class _FeaturesArray(object):
class DeviceInfo(_api.PairedDevice): class DeviceInfo(_api.PairedDevice):
"""A device attached to the receiver. """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) super(DeviceInfo, self).__init__(handle, number)
self.LOG = _Logger("Device[%d]" % (number)) self.LOG = _Logger("Device[%d]" % (number))
assert status_changed_callback
self.status_changed_callback = status_changed_callback self.status_changed_callback = status_changed_callback
self._status = status self._status = status
self.props = {} self.props = {}
@ -131,6 +133,7 @@ class DeviceInfo(_api.PairedDevice):
def __del__(self): def __del__(self):
super(ReceiverListener, self).__del__() super(ReceiverListener, self).__del__()
self._features.supported = False self._features.supported = False
self._features.device = None
@property @property
def status(self): def status(self):
@ -139,31 +142,36 @@ class DeviceInfo(_api.PairedDevice):
@status.setter @status.setter
def status(self, new_status): def status(self, new_status):
if new_status < STATUS.CONNECTED: if new_status < STATUS.CONNECTED:
self.props.clear() for p in list(self.props):
if p != PROPS.BATTERY_LEVEL:
del self.props[p]
else: else:
self._features._check() 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): old_status = self._status
self.LOG.debug("status %d => %d", self._status, new_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 self._status = new_status
if self.status_changed_callback: ui_flags = STATUS.UI_NOTIFY if new_status == STATUS.UNPAIRED else 0
ui_flags = STATUS.UI_NOTIFY if new_status == STATUS.UNPAIRED else 0 self.status_changed_callback(self, ui_flags)
self.status_changed_callback(self, ui_flags)
@property @property
def status_text(self): def status_text(self):
if self._status < STATUS.CONNECTED: if self._status < STATUS.CONNECTED:
return STATUS_NAME[self._status] return STATUS_NAME[self._status]
return STATUS_NAME[STATUS.CONNECTED]
@property
def properties_text(self):
t = [] 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]) 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]) 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]) 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): def process_event(self, code, data):
if code == 0x10 and data[:1] == b'\x8F': if code == 0x10 and data[:1] == b'\x8F':
@ -179,13 +187,10 @@ class DeviceInfo(_api.PairedDevice):
if type(status) == tuple: if type(status) == tuple:
ui_flags = status[1].pop(PROPS.UI_FLAGS, 0) ui_flags = status[1].pop(PROPS.UI_FLAGS, 0)
p = dict(self.props)
self.props.update(status[1]) self.props.update(status[1])
if self.status == status[0]: self.status = status[0]
if self.status_changed_callback and (ui_flags or p != self.props): if ui_flags:
self.status_changed_callback(self, ui_flags) self.status_changed_callback(self, ui_flags)
else:
self.status = status[0]
return True return True
self.LOG.warn("don't know how to handle processed event status %s", status) 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) status = self._device_status_from(event)
if status is not None: 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.LOG.info("new device %s", dev)
dev.status = status
self.change_status(STATUS.CONNECTED + 1 + len(self.receiver.devices))
if status == STATUS.CONNECTED:
dev.protocol, dev.name, dev.kind
self.status_changed(dev, STATUS.UI_NOTIFY) self.status_changed(dev, STATUS.UI_NOTIFY)
self.receiver.devices[event.devnumber] = dev self.receiver.devices[event.devnumber] = dev
self.change_status(STATUS.CONNECTED + len(self.receiver.devices))
if status == STATUS.CONNECTED: if status == STATUS.CONNECTED:
dev.serial, dev.firmware dev.serial, dev.firmware
return dev return dev

View File

@ -1,4 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python -u
NAME = 'Solaar' NAME = 'Solaar'
VERSION = '0.7.3' VERSION = '0.7.3'
@ -44,27 +44,18 @@ def _parse_arguments():
return args return args
def _check_requirements(): def _require(module, package):
try: try:
import pyudev __import__(module)
except ImportError: except ImportError:
return 'python-pyudev' import sys
sys.exit("%s: missing required package '%s'" % (NAME, package))
try:
import gi.repository
except ImportError:
return 'python-gi'
try:
from gi.repository import Gtk
except ImportError:
return 'gir1.2-gtk-3.0'
if __name__ == '__main__': if __name__ == '__main__':
req_fail = _check_requirements() _require('pyudev', 'python-pyudev')
if req_fail: _require('gi.repository', 'python-gi')
raise ImportError('missing required package: %s' % req_fail) _require('gi.repository.Gtk', 'gir1.2-gtk-3.0')
args = _parse_arguments() args = _parse_arguments()
@ -95,10 +86,17 @@ if __name__ == '__main__':
notify_missing = True notify_missing = True
def status_changed(receiver, device=None, ui_flags=0): 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: 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: if ui_flags & STATUS.UI_NOTIFY and ui.notify.available:
GObject.idle_add(ui.notify.show, device or receiver) GObject.idle_add(ui.notify.show, device or receiver)
@ -131,10 +129,10 @@ if __name__ == '__main__':
# print ("opened receiver", listener, listener.receiver) # print ("opened receiver", listener, listener.receiver)
notify_missing = True notify_missing = True
pairing.state = pairing.State(listener)
status_changed(listener.receiver, None, STATUS.UI_NOTIFY) status_changed(listener.receiver, None, STATUS.UI_NOTIFY)
listener.trigger_device_events()
GObject.timeout_add(5 * 1000, _check_still_scanning, listener) 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) GObject.timeout_add(50, check_for_listener, False)
Gtk.main() 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 return name if name and _ICON_THEME.has_icon(name) else fallback
def get_battery_icon(level): def get_battery_icon(level):
if level < 0:
return 'battery_unknown'
return 'battery_%03d' % (10 * ((level + 5) // 10)) return 'battery_%03d' % (10 * ((level + 5) // 10))
def icon_file(name): def icon_file(name):
@ -59,12 +61,3 @@ def find_children(container, *child_names):
result = [None] * count result = [None] * count
_iterate_children(container, names, result, count) _iterate_children(container, names, result, count)
return tuple(result) if count > 1 else result[0] 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() window.present()
return True return True
def _popup(window, trigger):
if not window.get_visible():
toggle(window, trigger)
def create(title, name, max_devices, systray=False): def create(title, name, max_devices, systray=False):
window = Gtk.Window() window = Gtk.Window()
@ -177,6 +180,7 @@ def create(title, name, max_devices, systray=False):
window.set_resizable(False) window.set_resizable(False)
window.toggle_visible = lambda i: toggle(window, i) window.toggle_visible = lambda i: toggle(window, i)
window.popup = lambda i: _popup(window, i)
if systray: if systray:
window.set_keep_above(True) window.set_keep_above(True)
@ -272,12 +276,16 @@ def _update_device_box(frame, dev):
status_icons = status.get_children() status_icons = status.get_children()
if dev.status < STATUS.CONNECTED: if dev.status < STATUS.CONNECTED:
label.set_sensitive(True) label.set_sensitive(False)
battery_icon, battery_label = status_icons[0:2] battery_icon, battery_label = status_icons[0:2]
battery_icon.set_sensitive(False) battery_icon.set_sensitive(False)
battery_label.set_markup('<small>%s</small>' % dev.status_text) battery_label.set_sensitive(False)
battery_label.set_sensitive(True) 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]: for c in status_icons[2:-1]:
c.set_visible(False) c.set_visible(False)

View File

@ -34,29 +34,33 @@ def create(window, menu_actions=None):
def update(icon, receiver): def update(icon, receiver):
battery_level = None battery_level = None
if receiver.status > STATUS.CONNECTED and receiver.devices: lines = [ui.NAME + ': ' + receiver.status_text, '']
lines = []
if receiver.status < STATUS.CONNECTED:
lines += (receiver.status_text, '')
if receiver.status > STATUS.CONNECTED:
devlist = sorted(receiver.devices.values(), key=lambda x: x.number) devlist = sorted(receiver.devices.values(), key=lambda x: x.number)
for dev in devlist: for dev in devlist:
name = '<b>' + dev.name + '</b>' lines.append('<b>' + dev.name + '</b>')
if dev.status < STATUS.CONNECTED:
lines.append(name + ' (' + dev.status_text + ')') 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: else:
lines.append(name) lines.append('\t' + '<small>waiting for status...</small>')
if dev.status > STATUS.CONNECTED:
lines.append(' ' + dev.status_text)
lines.append('') lines.append('')
if battery_level is None and PROPS.BATTERY_LEVEL in dev.props: if battery_level is None:
battery_level = dev.props[PROPS.BATTERY_LEVEL] if PROPS.BATTERY_LEVEL in dev.props:
battery_level = dev.props[PROPS.BATTERY_LEVEL]
text = '\n'.join(lines).rstrip('\n') icon.set_tooltip_markup('\n'.join(lines).rstrip('\n'))
icon.set_tooltip_markup(ui.NAME + ':\n' + text)
else:
icon.set_tooltip_text(ui.NAME + ': ' + receiver.status_text)
if battery_level is None: if battery_level is None:
icon.set_from_icon_name(ui.appicon(receiver.status)) icon.set_from_icon_name(ui.appicon(receiver.status))

View File

@ -6,4 +6,4 @@ LIB=`readlink -f $(dirname "$Z")/../lib`
export PYTHONPATH=$LIB export PYTHONPATH=$LIB
PYTHON=`which python python2 python3 | head -n 1` 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 export PYTHONPATH=$LIB
PYTHON=`which python python2 python3 | head -n 1` 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 export XDG_DATA_DIRS=$SHARE:$XDG_DATA_DIRS
PYTHON=`which python python2 python3 | head -n 1` 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``. :returns: an opaque device handle, or ``None``.
""" """
assert device_path
assert '/dev/hidraw' in device_path assert '/dev/hidraw' in device_path
return _os.open(device_path, _os.O_RDWR | _os.O_SYNC) 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(). :param device_handle: a device handle returned by open() or open_path().
""" """
assert device_handle
_os.close(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 one exists. If it does not, it will send the data through
the Control Endpoint (Endpoint 0). the Control Endpoint (Endpoint 0).
""" """
assert device_handle
bytes_written = _os.write(device_handle, data) bytes_written = _os.write(device_handle, data)
if bytes_written != len(data): if bytes_written != len(data):
raise OSError(errno=_errno.EIO, strerror='written %d bytes out of expected %d' % (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 :returns: the data packet read, an empty bytes string if a timeout was
reached, or None if there was an error while reading. reached, or None if there was an error while reading.
""" """
assert device_handle
timeout = None if timeout_ms < 0 else timeout_ms / 1000.0 timeout = None if timeout_ms < 0 else timeout_ms / 1000.0
rlist, wlist, xlist = _select([device_handle], [], [device_handle], timeout) 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: if index not in _DEVICE_STRINGS:
return None return None
assert device_handle
stat = _os.fstat(device_handle) stat = _os.fstat(device_handle)
dev = _Device.from_device_number(_Context(), 'char', stat.st_rdev) dev = _Device.from_device_number(_Context(), 'char', stat.st_rdev)
if dev: if dev:

View File

@ -7,7 +7,7 @@ def print_receiver(receiver):
print (" Serial : %s" % receiver.serial) print (" Serial : %s" % receiver.serial)
for f in receiver.firmware: for f in receiver.firmware:
print (" %-10s: %s" % (f.kind, f.version)) 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): def scan_devices(receiver):

View File

@ -32,9 +32,11 @@ class ThreadedHandle(object):
__slots__ = ['path', '_local', '_handles'] __slots__ = ['path', '_local', '_handles']
def __init__(self, initial_handle, path): def __init__(self, initial_handle, path):
assert initial_handle
if type(initial_handle) != int: if type(initial_handle) != int:
raise TypeError('expected int as initial handle, got %s' % repr(initial_handle)) raise TypeError('expected int as initial handle, got %s' % repr(initial_handle))
assert path
self.path = path self.path = path
self._local = _local() self._local = _local()
self._local.handle = initial_handle self._local.handle = initial_handle
@ -80,7 +82,9 @@ class ThreadedHandle(object):
class PairedDevice(object): class PairedDevice(object):
def __init__(self, handle, number): def __init__(self, handle, number):
assert handle
self.handle = handle self.handle = handle
assert number > 0 and number <= MAX_ATTACHED_DEVICES
self.number = number self.number = number
self._protocol = None self._protocol = None
@ -168,7 +172,9 @@ class Receiver(object):
max_devices = MAX_ATTACHED_DEVICES max_devices = MAX_ATTACHED_DEVICES
def __init__(self, handle, path=None): def __init__(self, handle, path=None):
assert handle
self.handle = handle self.handle = handle
assert path
self.path = path self.path = path
self._serial = None self._serial = None

View File

@ -38,14 +38,6 @@ class Test_UR_API(unittest.TestCase):
Test_UR_API.receiver = api.Receiver.open() Test_UR_API.receiver = api.Receiver.open()
self._check(check_device=False) 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): def test_10_ping_all_devices(self):
self._check(check_device=False) self._check(check_device=False)