reworked the way tasks are processed by the listener
This commit is contained in:
parent
62a91b56d2
commit
e7bb599689
167
app/receiver.py
167
app/receiver.py
|
@ -3,7 +3,6 @@
|
|||
#
|
||||
|
||||
from logging import getLogger as _Logger
|
||||
_LOG_LEVEL = 6
|
||||
|
||||
from threading import Event as _Event
|
||||
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):
|
||||
"""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.receiver = receiver
|
||||
self.number = number
|
||||
self._pair_code = pair_code
|
||||
self._serial = None
|
||||
self._codename = None
|
||||
self._name = None
|
||||
self._kind = None
|
||||
self._serial = None
|
||||
self._firmware = None
|
||||
self._features = None
|
||||
|
||||
self._status = status
|
||||
self.props = {}
|
||||
|
||||
self.features = _FeaturesArray(self)
|
||||
|
||||
@property
|
||||
def handle(self):
|
||||
return self.receiver.handle
|
||||
|
@ -71,8 +161,8 @@ class DeviceInfo(object):
|
|||
def name(self):
|
||||
if self._name is None:
|
||||
if self._status >= STATUS.CONNECTED:
|
||||
self._name = self.receiver.call_api(_api.get_device_name, self.number, self.features)
|
||||
return self._name or '?'
|
||||
self._name = _api.get_device_name(self.receiver.handle, self.number, self.features)
|
||||
return self._name or self.codename
|
||||
|
||||
@property
|
||||
def device_name(self):
|
||||
|
@ -81,33 +171,42 @@ class DeviceInfo(object):
|
|||
@property
|
||||
def kind(self):
|
||||
if self._kind is None:
|
||||
if self._status >= STATUS.CONNECTED:
|
||||
self._kind = self.receiver.call_api(_api.get_device_kind, self.number, self.features)
|
||||
if self._status < STATUS.CONNECTED:
|
||||
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 '?'
|
||||
|
||||
@property
|
||||
def serial(self):
|
||||
if self._serial is None:
|
||||
if self._status >= STATUS.CONNECTED:
|
||||
pass
|
||||
# dodgy
|
||||
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 '?'
|
||||
|
||||
@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
|
||||
def firmware(self):
|
||||
if self._firmware is None:
|
||||
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 ()
|
||||
|
||||
@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):
|
||||
return self.receiver.call_api(_api.ping, self.number)
|
||||
return _api.ping(self.receiver.handle, self.number)
|
||||
|
||||
def process_event(self, code, data):
|
||||
if code == 0x10 and data[:1] == b'\x8F':
|
||||
|
@ -224,20 +323,20 @@ class Receiver(_listener.EventsListener):
|
|||
return self.NAME
|
||||
|
||||
def count_devices(self):
|
||||
return self.call_api(_api.count_devices)
|
||||
return _api.count_devices(self._handle)
|
||||
|
||||
@property
|
||||
def serial(self):
|
||||
if self._serial is None:
|
||||
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 '?'
|
||||
|
||||
@property
|
||||
def firmware(self):
|
||||
if self._firmware is None:
|
||||
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 ('?', '?')
|
||||
|
||||
|
||||
|
@ -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)
|
||||
return None
|
||||
|
||||
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(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
|
||||
return DeviceInfo(self, event.devnumber, event.data[4:5], state)
|
||||
|
||||
def unpair_device(self, number):
|
||||
if number in self.devices:
|
||||
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:
|
||||
self.LOG.debug("remove device %s => %s", dev, _base._hex(reply))
|
||||
del self.devices[number]
|
||||
|
@ -346,7 +427,7 @@ class Receiver(_listener.EventsListener):
|
|||
: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)
|
||||
_Logger("receiver").debug("checking %s", rawdevice)
|
||||
handle = _base.try_open(rawdevice.path)
|
||||
if handle:
|
||||
receiver = Receiver(rawdevice.path, handle)
|
||||
|
|
|
@ -32,10 +32,8 @@ def _parse_arguments():
|
|||
|
||||
import logging
|
||||
log_level = logging.root.level - 10 * args.verbose
|
||||
log_format='%(asctime)s.%(msecs)03d %(levelname)8s [%(threadName)s] %(name)s: %(message)s'
|
||||
logging.basicConfig(level=log_level if log_level > 0 else 1,
|
||||
format=log_format,
|
||||
datefmt='%H:%M:%S')
|
||||
log_format='%(asctime)s %(levelname)8s [%(threadName)s] %(name)s: %(message)s'
|
||||
logging.basicConfig(level=max(log_level, 1), format=log_format)
|
||||
|
||||
return args
|
||||
|
||||
|
@ -83,3 +81,6 @@ if __name__ == '__main__':
|
|||
|
||||
w.stop()
|
||||
ui.notify.uninit()
|
||||
|
||||
import logging
|
||||
logging.shutdown()
|
||||
|
|
|
@ -19,10 +19,18 @@ def _toggle_action(name, label, function, *args):
|
|||
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):
|
||||
if action.get_active():
|
||||
|
@ -57,9 +65,6 @@ import pairing
|
|||
def _pair_device(action):
|
||||
action.set_sensitive(False)
|
||||
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.present()
|
||||
pair = _action('add', 'Pair new device', _pair_device)
|
||||
|
@ -69,9 +74,11 @@ def _unpair_device(action):
|
|||
dev = pairing.state.device(action.devnumber)
|
||||
action.devnumber = 0
|
||||
if dev:
|
||||
q = Gtk.MessageDialog.new(action.window,
|
||||
qdialog = Gtk.MessageDialog(action.window, 0,
|
||||
Gtk.MessageType.QUESTION, Gtk.ButtonsType.YES_NO,
|
||||
'Unpair device <b>%s</b>?', dev.name)
|
||||
if q.run() == Gtk.ResponseType.YES:
|
||||
"Unpair device '%s' ?" % dev.name)
|
||||
choice = qdialog.run()
|
||||
qdialog.destroy()
|
||||
if choice == Gtk.ResponseType.YES:
|
||||
pairing.state.unpair(dev.number)
|
||||
unpair = _action('remove', 'Unpair', _unpair_device)
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
#
|
||||
#
|
||||
|
||||
from gi.repository import (Gtk, Gdk)
|
||||
from gi.repository import (Gtk, Gdk, GObject)
|
||||
|
||||
import ui
|
||||
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)
|
||||
return action.create_tool_item()
|
||||
|
||||
|
||||
def _receiver_box(name):
|
||||
icon = Gtk.Image.new_from_icon_name(name, _SMALL_DEVICE_ICON_SIZE)
|
||||
|
||||
|
@ -65,7 +66,7 @@ def _receiver_box(name):
|
|||
return frame
|
||||
|
||||
|
||||
def _device_box():
|
||||
def _device_box(index):
|
||||
icon = Gtk.Image.new_from_icon_name('image-missing', _DEVICE_ICON_SIZE)
|
||||
icon.set_name('icon')
|
||||
icon.set_alignment(0.5, 0)
|
||||
|
@ -111,7 +112,10 @@ def _device_box():
|
|||
info_box.add(info_label)
|
||||
|
||||
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.pack_start(label, True, True, 0)
|
||||
|
@ -131,7 +135,6 @@ def _device_box():
|
|||
|
||||
|
||||
def toggle(window, trigger):
|
||||
# print 'window toggle', window, trigger
|
||||
if window.get_visible():
|
||||
position = window.get_position()
|
||||
window.hide()
|
||||
|
@ -158,7 +161,7 @@ def create(title, name, max_devices, systray=False):
|
|||
rbox = _receiver_box(name)
|
||||
vbox.add(rbox)
|
||||
for i in range(1, 1 + max_devices):
|
||||
dbox = _device_box()
|
||||
dbox = _device_box(i)
|
||||
vbox.add(dbox)
|
||||
vbox.set_visible(True)
|
||||
|
||||
|
@ -209,18 +212,12 @@ def _update_receiver_box(frame, receiver):
|
|||
|
||||
|
||||
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')
|
||||
|
||||
if frame.get_name() != dev.name:
|
||||
frame.set_name(dev.name)
|
||||
icon.set_from_icon_name(ui.get_icon(dev.name, dev.kind), _DEVICE_ICON_SIZE)
|
||||
label.set_markup('<b>' + dev.name + '</b>')
|
||||
frame.set_visible(True)
|
||||
|
||||
status = ui.find_children(frame, 'status')
|
||||
status_icons = status.get_children()
|
||||
|
@ -271,16 +268,21 @@ def _update_device_box(frame, dev):
|
|||
for b in toolbar.get_children()[:-1]:
|
||||
b.set_sensitive(True)
|
||||
|
||||
frame.set_visible(True)
|
||||
|
||||
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()
|
||||
controls = list(vbox.get_children())
|
||||
vbox = window.get_child()
|
||||
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)):
|
||||
dev = receiver.devices[index] if index in receiver.devices else None
|
||||
_update_device_box(controls[index], dev)
|
||||
for index in range(1, len(controls)):
|
||||
dev = receiver.devices[index] if index in receiver.devices else None
|
||||
frame = controls[index]
|
||||
if dev is None:
|
||||
frame.set_visible(False)
|
||||
frame.set_name(_PLACEHOLDER)
|
||||
else:
|
||||
GObject.idle_add(_update_device_box, frame, dev)
|
||||
|
|
|
@ -65,31 +65,19 @@ class Watcher(Thread):
|
|||
continue
|
||||
|
||||
_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
|
||||
notify_missing = True
|
||||
|
||||
self.update_ui(r)
|
||||
self.notify(r)
|
||||
|
||||
if self._active:
|
||||
if self._receiver:
|
||||
_l.debug("waiting for status_changed")
|
||||
sc = self._receiver.status_changed
|
||||
sc.wait()
|
||||
if not self._active:
|
||||
break
|
||||
sc.clear()
|
||||
if sc.urgent:
|
||||
_l.info("status_changed %s", sc.reason)
|
||||
|
@ -103,6 +91,7 @@ class Watcher(Thread):
|
|||
|
||||
if self._receiver:
|
||||
self._receiver.close()
|
||||
self._receiver = _DUMMY_RECEIVER
|
||||
|
||||
def stop(self):
|
||||
if self._active:
|
||||
|
@ -112,3 +101,4 @@ class Watcher(Thread):
|
|||
# break out of an eventual wait()
|
||||
self._receiver.status_changed.reason = None
|
||||
self._receiver.status_changed.set()
|
||||
self.join()
|
||||
|
|
|
@ -4,4 +4,4 @@ LIB=`dirname "$0"`/../lib
|
|||
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$LIB/native/`uname -m`
|
||||
export PYTHONPATH=$LIB
|
||||
|
||||
exec python -OO -u -m hidapi.hidconsole "$@"
|
||||
exec python -OOu -m hidapi.hidconsole "$@"
|
||||
|
|
2
bin/scan
2
bin/scan
|
@ -4,4 +4,4 @@ LIB=`dirname "$0"`/../lib
|
|||
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$LIB/native/`uname -m`
|
||||
export PYTHONPATH=$LIB
|
||||
|
||||
exec python -OO -m logitech.scanner "$@"
|
||||
exec python -OOu -m logitech.scanner "$@"
|
||||
|
|
|
@ -9,5 +9,5 @@ export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$LIB/native/`uname -m`
|
|||
export PYTHONPATH=$APP:$LIB
|
||||
export XDG_DATA_DIRS=$SHARE:$XDG_DATA_DIRS
|
||||
|
||||
exec python3 -OO -m solaar "$@"
|
||||
#exec python -OO -m profile -o $TMPDIR/profile.log app/solaar.py "$@"
|
||||
exec python -OOu -m solaar "$@"
|
||||
#exec python -OOu -m profile -o $TMPDIR/profile.log app/solaar.py "$@"
|
||||
|
|
|
@ -32,20 +32,12 @@ def _module(device_name):
|
|||
|
||||
def default_request_status(devinfo, listener=None):
|
||||
if FEATURE.BATTERY in devinfo.features:
|
||||
if listener:
|
||||
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)
|
||||
|
||||
reply = _api.get_device_battery_level(devinfo.handle, devinfo.number, features=devinfo.features)
|
||||
if reply:
|
||||
discharge, dischargeNext, status = reply
|
||||
return STATUS.CONNECTED, {PROPS.BATTERY_LEVEL: discharge, PROPS.BATTERY_STATUS: status}
|
||||
|
||||
if listener:
|
||||
reply = listener.call_api(_api.ping, devinfo.number)
|
||||
else:
|
||||
reply = _api.ping(devinfo.handle, devinfo.number)
|
||||
|
||||
reply = _api.ping(devinfo.handle, devinfo.number)
|
||||
return STATUS.CONNECTED if reply else STATUS.UNAVAILABLE
|
||||
|
||||
|
||||
|
|
|
@ -29,17 +29,9 @@ def _charge_status(data, hasLux=False):
|
|||
|
||||
|
||||
def request_status(devinfo, listener=None):
|
||||
def _trigger_solar_charge_events(handle, devinfo):
|
||||
return _api.request(handle, devinfo.number,
|
||||
feature=FEATURE.SOLAR_CHARGE, function=b'\x03', params=b'\x78\x01',
|
||||
features=devinfo.features)
|
||||
if listener is None:
|
||||
reply = _trigger_solar_charge_events(devinfo.handle, devinfo)
|
||||
elif listener:
|
||||
reply = listener.call_api(_trigger_solar_charge_events, devinfo)
|
||||
else:
|
||||
reply = 0
|
||||
|
||||
reply = _api.request(devinfo.handle, devinfo.number,
|
||||
feature=FEATURE.SOLAR_CHARGE, function=b'\x03', params=b'\x78\x01',
|
||||
features=devinfo.features)
|
||||
if reply is None:
|
||||
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':
|
||||
logging.debug("Solar key pressed")
|
||||
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
|
||||
|
|
|
@ -39,7 +39,7 @@ def scan_devices(receiver):
|
|||
for index in range(0, len(devinfo.features)):
|
||||
feature = devinfo.features[index]
|
||||
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:
|
||||
discharge, dischargeNext, status = api.get_device_battery_level(receiver, devinfo.number, features=devinfo.features)
|
||||
|
@ -75,11 +75,3 @@ if __name__ == '__main__':
|
|||
break
|
||||
else:
|
||||
print ("!! Logitech Unifying Receiver not found.")
|
||||
|
||||
|
||||
# import pyudev
|
||||
# ctx = pyudev.Context()
|
||||
# m = pyudev.Monitor.from_netlink(ctx)
|
||||
# m.filter_by(subsystem='hid')
|
||||
# for action, device in m:
|
||||
# print '%s: %s' % (action, device)
|
||||
|
|
|
@ -20,12 +20,24 @@ http://julien.danjou.info/blog/2012/logitech-k750-linux-support
|
|||
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 .exceptions import *
|
||||
from .api import *
|
||||
|
||||
|
||||
import logging
|
||||
logging.addLevelName(4, 'UR_TRACE')
|
||||
logging.addLevelName(5, 'UR_DEBUG')
|
||||
logging.addLevelName(6, 'UR_INFO')
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
# Logitech Unifying Receiver API.
|
||||
#
|
||||
|
||||
from logging import getLogger as _Logger
|
||||
from struct import pack as _pack
|
||||
from struct import unpack as _unpack
|
||||
|
||||
|
@ -19,8 +18,10 @@ from .exceptions import FeatureNotSupported as _FeatureNotSupported
|
|||
|
||||
|
||||
_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):
|
||||
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])
|
||||
|
||||
|
||||
|
@ -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.
|
||||
"""
|
||||
|
||||
feature_index = None
|
||||
if feature == FEATURE.ROOT:
|
||||
feature_index = b'\x00'
|
||||
else:
|
||||
if features is None:
|
||||
features = get_device_features(handle, devnumber)
|
||||
if features is None:
|
||||
_l.log(_LOG_LEVEL, "(%d) no features array available", devnumber)
|
||||
return None
|
||||
if feature in features:
|
||||
feature_index = _pack('!B', features.index(feature))
|
||||
feature_index = _get_feature_index(handle, devnumber, feature, features)
|
||||
if feature_index is None:
|
||||
# i/o read error
|
||||
return None
|
||||
|
||||
if feature_index is None:
|
||||
_l.warn("(%d) feature <%s:%s> not supported", devnumber, _hex(feature), FEATURE_NAME[feature])
|
||||
raise _FeatureNotSupported(devnumber, feature)
|
||||
feature_index = _pack('!B', feature_index)
|
||||
|
||||
if type(function) == int:
|
||||
function = _pack('!B', function)
|
||||
|
@ -132,9 +127,11 @@ def get_device_protocol(handle, devnumber):
|
|||
def find_device_by_name(handle, 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``.
|
||||
"""
|
||||
_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):
|
||||
features = get_device_features(handle, devnumber)
|
||||
|
@ -147,9 +144,11 @@ def find_device_by_name(handle, name):
|
|||
def list_devices(handle):
|
||||
"""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.
|
||||
"""
|
||||
_l.log(_LOG_LEVEL, "listing all devices")
|
||||
_log.debug("listing all devices")
|
||||
|
||||
devices = []
|
||||
|
||||
|
@ -162,7 +161,7 @@ def list_devices(handle):
|
|||
|
||||
|
||||
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``.
|
||||
"""
|
||||
|
@ -174,7 +173,7 @@ def get_device_info(handle, devnumber, name=None, features=None):
|
|||
d_kind = get_device_kind(handle, devnumber, features)
|
||||
d_name = get_device_name(handle, devnumber, features) if name is None else name
|
||||
devinfo = _AttachedDeviceInfo(handle, devnumber, d_kind, d_name, features)
|
||||
_l.log(_LOG_LEVEL, "(%d) found device %s", devnumber, devinfo)
|
||||
_log.debug("(%d) found device %s", devnumber, 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.
|
||||
"""
|
||||
_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:
|
||||
raise ValueError("invalid feature <%s>: it must be a two-byte string" % feature)
|
||||
|
||||
# FEATURE.ROOT should always be available for any attached devices
|
||||
reply = _base.request(handle, devnumber, FEATURE.ROOT, feature)
|
||||
if reply:
|
||||
# only consider active and supported features
|
||||
feature_index = ord(reply[0:1])
|
||||
if feature_index:
|
||||
feature_flags = ord(reply[1:2]) & 0xE0
|
||||
if _l.isEnabledFor(_LOG_LEVEL):
|
||||
if feature_flags:
|
||||
_l.log(_LOG_LEVEL, "(%d) feature <%s:%s> has index %d: %s",
|
||||
if feature_flags:
|
||||
_log.debug("(%d) feature <%s:%s> has index %d: %s",
|
||||
devnumber, _hex(feature), FEATURE_NAME[feature], feature_index,
|
||||
','.join([FEATURE_FLAGS[k] for k in FEATURE_FLAGS if feature_flags & k]))
|
||||
else:
|
||||
_l.log(_LOG_LEVEL, "(%d) feature <%s:%s> has index %d", devnumber, _hex(feature), FEATURE_NAME[feature], feature_index)
|
||||
else:
|
||||
_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:
|
||||
# raise E.FeatureNotSupported(devnumber, feature)
|
||||
|
||||
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)
|
||||
|
||||
|
||||
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):
|
||||
"""Returns an array of feature ids.
|
||||
|
||||
Their position in the array is the index to be used when requesting that
|
||||
feature on the device.
|
||||
"""
|
||||
_l.log(_LOG_LEVEL, "(%d) get device features", devnumber)
|
||||
_log.debug("(%d) get device features", devnumber)
|
||||
|
||||
# get the index of the FEATURE_SET
|
||||
# FEATURE.ROOT should always be available for all devices
|
||||
|
@ -235,11 +246,11 @@ def get_device_features(handle, devnumber):
|
|||
if not features_count:
|
||||
# this can happen if the device disappeard since the fs_index request
|
||||
# otherwise we should get at least a count of 1 (the FEATURE_SET we've just used above)
|
||||
_l.log(_LOG_LEVEL, "(%d) no features available?!", devnumber)
|
||||
_log.debug("(%d) no features available?!", devnumber)
|
||||
return None
|
||||
|
||||
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
|
||||
for index in range(1, 1 + features_count):
|
||||
|
@ -250,13 +261,12 @@ def get_device_features(handle, devnumber):
|
|||
feature = feature[0:2].upper()
|
||||
features[index] = feature
|
||||
|
||||
if _l.isEnabledFor(_LOG_LEVEL):
|
||||
if feature_flags:
|
||||
_l.log(_LOG_LEVEL, "(%d) feature <%s:%s> at index %d: %s",
|
||||
if feature_flags:
|
||||
_log.debug("(%d) feature <%s:%s> at index %d: %s",
|
||||
devnumber, _hex(feature), FEATURE_NAME[feature], index,
|
||||
','.join([FEATURE_FLAGS[k] for k in FEATURE_FLAGS if feature_flags & k]))
|
||||
else:
|
||||
_l.log(_LOG_LEVEL, "(%d) feature <%s:%s> at index %d", devnumber, _hex(feature), FEATURE_NAME[feature], index)
|
||||
else:
|
||||
_log.debug("(%d) feature <%s:%s> at index %d", devnumber, _hex(feature), FEATURE_NAME[feature], index)
|
||||
|
||||
features[0] = FEATURE.ROOT
|
||||
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.
|
||||
"""
|
||||
def _makeFirmwareInfo(level, kind, name='', version='', extras=None):
|
||||
return _FirmwareInfo(level, kind, name, version, extras)
|
||||
fw_fi = _get_feature_index(handle, devnumber, FEATURE.FIRMWARE, features)
|
||||
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:
|
||||
fw_count = ord(fw_count[:1])
|
||||
|
||||
fw = []
|
||||
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:
|
||||
level = ord(fw_info[:1]) & 0x0F
|
||||
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])
|
||||
if build:
|
||||
version += ' b%d' % build
|
||||
extras = fw_info[9:].rstrip(b'\x00')
|
||||
if extras:
|
||||
fw_info = _makeFirmwareInfo(level, kind, name, version, extras)
|
||||
else:
|
||||
fw_info = _makeFirmwareInfo(level, kind, name, version)
|
||||
extras = fw_info[9:].rstrip(b'\x00') or None
|
||||
fw_info = _FirmwareInfo(level, kind, name, version, extras)
|
||||
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:
|
||||
fw_info = _makeFirmwareInfo(level, FIRMWARE_KIND[-1])
|
||||
fw_info = _FirmwareInfo(level, FIRMWARE_KIND[-1], '', '', None)
|
||||
|
||||
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)
|
||||
|
||||
|
||||
|
@ -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
|
||||
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:
|
||||
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]
|
||||
|
||||
|
||||
|
@ -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
|
||||
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:
|
||||
name_length = ord(name_length[:1])
|
||||
|
||||
d_name = b''
|
||||
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:
|
||||
name_fragment = name_fragment[:name_length - len(d_name)]
|
||||
d_name += name_fragment
|
||||
|
@ -339,7 +355,7 @@ def get_device_name(handle, devnumber, features=None):
|
|||
break
|
||||
|
||||
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
|
||||
|
||||
|
||||
|
@ -348,22 +364,28 @@ def get_device_battery_level(handle, devnumber, features=None):
|
|||
|
||||
:raises FeatureNotSupported: if the device does not support this feature.
|
||||
"""
|
||||
battery = request(handle, devnumber, FEATURE.BATTERY, features=features)
|
||||
if battery:
|
||||
discharge, dischargeNext, status = _unpack('!BBB', battery[:3])
|
||||
_l.log(_LOG_LEVEL, "(%d) battery %d%% charged, next level %d%% charge, status %d = %s",
|
||||
bat_fi = _get_feature_index(handle, devnumber, FEATURE.BATTERY, features)
|
||||
if bat_fi is not None:
|
||||
battery = _base.request(handle, devnumber, _pack('!BB', bat_fi, 0))
|
||||
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])
|
||||
return (discharge, dischargeNext, BATTERY_STATUS[status])
|
||||
return (discharge, dischargeNext, BATTERY_STATUS[status])
|
||||
|
||||
|
||||
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:
|
||||
keys = []
|
||||
|
||||
count = ord(count[:1])
|
||||
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:
|
||||
key, key_task, flags = _unpack('!HHB', keydata[:5])
|
||||
rki = _ReprogrammableKeyInfo(index, key, KEY_NAME[key], key_task, KEY_NAME[key_task], flags)
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
# Unlikely to be used directly unless you're expanding the API.
|
||||
#
|
||||
|
||||
from logging import getLogger as _Logger
|
||||
from struct import pack as _pack
|
||||
from binascii import hexlify as _hexlify
|
||||
_hex = lambda d: _hexlify(d).decode('ascii').upper()
|
||||
|
@ -12,12 +11,13 @@ from .constants import ERROR_NAME
|
|||
from .exceptions import (NoReceiver as _NoReceiver,
|
||||
FeatureCallError as _FeatureCallError)
|
||||
|
||||
from logging import getLogger
|
||||
_log = getLogger('LUR').getChild('base')
|
||||
del getLogger
|
||||
|
||||
import hidapi as _hid
|
||||
|
||||
|
||||
_LOG_LEVEL = 4
|
||||
_l = _Logger('lur.base')
|
||||
|
||||
#
|
||||
# These values are defined by the Logitech documentation.
|
||||
# Overstepping these boundaries will only produce log warnings.
|
||||
|
@ -48,7 +48,7 @@ DEFAULT_TIMEOUT = 1000
|
|||
|
||||
def _logdebug_hook(reply_code, devnumber, data):
|
||||
"""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.
|
||||
|
@ -78,7 +78,7 @@ def list_receiver_devices():
|
|||
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):
|
||||
"""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:
|
||||
# could be a file permissions issue (did you add the udev rules?)
|
||||
# in any case, unreachable
|
||||
_l.log(_LOG_LEVEL, "[%s] open failed", path)
|
||||
_log.debug("[%s] open failed", path)
|
||||
return None
|
||||
|
||||
_l.log(_LOG_LEVEL, "[%s] receiver handle %X", path, receiver_handle)
|
||||
# ping on device id 0 (always an error)
|
||||
_hid.write(receiver_handle, _PING_RECEIVER)
|
||||
_hid.write(receiver_handle, _COUNT_DEVICES_REQUEST)
|
||||
|
||||
# if this is the right hidraw device, we'll receive a 'bad device' from the UR
|
||||
# otherwise, the read should produce nothing
|
||||
reply = _hid.read(receiver_handle, _MAX_REPLY_SIZE, DEFAULT_TIMEOUT)
|
||||
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
|
||||
_l.log(_LOG_LEVEL, "[%s] success: handle %X", path, receiver_handle)
|
||||
_log.info("[%s] success: handle %X", path, receiver_handle)
|
||||
return receiver_handle
|
||||
|
||||
# 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':
|
||||
# no idea what this is, but it comes up occasionally
|
||||
_l.log(_LOG_LEVEL, "[%s] %X mistery reply [%s]", path, receiver_handle, _hex(reply))
|
||||
else:
|
||||
_l.log(_LOG_LEVEL, "[%s] %X unknown reply [%s]", path, receiver_handle, _hex(reply))
|
||||
if reply == b'\x01\x00\x00\x00\x00\x00\x00\x00':
|
||||
# no idea what this is, but it comes up occasionally
|
||||
_log.debug("[%s] %X mistery reply [%s]", path, receiver_handle, _hex(reply))
|
||||
else:
|
||||
_log.debug("[%s] %X unknown reply [%s]", path, receiver_handle, _hex(reply))
|
||||
else:
|
||||
_l.log(_LOG_LEVEL, "[%s] %X no reply", path, receiver_handle)
|
||||
_log.debug("[%s] %X no reply", path, receiver_handle)
|
||||
|
||||
close(receiver_handle)
|
||||
|
||||
|
@ -132,7 +129,7 @@ def open():
|
|||
:returns: An open file handle for the found receiver, or ``None``.
|
||||
"""
|
||||
for rawdevice in list_receiver_devices():
|
||||
_l.log(_LOG_LEVEL, "checking %s", rawdevice)
|
||||
_log.info("checking %s", rawdevice)
|
||||
|
||||
receiver = try_open(rawdevice.path)
|
||||
if receiver:
|
||||
|
@ -146,10 +143,10 @@ def close(handle):
|
|||
if handle:
|
||||
try:
|
||||
_hid.close(handle)
|
||||
_l.log(_LOG_LEVEL, "closed receiver handle %X", handle)
|
||||
_log.info("closed receiver handle %X", handle)
|
||||
return True
|
||||
except:
|
||||
_l.exception("closing receiver handle %X", handle)
|
||||
_log.exception("closing receiver handle %X", handle)
|
||||
|
||||
return False
|
||||
|
||||
|
@ -168,15 +165,13 @@ def write(handle, devnumber, data):
|
|||
been physically removed from the machine, or the kernel driver has been
|
||||
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 _MAX_CALL_SIZE == 20
|
||||
# the data is padded to either 5 or 18 bytes
|
||||
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):
|
||||
_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)
|
||||
raise _NoReceiver
|
||||
|
||||
|
@ -197,26 +192,33 @@ def read(handle, timeout=DEFAULT_TIMEOUT):
|
|||
been physically removed from the machine, or the kernel driver has been
|
||||
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:
|
||||
_l.warn("(-) read failed, assuming receiver %X no longer available", handle)
|
||||
_log.warn("(-) read failed, assuming receiver %X no longer available", handle)
|
||||
close(handle)
|
||||
raise _NoReceiver
|
||||
|
||||
if data:
|
||||
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:
|
||||
_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])
|
||||
devnumber = ord(data[1:2])
|
||||
if _l.isEnabledFor(_LOG_LEVEL):
|
||||
_l.log(_LOG_LEVEL, "(%d) => r[%02X %02X %s %s]", devnumber, code, devnumber, _hex(data[2:4]), _hex(data[4:]))
|
||||
_log.debug("(%d) => r[%02X %02X %s %s]", devnumber, code, devnumber, _hex(data[2:4]), _hex(data[4:]))
|
||||
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):
|
||||
"""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.
|
||||
:raisees FeatureCallError: if the feature call replied with an error.
|
||||
"""
|
||||
if _l.isEnabledFor(_LOG_LEVEL):
|
||||
_l.log(_LOG_LEVEL, "(%d) request {%s} params [%s]", devnumber, _hex(feature_index_function), _hex(params))
|
||||
if type(params) == int:
|
||||
params = _pack('!B', params)
|
||||
|
||||
_log.debug("(%d) request {%s} params [%s]", devnumber, _hex(feature_index_function), _hex(params))
|
||||
if len(feature_index_function) != 2:
|
||||
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)
|
||||
while retries > 0:
|
||||
divisor = (6 - retries)
|
||||
reply = read(handle, int(DEFAULT_TIMEOUT * (divisor + 1) / 2 / divisor))
|
||||
retries -= 1
|
||||
context.write(handle, devnumber, feature_index_function + params)
|
||||
|
||||
read_times = _MAX_READ_TIMES
|
||||
while read_times > 0:
|
||||
divisor = (1 + _MAX_READ_TIMES - read_times)
|
||||
reply = context.read(handle, int(DEFAULT_TIMEOUT * (divisor + 1) / 2 / divisor))
|
||||
read_times -= 1
|
||||
|
||||
if not reply:
|
||||
# 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))
|
||||
# worst case scenario, this is a reply for a concurrent request
|
||||
# on this receiver
|
||||
if unhandled_hook:
|
||||
unhandled_hook(reply_code, reply_devnumber, reply_data)
|
||||
if _unhandled:
|
||||
_unhandled(reply_code, reply_devnumber, reply_data)
|
||||
continue
|
||||
|
||||
if reply_code == 0x10 and reply_data[:1] == b'\x8F' and reply_data[1:3] == feature_index_function:
|
||||
# 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
|
||||
|
||||
if reply_code == 0x10 and reply_data[:1] == b'\x8F':
|
||||
# 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
|
||||
|
||||
if reply_code == 0x11 and reply_data[0] == b'\xFF' and reply_data[1:3] == feature_index_function:
|
||||
# the feature call returned with an error
|
||||
error_code = ord(reply_data[3])
|
||||
_l.warn("(%d) request feature call error %d = %s: %s", devnumber, error_code, 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_function = feature_index_function[1:2]
|
||||
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:
|
||||
# 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:]
|
||||
|
||||
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
|
||||
# _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:]
|
||||
|
||||
# _l.log(_LOG_LEVEL, "(%d) unmatched reply {%s} (expected {%s})", devnumber, _hex(reply_data[:2]), _hex(feature_index_function))
|
||||
if unhandled_hook:
|
||||
unhandled_hook(reply_code, reply_devnumber, reply_data)
|
||||
# _log.debug("(%d) unmatched reply {%s} (expected {%s})", devnumber, _hex(reply_data[:2]), _hex(feature_index_function))
|
||||
if _unhandled:
|
||||
_unhandled(reply_code, reply_devnumber, reply_data)
|
||||
|
|
|
@ -2,8 +2,7 @@
|
|||
#
|
||||
#
|
||||
|
||||
from logging import getLogger as _Logger
|
||||
from threading import (Thread, Event, Lock)
|
||||
from threading import Thread as _Thread
|
||||
# from time import sleep as _sleep
|
||||
|
||||
from . import base as _base
|
||||
|
@ -12,108 +11,107 @@ from .common import Packet as _Packet
|
|||
|
||||
# for both Python 2 and 3
|
||||
try:
|
||||
from Queue import Queue
|
||||
from Queue import Queue as _Queue
|
||||
except ImportError:
|
||||
from queue import Queue
|
||||
from queue import Queue as _Queue
|
||||
|
||||
|
||||
_LOG_LEVEL = 6
|
||||
_l = _Logger('lur.listener')
|
||||
from logging import getLogger
|
||||
_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):
|
||||
# _l.log(_LOG_LEVEL, "starting dispatch")
|
||||
while listener._active: # or not listener._events.empty():
|
||||
event = listener._events.get()
|
||||
# _l.log(_LOG_LEVEL, "delivering event %s", event)
|
||||
try:
|
||||
event = listener._events.get(True, _READ_EVENT_TIMEOUT * 10)
|
||||
except:
|
||||
continue
|
||||
_log.debug("delivering event %s", event)
|
||||
try:
|
||||
callback(event)
|
||||
except:
|
||||
_l.exception("callback for %s", event)
|
||||
# _l.log(_LOG_LEVEL, "stopped dispatch")
|
||||
_log.exception("callback for %s", event)
|
||||
|
||||
|
||||
class EventsListener(Thread):
|
||||
class EventsListener(_Thread):
|
||||
"""Listener thread for events from the Unifying Receiver.
|
||||
|
||||
Incoming packets will be passed to the callback function in sequence, by a
|
||||
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):
|
||||
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._active = False
|
||||
|
||||
self._handle = receiver_handle
|
||||
|
||||
self._task = None
|
||||
self._task_processing = Lock()
|
||||
self._task_reply = None
|
||||
self._task_done = Event()
|
||||
self._tasks = _Queue(1)
|
||||
self._backup_unhandled_hook = _base.unhandled_hook
|
||||
_base.unhandled_hook = self.unhandled_hook
|
||||
|
||||
self._events = Queue(32)
|
||||
_base.unhandled_hook = self._unhandled
|
||||
|
||||
self._dispatcher = Thread(group='Unifying Receiver',
|
||||
name='%s-%X-dispatch' % (self.__class__.__name__, receiver_handle),
|
||||
self._events = _Queue(32)
|
||||
self._dispatcher = _Thread(group='Unifying Receiver',
|
||||
name=self.__class__.__name__ + '-dispatch',
|
||||
target=_event_dispatch, args=(self, events_callback))
|
||||
self._dispatcher.daemon = True
|
||||
|
||||
def run(self):
|
||||
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()
|
||||
|
||||
while self._active:
|
||||
event = None
|
||||
try:
|
||||
# _log.debug("read next event")
|
||||
event = _base.read(self._handle, _READ_EVENT_TIMEOUT)
|
||||
except _NoReceiver:
|
||||
self._handle = 0
|
||||
_l.warn("receiver disconnected")
|
||||
_log.warn("receiver disconnected")
|
||||
self._events.put(_Packet(0xFF, 0xFF, None))
|
||||
self._active = False
|
||||
break
|
||||
|
||||
if event:
|
||||
event = _Packet(*event)
|
||||
_l.log(_LOG_LEVEL, "queueing event %s", event)
|
||||
self._events.put(event)
|
||||
if event is not None:
|
||||
matched = False
|
||||
task = None if self._tasks.empty() else self._tasks.queue[0]
|
||||
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:
|
||||
(api_function, args, kwargs), self._task = self._task, None
|
||||
# _l.log(_LOG_LEVEL, "calling '%s.%s' with %s, %s", api_function.__module__, api_function.__name__, args, kwargs)
|
||||
try:
|
||||
self._task_reply = api_function.__call__(self._handle, *args, **kwargs)
|
||||
except _NoReceiver as nr:
|
||||
self._handle = 0
|
||||
_l.warn("receiver disconnected")
|
||||
self._events.put(_Packet(0xFF, 0xFF, None))
|
||||
self._task_reply = nr
|
||||
self._active = False
|
||||
break
|
||||
except Exception as e:
|
||||
# _l.exception("task %s.%s", api_function.__module__, api_function.__name__)
|
||||
self._task_reply = e
|
||||
finally:
|
||||
self._task_done.set()
|
||||
if matched:
|
||||
# _log.debug("request reply %s", event)
|
||||
task[-1] = event
|
||||
self._tasks.task_done()
|
||||
else:
|
||||
event = _Packet(*event)
|
||||
_log.info("queueing event %s", event)
|
||||
self._events.put(event)
|
||||
|
||||
_base.request_context = None
|
||||
_base.close(self._handle)
|
||||
_log.debug("stopped")
|
||||
self._handle = 0
|
||||
|
||||
def stop(self):
|
||||
"""Tells the listener to stop as soon as possible."""
|
||||
if self._active:
|
||||
_l.log(_LOG_LEVEL, "stopping")
|
||||
_log.debug("stopping")
|
||||
self._active = False
|
||||
# wait for the receiver handle to be closed
|
||||
self.join()
|
||||
|
@ -122,34 +120,27 @@ class EventsListener(Thread):
|
|||
def handle(self):
|
||||
return self._handle
|
||||
|
||||
def request(self, device, feature_function_index, params=b''):
|
||||
return self.call_api(_base.request, device, feature_function_index, params)
|
||||
def write(self, handle, devnumber, data):
|
||||
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):
|
||||
"""Make an UR API request through this listener's receiver.
|
||||
def read(self, handle, timeout=_base.DEFAULT_TIMEOUT):
|
||||
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
|
||||
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):
|
||||
def unhandled_hook(self, 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)
|
||||
|
||||
def __nonzero__(self):
|
||||
|
|
Loading…
Reference in New Issue