re-worked the UI a bit to give better info on devices status
This commit is contained in:
parent
6db4deafee
commit
4c5cf85091
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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 "$@"
|
||||
|
|
2
bin/scan
2
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 "$@"
|
||||
|
|
|
@ -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 "$@"
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
Loading…
Reference in New Issue