reworked the event listener

This commit is contained in:
Daniel Pavel 2012-10-10 06:37:03 +03:00
parent c61297a871
commit e34ad5104f
20 changed files with 304 additions and 214 deletions

View File

@ -2,27 +2,28 @@
#
#
import threading
from gi.repository import Gtk
from gi.repository import GObject
from .watcher import WatcherThread
from . import ui
from logitech.devices import constants as C
APP_TITLE = 'Solaar'
def _status_updated(watcher, icon, window):
while True:
watcher.status_changed.wait()
text = watcher.status_text
watcher.status_changed.clear()
icon_name = APP_TITLE + '-fail' if watcher.rstatus.code < C.STATUS.CONNECTED else APP_TITLE
if icon:
GObject.idle_add(ui.icon.update, icon, watcher.rstatus, text)
GObject.idle_add(ui.icon.update, icon, watcher.rstatus, text, icon_name)
if window:
GObject.idle_add(ui.window.update, window, watcher.rstatus, dict(watcher.devices))
GObject.idle_add(ui.window.update, window, watcher.rstatus, dict(watcher.devices), icon_name)
def run(config):
@ -30,16 +31,22 @@ def run(config):
ui.notify.init(APP_TITLE, config.notifications)
from .watcher import WatcherThread
watcher = WatcherThread(ui.notify.show)
watcher.start()
window = ui.window.create(APP_TITLE, watcher.rstatus, not config.start_hidden, config.close_to_tray)
tray_icon = ui.icon.create(APP_TITLE, (ui.window.toggle, window))
window.set_icon_name(APP_TITLE + '-fail')
tray_icon = ui.icon.create(APP_TITLE, (ui.window.toggle, window))
tray_icon.set_from_icon_name(APP_TITLE + '-fail')
import threading
ui_update_thread = threading.Thread(target=_status_updated, name='ui_update', args=(watcher, tray_icon, window))
ui_update_thread.daemon = True
ui_update_thread.start()
from gi.repository import Gtk
Gtk.main()
watcher.stop()

View File

@ -0,0 +1,41 @@
#
#
#
import logging
from logitech.unifying_receiver import api
from logitech import devices
from . import ui
import ui.pair
def full_scan(button, watcher):
if watcher.active and watcher.listener:
updated = False
for devnumber in range(1, 1 + api.C.MAX_ATTACHED_DEVICES):
devstatus = watcher.devices.get(devnumber)
if devstatus:
status = devices.request_status(devstatus, watcher.listener)
updated |= watcher._device_status_changed(devstatus, status)
else:
devstatus = watcher._new_device(devnumber)
updated |= devstatus is not None
if updated:
watcher._update_status_text()
return updated
def pair(button, watcher):
if watcher.active and watcher.listener:
logging.debug("pair")
parent = button.get_toplevel()
title = parent.get_title() + ': ' + button.get_tooltip_text()
w = ui.pair.create(parent, title)
w.run()
w.destroy()

View File

@ -4,14 +4,9 @@
from gi.repository import Gtk
from logitech.devices import constants as C
_ICON_OK = 'Solaar'
_ICON_FAIL = _ICON_OK + '-fail'
def create(title, click_action=None):
icon = Gtk.StatusIcon.new_from_icon_name(_ICON_OK)
icon = Gtk.StatusIcon()
icon.set_title(title)
icon.set_name(title)
@ -37,9 +32,8 @@ def create(title, click_action=None):
return icon
def update(icon, receiver, tooltip):
icon.set_tooltip_markup(tooltip)
if receiver.code < C.STATUS.CONNECTED:
icon.set_from_icon_name(_ICON_FAIL)
else:
icon.set_from_icon_name(_ICON_OK)
def update(icon, receiver, tooltip=None, icon_name=None):
if tooltip is not None:
icon.set_tooltip_markup(tooltip)
if icon_name is not None:
icon.set_from_icon_name(icon_name)

17
app/ui/pair.py Normal file
View File

@ -0,0 +1,17 @@
#
#
#
from gi.repository import Gtk
def create(parent_window, title):
window = Gtk.Dialog(title, parent_window, Gtk.DialogFlags.MODAL, buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL))
Gtk.Window.set_default_icon_name('add')
window.set_resizable(False)
# window.set_wmclass(title, 'status-window')
# window.set_role('pair')
return window

View File

@ -77,8 +77,11 @@ def _update_device_box(frame, devstatus):
expander.set_label(_PLACEHOLDER)
def update(window, receiver, devices):
def update(window, receiver, devices, icon_name=None):
if window and window.get_child():
if icon_name is not None:
window.set_icon_name(icon_name)
controls = list(window.get_child().get_children())
_update_receiver_box(controls[0], receiver)
for index in range(1, 1 + _MAX_DEVICES):
@ -107,9 +110,9 @@ def _receiver_box(rstatus):
buttons_box.set_layout(Gtk.ButtonBoxStyle.START)
vbox.pack_start(buttons_box, True, True, 0)
def _action(button, action):
def _action(button, function, params):
button.set_sensitive(False)
action()
function(button, *params)
button.set_sensitive(True)
def _add_button(name, icon, action):
@ -119,7 +122,9 @@ def _receiver_box(rstatus):
button.set_tooltip_text(name)
button.set_focus_on_click(False)
if action:
button.connect('clicked', _action, action)
function = action[0]
params = action[1:]
button.connect('clicked', _action, function, params)
else:
button.set_sensitive(False)
buttons_box.pack_start(button, False, False, 0)
@ -167,14 +172,12 @@ def _device_box():
def create(title, rstatus, show=True, close_to_tray=False):
window = Gtk.Window()
Gtk.Window.set_default_icon_name('mouse')
window.set_icon_name(title)
window.set_title(title)
window.set_keep_above(True)
window.set_deletable(False)
window.set_resizable(False)
window.set_size_request(200, 50)
# window.set_size_request(200, 50)
window.set_default_size(200, 50)
window.set_position(Gtk.WindowPosition.MOUSE)

View File

@ -5,13 +5,17 @@
import logging
import threading
import time
from binascii import hexlify as _hexlify
from logitech.unifying_receiver import api
from logitech.unifying_receiver.listener import EventsListener
from logitech import devices
from logitech.devices import constants as C
from . import actions
_l = logging.getLogger('watcher')
_STATUS_TIMEOUT = 31 # seconds
_THREAD_SLEEP = 2 # seconds
@ -30,11 +34,14 @@ class _DevStatus(api.AttachedDeviceInfo):
text = _INITIALIZING
refresh = None
def __str__(self):
return 'DevStatus(%d,%s,%d)' % (self.number, self.name, self.code)
class WatcherThread(threading.Thread):
"""Keeps a map of all attached devices and their statuses."""
def __init__(self, notify_callback=None):
super(WatcherThread, self).__init__(name='WatcherThread')
super(WatcherThread, self).__init__(group='Solaar', name='Watcher')
self.daemon = True
self.active = False
@ -44,9 +51,9 @@ class WatcherThread(threading.Thread):
self.listener = None
self.rstatus = _DevStatus(0, _UNIFYING_RECEIVER, _UNIFYING_RECEIVER, None, None)
self.rstatus.refresh = self.full_scan
self.rstatus.pair = None
self.rstatus = _DevStatus(0, 0xFF, None, _UNIFYING_RECEIVER, None, None)
self.rstatus.refresh = (actions.full_scan, self)
self.rstatus.pair = None # (actions.pair, self)
self.devices = {}
@ -55,7 +62,7 @@ class WatcherThread(threading.Thread):
while self.active:
if self.listener is None:
self._device_status_changed(self.rstatus, (C.STATUS.UNKNOWN, _INITIALIZING))
self._device_status_changed(self.rstatus, (C.STATUS.UNKNOWN, _SCANNING))
self._update_status_text()
receiver = api.open()
@ -65,57 +72,38 @@ class WatcherThread(threading.Thread):
for devinfo in api.list_devices(receiver):
self._new_device(devinfo)
logging.debug("initial scan finished: %s", self.devices)
if self.devices:
self._device_status_changed(self.rstatus, (C.STATUS.CONNECTED, _OKAY))
else:
self._device_status_changed(self.rstatus, (C.STATUS.CONNECTED, _NO_DEVICES))
self._update_status_text()
self._update_status_text()
self.listener = EventsListener(receiver, self._events_callback)
self.listener.start()
else:
self._device_status_changed(self.rstatus, (C.STATUS.UNAVAILABLE, _NO_RECEIVER))
elif not self.listener.active:
elif not self.listener:
self.listener = None
self._device_status_changed(self.rstatus, (C.STATUS.UNAVAILABLE, _NO_RECEIVER))
self.devices.clear()
if self.active:
update_icon = True
if self.listener and self.devices:
update_icon &= self._check_old_statuses()
if self.listener:
if self.devices:
update_icon = self._check_old_statuses()
else:
update_icon = self._device_status_changed(self.rstatus, (C.STATUS.CONNECTED, _NO_DEVICES))
else:
update_icon = self._device_status_changed(self.rstatus, (C.STATUS.UNAVAILABLE, _NO_RECEIVER))
if update_icon:
self._update_status_text()
if self.active:
if update_icon:
self._update_status_text()
time.sleep(_THREAD_SLEEP)
self.listener.stop()
if self.listener:
self.listener.stop()
api.close(self.listener.receiver)
self.listener = None
self.listener = None
def stop(self):
self.active = False
self.join()
def full_scan(self, *args):
if self.active and self.listener:
updated = False
for devnumber in range(1, 1 + api.C.MAX_ATTACHED_DEVICES):
devstatus = self.devices.get(devnumber)
if devstatus:
status = devices.request_status(devstatus, self.listener)
updated |= self._device_status_changed(devstatus, status)
else:
devstatus = self._new_device(devnumber)
updated |= devstatus is not None
if updated:
self._update_status_text()
def _request_status(self, devstatus):
if self.listener and devstatus:
status = devices.request_status(devstatus, self.listener)
@ -124,7 +112,7 @@ class WatcherThread(threading.Thread):
def _check_old_statuses(self):
updated = False
for devstatus in list(self.devices.values()):
for devstatus in self.devices.values():
if devstatus != self.rstatus:
if time.time() - devstatus.timestamp > _STATUS_TIMEOUT:
status = devices.ping(devstatus, self.listener)
@ -137,17 +125,20 @@ class WatcherThread(threading.Thread):
return None
if type(dev) == int:
# assert self.listener
dev = self.listener.request(api.get_device_info, dev)
if dev:
devstatus = _DevStatus(*dev)
devstatus.refresh = self._request_status
self.devices[dev.number] = devstatus
_l.debug("new devstatus %s", devstatus)
self._device_status_changed(devstatus, C.STATUS.CONNECTED)
logging.debug("new devstatus %s", devstatus)
self._device_status_changed(self.rstatus, (C.STATUS.CONNECTED, _OKAY))
return devstatus
def _events_callback(self, code, devnumber, data):
logging.debug("%s: event (%02x %02x [%s])", time.asctime(), code, devnumber, _hexlify(data))
# _l.debug("event %s", (code, devnumber, data))
updated = False
@ -160,12 +151,12 @@ class WatcherThread(threading.Thread):
status = devices.process_event(devstatus, self.listener, data)
updated |= self._device_status_changed(devstatus, status)
else:
logging.warn("unknown event code %02x", code)
_l.warn("unknown event code %02x", code)
elif devnumber:
self._new_device(devnumber)
updated = True
else:
logging.warn("don't know how to handle event (%02x, %02x, [%s])", code, devnumber, _hexlify(data))
_l.warn("don't know how to handle event %s", (code, devnumber, data))
if updated:
self._update_status_text()
@ -196,21 +187,21 @@ class WatcherThread(threading.Thread):
status_code = C.STATUS.UNKNOWN
status_text = ''
if not (status_code == C.STATUS.CONNECTED and old_status_code > C.STATUS.CONNECTED):
# if this is not just a ping for a device with an already known status
devstatus.code = status_code
devstatus.text = status_text
logging.debug("%s: device '%s' status update %s => %s: %s", time.asctime(), devstatus.name, old_status_code, status_code, status_text)
if ((status_code == old_status_code and status_text == devstatus.text) or
(status_code == C.STATUS.CONNECTED and old_status_code > C.STATUS.CONNECTED)):
# this is just successful ping for a device with an already known status
return False
devstatus.code = status_code
devstatus.text = status_text
_l.debug("%s status update %s => %s: %s", devstatus, old_status_code, status_code, status_text)
if self.notify:
if status_code < C.STATUS.CONNECTED or old_status_code < C.STATUS.CONNECTED or status_code < old_status_code:
self._notify(devstatus.code, devstatus.name, devstatus.text)
self.notify(devstatus.code, devstatus.name, devstatus.text)
return True
def _notify(self, *args):
if self.notify:
self.notify(*args)
def _update_status_text(self):
last_status_text = self.status_text

View File

@ -44,10 +44,10 @@ def _continuous_read(handle, timeout=1000):
if __name__ == '__main__':
import argparse
arg_parser = argparse.ArgumentParser()
arg_parser.add_argument('device', default=None,
help='linux device to connect to')
arg_parser.add_argument('--history', default='.hidconsole-history',
help='history file')
arg_parser.add_argument('device', default=None,
help='linux device to connect to')
args = arg_parser.parse_args()
import hidapi

View File

@ -18,8 +18,8 @@ NAME = 'Wireless Solar Keyboard K750'
#
#
def _trigger_solar_charge_events(receiver, devinfo):
return _api.request(receiver, devinfo.number,
def _trigger_solar_charge_events(handle, devinfo):
return _api.request(handle, devinfo.number,
feature=_api.C.FEATURE.SOLAR_CHARGE, function=b'\x03', params=b'\x78\x01',
features_array=devinfo.features)

View File

@ -23,3 +23,9 @@ http://6xq.net/git/lars/lshidpp.git/plain/doc/
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')

View File

@ -58,13 +58,13 @@ def request(handle, devnumber, feature, function=b'\x00', params=b'', features_a
if features_array is None:
features_array = get_device_features(handle, devnumber)
if features_array is None:
_l.log(_LOG_LEVEL, "(%d,%d) no features array available", handle, devnumber)
_l.log(_LOG_LEVEL, "(%d) no features array available", devnumber)
return None
if feature in features_array:
feature_index = _pack('!B', features_array.index(feature))
if feature_index is None:
_l.warn("(%d,%d) feature <%s:%s> not supported", handle, devnumber, _hexlify(feature), C.FEATURE_NAME[feature])
_l.warn("(%d) feature <%s:%s> not supported", devnumber, _hexlify(feature), C.FEATURE_NAME[feature])
raise E.FeatureNotSupported(devnumber, feature)
return _base.request(handle, devnumber, feature_index + function, params)
@ -91,7 +91,7 @@ def find_device_by_name(handle, device_name):
:returns: an AttachedDeviceInfo tuple, or ``None``.
"""
_l.log(_LOG_LEVEL, "(%d,) searching for device '%s'", handle, device_name)
_l.log(_LOG_LEVEL, "searching for device '%s'", device_name)
for devnumber in range(1, 1 + C.MAX_ATTACHED_DEVICES):
features_array = get_device_features(handle, devnumber)
@ -106,7 +106,7 @@ def list_devices(handle):
:returns: a list of AttachedDeviceInfo tuples.
"""
_l.log(_LOG_LEVEL, "(%d,) listing all devices", handle)
_l.log(_LOG_LEVEL, "listing all devices")
devices = []
@ -131,8 +131,8 @@ def get_device_info(handle, devnumber, device_name=None, features_array=None):
d_type = get_device_type(handle, devnumber, features_array)
d_name = get_device_name(handle, devnumber, features_array) if device_name is None else device_name
d_firmware = get_device_firmware(handle, devnumber, features_array)
devinfo = AttachedDeviceInfo(devnumber, d_type, d_name, d_firmware, features_array)
_l.log(_LOG_LEVEL, "(%d,%d) found device %s", handle, devnumber, devinfo)
devinfo = AttachedDeviceInfo(handle, devnumber, d_type, d_name, d_firmware, features_array)
_l.log(_LOG_LEVEL, "(%d) found device %s", devnumber, devinfo)
return devinfo
@ -141,7 +141,7 @@ def get_feature_index(handle, devnumber, feature):
:returns: An int, or ``None`` if the feature is not available.
"""
_l.log(_LOG_LEVEL, "(%d,%d) get feature index <%s:%s>", handle, devnumber, _hexlify(feature), C.FEATURE_NAME[feature])
_l.log(_LOG_LEVEL, "(%d) get feature index <%s:%s>", devnumber, _hexlify(feature), C.FEATURE_NAME[feature])
if len(feature) != 2:
raise ValueError("invalid feature <%s>: it must be a two-byte string" % feature)
@ -154,18 +154,18 @@ def get_feature_index(handle, devnumber, feature):
feature_flags = ord(reply[1:2]) & 0xE0
if _l.isEnabledFor(_LOG_LEVEL):
if feature_flags:
_l.log(_LOG_LEVEL, "(%d,%d) feature <%s:%s> has index %d: %s",
handle, devnumber, _hexlify(feature), C.FEATURE_NAME[feature], feature_index,
_l.log(_LOG_LEVEL, "(%d) feature <%s:%s> has index %d: %s",
devnumber, _hexlify(feature), C.FEATURE_NAME[feature], feature_index,
','.join([C.FEATURE_FLAGS[k] for k in C.FEATURE_FLAGS if feature_flags & k]))
else:
_l.log(_LOG_LEVEL, "(%d,%d) feature <%s:%s> has index %d", handle, devnumber, _hexlify(feature), C.FEATURE_NAME[feature], feature_index)
_l.log(_LOG_LEVEL, "(%d) feature <%s:%s> has index %d", devnumber, _hexlify(feature), C.FEATURE_NAME[feature], feature_index)
# if feature_flags:
# raise E.FeatureNotSupported(devnumber, feature)
return feature_index
_l.warn("(%d,%d) feature <%s:%s> not supported by the device", handle, devnumber, _hexlify(feature), C.FEATURE_NAME[feature])
_l.warn("(%d) feature <%s:%s> not supported by the device", devnumber, _hexlify(feature), C.FEATURE_NAME[feature])
raise E.FeatureNotSupported(devnumber, feature)
@ -175,13 +175,13 @@ def get_device_features(handle, devnumber):
Their position in the array is the index to be used when requesting that
feature on the device.
"""
_l.log(_LOG_LEVEL, "(%d,%d) get device features", handle, devnumber)
_l.log(_LOG_LEVEL, "(%d) get device features", devnumber)
# get the index of the FEATURE_SET
# FEATURE.ROOT should always be available for all devices
fs_index = _base.request(handle, devnumber, C.FEATURE.ROOT, C.FEATURE.FEATURE_SET)
if fs_index is None:
# _l.warn("(%d,%d) FEATURE_SET not available", handle, device)
# _l.warn("(%d) FEATURE_SET not available", device)
return None
fs_index = fs_index[:1]
@ -193,11 +193,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,%d) no features available?!", handle, devnumber)
_l.log(_LOG_LEVEL, "(%d) no features available?!", devnumber)
return None
features_count = ord(features_count[:1])
_l.log(_LOG_LEVEL, "(%d,%d) found %d features", handle, devnumber, features_count)
_l.log(_LOG_LEVEL, "(%d) found %d features", devnumber, features_count)
features = [None] * 0x20
for index in range(1, 1 + features_count):
@ -210,11 +210,11 @@ def get_device_features(handle, devnumber):
if _l.isEnabledFor(_LOG_LEVEL):
if feature_flags:
_l.log(_LOG_LEVEL, "(%d,%d) feature <%s:%s> at index %d: %s",
handle, devnumber, _hexlify(feature), C.FEATURE_NAME[feature], index,
_l.log(_LOG_LEVEL, "(%d) feature <%s:%s> at index %d: %s",
devnumber, _hexlify(feature), C.FEATURE_NAME[feature], index,
','.join([C.FEATURE_FLAGS[k] for k in C.FEATURE_FLAGS if feature_flags & k]))
else:
_l.log(_LOG_LEVEL, "(%d,%d) feature <%s:%s> at index %d", handle, devnumber, _hexlify(feature), C.FEATURE_NAME[feature], index)
_l.log(_LOG_LEVEL, "(%d) feature <%s:%s> at index %d", devnumber, _hexlify(feature), C.FEATURE_NAME[feature], index)
features[0] = C.FEATURE.ROOT
while features[-1] is None:
@ -258,7 +258,7 @@ def get_device_firmware(handle, devnumber, features_array=None):
fw_info = _makeFirmwareInfo(level=fw_level, type=C.FIRMWARE_TYPE[-1])
fw.append(fw_info)
_l.log(_LOG_LEVEL, "(%d:%d) firmware %s", handle, devnumber, fw_info)
_l.log(_LOG_LEVEL, "(%d) firmware %s", devnumber, fw_info)
return fw
@ -272,7 +272,7 @@ def get_device_type(handle, devnumber, features_array=None):
d_type = request(handle, devnumber, C.FEATURE.NAME, function=b'\x20', features_array=features_array)
if d_type:
d_type = ord(d_type[:1])
_l.log(_LOG_LEVEL, "(%d,%d) device type %d = %s", handle, devnumber, d_type, C.DEVICE_TYPE[d_type])
_l.log(_LOG_LEVEL, "(%d) device type %d = %s", devnumber, d_type, C.DEVICE_TYPE[d_type])
return C.DEVICE_TYPE[d_type]
@ -297,7 +297,7 @@ def get_device_name(handle, devnumber, features_array=None):
break
d_name = d_name.decode('ascii')
_l.log(_LOG_LEVEL, "(%d,%d) device name %s", handle, devnumber, d_name)
_l.log(_LOG_LEVEL, "(%d) device name %s", devnumber, d_name)
return d_name
@ -309,7 +309,8 @@ def get_device_battery_level(handle, devnumber, features_array=None):
battery = request(handle, devnumber, C.FEATURE.BATTERY, features_array=features_array)
if battery:
discharge, dischargeNext, status = _unpack('!BBB', battery[:3])
_l.log(_LOG_LEVEL, "(%d:%d) battery %d%% charged, next level %d%% charge, status %d = %s", discharge, dischargeNext, status, C.BATTERY_STATUSE[status])
_l.log(_LOG_LEVEL, "(%d) battery %d%% charged, next level %d%% charge, status %d = %s",
devnumber, discharge, dischargeNext, status, C.BATTERY_STATUSE[status])
return (discharge, dischargeNext, C.BATTERY_STATUS[status])

View File

@ -10,11 +10,10 @@ from binascii import hexlify as _hexlify
from . import constants as C
from . import exceptions as E
from . import unhandled as _unhandled
import hidapi as _hid
_LOG_LEVEL = 5
_LOG_LEVEL = 4
_l = logging.getLogger('lur.base')
#
@ -45,6 +44,31 @@ DEFAULT_TIMEOUT = 1000
#
#
def _logdebug_hook(reply_code, devnumber, data):
"""Default unhandled hook, logs the reply as DEBUG."""
_l.debug("UNHANDLED %s", (reply_code, devnumber, reply_code, data))
"""The function that will be called on unhandled incoming events.
The hook must be a function with the signature: ``_(int, int, str)``, where
the parameters are: (reply_code, devnumber, data).
This hook will only be called by the request() function, when it receives
replies that do not match the requested feature call. As such, it is not
suitable for intercepting broadcast events from the device (e.g. special
keys being pressed, battery charge events, etc), at least not in a timely
manner. However, these events *may* be delivered here if they happen while
doing a feature call to the device.
The default implementation logs the unhandled reply as DEBUG.
"""
unhandled_hook = _logdebug_hook
#
#
#
def list_receiver_devices():
"""List all the Linux devices exposed by the UR attached to the machine."""
# (Vendor ID, Product ID) = ('Logitech', 'Unifying Receiver')
@ -72,7 +96,7 @@ def try_open(path):
_l.log(_LOG_LEVEL, "[%s] open failed", path)
return None
_l.log(_LOG_LEVEL, "[%s] receiver handle (%d,)", path, receiver_handle)
_l.log(_LOG_LEVEL, "[%s] receiver handle 0x%x", path, receiver_handle)
# ping on device id 0 (always an error)
_hid.write(receiver_handle, b'\x10\x00\x00\x10\x00\x00\xAA')
@ -82,18 +106,18 @@ def try_open(path):
if reply:
if reply[:4] == b'\x10\x00\x8F\x00':
# 'device 0 unreachable' is the expected reply from a valid receiver handle
_l.log(_LOG_LEVEL, "[%s] success: handle (%d,)", path, receiver_handle)
_l.log(_LOG_LEVEL, "[%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] (%d,) mistery reply [%s]", path, receiver_handle, _hexlify(reply))
_l.log(_LOG_LEVEL, "[%s] %x mistery reply [%s]", path, receiver_handle, _hexlify(reply))
else:
_l.log(_LOG_LEVEL, "[%s] (%d,) unknown reply [%s]", path, receiver_handle, _hexlify(reply))
_l.log(_LOG_LEVEL, "[%s] %x unknown reply [%s]", path, receiver_handle, _hexlify(reply))
else:
_l.log(_LOG_LEVEL, "[%s] (%d,) no reply", path, receiver_handle)
_l.log(_LOG_LEVEL, "[%s] %x no reply", path, receiver_handle)
close(receiver_handle)
@ -118,10 +142,10 @@ def close(handle):
if handle:
try:
_hid.close(handle)
_l.log(_LOG_LEVEL, "(%d,) closed", handle)
_l.log(_LOG_LEVEL, "%x closed", handle)
return True
except:
_l.exception("(%d,) closing", handle)
_l.exception("%x closing", handle)
return False
@ -147,10 +171,10 @@ def write(handle, devnumber, data):
if _l.isEnabledFor(_LOG_LEVEL):
hexs = _hexlify(wdata)
_l.log(_LOG_LEVEL, "(%d,%d) <= w[%s %s %s %s]", handle, devnumber, hexs[0:2], hexs[2:4], hexs[4:8], hexs[8:])
_l.log(_LOG_LEVEL, "(%d) <= w[%s %s %s %s]", devnumber, hexs[0:2], hexs[2:4], hexs[4:8], hexs[8:])
if not _hid.write(handle, wdata):
_l.warn("(%d,%d) write failed, assuming receiver no longer available", handle, devnumber)
_l.warn("(%d) write failed, assuming receiver %x no longer available", devnumber, handle)
close(handle)
raise E.NoReceiver
@ -173,23 +197,23 @@ def read(handle, timeout=DEFAULT_TIMEOUT):
"""
data = _hid.read(handle, _MAX_REPLY_SIZE * 2, timeout)
if data is None:
_l.warn("(%d,*) read failed, assuming receiver no longer available", handle)
_l.warn("(-) read failed, assuming receiver %x no longer available", handle)
close(handle)
raise E.NoReceiver
if data:
if len(data) < _MIN_REPLY_SIZE:
_l.warn("(%d,*) => r[%s] read packet too short: %d bytes", handle, _hexlify(data), len(data))
_l.warn("(%d) => r[%s] read packet too short: %d bytes", ord(data[1:2]), _hexlify(data), len(data))
if len(data) > _MAX_REPLY_SIZE:
_l.warn("(%d,*) => r[%s] read packet too long: %d bytes", handle, _hexlify(data), len(data))
_l.warn("(%d) => r[%s] read packet too long: %d bytes", ord(data[1:2]), _hexlify(data), len(data))
if _l.isEnabledFor(_LOG_LEVEL):
hexs = _hexlify(data)
_l.log(_LOG_LEVEL, "(%d,*) => r[%s %s %s %s]", handle, hexs[0:2], hexs[2:4], hexs[4:8], hexs[8:])
_l.log(_LOG_LEVEL, "(%d) => r[%s %s %s %s]", ord(data[1:2]), hexs[0:2], hexs[2:4], hexs[4:8], hexs[8:])
code = ord(data[:1])
devnumber = ord(data[1:2])
return code, devnumber, data[2:]
# _l.log(_LOG_LEVEL, "(%d,*) => r[]", handle)
# _l.log(_LOG_LEVEL, "(-) => r[]", handle)
def request(handle, devnumber, feature_index_function, params=b'', features_array=None):
@ -210,7 +234,7 @@ def request(handle, devnumber, feature_index_function, params=b'', features_arra
:raisees FeatureCallError: if the feature call replied with an error.
"""
if _l.isEnabledFor(_LOG_LEVEL):
_l.log(_LOG_LEVEL, "(%d,%d) request {%s} params [%s]", handle, devnumber, _hexlify(feature_index_function), _hexlify(params))
_l.log(_LOG_LEVEL, "(%d) request {%s} params [%s]", devnumber, _hexlify(feature_index_function), _hexlify(params))
if len(feature_index_function) != 2:
raise ValueError('invalid feature_index_function {%s}: it must be a two-byte string' % _hexlify(feature_index_function))
@ -230,26 +254,27 @@ def request(handle, devnumber, feature_index_function, params=b'', features_arra
if reply_devnumber != devnumber:
# this message not for the device we're interested in
_l.log(_LOG_LEVEL, "(%d,%d) request got reply for unexpected device %d: [%s]", handle, devnumber, reply_devnumber, _hexlify(reply_data))
_l.log(_LOG_LEVEL, "(%d) request got reply for unexpected device %d: [%s]", devnumber, reply_devnumber, _hexlify(reply_data))
# worst case scenario, this is a reply for a concurrent request
# on this receiver
_unhandled._publish(reply_code, reply_devnumber, reply_data)
if unhandled_hook:
unhandled_hook(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,%d) request ping failed on {%s} call: [%s]", handle, devnumber, _hexlify(feature_index_function), _hexlify(reply_data))
_l.log(_LOG_LEVEL, "(%d) request ping failed on {%s} call: [%s]", devnumber, _hexlify(feature_index_function), _hexlify(reply_data))
return None
if reply_code == 0x10 and reply_data[:1] == b'\x8F':
# device not present
_l.log(_LOG_LEVEL, "(%d,%d) request ping failed: [%s]", handle, devnumber, _hexlify(reply_data))
_l.log(_LOG_LEVEL, "(%d) request ping failed: [%s]", devnumber, _hexlify(reply_data))
return None
if reply_code == 0x11 and reply_data[0] == b'\xFF' and reply_data[1:3] == feature_index_function:
# an error returned from the device
error_code = ord(reply_data[3])
_l.warn("(%d,%d) request feature call error %d = %s: %s", handle, devnumber, error_code, C.ERROR_NAME[error_code], _hexlify(reply_data))
_l.warn("(%d) request feature call error %d = %s: %s", devnumber, error_code, C.ERROR_NAME[error_code], _hexlify(reply_data))
feature_index = ord(feature_index_function[:1])
feature_function = feature_index_function[1:2]
feature = None if features_array is None else features_array[feature_index]
@ -257,8 +282,9 @@ def request(handle, devnumber, feature_index_function, params=b'', features_arra
if reply_code == 0x11 and reply_data[:2] == feature_index_function:
# a matching reply
# _l.log(_LOG_LEVEL, "(%d,%d) matched reply with feature-index-function [%s]", handle, devnumber, _hexlify(reply_data[2:]))
# _l.log(_LOG_LEVEL, "(%d) matched reply with feature-index-function [%s]", devnumber, _hexlify(reply_data[2:]))
return reply_data[2:]
_l.log(_LOG_LEVEL, "(%d,%d) unmatched reply {%s} (expected {%s})", handle, devnumber, _hexlify(reply_data[:2]), _hexlify(feature_index_function))
_unhandled._publish(reply_code, reply_devnumber, reply_data)
_l.log(_LOG_LEVEL, "(%d) unmatched reply {%s} (expected {%s})", devnumber, _hexlify(reply_data[:2]), _hexlify(feature_index_function))
if unhandled_hook:
unhandled_hook(reply_code, reply_devnumber, reply_data)

View File

@ -22,6 +22,7 @@ from collections import namedtuple
"""Tuple returned by list_devices and find_device_by_name."""
AttachedDeviceInfo = namedtuple('AttachedDeviceInfo', [
'handle',
'number',
'type',
'name',

View File

@ -5,40 +5,52 @@
import logging
import threading
from time import sleep as _sleep
from binascii import hexlify as _hexlify
from . import base as _base
from . import exceptions as E
# from . import unhandled as _unhandled
# for both Python 2 and 3
try:
import Queue as queue
except ImportError:
import queue
_LOG_LEVEL = 5
_l = logging.getLogger('lur.listener')
_READ_EVENT_TIMEOUT = int(_base.DEFAULT_TIMEOUT / 5) # ms
_IDLE_SLEEP = _base.DEFAULT_TIMEOUT / 2 # ms
_READ_EVENT_TIMEOUT = int(_base.DEFAULT_TIMEOUT / 4) # ms
_IDLE_SLEEP = _base.DEFAULT_TIMEOUT / 4 # ms
def _callback_caller(listener, callback):
# _l.log(_LOG_LEVEL, "%s starting callback caller", listener)
while listener._active:
event = listener.events.get()
if _l.isEnabledFor(_LOG_LEVEL):
_l.log(_LOG_LEVEL, "%s delivering event %s", listener, event)
callback.__call__(*event)
# _l.log(_LOG_LEVEL, "%s stopped callback caller", listener)
class EventsListener(threading.Thread):
"""Listener thread for events from the Unifying Receiver.
Incoming events (reply_code, devnumber, data) will be passed to the callback
function. The callback is called in the listener thread, so for best results
it should return as fast as possible.
function. The callback is called in a separate thread.
While this listener is running, you should use the request() method to make
regular UR API calls, otherwise the replies may be captured by the listener
and delivered as events to the callback. As an exception, you can make API
calls in the events callback.
regular UR API calls, otherwise the replies are very likely to be captured
by the listener and delivered as events to the callback. As an exception,
you can make API calls in the events callback.
"""
def __init__(self, receiver, events_callback):
super(EventsListener, self).__init__(name='Unifying_Receiver_Listener_' + hex(receiver))
super(EventsListener, self).__init__(group='Unifying Receiver', name='Events-%x' % receiver)
self.daemon = True
self.active = False
self._active = False
self.receiver = receiver
self.callback = events_callback
self.task = None
self.task_processing = threading.Lock()
@ -46,40 +58,50 @@ class EventsListener(threading.Thread):
self.task_reply = None
self.task_done = threading.Event()
self.events = queue.Queue(32)
self.event_caller = threading.Thread(group='Unifying Receiver', name='Callback-%x' % receiver, target=_callback_caller, args=(self, events_callback))
self.event_caller.daemon = True
self.__str_cached = 'Events(%x)' % self.receiver
def run(self):
self.active = True
_l.log(_LOG_LEVEL, "(%d) starting", self.receiver)
self._active = True
_l.log(_LOG_LEVEL, "%s started", self)
# last_hook = _unhandled.hook
# _unhandled.hook = self.callback
self.__str_cached = 'Events(%x:active)' % self.receiver
self.event_caller.start()
while self.active:
last_hook = _base.unhandled_hook
_base.unhandled_hook = self._unhandled
while self._active:
try:
# _l.log(_LOG_LEVEL, "(%d) reading next event", self.receiver)
event = _base.read(self.receiver, _READ_EVENT_TIMEOUT)
except E.NoReceiver:
_l.warn("(%d) receiver disconnected", self.receiver)
self.active = False
break
_l.warn("%s receiver disconnected", self)
self._active = False
if self.active:
if self._active:
if event:
if _l.isEnabledFor(_LOG_LEVEL):
_l.log(_LOG_LEVEL, "(%d) got event (%02x %02x [%s])", self.receiver, event[0], event[1], _hexlify(event[2]))
self.callback.__call__(*event)
elif self.task is None:
# _l.log(_LOG_LEVEL, "(%d) idle sleep", self.receiver)
# _l.log(_LOG_LEVEL, "%s queueing event %s", self, event)
self.events.put(event)
if self.task is None:
# _l.log(_LOG_LEVEL, "%s idle sleep", self)
_sleep(_IDLE_SLEEP / 1000.0)
else:
self.task_reply = self._make_request(*self.task)
self.task_done.set()
# _unhandled.hook = last_hook
self.__str_cached = 'Events(%x)' % self.receiver
_base.unhandled_hook = last_hook
def stop(self):
"""Tells the listener to stop as soon as possible."""
_l.log(_LOG_LEVEL, "(%d) stopping", self.receiver)
self.active = False
_l.log(_LOG_LEVEL, "%s stopping", self)
self._active = False
def request(self, api_function, *args, **kwargs):
"""Make an UR API request.
@ -88,7 +110,8 @@ class EventsListener(threading.Thread):
other args and kwargs will follow.
"""
# if _l.isEnabledFor(_LOG_LEVEL):
# _l.log(_LOG_LEVEL, "(%d) request '%s.%s' with %s, %s", self.receiver, api_function.__module__, api_function.__name__, args, kwargs)
# _l.log(_LOG_LEVEL, "%s request '%s.%s' with %s, %s", self, api_function.__module__, api_function.__name__, args, kwargs)
self.task_processing.acquire()
self.task_done.clear()
self.task = (api_function, args, kwargs)
@ -99,18 +122,29 @@ class EventsListener(threading.Thread):
self.task_processing.release()
# if _l.isEnabledFor(_LOG_LEVEL):
# _l.log(_LOG_LEVEL, "(%d) request '%s.%s' => [%s]", self.receiver, api_function.__module__, api_function.__name__, _hexlify(reply))
# _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 _make_request(self, api_function, args, kwargs):
if _l.isEnabledFor(_LOG_LEVEL):
_l.log(_LOG_LEVEL, "(%d) calling '%s.%s' with %s, %s", self.receiver, api_function.__module__, api_function.__name__, args, kwargs)
_l.log(_LOG_LEVEL, "%s calling '%s.%s' with %s, %s", self, api_function.__module__, api_function.__name__, args, kwargs)
try:
return api_function.__call__(self.receiver, *args, **kwargs)
except E.NoReceiver as nr:
self.task_reply = nr
self.active = False
self._active = False
except Exception as e:
self.task_reply = e
def _unhandled(self, reply_code, devnumber, data):
event = (reply_code, devnumber, data)
_l.log(_LOG_LEVEL, "%s queueing unhandled event %s", self, event)
self.events.put(event)
def __str__(self):
return self.__str_cached
def __nonzero__(self):
return self._active

View File

@ -8,7 +8,6 @@ from binascii import hexlify
from .. import base
from ..exceptions import *
from ..constants import *
from .. import unhandled
class Test_UR_Base(unittest.TestCase):
@ -157,7 +156,7 @@ class Test_UR_Base(unittest.TestCase):
global received_unhandled
received_unhandled = (code, device, data)
unhandled.hook = _unhandled
base.unhandled_hook = _unhandled
base.write(self.handle, self.device, FEATURE.ROOT + FEATURE.FEATURE_SET)
reply = base.request(self.handle, self.device, fs_index + b'\x00')
self.assertIsNotNone(reply, "request returned None reply")
@ -165,7 +164,7 @@ class Test_UR_Base(unittest.TestCase):
self.assertIsNotNone(received_unhandled, "extra message not received by unhandled hook")
received_unhandled = None
unhandled.hook = None
base.unhandled_hook = None
base.write(self.handle, self.device, FEATURE.ROOT + FEATURE.FEATURE_SET)
reply = base.request(self.handle, self.device, fs_index + b'\x00')
self.assertIsNotNone(reply, "request returned None reply")

View File

@ -1,36 +0,0 @@
#
# Optional hook for unhandled data packets received while talking to the UR.
# These are usually broadcast events received from the attached devices.
#
import logging
from binascii import hexlify as _hexlify
def _logdebug_hook(reply_code, devnumber, data):
"""Default unhandled hook, logs the reply as DEBUG."""
_l = logging.getLogger('lur.unhandled')
_l.debug("UNHANDLED (,%d) code 0x%02x data [%s]", devnumber, reply_code, _hexlify(data))
"""The function that will be called on unhandled incoming events.
The hook must be a function with the signature: ``_(int, int, str)``, where
the parameters are: (reply_code, devnumber, data).
This hook will only be called by the request() function, when it receives
replies that do not match the requested feature call. As such, it is not
suitable for intercepting broadcast events from the device (e.g. special
keys being pressed, battery charge events, etc), at least not in a timely
manner. However, these events *may* be delivered here if they happen while
doing a feature call to the device.
The default implementation logs the unhandled reply as DEBUG.
"""
hook = _logdebug_hook
def _publish(reply_code, devnumber, data):
"""Delivers a reply to the unhandled hook, if any."""
if hook is not None:
hook.__call__(reply_code, devnumber, data)

View File

@ -1,6 +1,7 @@
#!/bin/sh
cd `dirname "$0"`
export LD_LIBRARY_PATH=$PWD
cd -P `dirname "$0"`
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$PWD/native/`uname -m`
exec python -m unittest discover -v "$@"

6
solaar
View File

@ -1,10 +1,12 @@
#!/bin/sh
cd `dirname "$0"`
cd -P `dirname "$0"`
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$PWD/lib
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$PWD/lib/native/`uname -m`
export PYTHONPATH=$PWD:$PWD/lib
export XDG_DATA_DIRS=$PWD/resources:$XDG_DATA_DIRS
cd -
exec python -OO solaar.py "$@"
# exec python -OO -m profile -o $TMPDIR/profile.log solaar.py "$@"

View File

@ -22,7 +22,8 @@ if __name__ == '__main__':
import logging
log_level = logging.root.level - 10 * args.verbose
logging.basicConfig(level=log_level if log_level > 0 else 1)
log_format='%(asctime)s %(levelname)8s [%(threadName)s] %(name)s: %(message)s'
logging.basicConfig(level=log_level if log_level > 0 else 1, format=log_format)
import app
app.run(args)

View File

@ -1,8 +1,8 @@
#!/bin/sh
cd `dirname "$0"`/..
cd -P `dirname "$0"`/..
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$PWD/lib
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$PWD/lib/native/`uname -m`
export PYTHONPATH=$PWD/lib
cd -

View File

@ -1,8 +1,10 @@
#!/bin/sh
cd `dirname "$0"`/../lib
cd -P `dirname "$0"`/..
export LD_LIBRARY_PATH=$PWD
export PYTHONPATH=$PWD
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$PWD/lib/native/`uname -m`
export PYTHONPATH=$PWD/lib
cd -
exec python -OO -m cli.ur_scanner "$@"