re-worked how fd handles are used in multi-threading

This commit is contained in:
Daniel Pavel 2012-11-11 17:03:13 +02:00
parent d0ccd3e9c2
commit 50fedab19e
22 changed files with 595 additions and 507 deletions

View File

@ -64,6 +64,8 @@ class State(object):
if event.data == b'\x4A\x00\x01\x00\x00': if event.data == b'\x4A\x00\x01\x00\x00':
_l.debug("receiver gave up") _l.debug("receiver gave up")
self.success = False self.success = False
# self.success = True
# self.detected_device = self.listener.receiver.devices[1]
return True return True
return False return False
@ -78,5 +80,4 @@ class State(object):
return True return True
def unpair(self, device): def unpair(self, device):
_l.debug("unpair %s", device) return self.listener.unpair_device(device)
self.listener.unpair_device(device)

View File

@ -4,7 +4,6 @@
from logging import getLogger as _Logger from logging import getLogger as _Logger
from struct import pack as _pack from struct import pack as _pack
from time import sleep as _sleep
from logitech.unifying_receiver import base as _base from logitech.unifying_receiver import base as _base
from logitech.unifying_receiver import api as _api from logitech.unifying_receiver import api as _api
@ -24,14 +23,22 @@ class _FeaturesArray(object):
self.device = device self.device = device
self.features = None self.features = None
self.supported = True self.supported = True
self._check()
def __del__(self):
self.supported = False
self.device = None
def _check(self): def _check(self):
if self.supported: if self.supported:
if self.features is not None: if self.features is not None:
return True return True
if self.device.protocol < 2.0:
return False
if self.device.status >= STATUS.CONNECTED: if self.device.status >= STATUS.CONNECTED:
handle = self.device.handle handle = int(self.device.handle)
try: try:
index = _api.get_feature_index(handle, self.device.number, _api.FEATURE.FEATURE_SET) index = _api.get_feature_index(handle, self.device.number, _api.FEATURE.FEATURE_SET)
except _api._FeatureNotSupported: except _api._FeatureNotSupported:
@ -57,9 +64,13 @@ class _FeaturesArray(object):
if index < 0 or index >= len(self.features): if index < 0 or index >= len(self.features):
raise IndexError raise IndexError
if self.features[index] is None: if self.features[index] is None:
# print "features getitem at %d" % index
fs_index = self.features.index(_api.FEATURE.FEATURE_SET) fs_index = self.features.index(_api.FEATURE.FEATURE_SET)
feature = _base.request(self.device.handle, self.device.number, _pack('!BB', fs_index, 0x10), _pack('!B', index)) # technically fs_function is 0x10 for this call, but we add the index to differentiate possibly conflicting requests
fs_function = 0x10 | (index & 0x0F)
feature = _base.request(self.device.handle, self.device.number, _pack('!BB', fs_index, fs_function), _pack('!B', index))
if feature is not None: if feature is not None:
self.features[index] = feature[:2] self.features[index] = feature[:2]
@ -70,11 +81,13 @@ class _FeaturesArray(object):
if value in self.features: if value in self.features:
return True return True
# print "features contains %s" % repr(value)
for index in range(0, len(self.features)): for index in range(0, len(self.features)):
f = self.features[index] or self.__getitem__(index) f = self.features[index] or self.__getitem__(index)
assert f is not None assert f is not None
if f == value: if f == value:
return True return True
# we know the features are ordered by value
if f > value: if f > value:
break break
@ -105,23 +118,19 @@ class _FeaturesArray(object):
class DeviceInfo(_api.PairedDevice): class DeviceInfo(_api.PairedDevice):
"""A device attached to the receiver. """A device attached to the receiver.
""" """
def __init__(self, listener, number, status=STATUS.UNKNOWN): def __init__(self, handle, number, status=STATUS.UNKNOWN, status_changed_callback=None):
super(DeviceInfo, self).__init__(listener.handle, number) super(DeviceInfo, self).__init__(handle, number)
self._features = _FeaturesArray(self) self.LOG = _Logger("Device[%d]" % (number))
self.LOG = _Logger("Device[%d]" % number)
self._listener = listener
self.status_changed_callback = status_changed_callback
self._status = status self._status = status
self.props = {} self.props = {}
# read them now, otherwise it it temporarily hang the UI self._features = _FeaturesArray(self)
# if status >= STATUS.CONNECTED:
# n, k, s, f = self.name, self.kind, self.serial, self.firmware
@property def __del__(self):
def receiver(self): super(ReceiverListener, self).__del__()
return self._listener.receiver self._features.supported = False
@property @property
def status(self): def status(self):
@ -129,14 +138,18 @@ class DeviceInfo(_api.PairedDevice):
@status.setter @status.setter
def status(self, new_status): def status(self, new_status):
if new_status != self._status and not (new_status == STATUS.CONNECTED and self._status > new_status):
self.LOG.debug("status %d => %d", self._status, new_status)
urgent = new_status < STATUS.CONNECTED or self._status < STATUS.CONNECTED
self._status = new_status
self._listener.status_changed(self, urgent)
if new_status < STATUS.CONNECTED: if new_status < STATUS.CONNECTED:
self.props.clear() self.props.clear()
else:
self._features._check()
self.serial, self.codename, self.name, self.kind
if new_status != self._status and not (new_status == STATUS.CONNECTED and self._status > new_status):
self.LOG.debug("status %d => %d", self._status, new_status)
self._status = new_status
if self.status_changed_callback:
ui_flags = STATUS.UI_NOTIFY if new_status == STATUS.UNPAIRED else 0
self.status_changed_callback(self, ui_flags)
@property @property
def status_text(self): def status_text(self):
@ -165,11 +178,12 @@ class DeviceInfo(_api.PairedDevice):
return True return True
if type(status) == tuple: if type(status) == tuple:
ui_flags = status[1].pop(PROPS.UI_FLAGS, 0)
p = dict(self.props) p = dict(self.props)
self.props.update(status[1]) self.props.update(status[1])
if self.status == status[0]: if self.status == status[0]:
if p != self.props: if self.status_changed_callback and (ui_flags or p != self.props):
self._listener.status_changed(self) self.status_changed_callback(self, ui_flags)
else: else:
self.status = status[0] self.status = status[0]
return True return True
@ -179,7 +193,7 @@ class DeviceInfo(_api.PairedDevice):
return False return False
def __str__(self): def __str__(self):
return '<DeviceInfo(%d,%s,%d)>' % (self.number, self._name or '?', self._status) return '<DeviceInfo(%s,%d,%s,%d)>' % (self.handle, self.number, self.codename or '?', self._status)
# #
# #
@ -201,16 +215,13 @@ _RECEIVER_STATUS_NAME = _FallbackDict(
class ReceiverListener(_EventsListener): class ReceiverListener(_EventsListener):
"""Keeps the status of a Unifying Receiver. """Keeps the status of a Unifying Receiver.
""" """
def __init__(self, receiver, status_changed_callback=None): def __init__(self, receiver, status_changed_callback=None):
super(ReceiverListener, self).__init__(receiver.handle, self._events_handler) super(ReceiverListener, self).__init__(receiver.handle, self._events_handler)
self.LOG = _Logger("Receiver[%s]" % receiver.path)
self.receiver = receiver self.receiver = receiver
self.LOG = _Logger("ReceiverListener(%s)" % receiver.path)
self.events_filter = None self.events_filter = None
self.events_handler = None self.events_handler = None
self.status_changed_callback = status_changed_callback self.status_changed_callback = status_changed_callback
receiver.kind = receiver.name receiver.kind = receiver.name
@ -223,23 +234,28 @@ class ReceiverListener(_EventsListener):
else: else:
self.LOG.warn("initialization failed") self.LOG.warn("initialization failed")
if _base.request(receiver.handle, 0xFF, b'\x80\x02', b'\x02'): self.LOG.info("reports %d device(s) paired", len(receiver))
self.LOG.info("triggered device events")
else:
self.LOG.warn("failed to trigger device events")
self.LOG.info("receiver reports %d device(s) paired", len(receiver)) def __del__(self):
super(ReceiverListener, self).__del__()
self.receiver = None
def trigger_device_events(self):
if _base.request(int(self._handle), 0xFF, b'\x80\x02', b'\x02'):
self.LOG.info("triggered device events")
return True
self.LOG.warn("failed to trigger device events")
def change_status(self, new_status): def change_status(self, new_status):
if new_status != self.receiver.status: if new_status != self.receiver.status:
self.LOG.debug("status %d => %d", self.receiver.status, new_status) self.LOG.debug("status %d => %d", self.receiver.status, new_status)
self.receiver.status = new_status self.receiver.status = new_status
self.receiver.status_text = _RECEIVER_STATUS_NAME[new_status] self.receiver.status_text = _RECEIVER_STATUS_NAME[new_status]
self.status_changed(None, True) self.status_changed(None, STATUS.UI_NOTIFY)
def status_changed(self, device=None, urgent=False): def status_changed(self, device=None, ui_flags=0):
if self.status_changed_callback: if self.status_changed_callback:
self.status_changed_callback(self.receiver, device, urgent) self.status_changed_callback(self.receiver, device, ui_flags)
def _device_status_from(self, event): def _device_status_from(self, event):
state_code = ord(event.data[2:3]) & 0xC0 state_code = ord(event.data[2:3]) & 0xC0
@ -248,7 +264,7 @@ class ReceiverListener(_EventsListener):
STATUS.CONNECTED if state_code == 0x00 else \ STATUS.CONNECTED if state_code == 0x00 else \
None None
if state is None: if state is None:
self.LOG.warn("don't know how to handle state code 0x%02X: %s", state_code, event) self.LOG.warn("failed to identify status of device %d from 0x%02X: %s", event.devnumber, state_code, event)
return state return state
def _events_handler(self, event): def _events_handler(self, event):
@ -256,26 +272,20 @@ class ReceiverListener(_EventsListener):
return return
if event.code == 0x10 and event.data[0:2] == b'\x41\x04': if event.code == 0x10 and event.data[0:2] == b'\x41\x04':
if event.devnumber in self.receiver.devices: if event.devnumber in self.receiver.devices:
status = self._device_status_from(event) status = self._device_status_from(event)
if status is not None: if status is not None:
self.receiver.devices[event.devnumber].status = status self.receiver.devices[event.devnumber].status = status
else: else:
dev = self.make_device(event) self.make_device(event)
if dev is None:
self.LOG.warn("failed to make new device from %s", event)
else:
self.receiver.devices[event.devnumber] = dev
self.change_status(STATUS.CONNECTED + len(self.receiver.devices))
return return
if event.devnumber == 0xFF: if event.devnumber == 0xFF:
if event.code == 0xFF and event.data is None: if event.code == 0xFF and event.data is None:
# receiver disconnected
self.LOG.warn("disconnected") self.LOG.warn("disconnected")
self.receiver.devices = {} self.receiver.devices = {}
self.change_status(STATUS.UNAVAILABLE) self.change_status(STATUS.UNAVAILABLE)
self.receiver = None
return return
elif event.devnumber in self.receiver.devices: elif event.devnumber in self.receiver.devices:
dev = self.receiver.devices[event.devnumber] dev = self.receiver.devices[event.devnumber]
@ -285,7 +295,7 @@ class ReceiverListener(_EventsListener):
if self.events_handler and self.events_handler(event): if self.events_handler and self.events_handler(event):
return return
self.LOG.warn("don't know how to handle event %s", event) # self.LOG.warn("don't know how to handle event %s", event)
def make_device(self, event): def make_device(self, event):
if event.devnumber < 1 or event.devnumber > self.receiver.max_devices: if event.devnumber < 1 or event.devnumber > self.receiver.max_devices:
@ -294,12 +304,18 @@ class ReceiverListener(_EventsListener):
status = self._device_status_from(event) status = self._device_status_from(event)
if status is not None: if status is not None:
dev = DeviceInfo(self, event.devnumber, status) dev = DeviceInfo(self.handle, event.devnumber, status, self.status_changed)
self.LOG.info("new device %s", dev) self.LOG.info("new device %s", dev)
self.status_changed(dev, True)
return dev
self.LOG.error("failed to identify status of device %d from %s", event.devnumber, event) self.receiver.devices[event.devnumber] = dev
self.change_status(STATUS.CONNECTED + len(self.receiver.devices))
if status == STATUS.CONNECTED:
dev.protocol, dev.name, dev.kind
self.status_changed(dev, STATUS.UI_NOTIFY)
if status == STATUS.CONNECTED:
dev.serial, dev.firmware
return dev
def unpair_device(self, device): def unpair_device(self, device):
try: try:
@ -315,17 +331,16 @@ class ReceiverListener(_EventsListener):
return True return True
def __str__(self): def __str__(self):
return '<ReceiverListener(%s,%d)>' % (self.receiver.path, self.receiver.status) return '<ReceiverListener(%s,%d,%d)>' % (self.receiver.path, int(self.handle), self.receiver.status)
@classmethod @classmethod
def open(self, status_changed_callback=None): def open(self, status_changed_callback=None):
receiver = _api.Receiver.open() receiver = _api.Receiver.open()
if receiver: if receiver:
handle = receiver.handle
receiver.handle = _api.ThreadedHandle(handle, receiver.path)
rl = ReceiverListener(receiver, status_changed_callback) rl = ReceiverListener(receiver, status_changed_callback)
rl.start() rl.start()
while not rl._active:
_sleep(0.1)
return rl return rl
# #

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python #!/usr/bin/env python
NAME = 'Solaar' NAME = 'Solaar'
VERSION = '0.7.2' VERSION = '0.7.3'
__author__ = "Daniel Pavel <daniel.pavel@gmail.com>" __author__ = "Daniel Pavel <daniel.pavel@gmail.com>"
__version__ = VERSION __version__ = VERSION
__license__ = "GPL" __license__ = "GPL"
@ -13,6 +13,9 @@ __license__ = "GPL"
def _parse_arguments(): def _parse_arguments():
import argparse import argparse
arg_parser = argparse.ArgumentParser(prog=NAME.lower()) arg_parser = argparse.ArgumentParser(prog=NAME.lower())
arg_parser.add_argument('-q', '--quiet',
action='store_true',
help='disable all logging, takes precedence over --verbose')
arg_parser.add_argument('-v', '--verbose', arg_parser.add_argument('-v', '--verbose',
action='count', default=0, action='count', default=0,
help='increase the logger verbosity (may be repeated)') help='increase the logger verbosity (may be repeated)')
@ -30,9 +33,13 @@ def _parse_arguments():
args = arg_parser.parse_args() args = arg_parser.parse_args()
import logging import logging
log_level = logging.WARNING - 10 * args.verbose if args.quiet:
log_format='%(asctime)s %(levelname)8s [%(threadName)s] %(name)s: %(message)s' logging.root.addHandler(logging.NullHandler())
logging.basicConfig(level=max(log_level, logging.DEBUG), format=log_format) logging.root.setLevel(logging.CRITICAL)
else:
log_level = logging.WARNING - 10 * args.verbose
log_format='%(asctime)s %(levelname)8s [%(threadName)s] %(name)s: %(message)s'
logging.basicConfig(level=max(log_level, logging.DEBUG), format=log_format)
return args return args
@ -81,14 +88,18 @@ if __name__ == '__main__':
window.present() window.present()
import pairing import pairing
from logitech.devices.constants import STATUS
from gi.repository import Gtk, GObject from gi.repository import Gtk, GObject
listener = None listener = None
notify_missing = True notify_missing = True
def status_changed(receiver, device=None, urgent=False): def status_changed(receiver, device=None, ui_flags=0):
ui.update(receiver, icon, window, device) ui.update(receiver, icon, window, device)
if ui.notify.available and urgent: if ui_flags & STATUS.UI_POPUP:
window.present()
if ui_flags & STATUS.UI_NOTIFY and ui.notify.available:
GObject.idle_add(ui.notify.show, device or receiver) GObject.idle_add(ui.notify.show, device or receiver)
global listener global listener
@ -98,27 +109,34 @@ if __name__ == '__main__':
from receiver import ReceiverListener from receiver import ReceiverListener
def check_for_listener(retry=True): def check_for_listener(retry=True):
global listener, notify_missing def _check_still_scanning(listener):
if listener.receiver.status == STATUS.BOOTING:
listener.change_status(STATUS.CONNECTED)
global listener, notify_missing
if listener is None: if listener is None:
try: try:
listener = ReceiverListener.open(status_changed) listener = ReceiverListener.open(status_changed)
except OSError: except OSError:
ui.show_permissions_warning(window) ui.error(window, 'Permissions error',
'Found a possible Unifying Receiver device,\n'
'but did not have permission to open it.')
if listener is None: if listener is None:
pairing.state = None pairing.state = None
if notify_missing: if notify_missing:
status_changed(DUMMY, None, True) status_changed(DUMMY, None, STATUS.UI_NOTIFY)
notify_missing = False notify_missing = False
return retry return retry
# print ("opened receiver", listener, listener.receiver) # print ("opened receiver", listener, listener.receiver)
notify_missing = True notify_missing = True
pairing.state = pairing.State(listener) pairing.state = pairing.State(listener)
status_changed(listener.receiver, None, True) status_changed(listener.receiver, None, STATUS.UI_NOTIFY)
listener.trigger_device_events()
GObject.timeout_add(5 * 1000, _check_still_scanning, listener)
GObject.timeout_add(100, check_for_listener, False) GObject.timeout_add(50, check_for_listener, False)
Gtk.main() Gtk.main()
if listener is not None: if listener is not None:

View File

@ -6,8 +6,8 @@ from gi.repository import (GObject, Gtk)
GObject.threads_init() GObject.threads_init()
from solaar import NAME as _NAME from solaar import NAME
_APP_ICONS = (_NAME + '-fail', _NAME + '-init', _NAME) _APP_ICONS = (NAME + '-fail', NAME + '-init', NAME)
def appicon(receiver_status): def appicon(receiver_status):
return (_APP_ICONS[0] if receiver_status < 0 else return (_APP_ICONS[0] if receiver_status < 0 else
_APP_ICONS[1] if receiver_status < 1 else _APP_ICONS[1] if receiver_status < 1 else
@ -25,12 +25,9 @@ def icon_file(name):
return None return None
def show_permissions_warning(window): def error(window, title, text):
text = ('Found a possible Unifying Receiver device,\n'
'but did not have permission to open it.')
m = Gtk.MessageDialog(window, Gtk.DialogFlags.MODAL, Gtk.MessageType.ERROR, Gtk.ButtonsType.CLOSE, text) m = Gtk.MessageDialog(window, Gtk.DialogFlags.MODAL, Gtk.MessageType.ERROR, Gtk.ButtonsType.CLOSE, text)
m.set_title('Permissions error') m.set_title(title)
m.run() m.run()
m.destroy() m.destroy()

View File

@ -3,10 +3,9 @@
# #
# from sys import version as PYTTHON_VERSION # from sys import version as PYTTHON_VERSION
from gi.repository import Gtk from gi.repository import (Gtk, Gdk)
import ui.notify import ui
import ui.pair_window
from solaar import NAME as _NAME from solaar import NAME as _NAME
from solaar import VERSION as _VERSION from solaar import VERSION as _VERSION
@ -45,7 +44,8 @@ def _show_about_window(action):
about.set_logo_icon_name(_NAME) about.set_logo_icon_name(_NAME)
about.set_version(_VERSION) about.set_version(_VERSION)
about.set_license_type(Gtk.License.GPL_2_0) about.set_license_type(Gtk.License.GPL_2_0)
about.set_authors(('Daniel Pavel http://github.com/pwr', )) about.set_authors(('Daniel Pavel http://github.com/pwr',))
# about.add_credit_section('Testing', 'Douglas Wagner')
about.set_website('http://github.com/pwr/Solaar/wiki') about.set_website('http://github.com/pwr/Solaar/wiki')
about.set_website_label('Solaar Wiki') about.set_website_label('Solaar Wiki')
# about.set_comments('Using Python %s\n' % PYTTHON_VERSION.split(' ')[0]) # about.set_comments('Using Python %s\n' % PYTTHON_VERSION.split(' ')[0])
@ -64,11 +64,12 @@ import pairing
def _pair_device(action, frame): def _pair_device(action, frame):
window = frame.get_toplevel() window = frame.get_toplevel()
pair_dialog = ui.pair_window.create( action, pairing.state) pair_dialog = ui.pair_window.create(action, pairing.state)
# window.present()
pair_dialog.set_transient_for(window) pair_dialog.set_transient_for(window)
pair_dialog.set_modal(True) pair_dialog.set_modal(True)
pair_dialog.set_type_hint(Gdk.WindowTypeHint.DIALOG)
window.present()
pair_dialog.present() pair_dialog.present()
def pair(frame): def pair(frame):
@ -77,15 +78,19 @@ def pair(frame):
def _unpair_device(action, frame): def _unpair_device(action, frame):
window = frame.get_toplevel() window = frame.get_toplevel()
window.present() # window.present()
device = frame._device device = frame._device
qdialog = Gtk.MessageDialog(window, 0, qdialog = Gtk.MessageDialog(window, 0,
Gtk.MessageType.QUESTION, Gtk.ButtonsType.YES_NO, Gtk.MessageType.QUESTION, Gtk.ButtonsType.NONE,
"Unpair device\n%s ?" % device.name) "Unpair device\n%s ?" % device.name)
qdialog.set_icon_name('remove')
qdialog.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)
qdialog.add_button('Unpair', Gtk.ResponseType.ACCEPT)
choice = qdialog.run() choice = qdialog.run()
qdialog.destroy() qdialog.destroy()
if choice == Gtk.ResponseType.YES: if choice == Gtk.ResponseType.ACCEPT:
pairing.state.unpair(device) if not pairing.state.unpair(device):
ui.error(window, 'Unpairing failed', 'Failed to unpair device\n%s .' % device.name)
def unpair(frame): def unpair(frame):
return _action('remove', 'Unpair', _unpair_device, frame) return _action('remove', 'Unpair', _unpair_device, frame)

View File

@ -2,7 +2,7 @@
# #
# #
from gi.repository import (Gtk, Gdk) from gi.repository import (Gtk, Gdk, GObject)
import ui import ui
from logitech.devices.constants import (STATUS, PROPS) from logitech.devices.constants import (STATUS, PROPS)
@ -17,22 +17,6 @@ _PLACEHOLDER = '~'
# #
# #
def _info_text(dev):
items = [('Serial', dev.serial)] + [(f.kind, ((f.name + ' ') if f.name else '') + f.version) for f in dev.firmware]
if hasattr(dev, 'number'):
items += [('HID', dev.protocol)]
return '<small><tt>%s</tt></small>' % '\n'.join('%-10s: %s' % (item[0], str(item[1])) for item in items)
def _toggle_info(action, label_widget, box_widget, frame):
if action.get_active():
box_widget.set_visible(True)
if not label_widget.get_text():
label_widget.set_markup(_info_text(frame._device))
else:
box_widget.set_visible(False)
def _make_receiver_box(name): def _make_receiver_box(name):
frame = Gtk.Frame() frame = Gtk.Frame()
frame._device = None frame._device = None
@ -66,11 +50,12 @@ def _make_receiver_box(name):
info_box.add(info_label) info_box.add(info_label)
info_box.set_shadow_type(Gtk.ShadowType.ETCHED_IN) info_box.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
toggle_info_action = ui.action._toggle_action('info', 'Receiver info', _toggle_info, info_label, info_box, frame) toggle_info_action = ui.action._toggle_action('info', 'Receiver info', _toggle_info_box, info_label, info_box, frame, _update_receiver_info_label)
toolbar.insert(toggle_info_action.create_tool_item(), 0) toolbar.insert(toggle_info_action.create_tool_item(), 0)
toolbar.insert(ui.action.pair(frame).create_tool_item(), -1) toolbar.insert(ui.action.pair(frame).create_tool_item(), -1)
# toolbar.insert(ui.action.about.create_tool_item(), -1)
vbox = Gtk.VBox(homogeneous=False, spacing=2) vbox = Gtk.VBox(homogeneous=False, spacing=4)
vbox.set_border_width(4) vbox.set_border_width(4)
vbox.pack_start(hbox, True, True, 0) vbox.pack_start(hbox, True, True, 0)
vbox.pack_start(info_box, True, True, 0) vbox.pack_start(info_box, True, True, 0)
@ -127,11 +112,12 @@ def _make_device_box(index):
info_label.set_alignment(0, 0.5) info_label.set_alignment(0, 0.5)
info_label.set_padding(8, 2) info_label.set_padding(8, 2)
info_label.set_selectable(True) info_label.set_selectable(True)
info_label.fields = {}
info_box = Gtk.Frame() info_box = Gtk.Frame()
info_box.add(info_label) info_box.add(info_label)
toggle_info_action = ui.action._toggle_action('info', 'Device info', _toggle_info, info_label, info_box, frame) toggle_info_action = ui.action._toggle_action('info', 'Device info', _toggle_info_box, info_label, info_box, frame, _update_device_info_label)
toolbar.insert(toggle_info_action.create_tool_item(), 0) toolbar.insert(toggle_info_action.create_tool_item(), 0)
toolbar.insert(ui.action.unpair(frame).create_tool_item(), -1) toolbar.insert(ui.action.unpair(frame).create_tool_item(), -1)
@ -204,6 +190,57 @@ def create(title, name, max_devices, systray=False):
# #
# #
def _update_device_info_label(label, dev):
need_update = False
if 'serial' in label.fields:
serial = label.fields['serial']
else:
serial = label.fields['serial'] = dev.serial
need_update = True
if 'firmware' in label.fields:
firmware = label.fields['firmware']
else:
if dev.status >= STATUS.CONNECTED:
firmware = label.fields['firmware'] = dev.firmware
need_update = True
else:
firmware = None
if 'hid' in label.fields:
hid = label.fields['hid']
else:
if dev.status >= STATUS.CONNECTED:
hid = label.fields['hid'] = dev.protocol
need_update = True
else:
hid = None
if need_update:
items = [('Serial', serial)]
if firmware:
items += [(f.kind, f.name + ' ' + f.version) for f in firmware]
if hid:
items += [('HID', hid)]
label.set_markup('<small><tt>%s</tt></small>' % '\n'.join('%-10s: %s' % (item[0], str(item[1])) for item in items))
def _update_receiver_info_label(label, dev):
if label.get_visible() and label.get_text() == '':
items = [('Serial', dev.serial)] + \
[(f.kind, f.version) for f in dev.firmware]
label.set_markup('<small><tt>%s</tt></small>' % '\n'.join('%-10s: %s' % (item[0], str(item[1])) for item in items))
def _toggle_info_box(action, label_widget, box_widget, frame, update_function):
if action.get_active():
box_widget.set_visible(True)
update_function(label_widget, frame._device)
else:
box_widget.set_visible(False)
def _update_receiver_box(frame, receiver): def _update_receiver_box(frame, receiver):
label, toolbar, info_label = ui.find_children(frame, 'label', 'toolbar', 'info-label') label, toolbar, info_label = ui.find_children(frame, 'label', 'toolbar', 'info-label')
@ -220,44 +257,43 @@ def _update_receiver_box(frame, receiver):
def _update_device_box(frame, dev): def _update_device_box(frame, dev):
frame._device = dev frame._device = dev
# print dev.name, dev.kind
icon, label = ui.find_children(frame, 'icon', 'label') icon, label, info_label = ui.find_children(frame, 'icon', 'label', 'info-label')
if frame.get_name() != dev.name: first_run = frame.get_name() != dev.name
if first_run:
frame.set_name(dev.name) frame.set_name(dev.name)
icon_name = ui.get_icon(dev.name, dev.kind) icon_name = ui.get_icon(dev.name, dev.kind)
icon.set_from_icon_name(icon_name, _DEVICE_ICON_SIZE) icon.set_from_icon_name(icon_name, _DEVICE_ICON_SIZE)
label.set_markup('<b>' + dev.name + '</b>') label.set_markup('<b>' + dev.name + '</b>')
info_label = ui.find_children(frame, 'info-label')
info_label.set_text('')
status = ui.find_children(frame, 'status') status = ui.find_children(frame, 'status')
status_icons = status.get_children() status_icons = status.get_children()
toolbar = status_icons[-1]
if dev.status < STATUS.CONNECTED:
icon.set_sensitive(False)
label.set_sensitive(False)
status.set_sensitive(False)
for c in status_icons[1:-1]:
c.set_visible(False)
toolbar.get_children()[0].set_active(False)
else:
icon.set_sensitive(True)
label.set_sensitive(True)
status.set_sensitive(True)
if dev.status < STATUS.CONNECTED:
battery_icon, battery_label = status_icons[0:2]
battery_icon.set_sensitive(False)
battery_label.set_markup('<small>%s</small>' % dev.status_text)
battery_label.set_sensitive(True)
for c in status_icons[2:-1]:
c.set_visible(False)
else:
battery_icon, battery_label = status_icons[0:2] battery_icon, battery_label = status_icons[0:2]
battery_level = dev.props.get(PROPS.BATTERY_LEVEL) battery_level = dev.props.get(PROPS.BATTERY_LEVEL)
if battery_level is None: if battery_level is None:
battery_icon.set_from_icon_name('battery_unknown', _STATUS_ICON_SIZE)
battery_icon.set_sensitive(False) battery_icon.set_sensitive(False)
battery_label.set_visible(False) battery_icon.set_from_icon_name('battery_unknown', _STATUS_ICON_SIZE)
text = 'no status' if dev.protocol < 2.0 else 'waiting for status...'
battery_label.set_markup('<small>%s</small>' % text)
battery_label.set_sensitive(False)
else: else:
icon_name = 'battery_%03d' % (20 * ((battery_level + 10) // 20)) icon_name = 'battery_%03d' % (20 * ((battery_level + 10) // 20))
battery_icon.set_from_icon_name(icon_name, _STATUS_ICON_SIZE) battery_icon.set_from_icon_name(icon_name, _STATUS_ICON_SIZE)
battery_icon.set_sensitive(True) battery_icon.set_sensitive(True)
battery_label.set_text('%d%%' % battery_level) battery_label.set_text('%d%%' % battery_level)
battery_label.set_visible(True) battery_label.set_sensitive(True)
battery_status = dev.props.get(PROPS.BATTERY_STATUS) battery_status = dev.props.get(PROPS.BATTERY_STATUS)
battery_icon.set_tooltip_text(battery_status or '') battery_icon.set_tooltip_text(battery_status or '')
@ -274,10 +310,9 @@ def _update_device_box(frame, dev):
light_label.set_text('%d lux' % light_level) light_label.set_text('%d lux' % light_level)
light_label.set_visible(True) light_label.set_visible(True)
for b in toolbar.get_children()[:-1]: if first_run:
b.set_sensitive(True) frame.set_visible(True)
GObject.timeout_add(2000, _update_device_info_label, info_label, dev)
frame.set_visible(True)
def update(window, receiver, device=None): def update(window, receiver, device=None):

View File

@ -8,14 +8,21 @@ from gi.repository import (Gtk, GObject)
import ui import ui
def _create_page(assistant, text, kind): def _create_page(assistant, text, kind, icon_name=None):
p = Gtk.VBox(False, 12) p = Gtk.VBox(False, 12)
p.set_border_width(8) p.set_border_width(8)
if text: if text:
item = Gtk.HBox(homogeneous=False, spacing=16)
p.pack_start(item, False, True, 0)
label = Gtk.Label(text) label = Gtk.Label(text)
label.set_alignment(0, 0) label.set_alignment(0, 0)
p.pack_start(label, False, True, 0) item.pack_start(label, True, True, 0)
if icon_name:
icon = Gtk.Image.new_from_icon_name(icon_name, Gtk.IconSize.DIALOG)
item.pack_start(icon, False, False, 0)
assistant.append_page(p) assistant.append_page(p)
assistant.set_page_type(p, kind) assistant.set_page_type(p, kind)
@ -59,8 +66,9 @@ def _scan_complete_ui(assistant, device):
page = _create_page(assistant, page = _create_page(assistant,
'No new device detected.\n' 'No new device detected.\n'
'\n' '\n'
'Make sure your device is within range of the receiver,\nand it has a decent battery charge.\n', 'Make sure your device is within the\nreceiver\'s range, and it has\na decent battery charge.\n',
Gtk.AssistantPageType.CONFIRM) Gtk.AssistantPageType.CONFIRM,
'dialog-error')
else: else:
page = _create_page(assistant, page = _create_page(assistant,
None, None,
@ -110,7 +118,8 @@ def create(action, state):
'Turn on the device you want to pair.\n' 'Turn on the device you want to pair.\n'
'\n' '\n'
'If the device is already turned on,\nturn if off and on again.', 'If the device is already turned on,\nturn if off and on again.',
Gtk.AssistantPageType.INTRO) Gtk.AssistantPageType.INTRO,
'preferences-desktop-peripherals')
spinner = Gtk.Spinner() spinner = Gtk.Spinner()
spinner.set_visible(True) spinner.set_visible(True)
page_intro.pack_end(spinner, True, True, 16) page_intro.pack_end(spinner, True, True, 16)

View File

@ -50,6 +50,6 @@ def update(icon, receiver):
lines.append('') lines.append('')
text = '\n'.join(lines).rstrip('\n') text = '\n'.join(lines).rstrip('\n')
icon.set_tooltip_markup(text) icon.set_tooltip_markup(ui.NAME + ':\n' + text)
else: else:
icon.set_tooltip_text(receiver.status_text) icon.set_tooltip_text(ui.NAME + ': ' + receiver.status_text)

View File

@ -4,7 +4,4 @@ __author__ = "Daniel Pavel"
__license__ = "GPL" __license__ = "GPL"
__version__ = "0.4" __version__ = "0.4"
try: from hidapi.udev import *
from hidapi.udev import *
except ImportError:
from hidapi.native import *

View File

@ -55,7 +55,8 @@ if __name__ == '__main__':
print (".. Opening device %s" % args.device) print (".. Opening device %s" % args.device)
handle = hidapi.open_path(args.device.encode('utf-8')) handle = hidapi.open_path(args.device.encode('utf-8'))
if handle: if handle:
print (".. Opened handle %X, vendor %s product %s serial %s" % (handle, print (".. Opened handle %s, vendor %s product %s serial %s" % (
repr(handle),
repr(hidapi.get_manufacturer(handle)), repr(hidapi.get_manufacturer(handle)),
repr(hidapi.get_product(handle)), repr(hidapi.get_product(handle)),
repr(hidapi.get_serial(handle)))) repr(hidapi.get_serial(handle))))
@ -101,7 +102,7 @@ if __name__ == '__main__':
except Exception as e: except Exception as e:
print ('%s: %s' % (type(e).__name__, e)) print ('%s: %s' % (type(e).__name__, e))
print (".. Closing handle %X" % handle) print (".. Closing handle %s" % repr(handle))
hidapi.close(handle) hidapi.close(handle)
if interactive: if interactive:
readline.write_history_file(args.history) readline.write_history_file(args.history)

View File

@ -16,6 +16,10 @@ Currently the native libusb implementation (temporarily) detaches the device's
USB driver from the kernel, and it may cause the device to become unresponsive. USB driver from the kernel, and it may cause the device to become unresponsive.
""" """
#
# LEGACY, no longer supported
#
__version__ = '0.3-hidapi-0.7.0' __version__ = '0.3-hidapi-0.7.0'

View File

@ -8,9 +8,10 @@ necessary.
""" """
import os as _os import os as _os
import errno as _errno
from select import select as _select from select import select as _select
from pyudev import Context as _Context from pyudev import (Context as _Context,
from pyudev import Device as _Device Device as _Device)
native_implementation = 'udev' native_implementation = 'udev'
@ -124,6 +125,7 @@ def open_path(device_path):
:returns: an opaque device handle, or ``None``. :returns: an opaque device handle, or ``None``.
""" """
assert '/dev/hidraw' in device_path
return _os.open(device_path, _os.O_RDWR | _os.O_SYNC) return _os.open(device_path, _os.O_RDWR | _os.O_SYNC)
@ -155,14 +157,11 @@ def write(device_handle, data):
write() will send the data on the first OUT endpoint, if write() will send the data on the first OUT endpoint, if
one exists. If it does not, it will send the data through one exists. If it does not, it will send the data through
the Control Endpoint (Endpoint 0). the Control Endpoint (Endpoint 0).
:returns: ``True`` if the write was successful.
""" """
try: bytes_written = _os.write(device_handle, data)
bytes_written = _os.write(device_handle, data)
return bytes_written == len(data) if bytes_written != len(data):
except: raise OSError(errno=_errno.EIO, strerror='written %d bytes out of expected %d' % (bytes_written, len(data)))
pass
def read(device_handle, bytes_count, timeout_ms=-1): def read(device_handle, bytes_count, timeout_ms=-1):
@ -181,15 +180,19 @@ def read(device_handle, bytes_count, timeout_ms=-1):
:returns: the data packet read, an empty bytes string if a timeout was :returns: the data packet read, an empty bytes string if a timeout was
reached, or None if there was an error while reading. reached, or None if there was an error while reading.
""" """
try: timeout = None if timeout_ms < 0 else timeout_ms / 1000.0
timeout = None if timeout_ms < 0 else timeout_ms / 1000.0 rlist, wlist, xlist = _select([device_handle], [], [device_handle], timeout)
rlist, wlist, xlist = _select([device_handle], [], [], timeout)
if rlist: if xlist:
assert rlist == [device_handle] assert xlist == [device_handle]
return _os.read(device_handle, bytes_count) raise OSError(errno=_errno.EIO, strerror='exception on file descriptor %d' % device_handle)
if rlist:
assert rlist == [device_handle]
data = _os.read(device_handle, bytes_count)
return data
else:
return b'' return b''
except OSError:
pass
_DEVICE_STRINGS = { _DEVICE_STRINGS = {

View File

@ -5,7 +5,7 @@
import logging import logging
from .constants import (STATUS, PROPS) from .constants import (STATUS, PROPS)
from ..unifying_receiver.constants import (FEATURE, BATTERY_STATUS) from ..unifying_receiver.constants import (FEATURE, BATTERY_STATUS, BATTERY_OK)
from ..unifying_receiver import api as _api from ..unifying_receiver import api as _api
# #
@ -14,17 +14,17 @@ from ..unifying_receiver import api as _api
_DEVICE_MODULES = {} _DEVICE_MODULES = {}
def _module(device_name): def _module(device):
if device_name not in _DEVICE_MODULES: shortname = device.codename.lower().replace(' ', '_')
shortname = device_name.split(' ')[-1].lower() if shortname not in _DEVICE_MODULES:
try: try:
m = __import__(shortname, globals(), level=1) m = __import__(shortname, globals(), level=1)
_DEVICE_MODULES[device_name] = m _DEVICE_MODULES[shortname] = m
except: except:
# logging.exception(shortname) # logging.exception(shortname)
_DEVICE_MODULES[device_name] = None _DEVICE_MODULES[shortname] = None
return _DEVICE_MODULES[device_name] return _DEVICE_MODULES[shortname]
# #
# #
@ -34,8 +34,11 @@ def default_request_status(devinfo):
if FEATURE.BATTERY in devinfo.features: if FEATURE.BATTERY in devinfo.features:
reply = _api.get_device_battery_level(devinfo.handle, devinfo.number, features=devinfo.features) reply = _api.get_device_battery_level(devinfo.handle, devinfo.number, features=devinfo.features)
if reply: if reply:
discharge, dischargeNext, status = reply b_discharge, dischargeNext, b_status = reply
return STATUS.CONNECTED, {PROPS.BATTERY_LEVEL: discharge, PROPS.BATTERY_STATUS: status} return STATUS.CONNECTED, {
PROPS.BATTERY_LEVEL: b_discharge,
PROPS.BATTERY_STATUS: b_status,
}
reply = _api.ping(devinfo.handle, devinfo.number) reply = _api.ping(devinfo.handle, devinfo.number)
return STATUS.CONNECTED if reply else STATUS.UNAVAILABLE return STATUS.CONNECTED if reply else STATUS.UNAVAILABLE
@ -44,29 +47,33 @@ def default_request_status(devinfo):
def default_process_event(devinfo, data): def default_process_event(devinfo, data):
feature_index = ord(data[0:1]) feature_index = ord(data[0:1])
if feature_index >= len(devinfo.features): if feature_index >= len(devinfo.features):
logging.warn("mistery event %s for %s", repr(data), devinfo) # logging.warn("mistery event %s for %s", repr(data), devinfo)
return None return None
feature = devinfo.features[feature_index] feature = devinfo.features[feature_index]
feature_function = ord(data[1:2]) & 0xF0 feature_function = ord(data[1:2]) & 0xF0
if feature == FEATURE.BATTERY: if feature == FEATURE.BATTERY:
if feature_function == 0: if feature_function == 0x00:
discharge = ord(data[2:3]) b_discharge = ord(data[2:3])
status = BATTERY_STATUS[ord(data[3:4])] b_status = ord(data[3:4])
return STATUS.CONNECTED, {PROPS.BATTERY_LEVEL: discharge, PROPS.BATTERY_STATUS: status} return STATUS.CONNECTED, {
PROPS.BATTERY_LEVEL: b_discharge,
PROPS.BATTERY_STATUS: BATTERY_STATUS[b_status],
PROPS.UI_FLAGS: 0 if BATTERY_OK(b_status) else STATUS.UI_NOTIFY,
}
# ? # ?
elif feature == FEATURE.REPROGRAMMABLE_KEYS: elif feature == FEATURE.REPROGRAMMABLE_KEYS:
if feature_function == 0: if feature_function == 0x00:
logging.debug('reprogrammable key: %s', repr(data)) logging.debug('reprogrammable key: %s', repr(data))
# TODO # TODO
pass pass
# ? # ?
elif feature == FEATURE.WIRELESS: elif feature == FEATURE.WIRELESS:
if feature_function == 0: if feature_function == 0x00:
logging.debug("wireless status: %s", repr(data)) logging.debug("wireless status: %s", repr(data))
if data[2:5] == b'\x01\x01\x01': if data[2:5] == b'\x01\x01\x01':
return STATUS.CONNECTED return STATUS.CONNECTED, {PROPS.UI_FLAGS: STATUS.UI_NOTIFY}
# TODO # TODO
pass pass
# ? # ?
@ -79,7 +86,7 @@ def request_status(devinfo):
:param listener: the EventsListener that will be used to send the request, :param listener: the EventsListener that will be used to send the request,
and which will receive the status events from the device. and which will receive the status events from the device.
""" """
m = _module(devinfo.name) m = _module(devinfo)
if m and 'request_status' in m.__dict__: if m and 'request_status' in m.__dict__:
return m.request_status(devinfo) return m.request_status(devinfo)
return default_request_status(devinfo) return default_request_status(devinfo)
@ -95,6 +102,6 @@ def process_event(devinfo, data):
if default_result is not None: if default_result is not None:
return default_result return default_result
m = _module(devinfo.name) m = _module(devinfo)
if m and 'process_event' in m.__dict__: if m and 'process_event' in m.__dict__:
return m.process_event(devinfo, data) return m.process_event(devinfo, data)

View File

@ -4,8 +4,10 @@
STATUS = type('STATUS', (), STATUS = type('STATUS', (),
dict( dict(
UNKNOWN=-9999, UI_NOTIFY=0x01,
UNPAIRED=-1000, UI_POPUP=0x02,
UNKNOWN=-0xFFFF,
UNPAIRED=-0x1000,
UNAVAILABLE=-1, UNAVAILABLE=-1,
BOOTING=0, BOOTING=0,
CONNECTED=1, CONNECTED=1,
@ -26,6 +28,7 @@ PROPS = type('PROPS', (),
BATTERY_LEVEL='battery_level', BATTERY_LEVEL='battery_level',
BATTERY_STATUS='battery_status', BATTERY_STATUS='battery_status',
LIGHT_LEVEL='light_level', LIGHT_LEVEL='light_level',
UI_FLAGS='ui_flags',
)) ))
# when the receiver reports a device that is not connected # when the receiver reports a device that is not connected

View File

@ -47,4 +47,9 @@ def process_event(devinfo, data):
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) or _charge_status(data) if request_status(devinfo) == STATUS.UNAVAILABLE:
return STATUS.UNAVAILABLE, {PROPS.UI_FLAGS: STATUS.UI_POPUP | STATUS.UI_NOTIFY}
code, props = _charge_status(data)
props[PROPS.UI_FLAGS] = STATUS.UI_POPUP
return code, props

View File

@ -7,17 +7,26 @@ def print_receiver(receiver):
print (" Serial : %s" % receiver.serial) print (" Serial : %s" % receiver.serial)
for f in receiver.firmware: for f in receiver.firmware:
print (" %-10s: %s" % (f.kind, f.version)) print (" %-10s: %s" % (f.kind, f.version))
print (" Receiver reported %d paired device(s)" % len(receiver))
def scan_devices(receiver): def scan_devices(receiver):
for dev in receiver: for number in range(1, 1 + receiver.max_devices):
dev = receiver[number]
if dev is None:
dev = api.PairedDevice(receiver.handle, number)
if dev.codename is None:
continue
print ("--------") print ("--------")
print (str(dev)) print (str(dev))
print ("Codename : %s" % dev.codename)
print ("Name : %s" % dev.name) print ("Name : %s" % dev.name)
print ("Kind : %s" % dev.kind) print ("Kind : %s" % dev.kind)
print ("Serial number: %s" % dev.serial) print ("Serial number: %s" % dev.serial)
if not dev.protocol: if not dev.protocol:
print ("HID protocol : UNKNOWN") print ("Device is not connected at this time, no further info available.")
continue continue
print ("HID protocol : HID %01.1f" % dev.protocol) print ("HID protocol : HID %01.1f" % dev.protocol)
@ -27,7 +36,7 @@ def scan_devices(receiver):
firmware = dev.firmware firmware = dev.firmware
for fw in firmware: for fw in firmware:
print (" %-10s: %s %s" % (fw.kind, fw.name, fw.version)) print (" %-11s: %s %s" % (fw.kind, fw.name, fw.version))
all_features = api.get_device_features(dev.handle, dev.number) all_features = api.get_device_features(dev.handle, dev.number)
for index in range(0, len(all_features)): for index in range(0, len(all_features)):
@ -45,9 +54,7 @@ def scan_devices(receiver):
print (" %d reprogrammable keys found" % len(keys)) print (" %d reprogrammable keys found" % len(keys))
for k in keys: for k in keys:
flags = ','.join(KEY_FLAG_NAME[f] for f in KEY_FLAG_NAME if k.flags & f) flags = ','.join(KEY_FLAG_NAME[f] for f in KEY_FLAG_NAME if k.flags & f)
print (" %2d: %-12s => %-12s :%s" % (k.index, KEY_NAME[k.id], KEY_NAME[k.task], flags)) print (" %2d: %-12s => %-12s : %s" % (k.index, KEY_NAME[k.id], KEY_NAME[k.task], flags))
print ("--------")
if __name__ == '__main__': if __name__ == '__main__':
@ -65,9 +72,8 @@ if __name__ == '__main__':
receiver = api.Receiver.open() receiver = api.Receiver.open()
if receiver is None: if receiver is None:
print ("!! Logitech Unifying Receiver not found.") print ("Logitech Unifying Receiver not found.")
else: else:
print ("!! Found Logitech Unifying Receiver: %s" % receiver)
print_receiver(receiver) print_receiver(receiver)
scan_devices(receiver) scan_devices(receiver)
receiver.close() receiver.close()

View File

@ -5,6 +5,7 @@
from struct import pack as _pack from struct import pack as _pack
from struct import unpack as _unpack from struct import unpack as _unpack
import errno as _errno import errno as _errno
from threading import local as _local
from . import base as _base from . import base as _base
@ -27,6 +28,56 @@ del getLogger
# #
# #
class ThreadedHandle(object):
__slots__ = ['path', '_local', '_handles']
def __init__(self, initial_handle, path):
if type(initial_handle) != int:
raise TypeError('expected int as initial handle, got %s' % repr(initial_handle))
self.path = path
self._local = _local()
self._local.handle = initial_handle
self._handles = [initial_handle]
def _open(self):
handle = _base.open_path(self.path)
if handle is None:
_log.error("%s failed to open new handle", repr(self))
else:
# _log.debug("%s opened new handle %d", repr(self), handle)
self._local.handle = handle
self._handles.append(handle)
return handle
def close(self):
self._local = None
handles, self._handles = self._handles, []
_log.debug("%s closing %s", repr(self), handles)
for h in handles:
_base.close(h)
def __del__(self):
self.close()
def __int__(self):
if self._local:
try:
return self._local.handle
except:
return self._open()
def __str__(self):
return str(int(self))
def __repr__(self):
return '<LocalHandle[%s]>' % self.path
def __bool__(self):
return bool(self._handles)
__nonzero__ = __bool__
class PairedDevice(object): class PairedDevice(object):
def __init__(self, handle, number): def __init__(self, handle, number):
self.handle = handle self.handle = handle
@ -40,11 +91,15 @@ class PairedDevice(object):
self._serial = None self._serial = None
self._firmware = None self._firmware = None
def __del__(self):
self.handle = None
@property @property
def protocol(self): def protocol(self):
if self._protocol is None: if self._protocol is None:
self._protocol = _base.ping(self.handle, self.number) self._protocol = _base.ping(self.handle, self.number)
return 0 if self._protocol is None else self._protocol # _log.debug("device %d protocol %s", self.number, self._protocol)
return self._protocol or 0
@property @property
def features(self): def features(self):
@ -59,7 +114,8 @@ class PairedDevice(object):
codename = _base.request(self.handle, 0xFF, b'\x83\xB5', 0x40 + self.number - 1) codename = _base.request(self.handle, 0xFF, b'\x83\xB5', 0x40 + self.number - 1)
if codename: if codename:
self._codename = codename[2:].rstrip(b'\x00').decode('ascii') self._codename = codename[2:].rstrip(b'\x00').decode('ascii')
return self._codename or '?' # _log.debug("device %d codename %s", self.number, self._codename)
return self._codename
@property @property
def name(self): def name(self):
@ -70,7 +126,7 @@ class PairedDevice(object):
self._name, self._kind = _DEVICE_NAMES[self._codename] self._name, self._kind = _DEVICE_NAMES[self._codename]
else: else:
self._name = get_device_name(self.handle, self.number, self.features) self._name = get_device_name(self.handle, self.number, self.features)
return self._name or self.codename return self._name or self.codename or '?'
@property @property
def kind(self): def kind(self):
@ -87,6 +143,7 @@ class PairedDevice(object):
def firmware(self): def firmware(self):
if self._firmware is None and self.protocol >= 2.0: if self._firmware is None and self.protocol >= 2.0:
self._firmware = get_device_firmware(self.handle, self.number, self.features) self._firmware = get_device_firmware(self.handle, self.number, self.features)
# _log.debug("device %d firmware %s", self.number, self._firmware)
return self._firmware or () return self._firmware or ()
@property @property
@ -96,16 +153,14 @@ class PairedDevice(object):
serial = _base.request(self.handle, 0xFF, b'\x83\xB5', 0x30 + self.number - 1) serial = _base.request(self.handle, 0xFF, b'\x83\xB5', 0x30 + self.number - 1)
if prefix and serial: if prefix and serial:
self._serial = _base._hex(prefix[3:5]) + '-' + _base._hex(serial[1:5]) self._serial = _base._hex(prefix[3:5]) + '-' + _base._hex(serial[1:5])
# _log.debug("device %d serial %s", self.number, self._serial)
return self._serial or '?' return self._serial or '?'
def ping(self): def ping(self):
return _base.ping(self.handle, self.number) is not None return _base.ping(self.handle, self.number) is not None
def __str__(self): def __str__(self):
return '<PairedDevice(%X,%d,%s)>' % (self.handle, self.number, self._name or '?') return '<PairedDevice(%s,%d,%s)>' % (self.handle, self.number, self.codename or '?')
def __hash__(self):
return self.number
class Receiver(object): class Receiver(object):
@ -120,9 +175,12 @@ class Receiver(object):
self._firmware = None self._firmware = None
def close(self): def close(self):
handle, self.handle = self.handle, 0 handle, self.handle = self.handle, None
return (handle and _base.close(handle)) return (handle and _base.close(handle))
def __del__(self):
self.close()
@property @property
def serial(self): def serial(self):
if self._serial is None and self.handle: if self._serial is None and self.handle:
@ -153,7 +211,7 @@ class Receiver(object):
return self._firmware return self._firmware
def __iter__(self): def __iter__(self):
if self.handle == 0: if not self.handle:
return return
for number in range(1, 1 + MAX_ATTACHED_DEVICES): for number in range(1, 1 + MAX_ATTACHED_DEVICES):
@ -164,14 +222,14 @@ class Receiver(object):
def __getitem__(self, key): def __getitem__(self, key):
if type(key) != int: if type(key) != int:
raise TypeError('key must be an integer') raise TypeError('key must be an integer')
if self.handle == 0 or key < 0 or key > MAX_ATTACHED_DEVICES: if not self.handle or key < 0 or key > MAX_ATTACHED_DEVICES:
raise IndexError(key) raise IndexError(key)
return get_device(self.handle, key) if key > 0 else None return get_device(self.handle, key) if key > 0 else None
def __delitem__(self, key): def __delitem__(self, key):
if type(key) != int: if type(key) != int:
raise TypeError('key must be an integer') raise TypeError('key must be an integer')
if self.handle == 0 or key < 0 or key > MAX_ATTACHED_DEVICES: if not self.handle or key < 0 or key > MAX_ATTACHED_DEVICES:
raise IndexError(key) raise IndexError(key)
if key > 0: if key > 0:
_log.debug("unpairing device %d", key) _log.debug("unpairing device %d", key)
@ -180,13 +238,14 @@ class Receiver(object):
raise IndexError(key) raise IndexError(key)
def __len__(self): def __len__(self):
if self.handle == 0: if not self.handle:
return 0 return 0
# not really sure about this one... # not really sure about this one...
count = _base.request(self.handle, 0xFF, b'\x81\x00') count = _base.request(self.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])
def __contains__(self, dev): def __contains__(self, dev):
# print self, "contains", dev
if self.handle == 0: if self.handle == 0:
return False return False
if type(dev) == int: if type(dev) == int:
@ -194,10 +253,7 @@ class Receiver(object):
return dev.ping() return dev.ping()
def __str__(self): def __str__(self):
return '<Receiver(%X,%s)>' % (self.handle, self.path) return '<Receiver(%s,%s)>' % (self.handle, self.path)
def __hash__(self):
return self.handle
__bool__ = __nonzero__ = lambda self: self.handle != 0 __bool__ = __nonzero__ = lambda self: self.handle != 0
@ -212,7 +268,7 @@ class Receiver(object):
for rawdevice in _base.list_receiver_devices(): for rawdevice in _base.list_receiver_devices():
exception = None exception = None
try: try:
handle = _base.try_open(rawdevice.path) handle = _base.open_path(rawdevice.path)
if handle: if handle:
return Receiver(handle, rawdevice.path) return Receiver(handle, rawdevice.path)
except OSError as e: except OSError as e:
@ -295,13 +351,13 @@ def get_feature_index(handle, devnumber, feature):
if reply: if reply:
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 feature_flags: if feature_flags:
# _log.debug("device %d feature <%s:%s> has index %d: %s", _log.debug("device %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:
# _log.debug("device %d feature <%s:%s> has index %d", devnumber, _hex(feature), FEATURE_NAME[feature], feature_index) _log.debug("device %d feature <%s:%s> has index %d", devnumber, _hex(feature), FEATURE_NAME[feature], feature_index)
# only consider active and supported features? # only consider active and supported features?
# if feature_flags: # if feature_flags:
@ -322,9 +378,12 @@ def _get_feature_index(handle, devnumber, feature, features=None):
index = get_feature_index(handle, devnumber, feature) index = get_feature_index(handle, devnumber, feature)
if index is not None: if index is not None:
if len(features) <= index: try:
features += [None] * (index + 1 - len(features)) if len(features) <= index:
features[index] = feature features += [None] * (index + 1 - len(features))
features[index] = feature
except:
pass
# _log.debug("%s: found feature %s at %d", features, _base._hex(feature), index) # _log.debug("%s: found feature %s at %d", features, _base._hex(feature), index)
return index return index

View File

@ -3,6 +3,8 @@
# Unlikely to be used directly unless you're expanding the API. # Unlikely to be used directly unless you're expanding the API.
# #
import os as _os
from time import time as _timestamp
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()
@ -40,32 +42,7 @@ _MAX_REPLY_SIZE = _MAX_CALL_SIZE
"""Default timeout on read (in ms).""" """Default timeout on read (in ms)."""
DEFAULT_TIMEOUT = 1500 DEFAULT_TIMEOUT = 2000
#
#
#
def _logdebug_hook(reply_code, devnumber, data):
"""Default unhandled hook, logs the reply as DEBUG."""
_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 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
# #
# #
@ -76,13 +53,10 @@ def list_receiver_devices():
# (Vendor ID, Product ID) = ('Logitech', 'Unifying Receiver') # (Vendor ID, Product ID) = ('Logitech', 'Unifying Receiver')
# interface 2 if the actual receiver interface # interface 2 if the actual receiver interface
for d in _hid.enumerate(0x046d, 0xc52b, 2): for d in _hid.enumerate(0x046d, 0xc52b, 2):
if d.driver is None or d.driver == 'logitech-djreceiver': if d.driver == 'logitech-djreceiver':
yield d yield d
def open_path(path):
_COUNT_DEVICES_REQUEST = b'\x10\xFF\x81\x00\x00\x00\x00'
def try_open(path):
"""Checks if the given Linux device path points to the right UR device. """Checks if the given Linux device path points to the right UR device.
:param path: the Linux device path. :param path: the Linux device path.
@ -95,28 +69,7 @@ def try_open(path):
:returns: an open receiver handle if this is the right Linux device, or :returns: an open receiver handle if this is the right Linux device, or
``None``. ``None``.
""" """
receiver_handle = _hid.open_path(path) return _hid.open_path(path)
if receiver_handle is None:
# could be a file permissions issue (did you add the udev rules?)
# in any case, unreachable
_log.debug("[%s] open failed", path)
return None
_hid.write(receiver_handle, _COUNT_DEVICES_REQUEST)
# if this is the right hidraw device, we'll receive a 'bad device' from the UR
# otherwise, the read should produce nothing
reply = _hid.read(receiver_handle, _MAX_REPLY_SIZE, DEFAULT_TIMEOUT / 2)
if reply:
if reply[:5] == _COUNT_DEVICES_REQUEST[:5]:
# 'device 0 unreachable' is the expected reply from a valid receiver handle
_log.info("[%s] success: handle %X", path, receiver_handle)
return receiver_handle
_log.debug("[%s] %X ignored reply %s", path, receiver_handle, _hex(reply))
else:
_log.debug("[%s] %X no reply", path, receiver_handle)
close(receiver_handle)
def open(): def open():
@ -125,8 +78,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():
_log.info("checking %s", rawdevice) handle = open_path(rawdevice.path)
handle = try_open(rawdevice.path)
if handle: if handle:
return handle return handle
@ -135,11 +87,15 @@ def close(handle):
"""Closes a HID device handle.""" """Closes a HID device handle."""
if handle: if handle:
try: try:
_hid.close(handle) if type(handle) == int:
# _log.info("closed receiver handle %X", handle) _hid.close(handle)
else:
handle.close()
# _log.info("closed receiver handle %s", repr(handle))
return True return True
except: except:
_log.exception("closing receiver handle %X", handle) # _log.exception("closing receiver handle %s", repr(handle))
pass
return False return False
@ -162,11 +118,14 @@ def write(handle, devnumber, data):
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("<= w[10 %02X %s %s]", devnumber, _hex(wdata[2:4]), _hex(wdata[4:])) _log.debug("(%s) <= w[10 %02X %s %s]", handle, devnumber, _hex(wdata[2:4]), _hex(wdata[4:]))
if not _hid.write(handle, wdata):
_log.warn("write failed, assuming receiver %X no longer available", handle) try:
_hid.write(int(handle), wdata)
except Exception as reason:
_log.error("write failed, assuming handle %s no longer available", repr(handle))
close(handle) close(handle)
raise _NoReceiver raise _NoReceiver(reason)
def read(handle, timeout=DEFAULT_TIMEOUT): def read(handle, timeout=DEFAULT_TIMEOUT):
@ -185,32 +144,61 @@ 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, timeout) try:
if data is None: data = _hid.read(int(handle), _MAX_REPLY_SIZE, timeout)
_log.warn("read failed, assuming receiver %X no longer available", handle) except Exception as reason:
_log.error("read failed, assuming handle %s no longer available", repr(handle))
close(handle) close(handle)
raise _NoReceiver raise _NoReceiver(reason)
if data: if data:
if len(data) < _MIN_REPLY_SIZE: if len(data) < _MIN_REPLY_SIZE:
_log.warn("=> r[%s] read packet too short: %d bytes", _hex(data), len(data)) _log.warn("(%s) => r[%s] read packet too short: %d bytes", handle, _hex(data), len(data))
data += b'\x00' * (_MIN_REPLY_SIZE - len(data)) data += b'\x00' * (_MIN_REPLY_SIZE - len(data))
if len(data) > _MAX_REPLY_SIZE: if len(data) > _MAX_REPLY_SIZE:
_log.warn("=> r[%s] read packet too long: %d bytes", _hex(data), len(data)) _log.warn("(%s) => r[%s] read packet too long: %d bytes", handle, _hex(data), len(data))
code = ord(data[:1]) code = ord(data[:1])
devnumber = ord(data[1:2]) devnumber = ord(data[1:2])
_log.debug("=> r[%02X %02X %s %s]", code, devnumber, _hex(data[2:4]), _hex(data[4:])) _log.debug("(%s) => r[%02X %02X %s %s]", handle, code, devnumber, _hex(data[2:4]), _hex(data[4:]))
return code, devnumber, data[2:] return code, devnumber, data[2:]
# _l.log(_LOG_LEVEL, "(-) => r[]") # _l.log(_LOG_LEVEL, "(-) => r[]")
def _skip_incoming(handle):
ihandle = int(handle)
while True:
try:
data = _hid.read(ihandle, _MAX_REPLY_SIZE, 0)
except Exception as reason:
_log.error("read failed, assuming receiver %s no longer available", handle)
close(handle)
raise _NoReceiver(reason)
if data:
if unhandled_hook:
unhandled_hook(ord(data[:1]), ord(data[1:2]), data[2:])
else:
return
#
#
#
"""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.
"""
unhandled_hook = None
_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.
@ -232,118 +220,93 @@ def request(handle, devnumber, feature_index_function, params=b'', features=None
if type(params) == int: if type(params) == int:
params = _pack('!B', params) params = _pack('!B', params)
# _log.debug("device %d request {%s} params [%s]", devnumber, _hex(feature_index_function), _hex(params)) # _log.debug("%s device %d request {%s} params [%s]", handle, 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))
if request_context is None or handle != request_context.handle: _skip_incoming(handle)
context = _DEFAULT_REQUEST_CONTEXT ihandle = int(handle)
_unhandled = unhandled_hook write(ihandle, devnumber, feature_index_function + params)
else:
context = request_context
_unhandled = getattr(context, 'unhandled_hook')
context.write(handle, devnumber, feature_index_function + params) while True:
now = _timestamp()
reply = read(ihandle, DEFAULT_TIMEOUT)
delta = _timestamp() - now
read_times = _MAX_READ_TIMES if reply:
while read_times > 0: reply_code, reply_devnumber, reply_data = reply
divisor = (1 + _MAX_READ_TIMES - read_times) if reply_devnumber == devnumber:
reply = context.read(handle, int(DEFAULT_TIMEOUT * (divisor + 1) / 2 / divisor)) if reply_code == 0x10 and reply_data[:1] == b'\x8F' and reply_data[1:3] == feature_index_function:
read_times -= 1 # device not present
_log.debug("device %d request failed on {%s} call: [%s]", devnumber, _hex(feature_index_function), _hex(reply_data))
return None
if not reply: if reply_code == 0x10 and reply_data[:1] == b'\x8F':
# keep waiting... # device not present
continue _log.debug("device %d request failed: [%s]", devnumber, _hex(reply_data))
return None
reply_code, reply_devnumber, reply_data = reply if reply_code == 0x11 and reply_data[0] == b'\xFF' and reply_data[1:3] == feature_index_function:
# the feature call returned with an error
error_code = ord(reply_data[3])
_log.warn("device %d request feature call error %d = %s: %s", devnumber, error_code, ERROR_NAME[error_code], _hex(reply_data))
feature_index = ord(feature_index_function[:1])
feature_function = feature_index_function[1:2]
feature = None if features is None else features[feature_index] if feature_index < len(features) else None
raise _FeatureCallError(devnumber, feature, feature_index, feature_function, error_code, reply_data)
if reply_devnumber != devnumber: if reply_code == 0x11 and reply_data[:2] == feature_index_function:
# this message not for the device we're interested in # a matching reply
# _l.log(_LOG_LEVEL, "device %d request got reply for unexpected device %d: [%s]", devnumber, reply_devnumber, _hex(reply_data)) # _log.debug("device %d matched reply with feature-index-function [%s]", devnumber, _hex(reply_data[2:]))
# worst case scenario, this is a reply for a concurrent request return reply_data[2:]
# on this receiver
if _unhandled:
_unhandled(reply_code, reply_devnumber, reply_data)
continue
if reply_code == 0x10 and reply_data[:1] == b'\x8F' and reply_data[1:3] == feature_index_function: if reply_code == 0x10 and devnumber == 0xFF and reply_data[:2] == feature_index_function:
# device not present # direct calls to the receiver (device 0xFF) may also return successfully with reply code 0x10
_log.debug("device %d request failed on {%s} call: [%s]", devnumber, _hex(feature_index_function), _hex(reply_data)) # _log.debug("device %d matched reply with feature-index-function [%s]", devnumber, _hex(reply_data[2:]))
return reply_data[2:]
if unhandled_hook:
unhandled_hook(reply_code, reply_devnumber, reply_data)
if delta >= DEFAULT_TIMEOUT:
_log.warn("timeout on device %d request {%s} params[%s]", devnumber, _hex(feature_index_function), _hex(params))
return None return None
if reply_code == 0x10 and reply_data[:1] == b'\x8F':
# device not present
_log.debug("device %d request failed: [%s]", devnumber, _hex(reply_data))
return None
if reply_code == 0x11 and reply_data[0] == b'\xFF' and reply_data[1:3] == feature_index_function:
# the feature call returned with an error
error_code = ord(reply_data[3])
_log.warn("device %d request feature call error %d = %s: %s", devnumber, error_code, ERROR_NAME[error_code], _hex(reply_data))
feature_index = ord(feature_index_function[:1])
feature_function = feature_index_function[1:2]
feature = None if features is None else features[feature_index] if feature_index < len(features) else None
raise _FeatureCallError(devnumber, feature, feature_index, feature_function, error_code, reply_data)
if reply_code == 0x11 and reply_data[:2] == feature_index_function:
# a matching reply
# _log.debug("device %d matched reply with feature-index-function [%s]", devnumber, _hex(reply_data[2:]))
return reply_data[2:]
if reply_code == 0x10 and devnumber == 0xFF and reply_data[:2] == feature_index_function:
# direct calls to the receiver (device 0xFF) may also return successfully with reply code 0x10
# _log.debug("device %d matched reply with feature-index-function [%s]", devnumber, _hex(reply_data[2:]))
return reply_data[2:]
# _log.debug("device %d unmatched reply {%s} (expected {%s})", devnumber, _hex(reply_data[:2]), _hex(feature_index_function))
if _unhandled:
_unhandled(reply_code, reply_devnumber, reply_data)
def ping(handle, devnumber): def ping(handle, devnumber):
"""Check if a device is connected to the UR. """Check if a device is connected to the UR.
:returns: The HID protocol supported by the device, as a floating point number, if the device is active. :returns: The HID protocol supported by the device, as a floating point number, if the device is active.
""" """
if request_context is None or handle != request_context.handle: _log.debug("%s pinging device %d", handle, devnumber)
context = _DEFAULT_REQUEST_CONTEXT
_unhandled = unhandled_hook
else:
context = request_context
_unhandled = getattr(context, 'unhandled_hook')
context.write(handle, devnumber, b'\x00\x11\x00\x00\xAA') _skip_incoming(handle)
read_times = _MAX_READ_TIMES ihandle = int(handle)
while read_times > 0: write(ihandle, devnumber, b'\x00\x11\x00\x00\xAA')
divisor = (1 + _MAX_READ_TIMES - read_times)
reply = context.read(handle, int(DEFAULT_TIMEOUT * (divisor + 1) / 2 / divisor))
read_times -= 1
if not reply: while True:
# keep waiting... now = _timestamp()
continue reply = read(ihandle, DEFAULT_TIMEOUT)
delta = _timestamp() - now
reply_code, reply_devnumber, reply_data = reply if reply:
reply_code, reply_devnumber, reply_data = reply
if reply_devnumber == devnumber:
if reply_code == 0x11 and reply_data[:2] == b'\x00\x11' and reply_data[4:5] == b'\xAA':
# HID 2.0+ device, currently connected
return ord(reply_data[2:3]) + ord(reply_data[3:4]) / 10.0
if reply_devnumber != devnumber: if reply_code == 0x10 and reply_data == b'\x8F\x00\x11\x01\x00':
# this message not for the device we're interested in # HID 1.0 device, currently connected
# _l.log(_LOG_LEVEL, "device %d request got reply for unexpected device %d: [%s]", devnumber, reply_devnumber, _hex(reply_data)) return 1.0
# worst case scenario, this is a reply for a concurrent request
# on this receiver
if _unhandled:
_unhandled(reply_code, reply_devnumber, reply_data)
continue
if reply_code == 0x11 and reply_data[:2] == b'\x00\x11' and reply_data[4:5] == b'\xAA': if reply_code == 0x10 and reply_data[:3] == b'\x8F\x00\x11':
# HID 2.0+ device, currently connected # a disconnected device
return ord(reply_data[2:3]) + ord(reply_data[3:4]) / 10.0 return None
if reply_code == 0x10 and reply_data == b'\x8F\x00\x11\x01\x00': if unhandled_hook:
# HID 1.0 device, currently connected unhandled_hook(reply_code, reply_devnumber, reply_data)
return 1.0
if reply_code == 0x10 and reply_data[:3] == b'\x8F\x00\x11': if delta >= DEFAULT_TIMEOUT:
# a disconnected device _log.warn("timeout on device %d ping", devnumber)
return None return None
_log.warn("don't know how to interpret ping reply %s", reply)

View File

@ -65,6 +65,7 @@ FIRMWARE_KIND = FallbackDict(lambda x: 'Unknown', list2dict(_FIRMWARE_KINDS))
_BATTERY_STATUSES = ('Discharging (in use)', 'Recharging', 'Almost full', _BATTERY_STATUSES = ('Discharging (in use)', 'Recharging', 'Almost full',
'Full', 'Slow recharge', 'Invalid battery', 'Thermal error', 'Full', 'Slow recharge', 'Invalid battery', 'Thermal error',
'Charging error') 'Charging error')
BATTERY_OK = lambda status: status < 5
"""Names for possible battery status values.""" """Names for possible battery status values."""
BATTERY_STATUS = FallbackDict(lambda x: 'unknown', list2dict(_BATTERY_STATUSES)) BATTERY_STATUS = FallbackDict(lambda x: 'unknown', list2dict(_BATTERY_STATUSES))

View File

@ -2,12 +2,12 @@
# #
# #
from threading import Thread as _Thread import threading as _threading
# from time import sleep as _sleep
from . import base as _base from . import base as _base
from .exceptions import NoReceiver as _NoReceiver from .exceptions import NoReceiver as _NoReceiver
from .common import Packet as _Packet from .common import Packet as _Packet
from .constants import MAX_ATTACHED_DEVICES as _MAX_ATTACHED_DEVICES
# for both Python 2 and 3 # for both Python 2 and 3
try: try:
@ -21,26 +21,10 @@ _log = getLogger('LUR').getChild('listener')
del getLogger del getLogger
_READ_EVENT_TIMEOUT = int(_base.DEFAULT_TIMEOUT / 2) # ms class EventsListener(_threading.Thread):
def _event_dispatch(listener, callback):
while listener._active: # or not listener._events.empty():
try:
event = listener._events.get(True, _READ_EVENT_TIMEOUT * 10)
except:
continue
# _log.debug("delivering event %s", event)
try:
callback(event)
except:
_log.exception("callback for %s", event)
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.
separate thread.
""" """
def __init__(self, receiver_handle, events_callback): def __init__(self, receiver_handle, events_callback):
super(EventsListener, self).__init__(group='Unifying Receiver', name=self.__class__.__name__) super(EventsListener, self).__init__(group='Unifying Receiver', name=self.__class__.__name__)
@ -49,92 +33,65 @@ class EventsListener(_Thread):
self._active = False self._active = False
self._handle = receiver_handle self._handle = receiver_handle
self._queued_events = _Queue(32)
self._tasks = _Queue(1) self._events_callback = events_callback
self._backup_unhandled_hook = _base.unhandled_hook
_base.unhandled_hook = self.unhandled_hook
self._events = _Queue(32)
self._dispatcher = _Thread(group='Unifying Receiver',
name=self.__class__.__name__ + '-dispatch',
target=_event_dispatch, args=(self, events_callback))
self._dispatcher.daemon = True
def run(self): def run(self):
self._active = True self._active = True
_log.debug("started") _base.unhandled_hook = self._unhandled_hook
_base.request_context = self ihandle = int(self._handle)
_base.unhandled_hook = self._backup_unhandled_hook _log.info("started with %s (%d)", repr(self._handle), ihandle)
del self._backup_unhandled_hook
self._dispatcher.start()
while self._active: while self._active:
try: if self._queued_events.empty():
# _log.debug("read next event") try:
event = _base.read(self._handle, _READ_EVENT_TIMEOUT) # _log.debug("read next event")
except _NoReceiver: event = _base.read(ihandle)
self._handle = 0 # shortcut: we should only be looking at events for proper device numbers
_log.warn("receiver disconnected") except _NoReceiver:
self._events.put(_Packet(0xFF, 0xFF, None)) self._active = False
self._active = False self._handle = None
_log.warning("receiver disconnected")
event = (0xFF, 0xFF, None)
else: else:
if event is not None: # deliver any queued events
matched = False event = self._queued_events.get()
task = None if self._tasks.empty() else self._tasks.queue[0]
if task and task[-1] is None:
task_dev, task_data = task[:2]
if event[1] == task_dev:
# _log.debug("matching %s to (%d, %s)", event, task_dev, repr(task_data))
matched = event[2][:2] == task_data[:2] or (event[2][:1] in b'\x8F\xFF' and event[2][1:3] == task_data[:2])
if matched: if event:
# _log.debug("request reply %s", event) event = _Packet(*event)
task[-1] = event # _log.debug("processing event %s", event)
self._tasks.task_done() try:
else: self._events_callback(event)
event = _Packet(*event) except:
_log.info("queueing event %s", event) _log.exception("processing event %s", event)
self._events.put(event)
_base.request_context = None _base.unhandled_hook = None
handle, self._handle = self._handle, 0 handle, self._handle = self._handle, None
_base.close(handle) if handle:
_log.debug("stopped") _base.close(handle)
_log.info("stopped %s", repr(handle))
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:
_log.debug("stopping") _log.debug("stopping")
self._active = False self._active = False
# wait for the receiver handle to be closed handle, self._handle = self._handle, None
self.join() if handle:
_base.close(handle)
_log.info("stopped %s", repr(handle))
@property @property
def handle(self): def handle(self):
return self._handle return self._handle
def write(self, handle, devnumber, data): def _unhandled_hook(self, reply_code, devnumber, data):
assert handle == self._handle # only consider unhandled events that were sent from this thread,
# _log.debug("write %02X %s", devnumber, _base._hex(data)) # i.e. triggered during a callback of a previous event
task = [devnumber, data, None] if _threading.current_thread() == self:
self._tasks.put(task) event = _Packet(reply_code, devnumber, data)
_base.write(self._handle, devnumber, data) _log.info("queueing unhandled event %s", event)
# _log.debug("task queued %s", task) self._queued_events.put(event)
def read(self, handle, timeout=_base.DEFAULT_TIMEOUT):
assert handle == self._handle
# _log.debug("read %d", timeout)
assert not self._tasks.empty()
self._tasks.join()
task = self._tasks.get(False)
# _log.debug("task ready %s", task)
return task[-1]
def unhandled_hook(self, reply_code, devnumber, data):
event = _Packet(reply_code, devnumber, data)
_log.info("queueing unhandled event %s", event)
self._events.put(event)
def __bool__(self): def __bool__(self):
return bool(self._active and self._handle) return bool(self._active and self._handle)

View File

@ -30,16 +30,16 @@ class Test_UR_Base(unittest.TestCase):
# self.assertIsInstance(rawdevices, Iterable, "list_receiver_devices should have returned an iterable") # self.assertIsInstance(rawdevices, Iterable, "list_receiver_devices should have returned an iterable")
Test_UR_Base.ur_available = len(list(rawdevices)) > 0 Test_UR_Base.ur_available = len(list(rawdevices)) > 0
def test_20_try_open(self): def test_20_open_path(self):
if not self.ur_available: if not self.ur_available:
self.fail("No receiver found") self.fail("No receiver found")
for rawdevice in base.list_receiver_devices(): for rawdevice in base.list_receiver_devices():
handle = base.try_open(rawdevice.path) handle = base.open_path(rawdevice.path)
if handle is None: if handle is None:
continue continue
self.assertIsInstance(handle, int, "try_open should have returned an int") self.assertIsInstance(handle, int, "open_path should have returned an int")
if Test_UR_Base.handle is None: if Test_UR_Base.handle is None:
Test_UR_Base.handle = handle Test_UR_Base.handle = handle
@ -131,46 +131,46 @@ class Test_UR_Base(unittest.TestCase):
index = reply[:1] index = reply[:1]
self.assertGreater(index, b'\x00', "FEATURE_SET not available on device " + str(self.device)) self.assertGreater(index, b'\x00', "FEATURE_SET not available on device " + str(self.device))
def test_57_request_ignore_undhandled(self): # def test_57_request_ignore_undhandled(self):
if self.handle is None: # if self.handle is None:
self.fail("No receiver found") # self.fail("No receiver found")
if self.device is None: # if self.device is None:
self.fail("No devices attached") # self.fail("No devices attached")
fs_index = base.request(self.handle, self.device, FEATURE.ROOT, FEATURE.FEATURE_SET) # fs_index = base.request(self.handle, self.device, FEATURE.ROOT, FEATURE.FEATURE_SET)
self.assertIsNotNone(fs_index) # self.assertIsNotNone(fs_index)
fs_index = fs_index[:1] # fs_index = fs_index[:1]
self.assertGreater(fs_index, b'\x00') # self.assertGreater(fs_index, b'\x00')
global received_unhandled # global received_unhandled
received_unhandled = None # received_unhandled = None
def _unhandled(code, device, data): # def _unhandled(code, device, data):
self.assertIsNotNone(code) # self.assertIsNotNone(code)
self.assertIsInstance(code, int) # self.assertIsInstance(code, int)
self.assertIsNotNone(device) # self.assertIsNotNone(device)
self.assertIsInstance(device, int) # self.assertIsInstance(device, int)
self.assertIsNotNone(data) # self.assertIsNotNone(data)
self.assertIsInstance(data, str) # self.assertIsInstance(data, str)
global received_unhandled # global received_unhandled
received_unhandled = (code, device, data) # received_unhandled = (code, device, data)
base.unhandled_hook = _unhandled # base.unhandled_hook = _unhandled
base.write(self.handle, self.device, FEATURE.ROOT + FEATURE.FEATURE_SET) # base.write(self.handle, self.device, FEATURE.ROOT + FEATURE.FEATURE_SET)
reply = base.request(self.handle, self.device, fs_index + b'\x00') # reply = base.request(self.handle, self.device, fs_index + b'\x00')
self.assertIsNotNone(reply, "request returned None reply") # self.assertIsNotNone(reply, "request returned None reply")
self.assertNotEquals(reply[:1], b'\x00') # self.assertNotEquals(reply[:1], b'\x00')
self.assertIsNotNone(received_unhandled, "extra message not received by unhandled hook") # self.assertIsNotNone(received_unhandled, "extra message not received by unhandled hook")
received_unhandled = None # received_unhandled = None
base.unhandled_hook = None # base.unhandled_hook = None
base.write(self.handle, self.device, FEATURE.ROOT + FEATURE.FEATURE_SET) # base.write(self.handle, self.device, FEATURE.ROOT + FEATURE.FEATURE_SET)
reply = base.request(self.handle, self.device, fs_index + b'\x00') # reply = base.request(self.handle, self.device, fs_index + b'\x00')
self.assertIsNotNone(reply, "request returned None reply") # self.assertIsNotNone(reply, "request returned None reply")
self.assertNotEquals(reply[:1], b'\x00') # self.assertNotEquals(reply[:1], b'\x00')
self.assertIsNone(received_unhandled) # self.assertIsNone(received_unhandled)
del received_unhandled # del received_unhandled
# def test_90_receiver_missing(self): # def test_90_receiver_missing(self):
# if self.handle is None: # if self.handle is None:

View File

@ -41,7 +41,8 @@ class Test_UR_API(unittest.TestCase):
def test_05_ping_device_zero(self): def test_05_ping_device_zero(self):
self._check(check_device=False) self._check(check_device=False)
ok = api.ping(self.receiver.handle, 0) d = api.PairedDevice(self.receiver.handle, 0)
ok = d.ping()
self.assertIsNotNone(ok, "invalid ping reply") self.assertIsNotNone(ok, "invalid ping reply")
self.assertFalse(ok, "device zero replied") self.assertFalse(ok, "device zero replied")
@ -51,7 +52,8 @@ class Test_UR_API(unittest.TestCase):
devices = [] devices = []
for devnumber in range(1, 1 + MAX_ATTACHED_DEVICES): for devnumber in range(1, 1 + MAX_ATTACHED_DEVICES):
ok = api.ping(self.receiver.handle, devnumber) d = api.PairedDevice(self.receiver.handle, devnumber)
ok = d.ping()
self.assertIsNotNone(ok, "invalid ping reply") self.assertIsNotNone(ok, "invalid ping reply")
if ok: if ok:
devices.append(self.receiver[devnumber]) devices.append(self.receiver[devnumber])