re-worked how fd handles are used in multi-threading
This commit is contained in:
parent
d0ccd3e9c2
commit
50fedab19e
|
@ -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)
|
||||
|
|
125
app/receiver.py
125
app/receiver.py
|
@ -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
|
||||
|
||||
#
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 *
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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])
|
||||
|
|
Loading…
Reference in New Issue