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
|
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)
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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 "$@"
|
||||||
|
|
|
||||||
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 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 "$@"
|
||||||
|
|
|
||||||
|
|
@ -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 "$@"
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
|
||||||
|
|
@ -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')
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue