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':
_l.debug("receiver gave up")
self.success = False
# self.success = True
# self.detected_device = self.listener.receiver.devices[1]
return True
return False
@ -78,5 +80,4 @@ class State(object):
return True
def unpair(self, device):
_l.debug("unpair %s", device)
self.listener.unpair_device(device)
return self.listener.unpair_device(device)

View File

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

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python
NAME = 'Solaar'
VERSION = '0.7.2'
VERSION = '0.7.3'
__author__ = "Daniel Pavel <daniel.pavel@gmail.com>"
__version__ = VERSION
__license__ = "GPL"
@ -13,6 +13,9 @@ __license__ = "GPL"
def _parse_arguments():
import argparse
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',
action='count', default=0,
help='increase the logger verbosity (may be repeated)')
@ -30,9 +33,13 @@ def _parse_arguments():
args = arg_parser.parse_args()
import logging
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)
if args.quiet:
logging.root.addHandler(logging.NullHandler())
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
@ -81,14 +88,18 @@ if __name__ == '__main__':
window.present()
import pairing
from logitech.devices.constants import STATUS
from gi.repository import Gtk, GObject
listener = None
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)
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)
global listener
@ -98,27 +109,34 @@ if __name__ == '__main__':
from receiver import ReceiverListener
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:
try:
listener = ReceiverListener.open(status_changed)
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:
pairing.state = None
if notify_missing:
status_changed(DUMMY, None, True)
status_changed(DUMMY, None, STATUS.UI_NOTIFY)
notify_missing = False
return retry
# print ("opened receiver", listener, listener.receiver)
notify_missing = True
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()
if listener is not None:

View File

@ -6,8 +6,8 @@ from gi.repository import (GObject, Gtk)
GObject.threads_init()
from solaar import NAME as _NAME
_APP_ICONS = (_NAME + '-fail', _NAME + '-init', _NAME)
from solaar import NAME
_APP_ICONS = (NAME + '-fail', NAME + '-init', NAME)
def appicon(receiver_status):
return (_APP_ICONS[0] if receiver_status < 0 else
_APP_ICONS[1] if receiver_status < 1 else
@ -25,12 +25,9 @@ def icon_file(name):
return None
def show_permissions_warning(window):
text = ('Found a possible Unifying Receiver device,\n'
'but did not have permission to open it.')
def error(window, title, 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.destroy()

View File

@ -3,10 +3,9 @@
#
# from sys import version as PYTTHON_VERSION
from gi.repository import Gtk
from gi.repository import (Gtk, Gdk)
import ui.notify
import ui.pair_window
import ui
from solaar import NAME as _NAME
from solaar import VERSION as _VERSION
@ -45,7 +44,8 @@ def _show_about_window(action):
about.set_logo_icon_name(_NAME)
about.set_version(_VERSION)
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_label('Solaar Wiki')
# about.set_comments('Using Python %s\n' % PYTTHON_VERSION.split(' ')[0])
@ -64,11 +64,12 @@ import pairing
def _pair_device(action, frame):
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_modal(True)
window.present()
pair_dialog.set_type_hint(Gdk.WindowTypeHint.DIALOG)
pair_dialog.present()
def pair(frame):
@ -77,15 +78,19 @@ def pair(frame):
def _unpair_device(action, frame):
window = frame.get_toplevel()
window.present()
# window.present()
device = frame._device
qdialog = Gtk.MessageDialog(window, 0,
Gtk.MessageType.QUESTION, Gtk.ButtonsType.YES_NO,
Gtk.MessageType.QUESTION, Gtk.ButtonsType.NONE,
"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()
qdialog.destroy()
if choice == Gtk.ResponseType.YES:
pairing.state.unpair(device)
if choice == Gtk.ResponseType.ACCEPT:
if not pairing.state.unpair(device):
ui.error(window, 'Unpairing failed', 'Failed to unpair device\n%s .' % device.name)
def unpair(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
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):
frame = Gtk.Frame()
frame._device = None
@ -66,11 +50,12 @@ def _make_receiver_box(name):
info_box.add(info_label)
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(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.pack_start(hbox, 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_padding(8, 2)
info_label.set_selectable(True)
info_label.fields = {}
info_box = Gtk.Frame()
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(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):
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):
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)
icon_name = ui.get_icon(dev.name, dev.kind)
icon.set_from_icon_name(icon_name, _DEVICE_ICON_SIZE)
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_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_level = dev.props.get(PROPS.BATTERY_LEVEL)
if battery_level is None:
battery_icon.set_from_icon_name('battery_unknown', _STATUS_ICON_SIZE)
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:
icon_name = 'battery_%03d' % (20 * ((battery_level + 10) // 20))
battery_icon.set_from_icon_name(icon_name, _STATUS_ICON_SIZE)
battery_icon.set_sensitive(True)
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_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_visible(True)
for b in toolbar.get_children()[:-1]:
b.set_sensitive(True)
frame.set_visible(True)
if first_run:
frame.set_visible(True)
GObject.timeout_add(2000, _update_device_info_label, info_label, dev)
def update(window, receiver, device=None):

View File

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

View File

@ -50,6 +50,6 @@ def update(icon, receiver):
lines.append('')
text = '\n'.join(lines).rstrip('\n')
icon.set_tooltip_markup(text)
icon.set_tooltip_markup(ui.NAME + ':\n' + text)
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"
__version__ = "0.4"
try:
from hidapi.udev import *
except ImportError:
from hidapi.native import *
from hidapi.udev import *

View File

@ -55,7 +55,8 @@ if __name__ == '__main__':
print (".. Opening device %s" % args.device)
handle = hidapi.open_path(args.device.encode('utf-8'))
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_product(handle)),
repr(hidapi.get_serial(handle))))
@ -101,7 +102,7 @@ if __name__ == '__main__':
except Exception as e:
print ('%s: %s' % (type(e).__name__, e))
print (".. Closing handle %X" % handle)
print (".. Closing handle %s" % repr(handle))
hidapi.close(handle)
if interactive:
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.
"""
#
# LEGACY, no longer supported
#
__version__ = '0.3-hidapi-0.7.0'

View File

@ -8,9 +8,10 @@ necessary.
"""
import os as _os
import errno as _errno
from select import select as _select
from pyudev import Context as _Context
from pyudev import Device as _Device
from pyudev import (Context as _Context,
Device as _Device)
native_implementation = 'udev'
@ -124,6 +125,7 @@ def open_path(device_path):
:returns: an opaque device handle, or ``None``.
"""
assert '/dev/hidraw' in device_path
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
one exists. If it does not, it will send the data through
the Control Endpoint (Endpoint 0).
:returns: ``True`` if the write was successful.
"""
try:
bytes_written = _os.write(device_handle, data)
return bytes_written == len(data)
except:
pass
bytes_written = _os.write(device_handle, data)
if bytes_written != len(data):
raise OSError(errno=_errno.EIO, strerror='written %d bytes out of expected %d' % (bytes_written, len(data)))
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
reached, or None if there was an error while reading.
"""
try:
timeout = None if timeout_ms < 0 else timeout_ms / 1000.0
rlist, wlist, xlist = _select([device_handle], [], [], timeout)
if rlist:
assert rlist == [device_handle]
return _os.read(device_handle, bytes_count)
timeout = None if timeout_ms < 0 else timeout_ms / 1000.0
rlist, wlist, xlist = _select([device_handle], [], [device_handle], timeout)
if xlist:
assert xlist == [device_handle]
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''
except OSError:
pass
_DEVICE_STRINGS = {

View File

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

View File

@ -4,8 +4,10 @@
STATUS = type('STATUS', (),
dict(
UNKNOWN=-9999,
UNPAIRED=-1000,
UI_NOTIFY=0x01,
UI_POPUP=0x02,
UNKNOWN=-0xFFFF,
UNPAIRED=-0x1000,
UNAVAILABLE=-1,
BOOTING=0,
CONNECTED=1,
@ -26,6 +28,7 @@ PROPS = type('PROPS', (),
BATTERY_LEVEL='battery_level',
BATTERY_STATUS='battery_status',
LIGHT_LEVEL='light_level',
UI_FLAGS='ui_flags',
))
# 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':
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)
for f in receiver.firmware:
print (" %-10s: %s" % (f.kind, f.version))
print (" Receiver reported %d paired device(s)" % len(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 (str(dev))
print ("Codename : %s" % dev.codename)
print ("Name : %s" % dev.name)
print ("Kind : %s" % dev.kind)
print ("Serial number: %s" % dev.serial)
if not dev.protocol:
print ("HID protocol : UNKNOWN")
print ("Device is not connected at this time, no further info available.")
continue
print ("HID protocol : HID %01.1f" % dev.protocol)
@ -27,7 +36,7 @@ def scan_devices(receiver):
firmware = dev.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)
for index in range(0, len(all_features)):
@ -45,9 +54,7 @@ def scan_devices(receiver):
print (" %d reprogrammable keys found" % len(keys))
for k in keys:
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 ("--------")
print (" %2d: %-12s => %-12s : %s" % (k.index, KEY_NAME[k.id], KEY_NAME[k.task], flags))
if __name__ == '__main__':
@ -65,9 +72,8 @@ if __name__ == '__main__':
receiver = api.Receiver.open()
if receiver is None:
print ("!! Logitech Unifying Receiver not found.")
print ("Logitech Unifying Receiver not found.")
else:
print ("!! Found Logitech Unifying Receiver: %s" % receiver)
print_receiver(receiver)
scan_devices(receiver)
receiver.close()

View File

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

View File

@ -3,6 +3,8 @@
# 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 binascii import hexlify as _hexlify
_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 = 1500
#
#
#
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
DEFAULT_TIMEOUT = 2000
#
#
@ -76,13 +53,10 @@ def list_receiver_devices():
# (Vendor ID, Product ID) = ('Logitech', 'Unifying Receiver')
# interface 2 if the actual receiver interface
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
_COUNT_DEVICES_REQUEST = b'\x10\xFF\x81\x00\x00\x00\x00'
def try_open(path):
def open_path(path):
"""Checks if the given Linux device path points to the right UR device.
: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
``None``.
"""
receiver_handle = _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)
return _hid.open_path(path)
def open():
@ -125,8 +78,7 @@ def open():
:returns: An open file handle for the found receiver, or ``None``.
"""
for rawdevice in list_receiver_devices():
_log.info("checking %s", rawdevice)
handle = try_open(rawdevice.path)
handle = open_path(rawdevice.path)
if handle:
return handle
@ -135,11 +87,15 @@ def close(handle):
"""Closes a HID device handle."""
if handle:
try:
_hid.close(handle)
# _log.info("closed receiver handle %X", handle)
if type(handle) == int:
_hid.close(handle)
else:
handle.close()
# _log.info("closed receiver handle %s", repr(handle))
return True
except:
_log.exception("closing receiver handle %X", handle)
# _log.exception("closing receiver handle %s", repr(handle))
pass
return False
@ -162,11 +118,14 @@ def write(handle, devnumber, data):
assert _MAX_CALL_SIZE == 20
# the data is padded to either 5 or 18 bytes
wdata = _pack('!BB18s' if len(data) > 5 else '!BB5s', 0x10, devnumber, data)
_log.debug("<= w[10 %02X %s %s]", 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)
_log.debug("(%s) <= w[10 %02X %s %s]", handle, devnumber, _hex(wdata[2:4]), _hex(wdata[4:]))
try:
_hid.write(int(handle), wdata)
except Exception as reason:
_log.error("write failed, assuming handle %s no longer available", repr(handle))
close(handle)
raise _NoReceiver
raise _NoReceiver(reason)
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
unloaded. The handle will be closed automatically.
"""
data = _hid.read(handle, _MAX_REPLY_SIZE, timeout)
if data is None:
_log.warn("read failed, assuming receiver %X no longer available", handle)
try:
data = _hid.read(int(handle), _MAX_REPLY_SIZE, timeout)
except Exception as reason:
_log.error("read failed, assuming handle %s no longer available", repr(handle))
close(handle)
raise _NoReceiver
raise _NoReceiver(reason)
if data:
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))
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])
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:]
# _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):
"""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:
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:
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:
context = _DEFAULT_REQUEST_CONTEXT
_unhandled = unhandled_hook
else:
context = request_context
_unhandled = getattr(context, 'unhandled_hook')
_skip_incoming(handle)
ihandle = int(handle)
write(ihandle, devnumber, feature_index_function + params)
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
while read_times > 0:
divisor = (1 + _MAX_READ_TIMES - read_times)
reply = context.read(handle, int(DEFAULT_TIMEOUT * (divisor + 1) / 2 / divisor))
read_times -= 1
if reply:
reply_code, reply_devnumber, reply_data = reply
if reply_devnumber == devnumber:
if reply_code == 0x10 and reply_data[:1] == b'\x8F' and reply_data[1:3] == feature_index_function:
# 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:
# keep waiting...
continue
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
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:
# this message not for the device we're interested in
# _l.log(_LOG_LEVEL, "device %d request got reply for unexpected device %d: [%s]", devnumber, reply_devnumber, _hex(reply_data))
# worst case scenario, this is a reply for a concurrent request
# on this receiver
if _unhandled:
_unhandled(reply_code, reply_devnumber, reply_data)
continue
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 reply_data[:1] == b'\x8F' and reply_data[1:3] == feature_index_function:
# device not present
_log.debug("device %d request failed on {%s} call: [%s]", devnumber, _hex(feature_index_function), _hex(reply_data))
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:]
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
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):
"""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.
"""
if request_context is None or handle != request_context.handle:
context = _DEFAULT_REQUEST_CONTEXT
_unhandled = unhandled_hook
else:
context = request_context
_unhandled = getattr(context, 'unhandled_hook')
_log.debug("%s pinging device %d", handle, devnumber)
context.write(handle, devnumber, b'\x00\x11\x00\x00\xAA')
read_times = _MAX_READ_TIMES
while read_times > 0:
divisor = (1 + _MAX_READ_TIMES - read_times)
reply = context.read(handle, int(DEFAULT_TIMEOUT * (divisor + 1) / 2 / divisor))
read_times -= 1
_skip_incoming(handle)
ihandle = int(handle)
write(ihandle, devnumber, b'\x00\x11\x00\x00\xAA')
if not reply:
# keep waiting...
continue
while True:
now = _timestamp()
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:
# this message not for the device we're interested in
# _l.log(_LOG_LEVEL, "device %d request got reply for unexpected device %d: [%s]", devnumber, reply_devnumber, _hex(reply_data))
# worst case scenario, this is a reply for a concurrent request
# on this receiver
if _unhandled:
_unhandled(reply_code, reply_devnumber, reply_data)
continue
if reply_code == 0x10 and reply_data == b'\x8F\x00\x11\x01\x00':
# HID 1.0 device, currently connected
return 1.0
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_code == 0x10 and reply_data[:3] == b'\x8F\x00\x11':
# a disconnected device
return None
if reply_code == 0x10 and reply_data == b'\x8F\x00\x11\x01\x00':
# HID 1.0 device, currently connected
return 1.0
if unhandled_hook:
unhandled_hook(reply_code, reply_devnumber, reply_data)
if reply_code == 0x10 and reply_data[:3] == b'\x8F\x00\x11':
# a disconnected device
if delta >= DEFAULT_TIMEOUT:
_log.warn("timeout on device %d ping", devnumber)
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',
'Full', 'Slow recharge', 'Invalid battery', 'Thermal error',
'Charging error')
BATTERY_OK = lambda status: status < 5
"""Names for possible battery status values."""
BATTERY_STATUS = FallbackDict(lambda x: 'unknown', list2dict(_BATTERY_STATUSES))

View File

@ -2,12 +2,12 @@
#
#
from threading import Thread as _Thread
# from time import sleep as _sleep
import threading as _threading
from . import base as _base
from .exceptions import NoReceiver as _NoReceiver
from .common import Packet as _Packet
from .constants import MAX_ATTACHED_DEVICES as _MAX_ATTACHED_DEVICES
# for both Python 2 and 3
try:
@ -21,26 +21,10 @@ _log = getLogger('LUR').getChild('listener')
del getLogger
_READ_EVENT_TIMEOUT = int(_base.DEFAULT_TIMEOUT / 2) # ms
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):
class EventsListener(_threading.Thread):
"""Listener thread for events from the Unifying Receiver.
Incoming packets will be passed to the callback function in sequence, by a
separate thread.
Incoming packets will be passed to the callback function in sequence.
"""
def __init__(self, receiver_handle, events_callback):
super(EventsListener, self).__init__(group='Unifying Receiver', name=self.__class__.__name__)
@ -49,92 +33,65 @@ class EventsListener(_Thread):
self._active = False
self._handle = receiver_handle
self._tasks = _Queue(1)
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
self._queued_events = _Queue(32)
self._events_callback = events_callback
def run(self):
self._active = True
_log.debug("started")
_base.request_context = self
_base.unhandled_hook = self._backup_unhandled_hook
del self._backup_unhandled_hook
self._dispatcher.start()
_base.unhandled_hook = self._unhandled_hook
ihandle = int(self._handle)
_log.info("started with %s (%d)", repr(self._handle), ihandle)
while self._active:
try:
# _log.debug("read next event")
event = _base.read(self._handle, _READ_EVENT_TIMEOUT)
except _NoReceiver:
self._handle = 0
_log.warn("receiver disconnected")
self._events.put(_Packet(0xFF, 0xFF, None))
self._active = False
if self._queued_events.empty():
try:
# _log.debug("read next event")
event = _base.read(ihandle)
# shortcut: we should only be looking at events for proper device numbers
except _NoReceiver:
self._active = False
self._handle = None
_log.warning("receiver disconnected")
event = (0xFF, 0xFF, None)
else:
if event is not None:
matched = False
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])
# deliver any queued events
event = self._queued_events.get()
if matched:
# _log.debug("request reply %s", event)
task[-1] = event
self._tasks.task_done()
else:
event = _Packet(*event)
_log.info("queueing event %s", event)
self._events.put(event)
if event:
event = _Packet(*event)
# _log.debug("processing event %s", event)
try:
self._events_callback(event)
except:
_log.exception("processing event %s", event)
_base.request_context = None
handle, self._handle = self._handle, 0
_base.close(handle)
_log.debug("stopped")
_base.unhandled_hook = None
handle, self._handle = self._handle, None
if handle:
_base.close(handle)
_log.info("stopped %s", repr(handle))
def stop(self):
"""Tells the listener to stop as soon as possible."""
if self._active:
_log.debug("stopping")
self._active = False
# wait for the receiver handle to be closed
self.join()
handle, self._handle = self._handle, None
if handle:
_base.close(handle)
_log.info("stopped %s", repr(handle))
@property
def handle(self):
return self._handle
def write(self, handle, devnumber, data):
assert handle == self._handle
# _log.debug("write %02X %s", devnumber, _base._hex(data))
task = [devnumber, data, None]
self._tasks.put(task)
_base.write(self._handle, devnumber, data)
# _log.debug("task queued %s", task)
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 _unhandled_hook(self, reply_code, devnumber, data):
# only consider unhandled events that were sent from this thread,
# i.e. triggered during a callback of a previous event
if _threading.current_thread() == self:
event = _Packet(reply_code, devnumber, data)
_log.info("queueing unhandled event %s", event)
self._queued_events.put(event)
def __bool__(self):
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")
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:
self.fail("No receiver found")
for rawdevice in base.list_receiver_devices():
handle = base.try_open(rawdevice.path)
handle = base.open_path(rawdevice.path)
if handle is None:
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:
Test_UR_Base.handle = handle
@ -131,46 +131,46 @@ class Test_UR_Base(unittest.TestCase):
index = reply[:1]
self.assertGreater(index, b'\x00', "FEATURE_SET not available on device " + str(self.device))
def test_57_request_ignore_undhandled(self):
if self.handle is None:
self.fail("No receiver found")
if self.device is None:
self.fail("No devices attached")
# def test_57_request_ignore_undhandled(self):
# if self.handle is None:
# self.fail("No receiver found")
# if self.device is None:
# self.fail("No devices attached")
fs_index = base.request(self.handle, self.device, FEATURE.ROOT, FEATURE.FEATURE_SET)
self.assertIsNotNone(fs_index)
fs_index = fs_index[:1]
self.assertGreater(fs_index, b'\x00')
# fs_index = base.request(self.handle, self.device, FEATURE.ROOT, FEATURE.FEATURE_SET)
# self.assertIsNotNone(fs_index)
# fs_index = fs_index[:1]
# self.assertGreater(fs_index, b'\x00')
global received_unhandled
received_unhandled = None
# global received_unhandled
# received_unhandled = None
def _unhandled(code, device, data):
self.assertIsNotNone(code)
self.assertIsInstance(code, int)
self.assertIsNotNone(device)
self.assertIsInstance(device, int)
self.assertIsNotNone(data)
self.assertIsInstance(data, str)
global received_unhandled
received_unhandled = (code, device, data)
# def _unhandled(code, device, data):
# self.assertIsNotNone(code)
# self.assertIsInstance(code, int)
# self.assertIsNotNone(device)
# self.assertIsInstance(device, int)
# self.assertIsNotNone(data)
# self.assertIsInstance(data, str)
# global received_unhandled
# received_unhandled = (code, device, data)
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")
self.assertNotEquals(reply[:1], b'\x00')
self.assertIsNotNone(received_unhandled, "extra message not received by unhandled hook")
# 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")
# self.assertNotEquals(reply[:1], b'\x00')
# self.assertIsNotNone(received_unhandled, "extra message not received by unhandled hook")
received_unhandled = 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")
self.assertNotEquals(reply[:1], b'\x00')
self.assertIsNone(received_unhandled)
# received_unhandled = 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")
# self.assertNotEquals(reply[:1], b'\x00')
# self.assertIsNone(received_unhandled)
del received_unhandled
# del received_unhandled
# def test_90_receiver_missing(self):
# if self.handle is None:

View File

@ -41,7 +41,8 @@ class Test_UR_API(unittest.TestCase):
def test_05_ping_device_zero(self):
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.assertFalse(ok, "device zero replied")
@ -51,7 +52,8 @@ class Test_UR_API(unittest.TestCase):
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")
if ok:
devices.append(self.receiver[devnumber])