reworked the way tasks are processed by the listener

This commit is contained in:
Daniel Pavel 2012-10-27 18:40:54 +03:00
parent 62a91b56d2
commit e7bb599689
15 changed files with 412 additions and 325 deletions

View File

@ -3,7 +3,6 @@
# #
from logging import getLogger as _Logger from logging import getLogger as _Logger
_LOG_LEVEL = 6
from threading import Event as _Event from threading import Event as _Event
from struct import pack as _pack from struct import pack as _pack
@ -18,22 +17,113 @@ from logitech.devices.constants import (STATUS, STATUS_NAME, PROPS, NAMES)
# #
# #
class _FeaturesArray(object):
__slots__ = ('device', 'features', 'supported')
def __init__(self, device):
self.device = device
self.features = None
self.supported = True
def _check(self):
if not self.supported:
return False
if self.features is not None:
return True
if self.device.status >= STATUS.CONNECTED:
handle = self.device.receiver.handle
try:
index = _api.get_feature_index(handle, self.device.number, _api.FEATURE.FEATURE_SET)
except _api._FeatureNotSupported:
index = None
if index is None:
self.supported = False
else:
count = _base.request(handle, self.device.number, _pack('!B', index) + b'\x00')
if count is None:
self.supported = False
else:
count = ord(count[:1])
self.features = [None] * (1 + count)
self.features[0] = _api.FEATURE.ROOT
self.features[index] = _api.FEATURE.FEATURE_SET
return True
return False
__bool__ = __nonzero__ = _check
def __getitem__(self, index):
if not self._check():
return None
if index < 0 or index >= len(self.features):
raise IndexError
if self.features[index] is None:
fs_index = self.features.index(_api.FEATURE.FEATURE_SET)
feature = _base.request(self.device.receiver.handle, self.device.number, _pack('!BB', fs_index, 0x10), _pack('!B', index))
if feature is not None:
self.features[index] = feature[:2]
return self.features[index]
def __contains__(self, value):
if self._check():
if value in self.features:
return True
for index in range(0, len(self.features)):
f = self.features[index] or self.__getitem__(index)
assert f is not None
if f == value:
return True
if f > value:
break
return False
def index(self, value):
if self._check():
if self.features is not None and value in self.features:
return self.features.index(value)
raise ValueError("%s not in list" % repr(value))
def __iter__(self):
if self._check():
yield _api.FEATURE.ROOT
index = 1
last_index = len(self.features)
while index < last_index:
yield self.__getitem__(index)
index += 1
def __len__(self):
return len(self.features) if self._check() else 0
class DeviceInfo(object): class DeviceInfo(object):
"""A device attached to the receiver. """A device attached to the receiver.
""" """
def __init__(self, receiver, number, status=STATUS.UNKNOWN): def __init__(self, receiver, number, pair_code, status=STATUS.UNKNOWN):
self.LOG = _Logger("Device[%d]" % number) self.LOG = _Logger("Device[%d]" % number)
self.receiver = receiver self.receiver = receiver
self.number = number self.number = number
self._pair_code = pair_code
self._serial = None
self._codename = None
self._name = None self._name = None
self._kind = None self._kind = None
self._serial = None
self._firmware = None self._firmware = None
self._features = None
self._status = status self._status = status
self.props = {} self.props = {}
self.features = _FeaturesArray(self)
@property @property
def handle(self): def handle(self):
return self.receiver.handle return self.receiver.handle
@ -71,8 +161,8 @@ class DeviceInfo(object):
def name(self): def name(self):
if self._name is None: if self._name is None:
if self._status >= STATUS.CONNECTED: if self._status >= STATUS.CONNECTED:
self._name = self.receiver.call_api(_api.get_device_name, self.number, self.features) self._name = _api.get_device_name(self.receiver.handle, self.number, self.features)
return self._name or '?' return self._name or self.codename
@property @property
def device_name(self): def device_name(self):
@ -81,33 +171,42 @@ class DeviceInfo(object):
@property @property
def kind(self): def kind(self):
if self._kind is None: if self._kind is None:
if self._status >= STATUS.CONNECTED: if self._status < STATUS.CONNECTED:
self._kind = self.receiver.call_api(_api.get_device_kind, self.number, self.features) codename = self.codename
if codename in NAMES:
self._kind = NAMES[codename][-1]
else:
self._kind = _api.get_device_kind(self.receiver.handle, self.number, self.features)
return self._kind or '?' return self._kind or '?'
@property @property
def serial(self): def serial(self):
if self._serial is None: if self._serial is None:
if self._status >= STATUS.CONNECTED: # dodgy
pass b = bytearray(self._pair_code)
b[0] -= 0x10
serial = _base.request(self.receiver.handle, 0xFF, b'\x83\xB5', bytes(b))
if serial:
self._serial = _base._hex(serial[1:5])
return self._serial or '?' return self._serial or '?'
@property
def codename(self):
if self._codename is None:
codename = _base.request(self.receiver.handle, 0xFF, b'\x83\xB5', self._pair_code)
if codename:
self._codename = codename[2:].rstrip(b'\x00').decode('ascii')
return self._codename or '?'
@property @property
def firmware(self): def firmware(self):
if self._firmware is None: if self._firmware is None:
if self._status >= STATUS.CONNECTED: if self._status >= STATUS.CONNECTED:
self._firmware = self.receiver.call_api(_api.get_device_firmware, self.number, self.features) self._firmware = _api.get_device_firmware(self.receiver.handle, self.number, self.features)
return self._firmware or () return self._firmware or ()
@property
def features(self):
if self._features is None:
if self._status >= STATUS.CONNECTED:
self._features = self.receiver.call_api(_api.get_device_features, self.number)
return self._features or ()
def ping(self): def ping(self):
return self.receiver.call_api(_api.ping, self.number) return _api.ping(self.receiver.handle, self.number)
def process_event(self, code, data): def process_event(self, code, data):
if code == 0x10 and data[:1] == b'\x8F': if code == 0x10 and data[:1] == b'\x8F':
@ -224,20 +323,20 @@ class Receiver(_listener.EventsListener):
return self.NAME return self.NAME
def count_devices(self): def count_devices(self):
return self.call_api(_api.count_devices) return _api.count_devices(self._handle)
@property @property
def serial(self): def serial(self):
if self._serial is None: if self._serial is None:
if self: if self:
self._serial, self._firmware = self.call_api(_api.get_receiver_info) self._serial, self._firmware = _api.get_receiver_info(self._handle)
return self._serial or '?' return self._serial or '?'
@property @property
def firmware(self): def firmware(self):
if self._firmware is None: if self._firmware is None:
if self: if self:
self._serial, self._firmware = self.call_api(_api.get_receiver_info) self._serial, self._firmware = _api.get_receiver_info(self._handle)
return self._firmware or ('?', '?') return self._firmware or ('?', '?')
@ -303,30 +402,12 @@ class Receiver(_listener.EventsListener):
self.LOG.warn("don't know how to handle device status 0x%02X: %s", state_code, event) self.LOG.warn("don't know how to handle device status 0x%02X: %s", state_code, event)
return None return None
dev = DeviceInfo(self, event.devnumber, state) return DeviceInfo(self, event.devnumber, event.data[4:5], 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(0xFF, b'\x83\xB5', event.data[4:5])
if dev_id:
shortname = dev_id[2:].rstrip(b'\x00').decode('ascii')
if shortname in NAMES:
dev._name, dev._kind = NAMES[shortname]
else:
self.LOG.warn("could not identify inactive device %d: %s", event.devnumber, shortname)
b = bytearray(event.data[4:5])
b[0] -= 0x10
serial = self.request(0xFF, b'\x83\xB5', bytes(b))
if serial:
dev._serial = _base._hex(serial[1:5])
return dev
def unpair_device(self, number): def unpair_device(self, number):
if number in self.devices: if number in self.devices:
dev = self.devices[number] dev = self.devices[number]
reply = self.request(0xFF, b'\x80\xB2', _pack('!BB', 0x03, number)) reply = _base.request(self._handle, 0xFF, b'\x80\xB2', _pack('!BB', 0x03, number))
if reply: if reply:
self.LOG.debug("remove device %s => %s", dev, _base._hex(reply)) self.LOG.debug("remove device %s => %s", dev, _base._hex(reply))
del self.devices[number] del self.devices[number]
@ -346,7 +427,7 @@ class Receiver(_listener.EventsListener):
:returns: An open file handle for the found receiver, or ``None``. :returns: An open file handle for the found receiver, or ``None``.
""" """
for rawdevice in _base.list_receiver_devices(): for rawdevice in _base.list_receiver_devices():
_Logger("receiver").log(_LOG_LEVEL, "checking %s", rawdevice) _Logger("receiver").debug("checking %s", rawdevice)
handle = _base.try_open(rawdevice.path) handle = _base.try_open(rawdevice.path)
if handle: if handle:
receiver = Receiver(rawdevice.path, handle) receiver = Receiver(rawdevice.path, handle)

View File

@ -32,10 +32,8 @@ def _parse_arguments():
import logging import logging
log_level = logging.root.level - 10 * args.verbose log_level = logging.root.level - 10 * args.verbose
log_format='%(asctime)s.%(msecs)03d %(levelname)8s [%(threadName)s] %(name)s: %(message)s' log_format='%(asctime)s %(levelname)8s [%(threadName)s] %(name)s: %(message)s'
logging.basicConfig(level=log_level if log_level > 0 else 1, logging.basicConfig(level=max(log_level, 1), format=log_format)
format=log_format,
datefmt='%H:%M:%S')
return args return args
@ -83,3 +81,6 @@ if __name__ == '__main__':
w.stop() w.stop()
ui.notify.uninit() ui.notify.uninit()
import logging
logging.shutdown()

View File

@ -19,10 +19,18 @@ def _toggle_action(name, label, function, *args):
return action return action
# def wrap_action(action, prefire):
# def _wrap(aw, aa):
# prefire(aa)
aa.activate()
wrapper = _action(action.get_name(), action.get_label(), None)
wrapper.set_icon_name(action.get_icon_name())
wrapper.connect('activate', _wrap, action)
return wrapper
#
#
#
def _toggle_notifications(action): def _toggle_notifications(action):
if action.get_active(): if action.get_active():
@ -57,9 +65,6 @@ import pairing
def _pair_device(action): def _pair_device(action):
action.set_sensitive(False) action.set_sensitive(False)
pair_dialog = ui.pair_window.create(action, pairing.state) pair_dialog = ui.pair_window.create(action, pairing.state)
action.window.present()
pair_dialog.set_transient_for(action.window)
pair_dialog.set_destroy_with_parent(action.window)
pair_dialog.set_modal(True) pair_dialog.set_modal(True)
pair_dialog.present() pair_dialog.present()
pair = _action('add', 'Pair new device', _pair_device) pair = _action('add', 'Pair new device', _pair_device)
@ -69,9 +74,11 @@ def _unpair_device(action):
dev = pairing.state.device(action.devnumber) dev = pairing.state.device(action.devnumber)
action.devnumber = 0 action.devnumber = 0
if dev: if dev:
q = Gtk.MessageDialog.new(action.window, qdialog = Gtk.MessageDialog(action.window, 0,
Gtk.MessageType.QUESTION, Gtk.ButtonsType.YES_NO, Gtk.MessageType.QUESTION, Gtk.ButtonsType.YES_NO,
'Unpair device <b>%s</b>?', dev.name) "Unpair device '%s' ?" % dev.name)
if q.run() == Gtk.ResponseType.YES: choice = qdialog.run()
qdialog.destroy()
if choice == Gtk.ResponseType.YES:
pairing.state.unpair(dev.number) pairing.state.unpair(dev.number)
unpair = _action('remove', 'Unpair', _unpair_device) unpair = _action('remove', 'Unpair', _unpair_device)

View File

@ -2,7 +2,7 @@
# #
# #
from gi.repository import (Gtk, Gdk) from gi.repository import (Gtk, Gdk, GObject)
import ui import ui
from logitech.devices.constants import (STATUS, PROPS) from logitech.devices.constants import (STATUS, PROPS)
@ -22,6 +22,7 @@ def _toggle_info_button(label, widget):
action = ui.action._toggle_action('info', label, toggle, widget) action = ui.action._toggle_action('info', label, toggle, widget)
return action.create_tool_item() return action.create_tool_item()
def _receiver_box(name): def _receiver_box(name):
icon = Gtk.Image.new_from_icon_name(name, _SMALL_DEVICE_ICON_SIZE) icon = Gtk.Image.new_from_icon_name(name, _SMALL_DEVICE_ICON_SIZE)
@ -65,7 +66,7 @@ def _receiver_box(name):
return frame return frame
def _device_box(): def _device_box(index):
icon = Gtk.Image.new_from_icon_name('image-missing', _DEVICE_ICON_SIZE) icon = Gtk.Image.new_from_icon_name('image-missing', _DEVICE_ICON_SIZE)
icon.set_name('icon') icon.set_name('icon')
icon.set_alignment(0.5, 0) icon.set_alignment(0.5, 0)
@ -111,7 +112,10 @@ def _device_box():
info_box.add(info_label) info_box.add(info_label)
toolbar.insert(_toggle_info_button('Device info', info_box), 0) toolbar.insert(_toggle_info_button('Device info', info_box), 0)
toolbar.insert(ui.action.unpair.create_tool_item(), -1) def _set_number(action):
action.devnumber = index
unpair_action = ui.action.wrap_action(ui.action.unpair, _set_number)
toolbar.insert(unpair_action.create_tool_item(), -1)
vbox = Gtk.VBox(homogeneous=False, spacing=4) vbox = Gtk.VBox(homogeneous=False, spacing=4)
vbox.pack_start(label, True, True, 0) vbox.pack_start(label, True, True, 0)
@ -131,7 +135,6 @@ def _device_box():
def toggle(window, trigger): def toggle(window, trigger):
# print 'window toggle', window, trigger
if window.get_visible(): if window.get_visible():
position = window.get_position() position = window.get_position()
window.hide() window.hide()
@ -158,7 +161,7 @@ def create(title, name, max_devices, systray=False):
rbox = _receiver_box(name) rbox = _receiver_box(name)
vbox.add(rbox) vbox.add(rbox)
for i in range(1, 1 + max_devices): for i in range(1, 1 + max_devices):
dbox = _device_box() dbox = _device_box(i)
vbox.add(dbox) vbox.add(dbox)
vbox.set_visible(True) vbox.set_visible(True)
@ -209,18 +212,12 @@ def _update_receiver_box(frame, receiver):
def _update_device_box(frame, dev): def _update_device_box(frame, dev):
if dev is None:
frame.set_visible(False)
frame.set_name(_PLACEHOLDER)
return
icon, label, info_label = ui.find_children(frame, 'icon', 'label', 'info-label') icon, label, info_label = ui.find_children(frame, 'icon', 'label', 'info-label')
if frame.get_name() != dev.name: if frame.get_name() != dev.name:
frame.set_name(dev.name) frame.set_name(dev.name)
icon.set_from_icon_name(ui.get_icon(dev.name, dev.kind), _DEVICE_ICON_SIZE) icon.set_from_icon_name(ui.get_icon(dev.name, dev.kind), _DEVICE_ICON_SIZE)
label.set_markup('<b>' + dev.name + '</b>') label.set_markup('<b>' + dev.name + '</b>')
frame.set_visible(True)
status = ui.find_children(frame, 'status') status = ui.find_children(frame, 'status')
status_icons = status.get_children() status_icons = status.get_children()
@ -271,16 +268,21 @@ def _update_device_box(frame, dev):
for b in toolbar.get_children()[:-1]: for b in toolbar.get_children()[:-1]:
b.set_sensitive(True) b.set_sensitive(True)
frame.set_visible(True)
def update(window, receiver): def update(window, receiver):
if window and window.get_child(): window.set_icon_name(ui.appicon(receiver.status))
window.set_icon_name(ui.appicon(receiver.status))
vbox = window.get_child() vbox = window.get_child()
controls = list(vbox.get_children()) controls = list(vbox.get_children())
_update_receiver_box(controls[0], receiver) GObject.idle_add(_update_receiver_box, controls[0], receiver)
for index in range(1, len(controls)): for index in range(1, len(controls)):
dev = receiver.devices[index] if index in receiver.devices else None dev = receiver.devices[index] if index in receiver.devices else None
_update_device_box(controls[index], dev) frame = controls[index]
if dev is None:
frame.set_visible(False)
frame.set_name(_PLACEHOLDER)
else:
GObject.idle_add(_update_device_box, frame, dev)

View File

@ -65,31 +65,19 @@ class Watcher(Thread):
continue continue
_l.info("receiver %s ", r) _l.info("receiver %s ", r)
self.update_ui(r)
self.notify(r)
if r.count_devices() > 0:
# give it some time to read all devices
r.status_changed.clear()
_sleep(8, 0.4, r.status_changed.is_set)
if r.devices:
_l.info("%d device(s) found", len(r.devices))
for d in r.devices.values():
self.notify(d)
else:
# if no devices found so far, assume none at all
_l.info("no devices found")
r.status = STATUS.CONNECTED
self._receiver = r self._receiver = r
notify_missing = True notify_missing = True
self.update_ui(r)
self.notify(r)
if self._active: if self._active:
if self._receiver: if self._receiver:
_l.debug("waiting for status_changed") _l.debug("waiting for status_changed")
sc = self._receiver.status_changed sc = self._receiver.status_changed
sc.wait() sc.wait()
if not self._active:
break
sc.clear() sc.clear()
if sc.urgent: if sc.urgent:
_l.info("status_changed %s", sc.reason) _l.info("status_changed %s", sc.reason)
@ -103,6 +91,7 @@ class Watcher(Thread):
if self._receiver: if self._receiver:
self._receiver.close() self._receiver.close()
self._receiver = _DUMMY_RECEIVER
def stop(self): def stop(self):
if self._active: if self._active:
@ -112,3 +101,4 @@ class Watcher(Thread):
# break out of an eventual wait() # break out of an eventual wait()
self._receiver.status_changed.reason = None self._receiver.status_changed.reason = None
self._receiver.status_changed.set() self._receiver.status_changed.set()
self.join()

View File

@ -4,4 +4,4 @@ LIB=`dirname "$0"`/../lib
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$LIB/native/`uname -m` export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$LIB/native/`uname -m`
export PYTHONPATH=$LIB export PYTHONPATH=$LIB
exec python -OO -u -m hidapi.hidconsole "$@" exec python -OOu -m hidapi.hidconsole "$@"

View File

@ -4,4 +4,4 @@ LIB=`dirname "$0"`/../lib
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$LIB/native/`uname -m` export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$LIB/native/`uname -m`
export PYTHONPATH=$LIB export PYTHONPATH=$LIB
exec python -OO -m logitech.scanner "$@" exec python -OOu -m logitech.scanner "$@"

View File

@ -9,5 +9,5 @@ export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$LIB/native/`uname -m`
export PYTHONPATH=$APP:$LIB export PYTHONPATH=$APP:$LIB
export XDG_DATA_DIRS=$SHARE:$XDG_DATA_DIRS export XDG_DATA_DIRS=$SHARE:$XDG_DATA_DIRS
exec python3 -OO -m solaar "$@" exec python -OOu -m solaar "$@"
#exec python -OO -m profile -o $TMPDIR/profile.log app/solaar.py "$@" #exec python -OOu -m profile -o $TMPDIR/profile.log app/solaar.py "$@"

View File

@ -32,20 +32,12 @@ def _module(device_name):
def default_request_status(devinfo, listener=None): def default_request_status(devinfo, listener=None):
if FEATURE.BATTERY in devinfo.features: if FEATURE.BATTERY in devinfo.features:
if listener: reply = _api.get_device_battery_level(devinfo.handle, devinfo.number, features=devinfo.features)
reply = listener.call_api(_api.get_device_battery_level, devinfo.number, features=devinfo.features)
else:
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 STATUS.CONNECTED, {PROPS.BATTERY_LEVEL: discharge, PROPS.BATTERY_STATUS: status} return STATUS.CONNECTED, {PROPS.BATTERY_LEVEL: discharge, PROPS.BATTERY_STATUS: status}
if listener: reply = _api.ping(devinfo.handle, devinfo.number)
reply = listener.call_api(_api.ping, devinfo.number)
else:
reply = _api.ping(devinfo.handle, devinfo.number)
return STATUS.CONNECTED if reply else STATUS.UNAVAILABLE return STATUS.CONNECTED if reply else STATUS.UNAVAILABLE

View File

@ -29,17 +29,9 @@ def _charge_status(data, hasLux=False):
def request_status(devinfo, listener=None): def request_status(devinfo, listener=None):
def _trigger_solar_charge_events(handle, devinfo): reply = _api.request(devinfo.handle, devinfo.number,
return _api.request(handle, devinfo.number, feature=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:
reply = _trigger_solar_charge_events(devinfo.handle, devinfo)
elif listener:
reply = listener.call_api(_trigger_solar_charge_events, devinfo)
else:
reply = 0
if reply is None: if reply is None:
return STATUS.UNAVAILABLE return STATUS.UNAVAILABLE
@ -56,9 +48,3 @@ def process_event(devinfo, data, listener=None):
if data[:2] == b'\x09\x20' and data[7:11] == b'GOOD': if data[:2] == b'\x09\x20' and data[7:11] == b'GOOD':
logging.debug("Solar key pressed") logging.debug("Solar key pressed")
return request_status(devinfo, listener) or _charge_status(data) return request_status(devinfo, listener) or _charge_status(data)
if data[:2] == b'\x05\x00':
# wireless device status
if data[2:5] == b'\x01\x01\x01':
logging.debug("Keyboard just started")
return STATUS.CONNECTED

View File

@ -39,7 +39,7 @@ def scan_devices(receiver):
for index in range(0, len(devinfo.features)): for index in range(0, len(devinfo.features)):
feature = devinfo.features[index] feature = devinfo.features[index]
if feature: if feature:
print (" ~ Feature %-20s (%s) at index %d" % (FEATURE_NAME[feature], api._hex(feature), index)) print (" ~ Feature %-20s (%s) at index %02X" % (FEATURE_NAME[feature], api._hex(feature), index))
if FEATURE.BATTERY in devinfo.features: if FEATURE.BATTERY in devinfo.features:
discharge, dischargeNext, status = api.get_device_battery_level(receiver, devinfo.number, features=devinfo.features) discharge, dischargeNext, status = api.get_device_battery_level(receiver, devinfo.number, features=devinfo.features)
@ -75,11 +75,3 @@ 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)

View File

@ -20,12 +20,24 @@ http://julien.danjou.info/blog/2012/logitech-k750-linux-support
http://6xq.net/git/lars/lshidpp.git/plain/doc/ http://6xq.net/git/lars/lshidpp.git/plain/doc/
""" """
import logging
log = logging.getLogger('LUR')
log.propagate = 0
log.setLevel(logging.DEBUG)
if logging.root.level < logging.DEBUG:
handler = logging.FileHandler('lur.log', mode='w')
handler.setFormatter(logging.root.handlers[0].formatter)
else:
handler = logging.NullHandler()
log.addHandler(handler)
del handler
del log
del logging
from .constants import * from .constants import *
from .exceptions import * from .exceptions import *
from .api import * from .api import *
import logging
logging.addLevelName(4, 'UR_TRACE')
logging.addLevelName(5, 'UR_DEBUG')
logging.addLevelName(6, 'UR_INFO')

View File

@ -2,7 +2,6 @@
# Logitech Unifying Receiver API. # Logitech Unifying Receiver API.
# #
from logging import getLogger as _Logger
from struct import pack as _pack from struct import pack as _pack
from struct import unpack as _unpack from struct import unpack as _unpack
@ -19,8 +18,10 @@ from .exceptions import FeatureNotSupported as _FeatureNotSupported
_hex = _base._hex _hex = _base._hex
_LOG_LEVEL = 5
_l = _Logger('lur.api') from logging import getLogger
_log = getLogger('LUR').getChild('api')
del getLogger
# #
# #
@ -61,7 +62,7 @@ def get_receiver_info(handle):
def count_devices(handle): def count_devices(handle):
count = _base.request(handle, 0xFF, b'\x80\x02', b'\x02') count = _base.request(handle, 0xFF, b'\x81\x00')
return 0 if count is None else ord(count[1:2]) return 0 if count is None else ord(count[1:2])
@ -88,22 +89,16 @@ def request(handle, devnumber, feature, function=b'\x00', params=b'', features=N
:raises FeatureNotSupported: if the device does not support the feature. :raises FeatureNotSupported: if the device does not support the feature.
""" """
feature_index = None feature_index = None
if feature == FEATURE.ROOT: if feature == FEATURE.ROOT:
feature_index = b'\x00' feature_index = b'\x00'
else: else:
if features is None: feature_index = _get_feature_index(handle, devnumber, feature, features)
features = get_device_features(handle, devnumber) if feature_index is None:
if features is None: # i/o read error
_l.log(_LOG_LEVEL, "(%d) no features array available", devnumber) return None
return None
if feature in features:
feature_index = _pack('!B', features.index(feature))
if feature_index is None: feature_index = _pack('!B', feature_index)
_l.warn("(%d) feature <%s:%s> not supported", devnumber, _hex(feature), FEATURE_NAME[feature])
raise _FeatureNotSupported(devnumber, feature)
if type(function) == int: if type(function) == int:
function = _pack('!B', function) function = _pack('!B', function)
@ -132,9 +127,11 @@ def get_device_protocol(handle, devnumber):
def find_device_by_name(handle, name): def find_device_by_name(handle, name):
"""Searches for an attached device by name. """Searches for an attached device by name.
This function does it the hard way, querying all possible device numbers.
:returns: an AttachedDeviceInfo tuple, or ``None``. :returns: an AttachedDeviceInfo tuple, or ``None``.
""" """
_l.log(_LOG_LEVEL, "searching for device '%s'", name) _log.debug("searching for device '%s'", name)
for devnumber in range(1, 1 + MAX_ATTACHED_DEVICES): for devnumber in range(1, 1 + MAX_ATTACHED_DEVICES):
features = get_device_features(handle, devnumber) features = get_device_features(handle, devnumber)
@ -147,9 +144,11 @@ def find_device_by_name(handle, name):
def list_devices(handle): def list_devices(handle):
"""List all devices attached to the UR. """List all devices attached to the UR.
This function does it the hard way, querying all possible device numbers.
:returns: a list of AttachedDeviceInfo tuples. :returns: a list of AttachedDeviceInfo tuples.
""" """
_l.log(_LOG_LEVEL, "listing all devices") _log.debug("listing all devices")
devices = [] devices = []
@ -162,7 +161,7 @@ def list_devices(handle):
def get_device_info(handle, devnumber, name=None, features=None): def get_device_info(handle, devnumber, name=None, features=None):
"""Gets the complete info for a device (type, name, firmware versions, features). """Gets the complete info for a device (type, name, features).
:returns: an AttachedDeviceInfo tuple, or ``None``. :returns: an AttachedDeviceInfo tuple, or ``None``.
""" """
@ -174,7 +173,7 @@ def get_device_info(handle, devnumber, name=None, features=None):
d_kind = get_device_kind(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_kind, d_name, features) devinfo = _AttachedDeviceInfo(handle, devnumber, d_kind, d_name, features)
_l.log(_LOG_LEVEL, "(%d) found device %s", devnumber, devinfo) _log.debug("(%d) found device %s", devnumber, devinfo)
return devinfo return devinfo
@ -183,41 +182,53 @@ 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, _hex(feature), FEATURE_NAME[feature]) _log.debug("(%d) get feature index <%s:%s>", devnumber, _hex(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, FEATURE.ROOT, feature) reply = _base.request(handle, devnumber, FEATURE.ROOT, feature)
if reply: if reply:
# only consider active and supported features
feature_index = ord(reply[0:1]) feature_index = ord(reply[0:1])
if feature_index: if feature_index:
feature_flags = ord(reply[1:2]) & 0xE0 feature_flags = ord(reply[1:2]) & 0xE0
if _l.isEnabledFor(_LOG_LEVEL): if feature_flags:
if feature_flags: _log.debug("(%d) feature <%s:%s> has index %d: %s",
_l.log(_LOG_LEVEL, "(%d) feature <%s:%s> has index %d: %s",
devnumber, _hex(feature), FEATURE_NAME[feature], feature_index, devnumber, _hex(feature), FEATURE_NAME[feature], feature_index,
','.join([FEATURE_FLAGS[k] for k in 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, _hex(feature), FEATURE_NAME[feature], feature_index) _log.debug("(%d) feature <%s:%s> has index %d", devnumber, _hex(feature), FEATURE_NAME[feature], feature_index)
# only consider active and supported features?
# 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, _hex(feature), FEATURE_NAME[feature]) _log.warn("(%d) feature <%s:%s> not supported by the device", devnumber, _hex(feature), FEATURE_NAME[feature])
raise _FeatureNotSupported(devnumber, feature) raise _FeatureNotSupported(devnumber, feature)
def _get_feature_index(handle, devnumber, feature, features=None):
if features is None:
return get_feature_index(handle, devnumber, feature)
if feature in features:
return features.index(feature)
index = get_feature_index(handle, devnumber, feature)
if index is not None:
features[index] = feature
return index
def get_device_features(handle, devnumber): def get_device_features(handle, devnumber):
"""Returns an array of feature ids. """Returns an array of feature ids.
Their position in the array is the index to be used when requesting that Their position in the array is the index to be used when requesting that
feature on the device. feature on the device.
""" """
_l.log(_LOG_LEVEL, "(%d) get device features", devnumber) _log.debug("(%d) get device features", 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
@ -235,11 +246,11 @@ def get_device_features(handle, devnumber):
if not features_count: if not features_count:
# this can happen if the device disappeard since the fs_index request # this can happen if the device disappeard since the fs_index request
# otherwise we should get at least a count of 1 (the FEATURE_SET we've just used above) # otherwise we should get at least a count of 1 (the FEATURE_SET we've just used above)
_l.log(_LOG_LEVEL, "(%d) no features available?!", devnumber) _log.debug("(%d) no features available?!", devnumber)
return None return None
features_count = ord(features_count[:1]) features_count = ord(features_count[:1])
_l.log(_LOG_LEVEL, "(%d) found %d features", devnumber, features_count) _log.debug("(%d) found %d features", devnumber, features_count)
features = [None] * 0x20 features = [None] * 0x20
for index in range(1, 1 + features_count): for index in range(1, 1 + features_count):
@ -250,13 +261,12 @@ def get_device_features(handle, devnumber):
feature = feature[0:2].upper() feature = feature[0:2].upper()
features[index] = feature features[index] = feature
if _l.isEnabledFor(_LOG_LEVEL): if feature_flags:
if feature_flags: _log.debug("(%d) feature <%s:%s> at index %d: %s",
_l.log(_LOG_LEVEL, "(%d) feature <%s:%s> at index %d: %s",
devnumber, _hex(feature), FEATURE_NAME[feature], index, devnumber, _hex(feature), FEATURE_NAME[feature], index,
','.join([FEATURE_FLAGS[k] for k in 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, _hex(feature), FEATURE_NAME[feature], index) _log.debug("(%d) feature <%s:%s> at index %d", devnumber, _hex(feature), FEATURE_NAME[feature], index)
features[0] = FEATURE.ROOT features[0] = FEATURE.ROOT
while features[-1] is None: while features[-1] is None:
@ -269,16 +279,17 @@ 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, kind, name='', version='', extras=None): fw_fi = _get_feature_index(handle, devnumber, FEATURE.FIRMWARE, features)
return _FirmwareInfo(level, kind, name, version, extras) if fw_fi is None:
return None
fw_count = request(handle, devnumber, FEATURE.FIRMWARE, features=features) fw_count = _base.request(handle, devnumber, _pack('!BB', fw_fi, 0x00))
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, FEATURE.FIRMWARE, function=b'\x10', params=index, features=features) fw_info = _base.request(handle, devnumber, _pack('!BB', fw_fi, 0x10), params=index)
if fw_info: if fw_info:
level = ord(fw_info[:1]) & 0x0F level = ord(fw_info[:1]) & 0x0F
if level == 0 or level == 1: if level == 0 or level == 1:
@ -290,18 +301,15 @@ def get_device_firmware(handle, devnumber, features=None):
build, = _unpack('!H', fw_info[6:8]) build, = _unpack('!H', fw_info[6:8])
if build: if build:
version += ' b%d' % build version += ' b%d' % build
extras = fw_info[9:].rstrip(b'\x00') extras = fw_info[9:].rstrip(b'\x00') or None
if extras: fw_info = _FirmwareInfo(level, kind, name, version, extras)
fw_info = _makeFirmwareInfo(level, kind, name, version, extras)
else:
fw_info = _makeFirmwareInfo(level, kind, name, version)
elif level == 2: elif level == 2:
fw_info = _makeFirmwareInfo(2, FIRMWARE_KIND[2], version=ord(fw_info[1:2])) fw_info = _FirmwareInfo(2, FIRMWARE_KIND[2], '', ord(fw_info[1:2]), None)
else: else:
fw_info = _makeFirmwareInfo(level, FIRMWARE_KIND[-1]) fw_info = _FirmwareInfo(level, FIRMWARE_KIND[-1], '', '', None)
fw.append(fw_info) fw.append(fw_info)
_l.log(_LOG_LEVEL, "(%d) firmware %s", devnumber, fw_info) _log.debug("(%d) firmware %s", devnumber, fw_info)
return tuple(fw) return tuple(fw)
@ -312,10 +320,14 @@ def get_device_kind(handle, devnumber, features=None):
: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_kind = request(handle, devnumber, FEATURE.NAME, function=b'\x20', features=features) name_fi = _get_feature_index(handle, devnumber, FEATURE.NAME, features)
if name_fi is None:
return None
d_kind = _base.request(handle, devnumber, _pack('!BB', name_fi, 0x20))
if d_kind: if d_kind:
d_kind = ord(d_kind[:1]) d_kind = ord(d_kind[:1])
_l.log(_LOG_LEVEL, "(%d) device type %d = %s", devnumber, d_kind, DEVICE_KIND[d_kind]) _log.debug("(%d) device type %d = %s", devnumber, d_kind, DEVICE_KIND[d_kind])
return DEVICE_KIND[d_kind] return DEVICE_KIND[d_kind]
@ -325,13 +337,17 @@ 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, FEATURE.NAME, features=features) name_fi = _get_feature_index(handle, devnumber, FEATURE.NAME, features)
if name_fi is None:
return None
name_length = _base.request(handle, devnumber, _pack('!BB', name_fi, 0x00))
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, FEATURE.NAME, function=b'\x10', params=len(d_name), features=features) name_fragment = _base.request(handle, devnumber, _pack('!BB', name_fi, 0x10), len(d_name))
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,7 +355,7 @@ def get_device_name(handle, devnumber, features=None):
break break
d_name = d_name.decode('ascii') d_name = d_name.decode('ascii')
_l.log(_LOG_LEVEL, "(%d) device name %s", devnumber, d_name) _log.debug("(%d) device name %s", devnumber, d_name)
return d_name return d_name
@ -348,22 +364,28 @@ 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, FEATURE.BATTERY, features=features) bat_fi = _get_feature_index(handle, devnumber, FEATURE.BATTERY, features)
if battery: if bat_fi is not None:
discharge, dischargeNext, status = _unpack('!BBB', battery[:3]) battery = _base.request(handle, devnumber, _pack('!BB', bat_fi, 0))
_l.log(_LOG_LEVEL, "(%d) battery %d%% charged, next level %d%% charge, status %d = %s", if battery:
discharge, dischargeNext, status = _unpack('!BBB', battery[:3])
_log.debug("(%d) battery %d%% charged, next level %d%% charge, status %d = %s",
devnumber, discharge, dischargeNext, status, BATTERY_STATUS[status]) devnumber, discharge, dischargeNext, status, BATTERY_STATUS[status])
return (discharge, dischargeNext, 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, FEATURE.REPROGRAMMABLE_KEYS, features=features) rk_fi = _get_feature_index(handle, devnumber, FEATURE.REPROGRAMMABLE_KEYS, features)
if rk_fi is None:
return None
count = _base.request(handle, devnumber, _pack('!BB', rk_fi, 0))
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, FEATURE.REPROGRAMMABLE_KEYS, function=b'\x10', params=index, features=features) keydata = _base.request(handle, devnumber, _pack('!BB', rk_fi, 0x10), index)
if keydata: if keydata:
key, key_task, flags = _unpack('!HHB', keydata[:5]) key, key_task, flags = _unpack('!HHB', keydata[:5])
rki = _ReprogrammableKeyInfo(index, key, KEY_NAME[key], key_task, KEY_NAME[key_task], flags) rki = _ReprogrammableKeyInfo(index, key, KEY_NAME[key], key_task, KEY_NAME[key_task], flags)

View File

@ -3,7 +3,6 @@
# Unlikely to be used directly unless you're expanding the API. # Unlikely to be used directly unless you're expanding the API.
# #
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
_hex = lambda d: _hexlify(d).decode('ascii').upper() _hex = lambda d: _hexlify(d).decode('ascii').upper()
@ -12,12 +11,13 @@ from .constants import ERROR_NAME
from .exceptions import (NoReceiver as _NoReceiver, from .exceptions import (NoReceiver as _NoReceiver,
FeatureCallError as _FeatureCallError) FeatureCallError as _FeatureCallError)
from logging import getLogger
_log = getLogger('LUR').getChild('base')
del getLogger
import hidapi as _hid import hidapi as _hid
_LOG_LEVEL = 4
_l = _Logger('lur.base')
# #
# These values are defined by the Logitech documentation. # These values are defined by the Logitech documentation.
# Overstepping these boundaries will only produce log warnings. # Overstepping these boundaries will only produce log warnings.
@ -48,7 +48,7 @@ DEFAULT_TIMEOUT = 1000
def _logdebug_hook(reply_code, devnumber, data): def _logdebug_hook(reply_code, devnumber, data):
"""Default unhandled hook, logs the reply as DEBUG.""" """Default unhandled hook, logs the reply as DEBUG."""
_l.warn("UNHANDLED [%02X %02X %s %s] (%s)", reply_code, devnumber, _hex(data[:2]), _hex(data[2:]), repr(data)) _log.warn("UNHANDLED [%02X %02X %s %s] (%s)", reply_code, devnumber, _hex(data[:2]), _hex(data[2:]), repr(data))
"""The function that will be called on unhandled incoming events. """The function that will be called on unhandled incoming events.
@ -78,7 +78,7 @@ def list_receiver_devices():
return _hid.enumerate(0x046d, 0xc52b, 2) return _hid.enumerate(0x046d, 0xc52b, 2)
_PING_RECEIVER = b'\x10\xFF\x81\x00\x00\x00\x00' _COUNT_DEVICES_REQUEST = b'\x10\xFF\x81\x00\x00\x00\x00'
def try_open(path): def try_open(path):
"""Checks if the given Linux device path points to the right UR device. """Checks if the given Linux device path points to the right UR device.
@ -97,31 +97,28 @@ def try_open(path):
if receiver_handle is None: if receiver_handle is None:
# could be a file permissions issue (did you add the udev rules?) # could be a file permissions issue (did you add the udev rules?)
# in any case, unreachable # in any case, unreachable
_l.log(_LOG_LEVEL, "[%s] open failed", path) _log.debug("[%s] open failed", path)
return None return None
_l.log(_LOG_LEVEL, "[%s] receiver handle %X", path, receiver_handle) _hid.write(receiver_handle, _COUNT_DEVICES_REQUEST)
# ping on device id 0 (always an error)
_hid.write(receiver_handle, _PING_RECEIVER)
# if this is the right hidraw device, we'll receive a 'bad device' from the UR # if this is the right hidraw device, we'll receive a 'bad device' from the UR
# otherwise, the read should produce nothing # otherwise, the read should produce nothing
reply = _hid.read(receiver_handle, _MAX_REPLY_SIZE, DEFAULT_TIMEOUT) reply = _hid.read(receiver_handle, _MAX_REPLY_SIZE, DEFAULT_TIMEOUT)
if reply: if reply:
if reply[:5] == _PING_RECEIVER[:5]: if reply[:5] == _COUNT_DEVICES_REQUEST[:5]:
# 'device 0 unreachable' is the expected reply from a valid receiver handle # 'device 0 unreachable' is the expected reply from a valid receiver handle
_l.log(_LOG_LEVEL, "[%s] success: handle %X", path, receiver_handle) _log.info("[%s] success: handle %X", path, receiver_handle)
return receiver_handle return receiver_handle
# any other replies are ignored, and will assume this is the wrong Linux device # any other replies are ignored, and will assume this is the wrong Linux device
if _l.isEnabledFor(_LOG_LEVEL): if reply == b'\x01\x00\x00\x00\x00\x00\x00\x00':
if reply == b'\x01\x00\x00\x00\x00\x00\x00\x00': # no idea what this is, but it comes up occasionally
# no idea what this is, but it comes up occasionally _log.debug("[%s] %X mistery reply [%s]", path, receiver_handle, _hex(reply))
_l.log(_LOG_LEVEL, "[%s] %X mistery reply [%s]", path, receiver_handle, _hex(reply)) else:
else: _log.debug("[%s] %X unknown reply [%s]", path, receiver_handle, _hex(reply))
_l.log(_LOG_LEVEL, "[%s] %X unknown reply [%s]", path, receiver_handle, _hex(reply))
else: else:
_l.log(_LOG_LEVEL, "[%s] %X no reply", path, receiver_handle) _log.debug("[%s] %X no reply", path, receiver_handle)
close(receiver_handle) close(receiver_handle)
@ -132,7 +129,7 @@ def open():
:returns: An open file handle for the found receiver, or ``None``. :returns: An open file handle for the found receiver, or ``None``.
""" """
for rawdevice in list_receiver_devices(): for rawdevice in list_receiver_devices():
_l.log(_LOG_LEVEL, "checking %s", rawdevice) _log.info("checking %s", rawdevice)
receiver = try_open(rawdevice.path) receiver = try_open(rawdevice.path)
if receiver: if receiver:
@ -146,10 +143,10 @@ def close(handle):
if handle: if handle:
try: try:
_hid.close(handle) _hid.close(handle)
_l.log(_LOG_LEVEL, "closed receiver handle %X", handle) _log.info("closed receiver handle %X", handle)
return True return True
except: except:
_l.exception("closing receiver handle %X", handle) _log.exception("closing receiver handle %X", handle)
return False return False
@ -168,15 +165,13 @@ def write(handle, devnumber, data):
been physically removed from the machine, or the kernel driver has been been physically removed from the machine, or the kernel driver has been
unloaded. The handle will be closed automatically. unloaded. The handle will be closed automatically.
""" """
if _l.isEnabledFor(_LOG_LEVEL):
_l.log(_LOG_LEVEL, "(%d) <= w[10 %02X %s %s]", devnumber, devnumber, _hex(data[:2]), _hex(data[2:]))
assert _MIN_CALL_SIZE == 7 assert _MIN_CALL_SIZE == 7
assert _MAX_CALL_SIZE == 20 assert _MAX_CALL_SIZE == 20
# the data is padded to either 5 or 18 bytes # the data is padded to either 5 or 18 bytes
wdata = _pack('!BB18s' if len(data) > 5 else '!BB5s', 0x10, devnumber, data) wdata = _pack('!BB18s' if len(data) > 5 else '!BB5s', 0x10, devnumber, data)
_log.debug("(%d) <= w[10 %02X %s %s]", devnumber, devnumber, _hex(wdata[2:4]), _hex(wdata[4:]))
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) _log.warn("(%d) write failed, assuming receiver %X no longer available", devnumber, handle)
close(handle) close(handle)
raise _NoReceiver raise _NoReceiver
@ -197,26 +192,33 @@ def read(handle, timeout=DEFAULT_TIMEOUT):
been physically removed from the machine, or the kernel driver has been been physically removed from the machine, or the kernel driver has been
unloaded. The handle will be closed automatically. unloaded. The handle will be closed automatically.
""" """
data = _hid.read(handle, _MAX_REPLY_SIZE * 2, timeout) data = _hid.read(handle, _MAX_REPLY_SIZE, timeout)
if data is None: if data is None:
_l.warn("(-) read failed, assuming receiver %X no longer available", handle) _log.warn("(-) read failed, assuming receiver %X no longer available", handle)
close(handle) close(handle)
raise _NoReceiver raise _NoReceiver
if data: if data:
if len(data) < _MIN_REPLY_SIZE: if len(data) < _MIN_REPLY_SIZE:
_l.warn("(%d) => r[%s] read packet too short: %d bytes", ord(data[1:2]), _hex(data), len(data)) _log.warn("(%d) => r[%s] read packet too short: %d bytes", ord(data[1:2]), _hex(data), len(data))
data += b'\x00' * (_MIN_REPLY_SIZE - len(data))
if len(data) > _MAX_REPLY_SIZE: if len(data) > _MAX_REPLY_SIZE:
_l.warn("(%d) => r[%s] read packet too long: %d bytes", ord(data[1:2]), _hex(data), len(data)) _log.warn("(%d) => r[%s] read packet too long: %d bytes", ord(data[1:2]), _hex(data), len(data))
code = ord(data[:1]) code = ord(data[:1])
devnumber = ord(data[1:2]) devnumber = ord(data[1:2])
if _l.isEnabledFor(_LOG_LEVEL): _log.debug("(%d) => r[%02X %02X %s %s]", devnumber, code, devnumber, _hex(data[2:4]), _hex(data[4:]))
_l.log(_LOG_LEVEL, "(%d) => r[%02X %02X %s %s]", devnumber, code, devnumber, _hex(data[2:4]), _hex(data[4:]))
return code, devnumber, data[2:] return code, devnumber, data[2:]
# _l.log(_LOG_LEVEL, "(-) => r[]", handle) # _l.log(_LOG_LEVEL, "(-) => r[]")
_MAX_READ_TIMES = 3
request_context = None
from collections import namedtuple
_DEFAULT_REQUEST_CONTEXT_CLASS = namedtuple('_DEFAULT_REQUEST_CONTEXT_CLASS', ['write', 'read', 'unhandled_hook'])
_DEFAULT_REQUEST_CONTEXT = _DEFAULT_REQUEST_CONTEXT_CLASS(write=write, read=read, unhandled_hook=unhandled_hook)
del namedtuple
def request(handle, devnumber, feature_index_function, params=b'', features=None): def request(handle, devnumber, feature_index_function, params=b'', features=None):
"""Makes a feature call to a device and waits for a matching reply. """Makes a feature call to a device and waits for a matching reply.
@ -234,18 +236,27 @@ def request(handle, devnumber, feature_index_function, params=b'', features=None
available. available.
:raisees FeatureCallError: if the feature call replied with an error. :raisees FeatureCallError: if the feature call replied with an error.
""" """
if _l.isEnabledFor(_LOG_LEVEL): if type(params) == int:
_l.log(_LOG_LEVEL, "(%d) request {%s} params [%s]", devnumber, _hex(feature_index_function), _hex(params)) params = _pack('!B', params)
_log.debug("(%d) request {%s} params [%s]", devnumber, _hex(feature_index_function), _hex(params))
if len(feature_index_function) != 2: if len(feature_index_function) != 2:
raise ValueError('invalid feature_index_function {%s}: it must be a two-byte string' % _hex(feature_index_function)) raise ValueError('invalid feature_index_function {%s}: it must be a two-byte string' % _hex(feature_index_function))
retries = 5 if request_context is None or handle != request_context.handle:
context = _DEFAULT_REQUEST_CONTEXT
_unhandled = unhandled_hook
else:
context = request_context
_unhandled = getattr(context, 'unhandled_hook')
write(handle, devnumber, feature_index_function + params) context.write(handle, devnumber, feature_index_function + params)
while retries > 0:
divisor = (6 - retries) read_times = _MAX_READ_TIMES
reply = read(handle, int(DEFAULT_TIMEOUT * (divisor + 1) / 2 / divisor)) while read_times > 0:
retries -= 1 divisor = (1 + _MAX_READ_TIMES - read_times)
reply = context.read(handle, int(DEFAULT_TIMEOUT * (divisor + 1) / 2 / divisor))
read_times -= 1
if not reply: if not reply:
# keep waiting... # keep waiting...
@ -258,24 +269,24 @@ def request(handle, devnumber, feature_index_function, params=b'', features=None
# _l.log(_LOG_LEVEL, "(%d) request got reply for unexpected device %d: [%s]", devnumber, reply_devnumber, _hex(reply_data)) # _l.log(_LOG_LEVEL, "(%d) request got reply for unexpected device %d: [%s]", devnumber, reply_devnumber, _hex(reply_data))
# worst case scenario, this is a reply for a concurrent request # worst case scenario, this is a reply for a concurrent request
# on this receiver # on this receiver
if unhandled_hook: if _unhandled:
unhandled_hook(reply_code, reply_devnumber, reply_data) _unhandled(reply_code, reply_devnumber, reply_data)
continue continue
if reply_code == 0x10 and reply_data[:1] == b'\x8F' and reply_data[1:3] == feature_index_function: if reply_code == 0x10 and reply_data[:1] == b'\x8F' and reply_data[1:3] == feature_index_function:
# device not present # device not present
_l.log(_LOG_LEVEL, "(%d) request ping failed on {%s} call: [%s]", devnumber, _hex(feature_index_function), _hex(reply_data)) _log.debug("(%d) request ping failed on {%s} call: [%s]", devnumber, _hex(feature_index_function), _hex(reply_data))
return None return None
if reply_code == 0x10 and reply_data[:1] == b'\x8F': if reply_code == 0x10 and reply_data[:1] == b'\x8F':
# device not present # device not present
_l.log(_LOG_LEVEL, "(%d) request ping failed: [%s]", devnumber, _hex(reply_data)) _log.debug("(%d) request ping failed: [%s]", devnumber, _hex(reply_data))
return None return 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, ERROR_NAME[error_code], _hex(reply_data)) _log.warn("(%d) request feature call error %d = %s: %s", devnumber, error_code, ERROR_NAME[error_code], _hex(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
@ -283,14 +294,14 @@ def request(handle, devnumber, feature_index_function, params=b'', features=None
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
# _l.log(_LOG_LEVEL, "(%d) matched reply with feature-index-function [%s]", devnumber, _hex(reply_data[2:])) # _log.debug("(%d) matched reply with feature-index-function [%s]", devnumber, _hex(reply_data[2:]))
return reply_data[2:] return reply_data[2:]
if reply_code == 0x10 and devnumber == 0xFF and reply_data[:2] == feature_index_function: if reply_code == 0x10 and devnumber == 0xFF and reply_data[:2] == feature_index_function:
# direct calls to the receiver (device 0xFF) may also return successfully with reply code 0x10 # direct calls to the receiver (device 0xFF) may also return successfully with reply code 0x10
# _l.log(_LOG_LEVEL, "(%d) matched reply with feature-index-function [%s]", devnumber, _hex(reply_data[2:])) # _log.debug("(%d) matched reply with feature-index-function [%s]", devnumber, _hex(reply_data[2:]))
return reply_data[2:] return reply_data[2:]
# _l.log(_LOG_LEVEL, "(%d) unmatched reply {%s} (expected {%s})", devnumber, _hex(reply_data[:2]), _hex(feature_index_function)) # _log.debug("(%d) unmatched reply {%s} (expected {%s})", devnumber, _hex(reply_data[:2]), _hex(feature_index_function))
if unhandled_hook: if _unhandled:
unhandled_hook(reply_code, reply_devnumber, reply_data) _unhandled(reply_code, reply_devnumber, reply_data)

View File

@ -2,8 +2,7 @@
# #
# #
from logging import getLogger as _Logger from threading import Thread as _Thread
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
@ -12,108 +11,107 @@ from .common import Packet as _Packet
# for both Python 2 and 3 # for both Python 2 and 3
try: try:
from Queue import Queue from Queue import Queue as _Queue
except ImportError: except ImportError:
from queue import Queue from queue import Queue as _Queue
_LOG_LEVEL = 6 from logging import getLogger
_l = _Logger('lur.listener') _log = getLogger('LUR').getChild('listener')
del getLogger
_READ_EVENT_TIMEOUT = int(_base.DEFAULT_TIMEOUT / 4) # ms _READ_EVENT_TIMEOUT = int(_base.DEFAULT_TIMEOUT) # ms
def _event_dispatch(listener, callback): def _event_dispatch(listener, callback):
# _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() try:
# _l.log(_LOG_LEVEL, "delivering event %s", event) event = listener._events.get(True, _READ_EVENT_TIMEOUT * 10)
except:
continue
_log.debug("delivering event %s", event)
try: try:
callback(event) callback(event)
except: except:
_l.exception("callback for %s", event) _log.exception("callback for %s", event)
# _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 packets will be passed to the callback function in sequence, by a Incoming packets will be passed to the callback function in sequence, by a
separate thread. separate thread.
While this listener is running, you must use the call_api() method to make
regular UR API calls; otherwise the expected API replies are most likely to
be captured by the listener and delivered to the callback.
""" """
def __init__(self, receiver_handle, events_callback): def __init__(self, receiver_handle, events_callback):
super(EventsListener, self).__init__(group='Unifying Receiver', name='%s-%X' % (self.__class__.__name__, receiver_handle)) super(EventsListener, self).__init__(group='Unifying Receiver', name=self.__class__.__name__)
self.daemon = True self.daemon = True
self._active = False self._active = False
self._handle = receiver_handle self._handle = receiver_handle
self._task = None self._tasks = _Queue(1)
self._task_processing = Lock() self._backup_unhandled_hook = _base.unhandled_hook
self._task_reply = None _base.unhandled_hook = self.unhandled_hook
self._task_done = Event()
self._events = Queue(32) self._events = _Queue(32)
_base.unhandled_hook = self._unhandled self._dispatcher = _Thread(group='Unifying Receiver',
name=self.__class__.__name__ + '-dispatch',
self._dispatcher = Thread(group='Unifying Receiver',
name='%s-%X-dispatch' % (self.__class__.__name__, receiver_handle),
target=_event_dispatch, args=(self, events_callback)) target=_event_dispatch, args=(self, events_callback))
self._dispatcher.daemon = True self._dispatcher.daemon = True
def run(self): def run(self):
self._active = True self._active = True
_l.log(_LOG_LEVEL, "started") _log.debug("started")
_base.request_context = self
_base.unhandled_hook = self._backup_unhandled_hook
del self._backup_unhandled_hook
self._dispatcher.start() self._dispatcher.start()
while self._active: while self._active:
event = None event = None
try: try:
# _log.debug("read next event")
event = _base.read(self._handle, _READ_EVENT_TIMEOUT) event = _base.read(self._handle, _READ_EVENT_TIMEOUT)
except _NoReceiver: except _NoReceiver:
self._handle = 0 self._handle = 0
_l.warn("receiver disconnected") _log.warn("receiver disconnected")
self._events.put(_Packet(0xFF, 0xFF, None)) self._events.put(_Packet(0xFF, 0xFF, None))
self._active = False self._active = False
break break
if event: if event is not None:
event = _Packet(*event) matched = False
_l.log(_LOG_LEVEL, "queueing event %s", event) task = None if self._tasks.empty() else self._tasks.queue[0]
self._events.put(event) if task and task[0] and task[-1] is None:
devnumber, data = task[1:3]
if event[1] == devnumber:
# _log.debug("matching %s to %d, %s", event, devnumber, repr(data))
if event[0] == 0x11 or (event[0] == 0x10 and devnumber == 0xFF):
matched = (event[2][:2] == data[:2]) or (event[2][:1] == b'\xFF' and event[2][1:3] == data[:2])
elif event[0] == 0x10:
if event[2][:1] == b'\x8F' and event[2][1:3] == data[:2]:
matched = True
if self._task: if matched:
(api_function, args, kwargs), self._task = self._task, None # _log.debug("request reply %s", event)
# _l.log(_LOG_LEVEL, "calling '%s.%s' with %s, %s", api_function.__module__, api_function.__name__, args, kwargs) task[-1] = event
try: self._tasks.task_done()
self._task_reply = api_function.__call__(self._handle, *args, **kwargs) else:
except _NoReceiver as nr: event = _Packet(*event)
self._handle = 0 _log.info("queueing event %s", event)
_l.warn("receiver disconnected") self._events.put(event)
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.request_context = None
_base.close(self._handle) _base.close(self._handle)
_log.debug("stopped")
self._handle = 0 self._handle = 0
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") _log.debug("stopping")
self._active = False self._active = False
# wait for the receiver handle to be closed # wait for the receiver handle to be closed
self.join() self.join()
@ -122,34 +120,27 @@ class EventsListener(Thread):
def handle(self): def handle(self):
return self._handle return self._handle
def request(self, device, feature_function_index, params=b''): def write(self, handle, devnumber, data):
return self.call_api(_base.request, device, feature_function_index, params) assert handle == self._handle
# _log.debug("write %02X %s", devnumber, _base._hex(data))
task = [False, devnumber, data, None]
self._tasks.put(task)
_base.write(self._handle, devnumber, data)
task[0] = True
_log.debug("task queued %s", task)
def call_api(self, api_function, *args, **kwargs): def read(self, handle, timeout=_base.DEFAULT_TIMEOUT):
"""Make an UR API request through this listener's receiver. assert handle == self._handle
# _log.debug("read %d", timeout)
assert not self._tasks.empty()
self._tasks.join()
task = self._tasks.get(False)
_log.debug("task ready %s", task)
return task[-1]
The api_function must have a receiver handle as a first agument, all def unhandled_hook(self, reply_code, devnumber, data):
other passed args and kwargs will follow.
"""
# _l.log(_LOG_LEVEL, "%s request '%s.%s' with %s, %s", self, api_function.__module__, api_function.__name__, args, kwargs)
# if not self._active:
# return None
with self._task_processing:
self._task_done.clear()
self._task = (api_function, args, kwargs)
self._task_done.wait()
reply, self._task_reply = self._task_reply, None
# _l.log(_LOG_LEVEL, "%s request '%s.%s' => %s", self, api_function.__module__, api_function.__name__, repr(reply))
if isinstance(reply, Exception):
raise reply
return reply
def _unhandled(self, reply_code, devnumber, data):
event = _Packet(reply_code, devnumber, data) event = _Packet(reply_code, devnumber, data)
# _l.log(_LOG_LEVEL, "queueing unhandled event %s", event) _log.info("queueing unhandled event %s", event)
self._events.put(event) self._events.put(event)
def __nonzero__(self): def __nonzero__(self):