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). - 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. - Python GI (GObject Introspection), for Gtk bindings.
- Optional libnotify GI bindings, for desktop notifications.
- A hidapi native implementation (see the INSTALL file for details). - A hidapi native implementation (see the INSTALL file for details).
- Optional python-notify2 for desktop notifications.
Thanks 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 #!/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' 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__': if __name__ == '__main__':
import argparse import argparse
arg_parser = argparse.ArgumentParser(prog=APP_TITLE) 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)') 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') 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)') 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() args = arg_parser.parse_args()
import logging
log_level = logging.root.level - 10 * args.verbose log_level = logging.root.level - 10 * args.verbose
log_format='%(asctime)s %(levelname)8s [%(threadName)s] %(name)s: %(message)s' 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) 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() GObject.threads_init()
import ui
args.notifications &= args.systray args.notifications &= args.systray
if args.notifications: if args.notifications:
ui.notify.init(APP_TITLE) args.notifications &= ui.notify.init(APP_TITLE)
from watcher import Watcher import watcher
watcher = Watcher(APP_TITLE, ui.notify.show if args.notifications else None) tray_icon = None
watcher.start() 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) def _ui_update(receiver, tray_icon, window):
window.set_icon_name(APP_TITLE + '-fail') 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: if args.systray:
tray_icon = ui.icon.create(APP_TITLE, (ui.window.toggle, window)) def _toggle_notifications(item):
tray_icon.set_from_icon_name(APP_TITLE + '-fail') # 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: else:
tray_icon = None
window.present() window.present()
from threading import Thread from gi.repository import Gtk
status_check = Thread(group=APP_TITLE, name='StatusCheck', target=_status_check, args=(watcher, tray_icon, window))
status_check.daemon = True
status_check.start()
Gtk.main() Gtk.main()
watcher.stop() w.stop()
ui.notify.set_active(False) ui.notify.uninit()

View File

@ -5,7 +5,7 @@
from gi.repository import Gtk from gi.repository import Gtk
def create(title, click_action=None): def create(title, click_action=None, actions=None):
icon = Gtk.StatusIcon() icon = Gtk.StatusIcon()
icon.set_title(title) icon.set_title(title)
icon.set_name(title) icon.set_name(title)
@ -19,9 +19,30 @@ def create(title, click_action=None):
icon.connect('activate', click_action) icon.connect('activate', click_action)
menu = Gtk.Menu() menu = Gtk.Menu()
item = Gtk.MenuItem('Quit')
item.connect('activate', Gtk.main_quit) if actions:
menu.append(item) 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() menu.show_all()
icon.connect('popup_menu', icon.connect('popup_menu',
@ -32,8 +53,27 @@ def create(title, click_action=None):
return icon return icon
def update(icon, receiver, tooltip=None, icon_name=None): def update(icon, receiver, icon_name=None):
if tooltip is not None:
icon.set_tooltip_markup(tooltip)
if icon_name is not None: if icon_name is not None:
icon.set_from_icon_name(icon_name) 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: try:
import notify2 as _notify from gi.repository import Notify
from time import time as timestamp from gi.repository import Gtk
available = True # assumed to be working since the import succeeded from logitech.devices.constants import STATUS
_active = False # not yet active
_app_title = None # 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 = {} _notifications = {}
def init(app_title, active=True): def init(app_title=None):
"""Init the notifications system.""" """Init the notifications system."""
global _app_title global available
_app_title = app_title
return set_active(active)
def set_active(active=True):
global available, _active
if available: if available:
if active: logging.info("starting desktop notifications")
if not _active: if not Notify.is_initted():
try: try:
_notify.init(_app_title) return Notify.init(app_title or Notify.get_app_name())
_active = True except:
except: logging.exception("initializing desktop notifications")
logging.exception("initializing desktop notifications") available = False
available = False return available and Notify.is_initted()
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
def active(): def uninit():
return _active 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.""" """Show a notification with title and text."""
if available and _active: if available and Notify.is_initted():
n = None summary = dev.device_name
if title in _notifications:
n = _notifications[title]
if timestamp() - n.timestamp > _TIMEOUT:
del _notifications[title]
n = None
# if a notification with same name is already visible, reuse it to avoid spamming
n = _notifications.get(summary)
if n is None: if n is None:
n = _notify.Notification(title) n = _notifications[summary] = Notify.Notification()
_notifications[title] = n
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: try:
# logging.debug("showing notification %s", n) # logging.debug("showing %s", n)
n.show() n.show()
except Exception: except Exception:
logging.exception("showing notification %s", n) logging.exception("showing %s", n)
except ImportError: 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 available = False
active = False init = lambda app_title: False
def init(app_title, active=True): return False uninit = lambda: None
def active(): return False show = lambda status_code, title, text: None
def set_active(active=True): return False
def show(status_code, title, text, icon=None): pass

View File

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

View File

@ -2,231 +2,105 @@
# #
# #
from threading import (Thread, Event) from threading import Thread
import time import time
from logging import getLogger as _Logger import logging
from collections import namedtuple
from logitech.unifying_receiver import (api, base) from logitech.devices.constants import STATUS
from logitech.unifying_receiver.listener import EventsListener from receiver import Receiver
from logitech import devices
from logitech.devices import constants as C
_l = _Logger('watcher') _DUMMY_RECEIVER = namedtuple('_DUMMY_RECEIVER', ['NAME', 'kind', 'status', 'status_text', 'max_devices', 'devices'])
_DUMMY_RECEIVER.__nonzero__ = lambda _: False
_UNIFYING_RECEIVER = 'Unifying Receiver' _DUMMY_RECEIVER.device_name = Receiver.NAME
_NO_RECEIVER = 'Receiver not found.' DUMMY = _DUMMY_RECEIVER(Receiver.NAME, Receiver.NAME, STATUS.UNAVAILABLE, 'Receiver not found.', Receiver.max_devices, {})
_INITIALIZING = 'Initializing...'
_SCANNING = 'Scanning...'
_NO_DEVICES = 'No devices found.'
_OKAY = 'Status ok.'
class _DevStatus(api.AttachedDeviceInfo): def _sleep(seconds, granularity, breakout=lambda: False):
code = C.STATUS.UNKNOWN for index in range(0, int(seconds / granularity)):
text = _INITIALIZING if breakout():
return
def __str__(self): time.sleep(granularity)
return 'DevStatus(%d,%s,%d)' % (self.number, self.name, self.code)
class Watcher(Thread): class Watcher(Thread):
"""Keeps a map of all attached devices and their statuses.""" """Keeps an active receiver object if possible, and updates the UI when
def __init__(self, apptitle, notify=None): necessary.
"""
def __init__(self, apptitle, update_ui, notify=None):
super(Watcher, self).__init__(group=apptitle, name='Watcher') super(Watcher, self).__init__(group=apptitle, name='Watcher')
self.daemon = True self.daemon = True
self._active = False self._active = False
self.listener = None self.update_ui = update_ui
self.no_receiver = Event() self.notify = notify or (lambda d: None)
self.rstatus = _DevStatus(0, 0xFF, 'UR', _UNIFYING_RECEIVER, ()) self.receiver = DUMMY
self.rstatus.max_devices = api.C.MAX_ATTACHED_DEVICES
self.rstatus.pair = None
self.devices = {}
self.notify = notify
self.status_changed = Event()
def run(self): def run(self):
self._active = True self._active = True
notify_missing = True
while self._active: while self._active:
if self.listener is None: if self.receiver == DUMMY:
receiver = api.open() r = Receiver.open()
if receiver: if r:
self._device_status_changed(self.rstatus, C.STATUS.BOOTING, _INITIALIZING) 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 # give it some time to read all devices
base.request(receiver, 0xFF, b'\x80\x00', b'\x00\x01') and r.status_changed.clear()
base.request(receiver, 0xFF, b'\x81\x02')) _sleep(8, 0.4, r.status_changed.is_set)
if init: if r.devices:
_l.debug("receiver initialized ok") logging.info("%d device(s) found", len(r.devices))
for d in r.devices.values():
self.notify(d)
else: 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.receiver = r
notify_missing = True
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)
else: 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() if self._active:
self.no_receiver.clear() if self.receiver:
else: logging.debug("waiting for status_changed")
self._device_status_changed(self.rstatus, C.STATUS.UNAVAILABLE, _NO_RECEIVER) sc = self.receiver.status_changed
time.sleep(3) 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: if self.receiver:
self.listener.stop() self.receiver.close()
self.listener = None
def stop(self): def stop(self):
if self._active: if self._active:
_l.debug("stopping %s", self) logging.info("stopping %s", self)
self._active = False 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() 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): def _events_callback(self, event):
if event.code == 0xFF and event.devnumber == 0xFF and event.data is None: logging.warn("don't know how to handle event %s", event)
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)

View File

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

View File

@ -4,50 +4,49 @@
import logging import logging
from . import k750 from .constants import (STATUS, PROPS)
from . import constants as C from ..unifying_receiver.constants import (FEATURE, BATTERY_STATUS)
from ..unifying_receiver import api as _api from ..unifying_receiver import api as _api
# #
# #
# #
_REQUEST_STATUS_FUNCTIONS = { _DEVICE_MODULES = {}
C.NAME.K750: k750.request_status,
}
_PROCESS_EVENT_FUNCTIONS = { def _module(device_name):
C.NAME.K750: k750.process_event, 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): def default_request_status(devinfo, listener=None):
if _api.C.FEATURE.BATTERY in devinfo.features: if FEATURE.BATTERY in devinfo.features:
if listener is None: if listener:
reply = _api.get_device_battery_level(devinfo.handle, devinfo.number, features=devinfo.features)
elif listener:
reply = listener.request(_api.get_device_battery_level, devinfo.number, features=devinfo.features) reply = listener.request(_api.get_device_battery_level, devinfo.number, features=devinfo.features)
else: else:
reply = None reply = _api.get_device_battery_level(devinfo.handle, devinfo.number, features=devinfo.features)
if reply: if reply:
discharge, dischargeNext, status = 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): 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 = devinfo.features[feature_index]
feature_function = ord(data[1:2]) & 0xF0 feature_function = ord(data[1:2]) & 0xF0
if feature == _api.C.FEATURE.BATTERY: if feature == FEATURE.BATTERY:
if feature_function == 0: if feature_function == 0:
discharge = ord(data[2:3]) discharge = ord(data[2:3])
status = _api.C.BATTERY_STATUS[ord(data[3:4])] status = BATTERY_STATUS[ord(data[3:4])]
return C.STATUS.CONNECTED, {C.PROPS.BATTERY_LEVEL: discharge, C.PROPS.BATTERY_STATUS: status} 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: if feature_function == 0:
logging.debug('reprogrammable key: %s', repr(data)) logging.debug('reprogrammable key: %s', repr(data))
# TODO # TODO
pass pass
# ? # ?
elif feature == _api.C.FEATURE.WIRELESS: elif feature == FEATURE.WIRELESS:
if feature_function == 0: if feature_function == 0:
logging.debug("wireless status: %s", repr(data)) logging.debug("wireless status: %s", repr(data))
if data[2:5] == b'\x01\x01\x01':
return STATUS.CONNECTED
# TODO # TODO
pass pass
# ? # ?
@ -86,9 +87,10 @@ def request_status(devinfo, listener=None):
:param listener: the EventsListener that will be used to send the request, :param listener: the EventsListener that will be used to send the request,
and which will receive the status events from the device. and which will receive the status events from the device.
""" """
if devinfo.name in _REQUEST_STATUS_FUNCTIONS: m = _module(devinfo.name)
return _REQUEST_STATUS_FUNCTIONS[devinfo.name](devinfo, listener) if m and 'request_status' in m.__dict__:
return default_request_status(devinfo, listener) or ping(devinfo, listener) return m.request_status(devinfo, listener)
return default_request_status(devinfo, listener)
def process_event(devinfo, data, listener=None): def process_event(devinfo, data, listener=None):
@ -101,5 +103,6 @@ def process_event(devinfo, data, listener=None):
if default_result is not None: if default_result is not None:
return default_result return default_result
if devinfo.name in _PROCESS_EVENT_FUNCTIONS: m = _module(devinfo.name)
return _PROCESS_EVENT_FUNCTIONS[devinfo.name](devinfo, data, listener) 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_NAME = {
STATUS.UNKNOWN: '...',
STATUS.UNAVAILABLE: 'inactive', STATUS.UNAVAILABLE: 'inactive',
STATUS.BOOTING: 'initializing', STATUS.BOOTING: 'initializing',
STATUS.CONNECTED: 'connected', STATUS.CONNECTED: 'connected',
} }
# device properties that may be reported
PROPS = type('PROPS', (), PROPS = type('PROPS', (),
dict( dict(
TEXT='text',
BATTERY_LEVEL='battery_level', BATTERY_LEVEL='battery_level',
BATTERY_STATUS='battery_status', BATTERY_STATUS='battery_status',
LIGHT_LEVEL='light_level', LIGHT_LEVEL='light_level',
)) ))
# when the receiver reports a device that is not connected
NAME = type('NAME', (), # (and thus cannot be queried), guess the name and type
dict( # based on this table
M315='Wireless Mouse M315', NAMES = {
M325='Wireless Mouse M325', 'M315': ('Wireless Mouse M315', 'mouse'),
M510='Wireless Mouse M510', 'M325': ('Wireless Mouse M325', 'mouse'),
M515='Couch Mouse M515', 'M510': ('Wireless Mouse M510', 'mouse'),
M570='Wireless Trackball M570', 'M515': ('Couch Mouse M515', 'mouse'),
K270='Wireless Keyboard K270', 'M570': ('Wireless Trackball M570', 'trackball'),
K350='Wireless Keyboard K350', 'K270': ('Wireless Keyboard K270', 'keyboard'),
K750='Wireless Solar Keyboard K750', 'K350': ('Wireless Keyboard K350', 'keyboard'),
K800='Wireless Illuminated Keyboard K800', 'K750': ('Wireless Solar Keyboard K750', 'keyboard'),
)) 'K800': ('Wireless Illuminated Keyboard K800', 'keyboard'),
}
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

View File

@ -5,40 +5,33 @@
import logging import logging
from struct import unpack as _unpack 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 ..unifying_receiver import api as _api
from . import constants as C
# #
# #
# #
_CHARGE_LEVELS = (10, 25, 256)
def _charge_status(data, hasLux=False): def _charge_status(data, hasLux=False):
charge, lux = _unpack('!BH', data[2:5]) charge, lux = _unpack('!BH', data[2:5])
d = {}
_CHARGE_LEVELS = (10, 25, 256)
for i in range(0, len(_CHARGE_LEVELS)): for i in range(0, len(_CHARGE_LEVELS)):
if charge < _CHARGE_LEVELS[i]: if charge < _CHARGE_LEVELS[i]:
charge_index = i charge_index = i
break break
d[C.PROPS.BATTERY_LEVEL] = charge
text = 'Battery %d%%' % charge
if hasLux: return 0x10 << charge_index, {
d[C.PROPS.LIGHT_LEVEL] = lux PROPS.BATTERY_LEVEL: charge,
text = 'Light: %d lux' % lux + ', ' + text PROPS.LIGHT_LEVEL: lux if hasLux else None,
else: }
d[C.PROPS.LIGHT_LEVEL] = None
d[C.PROPS.TEXT] = text
return 0x10 << charge_index, d
def request_status(devinfo, listener=None): def request_status(devinfo, listener=None):
def _trigger_solar_charge_events(handle, devinfo): def _trigger_solar_charge_events(handle, devinfo):
return _api.request(handle, devinfo.number, 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) features=devinfo.features)
if listener is None: if listener is None:
reply = _trigger_solar_charge_events(devinfo.handle, devinfo) reply = _trigger_solar_charge_events(devinfo.handle, devinfo)
@ -48,7 +41,7 @@ def request_status(devinfo, listener=None):
reply = 0 reply = 0
if reply is None: if reply is None:
return C.STATUS.UNAVAILABLE return STATUS.UNAVAILABLE
def process_event(devinfo, data, listener=None): def process_event(devinfo, data, listener=None):
@ -68,4 +61,4 @@ def process_event(devinfo, data, listener=None):
# wireless device status # wireless device status
if data[2:5] == b'\x01\x01\x01': if data[2:5] == b'\x01\x01\x01':
logging.debug("Keyboard just started") 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 struct import unpack as _unpack
from binascii import hexlify as _hexlify 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 . 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 _LOG_LEVEL = 5
@ -83,7 +86,7 @@ def request(handle, devnumber, feature, function=b'\x00', params=b'', features=N
""" """
feature_index = None feature_index = None
if feature == C.FEATURE.ROOT: if feature == FEATURE.ROOT:
feature_index = b'\x00' feature_index = b'\x00'
else: else:
if features is None: 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)) feature_index = _pack('!B', features.index(feature))
if feature_index is None: if feature_index is None:
_l.warn("(%d) feature <%s:%s> not supported", devnumber, _hexlify(feature), C.FEATURE_NAME[feature]) _l.warn("(%d) feature <%s:%s> not supported", devnumber, _hexlify(feature), FEATURE_NAME[feature])
raise E.FeatureNotSupported(devnumber, feature) raise _FeatureNotSupported(devnumber, feature)
if type(function) == int: if type(function) == int:
function = _pack('!B', function) function = _pack('!B', function)
@ -129,7 +132,7 @@ def find_device_by_name(handle, name):
""" """
_l.log(_LOG_LEVEL, "searching for device '%s'", 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) features = get_device_features(handle, devnumber)
if features: if features:
d_name = get_device_name(handle, devnumber, features) d_name = get_device_name(handle, devnumber, features)
@ -146,7 +149,7 @@ def list_devices(handle):
devices = [] 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) features = get_device_features(handle, device)
if features: if features:
devices.append(get_device_info(handle, device, features=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: if features is None:
return 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 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) _l.log(_LOG_LEVEL, "(%d) found device %s", devnumber, devinfo)
return 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. :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: if len(feature) != 2:
raise ValueError("invalid feature <%s>: it must be a two-byte string" % feature) raise ValueError("invalid feature <%s>: it must be a two-byte string" % feature)
# FEATURE.ROOT should always be available for any attached devices # 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: if reply:
# only consider active and supported features # only consider active and supported features
feature_index = ord(reply[0:1]) feature_index = ord(reply[0:1])
@ -190,18 +193,18 @@ def get_feature_index(handle, devnumber, feature):
if _l.isEnabledFor(_LOG_LEVEL): if _l.isEnabledFor(_LOG_LEVEL):
if feature_flags: if feature_flags:
_l.log(_LOG_LEVEL, "(%d) feature <%s:%s> has index %d: %s", _l.log(_LOG_LEVEL, "(%d) feature <%s:%s> has index %d: %s",
devnumber, _hexlify(feature), C.FEATURE_NAME[feature], feature_index, devnumber, _hexlify(feature), FEATURE_NAME[feature], feature_index,
','.join([C.FEATURE_FLAGS[k] for k in C.FEATURE_FLAGS if feature_flags & k])) ','.join([FEATURE_FLAGS[k] for k in FEATURE_FLAGS if feature_flags & k]))
else: 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: # if feature_flags:
# raise E.FeatureNotSupported(devnumber, feature) # raise E.FeatureNotSupported(devnumber, feature)
return feature_index return feature_index
_l.warn("(%d) feature <%s:%s> not supported by the device", devnumber, _hexlify(feature), C.FEATURE_NAME[feature]) _l.warn("(%d) feature <%s:%s> not supported by the device", devnumber, _hexlify(feature), FEATURE_NAME[feature])
raise E.FeatureNotSupported(devnumber, feature) raise _FeatureNotSupported(devnumber, feature)
def get_device_features(handle, devnumber): def get_device_features(handle, devnumber):
@ -214,7 +217,7 @@ def get_device_features(handle, devnumber):
# get the index of the FEATURE_SET # get the index of the FEATURE_SET
# FEATURE.ROOT should always be available for all devices # 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: if fs_index is None:
# _l.warn("(%d) FEATURE_SET not available", device) # _l.warn("(%d) FEATURE_SET not available", device)
return None return None
@ -246,15 +249,15 @@ def get_device_features(handle, devnumber):
if _l.isEnabledFor(_LOG_LEVEL): if _l.isEnabledFor(_LOG_LEVEL):
if feature_flags: if feature_flags:
_l.log(_LOG_LEVEL, "(%d) feature <%s:%s> at index %d: %s", _l.log(_LOG_LEVEL, "(%d) feature <%s:%s> at index %d: %s",
devnumber, _hexlify(feature), C.FEATURE_NAME[feature], index, devnumber, _hexlify(feature), FEATURE_NAME[feature], index,
','.join([C.FEATURE_FLAGS[k] for k in C.FEATURE_FLAGS if feature_flags & k])) ','.join([FEATURE_FLAGS[k] for k in FEATURE_FLAGS if feature_flags & k]))
else: 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: while features[-1] is None:
del features[-1] del features[-1]
return features return tuple(features)
def get_device_firmware(handle, devnumber, features=None): 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. :returns: a list of FirmwareInfo tuples, ordered by firmware layer.
""" """
def _makeFirmwareInfo(level, type, name=None, version=None, build=None, extras=None): def _makeFirmwareInfo(level, kind, name=None, version=None, build=None, extras=None):
return FirmwareInfo(level, type, name, version, build, extras) 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: if fw_count:
fw_count = ord(fw_count[:1]) fw_count = ord(fw_count[:1])
fw = [] fw = []
for index in range(0, fw_count): 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: if fw_info:
fw_level = ord(fw_info[:1]) & 0x0F level = ord(fw_info[:1]) & 0x0F
if fw_level == 0 or fw_level == 1: if level == 0 or level == 1:
fw_type = C.FIRMWARE_TYPE[fw_level] kind = FIRMWARE_KIND[level]
name, = _unpack('!3s', fw_info[1:4]) name, = _unpack('!3s', fw_info[1:4])
name = name.decode('ascii') name = name.decode('ascii')
version = _hexlify(fw_info[4:6]) 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]) build, = _unpack('!H', fw_info[6:8])
extras = fw_info[9:].rstrip(b'\x00') extras = fw_info[9:].rstrip(b'\x00')
if extras: 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: else:
fw_info = _makeFirmwareInfo(level=fw_level, type=fw_type, name=name, version=version, build=build) fw_info = _makeFirmwareInfo(level, kind, name, version, build)
elif fw_level == 2: elif level == 2:
fw_info = _makeFirmwareInfo(level=2, type=C.FIRMWARE_TYPE[2], version=ord(fw_info[1:2])) fw_info = _makeFirmwareInfo(2, FIRMWARE_KIND[2], version=ord(fw_info[1:2]))
else: else:
fw_info = _makeFirmwareInfo(level=fw_level, type=C.FIRMWARE_TYPE[-1]) fw_info = _makeFirmwareInfo(level, FIRMWARE_KIND[-1])
fw.append(fw_info) fw.append(fw_info)
_l.log(_LOG_LEVEL, "(%d) firmware %s", devnumber, 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. """Reads a device's type.
:see DEVICE_TYPE: :see DEVICE_KIND:
:returns: a string describing the device type, or ``None`` if the device is :returns: a string describing the device type, or ``None`` if the device is
not available or does not support the ``NAME`` feature. not available or does not support the ``NAME`` feature.
""" """
d_type = request(handle, devnumber, C.FEATURE.NAME, function=b'\x20', features=features) d_kind = request(handle, devnumber, FEATURE.NAME, function=b'\x20', features=features)
if d_type: if d_kind:
d_type = ord(d_type[:1]) d_kind = ord(d_kind[:1])
_l.log(_LOG_LEVEL, "(%d) device type %d = %s", devnumber, d_type, C.DEVICE_TYPE[d_type]) _l.log(_LOG_LEVEL, "(%d) device type %d = %s", devnumber, d_kind, DEVICE_KIND[d_kind])
return C.DEVICE_TYPE[d_type] return DEVICE_KIND[d_kind]
def get_device_name(handle, devnumber, features=None): 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 :returns: a string with the device name, or ``None`` if the device is not
available or does not support the ``NAME`` feature. 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: if name_length:
name_length = ord(name_length[:1]) name_length = ord(name_length[:1])
d_name = b'' d_name = b''
while len(d_name) < name_length: 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: if name_fragment:
name_fragment = name_fragment[:name_length - len(d_name)] name_fragment = name_fragment[:name_length - len(d_name)]
d_name += name_fragment 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. :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: if battery:
discharge, dischargeNext, status = _unpack('!BBB', battery[:3]) discharge, dischargeNext, status = _unpack('!BBB', battery[:3])
_l.log(_LOG_LEVEL, "(%d) battery %d%% charged, next level %d%% charge, status %d = %s", _l.log(_LOG_LEVEL, "(%d) battery %d%% charged, next level %d%% charge, status %d = %s",
devnumber, discharge, dischargeNext, status, C.BATTERY_STATUSE[status]) devnumber, discharge, dischargeNext, status, BATTERY_STATUS[status])
return (discharge, dischargeNext, C.BATTERY_STATUS[status]) return (discharge, dischargeNext, BATTERY_STATUS[status])
def get_device_keys(handle, devnumber, features=None): 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: if count:
keys = [] keys = []
count = ord(count[:1]) count = ord(count[:1])
for index in range(0, count): 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: if keydata:
key, key_task, flags = _unpack('!HHB', keydata[:5]) 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 return keys

View File

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

View File

@ -3,6 +3,7 @@
# #
from binascii import hexlify as _hexlify from binascii import hexlify as _hexlify
from collections import namedtuple
class FallbackDict(dict): class FallbackDict(dict):
@ -21,20 +22,18 @@ def list2dict(values_list):
return dict(zip(range(0, len(values_list)), 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.""" """Tuple returned by list_devices and find_device_by_name."""
AttachedDeviceInfo = namedtuple('AttachedDeviceInfo', [ AttachedDeviceInfo = namedtuple('AttachedDeviceInfo', [
'handle', 'handle',
'number', 'number',
'type', 'kind',
'name', 'name',
'features']) 'features'])
"""Firmware information.""" """Firmware information."""
FirmwareInfo = namedtuple('FirmwareInfo', [ FirmwareInfo = namedtuple('FirmwareInfo', [
'level', 'level',
'type', 'kind',
'name', 'name',
'version', 'version',
'build', 'build',
@ -49,6 +48,7 @@ ReprogrammableKeyInfo = namedtuple('ReprogrammableKeyInfo', [
'task_name', 'task_name',
'flags']) 'flags'])
class Packet(namedtuple('Packet', ['code', 'devnumber', 'data'])): class Packet(namedtuple('Packet', ['code', 'devnumber', 'data'])):
def __str__(self): def __str__(self):
return 'Packet(0x%02x,%d,%s)' % (self.code, self.devnumber, '' if self.data is None else _hexlify(self.data)) 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' } FEATURE_FLAGS = { 0x20: 'internal', 0x40: 'hidden', 0x80: 'obsolete' }
_DEVICE_TYPES = ('Keyboard', 'Remote Control', 'NUMPAD', 'Mouse', _DEVICE_KINDS = ('keyboard', 'remote control', 'numpad', 'mouse',
'Touchpad', 'Trackball', 'Presenter', 'Receiver') 'touchpad', 'trackball', 'presenter', 'receiver')
"""Possible types of devices connected to an UR.""" """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.""" """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', _BATTERY_STATUSES = ('Discharging (in use)', 'Recharging', 'Almost full',

View File

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

View File

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

View File

@ -31,12 +31,12 @@ def scan_devices(receiver):
return return
for devinfo in devices: 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 # print " Protocol %s" % devinfo.protocol
firmware = api.get_device_firmware(receiver, devinfo.number, features=devinfo.features) firmware = api.get_device_firmware(receiver, devinfo.number, features=devinfo.features)
for fw in firmware: 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)): for index in range(0, len(devinfo.features)):
feature = devinfo.features[index] feature = devinfo.features[index]
@ -77,3 +77,11 @@ if __name__ == '__main__':
break break
else: else:
print ("!! Logitech Unifying Receiver not found.") 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