Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fb8cf26c51 | ||
|
|
41db725e15 | ||
|
|
f25d2ba183 | ||
|
|
66531635bc | ||
|
|
4c5cf85091 | ||
|
|
6db4deafee | ||
|
|
2c312c1a5b | ||
|
|
bcc2bf123e | ||
|
|
50fedab19e | ||
|
|
d0ccd3e9c2 | ||
|
|
4b2d8a8d5a | ||
|
|
c12364a7c7 | ||
|
|
560400e786 | ||
|
|
f7a4d89467 |
@@ -64,6 +64,8 @@ class State(object):
|
||||
if event.data == b'\x4A\x00\x01\x00\x00':
|
||||
_l.debug("receiver gave up")
|
||||
self.success = False
|
||||
# self.success = True
|
||||
# self.detected_device = self.listener.receiver.devices[1]
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -78,5 +80,4 @@ class State(object):
|
||||
return True
|
||||
|
||||
def unpair(self, device):
|
||||
_l.debug("unpair %s", device)
|
||||
self.listener.unpair_device(device)
|
||||
return self.listener.unpair_device(device)
|
||||
|
||||
151
app/receiver.py
@@ -4,7 +4,7 @@
|
||||
|
||||
from logging import getLogger as _Logger
|
||||
from struct import pack as _pack
|
||||
from time import sleep as _sleep
|
||||
from time import time as _timestamp
|
||||
|
||||
from logitech.unifying_receiver import base as _base
|
||||
from logitech.unifying_receiver import api as _api
|
||||
@@ -21,17 +21,26 @@ class _FeaturesArray(object):
|
||||
__slots__ = ('device', 'features', 'supported')
|
||||
|
||||
def __init__(self, device):
|
||||
assert device is not None
|
||||
self.device = device
|
||||
self.features = None
|
||||
self.supported = True
|
||||
|
||||
def __del__(self):
|
||||
self.supported = False
|
||||
self.device = None
|
||||
|
||||
def _check(self):
|
||||
# print ("%s check" % self.device)
|
||||
if self.supported:
|
||||
if self.features is not None:
|
||||
return True
|
||||
|
||||
if self.device.protocol < 2.0:
|
||||
return False
|
||||
|
||||
if self.device.status >= STATUS.CONNECTED:
|
||||
handle = self.device.handle
|
||||
handle = int(self.device.handle)
|
||||
try:
|
||||
index = _api.get_feature_index(handle, self.device.number, _api.FEATURE.FEATURE_SET)
|
||||
except _api._FeatureNotSupported:
|
||||
@@ -57,9 +66,13 @@ class _FeaturesArray(object):
|
||||
|
||||
if index < 0 or index >= len(self.features):
|
||||
raise IndexError
|
||||
|
||||
if self.features[index] is None:
|
||||
# print ("features getitem at %d" % index)
|
||||
fs_index = self.features.index(_api.FEATURE.FEATURE_SET)
|
||||
feature = _base.request(self.device.handle, self.device.number, _pack('!BB', fs_index, 0x10), _pack('!B', index))
|
||||
# technically fs_function is 0x10 for this call, but we add the index to differentiate possibly conflicting requests
|
||||
fs_function = 0x10 | (index & 0x0F)
|
||||
feature = _base.request(self.device.handle, self.device.number, _pack('!BB', fs_index, fs_function), _pack('!B', index))
|
||||
if feature is not None:
|
||||
self.features[index] = feature[:2]
|
||||
|
||||
@@ -70,11 +83,13 @@ class _FeaturesArray(object):
|
||||
if value in self.features:
|
||||
return True
|
||||
|
||||
# print ("features contains %s" % repr(value))
|
||||
for index in range(0, len(self.features)):
|
||||
f = self.features[index] or self.__getitem__(index)
|
||||
assert f is not None
|
||||
if f == value:
|
||||
return True
|
||||
# we know the features are ordered by value
|
||||
if f > value:
|
||||
break
|
||||
|
||||
@@ -105,23 +120,22 @@ class _FeaturesArray(object):
|
||||
class DeviceInfo(_api.PairedDevice):
|
||||
"""A device attached to the receiver.
|
||||
"""
|
||||
def __init__(self, listener, number, status=STATUS.UNKNOWN):
|
||||
super(DeviceInfo, self).__init__(listener.handle, number)
|
||||
self._features = _FeaturesArray(self)
|
||||
|
||||
self.LOG = _Logger("Device[%d]" % number)
|
||||
self._listener = listener
|
||||
def __init__(self, handle, number, status_changed_callback, status=STATUS.BOOTING):
|
||||
super(DeviceInfo, self).__init__(handle, number)
|
||||
self.LOG = _Logger("Device[%d]" % (number))
|
||||
|
||||
assert status_changed_callback
|
||||
self.status_changed_callback = status_changed_callback
|
||||
self._status = status
|
||||
self.status_updated = _timestamp()
|
||||
self.props = {}
|
||||
|
||||
# read them now, otherwise it it temporarily hang the UI
|
||||
# if status >= STATUS.CONNECTED:
|
||||
# n, k, s, f = self.name, self.kind, self.serial, self.firmware
|
||||
self._features = _FeaturesArray(self)
|
||||
|
||||
@property
|
||||
def receiver(self):
|
||||
return self._listener.receiver
|
||||
def __del__(self):
|
||||
super(ReceiverListener, self).__del__()
|
||||
self._features.supported = False
|
||||
self._features.device = None
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
@@ -129,28 +143,38 @@ class DeviceInfo(_api.PairedDevice):
|
||||
|
||||
@status.setter
|
||||
def status(self, new_status):
|
||||
if new_status != self._status and not (new_status == STATUS.CONNECTED and self._status > new_status):
|
||||
self.LOG.debug("status %d => %d", self._status, new_status)
|
||||
urgent = new_status < STATUS.CONNECTED or self._status < STATUS.CONNECTED
|
||||
self._status = new_status
|
||||
self._listener.status_changed(self, urgent)
|
||||
|
||||
if new_status < STATUS.CONNECTED:
|
||||
self.props.clear()
|
||||
for p in list(self.props):
|
||||
if p != PROPS.BATTERY_LEVEL:
|
||||
del self.props[p]
|
||||
else:
|
||||
self._features._check()
|
||||
self.protocol, self.codename, self.name, self.kind
|
||||
|
||||
self.status_updated = _timestamp()
|
||||
old_status = self._status
|
||||
if new_status != old_status and not (new_status == STATUS.CONNECTED and old_status > new_status):
|
||||
self.LOG.debug("status %d => %d", old_status, new_status)
|
||||
self._status = new_status
|
||||
ui_flags = STATUS.UI_NOTIFY if new_status == STATUS.UNPAIRED else 0
|
||||
self.status_changed_callback(self, ui_flags)
|
||||
|
||||
@property
|
||||
def status_text(self):
|
||||
if self._status < STATUS.CONNECTED:
|
||||
return STATUS_NAME[self._status]
|
||||
return STATUS_NAME[STATUS.CONNECTED]
|
||||
|
||||
@property
|
||||
def properties_text(self):
|
||||
t = []
|
||||
if self.props.get(PROPS.BATTERY_LEVEL):
|
||||
if self.props.get(PROPS.BATTERY_LEVEL) is not None:
|
||||
t.append('Battery: %d%%' % self.props[PROPS.BATTERY_LEVEL])
|
||||
if self.props.get(PROPS.BATTERY_STATUS):
|
||||
if self.props.get(PROPS.BATTERY_STATUS) is not None:
|
||||
t.append(self.props[PROPS.BATTERY_STATUS])
|
||||
if self.props.get(PROPS.LIGHT_LEVEL):
|
||||
if self.props.get(PROPS.LIGHT_LEVEL) is not None:
|
||||
t.append('Light: %d lux' % self.props[PROPS.LIGHT_LEVEL])
|
||||
return ', '.join(t) if t else STATUS_NAME[STATUS.CONNECTED]
|
||||
return ', '.join(t)
|
||||
|
||||
def process_event(self, code, data):
|
||||
if code == 0x10 and data[:1] == b'\x8F':
|
||||
@@ -165,13 +189,13 @@ class DeviceInfo(_api.PairedDevice):
|
||||
return True
|
||||
|
||||
if type(status) == tuple:
|
||||
p = dict(self.props)
|
||||
self.props.update(status[1])
|
||||
if self.status == status[0]:
|
||||
if p != self.props:
|
||||
self._listener.status_changed(self)
|
||||
else:
|
||||
self.status = status[0]
|
||||
new_status, new_props = status
|
||||
ui_flags = new_props.pop(PROPS.UI_FLAGS, 0)
|
||||
old_props = dict(self.props)
|
||||
self.props.update(new_props)
|
||||
self.status = new_status
|
||||
if ui_flags or old_props != self.props:
|
||||
self.status_changed_callback(self, ui_flags)
|
||||
return True
|
||||
|
||||
self.LOG.warn("don't know how to handle processed event status %s", status)
|
||||
@@ -179,7 +203,7 @@ class DeviceInfo(_api.PairedDevice):
|
||||
return False
|
||||
|
||||
def __str__(self):
|
||||
return '<DeviceInfo(%d,%s,%d)>' % (self.number, self._name or '?', self._status)
|
||||
return '<DeviceInfo(%s,%d,%s,%d)>' % (self.handle, self.number, self.codename or '?', self._status)
|
||||
|
||||
#
|
||||
#
|
||||
@@ -188,7 +212,7 @@ class DeviceInfo(_api.PairedDevice):
|
||||
_RECEIVER_STATUS_NAME = _FallbackDict(
|
||||
lambda x:
|
||||
'1 device found' if x == STATUS.CONNECTED + 1 else
|
||||
'%d devices found' if x > STATUS.CONNECTED else
|
||||
('%d devices found' % x) if x > STATUS.CONNECTED else
|
||||
'?',
|
||||
{
|
||||
STATUS.UNKNOWN: 'Initializing...',
|
||||
@@ -201,16 +225,13 @@ _RECEIVER_STATUS_NAME = _FallbackDict(
|
||||
class ReceiverListener(_EventsListener):
|
||||
"""Keeps the status of a Unifying Receiver.
|
||||
"""
|
||||
|
||||
def __init__(self, receiver, status_changed_callback=None):
|
||||
super(ReceiverListener, self).__init__(receiver.handle, self._events_handler)
|
||||
self.LOG = _Logger("Receiver[%s]" % receiver.path)
|
||||
|
||||
self.receiver = receiver
|
||||
|
||||
self.LOG = _Logger("ReceiverListener(%s)" % receiver.path)
|
||||
|
||||
self.events_filter = None
|
||||
self.events_handler = None
|
||||
|
||||
self.status_changed_callback = status_changed_callback
|
||||
|
||||
receiver.kind = receiver.name
|
||||
@@ -223,21 +244,28 @@ class ReceiverListener(_EventsListener):
|
||||
else:
|
||||
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))
|
||||
|
||||
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")
|
||||
else:
|
||||
self.LOG.warn("failed to trigger device events")
|
||||
return True
|
||||
self.LOG.warn("failed to trigger device events")
|
||||
|
||||
def change_status(self, new_status):
|
||||
if new_status != self.receiver.status:
|
||||
self.LOG.debug("status %d => %d", self.receiver.status, new_status)
|
||||
self.receiver.status = new_status
|
||||
self.receiver.status_text = _RECEIVER_STATUS_NAME[new_status]
|
||||
self.status_changed(None, True)
|
||||
self.status_changed(None, STATUS.UI_NOTIFY)
|
||||
|
||||
def status_changed(self, device=None, urgent=False):
|
||||
def status_changed(self, device=None, ui_flags=0):
|
||||
if self.status_changed_callback:
|
||||
self.status_changed_callback(self.receiver, device, urgent)
|
||||
self.status_changed_callback(self.receiver, device, ui_flags)
|
||||
|
||||
def _device_status_from(self, event):
|
||||
state_code = ord(event.data[2:3]) & 0xC0
|
||||
@@ -246,7 +274,7 @@ class ReceiverListener(_EventsListener):
|
||||
STATUS.CONNECTED if state_code == 0x00 else \
|
||||
None
|
||||
if state is None:
|
||||
self.LOG.warn("don't know how to handle state code 0x%02X: %s", state_code, event)
|
||||
self.LOG.warn("failed to identify status of device %d from 0x%02X: %s", event.devnumber, state_code, event)
|
||||
return state
|
||||
|
||||
def _events_handler(self, event):
|
||||
@@ -254,26 +282,20 @@ class ReceiverListener(_EventsListener):
|
||||
return
|
||||
|
||||
if event.code == 0x10 and event.data[0:2] == b'\x41\x04':
|
||||
|
||||
if event.devnumber in self.receiver.devices:
|
||||
status = self._device_status_from(event)
|
||||
if status is not None:
|
||||
self.receiver.devices[event.devnumber].status = status
|
||||
else:
|
||||
dev = self.make_device(event)
|
||||
if dev is None:
|
||||
self.LOG.warn("failed to make new device from %s", event)
|
||||
else:
|
||||
self.receiver.devices[event.devnumber] = dev
|
||||
self.change_status(STATUS.CONNECTED + len(self.receiver.devices))
|
||||
self.make_device(event)
|
||||
return
|
||||
|
||||
if event.devnumber == 0xFF:
|
||||
if event.code == 0xFF and event.data is None:
|
||||
# receiver disconnected
|
||||
self.LOG.warn("disconnected")
|
||||
self.receiver.devices = {}
|
||||
self.change_status(STATUS.UNAVAILABLE)
|
||||
self.receiver = None
|
||||
return
|
||||
elif event.devnumber in self.receiver.devices:
|
||||
dev = self.receiver.devices[event.devnumber]
|
||||
@@ -283,7 +305,7 @@ class ReceiverListener(_EventsListener):
|
||||
if self.events_handler and self.events_handler(event):
|
||||
return
|
||||
|
||||
self.LOG.warn("don't know how to handle event %s", event)
|
||||
# self.LOG.warn("don't know how to handle event %s", event)
|
||||
|
||||
def make_device(self, event):
|
||||
if event.devnumber < 1 or event.devnumber > self.receiver.max_devices:
|
||||
@@ -292,13 +314,16 @@ class ReceiverListener(_EventsListener):
|
||||
|
||||
status = self._device_status_from(event)
|
||||
if status is not None:
|
||||
dev = DeviceInfo(self, event.devnumber, status)
|
||||
dev = DeviceInfo(self.handle, event.devnumber, self.status_changed, status)
|
||||
self.LOG.info("new device %s", dev)
|
||||
self.status_changed(dev, True)
|
||||
dev.status = status
|
||||
self.status_changed(dev, STATUS.UI_NOTIFY)
|
||||
self.receiver.devices[event.devnumber] = dev
|
||||
self.change_status(STATUS.CONNECTED + len(self.receiver.devices))
|
||||
if status == STATUS.CONNECTED:
|
||||
dev.serial, dev.firmware
|
||||
return dev
|
||||
|
||||
self.LOG.error("failed to identify status of device %d from %s", event.devnumber, event)
|
||||
|
||||
def unpair_device(self, device):
|
||||
try:
|
||||
del self.receiver[device.number]
|
||||
@@ -313,16 +338,16 @@ class ReceiverListener(_EventsListener):
|
||||
return True
|
||||
|
||||
def __str__(self):
|
||||
return '<ReceiverListener(%s,%d)>' % (self.receiver.path, self.receiver.status)
|
||||
return '<ReceiverListener(%s,%d,%d)>' % (self.receiver.path, int(self.handle), self.receiver.status)
|
||||
|
||||
@classmethod
|
||||
def open(self, status_changed_callback=None):
|
||||
receiver = _api.Receiver.open()
|
||||
if receiver:
|
||||
handle = receiver.handle
|
||||
receiver.handle = _api.ThreadedHandle(handle, receiver.path)
|
||||
rl = ReceiverListener(receiver, status_changed_callback)
|
||||
rl.start()
|
||||
while not rl._active:
|
||||
_sleep(0.1)
|
||||
return rl
|
||||
|
||||
#
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/env python -u
|
||||
|
||||
NAME = 'Solaar'
|
||||
VERSION = '0.7.2'
|
||||
VERSION = '0.7.4'
|
||||
__author__ = "Daniel Pavel <daniel.pavel@gmail.com>"
|
||||
__version__ = VERSION
|
||||
__license__ = "GPL"
|
||||
@@ -13,6 +13,9 @@ __license__ = "GPL"
|
||||
def _parse_arguments():
|
||||
import argparse
|
||||
arg_parser = argparse.ArgumentParser(prog=NAME.lower())
|
||||
arg_parser.add_argument('-q', '--quiet',
|
||||
action='store_true',
|
||||
help='disable all logging, takes precedence over --verbose')
|
||||
arg_parser.add_argument('-v', '--verbose',
|
||||
action='count', default=0,
|
||||
help='increase the logger verbosity (may be repeated)')
|
||||
@@ -30,36 +33,31 @@ def _parse_arguments():
|
||||
args = arg_parser.parse_args()
|
||||
|
||||
import logging
|
||||
log_level = logging.WARNING - 10 * args.verbose
|
||||
log_format='%(asctime)s %(levelname)8s [%(threadName)s] %(name)s: %(message)s'
|
||||
logging.basicConfig(level=max(log_level, logging.DEBUG), format=log_format)
|
||||
if args.quiet:
|
||||
logging.root.addHandler(logging.NullHandler())
|
||||
logging.root.setLevel(logging.CRITICAL)
|
||||
else:
|
||||
log_level = logging.WARNING - 10 * args.verbose
|
||||
log_format='%(asctime)s %(levelname)8s [%(threadName)s] %(name)s: %(message)s'
|
||||
logging.basicConfig(level=max(log_level, logging.DEBUG), format=log_format)
|
||||
|
||||
return args
|
||||
|
||||
|
||||
def _check_requirements():
|
||||
def _require(module, package):
|
||||
try:
|
||||
import pyudev
|
||||
__import__(module)
|
||||
except ImportError:
|
||||
return 'python-pyudev'
|
||||
|
||||
try:
|
||||
import gi.repository
|
||||
except ImportError:
|
||||
return 'python-gi'
|
||||
|
||||
try:
|
||||
from gi.repository import Gtk
|
||||
except ImportError:
|
||||
return 'gir1.2-gtk-3.0'
|
||||
import sys
|
||||
sys.exit("%s: missing required package '%s'" % (NAME, package))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
args = _parse_arguments()
|
||||
_require('pyudev', 'python-pyudev')
|
||||
_require('gi.repository', 'python-gi')
|
||||
_require('gi.repository.Gtk', 'gir1.2-gtk-3.0')
|
||||
|
||||
req_fail = _check_requirements()
|
||||
if req_fail:
|
||||
raise ImportError('missing required package: %s' % req_fail)
|
||||
args = _parse_arguments()
|
||||
|
||||
import ui
|
||||
|
||||
@@ -81,14 +79,25 @@ if __name__ == '__main__':
|
||||
window.present()
|
||||
|
||||
import pairing
|
||||
from logitech.devices.constants import STATUS
|
||||
from gi.repository import Gtk, GObject
|
||||
|
||||
listener = None
|
||||
notify_missing = True
|
||||
|
||||
def status_changed(receiver, device=None, urgent=False):
|
||||
ui.update(receiver, icon, window, device)
|
||||
if ui.notify.available and urgent:
|
||||
def status_changed(receiver, device=None, ui_flags=0):
|
||||
assert receiver is not None
|
||||
if window:
|
||||
GObject.idle_add(ui.main_window.update, window, receiver, device)
|
||||
if icon:
|
||||
GObject.idle_add(ui.status_icon.update, icon, receiver)
|
||||
if ui_flags & STATUS.UI_POPUP:
|
||||
GObject.idle_add(window.popup, icon)
|
||||
|
||||
if device is None:
|
||||
# always notify on receiver updates
|
||||
ui_flags |= STATUS.UI_NOTIFY
|
||||
if ui_flags & STATUS.UI_NOTIFY and ui.notify.available:
|
||||
GObject.idle_add(ui.notify.show, device or receiver)
|
||||
|
||||
global listener
|
||||
@@ -98,27 +107,49 @@ if __name__ == '__main__':
|
||||
|
||||
from receiver import ReceiverListener
|
||||
def check_for_listener(retry=True):
|
||||
global listener, notify_missing
|
||||
def _check_still_scanning(listener):
|
||||
if listener.receiver.status == STATUS.BOOTING:
|
||||
listener.change_status(STATUS.CONNECTED)
|
||||
|
||||
global listener, notify_missing
|
||||
if listener is None:
|
||||
try:
|
||||
listener = ReceiverListener.open(status_changed)
|
||||
except OSError:
|
||||
ui.show_permissions_warning(window)
|
||||
ui.error(window, 'Permissions error',
|
||||
'Found a possible Unifying Receiver device,\n'
|
||||
'but did not have permission to open it.')
|
||||
|
||||
if listener is None:
|
||||
pairing.state = None
|
||||
if notify_missing:
|
||||
status_changed(DUMMY, None, True)
|
||||
status_changed(DUMMY, None, STATUS.UI_NOTIFY)
|
||||
notify_missing = False
|
||||
return retry
|
||||
|
||||
# print ("opened receiver", listener, listener.receiver)
|
||||
notify_missing = True
|
||||
status_changed(listener.receiver, None, STATUS.UI_NOTIFY)
|
||||
GObject.timeout_add(3 * 1000, _check_still_scanning, listener)
|
||||
pairing.state = pairing.State(listener)
|
||||
status_changed(listener.receiver, None, True)
|
||||
listener.trigger_device_events()
|
||||
|
||||
GObject.timeout_add(100, check_for_listener, False)
|
||||
_DEVICE_TIMEOUT = 3 * 60 # seconds
|
||||
_DEVICE_STATUS_CHECK = 30 # seconds
|
||||
from time import time as _timestamp
|
||||
|
||||
def check_for_inactive_devices():
|
||||
if listener and listener.receiver:
|
||||
for dev in listener.receiver.devices.values():
|
||||
if (dev.status < STATUS.CONNECTED and
|
||||
dev.props and
|
||||
_timestamp() - dev.status_updated > _DEVICE_TIMEOUT):
|
||||
dev.props.clear()
|
||||
status_changed(listener.receiver, dev)
|
||||
return True
|
||||
|
||||
GObject.timeout_add(50, check_for_listener, False)
|
||||
GObject.timeout_add(_DEVICE_STATUS_CHECK * 1000, check_for_inactive_devices)
|
||||
Gtk.main()
|
||||
|
||||
if listener is not None:
|
||||
|
||||
@@ -6,8 +6,8 @@ from gi.repository import (GObject, Gtk)
|
||||
GObject.threads_init()
|
||||
|
||||
|
||||
from solaar import NAME as _NAME
|
||||
_APP_ICONS = (_NAME + '-fail', _NAME + '-init', _NAME)
|
||||
from solaar import NAME
|
||||
_APP_ICONS = (NAME + '-fail', NAME + '-init', NAME)
|
||||
def appicon(receiver_status):
|
||||
return (_APP_ICONS[0] if receiver_status < 0 else
|
||||
_APP_ICONS[1] if receiver_status < 1 else
|
||||
@@ -19,18 +19,20 @@ _ICON_THEME = Gtk.IconTheme.get_default()
|
||||
def get_icon(name, fallback):
|
||||
return name if name and _ICON_THEME.has_icon(name) else fallback
|
||||
|
||||
def get_battery_icon(level):
|
||||
if level < 0:
|
||||
return 'battery_unknown'
|
||||
return 'battery_%03d' % (10 * ((level + 5) // 10))
|
||||
|
||||
def icon_file(name):
|
||||
if name and _ICON_THEME.has_icon(name):
|
||||
return _ICON_THEME.lookup_icon(name, 0, 0).get_filename()
|
||||
return None
|
||||
|
||||
|
||||
def show_permissions_warning(window):
|
||||
text = ('Found a possible Unifying Receiver device,\n'
|
||||
'but did not have permission to open it.')
|
||||
|
||||
def error(window, title, text):
|
||||
m = Gtk.MessageDialog(window, Gtk.DialogFlags.MODAL, Gtk.MessageType.ERROR, Gtk.ButtonsType.CLOSE, text)
|
||||
m.set_title('Permissions error')
|
||||
m.set_title(title)
|
||||
m.run()
|
||||
m.destroy()
|
||||
|
||||
@@ -59,12 +61,3 @@ def find_children(container, *child_names):
|
||||
result = [None] * count
|
||||
_iterate_children(container, names, result, count)
|
||||
return tuple(result) if count > 1 else result[0]
|
||||
|
||||
|
||||
def update(receiver, icon, window, reason):
|
||||
assert receiver is not None
|
||||
assert reason is not None
|
||||
if window:
|
||||
GObject.idle_add(main_window.update, window, receiver, reason)
|
||||
if icon:
|
||||
GObject.idle_add(status_icon.update, icon, receiver)
|
||||
|
||||
@@ -3,10 +3,9 @@
|
||||
#
|
||||
|
||||
# from sys import version as PYTTHON_VERSION
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import (Gtk, Gdk)
|
||||
|
||||
import ui.notify
|
||||
import ui.pair_window
|
||||
import ui
|
||||
from solaar import NAME as _NAME
|
||||
from solaar import VERSION as _VERSION
|
||||
|
||||
@@ -45,7 +44,8 @@ def _show_about_window(action):
|
||||
about.set_logo_icon_name(_NAME)
|
||||
about.set_version(_VERSION)
|
||||
about.set_license_type(Gtk.License.GPL_2_0)
|
||||
about.set_authors(('Daniel Pavel http://github.com/pwr', ))
|
||||
about.set_authors(('Daniel Pavel http://github.com/pwr',))
|
||||
# about.add_credit_section('Testing', 'Douglas Wagner')
|
||||
about.set_website('http://github.com/pwr/Solaar/wiki')
|
||||
about.set_website_label('Solaar Wiki')
|
||||
# about.set_comments('Using Python %s\n' % PYTTHON_VERSION.split(' ')[0])
|
||||
@@ -64,11 +64,12 @@ import pairing
|
||||
def _pair_device(action, frame):
|
||||
window = frame.get_toplevel()
|
||||
|
||||
pair_dialog = ui.pair_window.create( action, pairing.state)
|
||||
pair_dialog = ui.pair_window.create(action, pairing.state)
|
||||
# window.present()
|
||||
|
||||
pair_dialog.set_transient_for(window)
|
||||
pair_dialog.set_modal(True)
|
||||
|
||||
window.present()
|
||||
pair_dialog.set_type_hint(Gdk.WindowTypeHint.DIALOG)
|
||||
pair_dialog.present()
|
||||
|
||||
def pair(frame):
|
||||
@@ -77,15 +78,19 @@ def pair(frame):
|
||||
|
||||
def _unpair_device(action, frame):
|
||||
window = frame.get_toplevel()
|
||||
window.present()
|
||||
# window.present()
|
||||
device = frame._device
|
||||
qdialog = Gtk.MessageDialog(window, 0,
|
||||
Gtk.MessageType.QUESTION, Gtk.ButtonsType.YES_NO,
|
||||
Gtk.MessageType.QUESTION, Gtk.ButtonsType.NONE,
|
||||
"Unpair device\n%s ?" % device.name)
|
||||
qdialog.set_icon_name('remove')
|
||||
qdialog.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)
|
||||
qdialog.add_button('Unpair', Gtk.ResponseType.ACCEPT)
|
||||
choice = qdialog.run()
|
||||
qdialog.destroy()
|
||||
if choice == Gtk.ResponseType.YES:
|
||||
pairing.state.unpair(device)
|
||||
if choice == Gtk.ResponseType.ACCEPT:
|
||||
if not pairing.state.unpair(device):
|
||||
ui.error(window, 'Unpairing failed', 'Failed to unpair device\n%s .' % device.name)
|
||||
|
||||
def unpair(frame):
|
||||
return _action('remove', 'Unpair', _unpair_device, frame)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#
|
||||
#
|
||||
|
||||
from gi.repository import (Gtk, Gdk)
|
||||
from gi.repository import (Gtk, Gdk, GObject)
|
||||
|
||||
import ui
|
||||
from logitech.devices.constants import (STATUS, PROPS)
|
||||
@@ -17,24 +17,6 @@ _PLACEHOLDER = '~'
|
||||
#
|
||||
#
|
||||
|
||||
def _info_text(dev):
|
||||
fw_text = '\n'.join(['%-12s\t<tt>%s%s%s</tt>' %
|
||||
(f.kind, f.name, ' ' if f.name else '', f.version) for f in dev.firmware])
|
||||
return ('<small>'
|
||||
'Serial \t\t<tt>%s</tt>\n'
|
||||
'HID protocol\t<tt>%1.1f</tt>\n'
|
||||
'%s'
|
||||
'</small>' % (dev.serial, dev.protocol, fw_text))
|
||||
|
||||
def _toggle_info(action, label_widget, box_widget, frame):
|
||||
if action.get_active():
|
||||
box_widget.set_visible(True)
|
||||
if not label_widget.get_text():
|
||||
label_widget.set_markup(_info_text(frame._device))
|
||||
else:
|
||||
box_widget.set_visible(False)
|
||||
|
||||
|
||||
def _make_receiver_box(name):
|
||||
frame = Gtk.Frame()
|
||||
frame._device = None
|
||||
@@ -68,11 +50,12 @@ def _make_receiver_box(name):
|
||||
info_box.add(info_label)
|
||||
info_box.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
|
||||
|
||||
toggle_info_action = ui.action._toggle_action('info', 'Receiver info', _toggle_info, info_label, info_box, frame)
|
||||
toggle_info_action = ui.action._toggle_action('info', 'Receiver info', _toggle_info_box, info_label, info_box, frame, _update_receiver_info_label)
|
||||
toolbar.insert(toggle_info_action.create_tool_item(), 0)
|
||||
toolbar.insert(ui.action.pair(frame).create_tool_item(), -1)
|
||||
# toolbar.insert(ui.action.about.create_tool_item(), -1)
|
||||
|
||||
vbox = Gtk.VBox(homogeneous=False, spacing=2)
|
||||
vbox = Gtk.VBox(homogeneous=False, spacing=4)
|
||||
vbox.set_border_width(4)
|
||||
vbox.pack_start(hbox, True, True, 0)
|
||||
vbox.pack_start(info_box, True, True, 0)
|
||||
@@ -129,11 +112,12 @@ def _make_device_box(index):
|
||||
info_label.set_alignment(0, 0.5)
|
||||
info_label.set_padding(8, 2)
|
||||
info_label.set_selectable(True)
|
||||
info_label.fields = {}
|
||||
|
||||
info_box = Gtk.Frame()
|
||||
info_box.add(info_label)
|
||||
|
||||
toggle_info_action = ui.action._toggle_action('info', 'Device info', _toggle_info, info_label, info_box, frame)
|
||||
toggle_info_action = ui.action._toggle_action('info', 'Device info', _toggle_info_box, info_label, info_box, frame, _update_device_info_label)
|
||||
toolbar.insert(toggle_info_action.create_tool_item(), 0)
|
||||
toolbar.insert(ui.action.unpair(frame).create_tool_item(), -1)
|
||||
|
||||
@@ -167,6 +151,9 @@ def toggle(window, trigger):
|
||||
window.present()
|
||||
return True
|
||||
|
||||
def _popup(window, trigger):
|
||||
if not window.get_visible():
|
||||
toggle(window, trigger)
|
||||
|
||||
def create(title, name, max_devices, systray=False):
|
||||
window = Gtk.Window()
|
||||
@@ -193,6 +180,7 @@ def create(title, name, max_devices, systray=False):
|
||||
window.set_resizable(False)
|
||||
|
||||
window.toggle_visible = lambda i: toggle(window, i)
|
||||
window.popup = lambda i: _popup(window, i)
|
||||
|
||||
if systray:
|
||||
window.set_keep_above(True)
|
||||
@@ -206,6 +194,57 @@ def create(title, name, max_devices, systray=False):
|
||||
#
|
||||
#
|
||||
|
||||
def _update_device_info_label(label, dev):
|
||||
need_update = False
|
||||
|
||||
if 'serial' in label.fields:
|
||||
serial = label.fields['serial']
|
||||
else:
|
||||
serial = label.fields['serial'] = dev.serial
|
||||
need_update = True
|
||||
|
||||
if 'firmware' in label.fields:
|
||||
firmware = label.fields['firmware']
|
||||
else:
|
||||
if dev.status >= STATUS.CONNECTED:
|
||||
firmware = label.fields['firmware'] = dev.firmware
|
||||
need_update = True
|
||||
else:
|
||||
firmware = None
|
||||
|
||||
if 'hid' in label.fields:
|
||||
hid = label.fields['hid']
|
||||
else:
|
||||
if dev.status >= STATUS.CONNECTED:
|
||||
hid = label.fields['hid'] = dev.protocol
|
||||
need_update = True
|
||||
else:
|
||||
hid = None
|
||||
|
||||
if need_update:
|
||||
items = [('Serial', serial)]
|
||||
if firmware:
|
||||
items += [(f.kind, f.name + ' ' + f.version) for f in firmware]
|
||||
if hid:
|
||||
items += [('HID', hid)]
|
||||
|
||||
label.set_markup('<small><tt>%s</tt></small>' % '\n'.join('%-10s: %s' % (item[0], str(item[1])) for item in items))
|
||||
|
||||
|
||||
def _update_receiver_info_label(label, dev):
|
||||
if label.get_visible() and label.get_text() == '':
|
||||
items = [('Serial', dev.serial)] + \
|
||||
[(f.kind, f.version) for f in dev.firmware]
|
||||
label.set_markup('<small><tt>%s</tt></small>' % '\n'.join('%-10s: %s' % (item[0], str(item[1])) for item in items))
|
||||
|
||||
def _toggle_info_box(action, label_widget, box_widget, frame, update_function):
|
||||
if action.get_active():
|
||||
box_widget.set_visible(True)
|
||||
update_function(label_widget, frame._device)
|
||||
else:
|
||||
box_widget.set_visible(False)
|
||||
|
||||
|
||||
def _update_receiver_box(frame, receiver):
|
||||
label, toolbar, info_label = ui.find_children(frame, 'label', 'toolbar', 'info-label')
|
||||
|
||||
@@ -222,10 +261,12 @@ def _update_receiver_box(frame, receiver):
|
||||
|
||||
def _update_device_box(frame, dev):
|
||||
frame._device = dev
|
||||
# print (dev.name, dev.kind)
|
||||
|
||||
icon, label, info_label = ui.find_children(frame, 'icon', 'label', 'info-label')
|
||||
|
||||
if frame.get_name() != dev.name:
|
||||
first_run = frame.get_name() != dev.name
|
||||
if first_run:
|
||||
frame.set_name(dev.name)
|
||||
icon_name = ui.get_icon(dev.name, dev.kind)
|
||||
icon.set_from_icon_name(icon_name, _DEVICE_ICON_SIZE)
|
||||
@@ -233,31 +274,37 @@ def _update_device_box(frame, dev):
|
||||
|
||||
status = ui.find_children(frame, 'status')
|
||||
status_icons = status.get_children()
|
||||
toolbar = status_icons[-1]
|
||||
|
||||
if dev.status < STATUS.CONNECTED:
|
||||
icon.set_sensitive(False)
|
||||
label.set_sensitive(False)
|
||||
status.set_sensitive(False)
|
||||
for c in status_icons[1:-1]:
|
||||
for c in status_icons[2:-1]:
|
||||
c.set_visible(False)
|
||||
toolbar.get_children()[0].set_active(False)
|
||||
|
||||
battery_icon, battery_label = status_icons[0:2]
|
||||
battery_icon.set_sensitive(False)
|
||||
battery_label.set_sensitive(False)
|
||||
battery_level = dev.props.get(PROPS.BATTERY_LEVEL)
|
||||
if battery_level is None:
|
||||
battery_label.set_markup('<small>%s</small>' % dev.status_text)
|
||||
else:
|
||||
battery_label.set_markup('%d%%' % battery_level)
|
||||
|
||||
else:
|
||||
icon.set_sensitive(True)
|
||||
label.set_sensitive(True)
|
||||
status.set_sensitive(True)
|
||||
|
||||
battery_icon, battery_label = status_icons[0:2]
|
||||
battery_level = dev.props.get(PROPS.BATTERY_LEVEL)
|
||||
if battery_level is None:
|
||||
battery_icon.set_from_icon_name('battery_unknown', _STATUS_ICON_SIZE)
|
||||
battery_icon.set_sensitive(False)
|
||||
battery_label.set_visible(False)
|
||||
battery_icon.set_from_icon_name('battery_unknown', _STATUS_ICON_SIZE)
|
||||
text = 'no status' if dev.protocol < 2.0 else 'waiting for status...'
|
||||
battery_label.set_markup('<small>%s</small>' % text)
|
||||
battery_label.set_sensitive(True)
|
||||
else:
|
||||
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(ui.get_battery_icon(battery_level), _STATUS_ICON_SIZE)
|
||||
battery_icon.set_sensitive(True)
|
||||
battery_label.set_text('%d%%' % battery_level)
|
||||
battery_label.set_visible(True)
|
||||
battery_label.set_sensitive(True)
|
||||
|
||||
battery_status = dev.props.get(PROPS.BATTERY_STATUS)
|
||||
battery_icon.set_tooltip_text(battery_status or '')
|
||||
@@ -274,10 +321,9 @@ def _update_device_box(frame, dev):
|
||||
light_label.set_text('%d lux' % light_level)
|
||||
light_label.set_visible(True)
|
||||
|
||||
for b in toolbar.get_children()[:-1]:
|
||||
b.set_sensitive(True)
|
||||
|
||||
frame.set_visible(True)
|
||||
if first_run:
|
||||
frame.set_visible(True)
|
||||
GObject.timeout_add(2000, _update_device_info_label, info_label, dev)
|
||||
|
||||
|
||||
def update(window, receiver, device=None):
|
||||
|
||||
@@ -8,14 +8,21 @@ from gi.repository import (Gtk, GObject)
|
||||
import ui
|
||||
|
||||
|
||||
def _create_page(assistant, text, kind):
|
||||
def _create_page(assistant, text, kind, icon_name=None):
|
||||
p = Gtk.VBox(False, 12)
|
||||
p.set_border_width(8)
|
||||
|
||||
if text:
|
||||
item = Gtk.HBox(homogeneous=False, spacing=16)
|
||||
p.pack_start(item, False, True, 0)
|
||||
|
||||
label = Gtk.Label(text)
|
||||
label.set_alignment(0, 0)
|
||||
p.pack_start(label, False, True, 0)
|
||||
item.pack_start(label, True, True, 0)
|
||||
|
||||
if icon_name:
|
||||
icon = Gtk.Image.new_from_icon_name(icon_name, Gtk.IconSize.DIALOG)
|
||||
item.pack_start(icon, False, False, 0)
|
||||
|
||||
assistant.append_page(p)
|
||||
assistant.set_page_type(p, kind)
|
||||
@@ -59,8 +66,9 @@ def _scan_complete_ui(assistant, device):
|
||||
page = _create_page(assistant,
|
||||
'No new device detected.\n'
|
||||
'\n'
|
||||
'Make sure your device is within range of the receiver,\nand it has a decent battery charge.\n',
|
||||
Gtk.AssistantPageType.CONFIRM)
|
||||
'Make sure your device is within the\nreceiver\'s range, and it has\na decent battery charge.\n',
|
||||
Gtk.AssistantPageType.CONFIRM,
|
||||
'dialog-error')
|
||||
else:
|
||||
page = _create_page(assistant,
|
||||
None,
|
||||
@@ -110,7 +118,8 @@ def create(action, state):
|
||||
'Turn on the device you want to pair.\n'
|
||||
'\n'
|
||||
'If the device is already turned on,\nturn if off and on again.',
|
||||
Gtk.AssistantPageType.INTRO)
|
||||
Gtk.AssistantPageType.INTRO,
|
||||
'preferences-desktop-peripherals')
|
||||
spinner = Gtk.Spinner()
|
||||
spinner.set_visible(True)
|
||||
page_intro.pack_end(spinner, True, True, 16)
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
from gi.repository import Gtk
|
||||
import ui
|
||||
from logitech.devices.constants import (STATUS, PROPS)
|
||||
|
||||
|
||||
def create(window, menu_actions=None):
|
||||
@@ -31,25 +32,37 @@ def create(window, menu_actions=None):
|
||||
|
||||
|
||||
def update(icon, receiver):
|
||||
icon.set_from_icon_name(ui.appicon(receiver.status))
|
||||
battery_level = None
|
||||
|
||||
if receiver.devices:
|
||||
lines = []
|
||||
if receiver.status < 1:
|
||||
lines += (receiver.status_text, '')
|
||||
lines = [ui.NAME + ': ' + receiver.status_text, '']
|
||||
|
||||
devlist = [receiver.devices[d] for d in range(1, 1 + receiver.max_devices) if d in receiver.devices]
|
||||
if receiver.status > STATUS.CONNECTED:
|
||||
devlist = sorted(receiver.devices.values(), key=lambda x: x.number)
|
||||
for dev in devlist:
|
||||
name = '<b>' + dev.name + '</b>'
|
||||
if dev.status < 1:
|
||||
lines.append(name + ' (' + dev.status_text + ')')
|
||||
lines.append('<b>' + dev.name + '</b>')
|
||||
|
||||
p = dev.properties_text
|
||||
if p:
|
||||
p = '\t' + p
|
||||
if dev.status < STATUS.CONNECTED:
|
||||
p += ' <small>(' + dev.status_text + ')</small>'
|
||||
lines.append(p)
|
||||
elif dev.status < STATUS.CONNECTED:
|
||||
lines.append('\t<small>(' + dev.status_text + ')</small>')
|
||||
elif dev.protocol < 2.0:
|
||||
lines.append('\t' + '<small>no status</small>')
|
||||
else:
|
||||
lines.append(name)
|
||||
if dev.status > 1:
|
||||
lines.append(' ' + dev.status_text)
|
||||
lines.append('\t' + '<small>waiting for status...</small>')
|
||||
|
||||
lines.append('')
|
||||
|
||||
text = '\n'.join(lines).rstrip('\n')
|
||||
icon.set_tooltip_markup(text)
|
||||
if battery_level is None:
|
||||
if PROPS.BATTERY_LEVEL in dev.props:
|
||||
battery_level = dev.props[PROPS.BATTERY_LEVEL]
|
||||
|
||||
icon.set_tooltip_markup('\n'.join(lines).rstrip('\n'))
|
||||
|
||||
if battery_level is None:
|
||||
icon.set_from_icon_name(ui.appicon(receiver.status))
|
||||
else:
|
||||
icon.set_tooltip_text(receiver.status_text)
|
||||
icon.set_from_icon_name(ui.get_battery_icon(battery_level))
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
|
||||
Z=`readlink -f "$0"`
|
||||
LIB=`readlink -f $(dirname "$Z")/../lib`
|
||||
#export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$LIB/native/`arch`
|
||||
export PYTHONPATH=$LIB
|
||||
|
||||
PYTHON=`which python python2 python3 | head -n 1`
|
||||
exec $PYTHON -OOu -m hidapi.hidconsole "$@"
|
||||
exec $PYTHON -u -m hidapi.hidconsole "$@"
|
||||
|
||||
3
bin/scan
@@ -2,8 +2,7 @@
|
||||
|
||||
Z=`readlink -f "$0"`
|
||||
LIB=`readlink -f $(dirname "$Z")/../lib`
|
||||
#export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$LIB/native/`arch`
|
||||
export PYTHONPATH=$LIB
|
||||
|
||||
PYTHON=`which python python2 python3 | head -n 1`
|
||||
exec $PYTHON -OOu -m logitech.scanner "$@"
|
||||
exec $PYTHON -u -m logitech.scanner "$@"
|
||||
|
||||
@@ -5,9 +5,8 @@ APP=`readlink -f $(dirname "$Z")/../app`
|
||||
LIB=`readlink -f $(dirname "$Z")/../lib`
|
||||
SHARE=`readlink -f $(dirname "$Z")/../share`
|
||||
|
||||
#export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$LIB/native/`arch`
|
||||
export PYTHONPATH=$APP:$LIB
|
||||
export XDG_DATA_DIRS=$SHARE:$XDG_DATA_DIRS
|
||||
|
||||
PYTHON=`which python python2 python3 | head -n 1`
|
||||
exec $PYTHON -OOu -m solaar "$@"
|
||||
exec $PYTHON -u -m solaar "$@"
|
||||
|
||||
@@ -4,7 +4,4 @@ __author__ = "Daniel Pavel"
|
||||
__license__ = "GPL"
|
||||
__version__ = "0.4"
|
||||
|
||||
try:
|
||||
from hidapi.udev import *
|
||||
except ImportError:
|
||||
from hidapi.native import *
|
||||
from hidapi.udev import *
|
||||
|
||||
@@ -55,7 +55,8 @@ if __name__ == '__main__':
|
||||
print (".. Opening device %s" % args.device)
|
||||
handle = hidapi.open_path(args.device.encode('utf-8'))
|
||||
if handle:
|
||||
print (".. Opened handle %X, vendor %s product %s serial %s" % (handle,
|
||||
print (".. Opened handle %s, vendor %s product %s serial %s" % (
|
||||
repr(handle),
|
||||
repr(hidapi.get_manufacturer(handle)),
|
||||
repr(hidapi.get_product(handle)),
|
||||
repr(hidapi.get_serial(handle))))
|
||||
@@ -101,7 +102,7 @@ if __name__ == '__main__':
|
||||
except Exception as e:
|
||||
print ('%s: %s' % (type(e).__name__, e))
|
||||
|
||||
print (".. Closing handle %X" % handle)
|
||||
print (".. Closing handle %s" % repr(handle))
|
||||
hidapi.close(handle)
|
||||
if interactive:
|
||||
readline.write_history_file(args.history)
|
||||
|
||||
@@ -16,6 +16,10 @@ Currently the native libusb implementation (temporarily) detaches the device's
|
||||
USB driver from the kernel, and it may cause the device to become unresponsive.
|
||||
"""
|
||||
|
||||
#
|
||||
# LEGACY, no longer supported
|
||||
#
|
||||
|
||||
__version__ = '0.3-hidapi-0.7.0'
|
||||
|
||||
|
||||
|
||||
@@ -8,9 +8,10 @@ necessary.
|
||||
"""
|
||||
|
||||
import os as _os
|
||||
import errno as _errno
|
||||
from select import select as _select
|
||||
from pyudev import Context as _Context
|
||||
from pyudev import Device as _Device
|
||||
from pyudev import (Context as _Context,
|
||||
Device as _Device)
|
||||
|
||||
|
||||
native_implementation = 'udev'
|
||||
@@ -124,6 +125,8 @@ def open_path(device_path):
|
||||
|
||||
:returns: an opaque device handle, or ``None``.
|
||||
"""
|
||||
assert device_path
|
||||
assert '/dev/hidraw' in device_path
|
||||
return _os.open(device_path, _os.O_RDWR | _os.O_SYNC)
|
||||
|
||||
|
||||
@@ -132,6 +135,7 @@ def close(device_handle):
|
||||
|
||||
:param device_handle: a device handle returned by open() or open_path().
|
||||
"""
|
||||
assert device_handle
|
||||
_os.close(device_handle)
|
||||
|
||||
|
||||
@@ -155,14 +159,11 @@ def write(device_handle, data):
|
||||
write() will send the data on the first OUT endpoint, if
|
||||
one exists. If it does not, it will send the data through
|
||||
the Control Endpoint (Endpoint 0).
|
||||
|
||||
:returns: ``True`` if the write was successful.
|
||||
"""
|
||||
try:
|
||||
bytes_written = _os.write(device_handle, data)
|
||||
return bytes_written == len(data)
|
||||
except:
|
||||
pass
|
||||
assert device_handle
|
||||
bytes_written = _os.write(device_handle, data)
|
||||
if bytes_written != len(data):
|
||||
raise OSError(errno=_errno.EIO, strerror='written %d bytes out of expected %d' % (bytes_written, len(data)))
|
||||
|
||||
|
||||
def read(device_handle, bytes_count, timeout_ms=-1):
|
||||
@@ -181,15 +182,20 @@ def read(device_handle, bytes_count, timeout_ms=-1):
|
||||
:returns: the data packet read, an empty bytes string if a timeout was
|
||||
reached, or None if there was an error while reading.
|
||||
"""
|
||||
try:
|
||||
timeout = None if timeout_ms < 0 else timeout_ms / 1000.0
|
||||
rlist, wlist, xlist = _select([device_handle], [], [], timeout)
|
||||
if rlist:
|
||||
assert rlist == [device_handle]
|
||||
return _os.read(device_handle, bytes_count)
|
||||
assert device_handle
|
||||
timeout = None if timeout_ms < 0 else timeout_ms / 1000.0
|
||||
rlist, wlist, xlist = _select([device_handle], [], [device_handle], timeout)
|
||||
|
||||
if xlist:
|
||||
assert xlist == [device_handle]
|
||||
raise OSError(errno=_errno.EIO, strerror='exception on file descriptor %d' % device_handle)
|
||||
|
||||
if rlist:
|
||||
assert rlist == [device_handle]
|
||||
data = _os.read(device_handle, bytes_count)
|
||||
return data
|
||||
else:
|
||||
return b''
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
_DEVICE_STRINGS = {
|
||||
@@ -236,6 +242,7 @@ def get_indexed_string(device_handle, index):
|
||||
if index not in _DEVICE_STRINGS:
|
||||
return None
|
||||
|
||||
assert device_handle
|
||||
stat = _os.fstat(device_handle)
|
||||
dev = _Device.from_device_number(_Context(), 'char', stat.st_rdev)
|
||||
if dev:
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import logging
|
||||
|
||||
from .constants import (STATUS, PROPS)
|
||||
from ..unifying_receiver.constants import (FEATURE, BATTERY_STATUS)
|
||||
from ..unifying_receiver.constants import (FEATURE, BATTERY_STATUS, BATTERY_OK)
|
||||
from ..unifying_receiver import api as _api
|
||||
|
||||
#
|
||||
@@ -14,17 +14,17 @@ from ..unifying_receiver import api as _api
|
||||
|
||||
_DEVICE_MODULES = {}
|
||||
|
||||
def _module(device_name):
|
||||
if device_name not in _DEVICE_MODULES:
|
||||
shortname = device_name.split(' ')[-1].lower()
|
||||
def _module(device):
|
||||
shortname = device.codename.lower().replace(' ', '_')
|
||||
if shortname not in _DEVICE_MODULES:
|
||||
try:
|
||||
m = __import__(shortname, globals(), level=1)
|
||||
_DEVICE_MODULES[device_name] = m
|
||||
_DEVICE_MODULES[shortname] = m
|
||||
except:
|
||||
# logging.exception(shortname)
|
||||
_DEVICE_MODULES[device_name] = None
|
||||
_DEVICE_MODULES[shortname] = None
|
||||
|
||||
return _DEVICE_MODULES[device_name]
|
||||
return _DEVICE_MODULES[shortname]
|
||||
|
||||
#
|
||||
#
|
||||
@@ -34,8 +34,11 @@ def default_request_status(devinfo):
|
||||
if FEATURE.BATTERY in devinfo.features:
|
||||
reply = _api.get_device_battery_level(devinfo.handle, devinfo.number, features=devinfo.features)
|
||||
if reply:
|
||||
discharge, dischargeNext, status = reply
|
||||
return STATUS.CONNECTED, {PROPS.BATTERY_LEVEL: discharge, PROPS.BATTERY_STATUS: status}
|
||||
b_discharge, dischargeNext, b_status = reply
|
||||
return STATUS.CONNECTED, {
|
||||
PROPS.BATTERY_LEVEL: b_discharge,
|
||||
PROPS.BATTERY_STATUS: b_status,
|
||||
}
|
||||
|
||||
reply = _api.ping(devinfo.handle, devinfo.number)
|
||||
return STATUS.CONNECTED if reply else STATUS.UNAVAILABLE
|
||||
@@ -44,29 +47,33 @@ def default_request_status(devinfo):
|
||||
def default_process_event(devinfo, data):
|
||||
feature_index = ord(data[0:1])
|
||||
if feature_index >= len(devinfo.features):
|
||||
logging.warn("mistery event %s for %s", repr(data), devinfo)
|
||||
# logging.warn("mistery event %s for %s", repr(data), devinfo)
|
||||
return None
|
||||
|
||||
feature = devinfo.features[feature_index]
|
||||
feature_function = ord(data[1:2]) & 0xF0
|
||||
|
||||
if feature == FEATURE.BATTERY:
|
||||
if feature_function == 0:
|
||||
discharge = ord(data[2:3])
|
||||
status = BATTERY_STATUS[ord(data[3:4])]
|
||||
return STATUS.CONNECTED, {PROPS.BATTERY_LEVEL: discharge, PROPS.BATTERY_STATUS: status}
|
||||
if feature_function == 0x00:
|
||||
b_discharge = ord(data[2:3])
|
||||
b_status = ord(data[3:4])
|
||||
return STATUS.CONNECTED, {
|
||||
PROPS.BATTERY_LEVEL: b_discharge,
|
||||
PROPS.BATTERY_STATUS: BATTERY_STATUS[b_status],
|
||||
PROPS.UI_FLAGS: 0 if BATTERY_OK(b_status) else STATUS.UI_NOTIFY,
|
||||
}
|
||||
# ?
|
||||
elif feature == FEATURE.REPROGRAMMABLE_KEYS:
|
||||
if feature_function == 0:
|
||||
if feature_function == 0x00:
|
||||
logging.debug('reprogrammable key: %s', repr(data))
|
||||
# TODO
|
||||
pass
|
||||
# ?
|
||||
elif feature == FEATURE.WIRELESS:
|
||||
if feature_function == 0:
|
||||
if feature_function == 0x00:
|
||||
logging.debug("wireless status: %s", repr(data))
|
||||
if data[2:5] == b'\x01\x01\x01':
|
||||
return STATUS.CONNECTED
|
||||
return STATUS.CONNECTED, {PROPS.UI_FLAGS: STATUS.UI_NOTIFY}
|
||||
# TODO
|
||||
pass
|
||||
# ?
|
||||
@@ -79,7 +86,7 @@ def request_status(devinfo):
|
||||
:param listener: the EventsListener that will be used to send the request,
|
||||
and which will receive the status events from the device.
|
||||
"""
|
||||
m = _module(devinfo.name)
|
||||
m = _module(devinfo)
|
||||
if m and 'request_status' in m.__dict__:
|
||||
return m.request_status(devinfo)
|
||||
return default_request_status(devinfo)
|
||||
@@ -95,6 +102,6 @@ def process_event(devinfo, data):
|
||||
if default_result is not None:
|
||||
return default_result
|
||||
|
||||
m = _module(devinfo.name)
|
||||
m = _module(devinfo)
|
||||
if m and 'process_event' in m.__dict__:
|
||||
return m.process_event(devinfo, data)
|
||||
|
||||
@@ -4,8 +4,10 @@
|
||||
|
||||
STATUS = type('STATUS', (),
|
||||
dict(
|
||||
UNKNOWN=-9999,
|
||||
UNPAIRED=-1000,
|
||||
UI_NOTIFY=0x01,
|
||||
UI_POPUP=0x02,
|
||||
UNKNOWN=-0xFFFF,
|
||||
UNPAIRED=-0x1000,
|
||||
UNAVAILABLE=-1,
|
||||
BOOTING=0,
|
||||
CONNECTED=1,
|
||||
@@ -26,6 +28,7 @@ PROPS = type('PROPS', (),
|
||||
BATTERY_LEVEL='battery_level',
|
||||
BATTERY_STATUS='battery_status',
|
||||
LIGHT_LEVEL='light_level',
|
||||
UI_FLAGS='ui_flags',
|
||||
))
|
||||
|
||||
# when the receiver reports a device that is not connected
|
||||
|
||||
@@ -30,7 +30,7 @@ def _charge_status(data, hasLux=False):
|
||||
|
||||
def request_status(devinfo):
|
||||
reply = _api.request(devinfo.handle, devinfo.number,
|
||||
feature=FEATURE.SOLAR_CHARGE, function=b'\x03', params=b'\x78\x01',
|
||||
feature=FEATURE.SOLAR_CHARGE, function=b'\x06', params=b'\x78\x01',
|
||||
features=devinfo.features)
|
||||
if reply is None:
|
||||
return STATUS.UNAVAILABLE
|
||||
@@ -47,4 +47,9 @@ def process_event(devinfo, data):
|
||||
|
||||
if data[:2] == b'\x09\x20' and data[7:11] == b'GOOD':
|
||||
logging.debug("Solar key pressed")
|
||||
return request_status(devinfo) or _charge_status(data)
|
||||
if request_status(devinfo) == STATUS.UNAVAILABLE:
|
||||
return STATUS.UNAVAILABLE, {PROPS.UI_FLAGS: STATUS.UI_POPUP | STATUS.UI_NOTIFY}
|
||||
|
||||
code, props = _charge_status(data)
|
||||
props[PROPS.UI_FLAGS] = STATUS.UI_POPUP
|
||||
return code, props
|
||||
|
||||
@@ -7,17 +7,26 @@ def print_receiver(receiver):
|
||||
print (" Serial : %s" % receiver.serial)
|
||||
for f in receiver.firmware:
|
||||
print (" %-10s: %s" % (f.kind, f.version))
|
||||
print (" Reported %d paired device(s)" % len(receiver))
|
||||
|
||||
|
||||
def scan_devices(receiver):
|
||||
for dev in receiver:
|
||||
for number in range(1, 1 + receiver.max_devices):
|
||||
dev = receiver[number]
|
||||
if dev is None:
|
||||
dev = api.PairedDevice(receiver.handle, number)
|
||||
if dev.codename is None:
|
||||
continue
|
||||
|
||||
print ("--------")
|
||||
print (str(dev))
|
||||
print ("Codename : %s" % dev.codename)
|
||||
print ("Name : %s" % dev.name)
|
||||
print ("Kind : %s" % dev.kind)
|
||||
print ("Serial number: %s" % dev.serial)
|
||||
|
||||
if not dev.protocol:
|
||||
print ("HID protocol : UNKNOWN")
|
||||
print ("Device is not connected at this time, no further info available.")
|
||||
continue
|
||||
|
||||
print ("HID protocol : HID %01.1f" % dev.protocol)
|
||||
@@ -27,7 +36,7 @@ def scan_devices(receiver):
|
||||
|
||||
firmware = dev.firmware
|
||||
for fw in firmware:
|
||||
print (" %-10s: %s %s" % (fw.kind, fw.name, fw.version))
|
||||
print (" %-11s: %s %s" % (fw.kind, fw.name, fw.version))
|
||||
|
||||
all_features = api.get_device_features(dev.handle, dev.number)
|
||||
for index in range(0, len(all_features)):
|
||||
@@ -45,9 +54,7 @@ def scan_devices(receiver):
|
||||
print (" %d reprogrammable keys found" % len(keys))
|
||||
for k in keys:
|
||||
flags = ','.join(KEY_FLAG_NAME[f] for f in KEY_FLAG_NAME if k.flags & f)
|
||||
print (" %2d: %-12s => %-12s :%s" % (k.index, KEY_NAME[k.id], KEY_NAME[k.task], flags))
|
||||
|
||||
print ("--------")
|
||||
print (" %2d: %-12s => %-12s : %s" % (k.index, KEY_NAME[k.id], KEY_NAME[k.task], flags))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
@@ -65,9 +72,8 @@ if __name__ == '__main__':
|
||||
|
||||
receiver = api.Receiver.open()
|
||||
if receiver is None:
|
||||
print ("!! Logitech Unifying Receiver not found.")
|
||||
print ("Logitech Unifying Receiver not found.")
|
||||
else:
|
||||
print ("!! Found Logitech Unifying Receiver: %s" % receiver)
|
||||
print_receiver(receiver)
|
||||
scan_devices(receiver)
|
||||
receiver.close()
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
from struct import pack as _pack
|
||||
from struct import unpack as _unpack
|
||||
import errno as _errno
|
||||
from threading import local as _local
|
||||
|
||||
|
||||
from . import base as _base
|
||||
@@ -27,9 +28,63 @@ del getLogger
|
||||
#
|
||||
#
|
||||
|
||||
class ThreadedHandle(object):
|
||||
__slots__ = ['path', '_local', '_handles']
|
||||
|
||||
def __init__(self, initial_handle, path):
|
||||
assert initial_handle
|
||||
if type(initial_handle) != int:
|
||||
raise TypeError('expected int as initial handle, got %s' % repr(initial_handle))
|
||||
|
||||
assert path
|
||||
self.path = path
|
||||
self._local = _local()
|
||||
self._local.handle = initial_handle
|
||||
self._handles = [initial_handle]
|
||||
|
||||
def _open(self):
|
||||
handle = _base.open_path(self.path)
|
||||
if handle is None:
|
||||
_log.error("%s failed to open new handle", repr(self))
|
||||
else:
|
||||
# _log.debug("%s opened new handle %d", repr(self), handle)
|
||||
self._local.handle = handle
|
||||
self._handles.append(handle)
|
||||
return handle
|
||||
|
||||
def close(self):
|
||||
self._local = None
|
||||
handles, self._handles = self._handles, []
|
||||
_log.debug("%s closing %s", repr(self), handles)
|
||||
for h in handles:
|
||||
_base.close(h)
|
||||
|
||||
def __del__(self):
|
||||
self.close()
|
||||
|
||||
def __int__(self):
|
||||
if self._local:
|
||||
try:
|
||||
return self._local.handle
|
||||
except:
|
||||
return self._open()
|
||||
|
||||
def __str__(self):
|
||||
return str(int(self))
|
||||
|
||||
def __repr__(self):
|
||||
return '<LocalHandle[%s]>' % self.path
|
||||
|
||||
def __bool__(self):
|
||||
return bool(self._handles)
|
||||
__nonzero__ = __bool__
|
||||
|
||||
|
||||
class PairedDevice(object):
|
||||
def __init__(self, handle, number):
|
||||
assert handle
|
||||
self.handle = handle
|
||||
assert number > 0 and number <= MAX_ATTACHED_DEVICES
|
||||
self.number = number
|
||||
|
||||
self._protocol = None
|
||||
@@ -40,11 +95,15 @@ class PairedDevice(object):
|
||||
self._serial = None
|
||||
self._firmware = None
|
||||
|
||||
def __del__(self):
|
||||
self.handle = None
|
||||
|
||||
@property
|
||||
def protocol(self):
|
||||
if self._protocol is None:
|
||||
self._protocol = _base.ping(self.handle, self.number)
|
||||
return 0 if self._protocol is None else self._protocol
|
||||
# _log.debug("device %d protocol %s", self.number, self._protocol)
|
||||
return self._protocol or 0
|
||||
|
||||
@property
|
||||
def features(self):
|
||||
@@ -59,7 +118,8 @@ class PairedDevice(object):
|
||||
codename = _base.request(self.handle, 0xFF, b'\x83\xB5', 0x40 + self.number - 1)
|
||||
if codename:
|
||||
self._codename = codename[2:].rstrip(b'\x00').decode('ascii')
|
||||
return self._codename or '?'
|
||||
# _log.debug("device %d codename %s", self.number, self._codename)
|
||||
return self._codename
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@@ -70,7 +130,7 @@ class PairedDevice(object):
|
||||
self._name, self._kind = _DEVICE_NAMES[self._codename]
|
||||
else:
|
||||
self._name = get_device_name(self.handle, self.number, self.features)
|
||||
return self._name or self.codename
|
||||
return self._name or self.codename or '?'
|
||||
|
||||
@property
|
||||
def kind(self):
|
||||
@@ -87,6 +147,7 @@ class PairedDevice(object):
|
||||
def firmware(self):
|
||||
if self._firmware is None and self.protocol >= 2.0:
|
||||
self._firmware = get_device_firmware(self.handle, self.number, self.features)
|
||||
# _log.debug("device %d firmware %s", self.number, self._firmware)
|
||||
return self._firmware or ()
|
||||
|
||||
@property
|
||||
@@ -96,16 +157,14 @@ class PairedDevice(object):
|
||||
serial = _base.request(self.handle, 0xFF, b'\x83\xB5', 0x30 + self.number - 1)
|
||||
if prefix and serial:
|
||||
self._serial = _base._hex(prefix[3:5]) + '-' + _base._hex(serial[1:5])
|
||||
# _log.debug("device %d serial %s", self.number, self._serial)
|
||||
return self._serial or '?'
|
||||
|
||||
def ping(self):
|
||||
return _base.ping(self.handle, self.number) is not None
|
||||
|
||||
def __str__(self):
|
||||
return '<PairedDevice(%X,%d,%s)>' % (self.handle, self.number, self._name or '?')
|
||||
|
||||
def __hash__(self):
|
||||
return self.number
|
||||
return '<PairedDevice(%s,%d,%s)>' % (self.handle, self.number, self.codename or '?')
|
||||
|
||||
|
||||
class Receiver(object):
|
||||
@@ -113,16 +172,21 @@ class Receiver(object):
|
||||
max_devices = MAX_ATTACHED_DEVICES
|
||||
|
||||
def __init__(self, handle, path=None):
|
||||
assert handle
|
||||
self.handle = handle
|
||||
assert path
|
||||
self.path = path
|
||||
|
||||
self._serial = None
|
||||
self._firmware = None
|
||||
|
||||
def close(self):
|
||||
handle, self.handle = self.handle, 0
|
||||
handle, self.handle = self.handle, None
|
||||
return (handle and _base.close(handle))
|
||||
|
||||
def __del__(self):
|
||||
self.close()
|
||||
|
||||
@property
|
||||
def serial(self):
|
||||
if self._serial is None and self.handle:
|
||||
@@ -153,7 +217,7 @@ class Receiver(object):
|
||||
return self._firmware
|
||||
|
||||
def __iter__(self):
|
||||
if self.handle == 0:
|
||||
if not self.handle:
|
||||
return
|
||||
|
||||
for number in range(1, 1 + MAX_ATTACHED_DEVICES):
|
||||
@@ -164,14 +228,14 @@ class Receiver(object):
|
||||
def __getitem__(self, key):
|
||||
if type(key) != int:
|
||||
raise TypeError('key must be an integer')
|
||||
if self.handle == 0 or key < 0 or key > MAX_ATTACHED_DEVICES:
|
||||
if not self.handle or key < 0 or key > MAX_ATTACHED_DEVICES:
|
||||
raise IndexError(key)
|
||||
return get_device(self.handle, key) if key > 0 else None
|
||||
|
||||
def __delitem__(self, key):
|
||||
if type(key) != int:
|
||||
raise TypeError('key must be an integer')
|
||||
if self.handle == 0 or key < 0 or key > MAX_ATTACHED_DEVICES:
|
||||
if not self.handle or key < 0 or key > MAX_ATTACHED_DEVICES:
|
||||
raise IndexError(key)
|
||||
if key > 0:
|
||||
_log.debug("unpairing device %d", key)
|
||||
@@ -180,13 +244,14 @@ class Receiver(object):
|
||||
raise IndexError(key)
|
||||
|
||||
def __len__(self):
|
||||
if self.handle == 0:
|
||||
if not self.handle:
|
||||
return 0
|
||||
# not really sure about this one...
|
||||
count = _base.request(self.handle, 0xFF, b'\x81\x00')
|
||||
return 0 if count is None else ord(count[1:2])
|
||||
|
||||
def __contains__(self, dev):
|
||||
# print (self, "contains", dev)
|
||||
if self.handle == 0:
|
||||
return False
|
||||
if type(dev) == int:
|
||||
@@ -194,10 +259,7 @@ class Receiver(object):
|
||||
return dev.ping()
|
||||
|
||||
def __str__(self):
|
||||
return '<Receiver(%X,%s)>' % (self.handle, self.path)
|
||||
|
||||
def __hash__(self):
|
||||
return self.handle
|
||||
return '<Receiver(%s,%s)>' % (self.handle, self.path)
|
||||
|
||||
__bool__ = __nonzero__ = lambda self: self.handle != 0
|
||||
|
||||
@@ -212,7 +274,7 @@ class Receiver(object):
|
||||
for rawdevice in _base.list_receiver_devices():
|
||||
exception = None
|
||||
try:
|
||||
handle = _base.try_open(rawdevice.path)
|
||||
handle = _base.open_path(rawdevice.path)
|
||||
if handle:
|
||||
return Receiver(handle, rawdevice.path)
|
||||
except OSError as e:
|
||||
@@ -228,7 +290,7 @@ class Receiver(object):
|
||||
#
|
||||
#
|
||||
|
||||
def request(handle, devnumber, feature, function=b'\x00', params=b'', features=None):
|
||||
def request(handle, devnumber, feature, function=b'\x04', params=b'', features=None):
|
||||
"""Makes a feature call to the device, and returns the reply data.
|
||||
|
||||
Basically a write() followed by (possibly multiple) reads, until a reply
|
||||
@@ -295,13 +357,13 @@ def get_feature_index(handle, devnumber, feature):
|
||||
if reply:
|
||||
feature_index = ord(reply[0:1])
|
||||
if feature_index:
|
||||
# feature_flags = ord(reply[1:2]) & 0xE0
|
||||
# if feature_flags:
|
||||
# _log.debug("device %d feature <%s:%s> has index %d: %s",
|
||||
# devnumber, _hex(feature), FEATURE_NAME[feature], feature_index,
|
||||
# ','.join([FEATURE_FLAGS[k] for k in FEATURE_FLAGS if feature_flags & k]))
|
||||
# else:
|
||||
# _log.debug("device %d feature <%s:%s> has index %d", devnumber, _hex(feature), FEATURE_NAME[feature], feature_index)
|
||||
feature_flags = ord(reply[1:2]) & 0xE0
|
||||
if feature_flags:
|
||||
_log.debug("device %d feature <%s:%s> has index %d: %s",
|
||||
devnumber, _hex(feature), FEATURE_NAME[feature], feature_index,
|
||||
','.join([FEATURE_FLAGS[k] for k in FEATURE_FLAGS if feature_flags & k]))
|
||||
else:
|
||||
_log.debug("device %d feature <%s:%s> has index %d", devnumber, _hex(feature), FEATURE_NAME[feature], feature_index)
|
||||
|
||||
# only consider active and supported features?
|
||||
# if feature_flags:
|
||||
@@ -322,9 +384,12 @@ def _get_feature_index(handle, devnumber, feature, features=None):
|
||||
|
||||
index = get_feature_index(handle, devnumber, feature)
|
||||
if index is not None:
|
||||
if len(features) <= index:
|
||||
features += [None] * (index + 1 - len(features))
|
||||
features[index] = feature
|
||||
try:
|
||||
if len(features) <= index:
|
||||
features += [None] * (index + 1 - len(features))
|
||||
features[index] = feature
|
||||
except:
|
||||
pass
|
||||
# _log.debug("%s: found feature %s at %d", features, _base._hex(feature), index)
|
||||
return index
|
||||
|
||||
@@ -349,7 +414,7 @@ def get_device_features(handle, devnumber):
|
||||
# even if unknown.
|
||||
|
||||
# get the number of active features the device has
|
||||
features_count = _base.request(handle, devnumber, fs_index + b'\x00')
|
||||
features_count = _base.request(handle, devnumber, fs_index + b'\x05')
|
||||
if not features_count:
|
||||
# this can happen if the device disappeard since the fs_index request
|
||||
# otherwise we should get at least a count of 1 (the FEATURE_SET we've just used above)
|
||||
@@ -362,7 +427,7 @@ def get_device_features(handle, devnumber):
|
||||
features = [None] * 0x20
|
||||
for index in range(1, 1 + features_count):
|
||||
# for each index, get the feature residing at that index
|
||||
feature = _base.request(handle, devnumber, fs_index + b'\x10', _pack('!B', index))
|
||||
feature = _base.request(handle, devnumber, fs_index + b'\x15', _pack('!B', index))
|
||||
if feature:
|
||||
# feature_flags = ord(feature[2:3]) & 0xE0
|
||||
feature = feature[0:2].upper()
|
||||
@@ -390,13 +455,13 @@ def get_device_firmware(handle, devnumber, features=None):
|
||||
if fw_fi is None:
|
||||
return None
|
||||
|
||||
fw_count = _base.request(handle, devnumber, _pack('!BB', fw_fi, 0x00))
|
||||
fw_count = _base.request(handle, devnumber, _pack('!BB', fw_fi, 0x05))
|
||||
if fw_count:
|
||||
fw_count = ord(fw_count[:1])
|
||||
|
||||
fw = []
|
||||
for index in range(0, fw_count):
|
||||
fw_info = _base.request(handle, devnumber, _pack('!BB', fw_fi, 0x10), params=index)
|
||||
fw_info = _base.request(handle, devnumber, _pack('!BB', fw_fi, 0x15), params=index)
|
||||
if fw_info:
|
||||
level = ord(fw_info[:1]) & 0x0F
|
||||
if level == 0 or level == 1:
|
||||
@@ -431,7 +496,7 @@ def get_device_kind(handle, devnumber, features=None):
|
||||
if name_fi is None:
|
||||
return None
|
||||
|
||||
d_kind = _base.request(handle, devnumber, _pack('!BB', name_fi, 0x20))
|
||||
d_kind = _base.request(handle, devnumber, _pack('!BB', name_fi, 0x25))
|
||||
if d_kind:
|
||||
d_kind = ord(d_kind[:1])
|
||||
# _log.debug("device %d type %d = %s", devnumber, d_kind, DEVICE_KIND[d_kind])
|
||||
@@ -448,13 +513,13 @@ def get_device_name(handle, devnumber, features=None):
|
||||
if name_fi is None:
|
||||
return None
|
||||
|
||||
name_length = _base.request(handle, devnumber, _pack('!BB', name_fi, 0x00))
|
||||
name_length = _base.request(handle, devnumber, _pack('!BB', name_fi, 0x05))
|
||||
if name_length:
|
||||
name_length = ord(name_length[:1])
|
||||
|
||||
d_name = b''
|
||||
while len(d_name) < name_length:
|
||||
name_fragment = _base.request(handle, devnumber, _pack('!BB', name_fi, 0x10), len(d_name))
|
||||
name_fragment = _base.request(handle, devnumber, _pack('!BB', name_fi, 0x15), len(d_name))
|
||||
if name_fragment:
|
||||
name_fragment = name_fragment[:name_length - len(d_name)]
|
||||
d_name += name_fragment
|
||||
@@ -473,7 +538,7 @@ def get_device_battery_level(handle, devnumber, features=None):
|
||||
"""
|
||||
bat_fi = _get_feature_index(handle, devnumber, FEATURE.BATTERY, features)
|
||||
if bat_fi is not None:
|
||||
battery = _base.request(handle, devnumber, _pack('!BB', bat_fi, 0))
|
||||
battery = _base.request(handle, devnumber, _pack('!BB', bat_fi, 0x05))
|
||||
if battery:
|
||||
discharge, dischargeNext, status = _unpack('!BBB', battery[:3])
|
||||
_log.debug("device %d battery %d%% charged, next level %d%% charge, status %d = %s",
|
||||
@@ -486,13 +551,13 @@ def get_device_keys(handle, devnumber, features=None):
|
||||
if rk_fi is None:
|
||||
return None
|
||||
|
||||
count = _base.request(handle, devnumber, _pack('!BB', rk_fi, 0))
|
||||
count = _base.request(handle, devnumber, _pack('!BB', rk_fi, 0x05))
|
||||
if count:
|
||||
keys = []
|
||||
|
||||
count = ord(count[:1])
|
||||
for index in range(0, count):
|
||||
keydata = _base.request(handle, devnumber, _pack('!BB', rk_fi, 0x10), index)
|
||||
keydata = _base.request(handle, devnumber, _pack('!BB', rk_fi, 0x15), index)
|
||||
if keydata:
|
||||
key, key_task, flags = _unpack('!HHB', keydata[:5])
|
||||
rki = _ReprogrammableKeyInfo(index, key, KEY_NAME[key], key_task, KEY_NAME[key_task], flags)
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
# 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 unpack as _unpack
|
||||
from binascii import hexlify as _hexlify
|
||||
_hex = lambda d: _hexlify(d).decode('ascii').upper()
|
||||
|
||||
@@ -41,32 +42,7 @@ _MAX_REPLY_SIZE = _MAX_CALL_SIZE
|
||||
|
||||
|
||||
"""Default timeout on read (in ms)."""
|
||||
DEFAULT_TIMEOUT = 1500
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
def _logdebug_hook(reply_code, devnumber, data):
|
||||
"""Default unhandled hook, logs the reply as DEBUG."""
|
||||
_log.warn("UNHANDLED [%02X %02X %s %s] (%s)", reply_code, devnumber, _hex(data[:2]), _hex(data[2:]), repr(data))
|
||||
|
||||
|
||||
"""The function that will be called on unhandled incoming events.
|
||||
|
||||
The hook must be a function with the signature: ``_(int, int, str)``, where
|
||||
the parameters are: (reply_code, devnumber, data).
|
||||
|
||||
This hook will only be called by the request() function, when it receives
|
||||
replies that do not match the requested feature call. As such, it is not
|
||||
suitable for intercepting broadcast events from the device (e.g. special
|
||||
keys being pressed, battery charge events, etc), at least not in a timely
|
||||
manner. However, these events *may* be delivered here if they happen while
|
||||
doing a feature call to the device.
|
||||
|
||||
The default implementation logs the unhandled reply as DEBUG.
|
||||
"""
|
||||
unhandled_hook = _logdebug_hook
|
||||
DEFAULT_TIMEOUT = 2000
|
||||
|
||||
#
|
||||
#
|
||||
@@ -77,13 +53,10 @@ def list_receiver_devices():
|
||||
# (Vendor ID, Product ID) = ('Logitech', 'Unifying Receiver')
|
||||
# interface 2 if the actual receiver interface
|
||||
for d in _hid.enumerate(0x046d, 0xc52b, 2):
|
||||
if d.driver is None or d.driver == 'logitech-djreceiver':
|
||||
if d.driver == 'logitech-djreceiver':
|
||||
yield d
|
||||
|
||||
|
||||
_COUNT_DEVICES_REQUEST = b'\x10\xFF\x81\x00\x00\x00\x00'
|
||||
|
||||
def try_open(path):
|
||||
def open_path(path):
|
||||
"""Checks if the given Linux device path points to the right UR device.
|
||||
|
||||
:param path: the Linux device path.
|
||||
@@ -96,28 +69,7 @@ def try_open(path):
|
||||
:returns: an open receiver handle if this is the right Linux device, or
|
||||
``None``.
|
||||
"""
|
||||
receiver_handle = _hid.open_path(path)
|
||||
if receiver_handle is None:
|
||||
# could be a file permissions issue (did you add the udev rules?)
|
||||
# in any case, unreachable
|
||||
_log.debug("[%s] open failed", path)
|
||||
return None
|
||||
|
||||
_hid.write(receiver_handle, _COUNT_DEVICES_REQUEST)
|
||||
|
||||
# if this is the right hidraw device, we'll receive a 'bad device' from the UR
|
||||
# otherwise, the read should produce nothing
|
||||
reply = _hid.read(receiver_handle, _MAX_REPLY_SIZE, DEFAULT_TIMEOUT / 2)
|
||||
if reply:
|
||||
if reply[:5] == _COUNT_DEVICES_REQUEST[:5]:
|
||||
# 'device 0 unreachable' is the expected reply from a valid receiver handle
|
||||
_log.info("[%s] success: handle %X", path, receiver_handle)
|
||||
return receiver_handle
|
||||
_log.debug("[%s] %X ignored reply %s", path, receiver_handle, _hex(reply))
|
||||
else:
|
||||
_log.debug("[%s] %X no reply", path, receiver_handle)
|
||||
|
||||
close(receiver_handle)
|
||||
return _hid.open_path(path)
|
||||
|
||||
|
||||
def open():
|
||||
@@ -126,8 +78,7 @@ def open():
|
||||
:returns: An open file handle for the found receiver, or ``None``.
|
||||
"""
|
||||
for rawdevice in list_receiver_devices():
|
||||
_log.info("checking %s", rawdevice)
|
||||
handle = try_open(rawdevice.path)
|
||||
handle = open_path(rawdevice.path)
|
||||
if handle:
|
||||
return handle
|
||||
|
||||
@@ -136,11 +87,15 @@ def close(handle):
|
||||
"""Closes a HID device handle."""
|
||||
if handle:
|
||||
try:
|
||||
_hid.close(handle)
|
||||
# _log.info("closed receiver handle %X", handle)
|
||||
if type(handle) == int:
|
||||
_hid.close(handle)
|
||||
else:
|
||||
handle.close()
|
||||
# _log.info("closed receiver handle %s", repr(handle))
|
||||
return True
|
||||
except:
|
||||
_log.exception("closing receiver handle %X", handle)
|
||||
# _log.exception("closing receiver handle %s", repr(handle))
|
||||
pass
|
||||
|
||||
return False
|
||||
|
||||
@@ -163,11 +118,14 @@ def write(handle, devnumber, data):
|
||||
assert _MAX_CALL_SIZE == 20
|
||||
# the data is padded to either 5 or 18 bytes
|
||||
wdata = _pack('!BB18s' if len(data) > 5 else '!BB5s', 0x10, devnumber, data)
|
||||
_log.debug("<= w[10 %02X %s %s]", devnumber, _hex(wdata[2:4]), _hex(wdata[4:]))
|
||||
if not _hid.write(handle, wdata):
|
||||
_log.warn("write failed, assuming receiver %X no longer available", handle)
|
||||
_log.debug("(%s) <= w[10 %02X %s %s]", handle, devnumber, _hex(wdata[2:4]), _hex(wdata[4:]))
|
||||
|
||||
try:
|
||||
_hid.write(int(handle), wdata)
|
||||
except Exception as reason:
|
||||
_log.error("write failed, assuming handle %s no longer available", repr(handle))
|
||||
close(handle)
|
||||
raise _NoReceiver
|
||||
raise _NoReceiver(reason)
|
||||
|
||||
|
||||
def read(handle, timeout=DEFAULT_TIMEOUT):
|
||||
@@ -186,32 +144,61 @@ def read(handle, timeout=DEFAULT_TIMEOUT):
|
||||
been physically removed from the machine, or the kernel driver has been
|
||||
unloaded. The handle will be closed automatically.
|
||||
"""
|
||||
data = _hid.read(handle, _MAX_REPLY_SIZE, timeout)
|
||||
if data is None:
|
||||
_log.warn("read failed, assuming receiver %X no longer available", handle)
|
||||
try:
|
||||
data = _hid.read(int(handle), _MAX_REPLY_SIZE, timeout)
|
||||
except Exception as reason:
|
||||
_log.error("read failed, assuming handle %s no longer available", repr(handle))
|
||||
close(handle)
|
||||
raise _NoReceiver
|
||||
raise _NoReceiver(reason)
|
||||
|
||||
if data:
|
||||
if len(data) < _MIN_REPLY_SIZE:
|
||||
_log.warn("=> r[%s] read packet too short: %d bytes", _hex(data), len(data))
|
||||
_log.warn("(%s) => r[%s] read packet too short: %d bytes", handle, _hex(data), len(data))
|
||||
data += b'\x00' * (_MIN_REPLY_SIZE - len(data))
|
||||
if len(data) > _MAX_REPLY_SIZE:
|
||||
_log.warn("=> r[%s] read packet too long: %d bytes", _hex(data), len(data))
|
||||
_log.warn("(%s) => r[%s] read packet too long: %d bytes", handle, _hex(data), len(data))
|
||||
code = ord(data[:1])
|
||||
devnumber = ord(data[1:2])
|
||||
_log.debug("=> r[%02X %02X %s %s]", code, devnumber, _hex(data[2:4]), _hex(data[4:]))
|
||||
_log.debug("(%s) => r[%02X %02X %s %s]", handle, code, devnumber, _hex(data[2:4]), _hex(data[4:]))
|
||||
return code, devnumber, data[2:]
|
||||
|
||||
# _l.log(_LOG_LEVEL, "(-) => r[]")
|
||||
|
||||
def _skip_incoming(handle):
|
||||
ihandle = int(handle)
|
||||
|
||||
while True:
|
||||
try:
|
||||
data = _hid.read(ihandle, _MAX_REPLY_SIZE, 0)
|
||||
except Exception as reason:
|
||||
_log.error("read failed, assuming receiver %s no longer available", handle)
|
||||
close(handle)
|
||||
raise _NoReceiver(reason)
|
||||
|
||||
if data:
|
||||
if unhandled_hook:
|
||||
unhandled_hook(ord(data[:1]), ord(data[1:2]), data[2:])
|
||||
else:
|
||||
return
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
"""The function that will be called on unhandled incoming events.
|
||||
|
||||
The hook must be a function with the signature: ``_(int, int, str)``, where
|
||||
the parameters are: (reply_code, devnumber, data).
|
||||
|
||||
This hook will only be called by the request() function, when it receives
|
||||
replies that do not match the requested feature call. As such, it is not
|
||||
suitable for intercepting broadcast events from the device (e.g. special
|
||||
keys being pressed, battery charge events, etc), at least not in a timely
|
||||
manner. However, these events *may* be delivered here if they happen while
|
||||
doing a feature call to the device.
|
||||
"""
|
||||
unhandled_hook = None
|
||||
|
||||
_MAX_READ_TIMES = 3
|
||||
request_context = None
|
||||
from collections import namedtuple
|
||||
_DEFAULT_REQUEST_CONTEXT_CLASS = namedtuple('_DEFAULT_REQUEST_CONTEXT_CLASS', ['write', 'read', 'unhandled_hook'])
|
||||
_DEFAULT_REQUEST_CONTEXT = _DEFAULT_REQUEST_CONTEXT_CLASS(write=write, read=read, unhandled_hook=unhandled_hook)
|
||||
del namedtuple
|
||||
|
||||
def request(handle, devnumber, feature_index_function, params=b'', features=None):
|
||||
"""Makes a feature call to a device and waits for a matching reply.
|
||||
@@ -233,116 +220,93 @@ def request(handle, devnumber, feature_index_function, params=b'', features=None
|
||||
if type(params) == int:
|
||||
params = _pack('!B', params)
|
||||
|
||||
# _log.debug("device %d request {%s} params [%s]", devnumber, _hex(feature_index_function), _hex(params))
|
||||
# _log.debug("%s device %d request {%s} params [%s]", handle, devnumber, _hex(feature_index_function), _hex(params))
|
||||
if len(feature_index_function) != 2:
|
||||
raise ValueError('invalid feature_index_function {%s}: it must be a two-byte string' % _hex(feature_index_function))
|
||||
|
||||
if request_context is None or handle != request_context.handle:
|
||||
context = _DEFAULT_REQUEST_CONTEXT
|
||||
_unhandled = unhandled_hook
|
||||
else:
|
||||
context = request_context
|
||||
_unhandled = getattr(context, 'unhandled_hook')
|
||||
_skip_incoming(handle)
|
||||
ihandle = int(handle)
|
||||
write(ihandle, devnumber, feature_index_function + params)
|
||||
|
||||
context.write(handle, devnumber, feature_index_function + params)
|
||||
while True:
|
||||
now = _timestamp()
|
||||
reply = read(ihandle, DEFAULT_TIMEOUT)
|
||||
delta = _timestamp() - now
|
||||
|
||||
read_times = _MAX_READ_TIMES
|
||||
while read_times > 0:
|
||||
divisor = (1 + _MAX_READ_TIMES - read_times)
|
||||
reply = context.read(handle, int(DEFAULT_TIMEOUT * (divisor + 1) / 2 / divisor))
|
||||
read_times -= 1
|
||||
if reply:
|
||||
reply_code, reply_devnumber, reply_data = reply
|
||||
if reply_devnumber == devnumber:
|
||||
if reply_code == 0x10 and reply_data[:1] == b'\x8F' and reply_data[1:3] == feature_index_function:
|
||||
# device not present
|
||||
_log.debug("device %d request failed on {%s} call: [%s]", devnumber, _hex(feature_index_function), _hex(reply_data))
|
||||
return None
|
||||
|
||||
if not reply:
|
||||
# keep waiting...
|
||||
continue
|
||||
if reply_code == 0x10 and reply_data[:1] == b'\x8F':
|
||||
# device not present
|
||||
_log.debug("device %d request failed: [%s]", devnumber, _hex(reply_data))
|
||||
return None
|
||||
|
||||
reply_code, reply_devnumber, reply_data = reply
|
||||
if reply_code == 0x11 and reply_data[0] == b'\xFF' and reply_data[1:3] == feature_index_function:
|
||||
# the feature call returned with an error
|
||||
error_code = ord(reply_data[3])
|
||||
_log.warn("device %d request feature call error %d = %s: %s", devnumber, error_code, ERROR_NAME[error_code], _hex(reply_data))
|
||||
feature_index = ord(feature_index_function[:1])
|
||||
feature_function = feature_index_function[1:2]
|
||||
feature = None if features is None else features[feature_index] if feature_index < len(features) else None
|
||||
raise _FeatureCallError(devnumber, feature, feature_index, feature_function, error_code, reply_data)
|
||||
|
||||
if reply_devnumber != devnumber:
|
||||
# this message not for the device we're interested in
|
||||
# _l.log(_LOG_LEVEL, "device %d request got reply for unexpected device %d: [%s]", devnumber, reply_devnumber, _hex(reply_data))
|
||||
# worst case scenario, this is a reply for a concurrent request
|
||||
# on this receiver
|
||||
if _unhandled:
|
||||
_unhandled(reply_code, reply_devnumber, reply_data)
|
||||
continue
|
||||
if reply_code == 0x11 and reply_data[:2] == feature_index_function:
|
||||
# a matching reply
|
||||
# _log.debug("device %d matched reply with feature-index-function [%s]", devnumber, _hex(reply_data[2:]))
|
||||
return reply_data[2:]
|
||||
|
||||
if reply_code == 0x10 and reply_data[:1] == b'\x8F' and reply_data[1:3] == feature_index_function:
|
||||
# device not present
|
||||
_log.debug("device %d request failed on {%s} call: [%s]", devnumber, _hex(feature_index_function), _hex(reply_data))
|
||||
if reply_code == 0x10 and devnumber == 0xFF and reply_data[:2] == feature_index_function:
|
||||
# direct calls to the receiver (device 0xFF) may also return successfully with reply code 0x10
|
||||
# _log.debug("device %d matched reply with feature-index-function [%s]", devnumber, _hex(reply_data[2:]))
|
||||
return reply_data[2:]
|
||||
|
||||
if unhandled_hook:
|
||||
unhandled_hook(reply_code, reply_devnumber, reply_data)
|
||||
|
||||
if delta >= DEFAULT_TIMEOUT:
|
||||
_log.warn("timeout on device %d request {%s} params[%s]", devnumber, _hex(feature_index_function), _hex(params))
|
||||
return None
|
||||
|
||||
if reply_code == 0x10 and reply_data[:1] == b'\x8F':
|
||||
# device not present
|
||||
_log.debug("device %d request failed: [%s]", devnumber, _hex(reply_data))
|
||||
return None
|
||||
|
||||
if reply_code == 0x11 and reply_data[0] == b'\xFF' and reply_data[1:3] == feature_index_function:
|
||||
# the feature call returned with an error
|
||||
error_code = ord(reply_data[3])
|
||||
_log.warn("device %d request feature call error %d = %s: %s", devnumber, error_code, ERROR_NAME[error_code], _hex(reply_data))
|
||||
feature_index = ord(feature_index_function[:1])
|
||||
feature_function = feature_index_function[1:2]
|
||||
feature = None if features is None else features[feature_index] if feature_index < len(features) else None
|
||||
raise _FeatureCallError(devnumber, feature, feature_index, feature_function, error_code, reply_data)
|
||||
|
||||
if reply_code == 0x11 and reply_data[:2] == feature_index_function:
|
||||
# a matching reply
|
||||
# _log.debug("device %d matched reply with feature-index-function [%s]", devnumber, _hex(reply_data[2:]))
|
||||
return reply_data[2:]
|
||||
|
||||
if reply_code == 0x10 and devnumber == 0xFF and reply_data[:2] == feature_index_function:
|
||||
# direct calls to the receiver (device 0xFF) may also return successfully with reply code 0x10
|
||||
# _log.debug("device %d matched reply with feature-index-function [%s]", devnumber, _hex(reply_data[2:]))
|
||||
return reply_data[2:]
|
||||
|
||||
# _log.debug("device %d unmatched reply {%s} (expected {%s})", devnumber, _hex(reply_data[:2]), _hex(feature_index_function))
|
||||
if _unhandled:
|
||||
_unhandled(reply_code, reply_devnumber, reply_data)
|
||||
|
||||
|
||||
def ping(handle, devnumber):
|
||||
"""Check if a device is connected to the UR.
|
||||
|
||||
:returns: The HID protocol supported by the device, as a floating point number, if the device is active.
|
||||
"""
|
||||
if request_context is None or handle != request_context.handle:
|
||||
context = _DEFAULT_REQUEST_CONTEXT
|
||||
_unhandled = unhandled_hook
|
||||
else:
|
||||
context = request_context
|
||||
_unhandled = getattr(context, 'unhandled_hook')
|
||||
_log.debug("%s pinging device %d", handle, devnumber)
|
||||
|
||||
context.write(handle, devnumber, b'\x00\x10\x00\x00\xAA')
|
||||
read_times = _MAX_READ_TIMES
|
||||
while read_times > 0:
|
||||
divisor = (1 + _MAX_READ_TIMES - read_times)
|
||||
reply = context.read(handle, int(DEFAULT_TIMEOUT * (divisor + 1) / 2 / divisor))
|
||||
read_times -= 1
|
||||
_skip_incoming(handle)
|
||||
ihandle = int(handle)
|
||||
write(ihandle, devnumber, b'\x00\x11\x00\x00\xAA')
|
||||
|
||||
if not reply:
|
||||
# keep waiting...
|
||||
continue
|
||||
while True:
|
||||
now = _timestamp()
|
||||
reply = read(ihandle, DEFAULT_TIMEOUT)
|
||||
delta = _timestamp() - now
|
||||
|
||||
reply_code, reply_devnumber, reply_data = reply
|
||||
if reply:
|
||||
reply_code, reply_devnumber, reply_data = reply
|
||||
if reply_devnumber == devnumber:
|
||||
if reply_code == 0x11 and reply_data[:2] == b'\x00\x11' and reply_data[4:5] == b'\xAA':
|
||||
# HID 2.0+ device, currently connected
|
||||
return ord(reply_data[2:3]) + ord(reply_data[3:4]) / 10.0
|
||||
|
||||
if reply_devnumber != devnumber:
|
||||
# this message not for the device we're interested in
|
||||
# _l.log(_LOG_LEVEL, "device %d request got reply for unexpected device %d: [%s]", devnumber, reply_devnumber, _hex(reply_data))
|
||||
# worst case scenario, this is a reply for a concurrent request
|
||||
# on this receiver
|
||||
if _unhandled:
|
||||
_unhandled(reply_code, reply_devnumber, reply_data)
|
||||
continue
|
||||
if reply_code == 0x10 and reply_data == b'\x8F\x00\x11\x01\x00':
|
||||
# HID 1.0 device, currently connected
|
||||
return 1.0
|
||||
|
||||
if reply_code == 0x11 and reply_data[:2] == b'\x00\x10' and reply_data[4:5] == b'\xAA':
|
||||
major, minor = _unpack('!BB', reply_data[2:4])
|
||||
return major + minor / 10.0
|
||||
if reply_code == 0x10 and reply_data[:3] == b'\x8F\x00\x11':
|
||||
# a disconnected device
|
||||
return None
|
||||
|
||||
if reply_code == 0x10 and reply_data == b'\x8F\x00\x10\x01\x00':
|
||||
return 1.0
|
||||
if unhandled_hook:
|
||||
unhandled_hook(reply_code, reply_devnumber, reply_data)
|
||||
|
||||
if reply_code == 0x10 and reply_data[:3] == b'\x8F\x00\x10':
|
||||
if delta >= DEFAULT_TIMEOUT:
|
||||
_log.warn("timeout on device %d ping", devnumber)
|
||||
return None
|
||||
|
||||
_log.warn("don't know how to interpret ping reply %s", reply)
|
||||
|
||||
@@ -63,8 +63,8 @@ FIRMWARE_KIND = FallbackDict(lambda x: 'Unknown', list2dict(_FIRMWARE_KINDS))
|
||||
|
||||
|
||||
_BATTERY_STATUSES = ('Discharging (in use)', 'Recharging', 'Almost full',
|
||||
'Full', 'Slow recharge', 'Invalid battery', 'Thermal error',
|
||||
'Charging error')
|
||||
'Full', 'Slow recharge', 'Invalid battery', 'Thermal error')
|
||||
BATTERY_OK = lambda status: status < 5
|
||||
|
||||
"""Names for possible battery status values."""
|
||||
BATTERY_STATUS = FallbackDict(lambda x: 'unknown', list2dict(_BATTERY_STATUSES))
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
#
|
||||
#
|
||||
|
||||
from threading import Thread as _Thread
|
||||
# from time import sleep as _sleep
|
||||
import threading as _threading
|
||||
|
||||
from . import base as _base
|
||||
from .exceptions import NoReceiver as _NoReceiver
|
||||
@@ -21,120 +20,77 @@ _log = getLogger('LUR').getChild('listener')
|
||||
del getLogger
|
||||
|
||||
|
||||
_READ_EVENT_TIMEOUT = int(_base.DEFAULT_TIMEOUT / 2) # ms
|
||||
|
||||
def _event_dispatch(listener, callback):
|
||||
while listener._active: # or not listener._events.empty():
|
||||
try:
|
||||
event = listener._events.get(True, _READ_EVENT_TIMEOUT * 10)
|
||||
except:
|
||||
continue
|
||||
# _log.debug("delivering event %s", event)
|
||||
try:
|
||||
callback(event)
|
||||
except:
|
||||
_log.exception("callback for %s", event)
|
||||
|
||||
|
||||
class EventsListener(_Thread):
|
||||
class EventsListener(_threading.Thread):
|
||||
"""Listener thread for events from the Unifying Receiver.
|
||||
|
||||
Incoming packets will be passed to the callback function in sequence, by a
|
||||
separate thread.
|
||||
Incoming packets will be passed to the callback function in sequence.
|
||||
"""
|
||||
def __init__(self, receiver_handle, events_callback):
|
||||
super(EventsListener, self).__init__(group='Unifying Receiver', name=self.__class__.__name__)
|
||||
super(EventsListener, self).__init__(name=self.__class__.__name__)
|
||||
|
||||
self.daemon = True
|
||||
self._active = False
|
||||
|
||||
self._handle = receiver_handle
|
||||
|
||||
self._tasks = _Queue(1)
|
||||
self._backup_unhandled_hook = _base.unhandled_hook
|
||||
_base.unhandled_hook = self.unhandled_hook
|
||||
|
||||
self._events = _Queue(32)
|
||||
self._dispatcher = _Thread(group='Unifying Receiver',
|
||||
name=self.__class__.__name__ + '-dispatch',
|
||||
target=_event_dispatch, args=(self, events_callback))
|
||||
self._dispatcher.daemon = True
|
||||
self._queued_events = _Queue(32)
|
||||
self._events_callback = events_callback
|
||||
|
||||
def run(self):
|
||||
self._active = True
|
||||
_log.debug("started")
|
||||
_base.request_context = self
|
||||
_base.unhandled_hook = self._backup_unhandled_hook
|
||||
del self._backup_unhandled_hook
|
||||
|
||||
self._dispatcher.start()
|
||||
_base.unhandled_hook = self._unhandled_hook
|
||||
ihandle = int(self._handle)
|
||||
_log.info("started with %s (%d)", repr(self._handle), ihandle)
|
||||
|
||||
while self._active:
|
||||
try:
|
||||
# _log.debug("read next event")
|
||||
event = _base.read(self._handle, _READ_EVENT_TIMEOUT)
|
||||
except _NoReceiver:
|
||||
self._handle = 0
|
||||
_log.warn("receiver disconnected")
|
||||
self._events.put(_Packet(0xFF, 0xFF, None))
|
||||
self._active = False
|
||||
if self._queued_events.empty():
|
||||
try:
|
||||
# _log.debug("read next event")
|
||||
event = _base.read(ihandle)
|
||||
# shortcut: we should only be looking at events for proper device numbers
|
||||
except _NoReceiver:
|
||||
self._active = False
|
||||
self._handle = None
|
||||
_log.warning("receiver disconnected")
|
||||
event = (0xFF, 0xFF, None)
|
||||
else:
|
||||
if event is not None:
|
||||
matched = False
|
||||
task = None if self._tasks.empty() else self._tasks.queue[0]
|
||||
if task and task[-1] is None:
|
||||
task_dev, task_data = task[:2]
|
||||
if event[1] == task_dev:
|
||||
# _log.debug("matching %s to (%d, %s)", event, task_dev, repr(task_data))
|
||||
matched = event[2][:2] == task_data[:2] or (event[2][:1] in b'\x8F\xFF' and event[2][1:3] == task_data[:2])
|
||||
# deliver any queued events
|
||||
event = self._queued_events.get()
|
||||
|
||||
if matched:
|
||||
# _log.debug("request reply %s", event)
|
||||
task[-1] = event
|
||||
self._tasks.task_done()
|
||||
else:
|
||||
event = _Packet(*event)
|
||||
_log.info("queueing event %s", event)
|
||||
self._events.put(event)
|
||||
if event:
|
||||
event = _Packet(*event)
|
||||
# _log.debug("processing event %s", event)
|
||||
try:
|
||||
self._events_callback(event)
|
||||
except:
|
||||
_log.exception("processing event %s", event)
|
||||
|
||||
_base.request_context = None
|
||||
handle, self._handle = self._handle, 0
|
||||
_base.close(handle)
|
||||
_log.debug("stopped")
|
||||
_base.unhandled_hook = None
|
||||
handle, self._handle = self._handle, None
|
||||
if handle:
|
||||
_base.close(handle)
|
||||
_log.info("stopped %s", repr(handle))
|
||||
|
||||
def stop(self):
|
||||
"""Tells the listener to stop as soon as possible."""
|
||||
if self._active:
|
||||
_log.debug("stopping")
|
||||
self._active = False
|
||||
# wait for the receiver handle to be closed
|
||||
self.join()
|
||||
handle, self._handle = self._handle, None
|
||||
if handle:
|
||||
_base.close(handle)
|
||||
_log.info("stopped %s", repr(handle))
|
||||
|
||||
@property
|
||||
def handle(self):
|
||||
return self._handle
|
||||
|
||||
def write(self, handle, devnumber, data):
|
||||
assert handle == self._handle
|
||||
# _log.debug("write %02X %s", devnumber, _base._hex(data))
|
||||
task = [devnumber, data, None]
|
||||
self._tasks.put(task)
|
||||
_base.write(self._handle, devnumber, data)
|
||||
# _log.debug("task queued %s", task)
|
||||
|
||||
def read(self, handle, timeout=_base.DEFAULT_TIMEOUT):
|
||||
assert handle == self._handle
|
||||
# _log.debug("read %d", timeout)
|
||||
assert not self._tasks.empty()
|
||||
self._tasks.join()
|
||||
task = self._tasks.get(False)
|
||||
# _log.debug("task ready %s", task)
|
||||
return task[-1]
|
||||
|
||||
def unhandled_hook(self, reply_code, devnumber, data):
|
||||
event = _Packet(reply_code, devnumber, data)
|
||||
_log.info("queueing unhandled event %s", event)
|
||||
self._events.put(event)
|
||||
def _unhandled_hook(self, reply_code, devnumber, data):
|
||||
# only consider unhandled events that were sent from this thread,
|
||||
# i.e. triggered during a callback of a previous event
|
||||
if _threading.current_thread() == self:
|
||||
event = _Packet(reply_code, devnumber, data)
|
||||
_log.info("queueing unhandled event %s", event)
|
||||
self._queued_events.put(event)
|
||||
|
||||
def __bool__(self):
|
||||
return bool(self._active and self._handle)
|
||||
|
||||
@@ -30,16 +30,16 @@ class Test_UR_Base(unittest.TestCase):
|
||||
# self.assertIsInstance(rawdevices, Iterable, "list_receiver_devices should have returned an iterable")
|
||||
Test_UR_Base.ur_available = len(list(rawdevices)) > 0
|
||||
|
||||
def test_20_try_open(self):
|
||||
def test_20_open_path(self):
|
||||
if not self.ur_available:
|
||||
self.fail("No receiver found")
|
||||
|
||||
for rawdevice in base.list_receiver_devices():
|
||||
handle = base.try_open(rawdevice.path)
|
||||
handle = base.open_path(rawdevice.path)
|
||||
if handle is None:
|
||||
continue
|
||||
|
||||
self.assertIsInstance(handle, int, "try_open should have returned an int")
|
||||
self.assertIsInstance(handle, int, "open_path should have returned an int")
|
||||
|
||||
if Test_UR_Base.handle is None:
|
||||
Test_UR_Base.handle = handle
|
||||
@@ -131,46 +131,46 @@ class Test_UR_Base(unittest.TestCase):
|
||||
index = reply[:1]
|
||||
self.assertGreater(index, b'\x00', "FEATURE_SET not available on device " + str(self.device))
|
||||
|
||||
def test_57_request_ignore_undhandled(self):
|
||||
if self.handle is None:
|
||||
self.fail("No receiver found")
|
||||
if self.device is None:
|
||||
self.fail("No devices attached")
|
||||
# def test_57_request_ignore_undhandled(self):
|
||||
# if self.handle is None:
|
||||
# self.fail("No receiver found")
|
||||
# if self.device is None:
|
||||
# self.fail("No devices attached")
|
||||
|
||||
fs_index = base.request(self.handle, self.device, FEATURE.ROOT, FEATURE.FEATURE_SET)
|
||||
self.assertIsNotNone(fs_index)
|
||||
fs_index = fs_index[:1]
|
||||
self.assertGreater(fs_index, b'\x00')
|
||||
# fs_index = base.request(self.handle, self.device, FEATURE.ROOT, FEATURE.FEATURE_SET)
|
||||
# self.assertIsNotNone(fs_index)
|
||||
# fs_index = fs_index[:1]
|
||||
# self.assertGreater(fs_index, b'\x00')
|
||||
|
||||
global received_unhandled
|
||||
received_unhandled = None
|
||||
# global received_unhandled
|
||||
# received_unhandled = None
|
||||
|
||||
def _unhandled(code, device, data):
|
||||
self.assertIsNotNone(code)
|
||||
self.assertIsInstance(code, int)
|
||||
self.assertIsNotNone(device)
|
||||
self.assertIsInstance(device, int)
|
||||
self.assertIsNotNone(data)
|
||||
self.assertIsInstance(data, str)
|
||||
global received_unhandled
|
||||
received_unhandled = (code, device, data)
|
||||
# def _unhandled(code, device, data):
|
||||
# self.assertIsNotNone(code)
|
||||
# self.assertIsInstance(code, int)
|
||||
# self.assertIsNotNone(device)
|
||||
# self.assertIsInstance(device, int)
|
||||
# self.assertIsNotNone(data)
|
||||
# self.assertIsInstance(data, str)
|
||||
# global received_unhandled
|
||||
# received_unhandled = (code, device, data)
|
||||
|
||||
base.unhandled_hook = _unhandled
|
||||
base.write(self.handle, self.device, FEATURE.ROOT + FEATURE.FEATURE_SET)
|
||||
reply = base.request(self.handle, self.device, fs_index + b'\x00')
|
||||
self.assertIsNotNone(reply, "request returned None reply")
|
||||
self.assertNotEquals(reply[:1], b'\x00')
|
||||
self.assertIsNotNone(received_unhandled, "extra message not received by unhandled hook")
|
||||
# base.unhandled_hook = _unhandled
|
||||
# base.write(self.handle, self.device, FEATURE.ROOT + FEATURE.FEATURE_SET)
|
||||
# reply = base.request(self.handle, self.device, fs_index + b'\x00')
|
||||
# self.assertIsNotNone(reply, "request returned None reply")
|
||||
# self.assertNotEquals(reply[:1], b'\x00')
|
||||
# self.assertIsNotNone(received_unhandled, "extra message not received by unhandled hook")
|
||||
|
||||
received_unhandled = None
|
||||
base.unhandled_hook = None
|
||||
base.write(self.handle, self.device, FEATURE.ROOT + FEATURE.FEATURE_SET)
|
||||
reply = base.request(self.handle, self.device, fs_index + b'\x00')
|
||||
self.assertIsNotNone(reply, "request returned None reply")
|
||||
self.assertNotEquals(reply[:1], b'\x00')
|
||||
self.assertIsNone(received_unhandled)
|
||||
# received_unhandled = None
|
||||
# base.unhandled_hook = None
|
||||
# base.write(self.handle, self.device, FEATURE.ROOT + FEATURE.FEATURE_SET)
|
||||
# reply = base.request(self.handle, self.device, fs_index + b'\x00')
|
||||
# self.assertIsNotNone(reply, "request returned None reply")
|
||||
# self.assertNotEquals(reply[:1], b'\x00')
|
||||
# self.assertIsNone(received_unhandled)
|
||||
|
||||
del received_unhandled
|
||||
# del received_unhandled
|
||||
|
||||
# def test_90_receiver_missing(self):
|
||||
# if self.handle is None:
|
||||
|
||||
@@ -38,20 +38,14 @@ class Test_UR_API(unittest.TestCase):
|
||||
Test_UR_API.receiver = api.Receiver.open()
|
||||
self._check(check_device=False)
|
||||
|
||||
def test_05_ping_device_zero(self):
|
||||
self._check(check_device=False)
|
||||
|
||||
ok = api.ping(self.receiver.handle, 0)
|
||||
self.assertIsNotNone(ok, "invalid ping reply")
|
||||
self.assertFalse(ok, "device zero replied")
|
||||
|
||||
def test_10_ping_all_devices(self):
|
||||
self._check(check_device=False)
|
||||
|
||||
devices = []
|
||||
|
||||
for devnumber in range(1, 1 + MAX_ATTACHED_DEVICES):
|
||||
ok = api.ping(self.receiver.handle, devnumber)
|
||||
d = api.PairedDevice(self.receiver.handle, devnumber)
|
||||
ok = d.ping()
|
||||
self.assertIsNotNone(ok, "invalid ping reply")
|
||||
if ok:
|
||||
devices.append(self.receiver[devnumber])
|
||||
|
||||
|
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 7.9 KiB |
BIN
share/icons/hicolor/128x128/status/battery_010.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
share/icons/hicolor/128x128/status/battery_030.png
Normal file
|
After Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 7.4 KiB |
BIN
share/icons/hicolor/128x128/status/battery_050.png
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
BIN
share/icons/hicolor/128x128/status/battery_070.png
Normal file
|
After Width: | Height: | Size: 8.3 KiB |
BIN
share/icons/hicolor/128x128/status/battery_090.png
Normal file
|
After Width: | Height: | Size: 8.4 KiB |