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':
|
if event.data == b'\x4A\x00\x01\x00\x00':
|
||||||
_l.debug("receiver gave up")
|
_l.debug("receiver gave up")
|
||||||
self.success = False
|
self.success = False
|
||||||
|
# self.success = True
|
||||||
|
# self.detected_device = self.listener.receiver.devices[1]
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@ -78,5 +80,4 @@ class State(object):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def unpair(self, device):
|
def unpair(self, device):
|
||||||
_l.debug("unpair %s", device)
|
return self.listener.unpair_device(device)
|
||||||
self.listener.unpair_device(device)
|
|
||||||
|
|
125
app/receiver.py
125
app/receiver.py
|
@ -4,7 +4,6 @@
|
||||||
|
|
||||||
from logging import getLogger as _Logger
|
from logging import getLogger as _Logger
|
||||||
from struct import pack as _pack
|
from struct import pack as _pack
|
||||||
from time import sleep as _sleep
|
|
||||||
|
|
||||||
from logitech.unifying_receiver import base as _base
|
from logitech.unifying_receiver import base as _base
|
||||||
from logitech.unifying_receiver import api as _api
|
from logitech.unifying_receiver import api as _api
|
||||||
|
@ -24,14 +23,22 @@ class _FeaturesArray(object):
|
||||||
self.device = device
|
self.device = device
|
||||||
self.features = None
|
self.features = None
|
||||||
self.supported = True
|
self.supported = True
|
||||||
|
self._check()
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
self.supported = False
|
||||||
|
self.device = None
|
||||||
|
|
||||||
def _check(self):
|
def _check(self):
|
||||||
if self.supported:
|
if self.supported:
|
||||||
if self.features is not None:
|
if self.features is not None:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
if self.device.protocol < 2.0:
|
||||||
|
return False
|
||||||
|
|
||||||
if self.device.status >= STATUS.CONNECTED:
|
if self.device.status >= STATUS.CONNECTED:
|
||||||
handle = self.device.handle
|
handle = int(self.device.handle)
|
||||||
try:
|
try:
|
||||||
index = _api.get_feature_index(handle, self.device.number, _api.FEATURE.FEATURE_SET)
|
index = _api.get_feature_index(handle, self.device.number, _api.FEATURE.FEATURE_SET)
|
||||||
except _api._FeatureNotSupported:
|
except _api._FeatureNotSupported:
|
||||||
|
@ -57,9 +64,13 @@ class _FeaturesArray(object):
|
||||||
|
|
||||||
if index < 0 or index >= len(self.features):
|
if index < 0 or index >= len(self.features):
|
||||||
raise IndexError
|
raise IndexError
|
||||||
|
|
||||||
if self.features[index] is None:
|
if self.features[index] is None:
|
||||||
|
# print "features getitem at %d" % index
|
||||||
fs_index = self.features.index(_api.FEATURE.FEATURE_SET)
|
fs_index = self.features.index(_api.FEATURE.FEATURE_SET)
|
||||||
feature = _base.request(self.device.handle, self.device.number, _pack('!BB', fs_index, 0x10), _pack('!B', index))
|
# technically fs_function is 0x10 for this call, but we add the index to differentiate possibly conflicting requests
|
||||||
|
fs_function = 0x10 | (index & 0x0F)
|
||||||
|
feature = _base.request(self.device.handle, self.device.number, _pack('!BB', fs_index, fs_function), _pack('!B', index))
|
||||||
if feature is not None:
|
if feature is not None:
|
||||||
self.features[index] = feature[:2]
|
self.features[index] = feature[:2]
|
||||||
|
|
||||||
|
@ -70,11 +81,13 @@ class _FeaturesArray(object):
|
||||||
if value in self.features:
|
if value in self.features:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
# print "features contains %s" % repr(value)
|
||||||
for index in range(0, len(self.features)):
|
for index in range(0, len(self.features)):
|
||||||
f = self.features[index] or self.__getitem__(index)
|
f = self.features[index] or self.__getitem__(index)
|
||||||
assert f is not None
|
assert f is not None
|
||||||
if f == value:
|
if f == value:
|
||||||
return True
|
return True
|
||||||
|
# we know the features are ordered by value
|
||||||
if f > value:
|
if f > value:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
@ -105,23 +118,19 @@ class _FeaturesArray(object):
|
||||||
class DeviceInfo(_api.PairedDevice):
|
class DeviceInfo(_api.PairedDevice):
|
||||||
"""A device attached to the receiver.
|
"""A device attached to the receiver.
|
||||||
"""
|
"""
|
||||||
def __init__(self, listener, number, status=STATUS.UNKNOWN):
|
def __init__(self, handle, number, status=STATUS.UNKNOWN, status_changed_callback=None):
|
||||||
super(DeviceInfo, self).__init__(listener.handle, number)
|
super(DeviceInfo, self).__init__(handle, number)
|
||||||
self._features = _FeaturesArray(self)
|
self.LOG = _Logger("Device[%d]" % (number))
|
||||||
|
|
||||||
self.LOG = _Logger("Device[%d]" % number)
|
|
||||||
self._listener = listener
|
|
||||||
|
|
||||||
|
self.status_changed_callback = status_changed_callback
|
||||||
self._status = status
|
self._status = status
|
||||||
self.props = {}
|
self.props = {}
|
||||||
|
|
||||||
# read them now, otherwise it it temporarily hang the UI
|
self._features = _FeaturesArray(self)
|
||||||
# if status >= STATUS.CONNECTED:
|
|
||||||
# n, k, s, f = self.name, self.kind, self.serial, self.firmware
|
|
||||||
|
|
||||||
@property
|
def __del__(self):
|
||||||
def receiver(self):
|
super(ReceiverListener, self).__del__()
|
||||||
return self._listener.receiver
|
self._features.supported = False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def status(self):
|
def status(self):
|
||||||
|
@ -129,14 +138,18 @@ class DeviceInfo(_api.PairedDevice):
|
||||||
|
|
||||||
@status.setter
|
@status.setter
|
||||||
def status(self, new_status):
|
def status(self, new_status):
|
||||||
if new_status != self._status and not (new_status == STATUS.CONNECTED and self._status > new_status):
|
|
||||||
self.LOG.debug("status %d => %d", self._status, new_status)
|
|
||||||
urgent = new_status < STATUS.CONNECTED or self._status < STATUS.CONNECTED
|
|
||||||
self._status = new_status
|
|
||||||
self._listener.status_changed(self, urgent)
|
|
||||||
|
|
||||||
if new_status < STATUS.CONNECTED:
|
if new_status < STATUS.CONNECTED:
|
||||||
self.props.clear()
|
self.props.clear()
|
||||||
|
else:
|
||||||
|
self._features._check()
|
||||||
|
self.serial, self.codename, self.name, self.kind
|
||||||
|
|
||||||
|
if new_status != self._status and not (new_status == STATUS.CONNECTED and self._status > new_status):
|
||||||
|
self.LOG.debug("status %d => %d", self._status, new_status)
|
||||||
|
self._status = new_status
|
||||||
|
if self.status_changed_callback:
|
||||||
|
ui_flags = STATUS.UI_NOTIFY if new_status == STATUS.UNPAIRED else 0
|
||||||
|
self.status_changed_callback(self, ui_flags)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def status_text(self):
|
def status_text(self):
|
||||||
|
@ -165,11 +178,12 @@ class DeviceInfo(_api.PairedDevice):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if type(status) == tuple:
|
if type(status) == tuple:
|
||||||
|
ui_flags = status[1].pop(PROPS.UI_FLAGS, 0)
|
||||||
p = dict(self.props)
|
p = dict(self.props)
|
||||||
self.props.update(status[1])
|
self.props.update(status[1])
|
||||||
if self.status == status[0]:
|
if self.status == status[0]:
|
||||||
if p != self.props:
|
if self.status_changed_callback and (ui_flags or p != self.props):
|
||||||
self._listener.status_changed(self)
|
self.status_changed_callback(self, ui_flags)
|
||||||
else:
|
else:
|
||||||
self.status = status[0]
|
self.status = status[0]
|
||||||
return True
|
return True
|
||||||
|
@ -179,7 +193,7 @@ class DeviceInfo(_api.PairedDevice):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return '<DeviceInfo(%d,%s,%d)>' % (self.number, self._name or '?', self._status)
|
return '<DeviceInfo(%s,%d,%s,%d)>' % (self.handle, self.number, self.codename or '?', self._status)
|
||||||
|
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
|
@ -201,16 +215,13 @@ _RECEIVER_STATUS_NAME = _FallbackDict(
|
||||||
class ReceiverListener(_EventsListener):
|
class ReceiverListener(_EventsListener):
|
||||||
"""Keeps the status of a Unifying Receiver.
|
"""Keeps the status of a Unifying Receiver.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, receiver, status_changed_callback=None):
|
def __init__(self, receiver, status_changed_callback=None):
|
||||||
super(ReceiverListener, self).__init__(receiver.handle, self._events_handler)
|
super(ReceiverListener, self).__init__(receiver.handle, self._events_handler)
|
||||||
|
self.LOG = _Logger("Receiver[%s]" % receiver.path)
|
||||||
|
|
||||||
self.receiver = receiver
|
self.receiver = receiver
|
||||||
|
|
||||||
self.LOG = _Logger("ReceiverListener(%s)" % receiver.path)
|
|
||||||
|
|
||||||
self.events_filter = None
|
self.events_filter = None
|
||||||
self.events_handler = None
|
self.events_handler = None
|
||||||
|
|
||||||
self.status_changed_callback = status_changed_callback
|
self.status_changed_callback = status_changed_callback
|
||||||
|
|
||||||
receiver.kind = receiver.name
|
receiver.kind = receiver.name
|
||||||
|
@ -223,23 +234,28 @@ class ReceiverListener(_EventsListener):
|
||||||
else:
|
else:
|
||||||
self.LOG.warn("initialization failed")
|
self.LOG.warn("initialization failed")
|
||||||
|
|
||||||
if _base.request(receiver.handle, 0xFF, b'\x80\x02', b'\x02'):
|
self.LOG.info("reports %d device(s) paired", len(receiver))
|
||||||
self.LOG.info("triggered device events")
|
|
||||||
else:
|
|
||||||
self.LOG.warn("failed to trigger device events")
|
|
||||||
|
|
||||||
self.LOG.info("receiver reports %d device(s) paired", len(receiver))
|
def __del__(self):
|
||||||
|
super(ReceiverListener, self).__del__()
|
||||||
|
self.receiver = None
|
||||||
|
|
||||||
|
def trigger_device_events(self):
|
||||||
|
if _base.request(int(self._handle), 0xFF, b'\x80\x02', b'\x02'):
|
||||||
|
self.LOG.info("triggered device events")
|
||||||
|
return True
|
||||||
|
self.LOG.warn("failed to trigger device events")
|
||||||
|
|
||||||
def change_status(self, new_status):
|
def change_status(self, new_status):
|
||||||
if new_status != self.receiver.status:
|
if new_status != self.receiver.status:
|
||||||
self.LOG.debug("status %d => %d", self.receiver.status, new_status)
|
self.LOG.debug("status %d => %d", self.receiver.status, new_status)
|
||||||
self.receiver.status = new_status
|
self.receiver.status = new_status
|
||||||
self.receiver.status_text = _RECEIVER_STATUS_NAME[new_status]
|
self.receiver.status_text = _RECEIVER_STATUS_NAME[new_status]
|
||||||
self.status_changed(None, True)
|
self.status_changed(None, STATUS.UI_NOTIFY)
|
||||||
|
|
||||||
def status_changed(self, device=None, urgent=False):
|
def status_changed(self, device=None, ui_flags=0):
|
||||||
if self.status_changed_callback:
|
if self.status_changed_callback:
|
||||||
self.status_changed_callback(self.receiver, device, urgent)
|
self.status_changed_callback(self.receiver, device, ui_flags)
|
||||||
|
|
||||||
def _device_status_from(self, event):
|
def _device_status_from(self, event):
|
||||||
state_code = ord(event.data[2:3]) & 0xC0
|
state_code = ord(event.data[2:3]) & 0xC0
|
||||||
|
@ -248,7 +264,7 @@ class ReceiverListener(_EventsListener):
|
||||||
STATUS.CONNECTED if state_code == 0x00 else \
|
STATUS.CONNECTED if state_code == 0x00 else \
|
||||||
None
|
None
|
||||||
if state is None:
|
if state is None:
|
||||||
self.LOG.warn("don't know how to handle state code 0x%02X: %s", state_code, event)
|
self.LOG.warn("failed to identify status of device %d from 0x%02X: %s", event.devnumber, state_code, event)
|
||||||
return state
|
return state
|
||||||
|
|
||||||
def _events_handler(self, event):
|
def _events_handler(self, event):
|
||||||
|
@ -256,26 +272,20 @@ class ReceiverListener(_EventsListener):
|
||||||
return
|
return
|
||||||
|
|
||||||
if event.code == 0x10 and event.data[0:2] == b'\x41\x04':
|
if event.code == 0x10 and event.data[0:2] == b'\x41\x04':
|
||||||
|
|
||||||
if event.devnumber in self.receiver.devices:
|
if event.devnumber in self.receiver.devices:
|
||||||
status = self._device_status_from(event)
|
status = self._device_status_from(event)
|
||||||
if status is not None:
|
if status is not None:
|
||||||
self.receiver.devices[event.devnumber].status = status
|
self.receiver.devices[event.devnumber].status = status
|
||||||
else:
|
else:
|
||||||
dev = self.make_device(event)
|
self.make_device(event)
|
||||||
if dev is None:
|
|
||||||
self.LOG.warn("failed to make new device from %s", event)
|
|
||||||
else:
|
|
||||||
self.receiver.devices[event.devnumber] = dev
|
|
||||||
self.change_status(STATUS.CONNECTED + len(self.receiver.devices))
|
|
||||||
return
|
return
|
||||||
|
|
||||||
if event.devnumber == 0xFF:
|
if event.devnumber == 0xFF:
|
||||||
if event.code == 0xFF and event.data is None:
|
if event.code == 0xFF and event.data is None:
|
||||||
# receiver disconnected
|
|
||||||
self.LOG.warn("disconnected")
|
self.LOG.warn("disconnected")
|
||||||
self.receiver.devices = {}
|
self.receiver.devices = {}
|
||||||
self.change_status(STATUS.UNAVAILABLE)
|
self.change_status(STATUS.UNAVAILABLE)
|
||||||
|
self.receiver = None
|
||||||
return
|
return
|
||||||
elif event.devnumber in self.receiver.devices:
|
elif event.devnumber in self.receiver.devices:
|
||||||
dev = self.receiver.devices[event.devnumber]
|
dev = self.receiver.devices[event.devnumber]
|
||||||
|
@ -285,7 +295,7 @@ class ReceiverListener(_EventsListener):
|
||||||
if self.events_handler and self.events_handler(event):
|
if self.events_handler and self.events_handler(event):
|
||||||
return
|
return
|
||||||
|
|
||||||
self.LOG.warn("don't know how to handle event %s", event)
|
# self.LOG.warn("don't know how to handle event %s", event)
|
||||||
|
|
||||||
def make_device(self, event):
|
def make_device(self, event):
|
||||||
if event.devnumber < 1 or event.devnumber > self.receiver.max_devices:
|
if event.devnumber < 1 or event.devnumber > self.receiver.max_devices:
|
||||||
|
@ -294,12 +304,18 @@ class ReceiverListener(_EventsListener):
|
||||||
|
|
||||||
status = self._device_status_from(event)
|
status = self._device_status_from(event)
|
||||||
if status is not None:
|
if status is not None:
|
||||||
dev = DeviceInfo(self, event.devnumber, status)
|
dev = DeviceInfo(self.handle, event.devnumber, status, self.status_changed)
|
||||||
self.LOG.info("new device %s", dev)
|
self.LOG.info("new device %s", dev)
|
||||||
self.status_changed(dev, True)
|
|
||||||
return dev
|
|
||||||
|
|
||||||
self.LOG.error("failed to identify status of device %d from %s", event.devnumber, event)
|
self.receiver.devices[event.devnumber] = dev
|
||||||
|
self.change_status(STATUS.CONNECTED + len(self.receiver.devices))
|
||||||
|
|
||||||
|
if status == STATUS.CONNECTED:
|
||||||
|
dev.protocol, dev.name, dev.kind
|
||||||
|
self.status_changed(dev, STATUS.UI_NOTIFY)
|
||||||
|
if status == STATUS.CONNECTED:
|
||||||
|
dev.serial, dev.firmware
|
||||||
|
return dev
|
||||||
|
|
||||||
def unpair_device(self, device):
|
def unpair_device(self, device):
|
||||||
try:
|
try:
|
||||||
|
@ -315,17 +331,16 @@ class ReceiverListener(_EventsListener):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return '<ReceiverListener(%s,%d)>' % (self.receiver.path, self.receiver.status)
|
return '<ReceiverListener(%s,%d,%d)>' % (self.receiver.path, int(self.handle), self.receiver.status)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def open(self, status_changed_callback=None):
|
def open(self, status_changed_callback=None):
|
||||||
receiver = _api.Receiver.open()
|
receiver = _api.Receiver.open()
|
||||||
if receiver:
|
if receiver:
|
||||||
|
handle = receiver.handle
|
||||||
|
receiver.handle = _api.ThreadedHandle(handle, receiver.path)
|
||||||
rl = ReceiverListener(receiver, status_changed_callback)
|
rl = ReceiverListener(receiver, status_changed_callback)
|
||||||
rl.start()
|
rl.start()
|
||||||
|
|
||||||
while not rl._active:
|
|
||||||
_sleep(0.1)
|
|
||||||
return rl
|
return rl
|
||||||
|
|
||||||
#
|
#
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
|
|
||||||
NAME = 'Solaar'
|
NAME = 'Solaar'
|
||||||
VERSION = '0.7.2'
|
VERSION = '0.7.3'
|
||||||
__author__ = "Daniel Pavel <daniel.pavel@gmail.com>"
|
__author__ = "Daniel Pavel <daniel.pavel@gmail.com>"
|
||||||
__version__ = VERSION
|
__version__ = VERSION
|
||||||
__license__ = "GPL"
|
__license__ = "GPL"
|
||||||
|
@ -13,6 +13,9 @@ __license__ = "GPL"
|
||||||
def _parse_arguments():
|
def _parse_arguments():
|
||||||
import argparse
|
import argparse
|
||||||
arg_parser = argparse.ArgumentParser(prog=NAME.lower())
|
arg_parser = argparse.ArgumentParser(prog=NAME.lower())
|
||||||
|
arg_parser.add_argument('-q', '--quiet',
|
||||||
|
action='store_true',
|
||||||
|
help='disable all logging, takes precedence over --verbose')
|
||||||
arg_parser.add_argument('-v', '--verbose',
|
arg_parser.add_argument('-v', '--verbose',
|
||||||
action='count', default=0,
|
action='count', default=0,
|
||||||
help='increase the logger verbosity (may be repeated)')
|
help='increase the logger verbosity (may be repeated)')
|
||||||
|
@ -30,9 +33,13 @@ def _parse_arguments():
|
||||||
args = arg_parser.parse_args()
|
args = arg_parser.parse_args()
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
log_level = logging.WARNING - 10 * args.verbose
|
if args.quiet:
|
||||||
log_format='%(asctime)s %(levelname)8s [%(threadName)s] %(name)s: %(message)s'
|
logging.root.addHandler(logging.NullHandler())
|
||||||
logging.basicConfig(level=max(log_level, logging.DEBUG), format=log_format)
|
logging.root.setLevel(logging.CRITICAL)
|
||||||
|
else:
|
||||||
|
log_level = logging.WARNING - 10 * args.verbose
|
||||||
|
log_format='%(asctime)s %(levelname)8s [%(threadName)s] %(name)s: %(message)s'
|
||||||
|
logging.basicConfig(level=max(log_level, logging.DEBUG), format=log_format)
|
||||||
|
|
||||||
return args
|
return args
|
||||||
|
|
||||||
|
@ -81,14 +88,18 @@ if __name__ == '__main__':
|
||||||
window.present()
|
window.present()
|
||||||
|
|
||||||
import pairing
|
import pairing
|
||||||
|
from logitech.devices.constants import STATUS
|
||||||
from gi.repository import Gtk, GObject
|
from gi.repository import Gtk, GObject
|
||||||
|
|
||||||
listener = None
|
listener = None
|
||||||
notify_missing = True
|
notify_missing = True
|
||||||
|
|
||||||
def status_changed(receiver, device=None, urgent=False):
|
def status_changed(receiver, device=None, ui_flags=0):
|
||||||
ui.update(receiver, icon, window, device)
|
ui.update(receiver, icon, window, device)
|
||||||
if ui.notify.available and urgent:
|
if ui_flags & STATUS.UI_POPUP:
|
||||||
|
window.present()
|
||||||
|
|
||||||
|
if ui_flags & STATUS.UI_NOTIFY and ui.notify.available:
|
||||||
GObject.idle_add(ui.notify.show, device or receiver)
|
GObject.idle_add(ui.notify.show, device or receiver)
|
||||||
|
|
||||||
global listener
|
global listener
|
||||||
|
@ -98,27 +109,34 @@ if __name__ == '__main__':
|
||||||
|
|
||||||
from receiver import ReceiverListener
|
from receiver import ReceiverListener
|
||||||
def check_for_listener(retry=True):
|
def check_for_listener(retry=True):
|
||||||
global listener, notify_missing
|
def _check_still_scanning(listener):
|
||||||
|
if listener.receiver.status == STATUS.BOOTING:
|
||||||
|
listener.change_status(STATUS.CONNECTED)
|
||||||
|
|
||||||
|
global listener, notify_missing
|
||||||
if listener is None:
|
if listener is None:
|
||||||
try:
|
try:
|
||||||
listener = ReceiverListener.open(status_changed)
|
listener = ReceiverListener.open(status_changed)
|
||||||
except OSError:
|
except OSError:
|
||||||
ui.show_permissions_warning(window)
|
ui.error(window, 'Permissions error',
|
||||||
|
'Found a possible Unifying Receiver device,\n'
|
||||||
|
'but did not have permission to open it.')
|
||||||
|
|
||||||
if listener is None:
|
if listener is None:
|
||||||
pairing.state = None
|
pairing.state = None
|
||||||
if notify_missing:
|
if notify_missing:
|
||||||
status_changed(DUMMY, None, True)
|
status_changed(DUMMY, None, STATUS.UI_NOTIFY)
|
||||||
notify_missing = False
|
notify_missing = False
|
||||||
return retry
|
return retry
|
||||||
|
|
||||||
# print ("opened receiver", listener, listener.receiver)
|
# print ("opened receiver", listener, listener.receiver)
|
||||||
notify_missing = True
|
notify_missing = True
|
||||||
pairing.state = pairing.State(listener)
|
pairing.state = pairing.State(listener)
|
||||||
status_changed(listener.receiver, None, True)
|
status_changed(listener.receiver, None, STATUS.UI_NOTIFY)
|
||||||
|
listener.trigger_device_events()
|
||||||
|
GObject.timeout_add(5 * 1000, _check_still_scanning, listener)
|
||||||
|
|
||||||
GObject.timeout_add(100, check_for_listener, False)
|
GObject.timeout_add(50, check_for_listener, False)
|
||||||
Gtk.main()
|
Gtk.main()
|
||||||
|
|
||||||
if listener is not None:
|
if listener is not None:
|
||||||
|
|
|
@ -6,8 +6,8 @@ from gi.repository import (GObject, Gtk)
|
||||||
GObject.threads_init()
|
GObject.threads_init()
|
||||||
|
|
||||||
|
|
||||||
from solaar import NAME as _NAME
|
from solaar import NAME
|
||||||
_APP_ICONS = (_NAME + '-fail', _NAME + '-init', _NAME)
|
_APP_ICONS = (NAME + '-fail', NAME + '-init', NAME)
|
||||||
def appicon(receiver_status):
|
def appicon(receiver_status):
|
||||||
return (_APP_ICONS[0] if receiver_status < 0 else
|
return (_APP_ICONS[0] if receiver_status < 0 else
|
||||||
_APP_ICONS[1] if receiver_status < 1 else
|
_APP_ICONS[1] if receiver_status < 1 else
|
||||||
|
@ -25,12 +25,9 @@ def icon_file(name):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def show_permissions_warning(window):
|
def error(window, title, text):
|
||||||
text = ('Found a possible Unifying Receiver device,\n'
|
|
||||||
'but did not have permission to open it.')
|
|
||||||
|
|
||||||
m = Gtk.MessageDialog(window, Gtk.DialogFlags.MODAL, Gtk.MessageType.ERROR, Gtk.ButtonsType.CLOSE, text)
|
m = Gtk.MessageDialog(window, Gtk.DialogFlags.MODAL, Gtk.MessageType.ERROR, Gtk.ButtonsType.CLOSE, text)
|
||||||
m.set_title('Permissions error')
|
m.set_title(title)
|
||||||
m.run()
|
m.run()
|
||||||
m.destroy()
|
m.destroy()
|
||||||
|
|
||||||
|
|
|
@ -3,10 +3,9 @@
|
||||||
#
|
#
|
||||||
|
|
||||||
# from sys import version as PYTTHON_VERSION
|
# from sys import version as PYTTHON_VERSION
|
||||||
from gi.repository import Gtk
|
from gi.repository import (Gtk, Gdk)
|
||||||
|
|
||||||
import ui.notify
|
import ui
|
||||||
import ui.pair_window
|
|
||||||
from solaar import NAME as _NAME
|
from solaar import NAME as _NAME
|
||||||
from solaar import VERSION as _VERSION
|
from solaar import VERSION as _VERSION
|
||||||
|
|
||||||
|
@ -45,7 +44,8 @@ def _show_about_window(action):
|
||||||
about.set_logo_icon_name(_NAME)
|
about.set_logo_icon_name(_NAME)
|
||||||
about.set_version(_VERSION)
|
about.set_version(_VERSION)
|
||||||
about.set_license_type(Gtk.License.GPL_2_0)
|
about.set_license_type(Gtk.License.GPL_2_0)
|
||||||
about.set_authors(('Daniel Pavel http://github.com/pwr', ))
|
about.set_authors(('Daniel Pavel http://github.com/pwr',))
|
||||||
|
# about.add_credit_section('Testing', 'Douglas Wagner')
|
||||||
about.set_website('http://github.com/pwr/Solaar/wiki')
|
about.set_website('http://github.com/pwr/Solaar/wiki')
|
||||||
about.set_website_label('Solaar Wiki')
|
about.set_website_label('Solaar Wiki')
|
||||||
# about.set_comments('Using Python %s\n' % PYTTHON_VERSION.split(' ')[0])
|
# about.set_comments('Using Python %s\n' % PYTTHON_VERSION.split(' ')[0])
|
||||||
|
@ -64,11 +64,12 @@ import pairing
|
||||||
def _pair_device(action, frame):
|
def _pair_device(action, frame):
|
||||||
window = frame.get_toplevel()
|
window = frame.get_toplevel()
|
||||||
|
|
||||||
pair_dialog = ui.pair_window.create( action, pairing.state)
|
pair_dialog = ui.pair_window.create(action, pairing.state)
|
||||||
|
# window.present()
|
||||||
|
|
||||||
pair_dialog.set_transient_for(window)
|
pair_dialog.set_transient_for(window)
|
||||||
pair_dialog.set_modal(True)
|
pair_dialog.set_modal(True)
|
||||||
|
pair_dialog.set_type_hint(Gdk.WindowTypeHint.DIALOG)
|
||||||
window.present()
|
|
||||||
pair_dialog.present()
|
pair_dialog.present()
|
||||||
|
|
||||||
def pair(frame):
|
def pair(frame):
|
||||||
|
@ -77,15 +78,19 @@ def pair(frame):
|
||||||
|
|
||||||
def _unpair_device(action, frame):
|
def _unpair_device(action, frame):
|
||||||
window = frame.get_toplevel()
|
window = frame.get_toplevel()
|
||||||
window.present()
|
# window.present()
|
||||||
device = frame._device
|
device = frame._device
|
||||||
qdialog = Gtk.MessageDialog(window, 0,
|
qdialog = Gtk.MessageDialog(window, 0,
|
||||||
Gtk.MessageType.QUESTION, Gtk.ButtonsType.YES_NO,
|
Gtk.MessageType.QUESTION, Gtk.ButtonsType.NONE,
|
||||||
"Unpair device\n%s ?" % device.name)
|
"Unpair device\n%s ?" % device.name)
|
||||||
|
qdialog.set_icon_name('remove')
|
||||||
|
qdialog.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)
|
||||||
|
qdialog.add_button('Unpair', Gtk.ResponseType.ACCEPT)
|
||||||
choice = qdialog.run()
|
choice = qdialog.run()
|
||||||
qdialog.destroy()
|
qdialog.destroy()
|
||||||
if choice == Gtk.ResponseType.YES:
|
if choice == Gtk.ResponseType.ACCEPT:
|
||||||
pairing.state.unpair(device)
|
if not pairing.state.unpair(device):
|
||||||
|
ui.error(window, 'Unpairing failed', 'Failed to unpair device\n%s .' % device.name)
|
||||||
|
|
||||||
def unpair(frame):
|
def unpair(frame):
|
||||||
return _action('remove', 'Unpair', _unpair_device, frame)
|
return _action('remove', 'Unpair', _unpair_device, frame)
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
|
|
||||||
from gi.repository import (Gtk, Gdk)
|
from gi.repository import (Gtk, Gdk, GObject)
|
||||||
|
|
||||||
import ui
|
import ui
|
||||||
from logitech.devices.constants import (STATUS, PROPS)
|
from logitech.devices.constants import (STATUS, PROPS)
|
||||||
|
@ -17,22 +17,6 @@ _PLACEHOLDER = '~'
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
|
|
||||||
def _info_text(dev):
|
|
||||||
items = [('Serial', dev.serial)] + [(f.kind, ((f.name + ' ') if f.name else '') + f.version) for f in dev.firmware]
|
|
||||||
if hasattr(dev, 'number'):
|
|
||||||
items += [('HID', dev.protocol)]
|
|
||||||
|
|
||||||
return '<small><tt>%s</tt></small>' % '\n'.join('%-10s: %s' % (item[0], str(item[1])) for item in items)
|
|
||||||
|
|
||||||
def _toggle_info(action, label_widget, box_widget, frame):
|
|
||||||
if action.get_active():
|
|
||||||
box_widget.set_visible(True)
|
|
||||||
if not label_widget.get_text():
|
|
||||||
label_widget.set_markup(_info_text(frame._device))
|
|
||||||
else:
|
|
||||||
box_widget.set_visible(False)
|
|
||||||
|
|
||||||
|
|
||||||
def _make_receiver_box(name):
|
def _make_receiver_box(name):
|
||||||
frame = Gtk.Frame()
|
frame = Gtk.Frame()
|
||||||
frame._device = None
|
frame._device = None
|
||||||
|
@ -66,11 +50,12 @@ def _make_receiver_box(name):
|
||||||
info_box.add(info_label)
|
info_box.add(info_label)
|
||||||
info_box.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
|
info_box.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
|
||||||
|
|
||||||
toggle_info_action = ui.action._toggle_action('info', 'Receiver info', _toggle_info, info_label, info_box, frame)
|
toggle_info_action = ui.action._toggle_action('info', 'Receiver info', _toggle_info_box, info_label, info_box, frame, _update_receiver_info_label)
|
||||||
toolbar.insert(toggle_info_action.create_tool_item(), 0)
|
toolbar.insert(toggle_info_action.create_tool_item(), 0)
|
||||||
toolbar.insert(ui.action.pair(frame).create_tool_item(), -1)
|
toolbar.insert(ui.action.pair(frame).create_tool_item(), -1)
|
||||||
|
# toolbar.insert(ui.action.about.create_tool_item(), -1)
|
||||||
|
|
||||||
vbox = Gtk.VBox(homogeneous=False, spacing=2)
|
vbox = Gtk.VBox(homogeneous=False, spacing=4)
|
||||||
vbox.set_border_width(4)
|
vbox.set_border_width(4)
|
||||||
vbox.pack_start(hbox, True, True, 0)
|
vbox.pack_start(hbox, True, True, 0)
|
||||||
vbox.pack_start(info_box, True, True, 0)
|
vbox.pack_start(info_box, True, True, 0)
|
||||||
|
@ -127,11 +112,12 @@ def _make_device_box(index):
|
||||||
info_label.set_alignment(0, 0.5)
|
info_label.set_alignment(0, 0.5)
|
||||||
info_label.set_padding(8, 2)
|
info_label.set_padding(8, 2)
|
||||||
info_label.set_selectable(True)
|
info_label.set_selectable(True)
|
||||||
|
info_label.fields = {}
|
||||||
|
|
||||||
info_box = Gtk.Frame()
|
info_box = Gtk.Frame()
|
||||||
info_box.add(info_label)
|
info_box.add(info_label)
|
||||||
|
|
||||||
toggle_info_action = ui.action._toggle_action('info', 'Device info', _toggle_info, info_label, info_box, frame)
|
toggle_info_action = ui.action._toggle_action('info', 'Device info', _toggle_info_box, info_label, info_box, frame, _update_device_info_label)
|
||||||
toolbar.insert(toggle_info_action.create_tool_item(), 0)
|
toolbar.insert(toggle_info_action.create_tool_item(), 0)
|
||||||
toolbar.insert(ui.action.unpair(frame).create_tool_item(), -1)
|
toolbar.insert(ui.action.unpair(frame).create_tool_item(), -1)
|
||||||
|
|
||||||
|
@ -204,6 +190,57 @@ def create(title, name, max_devices, systray=False):
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
|
|
||||||
|
def _update_device_info_label(label, dev):
|
||||||
|
need_update = False
|
||||||
|
|
||||||
|
if 'serial' in label.fields:
|
||||||
|
serial = label.fields['serial']
|
||||||
|
else:
|
||||||
|
serial = label.fields['serial'] = dev.serial
|
||||||
|
need_update = True
|
||||||
|
|
||||||
|
if 'firmware' in label.fields:
|
||||||
|
firmware = label.fields['firmware']
|
||||||
|
else:
|
||||||
|
if dev.status >= STATUS.CONNECTED:
|
||||||
|
firmware = label.fields['firmware'] = dev.firmware
|
||||||
|
need_update = True
|
||||||
|
else:
|
||||||
|
firmware = None
|
||||||
|
|
||||||
|
if 'hid' in label.fields:
|
||||||
|
hid = label.fields['hid']
|
||||||
|
else:
|
||||||
|
if dev.status >= STATUS.CONNECTED:
|
||||||
|
hid = label.fields['hid'] = dev.protocol
|
||||||
|
need_update = True
|
||||||
|
else:
|
||||||
|
hid = None
|
||||||
|
|
||||||
|
if need_update:
|
||||||
|
items = [('Serial', serial)]
|
||||||
|
if firmware:
|
||||||
|
items += [(f.kind, f.name + ' ' + f.version) for f in firmware]
|
||||||
|
if hid:
|
||||||
|
items += [('HID', hid)]
|
||||||
|
|
||||||
|
label.set_markup('<small><tt>%s</tt></small>' % '\n'.join('%-10s: %s' % (item[0], str(item[1])) for item in items))
|
||||||
|
|
||||||
|
|
||||||
|
def _update_receiver_info_label(label, dev):
|
||||||
|
if label.get_visible() and label.get_text() == '':
|
||||||
|
items = [('Serial', dev.serial)] + \
|
||||||
|
[(f.kind, f.version) for f in dev.firmware]
|
||||||
|
label.set_markup('<small><tt>%s</tt></small>' % '\n'.join('%-10s: %s' % (item[0], str(item[1])) for item in items))
|
||||||
|
|
||||||
|
def _toggle_info_box(action, label_widget, box_widget, frame, update_function):
|
||||||
|
if action.get_active():
|
||||||
|
box_widget.set_visible(True)
|
||||||
|
update_function(label_widget, frame._device)
|
||||||
|
else:
|
||||||
|
box_widget.set_visible(False)
|
||||||
|
|
||||||
|
|
||||||
def _update_receiver_box(frame, receiver):
|
def _update_receiver_box(frame, receiver):
|
||||||
label, toolbar, info_label = ui.find_children(frame, 'label', 'toolbar', 'info-label')
|
label, toolbar, info_label = ui.find_children(frame, 'label', 'toolbar', 'info-label')
|
||||||
|
|
||||||
|
@ -220,44 +257,43 @@ def _update_receiver_box(frame, receiver):
|
||||||
|
|
||||||
def _update_device_box(frame, dev):
|
def _update_device_box(frame, dev):
|
||||||
frame._device = dev
|
frame._device = dev
|
||||||
|
# print dev.name, dev.kind
|
||||||
|
|
||||||
icon, label = ui.find_children(frame, 'icon', 'label')
|
icon, label, info_label = ui.find_children(frame, 'icon', 'label', 'info-label')
|
||||||
|
|
||||||
if frame.get_name() != dev.name:
|
first_run = frame.get_name() != dev.name
|
||||||
|
if first_run:
|
||||||
frame.set_name(dev.name)
|
frame.set_name(dev.name)
|
||||||
icon_name = ui.get_icon(dev.name, dev.kind)
|
icon_name = ui.get_icon(dev.name, dev.kind)
|
||||||
icon.set_from_icon_name(icon_name, _DEVICE_ICON_SIZE)
|
icon.set_from_icon_name(icon_name, _DEVICE_ICON_SIZE)
|
||||||
label.set_markup('<b>' + dev.name + '</b>')
|
label.set_markup('<b>' + dev.name + '</b>')
|
||||||
info_label = ui.find_children(frame, 'info-label')
|
|
||||||
info_label.set_text('')
|
|
||||||
|
|
||||||
status = ui.find_children(frame, 'status')
|
status = ui.find_children(frame, 'status')
|
||||||
status_icons = status.get_children()
|
status_icons = status.get_children()
|
||||||
toolbar = status_icons[-1]
|
|
||||||
if dev.status < STATUS.CONNECTED:
|
|
||||||
icon.set_sensitive(False)
|
|
||||||
label.set_sensitive(False)
|
|
||||||
status.set_sensitive(False)
|
|
||||||
for c in status_icons[1:-1]:
|
|
||||||
c.set_visible(False)
|
|
||||||
toolbar.get_children()[0].set_active(False)
|
|
||||||
else:
|
|
||||||
icon.set_sensitive(True)
|
|
||||||
label.set_sensitive(True)
|
|
||||||
status.set_sensitive(True)
|
|
||||||
|
|
||||||
|
if dev.status < STATUS.CONNECTED:
|
||||||
|
battery_icon, battery_label = status_icons[0:2]
|
||||||
|
battery_icon.set_sensitive(False)
|
||||||
|
battery_label.set_markup('<small>%s</small>' % dev.status_text)
|
||||||
|
battery_label.set_sensitive(True)
|
||||||
|
for c in status_icons[2:-1]:
|
||||||
|
c.set_visible(False)
|
||||||
|
|
||||||
|
else:
|
||||||
battery_icon, battery_label = status_icons[0:2]
|
battery_icon, battery_label = status_icons[0:2]
|
||||||
battery_level = dev.props.get(PROPS.BATTERY_LEVEL)
|
battery_level = dev.props.get(PROPS.BATTERY_LEVEL)
|
||||||
if battery_level is None:
|
if battery_level is None:
|
||||||
battery_icon.set_from_icon_name('battery_unknown', _STATUS_ICON_SIZE)
|
|
||||||
battery_icon.set_sensitive(False)
|
battery_icon.set_sensitive(False)
|
||||||
battery_label.set_visible(False)
|
battery_icon.set_from_icon_name('battery_unknown', _STATUS_ICON_SIZE)
|
||||||
|
text = 'no status' if dev.protocol < 2.0 else 'waiting for status...'
|
||||||
|
battery_label.set_markup('<small>%s</small>' % text)
|
||||||
|
battery_label.set_sensitive(False)
|
||||||
else:
|
else:
|
||||||
icon_name = 'battery_%03d' % (20 * ((battery_level + 10) // 20))
|
icon_name = 'battery_%03d' % (20 * ((battery_level + 10) // 20))
|
||||||
battery_icon.set_from_icon_name(icon_name, _STATUS_ICON_SIZE)
|
battery_icon.set_from_icon_name(icon_name, _STATUS_ICON_SIZE)
|
||||||
battery_icon.set_sensitive(True)
|
battery_icon.set_sensitive(True)
|
||||||
battery_label.set_text('%d%%' % battery_level)
|
battery_label.set_text('%d%%' % battery_level)
|
||||||
battery_label.set_visible(True)
|
battery_label.set_sensitive(True)
|
||||||
|
|
||||||
battery_status = dev.props.get(PROPS.BATTERY_STATUS)
|
battery_status = dev.props.get(PROPS.BATTERY_STATUS)
|
||||||
battery_icon.set_tooltip_text(battery_status or '')
|
battery_icon.set_tooltip_text(battery_status or '')
|
||||||
|
@ -274,10 +310,9 @@ def _update_device_box(frame, dev):
|
||||||
light_label.set_text('%d lux' % light_level)
|
light_label.set_text('%d lux' % light_level)
|
||||||
light_label.set_visible(True)
|
light_label.set_visible(True)
|
||||||
|
|
||||||
for b in toolbar.get_children()[:-1]:
|
if first_run:
|
||||||
b.set_sensitive(True)
|
frame.set_visible(True)
|
||||||
|
GObject.timeout_add(2000, _update_device_info_label, info_label, dev)
|
||||||
frame.set_visible(True)
|
|
||||||
|
|
||||||
|
|
||||||
def update(window, receiver, device=None):
|
def update(window, receiver, device=None):
|
||||||
|
|
|
@ -8,14 +8,21 @@ from gi.repository import (Gtk, GObject)
|
||||||
import ui
|
import ui
|
||||||
|
|
||||||
|
|
||||||
def _create_page(assistant, text, kind):
|
def _create_page(assistant, text, kind, icon_name=None):
|
||||||
p = Gtk.VBox(False, 12)
|
p = Gtk.VBox(False, 12)
|
||||||
p.set_border_width(8)
|
p.set_border_width(8)
|
||||||
|
|
||||||
if text:
|
if text:
|
||||||
|
item = Gtk.HBox(homogeneous=False, spacing=16)
|
||||||
|
p.pack_start(item, False, True, 0)
|
||||||
|
|
||||||
label = Gtk.Label(text)
|
label = Gtk.Label(text)
|
||||||
label.set_alignment(0, 0)
|
label.set_alignment(0, 0)
|
||||||
p.pack_start(label, False, True, 0)
|
item.pack_start(label, True, True, 0)
|
||||||
|
|
||||||
|
if icon_name:
|
||||||
|
icon = Gtk.Image.new_from_icon_name(icon_name, Gtk.IconSize.DIALOG)
|
||||||
|
item.pack_start(icon, False, False, 0)
|
||||||
|
|
||||||
assistant.append_page(p)
|
assistant.append_page(p)
|
||||||
assistant.set_page_type(p, kind)
|
assistant.set_page_type(p, kind)
|
||||||
|
@ -59,8 +66,9 @@ def _scan_complete_ui(assistant, device):
|
||||||
page = _create_page(assistant,
|
page = _create_page(assistant,
|
||||||
'No new device detected.\n'
|
'No new device detected.\n'
|
||||||
'\n'
|
'\n'
|
||||||
'Make sure your device is within range of the receiver,\nand it has a decent battery charge.\n',
|
'Make sure your device is within the\nreceiver\'s range, and it has\na decent battery charge.\n',
|
||||||
Gtk.AssistantPageType.CONFIRM)
|
Gtk.AssistantPageType.CONFIRM,
|
||||||
|
'dialog-error')
|
||||||
else:
|
else:
|
||||||
page = _create_page(assistant,
|
page = _create_page(assistant,
|
||||||
None,
|
None,
|
||||||
|
@ -110,7 +118,8 @@ def create(action, state):
|
||||||
'Turn on the device you want to pair.\n'
|
'Turn on the device you want to pair.\n'
|
||||||
'\n'
|
'\n'
|
||||||
'If the device is already turned on,\nturn if off and on again.',
|
'If the device is already turned on,\nturn if off and on again.',
|
||||||
Gtk.AssistantPageType.INTRO)
|
Gtk.AssistantPageType.INTRO,
|
||||||
|
'preferences-desktop-peripherals')
|
||||||
spinner = Gtk.Spinner()
|
spinner = Gtk.Spinner()
|
||||||
spinner.set_visible(True)
|
spinner.set_visible(True)
|
||||||
page_intro.pack_end(spinner, True, True, 16)
|
page_intro.pack_end(spinner, True, True, 16)
|
||||||
|
|
|
@ -50,6 +50,6 @@ def update(icon, receiver):
|
||||||
lines.append('')
|
lines.append('')
|
||||||
|
|
||||||
text = '\n'.join(lines).rstrip('\n')
|
text = '\n'.join(lines).rstrip('\n')
|
||||||
icon.set_tooltip_markup(text)
|
icon.set_tooltip_markup(ui.NAME + ':\n' + text)
|
||||||
else:
|
else:
|
||||||
icon.set_tooltip_text(receiver.status_text)
|
icon.set_tooltip_text(ui.NAME + ': ' + receiver.status_text)
|
||||||
|
|
|
@ -4,7 +4,4 @@ __author__ = "Daniel Pavel"
|
||||||
__license__ = "GPL"
|
__license__ = "GPL"
|
||||||
__version__ = "0.4"
|
__version__ = "0.4"
|
||||||
|
|
||||||
try:
|
from hidapi.udev import *
|
||||||
from hidapi.udev import *
|
|
||||||
except ImportError:
|
|
||||||
from hidapi.native import *
|
|
||||||
|
|
|
@ -55,7 +55,8 @@ if __name__ == '__main__':
|
||||||
print (".. Opening device %s" % args.device)
|
print (".. Opening device %s" % args.device)
|
||||||
handle = hidapi.open_path(args.device.encode('utf-8'))
|
handle = hidapi.open_path(args.device.encode('utf-8'))
|
||||||
if handle:
|
if handle:
|
||||||
print (".. Opened handle %X, vendor %s product %s serial %s" % (handle,
|
print (".. Opened handle %s, vendor %s product %s serial %s" % (
|
||||||
|
repr(handle),
|
||||||
repr(hidapi.get_manufacturer(handle)),
|
repr(hidapi.get_manufacturer(handle)),
|
||||||
repr(hidapi.get_product(handle)),
|
repr(hidapi.get_product(handle)),
|
||||||
repr(hidapi.get_serial(handle))))
|
repr(hidapi.get_serial(handle))))
|
||||||
|
@ -101,7 +102,7 @@ if __name__ == '__main__':
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print ('%s: %s' % (type(e).__name__, e))
|
print ('%s: %s' % (type(e).__name__, e))
|
||||||
|
|
||||||
print (".. Closing handle %X" % handle)
|
print (".. Closing handle %s" % repr(handle))
|
||||||
hidapi.close(handle)
|
hidapi.close(handle)
|
||||||
if interactive:
|
if interactive:
|
||||||
readline.write_history_file(args.history)
|
readline.write_history_file(args.history)
|
||||||
|
|
|
@ -16,6 +16,10 @@ Currently the native libusb implementation (temporarily) detaches the device's
|
||||||
USB driver from the kernel, and it may cause the device to become unresponsive.
|
USB driver from the kernel, and it may cause the device to become unresponsive.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
#
|
||||||
|
# LEGACY, no longer supported
|
||||||
|
#
|
||||||
|
|
||||||
__version__ = '0.3-hidapi-0.7.0'
|
__version__ = '0.3-hidapi-0.7.0'
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -8,9 +8,10 @@ necessary.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os as _os
|
import os as _os
|
||||||
|
import errno as _errno
|
||||||
from select import select as _select
|
from select import select as _select
|
||||||
from pyudev import Context as _Context
|
from pyudev import (Context as _Context,
|
||||||
from pyudev import Device as _Device
|
Device as _Device)
|
||||||
|
|
||||||
|
|
||||||
native_implementation = 'udev'
|
native_implementation = 'udev'
|
||||||
|
@ -124,6 +125,7 @@ def open_path(device_path):
|
||||||
|
|
||||||
:returns: an opaque device handle, or ``None``.
|
:returns: an opaque device handle, or ``None``.
|
||||||
"""
|
"""
|
||||||
|
assert '/dev/hidraw' in device_path
|
||||||
return _os.open(device_path, _os.O_RDWR | _os.O_SYNC)
|
return _os.open(device_path, _os.O_RDWR | _os.O_SYNC)
|
||||||
|
|
||||||
|
|
||||||
|
@ -155,14 +157,11 @@ def write(device_handle, data):
|
||||||
write() will send the data on the first OUT endpoint, if
|
write() will send the data on the first OUT endpoint, if
|
||||||
one exists. If it does not, it will send the data through
|
one exists. If it does not, it will send the data through
|
||||||
the Control Endpoint (Endpoint 0).
|
the Control Endpoint (Endpoint 0).
|
||||||
|
|
||||||
:returns: ``True`` if the write was successful.
|
|
||||||
"""
|
"""
|
||||||
try:
|
bytes_written = _os.write(device_handle, data)
|
||||||
bytes_written = _os.write(device_handle, data)
|
|
||||||
return bytes_written == len(data)
|
if bytes_written != len(data):
|
||||||
except:
|
raise OSError(errno=_errno.EIO, strerror='written %d bytes out of expected %d' % (bytes_written, len(data)))
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def read(device_handle, bytes_count, timeout_ms=-1):
|
def read(device_handle, bytes_count, timeout_ms=-1):
|
||||||
|
@ -181,15 +180,19 @@ def read(device_handle, bytes_count, timeout_ms=-1):
|
||||||
:returns: the data packet read, an empty bytes string if a timeout was
|
:returns: the data packet read, an empty bytes string if a timeout was
|
||||||
reached, or None if there was an error while reading.
|
reached, or None if there was an error while reading.
|
||||||
"""
|
"""
|
||||||
try:
|
timeout = None if timeout_ms < 0 else timeout_ms / 1000.0
|
||||||
timeout = None if timeout_ms < 0 else timeout_ms / 1000.0
|
rlist, wlist, xlist = _select([device_handle], [], [device_handle], timeout)
|
||||||
rlist, wlist, xlist = _select([device_handle], [], [], timeout)
|
|
||||||
if rlist:
|
if xlist:
|
||||||
assert rlist == [device_handle]
|
assert xlist == [device_handle]
|
||||||
return _os.read(device_handle, bytes_count)
|
raise OSError(errno=_errno.EIO, strerror='exception on file descriptor %d' % device_handle)
|
||||||
|
|
||||||
|
if rlist:
|
||||||
|
assert rlist == [device_handle]
|
||||||
|
data = _os.read(device_handle, bytes_count)
|
||||||
|
return data
|
||||||
|
else:
|
||||||
return b''
|
return b''
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
_DEVICE_STRINGS = {
|
_DEVICE_STRINGS = {
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from .constants import (STATUS, PROPS)
|
from .constants import (STATUS, PROPS)
|
||||||
from ..unifying_receiver.constants import (FEATURE, BATTERY_STATUS)
|
from ..unifying_receiver.constants import (FEATURE, BATTERY_STATUS, BATTERY_OK)
|
||||||
from ..unifying_receiver import api as _api
|
from ..unifying_receiver import api as _api
|
||||||
|
|
||||||
#
|
#
|
||||||
|
@ -14,17 +14,17 @@ from ..unifying_receiver import api as _api
|
||||||
|
|
||||||
_DEVICE_MODULES = {}
|
_DEVICE_MODULES = {}
|
||||||
|
|
||||||
def _module(device_name):
|
def _module(device):
|
||||||
if device_name not in _DEVICE_MODULES:
|
shortname = device.codename.lower().replace(' ', '_')
|
||||||
shortname = device_name.split(' ')[-1].lower()
|
if shortname not in _DEVICE_MODULES:
|
||||||
try:
|
try:
|
||||||
m = __import__(shortname, globals(), level=1)
|
m = __import__(shortname, globals(), level=1)
|
||||||
_DEVICE_MODULES[device_name] = m
|
_DEVICE_MODULES[shortname] = m
|
||||||
except:
|
except:
|
||||||
# logging.exception(shortname)
|
# logging.exception(shortname)
|
||||||
_DEVICE_MODULES[device_name] = None
|
_DEVICE_MODULES[shortname] = None
|
||||||
|
|
||||||
return _DEVICE_MODULES[device_name]
|
return _DEVICE_MODULES[shortname]
|
||||||
|
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
|
@ -34,8 +34,11 @@ def default_request_status(devinfo):
|
||||||
if FEATURE.BATTERY in devinfo.features:
|
if FEATURE.BATTERY in devinfo.features:
|
||||||
reply = _api.get_device_battery_level(devinfo.handle, devinfo.number, features=devinfo.features)
|
reply = _api.get_device_battery_level(devinfo.handle, devinfo.number, features=devinfo.features)
|
||||||
if reply:
|
if reply:
|
||||||
discharge, dischargeNext, status = reply
|
b_discharge, dischargeNext, b_status = reply
|
||||||
return STATUS.CONNECTED, {PROPS.BATTERY_LEVEL: discharge, PROPS.BATTERY_STATUS: status}
|
return STATUS.CONNECTED, {
|
||||||
|
PROPS.BATTERY_LEVEL: b_discharge,
|
||||||
|
PROPS.BATTERY_STATUS: b_status,
|
||||||
|
}
|
||||||
|
|
||||||
reply = _api.ping(devinfo.handle, devinfo.number)
|
reply = _api.ping(devinfo.handle, devinfo.number)
|
||||||
return STATUS.CONNECTED if reply else STATUS.UNAVAILABLE
|
return STATUS.CONNECTED if reply else STATUS.UNAVAILABLE
|
||||||
|
@ -44,29 +47,33 @@ def default_request_status(devinfo):
|
||||||
def default_process_event(devinfo, data):
|
def default_process_event(devinfo, data):
|
||||||
feature_index = ord(data[0:1])
|
feature_index = ord(data[0:1])
|
||||||
if feature_index >= len(devinfo.features):
|
if feature_index >= len(devinfo.features):
|
||||||
logging.warn("mistery event %s for %s", repr(data), devinfo)
|
# logging.warn("mistery event %s for %s", repr(data), devinfo)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
feature = devinfo.features[feature_index]
|
feature = devinfo.features[feature_index]
|
||||||
feature_function = ord(data[1:2]) & 0xF0
|
feature_function = ord(data[1:2]) & 0xF0
|
||||||
|
|
||||||
if feature == FEATURE.BATTERY:
|
if feature == FEATURE.BATTERY:
|
||||||
if feature_function == 0:
|
if feature_function == 0x00:
|
||||||
discharge = ord(data[2:3])
|
b_discharge = ord(data[2:3])
|
||||||
status = BATTERY_STATUS[ord(data[3:4])]
|
b_status = ord(data[3:4])
|
||||||
return STATUS.CONNECTED, {PROPS.BATTERY_LEVEL: discharge, PROPS.BATTERY_STATUS: status}
|
return STATUS.CONNECTED, {
|
||||||
|
PROPS.BATTERY_LEVEL: b_discharge,
|
||||||
|
PROPS.BATTERY_STATUS: BATTERY_STATUS[b_status],
|
||||||
|
PROPS.UI_FLAGS: 0 if BATTERY_OK(b_status) else STATUS.UI_NOTIFY,
|
||||||
|
}
|
||||||
# ?
|
# ?
|
||||||
elif feature == FEATURE.REPROGRAMMABLE_KEYS:
|
elif feature == FEATURE.REPROGRAMMABLE_KEYS:
|
||||||
if feature_function == 0:
|
if feature_function == 0x00:
|
||||||
logging.debug('reprogrammable key: %s', repr(data))
|
logging.debug('reprogrammable key: %s', repr(data))
|
||||||
# TODO
|
# TODO
|
||||||
pass
|
pass
|
||||||
# ?
|
# ?
|
||||||
elif feature == FEATURE.WIRELESS:
|
elif feature == FEATURE.WIRELESS:
|
||||||
if feature_function == 0:
|
if feature_function == 0x00:
|
||||||
logging.debug("wireless status: %s", repr(data))
|
logging.debug("wireless status: %s", repr(data))
|
||||||
if data[2:5] == b'\x01\x01\x01':
|
if data[2:5] == b'\x01\x01\x01':
|
||||||
return STATUS.CONNECTED
|
return STATUS.CONNECTED, {PROPS.UI_FLAGS: STATUS.UI_NOTIFY}
|
||||||
# TODO
|
# TODO
|
||||||
pass
|
pass
|
||||||
# ?
|
# ?
|
||||||
|
@ -79,7 +86,7 @@ def request_status(devinfo):
|
||||||
:param listener: the EventsListener that will be used to send the request,
|
:param listener: the EventsListener that will be used to send the request,
|
||||||
and which will receive the status events from the device.
|
and which will receive the status events from the device.
|
||||||
"""
|
"""
|
||||||
m = _module(devinfo.name)
|
m = _module(devinfo)
|
||||||
if m and 'request_status' in m.__dict__:
|
if m and 'request_status' in m.__dict__:
|
||||||
return m.request_status(devinfo)
|
return m.request_status(devinfo)
|
||||||
return default_request_status(devinfo)
|
return default_request_status(devinfo)
|
||||||
|
@ -95,6 +102,6 @@ def process_event(devinfo, data):
|
||||||
if default_result is not None:
|
if default_result is not None:
|
||||||
return default_result
|
return default_result
|
||||||
|
|
||||||
m = _module(devinfo.name)
|
m = _module(devinfo)
|
||||||
if m and 'process_event' in m.__dict__:
|
if m and 'process_event' in m.__dict__:
|
||||||
return m.process_event(devinfo, data)
|
return m.process_event(devinfo, data)
|
||||||
|
|
|
@ -4,8 +4,10 @@
|
||||||
|
|
||||||
STATUS = type('STATUS', (),
|
STATUS = type('STATUS', (),
|
||||||
dict(
|
dict(
|
||||||
UNKNOWN=-9999,
|
UI_NOTIFY=0x01,
|
||||||
UNPAIRED=-1000,
|
UI_POPUP=0x02,
|
||||||
|
UNKNOWN=-0xFFFF,
|
||||||
|
UNPAIRED=-0x1000,
|
||||||
UNAVAILABLE=-1,
|
UNAVAILABLE=-1,
|
||||||
BOOTING=0,
|
BOOTING=0,
|
||||||
CONNECTED=1,
|
CONNECTED=1,
|
||||||
|
@ -26,6 +28,7 @@ PROPS = type('PROPS', (),
|
||||||
BATTERY_LEVEL='battery_level',
|
BATTERY_LEVEL='battery_level',
|
||||||
BATTERY_STATUS='battery_status',
|
BATTERY_STATUS='battery_status',
|
||||||
LIGHT_LEVEL='light_level',
|
LIGHT_LEVEL='light_level',
|
||||||
|
UI_FLAGS='ui_flags',
|
||||||
))
|
))
|
||||||
|
|
||||||
# when the receiver reports a device that is not connected
|
# when the receiver reports a device that is not connected
|
||||||
|
|
|
@ -47,4 +47,9 @@ def process_event(devinfo, data):
|
||||||
|
|
||||||
if data[:2] == b'\x09\x20' and data[7:11] == b'GOOD':
|
if data[:2] == b'\x09\x20' and data[7:11] == b'GOOD':
|
||||||
logging.debug("Solar key pressed")
|
logging.debug("Solar key pressed")
|
||||||
return request_status(devinfo) or _charge_status(data)
|
if request_status(devinfo) == STATUS.UNAVAILABLE:
|
||||||
|
return STATUS.UNAVAILABLE, {PROPS.UI_FLAGS: STATUS.UI_POPUP | STATUS.UI_NOTIFY}
|
||||||
|
|
||||||
|
code, props = _charge_status(data)
|
||||||
|
props[PROPS.UI_FLAGS] = STATUS.UI_POPUP
|
||||||
|
return code, props
|
||||||
|
|
|
@ -7,17 +7,26 @@ def print_receiver(receiver):
|
||||||
print (" Serial : %s" % receiver.serial)
|
print (" Serial : %s" % receiver.serial)
|
||||||
for f in receiver.firmware:
|
for f in receiver.firmware:
|
||||||
print (" %-10s: %s" % (f.kind, f.version))
|
print (" %-10s: %s" % (f.kind, f.version))
|
||||||
|
print (" Receiver reported %d paired device(s)" % len(receiver))
|
||||||
|
|
||||||
|
|
||||||
def scan_devices(receiver):
|
def scan_devices(receiver):
|
||||||
for dev in receiver:
|
for number in range(1, 1 + receiver.max_devices):
|
||||||
|
dev = receiver[number]
|
||||||
|
if dev is None:
|
||||||
|
dev = api.PairedDevice(receiver.handle, number)
|
||||||
|
if dev.codename is None:
|
||||||
|
continue
|
||||||
|
|
||||||
print ("--------")
|
print ("--------")
|
||||||
print (str(dev))
|
print (str(dev))
|
||||||
|
print ("Codename : %s" % dev.codename)
|
||||||
print ("Name : %s" % dev.name)
|
print ("Name : %s" % dev.name)
|
||||||
print ("Kind : %s" % dev.kind)
|
print ("Kind : %s" % dev.kind)
|
||||||
print ("Serial number: %s" % dev.serial)
|
print ("Serial number: %s" % dev.serial)
|
||||||
|
|
||||||
if not dev.protocol:
|
if not dev.protocol:
|
||||||
print ("HID protocol : UNKNOWN")
|
print ("Device is not connected at this time, no further info available.")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
print ("HID protocol : HID %01.1f" % dev.protocol)
|
print ("HID protocol : HID %01.1f" % dev.protocol)
|
||||||
|
@ -27,7 +36,7 @@ def scan_devices(receiver):
|
||||||
|
|
||||||
firmware = dev.firmware
|
firmware = dev.firmware
|
||||||
for fw in firmware:
|
for fw in firmware:
|
||||||
print (" %-10s: %s %s" % (fw.kind, fw.name, fw.version))
|
print (" %-11s: %s %s" % (fw.kind, fw.name, fw.version))
|
||||||
|
|
||||||
all_features = api.get_device_features(dev.handle, dev.number)
|
all_features = api.get_device_features(dev.handle, dev.number)
|
||||||
for index in range(0, len(all_features)):
|
for index in range(0, len(all_features)):
|
||||||
|
@ -45,9 +54,7 @@ def scan_devices(receiver):
|
||||||
print (" %d reprogrammable keys found" % len(keys))
|
print (" %d reprogrammable keys found" % len(keys))
|
||||||
for k in keys:
|
for k in keys:
|
||||||
flags = ','.join(KEY_FLAG_NAME[f] for f in KEY_FLAG_NAME if k.flags & f)
|
flags = ','.join(KEY_FLAG_NAME[f] for f in KEY_FLAG_NAME if k.flags & f)
|
||||||
print (" %2d: %-12s => %-12s :%s" % (k.index, KEY_NAME[k.id], KEY_NAME[k.task], flags))
|
print (" %2d: %-12s => %-12s : %s" % (k.index, KEY_NAME[k.id], KEY_NAME[k.task], flags))
|
||||||
|
|
||||||
print ("--------")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
@ -65,9 +72,8 @@ if __name__ == '__main__':
|
||||||
|
|
||||||
receiver = api.Receiver.open()
|
receiver = api.Receiver.open()
|
||||||
if receiver is None:
|
if receiver is None:
|
||||||
print ("!! Logitech Unifying Receiver not found.")
|
print ("Logitech Unifying Receiver not found.")
|
||||||
else:
|
else:
|
||||||
print ("!! Found Logitech Unifying Receiver: %s" % receiver)
|
|
||||||
print_receiver(receiver)
|
print_receiver(receiver)
|
||||||
scan_devices(receiver)
|
scan_devices(receiver)
|
||||||
receiver.close()
|
receiver.close()
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
from struct import pack as _pack
|
from struct import pack as _pack
|
||||||
from struct import unpack as _unpack
|
from struct import unpack as _unpack
|
||||||
import errno as _errno
|
import errno as _errno
|
||||||
|
from threading import local as _local
|
||||||
|
|
||||||
|
|
||||||
from . import base as _base
|
from . import base as _base
|
||||||
|
@ -27,6 +28,56 @@ del getLogger
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
|
|
||||||
|
class ThreadedHandle(object):
|
||||||
|
__slots__ = ['path', '_local', '_handles']
|
||||||
|
|
||||||
|
def __init__(self, initial_handle, path):
|
||||||
|
if type(initial_handle) != int:
|
||||||
|
raise TypeError('expected int as initial handle, got %s' % repr(initial_handle))
|
||||||
|
|
||||||
|
self.path = path
|
||||||
|
self._local = _local()
|
||||||
|
self._local.handle = initial_handle
|
||||||
|
self._handles = [initial_handle]
|
||||||
|
|
||||||
|
def _open(self):
|
||||||
|
handle = _base.open_path(self.path)
|
||||||
|
if handle is None:
|
||||||
|
_log.error("%s failed to open new handle", repr(self))
|
||||||
|
else:
|
||||||
|
# _log.debug("%s opened new handle %d", repr(self), handle)
|
||||||
|
self._local.handle = handle
|
||||||
|
self._handles.append(handle)
|
||||||
|
return handle
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
self._local = None
|
||||||
|
handles, self._handles = self._handles, []
|
||||||
|
_log.debug("%s closing %s", repr(self), handles)
|
||||||
|
for h in handles:
|
||||||
|
_base.close(h)
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
def __int__(self):
|
||||||
|
if self._local:
|
||||||
|
try:
|
||||||
|
return self._local.handle
|
||||||
|
except:
|
||||||
|
return self._open()
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return str(int(self))
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return '<LocalHandle[%s]>' % self.path
|
||||||
|
|
||||||
|
def __bool__(self):
|
||||||
|
return bool(self._handles)
|
||||||
|
__nonzero__ = __bool__
|
||||||
|
|
||||||
|
|
||||||
class PairedDevice(object):
|
class PairedDevice(object):
|
||||||
def __init__(self, handle, number):
|
def __init__(self, handle, number):
|
||||||
self.handle = handle
|
self.handle = handle
|
||||||
|
@ -40,11 +91,15 @@ class PairedDevice(object):
|
||||||
self._serial = None
|
self._serial = None
|
||||||
self._firmware = None
|
self._firmware = None
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
self.handle = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def protocol(self):
|
def protocol(self):
|
||||||
if self._protocol is None:
|
if self._protocol is None:
|
||||||
self._protocol = _base.ping(self.handle, self.number)
|
self._protocol = _base.ping(self.handle, self.number)
|
||||||
return 0 if self._protocol is None else self._protocol
|
# _log.debug("device %d protocol %s", self.number, self._protocol)
|
||||||
|
return self._protocol or 0
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def features(self):
|
def features(self):
|
||||||
|
@ -59,7 +114,8 @@ class PairedDevice(object):
|
||||||
codename = _base.request(self.handle, 0xFF, b'\x83\xB5', 0x40 + self.number - 1)
|
codename = _base.request(self.handle, 0xFF, b'\x83\xB5', 0x40 + self.number - 1)
|
||||||
if codename:
|
if codename:
|
||||||
self._codename = codename[2:].rstrip(b'\x00').decode('ascii')
|
self._codename = codename[2:].rstrip(b'\x00').decode('ascii')
|
||||||
return self._codename or '?'
|
# _log.debug("device %d codename %s", self.number, self._codename)
|
||||||
|
return self._codename
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
|
@ -70,7 +126,7 @@ class PairedDevice(object):
|
||||||
self._name, self._kind = _DEVICE_NAMES[self._codename]
|
self._name, self._kind = _DEVICE_NAMES[self._codename]
|
||||||
else:
|
else:
|
||||||
self._name = get_device_name(self.handle, self.number, self.features)
|
self._name = get_device_name(self.handle, self.number, self.features)
|
||||||
return self._name or self.codename
|
return self._name or self.codename or '?'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def kind(self):
|
def kind(self):
|
||||||
|
@ -87,6 +143,7 @@ class PairedDevice(object):
|
||||||
def firmware(self):
|
def firmware(self):
|
||||||
if self._firmware is None and self.protocol >= 2.0:
|
if self._firmware is None and self.protocol >= 2.0:
|
||||||
self._firmware = get_device_firmware(self.handle, self.number, self.features)
|
self._firmware = get_device_firmware(self.handle, self.number, self.features)
|
||||||
|
# _log.debug("device %d firmware %s", self.number, self._firmware)
|
||||||
return self._firmware or ()
|
return self._firmware or ()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -96,16 +153,14 @@ class PairedDevice(object):
|
||||||
serial = _base.request(self.handle, 0xFF, b'\x83\xB5', 0x30 + self.number - 1)
|
serial = _base.request(self.handle, 0xFF, b'\x83\xB5', 0x30 + self.number - 1)
|
||||||
if prefix and serial:
|
if prefix and serial:
|
||||||
self._serial = _base._hex(prefix[3:5]) + '-' + _base._hex(serial[1:5])
|
self._serial = _base._hex(prefix[3:5]) + '-' + _base._hex(serial[1:5])
|
||||||
|
# _log.debug("device %d serial %s", self.number, self._serial)
|
||||||
return self._serial or '?'
|
return self._serial or '?'
|
||||||
|
|
||||||
def ping(self):
|
def ping(self):
|
||||||
return _base.ping(self.handle, self.number) is not None
|
return _base.ping(self.handle, self.number) is not None
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return '<PairedDevice(%X,%d,%s)>' % (self.handle, self.number, self._name or '?')
|
return '<PairedDevice(%s,%d,%s)>' % (self.handle, self.number, self.codename or '?')
|
||||||
|
|
||||||
def __hash__(self):
|
|
||||||
return self.number
|
|
||||||
|
|
||||||
|
|
||||||
class Receiver(object):
|
class Receiver(object):
|
||||||
|
@ -120,9 +175,12 @@ class Receiver(object):
|
||||||
self._firmware = None
|
self._firmware = None
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
handle, self.handle = self.handle, 0
|
handle, self.handle = self.handle, None
|
||||||
return (handle and _base.close(handle))
|
return (handle and _base.close(handle))
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
self.close()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def serial(self):
|
def serial(self):
|
||||||
if self._serial is None and self.handle:
|
if self._serial is None and self.handle:
|
||||||
|
@ -153,7 +211,7 @@ class Receiver(object):
|
||||||
return self._firmware
|
return self._firmware
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
if self.handle == 0:
|
if not self.handle:
|
||||||
return
|
return
|
||||||
|
|
||||||
for number in range(1, 1 + MAX_ATTACHED_DEVICES):
|
for number in range(1, 1 + MAX_ATTACHED_DEVICES):
|
||||||
|
@ -164,14 +222,14 @@ class Receiver(object):
|
||||||
def __getitem__(self, key):
|
def __getitem__(self, key):
|
||||||
if type(key) != int:
|
if type(key) != int:
|
||||||
raise TypeError('key must be an integer')
|
raise TypeError('key must be an integer')
|
||||||
if self.handle == 0 or key < 0 or key > MAX_ATTACHED_DEVICES:
|
if not self.handle or key < 0 or key > MAX_ATTACHED_DEVICES:
|
||||||
raise IndexError(key)
|
raise IndexError(key)
|
||||||
return get_device(self.handle, key) if key > 0 else None
|
return get_device(self.handle, key) if key > 0 else None
|
||||||
|
|
||||||
def __delitem__(self, key):
|
def __delitem__(self, key):
|
||||||
if type(key) != int:
|
if type(key) != int:
|
||||||
raise TypeError('key must be an integer')
|
raise TypeError('key must be an integer')
|
||||||
if self.handle == 0 or key < 0 or key > MAX_ATTACHED_DEVICES:
|
if not self.handle or key < 0 or key > MAX_ATTACHED_DEVICES:
|
||||||
raise IndexError(key)
|
raise IndexError(key)
|
||||||
if key > 0:
|
if key > 0:
|
||||||
_log.debug("unpairing device %d", key)
|
_log.debug("unpairing device %d", key)
|
||||||
|
@ -180,13 +238,14 @@ class Receiver(object):
|
||||||
raise IndexError(key)
|
raise IndexError(key)
|
||||||
|
|
||||||
def __len__(self):
|
def __len__(self):
|
||||||
if self.handle == 0:
|
if not self.handle:
|
||||||
return 0
|
return 0
|
||||||
# not really sure about this one...
|
# not really sure about this one...
|
||||||
count = _base.request(self.handle, 0xFF, b'\x81\x00')
|
count = _base.request(self.handle, 0xFF, b'\x81\x00')
|
||||||
return 0 if count is None else ord(count[1:2])
|
return 0 if count is None else ord(count[1:2])
|
||||||
|
|
||||||
def __contains__(self, dev):
|
def __contains__(self, dev):
|
||||||
|
# print self, "contains", dev
|
||||||
if self.handle == 0:
|
if self.handle == 0:
|
||||||
return False
|
return False
|
||||||
if type(dev) == int:
|
if type(dev) == int:
|
||||||
|
@ -194,10 +253,7 @@ class Receiver(object):
|
||||||
return dev.ping()
|
return dev.ping()
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return '<Receiver(%X,%s)>' % (self.handle, self.path)
|
return '<Receiver(%s,%s)>' % (self.handle, self.path)
|
||||||
|
|
||||||
def __hash__(self):
|
|
||||||
return self.handle
|
|
||||||
|
|
||||||
__bool__ = __nonzero__ = lambda self: self.handle != 0
|
__bool__ = __nonzero__ = lambda self: self.handle != 0
|
||||||
|
|
||||||
|
@ -212,7 +268,7 @@ class Receiver(object):
|
||||||
for rawdevice in _base.list_receiver_devices():
|
for rawdevice in _base.list_receiver_devices():
|
||||||
exception = None
|
exception = None
|
||||||
try:
|
try:
|
||||||
handle = _base.try_open(rawdevice.path)
|
handle = _base.open_path(rawdevice.path)
|
||||||
if handle:
|
if handle:
|
||||||
return Receiver(handle, rawdevice.path)
|
return Receiver(handle, rawdevice.path)
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
|
@ -295,13 +351,13 @@ def get_feature_index(handle, devnumber, feature):
|
||||||
if reply:
|
if reply:
|
||||||
feature_index = ord(reply[0:1])
|
feature_index = ord(reply[0:1])
|
||||||
if feature_index:
|
if feature_index:
|
||||||
# feature_flags = ord(reply[1:2]) & 0xE0
|
feature_flags = ord(reply[1:2]) & 0xE0
|
||||||
# if feature_flags:
|
if feature_flags:
|
||||||
# _log.debug("device %d feature <%s:%s> has index %d: %s",
|
_log.debug("device %d feature <%s:%s> has index %d: %s",
|
||||||
# devnumber, _hex(feature), FEATURE_NAME[feature], feature_index,
|
devnumber, _hex(feature), FEATURE_NAME[feature], feature_index,
|
||||||
# ','.join([FEATURE_FLAGS[k] for k in FEATURE_FLAGS if feature_flags & k]))
|
','.join([FEATURE_FLAGS[k] for k in FEATURE_FLAGS if feature_flags & k]))
|
||||||
# else:
|
else:
|
||||||
# _log.debug("device %d feature <%s:%s> has index %d", devnumber, _hex(feature), FEATURE_NAME[feature], feature_index)
|
_log.debug("device %d feature <%s:%s> has index %d", devnumber, _hex(feature), FEATURE_NAME[feature], feature_index)
|
||||||
|
|
||||||
# only consider active and supported features?
|
# only consider active and supported features?
|
||||||
# if feature_flags:
|
# if feature_flags:
|
||||||
|
@ -322,9 +378,12 @@ def _get_feature_index(handle, devnumber, feature, features=None):
|
||||||
|
|
||||||
index = get_feature_index(handle, devnumber, feature)
|
index = get_feature_index(handle, devnumber, feature)
|
||||||
if index is not None:
|
if index is not None:
|
||||||
if len(features) <= index:
|
try:
|
||||||
features += [None] * (index + 1 - len(features))
|
if len(features) <= index:
|
||||||
features[index] = feature
|
features += [None] * (index + 1 - len(features))
|
||||||
|
features[index] = feature
|
||||||
|
except:
|
||||||
|
pass
|
||||||
# _log.debug("%s: found feature %s at %d", features, _base._hex(feature), index)
|
# _log.debug("%s: found feature %s at %d", features, _base._hex(feature), index)
|
||||||
return index
|
return index
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
# Unlikely to be used directly unless you're expanding the API.
|
# Unlikely to be used directly unless you're expanding the API.
|
||||||
#
|
#
|
||||||
|
|
||||||
|
import os as _os
|
||||||
|
from time import time as _timestamp
|
||||||
from struct import pack as _pack
|
from struct import pack as _pack
|
||||||
from binascii import hexlify as _hexlify
|
from binascii import hexlify as _hexlify
|
||||||
_hex = lambda d: _hexlify(d).decode('ascii').upper()
|
_hex = lambda d: _hexlify(d).decode('ascii').upper()
|
||||||
|
@ -40,32 +42,7 @@ _MAX_REPLY_SIZE = _MAX_CALL_SIZE
|
||||||
|
|
||||||
|
|
||||||
"""Default timeout on read (in ms)."""
|
"""Default timeout on read (in ms)."""
|
||||||
DEFAULT_TIMEOUT = 1500
|
DEFAULT_TIMEOUT = 2000
|
||||||
|
|
||||||
#
|
|
||||||
#
|
|
||||||
#
|
|
||||||
|
|
||||||
def _logdebug_hook(reply_code, devnumber, data):
|
|
||||||
"""Default unhandled hook, logs the reply as DEBUG."""
|
|
||||||
_log.warn("UNHANDLED [%02X %02X %s %s] (%s)", reply_code, devnumber, _hex(data[:2]), _hex(data[2:]), repr(data))
|
|
||||||
|
|
||||||
|
|
||||||
"""The function that will be called on unhandled incoming events.
|
|
||||||
|
|
||||||
The hook must be a function with the signature: ``_(int, int, str)``, where
|
|
||||||
the parameters are: (reply_code, devnumber, data).
|
|
||||||
|
|
||||||
This hook will only be called by the request() function, when it receives
|
|
||||||
replies that do not match the requested feature call. As such, it is not
|
|
||||||
suitable for intercepting broadcast events from the device (e.g. special
|
|
||||||
keys being pressed, battery charge events, etc), at least not in a timely
|
|
||||||
manner. However, these events *may* be delivered here if they happen while
|
|
||||||
doing a feature call to the device.
|
|
||||||
|
|
||||||
The default implementation logs the unhandled reply as DEBUG.
|
|
||||||
"""
|
|
||||||
unhandled_hook = _logdebug_hook
|
|
||||||
|
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
|
@ -76,13 +53,10 @@ def list_receiver_devices():
|
||||||
# (Vendor ID, Product ID) = ('Logitech', 'Unifying Receiver')
|
# (Vendor ID, Product ID) = ('Logitech', 'Unifying Receiver')
|
||||||
# interface 2 if the actual receiver interface
|
# interface 2 if the actual receiver interface
|
||||||
for d in _hid.enumerate(0x046d, 0xc52b, 2):
|
for d in _hid.enumerate(0x046d, 0xc52b, 2):
|
||||||
if d.driver is None or d.driver == 'logitech-djreceiver':
|
if d.driver == 'logitech-djreceiver':
|
||||||
yield d
|
yield d
|
||||||
|
|
||||||
|
def open_path(path):
|
||||||
_COUNT_DEVICES_REQUEST = b'\x10\xFF\x81\x00\x00\x00\x00'
|
|
||||||
|
|
||||||
def try_open(path):
|
|
||||||
"""Checks if the given Linux device path points to the right UR device.
|
"""Checks if the given Linux device path points to the right UR device.
|
||||||
|
|
||||||
:param path: the Linux device path.
|
:param path: the Linux device path.
|
||||||
|
@ -95,28 +69,7 @@ def try_open(path):
|
||||||
:returns: an open receiver handle if this is the right Linux device, or
|
:returns: an open receiver handle if this is the right Linux device, or
|
||||||
``None``.
|
``None``.
|
||||||
"""
|
"""
|
||||||
receiver_handle = _hid.open_path(path)
|
return _hid.open_path(path)
|
||||||
if receiver_handle is None:
|
|
||||||
# could be a file permissions issue (did you add the udev rules?)
|
|
||||||
# in any case, unreachable
|
|
||||||
_log.debug("[%s] open failed", path)
|
|
||||||
return None
|
|
||||||
|
|
||||||
_hid.write(receiver_handle, _COUNT_DEVICES_REQUEST)
|
|
||||||
|
|
||||||
# if this is the right hidraw device, we'll receive a 'bad device' from the UR
|
|
||||||
# otherwise, the read should produce nothing
|
|
||||||
reply = _hid.read(receiver_handle, _MAX_REPLY_SIZE, DEFAULT_TIMEOUT / 2)
|
|
||||||
if reply:
|
|
||||||
if reply[:5] == _COUNT_DEVICES_REQUEST[:5]:
|
|
||||||
# 'device 0 unreachable' is the expected reply from a valid receiver handle
|
|
||||||
_log.info("[%s] success: handle %X", path, receiver_handle)
|
|
||||||
return receiver_handle
|
|
||||||
_log.debug("[%s] %X ignored reply %s", path, receiver_handle, _hex(reply))
|
|
||||||
else:
|
|
||||||
_log.debug("[%s] %X no reply", path, receiver_handle)
|
|
||||||
|
|
||||||
close(receiver_handle)
|
|
||||||
|
|
||||||
|
|
||||||
def open():
|
def open():
|
||||||
|
@ -125,8 +78,7 @@ def open():
|
||||||
:returns: An open file handle for the found receiver, or ``None``.
|
:returns: An open file handle for the found receiver, or ``None``.
|
||||||
"""
|
"""
|
||||||
for rawdevice in list_receiver_devices():
|
for rawdevice in list_receiver_devices():
|
||||||
_log.info("checking %s", rawdevice)
|
handle = open_path(rawdevice.path)
|
||||||
handle = try_open(rawdevice.path)
|
|
||||||
if handle:
|
if handle:
|
||||||
return handle
|
return handle
|
||||||
|
|
||||||
|
@ -135,11 +87,15 @@ def close(handle):
|
||||||
"""Closes a HID device handle."""
|
"""Closes a HID device handle."""
|
||||||
if handle:
|
if handle:
|
||||||
try:
|
try:
|
||||||
_hid.close(handle)
|
if type(handle) == int:
|
||||||
# _log.info("closed receiver handle %X", handle)
|
_hid.close(handle)
|
||||||
|
else:
|
||||||
|
handle.close()
|
||||||
|
# _log.info("closed receiver handle %s", repr(handle))
|
||||||
return True
|
return True
|
||||||
except:
|
except:
|
||||||
_log.exception("closing receiver handle %X", handle)
|
# _log.exception("closing receiver handle %s", repr(handle))
|
||||||
|
pass
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@ -162,11 +118,14 @@ def write(handle, devnumber, data):
|
||||||
assert _MAX_CALL_SIZE == 20
|
assert _MAX_CALL_SIZE == 20
|
||||||
# the data is padded to either 5 or 18 bytes
|
# the data is padded to either 5 or 18 bytes
|
||||||
wdata = _pack('!BB18s' if len(data) > 5 else '!BB5s', 0x10, devnumber, data)
|
wdata = _pack('!BB18s' if len(data) > 5 else '!BB5s', 0x10, devnumber, data)
|
||||||
_log.debug("<= w[10 %02X %s %s]", devnumber, _hex(wdata[2:4]), _hex(wdata[4:]))
|
_log.debug("(%s) <= w[10 %02X %s %s]", handle, devnumber, _hex(wdata[2:4]), _hex(wdata[4:]))
|
||||||
if not _hid.write(handle, wdata):
|
|
||||||
_log.warn("write failed, assuming receiver %X no longer available", handle)
|
try:
|
||||||
|
_hid.write(int(handle), wdata)
|
||||||
|
except Exception as reason:
|
||||||
|
_log.error("write failed, assuming handle %s no longer available", repr(handle))
|
||||||
close(handle)
|
close(handle)
|
||||||
raise _NoReceiver
|
raise _NoReceiver(reason)
|
||||||
|
|
||||||
|
|
||||||
def read(handle, timeout=DEFAULT_TIMEOUT):
|
def read(handle, timeout=DEFAULT_TIMEOUT):
|
||||||
|
@ -185,32 +144,61 @@ def read(handle, timeout=DEFAULT_TIMEOUT):
|
||||||
been physically removed from the machine, or the kernel driver has been
|
been physically removed from the machine, or the kernel driver has been
|
||||||
unloaded. The handle will be closed automatically.
|
unloaded. The handle will be closed automatically.
|
||||||
"""
|
"""
|
||||||
data = _hid.read(handle, _MAX_REPLY_SIZE, timeout)
|
try:
|
||||||
if data is None:
|
data = _hid.read(int(handle), _MAX_REPLY_SIZE, timeout)
|
||||||
_log.warn("read failed, assuming receiver %X no longer available", handle)
|
except Exception as reason:
|
||||||
|
_log.error("read failed, assuming handle %s no longer available", repr(handle))
|
||||||
close(handle)
|
close(handle)
|
||||||
raise _NoReceiver
|
raise _NoReceiver(reason)
|
||||||
|
|
||||||
if data:
|
if data:
|
||||||
if len(data) < _MIN_REPLY_SIZE:
|
if len(data) < _MIN_REPLY_SIZE:
|
||||||
_log.warn("=> r[%s] read packet too short: %d bytes", _hex(data), len(data))
|
_log.warn("(%s) => r[%s] read packet too short: %d bytes", handle, _hex(data), len(data))
|
||||||
data += b'\x00' * (_MIN_REPLY_SIZE - len(data))
|
data += b'\x00' * (_MIN_REPLY_SIZE - len(data))
|
||||||
if len(data) > _MAX_REPLY_SIZE:
|
if len(data) > _MAX_REPLY_SIZE:
|
||||||
_log.warn("=> r[%s] read packet too long: %d bytes", _hex(data), len(data))
|
_log.warn("(%s) => r[%s] read packet too long: %d bytes", handle, _hex(data), len(data))
|
||||||
code = ord(data[:1])
|
code = ord(data[:1])
|
||||||
devnumber = ord(data[1:2])
|
devnumber = ord(data[1:2])
|
||||||
_log.debug("=> r[%02X %02X %s %s]", code, devnumber, _hex(data[2:4]), _hex(data[4:]))
|
_log.debug("(%s) => r[%02X %02X %s %s]", handle, code, devnumber, _hex(data[2:4]), _hex(data[4:]))
|
||||||
return code, devnumber, data[2:]
|
return code, devnumber, data[2:]
|
||||||
|
|
||||||
# _l.log(_LOG_LEVEL, "(-) => r[]")
|
# _l.log(_LOG_LEVEL, "(-) => r[]")
|
||||||
|
|
||||||
|
def _skip_incoming(handle):
|
||||||
|
ihandle = int(handle)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
data = _hid.read(ihandle, _MAX_REPLY_SIZE, 0)
|
||||||
|
except Exception as reason:
|
||||||
|
_log.error("read failed, assuming receiver %s no longer available", handle)
|
||||||
|
close(handle)
|
||||||
|
raise _NoReceiver(reason)
|
||||||
|
|
||||||
|
if data:
|
||||||
|
if unhandled_hook:
|
||||||
|
unhandled_hook(ord(data[:1]), ord(data[1:2]), data[2:])
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
|
#
|
||||||
|
#
|
||||||
|
#
|
||||||
|
|
||||||
|
"""The function that will be called on unhandled incoming events.
|
||||||
|
|
||||||
|
The hook must be a function with the signature: ``_(int, int, str)``, where
|
||||||
|
the parameters are: (reply_code, devnumber, data).
|
||||||
|
|
||||||
|
This hook will only be called by the request() function, when it receives
|
||||||
|
replies that do not match the requested feature call. As such, it is not
|
||||||
|
suitable for intercepting broadcast events from the device (e.g. special
|
||||||
|
keys being pressed, battery charge events, etc), at least not in a timely
|
||||||
|
manner. However, these events *may* be delivered here if they happen while
|
||||||
|
doing a feature call to the device.
|
||||||
|
"""
|
||||||
|
unhandled_hook = None
|
||||||
|
|
||||||
_MAX_READ_TIMES = 3
|
|
||||||
request_context = None
|
|
||||||
from collections import namedtuple
|
|
||||||
_DEFAULT_REQUEST_CONTEXT_CLASS = namedtuple('_DEFAULT_REQUEST_CONTEXT_CLASS', ['write', 'read', 'unhandled_hook'])
|
|
||||||
_DEFAULT_REQUEST_CONTEXT = _DEFAULT_REQUEST_CONTEXT_CLASS(write=write, read=read, unhandled_hook=unhandled_hook)
|
|
||||||
del namedtuple
|
|
||||||
|
|
||||||
def request(handle, devnumber, feature_index_function, params=b'', features=None):
|
def request(handle, devnumber, feature_index_function, params=b'', features=None):
|
||||||
"""Makes a feature call to a device and waits for a matching reply.
|
"""Makes a feature call to a device and waits for a matching reply.
|
||||||
|
@ -232,118 +220,93 @@ def request(handle, devnumber, feature_index_function, params=b'', features=None
|
||||||
if type(params) == int:
|
if type(params) == int:
|
||||||
params = _pack('!B', params)
|
params = _pack('!B', params)
|
||||||
|
|
||||||
# _log.debug("device %d request {%s} params [%s]", devnumber, _hex(feature_index_function), _hex(params))
|
# _log.debug("%s device %d request {%s} params [%s]", handle, devnumber, _hex(feature_index_function), _hex(params))
|
||||||
if len(feature_index_function) != 2:
|
if len(feature_index_function) != 2:
|
||||||
raise ValueError('invalid feature_index_function {%s}: it must be a two-byte string' % _hex(feature_index_function))
|
raise ValueError('invalid feature_index_function {%s}: it must be a two-byte string' % _hex(feature_index_function))
|
||||||
|
|
||||||
if request_context is None or handle != request_context.handle:
|
_skip_incoming(handle)
|
||||||
context = _DEFAULT_REQUEST_CONTEXT
|
ihandle = int(handle)
|
||||||
_unhandled = unhandled_hook
|
write(ihandle, devnumber, feature_index_function + params)
|
||||||
else:
|
|
||||||
context = request_context
|
|
||||||
_unhandled = getattr(context, 'unhandled_hook')
|
|
||||||
|
|
||||||
context.write(handle, devnumber, feature_index_function + params)
|
while True:
|
||||||
|
now = _timestamp()
|
||||||
|
reply = read(ihandle, DEFAULT_TIMEOUT)
|
||||||
|
delta = _timestamp() - now
|
||||||
|
|
||||||
read_times = _MAX_READ_TIMES
|
if reply:
|
||||||
while read_times > 0:
|
reply_code, reply_devnumber, reply_data = reply
|
||||||
divisor = (1 + _MAX_READ_TIMES - read_times)
|
if reply_devnumber == devnumber:
|
||||||
reply = context.read(handle, int(DEFAULT_TIMEOUT * (divisor + 1) / 2 / divisor))
|
if reply_code == 0x10 and reply_data[:1] == b'\x8F' and reply_data[1:3] == feature_index_function:
|
||||||
read_times -= 1
|
# device not present
|
||||||
|
_log.debug("device %d request failed on {%s} call: [%s]", devnumber, _hex(feature_index_function), _hex(reply_data))
|
||||||
|
return None
|
||||||
|
|
||||||
if not reply:
|
if reply_code == 0x10 and reply_data[:1] == b'\x8F':
|
||||||
# keep waiting...
|
# device not present
|
||||||
continue
|
_log.debug("device %d request failed: [%s]", devnumber, _hex(reply_data))
|
||||||
|
return None
|
||||||
|
|
||||||
reply_code, reply_devnumber, reply_data = reply
|
if reply_code == 0x11 and reply_data[0] == b'\xFF' and reply_data[1:3] == feature_index_function:
|
||||||
|
# the feature call returned with an error
|
||||||
|
error_code = ord(reply_data[3])
|
||||||
|
_log.warn("device %d request feature call error %d = %s: %s", devnumber, error_code, ERROR_NAME[error_code], _hex(reply_data))
|
||||||
|
feature_index = ord(feature_index_function[:1])
|
||||||
|
feature_function = feature_index_function[1:2]
|
||||||
|
feature = None if features is None else features[feature_index] if feature_index < len(features) else None
|
||||||
|
raise _FeatureCallError(devnumber, feature, feature_index, feature_function, error_code, reply_data)
|
||||||
|
|
||||||
if reply_devnumber != devnumber:
|
if reply_code == 0x11 and reply_data[:2] == feature_index_function:
|
||||||
# this message not for the device we're interested in
|
# a matching reply
|
||||||
# _l.log(_LOG_LEVEL, "device %d request got reply for unexpected device %d: [%s]", devnumber, reply_devnumber, _hex(reply_data))
|
# _log.debug("device %d matched reply with feature-index-function [%s]", devnumber, _hex(reply_data[2:]))
|
||||||
# worst case scenario, this is a reply for a concurrent request
|
return reply_data[2:]
|
||||||
# on this receiver
|
|
||||||
if _unhandled:
|
|
||||||
_unhandled(reply_code, reply_devnumber, reply_data)
|
|
||||||
continue
|
|
||||||
|
|
||||||
if reply_code == 0x10 and reply_data[:1] == b'\x8F' and reply_data[1:3] == feature_index_function:
|
if reply_code == 0x10 and devnumber == 0xFF and reply_data[:2] == feature_index_function:
|
||||||
# device not present
|
# direct calls to the receiver (device 0xFF) may also return successfully with reply code 0x10
|
||||||
_log.debug("device %d request failed on {%s} call: [%s]", devnumber, _hex(feature_index_function), _hex(reply_data))
|
# _log.debug("device %d matched reply with feature-index-function [%s]", devnumber, _hex(reply_data[2:]))
|
||||||
|
return reply_data[2:]
|
||||||
|
|
||||||
|
if unhandled_hook:
|
||||||
|
unhandled_hook(reply_code, reply_devnumber, reply_data)
|
||||||
|
|
||||||
|
if delta >= DEFAULT_TIMEOUT:
|
||||||
|
_log.warn("timeout on device %d request {%s} params[%s]", devnumber, _hex(feature_index_function), _hex(params))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if reply_code == 0x10 and reply_data[:1] == b'\x8F':
|
|
||||||
# device not present
|
|
||||||
_log.debug("device %d request failed: [%s]", devnumber, _hex(reply_data))
|
|
||||||
return None
|
|
||||||
|
|
||||||
if reply_code == 0x11 and reply_data[0] == b'\xFF' and reply_data[1:3] == feature_index_function:
|
|
||||||
# the feature call returned with an error
|
|
||||||
error_code = ord(reply_data[3])
|
|
||||||
_log.warn("device %d request feature call error %d = %s: %s", devnumber, error_code, ERROR_NAME[error_code], _hex(reply_data))
|
|
||||||
feature_index = ord(feature_index_function[:1])
|
|
||||||
feature_function = feature_index_function[1:2]
|
|
||||||
feature = None if features is None else features[feature_index] if feature_index < len(features) else None
|
|
||||||
raise _FeatureCallError(devnumber, feature, feature_index, feature_function, error_code, reply_data)
|
|
||||||
|
|
||||||
if reply_code == 0x11 and reply_data[:2] == feature_index_function:
|
|
||||||
# a matching reply
|
|
||||||
# _log.debug("device %d matched reply with feature-index-function [%s]", devnumber, _hex(reply_data[2:]))
|
|
||||||
return reply_data[2:]
|
|
||||||
|
|
||||||
if reply_code == 0x10 and devnumber == 0xFF and reply_data[:2] == feature_index_function:
|
|
||||||
# direct calls to the receiver (device 0xFF) may also return successfully with reply code 0x10
|
|
||||||
# _log.debug("device %d matched reply with feature-index-function [%s]", devnumber, _hex(reply_data[2:]))
|
|
||||||
return reply_data[2:]
|
|
||||||
|
|
||||||
# _log.debug("device %d unmatched reply {%s} (expected {%s})", devnumber, _hex(reply_data[:2]), _hex(feature_index_function))
|
|
||||||
if _unhandled:
|
|
||||||
_unhandled(reply_code, reply_devnumber, reply_data)
|
|
||||||
|
|
||||||
|
|
||||||
def ping(handle, devnumber):
|
def ping(handle, devnumber):
|
||||||
"""Check if a device is connected to the UR.
|
"""Check if a device is connected to the UR.
|
||||||
|
|
||||||
:returns: The HID protocol supported by the device, as a floating point number, if the device is active.
|
:returns: The HID protocol supported by the device, as a floating point number, if the device is active.
|
||||||
"""
|
"""
|
||||||
if request_context is None or handle != request_context.handle:
|
_log.debug("%s pinging device %d", handle, devnumber)
|
||||||
context = _DEFAULT_REQUEST_CONTEXT
|
|
||||||
_unhandled = unhandled_hook
|
|
||||||
else:
|
|
||||||
context = request_context
|
|
||||||
_unhandled = getattr(context, 'unhandled_hook')
|
|
||||||
|
|
||||||
context.write(handle, devnumber, b'\x00\x11\x00\x00\xAA')
|
_skip_incoming(handle)
|
||||||
read_times = _MAX_READ_TIMES
|
ihandle = int(handle)
|
||||||
while read_times > 0:
|
write(ihandle, devnumber, b'\x00\x11\x00\x00\xAA')
|
||||||
divisor = (1 + _MAX_READ_TIMES - read_times)
|
|
||||||
reply = context.read(handle, int(DEFAULT_TIMEOUT * (divisor + 1) / 2 / divisor))
|
|
||||||
read_times -= 1
|
|
||||||
|
|
||||||
if not reply:
|
while True:
|
||||||
# keep waiting...
|
now = _timestamp()
|
||||||
continue
|
reply = read(ihandle, DEFAULT_TIMEOUT)
|
||||||
|
delta = _timestamp() - now
|
||||||
|
|
||||||
reply_code, reply_devnumber, reply_data = reply
|
if reply:
|
||||||
|
reply_code, reply_devnumber, reply_data = reply
|
||||||
|
if reply_devnumber == devnumber:
|
||||||
|
if reply_code == 0x11 and reply_data[:2] == b'\x00\x11' and reply_data[4:5] == b'\xAA':
|
||||||
|
# HID 2.0+ device, currently connected
|
||||||
|
return ord(reply_data[2:3]) + ord(reply_data[3:4]) / 10.0
|
||||||
|
|
||||||
if reply_devnumber != devnumber:
|
if reply_code == 0x10 and reply_data == b'\x8F\x00\x11\x01\x00':
|
||||||
# this message not for the device we're interested in
|
# HID 1.0 device, currently connected
|
||||||
# _l.log(_LOG_LEVEL, "device %d request got reply for unexpected device %d: [%s]", devnumber, reply_devnumber, _hex(reply_data))
|
return 1.0
|
||||||
# worst case scenario, this is a reply for a concurrent request
|
|
||||||
# on this receiver
|
|
||||||
if _unhandled:
|
|
||||||
_unhandled(reply_code, reply_devnumber, reply_data)
|
|
||||||
continue
|
|
||||||
|
|
||||||
if reply_code == 0x11 and reply_data[:2] == b'\x00\x11' and reply_data[4:5] == b'\xAA':
|
if reply_code == 0x10 and reply_data[:3] == b'\x8F\x00\x11':
|
||||||
# HID 2.0+ device, currently connected
|
# a disconnected device
|
||||||
return ord(reply_data[2:3]) + ord(reply_data[3:4]) / 10.0
|
return None
|
||||||
|
|
||||||
if reply_code == 0x10 and reply_data == b'\x8F\x00\x11\x01\x00':
|
if unhandled_hook:
|
||||||
# HID 1.0 device, currently connected
|
unhandled_hook(reply_code, reply_devnumber, reply_data)
|
||||||
return 1.0
|
|
||||||
|
|
||||||
if reply_code == 0x10 and reply_data[:3] == b'\x8F\x00\x11':
|
if delta >= DEFAULT_TIMEOUT:
|
||||||
# a disconnected device
|
_log.warn("timeout on device %d ping", devnumber)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
_log.warn("don't know how to interpret ping reply %s", reply)
|
|
||||||
|
|
|
@ -65,6 +65,7 @@ FIRMWARE_KIND = FallbackDict(lambda x: 'Unknown', list2dict(_FIRMWARE_KINDS))
|
||||||
_BATTERY_STATUSES = ('Discharging (in use)', 'Recharging', 'Almost full',
|
_BATTERY_STATUSES = ('Discharging (in use)', 'Recharging', 'Almost full',
|
||||||
'Full', 'Slow recharge', 'Invalid battery', 'Thermal error',
|
'Full', 'Slow recharge', 'Invalid battery', 'Thermal error',
|
||||||
'Charging error')
|
'Charging error')
|
||||||
|
BATTERY_OK = lambda status: status < 5
|
||||||
|
|
||||||
"""Names for possible battery status values."""
|
"""Names for possible battery status values."""
|
||||||
BATTERY_STATUS = FallbackDict(lambda x: 'unknown', list2dict(_BATTERY_STATUSES))
|
BATTERY_STATUS = FallbackDict(lambda x: 'unknown', list2dict(_BATTERY_STATUSES))
|
||||||
|
|
|
@ -2,12 +2,12 @@
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
|
|
||||||
from threading import Thread as _Thread
|
import threading as _threading
|
||||||
# from time import sleep as _sleep
|
|
||||||
|
|
||||||
from . import base as _base
|
from . import base as _base
|
||||||
from .exceptions import NoReceiver as _NoReceiver
|
from .exceptions import NoReceiver as _NoReceiver
|
||||||
from .common import Packet as _Packet
|
from .common import Packet as _Packet
|
||||||
|
from .constants import MAX_ATTACHED_DEVICES as _MAX_ATTACHED_DEVICES
|
||||||
|
|
||||||
# for both Python 2 and 3
|
# for both Python 2 and 3
|
||||||
try:
|
try:
|
||||||
|
@ -21,26 +21,10 @@ _log = getLogger('LUR').getChild('listener')
|
||||||
del getLogger
|
del getLogger
|
||||||
|
|
||||||
|
|
||||||
_READ_EVENT_TIMEOUT = int(_base.DEFAULT_TIMEOUT / 2) # ms
|
class EventsListener(_threading.Thread):
|
||||||
|
|
||||||
def _event_dispatch(listener, callback):
|
|
||||||
while listener._active: # or not listener._events.empty():
|
|
||||||
try:
|
|
||||||
event = listener._events.get(True, _READ_EVENT_TIMEOUT * 10)
|
|
||||||
except:
|
|
||||||
continue
|
|
||||||
# _log.debug("delivering event %s", event)
|
|
||||||
try:
|
|
||||||
callback(event)
|
|
||||||
except:
|
|
||||||
_log.exception("callback for %s", event)
|
|
||||||
|
|
||||||
|
|
||||||
class EventsListener(_Thread):
|
|
||||||
"""Listener thread for events from the Unifying Receiver.
|
"""Listener thread for events from the Unifying Receiver.
|
||||||
|
|
||||||
Incoming packets will be passed to the callback function in sequence, by a
|
Incoming packets will be passed to the callback function in sequence.
|
||||||
separate thread.
|
|
||||||
"""
|
"""
|
||||||
def __init__(self, receiver_handle, events_callback):
|
def __init__(self, receiver_handle, events_callback):
|
||||||
super(EventsListener, self).__init__(group='Unifying Receiver', name=self.__class__.__name__)
|
super(EventsListener, self).__init__(group='Unifying Receiver', name=self.__class__.__name__)
|
||||||
|
@ -49,92 +33,65 @@ class EventsListener(_Thread):
|
||||||
self._active = False
|
self._active = False
|
||||||
|
|
||||||
self._handle = receiver_handle
|
self._handle = receiver_handle
|
||||||
|
self._queued_events = _Queue(32)
|
||||||
self._tasks = _Queue(1)
|
self._events_callback = events_callback
|
||||||
self._backup_unhandled_hook = _base.unhandled_hook
|
|
||||||
_base.unhandled_hook = self.unhandled_hook
|
|
||||||
|
|
||||||
self._events = _Queue(32)
|
|
||||||
self._dispatcher = _Thread(group='Unifying Receiver',
|
|
||||||
name=self.__class__.__name__ + '-dispatch',
|
|
||||||
target=_event_dispatch, args=(self, events_callback))
|
|
||||||
self._dispatcher.daemon = True
|
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
self._active = True
|
self._active = True
|
||||||
_log.debug("started")
|
_base.unhandled_hook = self._unhandled_hook
|
||||||
_base.request_context = self
|
ihandle = int(self._handle)
|
||||||
_base.unhandled_hook = self._backup_unhandled_hook
|
_log.info("started with %s (%d)", repr(self._handle), ihandle)
|
||||||
del self._backup_unhandled_hook
|
|
||||||
|
|
||||||
self._dispatcher.start()
|
|
||||||
|
|
||||||
while self._active:
|
while self._active:
|
||||||
try:
|
if self._queued_events.empty():
|
||||||
# _log.debug("read next event")
|
try:
|
||||||
event = _base.read(self._handle, _READ_EVENT_TIMEOUT)
|
# _log.debug("read next event")
|
||||||
except _NoReceiver:
|
event = _base.read(ihandle)
|
||||||
self._handle = 0
|
# shortcut: we should only be looking at events for proper device numbers
|
||||||
_log.warn("receiver disconnected")
|
except _NoReceiver:
|
||||||
self._events.put(_Packet(0xFF, 0xFF, None))
|
self._active = False
|
||||||
self._active = False
|
self._handle = None
|
||||||
|
_log.warning("receiver disconnected")
|
||||||
|
event = (0xFF, 0xFF, None)
|
||||||
else:
|
else:
|
||||||
if event is not None:
|
# deliver any queued events
|
||||||
matched = False
|
event = self._queued_events.get()
|
||||||
task = None if self._tasks.empty() else self._tasks.queue[0]
|
|
||||||
if task and task[-1] is None:
|
|
||||||
task_dev, task_data = task[:2]
|
|
||||||
if event[1] == task_dev:
|
|
||||||
# _log.debug("matching %s to (%d, %s)", event, task_dev, repr(task_data))
|
|
||||||
matched = event[2][:2] == task_data[:2] or (event[2][:1] in b'\x8F\xFF' and event[2][1:3] == task_data[:2])
|
|
||||||
|
|
||||||
if matched:
|
if event:
|
||||||
# _log.debug("request reply %s", event)
|
event = _Packet(*event)
|
||||||
task[-1] = event
|
# _log.debug("processing event %s", event)
|
||||||
self._tasks.task_done()
|
try:
|
||||||
else:
|
self._events_callback(event)
|
||||||
event = _Packet(*event)
|
except:
|
||||||
_log.info("queueing event %s", event)
|
_log.exception("processing event %s", event)
|
||||||
self._events.put(event)
|
|
||||||
|
|
||||||
_base.request_context = None
|
_base.unhandled_hook = None
|
||||||
handle, self._handle = self._handle, 0
|
handle, self._handle = self._handle, None
|
||||||
_base.close(handle)
|
if handle:
|
||||||
_log.debug("stopped")
|
_base.close(handle)
|
||||||
|
_log.info("stopped %s", repr(handle))
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
"""Tells the listener to stop as soon as possible."""
|
"""Tells the listener to stop as soon as possible."""
|
||||||
if self._active:
|
if self._active:
|
||||||
_log.debug("stopping")
|
_log.debug("stopping")
|
||||||
self._active = False
|
self._active = False
|
||||||
# wait for the receiver handle to be closed
|
handle, self._handle = self._handle, None
|
||||||
self.join()
|
if handle:
|
||||||
|
_base.close(handle)
|
||||||
|
_log.info("stopped %s", repr(handle))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def handle(self):
|
def handle(self):
|
||||||
return self._handle
|
return self._handle
|
||||||
|
|
||||||
def write(self, handle, devnumber, data):
|
def _unhandled_hook(self, reply_code, devnumber, data):
|
||||||
assert handle == self._handle
|
# only consider unhandled events that were sent from this thread,
|
||||||
# _log.debug("write %02X %s", devnumber, _base._hex(data))
|
# i.e. triggered during a callback of a previous event
|
||||||
task = [devnumber, data, None]
|
if _threading.current_thread() == self:
|
||||||
self._tasks.put(task)
|
event = _Packet(reply_code, devnumber, data)
|
||||||
_base.write(self._handle, devnumber, data)
|
_log.info("queueing unhandled event %s", event)
|
||||||
# _log.debug("task queued %s", task)
|
self._queued_events.put(event)
|
||||||
|
|
||||||
def read(self, handle, timeout=_base.DEFAULT_TIMEOUT):
|
|
||||||
assert handle == self._handle
|
|
||||||
# _log.debug("read %d", timeout)
|
|
||||||
assert not self._tasks.empty()
|
|
||||||
self._tasks.join()
|
|
||||||
task = self._tasks.get(False)
|
|
||||||
# _log.debug("task ready %s", task)
|
|
||||||
return task[-1]
|
|
||||||
|
|
||||||
def unhandled_hook(self, reply_code, devnumber, data):
|
|
||||||
event = _Packet(reply_code, devnumber, data)
|
|
||||||
_log.info("queueing unhandled event %s", event)
|
|
||||||
self._events.put(event)
|
|
||||||
|
|
||||||
def __bool__(self):
|
def __bool__(self):
|
||||||
return bool(self._active and self._handle)
|
return bool(self._active and self._handle)
|
||||||
|
|
|
@ -30,16 +30,16 @@ class Test_UR_Base(unittest.TestCase):
|
||||||
# self.assertIsInstance(rawdevices, Iterable, "list_receiver_devices should have returned an iterable")
|
# self.assertIsInstance(rawdevices, Iterable, "list_receiver_devices should have returned an iterable")
|
||||||
Test_UR_Base.ur_available = len(list(rawdevices)) > 0
|
Test_UR_Base.ur_available = len(list(rawdevices)) > 0
|
||||||
|
|
||||||
def test_20_try_open(self):
|
def test_20_open_path(self):
|
||||||
if not self.ur_available:
|
if not self.ur_available:
|
||||||
self.fail("No receiver found")
|
self.fail("No receiver found")
|
||||||
|
|
||||||
for rawdevice in base.list_receiver_devices():
|
for rawdevice in base.list_receiver_devices():
|
||||||
handle = base.try_open(rawdevice.path)
|
handle = base.open_path(rawdevice.path)
|
||||||
if handle is None:
|
if handle is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
self.assertIsInstance(handle, int, "try_open should have returned an int")
|
self.assertIsInstance(handle, int, "open_path should have returned an int")
|
||||||
|
|
||||||
if Test_UR_Base.handle is None:
|
if Test_UR_Base.handle is None:
|
||||||
Test_UR_Base.handle = handle
|
Test_UR_Base.handle = handle
|
||||||
|
@ -131,46 +131,46 @@ class Test_UR_Base(unittest.TestCase):
|
||||||
index = reply[:1]
|
index = reply[:1]
|
||||||
self.assertGreater(index, b'\x00', "FEATURE_SET not available on device " + str(self.device))
|
self.assertGreater(index, b'\x00', "FEATURE_SET not available on device " + str(self.device))
|
||||||
|
|
||||||
def test_57_request_ignore_undhandled(self):
|
# def test_57_request_ignore_undhandled(self):
|
||||||
if self.handle is None:
|
# if self.handle is None:
|
||||||
self.fail("No receiver found")
|
# self.fail("No receiver found")
|
||||||
if self.device is None:
|
# if self.device is None:
|
||||||
self.fail("No devices attached")
|
# self.fail("No devices attached")
|
||||||
|
|
||||||
fs_index = base.request(self.handle, self.device, FEATURE.ROOT, FEATURE.FEATURE_SET)
|
# fs_index = base.request(self.handle, self.device, FEATURE.ROOT, FEATURE.FEATURE_SET)
|
||||||
self.assertIsNotNone(fs_index)
|
# self.assertIsNotNone(fs_index)
|
||||||
fs_index = fs_index[:1]
|
# fs_index = fs_index[:1]
|
||||||
self.assertGreater(fs_index, b'\x00')
|
# self.assertGreater(fs_index, b'\x00')
|
||||||
|
|
||||||
global received_unhandled
|
# global received_unhandled
|
||||||
received_unhandled = None
|
# received_unhandled = None
|
||||||
|
|
||||||
def _unhandled(code, device, data):
|
# def _unhandled(code, device, data):
|
||||||
self.assertIsNotNone(code)
|
# self.assertIsNotNone(code)
|
||||||
self.assertIsInstance(code, int)
|
# self.assertIsInstance(code, int)
|
||||||
self.assertIsNotNone(device)
|
# self.assertIsNotNone(device)
|
||||||
self.assertIsInstance(device, int)
|
# self.assertIsInstance(device, int)
|
||||||
self.assertIsNotNone(data)
|
# self.assertIsNotNone(data)
|
||||||
self.assertIsInstance(data, str)
|
# self.assertIsInstance(data, str)
|
||||||
global received_unhandled
|
# global received_unhandled
|
||||||
received_unhandled = (code, device, data)
|
# received_unhandled = (code, device, data)
|
||||||
|
|
||||||
base.unhandled_hook = _unhandled
|
# base.unhandled_hook = _unhandled
|
||||||
base.write(self.handle, self.device, FEATURE.ROOT + FEATURE.FEATURE_SET)
|
# base.write(self.handle, self.device, FEATURE.ROOT + FEATURE.FEATURE_SET)
|
||||||
reply = base.request(self.handle, self.device, fs_index + b'\x00')
|
# reply = base.request(self.handle, self.device, fs_index + b'\x00')
|
||||||
self.assertIsNotNone(reply, "request returned None reply")
|
# self.assertIsNotNone(reply, "request returned None reply")
|
||||||
self.assertNotEquals(reply[:1], b'\x00')
|
# self.assertNotEquals(reply[:1], b'\x00')
|
||||||
self.assertIsNotNone(received_unhandled, "extra message not received by unhandled hook")
|
# self.assertIsNotNone(received_unhandled, "extra message not received by unhandled hook")
|
||||||
|
|
||||||
received_unhandled = None
|
# received_unhandled = None
|
||||||
base.unhandled_hook = None
|
# base.unhandled_hook = None
|
||||||
base.write(self.handle, self.device, FEATURE.ROOT + FEATURE.FEATURE_SET)
|
# base.write(self.handle, self.device, FEATURE.ROOT + FEATURE.FEATURE_SET)
|
||||||
reply = base.request(self.handle, self.device, fs_index + b'\x00')
|
# reply = base.request(self.handle, self.device, fs_index + b'\x00')
|
||||||
self.assertIsNotNone(reply, "request returned None reply")
|
# self.assertIsNotNone(reply, "request returned None reply")
|
||||||
self.assertNotEquals(reply[:1], b'\x00')
|
# self.assertNotEquals(reply[:1], b'\x00')
|
||||||
self.assertIsNone(received_unhandled)
|
# self.assertIsNone(received_unhandled)
|
||||||
|
|
||||||
del received_unhandled
|
# del received_unhandled
|
||||||
|
|
||||||
# def test_90_receiver_missing(self):
|
# def test_90_receiver_missing(self):
|
||||||
# if self.handle is None:
|
# if self.handle is None:
|
||||||
|
|
|
@ -41,7 +41,8 @@ class Test_UR_API(unittest.TestCase):
|
||||||
def test_05_ping_device_zero(self):
|
def test_05_ping_device_zero(self):
|
||||||
self._check(check_device=False)
|
self._check(check_device=False)
|
||||||
|
|
||||||
ok = api.ping(self.receiver.handle, 0)
|
d = api.PairedDevice(self.receiver.handle, 0)
|
||||||
|
ok = d.ping()
|
||||||
self.assertIsNotNone(ok, "invalid ping reply")
|
self.assertIsNotNone(ok, "invalid ping reply")
|
||||||
self.assertFalse(ok, "device zero replied")
|
self.assertFalse(ok, "device zero replied")
|
||||||
|
|
||||||
|
@ -51,7 +52,8 @@ class Test_UR_API(unittest.TestCase):
|
||||||
devices = []
|
devices = []
|
||||||
|
|
||||||
for devnumber in range(1, 1 + MAX_ATTACHED_DEVICES):
|
for devnumber in range(1, 1 + MAX_ATTACHED_DEVICES):
|
||||||
ok = api.ping(self.receiver.handle, devnumber)
|
d = api.PairedDevice(self.receiver.handle, devnumber)
|
||||||
|
ok = d.ping()
|
||||||
self.assertIsNotNone(ok, "invalid ping reply")
|
self.assertIsNotNone(ok, "invalid ping reply")
|
||||||
if ok:
|
if ok:
|
||||||
devices.append(self.receiver[devnumber])
|
devices.append(self.receiver[devnumber])
|
||||||
|
|
Loading…
Reference in New Issue