Compare commits
68 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa72b89b3a | ||
|
|
fd3c88cb67 | ||
|
|
8b44ca913f | ||
|
|
7fe79a703e | ||
|
|
80c36a02a9 | ||
|
|
4bdfe9b9b8 | ||
|
|
767e8a0db4 | ||
|
|
d8a2ffa835 | ||
|
|
d38bec39b6 | ||
|
|
33a9ca060d | ||
|
|
30fedf418c | ||
|
|
5bdacb377c | ||
|
|
ee16892481 | ||
|
|
e2909f6165 | ||
|
|
205d25e341 | ||
|
|
f49ced2d92 | ||
|
|
b86dcce381 | ||
|
|
c4be58f074 | ||
|
|
b3f0bfa4fb | ||
|
|
37daf3a192 | ||
|
|
7ada4af31b | ||
|
|
67db483b0b | ||
|
|
357e118ace | ||
|
|
f2cdbe26b6 | ||
|
|
3569489ce7 | ||
|
|
6c3fa224e0 | ||
|
|
9066003240 | ||
|
|
f0007d0a13 | ||
|
|
ff6db1d00a | ||
|
|
27403a08d2 | ||
|
|
6d70d2aada | ||
|
|
0e551383ba | ||
|
|
b5b86ab8b8 | ||
|
|
61d0159e8a | ||
|
|
c41859816b | ||
|
|
5a99e55309 | ||
|
|
1b6e6692c0 | ||
|
|
116ba72f37 | ||
|
|
3fe9caf0e6 | ||
|
|
a403c3b596 | ||
|
|
2a44b0bb5b | ||
|
|
130a23dd4f | ||
|
|
db0d6e8bbc | ||
|
|
1cc532d600 | ||
|
|
8f5fa0cf9a | ||
|
|
89c6904d69 | ||
|
|
14663ca204 | ||
|
|
64d2b35ace | ||
|
|
ab5e09db93 | ||
|
|
932a015e49 | ||
|
|
d6b18cd426 | ||
|
|
84540fb087 | ||
|
|
5b8c983ab3 | ||
|
|
13a11e78f0 | ||
|
|
fb8cf26c51 | ||
|
|
41db725e15 | ||
|
|
f25d2ba183 | ||
|
|
66531635bc | ||
|
|
4c5cf85091 | ||
|
|
6db4deafee | ||
|
|
2c312c1a5b | ||
|
|
bcc2bf123e | ||
|
|
50fedab19e | ||
|
|
d0ccd3e9c2 | ||
|
|
4b2d8a8d5a | ||
|
|
c12364a7c7 | ||
|
|
560400e786 | ||
|
|
f7a4d89467 |
60
README
@@ -1,60 +0,0 @@
|
||||
Solaar
|
||||
------
|
||||
|
||||
|
||||
This application connects to a Logitech Unifying Receiver
|
||||
(http://www.logitech.com/en-us/66/6079) and listens for events from devices
|
||||
attached to it.
|
||||
|
||||
Currently the K750 solar keyboard is also queried for its solar charge status.
|
||||
Support for other devices could be added in the future, but the K750 keyboard is
|
||||
the only device I have and can test on.
|
||||
|
||||
|
||||
Requirements
|
||||
------------
|
||||
|
||||
- Python (2.7 or 3.2). Either version should work well.
|
||||
- Gtk 3; Gtk 2 should partially work with some problems.
|
||||
- Python GI (GObject Introspection), for Gtk bindings.
|
||||
- pyudev for enumerating udev devices.
|
||||
- Optional libnotify GI bindings, for desktop notifications.
|
||||
|
||||
The necessary packages for Debian/Ubuntu are `python-pyudev`/`python3-pyudev`,
|
||||
`python-gi`/`python3-gi`, `gir1.2-gtk-3.0`, and optionally `gir1.2-notify-0.7`.
|
||||
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
Normally USB devices are not accessible for r/w by regular users, so you will
|
||||
need to install a udev rule to allow access to the Logitech Unifying Receiver.
|
||||
|
||||
In rules.d/ you'll find a udev rule file, to be copied in /etc/udev/rules.d/ (as
|
||||
root).
|
||||
|
||||
In its current form it makes the UR device available for r/w by all users
|
||||
belonging to the 'plugdev' system group (standard Debian/Ubuntu group for
|
||||
pluggable devices). It may need changes, specific to your particular system's
|
||||
configuration.
|
||||
|
||||
If in doubt, replacing GROUP="plugdev" with GROUP="<your username>" should just
|
||||
work.
|
||||
|
||||
After you copy the file to /etc/udev/rules.d (and possibly modify it for your
|
||||
system), run 'udevadm control --reload-rules' as root for it to apply. Then
|
||||
physically remove the Unifying Receiver, wait 30 seconds and re-insert it.
|
||||
|
||||
|
||||
Thanks
|
||||
------
|
||||
|
||||
This project began as a third-hand clone of Noah K. Tilton's logitech-solar-k750
|
||||
project on GitHub (no longer available). It was developed further thanks to the
|
||||
diggings in Logitech's HID protocol done, among others, by Julien Danjou
|
||||
(http://julien.danjou.info/blog/2012/logitech-k750-linux-support) and
|
||||
Lars-Dominik Braun (http://6xq.net/git/lars/lshidpp.git).
|
||||
|
||||
|
||||
Cheers,
|
||||
-pwr
|
||||
101
README.md
Normal file
@@ -0,0 +1,101 @@
|
||||
**Solaar** is a Linux device manager for Logitech's
|
||||
[Unifying Receiver](http://www.logitech.com/en-us/66/6079) peripherals.
|
||||
|
||||
It comes in two flavors, command-line and GUI. Both are able to list the
|
||||
devices paired to a Unifying Receiver, show detailed info for each device, and
|
||||
also pair/unpair supported devices with the receiver.
|
||||
|
||||
## Supported Devices
|
||||
|
||||
**Solaar** will detect all devices paired with your Unifying Receiver, and at
|
||||
the very least display some basic information about them. Depending on the
|
||||
device, it may be able to read its battery status.
|
||||
|
||||
A few devices also have extended support, mostly because I was able to directly
|
||||
test on them:
|
||||
|
||||
* The [K750 Solar Keyboard](http://www.logitech.com/keyboards/keyboard/devices/7454)
|
||||
is also queried for its solar charge status. Pressing the Solar key on the
|
||||
keyboard will pop-up the application window and display the current lighting
|
||||
value (Lux) as reported by the keyboard, similar to Logitech's *Solar.app* for
|
||||
Windows.
|
||||
|
||||
Also, you can change the way the function keys (`F1`..`F12`) work, i.e.
|
||||
whether holding `FN` while pressing the function keys will generate the
|
||||
standard keycodes or the special function (yellow icons) keycodes.
|
||||
|
||||
* The [M705 Marathon Mouse](http://www.logitech.com/product/marathon-mouse-m705)
|
||||
supports turning on/off Smooth Scrolling (higher sensitivity on vertical
|
||||
scrolling with the wheel).
|
||||
|
||||
Extended support for other devices may be added in the future, depending on the
|
||||
documentation available, but the K750 keyboard and M705 mouse are the only
|
||||
devices I have and can test on right now.
|
||||
|
||||
## Requirements
|
||||
|
||||
You should have a reasonably new kernel (3.2+), with the `logitech-djreceiver`
|
||||
driver enabled and loaded; also, the `udev` package must be installed and the
|
||||
daemon running. If you have a modern Linux distribution (2011+), you're most
|
||||
likely good to go.
|
||||
|
||||
The command-line application (`bin/solaar-cli`) requires Python 2.7.3 or 3.2+
|
||||
(either version should work), and the `python-pyudev`/`python3-pyudev` package.
|
||||
|
||||
The GUI application (`bin/solaar`) also requires Gtk3, and its GObject
|
||||
Introspection bindings. The Debian/Ubuntu package names are
|
||||
`python-gi`/`python3-gi` and `gir1.2-gtk-3.0`; if you're using another
|
||||
distribution the required packages are most likely named something similar.
|
||||
If the desktop notifications bindings are also installed (`gir1.2-notify-0.7`),
|
||||
you will also get desktop notifications when devices come online/go offline.
|
||||
|
||||
## Installation
|
||||
|
||||
Normally USB devices are not accessible for r/w by regular users, so you will
|
||||
need to do a one-time udev rule installation to allow access to the Logitech
|
||||
Unifying Receiver.
|
||||
|
||||
You can run the `rules.d/install.sh` script from Solaar to do this installation
|
||||
automatically (it will switch to root when necessary), or you can do all the
|
||||
required steps by hand, as the root user:
|
||||
|
||||
1. copy `rules.d/99-logitech-unfiying-receiver.rules` from Solaar to
|
||||
`/etc/udev/rules.d/`
|
||||
|
||||
By default, the rule makes the Unifying Receiver device available for r/w by
|
||||
all users belonging to the `plugdev` system group (standard Debian/Ubuntu
|
||||
group for pluggable devices). It may need changes, specific to your
|
||||
particular system's configuration. If in doubt, replacing `GROUP="plugdev"`
|
||||
with `GROUP="<your username>"` should just work.
|
||||
|
||||
2. run `udevadm control --reload-rules` to let the udev daemon know about the new
|
||||
rule
|
||||
3. physically remove the Unifying Receiver, wait 10 seconds and re-insert it
|
||||
|
||||
## Known Issues
|
||||
|
||||
- When running under Ubuntu's Unity, the tray icon will probably not appear, nor
|
||||
will the application window. Either run the application with the '-S' option,
|
||||
or whitelist "Solaar" into the systray. For details, see
|
||||
[How do I access and enable more icons to be in the system tray?](http://askubuntu.com/questions/30742/how-do-i-access-and-enable-more-icons-to-be-in-the-system-tray).
|
||||
|
||||
Support for Unity's indicators is a planned feature.
|
||||
|
||||
- Running the command-line application (`bin/solaar-cli`) while the GUI
|
||||
application is also running *may* occasionally cause either of them to become
|
||||
confused about the state of the devices. I haven't encountered this often
|
||||
enough to be able to be able to diagnose it properly yet.
|
||||
|
||||
## Thanks
|
||||
|
||||
This project began as a third-hand clone of [Noah K. Tilton](https://github.com/noah)'s
|
||||
logitech-solar-k750 project on GitHub (no longer available). It was developed
|
||||
further thanks to the diggings in Logitech's HID++ protocol done by many other
|
||||
people:
|
||||
|
||||
- [Julien Danjou](http://julien.danjou.info/blog/2012/logitech-k750-linux-support),
|
||||
who also provided some internal
|
||||
[Logitech documentation](http://julien.danjou.info/blog/2012/logitech-unifying-upower)
|
||||
- [Lars-Dominik Braun](http://6xq.net/git/lars/lshidpp.git)
|
||||
- [Alexander Hofbauer](http://derhofbauer.at/blog/blog/2012/08/28/logitech-performance-mx)
|
||||
- [Clach04](http://bitbucket.org/clach04/logitech-unifying-receiver-tools)
|
||||
152
app/listener.py
Normal file
@@ -0,0 +1,152 @@
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
from logging import getLogger, DEBUG as _DEBUG
|
||||
_log = getLogger('listener')
|
||||
del getLogger
|
||||
|
||||
from types import MethodType as _MethodType
|
||||
|
||||
from logitech.unifying_receiver import (Receiver,
|
||||
listener as _listener,
|
||||
status as _status)
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
class _DUMMY_RECEIVER(object):
|
||||
__slots__ = []
|
||||
name = Receiver.name
|
||||
kind = None
|
||||
max_devices = Receiver.max_devices
|
||||
status = 'Receiver not found.'
|
||||
__bool__ = __nonzero__ = lambda self: False
|
||||
__unicode__ = __str__ = __repr__ = lambda self: 'DUMMY'
|
||||
DUMMY = _DUMMY_RECEIVER()
|
||||
|
||||
from collections import namedtuple
|
||||
_GHOST_DEVICE = namedtuple('_GHOST_DEVICE', ['number', 'name', 'kind', 'status'])
|
||||
del namedtuple
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
_POLL_TICK = 60 # seconds
|
||||
|
||||
|
||||
class ReceiverListener(_listener.EventsListener):
|
||||
"""Keeps the status of a Unifying Receiver.
|
||||
"""
|
||||
def __init__(self, receiver, status_changed_callback=None):
|
||||
super(ReceiverListener, self).__init__(receiver, self._events_handler)
|
||||
self.tick_period = _POLL_TICK
|
||||
self._last_tick = 0
|
||||
|
||||
self.status_changed_callback = status_changed_callback
|
||||
|
||||
receiver.status = _status.ReceiverStatus(receiver, self._status_changed)
|
||||
|
||||
# enhance the original register_new_device
|
||||
def _register_with_status(r, number):
|
||||
if bool(self):
|
||||
dev = r.__register_new_device(number)
|
||||
if dev is not None:
|
||||
# read these as soon as possible, they will be used everywhere
|
||||
dev.protocol, dev.codename
|
||||
dev.status = _status.DeviceStatus(dev, self._status_changed)
|
||||
self._status_changed(r)
|
||||
return dev
|
||||
receiver.__register_new_device = receiver.register_new_device
|
||||
receiver.register_new_device = _MethodType(_register_with_status, receiver)
|
||||
|
||||
def has_started(self):
|
||||
_log.info("events listener has started")
|
||||
self.receiver.enable_notifications()
|
||||
self.receiver.notify_devices()
|
||||
self._status_changed(self.receiver, _status.ALERT.LOW)
|
||||
|
||||
def has_stopped(self):
|
||||
_log.info("events listener has stopped")
|
||||
if self.receiver:
|
||||
self.receiver.enable_notifications(False)
|
||||
self.receiver.close()
|
||||
self.receiver = None
|
||||
self._status_changed(None, _status.ALERT.LOW)
|
||||
|
||||
def tick(self, timestamp):
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("tick: polling status: %s %s", self.receiver, list(iter(self.receiver)))
|
||||
|
||||
if self._last_tick > 0 and timestamp - self._last_tick > _POLL_TICK * 2:
|
||||
# if we missed a couple of polls, most likely the computer went into
|
||||
# sleep, and we have to reinitialize the receiver again
|
||||
_log.warn("possible sleep detected, closing this listener")
|
||||
self.stop()
|
||||
return
|
||||
self._last_tick = timestamp
|
||||
|
||||
# read these in case they haven't been read already
|
||||
self.receiver.serial, self.receiver.firmware
|
||||
if self.receiver.status.lock_open:
|
||||
# don't mess with stuff while pairing
|
||||
return
|
||||
|
||||
for dev in self.receiver:
|
||||
if dev.status is not None:
|
||||
dev.status.poll(timestamp)
|
||||
|
||||
def _status_changed(self, device, alert=_status.ALERT.NONE, reason=None):
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("status_changed %s: %s (%X) %s", device, None if device is None else device.status, alert, reason or '')
|
||||
if self.status_changed_callback:
|
||||
r = self.receiver or DUMMY
|
||||
if device is None or device.kind is None:
|
||||
self.status_changed_callback(r, None, alert, reason)
|
||||
else:
|
||||
if device.status is None:
|
||||
# device was unpaired, and since the object is weakref'ed
|
||||
# it won't be valid for much longer
|
||||
device = _GHOST_DEVICE(number=device.number, name=device.name, kind=device.kind, status=None)
|
||||
self.status_changed_callback(r, device, alert, reason)
|
||||
if device.status is None:
|
||||
self.status_changed_callback(r)
|
||||
|
||||
def _events_handler(self, event):
|
||||
assert self.receiver
|
||||
if event.devnumber == 0xFF:
|
||||
# a receiver event
|
||||
if self.receiver.status is not None:
|
||||
self.receiver.status.process_event(event)
|
||||
else:
|
||||
# a device event
|
||||
assert event.devnumber > 0 and event.devnumber <= self.receiver.max_devices
|
||||
already_known = event.devnumber in self.receiver
|
||||
dev = self.receiver[event.devnumber]
|
||||
if dev and dev.status is not None:
|
||||
dev.status.process_event(event)
|
||||
if self.receiver.status.lock_open and not already_known:
|
||||
# this should be the first event after a device was paired
|
||||
assert event.sub_id == 0x41 and event.address == 0x04
|
||||
_log.info("pairing detected new device")
|
||||
self.receiver.status.new_device = dev
|
||||
else:
|
||||
_log.warn("received event %s for invalid device %d: %s", event, event.devnumber, dev)
|
||||
|
||||
def __str__(self):
|
||||
return '<ReceiverListener(%s,%s)>' % (self.receiver.path, self.receiver.handle)
|
||||
__unicode__ = __str__
|
||||
|
||||
@classmethod
|
||||
def open(self, status_changed_callback=None):
|
||||
receiver = Receiver.open()
|
||||
if receiver:
|
||||
receiver.handle = _listener.ThreadedHandle(receiver.handle, receiver.path)
|
||||
receiver.kind = None
|
||||
rl = ReceiverListener(receiver, status_changed_callback)
|
||||
rl.start()
|
||||
return rl
|
||||
@@ -1,82 +0,0 @@
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
from logging import getLogger as _Logger
|
||||
_l = _Logger('pairing')
|
||||
|
||||
from logitech.unifying_receiver import base as _base
|
||||
|
||||
state = None
|
||||
|
||||
class State(object):
|
||||
TICK = 400
|
||||
PAIR_TIMEOUT = 60 * 1000 / TICK
|
||||
|
||||
def __init__(self, listener):
|
||||
self.listener = listener
|
||||
self.reset()
|
||||
|
||||
def device(self, number):
|
||||
return self.listener.devices.get(number)
|
||||
|
||||
def reset(self):
|
||||
self.success = None
|
||||
self.detected_device = None
|
||||
self._countdown = self.PAIR_TIMEOUT
|
||||
|
||||
def countdown(self, assistant):
|
||||
if self._countdown < 0 or not self.listener:
|
||||
return False
|
||||
|
||||
if self._countdown == self.PAIR_TIMEOUT:
|
||||
self.start_scan()
|
||||
self._countdown -= 1
|
||||
return True
|
||||
|
||||
self._countdown -= 1
|
||||
if self._countdown > 0 and self.success is None:
|
||||
return True
|
||||
|
||||
self.stop_scan()
|
||||
assistant.scan_complete(assistant, self.detected_device)
|
||||
return False
|
||||
|
||||
def start_scan(self):
|
||||
self.reset()
|
||||
self.listener.events_filter = self.filter_events
|
||||
reply = _base.request(self.listener.handle, 0xFF, b'\x80\xB2', b'\x01')
|
||||
_l.debug("start scan reply %s", repr(reply))
|
||||
|
||||
def stop_scan(self):
|
||||
if self._countdown >= 0:
|
||||
self._countdown = -1
|
||||
reply = _base.request(self.listener.handle, 0xFF, b'\x80\xB2', b'\x02')
|
||||
_l.debug("stop scan reply %s", repr(reply))
|
||||
self.listener.events_filter = None
|
||||
|
||||
def filter_events(self, event):
|
||||
if event.devnumber == 0xFF:
|
||||
if event.code == 0x10:
|
||||
if event.data == b'\x4A\x01\x00\x00\x00':
|
||||
_l.debug("receiver listening for device wakeup")
|
||||
return True
|
||||
if event.data == b'\x4A\x00\x01\x00\x00':
|
||||
_l.debug("receiver gave up")
|
||||
self.success = False
|
||||
return True
|
||||
return False
|
||||
|
||||
if event.devnumber in self.listener.receiver.devices:
|
||||
return False
|
||||
|
||||
_l.debug("event for new device? %s", event)
|
||||
if event.code == 0x10 and event.data[0:2] == b'\x41\x04':
|
||||
self.detected_device = self.listener.make_device(event)
|
||||
return True
|
||||
|
||||
return True
|
||||
|
||||
def unpair(self, device):
|
||||
_l.debug("unpair %s", device)
|
||||
self.listener.unpair_device(device)
|
||||
340
app/receiver.py
@@ -1,340 +0,0 @@
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
from logging import getLogger as _Logger
|
||||
from struct import pack as _pack
|
||||
from time import sleep as _sleep
|
||||
|
||||
from logitech.unifying_receiver import base as _base
|
||||
from logitech.unifying_receiver import api as _api
|
||||
from logitech.unifying_receiver.listener import EventsListener as _EventsListener
|
||||
from logitech.unifying_receiver.common import FallbackDict as _FallbackDict
|
||||
from logitech import devices as _devices
|
||||
from logitech.devices.constants import (STATUS, STATUS_NAME, PROPS)
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
class _FeaturesArray(object):
|
||||
__slots__ = ('device', 'features', 'supported')
|
||||
|
||||
def __init__(self, device):
|
||||
self.device = device
|
||||
self.features = None
|
||||
self.supported = True
|
||||
|
||||
def _check(self):
|
||||
if self.supported:
|
||||
if self.features is not None:
|
||||
return True
|
||||
|
||||
if self.device.status >= STATUS.CONNECTED:
|
||||
handle = self.device.handle
|
||||
try:
|
||||
index = _api.get_feature_index(handle, self.device.number, _api.FEATURE.FEATURE_SET)
|
||||
except _api._FeatureNotSupported:
|
||||
self.supported = False
|
||||
else:
|
||||
count = None if index is None else _base.request(handle, self.device.number, _pack('!BB', index, 0x00))
|
||||
if count is None:
|
||||
self.supported = False
|
||||
else:
|
||||
count = ord(count[:1])
|
||||
self.features = [None] * (1 + count)
|
||||
self.features[0] = _api.FEATURE.ROOT
|
||||
self.features[index] = _api.FEATURE.FEATURE_SET
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
__bool__ = __nonzero__ = _check
|
||||
|
||||
def __getitem__(self, index):
|
||||
if not self._check():
|
||||
return None
|
||||
|
||||
if index < 0 or index >= len(self.features):
|
||||
raise IndexError
|
||||
if self.features[index] is None:
|
||||
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))
|
||||
if feature is not None:
|
||||
self.features[index] = feature[:2]
|
||||
|
||||
return self.features[index]
|
||||
|
||||
def __contains__(self, value):
|
||||
if self._check():
|
||||
if value in self.features:
|
||||
return True
|
||||
|
||||
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
|
||||
if f > value:
|
||||
break
|
||||
|
||||
return False
|
||||
|
||||
def index(self, value):
|
||||
if self._check():
|
||||
if self.features is not None and value in self.features:
|
||||
return self.features.index(value)
|
||||
raise ValueError("%s not in list" % repr(value))
|
||||
|
||||
def __iter__(self):
|
||||
if self._check():
|
||||
yield _api.FEATURE.ROOT
|
||||
index = 1
|
||||
last_index = len(self.features)
|
||||
while index < last_index:
|
||||
yield self.__getitem__(index)
|
||||
index += 1
|
||||
|
||||
def __len__(self):
|
||||
return len(self.features) if self._check() else 0
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
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
|
||||
|
||||
self._status = status
|
||||
self.props = {}
|
||||
|
||||
# read them now, otherwise it it temporarily hang the UI
|
||||
# if status >= STATUS.CONNECTED:
|
||||
# n, k, s, f = self.name, self.kind, self.serial, self.firmware
|
||||
|
||||
@property
|
||||
def receiver(self):
|
||||
return self._listener.receiver
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
return self._status
|
||||
|
||||
@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()
|
||||
|
||||
@property
|
||||
def status_text(self):
|
||||
if self._status < STATUS.CONNECTED:
|
||||
return STATUS_NAME[self._status]
|
||||
|
||||
t = []
|
||||
if self.props.get(PROPS.BATTERY_LEVEL):
|
||||
t.append('Battery: %d%%' % self.props[PROPS.BATTERY_LEVEL])
|
||||
if self.props.get(PROPS.BATTERY_STATUS):
|
||||
t.append(self.props[PROPS.BATTERY_STATUS])
|
||||
if self.props.get(PROPS.LIGHT_LEVEL):
|
||||
t.append('Light: %d lux' % self.props[PROPS.LIGHT_LEVEL])
|
||||
return ', '.join(t) if t else STATUS_NAME[STATUS.CONNECTED]
|
||||
|
||||
def process_event(self, code, data):
|
||||
if code == 0x10 and data[:1] == b'\x8F':
|
||||
self.status = STATUS.UNAVAILABLE
|
||||
return True
|
||||
|
||||
if code == 0x11:
|
||||
status = _devices.process_event(self, data)
|
||||
if status:
|
||||
if type(status) == int:
|
||||
self.status = status
|
||||
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]
|
||||
return True
|
||||
|
||||
self.LOG.warn("don't know how to handle processed event status %s", status)
|
||||
|
||||
return False
|
||||
|
||||
def __str__(self):
|
||||
return '<DeviceInfo(%d,%s,%d)>' % (self.number, self._name or '?', self._status)
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
_RECEIVER_STATUS_NAME = _FallbackDict(
|
||||
lambda x:
|
||||
'1 device found' if x == STATUS.CONNECTED + 1 else
|
||||
'%d devices found' if x > STATUS.CONNECTED else
|
||||
'?',
|
||||
{
|
||||
STATUS.UNKNOWN: 'Initializing...',
|
||||
STATUS.UNAVAILABLE: 'Receiver not found.',
|
||||
STATUS.BOOTING: 'Scanning...',
|
||||
STATUS.CONNECTED: 'No devices found.',
|
||||
}
|
||||
)
|
||||
|
||||
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.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
|
||||
receiver.devices = {}
|
||||
receiver.status = STATUS.BOOTING
|
||||
receiver.status_text = _RECEIVER_STATUS_NAME[STATUS.BOOTING]
|
||||
|
||||
if _base.request(receiver.handle, 0xFF, b'\x80\x00', b'\x00\x01'):
|
||||
self.LOG.info("initialized")
|
||||
else:
|
||||
self.LOG.warn("initialization failed")
|
||||
|
||||
if _base.request(receiver.handle, 0xFF, b'\x80\x02', b'\x02'):
|
||||
self.LOG.info("triggered device events")
|
||||
else:
|
||||
self.LOG.warn("failed to trigger device events")
|
||||
|
||||
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)
|
||||
|
||||
def status_changed(self, device=None, urgent=False):
|
||||
if self.status_changed_callback:
|
||||
self.status_changed_callback(self.receiver, device, urgent)
|
||||
|
||||
def _device_status_from(self, event):
|
||||
state_code = ord(event.data[2:3]) & 0xC0
|
||||
state = STATUS.UNAVAILABLE if state_code == 0x40 else \
|
||||
STATUS.CONNECTED if state_code == 0x80 else \
|
||||
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)
|
||||
return state
|
||||
|
||||
def _events_handler(self, event):
|
||||
if self.events_filter and self.events_filter(event):
|
||||
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))
|
||||
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)
|
||||
return
|
||||
elif event.devnumber in self.receiver.devices:
|
||||
dev = self.receiver.devices[event.devnumber]
|
||||
if dev.process_event(event.code, event.data):
|
||||
return
|
||||
|
||||
if self.events_handler and self.events_handler(event):
|
||||
return
|
||||
|
||||
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:
|
||||
self.LOG.warn("got event for invalid device number %d: %s", event.devnumber, event)
|
||||
return None
|
||||
|
||||
status = self._device_status_from(event)
|
||||
if status is not None:
|
||||
dev = DeviceInfo(self, event.devnumber, status)
|
||||
self.LOG.info("new device %s", dev)
|
||||
self.status_changed(dev, True)
|
||||
return dev
|
||||
|
||||
self.LOG.error("failed to identify status of device %d from %s", event.devnumber, event)
|
||||
|
||||
def unpair_device(self, device):
|
||||
try:
|
||||
del self.receiver[device.number]
|
||||
except IndexError:
|
||||
self.LOG.error("failed to unpair device %s", device)
|
||||
return False
|
||||
|
||||
del self.receiver.devices[device.number]
|
||||
self.LOG.info("unpaired device %s", device)
|
||||
self.change_status(STATUS.CONNECTED + len(self.receiver.devices))
|
||||
device.status = STATUS.UNPAIRED
|
||||
return True
|
||||
|
||||
def __str__(self):
|
||||
return '<ReceiverListener(%s,%d)>' % (self.receiver.path, self.receiver.status)
|
||||
|
||||
@classmethod
|
||||
def open(self, status_changed_callback=None):
|
||||
receiver = _api.Receiver.open()
|
||||
if receiver:
|
||||
rl = ReceiverListener(receiver, status_changed_callback)
|
||||
rl.start()
|
||||
while not rl._active:
|
||||
_sleep(0.1)
|
||||
return rl
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
class _DUMMY_RECEIVER(object):
|
||||
__slots__ = ['name', 'max_devices', 'status', 'status_text', 'devices']
|
||||
name = kind = _api.Receiver.name
|
||||
max_devices = _api.Receiver.max_devices
|
||||
status = STATUS.UNAVAILABLE
|
||||
status_text = _RECEIVER_STATUS_NAME[STATUS.UNAVAILABLE]
|
||||
devices = {}
|
||||
__bool__ = __nonzero__ = lambda self: False
|
||||
DUMMY = _DUMMY_RECEIVER()
|
||||
155
app/solaar.py
@@ -1,7 +1,9 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/env python -u
|
||||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
NAME = 'Solaar'
|
||||
VERSION = '0.7.2'
|
||||
VERSION = '0.8.3'
|
||||
__author__ = "Daniel Pavel <daniel.pavel@gmail.com>"
|
||||
__version__ = VERSION
|
||||
__license__ = "GPL"
|
||||
@@ -10,67 +12,53 @@ __license__ = "GPL"
|
||||
#
|
||||
#
|
||||
|
||||
def _require(module, os_package):
|
||||
try:
|
||||
__import__(module)
|
||||
except ImportError:
|
||||
import sys
|
||||
sys.exit("%s: missing required package '%s'" % (NAME, os_package))
|
||||
|
||||
|
||||
def _parse_arguments():
|
||||
import argparse
|
||||
arg_parser = argparse.ArgumentParser(prog=NAME.lower())
|
||||
arg_parser.add_argument('-v', '--verbose',
|
||||
action='count', default=0,
|
||||
help='increase the logger verbosity (may be repeated)')
|
||||
arg_parser.add_argument('-S', '--no-systray',
|
||||
action='store_false',
|
||||
dest='systray',
|
||||
help='don\'t embed the application window into the systray')
|
||||
arg_parser.add_argument('-N', '--no-notifications',
|
||||
action='store_false',
|
||||
dest='notifications',
|
||||
arg_parser.add_argument('-S', '--no-systray', action='store_false', dest='systray',
|
||||
help='do not place an icon in the desktop\'s systray')
|
||||
arg_parser.add_argument('-N', '--no-notifications', action='store_false', dest='notifications',
|
||||
help='disable desktop notifications (shown only when in systray)')
|
||||
arg_parser.add_argument('-V', '--version',
|
||||
action='version',
|
||||
version='%(prog)s ' + __version__)
|
||||
arg_parser.add_argument('-d', '--debug', action='count', default=0,
|
||||
help='print logging messages, for debugging purposes (may be repeated for extra verbosity)')
|
||||
arg_parser.add_argument('-V', '--version', action='version', version='%(prog)s ' + __version__)
|
||||
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.debug > 0:
|
||||
log_level = logging.WARNING - 10 * args.debug
|
||||
log_format='%(asctime)s %(levelname)8s [%(threadName)s] %(name)s: %(message)s'
|
||||
logging.basicConfig(level=max(log_level, logging.DEBUG), format=log_format)
|
||||
else:
|
||||
logging.root.addHandler(logging.NullHandler())
|
||||
logging.root.setLevel(logging.CRITICAL)
|
||||
|
||||
return args
|
||||
|
||||
|
||||
def _check_requirements():
|
||||
try:
|
||||
import pyudev
|
||||
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'
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
args = _parse_arguments()
|
||||
|
||||
req_fail = _check_requirements()
|
||||
if req_fail:
|
||||
raise ImportError('missing required package: %s' % req_fail)
|
||||
|
||||
def _run(args):
|
||||
import ui
|
||||
|
||||
# check if the notifications are available and enabled
|
||||
# even if --no-notifications is given on the command line, still have to
|
||||
# check they are available, and decide whether to put the option in the
|
||||
# systray icon
|
||||
args.notifications &= args.systray
|
||||
if ui.notify.available and ui.notify.init(NAME):
|
||||
if args.systray and ui.notify.init(NAME):
|
||||
ui.action.toggle_notifications.set_active(args.notifications)
|
||||
if not args.notifications:
|
||||
ui.notify.uninit()
|
||||
else:
|
||||
ui.action.toggle_notifications = None
|
||||
|
||||
from receiver import DUMMY
|
||||
from listener import DUMMY
|
||||
window = ui.main_window.create(NAME, DUMMY.name, DUMMY.max_devices, args.systray)
|
||||
if args.systray:
|
||||
menu_actions = (ui.action.toggle_notifications,
|
||||
@@ -78,50 +66,63 @@ if __name__ == '__main__':
|
||||
icon = ui.status_icon.create(window, menu_actions)
|
||||
else:
|
||||
icon = None
|
||||
window.present()
|
||||
|
||||
import pairing
|
||||
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:
|
||||
GObject.idle_add(ui.notify.show, device or receiver)
|
||||
|
||||
# initializes the receiver listener
|
||||
def check_for_listener(notify=False):
|
||||
# print ("check_for_listener %s" % notify)
|
||||
global listener
|
||||
if not listener:
|
||||
GObject.timeout_add(5000, check_for_listener)
|
||||
listener = None
|
||||
listener = None
|
||||
|
||||
from receiver import ReceiverListener
|
||||
def check_for_listener(retry=True):
|
||||
global listener, notify_missing
|
||||
from listener import ReceiverListener
|
||||
try:
|
||||
listener = ReceiverListener.open(status_changed)
|
||||
except OSError:
|
||||
ui.error(window, 'Permissions error',
|
||||
'Found a possible Unifying Receiver device,\n'
|
||||
'but did not have permission to open it.')
|
||||
|
||||
if listener is None:
|
||||
try:
|
||||
listener = ReceiverListener.open(status_changed)
|
||||
except OSError:
|
||||
ui.show_permissions_warning(window)
|
||||
if notify:
|
||||
status_changed(DUMMY)
|
||||
else:
|
||||
return True
|
||||
|
||||
if listener is None:
|
||||
pairing.state = None
|
||||
if notify_missing:
|
||||
status_changed(DUMMY, None, True)
|
||||
notify_missing = False
|
||||
return retry
|
||||
from logitech.unifying_receiver import status
|
||||
|
||||
# print ("opened receiver", listener, listener.receiver)
|
||||
notify_missing = True
|
||||
pairing.state = pairing.State(listener)
|
||||
status_changed(listener.receiver, None, True)
|
||||
# callback delivering status events from the receiver/devices to the UI
|
||||
def status_changed(receiver, device=None, alert=status.ALERT.NONE, reason=None):
|
||||
if alert & status.ALERT.MED:
|
||||
GObject.idle_add(window.present)
|
||||
if window:
|
||||
GObject.idle_add(ui.main_window.update, window, receiver, device)
|
||||
if icon:
|
||||
GObject.idle_add(ui.status_icon.update, icon, receiver, device)
|
||||
|
||||
GObject.timeout_add(100, check_for_listener, False)
|
||||
if ui.notify.available:
|
||||
# always notify on receiver updates
|
||||
if device is None or alert & status.ALERT.LOW:
|
||||
GObject.idle_add(ui.notify.show, device or receiver, reason)
|
||||
|
||||
if receiver is DUMMY:
|
||||
GObject.timeout_add(3000, check_for_listener)
|
||||
|
||||
GObject.timeout_add(10, check_for_listener, True)
|
||||
Gtk.main()
|
||||
|
||||
if listener is not None:
|
||||
if listener:
|
||||
listener.stop()
|
||||
listener.join()
|
||||
|
||||
ui.notify.uninit()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
_require('pyudev', 'python-pyudev')
|
||||
_require('gi.repository', 'python-gi')
|
||||
_require('gi.repository.Gtk', 'gir1.2-gtk-3.0')
|
||||
|
||||
args = _parse_arguments()
|
||||
listener = None
|
||||
_run(args)
|
||||
|
||||
388
app/solaar_cli.py
Normal file
@@ -0,0 +1,388 @@
|
||||
#!/usr/bin/env python -u
|
||||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
import sys
|
||||
|
||||
import solaar
|
||||
NAME = 'solaar-cli'
|
||||
__author__ = solaar.__author__
|
||||
__version__ = solaar.__version__
|
||||
__license__ = solaar.__license__
|
||||
del solaar
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
def _fail(text):
|
||||
sys.exit("%s: error: %s" % (NAME, text))
|
||||
|
||||
|
||||
def _require(module, os_package):
|
||||
try:
|
||||
__import__(module)
|
||||
except ImportError:
|
||||
_fail("missing required package '%s'" % os_package)
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
def _receiver():
|
||||
from logitech.unifying_receiver import Receiver
|
||||
try:
|
||||
r = Receiver.open()
|
||||
except Exception as e:
|
||||
_fail(str(e))
|
||||
if r is None:
|
||||
_fail("Logitech Unifying Receiver not found")
|
||||
return r
|
||||
|
||||
|
||||
def _find_device(receiver, name):
|
||||
if len(name) == 1:
|
||||
try:
|
||||
number = int(name)
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
if number in range(1, 1 + receiver.max_devices):
|
||||
dev = receiver[number]
|
||||
if dev is None:
|
||||
_fail("no paired device with number %d" % number)
|
||||
return dev
|
||||
|
||||
if len(name) < 3:
|
||||
_fail("need at least 3 characters to match a device")
|
||||
|
||||
name = name.lower()
|
||||
if 'receiver'.startswith(name) or name.upper() == receiver.serial:
|
||||
return receiver
|
||||
|
||||
dev = None
|
||||
for d in receiver:
|
||||
if name.upper() == d.serial or name in d.name.lower() or name in d.codename.lower():
|
||||
if dev is None:
|
||||
dev = d
|
||||
else:
|
||||
_fail("'%s' matches multiple devices" % name)
|
||||
|
||||
if dev is None:
|
||||
_fail("no device found matching '%s'" % name)
|
||||
return dev
|
||||
|
||||
|
||||
def _print_receiver(receiver, verbose=False):
|
||||
paired_count = receiver.count()
|
||||
if not verbose:
|
||||
print ("-: Unifying Receiver [%s:%s] with %d devices" % (receiver.path, receiver.serial, paired_count))
|
||||
return
|
||||
|
||||
print ("-: Unifying Receiver")
|
||||
print (" Device path : %s" % receiver.path)
|
||||
print (" Serial : %s" % receiver.serial)
|
||||
for f in receiver.firmware:
|
||||
print (" %-11s: %s" % (f.kind, f.version))
|
||||
|
||||
print (" Has %d paired device(s)." % paired_count)
|
||||
|
||||
notifications = receiver.request(0x8100)
|
||||
if notifications:
|
||||
notifications = ord(notifications[0:1]) << 16 | ord(notifications[1:2]) << 8
|
||||
if notifications:
|
||||
from logitech.unifying_receiver import hidpp10
|
||||
print (" Enabled notifications: %06X = %s." % (notifications, ', '.join(hidpp10.NOTIFICATION_FLAG.flag_names(notifications))))
|
||||
else:
|
||||
print (" All notifications disabled.")
|
||||
|
||||
if paired_count > 0:
|
||||
activity = receiver.request(0x83B3)
|
||||
if activity:
|
||||
activity = [(d, ord(activity[d - 1:d])) for d in range(1, receiver.max_devices)]
|
||||
print(" Device activity counters: %s" % ', '.join(('%d=%d' % (d, a)) for d, a in activity if a > 0))
|
||||
|
||||
|
||||
def _print_device(dev, verbose=False):
|
||||
p = dev.protocol
|
||||
state = '' if p > 0 else ' inactive'
|
||||
|
||||
if not verbose:
|
||||
print ("%d: %s [%s:%s]%s" % (dev.number, dev.name, dev.codename, dev.serial, state))
|
||||
return
|
||||
|
||||
print ("%d: %s" % (dev.number, dev.name))
|
||||
print (" Codename : %s" % dev.codename)
|
||||
print (" Kind : %s" % dev.kind)
|
||||
if p == 0:
|
||||
print (" Protocol : unknown (device is inactive)")
|
||||
else:
|
||||
print (" Protocol : HID++ %1.1f" % p)
|
||||
print (" Polling rate : %d ms" % dev.polling_rate)
|
||||
print (" Wireless PID : %s" % dev.wpid)
|
||||
print (" Serial number: %s" % dev.serial)
|
||||
for fw in dev.firmware:
|
||||
print (" %-11s: %s" % (fw.kind, (fw.name + ' ' + fw.version).strip()))
|
||||
|
||||
if dev.power_switch_location:
|
||||
print (" The power switch is located on the %s" % dev.power_switch_location)
|
||||
|
||||
from logitech.unifying_receiver import hidpp10, hidpp20
|
||||
if p > 0:
|
||||
if dev.features:
|
||||
print (" Supports %d HID++ 2.0 features:" % len(dev.features))
|
||||
for index, feature in enumerate(dev.features):
|
||||
feature = dev.features[index]
|
||||
flags = dev.request(0x0000, feature.bytes(2))
|
||||
flags = 0 if flags is None else ord(flags[1:2])
|
||||
flags = hidpp20.FEATURE_FLAG.flag_names(flags)
|
||||
print (" %2d: %-20s {%04X} %s" % (index, feature, feature, flags))
|
||||
|
||||
if dev.keys:
|
||||
print (" Has %d reprogrammable keys:" % len(dev.keys))
|
||||
for k in dev.keys:
|
||||
flags = hidpp20.KEY_FLAG.flag_names(k.flags)
|
||||
print (" %2d: %-20s => %-20s %s" % (k.index, k.key, k.task, flags))
|
||||
|
||||
if p > 0:
|
||||
battery = hidpp20.get_battery(dev)
|
||||
if battery is None:
|
||||
battery = hidpp10.get_battery(dev)
|
||||
if battery:
|
||||
charge, status = battery
|
||||
print (" Battery is %d%% charged, %s" % (charge, status))
|
||||
else:
|
||||
print (" Battery status unavailable.")
|
||||
else:
|
||||
print (" Battery status is unknown (device is inactive).")
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
def show_devices(receiver, args):
|
||||
if args.device == 'all':
|
||||
_print_receiver(receiver, args.verbose)
|
||||
for dev in receiver:
|
||||
if args.verbose:
|
||||
print ("")
|
||||
_print_device(dev, args.verbose)
|
||||
else:
|
||||
dev = _find_device(receiver, args.device)
|
||||
if dev is receiver:
|
||||
_print_receiver(receiver, args.verbose)
|
||||
else:
|
||||
_print_device(dev, args.verbose)
|
||||
|
||||
|
||||
def pair_device(receiver, args):
|
||||
# get all current devices
|
||||
known_devices = [dev.number for dev in receiver]
|
||||
|
||||
from logitech.unifying_receiver import status
|
||||
r_status = status.ReceiverStatus(receiver, lambda *args, **kwargs: None)
|
||||
|
||||
done = [False]
|
||||
|
||||
def _events_handler(event):
|
||||
if event.devnumber == 0xFF:
|
||||
r_status.process_event(event)
|
||||
if not r_status.lock_open:
|
||||
done[0] = True
|
||||
elif event.sub_id == 0x41 and event.address == 0x04:
|
||||
if event.devnumber not in known_devices:
|
||||
r_status.new_device = receiver[event.devnumber]
|
||||
|
||||
from logitech.unifying_receiver import base
|
||||
base.events_hook = _events_handler
|
||||
|
||||
# check if it's necessary to set the notification flags
|
||||
notifications = receiver.request(0x8100)
|
||||
if notifications:
|
||||
notifications = ord(notifications[:1]) + ord(notifications[1:2]) + ord(notifications[2:3])
|
||||
if not notifications:
|
||||
receiver.enable_notifications()
|
||||
receiver.set_lock(False, timeout=20)
|
||||
print ("Pairing: turn your new device on (timing out in 20 seconds).")
|
||||
|
||||
while not done[0]:
|
||||
event = base.read(receiver.handle, 2000)
|
||||
if event:
|
||||
event = base.make_event(*event)
|
||||
if event:
|
||||
_events_handler(event)
|
||||
|
||||
if not notifications:
|
||||
receiver.enable_notifications(False)
|
||||
base.events_hook = None
|
||||
|
||||
if r_status.new_device:
|
||||
dev = r_status.new_device
|
||||
print ("Paired device %d: %s [%s:%s]" % (dev.number, dev.name, dev.codename, dev.serial))
|
||||
else:
|
||||
_fail(r_status[status.ERROR])
|
||||
|
||||
|
||||
def unpair_device(receiver, args):
|
||||
dev = _find_device(receiver, args.device)
|
||||
if dev is receiver:
|
||||
_fail("cannot unpair the receiver from itself!")
|
||||
|
||||
# query these now, it's last chance to get them
|
||||
number, name, codename, serial = dev.number, dev.name, dev.codename, dev.serial
|
||||
try:
|
||||
del receiver[number]
|
||||
print ("Unpaired %d: %s [%s:%s]" % (number, name, codename, serial))
|
||||
except Exception as e:
|
||||
_fail("failed to unpair device %s: %s" % (dev.name, e))
|
||||
|
||||
|
||||
def config_device(receiver, args):
|
||||
dev = _find_device(receiver, args.device)
|
||||
if dev is receiver:
|
||||
_fail("no settings for the receiver")
|
||||
|
||||
if not dev.settings:
|
||||
_fail("no settings for %s" % dev.name)
|
||||
|
||||
if not args.setting:
|
||||
print ("[%d:%s:%s]" % (dev.number, dev.name, dev.serial))
|
||||
for s in dev.settings:
|
||||
print ("")
|
||||
print ("# %s" % s.label)
|
||||
if s.choices:
|
||||
print ("# possible values: one of [%s], or higher/lower/highest/max/lowest/min" % ', '.join(str(v) for v in s.choices))
|
||||
else:
|
||||
print ("# possible values: true/t/yes/y/1 or false/f/no/n/0")
|
||||
value = s.read()
|
||||
if value is None:
|
||||
print ("# ! failed to read '%s'" % s.name)
|
||||
else:
|
||||
print ("%s=%s" % (s.name, value))
|
||||
return
|
||||
|
||||
setting = None
|
||||
for s in dev.settings:
|
||||
if args.setting.lower() == s.name.lower():
|
||||
setting = s
|
||||
break
|
||||
if setting is None:
|
||||
_fail("no setting '%s' for %s" % (args.setting, dev.name))
|
||||
|
||||
if args.value is None:
|
||||
result = setting.read()
|
||||
if result is None:
|
||||
_fail("failed to read '%s'" % setting.name)
|
||||
print ("%s = %s" % (setting.name, setting.read()))
|
||||
return
|
||||
|
||||
from logitech.unifying_receiver import settings as _settings
|
||||
|
||||
if setting.kind == _settings.KIND.toggle:
|
||||
value = args.value
|
||||
try:
|
||||
value = bool(int(value))
|
||||
except:
|
||||
if value.lower() in ['1', 'true', 'yes', 't', 'y']:
|
||||
value = True
|
||||
elif value.lower() in ['0', 'false', 'no', 'f', 'n']:
|
||||
value = False
|
||||
else:
|
||||
_fail("don't know how to interpret '%s' as boolean" % value)
|
||||
|
||||
elif setting.choices:
|
||||
value = args.value.lower()
|
||||
|
||||
if value in ['higher', 'lower']:
|
||||
old_value = setting.read()
|
||||
if old_value is None:
|
||||
_fail("could not read current value of '%s'" % setting.name)
|
||||
|
||||
old_index = setting.choices.index(old_value)
|
||||
if value == 'lower':
|
||||
if old_index == 0:
|
||||
sys.stderr.write("'%s' already at the lowest value")
|
||||
return
|
||||
value = setting.choices[old_index - 1:old_index][0]
|
||||
elif value == 'higher':
|
||||
if old_index == len(setting.choices) - 1:
|
||||
sys.stderr.write("'%s' already at the highest value")
|
||||
return
|
||||
value = setting.choices[old_index + 1:old_index + 2][0]
|
||||
elif value in ('highest', 'max'):
|
||||
value = setting.choices[-1:][0]
|
||||
elif value in ('lowest', 'min'):
|
||||
value = setting.choices[:1][0]
|
||||
elif value not in setting.choices:
|
||||
_fail("possible values for '%s' are: [%s]" % (setting.name, ', '.join(str(v) for v in setting.choices)))
|
||||
value = setting.choices[value]
|
||||
|
||||
else:
|
||||
raise NotImplemented
|
||||
|
||||
result = setting.write(value)
|
||||
if result is None:
|
||||
_fail("failed to set '%s' to '%s'" % (setting.name, value))
|
||||
print ("%s = %s" % (setting.name, result))
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
def _parse_arguments():
|
||||
import argparse
|
||||
arg_parser = argparse.ArgumentParser(prog=NAME.lower())
|
||||
arg_parser.add_argument('-d', '--debug', action='count', default=0,
|
||||
help='print logging messages, for debugging purposes (may be repeated for extra verbosity)')
|
||||
arg_parser.add_argument('-V', '--version', action='version', version='%(prog)s ' + __version__)
|
||||
|
||||
subparsers = arg_parser.add_subparsers(title='commands')
|
||||
|
||||
sp = subparsers.add_parser('show', help='show information about paired devices')
|
||||
sp.add_argument('device', nargs='?', default='all',
|
||||
help='device to show information about; may be a device number (1..6), a device serial, '
|
||||
'at least 3 characters of a device\'s name, "receiver", or "all" (the default)')
|
||||
sp.add_argument('-v', '--verbose', action='store_true',
|
||||
help='print all available information about the inspected device(s)')
|
||||
sp.set_defaults(cmd=show_devices)
|
||||
|
||||
sp = subparsers.add_parser('config', help='read/write device-specific settings',
|
||||
epilog='Please note that configuration only works on active devices.')
|
||||
sp.add_argument('device',
|
||||
help='device to configure; may be a device number (1..6), a device serial, '
|
||||
'or at least 3 characters of a device\'s name')
|
||||
sp.add_argument('setting', nargs='?',
|
||||
help='device-specific setting; leave empty to list available settings')
|
||||
sp.add_argument('value', nargs='?',
|
||||
help='new value for the setting')
|
||||
sp.set_defaults(cmd=config_device)
|
||||
|
||||
sp = subparsers.add_parser('pair', help='pair a new device',
|
||||
epilog='The Logitech Unifying Receiver supports up to 6 paired devices at the same time.')
|
||||
sp.set_defaults(cmd=pair_device)
|
||||
|
||||
sp = subparsers.add_parser('unpair', help='unpair a device')
|
||||
sp.add_argument('device',
|
||||
help='device to unpair; may be a device number (1..6), a device serial, '
|
||||
'or at least 3 characters of a device\'s name.')
|
||||
sp.set_defaults(cmd=unpair_device)
|
||||
|
||||
args = arg_parser.parse_args()
|
||||
|
||||
import logging
|
||||
if args.debug > 0:
|
||||
log_level = logging.WARNING - 10 * args.debug
|
||||
log_format='%(asctime)s %(levelname)8s %(name)s: %(message)s'
|
||||
logging.basicConfig(level=max(log_level, logging.DEBUG), format=log_format)
|
||||
else:
|
||||
logging.root.addHandler(logging.NullHandler())
|
||||
logging.root.setLevel(logging.CRITICAL)
|
||||
|
||||
return args
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
_require('pyudev', 'python-pyudev')
|
||||
args = _parse_arguments()
|
||||
receiver = _receiver()
|
||||
args.cmd(receiver, args)
|
||||
@@ -1,44 +1,91 @@
|
||||
# pass
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
from . import (notify, status_icon, main_window, pair_window, action)
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
from gi.repository import (GObject, Gtk)
|
||||
from gi.repository import GObject, Gtk
|
||||
GObject.threads_init()
|
||||
|
||||
_LARGE_SIZE = 64
|
||||
Gtk.IconSize.LARGE = Gtk.icon_size_register('large', _LARGE_SIZE, _LARGE_SIZE)
|
||||
# Gtk.IconSize.XLARGE = Gtk.icon_size_register('x-large', _LARGE_SIZE * 2, _LARGE_SIZE * 2)
|
||||
|
||||
from solaar import NAME as _NAME
|
||||
_APP_ICONS = (_NAME + '-fail', _NAME + '-init', _NAME)
|
||||
from . import notify, status_icon, main_window, pair_window, action
|
||||
|
||||
from solaar import NAME
|
||||
_APP_ICONS = (NAME + '-init', NAME + '-fail', NAME)
|
||||
def appicon(receiver_status):
|
||||
return (_APP_ICONS[0] if receiver_status < 0 else
|
||||
_APP_ICONS[1] if receiver_status < 1 else
|
||||
_APP_ICONS[2])
|
||||
return (_APP_ICONS[1] if isinstance(receiver_status, basestring)
|
||||
else _APP_ICONS[2] if receiver_status
|
||||
else _APP_ICONS[0])
|
||||
|
||||
|
||||
_ICON_THEME = Gtk.IconTheme.get_default()
|
||||
|
||||
def get_icon(name, fallback):
|
||||
return name if name and _ICON_THEME.has_icon(name) else fallback
|
||||
|
||||
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 get_battery_icon(level):
|
||||
if level < 0:
|
||||
return 'battery_unknown'
|
||||
return 'battery_%03d' % (10 * ((level + 5) // 10))
|
||||
|
||||
|
||||
def show_permissions_warning(window):
|
||||
text = ('Found a possible Unifying Receiver device,\n'
|
||||
'but did not have permission to open it.')
|
||||
_ICON_SETS = {}
|
||||
|
||||
def device_icon_set(name, kind=None):
|
||||
icon_set = _ICON_SETS.get(name)
|
||||
if icon_set is None:
|
||||
icon_set = Gtk.IconSet.new()
|
||||
_ICON_SETS[name] = icon_set
|
||||
|
||||
names = ['preferences-desktop-peripherals']
|
||||
if kind:
|
||||
if str(kind) == 'numpad':
|
||||
names += ('input-dialpad',)
|
||||
elif str(kind) == 'touchpad':
|
||||
names += ('input-tablet',)
|
||||
elif str(kind) == 'trackball':
|
||||
names += ('input-mouse',)
|
||||
names += ('input-' + str(kind),)
|
||||
|
||||
theme = Gtk.IconTheme.get_default()
|
||||
if theme.has_icon(name):
|
||||
names += (name,)
|
||||
|
||||
source = Gtk.IconSource.new()
|
||||
for n in names:
|
||||
source.set_icon_name(n)
|
||||
icon_set.add_source(source)
|
||||
icon_set.names = names
|
||||
|
||||
return icon_set
|
||||
|
||||
|
||||
def device_icon_file(name, kind=None):
|
||||
icon_set = device_icon_set(name, kind)
|
||||
assert icon_set
|
||||
theme = Gtk.IconTheme.get_default()
|
||||
for n in reversed(icon_set.names):
|
||||
if theme.has_icon(n):
|
||||
return theme.lookup_icon(n, _LARGE_SIZE, 0).get_filename()
|
||||
|
||||
|
||||
def icon_file(name, size=_LARGE_SIZE):
|
||||
theme = Gtk.IconTheme.get_default()
|
||||
if theme.has_icon(name):
|
||||
return theme.lookup_icon(name, size, 0).get_filename()
|
||||
|
||||
|
||||
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()
|
||||
|
||||
|
||||
def find_children(container, *child_names):
|
||||
assert container is not None
|
||||
assert isinstance(container, Gtk.Container)
|
||||
|
||||
def _iterate_children(widget, names, result, count):
|
||||
assert isinstance(widget, Gtk.Widget)
|
||||
wname = widget.get_name()
|
||||
if wname in names:
|
||||
index = names.index(wname)
|
||||
@@ -48,6 +95,7 @@ def find_children(container, *child_names):
|
||||
|
||||
if count > 0 and isinstance(widget, Gtk.Container):
|
||||
for w in widget:
|
||||
# if isinstance(w, Gtk.Widget):
|
||||
count = _iterate_children(w, names, result, count)
|
||||
if count == 0:
|
||||
break
|
||||
@@ -59,12 +107,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)
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
#
|
||||
#
|
||||
|
||||
# from sys import version as PYTTHON_VERSION
|
||||
from gi.repository import Gtk
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
import ui.notify
|
||||
import ui.pair_window
|
||||
# from sys import version as PYTHON_VERSION
|
||||
from gi.repository import Gtk, Gdk
|
||||
|
||||
import ui
|
||||
from solaar import NAME as _NAME
|
||||
from solaar import VERSION as _VERSION
|
||||
|
||||
@@ -40,15 +41,31 @@ toggle_notifications = _toggle_action('notifications', 'Notifications', _toggle_
|
||||
|
||||
def _show_about_window(action):
|
||||
about = Gtk.AboutDialog()
|
||||
|
||||
about.set_icon_name(_NAME)
|
||||
about.set_program_name(_NAME)
|
||||
about.set_logo_icon_name(_NAME)
|
||||
about.set_version(_VERSION)
|
||||
about.set_comments('Shows status of devices connected\nto a Logitech Unifying Receiver.')
|
||||
|
||||
about.set_copyright(b'\xC2\xA9'.decode('utf-8') + ' 2012 Daniel Pavel')
|
||||
about.set_license_type(Gtk.License.GPL_2_0)
|
||||
about.set_authors(('Daniel Pavel http://github.com/pwr', ))
|
||||
about.set_website('http://github.com/pwr/Solaar/wiki')
|
||||
|
||||
about.set_authors(('Daniel Pavel http://github.com/pwr',))
|
||||
try:
|
||||
about.add_credit_section('Testing', ('Douglas Wagner',))
|
||||
about.add_credit_section('Technical specifications\nprovided by',
|
||||
('Julien Danjou http://julien.danjou.info/blog/2012/logitech-unifying-upower',))
|
||||
except TypeError:
|
||||
# gtk3 < 3.6 has incorrect gi bindings
|
||||
pass
|
||||
except:
|
||||
# is the Gtk3 version too old?
|
||||
pass
|
||||
|
||||
about.set_website('http://pwr.github.com/Solaar/')
|
||||
about.set_website_label('Solaar Wiki')
|
||||
# about.set_comments('Using Python %s\n' % PYTTHON_VERSION.split(' ')[0])
|
||||
|
||||
about.run()
|
||||
about.destroy()
|
||||
about = _action('help-about', 'About ' + _NAME, _show_about_window)
|
||||
@@ -59,16 +76,15 @@ quit = _action('exit', 'Quit', Gtk.main_quit)
|
||||
#
|
||||
#
|
||||
|
||||
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, frame._device)
|
||||
pair_dialog.set_transient_for(window)
|
||||
pair_dialog.set_destroy_with_parent(True)
|
||||
pair_dialog.set_modal(True)
|
||||
|
||||
window.present()
|
||||
pair_dialog.set_type_hint(Gdk.WindowTypeHint.DIALOG)
|
||||
pair_dialog.set_position(Gtk.WindowPosition.CENTER)
|
||||
pair_dialog.present()
|
||||
|
||||
def pair(frame):
|
||||
@@ -77,15 +93,21 @@ 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:
|
||||
try:
|
||||
del device.receiver[device.number]
|
||||
except:
|
||||
ui.error(window, 'Unpairing failed', 'Failed to unpair device\n%s .' % device.name)
|
||||
|
||||
def unpair(frame):
|
||||
return _action('remove', 'Unpair', _unpair_device, frame)
|
||||
return _action('edit-delete', 'Unpair', _unpair_device, frame)
|
||||
|
||||
183
app/ui/config_panel.py
Normal file
@@ -0,0 +1,183 @@
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
from gi.repository import Gtk, GObject
|
||||
|
||||
import ui
|
||||
from logitech.unifying_receiver import settings as _settings
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
try:
|
||||
from Queue import Queue as _Queue
|
||||
except ImportError:
|
||||
from queue import Queue as _Queue
|
||||
_apply_queue = _Queue(8)
|
||||
|
||||
def _process_apply_queue():
|
||||
def _write_start(sbox):
|
||||
_, failed, spinner, control = sbox.get_children()
|
||||
control.set_sensitive(False)
|
||||
failed.set_visible(False)
|
||||
spinner.set_visible(True)
|
||||
spinner.start()
|
||||
|
||||
while True:
|
||||
task = _apply_queue.get()
|
||||
assert isinstance(task, tuple)
|
||||
if task[0] == 'write':
|
||||
_, setting, value, sbox = task
|
||||
GObject.idle_add(_write_start, sbox)
|
||||
value = setting.write(value)
|
||||
elif task[0] == 'read':
|
||||
_, setting, cached, sbox = task
|
||||
value = setting.read(cached)
|
||||
GObject.idle_add(_update_setting_item, sbox, value)
|
||||
|
||||
from threading import Thread as _Thread
|
||||
_queue_processor = _Thread(name='SettingsProcessor', target=_process_apply_queue)
|
||||
_queue_processor.daemon = True
|
||||
_queue_processor.start()
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
def _switch_notify(switch, _, setting, spinner):
|
||||
_apply_queue.put(('write', setting, switch.get_active() == True, switch.get_parent()))
|
||||
|
||||
|
||||
def _combo_notify(cbbox, setting, spinner):
|
||||
_apply_queue.put(('write', setting, cbbox.get_active_id(), cbbox.get_parent()))
|
||||
|
||||
|
||||
# def _scale_notify(scale, setting, spinner):
|
||||
# _apply_queue.put(('write', setting, scale.get_value(), scale.get_parent()))
|
||||
|
||||
|
||||
# def _snap_to_markers(scale, scroll, value, setting):
|
||||
# value = int(value)
|
||||
# candidate = None
|
||||
# delta = 0xFFFFFFFF
|
||||
# for c in setting.choices:
|
||||
# d = abs(value - int(c))
|
||||
# if d < delta:
|
||||
# candidate = c
|
||||
# delta = d
|
||||
|
||||
# assert candidate is not None
|
||||
# scale.set_value(int(candidate))
|
||||
# return True
|
||||
|
||||
|
||||
def _add_settings(box, device):
|
||||
for s in device.settings:
|
||||
sbox = Gtk.HBox(homogeneous=False, spacing=8)
|
||||
sbox.pack_start(Gtk.Label(s.label), False, False, 0)
|
||||
|
||||
spinner = Gtk.Spinner()
|
||||
spinner.set_tooltip_text('Working...')
|
||||
|
||||
failed = Gtk.Image.new_from_icon_name('dialog-warning', Gtk.IconSize.SMALL_TOOLBAR)
|
||||
failed.set_tooltip_text('Failed to read value from the device.')
|
||||
|
||||
if s.kind == _settings.KIND.toggle:
|
||||
control = Gtk.Switch()
|
||||
control.connect('notify::active', _switch_notify, s, spinner)
|
||||
elif s.kind == _settings.KIND.choice:
|
||||
control = Gtk.ComboBoxText()
|
||||
for entry in s.choices:
|
||||
control.append(str(entry), str(entry))
|
||||
control.connect('changed', _combo_notify, s, spinner)
|
||||
# elif s.kind == _settings.KIND.range:
|
||||
# first, second = s.choices[:2]
|
||||
# last = s.choices[-1:][0]
|
||||
# control = Gtk.HScale.new_with_range(first, last, second - first)
|
||||
# control.set_draw_value(False)
|
||||
# control.set_has_origin(False)
|
||||
# for entry in s.choices:
|
||||
# control.add_mark(int(entry), Gtk.PositionType.TOP, str(entry))
|
||||
# control.connect('change-value', _snap_to_markers, s)
|
||||
# control.connect('value-changed', _scale_notify, s, spinner)
|
||||
else:
|
||||
raise NotImplemented
|
||||
|
||||
control.set_sensitive(False) # the first read will enable it
|
||||
sbox.pack_end(control, False, False, 0)
|
||||
sbox.pack_end(spinner, False, False, 0)
|
||||
sbox.pack_end(failed, False, False, 0)
|
||||
|
||||
if s.description:
|
||||
sbox.set_tooltip_text(s.description)
|
||||
|
||||
sbox.show_all()
|
||||
spinner.start() # the first read will stop it
|
||||
failed.set_visible(False)
|
||||
box.pack_start(sbox, False, False, 0)
|
||||
yield sbox
|
||||
|
||||
|
||||
def _update_setting_item(sbox, value):
|
||||
_, failed, spinner, control = sbox.get_children()
|
||||
spinner.set_visible(False)
|
||||
spinner.stop()
|
||||
|
||||
if value is None:
|
||||
control.set_sensitive(False)
|
||||
failed.set_visible(True)
|
||||
return
|
||||
|
||||
failed.set_visible(False)
|
||||
control.set_sensitive(True)
|
||||
if isinstance(control, Gtk.Switch):
|
||||
control.set_active(value)
|
||||
elif isinstance(control, Gtk.ComboBoxText):
|
||||
control.set_active_id(str(value))
|
||||
# elif isinstance(control, Gtk.Scale):
|
||||
# control.set_value(int(value))
|
||||
else:
|
||||
raise NotImplemented
|
||||
|
||||
|
||||
def update(frame):
|
||||
box = ui.find_children(frame, 'config-box')
|
||||
assert box
|
||||
device = frame._device
|
||||
|
||||
if device is None:
|
||||
# remove all settings widgets
|
||||
# if another device gets paired here, it will add its own widgets
|
||||
box.foreach(lambda x, _: box.remove(x), None)
|
||||
return
|
||||
|
||||
if not device.settings:
|
||||
# nothing to do here
|
||||
return
|
||||
|
||||
if not box.get_visible():
|
||||
# no point in doing this, is there?
|
||||
return
|
||||
|
||||
force_read = False
|
||||
items = box.get_children()
|
||||
if not items:
|
||||
if device.status:
|
||||
items = list(_add_settings(box, device))
|
||||
assert len(device.settings) == len(items)
|
||||
force_read = True
|
||||
else:
|
||||
# don't bother adding settings for offline devices,
|
||||
# they're useless and might not guess all of them anyway
|
||||
return
|
||||
|
||||
device_active = bool(device.status)
|
||||
force_read |= device_active and not box.get_sensitive()
|
||||
box.set_sensitive(device_active)
|
||||
if device_active:
|
||||
for sbox, s in zip(items, device.settings):
|
||||
_apply_queue.put(('read', s, force_read, sbox))
|
||||
@@ -2,84 +2,94 @@
|
||||
#
|
||||
#
|
||||
|
||||
from gi.repository import (Gtk, Gdk)
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
from gi.repository import Gtk, Gdk, GObject
|
||||
|
||||
import ui
|
||||
from logitech.devices.constants import (STATUS, PROPS)
|
||||
from logitech.unifying_receiver import status as _status
|
||||
from . import config_panel as _config_panel
|
||||
|
||||
|
||||
_SMALL_DEVICE_ICON_SIZE = Gtk.IconSize.BUTTON
|
||||
_RECEIVER_ICON_SIZE = Gtk.IconSize.LARGE_TOOLBAR
|
||||
_DEVICE_ICON_SIZE = Gtk.IconSize.DIALOG
|
||||
_STATUS_ICON_SIZE = Gtk.IconSize.LARGE_TOOLBAR
|
||||
_PLACEHOLDER = '~'
|
||||
_FALLBACK_ICON = 'preferences-desktop-peripherals'
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
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
|
||||
frame.set_name(name)
|
||||
|
||||
icon_name = ui.get_icon(name, 'preferences-desktop-peripherals')
|
||||
icon = Gtk.Image.new_from_icon_name(icon_name, _SMALL_DEVICE_ICON_SIZE)
|
||||
icon_set = ui.device_icon_set(name)
|
||||
icon = Gtk.Image.new_from_icon_set(icon_set, _RECEIVER_ICON_SIZE)
|
||||
icon.set_name('icon')
|
||||
icon.set_padding(2, 2)
|
||||
|
||||
label = Gtk.Label('Scanning...')
|
||||
label.set_name('label')
|
||||
label.set_alignment(0, 0.5)
|
||||
|
||||
pairing_icon = Gtk.Image.new_from_icon_name('network-wireless', _RECEIVER_ICON_SIZE)
|
||||
pairing_icon.set_name('pairing-icon')
|
||||
pairing_icon.set_tooltip_text('The pairing lock is open.')
|
||||
pairing_icon._tick = 0
|
||||
|
||||
toolbar = Gtk.Toolbar()
|
||||
toolbar.set_name('toolbar')
|
||||
toolbar.set_style(Gtk.ToolbarStyle.ICONS)
|
||||
toolbar.set_icon_size(Gtk.IconSize.MENU)
|
||||
toolbar.set_icon_size(Gtk.IconSize.SMALL_TOOLBAR)
|
||||
toolbar.set_show_arrow(False)
|
||||
|
||||
hbox = Gtk.HBox(homogeneous=False, spacing=8)
|
||||
hbox.pack_start(icon, False, False, 0)
|
||||
hbox.pack_start(label, True, True, 0)
|
||||
hbox.pack_end(toolbar, False, False, 0)
|
||||
hbox.pack_start(pairing_icon, False, False, 0)
|
||||
hbox.pack_start(toolbar, False, False, 0)
|
||||
|
||||
info_label = Gtk.Label()
|
||||
info_label.set_markup('<small>reading ...</small>')
|
||||
info_label.set_name('info-label')
|
||||
info_label.set_alignment(0, 0.5)
|
||||
info_label.set_padding(8, 2)
|
||||
info_label.set_property('margin-left', 36)
|
||||
info_label.set_alignment(0, 0)
|
||||
info_label.set_selectable(True)
|
||||
|
||||
info_box = Gtk.Frame()
|
||||
info_box.add(info_label)
|
||||
info_box.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
|
||||
def _update_info_label():
|
||||
device = frame._device
|
||||
if info_label.get_visible() and '\n' not in info_label.get_text():
|
||||
items = [('Path', device.path), ('Serial', device.serial)] + \
|
||||
[(f.kind, f.version) for f in device.firmware]
|
||||
info_label.set_markup('<small><tt>' + '\n'.join('%-13s: %s' % item for item in items) + '</tt></small>')
|
||||
|
||||
toggle_info_action = ui.action._toggle_action('info', 'Receiver info', _toggle_info, info_label, info_box, frame)
|
||||
def _toggle_info_label(action):
|
||||
active = action.get_active()
|
||||
for c in vbox.get_children()[1:]:
|
||||
c.set_visible(active)
|
||||
|
||||
if active:
|
||||
GObject.timeout_add(50, _update_info_label)
|
||||
|
||||
toggle_info_action = ui.action._toggle_action('info', 'Details', _toggle_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.set_border_width(4)
|
||||
vbox.set_border_width(2)
|
||||
vbox.pack_start(hbox, True, True, 0)
|
||||
vbox.pack_start(info_box, True, True, 0)
|
||||
vbox.pack_start(Gtk.HSeparator(), False, False, 0)
|
||||
vbox.pack_start(info_label, True, True, 0)
|
||||
|
||||
frame.add(vbox)
|
||||
frame.show_all()
|
||||
info_box.set_visible(False)
|
||||
|
||||
pairing_icon.set_visible(False)
|
||||
_toggle_info_label(toggle_info_action)
|
||||
return frame
|
||||
|
||||
|
||||
@@ -88,17 +98,16 @@ def _make_device_box(index):
|
||||
frame._device = None
|
||||
frame.set_name(_PLACEHOLDER)
|
||||
|
||||
icon_name = 'preferences-desktop-peripherals'
|
||||
icon = Gtk.Image.new_from_icon_name(icon_name, _DEVICE_ICON_SIZE)
|
||||
icon = Gtk.Image.new_from_icon_name(_FALLBACK_ICON, _DEVICE_ICON_SIZE)
|
||||
icon.set_name('icon')
|
||||
icon.set_alignment(0.5, 0)
|
||||
|
||||
label = Gtk.Label('Initializing...')
|
||||
label.set_name('label')
|
||||
label.set_alignment(0, 0.5)
|
||||
label.set_padding(4, 4)
|
||||
label.set_padding(4, 0)
|
||||
|
||||
battery_icon = Gtk.Image.new_from_icon_name('battery_unknown', _STATUS_ICON_SIZE)
|
||||
battery_icon = Gtk.Image.new_from_icon_name(ui.get_battery_icon(-1), _STATUS_ICON_SIZE)
|
||||
|
||||
battery_label = Gtk.Label()
|
||||
battery_label.set_width_chars(6)
|
||||
@@ -110,71 +119,134 @@ def _make_device_box(index):
|
||||
light_label.set_alignment(0, 0.5)
|
||||
light_label.set_width_chars(8)
|
||||
|
||||
not_encrypted_icon = Gtk.Image.new_from_icon_name('security-low', _STATUS_ICON_SIZE - 1)
|
||||
not_encrypted_icon.set_name('not-encrypted')
|
||||
not_encrypted_icon.set_tooltip_text('The wireless link between this device and the Unifying Receiver is not encrypted.\n'
|
||||
'\n'
|
||||
'For pointing devices (mice, trackballs, trackpads), this is a minor security issue.\n'
|
||||
'\n'
|
||||
'It is, however, a major security issue for text-input devices (keyboards, numpads),\n'
|
||||
'because typed text can be sniffed inconspicuously by 3rd parties within range.')
|
||||
|
||||
toolbar = Gtk.Toolbar()
|
||||
toolbar.set_name('toolbar')
|
||||
toolbar.set_style(Gtk.ToolbarStyle.ICONS)
|
||||
toolbar.set_icon_size(Gtk.IconSize.MENU)
|
||||
toolbar.set_icon_size(_STATUS_ICON_SIZE - 1)
|
||||
toolbar.set_show_arrow(False)
|
||||
|
||||
status_box = Gtk.HBox(homogeneous=False, spacing=0)
|
||||
status_box = Gtk.HBox(homogeneous=False, spacing=2)
|
||||
status_box.set_name('status')
|
||||
status_box.pack_start(battery_icon, False, True, 0)
|
||||
status_box.pack_start(battery_label, False, True, 0)
|
||||
status_box.pack_start(light_icon, False, True, 0)
|
||||
status_box.pack_start(light_label, False, True, 0)
|
||||
status_box.pack_end(toolbar, False, False, 0)
|
||||
status_box.pack_end(not_encrypted_icon, False, False, 0)
|
||||
|
||||
status_vbox = Gtk.VBox(homogeneous=False, spacing=4)
|
||||
status_vbox.pack_start(label, True, True, 0)
|
||||
status_vbox.pack_start(status_box, True, True, 0)
|
||||
|
||||
device_box = Gtk.HBox(homogeneous=False, spacing=4)
|
||||
# device_box.set_border_width(4)
|
||||
device_box.pack_start(icon, False, False, 0)
|
||||
device_box.pack_start(status_vbox, True, True, 0)
|
||||
device_box.show_all()
|
||||
|
||||
info_label = Gtk.Label()
|
||||
info_label.set_markup('<small>reading ...</small>')
|
||||
info_label.set_name('info-label')
|
||||
info_label.set_alignment(0, 0.5)
|
||||
info_label.set_padding(8, 2)
|
||||
info_label.set_property('margin-left', 54)
|
||||
info_label.set_selectable(True)
|
||||
info_label.set_alignment(0, 0)
|
||||
|
||||
info_box = Gtk.Frame()
|
||||
info_box.add(info_label)
|
||||
def _update_info_label():
|
||||
if info_label.get_text().count('\n') < 5:
|
||||
device = frame._device
|
||||
assert device
|
||||
|
||||
toggle_info_action = ui.action._toggle_action('info', 'Device info', _toggle_info, info_label, info_box, frame)
|
||||
items = []
|
||||
hid = device.protocol
|
||||
if hid:
|
||||
items += [('Protocol', 'HID++ %1.1f' % device.protocol)]
|
||||
else:
|
||||
items += [('Protocol', 'unknown')]
|
||||
items += [('Polling rate', '%d ms' % device.polling_rate), ('Wireless PID', device.wpid), ('Serial', device.serial)]
|
||||
firmware = device.firmware
|
||||
if firmware:
|
||||
items += [(f.kind, (f.name + ' ' + f.version).strip()) for f in firmware]
|
||||
|
||||
info_label.set_markup('<small><tt>' + '\n'.join('%-13s: %s' % item for item in items) + '</tt></small>')
|
||||
|
||||
def _toggle_info_label(action, frame):
|
||||
active = action.get_active()
|
||||
if active:
|
||||
# toggle_config_action.set_active(False)
|
||||
ui.find_children(frame, 'toolbar').get_children()[-1].set_active(False)
|
||||
|
||||
vbox = frame.get_child()
|
||||
children = vbox.get_children()
|
||||
children[1].set_visible(active) # separator
|
||||
children[2].set_visible(active) # info label
|
||||
|
||||
if active:
|
||||
GObject.timeout_add(30, _update_info_label)
|
||||
|
||||
def _toggle_config(action, frame):
|
||||
active = action.get_active()
|
||||
if active:
|
||||
# toggle_info_action.set_active(False)
|
||||
ui.find_children(frame, 'toolbar').get_children()[0].set_active(False)
|
||||
|
||||
vbox = frame.get_child()
|
||||
children = vbox.get_children()
|
||||
children[1].set_visible(active) # separator
|
||||
children[3].set_visible(active) # config box
|
||||
children[4].set_visible(active) # unpair button
|
||||
|
||||
if active:
|
||||
GObject.timeout_add(30, _config_panel.update, frame)
|
||||
|
||||
toggle_info_action = ui.action._toggle_action('info', 'Details', _toggle_info_label, frame)
|
||||
toolbar.insert(toggle_info_action.create_tool_item(), 0)
|
||||
toolbar.insert(ui.action.unpair(frame).create_tool_item(), -1)
|
||||
toggle_config_action = ui.action._toggle_action('preferences-system', 'Configuration', _toggle_config, frame)
|
||||
toolbar.insert(toggle_config_action.create_tool_item(), -1)
|
||||
|
||||
vbox = Gtk.VBox(homogeneous=False, spacing=4)
|
||||
vbox.pack_start(label, True, True, 0)
|
||||
vbox.pack_start(status_box, True, True, 0)
|
||||
vbox.pack_start(info_box, True, True, 0)
|
||||
vbox = Gtk.VBox(homogeneous=False, spacing=2)
|
||||
vbox.set_border_width(2)
|
||||
vbox.pack_start(device_box, True, True, 0)
|
||||
vbox.pack_start(Gtk.HSeparator(), False, False, 0)
|
||||
vbox.pack_start(info_label, False, False, 0)
|
||||
|
||||
box = Gtk.HBox(homogeneous=False, spacing=4)
|
||||
box.set_border_width(4)
|
||||
box.pack_start(icon, False, False, 0)
|
||||
box.pack_start(vbox, True, True, 0)
|
||||
box.show_all()
|
||||
config_box = Gtk.VBox(homogeneous=False, spacing=4)
|
||||
config_box.set_name('config-box')
|
||||
config_box.set_property('margin', 8)
|
||||
vbox.pack_start(config_box, False, False, 0)
|
||||
|
||||
frame.add(box)
|
||||
info_box.set_visible(False)
|
||||
unpair = Gtk.Button('Unpair')
|
||||
unpair.set_image(Gtk.Image.new_from_icon_name('edit-delete', Gtk.IconSize.BUTTON))
|
||||
unpair.connect('clicked', ui.action._unpair_device, frame)
|
||||
unpair.set_relief(Gtk.ReliefStyle.NONE)
|
||||
unpair.set_property('margin-left', 106)
|
||||
unpair.set_property('margin-right', 106)
|
||||
unpair.set_property('can-focus', False) # exclude from tab-navigation
|
||||
vbox.pack_end(unpair, False, False, 0)
|
||||
|
||||
vbox.show_all()
|
||||
frame.add(vbox)
|
||||
|
||||
_toggle_info_label(toggle_info_action, frame)
|
||||
_toggle_config(toggle_config_action, frame)
|
||||
return frame
|
||||
|
||||
|
||||
def toggle(window, trigger):
|
||||
if window.get_visible():
|
||||
position = window.get_position()
|
||||
window.hide()
|
||||
window.move(*position)
|
||||
else:
|
||||
if trigger and type(trigger) == Gtk.StatusIcon:
|
||||
x, y = window.get_position()
|
||||
if x == 0 and y == 0:
|
||||
x, y, _ = Gtk.StatusIcon.position_menu(Gtk.Menu(), trigger)
|
||||
window.move(x, y)
|
||||
window.present()
|
||||
return True
|
||||
|
||||
|
||||
def create(title, name, max_devices, systray=False):
|
||||
window = Gtk.Window()
|
||||
window.set_title(title)
|
||||
window.set_icon_name(ui.appicon(0))
|
||||
window.set_role('status-window')
|
||||
|
||||
vbox = Gtk.VBox(homogeneous=False, spacing=4)
|
||||
vbox = Gtk.VBox(homogeneous=False, spacing=12)
|
||||
vbox.set_border_width(4)
|
||||
|
||||
rbox = _make_receiver_box(name)
|
||||
@@ -185,6 +257,7 @@ def create(title, name, max_devices, systray=False):
|
||||
vbox.set_visible(True)
|
||||
|
||||
window.add(vbox)
|
||||
window._vbox = vbox
|
||||
|
||||
geometry = Gdk.Geometry()
|
||||
geometry.min_width = 320
|
||||
@@ -192,13 +265,50 @@ def create(title, name, max_devices, systray=False):
|
||||
window.set_geometry_hints(vbox, geometry, Gdk.WindowHints.MIN_SIZE)
|
||||
window.set_resizable(False)
|
||||
|
||||
window.toggle_visible = lambda i: toggle(window, i)
|
||||
def _toggle_visible(w, trigger):
|
||||
if w.get_visible():
|
||||
# hiding moves the window to 0,0
|
||||
position = w.get_position()
|
||||
w.hide()
|
||||
w.move(*position)
|
||||
else:
|
||||
if isinstance(trigger, Gtk.StatusIcon):
|
||||
x, y = w.get_position()
|
||||
if x == 0 and y == 0:
|
||||
# if the window hasn't been shown yet, position it next to the status icon
|
||||
x, y, _ = Gtk.StatusIcon.position_menu(Gtk.Menu(), trigger)
|
||||
w.move(x, y)
|
||||
w.present()
|
||||
return True
|
||||
|
||||
if systray:
|
||||
window.set_keep_above(True)
|
||||
window.connect('delete-event', toggle)
|
||||
else:
|
||||
window.connect('delete-event', Gtk.main_quit)
|
||||
def _set_has_systray(w, systray):
|
||||
if systray != w._has_systray:
|
||||
w._has_systray = systray
|
||||
if systray:
|
||||
if w._delete_event_connection is None or not w.get_skip_taskbar_hint():
|
||||
w.set_skip_taskbar_hint(True)
|
||||
w.set_skip_pager_hint(True)
|
||||
if w._delete_event_connection:
|
||||
w.disconnect(w._delete_event_connection)
|
||||
w._delete_event_connection = w.connect('delete-event', _toggle_visible)
|
||||
else:
|
||||
if w._delete_event_connection is None or w.get_skip_taskbar_hint():
|
||||
w.set_skip_taskbar_hint(False)
|
||||
w.set_skip_pager_hint(False)
|
||||
if w._delete_event_connection:
|
||||
w.disconnect(w._delete_event_connection)
|
||||
w._delete_event_connection = w.connect('delete-event', Gtk.main_quit)
|
||||
w.present()
|
||||
|
||||
from types import MethodType
|
||||
window.toggle_visible = MethodType(_toggle_visible, window)
|
||||
window.set_has_systray = MethodType(_set_has_systray, window)
|
||||
del MethodType
|
||||
|
||||
window.set_keep_above(True)
|
||||
window._delete_event_connection = None
|
||||
window._has_systray = None
|
||||
window.set_has_systray(systray)
|
||||
|
||||
return window
|
||||
|
||||
@@ -207,63 +317,80 @@ def create(title, name, max_devices, systray=False):
|
||||
#
|
||||
|
||||
def _update_receiver_box(frame, receiver):
|
||||
label, toolbar, info_label = ui.find_children(frame, 'label', 'toolbar', 'info-label')
|
||||
icon, label, pairing_icon, toolbar = ui.find_children(frame, 'icon', 'label', 'pairing-icon', 'toolbar')
|
||||
|
||||
label.set_text(receiver.status_text or '')
|
||||
if receiver.status < STATUS.CONNECTED:
|
||||
label.set_text(str(receiver.status))
|
||||
if receiver:
|
||||
frame._device = receiver
|
||||
icon.set_sensitive(True)
|
||||
if receiver.status.lock_open:
|
||||
if pairing_icon._tick == 0:
|
||||
def _pairing_tick(i, s):
|
||||
if s and s.lock_open:
|
||||
i.set_sensitive(bool(i._tick % 2))
|
||||
i._tick += 1
|
||||
return True
|
||||
i.set_visible(False)
|
||||
i.set_sensitive(True)
|
||||
i._tick = 0
|
||||
pairing_icon.set_visible(True)
|
||||
GObject.timeout_add(1000, _pairing_tick, pairing_icon, receiver.status)
|
||||
else:
|
||||
pairing_icon.set_visible(False)
|
||||
pairing_icon.set_sensitive(True)
|
||||
pairing_icon._tick = 0
|
||||
toolbar.set_sensitive(True)
|
||||
else:
|
||||
frame._device = None
|
||||
icon.set_sensitive(False)
|
||||
pairing_icon.set_visible(False)
|
||||
toolbar.set_sensitive(False)
|
||||
toolbar.get_children()[0].set_active(False)
|
||||
info_label.set_text('')
|
||||
frame._device = None
|
||||
else:
|
||||
toolbar.set_sensitive(True)
|
||||
frame._device = receiver
|
||||
ui.find_children(frame, 'info-label').set_text('')
|
||||
|
||||
|
||||
def _update_device_box(frame, dev):
|
||||
frame._device = dev
|
||||
if dev is None:
|
||||
frame.set_visible(False)
|
||||
frame.set_name(_PLACEHOLDER)
|
||||
frame._device = None
|
||||
_config_panel.update(frame)
|
||||
return
|
||||
|
||||
icon, label, info_label = ui.find_children(frame, 'icon', 'label', 'info-label')
|
||||
icon, label, status_icons = ui.find_children(frame, 'icon', 'label', 'status')
|
||||
|
||||
if frame.get_name() != dev.name:
|
||||
first_run = frame.get_name() != dev.name
|
||||
if first_run:
|
||||
frame._device = dev
|
||||
frame.set_name(dev.name)
|
||||
icon_name = ui.get_icon(dev.name, dev.kind)
|
||||
icon.set_from_icon_name(icon_name, _DEVICE_ICON_SIZE)
|
||||
icon_set = ui.device_icon_set(dev.name, dev.kind)
|
||||
icon.set_from_icon_set(icon_set, _DEVICE_ICON_SIZE)
|
||||
label.set_markup('<b>' + dev.name + '</b>')
|
||||
toolbar = ui.find_children(frame, 'toolbar')
|
||||
for i in toolbar.get_children():
|
||||
i.set_active(False)
|
||||
|
||||
status = ui.find_children(frame, 'status')
|
||||
status_icons = status.get_children()
|
||||
toolbar = status_icons[-1]
|
||||
if dev.status < STATUS.CONNECTED:
|
||||
icon.set_sensitive(False)
|
||||
label.set_sensitive(False)
|
||||
status.set_sensitive(False)
|
||||
for c in status_icons[1:-1]:
|
||||
c.set_visible(False)
|
||||
toolbar.get_children()[0].set_active(False)
|
||||
else:
|
||||
icon.set_sensitive(True)
|
||||
battery_icon, battery_label, light_icon, light_label, not_encrypted_icon, _ = status_icons
|
||||
battery_level = dev.status.get(_status.BATTERY_LEVEL)
|
||||
|
||||
if dev.status:
|
||||
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(ui.get_battery_icon(-1), _STATUS_ICON_SIZE)
|
||||
battery_label.set_markup('<small>no status</small>')
|
||||
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_status = dev.status.get(_status.BATTERY_STATUS)
|
||||
battery_icon.set_tooltip_text(battery_status or '')
|
||||
|
||||
light_icon, light_label = status_icons[2:4]
|
||||
light_level = dev.props.get(PROPS.LIGHT_LEVEL)
|
||||
light_level = dev.status.get(_status.LIGHT_LEVEL)
|
||||
if light_level is None:
|
||||
light_icon.set_visible(False)
|
||||
light_label.set_visible(False)
|
||||
@@ -274,31 +401,39 @@ 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)
|
||||
not_encrypted_icon.set_visible(dev.status.get(_status.ENCRYPTED) == False)
|
||||
|
||||
else:
|
||||
label.set_sensitive(False)
|
||||
|
||||
battery_icon.set_sensitive(False)
|
||||
battery_label.set_sensitive(False)
|
||||
if battery_level is None:
|
||||
battery_label.set_markup('<small>inactive</small>')
|
||||
else:
|
||||
battery_label.set_markup('%d%%' % battery_level)
|
||||
|
||||
light_icon.set_visible(False)
|
||||
light_label.set_visible(False)
|
||||
not_encrypted_icon.set_visible(False)
|
||||
|
||||
frame.set_visible(True)
|
||||
_config_panel.update(frame)
|
||||
|
||||
|
||||
def update(window, receiver, device=None):
|
||||
# print ("update", receiver, receiver.status, device)
|
||||
assert receiver is not None
|
||||
# print ("update %s %s, %s" % (receiver, receiver.status, device))
|
||||
window.set_icon_name(ui.appicon(receiver.status))
|
||||
|
||||
vbox = window.get_child()
|
||||
vbox = window._vbox
|
||||
frames = list(vbox.get_children())
|
||||
assert len(frames) == 1 + receiver.max_devices, frames
|
||||
|
||||
if device is None:
|
||||
_update_receiver_box(frames[0], receiver)
|
||||
if receiver.status < STATUS.CONNECTED:
|
||||
for frame in frames[1:]:
|
||||
frame.set_visible(False)
|
||||
frame.set_name(_PLACEHOLDER)
|
||||
frame._device = None
|
||||
if device:
|
||||
_update_device_box(frames[device.number], None if device.status is None else device)
|
||||
else:
|
||||
frame = frames[device.number]
|
||||
if device.status == STATUS.UNPAIRED:
|
||||
frame.set_visible(False)
|
||||
frame.set_name(_PLACEHOLDER)
|
||||
frame._device = None
|
||||
else:
|
||||
_update_device_box(frame, device)
|
||||
_update_receiver_box(frames[0], receiver)
|
||||
if not receiver:
|
||||
for frame in frames[1:]:
|
||||
_update_device_box(frame, None)
|
||||
|
||||
@@ -2,27 +2,18 @@
|
||||
# Optional desktop notifications.
|
||||
#
|
||||
|
||||
import logging
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
|
||||
try:
|
||||
# this import is allowed to fail, in which case the entire feature is unavailable
|
||||
from gi.repository import Notify
|
||||
import logging
|
||||
|
||||
import ui
|
||||
from logitech.devices.constants import STATUS
|
||||
|
||||
# necessary because the notifications daemon does not know about our XDG_DATA_DIRS
|
||||
_icons = {}
|
||||
|
||||
def _icon(title):
|
||||
if title not in _icons:
|
||||
_icons[title] = ui.icon_file(title)
|
||||
|
||||
return _icons.get(title)
|
||||
|
||||
# assumed to be working since the import succeeded
|
||||
available = True
|
||||
|
||||
_notifications = {}
|
||||
|
||||
|
||||
@@ -47,7 +38,7 @@ try:
|
||||
Notify.uninit()
|
||||
|
||||
|
||||
def show(dev):
|
||||
def show(dev, reason=None):
|
||||
"""Show a notification with title and text."""
|
||||
if available and Notify.is_initted():
|
||||
summary = dev.name
|
||||
@@ -57,8 +48,13 @@ try:
|
||||
if n is None:
|
||||
n = _notifications[summary] = Notify.Notification()
|
||||
|
||||
n.update(summary, dev.status_text, _icon(summary) or dev.kind)
|
||||
urgency = Notify.Urgency.LOW if dev.status > STATUS.CONNECTED else Notify.Urgency.NORMAL
|
||||
message = reason or ('unpaired' if dev.status is None else
|
||||
(str(dev.status) or ('connected' if dev.status else 'inactive')))
|
||||
|
||||
# we need to use the filename here because the notifications daemon
|
||||
# is an external application that does not know about our icon sets
|
||||
n.update(summary, message, ui.device_icon_file(dev.name, dev.kind))
|
||||
urgency = Notify.Urgency.LOW if dev.status else Notify.Urgency.NORMAL
|
||||
n.set_urgency(urgency)
|
||||
|
||||
try:
|
||||
@@ -68,8 +64,7 @@ try:
|
||||
logging.exception("showing %s", n)
|
||||
|
||||
except ImportError:
|
||||
logging.warn("desktop notifications disabled")
|
||||
available = False
|
||||
init = lambda app_title: False
|
||||
uninit = lambda: None
|
||||
show = lambda dev: None
|
||||
show = lambda dev, reason: None
|
||||
|
||||
@@ -2,124 +2,197 @@
|
||||
#
|
||||
#
|
||||
|
||||
# import logging
|
||||
from gi.repository import (Gtk, GObject)
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
from gi.repository import Gtk, GObject
|
||||
|
||||
from logging import getLogger, DEBUG as _DEBUG
|
||||
_log = getLogger('pair-window')
|
||||
del getLogger
|
||||
|
||||
import ui
|
||||
from logitech.unifying_receiver import status as _status
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
_PAIRING_TIMEOUT = 30
|
||||
|
||||
|
||||
def _create_page(assistant, text, kind):
|
||||
p = Gtk.VBox(False, 12)
|
||||
p.set_border_width(8)
|
||||
def _create_page(assistant, kind, header=None, icon_name=None, text=None):
|
||||
p = Gtk.VBox(False, 8)
|
||||
assistant.append_page(p)
|
||||
assistant.set_page_type(p, kind)
|
||||
|
||||
if header:
|
||||
item = Gtk.HBox(False, 16)
|
||||
p.pack_start(item, False, True, 0)
|
||||
|
||||
label = Gtk.Label(header)
|
||||
label.set_alignment(0, 0)
|
||||
label.set_line_wrap(True)
|
||||
item.pack_start(label, True, True, 0)
|
||||
|
||||
if icon_name:
|
||||
icon = Gtk.Image.new_from_icon_name(icon_name, Gtk.IconSize.DIALOG)
|
||||
icon.set_alignment(1, 0)
|
||||
item.pack_start(icon, False, False, 0)
|
||||
|
||||
if text:
|
||||
label = Gtk.Label(text)
|
||||
label.set_alignment(0, 0)
|
||||
p.pack_start(label, False, True, 0)
|
||||
|
||||
assistant.append_page(p)
|
||||
assistant.set_page_type(p, kind)
|
||||
label.set_line_wrap(True)
|
||||
p.pack_start(label, False, False, 0)
|
||||
|
||||
p.show_all()
|
||||
return p
|
||||
|
||||
|
||||
def _device_confirmed(entry, _2, trigger, assistant, page):
|
||||
assistant.commit()
|
||||
assistant.set_page_complete(page, True)
|
||||
# def _fake_device(receiver):
|
||||
# from logitech.unifying_receiver import PairedDevice
|
||||
# dev = PairedDevice(receiver, 6)
|
||||
# dev._wpid = '1234'
|
||||
# dev._kind = 'touchpad'
|
||||
# dev._codename = 'T650'
|
||||
# dev._name = 'Wireless Rechargeable Touchpad T650'
|
||||
# dev._serial = '0123456789'
|
||||
# dev._protocol = 2.0
|
||||
# dev.status = _status.DeviceStatus(dev, lambda *foo: None)
|
||||
# dev.status['encrypted'] = False
|
||||
# return dev
|
||||
|
||||
def _check_lock_state(assistant, receiver):
|
||||
if not assistant.is_drawable():
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("assistant %s destroyed, bailing out", assistant)
|
||||
return False
|
||||
|
||||
if receiver.status.get(_status.ERROR):
|
||||
# receiver.status.new_device = _fake_device(receiver)
|
||||
_pairing_failed(assistant, receiver, receiver.status.pop(_status.ERROR))
|
||||
return False
|
||||
|
||||
if receiver.status.new_device:
|
||||
device, receiver.status.new_device = receiver.status.new_device, None
|
||||
_pairing_succeeded(assistant, receiver, device)
|
||||
return False
|
||||
|
||||
if not receiver.status.lock_open:
|
||||
_pairing_failed(assistant, receiver, 'failed to open pairing lock')
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _finish(assistant):
|
||||
# logging.debug("finish %s", assistant)
|
||||
assistant.destroy()
|
||||
|
||||
def _cancel(assistant, state):
|
||||
# logging.debug("cancel %s", assistant)
|
||||
state.stop_scan()
|
||||
_finish(assistant)
|
||||
|
||||
def _prepare(assistant, page, state):
|
||||
def _prepare(assistant, page, receiver):
|
||||
index = assistant.get_current_page()
|
||||
# logging.debug("prepare %s %d %s", assistant, index, page)
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("prepare %s %d %s", assistant, index, page)
|
||||
|
||||
if index == 0:
|
||||
state.reset()
|
||||
GObject.timeout_add(state.TICK, state.countdown, assistant)
|
||||
spinner = page.get_children()[-1]
|
||||
spinner.start()
|
||||
return
|
||||
|
||||
assistant.remove_page(0)
|
||||
state.stop_scan()
|
||||
|
||||
|
||||
def _scan_complete_ui(assistant, device):
|
||||
if device is None:
|
||||
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)
|
||||
if receiver.set_lock(False, timeout=_PAIRING_TIMEOUT):
|
||||
assert receiver.status.new_device is None
|
||||
assert receiver.status.get(_status.ERROR) is None
|
||||
spinner = page.get_children()[-1]
|
||||
spinner.start()
|
||||
GObject.timeout_add(2000, _check_lock_state, assistant, receiver)
|
||||
assistant.set_page_complete(page, True)
|
||||
else:
|
||||
GObject.idle_add(_pairing_failed, assistant, receiver, 'the pairing lock did not open')
|
||||
else:
|
||||
page = _create_page(assistant,
|
||||
None,
|
||||
Gtk.AssistantPageType.CONFIRM)
|
||||
assistant.remove_page(0)
|
||||
|
||||
hbox = Gtk.HBox(False, 16)
|
||||
device_icon = Gtk.Image()
|
||||
device_icon.set_from_icon_name(ui.get_icon(device.name, device.kind), Gtk.IconSize.DIALOG)
|
||||
hbox.pack_start(device_icon, False, False, 0)
|
||||
device_label = Gtk.Label(device.kind + '\n' + device.name)
|
||||
hbox.pack_start(device_label, False, False, 0)
|
||||
halign = Gtk.Alignment.new(0.5, 0.5, 0, 1)
|
||||
halign.add(hbox)
|
||||
page.pack_start(halign, False, True, 0)
|
||||
|
||||
hbox = Gtk.HBox(False, 16)
|
||||
hbox.pack_start(Gtk.Entry(), False, False, 0)
|
||||
hbox.pack_start(Gtk.ToggleButton('Test'), False, False, 0)
|
||||
halign = Gtk.Alignment.new(0.5, 0.5, 0, 1)
|
||||
halign.add(hbox)
|
||||
page.pack_start(halign, False, False, 0)
|
||||
def _finish(assistant, receiver):
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("finish %s", assistant)
|
||||
assistant.destroy()
|
||||
receiver.status.new_device = None
|
||||
if receiver.status.lock_open:
|
||||
receiver.set_lock()
|
||||
else:
|
||||
receiver.status[_status.ERROR] = None
|
||||
|
||||
entry_info = Gtk.Label('Use the controls above to confirm\n'
|
||||
'this is the device you want to pair.')
|
||||
entry_info.set_sensitive(False)
|
||||
page.pack_start(entry_info, False, False, 0)
|
||||
|
||||
page.show_all()
|
||||
assistant.set_page_complete(page, True)
|
||||
def _pairing_failed(assistant, receiver, error):
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("%s fail: %s", receiver, error)
|
||||
|
||||
assistant.commit()
|
||||
|
||||
header = 'Pairing failed: %s.' % error
|
||||
if 'timeout' in str(error):
|
||||
text = 'Make sure your device is within range,\nand it has a decent battery charge.'
|
||||
else:
|
||||
text = None
|
||||
_create_page(assistant, Gtk.AssistantPageType.SUMMARY, header, 'dialog-error', text)
|
||||
|
||||
assistant.next_page()
|
||||
|
||||
def _scan_complete(assistant, device):
|
||||
GObject.idle_add(_scan_complete_ui, assistant, device)
|
||||
assistant.commit()
|
||||
|
||||
|
||||
def create(action, state):
|
||||
def _pairing_succeeded(assistant, receiver, device):
|
||||
assert device
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("%s success: %s", receiver, device)
|
||||
|
||||
page = _create_page(assistant, Gtk.AssistantPageType.SUMMARY)
|
||||
|
||||
header = Gtk.Label('Found a new device:')
|
||||
header.set_alignment(0.5, 0)
|
||||
page.pack_start(header, False, False, 0)
|
||||
|
||||
device_icon = Gtk.Image()
|
||||
icon_set = ui.device_icon_set(device.name, device.kind)
|
||||
device_icon.set_from_icon_set(icon_set, Gtk.IconSize.LARGE)
|
||||
device_icon.set_alignment(0.5, 1)
|
||||
page.pack_start(device_icon, True, True, 0)
|
||||
|
||||
device_label = Gtk.Label()
|
||||
device_label.set_markup('<b>' + device.name + '</b>')
|
||||
device_label.set_alignment(0.5, 0)
|
||||
page.pack_start(device_label, True, True, 0)
|
||||
|
||||
hbox = Gtk.HBox(False, 8)
|
||||
hbox.pack_start(Gtk.Label(' '), False, False, 0)
|
||||
hbox.set_property('expand', False)
|
||||
hbox.set_property('halign', Gtk.Align.CENTER)
|
||||
page.pack_start(hbox, False, False, 0)
|
||||
|
||||
def _check_encrypted(dev):
|
||||
if assistant.is_drawable():
|
||||
if device.status.get('encrypted') == False:
|
||||
hbox.pack_start(Gtk.Image.new_from_icon_name('security-low', Gtk.IconSize.MENU), False, False, 0)
|
||||
hbox.pack_start(Gtk.Label('The wireless link is not encrypted!'), False, False, 0)
|
||||
hbox.show_all()
|
||||
else:
|
||||
return True
|
||||
GObject.timeout_add(500, _check_encrypted, device)
|
||||
|
||||
page.show_all()
|
||||
|
||||
assistant.next_page()
|
||||
assistant.commit()
|
||||
|
||||
|
||||
def create(action, receiver):
|
||||
assistant = Gtk.Assistant()
|
||||
assistant.set_title(action.get_label())
|
||||
assistant.set_icon_name(action.get_icon_name())
|
||||
|
||||
assistant.set_size_request(440, 240)
|
||||
assistant.set_size_request(400, 240)
|
||||
assistant.set_resizable(False)
|
||||
assistant.set_role('pair-device')
|
||||
|
||||
page_intro = _create_page(assistant,
|
||||
'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)
|
||||
page_intro = _create_page(assistant, Gtk.AssistantPageType.PROGRESS,
|
||||
'Turn on the device you want to pair.', 'preferences-desktop-peripherals',
|
||||
'If the device is already turned on,\nturn if off and on again.')
|
||||
spinner = Gtk.Spinner()
|
||||
spinner.set_visible(True)
|
||||
page_intro.pack_end(spinner, True, True, 16)
|
||||
page_intro.pack_end(spinner, True, True, 24)
|
||||
|
||||
assistant.scan_complete = _scan_complete
|
||||
|
||||
assistant.connect('prepare', _prepare, state)
|
||||
assistant.connect('cancel', _cancel, state)
|
||||
assistant.connect('close', _finish)
|
||||
assistant.connect('apply', _finish)
|
||||
assistant.connect('prepare', _prepare, receiver)
|
||||
assistant.connect('cancel', _finish, receiver)
|
||||
assistant.connect('close', _finish, receiver)
|
||||
|
||||
return assistant
|
||||
|
||||
@@ -2,16 +2,28 @@
|
||||
#
|
||||
#
|
||||
|
||||
from gi.repository import Gtk
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
from gi.repository import Gtk, GObject, GdkPixbuf
|
||||
|
||||
import ui
|
||||
from logitech.unifying_receiver import status as _status
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
_NO_DEVICES = [None] * 6
|
||||
|
||||
def create(window, menu_actions=None):
|
||||
name = window.get_title()
|
||||
icon = Gtk.StatusIcon()
|
||||
icon.set_title(window.get_title())
|
||||
icon.set_name(window.get_title())
|
||||
icon.set_from_icon_name(ui.appicon(0))
|
||||
icon.set_title(name)
|
||||
icon.set_name(name)
|
||||
icon.set_from_icon_name(ui.appicon(False))
|
||||
icon._devices = list(_NO_DEVICES)
|
||||
|
||||
icon.set_tooltip_text(name)
|
||||
icon.connect('activate', window.toggle_visible)
|
||||
|
||||
menu = Gtk.Menu()
|
||||
@@ -27,29 +39,79 @@ def create(window, menu_actions=None):
|
||||
menu.popup(None, None, icon.position_menu, icon, button, time),
|
||||
menu)
|
||||
|
||||
# use size-changed to detect if the systray is available or not
|
||||
def _size_changed(i, size, w):
|
||||
def _check_systray(i2, w2):
|
||||
w2.set_has_systray(i2.is_embedded() and i2.get_visible())
|
||||
GObject.timeout_add(250, _check_systray, i, w)
|
||||
icon.connect('size-changed', _size_changed, window)
|
||||
|
||||
return icon
|
||||
|
||||
|
||||
def update(icon, receiver):
|
||||
icon.set_from_icon_name(ui.appicon(receiver.status))
|
||||
_PIXMAPS = {}
|
||||
def _icon_with_battery(s):
|
||||
battery_icon = ui.get_battery_icon(s[_status.BATTERY_LEVEL])
|
||||
|
||||
if receiver.devices:
|
||||
lines = []
|
||||
if receiver.status < 1:
|
||||
lines += (receiver.status_text, '')
|
||||
name = '%s-%s' % (battery_icon, bool(s))
|
||||
if name not in _PIXMAPS:
|
||||
mask = ui.icon_file(ui.appicon(True) + '-mask', 128)
|
||||
assert mask
|
||||
mask = GdkPixbuf.Pixbuf.new_from_file(mask)
|
||||
assert mask.get_width() == 128 and mask.get_height() == 128
|
||||
mask.saturate_and_pixelate(mask, 0.7, False)
|
||||
|
||||
devlist = [receiver.devices[d] for d in range(1, 1 + receiver.max_devices) if d in receiver.devices]
|
||||
for dev in devlist:
|
||||
name = '<b>' + dev.name + '</b>'
|
||||
if dev.status < 1:
|
||||
lines.append(name + ' (' + dev.status_text + ')')
|
||||
battery = ui.icon_file(battery_icon, 128)
|
||||
assert battery
|
||||
battery = GdkPixbuf.Pixbuf.new_from_file(battery)
|
||||
assert battery.get_width() == 128 and battery.get_height() == 128
|
||||
if not s:
|
||||
battery.saturate_and_pixelate(battery, 0, True)
|
||||
|
||||
# TODO can the masking be done at runtime?
|
||||
battery.composite(mask, 0, 7, 80, 121, -32, 7, 1, 1, GdkPixbuf.InterpType.NEAREST, 255)
|
||||
_PIXMAPS[name] = mask
|
||||
|
||||
return _PIXMAPS[name]
|
||||
|
||||
def update(icon, receiver, device=None):
|
||||
# print ("icon update", receiver, receiver.status, len(receiver), device)
|
||||
battery_status = None
|
||||
|
||||
if device:
|
||||
icon._devices[device.number] = None if device.status is None else device
|
||||
if not receiver:
|
||||
icon._devices[:] = _NO_DEVICES
|
||||
if not icon.is_embedded():
|
||||
return
|
||||
|
||||
lines = [ui.NAME + ': ' + str(receiver.status), '']
|
||||
for dev in icon._devices:
|
||||
if dev is None:
|
||||
continue
|
||||
|
||||
lines.append('<b>' + dev.name + '</b>')
|
||||
|
||||
assert hasattr(dev, 'status') and dev.status is not None
|
||||
p = str(dev.status)
|
||||
if p:
|
||||
if not dev.status:
|
||||
p += ' <small>(inactive)</small>'
|
||||
else:
|
||||
if dev.status:
|
||||
p = '<small>no status</small>'
|
||||
else:
|
||||
lines.append(name)
|
||||
if dev.status > 1:
|
||||
lines.append(' ' + dev.status_text)
|
||||
lines.append('')
|
||||
p = '<small>(inactive)</small>'
|
||||
|
||||
text = '\n'.join(lines).rstrip('\n')
|
||||
icon.set_tooltip_markup(text)
|
||||
lines.append('\t' + p)
|
||||
lines.append('')
|
||||
|
||||
if battery_status is None and dev.status.get(_status.BATTERY_LEVEL):
|
||||
battery_status = dev.status
|
||||
|
||||
icon.set_tooltip_markup('\n'.join(lines).rstrip('\n'))
|
||||
|
||||
if battery_status is None:
|
||||
icon.set_from_icon_name(ui.appicon(receiver.status))
|
||||
else:
|
||||
icon.set_tooltip_text(receiver.status_text)
|
||||
icon.set_from_pixbuf(_icon_with_battery(battery_status))
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
Z=`readlink -f "$0"`
|
||||
LIB=`readlink -f $(dirname "$Z")/../lib`
|
||||
#export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$LIB/native/`arch`
|
||||
export PYTHONPATH=$LIB
|
||||
export PYTHONUNBUFFERED=yes
|
||||
|
||||
PYTHON=`which python python2 python3 | head -n 1`
|
||||
exec $PYTHON -OOu -m hidapi.hidconsole "$@"
|
||||
PYTHON=${PYTHON:-`which python python2 python3 | head -n 1`}
|
||||
exec $PYTHON -m hidapi.hidconsole "$@"
|
||||
|
||||
9
bin/scan
@@ -1,9 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
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 "$@"
|
||||
@@ -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
|
||||
export XDG_DATA_DIRS=${SHARE}_override:$SHARE:$XDG_DATA_DIRS
|
||||
|
||||
PYTHON=`which python python2 python3 | head -n 1`
|
||||
exec $PYTHON -OOu -m solaar "$@"
|
||||
PYTHON=${PYTHON:-`which python python2 python3 | head -n 1`}
|
||||
exec $PYTHON -m solaar "$@"
|
||||
|
||||
9
bin/solaar-cli
Executable file
@@ -0,0 +1,9 @@
|
||||
#!/bin/sh
|
||||
|
||||
Z=`readlink -f "$0"`
|
||||
APP=`readlink -f $(dirname "$Z")/../app`
|
||||
LIB=`readlink -f $(dirname "$Z")/../lib`
|
||||
export PYTHONPATH=$APP:$LIB
|
||||
|
||||
PYTHON=${PYTHON:-`which python python2 python3 | head -n 1`}
|
||||
exec $PYTHON -m solaar_cli "$@"
|
||||
42
docs/devices/m705.txt
Normal file
@@ -0,0 +1,42 @@
|
||||
registers:
|
||||
|
||||
# writing 0x10 in this register will generate an event
|
||||
# 10 02 0Dxx yyzz00
|
||||
# where 0D happens to be the battery register number
|
||||
# xx is the battery charge
|
||||
# yy, zz ?
|
||||
<< ( 0.001) [10 02 8100 000000] '\x10\x02\x81\x00\x00\x00\x00'
|
||||
>> ( 1.132) [10 02 8100 100000] '\x10\x02\x81\x00\x10\x00\x00'
|
||||
|
||||
# smooth scroll - possible values
|
||||
# - 00 (off)
|
||||
# - 02 ?, apparently off as well, default value at power-on
|
||||
# - 0x40 (on)
|
||||
<< ( 2.005) [10 02 8101 000000] '\x10\x02\x81\x01\x00\x00\x00'
|
||||
>> ( 2.052) [10 02 8101 020000] '\x10\x02\x81\x01\x02\x00\x00'
|
||||
|
||||
# battery status: percentage full, ?, ?
|
||||
<< ( 14.835) [10 02 810D 000000] '\x10\x02\x81\r\x00\x00\x00'
|
||||
>> ( 14.847) [10 02 810D 644734] '\x10\x02\x81\rdG4'
|
||||
|
||||
# accepts mask 0xF1
|
||||
# setting 0x10 turns off the movement events (but buttons still work)
|
||||
<< ( 221.495) [10 02 81D0 000000] '\x10\x02\x81\xd0\x00\x00\x00'
|
||||
>> ( 221.509) [10 02 81D0 000000] '\x10\x02\x81\xd0\x00\x00\x00'
|
||||
|
||||
# appears to be read-only?
|
||||
<< ( 223.527) [10 02 81D2 000000] '\x10\x02\x81\xd2\x00\x00\x00'
|
||||
>> ( 223.540) [10 02 81D2 000003] '\x10\x02\x81\xd2\x00\x00\x03'
|
||||
|
||||
# appears to be read-only?
|
||||
<< ( 225.557) [10 02 81D4 000000] '\x10\x02\x81\xd4\x00\x00\x00'
|
||||
>> ( 225.571) [10 02 81D4 000004] '\x10\x02\x81\xd4\x00\x00\x04'
|
||||
|
||||
# read-only, 01-04 firmware info
|
||||
<< ( 259.270) [10 02 81F1 000000] '\x10\x02\x81\xf1\x00\x00\x00'
|
||||
>> ( 259.283) [10 02 8F81 F10300] '\x10\x02\x8f\x81\xf1\x03\x00'
|
||||
|
||||
# writing 01 here will trigger an avalance of events, most likely
|
||||
# raw input from the mouse; disable by writing 00
|
||||
<< ( 261.300) [10 02 81F3 000000] '\x10\x02\x81\xf3\x00\x00\x00'
|
||||
>> ( 261.315) [10 02 81F3 000000] '\x10\x02\x81\xf3\x00\x00\x00'
|
||||
12
docs/devices/performance-mx.txt
Normal file
@@ -0,0 +1,12 @@
|
||||
|
||||
short register 0x63: values 0x81 .. 0x8F
|
||||
set DPI as 100 .. 1500
|
||||
|
||||
|
||||
short register 0x51: set leds
|
||||
value: ab cd 00
|
||||
where a/b/c/d values are 1=off, 2=on, 3=flash
|
||||
a = lower led
|
||||
b = red led
|
||||
c = upper led
|
||||
d = middle led
|
||||
BIN
docs/logitech/4301_k750_solarkeyboard_lightandbattery.pdf
Normal file
BIN
docs/logitech/6110_touchmouseraw.pdf
Normal file
278
docs/logitech/hid10.txt
Normal file
@@ -0,0 +1,278 @@
|
||||
*Read short register command*
|
||||
|
||||
10 ix 81 02 00 00 00
|
||||
|
||||
ix
|
||||
|
||||
Index 0x0n: Device #n
|
||||
|
||||
0xFF: Transceiver
|
||||
|
||||
*Response to Read command (success)*
|
||||
|
||||
10 ix 81 02 00 r1 r2
|
||||
|
||||
ix
|
||||
|
||||
Index (same as command)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
r1
|
||||
|
||||
Number of Connected Devices
|
||||
|
||||
bit 0..7: Number of connected devices (receivers only)
|
||||
|
||||
r2
|
||||
|
||||
Number of Remaining Pairing Slots
|
||||
|
||||
bit 0..7: Number of remaining pairing slots
|
||||
|
||||
|
||||
*Read long register command*
|
||||
|
||||
10 ix 83 B5 nn 00 00
|
||||
|
||||
ix
|
||||
|
||||
Index 0xFF: Transceiver
|
||||
|
||||
nn
|
||||
|
||||
0x20 Device 1
|
||||
|
||||
0x21 Device 2
|
||||
|
||||
0x22 Device 3
|
||||
|
||||
0x23 Device 4
|
||||
|
||||
0x24 Device 5
|
||||
|
||||
0x25 Device 6
|
||||
|
||||
0x26..0x2F Reserved for future extensions
|
||||
|
||||
*Response to Read command (success)*
|
||||
|
||||
11 ix 83 B5 nn r1 r2 r3 r4 r5 r6 r7 r8 r9 ra rb rc rd 00 00
|
||||
|
||||
ix
|
||||
|
||||
Index (same as command)
|
||||
|
||||
nn
|
||||
|
||||
(same format as above)
|
||||
|
||||
r1
|
||||
|
||||
Destination ID
|
||||
|
||||
r2
|
||||
|
||||
Reserved
|
||||
|
||||
r3
|
||||
|
||||
Wireless PID MSB
|
||||
|
||||
r4
|
||||
|
||||
Wireless PID LSB
|
||||
|
||||
r5
|
||||
|
||||
Reserved
|
||||
|
||||
r6
|
||||
|
||||
Reserved
|
||||
|
||||
r7
|
||||
|
||||
Device type
|
||||
|
||||
0 undefined
|
||||
|
||||
1 keyboard
|
||||
|
||||
2 mouse
|
||||
|
||||
3 numpad
|
||||
|
||||
4 presenter
|
||||
|
||||
5 reserved
|
||||
|
||||
6 reserved
|
||||
|
||||
7 remote control
|
||||
|
||||
8 trackball
|
||||
|
||||
9 touchpad
|
||||
|
||||
a tablet
|
||||
|
||||
b gamepad
|
||||
|
||||
c joystick
|
||||
|
||||
r8
|
||||
|
||||
Reserved
|
||||
|
||||
r9
|
||||
|
||||
Reserved
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Alternatively, if enabled, you can also receive a notification when a new
|
||||
device is paired:
|
||||
|
||||
This message is sent by a receiver to the host SW to report a freshly
|
||||
connected device. Enable the HID++ connection reporting by setting the
|
||||
corresponding bit in register 0x00 via HID++ Set Register command.
|
||||
|
||||
*Notification*
|
||||
|
||||
10 ix 41 r0 r1 r2 r3
|
||||
|
||||
ix
|
||||
|
||||
Index
|
||||
|
||||
r0
|
||||
|
||||
bits [0..2] Protocol type
|
||||
|
||||
0x03 = eQUAD
|
||||
|
||||
0x04 = eQuad step 4 DJ
|
||||
|
||||
bits [3..7] Reserved
|
||||
|
||||
r1
|
||||
|
||||
Device Info
|
||||
|
||||
bit0..3 = Device Type
|
||||
|
||||
0x00 = Unknown
|
||||
|
||||
0x01 = Keyboard
|
||||
|
||||
0x02 = Mouse
|
||||
|
||||
0x03 = Numpad
|
||||
|
||||
0x04 = Presenter
|
||||
|
||||
|
||||
r2
|
||||
|
||||
Wireless PID LSB
|
||||
|
||||
r3
|
||||
|
||||
Wireless PID MSB
|
||||
|
||||
To enable the notifications:
|
||||
Enable HID++ Notifications:
|
||||
|
||||
This register defines a number of flags that allow the SW to turn on or off
|
||||
individual spontaneous HID++ reports. Not setting a flag means default
|
||||
reporting. See the table below for more details on each flag.
|
||||
|
||||
For all bits: *0 = disabled* (default value at power-up), 1 = enabled.
|
||||
|
||||
|
||||
|
||||
*Read short register command*
|
||||
|
||||
10 ix 81 00 00 00 00
|
||||
|
||||
ix
|
||||
|
||||
Index 0x0n: Device #n
|
||||
|
||||
0xFF: Transceiver
|
||||
|
||||
*Response to Read command (success)*
|
||||
|
||||
10 ix 81 00 r0 r1 r2
|
||||
|
||||
ix
|
||||
|
||||
Index (same as command)
|
||||
|
||||
r0
|
||||
|
||||
HID++ Reporting Flags (Devices)
|
||||
|
||||
bit 0..3. reserved
|
||||
|
||||
bit 4: Battery Status
|
||||
|
||||
bit 5..7 reserved
|
||||
|
||||
r1
|
||||
|
||||
HID++ Reporting Flags (Receiver)
|
||||
|
||||
bit 0: Wireless notifications
|
||||
|
||||
bit 1..7 reserved
|
||||
|
||||
r2
|
||||
|
||||
|
||||
|
||||
|
||||
*Write short register command*
|
||||
|
||||
10 ix 80 00 p0 p1 p2
|
||||
|
||||
ix
|
||||
|
||||
Index 0x0n: Device #n
|
||||
|
||||
0xFF: Transceiver
|
||||
|
||||
p0
|
||||
|
||||
HID++ Reporting Flags (Devices)
|
||||
|
||||
(same format as above)
|
||||
|
||||
p1
|
||||
|
||||
HID++ Reporting Flags (Receiver)
|
||||
|
||||
(same format as above)
|
||||
|
||||
p2
|
||||
|
||||
|
||||
*Response to Write command (success)*
|
||||
|
||||
10 ix 80 00 zz zz zz
|
||||
|
||||
ix
|
||||
|
||||
Index (same as command)
|
||||
|
||||
zz
|
||||
|
||||
(don't care, recommended to return 0)
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
"""Generic Human Interface Device API."""
|
||||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
__author__ = "Daniel Pavel"
|
||||
__license__ = "GPL"
|
||||
__version__ = "0.4"
|
||||
__version__ = "0.5"
|
||||
|
||||
try:
|
||||
from hidapi.udev import *
|
||||
except ImportError:
|
||||
from hidapi.native import *
|
||||
from hidapi.udev import *
|
||||
|
||||
@@ -1,109 +1,206 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/env python -u
|
||||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
import os
|
||||
import sys
|
||||
from select import select as _select
|
||||
import time
|
||||
from binascii import hexlify, unhexlify
|
||||
_hex = lambda d: hexlify(d).decode('ascii').upper()
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
interactive = os.isatty(0)
|
||||
start_time = 0
|
||||
try:
|
||||
try: # python3 support
|
||||
read_packet = raw_input
|
||||
except:
|
||||
read_packet = input
|
||||
prompt = '?? Input: ' if interactive else ''
|
||||
|
||||
strhex = lambda d: hexlify(d).decode('ascii').upper()
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
from threading import Lock
|
||||
print_lock = Lock()
|
||||
|
||||
def _print(marker, data, scroll=False):
|
||||
t = time.time() - start_time
|
||||
if type(data) == unicode:
|
||||
s = marker + ' ' + data
|
||||
else:
|
||||
hexs = strhex(data)
|
||||
s = '%s (% 8.3f) [%s %s %s %s] %s' % (marker, t, hexs[0:2], hexs[2:4], hexs[4:8], hexs[8:], repr(data))
|
||||
|
||||
print_lock.acquire()
|
||||
|
||||
if interactive and scroll:
|
||||
sys.stdout.write('\033[s')
|
||||
sys.stdout.write('\033[S') # scroll up
|
||||
sys.stdout.write('\033[A\033[L\033[G') # insert new line above the current one, position on first column
|
||||
|
||||
hexs = _hex(data)
|
||||
s = '%s (% 8.3f) [%s %s %s %s] %s' % (marker, t, hexs[0:2], hexs[2:4], hexs[4:8], hexs[8:], repr(data))
|
||||
# scroll the entire screen above the current line up by 1 line
|
||||
sys.stdout.write('\033[s' # save cursor position
|
||||
'\033[S' # scroll up
|
||||
'\033[A' # cursor up
|
||||
'\033[L' # insert 1 line
|
||||
'\033[G') # move cursor to column 1
|
||||
sys.stdout.write(s)
|
||||
|
||||
if interactive and scroll:
|
||||
# restore cursor position
|
||||
sys.stdout.write('\033[u')
|
||||
else:
|
||||
sys.stdout.write('\n')
|
||||
|
||||
print_lock.release()
|
||||
|
||||
def _continuous_read(handle, timeout=1000):
|
||||
|
||||
def _error(text, scroll=False):
|
||||
_print("!!", text, scroll)
|
||||
|
||||
|
||||
def _continuous_read(handle, timeout=2000):
|
||||
while True:
|
||||
reply = hidapi.read(handle, 128, timeout)
|
||||
if reply is None:
|
||||
print ("!! Read failed, aborting")
|
||||
try:
|
||||
reply = hidapi.read(handle, 128, timeout)
|
||||
except OSError as e:
|
||||
_error("Read failed, aborting: " + str(e), True)
|
||||
break
|
||||
elif reply:
|
||||
_print('>>', reply, True)
|
||||
assert reply is not None
|
||||
if reply:
|
||||
_print(">>", reply, True)
|
||||
|
||||
|
||||
def _validate_input(line, hidpp=False):
|
||||
try:
|
||||
data = unhexlify(line.encode('ascii'))
|
||||
except Exception as e:
|
||||
_error("Invalid input: " + str(e))
|
||||
return None
|
||||
|
||||
if hidpp:
|
||||
if len(data) < 4:
|
||||
_error("Invalid HID++ request: need at least 4 bytes")
|
||||
return None
|
||||
if data[:1] not in b'\x10\x11':
|
||||
_error("Invalid HID++ request: first byte must be 0x10 or 0x11")
|
||||
return None
|
||||
if data[1:2] not in b'\xFF\x01\x02\x03\x04\x05\x06':
|
||||
_error("Invalid HID++ request: second byte must be 0xFF or one of 0x01..0x06")
|
||||
return None
|
||||
if data[:1] == b'\x10':
|
||||
if len(data) > 7:
|
||||
_error("Invalid HID++ request: maximum length of a 0x10 request is 7 bytes")
|
||||
return None
|
||||
while len(data) < 7:
|
||||
data = (data + b'\x00' * 7)[:7]
|
||||
elif data[:1] == b'\x11':
|
||||
if len(data) > 20:
|
||||
_error("Invalid HID++ request: maximum length of a 0x11 request is 20 bytes")
|
||||
return None
|
||||
while len(data) < 20:
|
||||
data = (data + b'\x00' * 20)[:20]
|
||||
|
||||
return data
|
||||
|
||||
def _open(device, hidpp):
|
||||
if hidpp and not device:
|
||||
for d in hidapi.enumerate(vendor_id=0x046d):
|
||||
if d.driver == 'logitech-djreceiver':
|
||||
device = d.path
|
||||
break
|
||||
if not device:
|
||||
sys.exit("!! No HID++ receiver found.")
|
||||
if not device:
|
||||
sys.exit("!! Device path required.")
|
||||
|
||||
print (".. Opening device %s" % device)
|
||||
handle = hidapi.open_path(device)
|
||||
if not handle:
|
||||
sys.exit("!! Failed to open %s, aborting." % device)
|
||||
|
||||
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))))
|
||||
if args.hidpp:
|
||||
if hidapi.get_manufacturer(handle) != 'Logitech':
|
||||
sys.exit("!! Only Logitech devices support the HID++ protocol.")
|
||||
print (".. HID++ validation enabled.")
|
||||
|
||||
return handle
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
if __name__ == '__main__':
|
||||
import argparse
|
||||
arg_parser = argparse.ArgumentParser()
|
||||
arg_parser.add_argument('--history', help='history file')
|
||||
arg_parser.add_argument('device', default=None, help='linux device to connect to')
|
||||
arg_parser.add_argument('--history', help='history file (default ~/.hidconsole-history)')
|
||||
arg_parser.add_argument('--hidpp', action='store_true', help='ensure input data is a valid HID++ request')
|
||||
arg_parser.add_argument('device', nargs='?', help='linux device to connect to (/dev/hidrawX); '
|
||||
'may be omitted if --hidpp is given, in which case it looks for the first Logitech receiver')
|
||||
args = arg_parser.parse_args()
|
||||
|
||||
import hidapi
|
||||
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,
|
||||
repr(hidapi.get_manufacturer(handle)),
|
||||
repr(hidapi.get_product(handle)),
|
||||
repr(hidapi.get_serial(handle))))
|
||||
if interactive:
|
||||
print (".. Press ^C/^D to exit, or type hex bytes to write to the device.")
|
||||
handle = _open(args.device, args.hidpp)
|
||||
|
||||
import readline
|
||||
if args.history is None:
|
||||
import os.path
|
||||
args.history = os.path.join(os.path.expanduser("~"), ".hidconsole-history")
|
||||
try:
|
||||
readline.read_history_file(args.history)
|
||||
except:
|
||||
# file may not exist yet
|
||||
pass
|
||||
|
||||
start_time = time.time()
|
||||
if interactive:
|
||||
print (".. Press ^C/^D to exit, or type hex bytes to write to the device.")
|
||||
|
||||
import readline
|
||||
if args.history is None:
|
||||
import os.path
|
||||
args.history = os.path.join(os.path.expanduser("~"), ".hidconsole-history")
|
||||
try:
|
||||
from threading import Thread
|
||||
t = Thread(target=_continuous_read, args=(handle,))
|
||||
t.daemon = True
|
||||
t.start()
|
||||
|
||||
prompt = '?? Input: ' if interactive else ''
|
||||
|
||||
while t.is_alive():
|
||||
line = read_packet(prompt).strip().replace(' ', '')
|
||||
if line:
|
||||
try:
|
||||
data = unhexlify(line.encode('ascii'))
|
||||
except Exception as e:
|
||||
print ("!! Invalid input.")
|
||||
else:
|
||||
_print('<<', data)
|
||||
hidapi.write(handle, data)
|
||||
# wait for some kind of reply
|
||||
if not interactive:
|
||||
rlist, wlist, xlist = _select([handle], [], [], 1)
|
||||
time.sleep(0.1)
|
||||
except EOFError:
|
||||
readline.read_history_file(args.history)
|
||||
except:
|
||||
# file may not exist yet
|
||||
pass
|
||||
except Exception as e:
|
||||
print ('%s: %s' % (type(e).__name__, e))
|
||||
|
||||
print (".. Closing handle %X" % handle)
|
||||
hidapi.close(handle)
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
from threading import Thread
|
||||
t = Thread(target=_continuous_read, args=(handle,))
|
||||
t.daemon = True
|
||||
t.start()
|
||||
|
||||
if interactive:
|
||||
readline.write_history_file(args.history)
|
||||
else:
|
||||
print ("!! Failed to open %s, aborting" % args.device)
|
||||
# move the cursor at the bottom of the screen
|
||||
sys.stdout.write('\033[300B') # move cusor at most 300 lines down, don't scroll
|
||||
|
||||
while t.is_alive():
|
||||
line = read_packet(prompt)
|
||||
line = line.strip().replace(' ', '')
|
||||
if not line:
|
||||
continue
|
||||
|
||||
data = _validate_input(line, args.hidpp)
|
||||
if data is None:
|
||||
continue
|
||||
|
||||
_print("<<", data)
|
||||
hidapi.write(handle, data)
|
||||
# wait for some kind of reply
|
||||
if args.hidpp and not interactive:
|
||||
if data[1:2] == b'\xFF':
|
||||
# the receiver will reply very fast, in a few milliseconds
|
||||
time.sleep(0.010)
|
||||
else:
|
||||
# the devices might reply quite slow
|
||||
rlist, wlist, xlist = _select([handle], [], [], 1)
|
||||
time.sleep(1)
|
||||
except EOFError:
|
||||
if interactive:
|
||||
print ("")
|
||||
except Exception as e:
|
||||
print ('%s: %s' % (type(e).__name__, e))
|
||||
|
||||
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'
|
||||
|
||||
|
||||
@@ -310,7 +314,7 @@ def send_feature_report(device_handle, data, report_number=None):
|
||||
:returns: ``True`` if the report was successfully written to the device.
|
||||
"""
|
||||
if report_number is not None:
|
||||
data = _pack('!B', report_number) + data
|
||||
data = _pack(b'!B', report_number) + data
|
||||
bytes_written = _native.hid_send_feature_report(device_handle, _C.c_char_p(data), len(data))
|
||||
return bytes_written > -1
|
||||
|
||||
@@ -326,7 +330,7 @@ def get_feature_report(device_handle, bytes_count, report_number=None):
|
||||
"""
|
||||
out_buffer = _C.create_string_buffer('\x00' * (bytes_count + 2))
|
||||
if report_number is not None:
|
||||
out_buffer[0] = _pack('!B', report_number)
|
||||
out_buffer[0] = _pack(b'!B', report_number)
|
||||
bytes_read = _native.hid_get_feature_report(device_handle, out_buffer, bytes_count)
|
||||
if bytes_read > -1:
|
||||
return out_buffer[:bytes_read]
|
||||
|
||||
@@ -7,10 +7,12 @@ The docstrings are mostly copied from the hidapi API header, with changes where
|
||||
necessary.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
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'
|
||||
@@ -31,6 +33,7 @@ DeviceInfo = namedtuple('DeviceInfo', [
|
||||
])
|
||||
del namedtuple
|
||||
|
||||
|
||||
#
|
||||
# exposed API
|
||||
# docstrings mostly copied from hidapi.h
|
||||
@@ -124,6 +127,8 @@ def open_path(device_path):
|
||||
|
||||
:returns: an opaque device handle, or ``None``.
|
||||
"""
|
||||
assert device_path
|
||||
assert device_path.startswith('/dev/hidraw')
|
||||
return _os.open(device_path, _os.O_RDWR | _os.O_SYNC)
|
||||
|
||||
|
||||
@@ -132,6 +137,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 +161,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 +184,21 @@ 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)
|
||||
assert data is not None
|
||||
return data
|
||||
else:
|
||||
return b''
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
_DEVICE_STRINGS = {
|
||||
@@ -236,6 +245,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:
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
__author__ = "Daniel Pavel"
|
||||
__license__ = "GPL"
|
||||
__version__ = "0.5"
|
||||
__version__ = "0.8"
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
import logging
|
||||
|
||||
from .constants import (STATUS, PROPS)
|
||||
from ..unifying_receiver.constants import (FEATURE, BATTERY_STATUS)
|
||||
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()
|
||||
try:
|
||||
m = __import__(shortname, globals(), level=1)
|
||||
_DEVICE_MODULES[device_name] = m
|
||||
except:
|
||||
# logging.exception(shortname)
|
||||
_DEVICE_MODULES[device_name] = None
|
||||
|
||||
return _DEVICE_MODULES[device_name]
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
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}
|
||||
|
||||
reply = _api.ping(devinfo.handle, devinfo.number)
|
||||
return STATUS.CONNECTED if reply else STATUS.UNAVAILABLE
|
||||
|
||||
|
||||
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)
|
||||
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}
|
||||
# ?
|
||||
elif feature == FEATURE.REPROGRAMMABLE_KEYS:
|
||||
if feature_function == 0:
|
||||
logging.debug('reprogrammable key: %s', repr(data))
|
||||
# TODO
|
||||
pass
|
||||
# ?
|
||||
elif feature == FEATURE.WIRELESS:
|
||||
if feature_function == 0:
|
||||
logging.debug("wireless status: %s", repr(data))
|
||||
if data[2:5] == b'\x01\x01\x01':
|
||||
return STATUS.CONNECTED
|
||||
# TODO
|
||||
pass
|
||||
# ?
|
||||
|
||||
|
||||
def request_status(devinfo):
|
||||
"""Trigger a status request for a device.
|
||||
|
||||
:param devinfo: the device info tuple.
|
||||
: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)
|
||||
if m and 'request_status' in m.__dict__:
|
||||
return m.request_status(devinfo)
|
||||
return default_request_status(devinfo)
|
||||
|
||||
|
||||
def process_event(devinfo, data):
|
||||
"""Process an event received for a device.
|
||||
|
||||
:param devinfo: the device info tuple.
|
||||
:param data: the event data (event packet sans the first two bytes: reply code and device number)
|
||||
"""
|
||||
default_result = default_process_event(devinfo, data)
|
||||
if default_result is not None:
|
||||
return default_result
|
||||
|
||||
m = _module(devinfo.name)
|
||||
if m and 'process_event' in m.__dict__:
|
||||
return m.process_event(devinfo, data)
|
||||
@@ -1,47 +0,0 @@
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
STATUS = type('STATUS', (),
|
||||
dict(
|
||||
UNKNOWN=-9999,
|
||||
UNPAIRED=-1000,
|
||||
UNAVAILABLE=-1,
|
||||
BOOTING=0,
|
||||
CONNECTED=1,
|
||||
))
|
||||
|
||||
STATUS_NAME = {
|
||||
STATUS.UNKNOWN: '...',
|
||||
STATUS.UNPAIRED: 'unpaired',
|
||||
STATUS.UNAVAILABLE: 'inactive',
|
||||
STATUS.BOOTING: 'initializing',
|
||||
STATUS.CONNECTED: 'connected',
|
||||
}
|
||||
|
||||
|
||||
# device properties that may be reported
|
||||
PROPS = type('PROPS', (),
|
||||
dict(
|
||||
BATTERY_LEVEL='battery_level',
|
||||
BATTERY_STATUS='battery_status',
|
||||
LIGHT_LEVEL='light_level',
|
||||
))
|
||||
|
||||
# when the receiver reports a device that is not connected
|
||||
# (and thus cannot be queried), guess the name and type
|
||||
# based on this table
|
||||
NAMES = {
|
||||
'M315': ('Wireless Mouse M315', 'mouse'),
|
||||
'M325': ('Wireless Mouse M325', 'mouse'),
|
||||
'M510': ('Wireless Mouse M510', 'mouse'),
|
||||
'M515': ('Couch Mouse M515', 'mouse'),
|
||||
'M525': ('Wireless Mouse M525', 'mouse'),
|
||||
'M570': ('Wireless Trackball M570', 'trackball'),
|
||||
'K270': ('Wireless Keyboard K270', 'keyboard'),
|
||||
'K350': ('Wireless Keyboard K350', 'keyboard'),
|
||||
'K750': ('Wireless Solar Keyboard K750', 'keyboard'),
|
||||
'K800': ('Wireless Illuminated Keyboard K800', 'keyboard'),
|
||||
'T650': ('Wireless Rechargeable Touchpad T650', 'touchpad'),
|
||||
'Performance MX': ('Performance Mouse MX', 'mouse'),
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
#
|
||||
# Functions specific to the K750 solar keyboard.
|
||||
#
|
||||
|
||||
import logging
|
||||
from struct import unpack as _unpack
|
||||
|
||||
from .constants import (STATUS, PROPS)
|
||||
from ..unifying_receiver.constants import FEATURE
|
||||
from ..unifying_receiver import api as _api
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
_CHARGE_LEVELS = (10, 25, 256)
|
||||
def _charge_status(data, hasLux=False):
|
||||
charge, lux = _unpack('!BH', data[2:5])
|
||||
|
||||
for i in range(0, len(_CHARGE_LEVELS)):
|
||||
if charge < _CHARGE_LEVELS[i]:
|
||||
charge_index = i
|
||||
break
|
||||
|
||||
return 0x10 << charge_index, {
|
||||
PROPS.BATTERY_LEVEL: charge,
|
||||
PROPS.LIGHT_LEVEL: lux if hasLux else None,
|
||||
}
|
||||
|
||||
|
||||
def request_status(devinfo):
|
||||
reply = _api.request(devinfo.handle, devinfo.number,
|
||||
feature=FEATURE.SOLAR_CHARGE, function=b'\x03', params=b'\x78\x01',
|
||||
features=devinfo.features)
|
||||
if reply is None:
|
||||
return STATUS.UNAVAILABLE
|
||||
|
||||
|
||||
def process_event(devinfo, data):
|
||||
if data[:2] == b'\x09\x00' and data[7:11] == b'GOOD':
|
||||
# usually sent after the keyboard is turned on or just connected
|
||||
return _charge_status(data)
|
||||
|
||||
if data[:2] == b'\x09\x10' and data[7:11] == b'GOOD':
|
||||
# regular solar charge events
|
||||
return _charge_status(data, True)
|
||||
|
||||
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)
|
||||
@@ -1,73 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
|
||||
def print_receiver(receiver):
|
||||
print (str(receiver))
|
||||
|
||||
print (" Serial : %s" % receiver.serial)
|
||||
for f in receiver.firmware:
|
||||
print (" %-10s: %s" % (f.kind, f.version))
|
||||
|
||||
|
||||
def scan_devices(receiver):
|
||||
for dev in receiver:
|
||||
print ("--------")
|
||||
print (str(dev))
|
||||
print ("Name : %s" % dev.name)
|
||||
print ("Kind : %s" % dev.kind)
|
||||
print ("Serial number: %s" % dev.serial)
|
||||
if not dev.protocol:
|
||||
print ("HID protocol : UNKNOWN")
|
||||
continue
|
||||
|
||||
print ("HID protocol : HID %01.1f" % dev.protocol)
|
||||
if dev.protocol < 2.0:
|
||||
print ("Features query not supported by this device")
|
||||
continue
|
||||
|
||||
firmware = dev.firmware
|
||||
for fw in firmware:
|
||||
print (" %-10s: %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)):
|
||||
feature = all_features[index]
|
||||
if feature:
|
||||
print (" ~ Feature %-20s (%s) at index %02X" % (FEATURE_NAME[feature], api._hex(feature), index))
|
||||
|
||||
if FEATURE.BATTERY in all_features:
|
||||
discharge, dischargeNext, status = api.get_device_battery_level(dev.handle, dev.number, features=all_features)
|
||||
print (" Battery %d charged (next level %d%), status %s" % (discharge, dischargeNext, status))
|
||||
|
||||
if FEATURE.REPROGRAMMABLE_KEYS in all_features:
|
||||
keys = api.get_device_keys(dev.handle, dev.number, features=all_features)
|
||||
if keys is not None and keys:
|
||||
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 ("--------")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import argparse
|
||||
arg_parser = argparse.ArgumentParser(prog='scan')
|
||||
arg_parser.add_argument('-v', '--verbose', action='store_true', default=False,
|
||||
help='log the HID data traffic')
|
||||
args = arg_parser.parse_args()
|
||||
|
||||
import logging
|
||||
logging.basicConfig(level=logging.DEBUG if args.verbose else logging.WARNING)
|
||||
|
||||
from .unifying_receiver import api
|
||||
from .unifying_receiver.constants import *
|
||||
|
||||
receiver = api.Receiver.open()
|
||||
if receiver is None:
|
||||
print ("!! Logitech Unifying Receiver not found.")
|
||||
else:
|
||||
print ("!! Found Logitech Unifying Receiver: %s" % receiver)
|
||||
print_receiver(receiver)
|
||||
scan_devices(receiver)
|
||||
receiver.close()
|
||||
@@ -6,30 +6,29 @@ implementation.
|
||||
|
||||
Incomplete. Based on a bit of documentation, trial-and-error, and guesswork.
|
||||
|
||||
Strongly recommended to use these functions from a single thread; calling
|
||||
multiple functions from different threads has a high chance of mixing the
|
||||
replies and causing apparent failures.
|
||||
|
||||
Basic order of operations is:
|
||||
- open() to obtain a UR handle
|
||||
- request() to make a feature call to one of the devices attached to the UR
|
||||
- close() to close the UR handle
|
||||
|
||||
References:
|
||||
http://julien.danjou.info/blog/2012/logitech-k750-linux-support
|
||||
http://6xq.net/git/lars/lshidpp.git/plain/doc/
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
import logging
|
||||
|
||||
if logging.root.level > logging.DEBUG:
|
||||
log = logging.getLogger('LUR')
|
||||
log.addHandler(logging.NullHandler())
|
||||
log.propagate = 0
|
||||
_DEBUG = logging.DEBUG
|
||||
_log = logging.getLogger('LUR')
|
||||
_log.setLevel(logging.root.level)
|
||||
# if logging.root.level > logging.DEBUG:
|
||||
# _log.addHandler(logging.NullHandler())
|
||||
# _log.propagate = 0
|
||||
|
||||
del logging
|
||||
|
||||
|
||||
from .constants import *
|
||||
from .exceptions import *
|
||||
from .api import *
|
||||
from .common import strhex
|
||||
from .base import NoReceiver, NoSuchDevice, DeviceUnreachable
|
||||
from .receiver import Receiver, PairedDevice, MAX_PAIRED_DEVICES
|
||||
from .hidpp20 import FeatureNotSupported, FeatureCallError
|
||||
|
||||
from . import listener
|
||||
from . import status
|
||||
|
||||
@@ -1,501 +0,0 @@
|
||||
#
|
||||
# Logitech Unifying Receiver API.
|
||||
#
|
||||
|
||||
from struct import pack as _pack
|
||||
from struct import unpack as _unpack
|
||||
import errno as _errno
|
||||
|
||||
|
||||
from . import base as _base
|
||||
from .common import (FirmwareInfo as _FirmwareInfo,
|
||||
ReprogrammableKeyInfo as _ReprogrammableKeyInfo)
|
||||
from .constants import (FEATURE, FEATURE_NAME, FEATURE_FLAGS,
|
||||
FIRMWARE_KIND, DEVICE_KIND,
|
||||
BATTERY_STATUS, KEY_NAME,
|
||||
MAX_ATTACHED_DEVICES)
|
||||
from .exceptions import FeatureNotSupported as _FeatureNotSupported
|
||||
|
||||
|
||||
_hex = _base._hex
|
||||
|
||||
from logging import getLogger
|
||||
_log = getLogger('LUR').getChild('api')
|
||||
del getLogger
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
class PairedDevice(object):
|
||||
def __init__(self, handle, number):
|
||||
self.handle = handle
|
||||
self.number = number
|
||||
|
||||
self._protocol = None
|
||||
self._features = None
|
||||
self._codename = None
|
||||
self._name = None
|
||||
self._kind = None
|
||||
self._serial = None
|
||||
self._firmware = 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
|
||||
|
||||
@property
|
||||
def features(self):
|
||||
if self._features is None:
|
||||
if self.protocol >= 2.0:
|
||||
self._features = [FEATURE.ROOT]
|
||||
return self._features
|
||||
|
||||
@property
|
||||
def codename(self):
|
||||
if self._codename is None:
|
||||
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 '?'
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
if self._name is None:
|
||||
if self.protocol < 2.0:
|
||||
from ..devices.constants import NAMES as _DEVICE_NAMES
|
||||
if self.codename in _DEVICE_NAMES:
|
||||
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
|
||||
|
||||
@property
|
||||
def kind(self):
|
||||
if self._kind is None:
|
||||
if self.protocol < 2.0:
|
||||
from ..devices.constants import NAMES as _DEVICE_NAMES
|
||||
if self.codename in _DEVICE_NAMES:
|
||||
self._name, self._kind = _DEVICE_NAMES[self._codename]
|
||||
else:
|
||||
self._kind = get_device_kind(self.handle, self.number, self.features)
|
||||
return self._kind or '?'
|
||||
|
||||
@property
|
||||
def firmware(self):
|
||||
if self._firmware is None and self.protocol >= 2.0:
|
||||
self._firmware = get_device_firmware(self.handle, self.number, self.features)
|
||||
return self._firmware or ()
|
||||
|
||||
@property
|
||||
def serial(self):
|
||||
if self._serial is None:
|
||||
prefix = _base.request(self.handle, 0xFF, b'\x83\xB5', 0x20 + self.number - 1)
|
||||
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])
|
||||
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
|
||||
|
||||
|
||||
class Receiver(object):
|
||||
name = 'Unifying Receiver'
|
||||
max_devices = MAX_ATTACHED_DEVICES
|
||||
|
||||
def __init__(self, handle, path=None):
|
||||
self.handle = handle
|
||||
self.path = path
|
||||
|
||||
self._serial = None
|
||||
self._firmware = None
|
||||
|
||||
def close(self):
|
||||
handle, self.handle = self.handle, 0
|
||||
return (handle and _base.close(handle))
|
||||
|
||||
@property
|
||||
def serial(self):
|
||||
if self._serial is None and self.handle:
|
||||
serial = _base.request(self.handle, 0xFF, b'\x83\xB5', b'\x03')
|
||||
if serial:
|
||||
self._serial = _hex(serial[1:5])
|
||||
return self._serial
|
||||
|
||||
@property
|
||||
def firmware(self):
|
||||
if self._firmware is None and self.handle:
|
||||
firmware = []
|
||||
|
||||
reply = _base.request(self.handle, 0xFF, b'\x83\xB5', b'\x02')
|
||||
if reply and reply[0:1] == b'\x02':
|
||||
fw_version = _hex(reply[1:5])
|
||||
fw_version = '%s.%s.B%s' % (fw_version[0:2], fw_version[2:4], fw_version[4:8])
|
||||
firmware.append(_FirmwareInfo(0, FIRMWARE_KIND[0], '', fw_version, None))
|
||||
|
||||
reply = _base.request(self.handle, 0xFF, b'\x81\xF1', b'\x04')
|
||||
if reply and reply[0:1] == b'\x04':
|
||||
bl_version = _hex(reply[1:3])
|
||||
bl_version = '%s.%s' % (bl_version[0:2], bl_version[2:4])
|
||||
firmware.append(_FirmwareInfo(1, FIRMWARE_KIND[1], '', bl_version, None))
|
||||
|
||||
self._firmware = tuple(firmware)
|
||||
|
||||
return self._firmware
|
||||
|
||||
def __iter__(self):
|
||||
if self.handle == 0:
|
||||
return
|
||||
|
||||
for number in range(1, 1 + MAX_ATTACHED_DEVICES):
|
||||
dev = get_device(self.handle, number)
|
||||
if dev is not None:
|
||||
yield dev
|
||||
|
||||
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:
|
||||
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:
|
||||
raise IndexError(key)
|
||||
if key > 0:
|
||||
_log.debug("unpairing device %d", key)
|
||||
reply = _base.request(self.handle, 0xFF, b'\x80\xB2', _pack('!BB', 0x03, key))
|
||||
if reply is None or reply[1:2] == b'\x8F':
|
||||
raise IndexError(key)
|
||||
|
||||
def __len__(self):
|
||||
if self.handle == 0:
|
||||
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):
|
||||
if self.handle == 0:
|
||||
return False
|
||||
if type(dev) == int:
|
||||
return dev > 0 and dev <= MAX_ATTACHED_DEVICES and _base.ping(self.handle, dev) is not None
|
||||
return dev.ping()
|
||||
|
||||
def __str__(self):
|
||||
return '<Receiver(%X,%s)>' % (self.handle, self.path)
|
||||
|
||||
def __hash__(self):
|
||||
return self.handle
|
||||
|
||||
__bool__ = __nonzero__ = lambda self: self.handle != 0
|
||||
|
||||
@classmethod
|
||||
def open(self):
|
||||
"""Opens the first Logitech Unifying Receiver found attached to the machine.
|
||||
|
||||
:returns: An open file handle for the found receiver, or ``None``.
|
||||
"""
|
||||
exception = None
|
||||
|
||||
for rawdevice in _base.list_receiver_devices():
|
||||
exception = None
|
||||
try:
|
||||
handle = _base.try_open(rawdevice.path)
|
||||
if handle:
|
||||
return Receiver(handle, rawdevice.path)
|
||||
except OSError as e:
|
||||
_log.exception("open %s", rawdevice.path)
|
||||
if e.errno == _errno.EACCES:
|
||||
exception = e
|
||||
|
||||
if exception:
|
||||
# only keep the last exception
|
||||
raise exception
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
def request(handle, devnumber, feature, function=b'\x00', 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
|
||||
matching the called feature is received. In theory the UR will always reply
|
||||
to feature call; otherwise this function will wait indefinitely.
|
||||
|
||||
Incoming data packets not matching the feature and function will be
|
||||
delivered to the unhandled hook (if any), and ignored.
|
||||
|
||||
:param function: the function to call on that feature, may be an byte value
|
||||
or a bytes string of length 1.
|
||||
:param params: optional bytes string to send as function parameters to the
|
||||
feature; may also be an integer if the function only takes a single byte as
|
||||
parameter.
|
||||
|
||||
The optional ``features`` parameter is a cached result of the
|
||||
get_device_features function for this device, necessary to find the feature
|
||||
index. If the ``features_arrary`` is not provided, one will be obtained by
|
||||
manually calling get_device_features before making the request call proper.
|
||||
|
||||
:raises FeatureNotSupported: if the device does not support the feature.
|
||||
"""
|
||||
feature_index = None
|
||||
if feature == FEATURE.ROOT:
|
||||
feature_index = b'\x00'
|
||||
else:
|
||||
feature_index = _get_feature_index(handle, devnumber, feature, features)
|
||||
if feature_index is None:
|
||||
# i/o read error
|
||||
return None
|
||||
|
||||
feature_index = _pack('!B', feature_index)
|
||||
|
||||
if type(function) == int:
|
||||
function = _pack('!B', function)
|
||||
if type(params) == int:
|
||||
params = _pack('!B', params)
|
||||
|
||||
return _base.request(handle, devnumber, feature_index + function, params)
|
||||
|
||||
|
||||
def get_device(handle, devnumber, features=None):
|
||||
"""Gets the complete info for a device (type, features).
|
||||
|
||||
:returns: a PairedDevice or ``None``.
|
||||
"""
|
||||
if _base.ping(handle, devnumber):
|
||||
devinfo = PairedDevice(handle, devnumber)
|
||||
# _log.debug("found device %s", devinfo)
|
||||
return devinfo
|
||||
|
||||
|
||||
def get_feature_index(handle, devnumber, feature):
|
||||
"""Reads the index of a device's feature.
|
||||
|
||||
:returns: An int, or ``None`` if the feature is not available.
|
||||
"""
|
||||
# _log.debug("device %d get feature index <%s:%s>", devnumber, _hex(feature), FEATURE_NAME[feature])
|
||||
if len(feature) != 2:
|
||||
raise ValueError("invalid feature <%s>: it must be a two-byte string" % feature)
|
||||
|
||||
# FEATURE.ROOT should always be available for any attached devices
|
||||
reply = _base.request(handle, devnumber, FEATURE.ROOT, 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)
|
||||
|
||||
# only consider active and supported features?
|
||||
# if feature_flags:
|
||||
# raise E.FeatureNotSupported(devnumber, feature)
|
||||
|
||||
return feature_index
|
||||
|
||||
_log.warn("device %d feature <%s:%s> not supported by the device", devnumber, _hex(feature), FEATURE_NAME[feature])
|
||||
raise _FeatureNotSupported(devnumber, feature)
|
||||
|
||||
|
||||
def _get_feature_index(handle, devnumber, feature, features=None):
|
||||
if features is None:
|
||||
return get_feature_index(handle, devnumber, feature)
|
||||
|
||||
if feature in features:
|
||||
return features.index(feature)
|
||||
|
||||
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
|
||||
# _log.debug("%s: found feature %s at %d", features, _base._hex(feature), index)
|
||||
return index
|
||||
|
||||
|
||||
def get_device_features(handle, devnumber):
|
||||
"""Returns an array of feature ids.
|
||||
|
||||
Their position in the array is the index to be used when requesting that
|
||||
feature on the device.
|
||||
"""
|
||||
# _log.debug("device %d get device features", devnumber)
|
||||
|
||||
# get the index of the FEATURE_SET
|
||||
# FEATURE.ROOT should always be available for all devices
|
||||
fs_index = _base.request(handle, devnumber, FEATURE.ROOT, FEATURE.FEATURE_SET)
|
||||
if fs_index is None:
|
||||
_log.warn("device %d FEATURE_SET not available", devnumber)
|
||||
return None
|
||||
fs_index = fs_index[:1]
|
||||
|
||||
# For debugging purposes, query all the available features on the device,
|
||||
# even if unknown.
|
||||
|
||||
# get the number of active features the device has
|
||||
features_count = _base.request(handle, devnumber, fs_index + b'\x00')
|
||||
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)
|
||||
_log.debug("device %d no features available?!", devnumber)
|
||||
return None
|
||||
|
||||
features_count = ord(features_count[:1])
|
||||
# _log.debug("device %d found %d features", devnumber, features_count)
|
||||
|
||||
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))
|
||||
if feature:
|
||||
# feature_flags = ord(feature[2:3]) & 0xE0
|
||||
feature = feature[0:2].upper()
|
||||
features[index] = feature
|
||||
|
||||
# if feature_flags:
|
||||
# _log.debug("device %d feature <%s:%s> at index %d: %s",
|
||||
# devnumber, _hex(feature), FEATURE_NAME[feature], index,
|
||||
# ','.join([FEATURE_FLAGS[k] for k in FEATURE_FLAGS if feature_flags & k]))
|
||||
# else:
|
||||
# _log.debug("device %d feature <%s:%s> at index %d", devnumber, _hex(feature), FEATURE_NAME[feature], index)
|
||||
|
||||
features[0] = FEATURE.ROOT
|
||||
while features[-1] is None:
|
||||
del features[-1]
|
||||
return tuple(features)
|
||||
|
||||
|
||||
def get_device_firmware(handle, devnumber, features=None):
|
||||
"""Reads a device's firmware info.
|
||||
|
||||
:returns: a list of FirmwareInfo tuples, ordered by firmware layer.
|
||||
"""
|
||||
fw_fi = _get_feature_index(handle, devnumber, FEATURE.FIRMWARE, features)
|
||||
if fw_fi is None:
|
||||
return None
|
||||
|
||||
fw_count = _base.request(handle, devnumber, _pack('!BB', fw_fi, 0x00))
|
||||
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)
|
||||
if fw_info:
|
||||
level = ord(fw_info[:1]) & 0x0F
|
||||
if level == 0 or level == 1:
|
||||
kind = FIRMWARE_KIND[level]
|
||||
name, = _unpack('!3s', fw_info[1:4])
|
||||
name = name.decode('ascii')
|
||||
version = _hex(fw_info[4:6])
|
||||
version = '%s.%s' % (version[0:2], version[2:4])
|
||||
build, = _unpack('!H', fw_info[6:8])
|
||||
if build:
|
||||
version += ' b%d' % build
|
||||
extras = fw_info[9:].rstrip(b'\x00') or None
|
||||
fw_info = _FirmwareInfo(level, kind, name, version, extras)
|
||||
elif level == 2:
|
||||
fw_info = _FirmwareInfo(2, FIRMWARE_KIND[2], '', ord(fw_info[1:2]), None)
|
||||
else:
|
||||
fw_info = _FirmwareInfo(level, FIRMWARE_KIND[-1], '', '', None)
|
||||
|
||||
fw.append(fw_info)
|
||||
# _log.debug("device %d firmware %s", devnumber, fw_info)
|
||||
return tuple(fw)
|
||||
|
||||
|
||||
def get_device_kind(handle, devnumber, features=None):
|
||||
"""Reads a device's type.
|
||||
|
||||
:see DEVICE_KIND:
|
||||
:returns: a string describing the device type, or ``None`` if the device is
|
||||
not available or does not support the ``NAME`` feature.
|
||||
"""
|
||||
name_fi = _get_feature_index(handle, devnumber, FEATURE.NAME, features)
|
||||
if name_fi is None:
|
||||
return None
|
||||
|
||||
d_kind = _base.request(handle, devnumber, _pack('!BB', name_fi, 0x20))
|
||||
if d_kind:
|
||||
d_kind = ord(d_kind[:1])
|
||||
# _log.debug("device %d type %d = %s", devnumber, d_kind, DEVICE_KIND[d_kind])
|
||||
return DEVICE_KIND[d_kind]
|
||||
|
||||
|
||||
def get_device_name(handle, devnumber, features=None):
|
||||
"""Reads a device's name.
|
||||
|
||||
:returns: a string with the device name, or ``None`` if the device is not
|
||||
available or does not support the ``NAME`` feature.
|
||||
"""
|
||||
name_fi = _get_feature_index(handle, devnumber, FEATURE.NAME, features)
|
||||
if name_fi is None:
|
||||
return None
|
||||
|
||||
name_length = _base.request(handle, devnumber, _pack('!BB', name_fi, 0x00))
|
||||
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))
|
||||
if name_fragment:
|
||||
name_fragment = name_fragment[:name_length - len(d_name)]
|
||||
d_name += name_fragment
|
||||
else:
|
||||
break
|
||||
|
||||
d_name = d_name.decode('ascii')
|
||||
# _log.debug("device %d name %s", devnumber, d_name)
|
||||
return d_name
|
||||
|
||||
|
||||
def get_device_battery_level(handle, devnumber, features=None):
|
||||
"""Reads a device's battery level.
|
||||
|
||||
:raises FeatureNotSupported: if the device does not support this feature.
|
||||
"""
|
||||
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))
|
||||
if battery:
|
||||
discharge, dischargeNext, status = _unpack('!BBB', battery[:3])
|
||||
_log.debug("device %d battery %d%% charged, next level %d%% charge, status %d = %s",
|
||||
devnumber, discharge, dischargeNext, status, BATTERY_STATUS[status])
|
||||
return (discharge, dischargeNext, BATTERY_STATUS[status])
|
||||
|
||||
|
||||
def get_device_keys(handle, devnumber, features=None):
|
||||
rk_fi = _get_feature_index(handle, devnumber, FEATURE.REPROGRAMMABLE_KEYS, features)
|
||||
if rk_fi is None:
|
||||
return None
|
||||
|
||||
count = _base.request(handle, devnumber, _pack('!BB', rk_fi, 0))
|
||||
if count:
|
||||
keys = []
|
||||
|
||||
count = ord(count[:1])
|
||||
for index in range(0, count):
|
||||
keydata = _base.request(handle, devnumber, _pack('!BB', rk_fi, 0x10), 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)
|
||||
keys.append(rki)
|
||||
|
||||
return keys
|
||||
@@ -3,87 +3,77 @@
|
||||
# Unlikely to be used directly unless you're expanding the API.
|
||||
#
|
||||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
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()
|
||||
from random import getrandbits as _random_bits
|
||||
|
||||
from .constants import ERROR_NAME
|
||||
from .exceptions import (NoReceiver as _NoReceiver,
|
||||
FeatureCallError as _FeatureCallError)
|
||||
|
||||
from logging import getLogger
|
||||
_log = getLogger('LUR').getChild('base')
|
||||
from logging import getLogger, DEBUG as _DEBUG
|
||||
_log = getLogger('LUR.base')
|
||||
del getLogger
|
||||
|
||||
from .common import strhex as _strhex, KwException as _KwException
|
||||
from . import hidpp10 as _hidpp10
|
||||
from . import hidpp20 as _hidpp20
|
||||
import hidapi as _hid
|
||||
|
||||
|
||||
#
|
||||
# These values are defined by the Logitech documentation.
|
||||
# Overstepping these boundaries will only produce log warnings.
|
||||
#
|
||||
#
|
||||
|
||||
"""Minimim lenght of a feature call packet."""
|
||||
_MIN_CALL_SIZE = 7
|
||||
|
||||
|
||||
"""Maximum lenght of a feature call packet."""
|
||||
_MAX_CALL_SIZE = 20
|
||||
|
||||
|
||||
"""Minimum size of a feature reply packet."""
|
||||
_MIN_REPLY_SIZE = _MIN_CALL_SIZE
|
||||
|
||||
|
||||
"""Maximum size of a feature reply packet."""
|
||||
_MAX_REPLY_SIZE = _MAX_CALL_SIZE
|
||||
|
||||
_SHORT_MESSAGE_SIZE = 7
|
||||
_LONG_MESSAGE_SIZE = 20
|
||||
_MEDIUM_MESSAGE_SIZE = 15
|
||||
_MAX_READ_SIZE = 32
|
||||
|
||||
"""Default timeout on read (in ms)."""
|
||||
DEFAULT_TIMEOUT = 1500
|
||||
DEFAULT_TIMEOUT = 3000
|
||||
_RECEIVER_REQUEST_TIMEOUT = 500
|
||||
_DEVICE_REQUEST_TIMEOUT = DEFAULT_TIMEOUT
|
||||
_PING_TIMEOUT = 5000
|
||||
|
||||
#
|
||||
# Exceptions that may be raised by this API.
|
||||
#
|
||||
|
||||
class NoReceiver(_KwException):
|
||||
"""Raised when trying to talk through a previously open handle, when the
|
||||
receiver is no longer available. Should only happen if the receiver is
|
||||
physically disconnected from the machine, or its kernel driver module is
|
||||
unloaded."""
|
||||
pass
|
||||
|
||||
|
||||
class NoSuchDevice(_KwException):
|
||||
"""Raised when trying to reach a device number not paired to the receiver."""
|
||||
pass
|
||||
|
||||
|
||||
class DeviceUnreachable(_KwException):
|
||||
"""Raised when a request is made to an unreachable (turned off) device."""
|
||||
pass
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
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
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
def list_receiver_devices():
|
||||
def receivers():
|
||||
"""List all the Linux devices exposed by the UR attached to the machine."""
|
||||
# (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
|
||||
|
||||
# apparently there are TWO product ids possible for the UR
|
||||
for d in _hid.enumerate(0x046d, 0xc532, 2):
|
||||
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 +86,7 @@ def try_open(path):
|
||||
:returns: an open receiver handle if this is the right Linux device, or
|
||||
``None``.
|
||||
"""
|
||||
receiver_handle = _hid.open_path(path)
|
||||
if receiver_handle is None:
|
||||
# could be a file permissions issue (did you add the udev rules?)
|
||||
# in any case, unreachable
|
||||
_log.debug("[%s] open failed", path)
|
||||
return None
|
||||
|
||||
_hid.write(receiver_handle, _COUNT_DEVICES_REQUEST)
|
||||
|
||||
# if this is the right hidraw device, we'll receive a 'bad device' from the UR
|
||||
# otherwise, the read should produce nothing
|
||||
reply = _hid.read(receiver_handle, _MAX_REPLY_SIZE, DEFAULT_TIMEOUT / 2)
|
||||
if reply:
|
||||
if reply[:5] == _COUNT_DEVICES_REQUEST[:5]:
|
||||
# 'device 0 unreachable' is the expected reply from a valid receiver handle
|
||||
_log.info("[%s] success: handle %X", path, receiver_handle)
|
||||
return receiver_handle
|
||||
_log.debug("[%s] %X ignored reply %s", path, receiver_handle, _hex(reply))
|
||||
else:
|
||||
_log.debug("[%s] %X no reply", path, receiver_handle)
|
||||
|
||||
close(receiver_handle)
|
||||
return _hid.open_path(path)
|
||||
|
||||
|
||||
def open():
|
||||
@@ -125,9 +94,8 @@ 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)
|
||||
for rawdevice in receivers():
|
||||
handle = open_path(rawdevice.path)
|
||||
if handle:
|
||||
return handle
|
||||
|
||||
@@ -136,17 +104,21 @@ 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
|
||||
|
||||
|
||||
def write(handle, devnumber, data):
|
||||
"""Writes some data to a certain device.
|
||||
"""Writes some data to the receiver, addressed to a certain device.
|
||||
|
||||
:param handle: an open UR handle.
|
||||
:param devnumber: attached device number.
|
||||
@@ -159,61 +131,122 @@ def write(handle, devnumber, data):
|
||||
been physically removed from the machine, or the kernel driver has been
|
||||
unloaded. The handle will be closed automatically.
|
||||
"""
|
||||
assert _MIN_CALL_SIZE == 7
|
||||
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)
|
||||
if len(data) > _SHORT_MESSAGE_SIZE - 2 or data[:1] == b'\x82':
|
||||
wdata = _pack(b'!BB18s', 0x11, devnumber, data)
|
||||
else:
|
||||
wdata = _pack(b'!BB5s', 0x10, devnumber, data)
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("(%s) <= w[%02X %02X %s %s]", handle, ord(wdata[:1]), devnumber, _strhex(wdata[2:4]), _strhex(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=reason)
|
||||
|
||||
|
||||
def read(handle, timeout=DEFAULT_TIMEOUT):
|
||||
"""Read some data from the receiver. Usually called after a write (feature
|
||||
call), to get the reply.
|
||||
|
||||
:param handle: an open UR handle.
|
||||
:param timeout: read timeout on the UR handle.
|
||||
|
||||
If any data was read in the given timeout, returns a tuple of
|
||||
(reply_code, devnumber, message data). The reply code is generally ``0x11``
|
||||
for a successful feature call, or ``0x10`` to indicate some error, e.g. the
|
||||
device is no longer available.
|
||||
(code, devnumber, message data).
|
||||
|
||||
:raises NoReceiver: if the receiver is no longer available, i.e. has
|
||||
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)
|
||||
reply = _read(handle, timeout)
|
||||
if reply:
|
||||
return reply[1:]
|
||||
|
||||
|
||||
def _read(handle, timeout):
|
||||
try:
|
||||
data = _hid.read(int(handle), _MAX_READ_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=reason)
|
||||
|
||||
if data:
|
||||
if len(data) < _MIN_REPLY_SIZE:
|
||||
_log.warn("=> r[%s] read packet too short: %d bytes", _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))
|
||||
code = ord(data[:1])
|
||||
report_id = ord(data[:1])
|
||||
assert (report_id == 0x10 and len(data) == _SHORT_MESSAGE_SIZE or
|
||||
report_id == 0x11 and len(data) == _LONG_MESSAGE_SIZE or
|
||||
report_id == 0x20 and len(data) == _MEDIUM_MESSAGE_SIZE)
|
||||
devnumber = ord(data[1:2])
|
||||
_log.debug("=> r[%02X %02X %s %s]", code, devnumber, _hex(data[2:4]), _hex(data[4:]))
|
||||
return code, devnumber, data[2:]
|
||||
|
||||
# _l.log(_LOG_LEVEL, "(-) => r[]")
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("(%s) => r[%02X %02X %s %s]", handle, report_id, devnumber, _strhex(data[2:4]), _strhex(data[4:]))
|
||||
|
||||
return report_id, devnumber, data[2:]
|
||||
|
||||
|
||||
def _skip_incoming(handle):
|
||||
"""Read anything already in the input buffer."""
|
||||
ihandle = int(handle)
|
||||
|
||||
while True:
|
||||
try:
|
||||
data = _hid.read(ihandle, _MAX_READ_SIZE, 0)
|
||||
except Exception as reason:
|
||||
_log.error("read failed, assuming receiver %s no longer available", handle)
|
||||
close(handle)
|
||||
raise NoReceiver(reason=reason)
|
||||
|
||||
if data:
|
||||
report_id = ord(data[:1])
|
||||
assert (report_id == 0x10 and len(data) == _SHORT_MESSAGE_SIZE or
|
||||
report_id == 0x11 and len(data) == _LONG_MESSAGE_SIZE or
|
||||
report_id == 0x20 and len(data) == _MEDIUM_MESSAGE_SIZE)
|
||||
_unhandled(report_id, ord(data[1:2]), data[2:])
|
||||
else:
|
||||
return
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
"""The function that may be called on incoming events.
|
||||
|
||||
The hook must be a callable accepting one tuple parameter, with the format
|
||||
``(<int> devnumber, <bytes[2]> request_id, <bytes> data)``.
|
||||
|
||||
This hook will only be called by the request()/ping() functions, when received
|
||||
replies do not match the expected request_id. 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.
|
||||
"""
|
||||
events_hook = None
|
||||
|
||||
def _unhandled(report_id, devnumber, data):
|
||||
"""Deliver a possible event to the unhandled_hook (if any)."""
|
||||
if events_hook:
|
||||
event = make_event(devnumber, data)
|
||||
if event:
|
||||
events_hook(event)
|
||||
|
||||
|
||||
_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)
|
||||
_Event = namedtuple('_Event', ['devnumber', 'sub_id', 'address', 'data'])
|
||||
_Event.__str__ = lambda self: 'Event(%d,%02X,%02X,%s)' % (self.devnumber, self.sub_id, self.address, _strhex(self.data))
|
||||
_Event.__unicode__ = _Event.__str__
|
||||
del namedtuple
|
||||
|
||||
def request(handle, devnumber, feature_index_function, params=b'', features=None):
|
||||
def make_event(devnumber, data):
|
||||
sub_id = ord(data[:1])
|
||||
if devnumber == 0xFF:
|
||||
if sub_id == 0x4A: # receiver lock event
|
||||
return _Event(devnumber, sub_id, ord(data[1:2]), data[2:])
|
||||
elif sub_id & 0x80 != 0x80:
|
||||
address = ord(data[1:2])
|
||||
if sub_id >= 0x40 or address & 0x01 == 0:
|
||||
return _Event(devnumber, sub_id, address, data[2:])
|
||||
|
||||
|
||||
def request(handle, devnumber, request_id, *params):
|
||||
"""Makes a feature call to a device and waits for a matching reply.
|
||||
|
||||
This function will skip all incoming messages and events not related to the
|
||||
@@ -222,83 +255,86 @@ def request(handle, devnumber, feature_index_function, params=b'', features=None
|
||||
|
||||
:param handle: an open UR handle.
|
||||
:param devnumber: attached device number.
|
||||
:param feature_index_function: a two-byte string of (feature_index, feature_function).
|
||||
:param request_id: a 16-bit integer.
|
||||
:param params: parameters for the feature call, 3 to 16 bytes.
|
||||
:param features: optional features array for the device, only used to fill
|
||||
the FeatureCallError exception if one occurs.
|
||||
:returns: the reply data packet, or ``None`` if the device is no longer
|
||||
available.
|
||||
:raisees FeatureCallError: if the feature call replied with an error.
|
||||
:returns: the reply data, or ``None`` if some error occured.
|
||||
"""
|
||||
if type(params) == int:
|
||||
params = _pack('!B', params)
|
||||
|
||||
# _log.debug("device %d request {%s} params [%s]", 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
|
||||
assert type(request_id) == int
|
||||
if devnumber != 0xFF and request_id < 0x8000:
|
||||
timeout = _DEVICE_REQUEST_TIMEOUT
|
||||
# for HID++ 2.0 feature request, randomize the swid to make it easier to
|
||||
# recognize the reply for this request. also, always set the last bit
|
||||
# (0) in swid, to make events easier to identify
|
||||
request_id = (request_id & 0xFFF0) | _random_bits(4) | 0x01
|
||||
else:
|
||||
context = request_context
|
||||
_unhandled = getattr(context, 'unhandled_hook')
|
||||
timeout = _RECEIVER_REQUEST_TIMEOUT
|
||||
request_str = _pack(b'!H', request_id)
|
||||
|
||||
context.write(handle, devnumber, feature_index_function + params)
|
||||
params = b''.join(_pack(b'B', p) if type(p) == int else p for p in params)
|
||||
# if _log.isEnabledFor(_DEBUG):
|
||||
# _log.debug("(%s) device %d request_id {%04X} params [%s]", handle, devnumber, request_id, _strhex(params))
|
||||
|
||||
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, request_str + params)
|
||||
|
||||
if not reply:
|
||||
# keep waiting...
|
||||
continue
|
||||
while True:
|
||||
now = _timestamp()
|
||||
reply = _read(handle, timeout)
|
||||
delta = _timestamp() - now
|
||||
|
||||
reply_code, reply_devnumber, reply_data = reply
|
||||
if reply:
|
||||
report_id, reply_devnumber, reply_data = reply
|
||||
if reply_devnumber == devnumber:
|
||||
if report_id == 0x10 and reply_data[:1] == b'\x8F' and reply_data[1:3] == request_str:
|
||||
error = ord(reply_data[3:4])
|
||||
|
||||
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 error == _hidpp10.ERROR.resource_error: # device unreachable
|
||||
# _log.warn("(%s) device %d error on request {%04X}: unknown device", handle, devnumber, request_id)
|
||||
# raise DeviceUnreachable(number=devnumber, request=request_id)
|
||||
|
||||
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 error == _hidpp10.ERROR.unknown_device: # unknown device
|
||||
# _log.error("(%s) device %d error on request {%04X}: unknown device", handle, devnumber, request_id)
|
||||
# raise NoSuchDevice(number=devnumber, request=request_id)
|
||||
|
||||
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
|
||||
_log.debug("(%s) device %d error on request {%04X}: %d = %s",
|
||||
handle, devnumber, request_id, error, _hidpp10.ERROR[error])
|
||||
break
|
||||
|
||||
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_data[:1] == b'\xFF' and reply_data[1:3] == request_str:
|
||||
# a HID++ 2.0 feature call returned with an error
|
||||
error = ord(reply_data[3:4])
|
||||
_log.error("(%s) device %d error on feature request {%04X}: %d = %s",
|
||||
handle, devnumber, request_id, error, _hidpp20.ERROR[error])
|
||||
raise _hidpp20.FeatureCallError(number=devnumber, request=request_id, error=error, params=params)
|
||||
|
||||
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_data[:2] == request_str:
|
||||
if request_id & 0xFF00 == 0x8300:
|
||||
# long registry r/w should return a long reply
|
||||
assert report_id == 0x11
|
||||
elif request_id & 0xF000 == 0x8000:
|
||||
# short registry r/w should return a short reply
|
||||
assert report_id == 0x10
|
||||
|
||||
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 devnumber == 0xFF:
|
||||
if request_id == 0x83B5 or request_id == 0x81F1:
|
||||
# these replies have to match the first parameter as well
|
||||
if reply_data[2:3] == params[:1]:
|
||||
return reply_data[2:]
|
||||
else:
|
||||
# hm, not mathing my request, and certainly not an event
|
||||
continue
|
||||
else:
|
||||
return reply_data[2:]
|
||||
else:
|
||||
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)
|
||||
_unhandled(report_id, reply_devnumber, reply_data)
|
||||
|
||||
if delta >= timeout:
|
||||
_log.warn("timeout on device %d request {%04X} params[%s]", devnumber, request_id, _strhex(params))
|
||||
break
|
||||
# raise DeviceUnreachable(number=devnumber, request=request_id)
|
||||
|
||||
|
||||
def ping(handle, devnumber):
|
||||
@@ -306,43 +342,49 @@ def ping(handle, devnumber):
|
||||
|
||||
: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')
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_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)
|
||||
|
||||
if not reply:
|
||||
# keep waiting...
|
||||
continue
|
||||
# randomize the swid and mark byte to positively identify the ping reply,
|
||||
# and set the last (0) bit in swid to make it easier to distinguish requests
|
||||
# from events
|
||||
request_id = 0x0010 | _random_bits(4) | 0x01
|
||||
request_str = _pack(b'!H', request_id)
|
||||
ping_mark = _pack(b'B', _random_bits(8))
|
||||
write(ihandle, devnumber, request_str + b'\x00\x00' + ping_mark)
|
||||
|
||||
reply_code, reply_devnumber, reply_data = reply
|
||||
while True:
|
||||
now = _timestamp()
|
||||
reply = _read(ihandle, _PING_TIMEOUT)
|
||||
delta = _timestamp() - now
|
||||
|
||||
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:
|
||||
report_id, number, data = reply
|
||||
if number == devnumber:
|
||||
if data[:2] == request_str and data[4:5] == ping_mark:
|
||||
# HID++ 2.0+ device, currently connected
|
||||
return ord(data[2:3]) + ord(data[3:4]) / 10.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 report_id == 0x10 and data[:1] == b'\x8F' and data[1:3] == request_str:
|
||||
assert data[-1:] == b'\x00'
|
||||
error = ord(data[3:4])
|
||||
|
||||
if reply_code == 0x10 and reply_data == b'\x8F\x00\x10\x01\x00':
|
||||
return 1.0
|
||||
if error == _hidpp10.ERROR.invalid_SubID__command: # a valid reply from a HID++ 1.0 device
|
||||
return 1.0
|
||||
|
||||
if reply_code == 0x10 and reply_data[:3] == b'\x8F\x00\x10':
|
||||
return None
|
||||
if error == _hidpp10.ERROR.resource_error: # device unreachable
|
||||
# raise DeviceUnreachable(number=devnumber, request=request_id)
|
||||
break
|
||||
|
||||
_log.warn("don't know how to interpret ping reply %s", reply)
|
||||
if error == _hidpp10.ERROR.unknown_device: # no paired device with that number
|
||||
_log.error("(%s) device %d error on ping request: unknown device", handle, devnumber)
|
||||
raise NoSuchDevice(number=devnumber, request=request_id)
|
||||
|
||||
_unhandled(report_id, number, data)
|
||||
|
||||
if delta >= _PING_TIMEOUT:
|
||||
_log.warn("(%s) timeout on device %d ping", handle, devnumber)
|
||||
# raise DeviceUnreachable(number=devnumber, request=request_id)
|
||||
|
||||
@@ -2,30 +2,153 @@
|
||||
# Some common functions and types.
|
||||
#
|
||||
|
||||
from collections import namedtuple
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
from binascii import hexlify as _hexlify
|
||||
_hex = lambda d: _hexlify(d).decode('ascii').upper()
|
||||
from struct import pack as _pack
|
||||
|
||||
|
||||
class FallbackDict(dict):
|
||||
def __init__(self, fallback_function=lambda x: None, *args, **kwargs):
|
||||
super(FallbackDict, self).__init__(*args, **kwargs)
|
||||
self.fallback = fallback_function
|
||||
class NamedInt(int):
|
||||
"""An reqular Python integer with an attached name.
|
||||
|
||||
def __getitem__(self, key):
|
||||
Careful when using this, because
|
||||
"""
|
||||
|
||||
def __new__(cls, value, name):
|
||||
obj = int.__new__(cls, value)
|
||||
obj.name = str(name)
|
||||
return obj
|
||||
|
||||
def bytes(self, count=2):
|
||||
value = int(self)
|
||||
if value.bit_length() > count * 8:
|
||||
raise ValueError('cannot fit %X into %d bytes' % (value, count))
|
||||
|
||||
return _pack(b'!L', value)[-count:]
|
||||
|
||||
def __hash__(self):
|
||||
return int(self)
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, int):
|
||||
return int(self) == int(other)
|
||||
|
||||
if isinstance(other, basestring):
|
||||
return self.name.lower() == other.lower()
|
||||
|
||||
def __ne__(self, other):
|
||||
if isinstance(other, int):
|
||||
return int(self) != int(other)
|
||||
|
||||
if isinstance(other, basestring):
|
||||
return self.name.lower() != other.lower()
|
||||
|
||||
def __lt__(self, other):
|
||||
if not isinstance(other, int):
|
||||
raise TypeError('unorderable types: %s < %s' % (type(self), type(other)))
|
||||
return int(self) < int(other)
|
||||
|
||||
def __le__(self, other):
|
||||
if not isinstance(other, int):
|
||||
raise TypeError('unorderable types: %s <= %s' % (type(self), type(other)))
|
||||
return int(self) <= int(other)
|
||||
|
||||
def __gt__(self, other):
|
||||
if not isinstance(other, int):
|
||||
raise TypeError('unorderable types: %s > %s' % (type(self), type(other)))
|
||||
return int(self) > int(other)
|
||||
|
||||
def __ge__(self, other):
|
||||
if not isinstance(other, int):
|
||||
raise TypeError('unorderable types: %s >= %s' % (type(self), type(other)))
|
||||
return int(self) >= int(other)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
__unicode__ = __str__
|
||||
|
||||
def __repr__(self):
|
||||
return 'NamedInt(%d, %s)' % (int(self), repr(self.name))
|
||||
|
||||
|
||||
class NamedInts(object):
|
||||
__slots__ = ['__dict__', '_values', '_indexed', '_fallback']
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
values = dict((k, NamedInt(v, k.lstrip('_') if k == k.upper() else
|
||||
k.replace('__', '/').replace('_', ' '))) for (k, v) in kwargs.items())
|
||||
self.__dict__ = values
|
||||
self._values = sorted(list(values.values()))
|
||||
self._indexed = dict((int(v), v) for v in self._values)
|
||||
self._fallback = None
|
||||
|
||||
def flag_names(self, value):
|
||||
unknown_bits = value
|
||||
for k in self._indexed:
|
||||
assert bin(k).count('1') == 1
|
||||
if k & value == k:
|
||||
unknown_bits &= ~k
|
||||
yield str(self._indexed[k])
|
||||
|
||||
if unknown_bits:
|
||||
yield 'unknown:%06X' % unknown_bits
|
||||
|
||||
def index(self, value):
|
||||
if value in self._values:
|
||||
return self._values.index(value)
|
||||
raise IndexError('%s not found' % value)
|
||||
|
||||
def __getitem__(self, index):
|
||||
if isinstance(index, int):
|
||||
if index in self._indexed:
|
||||
return self._indexed[int(index)]
|
||||
|
||||
if self._fallback and type(index) == int:
|
||||
value = NamedInt(index, self._fallback(index))
|
||||
self._indexed[index] = value
|
||||
self._values = sorted(self._values + [value])
|
||||
return value
|
||||
|
||||
elif type(index) == slice:
|
||||
return self._values[index]
|
||||
|
||||
else:
|
||||
if index in self._values:
|
||||
index = self._values.index(index)
|
||||
return self._values[index]
|
||||
|
||||
def __contains__(self, value):
|
||||
return value in self._values
|
||||
|
||||
def __iter__(self):
|
||||
return iter(sorted(self._values))
|
||||
|
||||
def __len__(self):
|
||||
return len(self._values)
|
||||
|
||||
def __repr__(self):
|
||||
return 'NamedInts(%s)' % ', '.join(repr(v) for v in self._values)
|
||||
|
||||
|
||||
def strhex(x):
|
||||
return _hexlify(x).decode('ascii').upper()
|
||||
|
||||
|
||||
class KwException(Exception):
|
||||
def __init__(self, **kwargs):
|
||||
super(KwException, self).__init__(kwargs)
|
||||
|
||||
def __getattr__(self, k):
|
||||
try:
|
||||
return super(FallbackDict, self).__getitem__(key)
|
||||
except KeyError:
|
||||
return self.fallback(key)
|
||||
return super(KwException, self).__getattr__(k)
|
||||
except AttributeError:
|
||||
return self.args[0][k]
|
||||
|
||||
|
||||
def list2dict(values_list):
|
||||
return dict(zip(range(0, len(values_list)), values_list))
|
||||
|
||||
from collections import namedtuple
|
||||
|
||||
"""Firmware information."""
|
||||
FirmwareInfo = namedtuple('FirmwareInfo', [
|
||||
'level',
|
||||
'kind',
|
||||
'name',
|
||||
'version',
|
||||
@@ -34,15 +157,8 @@ FirmwareInfo = namedtuple('FirmwareInfo', [
|
||||
"""Reprogrammable keys informations."""
|
||||
ReprogrammableKeyInfo = namedtuple('ReprogrammableKeyInfo', [
|
||||
'index',
|
||||
'id',
|
||||
'name',
|
||||
'key',
|
||||
'task',
|
||||
'task_name',
|
||||
'flags'])
|
||||
|
||||
|
||||
class Packet(namedtuple('Packet', ['code', 'devnumber', 'data'])):
|
||||
def __str__(self):
|
||||
return 'Packet(%02X,%02X,%s)' % (self.code, self.devnumber, 'None' if self.data is None else _hex(self.data))
|
||||
|
||||
del namedtuple
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
#
|
||||
# Constants used by the rest of the API.
|
||||
#
|
||||
|
||||
from struct import pack as _pack
|
||||
from binascii import hexlify as _hexlify
|
||||
_hex = lambda d: _hexlify(d).decode('ascii').upper()
|
||||
|
||||
from .common import (FallbackDict, list2dict)
|
||||
|
||||
|
||||
"""Possible features available on a Logitech device.
|
||||
|
||||
A particular device might not support all these features, and may support other
|
||||
unknown features as well.
|
||||
"""
|
||||
FEATURE = type('FEATURE', (),
|
||||
dict(
|
||||
ROOT=b'\x00\x00',
|
||||
FEATURE_SET=b'\x00\x01',
|
||||
FIRMWARE=b'\x00\x03',
|
||||
NAME=b'\x00\x05',
|
||||
BATTERY=b'\x10\x00',
|
||||
REPROGRAMMABLE_KEYS=b'\x1B\x00',
|
||||
WIRELESS=b'\x1D\x4B',
|
||||
SOLAR_CHARGE=b'\x43\x01',
|
||||
))
|
||||
|
||||
def _feature_name(key):
|
||||
if key is None:
|
||||
return None
|
||||
if type(key) == int:
|
||||
return FEATURE_NAME[_pack('!H', key)]
|
||||
return 'UNKNOWN_' + _hex(key)
|
||||
|
||||
|
||||
"""Feature names indexed by feature id."""
|
||||
FEATURE_NAME = FallbackDict(_feature_name)
|
||||
FEATURE_NAME[FEATURE.ROOT] = 'ROOT'
|
||||
FEATURE_NAME[FEATURE.FEATURE_SET] = 'FEATURE_SET'
|
||||
FEATURE_NAME[FEATURE.FIRMWARE] = 'FIRMWARE'
|
||||
FEATURE_NAME[FEATURE.NAME] = 'NAME'
|
||||
FEATURE_NAME[FEATURE.BATTERY] = 'BATTERY'
|
||||
FEATURE_NAME[FEATURE.REPROGRAMMABLE_KEYS] = 'REPROGRAMMABLE_KEYS'
|
||||
FEATURE_NAME[FEATURE.WIRELESS] = 'WIRELESS'
|
||||
FEATURE_NAME[FEATURE.SOLAR_CHARGE] = 'SOLAR_CHARGE'
|
||||
|
||||
|
||||
FEATURE_FLAGS = { 0x20: 'internal', 0x40: 'hidden', 0x80: 'obsolete' }
|
||||
|
||||
|
||||
_DEVICE_KINDS = ('keyboard', 'remote control', 'numpad', 'mouse',
|
||||
'touchpad', 'trackball', 'presenter', 'receiver')
|
||||
|
||||
"""Possible types of devices connected to an UR."""
|
||||
DEVICE_KIND = FallbackDict(lambda x: 'unknown', list2dict(_DEVICE_KINDS))
|
||||
|
||||
|
||||
_FIRMWARE_KINDS = ('Firmware', 'Bootloader', 'Hardware', 'Other')
|
||||
|
||||
"""Names of different firmware levels possible, indexed by level."""
|
||||
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')
|
||||
|
||||
"""Names for possible battery status values."""
|
||||
BATTERY_STATUS = FallbackDict(lambda x: 'unknown', list2dict(_BATTERY_STATUSES))
|
||||
|
||||
_KEY_NAMES = ( 'unknown_0000', 'Volume up', 'Volume down', 'Mute', 'Play/Pause',
|
||||
'Next', 'Previous', 'Stop', 'Application switcher',
|
||||
'unknown_0009', 'Calculator', 'unknown_000B', 'unknown_000C',
|
||||
'unknown_000D', 'Mail')
|
||||
|
||||
"""Standard names for reprogrammable keys."""
|
||||
KEY_NAME = FallbackDict(lambda x: 'unknown_%04X' % x, list2dict(_KEY_NAMES))
|
||||
|
||||
"""Possible flags on a reprogrammable key."""
|
||||
KEY_FLAG = type('KEY_FLAG', (), dict(
|
||||
REPROGRAMMABLE=0x10,
|
||||
FN_SENSITIVE=0x08,
|
||||
NONSTANDARD=0x04,
|
||||
IS_FN=0x02,
|
||||
MSE=0x01,
|
||||
))
|
||||
|
||||
KEY_FLAG_NAME = FallbackDict(lambda x: 'unknown')
|
||||
KEY_FLAG_NAME[KEY_FLAG.REPROGRAMMABLE] = 'reprogrammable'
|
||||
KEY_FLAG_NAME[KEY_FLAG.FN_SENSITIVE] = 'fn-sensitive'
|
||||
KEY_FLAG_NAME[KEY_FLAG.NONSTANDARD] = 'nonstandard'
|
||||
KEY_FLAG_NAME[KEY_FLAG.IS_FN] = 'is-fn'
|
||||
KEY_FLAG_NAME[KEY_FLAG.MSE] = 'mse'
|
||||
|
||||
_ERROR_NAMES = ('Ok', 'Unknown', 'Invalid argument', 'Out of range',
|
||||
'Hardware error', 'Logitech internal', 'Invalid feature index',
|
||||
'Invalid function', 'Busy', 'Unsupported')
|
||||
|
||||
"""Names for error codes."""
|
||||
ERROR_NAME = FallbackDict(lambda x: 'Unknown error', list2dict(_ERROR_NAMES))
|
||||
|
||||
|
||||
"""Maximum number of devices that can be attached to a single receiver."""
|
||||
MAX_ATTACHED_DEVICES = 6
|
||||
|
||||
|
||||
del FallbackDict
|
||||
del list2dict
|
||||
68
lib/logitech/unifying_receiver/descriptors.py
Normal file
@@ -0,0 +1,68 @@
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
from collections import namedtuple
|
||||
|
||||
from .common import NamedInts as _NamedInts
|
||||
from . import hidpp10
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
_DeviceDescriptor = namedtuple('_DeviceDescriptor',
|
||||
['name', 'kind', 'codename', 'registers', 'settings'])
|
||||
|
||||
DEVICES = {}
|
||||
|
||||
def _D(name, codename=None, kind=None, registers=None, settings=None):
|
||||
if kind is None:
|
||||
kind = (hidpp10.DEVICE_KIND.mouse if 'Mouse' in name
|
||||
else hidpp10.DEVICE_KIND.keyboard if 'Keyboard' in name
|
||||
else hidpp10.DEVICE_KIND.touchpad if 'Touchpad' in name
|
||||
else hidpp10.DEVICE_KIND.trackball if 'Trackball' in name
|
||||
else None)
|
||||
assert kind is not None
|
||||
|
||||
if codename is None:
|
||||
codename = name.split(' ')[-1]
|
||||
assert codename is not None
|
||||
|
||||
DEVICES[codename] = _DeviceDescriptor(name, kind, codename, registers, settings)
|
||||
|
||||
|
||||
_D('Wireless Mouse M315')
|
||||
_D('Wireless Mouse M325')
|
||||
_D('Wireless Mouse M505')
|
||||
_D('Wireless Mouse M510')
|
||||
_D('Couch Mouse M515')
|
||||
_D('Wireless Mouse M525')
|
||||
_D('Wireless Trackball M570')
|
||||
_D('Touch Mouse M600')
|
||||
_D('Marathon Mouse M705',
|
||||
registers=_NamedInts(battery=0x0D),
|
||||
settings=[hidpp10.SmoothScroll_Setting(0x01)],
|
||||
)
|
||||
_D('Wireless Keyboard K270')
|
||||
_D('Wireless Keyboard K350')
|
||||
_D('Wireless Keyboard K360')
|
||||
_D('Wireless Touch Keyboard K400')
|
||||
_D('Wireless Solar Keyboard K750')
|
||||
_D('Wireless Illuminated Keyboard K800')
|
||||
_D('Zone Touch Mouse T400')
|
||||
_D('Wireless Rechargeable Touchpad T650')
|
||||
_D('Logitech Cube', kind='mouse')
|
||||
_D('Anywhere Mouse MX', codename='Anywhere MX',
|
||||
registers=_NamedInts(battery=0x0D),
|
||||
)
|
||||
_D('Performance Mouse MX', codename='Performance MX',
|
||||
registers=_NamedInts(battery=0x0D),
|
||||
settings=[
|
||||
hidpp10.MouseDPI_Setting(0x63, _NamedInts(**dict((str(x * 100), 0x80 + x) for x in range(1, 16)))),
|
||||
],
|
||||
)
|
||||
|
||||
del namedtuple
|
||||
@@ -1,36 +0,0 @@
|
||||
#
|
||||
# Exceptions that may be raised by this API.
|
||||
#
|
||||
|
||||
from .constants import (FEATURE_NAME, ERROR_NAME)
|
||||
|
||||
|
||||
class NoReceiver(Exception):
|
||||
"""May be raised when trying to talk through a previously connected
|
||||
receiver that is no longer available. Should only happen if the receiver is
|
||||
physically disconnected from the machine, or its kernel driver module is
|
||||
unloaded."""
|
||||
pass
|
||||
|
||||
|
||||
class FeatureNotSupported(Exception):
|
||||
"""Raised when trying to request a feature not supported by the device."""
|
||||
def __init__(self, devnumber, feature):
|
||||
super(FeatureNotSupported, self).__init__(devnumber, feature, FEATURE_NAME[feature])
|
||||
self.devnumber = devnumber
|
||||
self.feature = feature
|
||||
self.feature_name = FEATURE_NAME[feature]
|
||||
|
||||
|
||||
class FeatureCallError(Exception):
|
||||
"""Raised if the device replied to a feature call with an error."""
|
||||
def __init__(self, devnumber, feature, feature_index, feature_function, error_code, data=None):
|
||||
super(FeatureCallError, self).__init__(devnumber, feature, feature_index, feature_function, error_code, ERROR_NAME[error_code])
|
||||
self.devnumber = devnumber
|
||||
self.feature = feature
|
||||
self.feature_name = None if feature is None else FEATURE_NAME[feature]
|
||||
self.feature_index = feature_index
|
||||
self.feature_function = feature_function
|
||||
self.error_code = error_code
|
||||
self.error_string = ERROR_NAME[error_code]
|
||||
self.data = data
|
||||
171
lib/logitech/unifying_receiver/hidpp10.py
Normal file
@@ -0,0 +1,171 @@
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
from .common import (strhex as _strhex,
|
||||
NamedInts as _NamedInts,
|
||||
FirmwareInfo as _FirmwareInfo)
|
||||
from . import settings as _settings
|
||||
from .hidpp20 import FIRMWARE_KIND
|
||||
|
||||
#
|
||||
# constants
|
||||
#
|
||||
|
||||
DEVICE_KIND = _NamedInts(
|
||||
keyboard=0x01,
|
||||
mouse=0x02,
|
||||
numpad=0x03,
|
||||
presenter=0x04,
|
||||
trackball=0x08,
|
||||
touchpad=0x09)
|
||||
|
||||
POWER_SWITCH_LOCATION = _NamedInts(
|
||||
base=0x01,
|
||||
top_case=0x02,
|
||||
edge_of_top_right_corner=0x03,
|
||||
top_left_corner=0x05,
|
||||
bottom_left_corner=0x06,
|
||||
top_right_corner=0x07,
|
||||
bottom_right_corner=0x08,
|
||||
top_edge=0x09,
|
||||
right_edge=0x0A,
|
||||
left_edge=0x0B,
|
||||
bottom_edge=0x0C)
|
||||
|
||||
NOTIFICATION_FLAG = _NamedInts(
|
||||
battery_status=0x100000,
|
||||
wireless=0x000100,
|
||||
software_present=0x0000800)
|
||||
|
||||
ERROR = _NamedInts(
|
||||
invalid_SubID__command=0x01,
|
||||
invalid_address=0x02,
|
||||
invalid_value=0x03,
|
||||
connection_request_failed=0x04,
|
||||
too_many_devices=0x05,
|
||||
already_exists=0x06,
|
||||
busy=0x07,
|
||||
unknown_device=0x08,
|
||||
resource_error=0x09,
|
||||
request_unavailable=0x0A,
|
||||
unsupported_parameter_value=0x0B,
|
||||
wrong_pin_code=0x0C)
|
||||
|
||||
PAIRING_ERRORS = _NamedInts(
|
||||
device_timeout=0x01,
|
||||
device_not_supported=0x02,
|
||||
too_many_devices=0x03,
|
||||
sequence_timeout=0x06)
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
|
||||
class SmoothScroll_Setting(_settings.Setting):
|
||||
def __init__(self, register):
|
||||
super(SmoothScroll_Setting, self).__init__('smooth-scroll', _settings.KIND.toggle,
|
||||
'Smooth Scrolling', 'High-sensitivity mode for vertical scroll with the wheel.')
|
||||
assert register is not None
|
||||
self.register = register
|
||||
|
||||
def read(self, cached=True):
|
||||
if (self._value is None or not cached) and self._device:
|
||||
ss = self.read_register()
|
||||
if ss:
|
||||
self._value = (ss[:1] == b'\x40')
|
||||
return self._value
|
||||
|
||||
def write(self, value):
|
||||
if self._device:
|
||||
reply = self.write_register(0x40 if bool(value) else 0x00)
|
||||
self._value = None
|
||||
if reply:
|
||||
return self.read()
|
||||
|
||||
|
||||
class MouseDPI_Setting(_settings.Setting):
|
||||
def __init__(self, register, choices):
|
||||
super(MouseDPI_Setting, self).__init__('dpi', _settings.KIND.choice,
|
||||
'Sensitivity (DPI)', choices=choices)
|
||||
assert choices
|
||||
assert isinstance(choices, _NamedInts)
|
||||
assert register is not None
|
||||
self.register = register
|
||||
|
||||
def read(self, cached=True):
|
||||
if (self._value is None or not cached) and self._device:
|
||||
dpi = self.read_register()
|
||||
if dpi:
|
||||
value = ord(dpi[:1])
|
||||
self._value = self.choices[value]
|
||||
assert self._value is not None
|
||||
return self._value
|
||||
|
||||
def write(self, value):
|
||||
if self._device:
|
||||
choice = self.choices[value]
|
||||
if choice is None:
|
||||
raise ValueError(repr(value))
|
||||
reply = self.write_register(value)
|
||||
self._value = None
|
||||
if reply:
|
||||
return self.read()
|
||||
|
||||
#
|
||||
# functions
|
||||
#
|
||||
|
||||
def get_battery(device):
|
||||
"""Reads a device's battery level, if provided by the HID++ 1.0 protocol."""
|
||||
if 'battery' in device.registers:
|
||||
register = device.registers['battery']
|
||||
|
||||
reply = device.request(0x8100 + (register & 0xFF))
|
||||
if reply:
|
||||
charge = ord(reply[:1])
|
||||
status = ord(reply[2:3]) & 0xF0
|
||||
status = ('discharging' if status == 0x30
|
||||
else 'charging' if status == 0x50
|
||||
else 'fully charged' if status == 0x90
|
||||
else None)
|
||||
return charge, status
|
||||
|
||||
|
||||
def get_serial(device):
|
||||
if device.kind is None:
|
||||
dev_id = 0x03
|
||||
receiver = device
|
||||
else:
|
||||
dev_id = 0x30 + device.number - 1
|
||||
receiver = device.receiver
|
||||
|
||||
serial = receiver.request(0x83B5, dev_id)
|
||||
if serial:
|
||||
return _strhex(serial[1:5])
|
||||
|
||||
|
||||
def get_firmware(device):
|
||||
firmware = []
|
||||
|
||||
reply = device.request(0x81F1, 0x01)
|
||||
if reply:
|
||||
fw_version = _strhex(reply[1:3])
|
||||
fw_version = '%s.%s' % (fw_version[0:2], fw_version[2:4])
|
||||
reply = device.request(0x81F1, 0x02)
|
||||
if reply:
|
||||
fw_version += '.B' + _strhex(reply[1:3])
|
||||
fw = _FirmwareInfo(FIRMWARE_KIND.Firmware, '', fw_version, None)
|
||||
firmware.append(fw)
|
||||
|
||||
reply = device.request(0x81F1, 0x04)
|
||||
if reply:
|
||||
bl_version = _strhex(reply[1:3])
|
||||
bl_version = '%s.%s' % (bl_version[0:2], bl_version[2:4])
|
||||
bl = _FirmwareInfo(FIRMWARE_KIND.Bootloader, '', bl_version, None)
|
||||
firmware.append(bl)
|
||||
|
||||
return tuple(firmware)
|
||||
413
lib/logitech/unifying_receiver/hidpp20.py
Normal file
@@ -0,0 +1,413 @@
|
||||
#
|
||||
# Logitech Unifying Receiver API.
|
||||
#
|
||||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
from struct import pack as _pack, unpack as _unpack
|
||||
from weakref import proxy as _proxy
|
||||
|
||||
from logging import getLogger, DEBUG as _DEBUG
|
||||
_log = getLogger('LUR').getChild('hidpp20')
|
||||
del getLogger
|
||||
|
||||
from . import settings as _settings
|
||||
from .common import (FirmwareInfo as _FirmwareInfo,
|
||||
ReprogrammableKeyInfo as _ReprogrammableKeyInfo,
|
||||
KwException as _KwException,
|
||||
NamedInts as _NamedInts)
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
"""Possible features available on a Logitech device.
|
||||
|
||||
A particular device might not support all these features, and may support other
|
||||
unknown features as well.
|
||||
"""
|
||||
FEATURE = _NamedInts(
|
||||
ROOT=0x0000,
|
||||
FEATURE_SET=0x0001,
|
||||
FIRMWARE=0x0003,
|
||||
NAME=0x0005,
|
||||
BATTERY=0x1000,
|
||||
REPROGRAMMABLE_KEYS=0x1B00,
|
||||
WIRELESS=0x1D4B,
|
||||
FN_STATUS=0x40A0,
|
||||
SOLAR_CHARGE=0x4301,
|
||||
TOUCH_MOUSE=0x6110)
|
||||
FEATURE._fallback = lambda x: 'unknown:%04X' % x
|
||||
|
||||
FEATURE_FLAG = _NamedInts(
|
||||
internal=0x20,
|
||||
hidden=0x40,
|
||||
obsolete=0x80)
|
||||
|
||||
DEVICE_KIND = _NamedInts(
|
||||
keyboard=0x00,
|
||||
remote_control=0x01,
|
||||
numpad=0x02,
|
||||
mouse=0x03,
|
||||
touchpad=0x04,
|
||||
trackball=0x05,
|
||||
presenter=0x06,
|
||||
receiver=0x07)
|
||||
|
||||
FIRMWARE_KIND = _NamedInts(
|
||||
Firmware=0x00,
|
||||
Bootloader=0x01,
|
||||
Hardware=0x02,
|
||||
Other=0x03)
|
||||
|
||||
BATTERY_OK = lambda status: status < 5
|
||||
|
||||
BATTERY_STATUS = _NamedInts(
|
||||
discharging=0x00,
|
||||
recharging=0x01,
|
||||
almost_full=0x02,
|
||||
full=0x03,
|
||||
slow_recharge=0x04,
|
||||
invalid_battery=0x05,
|
||||
thermal_error=0x06)
|
||||
|
||||
KEY = _NamedInts(
|
||||
Volume_Up=0x0001,
|
||||
Volume_Down=0x0002,
|
||||
Mute=0x0003,
|
||||
Play__Pause=0x0004,
|
||||
Next=0x0005,
|
||||
Previous=0x0006,
|
||||
Stop=0x0007,
|
||||
Application_Switcher=0x0008,
|
||||
Calculator=0x000A,
|
||||
Mail=0x000E,
|
||||
Home=0x001A,
|
||||
Music=0x001D,
|
||||
Search=0x0029,
|
||||
Sleep=0x002F)
|
||||
KEY._fallback = lambda x: 'unknown:%04X' % x
|
||||
|
||||
KEY_FLAG = _NamedInts(
|
||||
reprogrammable=0x10,
|
||||
FN_sensitive=0x08,
|
||||
nonstandard=0x04,
|
||||
is_FN=0x02,
|
||||
mse=0x01)
|
||||
|
||||
ERROR = _NamedInts(
|
||||
unknown=0x01,
|
||||
invalid_argument=0x02,
|
||||
out_of_range=0x03,
|
||||
hardware_error=0x04,
|
||||
logitech_internal=0x05,
|
||||
invalid_feature_index=0x06,
|
||||
invalid_function=0x07,
|
||||
busy=0x08,
|
||||
unsupported=0x09)
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
class FeatureNotSupported(_KwException):
|
||||
"""Raised when trying to request a feature not supported by the device."""
|
||||
pass
|
||||
|
||||
class FeatureCallError(_KwException):
|
||||
"""Raised if the device replied to a feature call with an error."""
|
||||
pass
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
class FeaturesArray(object):
|
||||
"""A sequence of features supported by a HID++ 2.0 device."""
|
||||
__slots__ = ('supported', 'device', 'features')
|
||||
|
||||
def __init__(self, device):
|
||||
assert device is not None
|
||||
self.device = _proxy(device)
|
||||
self.supported = True
|
||||
self.features = None
|
||||
|
||||
def __del__(self):
|
||||
self.supported = False
|
||||
|
||||
def _check(self):
|
||||
# print ("%s check" % self.device)
|
||||
if self.supported:
|
||||
assert self.device
|
||||
if self.features is not None:
|
||||
return True
|
||||
|
||||
protocol = self.device.protocol
|
||||
if protocol == 0:
|
||||
# device is not connected right now, will have to try later
|
||||
return False
|
||||
|
||||
# I _think_ this is universally true
|
||||
if protocol < 2.0:
|
||||
self.supported = False
|
||||
# self.device.features = None
|
||||
self.device = None
|
||||
return False
|
||||
|
||||
reply = self.device.request(int(FEATURE.ROOT), _pack(b'!H', FEATURE.FEATURE_SET))
|
||||
if reply is None:
|
||||
self.supported = False
|
||||
else:
|
||||
fs_index = ord(reply[0:1])
|
||||
if fs_index:
|
||||
count = self.device.request(fs_index << 8)
|
||||
if count is None:
|
||||
_log.warn("FEATURE_SET found, but failed to read features count")
|
||||
# most likely the device is unavailable
|
||||
return False
|
||||
else:
|
||||
count = ord(count[:1])
|
||||
assert count >= fs_index
|
||||
self.features = [None] * (1 + count)
|
||||
self.features[0] = FEATURE.ROOT
|
||||
self.features[fs_index] = FEATURE.FEATURE_SET
|
||||
return True
|
||||
else:
|
||||
self.supported = False
|
||||
|
||||
return False
|
||||
|
||||
__bool__ = __nonzero__ = _check
|
||||
|
||||
def __getitem__(self, index):
|
||||
if self._check():
|
||||
assert type(index) == int
|
||||
if index < 0 or index >= len(self.features):
|
||||
raise IndexError(index)
|
||||
|
||||
if self.features[index] is None:
|
||||
feature = self.device.feature_request(FEATURE.FEATURE_SET, 0x10, index)
|
||||
if feature:
|
||||
feature, = _unpack(b'!H', feature[:2])
|
||||
self.features[index] = FEATURE[feature]
|
||||
|
||||
return self.features[index]
|
||||
|
||||
def __contains__(self, value):
|
||||
if self._check():
|
||||
may_have = False
|
||||
for f in self.features:
|
||||
if f is None:
|
||||
may_have = True
|
||||
elif int(value) == int(f):
|
||||
return True
|
||||
elif int(value) < int(f):
|
||||
break
|
||||
|
||||
if may_have:
|
||||
reply = self.device.request(int(FEATURE.ROOT), _pack(b'!H', value))
|
||||
if reply:
|
||||
index = ord(reply[0:1])
|
||||
if index:
|
||||
self.features[index] = FEATURE[int(value)]
|
||||
return True
|
||||
|
||||
def index(self, value):
|
||||
if self._check():
|
||||
may_have = False
|
||||
for index, f in enumerate(self.features):
|
||||
if f is None:
|
||||
may_have = True
|
||||
elif int(value) == int(f):
|
||||
return index
|
||||
elif int(value) < int(f):
|
||||
raise ValueError("%s not in list" % repr(value))
|
||||
|
||||
if may_have:
|
||||
reply = self.device.request(int(FEATURE.ROOT), _pack(b'!H', value))
|
||||
if reply:
|
||||
index = ord(reply[0:1])
|
||||
self.features[index] = FEATURE[int(value)]
|
||||
return index
|
||||
|
||||
raise ValueError("%s not in list" % repr(value))
|
||||
|
||||
def __iter__(self):
|
||||
if self._check():
|
||||
yield FEATURE.ROOT
|
||||
index = 1
|
||||
last_index = len(self.features)
|
||||
while index < last_index:
|
||||
yield self.__getitem__(index)
|
||||
index += 1
|
||||
|
||||
def __len__(self):
|
||||
return len(self.features) if self._check() else 0
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
class KeysArray(object):
|
||||
"""A sequence of key mappings supported by a HID++ 2.0 device."""
|
||||
__slots__ = ('device', 'keys')
|
||||
|
||||
def __init__(self, device, count):
|
||||
assert device is not None
|
||||
self.device = _proxy(device)
|
||||
self.keys = [None] * count
|
||||
|
||||
def __getitem__(self, index):
|
||||
assert type(index) == int
|
||||
if index < 0 or index >= len(self.keys):
|
||||
raise IndexError(index)
|
||||
|
||||
if self.keys[index] is None:
|
||||
keydata = feature_request(self.device, FEATURE.REPROGRAMMABLE_KEYS, 0x10, index)
|
||||
if keydata:
|
||||
key, key_task, flags = _unpack(b'!HHB', keydata[:5])
|
||||
self.keys[index] = _ReprogrammableKeyInfo(index, KEY[key], KEY[key_task], flags)
|
||||
|
||||
return self.keys[index]
|
||||
|
||||
def index(self, value):
|
||||
for index, k in enumerate(self.keys):
|
||||
if k is not None and int(value) == int(k.key):
|
||||
return index
|
||||
|
||||
for index, k in enumerate(self.keys):
|
||||
if k is None:
|
||||
k = self.__getitem__(index)
|
||||
if k is not None:
|
||||
return index
|
||||
|
||||
def __iter__(self):
|
||||
for k in range(0, len(self.keys)):
|
||||
yield self.__getitem__(k)
|
||||
|
||||
def __len__(self):
|
||||
return len(self.keys)
|
||||
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
class ToggleFN_Setting(_settings.Setting):
|
||||
def __init__(self):
|
||||
super(ToggleFN_Setting, self).__init__('fn-swap', _settings.KIND.toggle, 'Swap Fx function',
|
||||
'When set, the F1..F12 keys will activate their special function,\n'
|
||||
'and you must hold the FN key to activate their standard function.\n'
|
||||
'\n'
|
||||
'When unset, the F1..F12 keys will activate their standard function,\n'
|
||||
'and you must hold the FN key to activate their special function.')
|
||||
|
||||
def read(self, cached=True):
|
||||
if (self._value is None or not cached) and self._device:
|
||||
fn = self._device.feature_request(FEATURE.FN_STATUS)
|
||||
if fn:
|
||||
self._value = (fn[:1] == b'\x01')
|
||||
return self._value
|
||||
|
||||
def write(self, value):
|
||||
if self._device:
|
||||
reply = self._device.feature_request(FEATURE.FN_STATUS, 0x10, 0x01 if value else 0x00)
|
||||
self._value = (reply[:1] == b'\x01') if reply else None
|
||||
return self._value
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
def feature_request(device, feature, function=0x00, *params):
|
||||
if device.features:
|
||||
if feature in device.features:
|
||||
feature_index = device.features.index(int(feature))
|
||||
return device.request((feature_index << 8) + (function & 0xFF), *params)
|
||||
|
||||
|
||||
def get_firmware(device):
|
||||
"""Reads a device's firmware info.
|
||||
|
||||
:returns: a list of FirmwareInfo tuples, ordered by firmware layer.
|
||||
"""
|
||||
count = feature_request(device, FEATURE.FIRMWARE)
|
||||
if count:
|
||||
count = ord(count[:1])
|
||||
|
||||
fw = []
|
||||
for index in range(0, count):
|
||||
fw_info = feature_request(device, FEATURE.FIRMWARE, 0x10, index)
|
||||
if fw_info:
|
||||
level = ord(fw_info[:1]) & 0x0F
|
||||
if level == 0 or level == 1:
|
||||
name, version_major, version_minor, build = _unpack(b'!3sBBH', fw_info[1:8])
|
||||
version = '%02X.%02X' % (version_major, version_minor)
|
||||
if build:
|
||||
version += '.B%04X' % build
|
||||
extras = fw_info[9:].rstrip(b'\x00') or None
|
||||
fw_info = _FirmwareInfo(FIRMWARE_KIND[level], name.decode('ascii'), version, extras)
|
||||
elif level == FIRMWARE_KIND.Hardware:
|
||||
fw_info = _FirmwareInfo(FIRMWARE_KIND.Hardware, '', ord(fw_info[1:2]), None)
|
||||
else:
|
||||
fw_info = _FirmwareInfo(FIRMWARE_KIND.Other, '', '', None)
|
||||
|
||||
fw.append(fw_info)
|
||||
# _log.debug("device %d firmware %s", devnumber, fw_info)
|
||||
return tuple(fw)
|
||||
|
||||
|
||||
def get_kind(device):
|
||||
"""Reads a device's type.
|
||||
|
||||
:see DEVICE_KIND:
|
||||
:returns: a string describing the device type, or ``None`` if the device is
|
||||
not available or does not support the ``NAME`` feature.
|
||||
"""
|
||||
kind = feature_request(device, FEATURE.NAME, 0x20)
|
||||
if kind:
|
||||
kind = ord(kind[:1])
|
||||
# _log.debug("device %d type %d = %s", devnumber, kind, DEVICE_KIND[kind])
|
||||
return DEVICE_KIND[kind]
|
||||
|
||||
|
||||
def get_name(device):
|
||||
"""Reads a device's name.
|
||||
|
||||
:returns: a string with the device name, or ``None`` if the device is not
|
||||
available or does not support the ``NAME`` feature.
|
||||
"""
|
||||
name_length = feature_request(device, FEATURE.NAME)
|
||||
if name_length:
|
||||
name_length = ord(name_length[:1])
|
||||
|
||||
name = b''
|
||||
while len(name) < name_length:
|
||||
fragment = feature_request(device, FEATURE.NAME, 0x10, len(name))
|
||||
if fragment:
|
||||
name += fragment[:name_length - len(name)]
|
||||
else:
|
||||
_log.error("failed to read whole name of %s (expected %d chars)", device, name_length)
|
||||
return None
|
||||
|
||||
return name.decode('ascii')
|
||||
|
||||
|
||||
def get_battery(device):
|
||||
"""Reads a device's battery level.
|
||||
|
||||
:raises FeatureNotSupported: if the device does not support this feature.
|
||||
"""
|
||||
battery = feature_request(device, FEATURE.BATTERY)
|
||||
if battery:
|
||||
discharge, dischargeNext, status = _unpack(b'!BBB', battery[:3])
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("device %d battery %d%% charged, next level %d%% charge, status %d = %s",
|
||||
device.number, discharge, dischargeNext, status, BATTERY_STATUS[status])
|
||||
return discharge, BATTERY_STATUS[status]
|
||||
|
||||
|
||||
def get_keys(device):
|
||||
count = feature_request(device, FEATURE.REPROGRAMMABLE_KEYS)
|
||||
if count:
|
||||
return KeysArray(device, ord(count[:1]))
|
||||
@@ -2,12 +2,10 @@
|
||||
#
|
||||
#
|
||||
|
||||
from threading import Thread as _Thread
|
||||
# from time import sleep as _sleep
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
from . import base as _base
|
||||
from .exceptions import NoReceiver as _NoReceiver
|
||||
from .common import Packet as _Packet
|
||||
import threading as _threading
|
||||
from time import time as _timestamp
|
||||
|
||||
# for both Python 2 and 3
|
||||
try:
|
||||
@@ -15,127 +13,171 @@ try:
|
||||
except ImportError:
|
||||
from queue import Queue as _Queue
|
||||
|
||||
|
||||
from logging import getLogger
|
||||
from logging import getLogger, DEBUG as _DEBUG
|
||||
_log = getLogger('LUR').getChild('listener')
|
||||
del getLogger
|
||||
|
||||
from . import base as _base
|
||||
|
||||
_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 ThreadedHandle(object):
|
||||
"""A thread-local wrapper with different open handles for each thread."""
|
||||
|
||||
__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 = _threading.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):
|
||||
if self._local:
|
||||
self._local = None
|
||||
handles, self._handles = self._handles, []
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("%s closing %s", repr(self), handles)
|
||||
for h in handles:
|
||||
_base.close(h)
|
||||
|
||||
def __del__(self):
|
||||
self.close()
|
||||
|
||||
def __index__(self):
|
||||
if self._local:
|
||||
try:
|
||||
return self._local.handle
|
||||
except:
|
||||
return self._open()
|
||||
__int__ = __index__
|
||||
|
||||
def __str__(self):
|
||||
if self._local:
|
||||
return str(int(self))
|
||||
__unicode__ = __str__
|
||||
|
||||
def __repr__(self):
|
||||
return '<ThreadedHandle(%s)>' % self.path
|
||||
|
||||
def __bool__(self):
|
||||
return bool(self._local)
|
||||
__nonzero__ = __bool__
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
_EVENT_READ_TIMEOUT = 500
|
||||
_IDLE_READS = 4
|
||||
|
||||
|
||||
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__)
|
||||
def __init__(self, receiver, events_callback):
|
||||
super(EventsListener, self).__init__(name=self.__class__.__name__)
|
||||
|
||||
self.daemon = True
|
||||
self._active = False
|
||||
|
||||
self._handle = receiver_handle
|
||||
self.receiver = receiver
|
||||
self._queued_events = _Queue(32)
|
||||
self._events_callback = events_callback
|
||||
|
||||
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.tick_period = 0
|
||||
|
||||
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
|
||||
_base.events_hook = self._events_hook
|
||||
ihandle = int(self.receiver.handle)
|
||||
_log.info("started with %s (%d)", self.receiver, ihandle)
|
||||
|
||||
self._dispatcher.start()
|
||||
self.has_started()
|
||||
|
||||
last_tick = 0
|
||||
idle_reads = 0
|
||||
|
||||
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, _EVENT_READ_TIMEOUT)
|
||||
except _base.NoReceiver:
|
||||
_log.warning("receiver disconnected")
|
||||
self.receiver.close()
|
||||
break
|
||||
|
||||
if event:
|
||||
event = _base.make_event(*event)
|
||||
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:
|
||||
# if _log.isEnabledFor(_DEBUG):
|
||||
# _log.debug("processing event %s", event)
|
||||
try:
|
||||
self._events_callback(event)
|
||||
except:
|
||||
_log.exception("processing event %s", event)
|
||||
elif self.tick_period:
|
||||
idle_reads += 1
|
||||
if idle_reads % _IDLE_READS == 0:
|
||||
idle_reads = 0
|
||||
now = _timestamp()
|
||||
if now - last_tick >= self.tick_period:
|
||||
last_tick = now
|
||||
self.tick(now)
|
||||
|
||||
_base.request_context = None
|
||||
handle, self._handle = self._handle, 0
|
||||
_base.close(handle)
|
||||
_log.debug("stopped")
|
||||
_base.unhandled_hook = None
|
||||
del self._queued_events
|
||||
|
||||
self.has_stopped()
|
||||
|
||||
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()
|
||||
self._active = False
|
||||
|
||||
@property
|
||||
def handle(self):
|
||||
return self._handle
|
||||
def has_started(self):
|
||||
"""Called right after the thread has started."""
|
||||
pass
|
||||
|
||||
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 has_stopped(self):
|
||||
"""Called right before the thread stops."""
|
||||
pass
|
||||
|
||||
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 tick(self, timestamp):
|
||||
"""Called about every tick_period seconds, if set."""
|
||||
pass
|
||||
|
||||
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 _events_hook(self, event):
|
||||
# only consider unhandled events that were sent from this thread,
|
||||
# i.e. triggered during a callback of a previous event
|
||||
if self._active and _threading.current_thread() == self:
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("queueing unhandled event %s", event)
|
||||
self._queued_events.put(event)
|
||||
|
||||
def __bool__(self):
|
||||
return bool(self._active and self._handle)
|
||||
return bool(self._active and self.receiver)
|
||||
__nonzero__ = __bool__
|
||||
|
||||
372
lib/logitech/unifying_receiver/receiver.py
Normal file
@@ -0,0 +1,372 @@
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
import errno as _errno
|
||||
from weakref import proxy as _proxy
|
||||
from collections import defaultdict as _defaultdict
|
||||
|
||||
from logging import getLogger
|
||||
_log = getLogger('LUR').getChild('receiver')
|
||||
del getLogger
|
||||
|
||||
from . import base as _base
|
||||
from . import hidpp10 as _hidpp10
|
||||
from . import hidpp20 as _hidpp20
|
||||
from .common import strhex as _strhex
|
||||
from .descriptors import DEVICES as _DEVICES
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
"""A receiver may have a maximum of 6 paired devices at a time."""
|
||||
MAX_PAIRED_DEVICES = 6
|
||||
|
||||
|
||||
class PairedDevice(object):
|
||||
def __init__(self, receiver, number):
|
||||
assert receiver
|
||||
self.receiver = _proxy(receiver)
|
||||
assert number > 0 and number <= MAX_PAIRED_DEVICES
|
||||
self.number = number
|
||||
|
||||
self._protocol = None
|
||||
self._wpid = None
|
||||
self._power_switch = None
|
||||
self._polling_rate = None
|
||||
self._codename = None
|
||||
self._name = None
|
||||
self._kind = None
|
||||
self._serial = None
|
||||
self._firmware = None
|
||||
self._keys = None
|
||||
|
||||
self.features = _hidpp20.FeaturesArray(self)
|
||||
self._registers = None
|
||||
self._settings = None
|
||||
|
||||
@property
|
||||
def protocol(self):
|
||||
if self._protocol is None:
|
||||
self._protocol = _base.ping(self.receiver.handle, self.number)
|
||||
# _log.debug("device %d protocol %s", self.number, self._protocol)
|
||||
return self._protocol or 0
|
||||
|
||||
@property
|
||||
def wpid(self):
|
||||
if self._wpid is None:
|
||||
pair_info = self.receiver.request(0x83B5, 0x20 + self.number - 1)
|
||||
if pair_info:
|
||||
self._wpid = _strhex(pair_info[3:5])
|
||||
if self._kind is None:
|
||||
kind = ord(pair_info[7:8]) & 0x0F
|
||||
self._kind = _hidpp10.DEVICE_KIND[kind]
|
||||
if self._polling_rate is None:
|
||||
self._polling_rate = ord(pair_info[2:3])
|
||||
return self._wpid
|
||||
|
||||
@property
|
||||
def polling_rate(self):
|
||||
if self._polling_rate is None:
|
||||
self.wpid, 0
|
||||
return self._polling_rate
|
||||
|
||||
@property
|
||||
def power_switch_location(self):
|
||||
if self._power_switch is None:
|
||||
ps = self.receiver.request(0x83B5, 0x30 + self.number - 1)
|
||||
if ps:
|
||||
ps = ord(ps[9:10]) & 0x0F
|
||||
self._power_switch = _hidpp10.POWER_SWITCH_LOCATION[ps]
|
||||
return self._power_switch
|
||||
|
||||
@property
|
||||
def codename(self):
|
||||
if self._codename is None:
|
||||
codename = self.receiver.request(0x83B5, 0x40 + self.number - 1)
|
||||
if codename:
|
||||
self._codename = codename[2:].rstrip(b'\x00').decode('utf-8')
|
||||
# _log.debug("device %d codename %s", self.number, self._codename)
|
||||
return self._codename
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
if self._name is None:
|
||||
if self.codename in _DEVICES:
|
||||
self._name, self._kind = _DEVICES[self._codename][:2]
|
||||
elif self.protocol >= 2.0:
|
||||
self._name = _hidpp20.get_name(self)
|
||||
return self._name or self.codename or '?'
|
||||
|
||||
@property
|
||||
def kind(self):
|
||||
if self._kind is None:
|
||||
pair_info = self.receiver.request(0x83B5, 0x20 + self.number - 1)
|
||||
if pair_info:
|
||||
kind = ord(pair_info[7:8]) & 0x0F
|
||||
self._kind = _hidpp10.DEVICE_KIND[kind]
|
||||
if self._wpid is None:
|
||||
self._wpid = _strhex(pair_info[3:5])
|
||||
if self._kind is None:
|
||||
if self.codename in _DEVICES:
|
||||
self._name, self._kind = _DEVICES[self._codename][:2]
|
||||
elif self.protocol >= 2.0:
|
||||
self._kind = _hidpp20.get_kind(self)
|
||||
return self._kind or '?'
|
||||
|
||||
@property
|
||||
def firmware(self):
|
||||
if self._firmware is None:
|
||||
p = self.protocol
|
||||
if p >= 2.0:
|
||||
self._firmware = _hidpp20.get_firmware(self)
|
||||
if self._firmware is None and p == 1.0:
|
||||
self._firmware = _hidpp10.get_firmware(self)
|
||||
return self._firmware or ()
|
||||
|
||||
@property
|
||||
def serial(self):
|
||||
if self._serial is None:
|
||||
self._serial = _hidpp10.get_serial(self)
|
||||
return self._serial or '?'
|
||||
|
||||
@property
|
||||
def keys(self):
|
||||
if self._keys is None:
|
||||
self._keys = _hidpp20.get_keys(self) or ()
|
||||
return self._keys
|
||||
|
||||
@property
|
||||
def registers(self):
|
||||
if self._registers is None:
|
||||
descriptor = _DEVICES.get(self.codename)
|
||||
if descriptor is None or descriptor.registers is None:
|
||||
self._registers = _defaultdict(lambda: None)
|
||||
else:
|
||||
self._registers = descriptor.registers
|
||||
return self._registers
|
||||
|
||||
@property
|
||||
def settings(self):
|
||||
if self._settings is None:
|
||||
descriptor = _DEVICES.get(self.codename)
|
||||
if descriptor is None or descriptor.settings is None:
|
||||
self._settings = []
|
||||
else:
|
||||
self._settings = [s(self) for s in descriptor.settings]
|
||||
|
||||
if _hidpp20.FEATURE.FN_STATUS in self.features:
|
||||
tfn = _hidpp20.ToggleFN_Setting()
|
||||
self._settings.insert(0, tfn(self))
|
||||
|
||||
return self._settings
|
||||
|
||||
def request(self, request_id, *params):
|
||||
return _base.request(self.receiver.handle, self.number, request_id, *params)
|
||||
|
||||
def feature_request(self, feature, function=0x00, *params):
|
||||
return _hidpp20.feature_request(self, feature, function, *params)
|
||||
|
||||
def ping(self):
|
||||
return _base.ping(self.receiver.handle, self.number) is not None
|
||||
|
||||
def __index__(self):
|
||||
return self.number
|
||||
__int__ = __index__
|
||||
|
||||
def __lt__(self, other):
|
||||
return self.number < other.number
|
||||
|
||||
def __le__(self, other):
|
||||
return self.number <= other.number
|
||||
|
||||
def __gt__(self, other):
|
||||
return self.number > other.number
|
||||
|
||||
def __ge__(self, other):
|
||||
return self.number >= other.number
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.receiver == other.receiver and self.number == other.number
|
||||
|
||||
def __ne__(self, other):
|
||||
return self.receiver != other.receiver or self.number != other.number
|
||||
|
||||
def __hash__(self):
|
||||
return self.number
|
||||
|
||||
def __str__(self):
|
||||
return '<PairedDevice(%d,%s)>' % (self.number, self.codename or '?')
|
||||
__unicode__ = __repr__ = __str__
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
class Receiver(object):
|
||||
"""A Unifying Receiver instance.
|
||||
|
||||
The paired devices are available through the sequence interface.
|
||||
"""
|
||||
name = 'Unifying Receiver'
|
||||
kind = None
|
||||
max_devices = MAX_PAIRED_DEVICES
|
||||
|
||||
def __init__(self, handle, path=None):
|
||||
assert handle
|
||||
self.handle = handle
|
||||
assert path
|
||||
self.path = path
|
||||
|
||||
self.number = 0xFF
|
||||
self._serial = None
|
||||
self._firmware = None
|
||||
self._devices = {}
|
||||
|
||||
def close(self):
|
||||
handle, self.handle = self.handle, None
|
||||
self._devices.clear()
|
||||
return (handle and _base.close(handle))
|
||||
|
||||
def __del__(self):
|
||||
self.close()
|
||||
|
||||
@property
|
||||
def serial(self):
|
||||
if self._serial is None and self.handle:
|
||||
self._serial = _hidpp10.get_serial(self)
|
||||
return self._serial
|
||||
|
||||
@property
|
||||
def firmware(self):
|
||||
if self._firmware is None and self.handle:
|
||||
self._firmware = _hidpp10.get_firmware(self)
|
||||
return self._firmware
|
||||
|
||||
def enable_notifications(self, enable=True):
|
||||
"""Enable or disable device (dis)connection events on this receiver."""
|
||||
if not self.handle:
|
||||
return False
|
||||
if enable:
|
||||
# set all possible flags
|
||||
ok = self.request(0x8000, 0xFF, 0xFF, 0xFF)
|
||||
else:
|
||||
# clear out all possible flags
|
||||
ok = self.request(0x8000)
|
||||
|
||||
if ok:
|
||||
_log.info("device notifications %s", 'enabled' if enable else 'disabled')
|
||||
else:
|
||||
_log.warn("failed to %s device notifications", 'enable' if enable else 'disable')
|
||||
return ok
|
||||
|
||||
def notify_devices(self):
|
||||
"""Scan all devices."""
|
||||
if self.handle:
|
||||
if not self.request(0x8002, 0x02):
|
||||
_log.warn("failed to trigger device events")
|
||||
|
||||
def register_new_device(self, number):
|
||||
if self._devices.get(number) is not None:
|
||||
raise IndexError("device number %d already registered" % number)
|
||||
dev = PairedDevice(self, number)
|
||||
# create a device object, but only use it if the receiver knows about it
|
||||
if dev.wpid:
|
||||
_log.info("found device %d (%s)", number, dev.wpid)
|
||||
self._devices[number] = dev
|
||||
return dev
|
||||
self._devices[number] = None
|
||||
|
||||
def set_lock(self, lock_closed=True, device=0, timeout=0):
|
||||
if self.handle:
|
||||
lock = 0x02 if lock_closed else 0x01
|
||||
reply = self.request(0x80B2, lock, device, timeout)
|
||||
if reply:
|
||||
return True
|
||||
_log.warn("failed to %s the receiver lock", 'close' if lock_closed else 'open')
|
||||
|
||||
def count(self):
|
||||
count = self.request(0x8102)
|
||||
return 0 if count is None else ord(count[1:2])
|
||||
|
||||
def request(self, request_id, *params):
|
||||
if self.handle:
|
||||
return _base.request(self.handle, 0xFF, request_id, *params)
|
||||
|
||||
def __iter__(self):
|
||||
for number in range(1, 1 + MAX_PAIRED_DEVICES):
|
||||
if number in self._devices:
|
||||
dev = self._devices[number]
|
||||
else:
|
||||
dev = self.__getitem__(number)
|
||||
if dev is not None:
|
||||
yield dev
|
||||
|
||||
def __getitem__(self, key):
|
||||
if not self.handle:
|
||||
return None
|
||||
|
||||
dev = self._devices.get(key)
|
||||
if dev is not None:
|
||||
return dev
|
||||
|
||||
if type(key) != int:
|
||||
raise TypeError('key must be an integer')
|
||||
if key < 1 or key > MAX_PAIRED_DEVICES:
|
||||
raise IndexError(key)
|
||||
|
||||
return self.register_new_device(key)
|
||||
|
||||
def __delitem__(self, key):
|
||||
if self._devices.get(key) is None:
|
||||
raise IndexError(key)
|
||||
|
||||
dev = self._devices[key]
|
||||
reply = self.request(0x80B2, 0x03, int(key))
|
||||
if reply:
|
||||
del self._devices[key]
|
||||
_log.warn("%s unpaired device %s", self, dev)
|
||||
else:
|
||||
_log.error("%s failed to unpair device %s", self, dev)
|
||||
raise IndexError(key)
|
||||
|
||||
def __len__(self):
|
||||
return len([d for d in self._devices.values() if d is not None])
|
||||
|
||||
def __contains__(self, dev):
|
||||
if type(dev) == int:
|
||||
return self._devices.get(dev) is not None
|
||||
|
||||
return self.__contains__(dev.number)
|
||||
|
||||
def __str__(self):
|
||||
return '<Receiver(%s,%s%s)>' % (self.path, '' if type(self.handle) == int else 'T', self.handle)
|
||||
__unicode__ = __repr__ = __str__
|
||||
|
||||
__bool__ = __nonzero__ = lambda self: self.handle is not None
|
||||
|
||||
@classmethod
|
||||
def open(self):
|
||||
"""Opens the first Logitech Unifying Receiver found attached to the machine.
|
||||
|
||||
:returns: An open file handle for the found receiver, or ``None``.
|
||||
"""
|
||||
exception = None
|
||||
|
||||
for rawdevice in _base.receivers():
|
||||
exception = None
|
||||
try:
|
||||
handle = _base.open_path(rawdevice.path)
|
||||
if handle:
|
||||
return Receiver(handle, rawdevice.path)
|
||||
except OSError as e:
|
||||
_log.exception("open %s", rawdevice.path)
|
||||
if e.errno == _errno.EACCES:
|
||||
exception = e
|
||||
|
||||
if exception:
|
||||
# only keep the last exception
|
||||
raise exception
|
||||
49
lib/logitech/unifying_receiver/settings.py
Normal file
@@ -0,0 +1,49 @@
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
from weakref import proxy as _proxy
|
||||
from copy import copy as _copy
|
||||
|
||||
from .common import NamedInts as _NamedInts
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
KIND = _NamedInts(toggle=0x1, choice=0x02, range=0x03)
|
||||
|
||||
class Setting(object):
|
||||
__slots__ = ['name', 'kind', 'label', 'description', 'choices', '_device', '_value', 'register']
|
||||
|
||||
def __init__(self, name, kind, label, description=None, choices=None):
|
||||
self.name = name
|
||||
self.kind = kind
|
||||
self.label = label
|
||||
self.description = description
|
||||
self.choices = choices
|
||||
self.register = None
|
||||
|
||||
def __call__(self, device):
|
||||
o = _copy(self)
|
||||
o._value = None
|
||||
o._device = _proxy(device)
|
||||
return o
|
||||
|
||||
def read_register(self):
|
||||
return self._device.request(0x8100 | (self.register & 0x2FF))
|
||||
|
||||
def write_register(self, value, value2=0):
|
||||
return self._device.request(0x8000 | (self.register & 0x2FF), int(value) & 0xFF, int(value2) & 0xFF)
|
||||
|
||||
def read(self, cached=True):
|
||||
raise NotImplemented
|
||||
|
||||
def write(self, value):
|
||||
raise NotImplemented
|
||||
|
||||
def __str__(self):
|
||||
return '<%s(%s=%s)>' % (self.__class__.__name__, self.name, self._value)
|
||||
__unicode__ = __repr__ = __str__
|
||||
297
lib/logitech/unifying_receiver/status.py
Normal file
@@ -0,0 +1,297 @@
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
from time import time as _timestamp
|
||||
from struct import unpack as _unpack
|
||||
from weakref import proxy as _proxy
|
||||
|
||||
from logging import getLogger, DEBUG as _DEBUG
|
||||
_log = getLogger('LUR.status')
|
||||
del getLogger
|
||||
|
||||
from .common import NamedInts as _NamedInts
|
||||
from . import hidpp10 as _hidpp10
|
||||
from . import hidpp20 as _hidpp20
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
ALERT = _NamedInts(NONE=0x00, LOW=0x01, MED=0x02, HIGH=0xFF)
|
||||
|
||||
# device properties that may be reported
|
||||
ENCRYPTED='encrypted'
|
||||
BATTERY_LEVEL='battery-level'
|
||||
BATTERY_STATUS='battery-status'
|
||||
LIGHT_LEVEL='light-level'
|
||||
ERROR='error'
|
||||
|
||||
# if not updates have been receiver from the device for a while, assume
|
||||
# it has gone offline and clear all its know properties.
|
||||
_STATUS_TIMEOUT = 120 # seconds
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
class ReceiverStatus(dict):
|
||||
def __init__(self, receiver, changed_callback):
|
||||
assert receiver
|
||||
self._receiver = _proxy(receiver)
|
||||
|
||||
assert changed_callback
|
||||
self._changed_callback = changed_callback
|
||||
|
||||
# self.updated = 0
|
||||
|
||||
self.lock_open = False
|
||||
self.new_device = None
|
||||
self[ERROR] = None
|
||||
|
||||
def __str__(self):
|
||||
count = len(self._receiver)
|
||||
return ('No devices found.' if count == 0 else
|
||||
'1 device found.' if count == 1 else
|
||||
'%d devices found.' % count)
|
||||
__unicode__ = __str__
|
||||
|
||||
def _changed(self, alert=ALERT.LOW, reason=None):
|
||||
# self.updated = _timestamp()
|
||||
self._changed_callback(self._receiver, alert=alert, reason=reason)
|
||||
|
||||
def process_event(self, event):
|
||||
if event.sub_id == 0x4A:
|
||||
self.lock_open = bool(event.address & 0x01)
|
||||
reason = 'pairing lock is ' + ('open' if self.lock_open else 'closed')
|
||||
_log.info("%s: %s", self._receiver, reason)
|
||||
if self.lock_open:
|
||||
self[ERROR] = None
|
||||
self.new_device = None
|
||||
|
||||
pair_error = ord(event.data[:1])
|
||||
if pair_error:
|
||||
self[ERROR] = _hidpp10.PAIRING_ERRORS[pair_error]
|
||||
self.new_device = None
|
||||
_log.warn("pairing error %d: %s", pair_error, self[ERROR])
|
||||
else:
|
||||
self[ERROR] = None
|
||||
|
||||
self._changed(reason=reason)
|
||||
return True
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
class DeviceStatus(dict):
|
||||
def __init__(self, device, changed_callback):
|
||||
assert device
|
||||
self._device = _proxy(device)
|
||||
|
||||
assert changed_callback
|
||||
self._changed_callback = changed_callback
|
||||
|
||||
self._active = None
|
||||
self.updated = 0
|
||||
|
||||
def __str__(self):
|
||||
t = []
|
||||
if self.get(BATTERY_LEVEL) is not None:
|
||||
b = 'Battery: %d%%' % self[BATTERY_LEVEL]
|
||||
if self.get(BATTERY_STATUS):
|
||||
b += ' (' + self[BATTERY_STATUS] + ')'
|
||||
t.append(b)
|
||||
if self.get(LIGHT_LEVEL) is not None:
|
||||
t.append('Light: %d lux' % self[LIGHT_LEVEL])
|
||||
return ', '.join(t)
|
||||
__unicode__ = __str__
|
||||
|
||||
def __bool__(self):
|
||||
return bool(self._active)
|
||||
__nonzero__ = __bool__
|
||||
|
||||
def _changed(self, active=True, alert=ALERT.NONE, reason=None, timestamp=None):
|
||||
assert self._changed_callback
|
||||
self._active = active
|
||||
if not active:
|
||||
battery = self.get(BATTERY_LEVEL)
|
||||
self.clear()
|
||||
if battery is not None:
|
||||
self[BATTERY_LEVEL] = battery
|
||||
if self.updated == 0:
|
||||
alert |= ALERT.LOW
|
||||
self.updated = timestamp or _timestamp()
|
||||
# if _log.isEnabledFor(_DEBUG):
|
||||
# _log.debug("device %d changed: active=%s %s", self._device.number, self._active, dict(self))
|
||||
self._changed_callback(self._device, alert, reason)
|
||||
|
||||
def poll(self, timestamp):
|
||||
if self._active:
|
||||
d = self._device
|
||||
# read these in case they haven't been read already
|
||||
d.protocol, d.serial, d.firmware
|
||||
|
||||
if BATTERY_LEVEL not in self:
|
||||
battery = _hidpp10.get_battery(d)
|
||||
if battery is None and d.protocol >= 2.0:
|
||||
battery = _hidpp20.get_battery(d)
|
||||
|
||||
# if battery is None and _hidpp20.FEATURE.SOLAR_CHARGE in d.features:
|
||||
# d.feature_request(_hidpp20.FEATURE.SOLAR_CHARGE, 0x00, 1, 1)
|
||||
# return
|
||||
|
||||
if battery:
|
||||
self[BATTERY_LEVEL], self[BATTERY_STATUS] = battery
|
||||
self._changed(timestamp=timestamp)
|
||||
elif BATTERY_STATUS in self:
|
||||
self[BATTERY_STATUS] = None
|
||||
self._changed(timestamp=timestamp)
|
||||
|
||||
elif len(self) > 0 and timestamp - self.updated > _STATUS_TIMEOUT:
|
||||
# if the device has been inactive for too long, clear out any known
|
||||
# properties, they are most likely obsolete anyway
|
||||
self.clear()
|
||||
self._changed(active=False, alert=ALERT.LOW, timestamp=timestamp)
|
||||
|
||||
def process_event(self, event):
|
||||
assert event.sub_id < 0x80
|
||||
|
||||
if event.sub_id == 0x40:
|
||||
if event.address == 0x02:
|
||||
# device un-paired
|
||||
self.clear()
|
||||
self._device.status = None
|
||||
self._changed(False, ALERT.HIGH, 'unpaired')
|
||||
else:
|
||||
_log.warn("device %d disconnection notification %s with unknown type %02X", self._device.number, event, event.address)
|
||||
return True
|
||||
|
||||
if event.sub_id == 0x41:
|
||||
if event.address == 0x04: # unifying protocol
|
||||
# wpid = _strhex(event.data[4:5] + event.data[3:4])
|
||||
# assert wpid == device.wpid
|
||||
|
||||
flags = ord(event.data[:1]) & 0xF0
|
||||
link_encrypyed = bool(flags & 0x20)
|
||||
link_established = not (flags & 0x40)
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
sw_present = bool(flags & 0x10)
|
||||
has_payload = bool(flags & 0x80)
|
||||
_log.debug("device %d connection notification: software=%s, encrypted=%s, link=%s, payload=%s",
|
||||
self._device.number, sw_present, link_encrypyed, link_established, has_payload)
|
||||
self[ENCRYPTED] = link_encrypyed
|
||||
self._changed(link_established)
|
||||
|
||||
elif event.address == 0x03:
|
||||
_log.warn("device %d connection notification %s with eQuad protocol, ignored", self._device.number, event)
|
||||
|
||||
else:
|
||||
_log.warn("device %d connection notification %s with unknown protocol %02X", self._device.number, event, event.address)
|
||||
|
||||
return True
|
||||
|
||||
if event.sub_id == 0x49:
|
||||
# raw input event? just ignore it
|
||||
# if event.address == 0x01, no idea what it is
|
||||
# if event.address == 0x03, it's an actual input event
|
||||
return True
|
||||
|
||||
if event.sub_id == 0x4B:
|
||||
if event.address == 0x01:
|
||||
_log.debug("device came online %d", event.devnumber)
|
||||
self._changed(alert=ALERT.LOW, reason='powered on')
|
||||
else:
|
||||
_log.warn("unknown event %s", event)
|
||||
return True
|
||||
|
||||
if event.sub_id >= 0x40:
|
||||
_log.warn("don't know how to handle event %s", event)
|
||||
return False
|
||||
|
||||
# this must be a feature event, assuming no device has more than 0x40 features
|
||||
if event.sub_id >= len(self._device.features):
|
||||
_log.warn("device %d got event from unknown feature index %02X", self._device.number, event.sub_id)
|
||||
return False
|
||||
|
||||
try:
|
||||
feature = self._device.features[event.sub_id]
|
||||
except IndexError:
|
||||
_log.warn("don't know how to handle event %s for feature with invalid index %02X", event, event.sub_id)
|
||||
return False
|
||||
|
||||
if feature == _hidpp20.FEATURE.BATTERY:
|
||||
if event.address == 0x00:
|
||||
discharge = ord(event.data[:1])
|
||||
battery_status = ord(event.data[1:2])
|
||||
self[BATTERY_LEVEL] = discharge
|
||||
self[BATTERY_STATUS] = BATTERY_STATUS[battery_status]
|
||||
if _hidpp20.BATTERY_OK(battery_status):
|
||||
alert = ALERT.NONE
|
||||
reason = self[ERROR] = None
|
||||
else:
|
||||
alert = ALERT.MED
|
||||
reason = self[ERROR] = self[BATTERY_STATUS]
|
||||
self._changed(alert=alert, reason=reason)
|
||||
else:
|
||||
_log.warn("don't know how to handle BATTERY event %s", event)
|
||||
return True
|
||||
|
||||
if feature == _hidpp20.FEATURE.REPROGRAMMABLE_KEYS:
|
||||
if event.address == 0x00:
|
||||
_log.debug('reprogrammable key: %s', event)
|
||||
else:
|
||||
_log.warn("don't know how to handle REPROGRAMMABLE KEYS event %s", event)
|
||||
return True
|
||||
|
||||
if feature == _hidpp20.FEATURE.WIRELESS:
|
||||
if event.address == 0x00:
|
||||
_log.debug("wireless status: %s", event)
|
||||
if event.data[0:3] == b'\x01\x01\x01':
|
||||
self._changed(alert=ALERT.LOW, reason='powered on')
|
||||
else:
|
||||
_log.warn("don't know how to handle WIRELESS event %s", event)
|
||||
return True
|
||||
|
||||
if feature == _hidpp20.FEATURE.SOLAR_CHARGE:
|
||||
if event.data[5:9] == b'GOOD':
|
||||
charge, lux, adc = _unpack(b'!BHH', event.data[:5])
|
||||
self[BATTERY_LEVEL] = charge
|
||||
# guesstimate the battery voltage, emphasis on 'guess'
|
||||
self[BATTERY_STATUS] = '%1.2fV' % (adc * 2.67793237653 / 0x0672)
|
||||
if event.address == 0x00:
|
||||
self[LIGHT_LEVEL] = None
|
||||
self._changed()
|
||||
elif event.address == 0x10:
|
||||
self[LIGHT_LEVEL] = lux
|
||||
if lux > 200: # guesstimate
|
||||
self[BATTERY_STATUS] += ', charging'
|
||||
self._changed()
|
||||
elif event.address == 0x20:
|
||||
_log.debug("Solar key pressed")
|
||||
# first cancel any reporting
|
||||
self._device.feature_request(_hidpp20.FEATURE.SOLAR_CHARGE)
|
||||
reports_count = 15
|
||||
reports_period = 2 # seconds
|
||||
self._changed(alert=ALERT.MED)
|
||||
# trigger a new report chain
|
||||
self._device.feature_request(_hidpp20.FEATURE.SOLAR_CHARGE, 0x00, reports_count, reports_period)
|
||||
else:
|
||||
self._changed()
|
||||
else:
|
||||
_log.warn("SOLAR CHARGE event not GOOD? %s", event)
|
||||
return True
|
||||
|
||||
if feature == _hidpp20.FEATURE.TOUCH_MOUSE:
|
||||
if event.address == 0x00:
|
||||
_log.debug("TOUCH MOUSE points event: %s", event)
|
||||
elif event.address == 0x10:
|
||||
touch = ord(event.data[:1])
|
||||
button_down = bool(touch & 0x02)
|
||||
mouse_lifted = bool(touch & 0x01)
|
||||
_log.debug("TOUCH MOUSE status: button_down=%s mouse_lifted=%s", button_down, mouse_lifted)
|
||||
return True
|
||||
|
||||
_log.warn("don't know how to handle event %s for feature %s (%02X)", event, feature, event.sub_id)
|
||||
@@ -1,3 +0,0 @@
|
||||
#
|
||||
# Tests for the logitech.unifying_receiver package.
|
||||
#
|
||||
@@ -1,17 +0,0 @@
|
||||
#
|
||||
# test loading the hidapi library
|
||||
#
|
||||
|
||||
import logging
|
||||
import unittest
|
||||
|
||||
|
||||
class Test_Import_HIDAPI(unittest.TestCase):
|
||||
def test_00_import_hidapi(self):
|
||||
import hidapi
|
||||
self.assertIsNotNone(hidapi)
|
||||
logging.info("hidapi loaded native implementation %s", hidapi.native_implementation)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -1,33 +0,0 @@
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
import unittest
|
||||
import struct
|
||||
|
||||
from ..constants import *
|
||||
|
||||
|
||||
class Test_UR_Constants(unittest.TestCase):
|
||||
|
||||
def test_10_feature_names(self):
|
||||
for code in range(0x0000, 0x10000):
|
||||
feature = struct.pack('!H', code)
|
||||
name = FEATURE_NAME[feature]
|
||||
self.assertIsNotNone(name)
|
||||
self.assertEqual(FEATURE_NAME[code], name)
|
||||
if name.startswith('UNKNOWN_'):
|
||||
self.assertEqual(code, struct.unpack('!H', feature)[0])
|
||||
else:
|
||||
self.assertTrue(hasattr(FEATURE, name))
|
||||
self.assertEqual(feature, getattr(FEATURE, name))
|
||||
|
||||
def test_20_error_names(self):
|
||||
for code in range(0, len(ERROR_NAME)):
|
||||
name = ERROR_NAME[code]
|
||||
self.assertIsNotNone(name)
|
||||
# self.assertEqual(code, ERROR_NAME.index(name))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -1,187 +0,0 @@
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
import unittest
|
||||
|
||||
from .. import base
|
||||
from ..exceptions import *
|
||||
from ..constants import *
|
||||
|
||||
|
||||
class Test_UR_Base(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.ur_available = False
|
||||
cls.handle = None
|
||||
cls.device = None
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
if cls.handle:
|
||||
base.close(cls.handle)
|
||||
cls.ur_available = False
|
||||
cls.handle = None
|
||||
cls.device = None
|
||||
|
||||
def test_10_list_receiver_devices(self):
|
||||
rawdevices = base.list_receiver_devices()
|
||||
self.assertIsNotNone(rawdevices, "list_receiver_devices returned None")
|
||||
# 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):
|
||||
if not self.ur_available:
|
||||
self.fail("No receiver found")
|
||||
|
||||
for rawdevice in base.list_receiver_devices():
|
||||
handle = base.try_open(rawdevice.path)
|
||||
if handle is None:
|
||||
continue
|
||||
|
||||
self.assertIsInstance(handle, int, "try_open should have returned an int")
|
||||
|
||||
if Test_UR_Base.handle is None:
|
||||
Test_UR_Base.handle = handle
|
||||
else:
|
||||
base.close(handle)
|
||||
base.close(Test_UR_Base.handle)
|
||||
Test_UR_Base.handle = None
|
||||
self.fail("try_open found multiple valid receiver handles")
|
||||
|
||||
self.assertIsNotNone(self.handle, "no valid receiver handles found")
|
||||
|
||||
def test_25_ping_device_zero(self):
|
||||
if self.handle is None:
|
||||
self.fail("No receiver found")
|
||||
|
||||
w = base.write(self.handle, 0, b'\x00\x10\x00\x00\xAA')
|
||||
self.assertIsNone(w, "write should have returned None")
|
||||
reply = base.read(self.handle, base.DEFAULT_TIMEOUT * 3)
|
||||
self.assertIsNotNone(reply, "None reply for ping")
|
||||
self.assertIsInstance(reply, tuple, "read should have returned a tuple")
|
||||
|
||||
reply_code, reply_device, reply_data = reply
|
||||
self.assertEqual(reply_device, 0, "got ping reply for valid device")
|
||||
self.assertGreater(len(reply_data), 4, "ping reply has wrong length: %s" % base._hex(reply_data))
|
||||
if reply_code == 0x10:
|
||||
# ping fail
|
||||
self.assertEqual(reply_data[:3], b'\x8F\x00\x10', "0x10 reply with unknown reply data: %s" % base._hex(reply_data))
|
||||
elif reply_code == 0x11:
|
||||
self.fail("Got valid ping from device 0")
|
||||
else:
|
||||
self.fail("ping got bad reply code: " + reply)
|
||||
|
||||
def test_30_ping_all_devices(self):
|
||||
if self.handle is None:
|
||||
self.fail("No receiver found")
|
||||
|
||||
devices = []
|
||||
|
||||
for device in range(1, 1 + MAX_ATTACHED_DEVICES):
|
||||
w = base.write(self.handle, device, b'\x00\x10\x00\x00\xAA')
|
||||
self.assertIsNone(w, "write should have returned None")
|
||||
reply = base.read(self.handle, base.DEFAULT_TIMEOUT * 3)
|
||||
self.assertIsNotNone(reply, "None reply for ping")
|
||||
self.assertIsInstance(reply, tuple, "read should have returned a tuple")
|
||||
|
||||
reply_code, reply_device, reply_data = reply
|
||||
self.assertEqual(reply_device, device, "ping reply for wrong device")
|
||||
self.assertGreater(len(reply_data), 4, "ping reply has wrong length: %s" % base._hex(reply_data))
|
||||
if reply_code == 0x10:
|
||||
# ping fail
|
||||
self.assertEqual(reply_data[:3], b'\x8F\x00\x10', "0x10 reply with unknown reply data: %s" % base._hex(reply_data))
|
||||
elif reply_code == 0x11:
|
||||
# ping ok
|
||||
self.assertEqual(reply_data[:2], b'\x00\x10', "0x11 reply with unknown reply data: %s" % base._hex(reply_data))
|
||||
self.assertEqual(reply_data[4:5], b'\xAA')
|
||||
devices.append(device)
|
||||
else:
|
||||
self.fail("ping got bad reply code: " + reply)
|
||||
|
||||
if devices:
|
||||
Test_UR_Base.device = devices[0]
|
||||
|
||||
def test_50_request_bad_device(self):
|
||||
if self.handle is None:
|
||||
self.fail("No receiver found")
|
||||
|
||||
device = 1 if self.device is None else self.device + 1
|
||||
reply = base.request(self.handle, device, FEATURE.ROOT, FEATURE.FEATURE_SET)
|
||||
self.assertIsNone(reply, "request returned valid reply")
|
||||
|
||||
def test_52_request_root_no_feature(self):
|
||||
if self.handle is None:
|
||||
self.fail("No receiver found")
|
||||
if self.device is None:
|
||||
self.fail("No devices attached")
|
||||
|
||||
reply = base.request(self.handle, self.device, FEATURE.ROOT)
|
||||
self.assertIsNotNone(reply, "request returned None reply")
|
||||
self.assertEqual(reply[:2], b'\x00\x00', "request returned for wrong feature id")
|
||||
|
||||
def test_55_request_root_feature_set(self):
|
||||
if self.handle is None:
|
||||
self.fail("No receiver found")
|
||||
if self.device is None:
|
||||
self.fail("No devices attached")
|
||||
|
||||
reply = base.request(self.handle, self.device, FEATURE.ROOT, FEATURE.FEATURE_SET)
|
||||
self.assertIsNotNone(reply, "request returned None reply")
|
||||
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")
|
||||
|
||||
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
|
||||
|
||||
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")
|
||||
|
||||
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
|
||||
|
||||
# def test_90_receiver_missing(self):
|
||||
# if self.handle is None:
|
||||
# self.fail("No receiver found")
|
||||
#
|
||||
# logging.warn("remove the receiver in 5 seconds or this test will fail")
|
||||
# import time
|
||||
# time.sleep(5)
|
||||
# with self.assertRaises(NoReceiver):
|
||||
# self.test_30_ping_all_devices()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -1,134 +0,0 @@
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
import unittest
|
||||
import warnings
|
||||
|
||||
from .. import api
|
||||
from ..constants import *
|
||||
from ..common import *
|
||||
|
||||
|
||||
class Test_UR_API(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.receiver = None
|
||||
cls.device = None
|
||||
cls.features = None
|
||||
cls.device_info = None
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
if cls.receiver:
|
||||
cls.receiver.close()
|
||||
cls.device = None
|
||||
cls.features = None
|
||||
cls.device_info = None
|
||||
|
||||
def _check(self, check_device=True, check_features=False):
|
||||
if self.receiver is None:
|
||||
self.fail("No receiver found")
|
||||
if check_device and self.device is None:
|
||||
self.fail("Found no devices attached.")
|
||||
if check_device and check_features and self.features is None:
|
||||
self.fail("no feature set available")
|
||||
|
||||
def test_00_open_receiver(self):
|
||||
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)
|
||||
self.assertIsNotNone(ok, "invalid ping reply")
|
||||
if ok:
|
||||
devices.append(self.receiver[devnumber])
|
||||
|
||||
if devices:
|
||||
Test_UR_API.device = devices[0].number
|
||||
|
||||
def test_30_get_feature_index(self):
|
||||
self._check()
|
||||
|
||||
fs_index = api.get_feature_index(self.receiver.handle, self.device, FEATURE.FEATURE_SET)
|
||||
self.assertIsNotNone(fs_index, "feature FEATURE_SET not available")
|
||||
self.assertGreater(fs_index, 0, "invalid FEATURE_SET index: " + str(fs_index))
|
||||
|
||||
def test_31_bad_feature(self):
|
||||
self._check()
|
||||
|
||||
reply = api.request(self.receiver.handle, self.device, FEATURE.ROOT, params=b'\xFF\xFF')
|
||||
self.assertIsNotNone(reply, "invalid reply")
|
||||
self.assertEqual(reply[:5], b'\x00' * 5, "invalid reply")
|
||||
|
||||
def test_40_get_device_features(self):
|
||||
self._check()
|
||||
|
||||
features = api.get_device_features(self.receiver.handle, self.device)
|
||||
self.assertIsNotNone(features, "failed to read features array")
|
||||
self.assertIn(FEATURE.FEATURE_SET, features, "feature FEATURE_SET not available")
|
||||
# cache this to simplify next tests
|
||||
Test_UR_API.features = features
|
||||
|
||||
def test_50_get_device_firmware(self):
|
||||
self._check(check_features=True)
|
||||
|
||||
d_firmware = api.get_device_firmware(self.receiver.handle, self.device, self.features)
|
||||
self.assertIsNotNone(d_firmware, "failed to get device firmware")
|
||||
self.assertGreater(len(d_firmware), 0, "device reported no firmware")
|
||||
for fw in d_firmware:
|
||||
self.assertIsInstance(fw, FirmwareInfo)
|
||||
|
||||
def test_52_get_device_kind(self):
|
||||
self._check(check_features=True)
|
||||
|
||||
d_kind = api.get_device_kind(self.receiver.handle, self.device, self.features)
|
||||
self.assertIsNotNone(d_kind, "failed to get device kind")
|
||||
self.assertGreater(len(d_kind), 0, "empty device kind")
|
||||
|
||||
def test_55_get_device_name(self):
|
||||
self._check(check_features=True)
|
||||
|
||||
d_name = api.get_device_name(self.receiver.handle, self.device, self.features)
|
||||
self.assertIsNotNone(d_name, "failed to read device name")
|
||||
self.assertGreater(len(d_name), 0, "empty device name")
|
||||
|
||||
def test_59_get_device_info(self):
|
||||
self._check(check_features=True)
|
||||
|
||||
device_info = api.get_device(self.receiver.handle, self.device, features=self.features)
|
||||
self.assertIsNotNone(device_info, "failed to read full device info")
|
||||
self.assertIsInstance(device_info, api.PairedDevice)
|
||||
Test_UR_API.device_info = device_info
|
||||
|
||||
def test_60_get_battery_level(self):
|
||||
self._check(check_features=True)
|
||||
|
||||
if FEATURE.BATTERY in self.features:
|
||||
battery = api.get_device_battery_level(self.receiver.handle, self.device, self.features)
|
||||
self.assertIsNotNone(battery, "failed to read battery level")
|
||||
self.assertIsInstance(battery, tuple, "result not a tuple")
|
||||
else:
|
||||
warnings.warn("BATTERY feature not supported by device %d" % self.device)
|
||||
|
||||
def test_70_list_devices(self):
|
||||
self._check(check_device=False)
|
||||
|
||||
for dev in self.receiver:
|
||||
self.assertIsNotNone(dev)
|
||||
self.assertIsInstance(dev, api.PairedDevice)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -1,7 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
cd -P `dirname "$0"`
|
||||
|
||||
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$PWD/native/`uname -m`
|
||||
|
||||
exec python -m unittest discover -v "$@"
|
||||
45
rules.d/install.sh
Executable file
@@ -0,0 +1,45 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
Z=$(readlink -f "$0")
|
||||
|
||||
RULES_D=/etc/udev/rules.d
|
||||
if ! test -d "$RULES_D"; then
|
||||
echo "$RULES_D not found; is udev installed?"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
RULE=99-logitech-unifying-receiver.rules
|
||||
|
||||
if test -n "$1"; then
|
||||
SOURCE=$1
|
||||
else
|
||||
SOURCE=$(dirname "$Z")/$RULE
|
||||
if ! id -G -n | grep -q -F plugdev; then
|
||||
GROUP=$(id -g -n)
|
||||
echo "User '$USER' does not belong to the 'plugdev' group, will use group '$GROUP' in the udev rule."
|
||||
TEMP_RULE=${TMPDIR:-/tmp}/$$-$RULE
|
||||
cp -f "$SOURCE" "$TEMP_RULE"
|
||||
SOURCE=$TEMP_RULE
|
||||
sed -i -e "s/GROUP=\"plugdev\"/GROUP=\"$GROUP\"/" "$SOURCE"
|
||||
fi
|
||||
fi
|
||||
|
||||
if test "$(id -u)" != "0"; then
|
||||
echo "Switching to root to install the udev rule."
|
||||
test -x /usr/bin/pkexec && exec /usr/bin/pkexec "$Z" "$SOURCE"
|
||||
test -x /usr/bin/sudo && exec /usr/bin/sudo -- "$Z" "$SOURCE"
|
||||
test -x /bin/su && exec /bin/su -c "\"$Z\" \"$SOURCE\""
|
||||
echo "Could not switch to root: none of pkexec, sudo or su were found?"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Installing $RULE."
|
||||
cp "$SOURCE" "$RULES_D/$RULE"
|
||||
chmod a+r "$RULES_D/$RULE"
|
||||
|
||||
echo "Reloading udev rules."
|
||||
udevadm control --reload-rules
|
||||
|
||||
echo "Done. Now remove the Unfiying Receiver, wait 10 seconds and plug it in again."
|
||||
BIN
share/icons/hicolor/128x128/apps/Solaar-mask.png
Normal file
|
After Width: | Height: | Size: 800 B |
|
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 |