reworked the receiver and devices into classes
This commit is contained in:
parent
5985105e0e
commit
f2dac70131
4
README
4
README
|
@ -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
|
||||||
|
|
|
@ -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
|
130
app/solaar.py
130
app/solaar.py
|
@ -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()
|
||||||
|
|
|
@ -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:
|
||||||
|
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(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)
|
||||||
|
|
|
@ -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
|
||||||
else:
|
return available and Notify.is_initted()
|
||||||
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
|
|
||||||
|
|
101
app/ui/window.py
101
app/ui/window.py
|
@ -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,9 +145,6 @@ 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.connect('clicked', rstatus.pair)
|
|
||||||
else:
|
|
||||||
pair_button.set_sensitive(False)
|
pair_button.set_sensitive(False)
|
||||||
toolbar.insert(pair_button, 0)
|
toolbar.insert(pair_button, 0)
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
264
app/watcher.py
264
app/watcher.py
|
@ -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:
|
||||||
|
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:
|
else:
|
||||||
self._device_status_changed(self.rstatus, C.STATUS.UNAVAILABLE, _NO_RECEIVER)
|
self.receiver = DUMMY
|
||||||
time.sleep(3)
|
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)
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 |
Loading…
Reference in New Issue