reworked the receiver and devices into classes

This commit is contained in:
Daniel Pavel 2012-10-18 13:40:37 +03:00
parent 5985105e0e
commit f2dac70131
20 changed files with 805 additions and 624 deletions

4
README
View File

@ -17,10 +17,10 @@ Requirements
------------
- Python (2.7 or 3.2).
- Gtk 3 (preferred), though Gtk 2 should work with minor problems.
- Gtk 3; Gtk 2 should partially work with some problems.
- Python GI (GObject Introspection), for Gtk bindings.
- Optional libnotify GI bindings, for desktop notifications.
- A hidapi native implementation (see the INSTALL file for details).
- Optional python-notify2 for desktop notifications.
Thanks

288
app/receiver.py Normal file
View File

@ -0,0 +1,288 @@
#
#
#
from logging import getLogger as _Logger
_LOG_LEVEL = 6
from threading import Event as _Event
from logitech.unifying_receiver import base as _base
from logitech.unifying_receiver import api as _api
from logitech.unifying_receiver import listener as _listener
from logitech import devices as _devices
from logitech.devices.constants import (STATUS, STATUS_NAME, PROPS, NAMES)
#
#
#
class DeviceInfo(object):
"""A device attached to the receiver.
"""
def __init__(self, receiver, number, status=STATUS.UNKNOWN):
self.LOG = _Logger("Device-%d" % number)
self.receiver = receiver
self.number = number
self._name = None
self._kind = None
self._firmware = None
self._features = None
self._status = status
self.props = {}
@property
def handle(self):
return self.receiver.handle
@property
def status(self):
return self._status
@status.setter
def status(self, new_status):
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)
urgent = new_status < STATUS.CONNECTED or self._status < STATUS.CONNECTED
self._status = new_status
self.receiver._device_changed(self, urgent)
@property
def status_text(self):
if self._status < STATUS.CONNECTED:
return STATUS_NAME[self._status]
t = []
if self.props.get(PROPS.BATTERY_LEVEL):
t.append('Battery: %d%%' % self.props[PROPS.BATTERY_LEVEL])
if self.props.get(PROPS.BATTERY_STATUS):
t.append(self.props[PROPS.BATTERY_STATUS])
if self.props.get(PROPS.LIGHT_LEVEL):
t.append('Light: %d lux' % self.props[PROPS.LIGHT_LEVEL])
return ', '.join(t) if t else STATUS_NAME[STATUS.CONNECTED]
@property
def name(self):
if self._name is None:
if self._status >= STATUS.CONNECTED:
self._name = self.receiver.request(_api.get_device_name, self.number, self.features)
return self._name or '?'
@property
def device_name(self):
return self.name
@property
def kind(self):
if self._kind is None:
if self._status >= STATUS.CONNECTED:
self._kind = self.receiver.request(_api.get_device_kind, self.number, self.features)
return self._kind or '?'
@property
def firmware(self):
if self._firmware is None:
if self._status >= STATUS.CONNECTED:
self._firmware = self.receiver.request(_api.get_device_firmware, self.number, self.features)
return self._firmware or ()
@property
def features(self):
if self._features is None:
if self._status >= STATUS.CONNECTED:
self._features = self.receiver.request(_api.get_device_features, self.number)
return self._features or ()
def ping(self):
return self.receiver.request(_api.ping, self.number)
def process_event(self, code, data):
if code == 0x10 and data[:1] == b'\x8F':
self.status = STATUS.UNAVAILABLE
return True
if code == 0x11:
status = _devices.process_event(self, data, self.receiver)
if status:
if type(status) == int:
self.status = status
return True
if type(status) == tuple:
p = dict(self.props)
self.props.update(status[1])
if self.status == status[0]:
if p != self.props:
self.receiver._device_changed(self)
else:
self.status = status[0]
return True
self.LOG.warn("don't know how to handle status %s", status)
return False
def __hash__(self):
return self.number
def __str__(self):
return 'DeviceInfo(%d,%s,%d)' % (self.number, self.name, self._status)
def __repr__(self):
return '<DeviceInfo(number=%d,name=%s,status=%d)>' % (self.number, self.name, self._status)
#
#
#
class Receiver(_listener.EventsListener):
"""Keeps the status of a Unifying Receiver.
"""
NAME = kind = 'Unifying Receiver'
max_devices = _api.MAX_ATTACHED_DEVICES
def __init__(self, path, handle):
super(Receiver, self).__init__(handle, self._events_handler)
self.path = path
self._status = STATUS.BOOTING
self.status_changed = _Event()
self.status_changed.urgent = False
self.status_changed.reason = None
self.LOG = _Logger("Receiver-%s" % path)
self.LOG.info("initializing")
self.devices = {}
self.events_handler = None
init = (_base.request(handle, 0xFF, b'\x81\x00') and
_base.request(handle, 0xFF, b'\x80\x00', b'\x00\x01') and
_base.request(handle, 0xFF, b'\x81\x02'))
if init:
self.LOG.info("initialized")
else:
self.LOG.warn("initialization failed")
if _base.request(handle, 0xFF, b'\x80\x02', b'\x02'):
self.LOG.info("triggered device events")
else:
self.LOG.warn("failed to trigger device events")
def close(self):
"""Closes the receiver's handle.
The receiver can no longer be used in API calls after this.
"""
self.LOG.info("closing")
self.stop()
@property
def status(self):
return self._status
@status.setter
def status(self, new_status):
if new_status != self._status:
self.LOG.debug("status %d => %d", self._status, new_status)
self._status = new_status
self.status_changed.reason = self
self.status_changed.urgent = True
self.status_changed.set()
@property
def status_text(self):
status = self._status
if status == STATUS.UNKNOWN:
return 'Initializing...'
if status == STATUS.UNAVAILABLE:
return 'Receiver not found.'
if status == STATUS.BOOTING:
return 'Scanning...'
if status == STATUS.CONNECTED:
return 'No devices found.'
if len(self.devices) > 1:
return '%d devices found' % len(self.devices)
return '1 device found'
@property
def device_name(self):
return self.NAME
def _device_changed(self, dev, urgent=False):
self.status_changed.reason = dev
self.status_changed.urgent = urgent
self.status_changed.set()
def _events_handler(self, event):
if event.code == 0x10 and event.data[0:2] == b'\x41\x04':
state_code = ord(event.data[2:3]) & 0xF0
state = STATUS.UNAVAILABLE if state_code == 0x60 else \
STATUS.CONNECTED if state_code == 0xA0 else \
STATUS.CONNECTED if state_code == 0x20 else \
None
if state is None:
self.LOG.warn("don't know how to handle status 0x%02x: %s", state_code, event)
return
if event.devnumber in self.devices:
self.devices[event.devnumber].status = state
return
if event.devnumber < 1 or event.devnumber > self.max_devices:
self.LOG.warn("got event for invalid device number %d: %s", event.devnumber, event)
return
dev = DeviceInfo(self, event.devnumber, state)
if state == STATUS.CONNECTED:
n, k = dev.name, dev.kind
else:
# we can query the receiver for the device short name
dev_id = self.request(_base.request, 0xFF, b'\x83\xB5', event.data[4:5])
if dev_id:
shortname = str(dev_id[2:].rstrip(b'\x00'))
if shortname in NAMES:
dev._name, dev._kind = NAMES[shortname]
else:
self.LOG.warn("could not properly detect inactive device %d: %s", event.devnumber, shortname)
self.devices[event.devnumber] = dev
self.LOG.info("new device ready %s", dev)
self.status = STATUS.CONNECTED + len(self.devices)
return
if event.devnumber == 0xFF:
if event.code == 0xFF and event.data is None:
# receiver disconnected
self.LOG.info("disconnected")
self.devices = {}
self.status = STATUS.UNAVAILABLE
return
self.LOG.warn("don't know how to handle event %s", event)
elif event.devnumber in self.devices:
dev = self.devices[event.devnumber]
if dev.process_event(event.code, event.data):
return
if self.events_handler:
self.events_handler(event)
def __str__(self):
return 'Receiver(%s,%x,%d:%d)' % (self.path, self._handle, self._active, self._status)
@classmethod
def open(self):
"""Opens the first Logitech Unifying Receiver found attached to the machine.
:returns: An open file handle for the found receiver, or ``None``.
"""
for rawdevice in _base.list_receiver_devices():
_Logger("receiver").log(_LOG_LEVEL, "checking %s", rawdevice)
handle = _base.try_open(rawdevice.path)
if handle:
receiver = Receiver(rawdevice.path, handle)
receiver.start()
return receiver
return None

View File

@ -1,105 +1,91 @@
#!/usr/bin/env python
__version__ = '0.4'
__version__ = '0.5'
#
#
#
import logging
# from gi import pygtkcompat
# pygtkcompat.enable_gtk()
from gi.repository import (Gtk, GObject)
from logitech.devices import constants as C
import ui
APP_TITLE = 'Solaar'
def _status_check(watcher, tray_icon, window):
last_text = None
while True:
watcher.status_changed.wait()
watcher.status_changed.clear()
if watcher.devices:
lines = []
if watcher.rstatus.code < C.STATUS.CONNECTED:
lines += (watcher.rstatus.text, '')
devstatuses = [watcher.devices[d] for d in range(1, 1 + watcher.rstatus.max_devices) if d in watcher.devices]
for devstatus in devstatuses:
if devstatus.text:
if ' ' in devstatus.text:
lines += ('<b>' + devstatus.name + '</b>', ' ' + devstatus.text)
else:
lines.append('<b>' + devstatus.name + '</b> ' + devstatus.text)
else:
lines.append('<b>' + devstatus.name + '</b>')
lines.append('')
text = '\n'.join(lines).rstrip('\n')
else:
text = watcher.rstatus.text
if text != last_text:
last_text = text
icon_name = APP_TITLE + '-fail' if watcher.rstatus.code < C.STATUS.CONNECTED else APP_TITLE
if tray_icon:
GObject.idle_add(ui.icon.update, tray_icon, watcher.rstatus, text, icon_name)
if window:
GObject.idle_add(ui.window.update, window, watcher.rstatus, watcher.devices, icon_name)
if __name__ == '__main__':
import argparse
arg_parser = argparse.ArgumentParser(prog=APP_TITLE)
arg_parser.add_argument('-v', '--verbose', action='count', default=0,
arg_parser.add_argument('-v', '--verbose',
action='count', default=0,
help='increase the logger verbosity (may be repeated)')
arg_parser.add_argument('-S', '--no-systray', action='store_false', dest='systray',
arg_parser.add_argument('-S', '--no-systray',
action='store_false',
dest='systray',
help='don\'t embed the application window into the systray')
arg_parser.add_argument('-N', '--no-notifications', action='store_false', dest='notifications',
arg_parser.add_argument('-N', '--no-notifications',
action='store_false',
dest='notifications',
help='disable desktop notifications (shown only when in systray)')
arg_parser.add_argument('-V', '--version', action='version', version='%(prog)s ' + __version__)
arg_parser.add_argument('-V', '--version',
action='version',
version='%(prog)s ' + __version__)
args = arg_parser.parse_args()
import logging
log_level = logging.root.level - 10 * args.verbose
log_format='%(asctime)s %(levelname)8s [%(threadName)s] %(name)s: %(message)s'
logging.basicConfig(level=log_level if log_level > 0 else 1, format=log_format)
log_format='%(asctime)s.%(msecs)03d %(levelname)8s [%(threadName)s] %(name)s: %(message)s'
logging.basicConfig(level=log_level if log_level > 0 else 1, format=log_format, datefmt='%H:%M:%S')
from gi.repository import GObject
GObject.threads_init()
import ui
args.notifications &= args.systray
if args.notifications:
ui.notify.init(APP_TITLE)
args.notifications &= ui.notify.init(APP_TITLE)
from watcher import Watcher
watcher = Watcher(APP_TITLE, ui.notify.show if args.notifications else None)
watcher.start()
import watcher
tray_icon = None
window = ui.window.create(APP_TITLE,
watcher.DUMMY.NAME,
watcher.DUMMY.max_devices,
args.systray)
window.set_icon_name(APP_TITLE + '-init')
window = ui.window.create(APP_TITLE, watcher.rstatus, args.systray)
window.set_icon_name(APP_TITLE + '-fail')
def _ui_update(receiver, tray_icon, window):
icon_name = APP_TITLE + '-fail' if receiver.status < 1 else APP_TITLE
if window:
GObject.idle_add(ui.window.update, window, receiver, icon_name)
if tray_icon:
GObject.idle_add(ui.icon.update, tray_icon, receiver, icon_name)
def _notify(device):
GObject.idle_add(ui.notify.show, device)
w = watcher.Watcher(APP_TITLE,
lambda r: _ui_update(r, tray_icon, window),
_notify if args.notifications else None)
w.start()
if args.systray:
tray_icon = ui.icon.create(APP_TITLE, (ui.window.toggle, window))
tray_icon.set_from_icon_name(APP_TITLE + '-fail')
def _toggle_notifications(item):
# logging.debug("toggle notifications %s", item)
if ui.notify.available:
if item.get_active():
ui.notify.init(APP_TITLE)
else:
ui.notify.uninit()
item.set_sensitive(ui.notify.available)
menu = (
('Notifications', _toggle_notifications if args.notifications else None, args.notifications),
)
tray_icon = ui.icon.create(APP_TITLE, (ui.window.toggle, window), menu)
tray_icon.set_from_icon_name(APP_TITLE + '-init')
else:
tray_icon = None
window.present()
from threading import Thread
status_check = Thread(group=APP_TITLE, name='StatusCheck', target=_status_check, args=(watcher, tray_icon, window))
status_check.daemon = True
status_check.start()
from gi.repository import Gtk
Gtk.main()
watcher.stop()
ui.notify.set_active(False)
w.stop()
ui.notify.uninit()

View File

@ -5,7 +5,7 @@
from gi.repository import Gtk
def create(title, click_action=None):
def create(title, click_action=None, actions=None):
icon = Gtk.StatusIcon()
icon.set_title(title)
icon.set_name(title)
@ -19,9 +19,30 @@ def create(title, click_action=None):
icon.connect('activate', click_action)
menu = Gtk.Menu()
item = Gtk.MenuItem('Quit')
item.connect('activate', Gtk.main_quit)
menu.append(item)
if actions:
for name, activate, checked in actions:
if checked is None:
item = Gtk.MenuItem(name)
if activate is None:
item.set_sensitive(False)
else:
item.connect('activate', activate)
else:
item = Gtk.CheckMenuItem(name)
if activate is None:
item.set_sensitive(False)
else:
item.set_active(checked or False)
item.connect('toggled', activate)
menu.append(item)
menu.append(Gtk.SeparatorMenuItem())
quit_item = Gtk.MenuItem('Quit')
quit_item.connect('activate', Gtk.main_quit)
menu.append(quit_item)
menu.show_all()
icon.connect('popup_menu',
@ -32,8 +53,27 @@ def create(title, click_action=None):
return icon
def update(icon, receiver, tooltip=None, icon_name=None):
if tooltip is not None:
icon.set_tooltip_markup(tooltip)
def update(icon, receiver, icon_name=None):
if icon_name is not None:
icon.set_from_icon_name(icon_name)
if receiver.devices:
lines = []
if receiver.status < 1:
lines += (receiver.status_text, '')
devlist = [receiver.devices[d] for d in range(1, 1 + receiver.max_devices) if d in receiver.devices]
for dev in devlist:
name = '<b>' + dev.name + '</b>'
if dev.status < 1:
lines.append(name + ' (' + dev.status_text + ')')
else:
lines.append(name)
if dev.status > 1:
lines.append(' ' + dev.status_text)
lines.append('')
text = '\n'.join(lines).rstrip('\n')
icon.set_tooltip_markup(text)
else:
icon.set_tooltip_text(receiver.status_text)

View File

@ -6,83 +6,72 @@ import logging
try:
import notify2 as _notify
from time import time as timestamp
from gi.repository import Notify
from gi.repository import Gtk
available = True # assumed to be working since the import succeeded
_active = False # not yet active
_app_title = None
from logitech.devices.constants import STATUS
# necessary because the notifications daemon does not know about our XDG_DATA_DIRS
theme = Gtk.IconTheme.get_default()
_icons = {}
def _icon(title):
if title not in _icons:
icon = theme.lookup_icon(title, 0, 0)
_icons[title] = icon.get_filename() if icon else None
return _icons.get(title)
# assumed to be working since the import succeeded
available = True
_TIMEOUT = 5 * 60 # after this many seconds assume the notification object is no longer valid
_notifications = {}
def init(app_title, active=True):
def init(app_title=None):
"""Init the notifications system."""
global _app_title
_app_title = app_title
return set_active(active)
def set_active(active=True):
global available, _active
global available
if available:
if active:
if not _active:
try:
_notify.init(_app_title)
_active = True
except:
logging.exception("initializing desktop notifications")
available = False
else:
if _active:
for n in _notifications.values():
try:
n.close()
except:
logging.exception("closing notification %s", n)
try:
_notify.uninit()
except:
logging.exception("stopping desktop notifications")
available = False
_active = False
return _active
logging.info("starting desktop notifications")
if not Notify.is_initted():
try:
return Notify.init(app_title or Notify.get_app_name())
except:
logging.exception("initializing desktop notifications")
available = False
return available and Notify.is_initted()
def active():
return _active
def uninit():
if available and Notify.is_initted():
logging.info("stopping desktop notifications")
_notifications.clear()
Notify.uninit()
def show(status_code, title, text='', icon=None):
def show(dev):
"""Show a notification with title and text."""
if available and _active:
n = None
if title in _notifications:
n = _notifications[title]
if timestamp() - n.timestamp > _TIMEOUT:
del _notifications[title]
n = None
if available and Notify.is_initted():
summary = dev.device_name
# if a notification with same name is already visible, reuse it to avoid spamming
n = _notifications.get(summary)
if n is None:
n = _notify.Notification(title)
_notifications[title] = n
n = _notifications[summary] = Notify.Notification()
n.update(summary, dev.status_text, _icon(summary) or dev.kind)
urgency = Notify.Urgency.LOW if dev.status > STATUS.CONNECTED else Notify.Urgency.NORMAL
n.set_urgency(urgency)
n.update(title, text, icon or title)
n.timestamp = timestamp()
try:
# logging.debug("showing notification %s", n)
# logging.debug("showing %s", n)
n.show()
except Exception:
logging.exception("showing notification %s", n)
logging.exception("showing %s", n)
except ImportError:
logging.warn("python-notify2 not found, desktop notifications are disabled")
logging.warn("Notify not found in gi.repository, desktop notifications are disabled")
available = False
active = False
def init(app_title, active=True): return False
def active(): return False
def set_active(active=True): return False
def show(status_code, title, text, icon=None): pass
init = lambda app_title: False
uninit = lambda: None
show = lambda status_code, title, text: None

View File

@ -4,7 +4,7 @@
from gi.repository import (Gtk, Gdk)
from logitech.devices import constants as C
from logitech.devices.constants import (STATUS, PROPS)
_SMALL_DEVICE_ICON_SIZE = Gtk.IconSize.BUTTON
@ -40,14 +40,14 @@ def _find_children(container, *child_names):
return result if count > 1 else result[0]
def _update_receiver_box(box, rstatus):
def _update_receiver_box(box, receiver):
label, buttons = _find_children(box, 'label', 'buttons')
label.set_text(rstatus.text or '')
buttons.set_visible(rstatus.code >= C.STATUS.CONNECTED)
label.set_text(receiver.status_text or '')
buttons.set_visible(receiver.status >= STATUS.CONNECTED)
def _update_device_box(frame, devstatus):
if devstatus is None:
def _update_device_box(frame, dev):
if dev is None:
frame.set_visible(False)
frame.set_name(_PLACEHOLDER)
return
@ -55,19 +55,19 @@ def _update_device_box(frame, devstatus):
icon, label = _find_children(frame, 'icon', 'label')
frame.set_visible(True)
if frame.get_name() != devstatus.name:
frame.set_name(devstatus.name)
if theme.has_icon(devstatus.name):
icon.set_from_icon_name(devstatus.name, _DEVICE_ICON_SIZE)
if frame.get_name() != dev.name:
frame.set_name(dev.name)
if theme.has_icon(dev.name):
icon.set_from_icon_name(dev.name, _DEVICE_ICON_SIZE)
else:
icon.set_from_icon_name(devstatus.type.lower(), _DEVICE_ICON_SIZE)
icon.set_tooltip_text(devstatus.name)
label.set_markup('<b>' + devstatus.name + '</b>')
icon.set_from_icon_name(dev.kind, _DEVICE_ICON_SIZE)
icon.set_tooltip_text(dev.name)
label.set_markup('<b>' + dev.name + '</b>')
status = _find_children(frame, 'status')
if devstatus.code < C.STATUS.CONNECTED:
if dev.status < STATUS.CONNECTED:
icon.set_sensitive(False)
icon.set_tooltip_text(devstatus.text)
icon.set_tooltip_text(dev.status_text)
label.set_sensitive(False)
status.set_visible(False)
return
@ -79,7 +79,7 @@ def _update_device_box(frame, devstatus):
status_icons = status.get_children()
battery_icon, battery_label = status_icons[0:2]
battery_level = getattr(devstatus, C.PROPS.BATTERY_LEVEL, None)
battery_level = dev.props.get(PROPS.BATTERY_LEVEL)
if battery_level is None:
battery_icon.set_sensitive(False)
battery_icon.set_from_icon_name('battery_unknown', _STATUS_ICON_SIZE)
@ -92,14 +92,14 @@ def _update_device_box(frame, devstatus):
battery_label.set_sensitive(True)
battery_label.set_text('%d%%' % battery_level)
battery_status = getattr(devstatus, C.PROPS.BATTERY_STATUS, None)
battery_status = dev.props.get(PROPS.BATTERY_STATUS)
if battery_status is None:
battery_icon.set_tooltip_text('')
else:
battery_icon.set_tooltip_text(battery_status)
light_icon, light_label = status_icons[2:4]
light_level = getattr(devstatus, C.PROPS.LIGHT_LEVEL, None)
light_level = dev.props.get(PROPS.LIGHT_LEVEL)
if light_level is None:
light_icon.set_visible(False)
light_label.set_visible(False)
@ -111,24 +111,30 @@ def _update_device_box(frame, devstatus):
light_label.set_text('%d lux' % light_level)
def update(window, rstatus, devices, icon_name=None):
def update(window, receiver, icon_name=None):
if window and window.get_child():
if icon_name is not None:
window.set_icon_name(icon_name)
vbox = window.get_child()
controls = list(vbox.get_children())
_update_receiver_box(controls[0], rstatus)
_update_receiver_box(controls[0], receiver)
for index in range(1, len(controls)):
_update_device_box(controls[index], devices.get(index))
dev = receiver.devices[index] if index in receiver.devices else None
_update_device_box(controls[index], dev)
#
#
#
def _receiver_box(rstatus):
def _receiver_box(name):
box = _device_box(False, False)
icon, status_box = _find_children(box, 'icon', 'status')
icon.set_from_icon_name(rstatus.name, _SMALL_DEVICE_ICON_SIZE)
icon.set_tooltip_text(rstatus.name)
icon.set_from_icon_name(name, _SMALL_DEVICE_ICON_SIZE)
icon.set_tooltip_text(name)
toolbar = Gtk.Toolbar()
toolbar.set_name('buttons')
@ -139,10 +145,7 @@ def _receiver_box(rstatus):
pair_button = Gtk.ToolButton()
pair_button.set_icon_name('add')
pair_button.set_tooltip_text('Pair new device')
if rstatus.pair:
pair_button.connect('clicked', rstatus.pair)
else:
pair_button.set_sensitive(False)
pair_button.set_sensitive(False)
toolbar.insert(pair_button, 0)
toolbar.show_all()
@ -205,17 +208,18 @@ def _device_box(has_status_icons=True, has_frame=True):
return box
def create(title, rstatus, systray=False):
def create(title, name, max_devices, systray=False):
window = Gtk.Window()
window.set_title(title)
# window.set_icon_name(title)
window.set_role('status-window')
vbox = Gtk.VBox(homogeneous=False, spacing=4)
vbox.set_border_width(4)
rbox = _receiver_box(rstatus)
rbox = _receiver_box(name)
vbox.add(rbox)
for i in range(1, 1 + rstatus.max_devices):
for i in range(1, 1 + max_devices):
dbox = _device_box()
vbox.add(dbox)
vbox.set_visible(True)
@ -229,35 +233,40 @@ def create(title, rstatus, systray=False):
window.set_resizable(False)
if systray:
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 _state_event(w, e):
# if e.new_window_state & Gdk.WindowState.ICONIFIED:
# w.hide()
# w.deiconify()
# return True
# window.connect('window-state-event', _state_event)
window.set_keep_above(True)
# window.set_deletable(False)
window.set_deletable(False)
# window.set_decorated(False)
window.set_position(Gtk.WindowPosition.MOUSE)
# window.set_type_hint(Gdk.WindowTypeHint.MENU)
# window.set_position(Gtk.WindowPosition.MOUSE)
# ulgy, but hides the minimize icon from the window
window.set_type_hint(Gdk.WindowTypeHint.MENU)
window.set_skip_taskbar_hint(True)
window.set_skip_pager_hint(True)
window.connect('window-state-event', _state_event)
window.connect('delete-event', lambda w, e: toggle(None, window) or True)
window.connect('delete-event', lambda w, e: toggle(None, w) or True)
else:
window.set_position(Gtk.WindowPosition.CENTER)
# window.set_position(Gtk.WindowPosition.CENTER)
window.connect('delete-event', Gtk.main_quit)
return window
def toggle(_, window):
def toggle(icon, window):
if window.get_visible():
# position = window.get_position()
position = window.get_position()
window.hide()
# window.move(*position)
window.move(*position)
else:
if icon:
x, y = window.get_position()
if x == 0 and y == 0:
x, y, _ = Gtk.StatusIcon.position_menu(Gtk.Menu(), icon)
window.move(x, y)
window.present()
return True

View File

@ -2,231 +2,105 @@
#
#
from threading import (Thread, Event)
from threading import Thread
import time
from logging import getLogger as _Logger
import logging
from collections import namedtuple
from logitech.unifying_receiver import (api, base)
from logitech.unifying_receiver.listener import EventsListener
from logitech import devices
from logitech.devices import constants as C
from logitech.devices.constants import STATUS
from receiver import Receiver
_l = _Logger('watcher')
_UNIFYING_RECEIVER = 'Unifying Receiver'
_NO_RECEIVER = 'Receiver not found.'
_INITIALIZING = 'Initializing...'
_SCANNING = 'Scanning...'
_NO_DEVICES = 'No devices found.'
_OKAY = 'Status ok.'
_DUMMY_RECEIVER = namedtuple('_DUMMY_RECEIVER', ['NAME', 'kind', 'status', 'status_text', 'max_devices', 'devices'])
_DUMMY_RECEIVER.__nonzero__ = lambda _: False
_DUMMY_RECEIVER.device_name = Receiver.NAME
DUMMY = _DUMMY_RECEIVER(Receiver.NAME, Receiver.NAME, STATUS.UNAVAILABLE, 'Receiver not found.', Receiver.max_devices, {})
class _DevStatus(api.AttachedDeviceInfo):
code = C.STATUS.UNKNOWN
text = _INITIALIZING
def __str__(self):
return 'DevStatus(%d,%s,%d)' % (self.number, self.name, self.code)
def _sleep(seconds, granularity, breakout=lambda: False):
for index in range(0, int(seconds / granularity)):
if breakout():
return
time.sleep(granularity)
class Watcher(Thread):
"""Keeps a map of all attached devices and their statuses."""
def __init__(self, apptitle, notify=None):
"""Keeps an active receiver object if possible, and updates the UI when
necessary.
"""
def __init__(self, apptitle, update_ui, notify=None):
super(Watcher, self).__init__(group=apptitle, name='Watcher')
self.daemon = True
self._active = False
self.listener = None
self.no_receiver = Event()
self.update_ui = update_ui
self.notify = notify or (lambda d: None)
self.rstatus = _DevStatus(0, 0xFF, 'UR', _UNIFYING_RECEIVER, ())
self.rstatus.max_devices = api.C.MAX_ATTACHED_DEVICES
self.rstatus.pair = None
self.devices = {}
self.notify = notify
self.status_changed = Event()
self.receiver = DUMMY
def run(self):
self._active = True
notify_missing = True
while self._active:
if self.listener is None:
receiver = api.open()
if receiver:
self._device_status_changed(self.rstatus, C.STATUS.BOOTING, _INITIALIZING)
if self.receiver == DUMMY:
r = Receiver.open()
if r:
logging.info("receiver %s ", r)
self.update_ui(r)
self.notify(r)
r.events_handler = self._events_callback
init = (base.request(receiver, 0xFF, b'\x81\x00') and
base.request(receiver, 0xFF, b'\x80\x00', b'\x00\x01') and
base.request(receiver, 0xFF, b'\x81\x02'))
if init:
_l.debug("receiver initialized ok")
# give it some time to read all devices
r.status_changed.clear()
_sleep(8, 0.4, r.status_changed.is_set)
if r.devices:
logging.info("%d device(s) found", len(r.devices))
for d in r.devices.values():
self.notify(d)
else:
_l.debug("receiver initialization failed")
# if no devices found so far, assume none at all
logging.info("no devices found")
r.status = STATUS.CONNECTED
self._device_status_changed(self.rstatus, C.STATUS.BOOTING, _SCANNING)
self.listener = EventsListener(receiver, self._events_callback)
self.listener.start()
_l.debug("requesting devices status")
self.listener.request(base.request, 0xFF, b'\x80\x02', b'\x02')
# give it some time to get the devices
time.sleep(3)
elif not self.listener:
self.listener = None
self.devices.clear()
if self.listener:
if self.devices:
self._device_status_changed(self.rstatus, C.STATUS.CONNECTED, _OKAY)
self.receiver = r
notify_missing = True
else:
self._device_status_changed(self.rstatus, C.STATUS.CONNECTED, _NO_DEVICES)
if notify_missing:
_sleep(0.8, 0.4, lambda: not self._active)
notify_missing = False
self.update_ui(DUMMY)
self.notify(DUMMY)
_sleep(4, 0.4, lambda: not self._active)
continue
self.no_receiver.wait()
self.no_receiver.clear()
else:
self._device_status_changed(self.rstatus, C.STATUS.UNAVAILABLE, _NO_RECEIVER)
time.sleep(3)
if self._active:
if self.receiver:
logging.debug("waiting for status_changed")
sc = self.receiver.status_changed
sc.wait()
sc.clear()
logging.debug("status_changed %s %d", sc.reason, sc.urgent)
self.update_ui(self.receiver)
if sc.reason and sc.urgent:
self.notify(sc.reason)
else:
self.receiver = DUMMY
self.update_ui(DUMMY)
self.notify(DUMMY)
if self.listener:
self.listener.stop()
self.listener = None
if self.receiver:
self.receiver.close()
def stop(self):
if self._active:
_l.debug("stopping %s", self)
logging.info("stopping %s", self)
self._active = False
self.no_receiver.set()
if self.receiver:
# break out of an eventual wait()
self.receiver.status_changed.reason = None
self.receiver.status_changed.set()
self.join()
def request_status(self, devstatus=None, **kwargs):
"""Trigger a status update on a device."""
if self.listener:
if devstatus is None or devstatus == self.rstatus:
for devstatus in self.devices.values():
self.request_status(devstatus)
else:
status = devices.request_status(devstatus, self.listener)
self._handle_status(devstatus, status)
def _handle_status(self, devstatus, status):
if status is not None:
if type(status) == int:
self._device_status_changed(devstatus, status)
else:
self._device_status_changed(devstatus, *status)
def _new_device(self, dev):
if not self._active:
return None
if type(dev) == int:
assert self.listener
dev = self.listener.request(api.get_device_info, dev)
if dev:
devstatus = _DevStatus(*dev)
self.devices[dev.number] = devstatus
self._device_status_changed(devstatus, C.STATUS.CONNECTED)
_l.debug("new devstatus %s", devstatus)
self._device_status_changed(self.rstatus, C.STATUS.CONNECTED, _OKAY)
return devstatus
def _device_status_changed(self, devstatus, status_code, status_data=None):
old_status_code = devstatus.code
status_text = devstatus.text
if status_data is None:
if status_code in C.STATUS_NAME:
status_text = C.STATUS_NAME[status_code]
elif isinstance(status_data, str):
status_text = status_data
elif isinstance(status_data, dict):
status_text = ''
for key, value in status_data.items():
if key == 'text':
status_text = value
else:
setattr(devstatus, key, value)
else:
_l.warn("don't know how to handle status %s", status_data)
return False
if status_code >= C.STATUS.CONNECTED and devstatus.type is None:
# ghost device that became active
if devstatus.code != C.STATUS.CONNECTED:
# initial update, while we're getting the devinfo
devstatus.code = C.STATUS.CONNECTED
devstatus.text = C.STATUS_NAME[C.STATUS.CONNECTED]
self.status_changed.set()
if self._new_device(devstatus.number) is None:
_l.warn("could not materialize device from %s", devstatus)
return False
if ((status_code == old_status_code and status_text == devstatus.text) or
(status_code == C.STATUS.CONNECTED and old_status_code > C.STATUS.CONNECTED)):
# this is just successful ping for a device with an already known status
return False
devstatus.code = status_code
devstatus.text = status_text
_l.debug("%s update %s => %s: %s", devstatus, old_status_code, status_code, status_text)
if self.notify and (status_code <= C.STATUS.CONNECTED or status_code != old_status_code):
self.notify(devstatus.code, devstatus.name, devstatus.text)
self.status_changed.set()
return True
def _events_callback(self, event):
if event.code == 0xFF and event.devnumber == 0xFF and event.data is None:
self.no_receiver.set()
return
if event.code == 0x10 and event.data[0:2] == b'\x41\x04':
# 2 = 0010 ping
# 6 = 0110 off
# a = 1010 on
change = ord(event.data[2:3]) & 0xF0
status_code = C.STATUS.UNAVAILABLE if change == 0x60 else \
C.STATUS.CONNECTED if change == 0xA0 else \
C.STATUS.CONNECTED if change == 0x20 else \
None
if status_code is None:
_l.warn("don't know how to handle status %x: %s", change, event)
return
if event.devnumber in self.devices:
devstatus = self.devices[event.devnumber]
self._device_status_changed(devstatus, status_code)
return
if status_code == C.STATUS.CONNECTED:
self._new_device(event.devnumber)
return
# a device the UR knows about, but is not connected at this time
dev_id = self.listener.request(base.request, 0xFF, b'\x83\xB5', event.data[4:5])
name = str(dev_id[2:].rstrip(b'\x00')) if dev_id else '?'
name = devices.C.FULL_NAME[name]
ghost = _DevStatus(handle=self.listener.receiver, number=event.devnumber, type=None, name=name, features=[])
self.devices[event.devnumber] = ghost
self._device_status_changed(ghost, C.STATUS.UNAVAILABLE)
self._device_status_changed(self.rstatus, C.STATUS.CONNECTED, _OKAY)
return
if event.devnumber in self.devices:
devstatus = self.devices[event.devnumber]
if event.code == 0x11:
status = devices.process_event(devstatus, event.data, self.listener)
self._handle_status(devstatus, status)
return
if event.code == 0x10 and event.data[:1] == b'\x8F':
self._device_status_changed(devstatus, C.STATUS.UNAVAILABLE)
return
_l.warn("don't know how to handle event %s", event)
logging.warn("don't know how to handle event %s", event)

View File

@ -1,9 +1,9 @@
#!/bin/sh
Z=`readlink -f "$0"`
APP=`dirname "$Z"`/../app
LIB=`dirname "$Z"`/../lib
SHARE=`dirname "$Z"`/../share
APP=`readlink -f $(dirname "$Z")/../app`
LIB=`readlink -f $(dirname "$Z")/../lib`
SHARE=`readlink -f $(dirname "$Z")/../share`
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$LIB/native/`uname -m`
export PYTHONPATH=$APP:$LIB

View File

@ -4,50 +4,49 @@
import logging
from . import k750
from . import constants as C
from .constants import (STATUS, PROPS)
from ..unifying_receiver.constants import (FEATURE, BATTERY_STATUS)
from ..unifying_receiver import api as _api
#
#
#
_REQUEST_STATUS_FUNCTIONS = {
C.NAME.K750: k750.request_status,
}
_DEVICE_MODULES = {}
_PROCESS_EVENT_FUNCTIONS = {
C.NAME.K750: k750.process_event,
}
def _module(device_name):
if device_name not in _DEVICE_MODULES:
shortname = device_name.split(' ')[-1].lower()
try:
m = __import__(shortname, globals(), level=1)
_DEVICE_MODULES[device_name] = m
except:
# logging.exception(shortname)
_DEVICE_MODULES[device_name] = None
return _DEVICE_MODULES[device_name]
#
#
#
def ping(devinfo, listener=None):
if listener is None:
reply = _api.ping(devinfo.number)
elif listener:
reply = listener.request(_api.ping, devinfo.number)
else:
return None
return C.STATUS.CONNECTED if reply else C.STATUS.UNAVAILABLE
def default_request_status(devinfo, listener=None):
if _api.C.FEATURE.BATTERY in devinfo.features:
if listener is None:
reply = _api.get_device_battery_level(devinfo.handle, devinfo.number, features=devinfo.features)
elif listener:
if FEATURE.BATTERY in devinfo.features:
if listener:
reply = listener.request(_api.get_device_battery_level, devinfo.number, features=devinfo.features)
else:
reply = None
reply = _api.get_device_battery_level(devinfo.handle, devinfo.number, features=devinfo.features)
if reply:
discharge, dischargeNext, status = reply
return C.STATUS.CONNECTED, {C.PROPS.BATTERY_LEVEL: discharge}
return STATUS.CONNECTED, {PROPS.BATTERY_LEVEL: discharge, PROPS.BATTERY_STATUS: status}
if listener:
reply = listener.request(_api.ping, devinfo.number)
else:
reply = _api.ping(devinfo.handle, devinfo.number)
return STATUS.CONNECTED if reply else STATUS.UNAVAILABLE
def default_process_event(devinfo, data, listener=None):
@ -59,21 +58,23 @@ def default_process_event(devinfo, data, listener=None):
feature = devinfo.features[feature_index]
feature_function = ord(data[1:2]) & 0xF0
if feature == _api.C.FEATURE.BATTERY:
if feature == FEATURE.BATTERY:
if feature_function == 0:
discharge = ord(data[2:3])
status = _api.C.BATTERY_STATUS[ord(data[3:4])]
return C.STATUS.CONNECTED, {C.PROPS.BATTERY_LEVEL: discharge, C.PROPS.BATTERY_STATUS: status}
status = BATTERY_STATUS[ord(data[3:4])]
return STATUS.CONNECTED, {PROPS.BATTERY_LEVEL: discharge, PROPS.BATTERY_STATUS: status}
# ?
elif feature == _api.C.FEATURE.REPROGRAMMABLE_KEYS:
elif feature == FEATURE.REPROGRAMMABLE_KEYS:
if feature_function == 0:
logging.debug('reprogrammable key: %s', repr(data))
# TODO
pass
# ?
elif feature == _api.C.FEATURE.WIRELESS:
elif feature == FEATURE.WIRELESS:
if feature_function == 0:
logging.debug("wireless status: %s", repr(data))
if data[2:5] == b'\x01\x01\x01':
return STATUS.CONNECTED
# TODO
pass
# ?
@ -86,9 +87,10 @@ def request_status(devinfo, listener=None):
:param listener: the EventsListener that will be used to send the request,
and which will receive the status events from the device.
"""
if devinfo.name in _REQUEST_STATUS_FUNCTIONS:
return _REQUEST_STATUS_FUNCTIONS[devinfo.name](devinfo, listener)
return default_request_status(devinfo, listener) or ping(devinfo, listener)
m = _module(devinfo.name)
if m and 'request_status' in m.__dict__:
return m.request_status(devinfo, listener)
return default_request_status(devinfo, listener)
def process_event(devinfo, data, listener=None):
@ -101,5 +103,6 @@ def process_event(devinfo, data, listener=None):
if default_result is not None:
return default_result
if devinfo.name in _PROCESS_EVENT_FUNCTIONS:
return _PROCESS_EVENT_FUNCTIONS[devinfo.name](devinfo, data, listener)
m = _module(devinfo.name)
if m and 'process_event' in m.__dict__:
return m.process_event(devinfo, data, listener)

View File

@ -11,47 +11,32 @@ STATUS = type('STATUS', (),
))
STATUS_NAME = {
STATUS.UNKNOWN: '...',
STATUS.UNAVAILABLE: 'inactive',
STATUS.BOOTING: 'initializing',
STATUS.CONNECTED: 'connected',
}
# device properties that may be reported
PROPS = type('PROPS', (),
dict(
TEXT='text',
BATTERY_LEVEL='battery_level',
BATTERY_STATUS='battery_status',
LIGHT_LEVEL='light_level',
))
NAME = type('NAME', (),
dict(
M315='Wireless Mouse M315',
M325='Wireless Mouse M325',
M510='Wireless Mouse M510',
M515='Couch Mouse M515',
M570='Wireless Trackball M570',
K270='Wireless Keyboard K270',
K350='Wireless Keyboard K350',
K750='Wireless Solar Keyboard K750',
K800='Wireless Illuminated Keyboard K800',
))
from ..unifying_receiver.common import FallbackDict
FULL_NAME = FallbackDict(lambda x: x,
dict(
M315=NAME.M315,
M325=NAME.M325,
M510=NAME.M510,
M515=NAME.M515,
M570=NAME.M570,
K270=NAME.K270,
K350=NAME.K350,
K750=NAME.K750,
K800=NAME.K800,
))
del FallbackDict
# when the receiver reports a device that is not connected
# (and thus cannot be queried), guess the name and type
# based on this table
NAMES = {
'M315': ('Wireless Mouse M315', 'mouse'),
'M325': ('Wireless Mouse M325', 'mouse'),
'M510': ('Wireless Mouse M510', 'mouse'),
'M515': ('Couch Mouse M515', 'mouse'),
'M570': ('Wireless Trackball M570', 'trackball'),
'K270': ('Wireless Keyboard K270', 'keyboard'),
'K350': ('Wireless Keyboard K350', 'keyboard'),
'K750': ('Wireless Solar Keyboard K750', 'keyboard'),
'K800': ('Wireless Illuminated Keyboard K800', 'keyboard'),
}

View File

@ -5,40 +5,33 @@
import logging
from struct import unpack as _unpack
from .constants import (STATUS, PROPS)
from ..unifying_receiver.constants import FEATURE
from ..unifying_receiver import api as _api
from . import constants as C
#
#
#
_CHARGE_LEVELS = (10, 25, 256)
def _charge_status(data, hasLux=False):
charge, lux = _unpack('!BH', data[2:5])
d = {}
_CHARGE_LEVELS = (10, 25, 256)
for i in range(0, len(_CHARGE_LEVELS)):
if charge < _CHARGE_LEVELS[i]:
charge_index = i
break
d[C.PROPS.BATTERY_LEVEL] = charge
text = 'Battery %d%%' % charge
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
return 0x10 << charge_index, {
PROPS.BATTERY_LEVEL: charge,
PROPS.LIGHT_LEVEL: lux if hasLux else None,
}
def request_status(devinfo, listener=None):
def _trigger_solar_charge_events(handle, devinfo):
return _api.request(handle, devinfo.number,
feature=_api.C.FEATURE.SOLAR_CHARGE, function=b'\x03', params=b'\x78\x01',
feature=FEATURE.SOLAR_CHARGE, function=b'\x03', params=b'\x78\x01',
features=devinfo.features)
if listener is None:
reply = _trigger_solar_charge_events(devinfo.handle, devinfo)
@ -48,7 +41,7 @@ def request_status(devinfo, listener=None):
reply = 0
if reply is None:
return C.STATUS.UNAVAILABLE
return STATUS.UNAVAILABLE
def process_event(devinfo, data, listener=None):
@ -68,4 +61,4 @@ def process_event(devinfo, data, listener=None):
# wireless device status
if data[2:5] == b'\x01\x01\x01':
logging.debug("Keyboard just started")
return C.STATUS.CONNECTED
return STATUS.CONNECTED

View File

@ -7,12 +7,15 @@ from struct import pack as _pack
from struct import unpack as _unpack
from binascii import hexlify as _hexlify
from .common import FirmwareInfo
from .common import AttachedDeviceInfo
from .common import ReprogrammableKeyInfo
from . import constants as C
from . import exceptions as E
from . import base as _base
from .common import (FirmwareInfo as _FirmwareInfo,
AttachedDeviceInfo as _AttachedDeviceInfo,
ReprogrammableKeyInfo as _ReprogrammableKeyInfo)
from .constants import (FEATURE, FEATURE_NAME, FEATURE_FLAGS,
FIRMWARE_KIND, DEVICE_KIND,
BATTERY_STATUS, KEY_NAME,
MAX_ATTACHED_DEVICES)
from .exceptions import FeatureNotSupported as _FeatureNotSupported
_LOG_LEVEL = 5
@ -83,7 +86,7 @@ def request(handle, devnumber, feature, function=b'\x00', params=b'', features=N
"""
feature_index = None
if feature == C.FEATURE.ROOT:
if feature == FEATURE.ROOT:
feature_index = b'\x00'
else:
if features is None:
@ -95,8 +98,8 @@ def request(handle, devnumber, feature, function=b'\x00', params=b'', features=N
feature_index = _pack('!B', features.index(feature))
if feature_index is None:
_l.warn("(%d) feature <%s:%s> not supported", devnumber, _hexlify(feature), C.FEATURE_NAME[feature])
raise E.FeatureNotSupported(devnumber, feature)
_l.warn("(%d) feature <%s:%s> not supported", devnumber, _hexlify(feature), FEATURE_NAME[feature])
raise _FeatureNotSupported(devnumber, feature)
if type(function) == int:
function = _pack('!B', function)
@ -129,7 +132,7 @@ def find_device_by_name(handle, name):
"""
_l.log(_LOG_LEVEL, "searching for device '%s'", name)
for devnumber in range(1, 1 + C.MAX_ATTACHED_DEVICES):
for devnumber in range(1, 1 + MAX_ATTACHED_DEVICES):
features = get_device_features(handle, devnumber)
if features:
d_name = get_device_name(handle, devnumber, features)
@ -146,7 +149,7 @@ def list_devices(handle):
devices = []
for device in range(1, 1 + C.MAX_ATTACHED_DEVICES):
for device in range(1, 1 + MAX_ATTACHED_DEVICES):
features = get_device_features(handle, device)
if features:
devices.append(get_device_info(handle, device, features=features))
@ -164,9 +167,9 @@ def get_device_info(handle, devnumber, name=None, features=None):
if features is None:
return None
d_type = get_device_type(handle, devnumber, features)
d_kind = get_device_kind(handle, devnumber, features)
d_name = get_device_name(handle, devnumber, features) if name is None else name
devinfo = AttachedDeviceInfo(handle, devnumber, d_type, d_name, features)
devinfo = _AttachedDeviceInfo(handle, devnumber, d_kind, d_name, features)
_l.log(_LOG_LEVEL, "(%d) found device %s", devnumber, devinfo)
return devinfo
@ -176,12 +179,12 @@ def get_feature_index(handle, devnumber, feature):
:returns: An int, or ``None`` if the feature is not available.
"""
_l.log(_LOG_LEVEL, "(%d) get feature index <%s:%s>", devnumber, _hexlify(feature), C.FEATURE_NAME[feature])
_l.log(_LOG_LEVEL, "(%d) get feature index <%s:%s>", devnumber, _hexlify(feature), 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, devnumber, C.FEATURE.ROOT, feature)
reply = _base.request(handle, devnumber, FEATURE.ROOT, feature)
if reply:
# only consider active and supported features
feature_index = ord(reply[0:1])
@ -190,18 +193,18 @@ def get_feature_index(handle, devnumber, feature):
if _l.isEnabledFor(_LOG_LEVEL):
if feature_flags:
_l.log(_LOG_LEVEL, "(%d) feature <%s:%s> has index %d: %s",
devnumber, _hexlify(feature), C.FEATURE_NAME[feature], feature_index,
','.join([C.FEATURE_FLAGS[k] for k in C.FEATURE_FLAGS if feature_flags & k]))
devnumber, _hexlify(feature), FEATURE_NAME[feature], feature_index,
','.join([FEATURE_FLAGS[k] for k in FEATURE_FLAGS if feature_flags & k]))
else:
_l.log(_LOG_LEVEL, "(%d) feature <%s:%s> has index %d", devnumber, _hexlify(feature), C.FEATURE_NAME[feature], feature_index)
_l.log(_LOG_LEVEL, "(%d) feature <%s:%s> has index %d", devnumber, _hexlify(feature), FEATURE_NAME[feature], feature_index)
# if feature_flags:
# raise E.FeatureNotSupported(devnumber, feature)
return feature_index
_l.warn("(%d) feature <%s:%s> not supported by the device", devnumber, _hexlify(feature), C.FEATURE_NAME[feature])
raise E.FeatureNotSupported(devnumber, feature)
_l.warn("(%d) feature <%s:%s> not supported by the device", devnumber, _hexlify(feature), FEATURE_NAME[feature])
raise _FeatureNotSupported(devnumber, feature)
def get_device_features(handle, devnumber):
@ -214,7 +217,7 @@ def 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, devnumber, C.FEATURE.ROOT, C.FEATURE.FEATURE_SET)
fs_index = _base.request(handle, devnumber, FEATURE.ROOT, FEATURE.FEATURE_SET)
if fs_index is None:
# _l.warn("(%d) FEATURE_SET not available", device)
return None
@ -246,15 +249,15 @@ def get_device_features(handle, devnumber):
if _l.isEnabledFor(_LOG_LEVEL):
if feature_flags:
_l.log(_LOG_LEVEL, "(%d) feature <%s:%s> at index %d: %s",
devnumber, _hexlify(feature), C.FEATURE_NAME[feature], index,
','.join([C.FEATURE_FLAGS[k] for k in C.FEATURE_FLAGS if feature_flags & k]))
devnumber, _hexlify(feature), FEATURE_NAME[feature], index,
','.join([FEATURE_FLAGS[k] for k in FEATURE_FLAGS if feature_flags & k]))
else:
_l.log(_LOG_LEVEL, "(%d) feature <%s:%s> at index %d", devnumber, _hexlify(feature), C.FEATURE_NAME[feature], index)
_l.log(_LOG_LEVEL, "(%d) feature <%s:%s> at index %d", devnumber, _hexlify(feature), FEATURE_NAME[feature], index)
features[0] = C.FEATURE.ROOT
features[0] = FEATURE.ROOT
while features[-1] is None:
del features[-1]
return features
return tuple(features)
def get_device_firmware(handle, devnumber, features=None):
@ -262,20 +265,20 @@ def get_device_firmware(handle, devnumber, features=None):
:returns: a list of FirmwareInfo tuples, ordered by firmware layer.
"""
def _makeFirmwareInfo(level, type, name=None, version=None, build=None, extras=None):
return FirmwareInfo(level, type, name, version, build, extras)
def _makeFirmwareInfo(level, kind, name=None, version=None, build=None, extras=None):
return _FirmwareInfo(level, kind, name, version, build, extras)
fw_count = request(handle, devnumber, C.FEATURE.FIRMWARE, features=features)
fw_count = request(handle, devnumber, FEATURE.FIRMWARE, features=features)
if fw_count:
fw_count = ord(fw_count[:1])
fw = []
for index in range(0, fw_count):
fw_info = request(handle, devnumber, C.FEATURE.FIRMWARE, function=b'\x10', params=index, features=features)
fw_info = request(handle, devnumber, FEATURE.FIRMWARE, function=b'\x10', params=index, features=features)
if fw_info:
fw_level = ord(fw_info[:1]) & 0x0F
if fw_level == 0 or fw_level == 1:
fw_type = C.FIRMWARE_TYPE[fw_level]
level = ord(fw_info[:1]) & 0x0F
if level == 0 or level == 1:
kind = FIRMWARE_KIND[level]
name, = _unpack('!3s', fw_info[1:4])
name = name.decode('ascii')
version = _hexlify(fw_info[4:6])
@ -283,31 +286,31 @@ def get_device_firmware(handle, devnumber, features=None):
build, = _unpack('!H', fw_info[6:8])
extras = fw_info[9:].rstrip(b'\x00')
if extras:
fw_info = _makeFirmwareInfo(level=fw_level, type=fw_type, name=name, version=version, build=build, extras=extras)
fw_info = _makeFirmwareInfo(level, kind, name, version, build, extras)
else:
fw_info = _makeFirmwareInfo(level=fw_level, type=fw_type, name=name, version=version, build=build)
elif fw_level == 2:
fw_info = _makeFirmwareInfo(level=2, type=C.FIRMWARE_TYPE[2], version=ord(fw_info[1:2]))
fw_info = _makeFirmwareInfo(level, kind, name, version, build)
elif level == 2:
fw_info = _makeFirmwareInfo(2, FIRMWARE_KIND[2], version=ord(fw_info[1:2]))
else:
fw_info = _makeFirmwareInfo(level=fw_level, type=C.FIRMWARE_TYPE[-1])
fw_info = _makeFirmwareInfo(level, FIRMWARE_KIND[-1])
fw.append(fw_info)
_l.log(_LOG_LEVEL, "(%d) firmware %s", devnumber, fw_info)
return fw
return tuple(fw)
def get_device_type(handle, devnumber, features=None):
def get_device_kind(handle, devnumber, features=None):
"""Reads a device's type.
:see DEVICE_TYPE:
:see DEVICE_KIND:
: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, devnumber, C.FEATURE.NAME, function=b'\x20', features=features)
if d_type:
d_type = ord(d_type[:1])
_l.log(_LOG_LEVEL, "(%d) device type %d = %s", devnumber, d_type, C.DEVICE_TYPE[d_type])
return C.DEVICE_TYPE[d_type]
d_kind = request(handle, devnumber, FEATURE.NAME, function=b'\x20', features=features)
if d_kind:
d_kind = ord(d_kind[:1])
_l.log(_LOG_LEVEL, "(%d) device type %d = %s", devnumber, d_kind, DEVICE_KIND[d_kind])
return DEVICE_KIND[d_kind]
def get_device_name(handle, devnumber, features=None):
@ -316,13 +319,13 @@ def get_device_name(handle, devnumber, features=None):
: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, devnumber, C.FEATURE.NAME, features=features)
name_length = request(handle, devnumber, FEATURE.NAME, features=features)
if name_length:
name_length = ord(name_length[:1])
d_name = b''
while len(d_name) < name_length:
name_fragment = request(handle, devnumber, C.FEATURE.NAME, function=b'\x10', params=len(d_name), features=features)
name_fragment = request(handle, devnumber, FEATURE.NAME, function=b'\x10', params=len(d_name), features=features)
if name_fragment:
name_fragment = name_fragment[:name_length - len(d_name)]
d_name += name_fragment
@ -339,24 +342,25 @@ def get_device_battery_level(handle, devnumber, features=None):
:raises FeatureNotSupported: if the device does not support this feature.
"""
battery = request(handle, devnumber, C.FEATURE.BATTERY, features=features)
battery = request(handle, devnumber, FEATURE.BATTERY, features=features)
if battery:
discharge, dischargeNext, status = _unpack('!BBB', battery[:3])
_l.log(_LOG_LEVEL, "(%d) battery %d%% charged, next level %d%% charge, status %d = %s",
devnumber, discharge, dischargeNext, status, C.BATTERY_STATUSE[status])
return (discharge, dischargeNext, C.BATTERY_STATUS[status])
devnumber, discharge, dischargeNext, status, BATTERY_STATUS[status])
return (discharge, dischargeNext, BATTERY_STATUS[status])
def get_device_keys(handle, devnumber, features=None):
count = request(handle, devnumber, C.FEATURE.REPROGRAMMABLE_KEYS, features=features)
count = request(handle, devnumber, FEATURE.REPROGRAMMABLE_KEYS, features=features)
if count:
keys = []
count = ord(count[:1])
for index in range(0, count):
keydata = request(handle, devnumber, C.FEATURE.REPROGRAMMABLE_KEYS, function=b'\x10', params=index, features=features)
keydata = request(handle, devnumber, FEATURE.REPROGRAMMABLE_KEYS, function=b'\x10', params=index, features=features)
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))
rki = _ReprogrammableKeyInfo(index, key, KEY_NAME[key], key_task, KEY_NAME[key_task], flags)
keys.append(rki)
return keys

View File

@ -7,8 +7,9 @@ from logging import getLogger as _Logger
from struct import pack as _pack
from binascii import hexlify as _hexlify
from . import constants as C
from . import exceptions as E
from .constants import ERROR_NAME
from .exceptions import (NoReceiver as _NoReceiver,
FeatureCallError as _FeatureCallError)
import hidapi as _hid
@ -96,7 +97,7 @@ def try_open(path):
_l.log(_LOG_LEVEL, "[%s] open failed", path)
return None
_l.log(_LOG_LEVEL, "[%s] receiver handle 0x%x", path, receiver_handle)
_l.log(_LOG_LEVEL, "[%s] receiver handle %x", path, receiver_handle)
# ping on device id 0 (always an error)
_hid.write(receiver_handle, b'\x10\x00\x00\x10\x00\x00\xAA')
@ -176,7 +177,7 @@ def write(handle, devnumber, data):
if not _hid.write(handle, wdata):
_l.warn("(%d) write failed, assuming receiver %x no longer available", devnumber, handle)
close(handle)
raise E.NoReceiver
raise _NoReceiver
def read(handle, timeout=DEFAULT_TIMEOUT):
@ -199,7 +200,7 @@ def read(handle, timeout=DEFAULT_TIMEOUT):
if data is None:
_l.warn("(-) read failed, assuming receiver %x no longer available", handle)
close(handle)
raise E.NoReceiver
raise _NoReceiver
if data:
if len(data) < _MIN_REPLY_SIZE:
@ -274,11 +275,11 @@ def request(handle, devnumber, feature_index_function, params=b'', features=None
if reply_code == 0x11 and reply_data[0] == b'\xFF' and reply_data[1:3] == feature_index_function:
# the feature call returned with an error
error_code = ord(reply_data[3])
_l.warn("(%d) request feature call error %d = %s: %s", devnumber, error_code, C.ERROR_NAME[error_code], _hexlify(reply_data))
_l.warn("(%d) request feature call error %d = %s: %s", devnumber, error_code, 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 is None else features[feature_index] if feature_index < len(features) else None
raise E.FeatureCallError(devnumber, feature, feature_index, feature_function, error_code, reply_data)
raise _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

View File

@ -3,6 +3,7 @@
#
from binascii import hexlify as _hexlify
from collections import namedtuple
class FallbackDict(dict):
@ -21,20 +22,18 @@ def list2dict(values_list):
return dict(zip(range(0, len(values_list)), values_list))
from collections import namedtuple
"""Tuple returned by list_devices and find_device_by_name."""
AttachedDeviceInfo = namedtuple('AttachedDeviceInfo', [
'handle',
'number',
'type',
'kind',
'name',
'features'])
"""Firmware information."""
FirmwareInfo = namedtuple('FirmwareInfo', [
'level',
'type',
'kind',
'name',
'version',
'build',
@ -49,6 +48,7 @@ ReprogrammableKeyInfo = namedtuple('ReprogrammableKeyInfo', [
'task_name',
'flags'])
class Packet(namedtuple('Packet', ['code', 'devnumber', 'data'])):
def __str__(self):
return 'Packet(0x%02x,%d,%s)' % (self.code, self.devnumber, '' if self.data is None else _hexlify(self.data))

View File

@ -48,17 +48,17 @@ FEATURE_NAME[FEATURE.SOLAR_CHARGE] = 'SOLAR_CHARGE'
FEATURE_FLAGS = { 0x20: 'internal', 0x40: 'hidden', 0x80: 'obsolete' }
_DEVICE_TYPES = ('Keyboard', 'Remote Control', 'NUMPAD', 'Mouse',
'Touchpad', 'Trackball', 'Presenter', 'Receiver')
_DEVICE_KINDS = ('keyboard', 'remote control', 'numpad', 'mouse',
'touchpad', 'trackball', 'presenter', 'receiver')
"""Possible types of devices connected to an UR."""
DEVICE_TYPE = FallbackDict(lambda x: 'unknown', list2dict(_DEVICE_TYPES))
DEVICE_KIND = FallbackDict(lambda x: 'unknown', list2dict(_DEVICE_KINDS))
_FIRMWARE_TYPES = ('Main (HID)', 'Bootloader', 'Hardware', 'Other')
_FIRMWARE_KINDS = ('Main (HID)', 'Bootloader', 'Hardware', 'Other')
"""Names of different firmware levels possible, indexed by level."""
FIRMWARE_TYPE = FallbackDict(lambda x: 'Unknown', list2dict(_FIRMWARE_TYPES))
FIRMWARE_KIND = FallbackDict(lambda x: 'Unknown', list2dict(_FIRMWARE_KINDS))
_BATTERY_STATUSES = ('Discharging (in use)', 'Recharging', 'Almost full',

View File

@ -2,7 +2,7 @@
# Exceptions that may be raised by this API.
#
from . import constants as C
from .constants import (FEATURE_NAME, ERROR_NAME)
class NoReceiver(Exception):
@ -16,21 +16,21 @@ class NoReceiver(Exception):
class FeatureNotSupported(Exception):
"""Raised when trying to request a feature not supported by the device."""
def __init__(self, devnumber, feature):
super(FeatureNotSupported, self).__init__(devnumber, feature, C.FEATURE_NAME[feature])
super(FeatureNotSupported, self).__init__(devnumber, feature, FEATURE_NAME[feature])
self.devnumber = devnumber
self.feature = feature
self.feature_name = C.FEATURE_NAME[feature]
self.feature_name = FEATURE_NAME[feature]
class FeatureCallError(Exception):
"""Raised if the device replied to a feature call with an error."""
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])
super(FeatureCallError, self).__init__(devnumber, feature, feature_index, feature_function, error_code, 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_name = None if feature is None else FEATURE_NAME[feature]
self.feature_index = feature_index
self.feature_function = feature_function
self.error_code = error_code
self.error_string = C.ERROR_NAME[error_code]
self.error_string = ERROR_NAME[error_code]
self.data = data

View File

@ -7,8 +7,8 @@ from threading import (Thread, Event, Lock)
# from time import sleep as _sleep
from . import base as _base
from . import exceptions as E
from .common import Packet
from .exceptions import NoReceiver as _NoReceiver
from .common import Packet as _Packet
# for both Python 2 and 3
try:
@ -17,95 +17,109 @@ except ImportError:
from queue import Queue
_LOG_LEVEL = 4
_LOG_LEVEL = 6
_l = _Logger('lur.listener')
_READ_EVENT_TIMEOUT = int(_base.DEFAULT_TIMEOUT / 4) # ms
def _callback_caller(listener, callback):
# _l.log(_LOG_LEVEL, "%s starting callback caller", listener)
while listener._active or not listener.events.empty():
event = listener.events.get()
_l.log(_LOG_LEVEL, "%s delivering event %s", listener, event)
def _event_dispatch(listener, callback):
# _l.log(_LOG_LEVEL, "starting dispatch")
while listener._active: # or not listener._events.empty():
event = listener._events.get()
_l.log(_LOG_LEVEL, "delivering event %s", event)
try:
callback.__call__(event)
callback(event)
except:
_l.exception("callback for %s", event)
# _l.log(_LOG_LEVEL, "%s stopped callback caller", listener)
# _l.log(_LOG_LEVEL, "stopped dispatch")
class EventsListener(Thread):
"""Listener thread for events from the Unifying Receiver.
Incoming events (reply_code, devnumber, data) will be passed to the callback
function in sequence, by a separate thread.
Incoming packets will be passed to the callback function in sequence, by a
separate thread.
While this listener is running, you should use the request() method to make
regular UR API calls, otherwise the expected API replies are most likely to
be captured by the listener and delivered as events to the callback.
While this listener is running, you must use the request() method to make
regular UR API calls; otherwise the expected API replies are most likely to
be captured by the listener and delivered to the callback.
"""
def __init__(self, receiver, events_callback):
super(EventsListener, self).__init__(group='Unifying Receiver', name='Events-%x' % receiver)
def __init__(self, receiver_handle, events_callback):
super(EventsListener, self).__init__(group='Unifying Receiver', name='%s-%x' % (self.__class__.__name__, receiver_handle))
self.daemon = True
self._active = False
self.receiver = receiver
self._handle = receiver_handle
self.task = None
self.task_processing = Lock()
self.task_reply = None
self.task_done = Event()
self._task = None
self._task_processing = Lock()
self._task_reply = None
self._task_done = Event()
self.events = Queue(32)
self._events = Queue(32)
_base.unhandled_hook = self._unhandled
self.event_caller = Thread(group='Unifying Receiver', name='Callback-%x' % receiver, target=_callback_caller, args=(self, events_callback))
self.event_caller.daemon = True
self.__str_cached = 'Events(%x)' % self.receiver
self._dispatcher = Thread(group='Unifying Receiver',
name='%s-%x-dispatch' % (self.__class__.__name__, receiver_handle),
target=_event_dispatch, args=(self, events_callback))
self._dispatcher.daemon = True
def run(self):
self._active = True
_l.log(_LOG_LEVEL, "%s started", self)
_l.log(_LOG_LEVEL, "started")
self.__str_cached = 'Events(%x:active)' % self.receiver
self.event_caller.start()
last_hook = _base.unhandled_hook
_base.unhandled_hook = self._unhandled
self._dispatcher.start()
while self._active:
event = None
try:
event = _base.read(self.receiver, _READ_EVENT_TIMEOUT)
except E.NoReceiver:
self.receiver = 0
_l.warn("%s receiver disconnected", self)
self.events.put(Packet(0xFF, 0xFF, None))
event = _base.read(self._handle, _READ_EVENT_TIMEOUT)
except _NoReceiver:
self._handle = 0
_l.warn("receiver disconnected")
self._events.put(_Packet(0xFF, 0xFF, None))
self._active = False
break
if event:
_l.log(_LOG_LEVEL, "%s queueing event %s", self, event)
self.events.put(Packet(*event))
_l.log(_LOG_LEVEL, "queueing event %s", event)
self._events.put(_Packet(*event))
if self.task:
task, self.task = self.task, None
self.task_reply = self._make_request(*task)
self.task_done.set()
if self._task:
(api_function, args, kwargs), self._task = self._task, None
# _l.log(_LOG_LEVEL, "calling '%s.%s' with %s, %s", api_function.__module__, api_function.__name__, args, kwargs)
try:
self._task_reply = api_function.__call__(self._handle, *args, **kwargs)
except _NoReceiver as nr:
self._handle = 0
_l.warn("receiver disconnected")
self._events.put(_Packet(0xFF, 0xFF, None))
self._task_reply = nr
self._active = False
break
except Exception as e:
# _l.exception("task %s.%s", api_function.__module__, api_function.__name__)
self._task_reply = e
finally:
self._task_done.set()
_base.close(self.receiver)
self.__str_cached = 'Events(%x)' % self.receiver
_base.unhandled_hook = last_hook
_base.close(self._handle)
def stop(self):
"""Tells the listener to stop as soon as possible."""
if self._active:
_l.log(_LOG_LEVEL, "stopping %s", self)
_l.log(_LOG_LEVEL, "stopping")
self._active = False
# wait for the receiver handle to be closed
self.join()
@property
def handle(self):
return self._handle
def request(self, api_function, *args, **kwargs):
"""Make an UR API request through this listener's receiver.
@ -114,37 +128,24 @@ class EventsListener(Thread):
"""
# _l.log(_LOG_LEVEL, "%s request '%s.%s' with %s, %s", self, api_function.__module__, api_function.__name__, args, kwargs)
if not self._active:
return None
# if not self._active:
# return None
with self.task_processing:
self.task_done.clear()
self.task = (api_function, args, kwargs)
self.task_done.wait()
reply, self.task_reply = self.task_reply, None
with self._task_processing:
self._task_done.clear()
self._task = (api_function, args, kwargs)
self._task_done.wait()
reply, self._task_reply = self._task_reply, None
# _l.log(_LOG_LEVEL, "%s request '%s.%s' => %s", self, api_function.__module__, api_function.__name__, repr(reply))
if isinstance(reply, Exception):
raise reply
return reply
def _make_request(self, api_function, args, kwargs):
_l.log(_LOG_LEVEL, "%s calling '%s.%s' with %s, %s", self, api_function.__module__, api_function.__name__, args, kwargs)
try:
return api_function.__call__(self.receiver, *args, **kwargs)
except E.NoReceiver as nr:
self.task_reply = nr
self._active = False
except Exception as e:
self.task_reply = e
def _unhandled(self, reply_code, devnumber, data):
event = Packet(reply_code, devnumber, data)
# _l.log(_LOG_LEVEL, "%s queueing unhandled event %s", self, event)
self.events.put(event)
def __str__(self):
return self.__str_cached
event = _Packet(reply_code, devnumber, data)
# _l.log(_LOG_LEVEL, "queueing unhandled event %s", event)
self._events.put(event)
def __nonzero__(self):
return self._active and self.receiver
return self._active and self._handle

View File

@ -91,12 +91,12 @@ class Test_UR_API(unittest.TestCase):
for fw in d_firmware:
self.assertIsInstance(fw, FirmwareInfo)
def test_52_get_device_type(self):
def test_52_get_device_kind(self):
self._check(check_features=True)
d_type = api.get_device_type(self.handle, self.device, self.features)
self.assertIsNotNone(d_type, "failed to get device type")
self.assertGreater(len(d_type), 0, "empty device type")
d_kind = api.get_device_kind(self.handle, self.device, self.features)
self.assertIsNotNone(d_kind, "failed to get device kind")
self.assertGreater(len(d_kind), 0, "empty device kind")
def test_55_get_device_name(self):
self._check(check_features=True)

View File

@ -31,12 +31,12 @@ def scan_devices(receiver):
return
for devinfo in devices:
print ("Device [%d] %s (%s)" % (devinfo.number, devinfo.name, devinfo.type))
print ("Device [%d] %s (%s)" % (devinfo.number, devinfo.name, devinfo.kind))
# print " Protocol %s" % devinfo.protocol
firmware = api.get_device_firmware(receiver, devinfo.number, features=devinfo.features)
for fw in firmware:
print (" %s firmware: %s version %s build %d" % (fw.type, fw.name, fw.version, fw.build))
print (" %s firmware: %s version %s build %d" % (fw.kind, fw.name, fw.version, fw.build))
for index in range(0, len(devinfo.features)):
feature = devinfo.features[index]
@ -77,3 +77,11 @@ if __name__ == '__main__':
break
else:
print ("!! Logitech Unifying Receiver not found.")
# import pyudev
# ctx = pyudev.Context()
# m = pyudev.Monitor.from_netlink(ctx)
# m.filter_by(subsystem='hid')
# for action, device in m:
# print '%s: %s' % (action, device)

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB