diff --git a/app/__init__.py b/app/__init__.py index 98976f4c..093969d6 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -6,11 +6,12 @@ import threading from gi.repository import Gtk from gi.repository import GObject -from . import constants as C from .watcher import WatcherThread from . import ui +APP_TITLE = 'Solaar' + def _status_updated(watcher, icon, window): while True: watcher.status_changed.wait() @@ -18,36 +19,32 @@ def _status_updated(watcher, icon, window): watcher.status_changed.clear() if icon: - GObject.idle_add(icon.set_tooltip_text, text) + GObject.idle_add(icon.set_tooltip_markup, text) if window: - ur_detected = watcher.has_receiver() - devices = [ watcher.devices[k] for k in watcher.devices ] if ur_detected else [] - GObject.idle_add(ui.window.update, window, ur_detected, devices) + GObject.idle_add(ui.window.update, window, dict(watcher.devices)) # def _pair_new_device(trigger, watcher): # pass -def run(images_path): +def run(): GObject.threads_init() - ui.init(images_path) - ui.notify.start(C.APP_TITLE, ui.image) + ui.notify.start(APP_TITLE) watcher = WatcherThread(ui.notify.show) watcher.start() - window = ui.window.create(C.APP_TITLE, ui.image) + window = ui.window.create(APP_TITLE, watcher.devices[0]) - menu_actions = [('Scan all devices', watcher.request_all_statuses), + menu_actions = [('Scan all devices', watcher.full_scan), # ('Pair new device', _pair_new_device, watcher), None, ('Quit', Gtk.main_quit)] - click_action = (ui.window.toggle, window) if window else None - tray_icon = ui.icon.create(ui.image('icon'), C.APP_TITLE, menu_actions, click_action) + tray_icon = ui.icon.create(APP_TITLE, menu_actions, (ui.window.toggle, window)) ui_update_thread = threading.Thread(target=_status_updated, name='ui_update', args=(watcher, tray_icon, window)) ui_update_thread.daemon = True diff --git a/app/constants.py b/app/constants.py deleted file mode 100644 index 39578bcc..00000000 --- a/app/constants.py +++ /dev/null @@ -1,10 +0,0 @@ -# -# Commonly used strings -# - -APP_TITLE = 'Solaar' -UNIFYING_RECEIVER = 'Unifying Receiver' -NO_DEVICES = 'No devices attached.' -SCANNING = 'Initializing...' -NO_RECEIVER = 'Unifying Receiver not found.' -FOUND_RECEIVER = 'Unifying Receiver found.' diff --git a/app/ui/__init__.py b/app/ui/__init__.py index 73ca10ae..2da03af6 100644 --- a/app/ui/__init__.py +++ b/app/ui/__init__.py @@ -1,26 +1,3 @@ # pass -import os.path as _os_path from . import (icon, notify, window) - - -_images_path = None -_IMAGES = {} - - -def init(images_path=None): - global _images_path - _images_path = images_path - - -def image(name): - if name in _IMAGES: - return _IMAGES[name] - - if _images_path: - path = _os_path.join(_images_path, name + '.png') - if _os_path.isfile(path): - _IMAGES[name] = path - return path - else: - _IMAGES[name] = None diff --git a/app/ui/icon.py b/app/ui/icon.py index 8bf0fcc1..b7218376 100644 --- a/app/ui/icon.py +++ b/app/ui/icon.py @@ -9,8 +9,8 @@ def _show_icon_menu(icon, button, time, menu): menu.popup(None, None, icon.position_menu, icon, button, time) -def create(app_icon, title, menu_actions, click_action=None): - icon = Gtk.StatusIcon.new_from_file(app_icon) +def create(title, menu_actions, click_action=None): + icon = Gtk.StatusIcon.new_from_icon_name(title) icon.set_title(title) icon.set_name(title) @@ -28,12 +28,11 @@ def create(app_icon, title, menu_actions, click_action=None): for action in menu_actions: if action: item = Gtk.MenuItem(action[0]) - function = action[1] args = action[2:] if len(action) > 2 else () - item.connect('activate', function, *args) - menu.append(item) + item.connect('activate', action[1], *args) else: - menu.append(Gtk.SeparatorMenuItem()) + item = Gtk.SeparatorMenuItem() + menu.append(item) menu.show_all() icon.connect('popup_menu', _show_icon_menu, menu) else: diff --git a/app/ui/notify.py b/app/ui/notify.py index 808553d8..8fa43fd2 100644 --- a/app/ui/notify.py +++ b/app/ui/notify.py @@ -7,28 +7,30 @@ try: available = True - _app_title = None - _images = lambda x: None _notifications = {} - def start(app_title, images=None): - global _app_title, _images + def start(app_title): + """Init the notifications system.""" _notify.init(app_title) - _app_title = app_title - _images = images or (lambda x: None) + return True def stop(): - global _app_title - _app_title = None - all(n.close() for n in list(_notifications.values())) - _notify.uninit() + """Stop the notifications system.""" + for n in list(_notifications.values()): + try: + n.close() + except Exception: + # DBUS + pass _notifications.clear() + _notify.uninit() - def show(status, title, text, icon=None): - if not _app_title: + def show(status_code, title, text, icon=None): + """Show a notification with title and text.""" + if not available: return if title in _notifications: @@ -40,11 +42,14 @@ try: # there's no need to show the same notification twice in a row return - path = _images('devices/' + title if icon is None else icon) - icon = ('error' if status < 0 else 'info') if path is None else path + icon = icon or title + notification.update(title, text, title) + try: + notification.show() + except Exception: + # DBUS + pass - notification.update(title, text, icon) - notification.show() except ImportError: import logging @@ -53,4 +58,4 @@ except ImportError: available = False def start(app_title): pass def stop(): pass - def show(status, title, text, icon=None): pass + def show(status_code, title, text, icon=None): pass diff --git a/app/ui/window.py b/app/ui/window.py index e9544e4b..ac18f334 100644 --- a/app/ui/window.py +++ b/app/ui/window.py @@ -4,210 +4,164 @@ from gi.repository import Gtk from gi.repository import Gdk -from gi.repository import GdkPixbuf - -from .. import constants as C -_DEVICE_ICON_SIZE = 48 -_STATUS_ICON_SIZE = 64 +_DEVICE_ICON_SIZE = Gtk.IconSize.DIALOG +_STATUS_ICON_SIZE = Gtk.IconSize.DIALOG _PLACEHOLDER = '~' -_images = None -_MAX_DEVICES = 7 - -_ICONS = {} +_MAX_DEVICES = 6 -def _icon(icon, title, size=_DEVICE_ICON_SIZE, fallback=None): - icon = icon or Gtk.Image() - - if title and title in _ICONS: - icon.set_from_pixbuf(_ICONS[title]) - else: - icon_file = _images(title) if title else None - if icon_file: - pixbuf = GdkPixbuf.Pixbuf().new_from_file(icon_file) - if pixbuf.get_width() > size or pixbuf.get_height() > size: - if pixbuf.get_width() > pixbuf.get_height(): - new_width = size - new_height = size * pixbuf.get_height() / pixbuf.get_width() - else: - new_width = size * pixbuf.get_width() / pixbuf.get_height() - new_height = size - pixbuf = pixbuf.scale_simple(new_width, new_height, GdkPixbuf.InterpType.HYPER) - icon.set_from_pixbuf(pixbuf) - _ICONS[title] = pixbuf - elif fallback: - icon.set_from_icon_name(fallback, size if size < _DEVICE_ICON_SIZE else Gtk.IconSize.DIALOG) - - if size >= _DEVICE_ICON_SIZE: - icon.set_size_request(size, size) - return icon - - -def update(window, ur_available, devices): +def update(window, devices): if not window or not window.get_child(): return + controls = list(window.get_child().get_children()) - first = controls[0] - first.set_visible(not ur_available or not devices) - if ur_available: - ur_status = C.FOUND_RECEIVER if devices else C.NO_DEVICES - else: - ur_status = C.NO_RECEIVER - _, label = first.get_children() - label.set_markup('%s\n%s' % (C.UNIFYING_RECEIVER, ur_status)) + first = controls[0].get_child() + icon, label = first.get_children() + rstatus = devices[0] + label.set_markup('%s\n%s' % (rstatus.name, rstatus.text)) - for index in range(1, _MAX_DEVICES): - box = controls[index] - devstatus = [d for d in devices if d.number == index] - devstatus = devstatus[0] if devstatus else None - box.set_visible(devstatus is not None) + for index in range(1, 1 + _MAX_DEVICES): + devstatus = devices.get(index) + controls[index].set_visible(devstatus is not None) + + box = controls[index].get_child() + icon, expander = box.get_children() if devstatus: - box.set_sensitive(devstatus.code >= 0) - icon, expander = box.get_children() - if not expander.get_data('devstatus'): - expander.set_data('devstatus', devstatus,) - _icon(icon, 'devices/' + devstatus.name, fallback=devstatus.type.lower()) + if icon.get_name() != devstatus.name: + icon.set_name(devstatus.name) + icon.set_from_icon_name(devstatus.name, _DEVICE_ICON_SIZE) - label = expander.get_label_widget() - if expander.get_expanded(): - label.set_markup('%s' % devstatus.name) + if devstatus.code < 0: + expander.set_sensitive(False) + expander.set_expanded(False) + expander.set_label('%s\n%s' % (devstatus.name, devstatus.text)) else: - label.set_markup('%s\n%s' % (devstatus.name, devstatus.props['text'])) + expander.set_sensitive(True) + ebox = expander.get_child() - ebox = expander.get_child() + texts = [] - # refresh_button = ebox.get_children()[0] - # refresh_button.connect('activate', devstatus.refresh) + light_icon = ebox.get_children()[-2] + light_level = getattr(devstatus, 'light_level', None) + light_icon.set_visible(light_level is not None) + if light_level is not None: + texts.append('Light: %d lux' % light_level) + icon_name = 'light_%02d' % (20 * ((light_level + 50) // 100)) + light_icon.set_from_icon_name(icon_name, _STATUS_ICON_SIZE) + light_icon.set_tooltip_text(texts[-1]) - texts = [] + battery_icon = ebox.get_children()[-1] + battery_level = getattr(devstatus, 'battery_level', None) + battery_icon.set_sensitive(battery_level is not None) + if battery_level is None: + battery_icon.set_from_icon_name('battery_unknown', _STATUS_ICON_SIZE) + battery_icon.set_tooltip_text('Battery: unknown') + else: + texts.append('Battery: %d%%' % battery_level) + icon_name = 'battery_%02d' % (20 * ((battery_level + 10) // 20)) + battery_icon.set_from_icon_name(icon_name, _STATUS_ICON_SIZE) + battery_icon.set_tooltip_text(texts[-1]) - battery_icon = ebox.get_children()[-1] - if 'battery-level' in devstatus.props: - level = devstatus.props['battery-level'] - icon_name = 'battery/' + str((level + 10) // 20) - _icon(battery_icon, icon_name, _STATUS_ICON_SIZE) - texts.append('Battery: ' + str(level) + '%') - else: - _icon(battery_icon, 'battery/unknown', _STATUS_ICON_SIZE) - texts.append('Battery: unknown') - battery_icon.set_tooltip_text(texts[-1]) + battery_status = getattr(devstatus, 'battery_status', None) + if battery_status is not None: + texts.append(battery_status) + battery_icon.set_tooltip_text(battery_icon.get_tooltip_text() + '\n' + battery_status) - light_icon = ebox.get_children()[-2] - if 'light-level' in devstatus.props: - lux = devstatus.props['light-level'] - icon_name = 'light/' + str((lux + 50) // 100) - _icon(light_icon, icon_name, _STATUS_ICON_SIZE) - - texts.append('Light: ' + str(lux) + ' lux') - light_icon.set_tooltip_text(texts[-1]) - light_icon.set_visible(True) - else: - light_icon.set_visible(False) - - label = ebox.get_children()[-3] - label.set_text('\n'.join(texts)) - -def _expander_activate(expander): - devstatus = expander.get_data('devstatus') - if devstatus: - label = expander.get_label_widget() - if expander.get_expanded(): - label.set_markup('%s\n%s' % (devstatus.name, devstatus.props['text'])) + if texts: + expander.set_label('%s\n%s' % (devstatus.name, ', '.join(texts))) + else: + expander.set_label('%s\n%s' % (devstatus.name, devstatus.text)) else: - label.set_markup('%s' % devstatus.name) + icon.set_name(_PLACEHOLDER) + expander.set_label(_PLACEHOLDER) def _device_box(title): - icon = _icon(None, 'devices/' + title) + icon = Gtk.Image.new_from_icon_name(title, _DEVICE_ICON_SIZE) icon.set_alignment(0.5, 0) + icon.set_name(title) - label = Gtk.Label() - label.set_markup('%s' % title) - label.set_alignment(0, 0.5) - label.set_can_focus(False) - - box = Gtk.HBox(spacing=10) + box = Gtk.HBox(homogeneous=False, spacing=8) box.pack_start(icon, False, False, 0) + box.set_border_width(8) - if title == C.UNIFYING_RECEIVER: - box.add(label) - else: + if title == _PLACEHOLDER: expander = Gtk.Expander() expander.set_can_focus(False) - expander.set_label_widget(label) - expander.connect('activate', _expander_activate) + expander.set_label(_PLACEHOLDER) + expander.set_use_markup(True) + expander.set_spacing(4) - ebox = Gtk.HBox(False, 10) - ebox.set_border_width(4) + ebox = Gtk.HBox(False, 8) - # refresh_button = Gtk.Button() - # refresh_button.set_image(_icon(None, None, size=Gtk.IconSize.SMALL_TOOLBAR, fallback='reload')) - # refresh_button.set_focus_on_click(False) - # refresh_button.set_can_focus(False) - # refresh_button.set_image_position(Gtk.PositionType.TOP) - # refresh_button.set_alignment(0.5, 0.5) - # refresh_button.set_relief(Gtk.ReliefStyle.NONE) - # refresh_button.set_size_request(20, 20) - # refresh_button.set_tooltip_text('Refresh') - # ebox.pack_start(refresh_button, False, False, 2) - - label = Gtk.Label() - label.set_alignment(0, 0.5) - ebox.pack_start(label, False, True, 8) - - light_icon = _icon(None, 'light/unknown', _STATUS_ICON_SIZE) - ebox.pack_end(light_icon, False, True, 0) - - battery_icon = _icon(None, 'battery/unknown', _STATUS_ICON_SIZE) + battery_icon = Gtk.Image.new_from_icon_name('battery_unknown', _STATUS_ICON_SIZE) ebox.pack_end(battery_icon, False, True, 0) + light_icon = Gtk.Image.new_from_icon_name('light_unknown', _STATUS_ICON_SIZE) + ebox.pack_end(light_icon, False, True, 0) + expander.add(ebox) box.pack_start(expander, True, True, 1) + else: + label = Gtk.Label() + label.set_can_focus(False) + label.set_markup('%s' % title) + label.set_alignment(0, 0) + box.add(label) - box.show_all() - box.set_visible(title != _PLACEHOLDER) - return box + frame = Gtk.Frame() + frame.add(box) + frame.show_all() + frame.set_visible(title != _PLACEHOLDER) + return frame -def create(title, images=None): - global _images - _images = images or (lambda x: None) +def create(title, rstatus): + vbox = Gtk.VBox(homogeneous=False, spacing=4) + vbox.set_border_width(4) - vbox = Gtk.VBox(spacing=8) - vbox.set_border_width(6) - - vbox.add(_device_box(C.UNIFYING_RECEIVER)) - for i in range(1, _MAX_DEVICES): + vbox.add(_device_box(rstatus.name)) + for i in range(1, 1 + _MAX_DEVICES): vbox.add(_device_box(_PLACEHOLDER)) vbox.set_visible(True) - window = Gtk.Window() # Gtk.WindowType.POPUP) + window = Gtk.Window() window.set_title(title) - window.set_icon_from_file(_images('icon')) + window.set_icon_name(title) window.set_keep_above(True) - window.set_decorated(False) - window.set_skip_taskbar_hint(True) - window.set_skip_pager_hint(True) + # window.set_skip_taskbar_hint(True) + # window.set_skip_pager_hint(True) window.set_deletable(False) window.set_resizable(False) window.set_position(Gtk.WindowPosition.MOUSE) - window.set_type_hint(Gdk.WindowTypeHint.TOOLTIP) - window.connect('focus-out-event', _hide) + window.set_type_hint(Gdk.WindowTypeHint.UTILITY) + window.set_wmclass(title, 'status-window') + window.set_role('status-window') + + window.connect('window-state-event', _state_event) + window.connect('delete-event', lambda w, e: toggle(None, window) or True) window.add(vbox) + window.present() return window -def _hide(window, _): - window.set_visible(False) - +def _state_event(window, event): + if event.new_window_state & Gdk.WindowState.ICONIFIED: + position = window.get_position() + window.hide() + window.deiconify() + window.move(*position) + return True def toggle(_, window): if window.get_visible(): - window.set_visible(False) + position = window.get_position() + window.hide() + window.move(*position) else: window.present() diff --git a/app/watcher.py b/app/watcher.py index 3b058496..1d78c162 100644 --- a/app/watcher.py +++ b/app/watcher.py @@ -6,25 +6,31 @@ import logging import threading import time -import constants as C from logitech.unifying_receiver import api from logitech.unifying_receiver.listener import EventsListener from logitech import devices -_STATUS_TIMEOUT = 97 # seconds -_THREAD_SLEEP = 7 # seconds -_FORGET_TIMEOUT = 5 * 60 # seconds +_STATUS_TIMEOUT = 34 # seconds +_THREAD_SLEEP = 5 # seconds + + +_UNIFYING_RECEIVER = 'Unifying Receiver' +_NO_DEVICES = 'No devices attached.' +_SCANNING = 'Initializing...' +_NO_RECEIVER = 'not found' +_FOUND_RECEIVER = 'found' class _DevStatus(api.AttachedDeviceInfo): timestamp = time.time() - code = devices.STATUS.CONNECTED - props = {devices.PROPS.TEXT: devices.STATUS_NAME[devices.STATUS.CONNECTED]} + code = devices.STATUS.UNKNOWN + text = '' refresh = None class WatcherThread(threading.Thread): + """Keeps a map of all attached devices and their statuses.""" def __init__(self, notify_callback=None): super(WatcherThread, self).__init__(name='WatcherThread') self.daemon = True @@ -35,39 +41,46 @@ class WatcherThread(threading.Thread): self.status_changed = threading.Event() self.listener = None - self.devices = {} + + self.rstatus = _DevStatus(0, _UNIFYING_RECEIVER, _UNIFYING_RECEIVER, None, None) + self.rstatus.refresh = self.full_scan + self.devices = {0: self.rstatus} def run(self): self.active = True - self._notify(0, C.UNIFYING_RECEIVER, C.SCANNING) + self._notify(0, _UNIFYING_RECEIVER, _SCANNING) while self.active: if self.listener is None: receiver = api.open() if receiver: - self._notify(1, C.UNIFYING_RECEIVER, C.FOUND_RECEIVER) + self._device_status_changed(self.rstatus, (devices.STATUS.CONNECTED, _FOUND_RECEIVER)) + for devinfo in api.list_devices(receiver): - devstatus = _DevStatus(*devinfo) - self.devices[devinfo.number] = devstatus - self._notify(devices.STATUS.CONNECTED, devstatus.name, devices.STATUS_NAME[devices.STATUS.CONNECTED]) + self._new_device(devinfo) + + if len(self.devices) == 1: + self._device_status_changed(self.rstatus, (devices.STATUS.CONNECTED, _NO_DEVICES)) + + self._update_status_text() + self.listener = EventsListener(receiver, self._events_callback) self.listener.start() - self._update_status() else: - self._notify(-1, C.UNIFYING_RECEIVER, C.NO_RECEIVER) + self._device_status_changed(self.rstatus, (devices.STATUS.UNAVAILABLE, _NO_RECEIVER)) elif not self.listener.active: self.listener = None - self._notify(-1, C.UNIFYING_RECEIVER, C.NO_RECEIVER) - self.devices.clear() + self._device_status_changed(self.rstatus, (devices.STATUS.UNAVAILABLE, _NO_RECEIVER)) + self.devices = {0: self.rstatus} if self.active: update_icon = True - if self.listener and self.devices: + if self.listener and len(self.devices) > 1: update_icon &= self._check_old_statuses() if self.active: if update_icon: - self._update_status() + self._update_status_text() time.sleep(_THREAD_SLEEP) def stop(self): @@ -76,49 +89,55 @@ class WatcherThread(threading.Thread): self.listener.stop() api.close(self.listener.receiver) - def has_receiver(self): - return self.listener is not None and self.listener.active - - def request_all_statuses(self, _=None): + def full_scan(self, _=None): updated = False - for d in range(1, 7): - devstatus = self.devices.get(d) + for devnumber in range(1, 1 + api.C.MAX_ATTACHED_DEVICES): + devstatus = self.devices.get(devnumber) if devstatus: status = devices.request_status(devstatus, self.listener) updated |= self._device_status_changed(devstatus, status) else: - devstatus = self._new_device(d) + devstatus = self._new_device(devnumber) updated |= devstatus is not None if updated: - self._update_status() + self._update_status_text() + + def _request_status(self, devstatus): + if devstatus: + status = devices.request_status(devstatus, self.listener) + self._device_status_changed(devstatus, status) def _check_old_statuses(self): updated = False for devstatus in list(self.devices.values()): - if time.time() - devstatus.timestamp > _STATUS_TIMEOUT: - status = devices.ping(devstatus, self.listener) - updated |= self._device_status_changed(devstatus, status) + if devstatus != self.rstatus: + if time.time() - devstatus.timestamp > _STATUS_TIMEOUT: + status = devices.ping(devstatus, self.listener) + updated |= self._device_status_changed(devstatus, status) return updated - def _new_device(self, device): - devinfo = api.get_device_info(self.listener.receiver, device) - if devinfo: - devstatus = _DevStatus(*devinfo) - self.devices[device] = devstatus - self._notify(devstatus.code, devstatus.name, devstatus.props[devices.PROPS.TEXT]) - return devinfo + def _new_device(self, dev): + if type(dev) == int: + dev = api.get_device_info(self.listener.receiver, dev) + logging.debug("new devstatus from %s", dev) + if dev: + devstatus = _DevStatus(*dev) + devstatus.refresh = self._request_status + self.devices[dev.number] = devstatus + self._device_status_changed(devstatus, devices.STATUS.CONNECTED) + return devstatus - def _events_callback(self, code, device, data): - logging.debug("%s: event %02x %d %s", time.asctime(), code, device, repr(data)) + def _events_callback(self, code, devnumber, data): + logging.debug("%s: event %02x %d %s", time.asctime(), code, devnumber, repr(data)) updated = False - if device in self.devices: - devstatus = self.devices[device] + if devnumber in self.devices: + devstatus = self.devices[devnumber] if code == 0x10 and data[0] == 'b\x8F': updated = True self._device_status_changed(devstatus, devices.STATUS.UNAVAILABLE) @@ -127,17 +146,16 @@ class WatcherThread(threading.Thread): updated |= self._device_status_changed(devstatus, status) else: logging.warn("unknown event code %02x", code) - elif device: - logging.debug("got event (%d, %d, %s) for new device", code, device, repr(data)) - self._new_device(device) + elif devnumber: + self._new_device(devnumber) updated = True else: - logging.warn("don't know how to handle event (%d, %d, %s)", code, device, data) + logging.warn("don't know how to handle event (%d, %d, %s)", code, devnumber, data) if updated: - self._update_status() + self._update_status_text() - def _device_status_changed(self, devstatus, status): + def _device_status_changed(self, devstatus, status=None): if status is None: return False @@ -147,15 +165,19 @@ class WatcherThread(threading.Thread): if type(status) == int: devstatus.code = status if devstatus.code in devices.STATUS_NAME: - devstatus.props[devices.PROPS.TEXT] = devices.STATUS_NAME[devstatus.code] + devstatus.text = devices.STATUS_NAME[devstatus.code] else: devstatus.code = status[0] - devstatus.props.update(status[1]) + if isinstance(status[1], str): + devstatus.text = status[1] + elif isinstance(status[1], dict): + for key, value in status[1].items(): + setattr(devstatus, key, value) if old_status_code != devstatus.code: - logging.debug("%s: device status changed %s => %s: %s", time.asctime(), old_status_code, devstatus.code, devstatus.props) - # if not (devstatus.code == 0 and old_status_code > 0): - self._notify(devstatus.code, devstatus.name, devstatus.props[devices.PROPS.TEXT]) + logging.debug("%s: device '%s' status changed %s => %s: %s", time.asctime(), devstatus.name, old_status_code, devstatus.code, devstatus.text) + if devstatus.code // 256 != old_status_code // 256: + self._notify(devstatus.code, devstatus.name, devstatus.text) return True @@ -163,38 +185,30 @@ class WatcherThread(threading.Thread): if self.notify: self.notify(*args) - def notify_full(self): - if self.listener and self.listener.active: - if self.devices: - for devstatus in self.devices.values(): - self._notify(0, devstatus.name, devstatus.props[devices.PROPS.TEXT]) - else: - self._notify(0, C.UNIFYING_RECEIVER, C.NO_DEVICES) - else: - self._notify(-1, C.UNIFYING_RECEIVER, C.NO_RECEIVER) - - def _update_status(self): + def _update_status_text(self): last_status_text = self.status_text - if self.listener and self.listener.active: - if self.devices: - all_statuses = [] - for d in self.devices: - devstatus = self.devices[d] - status_text = devstatus.props[devices.PROPS.TEXT] - if status_text: - if ' ' in status_text: - all_statuses.append(devstatus.name) - all_statuses.append(' ' + status_text) - else: - all_statuses.append(devstatus.name + ' ' + status_text) - else: - all_statuses.append(devstatus.name) - self.status_text = '\n'.join(all_statuses) - else: - self.status_text = C.NO_DEVICES + if self.rstatus.code < 0: + self.status_text = '' + self.rstatus.name + ': ' + self.rstatus.text else: - self.status_text = C.NO_RECEIVER + all_statuses = [] + for devnumber in range(1, 1 + api.C.MAX_ATTACHED_DEVICES): + if devnumber in self.devices: + devstatus = self.devices[devnumber] + if devstatus.text: + if ' ' in devstatus.text: + all_statuses.append('' + devstatus.name + '') + all_statuses.append(' ' + devstatus.text) + else: + all_statuses.append('' + devstatus.name + ': ' + devstatus.text) + else: + all_statuses.append('' + devstatus.name + '') + all_statuses.append('') + + if all_statuses: + self.status_text = '\n'.join(all_statuses).rstrip('\n') + else: + self.status_text = '' + self.rstatus.name + ': ' + _NO_DEVICES if self.status_text != last_status_text: self.status_changed.set() diff --git a/images/devices/Unifying Receiver.png b/images/devices/Unifying Receiver.png deleted file mode 100644 index 15fae366..00000000 Binary files a/images/devices/Unifying Receiver.png and /dev/null differ diff --git a/images/devices/Wireless Solar Keyboard K750.png b/images/devices/Wireless Solar Keyboard K750.png deleted file mode 100644 index 2f746bbb..00000000 Binary files a/images/devices/Wireless Solar Keyboard K750.png and /dev/null differ diff --git a/images/icon.png b/images/icon.png deleted file mode 100644 index 314ed231..00000000 Binary files a/images/icon.png and /dev/null differ diff --git a/images/light/0.png b/images/light/0.png deleted file mode 100644 index fb967968..00000000 Binary files a/images/light/0.png and /dev/null differ diff --git a/images/light/unknown.png b/images/light/unknown.png deleted file mode 100644 index c0c091b6..00000000 Binary files a/images/light/unknown.png and /dev/null differ diff --git a/lib/logitech/devices/constants.py b/lib/logitech/devices/constants.py index 1696c6b7..2d9da9dc 100644 --- a/lib/logitech/devices/constants.py +++ b/lib/logitech/devices/constants.py @@ -4,27 +4,21 @@ STATUS = type('STATUS', (), dict( - UNKNOWN=None, + UNKNOWN=-9999, UNAVAILABLE=-1, CONNECTED=0, - # ACTIVE=1, )) +STATUS_NAME = { + STATUS.UNAVAILABLE: 'disconnected?', + STATUS.CONNECTED: 'connected', + } + + PROPS = type('PROPS', (), dict( TEXT='text', - BATTERY_LEVEL='battery-level', - BATTERY_STATUS='battery-status', - LIGHT_LUX='lux', - LIGHT_LEVEL='light-level', + BATTERY_LEVEL='battery_level', + BATTERY_STATUS='battery_status', + LIGHT_LEVEL='light_level', )) - - -from collections import defaultdict - -STATUS_NAME = defaultdict(lambda x: None) -STATUS_NAME[STATUS.UNAVAILABLE] = 'disconnected' -STATUS_NAME[STATUS.CONNECTED] = 'connected' -# STATUS_NAME[STATUS.ACTIVE] = 'active' - -del defaultdict diff --git a/lib/logitech/devices/k750.py b/lib/logitech/devices/k750.py index b6c6f590..f3eeec3f 100644 --- a/lib/logitech/devices/k750.py +++ b/lib/logitech/devices/k750.py @@ -14,8 +14,6 @@ from . import constants as C NAME = 'Wireless Solar Keyboard K750' -_CHARGE_LIMITS = (75, 40, 20, 10, -1) - # # # @@ -26,23 +24,24 @@ def _trigger_solar_charge_events(receiver, devinfo): features_array=devinfo.features) -def _charge_status(data): +def _charge_status(data, hasLux=False): charge, lux = _unpack('!BH', data[2:5]) d = {} - for i in range(0, len(_CHARGE_LIMITS)): - if charge >= _CHARGE_LIMITS[i]: + _CHARGE_LEVELS = (10, 25, 256) + for i in range(0, len(_CHARGE_LEVELS)): + if charge < _CHARGE_LEVELS[i]: charge_index = i break - else: - charge_index = 0 d[C.PROPS.BATTERY_LEVEL] = charge text = 'Battery %d%%' % charge - if lux > 0: + if hasLux: d[C.PROPS.LIGHT_LEVEL] = lux text = 'Light: %d lux' % lux + ', ' + text + else: + d[C.PROPS.LIGHT_LEVEL] = None d[C.PROPS.TEXT] = text return 0x10 << charge_index, d @@ -61,7 +60,7 @@ def process_event(devinfo, listener, data): if data[:2] == b'\x09\x10' and data[7:11] == b'GOOD': # regular solar charge events - return _charge_status(data) + return _charge_status(data, True) if data[:2] == b'\x09\x20' and data[7:11] == b'GOOD': logging.debug("Solar key pressed") diff --git a/lib/logitech/unifying_receiver/api.py b/lib/logitech/unifying_receiver/api.py index d615bd48..408c0df1 100644 --- a/lib/logitech/unifying_receiver/api.py +++ b/lib/logitech/unifying_receiver/api.py @@ -42,7 +42,7 @@ def open(): close = _base.close -def request(handle, device, feature, function=b'\x00', params=b'', features_array=None): +def request(handle, devnumber, feature, function=b'\x00', params=b'', features_array=None): """Makes a feature call to the device, and returns the reply data. Basically a write() followed by (possibly multiple) reads, until a reply @@ -65,22 +65,22 @@ def request(handle, device, feature, function=b'\x00', params=b'', features_arra feature_index = b'\x00' else: if features_array is None: - features_array = get_device_features(handle, device) + features_array = get_device_features(handle, devnumber) if features_array is None: - _l.log(_LOG_LEVEL, "(%d,%d) no features array available", handle, device) + _l.log(_LOG_LEVEL, "(%d,%d) no features array available", handle, devnumber) return None if feature in features_array: feature_index = _pack('!B', features_array.index(feature)) if feature_index is None: - _l.warn("(%d,%d) feature <%s:%s> not supported", handle, device, _hexlify(feature), C.FEATURE_NAME[feature]) - raise E.FeatureNotSupported(device, feature) + _l.warn("(%d,%d) feature <%s:%s> not supported", handle, devnumber, _hexlify(feature), C.FEATURE_NAME[feature]) + raise E.FeatureNotSupported(devnumber, feature) - return _base.request(handle, device, feature_index + function, params) + return _base.request(handle, devnumber, feature_index + function, params) -def ping(handle, device): - """Pings a device number to check if it is attached to the UR. +def ping(handle, devnumber): + """Pings a device to check if it is attached to the UR. :returns: True if the device is connected to the UR, False if the device is not attached, None if no conclusive reply is received. @@ -92,39 +92,39 @@ def ping(handle, device): if not reply: return None - reply_code, reply_device, reply_data = reply + reply_code, reply_devnumber, reply_data = reply - if reply_device != device: + if reply_devnumber != devnumber: # oops - _l.log(_LOG_LEVEL, "(%d,%d) ping: reply for another device %d: %s", handle, device, reply_device, _hexlify(reply_data)) - _unhandled._publish(reply_code, reply_device, reply_data) + _l.log(_LOG_LEVEL, "(%d,%d) ping: reply for another device %d: %s", handle, devnumber, reply_devnumber, _hexlify(reply_data)) + _unhandled._publish(reply_code, reply_devnumber, reply_data) return _status(_base.read(handle)) if (reply_code == 0x11 and reply_data[:2] == b'\x00\x10' and reply_data[4:5] == ping_marker): # ping ok - _l.log(_LOG_LEVEL, "(%d,%d) ping: ok [%s]", handle, device, _hexlify(reply_data)) + _l.log(_LOG_LEVEL, "(%d,%d) ping: ok [%s]", handle, devnumber, _hexlify(reply_data)) return True if (reply_code == 0x10 and reply_data[:2] == b'\x8F\x00'): # ping failed - _l.log(_LOG_LEVEL, "(%d,%d) ping: device not present", handle, device) + _l.log(_LOG_LEVEL, "(%d,%d) ping: device not present", handle, devnumber) return False if (reply_code == 0x11 and reply_data[:2] == b'\x09\x00' and len(reply_data) == 18 and reply_data[7:11] == b'GOOD'): # some devices may reply with a SOLAR_CHARGE event before the # ping_ok reply, especially right after the device connected to the # receiver - _l.log(_LOG_LEVEL, "(%d,%d) ping: solar status [%s]", handle, device, _hexlify(reply_data)) - _unhandled._publish(reply_code, reply_device, reply_data) + _l.log(_LOG_LEVEL, "(%d,%d) ping: solar status [%s]", handle, devnumber, _hexlify(reply_data)) + _unhandled._publish(reply_code, reply_devnumber, reply_data) return _status(_base.read(handle)) # ugh - _l.log(_LOG_LEVEL, "(%d,%d) ping: unknown reply for this device: %d=[%s]", handle, device, reply_code, _hexlify(reply_data)) - _unhandled._publish(reply_code, reply_device, reply_data) + _l.log(_LOG_LEVEL, "(%d,%d) ping: unknown reply for this device: %d=[%s]", handle, devnumber, reply_code, _hexlify(reply_data)) + _unhandled._publish(reply_code, reply_devnumber, reply_data) return None - _l.log(_LOG_LEVEL, "(%d,%d) pinging", handle, device) - _base.write(handle, device, b'\x00\x10\x00\x00' + ping_marker) + _l.log(_LOG_LEVEL, "(%d,%d) pinging", handle, devnumber) + _base.write(handle, devnumber, b'\x00\x10\x00\x00' + ping_marker) # pings may take a while to reply success return _status(_base.read(handle, _base.DEFAULT_TIMEOUT * 3)) @@ -136,12 +136,12 @@ def find_device_by_name(handle, device_name): """ _l.log(_LOG_LEVEL, "(%d,) searching for device '%s'", handle, device_name) - for device in range(1, 1 + _base.MAX_ATTACHED_DEVICES): - features_array = get_device_features(handle, device) + for devnumber in range(1, 1 + C.MAX_ATTACHED_DEVICES): + features_array = get_device_features(handle, devnumber) if features_array: - d_name = get_device_name(handle, device, features_array) + d_name = get_device_name(handle, devnumber, features_array) if d_name == device_name: - return get_device_info(handle, device, device_name=d_name, features_array=features_array) + return get_device_info(handle, devnumber, device_name=d_name, features_array=features_array) def list_devices(handle): @@ -153,7 +153,7 @@ def list_devices(handle): devices = [] - for device in range(1, 1 + _base.MAX_ATTACHED_DEVICES): + for device in range(1, 1 + C.MAX_ATTACHED_DEVICES): features_array = get_device_features(handle, device) if features_array: devices.append(get_device_info(handle, device, features_array=features_array)) @@ -161,67 +161,67 @@ def list_devices(handle): return devices -def get_device_info(handle, device, device_name=None, features_array=None): +def get_device_info(handle, devnumber, device_name=None, features_array=None): """Gets the complete info for a device (type, name, firmwares, and features_array). :returns: an AttachedDeviceInfo tuple, or ``None``. """ if features_array is None: - features_array = get_device_features(handle, device) + features_array = get_device_features(handle, devnumber) if features_array is None: return None - d_type = get_device_type(handle, device, features_array) - d_name = get_device_name(handle, device, features_array) if device_name is None else device_name - d_firmware = get_device_firmware(handle, device, features_array) - devinfo = AttachedDeviceInfo(device, d_type, d_name, d_firmware, features_array) - _l.log(_LOG_LEVEL, "(%d,%d) found device %s", handle, device, devinfo) + d_type = get_device_type(handle, devnumber, features_array) + d_name = get_device_name(handle, devnumber, features_array) if device_name is None else device_name + d_firmware = get_device_firmware(handle, devnumber, features_array) + devinfo = AttachedDeviceInfo(devnumber, d_type, d_name, d_firmware, features_array) + _l.log(_LOG_LEVEL, "(%d,%d) found device %s", handle, devnumber, devinfo) return devinfo -def get_feature_index(handle, device, feature): +def get_feature_index(handle, devnumber, feature): """Reads the index of a device's feature. :returns: An int, or ``None`` if the feature is not available. """ - _l.log(_LOG_LEVEL, "(%d,%d) get feature index <%s:%s>", handle, device, _hexlify(feature), C.FEATURE_NAME[feature]) + _l.log(_LOG_LEVEL, "(%d,%d) get feature index <%s:%s>", handle, devnumber, _hexlify(feature), C.FEATURE_NAME[feature]) if len(feature) != 2: raise ValueError("invalid feature <%s>: it must be a two-byte string" % feature) # FEATURE.ROOT should always be available for any attached devices - reply = _base.request(handle, device, C.FEATURE.ROOT, feature) + reply = _base.request(handle, devnumber, C.FEATURE.ROOT, feature) if reply: # only consider active and supported features feature_index = ord(reply[0:1]) if feature_index: feature_flags = ord(reply[1:2]) & 0xE0 - _l.log(_LOG_LEVEL, "(%d,%d) feature <%s:%s> has index %d flags %02x", handle, device, _hexlify(feature), C.FEATURE_NAME[feature], feature_index, feature_flags) + _l.log(_LOG_LEVEL, "(%d,%d) feature <%s:%s> has index %d flags %02x", handle, devnumber, _hexlify(feature), C.FEATURE_NAME[feature], feature_index, feature_flags) if feature_flags == 0: return feature_index if feature_flags & 0x80: - _l.warn("(%d,%d) feature <%s:%s> not supported: obsolete", handle, device, _hexlify(feature), C.FEATURE_NAME[feature]) + _l.warn("(%d,%d) feature <%s:%s> not supported: obsolete", handle, devnumber, _hexlify(feature), C.FEATURE_NAME[feature]) if feature_flags & 0x40: - _l.warn("(%d,%d) feature <%s:%s> not supported: hidden", handle, device, _hexlify(feature), C.FEATURE_NAME[feature]) + _l.warn("(%d,%d) feature <%s:%s> not supported: hidden", handle, devnumber, _hexlify(feature), C.FEATURE_NAME[feature]) if feature_flags & 0x20: - _l.warn("(%d,%d) feature <%s:%s> not supported: Logitech internal", handle, device, _hexlify(feature), C.FEATURE_NAME[feature]) - raise E.FeatureNotSupported(device, feature) + _l.warn("(%d,%d) feature <%s:%s> not supported: Logitech internal", handle, devnumber, _hexlify(feature), C.FEATURE_NAME[feature]) + raise E.FeatureNotSupported(devnumber, feature) else: - _l.warn("(%d,%d) feature <%s:%s> not supported by the device", handle, device, _hexlify(feature), C.FEATURE_NAME[feature]) - raise E.FeatureNotSupported(device, feature) + _l.warn("(%d,%d) feature <%s:%s> not supported by the device", handle, devnumber, _hexlify(feature), C.FEATURE_NAME[feature]) + raise E.FeatureNotSupported(devnumber, feature) -def get_device_features(handle, device): +def get_device_features(handle, devnumber): """Returns an array of feature ids. Their position in the array is the index to be used when requesting that feature on the device. """ - _l.log(_LOG_LEVEL, "(%d,%d) get device features", handle, device) + _l.log(_LOG_LEVEL, "(%d,%d) get device features", handle, devnumber) # get the index of the FEATURE_SET # FEATURE.ROOT should always be available for all devices - fs_index = _base.request(handle, device, C.FEATURE.ROOT, C.FEATURE.FEATURE_SET) + fs_index = _base.request(handle, devnumber, C.FEATURE.ROOT, C.FEATURE.FEATURE_SET) if fs_index is None: # _l.warn("(%d,%d) FEATURE_SET not available", handle, device) return None @@ -231,24 +231,24 @@ def get_device_features(handle, device): # even if unknown. # get the number of active features the device has - features_count = _base.request(handle, device, fs_index + b'\x00') + features_count = _base.request(handle, devnumber, fs_index + b'\x00') if not features_count: # this can happen if the device disappeard since the fs_index request # otherwise we should get at least a count of 1 (the FEATURE_SET we've just used above) - _l.log(_LOG_LEVEL, "(%d,%d) no features available?!", handle, device) + _l.log(_LOG_LEVEL, "(%d,%d) no features available?!", handle, devnumber) return None features_count = ord(features_count[:1]) - _l.log(_LOG_LEVEL, "(%d,%d) found %d features", handle, device, features_count) + _l.log(_LOG_LEVEL, "(%d,%d) found %d features", handle, devnumber, features_count) features = [None] * 0x20 for index in range(1, 1 + features_count): # for each index, get the feature residing at that index - feature = _base.request(handle, device, fs_index + b'\x10', _pack('!B', index)) + feature = _base.request(handle, devnumber, fs_index + b'\x10', _pack('!B', index)) if feature: feature = feature[0:2].upper() features[index] = feature - _l.log(_LOG_LEVEL, "(%d,%d) feature <%s:%s> at index %d", handle, device, _hexlify(feature), C.FEATURE_NAME[feature], index) + _l.log(_LOG_LEVEL, "(%d,%d) feature <%s:%s> at index %d", handle, devnumber, _hexlify(feature), C.FEATURE_NAME[feature], index) features[0] = C.FEATURE.ROOT while features[-1] is None: @@ -256,7 +256,7 @@ def get_device_features(handle, device): return features -def get_device_firmware(handle, device, features_array=None): +def get_device_firmware(handle, devnumber, features_array=None): """Reads a device's firmware info. :returns: a list of FirmwareInfo tuples, ordered by firmware layer. @@ -264,14 +264,14 @@ def get_device_firmware(handle, device, features_array=None): def _makeFirmwareInfo(level, type, name=None, version=None, build=None, extras=None): return FirmwareInfo(level, type, name, version, build, extras) - fw_count = request(handle, device, C.FEATURE.FIRMWARE, features_array=features_array) + fw_count = request(handle, devnumber, C.FEATURE.FIRMWARE, features_array=features_array) if fw_count: fw_count = ord(fw_count[:1]) fw = [] for index in range(0, fw_count): index = _pack('!B', index) - fw_info = request(handle, device, C.FEATURE.FIRMWARE, function=b'\x10', params=index, features_array=features_array) + fw_info = request(handle, devnumber, C.FEATURE.FIRMWARE, function=b'\x10', params=index, features_array=features_array) if fw_info: fw_level = ord(fw_info[:1]) & 0x0F if fw_level == 0 or fw_level == 1: @@ -295,67 +295,67 @@ def get_device_firmware(handle, device, features_array=None): fw_info = _makeFirmwareInfo(level=fw_level, type=C.FIRMWARE_TYPE[-1]) fw.append(fw_info) - _l.log(_LOG_LEVEL, "(%d:%d) firmware %s", handle, device, fw_info) + _l.log(_LOG_LEVEL, "(%d:%d) firmware %s", handle, devnumber, fw_info) return fw -def get_device_type(handle, device, features_array=None): +def get_device_type(handle, devnumber, features_array=None): """Reads a device's type. :see DEVICE_TYPE: :returns: a string describing the device type, or ``None`` if the device is not available or does not support the ``NAME`` feature. """ - d_type = request(handle, device, C.FEATURE.NAME, function=b'\x20', features_array=features_array) + d_type = request(handle, devnumber, C.FEATURE.NAME, function=b'\x20', features_array=features_array) if d_type: d_type = ord(d_type[:1]) - _l.log(_LOG_LEVEL, "(%d,%d) device type %d = %s", handle, device, d_type, C.DEVICE_TYPE[d_type]) + _l.log(_LOG_LEVEL, "(%d,%d) device type %d = %s", handle, devnumber, d_type, C.DEVICE_TYPE[d_type]) return C.DEVICE_TYPE[d_type] -def get_device_name(handle, device, features_array=None): +def get_device_name(handle, devnumber, features_array=None): """Reads a device's name. :returns: a string with the device name, or ``None`` if the device is not available or does not support the ``NAME`` feature. """ - name_length = request(handle, device, C.FEATURE.NAME, features_array=features_array) + name_length = request(handle, devnumber, C.FEATURE.NAME, features_array=features_array) if name_length: name_length = ord(name_length[:1]) d_name = b'' while len(d_name) < name_length: name_index = _pack('!B', len(d_name)) - name_fragment = request(handle, device, C.FEATURE.NAME, function=b'\x10', params=name_index, features_array=features_array) + name_fragment = request(handle, devnumber, C.FEATURE.NAME, function=b'\x10', params=name_index, features_array=features_array) name_fragment = name_fragment[:name_length - len(d_name)] d_name += name_fragment d_name = d_name.decode('ascii') - _l.log(_LOG_LEVEL, "(%d,%d) device name %s", handle, device, d_name) + _l.log(_LOG_LEVEL, "(%d,%d) device name %s", handle, devnumber, d_name) return d_name -def get_device_battery_level(handle, device, features_array=None): +def get_device_battery_level(handle, devnumber, features_array=None): """Reads a device's battery level. :raises FeatureNotSupported: if the device does not support this feature. """ - battery = request(handle, device, C.FEATURE.BATTERY, features_array=features_array) + battery = request(handle, devnumber, C.FEATURE.BATTERY, features_array=features_array) if battery: discharge, dischargeNext, status = _unpack('!BBB', battery[:3]) _l.log(_LOG_LEVEL, "(%d:%d) battery %d%% charged, next level %d%% charge, status %d = %s", discharge, dischargeNext, status, C.BATTERY_STATUSE[status]) return (discharge, dischargeNext, C.BATTERY_STATUS[status]) -def get_device_keys(handle, device, features_array=None): - count = request(handle, device, C.FEATURE.REPROGRAMMABLE_KEYS, features_array=features_array) +def get_device_keys(handle, devnumber, features_array=None): + count = request(handle, devnumber, C.FEATURE.REPROGRAMMABLE_KEYS, features_array=features_array) if count: keys = [] count = ord(count[:1]) for index in range(0, count): keyindex = _pack('!B', index) - keydata = request(handle, device, C.FEATURE.REPROGRAMMABLE_KEYS, function=b'\x10', params=keyindex, features_array=features_array) + keydata = request(handle, devnumber, C.FEATURE.REPROGRAMMABLE_KEYS, function=b'\x10', params=keyindex, features_array=features_array) if keydata: key, key_task, flags = _unpack('!HHB', keydata[:5]) keys.append(ReprogrammableKeyInfo(index, key, C.KEY_NAME[key], key_task, C.KEY_NAME[key_task], flags)) diff --git a/lib/logitech/unifying_receiver/base.py b/lib/logitech/unifying_receiver/base.py index 6b57c5db..1bad01d0 100644 --- a/lib/logitech/unifying_receiver/base.py +++ b/lib/logitech/unifying_receiver/base.py @@ -41,10 +41,6 @@ _MAX_REPLY_SIZE = _MAX_CALL_SIZE """Default timeout on read (in ms).""" DEFAULT_TIMEOUT = 1000 - -"""Maximum number of devices attached to a UR.""" -MAX_ATTACHED_DEVICES = 6 - # # # @@ -57,7 +53,7 @@ def list_receiver_devices(): def try_open(path): - """Checks if the given device path points to the right UR device. + """Checks if the given Linux device path points to the right UR device. :param path: the Linux device path. @@ -128,11 +124,11 @@ def close(handle): # return _write(handle, device, data) -def write(handle, device, data): +def write(handle, devnumber, data): """Writes some data to a certain device. :param handle: an open UR handle. - :param device: attached device number. + :param devnumber: attached device number. :param data: data to send, up to 5 bytes. The first two (required) bytes of data must be the feature index for the @@ -146,16 +142,16 @@ def write(handle, device, data): data += b'\x00' * (_MIN_CALL_SIZE - 2 - len(data)) elif len(data) > _MIN_CALL_SIZE - 2: data += b'\x00' * (_MAX_CALL_SIZE - 2 - len(data)) - wdata = _pack('!BB', 0x10, device) + data + wdata = _pack('!BB', 0x10, devnumber) + data - _l.log(_LOG_LEVEL, "(%d,%d) <= w[%s]", handle, device, _hexlify(wdata)) + _l.log(_LOG_LEVEL, "(%d,%d) <= w[%s]", handle, devnumber, _hexlify(wdata)) if len(wdata) < _MIN_CALL_SIZE: - _l.warn("(%d:%d) <= w[%s] call packet too short: %d bytes", handle, device, _hexlify(wdata), len(wdata)) + _l.warn("(%d:%d) <= w[%s] call packet too short: %d bytes", handle, devnumber, _hexlify(wdata), len(wdata)) if len(wdata) > _MAX_CALL_SIZE: - _l.warn("(%d:%d) <= w[%s] call packet too long: %d bytes", handle, device, _hexlify(wdata), len(wdata)) + _l.warn("(%d:%d) <= w[%s] call packet too long: %d bytes", handle, devnumber, _hexlify(wdata), len(wdata)) if not _hid.write(handle, wdata): - _l.warn("(%d,%d) write failed, assuming receiver no longer available", handle, device) + _l.warn("(%d,%d) write failed, assuming receiver no longer available", handle, devnumber) close(handle) raise E.NoReceiver @@ -168,9 +164,9 @@ def read(handle, timeout=DEFAULT_TIMEOUT): :param timeout: read timeout on the UR handle. If any data was read in the given timeout, returns a tuple of - (reply_code, device, message data). The reply code should be ``0x11`` for a - successful feature call, or ``0x10`` to indicate some error, e.g. the device - is no longer available. + (reply_code, devnumber, message data). The reply code is generally ``0x11`` + for a successful feature call, or ``0x10`` to indicate some error, e.g. the + device is no longer available. :raises NoReceiver: if the receiver is no longer available, i.e. has been physically removed from the machine, or the kernel driver has been @@ -189,21 +185,21 @@ def read(handle, timeout=DEFAULT_TIMEOUT): if len(data) > _MAX_REPLY_SIZE: _l.warn("(%d,*) => r[%s] read packet too long: %d bytes", handle, _hexlify(data), len(data)) code = ord(data[:1]) - device = ord(data[1:2]) - return code, device, data[2:] + devnumber = ord(data[1:2]) + return code, devnumber, data[2:] _l.log(_LOG_LEVEL, "(%d,*) => r[]", handle) -def request(handle, device, feature_index_function, params=b'', features_array=None): - """Makes a feature call device and waits for a matching reply. +def request(handle, devnumber, feature_index_function, params=b'', features_array=None): + """Makes a feature call to a device and waits for a matching reply. This function will skip all incoming messages and events not related to the device we're requesting for, or the feature specified in the initial request; it will also wait for a matching reply indefinitely. :param handle: an open UR handle. - :param device: attached device number. + :param devnumber: attached device number. :param feature_index_function: a two-byte string of (feature_index, feature_function). :param params: parameters for the feature call, 3 to 16 bytes. :param features_array: optional features array for the device, only used to @@ -212,13 +208,13 @@ def request(handle, device, feature_index_function, params=b'', features_array=N available. :raisees FeatureCallError: if the feature call replied with an error. """ - _l.log(_LOG_LEVEL, "(%d,%d) request {%s} params [%s]", handle, device, _hexlify(feature_index_function), _hexlify(params)) + _l.log(_LOG_LEVEL, "(%d,%d) request {%s} params [%s]", handle, devnumber, _hexlify(feature_index_function), _hexlify(params)) if len(feature_index_function) != 2: raise ValueError('invalid feature_index_function {%s}: it must be a two-byte string' % _hexlify(feature_index_function)) retries = 5 - write(handle, device, feature_index_function + params) + write(handle, devnumber, feature_index_function + params) while retries > 0: reply = read(handle) retries -= 1 @@ -227,39 +223,39 @@ def request(handle, device, feature_index_function, params=b'', features_array=N # keep waiting... continue - reply_code, reply_device, reply_data = reply + reply_code, reply_devnumber, reply_data = reply - if reply_device != device: + if reply_devnumber != devnumber: # this message not for the device we're interested in - _l.log(_LOG_LEVEL, "(%d,%d) request got reply for unexpected device %d: [%s]", handle, device, reply_device, _hexlify(reply_data)) + _l.log(_LOG_LEVEL, "(%d,%d) request got reply for unexpected device %d: [%s]", handle, devnumber, reply_devnumber, _hexlify(reply_data)) # worst case scenario, this is a reply for a concurrent request # on this receiver - _unhandled._publish(reply_code, reply_device, reply_data) + _unhandled._publish(reply_code, reply_devnumber, reply_data) continue if reply_code == 0x10 and reply_data[:1] == b'\x8F' and reply_data[1:2] == feature_index_function: # device not present - _l.log(_LOG_LEVEL, "(%d,%d) request ping failed on {%s} call: [%s]", handle, device, _hexlify(feature_index_function), _hexlify(reply_data)) + _l.log(_LOG_LEVEL, "(%d,%d) request ping failed on {%s} call: [%s]", handle, devnumber, _hexlify(feature_index_function), _hexlify(reply_data)) return None if reply_code == 0x10 and reply_data[:1] == b'\x8F': # device not present - _l.log(_LOG_LEVEL, "(%d,%d) request ping failed: [%s]", handle, device, _hexlify(reply_data)) + _l.log(_LOG_LEVEL, "(%d,%d) request ping failed: [%s]", handle, devnumber, _hexlify(reply_data)) return None if reply_code == 0x11 and reply_data[0] == b'\xFF' and reply_data[1:3] == feature_index_function: # an error returned from the device error_code = ord(reply_data[3]) - _l.warn("(%d,%d) request feature call error %d = %s: %s", handle, device, error_code, C.ERROR_NAME[error_code], _hexlify(reply_data)) + _l.warn("(%d,%d) request feature call error %d = %s: %s", handle, devnumber, error_code, C.ERROR_NAME[error_code], _hexlify(reply_data)) feature_index = ord(feature_index_function[:1]) feature_function = feature_index_function[1:2] feature = None if features_array is None else features_array[feature_index] - raise E.FeatureCallError(device, feature, feature_index, feature_function, error_code, reply_data) + raise E.FeatureCallError(devnumber, feature, feature_index, feature_function, error_code, reply_data) if reply_code == 0x11 and reply_data[:2] == feature_index_function: # a matching reply - _l.log(_LOG_LEVEL, "(%d,%d) matched reply with feature-index-function [%s]", handle, device, _hexlify(reply_data[2:])) + _l.log(_LOG_LEVEL, "(%d,%d) matched reply with feature-index-function [%s]", handle, devnumber, _hexlify(reply_data[2:])) return reply_data[2:] - _l.log(_LOG_LEVEL, "(%d,%d) unmatched reply {%s} (expected {%s})", handle, device, _hexlify(reply_data[:2]), _hexlify(feature_index_function)) - _unhandled._publish(reply_code, reply_device, reply_data) + _l.log(_LOG_LEVEL, "(%d,%d) unmatched reply {%s} (expected {%s})", handle, devnumber, _hexlify(reply_data[:2]), _hexlify(feature_index_function)) + _unhandled._publish(reply_code, reply_devnumber, reply_data) diff --git a/lib/logitech/unifying_receiver/constants.py b/lib/logitech/unifying_receiver/constants.py index a217a905..7429a1cb 100644 --- a/lib/logitech/unifying_receiver/constants.py +++ b/lib/logitech/unifying_receiver/constants.py @@ -97,5 +97,9 @@ _ERROR_NAMES = ('Ok', 'Unknown', 'Invalid argument', 'Out of range', ERROR_NAME = FallbackDict(lambda x: 'Unknown error', list2dict(_ERROR_NAMES)) +"""Maximum number of devices that can be attached to a single receiver.""" +MAX_ATTACHED_DEVICES = 6 + + del FallbackDict del list2dict diff --git a/lib/logitech/unifying_receiver/exceptions.py b/lib/logitech/unifying_receiver/exceptions.py index 77e71469..452cd5a2 100644 --- a/lib/logitech/unifying_receiver/exceptions.py +++ b/lib/logitech/unifying_receiver/exceptions.py @@ -15,18 +15,18 @@ class NoReceiver(Exception): class FeatureNotSupported(Exception): """Raised when trying to request a feature not supported by the device.""" - def __init__(self, device, feature): - super(FeatureNotSupported, self).__init__(device, feature, C.FEATURE_NAME[feature]) - self.device = device + def __init__(self, devnumber, feature): + super(FeatureNotSupported, self).__init__(devnumber, feature, C.FEATURE_NAME[feature]) + self.devnumber = devnumber self.feature = feature self.feature_name = C.FEATURE_NAME[feature] class FeatureCallError(Exception): """Raised if the device replied to a feature call with an error.""" - def __init__(self, device, feature, feature_index, feature_function, error_code, data=None): - super(FeatureCallError, self).__init__(device, feature, feature_index, feature_function, error_code, C.ERROR_NAME[error_code]) - self.device = device + def __init__(self, devnumber, feature, feature_index, feature_function, error_code, data=None): + super(FeatureCallError, self).__init__(devnumber, feature, feature_index, feature_function, error_code, C.ERROR_NAME[error_code]) + self.devnumber = devnumber self.feature = feature self.feature_name = None if feature is None else C.FEATURE_NAME[feature] self.feature_index = feature_index diff --git a/lib/logitech/unifying_receiver/listener.py b/lib/logitech/unifying_receiver/listener.py index 23eabf39..6e066258 100644 --- a/lib/logitech/unifying_receiver/listener.py +++ b/lib/logitech/unifying_receiver/listener.py @@ -23,14 +23,14 @@ _IDLE_SLEEP = 950 # ms class EventsListener(threading.Thread): """Listener thread for events from the Unifying Receiver. - Incoming events (code, device, data) will be delivered to the callback - function. The callback is called in the listener thread, so it should return - as fast as possible. + Incoming events (reply_code, devnumber, data) will be passed to the callback + function. The callback is called in the listener thread, so for best results + it should return as fast as possible. While this listener is running, you should use the request() method to make - regular UR API calls, otherwise the replies will be captured by the listener - and delivered as events to the callback. As an exception, you can make UR - API calls in the events callback. + regular UR API calls, otherwise the replies may be captured by the listener + and delivered as events to the callback. As an exception, you can make API + calls in the events callback. """ def __init__(self, receiver, events_callback): super(EventsListener, self).__init__(name='Unifying_Receiver_Listener_' + hex(receiver)) diff --git a/lib/logitech/unifying_receiver/tests/test_30_base.py b/lib/logitech/unifying_receiver/tests/test_30_base.py index 4d5c282c..3185098d 100644 --- a/lib/logitech/unifying_receiver/tests/test_30_base.py +++ b/lib/logitech/unifying_receiver/tests/test_30_base.py @@ -80,7 +80,7 @@ class Test_UR_Base(unittest.TestCase): devices = [] - for device in range(1, 1 + base.MAX_ATTACHED_DEVICES): + for device in range(1, 1 + MAX_ATTACHED_DEVICES): w = base.write(self.handle, device, b'\x00\x10\x00\x00\xAA') self.assertIsNone(w, "write should have returned None") reply = base.read(self.handle, base.DEFAULT_TIMEOUT * 3) diff --git a/lib/logitech/unifying_receiver/tests/test_50_api.py b/lib/logitech/unifying_receiver/tests/test_50_api.py index 8b5e347b..24eb61fc 100644 --- a/lib/logitech/unifying_receiver/tests/test_50_api.py +++ b/lib/logitech/unifying_receiver/tests/test_50_api.py @@ -45,7 +45,7 @@ class Test_UR_API(unittest.TestCase): devices = [] - for device in range(1, 1 + api._base.MAX_ATTACHED_DEVICES): + for device in range(1, 1 + MAX_ATTACHED_DEVICES): ok = api.ping(self.handle, device) self.assertIsNotNone(ok, "invalid ping reply") if ok: diff --git a/lib/logitech/unifying_receiver/unhandled.py b/lib/logitech/unifying_receiver/unhandled.py index 0b5b151e..2facbbad 100644 --- a/lib/logitech/unifying_receiver/unhandled.py +++ b/lib/logitech/unifying_receiver/unhandled.py @@ -7,16 +7,16 @@ import logging from binascii import hexlify as _hexlify -def _logdebug_hook(reply_code, device, data): +def _logdebug_hook(reply_code, devnumber, data): """Default unhandled hook, logs the reply as DEBUG.""" _l = logging.getLogger('logitech.unifying_receiver.unhandled') - _l.debug("UNHANDLED (,%d) code 0x%02x data [%s]", device, reply_code, _hexlify(data)) + _l.debug("UNHANDLED (,%d) code 0x%02x data [%s]", devnumber, reply_code, _hexlify(data)) """The function that will be called on unhandled incoming events. The hook must be a function with the signature: ``_(int, int, str)``, where -the parameters are: (reply code, device number, data). +the parameters are: (reply_code, devnumber, data). This hook will only be called by the request() function, when it receives replies that do not match the requested feature call. As such, it is not @@ -30,7 +30,7 @@ The default implementation logs the unhandled reply as DEBUG. hook = _logdebug_hook -def _publish(reply_code, device, data): +def _publish(reply_code, devnumber, data): """Delivers a reply to the unhandled hook, if any.""" if hook is not None: - hook.__call__(reply_code, device, data) + hook.__call__(reply_code, devnumber, data) diff --git a/resources/README b/resources/README new file mode 100644 index 00000000..3b7e38ae --- /dev/null +++ b/resources/README @@ -0,0 +1,3 @@ +Battery and weather icons from the Oxygen icon theme. +Lightbulb icon from the GNOME icon theme. +Unifying receiver and Wireless Keyboard K750 icons from Logitech web pages. diff --git a/resources/icons/Solaar.png b/resources/icons/Solaar.png new file mode 100644 index 00000000..45312e3d Binary files /dev/null and b/resources/icons/Solaar.png differ diff --git a/resources/icons/Unifying Receiver.png b/resources/icons/Unifying Receiver.png new file mode 100644 index 00000000..45312e3d Binary files /dev/null and b/resources/icons/Unifying Receiver.png differ diff --git a/resources/icons/Wireless Solar Keyboard K750.png b/resources/icons/Wireless Solar Keyboard K750.png new file mode 100644 index 00000000..952d376c Binary files /dev/null and b/resources/icons/Wireless Solar Keyboard K750.png differ diff --git a/images/battery/0.png b/resources/icons/battery_00.png similarity index 100% rename from images/battery/0.png rename to resources/icons/battery_00.png diff --git a/images/battery/5.png b/resources/icons/battery_100.png similarity index 100% rename from images/battery/5.png rename to resources/icons/battery_100.png diff --git a/images/battery/1.png b/resources/icons/battery_20.png similarity index 100% rename from images/battery/1.png rename to resources/icons/battery_20.png diff --git a/images/battery/2.png b/resources/icons/battery_40.png similarity index 100% rename from images/battery/2.png rename to resources/icons/battery_40.png diff --git a/images/battery/3.png b/resources/icons/battery_60.png similarity index 100% rename from images/battery/3.png rename to resources/icons/battery_60.png diff --git a/images/battery/4.png b/resources/icons/battery_80.png similarity index 100% rename from images/battery/4.png rename to resources/icons/battery_80.png diff --git a/images/battery/unknown.png b/resources/icons/battery_unknown.png similarity index 100% rename from images/battery/unknown.png rename to resources/icons/battery_unknown.png diff --git a/resources/icons/light_00.png b/resources/icons/light_00.png new file mode 100644 index 00000000..58f20dcc Binary files /dev/null and b/resources/icons/light_00.png differ diff --git a/images/light/5.png b/resources/icons/light_100.png similarity index 100% rename from images/light/5.png rename to resources/icons/light_100.png diff --git a/images/light/1.png b/resources/icons/light_20.png similarity index 100% rename from images/light/1.png rename to resources/icons/light_20.png diff --git a/images/light/2.png b/resources/icons/light_40.png similarity index 100% rename from images/light/2.png rename to resources/icons/light_40.png diff --git a/images/light/3.png b/resources/icons/light_60.png similarity index 100% rename from images/light/3.png rename to resources/icons/light_60.png diff --git a/images/light/4.png b/resources/icons/light_80.png similarity index 100% rename from images/light/4.png rename to resources/icons/light_80.png diff --git a/resources/icons/light_unknown.png b/resources/icons/light_unknown.png new file mode 100644 index 00000000..77d0db90 Binary files /dev/null and b/resources/icons/light_unknown.png differ diff --git a/solaar b/solaar index 2d96e6fb..6c32b727 100755 --- a/solaar +++ b/solaar @@ -4,6 +4,7 @@ cd `dirname "$0"` export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$PWD/lib export PYTHONPATH=$PWD:$PWD/lib +export XDG_DATA_DIRS=$PWD/resources:$XDG_DATA_DIRS exec python -OO solaar.py "$@" -#exec python -OO -m profile -o $TMPDIR/profile.log solaar.py "$@" +# exec python -OO -m profile -o $TMPDIR/profile.log solaar.py "$@" diff --git a/solaar.py b/solaar.py index 51d86ca9..2ed7aeab 100644 --- a/solaar.py +++ b/solaar.py @@ -7,7 +7,6 @@ __version__ = '0.4' # import logging -import os.path if __name__ == '__main__': @@ -20,8 +19,5 @@ if __name__ == '__main__': log_level = logging.root.level - 10 * args.verbose logging.basicConfig(level=log_level if log_level > 0 else 1) - images_path = os.path.join(__file__, '..', 'images') - images_path = os.path.abspath(os.path.normpath(images_path)) - import app - app.run(images_path) + app.run()